plugin/template (#1298)
* Add a template plugin The template plugin matches the incoming query by class, type and regex and templates a response with go templates. * Fix go style errors * Fix template README example * Fix corefile example in plugin/template * Clarify plugin/template/README.md Add more details and external links where needed. * Fix code issues in plugin/template * Add template metrics * Add section and template to template plugin metrics * Fix style / remove extra newline on go imports * Fix typo in plugin/template/README.md * Update README.md I've change the format a bit in a PR that I merged yesterday. * Add authority section to plugin/template * Fix naming of incoming query name in plugin/template/README.md * Fix doc syntax in plugin/template/README.md * Add authority section to plugin/template/README.md config overview * Add metric labels to plugin/template/README.md metrics section * Use request.Request to pass state to the template matcher
This commit is contained in:
parent
c59f5f6e86
commit
a322d90f6f
7 changed files with 965 additions and 0 deletions
|
@ -38,6 +38,7 @@ loadbalance:loadbalance
|
|||
dnssec:dnssec
|
||||
autopath:autopath
|
||||
reverse:reverse
|
||||
template:template
|
||||
hosts:hosts
|
||||
federation:federation
|
||||
kubernetes:kubernetes
|
||||
|
|
191
plugin/template/README.md
Normal file
191
plugin/template/README.md
Normal file
|
@ -0,0 +1,191 @@
|
|||
# template
|
||||
|
||||
*template* - allows for dynamic responses based on the incoming query.
|
||||
|
||||
## Description
|
||||
|
||||
The *template* plugin allows you to dynamically repond to queries by just writing a (Go) template.
|
||||
|
||||
## Syntax
|
||||
|
||||
~~~
|
||||
template CLASS TYPE [REGEX...] {
|
||||
[answer RR]
|
||||
[answer RR]
|
||||
[additional RR]
|
||||
[authority RR]
|
||||
[...]
|
||||
[rcode responsecode]
|
||||
}
|
||||
~~~
|
||||
|
||||
* **CLASS** the query class (usually IN or ANY)
|
||||
* **TYPE** the query type (A, PTR, ...)
|
||||
* **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.
|
||||
* `RR` A [RFC 1035](https://tools.ietf.org/html/rfc1035#section-5) style `<rr>` fragment build by a [Go template](https://golang.org/pkg/text/template/) that contains the answer.
|
||||
* `responsecode` A response code (`NXDOMAIN, SERVFAIL, ...`). The default is `SUCCESS`.
|
||||
|
||||
At least one answer section or rcode is needed.
|
||||
|
||||
[Also see](#also-see) contains an additional reading list.
|
||||
|
||||
## Templates
|
||||
|
||||
Each resource record is a full-featured [Go template](https://golang.org/pkg/text/template/) with the following predefined data
|
||||
* `.Name` the query name, as a string
|
||||
* `.Class` the query class (usually `IN`)
|
||||
* `.Type` the RR type requested (e.g. `PTR`)
|
||||
* `.Match` an array of all matches. `index .Match 0` refers to the whole match.
|
||||
* `.Group` a map of the named capture groups.
|
||||
* `.Message` the incoming DNS query message.
|
||||
* `.Question` the matched question section.
|
||||
|
||||
The output of the template must be a [RFC 1035](https://tools.ietf.org/html/rfc1035) style resource record line (commonly refered to as a "zone file").
|
||||
|
||||
**WARNING** there is a syntactical problem with Go templates and caddy config files. Expressions like `{{$var}}` will be interpreted as a reference to an environment variable by caddy/coredns while `{{ $var }}` will work. Try to avoid template variables. See [Bugs](#bugs).
|
||||
|
||||
## Metrics
|
||||
|
||||
If monitoring is enabled (via the *prometheus* directive) then the following metrics are exported:
|
||||
- `coredns_template_matches_total{regex}` the total number of matched requests by regex.
|
||||
- `coredns_template_template_failures_total{regex,section,template}` the number of times the Go templating failed. Regex, section and template label values can be used to map the error back to the config file.
|
||||
- `coredns_template_rr_failures_total{regex,section,template}` the number of times the templated resource record was invalid and could not be parsed. Regex, section and template label values can be used to map the error back to the config file.
|
||||
|
||||
Both failure cases indicate a problem with the template configuration.
|
||||
|
||||
## Examples
|
||||
|
||||
### Resolve .invalid as NXDOMAIN
|
||||
|
||||
The `.invalid` domain is a reserved TLD (see [RFC-2606 Reserved Top Level DNS Names](https://tools.ietf.org/html/rfc2606#section-2)) to indicate invalid domains.
|
||||
|
||||
~~~ corefile
|
||||
. {
|
||||
proxy . 8.8.8.8
|
||||
|
||||
template ANY ANY "[.]invalid[.]$" {
|
||||
rcode NXDOMAIN
|
||||
answer "invalid. 60 {{ .Class }} SOA a.invalid. b.invalid. (1 60 60 60 60)"
|
||||
}
|
||||
}
|
||||
~~~
|
||||
|
||||
1. A query to .invalid will result in NXDOMAIN (rcode)
|
||||
2. A dummy SOA record is send to hand out a TTL of 60s for caching
|
||||
3. Querying `.invalid` of `CH` will also cause a NXDOMAIN/SOA response
|
||||
|
||||
### Block invalid search domain completions
|
||||
|
||||
Imagine you run `example.com` with a datacenter `dc1.example.com`. The datacenter domain
|
||||
is part of the DNS search domain.
|
||||
However `something.example.com.dc1.example.com` would indicates a fully qualified
|
||||
domain name (`something.example.com`) that inadvertely has the default domain or search
|
||||
path (`dc1.example.com`) added.
|
||||
|
||||
~~~ corefile
|
||||
. {
|
||||
proxy . 8.8.8.8
|
||||
|
||||
template IN ANY "[.](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)"
|
||||
}
|
||||
}
|
||||
~~~
|
||||
|
||||
1. Using numbered matches works well if there are very few groups (1-4)
|
||||
|
||||
### Resolve A/PTR for .example
|
||||
|
||||
~~~ corefile
|
||||
. {
|
||||
proxy . 8.8.8.8
|
||||
|
||||
# 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[.]$ {
|
||||
answer "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}"
|
||||
}
|
||||
|
||||
# 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[.]$ {
|
||||
answer "{{ .Name }} 60 IN PTR ip-10-{{ .Group.b }}-{{ .Group.c }}-{{ .Group.d }}.example.com."
|
||||
}
|
||||
}
|
||||
~~~
|
||||
|
||||
An IPv4 address consists of 4 bytes, `a.b.c.d`. Named groups make it less error prone to reverse the
|
||||
ip in the PTR case. Try to use named groups to explain what your regex and template are doing.
|
||||
|
||||
Note that the A record is actually a wildcard, any subdomain of the ip will resolve to the ip.
|
||||
|
||||
Having templates to map certain PTR/A pairs is a common pattern.
|
||||
|
||||
### Resolve multiple ip patterns
|
||||
|
||||
~~~ corefile
|
||||
. {
|
||||
proxy . 8.8.8.8
|
||||
|
||||
template IN A "^ip-(?P<a>10)-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]dc[.]example[.]$" "^(?P<a>[0-9]*)[.](?P<b>[0-9]*)[.](?P<c>[0-9]*)[.](?P<d>[0-9]*)[.]ext[.]example[.]$" {
|
||||
answer "{{ .Name }} 60 IN A {{ .Group.a}}.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}"
|
||||
}
|
||||
}
|
||||
~~~
|
||||
|
||||
Named capture groups can be used to template one response for multiple patterns.
|
||||
|
||||
### Resolve A and MX records for ip templates in .example
|
||||
|
||||
~~~ corefile
|
||||
. {
|
||||
proxy . 8.8.8.8
|
||||
|
||||
template IN A ^ip-10-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]example[.]$ {
|
||||
answer "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}"
|
||||
}
|
||||
template IN MX ^ip-10-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]example[.]$ {
|
||||
answer "{{ .Name }} 60 IN MX 10 {{ .Name }}"
|
||||
additional "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}"
|
||||
}
|
||||
}
|
||||
~~~
|
||||
|
||||
### Adding authoritative nameservers to the response
|
||||
|
||||
~~~ corefile
|
||||
. {
|
||||
proxy . 8.8.8.8
|
||||
|
||||
template IN A ^ip-10-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]example[.]$ {
|
||||
answer "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}"
|
||||
authority "example. 60 IN NS ns0.example."
|
||||
authority "example. 60 IN NS ns1.example."
|
||||
additional "ns0.example. 60 IN A 203.0.113.8"
|
||||
additional "ns1.example. 60 IN A 198.51.100.8"
|
||||
}
|
||||
template IN MX ^ip-10-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]example[.]$ {
|
||||
answer "{{ .Name }} 60 IN MX 10 {{ .Name }}"
|
||||
additional "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}"
|
||||
authority "example. 60 IN NS ns0.example."
|
||||
authority "example. 60 IN NS ns1.example."
|
||||
additional "ns0.example. 60 IN A 203.0.113.8"
|
||||
additional "ns1.example. 60 IN A 198.51.100.8"
|
||||
}
|
||||
}
|
||||
~~~
|
||||
|
||||
# Also see
|
||||
|
||||
- [Go regexp](https://golang.org/pkg/regexp/) for details about the regex implementation
|
||||
- [RE2 syntax reference](https://github.com/google/re2/wiki/Syntax) for details about the regex syntax
|
||||
- [RFC-1034](https://tools.ietf.org/html/rfc1034#section-3.6.1) and [RFC 1035](https://tools.ietf.org/html/rfc1035#section-5) for the resource record format
|
||||
- [Go template](https://golang.org/pkg/text/template/) for the template language reference
|
||||
|
||||
# Bugs
|
||||
|
||||
CoreDNS supports [caddyfile environment variables](https://caddyserver.com/docs/caddyfile#env)
|
||||
with notion of `{$ENV_VAR}`. This parser feature will break [Go template variables](https://golang.org/pkg/text/template/#hdr-Variables) notations like`{{$variable}}`.
|
||||
The equivalent notation `{{ $variable }}` will work.
|
||||
Try to avoid Go template variables in the context of this plugin.
|
43
plugin/template/metrics.go
Normal file
43
plugin/template/metrics.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
package template
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/coredns/coredns/plugin"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
// Metrics for template.
|
||||
var (
|
||||
TemplateMatchesCount = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: "template",
|
||||
Name: "matches_total",
|
||||
Help: "Counter of template regex matches.",
|
||||
}, []string{"regex"})
|
||||
TemplateFailureCount = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: "template",
|
||||
Name: "template_failures_total",
|
||||
Help: "Counter of go template failures.",
|
||||
}, []string{"regex", "section", "template"})
|
||||
TemplateRRFailureCount = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: "template",
|
||||
Name: "rr_failures_total",
|
||||
Help: "Counter of mis-templated RRs.",
|
||||
}, []string{"regex", "section", "template"})
|
||||
)
|
||||
|
||||
// OnStartupMetrics sets up the metrics on startup.
|
||||
func OnStartupMetrics() error {
|
||||
metricsOnce.Do(func() {
|
||||
prometheus.MustRegister(TemplateMatchesCount)
|
||||
prometheus.MustRegister(TemplateFailureCount)
|
||||
prometheus.MustRegister(TemplateRRFailureCount)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
var metricsOnce sync.Once
|
143
plugin/template/setup.go
Normal file
143
plugin/template/setup.go
Normal file
|
@ -0,0 +1,143 @@
|
|||
package template
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
gotmpl "text/template"
|
||||
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
"github.com/coredns/coredns/plugin"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterPlugin("template", caddy.Plugin{
|
||||
ServerType: "dns",
|
||||
Action: setupTemplate,
|
||||
})
|
||||
}
|
||||
|
||||
func setupTemplate(c *caddy.Controller) error {
|
||||
templates, err := templateParse(c)
|
||||
if err != nil {
|
||||
return plugin.Error("template", err)
|
||||
}
|
||||
|
||||
c.OnStartup(OnStartupMetrics)
|
||||
|
||||
dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
|
||||
return Handler{Next: next, Templates: templates}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func templateParse(c *caddy.Controller) (templates []template, err error) {
|
||||
templates = make([]template, 0)
|
||||
|
||||
for c.Next() {
|
||||
t := template{}
|
||||
if !c.NextArg() {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
|
||||
class, ok := dns.StringToClass[c.Val()]
|
||||
if !ok {
|
||||
return nil, c.Errf("invalid query class %s", c.Val())
|
||||
}
|
||||
t.class = class
|
||||
|
||||
if !c.NextArg() {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
queryType, ok := dns.StringToType[c.Val()]
|
||||
if !ok {
|
||||
return nil, c.Errf("invalid RR type %s", c.Val())
|
||||
}
|
||||
t.qtype = queryType
|
||||
|
||||
t.regex = make([]*regexp.Regexp, 0)
|
||||
templatePrefix := ""
|
||||
|
||||
for _, regex := range c.RemainingArgs() {
|
||||
r, err := regexp.Compile(regex)
|
||||
if err != nil {
|
||||
return nil, c.Errf("could not parse regex: %s, %v", regex, err)
|
||||
}
|
||||
templatePrefix = templatePrefix + regex + " "
|
||||
t.regex = append(t.regex, r)
|
||||
}
|
||||
|
||||
if len(t.regex) == 0 {
|
||||
t.regex = append(t.regex, regexp.MustCompile(".*"))
|
||||
templatePrefix = ".* "
|
||||
}
|
||||
|
||||
t.answer = make([]*gotmpl.Template, 0)
|
||||
|
||||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
case "answer":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) == 0 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
for _, answer := range args {
|
||||
tmpl, err := gotmpl.New("answer").Parse(answer)
|
||||
if err != nil {
|
||||
return nil, c.Errf("could not compile template: %s, %v", c.Val(), err)
|
||||
}
|
||||
t.answer = append(t.answer, tmpl)
|
||||
}
|
||||
|
||||
case "additional":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) == 0 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
for _, additional := range args {
|
||||
tmpl, err := gotmpl.New("additional").Parse(additional)
|
||||
if err != nil {
|
||||
return nil, c.Errf("could not compile template: %s, %v\n", c.Val(), err)
|
||||
}
|
||||
t.additional = append(t.additional, tmpl)
|
||||
}
|
||||
|
||||
case "authority":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) == 0 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
for _, authority := range args {
|
||||
tmpl, err := gotmpl.New("authority").Parse(authority)
|
||||
if err != nil {
|
||||
return nil, c.Errf("could not compile template: %s, %v\n", c.Val(), err)
|
||||
}
|
||||
t.authority = append(t.authority, tmpl)
|
||||
}
|
||||
|
||||
case "rcode":
|
||||
if !c.NextArg() {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
rcode, ok := dns.StringToRcode[c.Val()]
|
||||
if !ok {
|
||||
return nil, c.Errf("unknown rcode %s", c.Val())
|
||||
}
|
||||
t.rcode = rcode
|
||||
|
||||
default:
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
}
|
||||
|
||||
if len(t.answer) == 0 && len(t.additional) == 0 && t.rcode == dns.RcodeSuccess {
|
||||
return nil, c.Errf("no answer section for template %s %sfound", t.qtype, templatePrefix)
|
||||
}
|
||||
|
||||
templates = append(templates, t)
|
||||
}
|
||||
|
||||
return templates, nil
|
||||
}
|
141
plugin/template/setup_test.go
Normal file
141
plugin/template/setup_test.go
Normal file
|
@ -0,0 +1,141 @@
|
|||
package template
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
c := caddy.NewTestController("dns", `template ANY ANY {
|
||||
rcode
|
||||
}`)
|
||||
err := setupTemplate(c)
|
||||
if err == nil {
|
||||
t.Errorf("Expected setupTemplate to fail on broken template, got no error")
|
||||
}
|
||||
c = caddy.NewTestController("dns", `template ANY ANY {
|
||||
rcode NXDOMAIN
|
||||
}`)
|
||||
err = setupTemplate(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetupParse(t *testing.T) {
|
||||
|
||||
serverBlockKeys := []string{"domain.com.:8053", "dynamic.domain.com.:8053"}
|
||||
|
||||
tests := []struct {
|
||||
inputFileRules string
|
||||
shouldErr bool
|
||||
}{
|
||||
// parse errors
|
||||
{`template`, true},
|
||||
{`template X`, true},
|
||||
{`template ANY`, true},
|
||||
{`template ANY X`, true},
|
||||
{`template ANY ANY (?P<x>`, true},
|
||||
{
|
||||
`template ANY ANY {
|
||||
|
||||
}`,
|
||||
true,
|
||||
},
|
||||
{
|
||||
`template ANY ANY .* {
|
||||
notavailable
|
||||
}`,
|
||||
true,
|
||||
},
|
||||
{
|
||||
`template ANY ANY {
|
||||
answer
|
||||
}`,
|
||||
true,
|
||||
},
|
||||
{
|
||||
`template ANY ANY {
|
||||
additional
|
||||
}`,
|
||||
true,
|
||||
},
|
||||
{
|
||||
`template ANY ANY {
|
||||
rcode
|
||||
}`,
|
||||
true,
|
||||
},
|
||||
{
|
||||
`template ANY ANY {
|
||||
rcode UNDEFINED
|
||||
}`,
|
||||
true,
|
||||
},
|
||||
{
|
||||
`template ANY ANY {
|
||||
answer "{{"
|
||||
}`,
|
||||
true,
|
||||
},
|
||||
{
|
||||
`template ANY ANY {
|
||||
additional "{{"
|
||||
}`,
|
||||
true,
|
||||
},
|
||||
{
|
||||
`template ANY ANY {
|
||||
authority "{{"
|
||||
}`,
|
||||
true,
|
||||
},
|
||||
// examples
|
||||
{
|
||||
`template ANY A 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 }}."
|
||||
}`,
|
||||
false,
|
||||
},
|
||||
{
|
||||
`template IN ANY "[.](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)"
|
||||
}`,
|
||||
false,
|
||||
},
|
||||
{
|
||||
`template IN A ^ip-10-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]example[.]$ {
|
||||
answer "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}"
|
||||
}
|
||||
template IN MX ^ip-10-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]example[.]$ {
|
||||
answer "{{ .Name }} 60 IN MX 10 {{ .Name }}"
|
||||
additional "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}"
|
||||
}`,
|
||||
false,
|
||||
},
|
||||
{
|
||||
`template IN MX ^ip-10-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]example[.]$ {
|
||||
answer "{{ .Name }} 60 IN MX 10 {{ .Name }}"
|
||||
additional "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}"
|
||||
authority "example. 60 IN NS ns0.example."
|
||||
authority "example. 60 IN NS ns1.example."
|
||||
additional "ns0.example. 60 IN A 203.0.113.8"
|
||||
additional "ns1.example. 60 IN A 198.51.100.8"
|
||||
}`,
|
||||
false,
|
||||
},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := caddy.NewTestController("dns", test.inputFileRules)
|
||||
c.ServerBlockKeys = serverBlockKeys
|
||||
templates, err := templateParse(c)
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Fatalf("Test %d expected errors, but got no error\n---\n%s\n---\n%v", i, test.inputFileRules, templates)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Fatalf("Test %d expected no errors, but got '%v'", i, err)
|
||||
}
|
||||
}
|
||||
}
|
152
plugin/template/template.go
Normal file
152
plugin/template/template.go
Normal file
|
@ -0,0 +1,152 @@
|
|||
package template
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"regexp"
|
||||
"strconv"
|
||||
gotmpl "text/template"
|
||||
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/coredns/coredns/request"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// Handler is a plugin handler that takes a query and templates a response.
|
||||
type Handler struct {
|
||||
Next plugin.Handler
|
||||
Templates []template
|
||||
}
|
||||
|
||||
type template struct {
|
||||
rcode int
|
||||
class uint16
|
||||
qtype uint16
|
||||
regex []*regexp.Regexp
|
||||
answer []*gotmpl.Template
|
||||
additional []*gotmpl.Template
|
||||
authority []*gotmpl.Template
|
||||
}
|
||||
|
||||
type templateData struct {
|
||||
Name string
|
||||
Regex string
|
||||
Match []string
|
||||
Group map[string]string
|
||||
Class string
|
||||
Type string
|
||||
Message *dns.Msg
|
||||
Question *dns.Question
|
||||
}
|
||||
|
||||
// ServeDNS implements the plugin.Handler interface.
|
||||
func (h Handler) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
state := request.Request{W: w, Req: r}
|
||||
|
||||
for _, template := range h.Templates {
|
||||
data, match := template.match(state)
|
||||
if !match {
|
||||
continue
|
||||
}
|
||||
|
||||
TemplateMatchesCount.WithLabelValues(data.Regex).Inc()
|
||||
|
||||
if template.rcode == dns.RcodeServerFailure {
|
||||
return template.rcode, nil
|
||||
}
|
||||
|
||||
msg := new(dns.Msg)
|
||||
msg.SetReply(r)
|
||||
msg.Authoritative, msg.RecursionAvailable, msg.Compress = true, true, true
|
||||
msg.Rcode = template.rcode
|
||||
|
||||
for _, answer := range template.answer {
|
||||
rr, err := executeRRTemplate("answer", answer, data)
|
||||
if err != nil {
|
||||
return dns.RcodeServerFailure, err
|
||||
}
|
||||
msg.Answer = append(msg.Answer, rr)
|
||||
}
|
||||
for _, additional := range template.additional {
|
||||
rr, err := executeRRTemplate("additional", additional, data)
|
||||
if err != nil {
|
||||
return dns.RcodeServerFailure, err
|
||||
}
|
||||
msg.Extra = append(msg.Extra, rr)
|
||||
}
|
||||
for _, authority := range template.authority {
|
||||
rr, err := executeRRTemplate("authority", authority, data)
|
||||
if err != nil {
|
||||
return dns.RcodeServerFailure, err
|
||||
}
|
||||
msg.Ns = append(msg.Ns, rr)
|
||||
}
|
||||
|
||||
state.SizeAndDo(msg)
|
||||
w.WriteMsg(msg)
|
||||
return template.rcode, nil
|
||||
}
|
||||
return plugin.NextOrFailure(h.Name(), h.Next, ctx, w, r)
|
||||
}
|
||||
|
||||
// Name implements the plugin.Handler interface.
|
||||
func (h Handler) Name() string {
|
||||
return "template"
|
||||
}
|
||||
|
||||
func executeRRTemplate(section string, template *gotmpl.Template, data templateData) (dns.RR, error) {
|
||||
buffer := &bytes.Buffer{}
|
||||
err := template.Execute(buffer, data)
|
||||
if err != nil {
|
||||
TemplateFailureCount.WithLabelValues(data.Regex, section, template.Tree.Root.String()).Inc()
|
||||
return nil, err
|
||||
}
|
||||
rr, err := dns.NewRR(buffer.String())
|
||||
if err != nil {
|
||||
TemplateRRFailureCount.WithLabelValues(data.Regex, section, template.Tree.Root.String()).Inc()
|
||||
return rr, err
|
||||
}
|
||||
return rr, nil
|
||||
}
|
||||
|
||||
func (t template) match(state request.Request) (templateData, bool) {
|
||||
q := state.Req.Question[0]
|
||||
data := templateData{}
|
||||
|
||||
if t.class != dns.ClassANY && q.Qclass != dns.ClassANY && q.Qclass != t.class {
|
||||
return data, false
|
||||
}
|
||||
if t.qtype != dns.TypeANY && q.Qtype != dns.TypeANY && q.Qtype != t.qtype {
|
||||
return data, false
|
||||
}
|
||||
for _, regex := range t.regex {
|
||||
if !regex.MatchString(state.Name()) {
|
||||
continue
|
||||
}
|
||||
|
||||
data.Regex = regex.String()
|
||||
data.Name = state.Name()
|
||||
data.Question = &q
|
||||
data.Message = state.Req
|
||||
data.Class = dns.ClassToString[q.Qclass]
|
||||
data.Type = dns.TypeToString[q.Qtype]
|
||||
|
||||
matches := regex.FindStringSubmatch(state.Name())
|
||||
data.Match = make([]string, len(matches))
|
||||
data.Group = make(map[string]string)
|
||||
groupNames := regex.SubexpNames()
|
||||
for i, m := range matches {
|
||||
data.Match[i] = m
|
||||
data.Group[strconv.Itoa(i)] = m
|
||||
}
|
||||
for i, m := range matches {
|
||||
if len(groupNames[i]) > 0 {
|
||||
data.Group[groupNames[i]] = m
|
||||
}
|
||||
}
|
||||
|
||||
return data, true
|
||||
}
|
||||
return data, false
|
||||
}
|
294
plugin/template/template_test.go
Normal file
294
plugin/template/template_test.go
Normal file
|
@ -0,0 +1,294 @@
|
|||
package template
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/coredns/coredns/plugin/test"
|
||||
|
||||
gotmpl "text/template"
|
||||
|
||||
"github.com/coredns/coredns/plugin/pkg/dnstest"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func TestHandler(t *testing.T) {
|
||||
rcodeFallthrough := 3841 // reserved for private use, used to indicate a fallthrough
|
||||
exampleDomainATemplate := template{
|
||||
class: dns.ClassINET,
|
||||
qtype: dns.TypeA,
|
||||
regex: []*regexp.Regexp{regexp.MustCompile("(^|[.])ip-10-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]example[.]$")},
|
||||
answer: []*gotmpl.Template{gotmpl.Must(gotmpl.New("answer").Parse("{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}"))},
|
||||
}
|
||||
exampleDomainANSTemplate := template{
|
||||
class: dns.ClassINET,
|
||||
qtype: dns.TypeA,
|
||||
regex: []*regexp.Regexp{regexp.MustCompile("(^|[.])ip-10-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]example[.]$")},
|
||||
answer: []*gotmpl.Template{gotmpl.Must(gotmpl.New("answer").Parse("{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}"))},
|
||||
additional: []*gotmpl.Template{gotmpl.Must(gotmpl.New("additional").Parse("ns0.example. IN A 203.0.113.8"))},
|
||||
authority: []*gotmpl.Template{gotmpl.Must(gotmpl.New("authority").Parse("example. IN NS ns0.example.com."))},
|
||||
}
|
||||
exampleDomainMXTemplate := template{
|
||||
class: dns.ClassINET,
|
||||
qtype: dns.TypeMX,
|
||||
regex: []*regexp.Regexp{regexp.MustCompile("(^|[.])ip-10-(?P<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]example[.]$")},
|
||||
answer: []*gotmpl.Template{gotmpl.Must(gotmpl.New("answer").Parse("{{ .Name }} 60 MX 10 {{ .Name }}"))},
|
||||
additional: []*gotmpl.Template{gotmpl.Must(gotmpl.New("additional").Parse("{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}"))},
|
||||
}
|
||||
invalidDomainTemplate := template{
|
||||
class: dns.ClassANY,
|
||||
qtype: dns.TypeANY,
|
||||
regex: []*regexp.Regexp{regexp.MustCompile("[.]invalid[.]$")},
|
||||
rcode: dns.RcodeNameError,
|
||||
answer: []*gotmpl.Template{gotmpl.Must(gotmpl.New("answer").Parse("invalid. 60 {{ .Class }} SOA a.invalid. b.invalid. (1 60 60 60 60)"))},
|
||||
}
|
||||
rcodeServfailTemplate := template{
|
||||
class: dns.ClassANY,
|
||||
qtype: dns.TypeANY,
|
||||
regex: []*regexp.Regexp{regexp.MustCompile(".*")},
|
||||
rcode: dns.RcodeServerFailure,
|
||||
}
|
||||
brokenTemplate := template{
|
||||
class: dns.ClassINET,
|
||||
qtype: dns.TypeA,
|
||||
regex: []*regexp.Regexp{regexp.MustCompile("[.]example[.]$")},
|
||||
answer: []*gotmpl.Template{gotmpl.Must(gotmpl.New("answer").Parse("{{ .Name }} 60 IN TXT \"{{ index .Match 2 }}\""))},
|
||||
}
|
||||
nonRRTemplate := template{
|
||||
class: dns.ClassINET,
|
||||
qtype: dns.TypeA,
|
||||
regex: []*regexp.Regexp{regexp.MustCompile("[.]example[.]$")},
|
||||
answer: []*gotmpl.Template{gotmpl.Must(gotmpl.New("answer").Parse("{{ .Name }}"))},
|
||||
}
|
||||
nonRRAdditionalTemplate := template{
|
||||
class: dns.ClassINET,
|
||||
qtype: dns.TypeA,
|
||||
regex: []*regexp.Regexp{regexp.MustCompile("[.]example[.]$")},
|
||||
additional: []*gotmpl.Template{gotmpl.Must(gotmpl.New("answer").Parse("{{ .Name }}"))},
|
||||
}
|
||||
nonRRAuthoritativeTemplate := template{
|
||||
class: dns.ClassINET,
|
||||
qtype: dns.TypeA,
|
||||
regex: []*regexp.Regexp{regexp.MustCompile("[.]example[.]$")},
|
||||
authority: []*gotmpl.Template{gotmpl.Must(gotmpl.New("authority").Parse("{{ .Name }}"))},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
tmpl template
|
||||
qname string
|
||||
qclass uint16
|
||||
qtype uint16
|
||||
name string
|
||||
expectedCode int
|
||||
expectedErr string
|
||||
verifyResponse func(*dns.Msg) error
|
||||
}{
|
||||
{
|
||||
name: "RcodeServFail",
|
||||
tmpl: rcodeServfailTemplate,
|
||||
qclass: dns.ClassANY,
|
||||
qtype: dns.TypeANY,
|
||||
qname: "test.invalid.",
|
||||
expectedCode: dns.RcodeServerFailure,
|
||||
verifyResponse: func(r *dns.Msg) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ExampleDomainNameMismatch",
|
||||
tmpl: exampleDomainATemplate,
|
||||
qclass: dns.ClassINET,
|
||||
qtype: dns.TypeA,
|
||||
qname: "test.invalid.",
|
||||
expectedCode: rcodeFallthrough,
|
||||
},
|
||||
{
|
||||
name: "BrokenTemplate",
|
||||
tmpl: brokenTemplate,
|
||||
qclass: dns.ClassINET,
|
||||
qtype: dns.TypeANY,
|
||||
qname: "test.example.",
|
||||
expectedCode: dns.RcodeServerFailure,
|
||||
expectedErr: `template: answer:1:26: executing "answer" at <index .Match 2>: error calling index: index out of range: 2`,
|
||||
verifyResponse: func(r *dns.Msg) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NonRRTemplate",
|
||||
tmpl: nonRRTemplate,
|
||||
qclass: dns.ClassINET,
|
||||
qtype: dns.TypeANY,
|
||||
qname: "test.example.",
|
||||
expectedCode: dns.RcodeServerFailure,
|
||||
expectedErr: `dns: not a TTL: "test.example." at line: 1:13`,
|
||||
verifyResponse: func(r *dns.Msg) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NonRRAdditionalTemplate",
|
||||
tmpl: nonRRAdditionalTemplate,
|
||||
qclass: dns.ClassINET,
|
||||
qtype: dns.TypeANY,
|
||||
qname: "test.example.",
|
||||
expectedCode: dns.RcodeServerFailure,
|
||||
expectedErr: `dns: not a TTL: "test.example." at line: 1:13`,
|
||||
verifyResponse: func(r *dns.Msg) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NonRRAuthorityTemplate",
|
||||
tmpl: nonRRAuthoritativeTemplate,
|
||||
qclass: dns.ClassINET,
|
||||
qtype: dns.TypeANY,
|
||||
qname: "test.example.",
|
||||
expectedCode: dns.RcodeServerFailure,
|
||||
expectedErr: `dns: not a TTL: "test.example." at line: 1:13`,
|
||||
verifyResponse: func(r *dns.Msg) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ExampleDomainMatch",
|
||||
tmpl: exampleDomainATemplate,
|
||||
qclass: dns.ClassINET,
|
||||
qtype: dns.TypeA,
|
||||
qname: "ip-10-95-12-8.example.",
|
||||
verifyResponse: func(r *dns.Msg) error {
|
||||
if len(r.Answer) != 1 {
|
||||
return fmt.Errorf("expected 1 answer, got %v", len(r.Answer))
|
||||
}
|
||||
if r.Answer[0].Header().Rrtype != dns.TypeA {
|
||||
return fmt.Errorf("expected an A record anwser, got %v", dns.TypeToString[r.Answer[0].Header().Rrtype])
|
||||
}
|
||||
if r.Answer[0].(*dns.A).A.String() != "10.95.12.8" {
|
||||
return fmt.Errorf("expected an A record for 10.95.12.8, got %v", r.Answer[0].String())
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ExampleDomainMXMatch",
|
||||
tmpl: exampleDomainMXTemplate,
|
||||
qclass: dns.ClassINET,
|
||||
qtype: dns.TypeMX,
|
||||
qname: "ip-10-95-12-8.example.",
|
||||
verifyResponse: func(r *dns.Msg) error {
|
||||
if len(r.Answer) != 1 {
|
||||
return fmt.Errorf("expected 1 answer, got %v", len(r.Answer))
|
||||
}
|
||||
if r.Answer[0].Header().Rrtype != dns.TypeMX {
|
||||
return fmt.Errorf("expected an A record anwser, got %v", dns.TypeToString[r.Answer[0].Header().Rrtype])
|
||||
}
|
||||
if len(r.Extra) != 1 {
|
||||
return fmt.Errorf("expected 1 extra record, got %v", len(r.Extra))
|
||||
}
|
||||
if r.Extra[0].Header().Rrtype != dns.TypeA {
|
||||
return fmt.Errorf("expected an additional A record, got %v", dns.TypeToString[r.Extra[0].Header().Rrtype])
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ExampleDomainANSMatch",
|
||||
tmpl: exampleDomainANSTemplate,
|
||||
qclass: dns.ClassINET,
|
||||
qtype: dns.TypeA,
|
||||
qname: "ip-10-95-12-8.example.",
|
||||
verifyResponse: func(r *dns.Msg) error {
|
||||
if len(r.Answer) != 1 {
|
||||
return fmt.Errorf("expected 1 answer, got %v", len(r.Answer))
|
||||
}
|
||||
if r.Answer[0].Header().Rrtype != dns.TypeA {
|
||||
return fmt.Errorf("expected an A record anwser, got %v", dns.TypeToString[r.Answer[0].Header().Rrtype])
|
||||
}
|
||||
if len(r.Extra) != 1 {
|
||||
return fmt.Errorf("expected 1 extra record, got %v", len(r.Extra))
|
||||
}
|
||||
if r.Extra[0].Header().Rrtype != dns.TypeA {
|
||||
return fmt.Errorf("expected an additional A record, got %v", dns.TypeToString[r.Extra[0].Header().Rrtype])
|
||||
}
|
||||
if len(r.Ns) != 1 {
|
||||
return fmt.Errorf("expected 1 authoritative record, got %v", len(r.Extra))
|
||||
}
|
||||
if r.Ns[0].Header().Rrtype != dns.TypeNS {
|
||||
return fmt.Errorf("expected an authoritative NS record, got %v", dns.TypeToString[r.Extra[0].Header().Rrtype])
|
||||
}
|
||||
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",
|
||||
tmpl: invalidDomainTemplate,
|
||||
qclass: dns.ClassINET,
|
||||
qtype: dns.TypeMX,
|
||||
qname: "test.invalid.",
|
||||
expectedCode: dns.RcodeNameError,
|
||||
verifyResponse: func(r *dns.Msg) error {
|
||||
if len(r.Answer) != 1 {
|
||||
return fmt.Errorf("expected 1 answer, got %v", len(r.Answer))
|
||||
}
|
||||
if r.Answer[0].Header().Rrtype != dns.TypeSOA {
|
||||
return fmt.Errorf("expected an SOA record anwser, got %v", dns.TypeToString[r.Answer[0].Header().Rrtype])
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.TODO()
|
||||
|
||||
for _, tr := range tests {
|
||||
handler := Handler{
|
||||
Next: test.NextHandler(rcodeFallthrough, nil),
|
||||
Templates: []template{tr.tmpl},
|
||||
}
|
||||
req := &dns.Msg{
|
||||
Question: []dns.Question{{
|
||||
Name: tr.qname,
|
||||
Qclass: tr.qclass,
|
||||
Qtype: tr.qtype,
|
||||
}},
|
||||
}
|
||||
rec := dnstest.NewRecorder(&test.ResponseWriter{})
|
||||
code, err := handler.ServeDNS(ctx, rec, req)
|
||||
if err == nil && tr.expectedErr != "" {
|
||||
t.Errorf("Test %v expected error: %v, got nothing", tr.name, tr.expectedErr)
|
||||
}
|
||||
if err != nil && tr.expectedErr == "" {
|
||||
t.Errorf("Test %v expected no error got: %v", tr.name, err)
|
||||
}
|
||||
if err != nil && tr.expectedErr != "" && err.Error() != tr.expectedErr {
|
||||
t.Errorf("Test %v expected error: %v, got: %v", tr.name, tr.expectedErr, err)
|
||||
}
|
||||
if code != tr.expectedCode {
|
||||
t.Errorf("Test %v expected response code %v, got %v", tr.name, tr.expectedCode, code)
|
||||
}
|
||||
if err == nil && code != rcodeFallthrough {
|
||||
// only verify if we got no error and expected no error
|
||||
if err := tr.verifyResponse(rec.Msg); err != nil {
|
||||
t.Errorf("Test %v could not verify the response: %v", tr.name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue