aboutsummaryrefslogtreecommitdiff
path: root/plugin
diff options
context:
space:
mode:
authorGravatar Erik Johansson <ejohansson@spotify.com> 2022-09-15 12:25:58 -0700
committerGravatar GitHub <noreply@github.com> 2022-09-15 15:25:58 -0400
commite93e932ce8d851ed5f5645ad37c35b56481be316 (patch)
treeb7f5d8db7e95378b94d71f3b21f462c62404309b /plugin
parent7beb76c045ddbe222ad4920b408149ae70f3fbcd (diff)
downloadcoredns-e93e932ce8d851ed5f5645ad37c35b56481be316.tar.gz
coredns-e93e932ce8d851ed5f5645ad37c35b56481be316.tar.zst
coredns-e93e932ce8d851ed5f5645ad37c35b56481be316.zip
plugin/template: Add parseInt template function (#5609)
* plugin/template: Add parseInt template function Signed-off-by: Erik Johansson <ejohansson@spotify.com>
Diffstat (limited to 'plugin')
-rw-r--r--plugin/template/README.md21
-rw-r--r--plugin/template/setup.go6
-rw-r--r--plugin/template/setup_test.go21
-rw-r--r--plugin/template/template.go7
-rw-r--r--plugin/template/template_test.go81
5 files changed, 116 insertions, 20 deletions
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<b>[a-f0-9]{2})(?P<c>[a-f0-9]{2})(?P<d>[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<x>`, false},
{
@@ -129,6 +143,13 @@ func TestSetupParse(t *testing.T) {
false,
},
{
+ `template IN A example {
+ match ^ip0a(?P<b>[a-f0-9]{2})(?P<c>[a-f0-9]{2})(?P<d>[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<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[0-9]*)[.]example[.]$
answer "{{ .Name }} 60 IN MX 10 {{ .Name }}"
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<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(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<b>[a-f0-9]{2})(?P<c>[a-f0-9]{2})(?P<d>[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<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."))},
+ 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<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 }}"))},
+ 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<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[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<b>[0-9]*)-(?P<c>[0-9]*)-(?P<d>[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,
@@ -243,6 +259,37 @@ func TestHandler(t *testing.T) {
},
},
{
+ 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 <parseInt \"gg\" 16 8>: error calling parseInt: strconv.ParseUint: parsing \"gg\": invalid syntax",
+ verifyResponse: func(r *dns.Msg) error {
+ return nil
+ },
+ },
+ {
name: "ExampleDomainMXMatch",
tmpl: exampleDomainMXTemplate,
qclass: dns.ClassINET,