diff options
Diffstat (limited to 'plugin/file')
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) + } + } +} |