Skip to content

Recursive Rewrite Rules

hold treats a DNS question as a pair: its fully qualified name and requested record type. Each rule has a source pattern for that pair and either a new pair or the special target BLACKHOLE.

In simplified pseudo-code, a single pass looks like this:

for rule in rules:
    if rule.source fully matches (name, record_type):
        captures = values matched by placeholders
        return fill(rule.target, captures)
return input

With is_recursive: true, hold feeds each result back into the same transformer until no rule changes it:

value = question
repeat:
    next = rewrite_once(value)
    if next == value: return value
    if next was already seen: raise circular-loop error
    value = next

This turns a collection of small, composable rules into a transformation graph. You can normalize a convenient suffix first, map the normalized name to an address second, and still have the final result checked against the blocklist.

Captures and substitutions

{SUBDOMAIN} matches and captures one DNS label. {SUBDOMAINS} matches a dotted sequence. A placeholder used in a target is replaced with the text captured by the source rule:

{SUBDOMAIN}\.service\.  =>  {SUBDOMAIN}.lan.
api.service.            =>  api.lan.

The other source fields are regular expressions too. For example, A|AAAA|CNAME allows a rule to apply to three record types. Patterns are full matches, literal dots need escaping, and names include the trailing root dot.

Transformation examples

# Preserve a service label, then resolve every local service to one host.
{name}.service. -> {name}.lan. -> 192.168.1.10

# Normalize an overlay suffix, then select its private resolver.
{name}.vpn. -> {name}.example.net. -> resolver 100.100.100.100

# Block directly or after another rewrite reaches the blocked name.
ads.example. -> BLACKHOLE
tracking.short. -> ads.example. -> BLACKHOLE

The corresponding abbreviated YAML is:

rewriter:
  is_recursive: true
  items:
    - source: {name: '{SUBDOMAIN}\.service\.', records: 'A|AAAA|CNAME'}
      target: {name: '{SUBDOMAIN}.lan.', records: CNAME}
    - source: {name: '{SUBDOMAIN}\.lan\.', records: 'A|AAAA|CNAME'}
      target: {name: 192.168.1.10, records: A}
    - source: {name: 'ads\.example\.', records: 'A|AAAA|CNAME'}
      target: BLACKHOLE

Rule order matters when source patterns overlap. Prefer narrow patterns and test each chain independently. Circular rewrite chains are detected and rejected, rather than being followed forever.

Upstream routing

The client.items list uses the same pattern machinery to select a DNS resolver. This keeps routing rules beside rewrite rules: public names can go to the default DNS-over-HTTPS service, while .lan, VPN, and reverse-DNS names go directly to the resolver authoritative for that network. See the example settings.