aboutsummaryrefslogtreecommitdiff
path: root/plugin/template/README.md
blob: 1bca906624c811ff9f5cc7cf53cc6127bdee8ff0 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
# template

## Name

*template* - allows for dynamic responses based on the incoming query.

## Description

The *template* plugin allows you to dynamically respond to queries by just writing a (Go) template.

## Syntax

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

* **CLASS** the query class (usually IN or ANY).
* **TYPE** the query type (A, PTR, ... can be ANY to match all types).
* **ZONE** the zone scope(s) for this template. Defaults to the server zones.
* `match` **REGEX** [Go regexp](https://golang.org/pkg/regexp/) that are matched against the incoming question name.
  Specifying no regex matches everything (default: `.*`). First matching regex wins.
* `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.
* `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).
              **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 is 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. Without
  `fallthrough`, when the _template_'s **ZONE** matches a query but no regex match then a `SERVFAIL` response is returned.

[Also see](#also-see) contains an additional reading list.

## Templates

Each resource record is a full-featured [Go template](https://golang.org/pkg/text/template/) with the following predefined data

* `.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
* `.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)

* `parseInt` interprets a string in the given base and bit size. Equivalent to [strconv.ParseUint](https://golang.org/pkg/strconv#ParseUint).

The output of the template must be a [RFC 1035](https://tools.ietf.org/html/rfc1035) style resource record (commonly referred to as a "zone file").

**WARNING** there is 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).

## Metrics

If monitoring is enabled (via the *prometheus* plugin) then the following metrics are exported:

* `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. 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.

## Examples

### Resolve everything to NXDOMAIN

The most simplistic template is

~~~ corefile
. {
    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://tools.ietf.org/html/rfc2606#section-2)) to indicate invalid domains.

~~~ corefile
. {
    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

Imagine you run `example.com` with a datacenter `dc1.example.com`. The datacenter domain
is part of the DNS search domain.
However `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`) added.

~~~ corefile
. {
    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

~~~ corefile
. {
    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.

### Resolve A/PTR for .example

~~~ corefile
. {
    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.

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 hexadecimal ip pattern using parseInt

~~~ corefile
. {
    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.`

### Resolve multiple ip patterns

~~~ corefile
. {
    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.

### Resolve A and MX records for IP templates in .example

~~~ corefile
. {
    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
    }
}
~~~

### Adding authoritative nameservers to the response

~~~ corefile
. {
    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
    }
}
~~~

### 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.

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

## Also see

* [Go regexp](https://golang.org/pkg/regexp/) for details about the regex implementation
* [RE2 syntax reference](https://github.com/google/re2/wiki/Syntax) for details about the regex syntax
* [RFC 1034](https://tools.ietf.org/html/rfc1034#section-3.6.1) and [RFC 1035](https://tools.ietf.org/html/rfc1035#section-5) for the resource record format
* [Go template](https://golang.org/pkg/text/template/) for the template language reference

## 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.