diff --git a/plugin/template/README.md b/plugin/template/README.md index 55889f7bc..5e14ae24a 100644 --- a/plugin/template/README.md +++ b/plugin/template/README.md @@ -54,6 +54,10 @@ Each resource record is a full-featured [Go template](https://golang.org/pkg/tex * `.Meta` a function that takes a metadata name and returns the value, if the metadata plugin is enabled. For example, `.Meta "kubernetes/client-namespace"` +and the following predefined [template functions](https://golang.org/pkg/text/template#hdr-Functions) + +* `parseInt` interprets a string in the given base and bit size. Equivalent to [strconv.ParseUint](https://golang.org/pkg/strconv#ParseUint). + The output of the template must be a [RFC 1035](https://tools.ietf.org/html/rfc1035) style resource record (commonly referred to as a "zone file"). **WARNING** there is a syntactical problem with Go templates and CoreDNS config files. Expressions @@ -177,6 +181,23 @@ Having templates to map certain PTR/A pairs is a common pattern. Fallthrough is needed for mixed domains where only some responses are templated. +### Resolve hexadecimal ip pattern using parseInt + +~~~ corefile +. { + forward . 8.8.8.8 + + template IN A example { + match "^ip0a(?P[a-f0-9]{2})(?P[a-f0-9]{2})(?P[a-f0-9]{2})[.]example[.]$" + answer "{{ .Name }} 60 IN A 10.{{ parseInt .Group.b 16 8 }}.{{ parseInt .Group.c 16 8 }}.{{ parseInt .Group.d 16 8 }}" + fallthrough + } +} +~~~ + +An IPv4 address can be expressed in a more compact form using its hexadecimal encoding. +For example `ip-10-123-123.example.` can instead be expressed as `ip0a7b7b7b.example.` + ### Resolve multiple ip patterns ~~~ corefile diff --git a/plugin/template/setup.go b/plugin/template/setup.go index 44d774b6f..fc6b75165 100644 --- a/plugin/template/setup.go +++ b/plugin/template/setup.go @@ -80,7 +80,7 @@ func templateParse(c *caddy.Controller) (handler Handler, err error) { return handler, c.ArgErr() } for _, answer := range args { - tmpl, err := gotmpl.New("answer").Parse(answer) + tmpl, err := newTemplate("answer", answer) if err != nil { return handler, c.Errf("could not compile template: %s, %v", c.Val(), err) } @@ -93,7 +93,7 @@ func templateParse(c *caddy.Controller) (handler Handler, err error) { return handler, c.ArgErr() } for _, additional := range args { - tmpl, err := gotmpl.New("additional").Parse(additional) + tmpl, err := newTemplate("additional", additional) if err != nil { return handler, c.Errf("could not compile template: %s, %v\n", c.Val(), err) } @@ -106,7 +106,7 @@ func templateParse(c *caddy.Controller) (handler Handler, err error) { return handler, c.ArgErr() } for _, authority := range args { - tmpl, err := gotmpl.New("authority").Parse(authority) + tmpl, err := newTemplate("authority", authority) if err != nil { return handler, c.Errf("could not compile template: %s, %v\n", c.Val(), err) } diff --git a/plugin/template/setup_test.go b/plugin/template/setup_test.go index 39096cc8d..9f03eebcc 100644 --- a/plugin/template/setup_test.go +++ b/plugin/template/setup_test.go @@ -83,6 +83,20 @@ func TestSetupParse(t *testing.T) { }`, true, }, + { + `template ANY ANY { + answer "{{ notAFunction }}" + }`, + true, + }, + { + `template ANY ANY { + answer "{{ parseInt }}" + additional "{{ parseInt }}" + authority "{{ parseInt }}" + }`, + false, + }, // examples {`template ANY ANY (?P`, false}, { @@ -128,6 +142,13 @@ func TestSetupParse(t *testing.T) { }`, false, }, + { + `template IN A example { + match ^ip0a(?P[a-f0-9]{2})(?P[a-f0-9]{2})(?P[a-f0-9]{2})[.]example[.]$ + answer "{{ .Name }} 3600 IN A 10.{{ parseInt .Group.b 16 8 }}.{{ parseInt .Group.c 16 8 }}.{{ parseInt .Group.d 16 8 }}" + }`, + false, + }, { `template IN MX example { match ^ip-10-(?P[0-9]*)-(?P[0-9]*)-(?P[0-9]*)[.]example[.]$ diff --git a/plugin/template/template.go b/plugin/template/template.go index 7b6647109..48e9d6c54 100644 --- a/plugin/template/template.go +++ b/plugin/template/template.go @@ -150,6 +150,13 @@ func executeRRTemplate(server, view, section string, template *gotmpl.Template, return rr, nil } +func newTemplate(name, text string) (*gotmpl.Template, error) { + funcMap := gotmpl.FuncMap{ + "parseInt": strconv.ParseUint, + } + return gotmpl.New(name).Funcs(funcMap).Parse(text) +} + func (t template) match(ctx context.Context, state request.Request) (*templateData, bool, bool) { q := state.Req.Question[0] data := &templateData{md: metadata.ValueFuncs(ctx), Remote: state.IP()} diff --git a/plugin/template/template_test.go b/plugin/template/template_test.go index 25e8df748..3587742aa 100644 --- a/plugin/template/template_test.go +++ b/plugin/template/template_test.go @@ -19,7 +19,15 @@ import ( func TestHandler(t *testing.T) { exampleDomainATemplate := template{ regex: []*regexp.Regexp{regexp.MustCompile("(^|[.])ip-10-(?P[0-9]*)-(?P[0-9]*)-(?P[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(newTemplate("answer", "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}"))}, + qclass: dns.ClassANY, + qtype: dns.TypeANY, + fall: fall.Root, + zones: []string{"."}, + } + exampleDomainAParseIntTemplate := template{ + regex: []*regexp.Regexp{regexp.MustCompile("^ip0a(?P[a-f0-9]{2})(?P[a-f0-9]{2})(?P[a-f0-9]{2})[.]example[.]$")}, + answer: []*gotmpl.Template{gotmpl.Must(newTemplate("answer", "{{ .Name }} 60 IN A 10.{{ parseInt .Group.b 16 8 }}.{{ parseInt .Group.c 16 8 }}.{{ parseInt .Group.d 16 8 }}"))}, qclass: dns.ClassANY, qtype: dns.TypeANY, fall: fall.Root, @@ -27,7 +35,7 @@ func TestHandler(t *testing.T) { } exampleDomainIPATemplate := template{ regex: []*regexp.Regexp{regexp.MustCompile(".*")}, - answer: []*gotmpl.Template{gotmpl.Must(gotmpl.New("answer").Parse("{{ .Name }} 60 IN A {{ .Remote }}"))}, + answer: []*gotmpl.Template{gotmpl.Must(newTemplate("answer", "{{ .Name }} 60 IN A {{ .Remote }}"))}, qclass: dns.ClassINET, qtype: dns.TypeA, fall: fall.Root, @@ -35,9 +43,9 @@ func TestHandler(t *testing.T) { } exampleDomainANSTemplate := template{ regex: []*regexp.Regexp{regexp.MustCompile("(^|[.])ip-10-(?P[0-9]*)-(?P[0-9]*)-(?P[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."))}, + answer: []*gotmpl.Template{gotmpl.Must(newTemplate("answer", "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}"))}, + additional: []*gotmpl.Template{gotmpl.Must(newTemplate("additional", "ns0.example. IN A 203.0.113.8"))}, + authority: []*gotmpl.Template{gotmpl.Must(newTemplate("authority", "example. IN NS ns0.example.com."))}, qclass: dns.ClassANY, qtype: dns.TypeANY, fall: fall.Root, @@ -45,8 +53,8 @@ func TestHandler(t *testing.T) { } exampleDomainMXTemplate := template{ regex: []*regexp.Regexp{regexp.MustCompile("(^|[.])ip-10-(?P[0-9]*)-(?P[0-9]*)-(?P[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 }}"))}, + answer: []*gotmpl.Template{gotmpl.Must(newTemplate("answer", "{{ .Name }} 60 MX 10 {{ .Name }}"))}, + additional: []*gotmpl.Template{gotmpl.Must(newTemplate("additional", "{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}"))}, qclass: dns.ClassANY, qtype: dns.TypeANY, fall: fall.Root, @@ -55,7 +63,7 @@ func TestHandler(t *testing.T) { invalidDomainTemplate := template{ 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)"))}, + answer: []*gotmpl.Template{gotmpl.Must(newTemplate("answer", "invalid. 60 {{ .Class }} SOA a.invalid. b.invalid. (1 60 60 60 60)"))}, qclass: dns.ClassANY, qtype: dns.TypeANY, fall: fall.Root, @@ -71,7 +79,15 @@ func TestHandler(t *testing.T) { } brokenTemplate := template{ regex: []*regexp.Regexp{regexp.MustCompile("[.]example[.]$")}, - answer: []*gotmpl.Template{gotmpl.Must(gotmpl.New("answer").Parse("{{ .Name }} 60 IN TXT \"{{ index .Match 2 }}\""))}, + answer: []*gotmpl.Template{gotmpl.Must(newTemplate("answer", "{{ .Name }} 60 IN TXT \"{{ index .Match 2 }}\""))}, + qclass: dns.ClassANY, + qtype: dns.TypeANY, + fall: fall.Root, + zones: []string{"."}, + } + brokenParseIntTemplate := template{ + regex: []*regexp.Regexp{regexp.MustCompile("[.]example[.]$")}, + answer: []*gotmpl.Template{gotmpl.Must(newTemplate("answer", "{{ .Name }} 60 IN TXT \"{{ parseInt \"gg\" 16 8 }}\""))}, qclass: dns.ClassANY, qtype: dns.TypeANY, fall: fall.Root, @@ -79,7 +95,7 @@ func TestHandler(t *testing.T) { } nonRRTemplate := template{ regex: []*regexp.Regexp{regexp.MustCompile("[.]example[.]$")}, - answer: []*gotmpl.Template{gotmpl.Must(gotmpl.New("answer").Parse("{{ .Name }}"))}, + answer: []*gotmpl.Template{gotmpl.Must(newTemplate("answer", "{{ .Name }}"))}, qclass: dns.ClassANY, qtype: dns.TypeANY, fall: fall.Root, @@ -87,7 +103,7 @@ func TestHandler(t *testing.T) { } nonRRAdditionalTemplate := template{ regex: []*regexp.Regexp{regexp.MustCompile("[.]example[.]$")}, - additional: []*gotmpl.Template{gotmpl.Must(gotmpl.New("answer").Parse("{{ .Name }}"))}, + additional: []*gotmpl.Template{gotmpl.Must(newTemplate("answer", "{{ .Name }}"))}, qclass: dns.ClassANY, qtype: dns.TypeANY, fall: fall.Root, @@ -95,7 +111,7 @@ func TestHandler(t *testing.T) { } nonRRAuthoritativeTemplate := template{ regex: []*regexp.Regexp{regexp.MustCompile("[.]example[.]$")}, - authority: []*gotmpl.Template{gotmpl.Must(gotmpl.New("authority").Parse("{{ .Name }}"))}, + authority: []*gotmpl.Template{gotmpl.Must(newTemplate("answer", "{{ .Name }}"))}, qclass: dns.ClassANY, qtype: dns.TypeANY, fall: fall.Root, @@ -103,7 +119,7 @@ func TestHandler(t *testing.T) { } cnameTemplate := template{ regex: []*regexp.Regexp{regexp.MustCompile("example[.]net[.]")}, - answer: []*gotmpl.Template{gotmpl.Must(gotmpl.New("answer").Parse("example.net 60 IN CNAME target.example.com"))}, + answer: []*gotmpl.Template{gotmpl.Must(newTemplate("answer", "example.net 60 IN CNAME target.example.com"))}, qclass: dns.ClassANY, qtype: dns.TypeANY, fall: fall.Root, @@ -111,9 +127,9 @@ func TestHandler(t *testing.T) { } mdTemplate := template{ regex: []*regexp.Regexp{regexp.MustCompile("(^|[.])ip-10-(?P[0-9]*)-(?P[0-9]*)-(?P[0-9]*)[.]example[.]$")}, - answer: []*gotmpl.Template{gotmpl.Must(gotmpl.New("answer").Parse(`{{ .Meta "foo" }}-{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}`))}, - additional: []*gotmpl.Template{gotmpl.Must(gotmpl.New("additional").Parse(`{{ .Meta "bar" }}.example. IN A 203.0.113.8`))}, - authority: []*gotmpl.Template{gotmpl.Must(gotmpl.New("authority").Parse(`example. IN NS {{ .Meta "bar" }}.example.com.`))}, + answer: []*gotmpl.Template{gotmpl.Must(newTemplate("answer", `{{ .Meta "foo" }}-{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}`))}, + additional: []*gotmpl.Template{gotmpl.Must(newTemplate("additional", `{{ .Meta "bar" }}.example. IN A 203.0.113.8`))}, + authority: []*gotmpl.Template{gotmpl.Must(newTemplate("authority", `example. IN NS {{ .Meta "bar" }}.example.com.`))}, qclass: dns.ClassANY, qtype: dns.TypeANY, fall: fall.Root, @@ -121,7 +137,7 @@ func TestHandler(t *testing.T) { } mdMissingTemplate := template{ regex: []*regexp.Regexp{regexp.MustCompile("(^|[.])ip-10-(?P[0-9]*)-(?P[0-9]*)-(?P[0-9]*)[.]example[.]$")}, - answer: []*gotmpl.Template{gotmpl.Must(gotmpl.New("answer").Parse(`{{ .Meta "foofoo" }}{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}`))}, + answer: []*gotmpl.Template{gotmpl.Must(newTemplate("answer", `{{ .Meta "foofoo" }}{{ .Name }} 60 IN A 10.{{ .Group.b }}.{{ .Group.c }}.{{ .Group.d }}`))}, qclass: dns.ClassANY, qtype: dns.TypeANY, fall: fall.Root, @@ -242,6 +258,37 @@ func TestHandler(t *testing.T) { return nil }, }, + { + name: "ExampleDomainMatchHexIp", + tmpl: exampleDomainAParseIntTemplate, + qclass: dns.ClassINET, + qtype: dns.TypeA, + qname: "ip0a5f0c09.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 answer, got %v", dns.TypeToString[r.Answer[0].Header().Rrtype]) + } + if r.Answer[0].(*dns.A).A.String() != "10.95.12.9" { + return fmt.Errorf("expected an A record for 10.95.12.9, got %v", r.Answer[0].String()) + } + return nil + }, + }, + { + name: "BrokenParseIntTemplate", + tmpl: brokenParseIntTemplate, + qclass: dns.ClassINET, + qtype: dns.TypeANY, + qname: "test.example.", + expectedCode: dns.RcodeServerFailure, + expectedErr: "template: answer:1:26: executing \"answer\" at : error calling parseInt: strconv.ParseUint: parsing \"gg\": invalid syntax", + verifyResponse: func(r *dns.Msg) error { + return nil + }, + }, { name: "ExampleDomainMXMatch", tmpl: exampleDomainMXTemplate,