mirror of
				https://github.com/coredns/coredns.git
				synced 2025-10-30 17:53:21 -04:00 
			
		
		
		
	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
This commit is contained in:
		
				
					committed by
					
						 Miek Gieben
						Miek Gieben
					
				
			
			
				
	
			
			
			
						parent
						
							a7590897fb
						
					
				
				
					commit
					0091e1c9dc
				
			| @@ -9,29 +9,35 @@ The *template* plugin allows you to dynamically repond to queries by just writin | |||||||
| ## Syntax | ## Syntax | ||||||
|  |  | ||||||
| ~~~ | ~~~ | ||||||
| template CLASS TYPE [REGEX...] { | template CLASS TYPE [ZONE...] { | ||||||
|  |     [match REGEX...] | ||||||
|     [answer RR] |     [answer RR] | ||||||
|     [additional RR] |     [additional RR] | ||||||
|     [authority RR] |     [authority RR] | ||||||
|     [...] |     [...] | ||||||
|     [rcode CODE] |     [rcode responsecode] | ||||||
|  |     [fallthrough [fallthrough zone...]] | ||||||
| } | } | ||||||
| ~~~ | ~~~ | ||||||
|  |  | ||||||
| * **CLASS** the query class (usually IN or ANY) | * **CLASS** the query class (usually IN or ANY). | ||||||
| * **TYPE** the query type (A, PTR, ...) | * **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. | * **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 | * `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. |   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`. | * `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. | [Also see](#also-see) contains an additional reading list. | ||||||
|  |  | ||||||
| ## Templates | ## Templates | ||||||
|  |  | ||||||
| Each resource record is a full-featured [Go template](https://golang.org/pkg/text/template/) with the following predefined data | 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). | * `.Name` the query name, as a string (lowercased). | ||||||
| * `.Class` the query class (usually `IN`). | * `.Class` the query class (usually `IN`). | ||||||
| * `.Type` the RR type requested (e.g. `PTR`). | * `.Type` the RR type requested (e.g. `PTR`). | ||||||
| @@ -57,6 +63,22 @@ Both failure cases indicate a problem with the template configuration. | |||||||
|  |  | ||||||
| ## Examples | ## 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 | ### 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. | 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 |     proxy . 8.8.8.8 | ||||||
|  |  | ||||||
|     template ANY ANY "[.]invalid[.]$" { |     template ANY ANY invalid { | ||||||
|       rcode NXDOMAIN |       rcode NXDOMAIN | ||||||
|       answer "invalid. 60 {{ .Class }} SOA a.invalid. b.invalid. (1 60 60 60 60)" |       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) | 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 | 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 | 3. Querying `.invalid` of `CH` will also cause a NXDOMAIN/SOA response | ||||||
|  | 4. The default regex is `.*` | ||||||
|  |  | ||||||
| ### Block invalid search domain completions | ### Block invalid search domain completions | ||||||
|  |  | ||||||
| @@ -88,14 +111,29 @@ path (`dc1.example.com`) added. | |||||||
| . { | . { | ||||||
|     proxy . 8.8.8.8 |     proxy . 8.8.8.8 | ||||||
|  |  | ||||||
|     template IN ANY "[.](example[.]com[.]dc1[.]example[.]com[.])$" { |     template IN ANY example.com.dc1.example.com { | ||||||
|       rcode NXDOMAIN |       rcode NXDOMAIN | ||||||
|       answer "{{ index .Match 1 }} 60 IN SOA a.{{ index .Match 1 }} b.{{ index .Match 1 }} (1 60 60 60 60)" |       answer "{{ .Zone }} 60 IN SOA a.{{ .Zone }} b.{{ .Zone }} (1 60 60 60 60)" | ||||||
|     } |     } | ||||||
| } | } | ||||||
| ~~~ | ~~~ | ||||||
|  |  | ||||||
| Using numbered matches works well if there are a few groups (1-4). | 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 | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | ~~~ | ||||||
|  |  | ||||||
|  | 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 | ### 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 |     # 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 }}" |       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 |     # 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." |       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. | 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 | ### Resolve multiple ip patterns | ||||||
|  |  | ||||||
| ~~~ corefile | ~~~ corefile | ||||||
| . { | . { | ||||||
|     proxy . 8.8.8.8 |     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 }}" |       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 |     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 }}" |       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 }}" |       answer "{{ .Name }} 60 IN MX 10 {{ .Name }}" | ||||||
|       additional "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}" |       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 |     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 }}" |       answer "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}" | ||||||
|       authority  "example. 60 IN NS ns0.example." |       authority  "example. 60 IN NS ns0.example." | ||||||
|       authority  "example. 60 IN NS ns1.example." |       authority  "example. 60 IN NS ns1.example." | ||||||
|       additional "ns0.example. 60 IN A 203.0.113.8" |       additional "ns0.example. 60 IN A 203.0.113.8" | ||||||
|       additional "ns1.example. 60 IN A 198.51.100.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 }}" |       answer "{{ .Name }} 60 IN MX 10 {{ .Name }}" | ||||||
|       additional "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}" |       additional "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}" | ||||||
|       authority  "example. 60 IN NS ns0.example." |       authority  "example. 60 IN NS ns0.example." | ||||||
|       authority  "example. 60 IN NS ns1.example." |       authority  "example. 60 IN NS ns1.example." | ||||||
|       additional "ns0.example. 60 IN A 203.0.113.8" |       additional "ns0.example. 60 IN A 203.0.113.8" | ||||||
|       additional "ns1.example. 60 IN A 198.51.100.8" |       additional "ns1.example. 60 IN A 198.51.100.8" | ||||||
|  |       fallthrough | ||||||
|     } |     } | ||||||
| } | } | ||||||
| ~~~ | ~~~ | ||||||
|   | |||||||
| @@ -3,7 +3,10 @@ package template | |||||||
| import ( | import ( | ||||||
| 	"sync" | 	"sync" | ||||||
|  |  | ||||||
|  | 	"github.com/coredns/coredns/core/dnsserver" | ||||||
| 	"github.com/coredns/coredns/plugin" | 	"github.com/coredns/coredns/plugin" | ||||||
|  | 	"github.com/coredns/coredns/plugin/metrics" | ||||||
|  | 	"github.com/mholt/caddy" | ||||||
|  |  | ||||||
| 	"github.com/prometheus/client_golang/prometheus" | 	"github.com/prometheus/client_golang/prometheus" | ||||||
| ) | ) | ||||||
| @@ -15,28 +18,38 @@ var ( | |||||||
| 		Subsystem: "template", | 		Subsystem: "template", | ||||||
| 		Name:      "matches_total", | 		Name:      "matches_total", | ||||||
| 		Help:      "Counter of template regex matches.", | 		Help:      "Counter of template regex matches.", | ||||||
| 	}, []string{"regex"}) | 	}, []string{"zone", "class", "type"}) | ||||||
| 	TemplateFailureCount = prometheus.NewCounterVec(prometheus.CounterOpts{ | 	TemplateFailureCount = prometheus.NewCounterVec(prometheus.CounterOpts{ | ||||||
| 		Namespace: plugin.Namespace, | 		Namespace: plugin.Namespace, | ||||||
| 		Subsystem: "template", | 		Subsystem: "template", | ||||||
| 		Name:      "template_failures_total", | 		Name:      "template_failures_total", | ||||||
| 		Help:      "Counter of go template failures.", | 		Help:      "Counter of go template failures.", | ||||||
| 	}, []string{"regex", "section", "template"}) | 	}, []string{"zone", "class", "type", "section", "template"}) | ||||||
| 	TemplateRRFailureCount = prometheus.NewCounterVec(prometheus.CounterOpts{ | 	TemplateRRFailureCount = prometheus.NewCounterVec(prometheus.CounterOpts{ | ||||||
| 		Namespace: plugin.Namespace, | 		Namespace: plugin.Namespace, | ||||||
| 		Subsystem: "template", | 		Subsystem: "template", | ||||||
| 		Name:      "rr_failures_total", | 		Name:      "rr_failures_total", | ||||||
| 		Help:      "Counter of mis-templated RRs.", | 		Help:      "Counter of mis-templated RRs.", | ||||||
| 	}, []string{"regex", "section", "template"}) | 	}, []string{"zone", "class", "type", "section", "template"}) | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // OnStartupMetrics sets up the metrics on startup. | // OnStartupMetrics sets up the metrics on startup. | ||||||
| func OnStartupMetrics() error { | func setupMetrics(c *caddy.Controller) error { | ||||||
| 	metricsOnce.Do(func() { | 	c.OnStartup(func() error { | ||||||
| 		prometheus.MustRegister(TemplateMatchesCount) | 		metricsOnce.Do(func() { | ||||||
| 		prometheus.MustRegister(TemplateFailureCount) | 			m := dnsserver.GetConfig(c).Handler("prometheus") | ||||||
| 		prometheus.MustRegister(TemplateRRFailureCount) | 			if m == nil { | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 			if x, ok := m.(*metrics.Metrics); ok { | ||||||
|  | 				x.MustRegister(TemplateMatchesCount) | ||||||
|  | 				x.MustRegister(TemplateFailureCount) | ||||||
|  | 				x.MustRegister(TemplateRRFailureCount) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 		return nil | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -19,74 +19,86 @@ func init() { | |||||||
| } | } | ||||||
|  |  | ||||||
| func setupTemplate(c *caddy.Controller) error { | func setupTemplate(c *caddy.Controller) error { | ||||||
| 	templates, err := templateParse(c) | 	handler, err := templateParse(c) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return plugin.Error("template", err) | 		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 { | 	dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { | ||||||
| 		return Handler{Next: next, Templates: templates} | 		handler.Next = next | ||||||
|  | 		return handler | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func templateParse(c *caddy.Controller) (templates []template, err error) { | func templateParse(c *caddy.Controller) (handler Handler, err error) { | ||||||
| 	templates = make([]template, 0) | 	handler.Templates = make([]template, 0) | ||||||
|  |  | ||||||
| 	for c.Next() { | 	for c.Next() { | ||||||
| 		t := template{} |  | ||||||
| 		if !c.NextArg() { |  | ||||||
| 			return nil, c.ArgErr() |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
|  | 		if !c.NextArg() { | ||||||
|  | 			return handler, c.ArgErr() | ||||||
|  | 		} | ||||||
| 		class, ok := dns.StringToClass[c.Val()] | 		class, ok := dns.StringToClass[c.Val()] | ||||||
| 		if !ok { | 		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() { | 		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 { | 		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 |  | ||||||
|  | 		zones := c.RemainingArgs() | ||||||
|  | 		if len(zones) == 0 { | ||||||
|  | 			zones = make([]string, len(c.ServerBlockKeys)) | ||||||
|  | 			copy(zones, c.ServerBlockKeys) | ||||||
|  | 		} | ||||||
|  | 		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) | 		t.regex = make([]*regexp.Regexp, 0) | ||||||
| 		templatePrefix := "" | 		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) |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		if len(t.regex) == 0 { |  | ||||||
| 			t.regex = append(t.regex, regexp.MustCompile(".*")) |  | ||||||
| 			templatePrefix = ".* " |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		t.answer = make([]*gotmpl.Template, 0) | 		t.answer = make([]*gotmpl.Template, 0) | ||||||
|  |  | ||||||
| 		for c.NextBlock() { | 		for c.NextBlock() { | ||||||
| 			switch c.Val() { | 			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": | 			case "answer": | ||||||
| 				args := c.RemainingArgs() | 				args := c.RemainingArgs() | ||||||
| 				if len(args) == 0 { | 				if len(args) == 0 { | ||||||
| 					return nil, c.ArgErr() | 					return handler, c.ArgErr() | ||||||
| 				} | 				} | ||||||
| 				for _, answer := range args { | 				for _, answer := range args { | ||||||
| 					tmpl, err := gotmpl.New("answer").Parse(answer) | 					tmpl, err := gotmpl.New("answer").Parse(answer) | ||||||
| 					if err != nil { | 					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) | 					t.answer = append(t.answer, tmpl) | ||||||
| 				} | 				} | ||||||
| @@ -94,12 +106,12 @@ func templateParse(c *caddy.Controller) (templates []template, err error) { | |||||||
| 			case "additional": | 			case "additional": | ||||||
| 				args := c.RemainingArgs() | 				args := c.RemainingArgs() | ||||||
| 				if len(args) == 0 { | 				if len(args) == 0 { | ||||||
| 					return nil, c.ArgErr() | 					return handler, c.ArgErr() | ||||||
| 				} | 				} | ||||||
| 				for _, additional := range args { | 				for _, additional := range args { | ||||||
| 					tmpl, err := gotmpl.New("additional").Parse(additional) | 					tmpl, err := gotmpl.New("additional").Parse(additional) | ||||||
| 					if err != nil { | 					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) | 					t.additional = append(t.additional, tmpl) | ||||||
| 				} | 				} | ||||||
| @@ -107,37 +119,49 @@ func templateParse(c *caddy.Controller) (templates []template, err error) { | |||||||
| 			case "authority": | 			case "authority": | ||||||
| 				args := c.RemainingArgs() | 				args := c.RemainingArgs() | ||||||
| 				if len(args) == 0 { | 				if len(args) == 0 { | ||||||
| 					return nil, c.ArgErr() | 					return handler, c.ArgErr() | ||||||
| 				} | 				} | ||||||
| 				for _, authority := range args { | 				for _, authority := range args { | ||||||
| 					tmpl, err := gotmpl.New("authority").Parse(authority) | 					tmpl, err := gotmpl.New("authority").Parse(authority) | ||||||
| 					if err != nil { | 					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) | 					t.authority = append(t.authority, tmpl) | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
| 			case "rcode": | 			case "rcode": | ||||||
| 				if !c.NextArg() { | 				if !c.NextArg() { | ||||||
| 					return nil, c.ArgErr() | 					return handler, c.ArgErr() | ||||||
| 				} | 				} | ||||||
| 				rcode, ok := dns.StringToRcode[c.Val()] | 				rcode, ok := dns.StringToRcode[c.Val()] | ||||||
| 				if !ok { | 				if !ok { | ||||||
| 					return nil, c.Errf("unknown rcode %s", c.Val()) | 					return handler, c.Errf("unknown rcode %s", c.Val()) | ||||||
| 				} | 				} | ||||||
| 				t.rcode = rcode | 				t.rcode = rcode | ||||||
|  |  | ||||||
|  | 			case "fallthrough": | ||||||
|  | 				args := c.RemainingArgs() | ||||||
|  | 				if len(args) > 0 { | ||||||
|  | 					t.fthrough.SetZonesFromArgs(c.RemainingArgs()) | ||||||
|  | 				} else { | ||||||
|  | 					t.fthrough.SetZonesFromArgs(zones) | ||||||
|  | 				} | ||||||
|  |  | ||||||
| 			default: | 			default: | ||||||
| 				return nil, c.ArgErr() | 				return handler, c.ArgErr() | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if len(t.answer) == 0 && len(t.additional) == 0 && t.rcode == dns.RcodeSuccess { | 		if len(t.regex) == 0 { | ||||||
| 			return nil, c.Errf("no answer section for template %s %sfound", t.qtype, templatePrefix) | 			t.regex = append(t.regex, regexp.MustCompile(".*")) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		templates = append(templates, t) | 		if len(t.answer) == 0 && len(t.additional) == 0 && t.rcode == dns.RcodeSuccess { | ||||||
|  | 			return handler, c.Errf("no answer section for template found: %v", handler) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		handler.Templates = append(handler.Templates, t) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return templates, nil | 	return | ||||||
| } | } | ||||||
|   | |||||||
| @@ -93,30 +93,37 @@ func TestSetupParse(t *testing.T) { | |||||||
| 		}, | 		}, | ||||||
| 		// examples | 		// 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 }}." | 				answer "{{ .Name }} A {{ .Group.a }}.{{ .Group.b }}.{{ .Group.c }}.{{ .Grup.d }}." | ||||||
|  | 				fallthrough | ||||||
| 			}`, | 			}`, | ||||||
| 			false, | 			false, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			`template IN ANY "[.](example[.]com[.]dc1[.]example[.]com[.])$" { | 			`template IN ANY example.com { | ||||||
|  | 				match "[.](example[.]com[.]dc1[.]example[.]com[.])$" | ||||||
| 				rcode NXDOMAIN | 				rcode NXDOMAIN | ||||||
| 				answer "{{ index .Match 1 }} 60 IN SOA a.{{ index .Match 1 }} b.{{ index .Match 1 }} (1 60 60 60 60)" | 				answer "{{ index .Match 1 }} 60 IN SOA a.{{ index .Match 1 }} b.{{ index .Match 1 }} (1 60 60 60 60)" | ||||||
|  | 				fallthrough example.com | ||||||
| 			}`, | 			}`, | ||||||
| 			false, | 			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 }}" | 				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 }}" | 				answer "{{ .Name }} 60 IN MX 10 {{ .Name }}" | ||||||
| 				additional "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}" | 				additional "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}" | ||||||
| 			}`, | 			}`, | ||||||
| 			false, | 			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 }}" | 					answer "{{ .Name }} 60 IN MX 10 {{ .Name }}" | ||||||
| 					additional "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}" | 					additional "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}" | ||||||
| 					authority  "example. 60 IN NS ns0.example." | 					authority  "example. 60 IN NS ns0.example." | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ import ( | |||||||
| 	gotmpl "text/template" | 	gotmpl "text/template" | ||||||
|  |  | ||||||
| 	"github.com/coredns/coredns/plugin" | 	"github.com/coredns/coredns/plugin" | ||||||
|  | 	"github.com/coredns/coredns/plugin/pkg/fall" | ||||||
| 	"github.com/coredns/coredns/request" | 	"github.com/coredns/coredns/request" | ||||||
|  |  | ||||||
| 	"github.com/miekg/dns" | 	"github.com/miekg/dns" | ||||||
| @@ -15,21 +16,26 @@ import ( | |||||||
|  |  | ||||||
| // Handler is a plugin handler that takes a query and templates a response. | // Handler is a plugin handler that takes a query and templates a response. | ||||||
| type Handler struct { | type Handler struct { | ||||||
|  | 	Zones []string | ||||||
|  |  | ||||||
| 	Next      plugin.Handler | 	Next      plugin.Handler | ||||||
| 	Templates []template | 	Templates []template | ||||||
| } | } | ||||||
|  |  | ||||||
| type template struct { | type template struct { | ||||||
|  | 	zones      []string | ||||||
| 	rcode      int | 	rcode      int | ||||||
| 	class      uint16 |  | ||||||
| 	qtype      uint16 |  | ||||||
| 	regex      []*regexp.Regexp | 	regex      []*regexp.Regexp | ||||||
| 	answer     []*gotmpl.Template | 	answer     []*gotmpl.Template | ||||||
| 	additional []*gotmpl.Template | 	additional []*gotmpl.Template | ||||||
| 	authority  []*gotmpl.Template | 	authority  []*gotmpl.Template | ||||||
|  | 	qclass     uint16 | ||||||
|  | 	qtype      uint16 | ||||||
|  | 	fthrough   fall.F | ||||||
| } | } | ||||||
|  |  | ||||||
| type templateData struct { | type templateData struct { | ||||||
|  | 	Zone     string | ||||||
| 	Name     string | 	Name     string | ||||||
| 	Regex    string | 	Regex    string | ||||||
| 	Match    []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) { | func (h Handler) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { | ||||||
| 	state := request.Request{W: w, Req: r} | 	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 { | 	for _, template := range h.Templates { | ||||||
| 		data, match := template.match(state) | 		data, match, fthrough := template.match(state, zone) | ||||||
| 		if !match { | 		if !match { | ||||||
|  | 			if !fthrough { | ||||||
|  | 				return dns.RcodeNameError, nil | ||||||
|  | 			} | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		TemplateMatchesCount.WithLabelValues(data.Regex).Inc() | 		TemplateMatchesCount.WithLabelValues(data.Zone, data.Class, data.Type).Inc() | ||||||
|  |  | ||||||
| 		if template.rcode == dns.RcodeServerFailure { | 		if template.rcode == dns.RcodeServerFailure { | ||||||
| 			return template.rcode, nil | 			return template.rcode, nil | ||||||
| @@ -87,7 +101,8 @@ func (h Handler) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) | |||||||
| 		w.WriteMsg(msg) | 		w.WriteMsg(msg) | ||||||
| 		return template.rcode, nil | 		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. | // Name implements the plugin.Handler interface. | ||||||
| @@ -97,38 +112,53 @@ func executeRRTemplate(section string, template *gotmpl.Template, data templateD | |||||||
| 	buffer := &bytes.Buffer{} | 	buffer := &bytes.Buffer{} | ||||||
| 	err := template.Execute(buffer, data) | 	err := template.Execute(buffer, data) | ||||||
| 	if err != nil { | 	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 | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 	rr, err := dns.NewRR(buffer.String()) | 	rr, err := dns.NewRR(buffer.String()) | ||||||
| 	if err != nil { | 	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, err | ||||||
| 	} | 	} | ||||||
| 	return rr, nil | 	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] | 	q := state.Req.Question[0] | ||||||
| 	data := templateData{} | 	data := templateData{} | ||||||
|  |  | ||||||
| 	if t.class != dns.ClassANY && q.Qclass != dns.ClassANY && q.Qclass != t.class { | 	zone = plugin.Zones(t.zones).Matches(state.Name()) | ||||||
| 		return data, false | 	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 { | 	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 { | 	for _, regex := range t.regex { | ||||||
| 		if !regex.MatchString(state.Name()) { | 		if !regex.MatchString(state.Name()) { | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		data.Zone = zone | ||||||
| 		data.Regex = regex.String() | 		data.Regex = regex.String() | ||||||
| 		data.Name = state.Name() | 		data.Name = state.Name() | ||||||
| 		data.Question = &q | 		data.Question = &q | ||||||
| 		data.Message = state.Req | 		data.Message = state.Req | ||||||
| 		data.Class = dns.ClassToString[q.Qclass] | 		if q.Qclass != dns.ClassANY { | ||||||
| 		data.Type = dns.TypeToString[q.Qtype] | 			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()) | 		matches := regex.FindStringSubmatch(state.Name()) | ||||||
| 		data.Match = make([]string, len(matches)) | 		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()) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -7,80 +7,100 @@ import ( | |||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
| 	"github.com/coredns/coredns/plugin/test" | 	"github.com/coredns/coredns/plugin/test" | ||||||
|  | 	"github.com/mholt/caddy" | ||||||
|  |  | ||||||
| 	gotmpl "text/template" | 	gotmpl "text/template" | ||||||
|  |  | ||||||
| 	"github.com/coredns/coredns/plugin/pkg/dnstest" | 	"github.com/coredns/coredns/plugin/pkg/dnstest" | ||||||
|  | 	"github.com/coredns/coredns/plugin/pkg/fall" | ||||||
| 	"github.com/miekg/dns" | 	"github.com/miekg/dns" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func TestHandler(t *testing.T) { | func TestHandler(t *testing.T) { | ||||||
| 	rcodeFallthrough := 3841 // reserved for private use, used to indicate a fallthrough | 	rcodeFallthrough := 3841 // reserved for private use, used to indicate a fallthrough | ||||||
| 	exampleDomainATemplate := template{ | 	exampleDomainATemplate := template{ | ||||||
| 		class:  dns.ClassINET, | 		regex:    []*regexp.Regexp{regexp.MustCompile("(^|[.])ip-10-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]example[.]$")}, | ||||||
| 		qtype:  dns.TypeA, | 		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[.]$")}, | 		qclass:   dns.ClassANY, | ||||||
| 		answer: []*gotmpl.Template{gotmpl.Must(gotmpl.New("answer").Parse("{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}"))}, | 		qtype:    dns.TypeANY, | ||||||
|  | 		fthrough: fall.F{Zones: []string{"."}}, | ||||||
|  | 		zones:    []string{"."}, | ||||||
| 	} | 	} | ||||||
| 	exampleDomainANSTemplate := template{ | 	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[.]$")}, | 		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 }}"))}, | 		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"))}, | 		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."))}, | 		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{ | 	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[.]$")}, | 		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 }}"))}, | 		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 }}"))}, | 		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{ | 	invalidDomainTemplate := template{ | ||||||
| 		class:  dns.ClassANY, | 		regex:    []*regexp.Regexp{regexp.MustCompile("[.]invalid[.]$")}, | ||||||
| 		qtype:  dns.TypeANY, | 		rcode:    dns.RcodeNameError, | ||||||
| 		regex:  []*regexp.Regexp{regexp.MustCompile("[.]invalid[.]$")}, | 		answer:   []*gotmpl.Template{gotmpl.Must(gotmpl.New("answer").Parse("invalid. 60 {{ .Class }} SOA a.invalid. b.invalid. (1 60 60 60 60)"))}, | ||||||
| 		rcode:  dns.RcodeNameError, | 		qclass:   dns.ClassANY, | ||||||
| 		answer: []*gotmpl.Template{gotmpl.Must(gotmpl.New("answer").Parse("invalid. 60 {{ .Class }} SOA a.invalid. b.invalid. (1 60 60 60 60)"))}, | 		qtype:    dns.TypeANY, | ||||||
|  | 		fthrough: fall.F{Zones: []string{"."}}, | ||||||
|  | 		zones:    []string{"."}, | ||||||
| 	} | 	} | ||||||
| 	rcodeServfailTemplate := template{ | 	rcodeServfailTemplate := template{ | ||||||
| 		class: dns.ClassANY, | 		regex:    []*regexp.Regexp{regexp.MustCompile(".*")}, | ||||||
| 		qtype: dns.TypeANY, | 		rcode:    dns.RcodeServerFailure, | ||||||
| 		regex: []*regexp.Regexp{regexp.MustCompile(".*")}, | 		qclass:   dns.ClassANY, | ||||||
| 		rcode: dns.RcodeServerFailure, | 		qtype:    dns.TypeANY, | ||||||
|  | 		fthrough: fall.F{Zones: []string{"."}}, | ||||||
|  | 		zones:    []string{"."}, | ||||||
| 	} | 	} | ||||||
| 	brokenTemplate := template{ | 	brokenTemplate := template{ | ||||||
| 		class:  dns.ClassINET, | 		regex:    []*regexp.Regexp{regexp.MustCompile("[.]example[.]$")}, | ||||||
| 		qtype:  dns.TypeA, | 		answer:   []*gotmpl.Template{gotmpl.Must(gotmpl.New("answer").Parse("{{ .Name }} 60 IN TXT \"{{ index .Match 2 }}\""))}, | ||||||
| 		regex:  []*regexp.Regexp{regexp.MustCompile("[.]example[.]$")}, | 		qclass:   dns.ClassANY, | ||||||
| 		answer: []*gotmpl.Template{gotmpl.Must(gotmpl.New("answer").Parse("{{ .Name }} 60 IN TXT \"{{ index .Match 2 }}\""))}, | 		qtype:    dns.TypeANY, | ||||||
|  | 		fthrough: fall.F{Zones: []string{"."}}, | ||||||
|  | 		zones:    []string{"."}, | ||||||
| 	} | 	} | ||||||
| 	nonRRTemplate := template{ | 	nonRRTemplate := template{ | ||||||
| 		class:  dns.ClassINET, | 		regex:    []*regexp.Regexp{regexp.MustCompile("[.]example[.]$")}, | ||||||
| 		qtype:  dns.TypeA, | 		answer:   []*gotmpl.Template{gotmpl.Must(gotmpl.New("answer").Parse("{{ .Name }}"))}, | ||||||
| 		regex:  []*regexp.Regexp{regexp.MustCompile("[.]example[.]$")}, | 		qclass:   dns.ClassANY, | ||||||
| 		answer: []*gotmpl.Template{gotmpl.Must(gotmpl.New("answer").Parse("{{ .Name }}"))}, | 		qtype:    dns.TypeANY, | ||||||
|  | 		fthrough: fall.F{Zones: []string{"."}}, | ||||||
|  | 		zones:    []string{"."}, | ||||||
| 	} | 	} | ||||||
| 	nonRRAdditionalTemplate := template{ | 	nonRRAdditionalTemplate := template{ | ||||||
| 		class:      dns.ClassINET, |  | ||||||
| 		qtype:      dns.TypeA, |  | ||||||
| 		regex:      []*regexp.Regexp{regexp.MustCompile("[.]example[.]$")}, | 		regex:      []*regexp.Regexp{regexp.MustCompile("[.]example[.]$")}, | ||||||
| 		additional: []*gotmpl.Template{gotmpl.Must(gotmpl.New("answer").Parse("{{ .Name }}"))}, | 		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{ | 	nonRRAuthoritativeTemplate := template{ | ||||||
| 		class:     dns.ClassINET, |  | ||||||
| 		qtype:     dns.TypeA, |  | ||||||
| 		regex:     []*regexp.Regexp{regexp.MustCompile("[.]example[.]$")}, | 		regex:     []*regexp.Regexp{regexp.MustCompile("[.]example[.]$")}, | ||||||
| 		authority: []*gotmpl.Template{gotmpl.Must(gotmpl.New("authority").Parse("{{ .Name }}"))}, | 		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 { | 	tests := []struct { | ||||||
| 		tmpl           template | 		tmpl           template | ||||||
| 		qname          string | 		qname          string | ||||||
|  | 		name           string | ||||||
| 		qclass         uint16 | 		qclass         uint16 | ||||||
| 		qtype          uint16 | 		qtype          uint16 | ||||||
| 		name           string |  | ||||||
| 		expectedCode   int | 		expectedCode   int | ||||||
| 		expectedErr    string | 		expectedErr    string | ||||||
| 		verifyResponse func(*dns.Msg) error | 		verifyResponse func(*dns.Msg) error | ||||||
| @@ -88,8 +108,6 @@ func TestHandler(t *testing.T) { | |||||||
| 		{ | 		{ | ||||||
| 			name:         "RcodeServFail", | 			name:         "RcodeServFail", | ||||||
| 			tmpl:         rcodeServfailTemplate, | 			tmpl:         rcodeServfailTemplate, | ||||||
| 			qclass:       dns.ClassANY, |  | ||||||
| 			qtype:        dns.TypeANY, |  | ||||||
| 			qname:        "test.invalid.", | 			qname:        "test.invalid.", | ||||||
| 			expectedCode: dns.RcodeServerFailure, | 			expectedCode: dns.RcodeServerFailure, | ||||||
| 			verifyResponse: func(r *dns.Msg) error { | 			verifyResponse: func(r *dns.Msg) error { | ||||||
| @@ -221,22 +239,6 @@ func TestHandler(t *testing.T) { | |||||||
| 				return nil | 				return nil | ||||||
| 			}, | 			}, | ||||||
| 		}, | 		}, | ||||||
| 		{ |  | ||||||
| 			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", | 			name:         "ExampleInvalidNXDOMAIN", | ||||||
| 			tmpl:         invalidDomainTemplate, | 			tmpl:         invalidDomainTemplate, | ||||||
| @@ -261,6 +263,7 @@ func TestHandler(t *testing.T) { | |||||||
| 	for _, tr := range tests { | 	for _, tr := range tests { | ||||||
| 		handler := Handler{ | 		handler := Handler{ | ||||||
| 			Next:      test.NextHandler(rcodeFallthrough, nil), | 			Next:      test.NextHandler(rcodeFallthrough, nil), | ||||||
|  | 			Zones:     []string{"."}, | ||||||
| 			Templates: []template{tr.tmpl}, | 			Templates: []template{tr.tmpl}, | ||||||
| 		} | 		} | ||||||
| 		req := &dns.Msg{ | 		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]) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user