diff options
author | 2018-01-09 22:30:58 +0100 | |
---|---|---|
committer | 2018-01-09 21:30:58 +0000 | |
commit | 0091e1c9dc0ee093471b8e0d91133ac3c9a51581 (patch) | |
tree | 663dd4015a048a3405ee2f455899046fa5d4c30e /plugin/template | |
parent | a7590897fbe433129e661d7d85f4635a8601ade1 (diff) | |
download | coredns-0091e1c9dc0ee093471b8e0d91133ac3c9a51581.tar.gz coredns-0091e1c9dc0ee093471b8e0d91133ac3c9a51581.tar.zst coredns-0091e1c9dc0ee093471b8e0d91133ac3c9a51581.zip |
Align plugin/template usage and syntax with other plugins (#1360)
* Align plugin/template usage and syntax with other plugins
* Use new fallthrough logic in plugin/template
* Use zone name normalization for plugin/template
* Test fallthrough parsing in plugin/template
* Rework scoping of match checks
Most matches are not plugin global but per template. The plugin does only a
very rough check while detailed checks are done per-template.
Per template checks include:
- Zones
- Class/Type
- Regex
- Fallthrough
* Remove trailing `.` from fully qualified domain names
* Register template metrics with zone/class/type instead of regex
* Remove trailing fqdn dot from multiple testcases
Diffstat (limited to 'plugin/template')
-rw-r--r-- | plugin/template/README.md | 84 | ||||
-rw-r--r-- | plugin/template/metrics.go | 29 | ||||
-rw-r--r-- | plugin/template/setup.go | 104 | ||||
-rw-r--r-- | plugin/template/setup_test.go | 17 | ||||
-rw-r--r-- | plugin/template/template.go | 61 | ||||
-rw-r--r-- | plugin/template/template_test.go | 245 |
6 files changed, 409 insertions, 131 deletions
diff --git a/plugin/template/README.md b/plugin/template/README.md index 6f21cd29b..c951cac83 100644 --- a/plugin/template/README.md +++ b/plugin/template/README.md @@ -9,29 +9,35 @@ The *template* plugin allows you to dynamically repond to queries by just writin ## Syntax ~~~ -template CLASS TYPE [REGEX...] { +template CLASS TYPE [ZONE...] { + [match REGEX...] [answer RR] [additional RR] [authority RR] [...] - [rcode CODE] + [rcode responsecode] + [fallthrough [fallthrough zone...]] } ~~~ -* **CLASS** the query class (usually IN or ANY) -* **TYPE** the query type (A, PTR, ...) +* **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. * **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 build by a [Go template](https://golang.org/pkg/text/template/) that contains the reply. * `rcode` **CODE** A response code (`NXDOMAIN, SERVFAIL, ...`). The default is `SUCCESS`. +* `fallthrough` Continue with the next plugin if the zone matched but no regex did not match. +* `fallthrough zone` One or more zones that may fall through to other plugins. Defaults to all zones of the template. -At least one answer section or rcode is needed. +At least one `answer` or `rcode` directive is needed (e.g. `rcode NXDOMAIN`). [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`). @@ -57,6 +63,22 @@ Both failure cases indicate a problem with the template configuration. ## 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. @@ -65,7 +87,7 @@ The `.invalid` domain is a reserved TLD (see [RFC-2606 Reserved Top Level DNS Na . { proxy . 8.8.8.8 - template ANY ANY "[.]invalid[.]$" { + template ANY ANY invalid { rcode NXDOMAIN answer "invalid. 60 {{ .Class }} SOA a.invalid. b.invalid. (1 60 60 60 60)" } @@ -75,6 +97,7 @@ The `.invalid` domain is a reserved TLD (see [RFC-2606 Reserved Top Level DNS Na 1. A query to .invalid will result in NXDOMAIN (rcode) 2. A dummy SOA record is send to hand out a TTL of 60s for caching 3. Querying `.invalid` of `CH` will also cause a NXDOMAIN/SOA response +4. The default regex is `.*` ### Block invalid search domain completions @@ -88,14 +111,29 @@ path (`dc1.example.com`) added. . { proxy . 8.8.8.8 - template IN ANY "[.](example[.]com[.]dc1[.]example[.]com[.])$" { + template IN ANY example.com.dc1.example.com { + rcode NXDOMAIN + answer "{{ .Zone }} 60 IN SOA a.{{ .Zone }} b.{{ .Zone }} (1 60 60 60 60)" + } +} +~~~ + +A more verbose regex based equivalent would be + +~~~ corefile +. { + proxy . 8.8.8.8 + + template IN ANY example.com { + match "(example.com.dc1.example.com)$" rcode NXDOMAIN answer "{{ index .Match 1 }} 60 IN SOA a.{{ index .Match 1 }} b.{{ index .Match 1 }} (1 60 60 60 60)" + fallthrough } } ~~~ -Using numbered matches works well if there are a few groups (1-4). +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 @@ -105,13 +143,16 @@ Using numbered matches works well if there are a few groups (1-4). # ip-a-b-c-d.example.com A a.b.c.d - template IN A (^|[.])ip-10-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]example[.]$ { + 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 } # d.c.b.a.in-addr.arpa PTR ip-a-b-c-d.example - template IN PTR ^(?P<d>[0-9]*)[.](?P<c>[0-9]*)[.](?P<b>[0-9]*)[.]10[.]in-addr[.]arpa[.]$ { + template IN PTR 10.in-addr.arpa. { + match ^(?P<d>[0-9]*)[.](?P<c>[0-9]*)[.](?P<b>[0-9]*)[.]10[.]in-addr[.]arpa[.]$ answer "{{ .Name }} 60 IN PTR ip-10-{{ .Group.b }}-{{ .Group.c }}-{{ .Group.d }}.example.com." } } @@ -124,14 +165,19 @@ Note that the A record is actually a wildcard, any subdomain of the ip will reso 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 multiple ip patterns ~~~ corefile . { proxy . 8.8.8.8 - template IN A "^ip-(?P<a>10)-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]dc[.]example[.]$" "^(?P<a>[0-9]*)[.](?P<b>[0-9]*)[.](?P<c>[0-9]*)[.](?P<d>[0-9]*)[.]ext[.]example[.]$" { + 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 } } ~~~ @@ -144,12 +190,16 @@ Named capture groups can be used to template one response for multiple patterns. . { proxy . 8.8.8.8 - template IN A ^ip-10-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]example[.]$ { + 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 ^ip-10-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]example[.]$ { + 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 } } ~~~ @@ -160,20 +210,24 @@ Named capture groups can be used to template one response for multiple patterns. . { proxy . 8.8.8.8 - template IN A ^ip-10-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]example[.]$ { + 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 ^ip-10-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]example[.]$ { + 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 } } ~~~ diff --git a/plugin/template/metrics.go b/plugin/template/metrics.go index f602548f4..a963b3a31 100644 --- a/plugin/template/metrics.go +++ b/plugin/template/metrics.go @@ -3,7 +3,10 @@ package template import ( "sync" + "github.com/coredns/coredns/core/dnsserver" "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/metrics" + "github.com/mholt/caddy" "github.com/prometheus/client_golang/prometheus" ) @@ -15,28 +18,38 @@ var ( Subsystem: "template", Name: "matches_total", Help: "Counter of template regex matches.", - }, []string{"regex"}) + }, []string{"zone", "class", "type"}) TemplateFailureCount = prometheus.NewCounterVec(prometheus.CounterOpts{ Namespace: plugin.Namespace, Subsystem: "template", Name: "template_failures_total", Help: "Counter of go template failures.", - }, []string{"regex", "section", "template"}) + }, []string{"zone", "class", "type", "section", "template"}) TemplateRRFailureCount = prometheus.NewCounterVec(prometheus.CounterOpts{ Namespace: plugin.Namespace, Subsystem: "template", Name: "rr_failures_total", Help: "Counter of mis-templated RRs.", - }, []string{"regex", "section", "template"}) + }, []string{"zone", "class", "type", "section", "template"}) ) // OnStartupMetrics sets up the metrics on startup. -func OnStartupMetrics() error { - metricsOnce.Do(func() { - prometheus.MustRegister(TemplateMatchesCount) - prometheus.MustRegister(TemplateFailureCount) - prometheus.MustRegister(TemplateRRFailureCount) +func setupMetrics(c *caddy.Controller) error { + c.OnStartup(func() error { + metricsOnce.Do(func() { + m := dnsserver.GetConfig(c).Handler("prometheus") + if m == nil { + return + } + if x, ok := m.(*metrics.Metrics); ok { + x.MustRegister(TemplateMatchesCount) + x.MustRegister(TemplateFailureCount) + x.MustRegister(TemplateRRFailureCount) + } + }) + return nil }) + return nil } diff --git a/plugin/template/setup.go b/plugin/template/setup.go index 61b6728c9..2065ef649 100644 --- a/plugin/template/setup.go +++ b/plugin/template/setup.go @@ -19,74 +19,86 @@ func init() { } func setupTemplate(c *caddy.Controller) error { - templates, err := templateParse(c) + handler, err := templateParse(c) if err != nil { return plugin.Error("template", err) } - c.OnStartup(OnStartupMetrics) + if err := setupMetrics(c); err != nil { + return plugin.Error("template", err) + } dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { - return Handler{Next: next, Templates: templates} + handler.Next = next + return handler }) return nil } -func templateParse(c *caddy.Controller) (templates []template, err error) { - templates = make([]template, 0) +func templateParse(c *caddy.Controller) (handler Handler, err error) { + handler.Templates = make([]template, 0) for c.Next() { - t := template{} + if !c.NextArg() { - return nil, c.ArgErr() + return handler, c.ArgErr() } - class, ok := dns.StringToClass[c.Val()] if !ok { - return nil, c.Errf("invalid query class %s", c.Val()) + return handler, c.Errf("invalid query class %s", c.Val()) } - t.class = class if !c.NextArg() { - return nil, c.ArgErr() + return handler, c.ArgErr() } - queryType, ok := dns.StringToType[c.Val()] + qtype, ok := dns.StringToType[c.Val()] if !ok { - return nil, c.Errf("invalid RR type %s", c.Val()) + return handler, c.Errf("invalid RR class %s", c.Val()) } - t.qtype = queryType - t.regex = make([]*regexp.Regexp, 0) - templatePrefix := "" - - for _, regex := range c.RemainingArgs() { - r, err := regexp.Compile(regex) - if err != nil { - return nil, c.Errf("could not parse regex: %s, %v", regex, err) - } - templatePrefix = templatePrefix + regex + " " - t.regex = append(t.regex, r) + zones := c.RemainingArgs() + if len(zones) == 0 { + zones = make([]string, len(c.ServerBlockKeys)) + copy(zones, c.ServerBlockKeys) } - - if len(t.regex) == 0 { - t.regex = append(t.regex, regexp.MustCompile(".*")) - templatePrefix = ".* " + for i, str := range zones { + zones[i] = plugin.Host(str).Normalize() } + handler.Zones = append(handler.Zones, zones...) + + t := template{qclass: class, qtype: qtype, zones: zones} + + t.regex = make([]*regexp.Regexp, 0) + templatePrefix := "" t.answer = make([]*gotmpl.Template, 0) for c.NextBlock() { switch c.Val() { + case "match": + args := c.RemainingArgs() + if len(args) == 0 { + return handler, c.ArgErr() + } + for _, regex := range args { + r, err := regexp.Compile(regex) + if err != nil { + return handler, c.Errf("could not parse regex: %s, %v", regex, err) + } + templatePrefix = templatePrefix + regex + " " + t.regex = append(t.regex, r) + } + case "answer": args := c.RemainingArgs() if len(args) == 0 { - return nil, c.ArgErr() + return handler, c.ArgErr() } for _, answer := range args { tmpl, err := gotmpl.New("answer").Parse(answer) if err != nil { - return nil, c.Errf("could not compile template: %s, %v", c.Val(), err) + return handler, c.Errf("could not compile template: %s, %v", c.Val(), err) } t.answer = append(t.answer, tmpl) } @@ -94,12 +106,12 @@ func templateParse(c *caddy.Controller) (templates []template, err error) { case "additional": args := c.RemainingArgs() if len(args) == 0 { - return nil, c.ArgErr() + return handler, c.ArgErr() } for _, additional := range args { tmpl, err := gotmpl.New("additional").Parse(additional) if err != nil { - return nil, c.Errf("could not compile template: %s, %v\n", c.Val(), err) + return handler, c.Errf("could not compile template: %s, %v\n", c.Val(), err) } t.additional = append(t.additional, tmpl) } @@ -107,37 +119,49 @@ func templateParse(c *caddy.Controller) (templates []template, err error) { case "authority": args := c.RemainingArgs() if len(args) == 0 { - return nil, c.ArgErr() + return handler, c.ArgErr() } for _, authority := range args { tmpl, err := gotmpl.New("authority").Parse(authority) if err != nil { - return nil, c.Errf("could not compile template: %s, %v\n", c.Val(), err) + return handler, c.Errf("could not compile template: %s, %v\n", c.Val(), err) } t.authority = append(t.authority, tmpl) } case "rcode": if !c.NextArg() { - return nil, c.ArgErr() + return handler, c.ArgErr() } rcode, ok := dns.StringToRcode[c.Val()] if !ok { - return nil, c.Errf("unknown rcode %s", c.Val()) + return handler, c.Errf("unknown rcode %s", c.Val()) } t.rcode = rcode + case "fallthrough": + args := c.RemainingArgs() + if len(args) > 0 { + t.fthrough.SetZonesFromArgs(c.RemainingArgs()) + } else { + t.fthrough.SetZonesFromArgs(zones) + } + default: - return nil, c.ArgErr() + return handler, c.ArgErr() } } + if len(t.regex) == 0 { + t.regex = append(t.regex, regexp.MustCompile(".*")) + } + if len(t.answer) == 0 && len(t.additional) == 0 && t.rcode == dns.RcodeSuccess { - return nil, c.Errf("no answer section for template %s %sfound", t.qtype, templatePrefix) + return handler, c.Errf("no answer section for template found: %v", handler) } - templates = append(templates, t) + handler.Templates = append(handler.Templates, t) } - return templates, nil + return } diff --git a/plugin/template/setup_test.go b/plugin/template/setup_test.go index a488ff2ff..8233aed66 100644 --- a/plugin/template/setup_test.go +++ b/plugin/template/setup_test.go @@ -93,30 +93,37 @@ func TestSetupParse(t *testing.T) { }, // examples { - `template ANY A ip-(?P<a>[0-9]*)-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]example[.]com { + `template ANY A example.com { + match ip-(?P<a>[0-9]*)-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]example[.]com answer "{{ .Name }} A {{ .Group.a }}.{{ .Group.b }}.{{ .Group.c }}.{{ .Grup.d }}." + fallthrough }`, false, }, { - `template IN ANY "[.](example[.]com[.]dc1[.]example[.]com[.])$" { + `template IN ANY example.com { + match "[.](example[.]com[.]dc1[.]example[.]com[.])$" rcode NXDOMAIN answer "{{ index .Match 1 }} 60 IN SOA a.{{ index .Match 1 }} b.{{ index .Match 1 }} (1 60 60 60 60)" + fallthrough example.com }`, false, }, { - `template IN A ^ip-10-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]example[.]$ { + `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 }}" } - template IN MX ^ip-10-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]example[.]$ { + 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 }}" }`, false, }, { - `template IN MX ^ip-10-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]example[.]$ { + `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." diff --git a/plugin/template/template.go b/plugin/template/template.go index 62c284177..b11292dbd 100644 --- a/plugin/template/template.go +++ b/plugin/template/template.go @@ -8,6 +8,7 @@ import ( gotmpl "text/template" "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/fall" "github.com/coredns/coredns/request" "github.com/miekg/dns" @@ -15,21 +16,26 @@ import ( // Handler is a plugin handler that takes a query and templates a response. type Handler struct { + Zones []string + Next plugin.Handler Templates []template } type template struct { + zones []string rcode int - class uint16 - qtype uint16 regex []*regexp.Regexp answer []*gotmpl.Template additional []*gotmpl.Template authority []*gotmpl.Template + qclass uint16 + qtype uint16 + fthrough fall.F } type templateData struct { + Zone string Name string Regex string Match []string @@ -44,13 +50,21 @@ type templateData struct { func (h Handler) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { state := request.Request{W: w, Req: r} + zone := plugin.Zones(h.Zones).Matches(state.Name()) + if zone == "" { + return plugin.NextOrFailure(h.Name(), h.Next, ctx, w, r) + } + for _, template := range h.Templates { - data, match := template.match(state) + data, match, fthrough := template.match(state, zone) if !match { + if !fthrough { + return dns.RcodeNameError, nil + } continue } - TemplateMatchesCount.WithLabelValues(data.Regex).Inc() + TemplateMatchesCount.WithLabelValues(data.Zone, data.Class, data.Type).Inc() if template.rcode == dns.RcodeServerFailure { return template.rcode, nil @@ -87,7 +101,8 @@ func (h Handler) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) w.WriteMsg(msg) return template.rcode, nil } - return plugin.NextOrFailure(h.Name(), h.Next, ctx, w, r) + + return h.Next.ServeDNS(ctx, w, r) } // Name implements the plugin.Handler interface. @@ -97,38 +112,53 @@ func executeRRTemplate(section string, template *gotmpl.Template, data templateD buffer := &bytes.Buffer{} err := template.Execute(buffer, data) if err != nil { - TemplateFailureCount.WithLabelValues(data.Regex, section, template.Tree.Root.String()).Inc() + TemplateFailureCount.WithLabelValues(data.Zone, data.Class, data.Type, section, template.Tree.Root.String()).Inc() return nil, err } rr, err := dns.NewRR(buffer.String()) if err != nil { - TemplateRRFailureCount.WithLabelValues(data.Regex, section, template.Tree.Root.String()).Inc() + TemplateRRFailureCount.WithLabelValues(data.Zone, data.Class, data.Type, section, template.Tree.Root.String()).Inc() return rr, err } return rr, nil } -func (t template) match(state request.Request) (templateData, bool) { +func (t template) match(state request.Request, zone string) (templateData, bool, bool) { q := state.Req.Question[0] data := templateData{} - if t.class != dns.ClassANY && q.Qclass != dns.ClassANY && q.Qclass != t.class { - return data, false + zone = plugin.Zones(t.zones).Matches(state.Name()) + if zone == "" { + return data, false, true + } + + if t.qclass != dns.ClassANY && q.Qclass != dns.ClassANY && q.Qclass != t.qclass { + return data, false, true } if t.qtype != dns.TypeANY && q.Qtype != dns.TypeANY && q.Qtype != t.qtype { - return data, false + return data, false, true } + for _, regex := range t.regex { if !regex.MatchString(state.Name()) { continue } + data.Zone = zone data.Regex = regex.String() data.Name = state.Name() data.Question = &q data.Message = state.Req - data.Class = dns.ClassToString[q.Qclass] - data.Type = dns.TypeToString[q.Qtype] + if q.Qclass != dns.ClassANY { + data.Class = dns.ClassToString[q.Qclass] + } else { + data.Class = dns.ClassToString[t.qclass] + } + if q.Qtype != dns.TypeANY { + data.Type = dns.TypeToString[q.Qtype] + } else { + data.Type = dns.TypeToString[t.qtype] + } matches := regex.FindStringSubmatch(state.Name()) data.Match = make([]string, len(matches)) @@ -144,7 +174,8 @@ func (t template) match(state request.Request) (templateData, bool) { } } - return data, true + return data, true, false } - return data, false + + return data, false, t.fthrough.Through(state.Name()) } diff --git a/plugin/template/template_test.go b/plugin/template/template_test.go index 0e1470644..a815e8c7c 100644 --- a/plugin/template/template_test.go +++ b/plugin/template/template_test.go @@ -7,80 +7,100 @@ import ( "testing" "github.com/coredns/coredns/plugin/test" + "github.com/mholt/caddy" gotmpl "text/template" "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/pkg/fall" "github.com/miekg/dns" ) func TestHandler(t *testing.T) { rcodeFallthrough := 3841 // reserved for private use, used to indicate a fallthrough exampleDomainATemplate := template{ - class: dns.ClassINET, - qtype: dns.TypeA, - regex: []*regexp.Regexp{regexp.MustCompile("(^|[.])ip-10-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]example[.]$")}, - answer: []*gotmpl.Template{gotmpl.Must(gotmpl.New("answer").Parse("{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}"))}, + regex: []*regexp.Regexp{regexp.MustCompile("(^|[.])ip-10-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]example[.]$")}, + answer: []*gotmpl.Template{gotmpl.Must(gotmpl.New("answer").Parse("{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}"))}, + qclass: dns.ClassANY, + qtype: dns.TypeANY, + fthrough: fall.F{Zones: []string{"."}}, + zones: []string{"."}, } exampleDomainANSTemplate := template{ - class: dns.ClassINET, - qtype: dns.TypeA, regex: []*regexp.Regexp{regexp.MustCompile("(^|[.])ip-10-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]example[.]$")}, answer: []*gotmpl.Template{gotmpl.Must(gotmpl.New("answer").Parse("{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}"))}, additional: []*gotmpl.Template{gotmpl.Must(gotmpl.New("additional").Parse("ns0.example. IN A 203.0.113.8"))}, authority: []*gotmpl.Template{gotmpl.Must(gotmpl.New("authority").Parse("example. IN NS ns0.example.com."))}, + qclass: dns.ClassANY, + qtype: dns.TypeANY, + fthrough: fall.F{Zones: []string{"."}}, + zones: []string{"."}, } exampleDomainMXTemplate := template{ - class: dns.ClassINET, - qtype: dns.TypeMX, regex: []*regexp.Regexp{regexp.MustCompile("(^|[.])ip-10-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]example[.]$")}, answer: []*gotmpl.Template{gotmpl.Must(gotmpl.New("answer").Parse("{{ .Name }} 60 MX 10 {{ .Name }}"))}, additional: []*gotmpl.Template{gotmpl.Must(gotmpl.New("additional").Parse("{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}"))}, + qclass: dns.ClassANY, + qtype: dns.TypeANY, + fthrough: fall.F{Zones: []string{"."}}, + zones: []string{"."}, } invalidDomainTemplate := template{ - class: dns.ClassANY, - qtype: dns.TypeANY, - regex: []*regexp.Regexp{regexp.MustCompile("[.]invalid[.]$")}, - rcode: dns.RcodeNameError, - answer: []*gotmpl.Template{gotmpl.Must(gotmpl.New("answer").Parse("invalid. 60 {{ .Class }} SOA a.invalid. b.invalid. (1 60 60 60 60)"))}, + regex: []*regexp.Regexp{regexp.MustCompile("[.]invalid[.]$")}, + rcode: dns.RcodeNameError, + answer: []*gotmpl.Template{gotmpl.Must(gotmpl.New("answer").Parse("invalid. 60 {{ .Class }} SOA a.invalid. b.invalid. (1 60 60 60 60)"))}, + qclass: dns.ClassANY, + qtype: dns.TypeANY, + fthrough: fall.F{Zones: []string{"."}}, + zones: []string{"."}, } rcodeServfailTemplate := template{ - class: dns.ClassANY, - qtype: dns.TypeANY, - regex: []*regexp.Regexp{regexp.MustCompile(".*")}, - rcode: dns.RcodeServerFailure, + regex: []*regexp.Regexp{regexp.MustCompile(".*")}, + rcode: dns.RcodeServerFailure, + qclass: dns.ClassANY, + qtype: dns.TypeANY, + fthrough: fall.F{Zones: []string{"."}}, + zones: []string{"."}, } brokenTemplate := template{ - class: dns.ClassINET, - qtype: dns.TypeA, - regex: []*regexp.Regexp{regexp.MustCompile("[.]example[.]$")}, - answer: []*gotmpl.Template{gotmpl.Must(gotmpl.New("answer").Parse("{{ .Name }} 60 IN TXT \"{{ index .Match 2 }}\""))}, + regex: []*regexp.Regexp{regexp.MustCompile("[.]example[.]$")}, + answer: []*gotmpl.Template{gotmpl.Must(gotmpl.New("answer").Parse("{{ .Name }} 60 IN TXT \"{{ index .Match 2 }}\""))}, + qclass: dns.ClassANY, + qtype: dns.TypeANY, + fthrough: fall.F{Zones: []string{"."}}, + zones: []string{"."}, } nonRRTemplate := template{ - class: dns.ClassINET, - qtype: dns.TypeA, - regex: []*regexp.Regexp{regexp.MustCompile("[.]example[.]$")}, - answer: []*gotmpl.Template{gotmpl.Must(gotmpl.New("answer").Parse("{{ .Name }}"))}, + regex: []*regexp.Regexp{regexp.MustCompile("[.]example[.]$")}, + answer: []*gotmpl.Template{gotmpl.Must(gotmpl.New("answer").Parse("{{ .Name }}"))}, + qclass: dns.ClassANY, + qtype: dns.TypeANY, + fthrough: fall.F{Zones: []string{"."}}, + zones: []string{"."}, } nonRRAdditionalTemplate := template{ - class: dns.ClassINET, - qtype: dns.TypeA, regex: []*regexp.Regexp{regexp.MustCompile("[.]example[.]$")}, additional: []*gotmpl.Template{gotmpl.Must(gotmpl.New("answer").Parse("{{ .Name }}"))}, + qclass: dns.ClassANY, + qtype: dns.TypeANY, + fthrough: fall.F{Zones: []string{"."}}, + zones: []string{"."}, } nonRRAuthoritativeTemplate := template{ - class: dns.ClassINET, - qtype: dns.TypeA, regex: []*regexp.Regexp{regexp.MustCompile("[.]example[.]$")}, authority: []*gotmpl.Template{gotmpl.Must(gotmpl.New("authority").Parse("{{ .Name }}"))}, + qclass: dns.ClassANY, + qtype: dns.TypeANY, + fthrough: fall.F{Zones: []string{"."}}, + zones: []string{"."}, } tests := []struct { tmpl template qname string + name string qclass uint16 qtype uint16 - name string expectedCode int expectedErr string verifyResponse func(*dns.Msg) error @@ -88,8 +108,6 @@ func TestHandler(t *testing.T) { { name: "RcodeServFail", tmpl: rcodeServfailTemplate, - qclass: dns.ClassANY, - qtype: dns.TypeANY, qname: "test.invalid.", expectedCode: dns.RcodeServerFailure, verifyResponse: func(r *dns.Msg) error { @@ -222,22 +240,6 @@ func TestHandler(t *testing.T) { }, }, { - name: "ExampleDomainMismatchType", - tmpl: exampleDomainATemplate, - qclass: dns.ClassINET, - qtype: dns.TypeMX, - qname: "ip-10-95-12-8.example.", - expectedCode: rcodeFallthrough, - }, - { - name: "ExampleDomainMismatchClass", - tmpl: exampleDomainATemplate, - qclass: dns.ClassCHAOS, - qtype: dns.TypeA, - qname: "ip-10-95-12-8.example.", - expectedCode: rcodeFallthrough, - }, - { name: "ExampleInvalidNXDOMAIN", tmpl: invalidDomainTemplate, qclass: dns.ClassINET, @@ -261,6 +263,7 @@ func TestHandler(t *testing.T) { for _, tr := range tests { handler := Handler{ Next: test.NextHandler(rcodeFallthrough, nil), + Zones: []string{"."}, Templates: []template{tr.tmpl}, } req := &dns.Msg{ @@ -292,3 +295,149 @@ func TestHandler(t *testing.T) { } } } + +// TestMultiSection verfies that a corefile with mutliple but different template sections works +func TestMultiSection(t *testing.T) { + rcodeFallthrough := 3841 // reserved for private use, used to indicate a fallthrough + ctx := context.TODO() + + multisectionConfig := ` + # Implicit section (see c.ServerBlockKeys) + # test.:8053 { + + # REFUSE IN A for the server zone (test.) + template IN A { + rcode REFUSED + } + # Fallthrough everyting IN TXT for test. + template IN TXT { + match "$^" + rcode SERVFAIL + fallthrough + } + # Answer CH TXT *.coredns.invalid. / coredns.invalid. + template CH TXT coredns.invalid { + answer "{{ .Name }} 60 CH TXT \"test\"" + } + # Anwser example. ip templates and fallthrough otherwise + 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 + } + # Answer MX record requests for ip templates in example. and never fall through + 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 }}" + } + ` + c := caddy.NewTestController("dns", multisectionConfig) + c.ServerBlockKeys = []string{"test.:8053"} + + handler, err := templateParse(c) + if err != nil { + t.Fatalf("TestMultiSection could not parse config: %v", err) + } + + handler.Next = test.NextHandler(rcodeFallthrough, nil) + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + + // Asking for test. IN A -> REFUSED + + req := &dns.Msg{Question: []dns.Question{{Name: "some.test.", Qclass: dns.ClassINET, Qtype: dns.TypeA}}} + code, err := handler.ServeDNS(ctx, rec, req) + if err != nil { + t.Fatalf("TestMultiSection expected no error resolving some.test. A, got: %v", err) + } + if code != dns.RcodeRefused { + t.Fatalf("TestMultiSection expected response code REFUSED got: %v", code) + } + + // Asking for test. IN TXT -> fallthrough + + req = &dns.Msg{Question: []dns.Question{{Name: "some.test.", Qclass: dns.ClassINET, Qtype: dns.TypeTXT}}} + code, err = handler.ServeDNS(ctx, rec, req) + if err != nil { + t.Fatalf("TestMultiSection expected no error resolving some.test. TXT, got: %v", err) + } + if code != rcodeFallthrough { + t.Fatalf("TestMultiSection expected response code fallthrough got: %v", code) + } + + // Asking for coredns.invalid. CH TXT -> TXT "test" + + req = &dns.Msg{Question: []dns.Question{{Name: "coredns.invalid.", Qclass: dns.ClassCHAOS, Qtype: dns.TypeTXT}}} + code, err = handler.ServeDNS(ctx, rec, req) + if err != nil { + t.Fatalf("TestMultiSection expected no error resolving coredns.invalid. TXT, got: %v", err) + } + if code != dns.RcodeSuccess { + t.Fatalf("TestMultiSection expected success response for coredns.invalid. TXT got: %v", code) + } + if len(rec.Msg.Answer) != 1 { + t.Fatalf("TestMultiSection expected one answer for coredns.invalid. TXT got: %v", rec.Msg.Answer) + } + if rec.Msg.Answer[0].Header().Rrtype != dns.TypeTXT || rec.Msg.Answer[0].(*dns.TXT).Txt[0] != "test" { + t.Fatalf("TestMultiSection a \"test\" answer for coredns.invalid. TXT got: %v", rec.Msg.Answer[0]) + } + + // Asking for an ip template in example + + req = &dns.Msg{Question: []dns.Question{{Name: "ip-10-11-12-13.example.", Qclass: dns.ClassINET, Qtype: dns.TypeA}}} + code, err = handler.ServeDNS(ctx, rec, req) + if err != nil { + t.Fatalf("TestMultiSection expected no error resolving ip-10-11-12-13.example. IN A, got: %v", err) + } + if code != dns.RcodeSuccess { + t.Fatalf("TestMultiSection expected success response ip-10-11-12-13.example. IN A got: %v, %v", code, dns.RcodeToString[code]) + } + if len(rec.Msg.Answer) != 1 { + t.Fatalf("TestMultiSection expected one answer for ip-10-11-12-13.example. IN A got: %v", rec.Msg.Answer) + } + if rec.Msg.Answer[0].Header().Rrtype != dns.TypeA { + t.Fatalf("TestMultiSection an A RR answer for ip-10-11-12-13.example. IN A got: %v", rec.Msg.Answer[0]) + } + + // Asking for an MX ip template in example + + req = &dns.Msg{Question: []dns.Question{{Name: "ip-10-11-12-13.example.", Qclass: dns.ClassINET, Qtype: dns.TypeMX}}} + code, err = handler.ServeDNS(ctx, rec, req) + if err != nil { + t.Fatalf("TestMultiSection expected no error resolving ip-10-11-12-13.example. IN MX, got: %v", err) + } + if code != dns.RcodeSuccess { + t.Fatalf("TestMultiSection expected success response ip-10-11-12-13.example. IN MX got: %v, %v", code, dns.RcodeToString[code]) + } + if len(rec.Msg.Answer) != 1 { + t.Fatalf("TestMultiSection expected one answer for ip-10-11-12-13.example. IN MX got: %v", rec.Msg.Answer) + } + if rec.Msg.Answer[0].Header().Rrtype != dns.TypeMX { + t.Fatalf("TestMultiSection an A RR answer for ip-10-11-12-13.example. IN MX got: %v", rec.Msg.Answer[0]) + } + + // Test that something.example. A does fall through but something.example. MX does not + + req = &dns.Msg{Question: []dns.Question{{Name: "something.example.", Qclass: dns.ClassINET, Qtype: dns.TypeA}}} + code, err = handler.ServeDNS(ctx, rec, req) + if err != nil { + t.Fatalf("TestMultiSection expected no error resolving something.example. IN A, got: %v", err) + } + if code != rcodeFallthrough { + t.Fatalf("TestMultiSection expected a fall through resolving something.example. IN A, got: %v, %v", code, dns.RcodeToString[code]) + } + + req = &dns.Msg{Question: []dns.Question{{Name: "something.example.", Qclass: dns.ClassINET, Qtype: dns.TypeMX}}} + code, err = handler.ServeDNS(ctx, rec, req) + if err != nil { + t.Fatalf("TestMultiSection expected no error resolving something.example. IN MX, got: %v", err) + } + if code == rcodeFallthrough { + t.Fatalf("TestMultiSection expected no fall through resolving something.example. IN MX") + } + if code != dns.RcodeNameError { + t.Fatalf("TestMultiSection expected NXDOMAIN resolving something.example. IN MX, got %v, %v", code, dns.RcodeToString[code]) + } + +} |