COREDNS-TEMPLATE(7)

πŸ“¦
version 1.15.0 Β· 3 versions
, in opensuse-leap 16.0

🌸 COREDNS-TEMPLATE(7) 🌸

okay bestie so this is a page from the CoreDNS Plugins manual, and it's about the template plugin β€” literally one of the most fun ones. grab a snack, we're doing this πŸ’…


✨ NAME ✨

template β€” lets you build dynamic responses based on the incoming query. yes, actually. you just write a template and boom, answers. πŸŽ€


πŸ’– DESCRIPTION πŸ’–

The template plugin lets you dynamically respond to queries by just writing a (Go) template. that's it. that's the whole vibe. you write a template, it answers. iconic behavior fr fr ✨


πŸ¦‹ SYNTAX πŸ¦‹

template CLASS TYPE [ZONE...] {
    match REGEX...
    var NAME EXPRESSION
    expr EXPRESSION
    answer RR
    additional RR
    authority RR
    rcode CODE
    ederror EXTENDED_ERROR_CODE [EXTRA_REASON]
    fallthrough [FALLTHROUGH-ZONE...]
}

and now the breakdown, one by one, no skips πŸ‘‘

  • CLASS β€” the query class. usually IN or ANY. πŸ’…
  • TYPE β€” the query type (A, PTR, ... and it can be ANY, which matches all types).
  • ZONE β€” the zone scope(s) for this template. defaults to the server zones. 🌈
  • match REGEX... β€” Go regexp patterns (https://golang.org/pkg/regexp/) that get matched against the incoming question name. if you specify no regex at all, it matches everything (default: .*). first matching regex wins. and heads up β€” regex patterns must not exceed 10000 characters. 😭
  • answer|additional|authority RR β€” an RFC 1035 (https://www.rfc-editor.org/info/rfc1035/#section-5) style resource record fragment, built by a Go template (https://golang.org/pkg/text/template/) that contains the reply. if you specify no answer, you get a response with an empty answer section. that's the deal. πŸ™ˆ
  • var NAME EXPRESSION β€” sets the variable NAME to the result of EXPRESSION, evaluated for each matching query and available to the templates as .Var.NAME. you can set multiple variables, and each one may use the variables declared before it. see the Expressions section below, iykyk πŸ’œ
  • expr EXPRESSION β€” a condition that has to evaluate to true for the query to match. all conditions must be true for a complete match; otherwise the query is subject to fallthrough, as if no regex had matched. conditions may use the variables declared with var. again, Expressions section, we got you 🫢
  • rcode CODE β€” a response code (NXDOMAIN, SERVFAIL, ...). the default is NOERROR. valid response code values are per the RcodeToString map defined by the miekg/dns package in msg.go. ⭐
  • ederror EXTENDEDERRORCODE β€” an extended DNS error code as a number defined in RFC8914 (0, 1, 2,..., 24). and EXTRA_REASON is an additional string explaining the reason for returning the error. πŸ“
  • fallthrough β€” continue with the next template instance if the template's ZONE matches a query name but no regex match. if there's no next template, continue resolution with the next plugin. if [FALLTHROUGH-ZONE...] are listed (for example in-addr.arpa and ip6.arpa), then only queries for those zones will be subject to fallthrough. and listen β€” without fallthrough, when the template's ZONE matches a query but no regex match, then a SERVFAIL response is returned. dramatic but true πŸ’€
also see (#also-see) contains an additional reading list, if you're the kind of girl who reads the whole syllabus. respect. πŸ“š

🧚 TEMPLATES 🧚

every single resource record is a full-featured Go template (https://golang.org/pkg/text/template/) with the following predefined data β€” memorize these, they're your new besties:

  • .Zone β€” the matched zone string (e.g. example.). 🌸
  • .Name β€” the query name, as a string (lowercased).
  • .Class β€” the query class (usually IN).
  • .Type β€” the RR type requested (e.g. PTR).
  • .Match β€” an array of all matches. index .Match 0 refers to the whole match. πŸ”₯
  • .Group β€” a map of the named capture groups.
  • .Message β€” the complete incoming DNS message.
  • .Question β€” the matched question section.
  • .Remote β€” client's IP address. πŸ‘€
  • .Var β€” the variables defined with var (e.g. .Var.myvariable).
  • .Meta β€” a function that takes a metadata name and returns the value, if the metadata plugin is enabled. for example, .Meta "kubernetes/client-namespace" πŸ’«

and the following predefined template functions (https://golang.org/pkg/text/template#hdr-Functions):

the output of the template must be an RFC 1035 (https://www.rfc-editor.org/info/rfc1035/) style resource record β€” commonly referred to as a "zone file". periodt.

WARNING ⚠️ there's a syntactical problem with Go templates and CoreDNS config files. expressions like {{$var}} will be interpreted as a reference to an environment variable by CoreDNS (and Caddy), while {{ $var }} will work. see Bugs (#bugs) and corefile(5). no cap, put the spaces in. 😭

πŸ’œ EXPRESSIONS πŸ’œ

the EXPRESSION of a var or expr is written in the expr language β€” the same one used by the view plugin. see https://expr-lang.org/docs/language-definition as a detailed reference for valid syntax. go off, read the docs, they're giving thorough. ✨

expressions can reference the DNS query functions and utility functions listed in the view plugin's documentation, the variables declared by preceding var options, and:

  • group(name string) string β€” the capture group named name of the matching regex, or "" if there's no such group. 🌈
  • group(index int) string β€” the index-th capture group of the matching regex, group(0) being the entire match, or "" if there's no such group. πŸ¦‹

and a variable name must be a valid identifier, and it must not be the name of an existing function, or of a keyword or literal of the expr language β€” like len or true. don't do it. it will not slay. πŸ™ˆ


πŸ“Š METRICS πŸ“Š

if monitoring is enabled (via the prometheus plugin) then the following metrics are exported, bestie:

  • coredns_template_matches_total{server, zone, view, class, type} β€” the total number of matched requests by regex. πŸ”₯
  • coredns_template_template_failures_total{server, zone, view, class, type, section, template} β€” the number of times the Go templating failed. the Regex, section and template label values can be used to map the error back to the config file. 😭
  • coredns_template_rr_failures_total{server, zone, view, class, type, section, template} β€” the number of times the templated resource record was invalid and could not be parsed. Regex, section and template label values can be used to map the error back to the config file. πŸ’€

both failure cases indicate a problem with the template configuration. the server label indicates the server incrementing the metric β€” see the metrics plugin for details. πŸ‘‘


(part 2 is coming, don't go anywhere 🫢)

✨ EXAMPLES ✨

Bestie, this is the fun part β€” real configs you can literally copy-paste. Each one comes with a lil breakdown of what's happening so nothing feels like ✨magic✨ you don't understand.


🚫 RESOLVE EVERYTHING TO NXDOMAIN

The most babygirl-simple template there is, no cap:

. {
    template ANY ANY {
      rcode NXDOMAIN
    }
}
  1. This template uses the default zone (. or all queries)
  2. All queries will be answered (no fallthrough)
  3. The answer is always NXDOMAIN

πŸ”’ RESOLVE .INVALID AS NXDOMAIN

The .invalid domain is a reserved TLD (see RFC 2606 Reserved Top Level DNS Names https://www.rfc-editor.org/info/rfc2606/#section-2) β€” it literally exists to mark domains as invalid.

. {
    forward . 8.8.8.8

    template ANY ANY invalid {
      rcode NXDOMAIN
      authority "invalid. 60 {{ .Class }} SOA ns.invalid. hostmaster.invalid. (1 60 60 60 60)"
      ederror 21 "Blocked according to RFC2606"
    }
}
  1. A query to .invalid will result in NXDOMAIN (rcode)
  2. A dummy SOA record is sent to hand out a TTL of 60s for caching purposes
  3. Querying .invalid in the CH class will also cause a NXDOMAIN/SOA response
  4. The default regex is .*

πŸ›‘ BLOCK INVALID SEARCH DOMAIN COMPLETIONS

Okay so picture this: you run example.com with a datacenter dc1.example.com. The datacenter domain is part of the DNS search domain. But something.example.com.dc1.example.com would indicate a fully qualified domain name (something.example.com) that inadvertently has the default domain or search path (dc1.example.com) tacked on. Rude. We block it:

. {
    forward . 8.8.8.8

    template IN ANY example.com.dc1.example.com {
      rcode NXDOMAIN
      authority "{{ .Zone }} 60 IN SOA ns.example.com hostmaster.example.com (1 60 60 60 60)"
    }
}

A more verbose regex-based equivalent would be:

. {
    forward . 8.8.8.8

    template IN ANY example.com {
      match "example\.com\.(dc1\.example\.com\.)$"
      rcode NXDOMAIN
      authority "{{ index .Match 1 }} 60 IN SOA ns.{{ index .Match 1 }} hostmaster.{{ index .Match 1 }} (1 60 60 60 60)"
      fallthrough
    }
}

The regex-based version can do more complex matching/templating while zone-based templating is easier to read and use. Trade-offs, bestie πŸ’…


πŸ” RESOLVE A/PTR FOR .EXAMPLE

. {
    forward . 8.8.8.8

    # ip-a-b-c-d.example A a.b.c.d

    template IN A example {
      match (^|[.])ip-(?P<a>[0-9]*)-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]example[.]$
      answer "{{ .Name }} 60 IN A {{ .Group.a }}.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}"
      fallthrough
    }

    # d.c.b.a.in-addr.arpa PTR ip-a-b-c-d.example

    template IN PTR in-addr.arpa {
      match ^(?P<d>[0-9]*)[.](?P<c>[0-9]*)[.](?P<b>[0-9]*)[.](?P<a>[0-9]*)[.]in-addr[.]arpa[.]$
      answer "{{ .Name }} 60 IN PTR ip-{{ .Group.a }}-{{ .Group.b }}-{{ .Group.c }}-{{ .Group.d }}.example."
    }
}

An IPv4 address consists of 4 bytes, a.b.c.d. Named groups make it less error-prone to reverse the IP address in the PTR case. Try to use named groups to explain what your regex and template are doing β€” future you will thank you, fr fr.

Note that the A record is actually a wildcard: any subdomain of the IP address will resolve to the IP address.

Having templates to map certain PTR/A pairs is a common pattern.

fallthrough is needed for mixed domains where only some responses are templated.


🏠 RESOLVE DEVICE ADDRESSES FOR HTTPS ON LAN

For an embedded device on a local network to be trusted, it needs to have a certificate signed by a CA and the CA needs to verify that the device controls the name for which it is issued. As the local address may change, a certificate is issued for the wildcard subdomain *.<id>.example.com instead, where <id> is a unique id for the given device. The same trick as in the example above can be used to find out the actual IP address.

Resolving to public IP addresses would allow for this to be used in phishing attacks, so an additional expression must be satisfied to limit the allowed IP range. Safety first, bestie 🫢
. {
    forward . 8.8.8.8

    template IN A example.com {
      match ^(?P<ip>[0-9]{1,3}(-[0-9]{1,3}){3})[.](?P<id>[a-z2-7]{26})[.]example[.]com[.]$
      var ip replace(group('ip'), '-', '.')
      expr "any(['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16', '169.254.0.0/16'], incidr(ip, #))"
      answer "{{ .Name }} 60 IN A {{ .Var.ip }}"
      fallthrough
    }
}

The regular expression for the unique device id matches a base32 encoded string of a 128-bit device id, with the padding removed.

Note that an expression using # must be quoted, or it will be interpreted as a comment. Yeah. That's a whole thing. Don't get got πŸ’€

πŸ”’ RESOLVE HEXADECIMAL IP PATTERN USING PARSEINT

. {
    forward . 8.8.8.8

    template IN A example {
      match "^ip0a(?P<b>[a-f0-9]{2})(?P<c>[a-f0-9]{2})(?P<d>[a-f0-9]{2})[.]example[.]$"
      answer "{{ .Name }} 60 IN A 10.{{ parseInt .Group.b 16 8 }}.{{ parseInt .Group.c 16 8 }}.{{ parseInt .Group.d 16 8 }}"
      fallthrough
    }
}

An IPv4 address can be expressed in a more compact form using its hexadecimal encoding. For example ip-10-123-123.example. can instead be expressed as ip0a7b7b7b.example. β€” compact and kinda iconic ngl.


🎯 RESOLVE MULTIPLE IP PATTERNS

. {
    forward . 8.8.8.8

    template IN A example {
      match "^ip-(?P<a>10)-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]dc[.]example[.]$"
      match "^(?P<a>[0-9]*)[.](?P<b>[0-9]*)[.](?P<c>[0-9]*)[.](?P<d>[0-9]*)[.]ext[.]example[.]$"
      answer "{{ .Name }} 60 IN A {{ .Group.a}}.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}"
      fallthrough
    }
}

Named capture groups can be used to template one response for multiple patterns. One template, many fits. Efficiency queen πŸ‘‘


πŸ“¬ RESOLVE A AND MX RECORDS FOR IP TEMPLATES IN .EXAMPLE

. {
    forward . 8.8.8.8

    template IN A example {
      match ^ip-10-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]example[.]$
      answer "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}"
      fallthrough
    }
    template IN MX example {
      match ^ip-10-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]example[.]$
      answer "{{ .Name }} 60 IN MX 10 {{ .Name }}"
      additional "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}"
      fallthrough
    }
}

Two templates, same match, different record types. It's giving coordination ✨


πŸ‘‘ ADDING AUTHORITATIVE NAMESERVERS TO THE RESPONSE

. {
    forward . 8.8.8.8

    template IN A example {
      match ^ip-10-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]example[.]$
      answer "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}"
      authority  "example. 60 IN NS ns0.example."
      authority  "example. 60 IN NS ns1.example."
      additional "ns0.example. 60 IN A 203.0.113.8"
      additional "ns1.example. 60 IN A 198.51.100.8"
      fallthrough
    }
    template IN MX example {
      match ^ip-10-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]example[.]$
      answer "{{ .Name }} 60 IN MX 10 {{ .Name }}"
      additional "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}"
      authority  "example. 60 IN NS ns0.example."
      authority  "example. 60 IN NS ns1.example."
      additional "ns0.example. 60 IN A 203.0.113.8"
      additional "ns1.example. 60 IN A 198.51.100.8"
      fallthrough
    }
}

Look at all those authority and additional lines β€” serving the full ensemble, not just the main character answer πŸ’…


πŸ¦‹ FABRICATE A CNAME

This example responds with a CNAME to google.com for any DNS query made exactly for foogle.com. The answer will also contain a record for google.com if the upstream nameserver can return a record for it of the requested type.

. {
  template IN ANY foogle.com {
    match "^foogle\.com\.$"
    answer "foogle.com 60 IN CNAME google.com"
  }
  forward . 8.8.8.8
}

foogle.com β†’ google.com. It's giving legally distinct πŸ˜­πŸŽ€


πŸ‘€ ALSO SEE


πŸ’€ BUGS

CoreDNS supports caddyfile environment variables (https://caddyserver.com/docs/caddyfile#env) with notion of {$ENV_VAR}. This parser feature will break Go template variables (https://golang.org/pkg/text/template/#hdr-Variables) notations like {{$variable}}. The equivalent notation {{ $variable }} will work. Try to avoid Go template variables in the context of this plugin.

So: {{$variable}} = broken, {{ $variable }} = works. The space is doing SO much heavy lifting here, bestie. Respect it πŸ™ˆ

And that's the whole page, babes! This was COREDNS-TEMPLATE(7) from CoreDNS, dated October 2026 β€” go forth and template responsibly, and remember: named capture groups are your besties πŸ«Άβœ¨πŸ’–

πŸ“¦ where this page comes from