Templates with expr-lang (#8450)

* plugin/template: Add expr-lang variables

Signed-off-by: Andri Yngvason <andri@yngvason.is>

* plugin/template: Add extra expressions that must match

Signed-off-by: Andri Yngvason <andri@yngvason.is>

* plugin/template: README: Add embedded device resolution example

Signed-off-by: Andri Yngvason <andri@yngvason.is>

---------

Signed-off-by: Andri Yngvason <andri@yngvason.is>
This commit is contained in:
Andri Yngvason
2026-08-20 03:27:03 +00:00
committed by GitHub
parent a53ecb7c2d
commit a1154dcee5
6 changed files with 982 additions and 0 deletions

View File

@@ -13,6 +13,8 @@ The *template* plugin allows you to dynamically respond to queries by just writi
~~~
template CLASS TYPE [ZONE...] {
match REGEX...
var NAME EXPRESSION
expr EXPRESSION
answer RR
additional RR
authority RR
@@ -31,6 +33,12 @@ template CLASS TYPE [ZONE...] {
* `answer|additional|authority` **RR** A [RFC 1035](https://tools.ietf.org/html/rfc1035#section-5) style resource record fragment
built by a [Go template](https://golang.org/pkg/text/template/) that contains the reply. Specifying no answer will result
in a response with an empty answer section.
* `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`. Multiple variables may be set; each one may use the variables declared before it.
See the **Expressions** section.
* `expr` **EXPRESSION** a condition that must 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`. See the **Expressions** section.
* `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` **EXTENDED_ERROR_CODE** is an extended DNS error code as a number defined in `RFC8914` (0, 1, 2,..., 24).
@@ -55,6 +63,7 @@ Each resource record is a full-featured [Go template](https://golang.org/pkg/tex
* `.Message` the complete incoming DNS message.
* `.Question` the matched question section.
* `.Remote` clients 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"`
@@ -68,6 +77,21 @@ The output of the template must be a [RFC 1035](https://tools.ietf.org/html/rfc1
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).
## Expressions
The **EXPRESSION** of a `var` or `expr` is written in the expr language, the same as used by the *view* plugin. See
https://expr-lang.org/docs/language-definition as a detailed reference for valid syntax.
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 is 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 is no such group.
A variable name must be a valid identifier and must not be the name of an existing function, or of a keyword or literal of
the expr language, such as `len` or `true`.
## Metrics
If monitoring is enabled (via the *prometheus* plugin) then the following metrics are exported:
@@ -186,6 +210,36 @@ 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.
~~~ corefile
. {
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.
### Resolve hexadecimal ip pattern using parseInt
~~~ corefile