mirror of
https://github.com/coredns/coredns.git
synced 2025-10-28 08:44:17 -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
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