aboutsummaryrefslogtreecommitdiff
path: root/plugin/file
diff options
context:
space:
mode:
Diffstat (limited to 'plugin/file')
-rw-r--r--plugin/file/README.md55
-rw-r--r--plugin/file/closest.go24
-rw-r--r--plugin/file/closest_test.go38
-rw-r--r--plugin/file/cname_test.go124
-rw-r--r--plugin/file/delegation_test.go207
-rw-r--r--plugin/file/dname.go44
-rw-r--r--plugin/file/dname_test.go300
-rw-r--r--plugin/file/dnssec_test.go358
-rw-r--r--plugin/file/dnssex_test.go145
-rw-r--r--plugin/file/ds_test.go75
-rw-r--r--plugin/file/ent_test.go159
-rw-r--r--plugin/file/example_org.go113
-rw-r--r--plugin/file/file.go138
-rw-r--r--plugin/file/file_test.go31
-rw-r--r--plugin/file/glue_test.go253
-rw-r--r--plugin/file/include_test.go32
-rw-r--r--plugin/file/lookup.go467
-rw-r--r--plugin/file/lookup_test.go194
-rw-r--r--plugin/file/notify.go82
-rw-r--r--plugin/file/nsec3_test.go28
-rw-r--r--plugin/file/reload.go72
-rw-r--r--plugin/file/reload_test.go82
-rw-r--r--plugin/file/secondary.go199
-rw-r--r--plugin/file/secondary_test.go168
-rw-r--r--plugin/file/setup.go171
-rw-r--r--plugin/file/setup_test.go77
-rw-r--r--plugin/file/tree/all.go48
-rw-r--r--plugin/file/tree/elem.go136
-rw-r--r--plugin/file/tree/less.go59
-rw-r--r--plugin/file/tree/less_test.go81
-rw-r--r--plugin/file/tree/print.go62
-rw-r--r--plugin/file/tree/tree.go455
-rw-r--r--plugin/file/wildcard.go13
-rw-r--r--plugin/file/wildcard_test.go289
-rw-r--r--plugin/file/xfr.go62
-rw-r--r--plugin/file/xfr_test.go34
-rw-r--r--plugin/file/zone.go190
-rw-r--r--plugin/file/zone_test.go30
38 files changed, 5095 insertions, 0 deletions
diff --git a/plugin/file/README.md b/plugin/file/README.md
new file mode 100644
index 000000000..d7e1590b4
--- /dev/null
+++ b/plugin/file/README.md
@@ -0,0 +1,55 @@
+# file
+
+*file* enables serving zone data from an RFC 1035-style master file.
+
+The file plugin is used for an "old-style" DNS server. It serves from a preloaded file that exists
+on disk. If the zone file contains signatures (i.e. is signed, i.e. DNSSEC) correct DNSSEC answers
+are returned. Only NSEC is supported! If you use this setup *you* are responsible for resigning the
+zonefile.
+
+## Syntax
+
+~~~
+file DBFILE [ZONES...]
+~~~
+
+* **DBFILE** the database file to read and parse. If the path is relative the path from the *root*
+ directive will be prepended to it.
+* **ZONES** zones it should be authoritative for. If empty, the zones from the configuration block
+ are used.
+
+If you want to round robin A and AAAA responses look at the *loadbalance* plugin.
+
+TSIG key configuration is TODO; directive format for transfer will probably be extended with
+TSIG key information, something like `transfer out [ADDRESS...] key [NAME[:ALG]] [BASE64]`
+
+~~~
+file DBFILE [ZONES... ] {
+ transfer to ADDRESS...
+ no_reload
+ upstream ADDRESS...
+}
+~~~
+
+* `transfer` enables zone transfers. It may be specified multiples times. `To` or `from` signals
+ the direction. **ADDRESS** must be denoted in CIDR notation (127.0.0.1/32 etc.) or just as plain
+ addresses. The special wildcard `*` means: the entire internet (only valid for 'transfer to').
+ When an address is specified a notify message will be send whenever the zone is reloaded.
+* `no_reload` by default CoreDNS will reload a zone from disk whenever it detects a change to the
+ file. This option disables that behavior.
+* `upstream` defines upstream resolvers to be used resolve external names found (think CNAMEs)
+ pointing to external names. This is only really useful when CoreDNS is configured as a proxy, for
+ normal authoritative serving you don't need *or* want to use this. **ADDRESS** can be an IP
+ address, and IP:port or a string pointing to a file that is structured as /etc/resolv.conf.
+
+## Examples
+
+Load the `example.org` zone from `example.org.signed` and allow transfers to the internet, but send
+notifies to 10.240.1.1
+
+~~~
+file example.org.signed example.org {
+ transfer to *
+ transfer to 10.240.1.1
+}
+~~~
diff --git a/plugin/file/closest.go b/plugin/file/closest.go
new file mode 100644
index 000000000..64652af83
--- /dev/null
+++ b/plugin/file/closest.go
@@ -0,0 +1,24 @@
+package file
+
+import (
+ "github.com/coredns/coredns/plugin/file/tree"
+
+ "github.com/miekg/dns"
+)
+
+// ClosestEncloser returns the closest encloser for qname.
+func (z *Zone) ClosestEncloser(qname string) (*tree.Elem, bool) {
+
+ offset, end := dns.NextLabel(qname, 0)
+ for !end {
+ elem, _ := z.Tree.Search(qname)
+ if elem != nil {
+ return elem, true
+ }
+ qname = qname[offset:]
+
+ offset, end = dns.NextLabel(qname, offset)
+ }
+
+ return z.Tree.Search(z.origin)
+}
diff --git a/plugin/file/closest_test.go b/plugin/file/closest_test.go
new file mode 100644
index 000000000..b37495493
--- /dev/null
+++ b/plugin/file/closest_test.go
@@ -0,0 +1,38 @@
+package file
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestClosestEncloser(t *testing.T) {
+ z, err := Parse(strings.NewReader(dbMiekNL), testzone, "stdin", 0)
+ if err != nil {
+ t.Fatalf("expect no error when reading zone, got %q", err)
+ }
+
+ tests := []struct {
+ in, out string
+ }{
+ {"miek.nl.", "miek.nl."},
+ {"www.miek.nl.", "www.miek.nl."},
+
+ {"blaat.miek.nl.", "miek.nl."},
+ {"blaat.www.miek.nl.", "www.miek.nl."},
+ {"www.blaat.miek.nl.", "miek.nl."},
+ {"blaat.a.miek.nl.", "a.miek.nl."},
+ }
+
+ for _, tc := range tests {
+ ce, _ := z.ClosestEncloser(tc.in)
+ if ce == nil {
+ if z.origin != tc.out {
+ t.Errorf("Expected ce to be %s for %s, got %s", tc.out, tc.in, ce.Name())
+ }
+ continue
+ }
+ if ce.Name() != tc.out {
+ t.Errorf("Expected ce to be %s for %s, got %s", tc.out, tc.in, ce.Name())
+ }
+ }
+}
diff --git a/plugin/file/cname_test.go b/plugin/file/cname_test.go
new file mode 100644
index 000000000..1178a7512
--- /dev/null
+++ b/plugin/file/cname_test.go
@@ -0,0 +1,124 @@
+package file
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/proxy"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+func TestLookupCNAMEChain(t *testing.T) {
+ name := "example.org."
+ zone, err := Parse(strings.NewReader(dbExampleCNAME), name, "stdin", 0)
+ if err != nil {
+ t.Fatalf("Expected no error when reading zone, got %q", err)
+ }
+
+ fm := File{Next: test.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{name: zone}, Names: []string{name}}}
+ ctx := context.TODO()
+
+ for _, tc := range cnameTestCases {
+ m := tc.Msg()
+
+ rec := dnsrecorder.New(&test.ResponseWriter{})
+ _, err := fm.ServeDNS(ctx, rec, m)
+ if err != nil {
+ t.Errorf("Expected no error, got %v\n", err)
+ return
+ }
+
+ resp := rec.Msg
+ test.SortAndCheck(t, resp, tc)
+ }
+}
+
+var cnameTestCases = []test.Case{
+ {
+ Qname: "a.example.org.", Qtype: dns.TypeA,
+ Answer: []dns.RR{
+ test.A("a.example.org. 1800 IN A 127.0.0.1"),
+ },
+ },
+ {
+ Qname: "www3.example.org.", Qtype: dns.TypeCNAME,
+ Answer: []dns.RR{
+ test.CNAME("www3.example.org. 1800 IN CNAME www2.example.org."),
+ },
+ },
+ {
+ Qname: "dangling.example.org.", Qtype: dns.TypeA,
+ Answer: []dns.RR{
+ test.CNAME("dangling.example.org. 1800 IN CNAME foo.example.org."),
+ },
+ },
+ {
+ Qname: "www3.example.org.", Qtype: dns.TypeA,
+ Answer: []dns.RR{
+ test.A("a.example.org. 1800 IN A 127.0.0.1"),
+ test.CNAME("www.example.org. 1800 IN CNAME a.example.org."),
+ test.CNAME("www1.example.org. 1800 IN CNAME www.example.org."),
+ test.CNAME("www2.example.org. 1800 IN CNAME www1.example.org."),
+ test.CNAME("www3.example.org. 1800 IN CNAME www2.example.org."),
+ },
+ },
+}
+
+func TestLookupCNAMEExternal(t *testing.T) {
+ name := "example.org."
+ zone, err := Parse(strings.NewReader(dbExampleCNAME), name, "stdin", 0)
+ if err != nil {
+ t.Fatalf("Expected no error when reading zone, got %q", err)
+ }
+ zone.Proxy = proxy.NewLookup([]string{"8.8.8.8:53"}) // TODO(miek): point to local instance
+
+ fm := File{Next: test.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{name: zone}, Names: []string{name}}}
+ ctx := context.TODO()
+
+ for _, tc := range exernalTestCases {
+ m := tc.Msg()
+
+ rec := dnsrecorder.New(&test.ResponseWriter{})
+ _, err := fm.ServeDNS(ctx, rec, m)
+ if err != nil {
+ t.Errorf("Expected no error, got %v\n", err)
+ return
+ }
+
+ resp := rec.Msg
+ test.SortAndCheck(t, resp, tc)
+ }
+}
+
+var exernalTestCases = []test.Case{
+ {
+ Qname: "external.example.org.", Qtype: dns.TypeA,
+ Answer: []dns.RR{
+ test.CNAME("external.example.org. 1800 CNAME www.example.net."),
+ // magic 303 TTL that says: don't check TTL.
+ test.A("www.example.net. 303 IN A 93.184.216.34"),
+ },
+ },
+}
+
+const dbExampleCNAME = `
+$TTL 30M
+$ORIGIN example.org.
+@ IN SOA linode.atoom.net. miek.miek.nl. (
+ 1282630057 ; Serial
+ 4H ; Refresh
+ 1H ; Retry
+ 7D ; Expire
+ 4H ) ; Negative Cache TTL
+
+a IN A 127.0.0.1
+www3 IN CNAME www2
+www2 IN CNAME www1
+www1 IN CNAME www
+www IN CNAME a
+dangling IN CNAME foo
+external IN CNAME www.example.net.`
diff --git a/plugin/file/delegation_test.go b/plugin/file/delegation_test.go
new file mode 100644
index 000000000..1ad9804f4
--- /dev/null
+++ b/plugin/file/delegation_test.go
@@ -0,0 +1,207 @@
+package file
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+var delegationTestCases = []test.Case{
+ {
+ Qname: "a.delegated.miek.nl.", Qtype: dns.TypeTXT,
+ Ns: []dns.RR{
+ test.NS("delegated.miek.nl. 1800 IN NS a.delegated.miek.nl."),
+ test.NS("delegated.miek.nl. 1800 IN NS ns-ext.nlnetlabs.nl."),
+ },
+ Extra: []dns.RR{
+ test.A("a.delegated.miek.nl. 1800 IN A 139.162.196.78"),
+ test.AAAA("a.delegated.miek.nl. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"),
+ },
+ },
+ {
+ Qname: "delegated.miek.nl.", Qtype: dns.TypeNS,
+ Answer: []dns.RR{
+ test.NS("delegated.miek.nl. 1800 IN NS a.delegated.miek.nl."),
+ test.NS("delegated.miek.nl. 1800 IN NS ns-ext.nlnetlabs.nl."),
+ },
+ Extra: []dns.RR{
+ test.A("a.delegated.miek.nl. 1800 IN A 139.162.196.78"),
+ test.AAAA("a.delegated.miek.nl. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"),
+ },
+ },
+ {
+ Qname: "foo.delegated.miek.nl.", Qtype: dns.TypeA,
+ Ns: []dns.RR{
+ test.NS("delegated.miek.nl. 1800 IN NS a.delegated.miek.nl."),
+ test.NS("delegated.miek.nl. 1800 IN NS ns-ext.nlnetlabs.nl."),
+ },
+ Extra: []dns.RR{
+ test.A("a.delegated.miek.nl. 1800 IN A 139.162.196.78"),
+ test.AAAA("a.delegated.miek.nl. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"),
+ },
+ },
+ {
+ Qname: "foo.delegated.miek.nl.", Qtype: dns.TypeTXT,
+ Ns: []dns.RR{
+ test.NS("delegated.miek.nl. 1800 IN NS a.delegated.miek.nl."),
+ test.NS("delegated.miek.nl. 1800 IN NS ns-ext.nlnetlabs.nl."),
+ },
+ Extra: []dns.RR{
+ test.A("a.delegated.miek.nl. 1800 IN A 139.162.196.78"),
+ test.AAAA("a.delegated.miek.nl. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"),
+ },
+ },
+ {
+ Qname: "miek.nl.", Qtype: dns.TypeSOA,
+ Answer: []dns.RR{
+ test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"),
+ },
+ Ns: miekAuth,
+ },
+ {
+ Qname: "miek.nl.", Qtype: dns.TypeAAAA,
+ Ns: []dns.RR{
+ test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"),
+ },
+ },
+}
+
+var secureDelegationTestCases = []test.Case{
+ {
+ Qname: "a.delegated.example.org.", Qtype: dns.TypeTXT,
+ Do: true,
+ Ns: []dns.RR{
+ test.DS("delegated.example.org. 1800 IN DS 10056 5 1 EE72CABD1927759CDDA92A10DBF431504B9E1F13"),
+ test.DS("delegated.example.org. 1800 IN DS 10056 5 2 E4B05F87725FA86D9A64F1E53C3D0E6250946599DFE639C45955B0ED416CDDFA"),
+ test.NS("delegated.example.org. 1800 IN NS a.delegated.example.org."),
+ test.NS("delegated.example.org. 1800 IN NS ns-ext.nlnetlabs.nl."),
+ test.RRSIG("delegated.example.org. 1800 IN RRSIG DS 13 3 1800 20161129153240 20161030153240 49035 example.org. rlNNzcUmtbjLSl02ZzQGUbWX75yCUx0Mug1jHtKVqRq1hpPE2S3863tIWSlz+W9wz4o19OI4jbznKKqk+DGKog=="),
+ },
+ Extra: []dns.RR{
+ test.OPT(4096, true),
+ test.A("a.delegated.example.org. 1800 IN A 139.162.196.78"),
+ test.AAAA("a.delegated.example.org. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"),
+ },
+ },
+ {
+ Qname: "delegated.example.org.", Qtype: dns.TypeNS,
+ Do: true,
+ Answer: []dns.RR{
+ test.NS("delegated.example.org. 1800 IN NS a.delegated.example.org."),
+ test.NS("delegated.example.org. 1800 IN NS ns-ext.nlnetlabs.nl."),
+ },
+ Extra: []dns.RR{
+ test.OPT(4096, true),
+ test.A("a.delegated.example.org. 1800 IN A 139.162.196.78"),
+ test.AAAA("a.delegated.example.org. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"),
+ },
+ },
+ {
+ Qname: "foo.delegated.example.org.", Qtype: dns.TypeA,
+ Do: true,
+ Ns: []dns.RR{
+ test.DS("delegated.example.org. 1800 IN DS 10056 5 1 EE72CABD1927759CDDA92A10DBF431504B9E1F13"),
+ test.DS("delegated.example.org. 1800 IN DS 10056 5 2 E4B05F87725FA86D9A64F1E53C3D0E6250946599DFE639C45955B0ED416CDDFA"),
+ test.NS("delegated.example.org. 1800 IN NS a.delegated.example.org."),
+ test.NS("delegated.example.org. 1800 IN NS ns-ext.nlnetlabs.nl."),
+ test.RRSIG("delegated.example.org. 1800 IN RRSIG DS 13 3 1800 20161129153240 20161030153240 49035 example.org. rlNNzcUmtbjLSl02ZzQGUbWX75yCUx0Mug1jHtKVqRq1hpPE2S3863tIWSlz+W9wz4o19OI4jbznKKqk+DGKog=="),
+ },
+ Extra: []dns.RR{
+ test.OPT(4096, true),
+ test.A("a.delegated.example.org. 1800 IN A 139.162.196.78"),
+ test.AAAA("a.delegated.example.org. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"),
+ },
+ },
+ {
+ Qname: "foo.delegated.example.org.", Qtype: dns.TypeTXT,
+ Do: true,
+ Ns: []dns.RR{
+ test.DS("delegated.example.org. 1800 IN DS 10056 5 1 EE72CABD1927759CDDA92A10DBF431504B9E1F13"),
+ test.DS("delegated.example.org. 1800 IN DS 10056 5 2 E4B05F87725FA86D9A64F1E53C3D0E6250946599DFE639C45955B0ED416CDDFA"),
+ test.NS("delegated.example.org. 1800 IN NS a.delegated.example.org."),
+ test.NS("delegated.example.org. 1800 IN NS ns-ext.nlnetlabs.nl."),
+ test.RRSIG("delegated.example.org. 1800 IN RRSIG DS 13 3 1800 20161129153240 20161030153240 49035 example.org. rlNNzcUmtbjLSl02ZzQGUbWX75yCUx0Mug1jHtKVqRq1hpPE2S3863tIWSlz+W9wz4o19OI4jbznKKqk+DGKog=="),
+ },
+ Extra: []dns.RR{
+ test.OPT(4096, true),
+ test.A("a.delegated.example.org. 1800 IN A 139.162.196.78"),
+ test.AAAA("a.delegated.example.org. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"),
+ },
+ },
+}
+
+var miekAuth = []dns.RR{
+ test.NS("miek.nl. 1800 IN NS ext.ns.whyscream.net."),
+ test.NS("miek.nl. 1800 IN NS linode.atoom.net."),
+ test.NS("miek.nl. 1800 IN NS ns-ext.nlnetlabs.nl."),
+ test.NS("miek.nl. 1800 IN NS omval.tednet.nl."),
+}
+
+func TestLookupDelegation(t *testing.T) {
+ testDelegation(t, dbMiekNLDelegation, testzone, delegationTestCases)
+}
+
+func TestLookupSecureDelegation(t *testing.T) {
+ testDelegation(t, exampleOrgSigned, "example.org.", secureDelegationTestCases)
+}
+
+func testDelegation(t *testing.T, z, origin string, testcases []test.Case) {
+ zone, err := Parse(strings.NewReader(z), origin, "stdin", 0)
+ if err != nil {
+ t.Fatalf("Expect no error when reading zone, got %q", err)
+ }
+
+ fm := File{Next: test.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{origin: zone}, Names: []string{origin}}}
+ ctx := context.TODO()
+
+ for _, tc := range testcases {
+ m := tc.Msg()
+
+ rec := dnsrecorder.New(&test.ResponseWriter{})
+ _, err := fm.ServeDNS(ctx, rec, m)
+ if err != nil {
+ t.Errorf("Expected no error, got %q\n", err)
+ return
+ }
+
+ resp := rec.Msg
+ test.SortAndCheck(t, resp, tc)
+ }
+}
+
+const dbMiekNLDelegation = `
+$TTL 30M
+$ORIGIN miek.nl.
+@ IN SOA linode.atoom.net. miek.miek.nl. (
+ 1282630057 ; Serial
+ 4H ; Refresh
+ 1H ; Retry
+ 7D ; Expire
+ 4H ) ; Negative Cache TTL
+ IN NS linode.atoom.net.
+ IN NS ns-ext.nlnetlabs.nl.
+ IN NS omval.tednet.nl.
+ IN NS ext.ns.whyscream.net.
+
+ IN MX 1 aspmx.l.google.com.
+ IN MX 5 alt1.aspmx.l.google.com.
+ IN MX 5 alt2.aspmx.l.google.com.
+ IN MX 10 aspmx2.googlemail.com.
+ IN MX 10 aspmx3.googlemail.com.
+
+delegated IN NS a.delegated
+ IN NS ns-ext.nlnetlabs.nl.
+
+a.delegated IN TXT "obscured"
+ IN A 139.162.196.78
+ IN AAAA 2a01:7e00::f03c:91ff:fef1:6735
+
+a IN A 139.162.196.78
+ IN AAAA 2a01:7e00::f03c:91ff:fef1:6735
+www IN CNAME a
+archive IN CNAME a`
diff --git a/plugin/file/dname.go b/plugin/file/dname.go
new file mode 100644
index 000000000..f552bfdfd
--- /dev/null
+++ b/plugin/file/dname.go
@@ -0,0 +1,44 @@
+package file
+
+import (
+ "github.com/coredns/coredns/plugin/pkg/dnsutil"
+
+ "github.com/miekg/dns"
+)
+
+// substituteDNAME performs the DNAME substitution defined by RFC 6672,
+// assuming the QTYPE of the query is not DNAME. It returns an empty
+// string if there is no match.
+func substituteDNAME(qname, owner, target string) string {
+ if dns.IsSubDomain(owner, qname) && qname != owner {
+ labels := dns.SplitDomainName(qname)
+ labels = append(labels[0:len(labels)-dns.CountLabel(owner)], dns.SplitDomainName(target)...)
+
+ return dnsutil.Join(labels)
+ }
+
+ return ""
+}
+
+// synthesizeCNAME returns a CNAME RR pointing to the resulting name of
+// the DNAME substitution. The owner name of the CNAME is the QNAME of
+// the query and the TTL is the same as the corresponding DNAME RR.
+//
+// It returns nil if the DNAME substitution has no match.
+func synthesizeCNAME(qname string, d *dns.DNAME) *dns.CNAME {
+ target := substituteDNAME(qname, d.Header().Name, d.Target)
+ if target == "" {
+ return nil
+ }
+
+ r := new(dns.CNAME)
+ r.Hdr = dns.RR_Header{
+ Name: qname,
+ Rrtype: dns.TypeCNAME,
+ Class: dns.ClassINET,
+ Ttl: d.Header().Ttl,
+ }
+ r.Target = target
+
+ return r
+}
diff --git a/plugin/file/dname_test.go b/plugin/file/dname_test.go
new file mode 100644
index 000000000..92e33dde7
--- /dev/null
+++ b/plugin/file/dname_test.go
@@ -0,0 +1,300 @@
+package file
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+// RFC 6672, Section 2.2. Assuming QTYPE != DNAME.
+var dnameSubstitutionTestCases = []struct {
+ qname string
+ owner string
+ target string
+ expected string
+}{
+ {"com.", "example.com.", "example.net.", ""},
+ {"example.com.", "example.com.", "example.net.", ""},
+ {"a.example.com.", "example.com.", "example.net.", "a.example.net."},
+ {"a.b.example.com.", "example.com.", "example.net.", "a.b.example.net."},
+ {"ab.example.com.", "b.example.com.", "example.net.", ""},
+ {"foo.example.com.", "example.com.", "example.net.", "foo.example.net."},
+ {"a.x.example.com.", "x.example.com.", "example.net.", "a.example.net."},
+ {"a.example.com.", "example.com.", "y.example.net.", "a.y.example.net."},
+ {"cyc.example.com.", "example.com.", "example.com.", "cyc.example.com."},
+ {"cyc.example.com.", "example.com.", "c.example.com.", "cyc.c.example.com."},
+ {"shortloop.x.x.", "x.", ".", "shortloop.x."},
+ {"shortloop.x.", "x.", ".", "shortloop."},
+}
+
+func TestDNAMESubstitution(t *testing.T) {
+ for i, tc := range dnameSubstitutionTestCases {
+ result := substituteDNAME(tc.qname, tc.owner, tc.target)
+ if result != tc.expected {
+ if result == "" {
+ result = "<no match>"
+ }
+
+ t.Errorf("Case %d: Expected %s -> %s, got %v", i, tc.qname, tc.expected, result)
+ return
+ }
+ }
+}
+
+var dnameTestCases = []test.Case{
+ {
+ Qname: "dname.miek.nl.", Qtype: dns.TypeDNAME,
+ Answer: []dns.RR{
+ test.DNAME("dname.miek.nl. 1800 IN DNAME test.miek.nl."),
+ },
+ Ns: miekAuth,
+ },
+ {
+ Qname: "dname.miek.nl.", Qtype: dns.TypeA,
+ Answer: []dns.RR{
+ test.A("dname.miek.nl. 1800 IN A 127.0.0.1"),
+ },
+ Ns: miekAuth,
+ },
+ {
+ Qname: "dname.miek.nl.", Qtype: dns.TypeMX,
+ Answer: []dns.RR{},
+ Ns: []dns.RR{
+ test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"),
+ },
+ },
+ {
+ Qname: "a.dname.miek.nl.", Qtype: dns.TypeA,
+ Answer: []dns.RR{
+ test.CNAME("a.dname.miek.nl. 1800 IN CNAME a.test.miek.nl."),
+ test.A("a.test.miek.nl. 1800 IN A 139.162.196.78"),
+ test.DNAME("dname.miek.nl. 1800 IN DNAME test.miek.nl."),
+ },
+ Ns: miekAuth,
+ },
+ {
+ Qname: "www.dname.miek.nl.", Qtype: dns.TypeA,
+ Answer: []dns.RR{
+ test.A("a.test.miek.nl. 1800 IN A 139.162.196.78"),
+ test.DNAME("dname.miek.nl. 1800 IN DNAME test.miek.nl."),
+ test.CNAME("www.dname.miek.nl. 1800 IN CNAME www.test.miek.nl."),
+ test.CNAME("www.test.miek.nl. 1800 IN CNAME a.test.miek.nl."),
+ },
+ Ns: miekAuth,
+ },
+}
+
+func TestLookupDNAME(t *testing.T) {
+ zone, err := Parse(strings.NewReader(dbMiekNLDNAME), testzone, "stdin", 0)
+ if err != nil {
+ t.Fatalf("Expect no error when reading zone, got %q", err)
+ }
+
+ fm := File{Next: test.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{testzone: zone}, Names: []string{testzone}}}
+ ctx := context.TODO()
+
+ for _, tc := range dnameTestCases {
+ m := tc.Msg()
+
+ rec := dnsrecorder.New(&test.ResponseWriter{})
+ _, err := fm.ServeDNS(ctx, rec, m)
+ if err != nil {
+ t.Errorf("Expected no error, got %v\n", err)
+ return
+ }
+
+ resp := rec.Msg
+ test.SortAndCheck(t, resp, tc)
+ }
+}
+
+var dnameDnssecTestCases = []test.Case{
+ {
+ // We have no auth section, because the test zone does not have nameservers.
+ Qname: "ns.example.org.", Qtype: dns.TypeA,
+ Answer: []dns.RR{
+ test.A("ns.example.org. 1800 IN A 127.0.0.1"),
+ },
+ },
+ {
+ Qname: "dname.example.org.", Qtype: dns.TypeDNAME,
+ Do: true,
+ Answer: []dns.RR{
+ test.DNAME("dname.example.org. 1800 IN DNAME test.example.org."),
+ test.RRSIG("dname.example.org. 1800 IN RRSIG DNAME 5 3 1800 20170702091734 20170602091734 54282 example.org. HvXtiBM="),
+ },
+ Extra: []dns.RR{test.OPT(4096, true)},
+ },
+ {
+ Qname: "a.dname.example.org.", Qtype: dns.TypeA,
+ Do: true,
+ Answer: []dns.RR{
+ test.CNAME("a.dname.example.org. 1800 IN CNAME a.test.example.org."),
+ test.DNAME("dname.example.org. 1800 IN DNAME test.example.org."),
+ test.RRSIG("dname.example.org. 1800 IN RRSIG DNAME 5 3 1800 20170702091734 20170602091734 54282 example.org. HvXtiBM="),
+ },
+ Extra: []dns.RR{test.OPT(4096, true)},
+ },
+}
+
+func TestLookupDNAMEDNSSEC(t *testing.T) {
+ zone, err := Parse(strings.NewReader(dbExampleDNAMESigned), testzone, "stdin", 0)
+ if err != nil {
+ t.Fatalf("Expect no error when reading zone, got %q", err)
+ }
+
+ fm := File{Next: test.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{"example.org.": zone}, Names: []string{"example.org."}}}
+ ctx := context.TODO()
+
+ for _, tc := range dnameDnssecTestCases {
+ m := tc.Msg()
+
+ rec := dnsrecorder.New(&test.ResponseWriter{})
+ _, err := fm.ServeDNS(ctx, rec, m)
+ if err != nil {
+ t.Errorf("Expected no error, got %v\n", err)
+ return
+ }
+
+ resp := rec.Msg
+ test.SortAndCheck(t, resp, tc)
+ }
+}
+
+const dbMiekNLDNAME = `
+$TTL 30M
+$ORIGIN miek.nl.
+@ IN SOA linode.atoom.net. miek.miek.nl. (
+ 1282630057 ; Serial
+ 4H ; Refresh
+ 1H ; Retry
+ 7D ; Expire
+ 4H ) ; Negative Cache TTL
+ IN NS linode.atoom.net.
+ IN NS ns-ext.nlnetlabs.nl.
+ IN NS omval.tednet.nl.
+ IN NS ext.ns.whyscream.net.
+
+test IN MX 1 aspmx.l.google.com.
+ IN MX 5 alt1.aspmx.l.google.com.
+ IN MX 5 alt2.aspmx.l.google.com.
+ IN MX 10 aspmx2.googlemail.com.
+ IN MX 10 aspmx3.googlemail.com.
+a.test IN A 139.162.196.78
+ IN AAAA 2a01:7e00::f03c:91ff:fef1:6735
+www.test IN CNAME a.test
+
+dname IN DNAME test
+dname IN A 127.0.0.1
+a.dname IN A 127.0.0.1
+`
+
+const dbExampleDNAMESigned = `
+; File written on Fri Jun 2 10:17:34 2017
+; dnssec_signzone version 9.10.3-P4-Debian
+example.org. 1800 IN SOA a.example.org. b.example.org. (
+ 1282630057 ; serial
+ 14400 ; refresh (4 hours)
+ 3600 ; retry (1 hour)
+ 604800 ; expire (1 week)
+ 14400 ; minimum (4 hours)
+ )
+ 1800 RRSIG SOA 5 2 1800 (
+ 20170702091734 20170602091734 54282 example.org.
+ mr5eQtFs1GubgwaCcqrpiF6Cgi822OkESPeV
+ X0OJYq3JzthJjHw8TfYAJWQ2yGqhlePHir9h
+ FT/uFZdYyytHq+qgIUbJ9IVCrq0gZISZdHML
+ Ry1DNffMR9CpD77KocOAUABfopcvH/3UGOHn
+ TFxkAr447zPaaoC68JYGxYLfZk8= )
+ 1800 NS ns.example.org.
+ 1800 RRSIG NS 5 2 1800 (
+ 20170702091734 20170602091734 54282 example.org.
+ McM4UdMxkscVQkJnnEbdqwyjpPgq5a/EuOLA
+ r2MvG43/cwOaWULiZoNzLi5Rjzhf+GTeVTan
+ jw6EsL3gEuYI1nznwlLQ04/G0XAHjbq5VvJc
+ rlscBD+dzf774yfaTjRNoeo2xTem6S7nyYPW
+ Y+1f6xkrsQPLYJfZ6VZ9QqyupBw= )
+ 14400 NSEC dname.example.org. NS SOA RRSIG NSEC DNSKEY
+ 14400 RRSIG NSEC 5 2 14400 (
+ 20170702091734 20170602091734 54282 example.org.
+ VT+IbjDFajM0doMKFipdX3+UXfCn3iHIxg5x
+ LElp4Q/YddTbX+6tZf53+EO+G8Kye3JDLwEl
+ o8VceijNeF3igZ+LiZuXCei5Qg/TJ7IAUnAO
+ xd85IWwEYwyKkKd6Z2kXbAN2pdcHE8EmboQd
+ wfTr9oyWhpZk1Z+pN8vdejPrG0M= )
+ 1800 DNSKEY 256 3 5 (
+ AwEAAczLlmTk5bMXUzpBo/Jta6MWSZYy3Nfw
+ gz8t/pkfSh4IlFF6vyXZhEqCeQsCBdD7ltkD
+ h5qd4A+nFrYOMwsi5XIjoHMlJN15xwFS9EgS
+ ZrZmuxePIEiYB5KccEf9JQMgM1t07Iu1FnrY
+ 02OuAqGWcO4tuyTLaK3QP4MLQOfAgKqf
+ ) ; ZSK; alg = RSASHA1; key id = 54282
+ 1800 RRSIG DNSKEY 5 2 1800 (
+ 20170702091734 20170602091734 54282 example.org.
+ MBgSRtZ6idJblLIHxZWpWL/1oqIwImb1mkl7
+ hDFxqV6Hw19yLX06P7gcJEWiisdZBkVEfcOK
+ LeMJly05vgKfrMzLgIu2Ry4bL8AMKc8NMXBG
+ b1VDCEBW69P2omogj2KnORHDCZQr/BX9+wBU
+ 5rIMTTKlMSI5sT6ecJHHEymtiac= )
+dname.example.org. 1800 IN A 127.0.0.1
+ 1800 RRSIG A 5 3 1800 (
+ 20170702091734 20170602091734 54282 example.org.
+ LPCK2nLyDdGwvmzGLkUO2atEUjoc+aEspkC3
+ keZCdXZaLnAwBH7dNAjvvXzzy0WrgWeiyDb4
+ +rJ2N0oaKEZicM4QQDHKhugJblKbU5G4qTey
+ LSEaV3vvQnzGd0S6dCqnwfPj9czagFN7Zlf5
+ DmLtdxx0aiDPCUpqT0+H/vuGPfk= )
+ 1800 DNAME test.example.org.
+ 1800 RRSIG DNAME 5 3 1800 (
+ 20170702091734 20170602091734 54282 example.org.
+ HvX79T1flWJ8H9/1XZjX6gz8rP/o2jbfPXJ9
+ vC7ids/ZJilSReabLru4DCqcw1IV2DM/CZdE
+ tBnED/T2PJXvMut9tnYMrz+ZFPxoV6XyA3Z7
+ bok3B0OuxizzAN2EXdol04VdbMHoWUzjQCzi
+ 0Ri12zLGRPzDepZ7FolgD+JtiBM= )
+ 14400 NSEC a.dname.example.org. A DNAME RRSIG NSEC
+ 14400 RRSIG NSEC 5 3 14400 (
+ 20170702091734 20170602091734 54282 example.org.
+ U3ZPYMUBJl3wF2SazQv/kBf6ec0CH+7n0Hr9
+ w6lBKkiXz7P9WQzJDVnTHEZOrbDI6UetFGyC
+ 6qcaADCASZ9Wxc+riyK1Hl4ox+Y/CHJ97WHy
+ oS2X//vEf6qmbHQXin0WQtFdU/VCRYF40X5v
+ 8VfqOmrr8iKiEqXND8XNVf58mTw= )
+a.dname.example.org. 1800 IN A 127.0.0.1
+ 1800 RRSIG A 5 4 1800 (
+ 20170702091734 20170602091734 54282 example.org.
+ y7RHBWZwli8SJQ4BgTmdXmYS3KGHZ7AitJCx
+ zXFksMQtNoOfVEQBwnFqjAb8ezcV5u92h1gN
+ i1EcuxCFiElML1XFT8dK2GnlPAga9w3oIwd5
+ wzW/YHcnR0P9lF56Sl7RoIt6+jJqOdRfixS6
+ TDoLoXsNbOxQ+qV3B8pU2Tam204= )
+ 14400 NSEC ns.example.org. A RRSIG NSEC
+ 14400 RRSIG NSEC 5 4 14400 (
+ 20170702091734 20170602091734 54282 example.org.
+ Tmu27q3+xfONSZZtZLhejBUVtEw+83ZU1AFb
+ Rsxctjry/x5r2JSxw/sgSAExxX/7tx/okZ8J
+ oJqtChpsr91Kiw3eEBgINi2lCYIpMJlW4cWz
+ 8bYlHfR81VsKYgy/cRgrq1RRvBoJnw+nwSty
+ mKPIvUtt67LAvLxJheSCEMZLCKI= )
+ns.example.org. 1800 IN A 127.0.0.1
+ 1800 RRSIG A 5 3 1800 (
+ 20170702091734 20170602091734 54282 example.org.
+ mhi1SGaaAt+ndQEg5uKWKCH0HMzaqh/9dUK3
+ p2wWMBrLbTZrcWyz10zRnvehicXDCasbBrer
+ ZpDQnz5AgxYYBURvdPfUzx1XbNuRJRE4l5PN
+ CEUTlTWcqCXnlSoPKEJE5HRf7v0xg2BrBUfM
+ 4mZnW2bFLwjrRQ5mm/mAmHmTROk= )
+ 14400 NSEC example.org. A RRSIG NSEC
+ 14400 RRSIG NSEC 5 3 14400 (
+ 20170702091734 20170602091734 54282 example.org.
+ loHcdjX+NIWLAkUDfPSy2371wrfUvrBQTfMO
+ 17eO2Y9E/6PE935NF5bjQtZBRRghyxzrFJhm
+ vY1Ad5ZTb+NLHvdSWbJQJog+eCc7QWp64WzR
+ RXpMdvaE6ZDwalWldLjC3h8QDywDoFdndoRY
+ eHOsmTvvtWWqtO6Fa5A8gmHT5HA= )
+`
diff --git a/plugin/file/dnssec_test.go b/plugin/file/dnssec_test.go
new file mode 100644
index 000000000..17b122c7e
--- /dev/null
+++ b/plugin/file/dnssec_test.go
@@ -0,0 +1,358 @@
+package file
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+var dnssecTestCases = []test.Case{
+ {
+ Qname: "miek.nl.", Qtype: dns.TypeSOA, Do: true,
+ Answer: []dns.RR{
+ test.RRSIG("miek.nl. 1800 IN RRSIG SOA 8 2 1800 20160426031301 20160327031301 12051 miek.nl. FIrzy07acBbtyQczy1dc="),
+ test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"),
+ },
+ Ns: auth,
+ Extra: []dns.RR{test.OPT(4096, true)},
+ },
+ {
+ Qname: "miek.nl.", Qtype: dns.TypeAAAA, Do: true,
+ Answer: []dns.RR{
+ test.AAAA("miek.nl. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"),
+ test.RRSIG("miek.nl. 1800 IN RRSIG AAAA 8 2 1800 20160426031301 20160327031301 12051 miek.nl. SsRT="),
+ },
+ Ns: auth,
+ Extra: []dns.RR{test.OPT(4096, true)},
+ },
+ {
+ Qname: "miek.nl.", Qtype: dns.TypeNS, Do: true,
+ Answer: []dns.RR{
+ test.NS("miek.nl. 1800 IN NS ext.ns.whyscream.net."),
+ test.NS("miek.nl. 1800 IN NS linode.atoom.net."),
+ test.NS("miek.nl. 1800 IN NS ns-ext.nlnetlabs.nl."),
+ test.NS("miek.nl. 1800 IN NS omval.tednet.nl."),
+ test.RRSIG("miek.nl. 1800 IN RRSIG NS 8 2 1800 20160426031301 20160327031301 12051 miek.nl. ZLtsQhwaz+lHfNpztFoR1Vxs="),
+ },
+ Extra: []dns.RR{test.OPT(4096, true)},
+ },
+ {
+ Qname: "miek.nl.", Qtype: dns.TypeMX, Do: true,
+ Answer: []dns.RR{
+ test.MX("miek.nl. 1800 IN MX 1 aspmx.l.google.com."),
+ test.MX("miek.nl. 1800 IN MX 10 aspmx2.googlemail.com."),
+ test.MX("miek.nl. 1800 IN MX 10 aspmx3.googlemail.com."),
+ test.MX("miek.nl. 1800 IN MX 5 alt1.aspmx.l.google.com."),
+ test.MX("miek.nl. 1800 IN MX 5 alt2.aspmx.l.google.com."),
+ test.RRSIG("miek.nl. 1800 IN RRSIG MX 8 2 1800 20160426031301 20160327031301 12051 miek.nl. kLqG+iOr="),
+ },
+ Ns: auth,
+ Extra: []dns.RR{test.OPT(4096, true)},
+ },
+ {
+ Qname: "www.miek.nl.", Qtype: dns.TypeA, Do: true,
+ Answer: []dns.RR{
+ test.A("a.miek.nl. 1800 IN A 139.162.196.78"),
+ test.RRSIG("a.miek.nl. 1800 IN RRSIG A 8 3 1800 20160426031301 20160327031301 12051 miek.nl. lxLotCjWZ3kihTxk="),
+ test.CNAME("www.miek.nl. 1800 IN CNAME a.miek.nl."),
+ test.RRSIG("www.miek.nl. 1800 RRSIG CNAME 8 3 1800 20160426031301 20160327031301 12051 miek.nl. NVZmMJaypS+wDL2Lar4Zw1zF"),
+ },
+ Ns: auth,
+ Extra: []dns.RR{
+ test.OPT(4096, true),
+ },
+ },
+ {
+ // NoData
+ Qname: "a.miek.nl.", Qtype: dns.TypeSRV, Do: true,
+ Ns: []dns.RR{
+ test.NSEC("a.miek.nl. 14400 IN NSEC archive.miek.nl. A AAAA RRSIG NSEC"),
+ test.RRSIG("a.miek.nl. 14400 IN RRSIG NSEC 8 3 14400 20160426031301 20160327031301 12051 miek.nl. GqnF6cutipmSHEao="),
+ test.RRSIG("miek.nl. 1800 IN RRSIG SOA 8 2 1800 20160426031301 20160327031301 12051 miek.nl. FIrzy07acBbtyQczy1dc="),
+ test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"),
+ },
+ Extra: []dns.RR{test.OPT(4096, true)},
+ },
+ {
+ Qname: "b.miek.nl.", Qtype: dns.TypeA, Do: true,
+ Rcode: dns.RcodeNameError,
+ Ns: []dns.RR{
+ test.NSEC("archive.miek.nl. 14400 IN NSEC go.dns.miek.nl. CNAME RRSIG NSEC"),
+ test.RRSIG("archive.miek.nl. 14400 IN RRSIG NSEC 8 3 14400 20160426031301 20160327031301 12051 miek.nl. jEpx8lcp4do5fWXg="),
+ test.NSEC("miek.nl. 14400 IN NSEC a.miek.nl. A NS SOA MX AAAA RRSIG NSEC DNSKEY"),
+ test.RRSIG("miek.nl. 14400 IN RRSIG NSEC 8 2 14400 20160426031301 20160327031301 12051 miek.nl. mFfc3r/9PSC1H6oSpdC"),
+ test.RRSIG("miek.nl. 1800 IN RRSIG SOA 8 2 1800 20160426031301 20160327031301 12051 miek.nl. FIrzy07acBbtyQczy1dc="),
+ test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"),
+ },
+ Extra: []dns.RR{test.OPT(4096, true)},
+ },
+ {
+ Qname: "b.blaat.miek.nl.", Qtype: dns.TypeA, Do: true,
+ Rcode: dns.RcodeNameError,
+ Ns: []dns.RR{
+ test.NSEC("archive.miek.nl. 14400 IN NSEC go.dns.miek.nl. CNAME RRSIG NSEC"),
+ test.RRSIG("archive.miek.nl. 14400 IN RRSIG NSEC 8 3 14400 20160426031301 20160327031301 12051 miek.nl. jEpx8lcp4do5fWXg="),
+ test.NSEC("miek.nl. 14400 IN NSEC a.miek.nl. A NS SOA MX AAAA RRSIG NSEC DNSKEY"),
+ test.RRSIG("miek.nl. 14400 IN RRSIG NSEC 8 2 14400 20160426031301 20160327031301 12051 miek.nl. mFfc3r/9PSC1H6oSpdC"),
+ test.RRSIG("miek.nl. 1800 IN RRSIG SOA 8 2 1800 20160426031301 20160327031301 12051 miek.nl. FIrzy07acBbtyQczy1dc="),
+ test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"),
+ },
+ Extra: []dns.RR{test.OPT(4096, true)},
+ },
+ {
+ Qname: "b.a.miek.nl.", Qtype: dns.TypeA, Do: true,
+ Rcode: dns.RcodeNameError,
+ Ns: []dns.RR{
+ // dedupped NSEC, because 1 nsec tells all
+ test.NSEC("a.miek.nl. 14400 IN NSEC archive.miek.nl. A AAAA RRSIG NSEC"),
+ test.RRSIG("a.miek.nl. 14400 IN RRSIG NSEC 8 3 14400 20160426031301 20160327031301 12051 miek.nl. GqnF6cut/RRGPQ1QGQE1ipmSHEao="),
+ test.RRSIG("miek.nl. 1800 IN RRSIG SOA 8 2 1800 20160426031301 20160327031301 12051 miek.nl. FIrzy07acBbtyQczy1dc="),
+ test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"),
+ },
+ Extra: []dns.RR{test.OPT(4096, true)},
+ },
+}
+
+var auth = []dns.RR{
+ test.NS("miek.nl. 1800 IN NS ext.ns.whyscream.net."),
+ test.NS("miek.nl. 1800 IN NS linode.atoom.net."),
+ test.NS("miek.nl. 1800 IN NS ns-ext.nlnetlabs.nl."),
+ test.NS("miek.nl. 1800 IN NS omval.tednet.nl."),
+ test.RRSIG("miek.nl. 1800 IN RRSIG NS 8 2 1800 20160426031301 20160327031301 12051 miek.nl. ZLtsQhwazbqSpztFoR1Vxs="),
+}
+
+func TestLookupDNSSEC(t *testing.T) {
+ zone, err := Parse(strings.NewReader(dbMiekNLSigned), testzone, "stdin", 0)
+ if err != nil {
+ t.Fatalf("Expected no error when reading zone, got %q", err)
+ }
+
+ fm := File{Next: test.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{testzone: zone}, Names: []string{testzone}}}
+ ctx := context.TODO()
+
+ for _, tc := range dnssecTestCases {
+ m := tc.Msg()
+
+ rec := dnsrecorder.New(&test.ResponseWriter{})
+ _, err := fm.ServeDNS(ctx, rec, m)
+ if err != nil {
+ t.Errorf("Expected no error, got %v\n", err)
+ return
+ }
+
+ resp := rec.Msg
+ test.SortAndCheck(t, resp, tc)
+ }
+}
+
+func BenchmarkFileLookupDNSSEC(b *testing.B) {
+ zone, err := Parse(strings.NewReader(dbMiekNLSigned), testzone, "stdin", 0)
+ if err != nil {
+ return
+ }
+
+ fm := File{Next: test.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{testzone: zone}, Names: []string{testzone}}}
+ ctx := context.TODO()
+ rec := dnsrecorder.New(&test.ResponseWriter{})
+
+ tc := test.Case{
+ Qname: "b.miek.nl.", Qtype: dns.TypeA, Do: true,
+ Rcode: dns.RcodeNameError,
+ Ns: []dns.RR{
+ test.NSEC("archive.miek.nl. 14400 IN NSEC go.dns.miek.nl. CNAME RRSIG NSEC"),
+ test.RRSIG("archive.miek.nl. 14400 IN RRSIG NSEC 8 3 14400 20160426031301 20160327031301 12051 miek.nl. jEpx8lcp4do5fWXg="),
+ test.NSEC("miek.nl. 14400 IN NSEC a.miek.nl. A NS SOA MX AAAA RRSIG NSEC DNSKEY"),
+ test.RRSIG("miek.nl. 14400 IN RRSIG NSEC 8 2 14400 20160426031301 20160327031301 12051 miek.nl. mFfc3r/9PSC1H6oSpdC"),
+ test.RRSIG("miek.nl. 1800 IN RRSIG SOA 8 2 1800 20160426031301 20160327031301 12051 miek.nl. FIrzy07acBbtyQczy1dc="),
+ test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"),
+ },
+ }
+
+ m := tc.Msg()
+
+ b.ResetTimer()
+
+ for i := 0; i < b.N; i++ {
+ fm.ServeDNS(ctx, rec, m)
+ }
+}
+
+const dbMiekNLSigned = `
+; File written on Sun Mar 27 04:13:01 2016
+; dnssec_signzone version 9.10.3-P4-Ubuntu
+miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. (
+ 1459051981 ; serial
+ 14400 ; refresh (4 hours)
+ 3600 ; retry (1 hour)
+ 604800 ; expire (1 week)
+ 14400 ; minimum (4 hours)
+ )
+ 1800 RRSIG SOA 8 2 1800 (
+ 20160426031301 20160327031301 12051 miek.nl.
+ FIrzy07acBzrf6kNW13Ypmq/ahojoMqOj0qJ
+ ixTevTvwOEcVuw9GlJoYIHTYg+hm1sZHtx9K
+ RiVmYsm8SHKsJA1WzixtT4K7vQvM+T+qbeOJ
+ xA6YTivKUcGRWRXQlOTUAlHS/KqBEfmxKgRS
+ 68G4oOEClFDSJKh7RbtyQczy1dc= )
+ 1800 NS ext.ns.whyscream.net.
+ 1800 NS omval.tednet.nl.
+ 1800 NS linode.atoom.net.
+ 1800 NS ns-ext.nlnetlabs.nl.
+ 1800 RRSIG NS 8 2 1800 (
+ 20160426031301 20160327031301 12051 miek.nl.
+ ZLtsQhwaz+CwrgzgFiEAqbqS/JH65MYjziA3
+ 6EXwlGDy41lcfGm71PpxA7cDzFhWNkJNk4QF
+ q48wtpP4IGPPpHbnJHKDUXj6se7S+ylAGbS+
+ VgVJ4YaVcE6xA9ZVhVpz8CSSjeH34vmqq9xj
+ zmFjofuDvraZflHfNpztFoR1Vxs= )
+ 1800 A 139.162.196.78
+ 1800 RRSIG A 8 2 1800 (
+ 20160426031301 20160327031301 12051 miek.nl.
+ hl+6Q075tsCkxIqbop8zZ6U8rlFvooz7Izzx
+ MgCZYVLcg75El28EXKIhBfRb1dPaKbd+v+AD
+ wrJMHL131pY5sU2Ly05K+7CqmmyaXgDaVsKS
+ rSw/TbhGDIItBemeseeuXGAKAbY2+gE7kNN9
+ mZoQ9hRB3SrxE2jhctv66DzYYQQ= )
+ 1800 MX 1 aspmx.l.google.com.
+ 1800 MX 5 alt1.aspmx.l.google.com.
+ 1800 MX 5 alt2.aspmx.l.google.com.
+ 1800 MX 10 aspmx2.googlemail.com.
+ 1800 MX 10 aspmx3.googlemail.com.
+ 1800 RRSIG MX 8 2 1800 (
+ 20160426031301 20160327031301 12051 miek.nl.
+ kLqG+iOrKSzms1H9Et9me8Zts1rbyeCFSVQD
+ G9is/u6ec3Lqg2vwJddf/yRsjVpVgadWSAkc
+ GSDuD2dK8oBeP24axWc3Z1OY2gdMI7w+PKWT
+ Z+pjHVjbjM47Ii/a6jk5SYeOwpGMsdEwhtTP
+ vk2O2WGljifqV3uE7GshF5WNR10= )
+ 1800 AAAA 2a01:7e00::f03c:91ff:fef1:6735
+ 1800 RRSIG AAAA 8 2 1800 (
+ 20160426031301 20160327031301 12051 miek.nl.
+ SsRTHytW4YTAuHovHQgfIMhNwMtMp4gaAU/Z
+ lgTO+IkBb9y9F8uHrf25gG6RqA1bnGV/gezV
+ NU5negXm50bf1BNcyn3aCwEbA0rCGYIL+nLJ
+ szlBVbBu6me/Ym9bbJlfgfHRDfsVy2ZkNL+B
+ jfNQtGCSDoJwshjcqJlfIVSardo= )
+ 14400 NSEC a.miek.nl. A NS SOA MX AAAA RRSIG NSEC DNSKEY
+ 14400 RRSIG NSEC 8 2 14400 (
+ 20160426031301 20160327031301 12051 miek.nl.
+ mFfc3r/9PSC1H6oSpdC+FDy/Iu02W2Tf0x+b
+ n6Lpe1gCC1uvcSUrrmBNlyAWRr5Zm+ZXssEb
+ cKddRGiu/5sf0bUWrs4tqokL/HUl10X/sBxb
+ HfwNAeD7R7+CkpMv67li5AhsDgmQzpX2r3P6
+ /6oZyLvODGobysbmzeWM6ckE8IE= )
+ 1800 DNSKEY 256 3 8 (
+ AwEAAcNEU67LJI5GEgF9QLNqLO1SMq1EdoQ6
+ E9f85ha0k0ewQGCblyW2836GiVsm6k8Kr5EC
+ IoMJ6fZWf3CQSQ9ycWfTyOHfmI3eQ/1Covhb
+ 2y4bAmL/07PhrL7ozWBW3wBfM335Ft9xjtXH
+ Py7ztCbV9qZ4TVDTW/Iyg0PiwgoXVesz
+ ) ; ZSK; alg = RSASHA256; key id = 12051
+ 1800 DNSKEY 257 3 8 (
+ AwEAAcWdjBl4W4wh/hPxMDcBytmNCvEngIgB
+ 9Ut3C2+QI0oVz78/WK9KPoQF7B74JQ/mjO4f
+ vIncBmPp6mFNxs9/WQX0IXf7oKviEVOXLjct
+ R4D1KQLX0wprvtUIsQFIGdXaO6suTT5eDbSd
+ 6tTwu5xIkGkDmQhhH8OQydoEuCwV245ZwF/8
+ AIsqBYDNQtQ6zhd6jDC+uZJXg/9LuPOxFHbi
+ MTjp6j3CCW0kHbfM/YHZErWWtjPj3U3Z7knQ
+ SIm5PO5FRKBEYDdr5UxWJ/1/20SrzI3iztvP
+ wHDsA2rdHm/4YRzq7CvG4N0t9ac/T0a0Sxba
+ /BUX2UVPWaIVBdTRBtgHi0s=
+ ) ; KSK; alg = RSASHA256; key id = 33694
+ 1800 RRSIG DNSKEY 8 2 1800 (
+ 20160426031301 20160327031301 12051 miek.nl.
+ o/D6o8+/bNGQyyRvwZ2hM0BJ+3HirvNjZoko
+ yGhGe9sPSrYU39WF3JVIQvNJFK6W3/iwlKir
+ TPOeYlN6QilnztFq1vpCxwj2kxJaIJhZecig
+ LsKxY/fOHwZlIbBLZZadQG6JoGRLHnImSzpf
+ xtyVaXQtfnJFC07HHt9np3kICfE= )
+ 1800 RRSIG DNSKEY 8 2 1800 (
+ 20160426031301 20160327031301 33694 miek.nl.
+ Ak/mbbQVQV+nUgw5Sw/c+TSoYqIwbLARzuNE
+ QJvJNoRR4tKVOY6qSxQv+j5S7vzyORZ+yeDp
+ NlEa1T9kxZVBMABoOtLX5kRqZncgijuH8fxb
+ L57Sv2IzINI9+DOcy9Q9p9ygtwYzQKrYoNi1
+ 0hwHi6emGkVG2gGghruMinwOJASGgQy487Yd
+ eIpcEKJRw73nxd2le/4/Vafy+mBpKWOczfYi
+ 5m9MSSxcK56NFYjPG7TvdIw0m70F/smY9KBP
+ pGWEdzRQDlqfZ4fpDaTAFGyRX0mPFzMbs1DD
+ 3hQ4LHUSi/NgQakdH9eF42EVEDeL4cI69K98
+ 6NNk6X9TRslO694HKw== )
+a.miek.nl. 1800 IN A 139.162.196.78
+ 1800 RRSIG A 8 3 1800 (
+ 20160426031301 20160327031301 12051 miek.nl.
+ lxLotCjWZ3kikNNcePu6HOCqMHDINKFRJRD8
+ laz2KQ9DKtgXPdnRw5RJvVITSj8GUVzw1ec1
+ CYVEKu/eMw/rc953Zns528QBypGPeMNLe2vu
+ C6a6UhZnGHA48dSd9EX33eSJs0MP9xsC9csv
+ LGdzYmv++eslkKxkhSOk2j/hTxk= )
+ 1800 AAAA 2a01:7e00::f03c:91ff:fef1:6735
+ 1800 RRSIG AAAA 8 3 1800 (
+ 20160426031301 20160327031301 12051 miek.nl.
+ ji3QMlaUzlK85ppB5Pc+y2WnfqOi6qrm6dm1
+ bXgsEov/5UV1Lmcv8+Y5NBbTbBlXGlWcpqNp
+ uWpf9z3lbguDWznpnasN2MM8t7yxo/Cr7WRf
+ QCzui7ewpWiA5hq7j0kVbM4nnDc6cO+U93hO
+ mMhVbeVI70HM2m0HaHkziEyzVZk= )
+ 14400 NSEC archive.miek.nl. A AAAA RRSIG NSEC
+ 14400 RRSIG NSEC 8 3 14400 (
+ 20160426031301 20160327031301 12051 miek.nl.
+ GqnF6cut/KCxbnJj27MCjjVGkjObV0hLhHOP
+ E1/GXAUTEKG6BWxJq8hidS3p/yrOmP5PEL9T
+ 4FjBp0/REdVmGpuLaiHyMselES82p/uMMdY5
+ QqRM6LHhZdO1zsRbyzOZbm5MsW6GR7K2kHlX
+ 9TdBIULiRRGPQ1QGQE1ipmSHEao= )
+archive.miek.nl. 1800 IN CNAME a.miek.nl.
+ 1800 RRSIG CNAME 8 3 1800 (
+ 20160426031301 20160327031301 12051 miek.nl.
+ s4zVJiDrVuUiUFr8CNQLuXYYfpqpl8rovL50
+ BYsub/xK756NENiOTAOjYH6KYg7RSzsygJjV
+ YQwXolZly2/KXAr48SCtxzkGFxLexxiKcFaj
+ vm7ZDl7Btoa5l68qmBcxOX5E/W0IKITi4PNK
+ mhBs7dlaf0IbPGNgMxae72RosxM= )
+ 14400 NSEC go.dns.miek.nl. CNAME RRSIG NSEC
+ 14400 RRSIG NSEC 8 3 14400 (
+ 20160426031301 20160327031301 12051 miek.nl.
+ jEp7LsoK++/PRFh2HieLzasA1jXBpp90NyDf
+ RfpfOxdM69yRKfvXMc2bazIiMuDhxht79dGI
+ Gj02cn1cvX60SlaHkeFtqTdJcHdK9rbI65EK
+ YHFZFzGh9XVnuMJKpUsm/xS1dnUSAnXN8q+0
+ xBlUDlQpsAFv/cx8lcp4do5fWXg= )
+go.dns.miek.nl. 1800 IN TXT "Hello!"
+ 1800 RRSIG TXT 8 4 1800 (
+ 20160426031301 20160327031301 12051 miek.nl.
+ O0uo1NsXTq2TTfgOmGbHQQEchrcpllaDAMMX
+ dTDizw3t+vZ5SR32qJ8W7y6VXLgUqJgcdRxS
+ Fou1pp+t5juRZSQ0LKgxMpZAgHorkzPvRf1b
+ E9eBKrDSuLGagsQRwHeldFGFgsXtCbf07vVH
+ zoKR8ynuG4/cAoY0JzMhCts+56U= )
+ 14400 NSEC www.miek.nl. TXT RRSIG NSEC
+ 14400 RRSIG NSEC 8 4 14400 (
+ 20160426031301 20160327031301 12051 miek.nl.
+ BW6qo7kYe3Z+Y0ebaVTWTy1c3bpdf8WUEoXq
+ WDQxLDEj2fFiuEBDaSN5lTWRg3wj8kZmr6Uk
+ LvX0P29lbATFarIgkyiAdbOEdaf88nMfqBW8
+ z2T5xrPQcN0F13uehmv395yAJs4tebRxErMl
+ KdkVF0dskaDvw8Wo3YgjHUf6TXM= )
+www.miek.nl. 1800 IN CNAME a.miek.nl.
+ 1800 RRSIG CNAME 8 3 1800 (
+ 20160426031301 20160327031301 12051 miek.nl.
+ MiQQh2lScoNiNVZmMJaypS+wDL2Lar4Zw1zF
+ Uo4tL16BfQOt7yl8gXdAH2JMFqoKAoIdM2K6
+ XwFOwKTOGSW0oNCOcaE7ts+1Z1U0H3O2tHfq
+ FAzfg1s9pQ5zxk8J/bJgkVIkw2/cyB0y1/PK
+ EmIqvChBSb4NchTuMCSqo63LJM8= )
+ 14400 NSEC miek.nl. CNAME RRSIG NSEC
+ 14400 RRSIG NSEC 8 3 14400 (
+ 20160426031301 20160327031301 12051 miek.nl.
+ OPPZ8iaUPrVKEP4cqeCiiv1WLRAY30GRIhc/
+ me0gBwFkbmTEnvB+rUp831OJZDZBNKv4QdZj
+ Uyc26wKUOQeUyMJqv4IRDgxH7nq9GB5JRjYZ
+ IVxtGD1aqWLXz+8aMaf9ARJjtYUd3K4lt8Wz
+ LbJSo5Wdq7GOWqhgkY5n3XD0/FA= )`
diff --git a/plugin/file/dnssex_test.go b/plugin/file/dnssex_test.go
new file mode 100644
index 000000000..d9a0a4568
--- /dev/null
+++ b/plugin/file/dnssex_test.go
@@ -0,0 +1,145 @@
+package file
+
+const dbDnssexNLSigned = `
+; File written on Tue Mar 29 21:02:24 2016
+; dnssec_signzone version 9.10.3-P4-Ubuntu
+dnssex.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. (
+ 1459281744 ; serial
+ 14400 ; refresh (4 hours)
+ 3600 ; retry (1 hour)
+ 604800 ; expire (1 week)
+ 14400 ; minimum (4 hours)
+ )
+ 1800 RRSIG SOA 8 2 1800 (
+ 20160428190224 20160329190224 14460 dnssex.nl.
+ CA/Y3m9hCOiKC/8ieSOv8SeP964BUdG/8MC3
+ WtKljUosK9Z9bBGrVizDjjqgq++lyH8BZJcT
+ aabAsERs4xj5PRtcxicwQXZACX5VYjXHQeZm
+ CyytFU5wq2gcXSmvUH86zZzftx3RGPvn1aOo
+ TlcvoC3iF8fYUCpROlUS0YR8Cdw= )
+ 1800 NS omval.tednet.nl.
+ 1800 NS linode.atoom.net.
+ 1800 NS ns-ext.nlnetlabs.nl.
+ 1800 RRSIG NS 8 2 1800 (
+ 20160428190224 20160329190224 14460 dnssex.nl.
+ dLIeEvP86jj5nd3orv9bH7hTvkblF4Na0sbl
+ k6fJA6ha+FPN1d6Pig3NNEEVQ/+wlOp/JTs2
+ v07L7roEEUCbBprI8gMSld2gFDwNLW3DAB4M
+ WD/oayYdAnumekcLzhgvWixTABjWAGRTGQsP
+ sVDFXsGMf9TGGC9FEomgkCVeNC0= )
+ 1800 A 139.162.196.78
+ 1800 RRSIG A 8 2 1800 (
+ 20160428190224 20160329190224 14460 dnssex.nl.
+ LKJKLzPiSEDWOLAag2YpfD5EJCuDcEAJu+FZ
+ Xy+4VyOv9YvRHCTL4vbrevOo5+XymY2RxU1q
+ j+6leR/Fe7nlreSj2wzAAk2bIYn4m6r7hqeO
+ aKZsUFfpX8cNcFtGEywfHndCPELbRxFeEziP
+ utqHFLPNMX5nYCpS28w4oJ5sAnM= )
+ 1800 TXT "Doing It Safe Is Better"
+ 1800 RRSIG TXT 8 2 1800 (
+ 20160428190224 20160329190224 14460 dnssex.nl.
+ f6S+DUfJK1UYdOb3AHgUXzFTTtu+yLp/Fv7S
+ Hv0CAGhXAVw+nBbK719igFvBtObS33WKwzxD
+ 1pQNMaJcS6zeevtD+4PKB1KDC4fyJffeEZT6
+ E30jGR8Y29/xA+Fa4lqDNnj9zP3b8TiABCle
+ ascY5abkgWCALLocFAzFJQ/27YQ= )
+ 1800 AAAA 2a01:7e00::f03c:91ff:fef1:6735
+ 1800 RRSIG AAAA 8 2 1800 (
+ 20160428190224 20160329190224 14460 dnssex.nl.
+ PWcPSawEUBAfCuv0liEOQ8RYe7tfNW4rubIJ
+ LE+dbrub1DUer3cWrDoCYFtOufvcbkYJQ2CQ
+ AGjJmAQ5J2aqYDOPMrKa615V0KT3ifbZJcGC
+ gkIic4U/EXjaQpRoLdDzR9MyVXOmbA6sKYzj
+ ju1cNkLqM8D7Uunjl4pIr6rdSFo= )
+ 14400 NSEC *.dnssex.nl. A NS SOA TXT AAAA RRSIG NSEC DNSKEY
+ 14400 RRSIG NSEC 8 2 14400 (
+ 20160428190224 20160329190224 14460 dnssex.nl.
+ oIvM6JZIlNc1aNKGTxv58ApSnDr1nDPPgnD9
+ 9oJZRIn7eb5WnpeDz2H3z5+x6Bhlp5hJJaUp
+ KJ3Ss6Jg/IDnrmIvKmgq6L6gHj1Y1IiHmmU8
+ VeZTRzdTsDx/27OsN23roIvsytjveNSEMfIm
+ iLZ23x5kg1kBdJ9p3xjYHm5lR+8= )
+ 1800 DNSKEY 256 3 8 (
+ AwEAAazSO6uvLPEVknDA8yxjFe8nnAMU7txp
+ wb19k55hQ81WV3G4bpBM1NdN6sbYHrkXaTNx
+ 2bQWAkvX6pz0XFx3z/MPhW+vkakIWFYpyQ7R
+ AT5LIJfToVfiCDiyhhF0zVobKBInO9eoGjd9
+ BAW3TUt+LmNAO/Ak5D5BX7R3CuA7v9k7
+ ) ; ZSK; alg = RSASHA256; key id = 14460
+ 1800 DNSKEY 257 3 8 (
+ AwEAAbyeaV9zg0IqdtgYoqK5jJ239anzwG2i
+ gvH1DxSazLyaoNvEkCIvPgMLW/JWfy7Z1mQp
+ SMy9DtzL5pzRyQgw7kIeXLbi6jufUFd9pxN+
+ xnzKLf9mY5AcnGToTrbSL+jnMT67wG+c34+Q
+ PeVfucHNUePBxsbz2+4xbXiViSQyCQGv
+ ) ; KSK; alg = RSASHA256; key id = 18772
+ 1800 RRSIG DNSKEY 8 2 1800 (
+ 20160428190224 20160329190224 14460 dnssex.nl.
+ cFSFtJE+DBGNxb52AweFaVHBe5Ue5MDpqNdC
+ TIneUnEhP2m+vK4zJ/TraK0WdQFpsX63pod8
+ PZ9y03vHUfewivyonCCBD3DcNdoU9subhN22
+ tez9Ct8Z5/9E4RAz7orXal4M1VUEhRcXSEH8
+ SJW20mfVsqJAiKqqNeGB/pAj23I= )
+ 1800 RRSIG DNSKEY 8 2 1800 (
+ 20160428190224 20160329190224 18772 dnssex.nl.
+ oiiwo/7NYacePqohEp50261elhm6Dieh4j2S
+ VZGAHU5gqLIQeW9CxKJKtSCkBVgUo4cvO4Rn
+ 2tzArAuclDvBrMXRIoct8u7f96moeFE+x5FI
+ DYqICiV6k449ljj9o4t/5G7q2CRsEfxZKpTI
+ A/L0+uDk0RwVVzL45+TnilcsmZs= )
+*.dnssex.nl. 1800 IN TXT "Doing It Safe Is Better"
+ 1800 RRSIG TXT 8 2 1800 (
+ 20160428190224 20160329190224 14460 dnssex.nl.
+ FUZSTyvZfeuuOpCmNzVKOfITRHJ6/ygjmnnb
+ XGBxVUyQjoLuYXwD5XqZWGw4iKH6QeSDfGCx
+ 4MPqA4qQmW7Wwth7mat9yMfA4+p2sO84bysl
+ 7/BG9+W2G+q1uQiM9bX9V42P2X/XuW5Y/t9Y
+ 8u1sljQ7D8WwS6naH/vbaJxnDBw= )
+ 14400 NSEC a.dnssex.nl. TXT RRSIG NSEC
+ 14400 RRSIG NSEC 8 2 14400 (
+ 20160428190224 20160329190224 14460 dnssex.nl.
+ os6INm6q2eXknD5z8TpfbK00uxVbQefMvHcR
+ /RNX/kh0xXvzAaaDOV+Ge/Ko+2dXnKP+J1LY
+ G9ffXNpdbaQy5ygzH5F041GJst4566GdG/jt
+ 7Z7vLHYxEBTpZfxo+PLsXQXH3VTemZyuWyDf
+ qJzafXJVH1F0nDrcXmMlR6jlBHA= )
+www.dnssex.nl. 1800 IN CNAME a.dnssex.nl.
+ 1800 RRSIG CNAME 8 3 1800 (
+ 20160428190224 20160329190224 14460 dnssex.nl.
+ Omv42q/uVvdNsWQoSrQ6m6w6U7r7Abga7uF4
+ 25b3gZlse0C+WyMyGFMGUbapQm7azvBpreeo
+ uKJHjzd+ufoG+Oul6vU9vyoj+ejgHzGLGbJQ
+ HftfP+UqP5SWvAaipP/LULTWKPuiBcLDLiBI
+ PGTfsq0DB6R+qCDTV0fNnkgxEBQ= )
+ 14400 NSEC dnssex.nl. CNAME RRSIG NSEC
+ 14400 RRSIG NSEC 8 3 14400 (
+ 20160428190224 20160329190224 14460 dnssex.nl.
+ TBN3ddfZW+kC84/g3QlNNJMeLZoyCalPQylt
+ KXXLPGuxfGpl3RYRY8KaHbP+5a8MnHjqjuMB
+ Lofb7yKMFxpSzMh8E36vnOqry1mvkSakNj9y
+ 9jM8PwDjcpYUwn/ql76MsmNgEV5CLeQ7lyH4
+ AOrL79yOSQVI3JHJIjKSiz88iSw= )
+a.dnssex.nl. 1800 IN A 139.162.196.78
+ 1800 RRSIG A 8 3 1800 (
+ 20160428190224 20160329190224 14460 dnssex.nl.
+ OXHpFj9nSpKi5yA/ULH7MOpGAWfyJ2yC/2xa
+ Pw0fqSY4QvcRt+V3adcFA4H9+P1b32GpxEjB
+ lXmCJID+H4lYkhUR4r4IOZBVtKG2SJEBZXip
+ pH00UkOIBiXxbGzfX8VL04v2G/YxUgLW57kA
+ aknaeTOkJsO20Y+8wmR9EtzaRFI= )
+ 1800 AAAA 2a01:7e00::f03c:91ff:fef1:6735
+ 1800 RRSIG AAAA 8 3 1800 (
+ 20160428190224 20160329190224 14460 dnssex.nl.
+ jrepc/VnRzJypnrG0WDEqaAr3HMjWrPxJNX0
+ 86gbFjZG07QxBmrA1rj0jM9YEWTjjyWb2tT7
+ lQhzKDYX/0XdOVUeeOM4FoSks80V+pWR8fvj
+ AZ5HmX69g36tLosMDKNR4lXcrpv89QovG4Hr
+ /r58fxEKEFJqrLDjMo6aOrg+uKA= )
+ 14400 NSEC www.dnssex.nl. A AAAA RRSIG NSEC
+ 14400 RRSIG NSEC 8 3 14400 (
+ 20160428190224 20160329190224 14460 dnssex.nl.
+ S+UM62wXRNNFN3QDWK5YFWUbHBXC4aqaqinZ
+ A2ZDeC+IQgyw7vazPz7cLI5T0YXXks0HTMlr
+ soEjKnnRZsqSO9EuUavPNE1hh11Jjm0fB+5+
+ +Uro0EmA5Dhgc0Z2VpbXVQEhNDf/pI1gem15
+ RffN2tBYNykZn4Has2ySgRaaRYQ= )`
diff --git a/plugin/file/ds_test.go b/plugin/file/ds_test.go
new file mode 100644
index 000000000..e1087a81d
--- /dev/null
+++ b/plugin/file/ds_test.go
@@ -0,0 +1,75 @@
+package file
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+var dsTestCases = []test.Case{
+ {
+ Qname: "a.delegated.miek.nl.", Qtype: dns.TypeDS,
+ Ns: []dns.RR{
+ test.NS("delegated.miek.nl. 1800 IN NS a.delegated.miek.nl."),
+ test.NS("delegated.miek.nl. 1800 IN NS ns-ext.nlnetlabs.nl."),
+ },
+ Extra: []dns.RR{
+ test.A("a.delegated.miek.nl. 1800 IN A 139.162.196.78"),
+ test.AAAA("a.delegated.miek.nl. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"),
+ },
+ },
+ {
+ Qname: "_udp.delegated.miek.nl.", Qtype: dns.TypeDS,
+ Ns: []dns.RR{
+ test.NS("delegated.miek.nl. 1800 IN NS a.delegated.miek.nl."),
+ test.NS("delegated.miek.nl. 1800 IN NS ns-ext.nlnetlabs.nl."),
+ },
+ Extra: []dns.RR{
+ test.A("a.delegated.miek.nl. 1800 IN A 139.162.196.78"),
+ test.AAAA("a.delegated.miek.nl. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"),
+ },
+ },
+ {
+ // This works *here* because we skip the server routing for DS in core/dnsserver/server.go
+ Qname: "_udp.miek.nl.", Qtype: dns.TypeDS,
+ Rcode: dns.RcodeNameError,
+ Ns: []dns.RR{
+ test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"),
+ },
+ },
+ {
+ Qname: "miek.nl.", Qtype: dns.TypeDS,
+ Ns: []dns.RR{
+ test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"),
+ },
+ },
+}
+
+func TestLookupDS(t *testing.T) {
+ zone, err := Parse(strings.NewReader(dbMiekNLDelegation), testzone, "stdin", 0)
+ if err != nil {
+ t.Fatalf("Expected no error when reading zone, got %q", err)
+ }
+
+ fm := File{Next: test.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{testzone: zone}, Names: []string{testzone}}}
+ ctx := context.TODO()
+
+ for _, tc := range dsTestCases {
+ m := tc.Msg()
+
+ rec := dnsrecorder.New(&test.ResponseWriter{})
+ _, err := fm.ServeDNS(ctx, rec, m)
+ if err != nil {
+ t.Errorf("Expected no error, got %v\n", err)
+ return
+ }
+
+ resp := rec.Msg
+ test.SortAndCheck(t, resp, tc)
+ }
+}
diff --git a/plugin/file/ent_test.go b/plugin/file/ent_test.go
new file mode 100644
index 000000000..6f4f1db6c
--- /dev/null
+++ b/plugin/file/ent_test.go
@@ -0,0 +1,159 @@
+package file
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+var entTestCases = []test.Case{
+ {
+ Qname: "b.c.miek.nl.", Qtype: dns.TypeA,
+ Ns: []dns.RR{
+ test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"),
+ },
+ },
+ {
+ Qname: "b.c.miek.nl.", Qtype: dns.TypeA, Do: true,
+ Ns: []dns.RR{
+ test.NSEC("a.miek.nl. 14400 IN NSEC a.b.c.miek.nl. A RRSIG NSEC"),
+ test.RRSIG("a.miek.nl. 14400 IN RRSIG NSEC 8 3 14400 20160502144311 20160402144311 12051 miek.nl. d5XZEy6SUpq98ZKUlzqhAfkLI9pQPc="),
+ test.RRSIG("miek.nl. 1800 IN RRSIG SOA 8 2 1800 20160502144311 20160402144311 12051 miek.nl. KegoBxA3Tbrhlc4cEdkRiteIkOfsq"),
+ test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"),
+ },
+ Extra: []dns.RR{test.OPT(4096, true)},
+ },
+}
+
+func TestLookupEnt(t *testing.T) {
+ zone, err := Parse(strings.NewReader(dbMiekENTNL), testzone, "stdin", 0)
+ if err != nil {
+ t.Fatalf("expect no error when reading zone, got %q", err)
+ }
+
+ fm := File{Next: test.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{testzone: zone}, Names: []string{testzone}}}
+ ctx := context.TODO()
+
+ for _, tc := range entTestCases {
+ m := tc.Msg()
+
+ rec := dnsrecorder.New(&test.ResponseWriter{})
+ _, err := fm.ServeDNS(ctx, rec, m)
+ if err != nil {
+ t.Errorf("expected no error, got %v\n", err)
+ return
+ }
+
+ resp := rec.Msg
+ test.SortAndCheck(t, resp, tc)
+ }
+}
+
+// fdjfdjkf
+const dbMiekENTNL = `; File written on Sat Apr 2 16:43:11 2016
+; dnssec_signzone version 9.10.3-P4-Ubuntu
+miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. (
+ 1282630057 ; serial
+ 14400 ; refresh (4 hours)
+ 3600 ; retry (1 hour)
+ 604800 ; expire (1 week)
+ 14400 ; minimum (4 hours)
+ )
+ 1800 RRSIG SOA 8 2 1800 (
+ 20160502144311 20160402144311 12051 miek.nl.
+ KegoBxA3Tbrhlc4cEdkRiteIkOfsqD4oCLLM
+ ISJ5bChWy00LGHUlAnHVu5Ti96hUjVNmGSxa
+ xtGSuAAMFCr52W8pAB8LBIlu9B6QZUPHMccr
+ SuzxAX3ioawk2uTjm+k8AGPT4RoQdXemGLAp
+ zJTASolTVmeMTh5J0sZTZJrtvZ0= )
+ 1800 NS linode.atoom.net.
+ 1800 RRSIG NS 8 2 1800 (
+ 20160502144311 20160402144311 12051 miek.nl.
+ m0cOHL6Rre/0jZPXe+0IUjs/8AFASRCvDbSx
+ ZQsRDSlZgS6RoMP3OC77cnrKDVlfZ2Vhq3Ce
+ nYPoGe0/atB92XXsilmstx4HTSU64gsV9iLN
+ Xkzk36617t7zGOl/qumqfaUXeA9tihItzEim
+ 6SGnufVZI4o8xeyaVCNDDuN0bvY= )
+ 14400 NSEC a.miek.nl. NS SOA RRSIG NSEC DNSKEY
+ 14400 RRSIG NSEC 8 2 14400 (
+ 20160502144311 20160402144311 12051 miek.nl.
+ BCWVgwxWrs4tBjS9QXKkftCUbiLi40NyH1yA
+ nbFy1wCKQ2jDH00810+ia4b66QrjlAKgxE9z
+ 9U7MKSMV86sNkyAtlCi+2OnjtWF6sxPdJO7k
+ CHeg46XBjrQuiJRY8CneQX56+IEPdufLeqPR
+ l+ocBQ2UkGhXmQdWp3CFDn2/eqU= )
+ 1800 DNSKEY 256 3 8 (
+ AwEAAcNEU67LJI5GEgF9QLNqLO1SMq1EdoQ6
+ E9f85ha0k0ewQGCblyW2836GiVsm6k8Kr5EC
+ IoMJ6fZWf3CQSQ9ycWfTyOHfmI3eQ/1Covhb
+ 2y4bAmL/07PhrL7ozWBW3wBfM335Ft9xjtXH
+ Py7ztCbV9qZ4TVDTW/Iyg0PiwgoXVesz
+ ) ; ZSK; alg = RSASHA256; key id = 12051
+ 1800 DNSKEY 257 3 8 (
+ AwEAAcWdjBl4W4wh/hPxMDcBytmNCvEngIgB
+ 9Ut3C2+QI0oVz78/WK9KPoQF7B74JQ/mjO4f
+ vIncBmPp6mFNxs9/WQX0IXf7oKviEVOXLjct
+ R4D1KQLX0wprvtUIsQFIGdXaO6suTT5eDbSd
+ 6tTwu5xIkGkDmQhhH8OQydoEuCwV245ZwF/8
+ AIsqBYDNQtQ6zhd6jDC+uZJXg/9LuPOxFHbi
+ MTjp6j3CCW0kHbfM/YHZErWWtjPj3U3Z7knQ
+ SIm5PO5FRKBEYDdr5UxWJ/1/20SrzI3iztvP
+ wHDsA2rdHm/4YRzq7CvG4N0t9ac/T0a0Sxba
+ /BUX2UVPWaIVBdTRBtgHi0s=
+ ) ; KSK; alg = RSASHA256; key id = 33694
+ 1800 RRSIG DNSKEY 8 2 1800 (
+ 20160502144311 20160402144311 12051 miek.nl.
+ YNpi1jRDQKpnsQEjIjxqy+kJGaYnV16e8Iug
+ 40c82y4pee7kIojFUllSKP44qiJpCArxF557
+ tfjfwBd6c4hkqCScGPZXJ06LMyG4u//rhVMh
+ 4hyKcxzQFKxmrFlj3oQGksCI8lxGX6RxiZuR
+ qv2ol2lUWrqetpAL+Zzwt71884E= )
+ 1800 RRSIG DNSKEY 8 2 1800 (
+ 20160502144311 20160402144311 33694 miek.nl.
+ jKpLDEeyadgM0wDgzEk6sBBdWr2/aCrkAOU/
+ w6dYIafN98f21oIYQfscV1gc7CTsA0vwzzUu
+ x0QgwxoNLMvSxxjOiW/2MzF8eozczImeCWbl
+ ad/pVCYH6Jn5UBrZ5RCWMVcs2RP5KDXWeXKs
+ jEN/0EmQg5qNd4zqtlPIQinA9I1HquJAnS56
+ pFvYyGIbZmGEbhR18sXVBeTWYr+zOMHn2quX
+ 0kkrx2udz+sPg7i4yRsLdhw138gPRy1qvbaC
+ 8ELs1xo1mC9pTlDOhz24Q3iXpVAU1lXLYOh9
+ nUP1/4UvZEYXHBUQk/XPRciojniWjAF825x3
+ QoSivMHblBwRdAKJSg== )
+a.miek.nl. 1800 IN A 127.0.0.1
+ 1800 RRSIG A 8 3 1800 (
+ 20160502144311 20160402144311 12051 miek.nl.
+ lUOYdSxScjyYz+Ebc+nb6iTNgCohqj7K+Dat
+ 97KE7haV2nP3LxdYuDCJYZpeyhsXDLHd4bFI
+ bInYPwJiC6DUCxPCuCWy0KYlZOWW8KCLX3Ia
+ BOPQbvIwLsJhnX+/tyMD9mXortoqATO79/6p
+ nNxvFeM8pFDwaih17fXMuFR/BsI= )
+ 14400 NSEC a.b.c.miek.nl. A RRSIG NSEC
+ 14400 RRSIG NSEC 8 3 14400 (
+ 20160502144311 20160402144311 12051 miek.nl.
+ d5XZEy6SUp+TPRJQED+0R65zf2Yeo/1dlEA2
+ jYYvkXGSHXke4sg9nH8U3nr1rLcuqA1DsQgH
+ uMIjdENvXuZ+WCSwvIbhC+JEI6AyQ6Gfaf/D
+ I3mfu60C730IRByTrKM5C2rt11lwRQlbdaUY
+ h23/nn/q98ZKUlzqhAfkLI9pQPc= )
+a.b.c.miek.nl. 1800 IN A 127.0.0.1
+ 1800 RRSIG A 8 5 1800 (
+ 20160502144311 20160402144311 12051 miek.nl.
+ FwgU5+fFD4hEebco3gvKQt3PXfY+dcOJr8dl
+ Ky4WLsONIdhP+4e9oprPisSLxImErY21BcrW
+ xzu1IZrYDsS8XBVV44lBx5WXEKvAOrUcut/S
+ OWhFZW7ncdIQCp32ZBIatiLRJEqXUjx+guHs
+ noFLiHix35wJWsRKwjGLIhH1fbs= )
+ 14400 NSEC miek.nl. A RRSIG NSEC
+ 14400 RRSIG NSEC 8 5 14400 (
+ 20160502144311 20160402144311 12051 miek.nl.
+ lXgOqm9/jRRYvaG5jC1CDvTtGYxMroTzf4t4
+ jeYGb60+qI0q9sHQKfAJvoQ5o8o1qfR7OuiF
+ f544ipYT9eTcJRyGAOoJ37yMie7ZIoVJ91tB
+ r8YdzZ9Q6x3v1cbwTaQiacwhPZhGYOw63qIs
+ q5IQErIPos2sNk+y9D8BEce2DO4= )`
diff --git a/plugin/file/example_org.go b/plugin/file/example_org.go
new file mode 100644
index 000000000..eba18e0e4
--- /dev/null
+++ b/plugin/file/example_org.go
@@ -0,0 +1,113 @@
+package file
+
+// exampleOrgSigned is a fake signed example.org zone with two delegations,
+// one signed (with DSs) and one "normal".
+const exampleOrgSigned = `
+example.org. 1800 IN SOA a.iana-servers.net. devnull.example.org. (
+ 1282630057 ; serial
+ 14400 ; refresh (4 hours)
+ 3600 ; retry (1 hour)
+ 604800 ; expire (1 week)
+ 14400 ; minimum (4 hours)
+ )
+ 1800 RRSIG SOA 13 2 1800 (
+ 20161129153240 20161030153240 49035 example.org.
+ GVnMpFmN+6PDdgCtlYDEYBsnBNDgYmEJNvos
+ Bk9+PNTPNWNst+BXCpDadTeqRwrr1RHEAQ7j
+ YWzNwqn81pN+IA== )
+ 1800 NS a.iana-servers.net.
+ 1800 NS b.iana-servers.net.
+ 1800 RRSIG NS 13 2 1800 (
+ 20161129153240 20161030153240 49035 example.org.
+ llrHoIuwjnbo28LOt4p5zWAs98XGqrXicKVI
+ Qxyaf/ORM8boJvW2XrKr3nj6Y8FKMhzd287D
+ 5PBzVCL6MZyjQg== )
+ 14400 NSEC a.example.org. NS SOA RRSIG NSEC DNSKEY
+ 14400 RRSIG NSEC 13 2 14400 (
+ 20161129153240 20161030153240 49035 example.org.
+ BQROf1swrmYi3GqpP5M/h5vTB8jmJ/RFnlaX
+ 7fjxvV7aMvXCsr3ekWeB2S7L6wWFihDYcKJg
+ 9BxVPqxzBKeaqg== )
+ 1800 DNSKEY 256 3 13 (
+ UNTqlHbC51EbXuY0rshW19Iz8SkCuGVS+L0e
+ bQj53dvtNlaKfWmtTauC797FoyVLbQwoMy/P
+ G68SXgLCx8g+9g==
+ ) ; ZSK; alg = ECDSAP256SHA256; key id = 49035
+ 1800 RRSIG DNSKEY 13 2 1800 (
+ 20161129153240 20161030153240 49035 example.org.
+ LnLHyqYJaCMOt7EHB4GZxzAzWLwEGCTFiEhC
+ jj1X1VuQSjJcN42Zd3yF+jihSW6huknrig0Z
+ Mqv0FM6mJ/qPKg== )
+a.delegated.example.org. 1800 IN A 139.162.196.78
+ 1800 TXT "obscured"
+ 1800 AAAA 2a01:7e00::f03c:91ff:fef1:6735
+archive.example.org. 1800 IN CNAME a.example.org.
+ 1800 RRSIG CNAME 13 3 1800 (
+ 20161129153240 20161030153240 49035 example.org.
+ SDFW1z/PN9knzH8BwBvmWK0qdIwMVtGrMgRw
+ 7lgy4utRrdrRdCSLZy3xpkmkh1wehuGc4R0S
+ 05Z3DPhB0Fg5BA== )
+ 14400 NSEC delegated.example.org. CNAME RRSIG NSEC
+ 14400 RRSIG NSEC 13 3 14400 (
+ 20161129153240 20161030153240 49035 example.org.
+ DQqLSVNl8F6v1K09wRU6/M6hbHy2VUddnOwn
+ JusJjMlrAOmoOctCZ/N/BwqCXXBA+d9yFGdH
+ knYumXp+BVPBAQ== )
+www.example.org. 1800 IN CNAME a.example.org.
+ 1800 RRSIG CNAME 13 3 1800 (
+ 20161129153240 20161030153240 49035 example.org.
+ adzujOxCV0uBV4OayPGfR11iWBLiiSAnZB1R
+ slmhBFaDKOKSNYijGtiVPeaF+EuZs63pzd4y
+ 6Nm2Iq9cQhAwAA== )
+ 14400 NSEC example.org. CNAME RRSIG NSEC
+ 14400 RRSIG NSEC 13 3 14400 (
+ 20161129153240 20161030153240 49035 example.org.
+ jy3f96GZGBaRuQQjuqsoP1YN8ObZF37o+WkV
+ PL7TruzI7iNl0AjrUDy9FplP8Mqk/HWyvlPe
+ N3cU+W8NYlfDDQ== )
+a.example.org. 1800 IN A 139.162.196.78
+ 1800 RRSIG A 13 3 1800 (
+ 20161129153240 20161030153240 49035 example.org.
+ 41jFz0Dr8tZBN4Kv25S5dD4vTmviFiLx7xSA
+ qMIuLFm0qibKL07perKpxqgLqM0H1wreT4xz
+ I9Y4Dgp1nsOuMA== )
+ 1800 AAAA 2a01:7e00::f03c:91ff:fef1:6735
+ 1800 RRSIG AAAA 13 3 1800 (
+ 20161129153240 20161030153240 49035 example.org.
+ brHizDxYCxCHrSKIu+J+XQbodRcb7KNRdN4q
+ VOWw8wHqeBsFNRzvFF6jwPQYphGP7kZh1KAb
+ VuY5ZVVhM2kHjw== )
+ 14400 NSEC archive.example.org. A AAAA RRSIG NSEC
+ 14400 RRSIG NSEC 13 3 14400 (
+ 20161129153240 20161030153240 49035 example.org.
+ zIenVlg5ScLr157EWigrTGUgrv7W/1s49Fic
+ i2k+OVjZfT50zw+q5X6DPKkzfAiUhIuqs53r
+ hZUzZwV/1Wew9Q== )
+delegated.example.org. 1800 IN NS a.delegated.example.org.
+ 1800 IN NS ns-ext.nlnetlabs.nl.
+ 1800 DS 10056 5 1 (
+ EE72CABD1927759CDDA92A10DBF431504B9E
+ 1F13 )
+ 1800 DS 10056 5 2 (
+ E4B05F87725FA86D9A64F1E53C3D0E625094
+ 6599DFE639C45955B0ED416CDDFA )
+ 1800 RRSIG DS 13 3 1800 (
+ 20161129153240 20161030153240 49035 example.org.
+ rlNNzcUmtbjLSl02ZzQGUbWX75yCUx0Mug1j
+ HtKVqRq1hpPE2S3863tIWSlz+W9wz4o19OI4
+ jbznKKqk+DGKog== )
+ 14400 NSEC sub.example.org. NS DS RRSIG NSEC
+ 14400 RRSIG NSEC 13 3 14400 (
+ 20161129153240 20161030153240 49035 example.org.
+ lNQ5kRTB26yvZU5bFn84LYFCjwWTmBcRCDbD
+ cqWZvCSw4LFOcqbz1/wJKIRjIXIqnWIrfIHe
+ fZ9QD5xZsrPgUQ== )
+sub.example.org. 1800 IN NS sub1.example.net.
+ 1800 IN NS sub2.example.net.
+ 14400 NSEC www.example.org. NS RRSIG NSEC
+ 14400 RRSIG NSEC 13 3 14400 (
+ 20161129153240 20161030153240 49035 example.org.
+ VYjahdV+TTkA3RBdnUI0hwXDm6U5k/weeZZr
+ ix1znORpOELbeLBMJW56cnaG+LGwOQfw9qqj
+ bOuULDst84s4+g== )
+`
diff --git a/plugin/file/file.go b/plugin/file/file.go
new file mode 100644
index 000000000..89c2df90a
--- /dev/null
+++ b/plugin/file/file.go
@@ -0,0 +1,138 @@
+// Package file implements a file backend.
+package file
+
+import (
+ "fmt"
+ "io"
+ "log"
+
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+type (
+ // File is the plugin that reads zone data from disk.
+ File struct {
+ Next plugin.Handler
+ Zones Zones
+ }
+
+ // Zones maps zone names to a *Zone.
+ Zones struct {
+ Z map[string]*Zone // A map mapping zone (origin) to the Zone's data
+ Names []string // All the keys from the map Z as a string slice.
+ }
+)
+
+// ServeDNS implements the plugin.Handle interface.
+func (f File) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
+ state := request.Request{W: w, Req: r}
+
+ qname := state.Name()
+ // TODO(miek): match the qname better in the map
+ zone := plugin.Zones(f.Zones.Names).Matches(qname)
+ if zone == "" {
+ return plugin.NextOrFailure(f.Name(), f.Next, ctx, w, r)
+ }
+
+ z, ok := f.Zones.Z[zone]
+ if !ok || z == nil {
+ return dns.RcodeServerFailure, nil
+ }
+
+ // This is only for when we are a secondary zones.
+ if r.Opcode == dns.OpcodeNotify {
+ if z.isNotify(state) {
+ m := new(dns.Msg)
+ m.SetReply(r)
+ m.Authoritative, m.RecursionAvailable, m.Compress = true, true, true
+ state.SizeAndDo(m)
+ w.WriteMsg(m)
+
+ log.Printf("[INFO] Notify from %s for %s: checking transfer", state.IP(), zone)
+ ok, err := z.shouldTransfer()
+ if ok {
+ z.TransferIn()
+ } else {
+ log.Printf("[INFO] Notify from %s for %s: no serial increase seen", state.IP(), zone)
+ }
+ if err != nil {
+ log.Printf("[WARNING] Notify from %s for %s: failed primary check: %s", state.IP(), zone, err)
+ }
+ return dns.RcodeSuccess, nil
+ }
+ log.Printf("[INFO] Dropping notify from %s for %s", state.IP(), zone)
+ return dns.RcodeSuccess, nil
+ }
+
+ if z.Expired != nil && *z.Expired {
+ log.Printf("[ERROR] Zone %s is expired", zone)
+ return dns.RcodeServerFailure, nil
+ }
+
+ if state.QType() == dns.TypeAXFR || state.QType() == dns.TypeIXFR {
+ xfr := Xfr{z}
+ return xfr.ServeDNS(ctx, w, r)
+ }
+
+ answer, ns, extra, result := z.Lookup(state, qname)
+
+ m := new(dns.Msg)
+ m.SetReply(r)
+ m.Authoritative, m.RecursionAvailable, m.Compress = true, true, true
+ m.Answer, m.Ns, m.Extra = answer, ns, extra
+
+ switch result {
+ case Success:
+ case NoData:
+ case NameError:
+ m.Rcode = dns.RcodeNameError
+ case Delegation:
+ m.Authoritative = false
+ case ServerFailure:
+ return dns.RcodeServerFailure, nil
+ }
+
+ state.SizeAndDo(m)
+ m, _ = state.Scrub(m)
+ w.WriteMsg(m)
+ return dns.RcodeSuccess, nil
+}
+
+// Name implements the Handler interface.
+func (f File) Name() string { return "file" }
+
+// Parse parses the zone in filename and returns a new Zone or an error.
+// If serial >= 0 it will reload the zone, if the SOA hasn't changed
+// it returns an error indicating nothing was read.
+func Parse(f io.Reader, origin, fileName string, serial int64) (*Zone, error) {
+ tokens := dns.ParseZone(f, dns.Fqdn(origin), fileName)
+ z := NewZone(origin, fileName)
+ seenSOA := false
+ for x := range tokens {
+ if x.Error != nil {
+ return nil, x.Error
+ }
+
+ if !seenSOA && serial >= 0 {
+ if s, ok := x.RR.(*dns.SOA); ok {
+ if s.Serial == uint32(serial) { // same zone
+ return nil, fmt.Errorf("no change in serial: %d", serial)
+ }
+ seenSOA = true
+ }
+ }
+
+ if err := z.Insert(x.RR); err != nil {
+ return nil, err
+ }
+ }
+ if !seenSOA {
+ return nil, fmt.Errorf("file %q has no SOA record", fileName)
+ }
+
+ return z, nil
+}
diff --git a/plugin/file/file_test.go b/plugin/file/file_test.go
new file mode 100644
index 000000000..02668785b
--- /dev/null
+++ b/plugin/file/file_test.go
@@ -0,0 +1,31 @@
+package file
+
+import (
+ "strings"
+ "testing"
+)
+
+func BenchmarkFileParseInsert(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ Parse(strings.NewReader(dbMiekENTNL), testzone, "stdin", 0)
+ }
+}
+
+func TestParseNoSOA(t *testing.T) {
+ _, err := Parse(strings.NewReader(dbNoSOA), "example.org.", "stdin", 0)
+ if err == nil {
+ t.Fatalf("zone %q should have failed to load", "example.org.")
+ }
+ if !strings.Contains(err.Error(), "no SOA record") {
+ t.Fatalf("zone %q should have failed to load with no soa error: %s", "example.org.", err)
+ }
+}
+
+const dbNoSOA = `
+$TTL 1M
+$ORIGIN example.org.
+
+www IN A 192.168.0.14
+mail IN A 192.168.0.15
+imap IN CNAME mail
+`
diff --git a/plugin/file/glue_test.go b/plugin/file/glue_test.go
new file mode 100644
index 000000000..3880953c2
--- /dev/null
+++ b/plugin/file/glue_test.go
@@ -0,0 +1,253 @@
+package file
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+// another personal zone (helps in testing as my secondary is NSD
+// atoom = atom in English.
+var atoomTestCases = []test.Case{
+ {
+ Qname: atoom, Qtype: dns.TypeNS, Do: true,
+ Answer: []dns.RR{
+ test.NS("atoom.net. 1800 IN NS linode.atoom.net."),
+ test.NS("atoom.net. 1800 IN NS ns-ext.nlnetlabs.nl."),
+ test.NS("atoom.net. 1800 IN NS omval.tednet.nl."),
+ test.RRSIG("atoom.net. 1800 IN RRSIG NS 8 2 1800 20170112031301 20161213031301 53289 atoom.net. DLe+G1 jlw="),
+ },
+ Extra: []dns.RR{
+ test.OPT(4096, true),
+ test.A("linode.atoom.net. 1800 IN A 176.58.119.54"),
+ test.AAAA("linode.atoom.net. 1800 IN AAAA 2a01:7e00::f03c:91ff:fe79:234c"),
+ test.RRSIG("linode.atoom.net. 1800 IN RRSIG A 8 3 1800 20170112031301 20161213031301 53289 atoom.net. Z4Ka4OLDoyxj72CL vkI="),
+ test.RRSIG("linode.atoom.net. 1800 IN RRSIG AAAA 8 3 1800 20170112031301 20161213031301 53289 atoom.net. l+9Qc914zFH/okG2fzJ1q olQ="),
+ },
+ },
+}
+
+func TestLookupGlue(t *testing.T) {
+ zone, err := Parse(strings.NewReader(dbAtoomNetSigned), atoom, "stdin", 0)
+ if err != nil {
+ t.Fatalf("Expected no error when reading zone, got %q", err)
+ }
+
+ fm := File{Next: test.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{atoom: zone}, Names: []string{atoom}}}
+ ctx := context.TODO()
+
+ for _, tc := range atoomTestCases {
+ m := tc.Msg()
+
+ rec := dnsrecorder.New(&test.ResponseWriter{})
+ _, err := fm.ServeDNS(ctx, rec, m)
+ if err != nil {
+ t.Errorf("Expected no error, got %v\n", err)
+ return
+ }
+
+ resp := rec.Msg
+ test.SortAndCheck(t, resp, tc)
+ }
+}
+
+const dbAtoomNetSigned = `
+; File written on Tue Dec 13 04:13:01 2016
+; dnssec_signzone version 9.10.3-P4-Debian
+atoom.net. 1800 IN SOA linode.atoom.net. miek.miek.nl. (
+ 1481602381 ; serial
+ 14400 ; refresh (4 hours)
+ 3600 ; retry (1 hour)
+ 604800 ; expire (1 week)
+ 14400 ; minimum (4 hours)
+ )
+ 1800 RRSIG SOA 8 2 1800 (
+ 20170112031301 20161213031301 53289 atoom.net.
+ GZ30uFuGATKzwHXgpEwK70qjdXSAqmbB5d4z
+ e7WTibvJDPLa1ptZBI7Zuod2KMOkT1ocSvhL
+ U7makhdv0BQx+5RSaP25mAmPIzfU7/T7R+DJ
+ 5q1GLlDSvOprfyMUlwOgZKZinesSdUa9gRmu
+ 8E+XnPNJ/jcTrGzzaDjn1/irrM0= )
+ 1800 NS omval.tednet.nl.
+ 1800 NS linode.atoom.net.
+ 1800 NS ns-ext.nlnetlabs.nl.
+ 1800 RRSIG NS 8 2 1800 (
+ 20170112031301 20161213031301 53289 atoom.net.
+ D8Sd9JpXIOxOrUF5Hi1ASutyQwP7JNu8XZxA
+ rse86A6L01O8H8sCNib2VEoJjHuZ/dDEogng
+ OgmfqeFy04cpSX19GAk3bkx8Lr6aEat3nqIC
+ XA/xsCCfXy0NKZpI05zntHPbbP5tF/NvpE7n
+ 0+oLtlHSPEg1ZnEgwNoLe+G1jlw= )
+ 1800 A 176.58.119.54
+ 1800 RRSIG A 8 2 1800 (
+ 20170112031301 20161213031301 53289 atoom.net.
+ mrjiUFNCqDgCW8TuhjzcMh0V841uC224QvwH
+ 0+OvYhcve9twbX3Y12PSFmz77Xz3Jg9WAj4I
+ qhh3iHUac4dzUXyC702DT62yMF/9CMUO0+Ee
+ b6wRtvPHr2Tt0i/xV/BTbArInIvurXJrvKvo
+ LsZHOfsg7dZs6Mvdpe/CgwRExpk= )
+ 1800 AAAA 2a01:7e00::f03c:91ff:fe79:234c
+ 1800 RRSIG AAAA 8 2 1800 (
+ 20170112031301 20161213031301 53289 atoom.net.
+ EkMxX2vUaP4h0qbWlHaT4yNhm8MrPMZTn/3R
+ zNw+i3oF2cLMWKh6GCfuIX/x5ID706o8kfum
+ bxTYwuTe1LJ+GoZHWEiH8VCa1laTlh8l3qSi
+ PZKU8339rr5cCYluk6p9PbAuRkYYOEruNg42
+ wPOx46dsAlvp2XpOaOeJtU64QGQ= )
+ 14400 NSEC deb.atoom.net. A NS SOA AAAA RRSIG NSEC DNSKEY
+ 14400 RRSIG NSEC 8 2 14400 (
+ 20170112031301 20161213031301 53289 atoom.net.
+ P7Stx7lqRKl8tbTAAaJ0W6UhgJwZz3cjpM8z
+ eplbhXEVohKtyJ9xgptKt1vreH6lkhzciar5
+ EB9Nj0VOmcthiht/+As8aEKmf8UlcJ2EbLII
+ NT7NUaasxsrLE2rjjX5mEtzOZ1uQAGiU8Hnk
+ XdGweTgIVFuiCcMCgaKpC2TRrMw= )
+ 1800 DNSKEY 256 3 8 (
+ AwEAAeDZTH9YT9qLMPlq4VrxX7H3GbWcqCrC
+ tXc9RT/hf96GN+ttnnEQVaJY8Gbly3IZpYQW
+ MwaCi0t30UULXE3s9FUQtl4AMbplyiz9EF8L
+ /XoBS1yhGm5WV5u608ihoPaRkYNyVV3egb5Y
+ hA5EXWy2vfsa1XWPpxvSAhlqM0YENtP3
+ ) ; ZSK; alg = RSASHA256; key id = 53289
+ 1800 DNSKEY 257 3 8 (
+ AwEAAepN7Vo8enDCruVduVlGxTDIv7QG0wJQ
+ fTL1hMy4k0Yf/7dXzrn5bZT4ytBvH1hoBImH
+ mtTrQo6DQlBBVXDJXTyQjQozaHpN1HhTJJTz
+ IXl8UrdbkLWvz6QSeJPmBBYQRAqylUA2KE29
+ nxyiNboheDLiIWyQ7Q/Op7lYaKMdb555kQAs
+ b/XT4Tb3/3BhAjcofNofNBjDjPq2i8pAo8HU
+ 5mW5/Pl+ZT/S0aqQPnCkHk/iofSRu3ZdBzkH
+ 54eoC+BdyXb7gTbPGRr+1gMbf/rzhRiZ4vnX
+ NoEzGAXmorKzJHANNb6KQ/932V9UDHm9wbln
+ 6y3s7IBvsMX5KF8vo81Stkc=
+ ) ; KSK; alg = RSASHA256; key id = 19114
+ 1800 RRSIG DNSKEY 8 2 1800 (
+ 20170112031301 20161213031301 19114 atoom.net.
+ IEjViubKdef8RWB5bcnirqVcqDk16irkywJZ
+ sBjMyNs03/a+sl0UHEGAB7qCC+Rn+RDaM5It
+ WF+Gha6BwRIN9NuSg3BwB2h1nJtHw61pMVU9
+ 2j9Q3pq7X1xoTBAcwY95t5a1xlw0iTCaLu1L
+ Iu/PbVp1gj1o8BF/PiYilvZJGUjaTgsi+YNi
+ 2kiWpp6afO78/W4nfVx+lQBmpyfX1lwL5PEC
+ 9f5PMbzRmOapvUBc2XdddGywLdmlNsLHimGV
+ t7kkHZHOWQR1TvvMbU3dsC0bFCrBVGDhEuxC
+ hATR+X5YV0AyDSyrew7fOGJKrapwMWS3yRLr
+ FAt0Vcxno5lwQImbCQ== )
+ 1800 RRSIG DNSKEY 8 2 1800 (
+ 20170112031301 20161213031301 53289 atoom.net.
+ sSxdgPT+gFZPN0ot6lZRGqOwvONUEsg0uEbf
+ kh19JlWHu/qvq5HOOK2VOW/UnswpVmtpFk0W
+ z/jiCNHifjpCCVn5tfCMZDLGekmPOjdobw24
+ swBuGjnn0NHvxHoN6S+mb+AR6V/dLjquNUda
+ yzBc2Ua+XtQ7SCLKIvEhcNg9H3o= )
+deb.atoom.net. 1800 IN A 176.58.119.54
+ 1800 RRSIG A 8 3 1800 (
+ 20170112031301 20161213031301 53289 atoom.net.
+ ZW7jm/VDa/I9DxWlE7Cm+HHymiVv4Wk5UGYI
+ Uf/g0EfxLCBR6SwL5QKuV1z7xoWKaiNqqrmc
+ gg35xgskKyS8QHgCCODhDzcIKe+MSsBXbY04
+ AtrC5dV3JJQoA65Ng/48hwcyghAjXKrA2Yyq
+ GXf2DSvWeIV9Jmk0CsOELP24dpk= )
+ 1800 TXT "v=spf1 a ip6:2a01:7e00::f03c:91ff:fe79:234c ~all"
+ 1800 RRSIG TXT 8 3 1800 (
+ 20170112031301 20161213031301 53289 atoom.net.
+ fpvVJ+Z6tzSd9yETn/PhLSCRISwRD1c3ET80
+ 8twnx3XfAPQfV2R8dw7pz8Vw4TSxvf19bAZc
+ PWRjW682gb7gAxoJshCXBYabMfqExrBc9V1S
+ ezwm3D93xNMyegxzHx2b/H8qp3ZWdsMLTvvN
+ Azu7P4iyO+WRWT0R7bJGrdTwRz8= )
+ 1800 AAAA 2a01:7e00::f03c:91ff:fe79:234c
+ 1800 RRSIG AAAA 8 3 1800 (
+ 20170112031301 20161213031301 53289 atoom.net.
+ aaPF6NqXfWamzi+xUDVeYa7StJUVM1tDsL34
+ w5uozFRZ0f4K/Z88Kk5CgztxmtpNNKGdLWa0
+ iryUJsbVWAbSQfrZNkNckBtczMNxGgjqn97A
+ 2//F6ajH/qrR3dWcCm+VJMgu3UPqAxLiCaYO
+ GQUx6Y8JA1VIM/RJAM6BhgNxjD0= )
+ 14400 NSEC lafhart.atoom.net. A TXT AAAA RRSIG NSEC
+ 14400 RRSIG NSEC 8 3 14400 (
+ 20170112031301 20161213031301 53289 atoom.net.
+ 1Llad64NDWcz8CyBu2TsyANrJ9Tpfm5257sY
+ FPYF579p3c9Imwp9kYEO1zMEKgNoXBN/sQnd
+ YCugq3r2GAI6bfJj8sV5bt6GKuZcGHMESug4
+ uh2gU0NDcCA4GPdBYGdusePwV0RNpcRnVCFA
+ fsACp+22j3uwRUbCh0re0ufbAs4= )
+lafhart.atoom.net. 1800 IN A 178.79.160.171
+ 1800 RRSIG A 8 3 1800 (
+ 20170112031301 20161213031301 53289 atoom.net.
+ fruP6cvMVICXEV8NcheS73NWLCEKlO1FgW6B
+ 35D2GhtfYZe+M23V5YBRtlVCCrAdS0etdCOf
+ xH9yt3u2kVvDXuMRiQr1zJPRDEq3cScYumpd
+ bOO8cjHiCic5lEcRVWNNHXyGtpqTvrp9CxOu
+ IQw1WgAlZyKj43zGg3WZi6OTKLg= )
+ 14400 NSEC linode.atoom.net. A RRSIG NSEC
+ 14400 RRSIG NSEC 8 3 14400 (
+ 20170112031301 20161213031301 53289 atoom.net.
+ 2AUWXbScL0jIJ7G6UsJAlUs+bgSprZ1zY6v/
+ iVB5BAYwZD6pPky7LZdzvPEHh0aNLGIFbbU8
+ SDJI7u/e4RUTlE+8yyjl6obZNfNKyJFqE5xN
+ 1BJ8sjFrVn6KaHIDKEOZunNb1MlMfCRkLg9O
+ 94zg04XEgVUfaYCPxvLs3fCEgzw= )
+voordeur.atoom.net. 1800 IN A 77.249.87.46
+ 1800 RRSIG A 8 3 1800 (
+ 20170112031301 20161213031301 53289 atoom.net.
+ SzJz0NaKLRA/lW4CxgMHgeuQLp5QqFEjQv3I
+ zfPtY4joQsZn8RN8RLECcpcPKjbC8Dj6mxIJ
+ dd2vwhsCVlZKMNcZUOfpB7eGx1TR9HnzMkY9
+ OdTt30a9+tktagrJEoy31vAhj1hJqLbSgvOa
+ pRr1P4ZpQ53/qH8JX/LOmqfWTdg= )
+ 14400 NSEC www.atoom.net. A RRSIG NSEC
+ 14400 RRSIG NSEC 8 3 14400 (
+ 20170112031301 20161213031301 53289 atoom.net.
+ CETJhUJy1rKjVj9wsW1549gth+/Z37//BI6S
+ nxJ+2Oq63jEjlbznmyo5hvFW54DbVUod+cLo
+ N9PdlNQDr1XsRBgWhkKW37RkuoRVEPwqRykv
+ xzn9i7CgYKAAHFyWMGihBLkV9ByPp8GDR8Zr
+ DEkrG3ErDlBcwi3FqGZFsSOW2xg= )
+www.atoom.net. 1800 IN CNAME deb.atoom.net.
+ 1800 RRSIG CNAME 8 3 1800 (
+ 20170112031301 20161213031301 53289 atoom.net.
+ 1lhG6iTtbeesBCVOrA8a7+V2gogCuXzKgSi8
+ 6K0Pzq2CwqTScdNcZvcDOIbLq45Am5p09PIj
+ lXnd2fw6WAxphwvRhmwCve3uTZMUt5STw7oi
+ 0rED7GMuFUSC/BX0XVly7NET3ECa1vaK6RhO
+ hDSsKPWFI7to4d1z6tQ9j9Kvm4Y= )
+ 14400 NSEC atoom.net. CNAME RRSIG NSEC
+ 14400 RRSIG NSEC 8 3 14400 (
+ 20170112031301 20161213031301 53289 atoom.net.
+ CC4yCYP1q75/gTmPz+mVM6Lam2foPP5oTccY
+ RtROuTkgbt8DtAoPe304vmNazWBlGidnWJeD
+ YyAAe3znIHP0CgrxjD/hRL9FUzMnVrvB3mnx
+ 4W13wP1rE97RqJxV1kk22Wl3uCkVGy7LCjb0
+ JLFvzCe2fuMe7YcTzI+t1rioTP0= )
+linode.atoom.net. 1800 IN A 176.58.119.54
+ 1800 RRSIG A 8 3 1800 (
+ 20170112031301 20161213031301 53289 atoom.net.
+ Z4Ka4OLDha4eQNWs3GtUd1Cumr48RUnH523I
+ nZzGXtpQNou70qsm5Jt8n/HmsZ4L5DoxomRz
+ rgZTGnrqj43+A16UUGfVEk6SfUUHOgxgspQW
+ zoaqk5/5mQO1ROsLKY8RqaRqzvbToHvqeZEh
+ VkTPVA02JK9UFlKqoyxj72CLvkI= )
+ 1800 AAAA 2a01:7e00::f03c:91ff:fe79:234c
+ 1800 RRSIG AAAA 8 3 1800 (
+ 20170112031301 20161213031301 53289 atoom.net.
+ l+9Qce/EQyKrTJVKLv7iatjuCO285ckd5Oie
+ P2LzWVsL4tW04oHzieKZwIuNBRE+px8g5qrT
+ LIK2TikCGL1xHAd7CT7gbCtDcZ7jHmSTmMTJ
+ 405nOV3G3xWelreLI5Fn5ck8noEsF64kiw1y
+ XfkyQn2B914zFH/okG2fzJ1qolQ= )
+ 14400 NSEC voordeur.atoom.net. A AAAA RRSIG NSEC
+ 14400 RRSIG NSEC 8 3 14400 (
+ 20170112031301 20161213031301 53289 atoom.net.
+ Owzmz7QrVL2Gw2njEsUVEknMl2amx1HG9X3K
+ tO+Ihyy4tApiUFxUjAu3P/30QdqbB85h7s//
+ ipwX/AmQJNoxTScR3nHt9qDqJ044DPmiuh0l
+ NuIjguyZRANApmKCTA6AoxXIUqToIIjfVzi/
+ PxXE6T3YIPlK7Bxgv1lcCBJ1fmE= )`
+
+const atoom = "atoom.net."
diff --git a/plugin/file/include_test.go b/plugin/file/include_test.go
new file mode 100644
index 000000000..fad91df5c
--- /dev/null
+++ b/plugin/file/include_test.go
@@ -0,0 +1,32 @@
+package file
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/coredns/coredns/plugin/test"
+)
+
+// Make sure the external miekg/dns dependency is up to date
+
+func TestInclude(t *testing.T) {
+
+ name, rm, err := test.TempFile(".", "foo\tIN\tA\t127.0.0.1\n")
+ if err != nil {
+ t.Fatalf("Unable to create tmpfile %q: %s", name, err)
+ }
+ defer rm()
+
+ zone := `$ORIGIN example.org.
+@ IN SOA sns.dns.icann.org. noc.dns.icann.org. 2017042766 7200 3600 1209600 3600
+$INCLUDE ` + name + "\n"
+
+ z, err := Parse(strings.NewReader(zone), "example.org.", "test", 0)
+ if err != nil {
+ t.Errorf("Unable to parse zone %q: %s", "example.org.", err)
+ }
+
+ if _, ok := z.Search("foo.example.org."); !ok {
+ t.Errorf("Failed to find %q in parsed zone", "foo.example.org.")
+ }
+}
diff --git a/plugin/file/lookup.go b/plugin/file/lookup.go
new file mode 100644
index 000000000..cf2f06841
--- /dev/null
+++ b/plugin/file/lookup.go
@@ -0,0 +1,467 @@
+package file
+
+import (
+ "github.com/coredns/coredns/plugin/file/tree"
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+)
+
+// Result is the result of a Lookup
+type Result int
+
+const (
+ // Success is a successful lookup.
+ Success Result = iota
+ // NameError indicates a nameerror
+ NameError
+ // Delegation indicates the lookup resulted in a delegation.
+ Delegation
+ // NoData indicates the lookup resulted in a NODATA.
+ NoData
+ // ServerFailure indicates a server failure during the lookup.
+ ServerFailure
+)
+
+// Lookup looks up qname and qtype in the zone. When do is true DNSSEC records are included.
+// Three sets of records are returned, one for the answer, one for authority and one for the additional section.
+func (z *Zone) Lookup(state request.Request, qname string) ([]dns.RR, []dns.RR, []dns.RR, Result) {
+
+ qtype := state.QType()
+ do := state.Do()
+
+ if !z.NoReload {
+ z.reloadMu.RLock()
+ }
+ defer func() {
+ if !z.NoReload {
+ z.reloadMu.RUnlock()
+ }
+ }()
+
+ // If z is a secondary zone we might not have transferred it, meaning we have
+ // all zone context setup, except the actual record. This means (for one thing) the apex
+ // is empty and we don't have a SOA record.
+ soa := z.Apex.SOA
+ if soa == nil {
+ return nil, nil, nil, ServerFailure
+ }
+
+ if qtype == dns.TypeSOA {
+ return z.soa(do), z.ns(do), nil, Success
+ }
+ if qtype == dns.TypeNS && qname == z.origin {
+ nsrrs := z.ns(do)
+ glue := z.Glue(nsrrs, do)
+ return nsrrs, nil, glue, Success
+ }
+
+ var (
+ found, shot bool
+ parts string
+ i int
+ elem, wildElem *tree.Elem
+ )
+
+ // Lookup:
+ // * Per label from the right, look if it exists. We do this to find potential
+ // delegation records.
+ // * If the per-label search finds nothing, we will look for the wildcard at the
+ // level. If found we keep it around. If we don't find the complete name we will
+ // use the wildcard.
+ //
+ // Main for-loop handles delegation and finding or not finding the qname.
+ // If found we check if it is a CNAME/DNAME and do CNAME processing
+ // We also check if we have type and do a nodata resposne.
+ //
+ // If not found, we check the potential wildcard, and use that for further processing.
+ // If not found and no wildcard we will process this as an NXDOMAIN response.
+ for {
+ parts, shot = z.nameFromRight(qname, i)
+ // We overshot the name, break and check if we previously found something.
+ if shot {
+ break
+ }
+
+ elem, found = z.Tree.Search(parts)
+ if !found {
+ // Apex will always be found, when we are here we can search for a wildcard
+ // and save the result of that search. So when nothing match, but we have a
+ // wildcard we should expand the wildcard.
+
+ wildcard := replaceWithAsteriskLabel(parts)
+ if wild, found := z.Tree.Search(wildcard); found {
+ wildElem = wild
+ }
+
+ // Keep on searching, because maybe we hit an empty-non-terminal (which aren't
+ // stored in the tree. Only when we have match the full qname (and possible wildcard
+ // we can be confident that we didn't find anything.
+ i++
+ continue
+ }
+
+ // If we see DNAME records, we should return those.
+ if dnamerrs := elem.Types(dns.TypeDNAME); dnamerrs != nil {
+ // Only one DNAME is allowed per name. We just pick the first one to synthesize from.
+ dname := dnamerrs[0]
+ if cname := synthesizeCNAME(state.Name(), dname.(*dns.DNAME)); cname != nil {
+ answer, ns, extra, rcode := z.searchCNAME(state, elem, []dns.RR{cname})
+
+ if do {
+ sigs := elem.Types(dns.TypeRRSIG)
+ sigs = signatureForSubType(sigs, dns.TypeDNAME)
+ dnamerrs = append(dnamerrs, sigs...)
+ }
+
+ // The relevant DNAME RR should be included in the answer section,
+ // if the DNAME is being employed as a substitution instruction.
+ answer = append(dnamerrs, answer...)
+
+ return answer, ns, extra, rcode
+ }
+ // The domain name that owns a DNAME record is allowed to have other RR types
+ // at that domain name, except those have restrictions on what they can coexist
+ // with (e.g. another DNAME). So there is nothing special left here.
+ }
+
+ // If we see NS records, it means the name as been delegated, and we should return the delegation.
+ if nsrrs := elem.Types(dns.TypeNS); nsrrs != nil {
+ glue := z.Glue(nsrrs, do)
+ // If qtype == NS, we should returns success to put RRs in answer.
+ if qtype == dns.TypeNS {
+ return nsrrs, nil, glue, Success
+ }
+
+ if do {
+ dss := z.typeFromElem(elem, dns.TypeDS, do)
+ nsrrs = append(nsrrs, dss...)
+ }
+
+ return nil, nsrrs, glue, Delegation
+ }
+
+ i++
+ }
+
+ // What does found and !shot mean - do we ever hit it?
+ if found && !shot {
+ return nil, nil, nil, ServerFailure
+ }
+
+ // Found entire name.
+ if found && shot {
+
+ if rrs := elem.Types(dns.TypeCNAME); len(rrs) > 0 && qtype != dns.TypeCNAME {
+ return z.searchCNAME(state, elem, rrs)
+ }
+
+ rrs := elem.Types(qtype, qname)
+
+ // NODATA
+ if len(rrs) == 0 {
+ ret := z.soa(do)
+ if do {
+ nsec := z.typeFromElem(elem, dns.TypeNSEC, do)
+ ret = append(ret, nsec...)
+ }
+ return nil, ret, nil, NoData
+ }
+
+ // Additional section processing for MX, SRV. Check response and see if any of the names are in baliwick -
+ // if so add IP addresses to the additional section.
+ additional := additionalProcessing(z, rrs, do)
+
+ if do {
+ sigs := elem.Types(dns.TypeRRSIG)
+ sigs = signatureForSubType(sigs, qtype)
+ rrs = append(rrs, sigs...)
+ }
+
+ return rrs, z.ns(do), additional, Success
+
+ }
+
+ // Haven't found the original name.
+
+ // Found wildcard.
+ if wildElem != nil {
+ auth := z.ns(do)
+
+ if rrs := wildElem.Types(dns.TypeCNAME, qname); len(rrs) > 0 {
+ return z.searchCNAME(state, wildElem, rrs)
+ }
+
+ rrs := wildElem.Types(qtype, qname)
+
+ // NODATA response.
+ if len(rrs) == 0 {
+ ret := z.soa(do)
+ if do {
+ nsec := z.typeFromElem(wildElem, dns.TypeNSEC, do)
+ ret = append(ret, nsec...)
+ }
+ return nil, ret, nil, Success
+ }
+
+ if do {
+ // An NSEC is needed to say no longer name exists under this wildcard.
+ if deny, found := z.Tree.Prev(qname); found {
+ nsec := z.typeFromElem(deny, dns.TypeNSEC, do)
+ auth = append(auth, nsec...)
+ }
+
+ sigs := wildElem.Types(dns.TypeRRSIG, qname)
+ sigs = signatureForSubType(sigs, qtype)
+ rrs = append(rrs, sigs...)
+
+ }
+ return rrs, auth, nil, Success
+ }
+
+ rcode := NameError
+
+ // Hacky way to get around empty-non-terminals. If a longer name does exist, but this qname, does not, it
+ // must be an empty-non-terminal. If so, we do the proper NXDOMAIN handling, but set the rcode to be success.
+ if x, found := z.Tree.Next(qname); found {
+ if dns.IsSubDomain(qname, x.Name()) {
+ rcode = Success
+ }
+ }
+
+ ret := z.soa(do)
+ if do {
+ deny, _ := z.Tree.Prev(qname) // TODO(miek): *found* was not used here.
+ nsec := z.typeFromElem(deny, dns.TypeNSEC, do)
+ ret = append(ret, nsec...)
+
+ if rcode != NameError {
+ goto Out
+ }
+
+ ce, found := z.ClosestEncloser(qname)
+
+ // wildcard denial only for NXDOMAIN
+ if found {
+ // wildcard denial
+ wildcard := "*." + ce.Name()
+ if ss, found := z.Tree.Prev(wildcard); found {
+ // Only add this nsec if it is different than the one already added
+ if ss.Name() != deny.Name() {
+ nsec := z.typeFromElem(ss, dns.TypeNSEC, do)
+ ret = append(ret, nsec...)
+ }
+ }
+ }
+
+ }
+Out:
+ return nil, ret, nil, rcode
+}
+
+// Return type tp from e and add signatures (if they exists) and do is true.
+func (z *Zone) typeFromElem(elem *tree.Elem, tp uint16, do bool) []dns.RR {
+ rrs := elem.Types(tp)
+ if do {
+ sigs := elem.Types(dns.TypeRRSIG)
+ sigs = signatureForSubType(sigs, tp)
+ if len(sigs) > 0 {
+ rrs = append(rrs, sigs...)
+ }
+ }
+ return rrs
+}
+
+func (z *Zone) soa(do bool) []dns.RR {
+ if do {
+ ret := append([]dns.RR{z.Apex.SOA}, z.Apex.SIGSOA...)
+ return ret
+ }
+ return []dns.RR{z.Apex.SOA}
+}
+
+func (z *Zone) ns(do bool) []dns.RR {
+ if do {
+ ret := append(z.Apex.NS, z.Apex.SIGNS...)
+ return ret
+ }
+ return z.Apex.NS
+}
+
+// TODO(miek): should be better named, like aditionalProcessing?
+func (z *Zone) searchCNAME(state request.Request, elem *tree.Elem, rrs []dns.RR) ([]dns.RR, []dns.RR, []dns.RR, Result) {
+
+ qtype := state.QType()
+ do := state.Do()
+
+ if do {
+ sigs := elem.Types(dns.TypeRRSIG)
+ sigs = signatureForSubType(sigs, dns.TypeCNAME)
+ if len(sigs) > 0 {
+ rrs = append(rrs, sigs...)
+ }
+ }
+
+ targetName := rrs[0].(*dns.CNAME).Target
+ elem, _ = z.Tree.Search(targetName)
+ if elem == nil {
+ if !dns.IsSubDomain(z.origin, targetName) {
+ rrs = append(rrs, z.externalLookup(state, targetName, qtype)...)
+ }
+ return rrs, z.ns(do), nil, Success
+ }
+
+ i := 0
+
+Redo:
+ cname := elem.Types(dns.TypeCNAME)
+ if len(cname) > 0 {
+ rrs = append(rrs, cname...)
+
+ if do {
+ sigs := elem.Types(dns.TypeRRSIG)
+ sigs = signatureForSubType(sigs, dns.TypeCNAME)
+ if len(sigs) > 0 {
+ rrs = append(rrs, sigs...)
+ }
+ }
+ targetName := cname[0].(*dns.CNAME).Target
+ elem, _ = z.Tree.Search(targetName)
+ if elem == nil {
+ if !dns.IsSubDomain(z.origin, targetName) {
+ if !dns.IsSubDomain(z.origin, targetName) {
+ rrs = append(rrs, z.externalLookup(state, targetName, qtype)...)
+ }
+ }
+ return rrs, z.ns(do), nil, Success
+ }
+
+ i++
+ if i > maxChain {
+ return rrs, z.ns(do), nil, Success
+ }
+
+ goto Redo
+ }
+
+ targets := cnameForType(elem.All(), qtype)
+ if len(targets) > 0 {
+ rrs = append(rrs, targets...)
+
+ if do {
+ sigs := elem.Types(dns.TypeRRSIG)
+ sigs = signatureForSubType(sigs, qtype)
+ if len(sigs) > 0 {
+ rrs = append(rrs, sigs...)
+ }
+ }
+ }
+
+ return rrs, z.ns(do), nil, Success
+}
+
+func cnameForType(targets []dns.RR, origQtype uint16) []dns.RR {
+ ret := []dns.RR{}
+ for _, target := range targets {
+ if target.Header().Rrtype == origQtype {
+ ret = append(ret, target)
+ }
+ }
+ return ret
+}
+
+func (z *Zone) externalLookup(state request.Request, target string, qtype uint16) []dns.RR {
+ m, e := z.Proxy.Lookup(state, target, qtype)
+ if e != nil {
+ // TODO(miek): debugMsg for this as well? Log?
+ return nil
+ }
+ return m.Answer
+}
+
+// signatureForSubType range through the signature and return the correct ones for the subtype.
+func signatureForSubType(rrs []dns.RR, subtype uint16) []dns.RR {
+ sigs := []dns.RR{}
+ for _, sig := range rrs {
+ if s, ok := sig.(*dns.RRSIG); ok {
+ if s.TypeCovered == subtype {
+ sigs = append(sigs, s)
+ }
+ }
+ }
+ return sigs
+}
+
+// Glue returns any potential glue records for nsrrs.
+func (z *Zone) Glue(nsrrs []dns.RR, do bool) []dns.RR {
+ glue := []dns.RR{}
+ for _, rr := range nsrrs {
+ if ns, ok := rr.(*dns.NS); ok && dns.IsSubDomain(ns.Header().Name, ns.Ns) {
+ glue = append(glue, z.searchGlue(ns.Ns, do)...)
+ }
+ }
+ return glue
+}
+
+// searchGlue looks up A and AAAA for name.
+func (z *Zone) searchGlue(name string, do bool) []dns.RR {
+ glue := []dns.RR{}
+
+ // A
+ if elem, found := z.Tree.Search(name); found {
+ glue = append(glue, elem.Types(dns.TypeA)...)
+ if do {
+ sigs := elem.Types(dns.TypeRRSIG)
+ sigs = signatureForSubType(sigs, dns.TypeA)
+ glue = append(glue, sigs...)
+ }
+ }
+
+ // AAAA
+ if elem, found := z.Tree.Search(name); found {
+ glue = append(glue, elem.Types(dns.TypeAAAA)...)
+ if do {
+ sigs := elem.Types(dns.TypeRRSIG)
+ sigs = signatureForSubType(sigs, dns.TypeAAAA)
+ glue = append(glue, sigs...)
+ }
+ }
+ return glue
+}
+
+// additionalProcessing checks the current answer section and retrieves A or AAAA records
+// (and possible SIGs) to need to be put in the additional section.
+func additionalProcessing(z *Zone, answer []dns.RR, do bool) (extra []dns.RR) {
+ for _, rr := range answer {
+ name := ""
+ switch x := rr.(type) {
+ case *dns.SRV:
+ name = x.Target
+ case *dns.MX:
+ name = x.Mx
+ }
+ if !dns.IsSubDomain(z.origin, name) {
+ continue
+ }
+
+ elem, _ := z.Tree.Search(name)
+ if elem == nil {
+ continue
+ }
+
+ sigs := elem.Types(dns.TypeRRSIG)
+ for _, addr := range []uint16{dns.TypeA, dns.TypeAAAA} {
+ if a := elem.Types(addr); a != nil {
+ extra = append(extra, a...)
+ if do {
+ sig := signatureForSubType(sigs, addr)
+ extra = append(extra, sig...)
+ }
+ }
+ }
+ }
+
+ return extra
+}
+
+const maxChain = 8
diff --git a/plugin/file/lookup_test.go b/plugin/file/lookup_test.go
new file mode 100644
index 000000000..8fd93fd8e
--- /dev/null
+++ b/plugin/file/lookup_test.go
@@ -0,0 +1,194 @@
+package file
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+var dnsTestCases = []test.Case{
+ {
+ Qname: "www.miek.nl.", Qtype: dns.TypeA,
+ Answer: []dns.RR{
+ test.A("a.miek.nl. 1800 IN A 139.162.196.78"),
+ test.CNAME("www.miek.nl. 1800 IN CNAME a.miek.nl."),
+ },
+ Ns: miekAuth,
+ },
+ {
+ Qname: "www.miek.nl.", Qtype: dns.TypeAAAA,
+ Answer: []dns.RR{
+ test.AAAA("a.miek.nl. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"),
+ test.CNAME("www.miek.nl. 1800 IN CNAME a.miek.nl."),
+ },
+ Ns: miekAuth,
+ },
+ {
+ Qname: "miek.nl.", Qtype: dns.TypeSOA,
+ Answer: []dns.RR{
+ test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"),
+ },
+ Ns: miekAuth,
+ },
+ {
+ Qname: "miek.nl.", Qtype: dns.TypeAAAA,
+ Answer: []dns.RR{
+ test.AAAA("miek.nl. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"),
+ },
+ Ns: miekAuth,
+ },
+ {
+ Qname: "mIeK.NL.", Qtype: dns.TypeAAAA,
+ Answer: []dns.RR{
+ test.AAAA("miek.nl. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"),
+ },
+ Ns: miekAuth,
+ },
+ {
+ Qname: "miek.nl.", Qtype: dns.TypeMX,
+ Answer: []dns.RR{
+ test.MX("miek.nl. 1800 IN MX 1 aspmx.l.google.com."),
+ test.MX("miek.nl. 1800 IN MX 10 aspmx2.googlemail.com."),
+ test.MX("miek.nl. 1800 IN MX 10 aspmx3.googlemail.com."),
+ test.MX("miek.nl. 1800 IN MX 5 alt1.aspmx.l.google.com."),
+ test.MX("miek.nl. 1800 IN MX 5 alt2.aspmx.l.google.com."),
+ },
+ Ns: miekAuth,
+ },
+ {
+ Qname: "a.miek.nl.", Qtype: dns.TypeSRV,
+ Ns: []dns.RR{
+ test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"),
+ },
+ },
+ {
+ Qname: "b.miek.nl.", Qtype: dns.TypeA,
+ Rcode: dns.RcodeNameError,
+ Ns: []dns.RR{
+ test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"),
+ },
+ },
+ {
+ Qname: "srv.miek.nl.", Qtype: dns.TypeSRV,
+ Answer: []dns.RR{
+ test.SRV("srv.miek.nl. 1800 IN SRV 10 10 8080 a.miek.nl."),
+ },
+ Extra: []dns.RR{
+ test.A("a.miek.nl. 1800 IN A 139.162.196.78"),
+ test.AAAA("a.miek.nl. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"),
+ },
+ Ns: miekAuth,
+ },
+ {
+ Qname: "mx.miek.nl.", Qtype: dns.TypeMX,
+ Answer: []dns.RR{
+ test.MX("mx.miek.nl. 1800 IN MX 10 a.miek.nl."),
+ },
+ Extra: []dns.RR{
+ test.A("a.miek.nl. 1800 IN A 139.162.196.78"),
+ test.AAAA("a.miek.nl. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"),
+ },
+ Ns: miekAuth,
+ },
+}
+
+const (
+ testzone = "miek.nl."
+ testzone1 = "dnssex.nl."
+)
+
+func TestLookup(t *testing.T) {
+ zone, err := Parse(strings.NewReader(dbMiekNL), testzone, "stdin", 0)
+ if err != nil {
+ t.Fatalf("expect no error when reading zone, got %q", err)
+ }
+
+ fm := File{Next: test.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{testzone: zone}, Names: []string{testzone}}}
+ ctx := context.TODO()
+
+ for _, tc := range dnsTestCases {
+ m := tc.Msg()
+
+ rec := dnsrecorder.New(&test.ResponseWriter{})
+ _, err := fm.ServeDNS(ctx, rec, m)
+ if err != nil {
+ t.Errorf("expected no error, got %v\n", err)
+ return
+ }
+
+ resp := rec.Msg
+ test.SortAndCheck(t, resp, tc)
+ }
+}
+
+func TestLookupNil(t *testing.T) {
+ fm := File{Next: test.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{testzone: nil}, Names: []string{testzone}}}
+ ctx := context.TODO()
+
+ m := dnsTestCases[0].Msg()
+ rec := dnsrecorder.New(&test.ResponseWriter{})
+ fm.ServeDNS(ctx, rec, m)
+}
+
+func BenchmarkFileLookup(b *testing.B) {
+ zone, err := Parse(strings.NewReader(dbMiekNL), testzone, "stdin", 0)
+ if err != nil {
+ return
+ }
+
+ fm := File{Next: test.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{testzone: zone}, Names: []string{testzone}}}
+ ctx := context.TODO()
+ rec := dnsrecorder.New(&test.ResponseWriter{})
+
+ tc := test.Case{
+ Qname: "www.miek.nl.", Qtype: dns.TypeA,
+ Answer: []dns.RR{
+ test.CNAME("www.miek.nl. 1800 IN CNAME a.miek.nl."),
+ test.A("a.miek.nl. 1800 IN A 139.162.196.78"),
+ },
+ }
+
+ m := tc.Msg()
+
+ b.ResetTimer()
+
+ for i := 0; i < b.N; i++ {
+ fm.ServeDNS(ctx, rec, m)
+ }
+}
+
+const dbMiekNL = `
+$TTL 30M
+$ORIGIN miek.nl.
+@ IN SOA linode.atoom.net. miek.miek.nl. (
+ 1282630057 ; Serial
+ 4H ; Refresh
+ 1H ; Retry
+ 7D ; Expire
+ 4H ) ; Negative Cache TTL
+ IN NS linode.atoom.net.
+ IN NS ns-ext.nlnetlabs.nl.
+ IN NS omval.tednet.nl.
+ IN NS ext.ns.whyscream.net.
+
+ IN MX 1 aspmx.l.google.com.
+ IN MX 5 alt1.aspmx.l.google.com.
+ IN MX 5 alt2.aspmx.l.google.com.
+ IN MX 10 aspmx2.googlemail.com.
+ IN MX 10 aspmx3.googlemail.com.
+
+ IN A 139.162.196.78
+ IN AAAA 2a01:7e00::f03c:91ff:fef1:6735
+
+a IN A 139.162.196.78
+ IN AAAA 2a01:7e00::f03c:91ff:fef1:6735
+www IN CNAME a
+archive IN CNAME a
+
+srv IN SRV 10 10 8080 a.miek.nl.
+mx IN MX 10 a.miek.nl.`
diff --git a/plugin/file/notify.go b/plugin/file/notify.go
new file mode 100644
index 000000000..68850e0d3
--- /dev/null
+++ b/plugin/file/notify.go
@@ -0,0 +1,82 @@
+package file
+
+import (
+ "fmt"
+ "log"
+ "net"
+
+ "github.com/coredns/coredns/plugin/pkg/rcode"
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+)
+
+// isNotify checks if state is a notify message and if so, will *also* check if it
+// is from one of the configured masters. If not it will not be a valid notify
+// message. If the zone z is not a secondary zone the message will also be ignored.
+func (z *Zone) isNotify(state request.Request) bool {
+ if state.Req.Opcode != dns.OpcodeNotify {
+ return false
+ }
+ if len(z.TransferFrom) == 0 {
+ return false
+ }
+ // If remote IP matches we accept.
+ remote := state.IP()
+ for _, f := range z.TransferFrom {
+ from, _, err := net.SplitHostPort(f)
+ if err != nil {
+ continue
+ }
+ if from == remote {
+ return true
+ }
+ }
+ return false
+}
+
+// Notify will send notifies to all configured TransferTo IP addresses.
+func (z *Zone) Notify() {
+ go notify(z.origin, z.TransferTo)
+}
+
+// notify sends notifies to the configured remote servers. It will try up to three times
+// before giving up on a specific remote. We will sequentially loop through "to"
+// until they all have replied (or have 3 failed attempts).
+func notify(zone string, to []string) error {
+ m := new(dns.Msg)
+ m.SetNotify(zone)
+ c := new(dns.Client)
+
+ for _, t := range to {
+ if t == "*" {
+ continue
+ }
+ if err := notifyAddr(c, m, t); err != nil {
+ log.Printf("[ERROR] " + err.Error())
+ } else {
+ log.Printf("[INFO] Sent notify for zone %q to %q", zone, t)
+ }
+ }
+ return nil
+}
+
+func notifyAddr(c *dns.Client, m *dns.Msg, s string) error {
+ var err error
+
+ code := dns.RcodeServerFailure
+ for i := 0; i < 3; i++ {
+ ret, _, err := c.Exchange(m, s)
+ if err != nil {
+ continue
+ }
+ code = ret.Rcode
+ if code == dns.RcodeSuccess {
+ return nil
+ }
+ }
+ if err != nil {
+ return fmt.Errorf("notify for zone %q was not accepted by %q: %q", m.Question[0].Name, s, err)
+ }
+ return fmt.Errorf("notify for zone %q was not accepted by %q: rcode was %q", m.Question[0].Name, s, rcode.ToString(code))
+}
diff --git a/plugin/file/nsec3_test.go b/plugin/file/nsec3_test.go
new file mode 100644
index 000000000..6611056cb
--- /dev/null
+++ b/plugin/file/nsec3_test.go
@@ -0,0 +1,28 @@
+package file
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestParseNSEC3PARAM(t *testing.T) {
+ _, err := Parse(strings.NewReader(nsec3paramTest), "miek.nl", "stdin", 0)
+ if err == nil {
+ t.Fatalf("expected error when reading zone, got nothing")
+ }
+}
+
+func TestParseNSEC3(t *testing.T) {
+ _, err := Parse(strings.NewReader(nsec3Test), "miek.nl", "stdin", 0)
+ if err == nil {
+ t.Fatalf("expected error when reading zone, got nothing")
+ }
+}
+
+const nsec3paramTest = `miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1460175181 14400 3600 604800 14400
+miek.nl. 1800 IN NS omval.tednet.nl.
+miek.nl. 0 IN NSEC3PARAM 1 0 5 A3DEBC9CC4F695C7`
+
+const nsec3Test = `example.org. 1800 IN SOA sns.dns.icann.org. noc.dns.icann.org. 2016082508 7200 3600 1209600 3600
+aub8v9ce95ie18spjubsr058h41n7pa5.example.org. 284 IN NSEC3 1 1 5 D0CBEAAF0AC77314 AUB95P93VPKP55G6U5S4SGS7LS61ND85 NS SOA TXT RRSIG DNSKEY NSEC3PARAM
+aub8v9ce95ie18spjubsr058h41n7pa5.example.org. 284 IN RRSIG NSEC3 8 2 600 20160910232502 20160827231002 14028 example.org. XBNpA7KAIjorPbXvTinOHrc1f630aHic2U716GHLHA4QMx9cl9ss4QjR Wj2UpDM9zBW/jNYb1xb0yjQoez/Jv200w0taSWjRci5aUnRpOi9bmcrz STHb6wIUjUsbJ+NstQsUwVkj6679UviF1FqNwr4GlJnWG3ZrhYhE+NI6 s0k=`
diff --git a/plugin/file/reload.go b/plugin/file/reload.go
new file mode 100644
index 000000000..18e949a94
--- /dev/null
+++ b/plugin/file/reload.go
@@ -0,0 +1,72 @@
+package file
+
+import (
+ "log"
+ "os"
+ "path"
+
+ "github.com/fsnotify/fsnotify"
+)
+
+// Reload reloads a zone when it is changed on disk. If z.NoRoload is true, no reloading will be done.
+func (z *Zone) Reload() error {
+ if z.NoReload {
+ return nil
+ }
+ watcher, err := fsnotify.NewWatcher()
+ if err != nil {
+ return err
+ }
+ err = watcher.Add(path.Dir(z.file))
+ if err != nil {
+ return err
+ }
+
+ go func() {
+ // TODO(miek): needs to be killed on reload.
+ for {
+ select {
+ case event := <-watcher.Events:
+ if path.Clean(event.Name) == z.file {
+
+ reader, err := os.Open(z.file)
+ if err != nil {
+ log.Printf("[ERROR] Failed to open `%s' for `%s': %v", z.file, z.origin, err)
+ continue
+ }
+
+ serial := z.SOASerialIfDefined()
+ zone, err := Parse(reader, z.origin, z.file, serial)
+ if err != nil {
+ log.Printf("[WARNING] Parsing zone `%s': %v", z.origin, err)
+ continue
+ }
+
+ // copy elements we need
+ z.reloadMu.Lock()
+ z.Apex = zone.Apex
+ z.Tree = zone.Tree
+ z.reloadMu.Unlock()
+
+ log.Printf("[INFO] Successfully reloaded zone `%s'", z.origin)
+ z.Notify()
+ }
+ case <-z.ReloadShutdown:
+ watcher.Close()
+ return
+ }
+ }
+ }()
+ return nil
+}
+
+// SOASerialIfDefined returns the SOA's serial if the zone has a SOA record in the Apex, or
+// -1 otherwise.
+func (z *Zone) SOASerialIfDefined() int64 {
+ z.reloadMu.Lock()
+ defer z.reloadMu.Unlock()
+ if z.Apex.SOA != nil {
+ return int64(z.Apex.SOA.Serial)
+ }
+ return -1
+}
diff --git a/plugin/file/reload_test.go b/plugin/file/reload_test.go
new file mode 100644
index 000000000..601c426d3
--- /dev/null
+++ b/plugin/file/reload_test.go
@@ -0,0 +1,82 @@
+package file
+
+import (
+ "io/ioutil"
+ "log"
+ "os"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/coredns/coredns/plugin/test"
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+)
+
+func TestZoneReload(t *testing.T) {
+ log.SetOutput(ioutil.Discard)
+
+ fileName, rm, err := test.TempFile(".", reloadZoneTest)
+ if err != nil {
+ t.Fatalf("failed to create zone: %s", err)
+ }
+ defer rm()
+ reader, err := os.Open(fileName)
+ if err != nil {
+ t.Fatalf("failed to open zone: %s", err)
+ }
+ z, err := Parse(reader, "miek.nl", fileName, 0)
+ if err != nil {
+ t.Fatalf("failed to parse zone: %s", err)
+ }
+
+ z.Reload()
+
+ r := new(dns.Msg)
+ r.SetQuestion("miek.nl", dns.TypeSOA)
+ state := request.Request{W: &test.ResponseWriter{}, Req: r}
+ if _, _, _, res := z.Lookup(state, "miek.nl."); res != Success {
+ t.Fatalf("failed to lookup, got %d", res)
+ }
+
+ r = new(dns.Msg)
+ r.SetQuestion("miek.nl", dns.TypeNS)
+ state = request.Request{W: &test.ResponseWriter{}, Req: r}
+ if _, _, _, res := z.Lookup(state, "miek.nl."); res != Success {
+ t.Fatalf("failed to lookup, got %d", res)
+ }
+
+ if len(z.All()) != 5 {
+ t.Fatalf("expected 5 RRs, got %d", len(z.All()))
+ }
+ if err := ioutil.WriteFile(fileName, []byte(reloadZone2Test), 0644); err != nil {
+ t.Fatalf("failed to write new zone data: %s", err)
+ }
+ // Could still be racy, but we need to wait a bit for the event to be seen
+ time.Sleep(1 * time.Second)
+
+ if len(z.All()) != 3 {
+ t.Fatalf("expected 3 RRs, got %d", len(z.All()))
+ }
+}
+
+func TestZoneReloadSOAChange(t *testing.T) {
+ _, err := Parse(strings.NewReader(reloadZoneTest), "miek.nl.", "stdin", 1460175181)
+ if err == nil {
+ t.Fatalf("zone should not have been re-parsed")
+ }
+
+}
+
+const reloadZoneTest = `miek.nl. 1627 IN SOA linode.atoom.net. miek.miek.nl. 1460175181 14400 3600 604800 14400
+miek.nl. 1627 IN NS ext.ns.whyscream.net.
+miek.nl. 1627 IN NS omval.tednet.nl.
+miek.nl. 1627 IN NS linode.atoom.net.
+miek.nl. 1627 IN NS ns-ext.nlnetlabs.nl.
+`
+
+const reloadZone2Test = `miek.nl. 1627 IN SOA linode.atoom.net. miek.miek.nl. 1460175182 14400 3600 604800 14400
+miek.nl. 1627 IN NS ext.ns.whyscream.net.
+miek.nl. 1627 IN NS omval.tednet.nl.
+`
diff --git a/plugin/file/secondary.go b/plugin/file/secondary.go
new file mode 100644
index 000000000..a37d62442
--- /dev/null
+++ b/plugin/file/secondary.go
@@ -0,0 +1,199 @@
+package file
+
+import (
+ "log"
+ "math/rand"
+ "time"
+
+ "github.com/miekg/dns"
+)
+
+// TransferIn retrieves the zone from the masters, parses it and sets it live.
+func (z *Zone) TransferIn() error {
+ if len(z.TransferFrom) == 0 {
+ return nil
+ }
+ m := new(dns.Msg)
+ m.SetAxfr(z.origin)
+
+ z1 := z.Copy()
+ var (
+ Err error
+ tr string
+ )
+
+Transfer:
+ for _, tr = range z.TransferFrom {
+ t := new(dns.Transfer)
+ c, err := t.In(m, tr)
+ if err != nil {
+ log.Printf("[ERROR] Failed to setup transfer `%s' with `%q': %v", z.origin, tr, err)
+ Err = err
+ continue Transfer
+ }
+ for env := range c {
+ if env.Error != nil {
+ log.Printf("[ERROR] Failed to transfer `%s' from %q: %v", z.origin, tr, env.Error)
+ Err = env.Error
+ continue Transfer
+ }
+ for _, rr := range env.RR {
+ if err := z1.Insert(rr); err != nil {
+ log.Printf("[ERROR] Failed to parse transfer `%s' from: %q: %v", z.origin, tr, err)
+ Err = err
+ continue Transfer
+ }
+ }
+ }
+ Err = nil
+ break
+ }
+ if Err != nil {
+ return Err
+ }
+
+ z.Tree = z1.Tree
+ z.Apex = z1.Apex
+ *z.Expired = false
+ log.Printf("[INFO] Transferred: %s from %s", z.origin, tr)
+ return nil
+}
+
+// shouldTransfer checks the primaries of zone, retrieves the SOA record, checks the current serial
+// and the remote serial and will return true if the remote one is higher than the locally configured one.
+func (z *Zone) shouldTransfer() (bool, error) {
+ c := new(dns.Client)
+ c.Net = "tcp" // do this query over TCP to minimize spoofing
+ m := new(dns.Msg)
+ m.SetQuestion(z.origin, dns.TypeSOA)
+
+ var Err error
+ serial := -1
+
+Transfer:
+ for _, tr := range z.TransferFrom {
+ Err = nil
+ ret, _, err := c.Exchange(m, tr)
+ if err != nil || ret.Rcode != dns.RcodeSuccess {
+ Err = err
+ continue
+ }
+ for _, a := range ret.Answer {
+ if a.Header().Rrtype == dns.TypeSOA {
+ serial = int(a.(*dns.SOA).Serial)
+ break Transfer
+ }
+ }
+ }
+ if serial == -1 {
+ return false, Err
+ }
+ if z.Apex.SOA == nil {
+ return true, Err
+ }
+ return less(z.Apex.SOA.Serial, uint32(serial)), Err
+}
+
+// less return true of a is smaller than b when taking RFC 1982 serial arithmetic into account.
+func less(a, b uint32) bool {
+ if a < b {
+ return (b - a) <= MaxSerialIncrement
+ }
+ return (a - b) > MaxSerialIncrement
+}
+
+// Update updates the secondary zone according to its SOA. It will run for the life time of the server
+// and uses the SOA parameters. Every refresh it will check for a new SOA number. If that fails (for all
+// server) it wil retry every retry interval. If the zone failed to transfer before the expire, the zone
+// will be marked expired.
+func (z *Zone) Update() error {
+ // If we don't have a SOA, we don't have a zone, wait for it to appear.
+ for z.Apex.SOA == nil {
+ time.Sleep(1 * time.Second)
+ }
+ retryActive := false
+
+Restart:
+ refresh := time.Second * time.Duration(z.Apex.SOA.Refresh)
+ retry := time.Second * time.Duration(z.Apex.SOA.Retry)
+ expire := time.Second * time.Duration(z.Apex.SOA.Expire)
+
+ if refresh < time.Hour {
+ refresh = time.Hour
+ }
+ if retry < time.Hour {
+ retry = time.Hour
+ }
+ if refresh > 24*time.Hour {
+ refresh = 24 * time.Hour
+ }
+ if retry > 12*time.Hour {
+ retry = 12 * time.Hour
+ }
+
+ refreshTicker := time.NewTicker(refresh)
+ retryTicker := time.NewTicker(retry)
+ expireTicker := time.NewTicker(expire)
+
+ for {
+ select {
+ case <-expireTicker.C:
+ if !retryActive {
+ break
+ }
+ *z.Expired = true
+
+ case <-retryTicker.C:
+ if !retryActive {
+ break
+ }
+
+ time.Sleep(jitter(2000)) // 2s randomize
+
+ ok, err := z.shouldTransfer()
+ if err != nil && ok {
+ if err := z.TransferIn(); err != nil {
+ // transfer failed, leave retryActive true
+ break
+ }
+ retryActive = false
+ // transfer OK, possible new SOA, stop timers and redo
+ refreshTicker.Stop()
+ retryTicker.Stop()
+ expireTicker.Stop()
+ goto Restart
+ }
+
+ case <-refreshTicker.C:
+
+ time.Sleep(jitter(5000)) // 5s randomize
+
+ ok, err := z.shouldTransfer()
+ retryActive = err != nil
+ if err != nil && ok {
+ if err := z.TransferIn(); err != nil {
+ // transfer failed
+ retryActive = true
+ break
+ }
+ retryActive = false
+ // transfer OK, possible new SOA, stop timers and redo
+ refreshTicker.Stop()
+ retryTicker.Stop()
+ expireTicker.Stop()
+ goto Restart
+ }
+ }
+ }
+}
+
+// jitter returns a random duration between [0,n) * time.Millisecond
+func jitter(n int) time.Duration {
+ r := rand.Intn(n)
+ return time.Duration(r) * time.Millisecond
+
+}
+
+// MaxSerialIncrement is the maximum difference between two serial numbers. If the difference between
+// two serials is greater than this number, the smaller one is considered greater.
+const MaxSerialIncrement uint32 = 2147483647
diff --git a/plugin/file/secondary_test.go b/plugin/file/secondary_test.go
new file mode 100644
index 000000000..8f2c2e15f
--- /dev/null
+++ b/plugin/file/secondary_test.go
@@ -0,0 +1,168 @@
+package file
+
+import (
+ "fmt"
+ "io/ioutil"
+ "log"
+ "testing"
+
+ "github.com/coredns/coredns/plugin/test"
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+)
+
+// TODO(miek): should test notifies as well, ie start test server (a real coredns one)...
+// setup other test server that sends notify, see if CoreDNS comes calling for a zone
+// tranfer
+
+func TestLess(t *testing.T) {
+ const (
+ min = 0
+ max = 4294967295
+ low = 12345
+ high = 4000000000
+ )
+
+ if less(min, max) {
+ t.Fatalf("less: should be false")
+ }
+ if !less(max, min) {
+ t.Fatalf("less: should be true")
+ }
+ if !less(high, low) {
+ t.Fatalf("less: should be true")
+ }
+ if !less(7, 9) {
+ t.Fatalf("less; should be true")
+ }
+}
+
+type soa struct {
+ serial uint32
+}
+
+func (s *soa) Handler(w dns.ResponseWriter, req *dns.Msg) {
+ m := new(dns.Msg)
+ m.SetReply(req)
+ switch req.Question[0].Qtype {
+ case dns.TypeSOA:
+ m.Answer = make([]dns.RR, 1)
+ m.Answer[0] = test.SOA(fmt.Sprintf("%s IN SOA bla. bla. %d 0 0 0 0 ", testZone, s.serial))
+ w.WriteMsg(m)
+ case dns.TypeAXFR:
+ m.Answer = make([]dns.RR, 4)
+ m.Answer[0] = test.SOA(fmt.Sprintf("%s IN SOA bla. bla. %d 0 0 0 0 ", testZone, s.serial))
+ m.Answer[1] = test.A(fmt.Sprintf("%s IN A 127.0.0.1", testZone))
+ m.Answer[2] = test.A(fmt.Sprintf("%s IN A 127.0.0.1", testZone))
+ m.Answer[3] = test.SOA(fmt.Sprintf("%s IN SOA bla. bla. %d 0 0 0 0 ", testZone, s.serial))
+ w.WriteMsg(m)
+ }
+}
+
+func (s *soa) TransferHandler(w dns.ResponseWriter, req *dns.Msg) {
+ m := new(dns.Msg)
+ m.SetReply(req)
+ m.Answer = make([]dns.RR, 1)
+ m.Answer[0] = test.SOA(fmt.Sprintf("%s IN SOA bla. bla. %d 0 0 0 0 ", testZone, s.serial))
+ w.WriteMsg(m)
+}
+
+const testZone = "secondary.miek.nl."
+
+func TestShouldTransfer(t *testing.T) {
+ soa := soa{250}
+ log.SetOutput(ioutil.Discard)
+
+ dns.HandleFunc(testZone, soa.Handler)
+ defer dns.HandleRemove(testZone)
+
+ s, addrstr, err := test.TCPServer("127.0.0.1:0")
+ if err != nil {
+ t.Fatalf("unable to run test server: %v", err)
+ }
+ defer s.Shutdown()
+
+ z := new(Zone)
+ z.origin = testZone
+ z.TransferFrom = []string{addrstr}
+
+ // when we have a nil SOA (initial state)
+ should, err := z.shouldTransfer()
+ if err != nil {
+ t.Fatalf("unable to run shouldTransfer: %v", err)
+ }
+ if !should {
+ t.Fatalf("shouldTransfer should return true for serial: %d", soa.serial)
+ }
+ // Serial smaller
+ z.Apex.SOA = test.SOA(fmt.Sprintf("%s IN SOA bla. bla. %d 0 0 0 0 ", testZone, soa.serial-1))
+ should, err = z.shouldTransfer()
+ if err != nil {
+ t.Fatalf("unable to run shouldTransfer: %v", err)
+ }
+ if !should {
+ t.Fatalf("shouldTransfer should return true for serial: %q", soa.serial-1)
+ }
+ // Serial equal
+ z.Apex.SOA = test.SOA(fmt.Sprintf("%s IN SOA bla. bla. %d 0 0 0 0 ", testZone, soa.serial))
+ should, err = z.shouldTransfer()
+ if err != nil {
+ t.Fatalf("unable to run shouldTransfer: %v", err)
+ }
+ if should {
+ t.Fatalf("shouldTransfer should return false for serial: %d", soa.serial)
+ }
+}
+
+func TestTransferIn(t *testing.T) {
+ soa := soa{250}
+ log.SetOutput(ioutil.Discard)
+
+ dns.HandleFunc(testZone, soa.Handler)
+ defer dns.HandleRemove(testZone)
+
+ s, addrstr, err := test.TCPServer("127.0.0.1:0")
+ if err != nil {
+ t.Fatalf("unable to run test server: %v", err)
+ }
+ defer s.Shutdown()
+
+ z := new(Zone)
+ z.Expired = new(bool)
+ z.origin = testZone
+ z.TransferFrom = []string{addrstr}
+
+ err = z.TransferIn()
+ if err != nil {
+ t.Fatalf("unable to run TransferIn: %v", err)
+ }
+ if z.Apex.SOA.String() != fmt.Sprintf("%s 3600 IN SOA bla. bla. 250 0 0 0 0", testZone) {
+ t.Fatalf("unknown SOA transferred")
+ }
+}
+
+func TestIsNotify(t *testing.T) {
+ z := new(Zone)
+ z.Expired = new(bool)
+ z.origin = testZone
+ state := newRequest(testZone, dns.TypeSOA)
+ // need to set opcode
+ state.Req.Opcode = dns.OpcodeNotify
+
+ z.TransferFrom = []string{"10.240.0.1:53"} // IP from from testing/responseWriter
+ if !z.isNotify(state) {
+ t.Fatal("should have been valid notify")
+ }
+ z.TransferFrom = []string{"10.240.0.2:53"}
+ if z.isNotify(state) {
+ t.Fatal("should have been invalid notify")
+ }
+}
+
+func newRequest(zone string, qtype uint16) request.Request {
+ m := new(dns.Msg)
+ m.SetQuestion("example.com.", dns.TypeA)
+ m.SetEdns0(4097, true)
+ return request.Request{W: &test.ResponseWriter{}, Req: m}
+}
diff --git a/plugin/file/setup.go b/plugin/file/setup.go
new file mode 100644
index 000000000..bf0523c54
--- /dev/null
+++ b/plugin/file/setup.go
@@ -0,0 +1,171 @@
+package file
+
+import (
+ "fmt"
+ "os"
+ "path"
+
+ "github.com/coredns/coredns/core/dnsserver"
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/pkg/dnsutil"
+ "github.com/coredns/coredns/plugin/proxy"
+
+ "github.com/mholt/caddy"
+)
+
+func init() {
+ caddy.RegisterPlugin("file", caddy.Plugin{
+ ServerType: "dns",
+ Action: setup,
+ })
+}
+
+func setup(c *caddy.Controller) error {
+ zones, err := fileParse(c)
+ if err != nil {
+ return plugin.Error("file", err)
+ }
+
+ // Add startup functions to notify the master(s).
+ for _, n := range zones.Names {
+ z := zones.Z[n]
+ c.OnStartup(func() error {
+ z.StartupOnce.Do(func() {
+ if len(z.TransferTo) > 0 {
+ z.Notify()
+ }
+ z.Reload()
+ })
+ return nil
+ })
+ }
+
+ dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
+ return File{Next: next, Zones: zones}
+ })
+
+ return nil
+}
+
+func fileParse(c *caddy.Controller) (Zones, error) {
+ z := make(map[string]*Zone)
+ names := []string{}
+ origins := []string{}
+
+ config := dnsserver.GetConfig(c)
+
+ for c.Next() {
+ // file db.file [zones...]
+ if !c.NextArg() {
+ return Zones{}, c.ArgErr()
+ }
+ fileName := c.Val()
+
+ origins = make([]string, len(c.ServerBlockKeys))
+ copy(origins, c.ServerBlockKeys)
+ args := c.RemainingArgs()
+ if len(args) > 0 {
+ origins = args
+ }
+
+ if !path.IsAbs(fileName) && config.Root != "" {
+ fileName = path.Join(config.Root, fileName)
+ }
+
+ reader, err := os.Open(fileName)
+ if err != nil {
+ // bail out
+ return Zones{}, err
+ }
+
+ for i := range origins {
+ origins[i] = plugin.Host(origins[i]).Normalize()
+ zone, err := Parse(reader, origins[i], fileName, 0)
+ if err == nil {
+ z[origins[i]] = zone
+ } else {
+ return Zones{}, err
+ }
+ names = append(names, origins[i])
+ }
+
+ noReload := false
+ prxy := proxy.Proxy{}
+ t := []string{}
+ var e error
+
+ for c.NextBlock() {
+ switch c.Val() {
+ case "transfer":
+ t, _, e = TransferParse(c, false)
+ if e != nil {
+ return Zones{}, e
+ }
+
+ case "no_reload":
+ noReload = true
+
+ case "upstream":
+ args := c.RemainingArgs()
+ if len(args) == 0 {
+ return Zones{}, c.ArgErr()
+ }
+ ups, err := dnsutil.ParseHostPortOrFile(args...)
+ if err != nil {
+ return Zones{}, err
+ }
+ prxy = proxy.NewLookup(ups)
+ default:
+ return Zones{}, c.Errf("unknown property '%s'", c.Val())
+ }
+
+ for _, origin := range origins {
+ if t != nil {
+ z[origin].TransferTo = append(z[origin].TransferTo, t...)
+ }
+ z[origin].NoReload = noReload
+ z[origin].Proxy = prxy
+ }
+ }
+ }
+ return Zones{Z: z, Names: names}, nil
+}
+
+// TransferParse parses transfer statements: 'transfer to [address...]'.
+func TransferParse(c *caddy.Controller, secondary bool) (tos, froms []string, err error) {
+ if !c.NextArg() {
+ return nil, nil, c.ArgErr()
+ }
+ value := c.Val()
+ switch value {
+ case "to":
+ tos = c.RemainingArgs()
+ for i := range tos {
+ if tos[i] != "*" {
+ normalized, err := dnsutil.ParseHostPort(tos[i], "53")
+ if err != nil {
+ return nil, nil, err
+ }
+ tos[i] = normalized
+ }
+ }
+
+ case "from":
+ if !secondary {
+ return nil, nil, fmt.Errorf("can't use `transfer from` when not being a secondary")
+ }
+ froms = c.RemainingArgs()
+ for i := range froms {
+ if froms[i] != "*" {
+ normalized, err := dnsutil.ParseHostPort(froms[i], "53")
+ if err != nil {
+ return nil, nil, err
+ }
+ froms[i] = normalized
+ } else {
+ return nil, nil, fmt.Errorf("can't use '*' in transfer from")
+ }
+ }
+ }
+ return
+}
diff --git a/plugin/file/setup_test.go b/plugin/file/setup_test.go
new file mode 100644
index 000000000..62e8476f6
--- /dev/null
+++ b/plugin/file/setup_test.go
@@ -0,0 +1,77 @@
+package file
+
+import (
+ "testing"
+
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/mholt/caddy"
+)
+
+func TestFileParse(t *testing.T) {
+ zoneFileName1, rm, err := test.TempFile(".", dbMiekNL)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer rm()
+
+ zoneFileName2, rm, err := test.TempFile(".", dbDnssexNLSigned)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer rm()
+
+ tests := []struct {
+ inputFileRules string
+ shouldErr bool
+ expectedZones Zones
+ }{
+ {
+ `file ` + zoneFileName1 + ` miek.nl {
+ transfer from 127.0.0.1
+ }`,
+ true,
+ Zones{},
+ },
+ {
+ `file`,
+ true,
+ Zones{},
+ },
+ {
+ `file ` + zoneFileName1 + ` miek.nl.`,
+ false,
+ Zones{Names: []string{"miek.nl."}},
+ },
+ {
+ `file ` + zoneFileName2 + ` dnssex.nl.`,
+ false,
+ Zones{Names: []string{"dnssex.nl."}},
+ },
+ {
+ `file ` + zoneFileName2 + ` 10.0.0.0/8`,
+ false,
+ Zones{Names: []string{"10.in-addr.arpa."}},
+ },
+ }
+
+ for i, test := range tests {
+ c := caddy.NewTestController("dns", test.inputFileRules)
+ actualZones, err := fileParse(c)
+
+ if err == nil && test.shouldErr {
+ t.Fatalf("Test %d expected errors, but got no error", i)
+ } else if err != nil && !test.shouldErr {
+ t.Fatalf("Test %d expected no errors, but got '%v'", i, err)
+ } else {
+ if len(actualZones.Names) != len(test.expectedZones.Names) {
+ t.Fatalf("Test %d expected %v, got %v", i, test.expectedZones.Names, actualZones.Names)
+ }
+ for j, name := range test.expectedZones.Names {
+ if actualZones.Names[j] != name {
+ t.Fatalf("Test %d expected %v for %d th zone, got %v", i, name, j, actualZones.Names[j])
+ }
+ }
+ }
+ }
+}
diff --git a/plugin/file/tree/all.go b/plugin/file/tree/all.go
new file mode 100644
index 000000000..fd806365f
--- /dev/null
+++ b/plugin/file/tree/all.go
@@ -0,0 +1,48 @@
+package tree
+
+// All traverses tree and returns all elements
+func (t *Tree) All() []*Elem {
+ if t.Root == nil {
+ return nil
+ }
+ found := t.Root.all(nil)
+ return found
+}
+
+func (n *Node) all(found []*Elem) []*Elem {
+ if n.Left != nil {
+ found = n.Left.all(found)
+ }
+ found = append(found, n.Elem)
+ if n.Right != nil {
+ found = n.Right.all(found)
+ }
+ return found
+}
+
+// Do performs fn on all values stored in the tree. A boolean is returned indicating whether the
+// Do traversal was interrupted by an Operation returning true. If fn alters stored values' sort
+// relationships, future tree operation behaviors are undefined.
+func (t *Tree) Do(fn func(e *Elem) bool) bool {
+ if t.Root == nil {
+ return false
+ }
+ return t.Root.do(fn)
+}
+
+func (n *Node) do(fn func(e *Elem) bool) (done bool) {
+ if n.Left != nil {
+ done = n.Left.do(fn)
+ if done {
+ return
+ }
+ }
+ done = fn(n.Elem)
+ if done {
+ return
+ }
+ if n.Right != nil {
+ done = n.Right.do(fn)
+ }
+ return
+}
diff --git a/plugin/file/tree/elem.go b/plugin/file/tree/elem.go
new file mode 100644
index 000000000..6317cc912
--- /dev/null
+++ b/plugin/file/tree/elem.go
@@ -0,0 +1,136 @@
+package tree
+
+import "github.com/miekg/dns"
+
+// Elem is an element in the tree.
+type Elem struct {
+ m map[uint16][]dns.RR
+ name string // owner name
+}
+
+// newElem returns a new elem.
+func newElem(rr dns.RR) *Elem {
+ e := Elem{m: make(map[uint16][]dns.RR)}
+ e.m[rr.Header().Rrtype] = []dns.RR{rr}
+ return &e
+}
+
+// Types returns the RRs with type qtype from e. If qname is given (only the
+// first one is used), the RR are copied and the owner is replaced with qname[0].
+func (e *Elem) Types(qtype uint16, qname ...string) []dns.RR {
+ rrs := e.m[qtype]
+
+ if rrs != nil && len(qname) > 0 {
+ copied := make([]dns.RR, len(rrs))
+ for i := range rrs {
+ copied[i] = dns.Copy(rrs[i])
+ copied[i].Header().Name = qname[0]
+ }
+ return copied
+ }
+ return rrs
+}
+
+// All returns all RRs from e, regardless of type.
+func (e *Elem) All() []dns.RR {
+ list := []dns.RR{}
+ for _, rrs := range e.m {
+ list = append(list, rrs...)
+ }
+ return list
+}
+
+// Name returns the name for this node.
+func (e *Elem) Name() string {
+ if e.name != "" {
+ return e.name
+ }
+ for _, rrs := range e.m {
+ e.name = rrs[0].Header().Name
+ return e.name
+ }
+ return ""
+}
+
+// Empty returns true is e does not contain any RRs, i.e. is an
+// empty-non-terminal.
+func (e *Elem) Empty() bool {
+ return len(e.m) == 0
+}
+
+// Insert inserts rr into e. If rr is equal to existing rrs this is a noop.
+func (e *Elem) Insert(rr dns.RR) {
+ t := rr.Header().Rrtype
+ if e.m == nil {
+ e.m = make(map[uint16][]dns.RR)
+ e.m[t] = []dns.RR{rr}
+ return
+ }
+ rrs, ok := e.m[t]
+ if !ok {
+ e.m[t] = []dns.RR{rr}
+ return
+ }
+ for _, er := range rrs {
+ if equalRdata(er, rr) {
+ return
+ }
+ }
+
+ rrs = append(rrs, rr)
+ e.m[t] = rrs
+}
+
+// Delete removes rr from e. When e is empty after the removal the returned bool is true.
+func (e *Elem) Delete(rr dns.RR) (empty bool) {
+ if e.m == nil {
+ return true
+ }
+
+ t := rr.Header().Rrtype
+ rrs, ok := e.m[t]
+ if !ok {
+ return
+ }
+
+ for i, er := range rrs {
+ if equalRdata(er, rr) {
+ rrs = removeFromSlice(rrs, i)
+ e.m[t] = rrs
+ empty = len(rrs) == 0
+ if empty {
+ delete(e.m, t)
+ }
+ return
+ }
+ }
+ return
+}
+
+// Less is a tree helper function that calls less.
+func Less(a *Elem, name string) int { return less(name, a.Name()) }
+
+// Assuming the same type and name this will check if the rdata is equal as well.
+func equalRdata(a, b dns.RR) bool {
+ switch x := a.(type) {
+ // TODO(miek): more types, i.e. all types. + tests for this.
+ case *dns.A:
+ return x.A.Equal(b.(*dns.A).A)
+ case *dns.AAAA:
+ return x.AAAA.Equal(b.(*dns.AAAA).AAAA)
+ case *dns.MX:
+ if x.Mx == b.(*dns.MX).Mx && x.Preference == b.(*dns.MX).Preference {
+ return true
+ }
+ }
+ return false
+}
+
+// removeFromSlice removes index i from the slice.
+func removeFromSlice(rrs []dns.RR, i int) []dns.RR {
+ if i >= len(rrs) {
+ return rrs
+ }
+ rrs = append(rrs[:i], rrs[i+1:]...)
+ return rrs
+}
diff --git a/plugin/file/tree/less.go b/plugin/file/tree/less.go
new file mode 100644
index 000000000..3b8340088
--- /dev/null
+++ b/plugin/file/tree/less.go
@@ -0,0 +1,59 @@
+package tree
+
+import (
+ "bytes"
+
+ "github.com/miekg/dns"
+)
+
+// less returns <0 when a is less than b, 0 when they are equal and
+// >0 when a is larger than b.
+// The function orders names in DNSSEC canonical order: RFC 4034s section-6.1
+//
+// See http://bert-hubert.blogspot.co.uk/2015/10/how-to-do-fast-canonical-ordering-of.html
+// for a blog article on this implementation, although here we still go label by label.
+//
+// The values of a and b are *not* lowercased before the comparison!
+func less(a, b string) int {
+ i := 1
+ aj := len(a)
+ bj := len(b)
+ for {
+ ai, oka := dns.PrevLabel(a, i)
+ bi, okb := dns.PrevLabel(b, i)
+ if oka && okb {
+ return 0
+ }
+
+ // sadly this []byte will allocate... TODO(miek): check if this is needed
+ // for a name, otherwise compare the strings.
+ ab := []byte(a[ai:aj])
+ bb := []byte(b[bi:bj])
+ doDDD(ab)
+ doDDD(bb)
+
+ res := bytes.Compare(ab, bb)
+ if res != 0 {
+ return res
+ }
+
+ i++
+ aj, bj = ai, bi
+ }
+}
+
+func doDDD(b []byte) {
+ lb := len(b)
+ for i := 0; i < lb; i++ {
+ if i+3 < lb && b[i] == '\\' && isDigit(b[i+1]) && isDigit(b[i+2]) && isDigit(b[i+3]) {
+ b[i] = dddToByte(b[i:])
+ for j := i + 1; j < lb-3; j++ {
+ b[j] = b[j+3]
+ }
+ lb -= 3
+ }
+ }
+}
+
+func isDigit(b byte) bool { return b >= '0' && b <= '9' }
+func dddToByte(s []byte) byte { return (s[1]-'0')*100 + (s[2]-'0')*10 + (s[3] - '0') }
diff --git a/plugin/file/tree/less_test.go b/plugin/file/tree/less_test.go
new file mode 100644
index 000000000..ed021b66f
--- /dev/null
+++ b/plugin/file/tree/less_test.go
@@ -0,0 +1,81 @@
+package tree
+
+import (
+ "sort"
+ "strings"
+ "testing"
+)
+
+type set []string
+
+func (p set) Len() int { return len(p) }
+func (p set) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
+func (p set) Less(i, j int) bool { d := less(p[i], p[j]); return d <= 0 }
+
+func TestLess(t *testing.T) {
+ tests := []struct {
+ in []string
+ out []string
+ }{
+ {
+ []string{"aaa.powerdns.de", "bbb.powerdns.net.", "xxx.powerdns.com."},
+ []string{"xxx.powerdns.com.", "aaa.powerdns.de", "bbb.powerdns.net."},
+ },
+ {
+ []string{"aaa.POWERDNS.de", "bbb.PoweRdnS.net.", "xxx.powerdns.com."},
+ []string{"xxx.powerdns.com.", "aaa.POWERDNS.de", "bbb.PoweRdnS.net."},
+ },
+ {
+ []string{"aaa.aaaa.aa.", "aa.aaa.a.", "bbb.bbbb.bb."},
+ []string{"aa.aaa.a.", "aaa.aaaa.aa.", "bbb.bbbb.bb."},
+ },
+ {
+ []string{"aaaaa.", "aaa.", "bbb."},
+ []string{"aaa.", "aaaaa.", "bbb."},
+ },
+ {
+ []string{"a.a.a.a.", "a.a.", "a.a.a."},
+ []string{"a.a.", "a.a.a.", "a.a.a.a."},
+ },
+ {
+ []string{"example.", "z.example.", "a.example."},
+ []string{"example.", "a.example.", "z.example."},
+ },
+ {
+ []string{"a.example.", "Z.a.example.", "z.example.", "yljkjljk.a.example.", "\\001.z.example.", "example.", "*.z.example.", "\\200.z.example.", "zABC.a.EXAMPLE."},
+ []string{"example.", "a.example.", "yljkjljk.a.example.", "Z.a.example.", "zABC.a.EXAMPLE.", "z.example.", "\\001.z.example.", "*.z.example.", "\\200.z.example."},
+ },
+ {
+ // RFC3034 example.
+ []string{"a.example.", "Z.a.example.", "z.example.", "yljkjljk.a.example.", "example.", "*.z.example.", "zABC.a.EXAMPLE."},
+ []string{"example.", "a.example.", "yljkjljk.a.example.", "Z.a.example.", "zABC.a.EXAMPLE.", "z.example.", "*.z.example."},
+ },
+ }
+
+Tests:
+ for j, test := range tests {
+ // Need to lowercase these example as the Less function does lowercase for us anymore.
+ for i, b := range test.in {
+ test.in[i] = strings.ToLower(b)
+ }
+ for i, b := range test.out {
+ test.out[i] = strings.ToLower(b)
+ }
+
+ sort.Sort(set(test.in))
+ for i := 0; i < len(test.in); i++ {
+ if test.in[i] != test.out[i] {
+ t.Errorf("Test %d: expected %s, got %s\n", j, test.out[i], test.in[i])
+ n := ""
+ for k, in := range test.in {
+ if k+1 == len(test.in) {
+ n = "\n"
+ }
+ t.Logf("%s <-> %s\n%s", in, test.out[k], n)
+ }
+ continue Tests
+ }
+
+ }
+ }
+}
diff --git a/plugin/file/tree/print.go b/plugin/file/tree/print.go
new file mode 100644
index 000000000..bd86ef690
--- /dev/null
+++ b/plugin/file/tree/print.go
@@ -0,0 +1,62 @@
+package tree
+
+import "fmt"
+
+// Print prints a Tree. Main use is to aid in debugging.
+func (t *Tree) Print() {
+ if t.Root == nil {
+ fmt.Println("<nil>")
+ }
+ t.Root.print()
+}
+
+func (n *Node) print() {
+ q := newQueue()
+ q.push(n)
+
+ nodesInCurrentLevel := 1
+ nodesInNextLevel := 0
+
+ for !q.empty() {
+ do := q.pop()
+ nodesInCurrentLevel--
+
+ if do != nil {
+ fmt.Print(do.Elem.Name(), " ")
+ q.push(do.Left)
+ q.push(do.Right)
+ nodesInNextLevel += 2
+ }
+ if nodesInCurrentLevel == 0 {
+ fmt.Println()
+ }
+ nodesInCurrentLevel = nodesInNextLevel
+ nodesInNextLevel = 0
+ }
+ fmt.Println()
+}
+
+type queue []*Node
+
+// newQueue returns a new queue.
+func newQueue() queue {
+ q := queue([]*Node{})
+ return q
+}
+
+// push pushes n to the end of the queue.
+func (q *queue) push(n *Node) {
+ *q = append(*q, n)
+}
+
+// pop pops the first element off the queue.
+func (q *queue) pop() *Node {
+ n := (*q)[0]
+ *q = (*q)[1:]
+ return n
+}
+
+// empty returns true when the queue contains zero nodes.
+func (q *queue) empty() bool {
+ return len(*q) == 0
+}
diff --git a/plugin/file/tree/tree.go b/plugin/file/tree/tree.go
new file mode 100644
index 000000000..ed33c09a4
--- /dev/null
+++ b/plugin/file/tree/tree.go
@@ -0,0 +1,455 @@
+// Copyright ©2012 The bíogo Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found at the end of this file.
+
+// Package tree implements Left-Leaning Red Black trees as described by Robert Sedgewick.
+//
+// More details relating to the implementation are available at the following locations:
+//
+// http://www.cs.princeton.edu/~rs/talks/LLRB/LLRB.pdf
+// http://www.cs.princeton.edu/~rs/talks/LLRB/Java/RedBlackBST.java
+// http://www.teachsolaisgames.com/articles/balanced_left_leaning.html
+//
+// Heavily modified by Miek Gieben for use in DNS zones.
+package tree
+
+import "github.com/miekg/dns"
+
+const (
+ td234 = iota
+ bu23
+)
+
+// Operation mode of the LLRB tree.
+const mode = bu23
+
+func init() {
+ if mode != td234 && mode != bu23 {
+ panic("tree: unknown mode")
+ }
+}
+
+// A Color represents the color of a Node.
+type Color bool
+
+const (
+ // Red as false give us the defined behaviour that new nodes are red. Although this
+ // is incorrect for the root node, that is resolved on the first insertion.
+ red Color = false
+ black Color = true
+)
+
+// A Node represents a node in the LLRB tree.
+type Node struct {
+ Elem *Elem
+ Left, Right *Node
+ Color Color
+}
+
+// A Tree manages the root node of an LLRB tree. Public methods are exposed through this type.
+type Tree struct {
+ Root *Node // Root node of the tree.
+ Count int // Number of elements stored.
+}
+
+// Helper methods
+
+// color returns the effect color of a Node. A nil node returns black.
+func (n *Node) color() Color {
+ if n == nil {
+ return black
+ }
+ return n.Color
+}
+
+// (a,c)b -rotL-> ((a,)b,)c
+func (n *Node) rotateLeft() (root *Node) {
+ // Assumes: n has two children.
+ root = n.Right
+ n.Right = root.Left
+ root.Left = n
+ root.Color = n.Color
+ n.Color = red
+ return
+}
+
+// (a,c)b -rotR-> (,(,c)b)a
+func (n *Node) rotateRight() (root *Node) {
+ // Assumes: n has two children.
+ root = n.Left
+ n.Left = root.Right
+ root.Right = n
+ root.Color = n.Color
+ n.Color = red
+ return
+}
+
+// (aR,cR)bB -flipC-> (aB,cB)bR | (aB,cB)bR -flipC-> (aR,cR)bB
+func (n *Node) flipColors() {
+ // Assumes: n has two children.
+ n.Color = !n.Color
+ n.Left.Color = !n.Left.Color
+ n.Right.Color = !n.Right.Color
+}
+
+// fixUp ensures that black link balance is correct, that red nodes lean left,
+// and that 4 nodes are split in the case of BU23 and properly balanced in TD234.
+func (n *Node) fixUp() *Node {
+ if n.Right.color() == red {
+ if mode == td234 && n.Right.Left.color() == red {
+ n.Right = n.Right.rotateRight()
+ }
+ n = n.rotateLeft()
+ }
+ if n.Left.color() == red && n.Left.Left.color() == red {
+ n = n.rotateRight()
+ }
+ if mode == bu23 && n.Left.color() == red && n.Right.color() == red {
+ n.flipColors()
+ }
+ return n
+}
+
+func (n *Node) moveRedLeft() *Node {
+ n.flipColors()
+ if n.Right.Left.color() == red {
+ n.Right = n.Right.rotateRight()
+ n = n.rotateLeft()
+ n.flipColors()
+ if mode == td234 && n.Right.Right.color() == red {
+ n.Right = n.Right.rotateLeft()
+ }
+ }
+ return n
+}
+
+func (n *Node) moveRedRight() *Node {
+ n.flipColors()
+ if n.Left.Left.color() == red {
+ n = n.rotateRight()
+ n.flipColors()
+ }
+ return n
+}
+
+// Len returns the number of elements stored in the Tree.
+func (t *Tree) Len() int {
+ return t.Count
+}
+
+// Search returns the first match of qname in the Tree.
+func (t *Tree) Search(qname string) (*Elem, bool) {
+ if t.Root == nil {
+ return nil, false
+ }
+ n, res := t.Root.search(qname)
+ if n == nil {
+ return nil, res
+ }
+ return n.Elem, res
+}
+
+// search searches the tree for qname and type.
+func (n *Node) search(qname string) (*Node, bool) {
+ for n != nil {
+ switch c := Less(n.Elem, qname); {
+ case c == 0:
+ return n, true
+ case c < 0:
+ n = n.Left
+ default:
+ n = n.Right
+ }
+ }
+
+ return n, false
+}
+
+// Insert inserts rr into the Tree at the first match found
+// with e or when a nil node is reached.
+func (t *Tree) Insert(rr dns.RR) {
+ var d int
+ t.Root, d = t.Root.insert(rr)
+ t.Count += d
+ t.Root.Color = black
+}
+
+// insert inserts rr in to the tree.
+func (n *Node) insert(rr dns.RR) (root *Node, d int) {
+ if n == nil {
+ return &Node{Elem: newElem(rr)}, 1
+ } else if n.Elem == nil {
+ n.Elem = newElem(rr)
+ return n, 1
+ }
+
+ if mode == td234 {
+ if n.Left.color() == red && n.Right.color() == red {
+ n.flipColors()
+ }
+ }
+
+ switch c := Less(n.Elem, rr.Header().Name); {
+ case c == 0:
+ n.Elem.Insert(rr)
+ case c < 0:
+ n.Left, d = n.Left.insert(rr)
+ default:
+ n.Right, d = n.Right.insert(rr)
+ }
+
+ if n.Right.color() == red && n.Left.color() == black {
+ n = n.rotateLeft()
+ }
+ if n.Left.color() == red && n.Left.Left.color() == red {
+ n = n.rotateRight()
+ }
+
+ if mode == bu23 {
+ if n.Left.color() == red && n.Right.color() == red {
+ n.flipColors()
+ }
+ }
+
+ root = n
+
+ return
+}
+
+// DeleteMin deletes the node with the minimum value in the tree.
+func (t *Tree) DeleteMin() {
+ if t.Root == nil {
+ return
+ }
+ var d int
+ t.Root, d = t.Root.deleteMin()
+ t.Count += d
+ if t.Root == nil {
+ return
+ }
+ t.Root.Color = black
+}
+
+func (n *Node) deleteMin() (root *Node, d int) {
+ if n.Left == nil {
+ return nil, -1
+ }
+ if n.Left.color() == black && n.Left.Left.color() == black {
+ n = n.moveRedLeft()
+ }
+ n.Left, d = n.Left.deleteMin()
+
+ root = n.fixUp()
+
+ return
+}
+
+// DeleteMax deletes the node with the maximum value in the tree.
+func (t *Tree) DeleteMax() {
+ if t.Root == nil {
+ return
+ }
+ var d int
+ t.Root, d = t.Root.deleteMax()
+ t.Count += d
+ if t.Root == nil {
+ return
+ }
+ t.Root.Color = black
+}
+
+func (n *Node) deleteMax() (root *Node, d int) {
+ if n.Left != nil && n.Left.color() == red {
+ n = n.rotateRight()
+ }
+ if n.Right == nil {
+ return nil, -1
+ }
+ if n.Right.color() == black && n.Right.Left.color() == black {
+ n = n.moveRedRight()
+ }
+ n.Right, d = n.Right.deleteMax()
+
+ root = n.fixUp()
+
+ return
+}
+
+// Delete removes rr from the tree, is the node turns empty, that node is deleted with DeleteNode.
+func (t *Tree) Delete(rr dns.RR) {
+ if t.Root == nil {
+ return
+ }
+
+ el, _ := t.Search(rr.Header().Name)
+ if el == nil {
+ t.deleteNode(rr)
+ return
+ }
+ // Delete from this element.
+ empty := el.Delete(rr)
+ if empty {
+ t.deleteNode(rr)
+ return
+ }
+}
+
+// DeleteNode deletes the node that matches rr according to Less().
+func (t *Tree) deleteNode(rr dns.RR) {
+ if t.Root == nil {
+ return
+ }
+ var d int
+ t.Root, d = t.Root.delete(rr)
+ t.Count += d
+ if t.Root == nil {
+ return
+ }
+ t.Root.Color = black
+}
+
+func (n *Node) delete(rr dns.RR) (root *Node, d int) {
+ if Less(n.Elem, rr.Header().Name) < 0 {
+ if n.Left != nil {
+ if n.Left.color() == black && n.Left.Left.color() == black {
+ n = n.moveRedLeft()
+ }
+ n.Left, d = n.Left.delete(rr)
+ }
+ } else {
+ if n.Left.color() == red {
+ n = n.rotateRight()
+ }
+ if n.Right == nil && Less(n.Elem, rr.Header().Name) == 0 {
+ return nil, -1
+ }
+ if n.Right != nil {
+ if n.Right.color() == black && n.Right.Left.color() == black {
+ n = n.moveRedRight()
+ }
+ if Less(n.Elem, rr.Header().Name) == 0 {
+ n.Elem = n.Right.min().Elem
+ n.Right, d = n.Right.deleteMin()
+ } else {
+ n.Right, d = n.Right.delete(rr)
+ }
+ }
+ }
+
+ root = n.fixUp()
+ return
+}
+
+// Min returns the minimum value stored in the tree.
+func (t *Tree) Min() *Elem {
+ if t.Root == nil {
+ return nil
+ }
+ return t.Root.min().Elem
+}
+
+func (n *Node) min() *Node {
+ for ; n.Left != nil; n = n.Left {
+ }
+ return n
+}
+
+// Max returns the maximum value stored in the tree.
+func (t *Tree) Max() *Elem {
+ if t.Root == nil {
+ return nil
+ }
+ return t.Root.max().Elem
+}
+
+func (n *Node) max() *Node {
+ for ; n.Right != nil; n = n.Right {
+ }
+ return n
+}
+
+// Prev returns the greatest value equal to or less than the qname according to Less().
+func (t *Tree) Prev(qname string) (*Elem, bool) {
+ if t.Root == nil {
+ return nil, false
+ }
+
+ n := t.Root.floor(qname)
+ if n == nil {
+ return nil, false
+ }
+ return n.Elem, true
+}
+
+func (n *Node) floor(qname string) *Node {
+ if n == nil {
+ return nil
+ }
+ switch c := Less(n.Elem, qname); {
+ case c == 0:
+ return n
+ case c <= 0:
+ return n.Left.floor(qname)
+ default:
+ if r := n.Right.floor(qname); r != nil {
+ return r
+ }
+ }
+ return n
+}
+
+// Next returns the smallest value equal to or greater than the qname according to Less().
+func (t *Tree) Next(qname string) (*Elem, bool) {
+ if t.Root == nil {
+ return nil, false
+ }
+ n := t.Root.ceil(qname)
+ if n == nil {
+ return nil, false
+ }
+ return n.Elem, true
+}
+
+func (n *Node) ceil(qname string) *Node {
+ if n == nil {
+ return nil
+ }
+ switch c := Less(n.Elem, qname); {
+ case c == 0:
+ return n
+ case c > 0:
+ return n.Right.ceil(qname)
+ default:
+ if l := n.Left.ceil(qname); l != nil {
+ return l
+ }
+ }
+ return n
+}
+
+/*
+Copyright ©2012 The bíogo Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+* Neither the name of the bíogo project nor the names of its authors and
+ contributors may be used to endorse or promote products derived from this
+ software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
diff --git a/plugin/file/wildcard.go b/plugin/file/wildcard.go
new file mode 100644
index 000000000..9526cb53f
--- /dev/null
+++ b/plugin/file/wildcard.go
@@ -0,0 +1,13 @@
+package file
+
+import "github.com/miekg/dns"
+
+// replaceWithWildcard replaces the left most label with '*'.
+func replaceWithAsteriskLabel(qname string) (wildcard string) {
+ i, shot := dns.NextLabel(qname, 0)
+ if shot {
+ return ""
+ }
+
+ return "*." + qname[i:]
+}
diff --git a/plugin/file/wildcard_test.go b/plugin/file/wildcard_test.go
new file mode 100644
index 000000000..038d37a43
--- /dev/null
+++ b/plugin/file/wildcard_test.go
@@ -0,0 +1,289 @@
+package file
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/coredns/coredns/plugin/pkg/dnsrecorder"
+ "github.com/coredns/coredns/plugin/test"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+var wildcardTestCases = []test.Case{
+ {
+ Qname: "wild.dnssex.nl.", Qtype: dns.TypeTXT,
+ Answer: []dns.RR{
+ test.TXT(`wild.dnssex.nl. 1800 IN TXT "Doing It Safe Is Better"`),
+ },
+ Ns: dnssexAuth[:len(dnssexAuth)-1], // remove RRSIG on the end
+ },
+ {
+ Qname: "a.wild.dnssex.nl.", Qtype: dns.TypeTXT,
+ Answer: []dns.RR{
+ test.TXT(`a.wild.dnssex.nl. 1800 IN TXT "Doing It Safe Is Better"`),
+ },
+ Ns: dnssexAuth[:len(dnssexAuth)-1], // remove RRSIG on the end
+ },
+ {
+ Qname: "wild.dnssex.nl.", Qtype: dns.TypeTXT, Do: true,
+ Answer: []dns.RR{
+ test.RRSIG("wild.dnssex.nl. 1800 IN RRSIG TXT 8 2 1800 20160428190224 20160329190224 14460 dnssex.nl. FUZSTyvZfeuuOpCm"),
+ test.TXT(`wild.dnssex.nl. 1800 IN TXT "Doing It Safe Is Better"`),
+ },
+ Ns: append([]dns.RR{
+ test.NSEC("a.dnssex.nl. 14400 IN NSEC www.dnssex.nl. A AAAA RRSIG NSEC"),
+ test.RRSIG("a.dnssex.nl. 14400 IN RRSIG NSEC 8 3 14400 20160428190224 20160329190224 14460 dnssex.nl. S+UMs2ySgRaaRY"),
+ }, dnssexAuth...),
+ Extra: []dns.RR{test.OPT(4096, true)},
+ },
+ {
+ Qname: "a.wild.dnssex.nl.", Qtype: dns.TypeTXT, Do: true,
+ Answer: []dns.RR{
+ test.RRSIG("a.wild.dnssex.nl. 1800 IN RRSIG TXT 8 2 1800 20160428190224 20160329190224 14460 dnssex.nl. FUZSTyvZfeuuOpCm"),
+ test.TXT(`a.wild.dnssex.nl. 1800 IN TXT "Doing It Safe Is Better"`),
+ },
+ Ns: append([]dns.RR{
+ test.NSEC("a.dnssex.nl. 14400 IN NSEC www.dnssex.nl. A AAAA RRSIG NSEC"),
+ test.RRSIG("a.dnssex.nl. 14400 IN RRSIG NSEC 8 3 14400 20160428190224 20160329190224 14460 dnssex.nl. S+UMs2ySgRaaRY"),
+ }, dnssexAuth...),
+ Extra: []dns.RR{test.OPT(4096, true)},
+ },
+ // nodata responses
+ {
+ Qname: "wild.dnssex.nl.", Qtype: dns.TypeSRV,
+ Ns: []dns.RR{
+ test.SOA(`dnssex.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1459281744 14400 3600 604800 14400`),
+ },
+ },
+ {
+ Qname: "wild.dnssex.nl.", Qtype: dns.TypeSRV, Do: true,
+ Ns: []dns.RR{
+ // TODO(miek): needs closest encloser proof as well? This is the wrong answer
+ test.NSEC(`*.dnssex.nl. 14400 IN NSEC a.dnssex.nl. TXT RRSIG NSEC`),
+ test.RRSIG(`*.dnssex.nl. 14400 IN RRSIG NSEC 8 2 14400 20160428190224 20160329190224 14460 dnssex.nl. os6INm6q2eXknD5z8TaaDOV+Ge/Ko+2dXnKP+J1fqJzafXJVH1F0nDrcXmMlR6jlBHA=`),
+ test.RRSIG(`dnssex.nl. 1800 IN RRSIG SOA 8 2 1800 20160428190224 20160329190224 14460 dnssex.nl. CA/Y3m9hCOiKC/8ieSOv8SeP964Bq++lyH8BZJcTaabAsERs4xj5PRtcxicwQXZiF8fYUCpROlUS0YR8Cdw=`),
+ test.SOA(`dnssex.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1459281744 14400 3600 604800 14400`),
+ },
+ Extra: []dns.RR{test.OPT(4096, true)},
+ },
+}
+
+var dnssexAuth = []dns.RR{
+ test.NS("dnssex.nl. 1800 IN NS linode.atoom.net."),
+ test.NS("dnssex.nl. 1800 IN NS ns-ext.nlnetlabs.nl."),
+ test.NS("dnssex.nl. 1800 IN NS omval.tednet.nl."),
+ test.RRSIG("dnssex.nl. 1800 IN RRSIG NS 8 2 1800 20160428190224 20160329190224 14460 dnssex.nl. dLIeEvP86jj5ndkcLzhgvWixTABjWAGRTGQsPsVDFXsGMf9TGGC9FEomgkCVeNC0="),
+}
+
+func TestLookupWildcard(t *testing.T) {
+ zone, err := Parse(strings.NewReader(dbDnssexNLSigned), testzone1, "stdin", 0)
+ if err != nil {
+ t.Fatalf("Expect no error when reading zone, got %q", err)
+ }
+
+ fm := File{Next: test.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{testzone1: zone}, Names: []string{testzone1}}}
+ ctx := context.TODO()
+
+ for _, tc := range wildcardTestCases {
+ m := tc.Msg()
+
+ rec := dnsrecorder.New(&test.ResponseWriter{})
+ _, err := fm.ServeDNS(ctx, rec, m)
+ if err != nil {
+ t.Errorf("Expected no error, got %v\n", err)
+ return
+ }
+
+ resp := rec.Msg
+ test.SortAndCheck(t, resp, tc)
+ }
+}
+
+var wildcardDoubleTestCases = []test.Case{
+ {
+ Qname: "wild.w.example.org.", Qtype: dns.TypeTXT,
+ Answer: []dns.RR{
+ test.TXT(`wild.w.example.org. IN TXT "Wildcard"`),
+ },
+ Ns: exampleAuth,
+ },
+ {
+ Qname: "wild.c.example.org.", Qtype: dns.TypeTXT,
+ Answer: []dns.RR{
+ test.TXT(`wild.c.example.org. IN TXT "c Wildcard"`),
+ },
+ Ns: exampleAuth,
+ },
+ {
+ Qname: "wild.d.example.org.", Qtype: dns.TypeTXT,
+ Answer: []dns.RR{
+ test.TXT(`alias.example.org. IN TXT "Wildcard CNAME expansion"`),
+ test.CNAME(`wild.d.example.org. IN CNAME alias.example.org`),
+ },
+ Ns: exampleAuth,
+ },
+ {
+ Qname: "alias.example.org.", Qtype: dns.TypeTXT,
+ Answer: []dns.RR{
+ test.TXT(`alias.example.org. IN TXT "Wildcard CNAME expansion"`),
+ },
+ Ns: exampleAuth,
+ },
+}
+
+var exampleAuth = []dns.RR{
+ test.NS("example.org. 3600 IN NS a.iana-servers.net."),
+ test.NS("example.org. 3600 IN NS b.iana-servers.net."),
+}
+
+func TestLookupDoubleWildcard(t *testing.T) {
+ zone, err := Parse(strings.NewReader(exampleOrg), "example.org.", "stdin", 0)
+ if err != nil {
+ t.Fatalf("Expect no error when reading zone, got %q", err)
+ }
+
+ fm := File{Next: test.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{"example.org.": zone}, Names: []string{"example.org."}}}
+ ctx := context.TODO()
+
+ for _, tc := range wildcardDoubleTestCases {
+ m := tc.Msg()
+
+ rec := dnsrecorder.New(&test.ResponseWriter{})
+ _, err := fm.ServeDNS(ctx, rec, m)
+ if err != nil {
+ t.Errorf("Expected no error, got %v\n", err)
+ return
+ }
+
+ resp := rec.Msg
+ test.SortAndCheck(t, resp, tc)
+ }
+}
+
+func TestReplaceWithAsteriskLabel(t *testing.T) {
+ tests := []struct {
+ in, out string
+ }{
+ {".", ""},
+ {"miek.nl.", "*.nl."},
+ {"www.miek.nl.", "*.miek.nl."},
+ }
+
+ for _, tc := range tests {
+ got := replaceWithAsteriskLabel(tc.in)
+ if got != tc.out {
+ t.Errorf("Expected to be %s, got %s", tc.out, got)
+ }
+ }
+}
+
+var apexWildcardTestCases = []test.Case{
+ {
+ Qname: "foo.example.org.", Qtype: dns.TypeA,
+ Answer: []dns.RR{test.A(`foo.example.org. 3600 IN A 127.0.0.54`)},
+ Ns: []dns.RR{test.NS(`example.org. 3600 IN NS b.iana-servers.net.`)},
+ },
+ {
+ Qname: "bar.example.org.", Qtype: dns.TypeA,
+ Answer: []dns.RR{test.A(`bar.example.org. 3600 IN A 127.0.0.53`)},
+ Ns: []dns.RR{test.NS(`example.org. 3600 IN NS b.iana-servers.net.`)},
+ },
+}
+
+func TestLookupApexWildcard(t *testing.T) {
+ const name = "example.org."
+ zone, err := Parse(strings.NewReader(apexWildcard), name, "stdin", 0)
+ if err != nil {
+ t.Fatalf("Expect no error when reading zone, got %q", err)
+ }
+
+ fm := File{Next: test.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{name: zone}, Names: []string{name}}}
+ ctx := context.TODO()
+
+ for _, tc := range apexWildcardTestCases {
+ m := tc.Msg()
+
+ rec := dnsrecorder.New(&test.ResponseWriter{})
+ _, err := fm.ServeDNS(ctx, rec, m)
+ if err != nil {
+ t.Errorf("Expected no error, got %v\n", err)
+ return
+ }
+
+ resp := rec.Msg
+ test.SortAndCheck(t, resp, tc)
+ }
+}
+
+var multiWildcardTestCases = []test.Case{
+ {
+ Qname: "foo.example.org.", Qtype: dns.TypeA,
+ Answer: []dns.RR{test.A(`foo.example.org. 3600 IN A 127.0.0.54`)},
+ Ns: []dns.RR{test.NS(`example.org. 3600 IN NS b.iana-servers.net.`)},
+ },
+ {
+ Qname: "bar.example.org.", Qtype: dns.TypeA,
+ Answer: []dns.RR{test.A(`bar.example.org. 3600 IN A 127.0.0.53`)},
+ Ns: []dns.RR{test.NS(`example.org. 3600 IN NS b.iana-servers.net.`)},
+ },
+ {
+ Qname: "bar.intern.example.org.", Qtype: dns.TypeA,
+ Answer: []dns.RR{test.A(`bar.intern.example.org. 3600 IN A 127.0.1.52`)},
+ Ns: []dns.RR{test.NS(`example.org. 3600 IN NS b.iana-servers.net.`)},
+ },
+}
+
+func TestLookupMultiWildcard(t *testing.T) {
+ const name = "example.org."
+ zone, err := Parse(strings.NewReader(doubleWildcard), name, "stdin", 0)
+ if err != nil {
+ t.Fatalf("Expect no error when reading zone, got %q", err)
+ }
+
+ fm := File{Next: test.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{name: zone}, Names: []string{name}}}
+ ctx := context.TODO()
+
+ for _, tc := range multiWildcardTestCases {
+ m := tc.Msg()
+
+ rec := dnsrecorder.New(&test.ResponseWriter{})
+ _, err := fm.ServeDNS(ctx, rec, m)
+ if err != nil {
+ t.Errorf("Expected no error, got %v\n", err)
+ return
+ }
+
+ resp := rec.Msg
+ test.SortAndCheck(t, resp, tc)
+ }
+}
+
+const exampleOrg = `; example.org test file
+example.org. IN SOA sns.dns.icann.org. noc.dns.icann.org. 2015082541 7200 3600 1209600 3600
+example.org. IN NS b.iana-servers.net.
+example.org. IN NS a.iana-servers.net.
+example.org. IN A 127.0.0.1
+example.org. IN A 127.0.0.2
+*.w.example.org. IN TXT "Wildcard"
+a.b.c.w.example.org. IN TXT "Not a wildcard"
+*.c.example.org. IN TXT "c Wildcard"
+*.d.example.org. IN CNAME alias.example.org.
+alias.example.org. IN TXT "Wildcard CNAME expansion"
+`
+
+const apexWildcard = `; example.org test file with wildcard at apex
+example.org. IN SOA sns.dns.icann.org. noc.dns.icann.org. 2015082541 7200 3600 1209600 3600
+example.org. IN NS b.iana-servers.net.
+*.example.org. IN A 127.0.0.53
+foo.example.org. IN A 127.0.0.54
+`
+
+const doubleWildcard = `; example.org test file with wildcard at apex
+example.org. IN SOA sns.dns.icann.org. noc.dns.icann.org. 2015082541 7200 3600 1209600 3600
+example.org. IN NS b.iana-servers.net.
+*.example.org. IN A 127.0.0.53
+*.intern.example.org. IN A 127.0.1.52
+foo.example.org. IN A 127.0.0.54
+`
diff --git a/plugin/file/xfr.go b/plugin/file/xfr.go
new file mode 100644
index 000000000..4a03779ed
--- /dev/null
+++ b/plugin/file/xfr.go
@@ -0,0 +1,62 @@
+package file
+
+import (
+ "fmt"
+ "log"
+
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+// Xfr serves up an AXFR.
+type Xfr struct {
+ *Zone
+}
+
+// ServeDNS implements the plugin.Handler interface.
+func (x Xfr) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
+ state := request.Request{W: w, Req: r}
+ if !x.TransferAllowed(state) {
+ return dns.RcodeServerFailure, nil
+ }
+ if state.QType() != dns.TypeAXFR && state.QType() != dns.TypeIXFR {
+ return 0, plugin.Error(x.Name(), fmt.Errorf("xfr called with non transfer type: %d", state.QType()))
+ }
+
+ records := x.All()
+ if len(records) == 0 {
+ return dns.RcodeServerFailure, nil
+ }
+
+ ch := make(chan *dns.Envelope)
+ defer close(ch)
+ tr := new(dns.Transfer)
+ go tr.Out(w, r, ch)
+
+ j, l := 0, 0
+ records = append(records, records[0]) // add closing SOA to the end
+ log.Printf("[INFO] Outgoing transfer of %d records of zone %s to %s started", len(records), x.origin, state.IP())
+ for i, r := range records {
+ l += dns.Len(r)
+ if l > transferLength {
+ ch <- &dns.Envelope{RR: records[j:i]}
+ l = 0
+ j = i
+ }
+ }
+ if j < len(records) {
+ ch <- &dns.Envelope{RR: records[j:]}
+ }
+
+ w.Hijack()
+ // w.Close() // Client closes connection
+ return dns.RcodeSuccess, nil
+}
+
+// Name implements the plugin.Hander interface.
+func (x Xfr) Name() string { return "xfr" }
+
+const transferLength = 1000 // Start a new envelop after message reaches this size in bytes. Intentionally small to test multi envelope parsing.
diff --git a/plugin/file/xfr_test.go b/plugin/file/xfr_test.go
new file mode 100644
index 000000000..69ad68e64
--- /dev/null
+++ b/plugin/file/xfr_test.go
@@ -0,0 +1,34 @@
+package file
+
+import (
+ "fmt"
+ "strings"
+)
+
+func ExampleZone_All() {
+ zone, err := Parse(strings.NewReader(dbMiekNL), testzone, "stdin", 0)
+ if err != nil {
+ return
+ }
+ records := zone.All()
+ for _, r := range records {
+ fmt.Printf("%+v\n", r)
+ }
+ // Output
+ // xfr_test.go:15: miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400
+ // xfr_test.go:15: www.miek.nl. 1800 IN CNAME a.miek.nl.
+ // xfr_test.go:15: miek.nl. 1800 IN NS linode.atoom.net.
+ // xfr_test.go:15: miek.nl. 1800 IN NS ns-ext.nlnetlabs.nl.
+ // xfr_test.go:15: miek.nl. 1800 IN NS omval.tednet.nl.
+ // xfr_test.go:15: miek.nl. 1800 IN NS ext.ns.whyscream.net.
+ // xfr_test.go:15: miek.nl. 1800 IN MX 1 aspmx.l.google.com.
+ // xfr_test.go:15: miek.nl. 1800 IN MX 5 alt1.aspmx.l.google.com.
+ // xfr_test.go:15: miek.nl. 1800 IN MX 5 alt2.aspmx.l.google.com.
+ // xfr_test.go:15: miek.nl. 1800 IN MX 10 aspmx2.googlemail.com.
+ // xfr_test.go:15: miek.nl. 1800 IN MX 10 aspmx3.googlemail.com.
+ // xfr_test.go:15: miek.nl. 1800 IN A 139.162.196.78
+ // xfr_test.go:15: miek.nl. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735
+ // xfr_test.go:15: archive.miek.nl. 1800 IN CNAME a.miek.nl.
+ // xfr_test.go:15: a.miek.nl. 1800 IN A 139.162.196.78
+ // xfr_test.go:15: a.miek.nl. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735
+}
diff --git a/plugin/file/zone.go b/plugin/file/zone.go
new file mode 100644
index 000000000..1cef9dc3a
--- /dev/null
+++ b/plugin/file/zone.go
@@ -0,0 +1,190 @@
+package file
+
+import (
+ "fmt"
+ "net"
+ "path"
+ "strings"
+ "sync"
+
+ "github.com/coredns/coredns/plugin/file/tree"
+ "github.com/coredns/coredns/plugin/proxy"
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+)
+
+// Zone defines a structure that contains all data related to a DNS zone.
+type Zone struct {
+ origin string
+ origLen int
+ file string
+ *tree.Tree
+ Apex Apex
+
+ TransferTo []string
+ StartupOnce sync.Once
+ TransferFrom []string
+ Expired *bool
+
+ NoReload bool
+ reloadMu sync.RWMutex
+ ReloadShutdown chan bool
+ Proxy proxy.Proxy // Proxy for looking up names during the resolution process
+}
+
+// Apex contains the apex records of a zone: SOA, NS and their potential signatures.
+type Apex struct {
+ SOA *dns.SOA
+ NS []dns.RR
+ SIGSOA []dns.RR
+ SIGNS []dns.RR
+}
+
+// NewZone returns a new zone.
+func NewZone(name, file string) *Zone {
+ z := &Zone{
+ origin: dns.Fqdn(name),
+ origLen: dns.CountLabel(dns.Fqdn(name)),
+ file: path.Clean(file),
+ Tree: &tree.Tree{},
+ Expired: new(bool),
+ ReloadShutdown: make(chan bool),
+ }
+ *z.Expired = false
+
+ return z
+}
+
+// Copy copies a zone.
+func (z *Zone) Copy() *Zone {
+ z1 := NewZone(z.origin, z.file)
+ z1.TransferTo = z.TransferTo
+ z1.TransferFrom = z.TransferFrom
+ z1.Expired = z.Expired
+
+ z1.Apex = z.Apex
+ return z1
+}
+
+// Insert inserts r into z.
+func (z *Zone) Insert(r dns.RR) error {
+ r.Header().Name = strings.ToLower(r.Header().Name)
+
+ switch h := r.Header().Rrtype; h {
+ case dns.TypeNS:
+ r.(*dns.NS).Ns = strings.ToLower(r.(*dns.NS).Ns)
+
+ if r.Header().Name == z.origin {
+ z.Apex.NS = append(z.Apex.NS, r)
+ return nil
+ }
+ case dns.TypeSOA:
+ r.(*dns.SOA).Ns = strings.ToLower(r.(*dns.SOA).Ns)
+ r.(*dns.SOA).Mbox = strings.ToLower(r.(*dns.SOA).Mbox)
+
+ z.Apex.SOA = r.(*dns.SOA)
+ return nil
+ case dns.TypeNSEC3, dns.TypeNSEC3PARAM:
+ return fmt.Errorf("NSEC3 zone is not supported, dropping RR: %s for zone: %s", r.Header().Name, z.origin)
+ case dns.TypeRRSIG:
+ x := r.(*dns.RRSIG)
+ switch x.TypeCovered {
+ case dns.TypeSOA:
+ z.Apex.SIGSOA = append(z.Apex.SIGSOA, x)
+ return nil
+ case dns.TypeNS:
+ if r.Header().Name == z.origin {
+ z.Apex.SIGNS = append(z.Apex.SIGNS, x)
+ return nil
+ }
+ }
+ case dns.TypeCNAME:
+ r.(*dns.CNAME).Target = strings.ToLower(r.(*dns.CNAME).Target)
+ case dns.TypeMX:
+ r.(*dns.MX).Mx = strings.ToLower(r.(*dns.MX).Mx)
+ case dns.TypeSRV:
+ r.(*dns.SRV).Target = strings.ToLower(r.(*dns.SRV).Target)
+ }
+
+ z.Tree.Insert(r)
+ return nil
+}
+
+// Delete deletes r from z.
+func (z *Zone) Delete(r dns.RR) { z.Tree.Delete(r) }
+
+// TransferAllowed checks if incoming request for transferring the zone is allowed according to the ACLs.
+func (z *Zone) TransferAllowed(state request.Request) bool {
+ for _, t := range z.TransferTo {
+ if t == "*" {
+ return true
+ }
+ // If remote IP matches we accept.
+ remote := state.IP()
+ to, _, err := net.SplitHostPort(t)
+ if err != nil {
+ continue
+ }
+ if to == remote {
+ return true
+ }
+ }
+ // TODO(miek): future matching against IP/CIDR notations
+ return false
+}
+
+// All returns all records from the zone, the first record will be the SOA record,
+// otionally followed by all RRSIG(SOA)s.
+func (z *Zone) All() []dns.RR {
+ if !z.NoReload {
+ z.reloadMu.RLock()
+ defer z.reloadMu.RUnlock()
+ }
+
+ records := []dns.RR{}
+ allNodes := z.Tree.All()
+ for _, a := range allNodes {
+ records = append(records, a.All()...)
+ }
+
+ if len(z.Apex.SIGNS) > 0 {
+ records = append(z.Apex.SIGNS, records...)
+ }
+ records = append(z.Apex.NS, records...)
+
+ if len(z.Apex.SIGSOA) > 0 {
+ records = append(z.Apex.SIGSOA, records...)
+ }
+ return append([]dns.RR{z.Apex.SOA}, records...)
+}
+
+// Print prints the zone's tree to stdout.
+func (z *Zone) Print() {
+ z.Tree.Print()
+}
+
+// NameFromRight returns the labels from the right, staring with the
+// origin and then i labels extra. When we are overshooting the name
+// the returned boolean is set to true.
+func (z *Zone) nameFromRight(qname string, i int) (string, bool) {
+ if i <= 0 {
+ return z.origin, false
+ }
+
+ for j := 1; j <= z.origLen; j++ {
+ if _, shot := dns.PrevLabel(qname, j); shot {
+ return qname, shot
+ }
+ }
+
+ k := 0
+ shot := false
+ for j := 1; j <= i; j++ {
+ k, shot = dns.PrevLabel(qname, j+z.origLen)
+ if shot {
+ return qname, shot
+ }
+ }
+ return qname[k:], false
+}
diff --git a/plugin/file/zone_test.go b/plugin/file/zone_test.go
new file mode 100644
index 000000000..c9ff174db
--- /dev/null
+++ b/plugin/file/zone_test.go
@@ -0,0 +1,30 @@
+package file
+
+import "testing"
+
+func TestNameFromRight(t *testing.T) {
+ z := NewZone("example.org.", "stdin")
+
+ tests := []struct {
+ in string
+ labels int
+ shot bool
+ expected string
+ }{
+ {"example.org.", 0, false, "example.org."},
+ {"a.example.org.", 0, false, "example.org."},
+ {"a.example.org.", 1, false, "a.example.org."},
+ {"a.example.org.", 2, true, "a.example.org."},
+ {"a.b.example.org.", 2, false, "a.b.example.org."},
+ }
+
+ for i, tc := range tests {
+ got, shot := z.nameFromRight(tc.in, tc.labels)
+ if got != tc.expected {
+ t.Errorf("Test %d: expected %s, got %s\n", i, tc.expected, got)
+ }
+ if shot != tc.shot {
+ t.Errorf("Test %d: expected shot to be %t, got %t\n", i, tc.shot, shot)
+ }
+ }
+}