diff options
-rw-r--r-- | middleware/file/closest.go | 62 | ||||
-rw-r--r-- | middleware/file/closest_test.go | 14 | ||||
-rw-r--r-- | middleware/file/delegation_test.go | 108 | ||||
-rw-r--r-- | middleware/file/ent_test.go | 3 | ||||
-rw-r--r-- | middleware/file/example_org.go | 113 | ||||
-rw-r--r-- | middleware/file/file_test.go | 316 | ||||
-rw-r--r-- | middleware/file/lookup.go | 321 | ||||
-rw-r--r-- | middleware/file/tree/elem.go | 11 | ||||
-rw-r--r-- | middleware/file/tree/less.go | 3 | ||||
-rw-r--r-- | middleware/file/tree/less_test.go | 2 | ||||
-rw-r--r-- | middleware/file/tree/print.go | 58 | ||||
-rw-r--r-- | middleware/file/tree/tree.go | 103 | ||||
-rw-r--r-- | middleware/file/wildcard.go | 13 | ||||
-rw-r--r-- | middleware/file/wildcard_test.go | 26 | ||||
-rw-r--r-- | middleware/file/zone.go | 40 | ||||
-rw-r--r-- | middleware/file/zone_test.go | 30 | ||||
-rw-r--r-- | middleware/test/helpers.go | 3 | ||||
-rw-r--r-- | test/wildcard_test.go | 2 |
18 files changed, 631 insertions, 597 deletions
diff --git a/middleware/file/closest.go b/middleware/file/closest.go index 741327fde..2ee14e6d8 100644 --- a/middleware/file/closest.go +++ b/middleware/file/closest.go @@ -1,66 +1,24 @@ package file -import "github.com/miekg/dns" +import ( + "github.com/miekg/coredns/middleware/file/tree" + + "github.com/miekg/dns" +) // ClosestEncloser returns the closest encloser for rr. -func (z *Zone) ClosestEncloser(qname string, qtype uint16) string { - // tree/tree.go does not store a parent *Node pointer, so we can't - // just follow up the tree. TODO(miek): fix. +func (z *Zone) ClosestEncloser(qname string) (*tree.Elem, bool) { + offset, end := dns.NextLabel(qname, 0) for !end { - elem, _ := z.Tree.Search(qname, qtype) + elem, _ := z.Tree.Search(qname) if elem != nil { - return elem.Name() + return elem, true } qname = qname[offset:] offset, end = dns.NextLabel(qname, offset) } - return z.Apex.SOA.Header().Name -} - -// nameErrorProof finds the closest encloser and return an NSEC that proofs -// the wildcard does not exist and an NSEC that proofs the name does no exist. -func (z *Zone) nameErrorProof(qname string, qtype uint16) []dns.RR { - elem := z.Tree.Prev(qname) - if elem == nil { - return nil - } - nsec := z.lookupNSEC(elem, true) - nsecIndex := 0 - for i := 0; i < len(nsec); i++ { - if nsec[i].Header().Rrtype == dns.TypeNSEC { - nsecIndex = i - break - } - } - - // We do this lookup twice, once for wildcard and once for the name proof. TODO(miek): fix - ce := z.ClosestEncloser(qname, qtype) - elem = z.Tree.Prev("*." + ce) - if elem == nil { - // Root? - return nil - } - nsec1 := z.lookupNSEC(elem, true) - nsec1Index := 0 - for i := 0; i < len(nsec1); i++ { - if nsec1[i].Header().Rrtype == dns.TypeNSEC { - nsec1Index = i - break - } - } - - if len(nsec) == 0 || len(nsec1) == 0 { - return nsec - } - - // Check for duplicate NSEC. - if nsec[nsecIndex].Header().Name == nsec1[nsec1Index].Header().Name && - nsec[nsecIndex].(*dns.NSEC).NextDomain == nsec1[nsec1Index].(*dns.NSEC).NextDomain { - return nsec - } - - return append(nsec, nsec1...) + return z.Tree.Search(z.origin) } diff --git a/middleware/file/closest_test.go b/middleware/file/closest_test.go index 91b65f231..591577430 100644 --- a/middleware/file/closest_test.go +++ b/middleware/file/closest_test.go @@ -3,8 +3,6 @@ package file import ( "strings" "testing" - - "github.com/miekg/dns" ) func TestClosestEncloser(t *testing.T) { @@ -26,9 +24,15 @@ func TestClosestEncloser(t *testing.T) { } for _, tc := range tests { - ce := z.ClosestEncloser(tc.in, dns.TypeA) - if ce != tc.out { - t.Errorf("expected ce to be %s for %s, got %s", tc.out, tc.in, ce) + 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/middleware/file/delegation_test.go b/middleware/file/delegation_test.go index e9a20eb25..63ca1d264 100644 --- a/middleware/file/delegation_test.go +++ b/middleware/file/delegation_test.go @@ -30,6 +30,32 @@ var delegationTestCases = []test.Case{ 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, @@ -45,22 +71,94 @@ var delegationTestCases = []test.Case{ }, } +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"), + }, + }, +} + func TestLookupDelegation(t *testing.T) { - zone, err := Parse(strings.NewReader(dbMiekNLDelegation), testzone, "stdin") + 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") if err != nil { - t.Fatalf("expect no error when reading zone, got %q", err) + 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}}} + fm := File{Next: test.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{origin: zone}, Names: []string{origin}}} ctx := context.TODO() - for _, tc := range delegationTestCases { + 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 %v\n", err) + t.Errorf("Expected no error, got %q\n", err) return } diff --git a/middleware/file/ent_test.go b/middleware/file/ent_test.go index 324c6cfb6..4ce1403bf 100644 --- a/middleware/file/ent_test.go +++ b/middleware/file/ent_test.go @@ -31,7 +31,7 @@ var entTestCases = []test.Case{ }, } -func TestLookupENT(t *testing.T) { +func TestLookupEnt(t *testing.T) { zone, err := Parse(strings.NewReader(dbMiekENTNL), testzone, "stdin") if err != nil { t.Fatalf("expect no error when reading zone, got %q", err) @@ -73,6 +73,7 @@ func TestLookupENT(t *testing.T) { } } +// 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. ( diff --git a/middleware/file/example_org.go b/middleware/file/example_org.go new file mode 100644 index 000000000..eba18e0e4 --- /dev/null +++ b/middleware/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/middleware/file/file_test.go b/middleware/file/file_test.go index 880dd841c..768817900 100644 --- a/middleware/file/file_test.go +++ b/middleware/file/file_test.go @@ -10,319 +10,3 @@ func BenchmarkParseInsert(b *testing.B) { Parse(strings.NewReader(dbMiekENTNL), testzone, "stdin") } } - -/* -var testDir = filepath.Join(os.TempDir(), "coredns_testdir") -var ErrCustom = errors.New("Custom Error") - -// testFiles is a map with relative paths to test files as keys and file content as values. -// The map represents the following structure: -// - $TEMP/coredns_testdir/ -// '-- file1.html -// '-- dirwithindex/ -// '---- index.html -// '-- dir/ -// '---- file2.html -// '---- hidden.html -var testFiles = map[string]string{ - "file1.html": "<h1>file1.html</h1>", - filepath.Join("dirwithindex", "index.html"): "<h1>dirwithindex/index.html</h1>", - filepath.Join("dir", "file2.html"): "<h1>dir/file2.html</h1>", - filepath.Join("dir", "hidden.html"): "<h1>dir/hidden.html</h1>", -} - -// TestServeHTTP covers positive scenarios when serving files. -func TestServeHTTP(t *testing.T) { - - beforeServeHTTPTest(t) - defer afterServeHTTPTest(t) - - fileserver := FileServer(http.Dir(testDir), []string{"hidden.html"}) - - movedPermanently := "Moved Permanently" - - tests := []struct { - url string - - expectedStatus int - expectedBodyContent string - }{ - // Test 0 - access without any path - { - url: "https://foo", - expectedStatus: http.StatusNotFound, - }, - // Test 1 - access root (without index.html) - { - url: "https://foo/", - expectedStatus: http.StatusNotFound, - }, - // Test 2 - access existing file - { - url: "https://foo/file1.html", - expectedStatus: http.StatusOK, - expectedBodyContent: testFiles["file1.html"], - }, - // Test 3 - access folder with index file with trailing slash - { - url: "https://foo/dirwithindex/", - expectedStatus: http.StatusOK, - expectedBodyContent: testFiles[filepath.Join("dirwithindex", "index.html")], - }, - // Test 4 - access folder with index file without trailing slash - { - url: "https://foo/dirwithindex", - expectedStatus: http.StatusMovedPermanently, - expectedBodyContent: movedPermanently, - }, - // Test 5 - access folder without index file - { - url: "https://foo/dir/", - expectedStatus: http.StatusNotFound, - }, - // Test 6 - access folder without trailing slash - { - url: "https://foo/dir", - expectedStatus: http.StatusMovedPermanently, - expectedBodyContent: movedPermanently, - }, - // Test 6 - access file with trailing slash - { - url: "https://foo/file1.html/", - expectedStatus: http.StatusMovedPermanently, - expectedBodyContent: movedPermanently, - }, - // Test 7 - access not existing path - { - url: "https://foo/not_existing", - expectedStatus: http.StatusNotFound, - }, - // Test 8 - access a file, marked as hidden - { - url: "https://foo/dir/hidden.html", - expectedStatus: http.StatusNotFound, - }, - // Test 9 - access a index file directly - { - url: "https://foo/dirwithindex/index.html", - expectedStatus: http.StatusOK, - expectedBodyContent: testFiles[filepath.Join("dirwithindex", "index.html")], - }, - // Test 10 - send a request with query params - { - url: "https://foo/dir?param1=val", - expectedStatus: http.StatusMovedPermanently, - expectedBodyContent: movedPermanently, - }, - } - - for i, test := range tests { - responseRecorder := httptest.NewRecorder() - request, err := http.NewRequest("GET", test.url, strings.NewReader("")) - status, err := fileserver.ServeHTTP(responseRecorder, request) - - // check if error matches expectations - if err != nil { - t.Errorf(getTestPrefix(i)+"Serving file at %s failed. Error was: %v", test.url, err) - } - - // check status code - if test.expectedStatus != status { - t.Errorf(getTestPrefix(i)+"Expected status %d, found %d", test.expectedStatus, status) - } - - // check body content - if !strings.Contains(responseRecorder.Body.String(), test.expectedBodyContent) { - t.Errorf(getTestPrefix(i)+"Expected body to contain %q, found %q", test.expectedBodyContent, responseRecorder.Body.String()) - } - } - -} - -// beforeServeHTTPTest creates a test directory with the structure, defined in the variable testFiles -func beforeServeHTTPTest(t *testing.T) { - // make the root test dir - err := os.Mkdir(testDir, os.ModePerm) - if err != nil { - if !os.IsExist(err) { - t.Fatalf("Failed to create test dir. Error was: %v", err) - return - } - } - - for relFile, fileContent := range testFiles { - absFile := filepath.Join(testDir, relFile) - - // make sure the parent directories exist - parentDir := filepath.Dir(absFile) - _, err = os.Stat(parentDir) - if err != nil { - os.MkdirAll(parentDir, os.ModePerm) - } - - // now create the test files - f, err := os.Create(absFile) - if err != nil { - t.Fatalf("Failed to create test file %s. Error was: %v", absFile, err) - return - } - - // and fill them with content - _, err = f.WriteString(fileContent) - if err != nil { - t.Fatalf("Failed to write to %s. Error was: %v", absFile, err) - return - } - f.Close() - } - -} - -// afterServeHTTPTest removes the test dir and all its content -func afterServeHTTPTest(t *testing.T) { - // cleans up everything under the test dir. No need to clean the individual files. - err := os.RemoveAll(testDir) - if err != nil { - t.Fatalf("Failed to clean up test dir %s. Error was: %v", testDir, err) - } -} - -// failingFS implements the http.FileSystem interface. The Open method always returns the error, assigned to err -type failingFS struct { - err error // the error to return when Open is called - fileImpl http.File // inject the file implementation -} - -// Open returns the assigned failingFile and error -func (f failingFS) Open(path string) (http.File, error) { - return f.fileImpl, f.err -} - -// failingFile implements http.File but returns a predefined error on every Stat() method call. -type failingFile struct { - http.File - err error -} - -// Stat returns nil FileInfo and the provided error on every call -func (ff failingFile) Stat() (os.FileInfo, error) { - return nil, ff.err -} - -// Close is noop and returns no error -func (ff failingFile) Close() error { - return nil -} - -// TestServeHTTPFailingFS tests error cases where the Open function fails with various errors. -func TestServeHTTPFailingFS(t *testing.T) { - - tests := []struct { - fsErr error - expectedStatus int - expectedErr error - expectedHeaders map[string]string - }{ - { - fsErr: os.ErrNotExist, - expectedStatus: http.StatusNotFound, - expectedErr: nil, - }, - { - fsErr: os.ErrPermission, - expectedStatus: http.StatusForbidden, - expectedErr: os.ErrPermission, - }, - { - fsErr: ErrCustom, - expectedStatus: http.StatusServiceUnavailable, - expectedErr: ErrCustom, - expectedHeaders: map[string]string{"Retry-After": "5"}, - }, - } - - for i, test := range tests { - // initialize a file server with the failing FileSystem - fileserver := FileServer(failingFS{err: test.fsErr}, nil) - - // prepare the request and response - request, err := http.NewRequest("GET", "https://foo/", nil) - if err != nil { - t.Fatalf("Failed to build request. Error was: %v", err) - } - responseRecorder := httptest.NewRecorder() - - status, actualErr := fileserver.ServeHTTP(responseRecorder, request) - - // check the status - if status != test.expectedStatus { - t.Errorf(getTestPrefix(i)+"Expected status %d, found %d", test.expectedStatus, status) - } - - // check the error - if actualErr != test.expectedErr { - t.Errorf(getTestPrefix(i)+"Expected err %v, found %v", test.expectedErr, actualErr) - } - - // check the headers - a special case for server under load - if test.expectedHeaders != nil && len(test.expectedHeaders) > 0 { - for expectedKey, expectedVal := range test.expectedHeaders { - actualVal := responseRecorder.Header().Get(expectedKey) - if expectedVal != actualVal { - t.Errorf(getTestPrefix(i)+"Expected header %s: %s, found %s", expectedKey, expectedVal, actualVal) - } - } - } - } -} - -// TestServeHTTPFailingStat tests error cases where the initial Open function succeeds, but the Stat method on the opened file fails. -func TestServeHTTPFailingStat(t *testing.T) { - - tests := []struct { - statErr error - expectedStatus int - expectedErr error - }{ - { - statErr: os.ErrNotExist, - expectedStatus: http.StatusNotFound, - expectedErr: nil, - }, - { - statErr: os.ErrPermission, - expectedStatus: http.StatusForbidden, - expectedErr: os.ErrPermission, - }, - { - statErr: ErrCustom, - expectedStatus: http.StatusInternalServerError, - expectedErr: ErrCustom, - }, - } - - for i, test := range tests { - // initialize a file server. The FileSystem will not fail, but calls to the Stat method of the returned File object will - fileserver := FileServer(failingFS{err: nil, fileImpl: failingFile{err: test.statErr}}, nil) - - // prepare the request and response - request, err := http.NewRequest("GET", "https://foo/", nil) - if err != nil { - t.Fatalf("Failed to build request. Error was: %v", err) - } - responseRecorder := httptest.NewRecorder() - - status, actualErr := fileserver.ServeHTTP(responseRecorder, request) - - // check the status - if status != test.expectedStatus { - t.Errorf(getTestPrefix(i)+"Expected status %d, found %d", test.expectedStatus, status) - } - - // check the error - if actualErr != test.expectedErr { - t.Errorf(getTestPrefix(i)+"Expected err %v, found %v", test.expectedErr, actualErr) - } - } -} -*/ diff --git a/middleware/file/lookup.go b/middleware/file/lookup.go index 970d593b6..16325d3e9 100644 --- a/middleware/file/lookup.go +++ b/middleware/file/lookup.go @@ -25,165 +25,233 @@ const ( // 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(qname string, qtype uint16, do bool) ([]dns.RR, []dns.RR, []dns.RR, Result) { - if qtype == dns.TypeSOA { - if !z.NoReload { - z.reloadMu.RLock() - } - - r1, r2, r3, res := z.lookupSOA(do) - + if !z.NoReload { + z.reloadMu.RLock() + } + defer func() { if !z.NoReload { z.reloadMu.RUnlock() } - return r1, r2, r3, res + }() + + if qtype == dns.TypeSOA { + soa := z.soa(do) + return soa, nil, nil, Success } if qtype == dns.TypeNS && qname == z.origin { - if !z.NoReload { - z.reloadMu.RLock() + nsrrs := z.ns(do) + glue := z.Glue(nsrrs) + 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 and do CNAME processing (DNAME should be added as well) + // 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 } - r1, r2, r3, res := z.lookupNS(do) + 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. There can only be one wildcard, + // so when we found one, we won't look for another. - if !z.NoReload { - z.reloadMu.RUnlock() + if wildElem == nil { + wildcard := replaceWithAsteriskLabel(parts) + wildElem, _ = z.Tree.Search(wildcard) + } + + // 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 } - return r1, r2, r3, res - } - if !z.NoReload { - z.reloadMu.RLock() + // 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) + // 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++ } - elem, res := z.Tree.Search(qname, qtype) - if !z.NoReload { - z.reloadMu.RUnlock() + // What does found and !shot mean - do we ever hit it? + if found && !shot { + return nil, nil, nil, ServerFailure } - if elem == nil { - if res == tree.EmptyNonTerminal { - return z.emptyNonTerminal(qname, do) - } - return z.nameError(qname, qtype, do) - } - if res == tree.Delegation { - rrs := elem.Types(dns.TypeNS) - glue := []dns.RR{} - for _, ns := range rrs { - if dns.IsSubDomain(ns.Header().Name, ns.(*dns.NS).Ns) { - // Even with Do, this should be unsigned. - elem, res := z.Tree.SearchGlue(ns.(*dns.NS).Ns) - if res == tree.Found { - glue = append(glue, elem.Types(dns.TypeAAAA)...) - glue = append(glue, elem.Types(dns.TypeA)...) - } + // Found entire name. + if found && shot { + + if rrs := elem.Types(dns.TypeCNAME, qname); len(rrs) > 0 { + return z.searchCNAME(rrs, qtype, do) + } + + 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 } - return nil, rrs, glue, Delegation - } - rrs := elem.Types(dns.TypeCNAME, qname) - if len(rrs) > 0 { // should only ever be 1 actually; TODO(miek) check for this? - return z.lookupCNAME(rrs, qtype, do) - } + if do { + sigs := elem.Types(dns.TypeRRSIG) + sigs = signatureForSubType(sigs, qtype) + rrs = append(rrs, sigs...) + } - rrs = elem.Types(qtype, qname) - if len(rrs) == 0 { - return z.noData(elem, do) - } + return rrs, nil, nil, Success - if do { - sigs := elem.Types(dns.TypeRRSIG, qname) - sigs = signatureForSubType(sigs, qtype) - rrs = append(rrs, sigs...) } - return rrs, nil, nil, Success -} - -func (z *Zone) noData(elem *tree.Elem, do bool) ([]dns.RR, []dns.RR, []dns.RR, Result) { - soa, _, _, _ := z.lookupSOA(do) - nsec := z.lookupNSEC(elem, do) - return nil, append(soa, nsec...), nil, Success -} + // Haven't found the original name. -func (z *Zone) emptyNonTerminal(qname string, do bool) ([]dns.RR, []dns.RR, []dns.RR, Result) { - soa, _, _, _ := z.lookupSOA(do) + if wildElem != nil { - elem := z.Tree.Prev(qname) - nsec := z.lookupNSEC(elem, do) - return nil, append(soa, nsec...), nil, Success -} + if rrs := wildElem.Types(dns.TypeCNAME, qname); len(rrs) > 0 { + return z.searchCNAME(rrs, qtype, do) + } -func (z *Zone) nameError(qname string, qtype uint16, do bool) ([]dns.RR, []dns.RR, []dns.RR, Result) { - // Is there a wildcard? - ce := z.ClosestEncloser(qname, qtype) - elem, _ := z.Tree.Search("*."+ce, qtype) // use result here? + rrs := wildElem.Types(qtype, qname) - if elem != nil { - ret := elem.Types(qtype) // there can only be one of these (or zero) - switch { - case ret != nil: + // NODATA + if len(rrs) == 0 { + ret := z.soa(do) if do { - sigs := elem.Types(dns.TypeRRSIG) - sigs = signatureForSubType(sigs, qtype) - ret = append(ret, sigs...) + // Do we need to add closest encloser here as well. + // closest encloser + // ce, _ := z.ClosestEncloser(qname) + // println("CLOSEST ENCLOSER", ce.Name()) // need to add this too. + nsec := z.typeFromElem(wildElem, dns.TypeNSEC, do) + ret = append(ret, nsec...) } - ret = wildcardReplace(qname, ce, ret) - return ret, nil, nil, Success - case ret == nil: - // nodata, nsec from the wildcard - type does not exist - // nsec proof that name does not exist - // TODO(miek) + return nil, ret, nil, Success + } + if do { + sigs := wildElem.Types(dns.TypeRRSIG, qname) + sigs = signatureForSubType(sigs, qtype) + rrs = append(rrs, sigs...) + } + return rrs, nil, nil, Success } - // name error - ret := []dns.RR{z.Apex.SOA} + 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 the 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 { - ret = append(ret, z.Apex.SIGSOA...) - ret = append(ret, z.nameErrorProof(qname, qtype)...) + deny, found := z.Tree.Prev(qname) + 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...) + } + } + } + } - return nil, ret, nil, NameError +Out: + return nil, ret, nil, rcode } -func (z *Zone) lookupSOA(do bool) ([]dns.RR, []dns.RR, []dns.RR, Result) { +// 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 { - ret := append([]dns.RR{z.Apex.SOA}, z.Apex.SIGSOA...) - return ret, nil, nil, Success + sigs := elem.Types(dns.TypeRRSIG) + sigs = signatureForSubType(sigs, tp) + if len(sigs) > 0 { + rrs = append(rrs, sigs...) + } } - return []dns.RR{z.Apex.SOA}, nil, nil, Success + return rrs } -func (z *Zone) lookupNS(do bool) ([]dns.RR, []dns.RR, []dns.RR, Result) { +func (z *Zone) soa(do bool) []dns.RR { if do { - ret := append(z.Apex.NS, z.Apex.SIGNS...) - return ret, nil, nil, Success + ret := append([]dns.RR{z.Apex.SOA}, z.Apex.SIGSOA...) + return ret } - return z.Apex.NS, nil, nil, Success + return []dns.RR{z.Apex.SOA} } -// lookupNSEC looks up nsec and sigs. -func (z *Zone) lookupNSEC(elem *tree.Elem, do bool) []dns.RR { - if !do { - return nil - } - nsec := elem.Types(dns.TypeNSEC) +func (z *Zone) ns(do bool) []dns.RR { if do { - sigs := elem.Types(dns.TypeRRSIG) - sigs = signatureForSubType(sigs, dns.TypeNSEC) - if len(sigs) > 0 { - nsec = append(nsec, sigs...) - } + ret := append(z.Apex.NS, z.Apex.SIGNS...) + return ret } - return nsec + return z.Apex.NS } -func (z *Zone) lookupCNAME(rrs []dns.RR, qtype uint16, do bool) ([]dns.RR, []dns.RR, []dns.RR, Result) { - elem, _ := z.Tree.Search(rrs[0].(*dns.CNAME).Target, qtype) +func (z *Zone) searchCNAME(rrs []dns.RR, qtype uint16, do bool) ([]dns.RR, []dns.RR, []dns.RR, Result) { + elem, _ := z.Tree.Search(rrs[0].(*dns.CNAME).Target) if elem == nil { return rrs, nil, nil, Success } + // RECURSIVE SEARCH, up to 8 deep. Also: tests. targets := cnameForType(elem.All(), qtype) if do { sigs := elem.Types(dns.TypeRRSIG) @@ -205,8 +273,7 @@ func cnameForType(targets []dns.RR, origQtype uint16) []dns.RR { return ret } -// signatureForSubType range through the signature and return the correct -// ones for the subtype. +// 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 { @@ -219,13 +286,29 @@ func signatureForSubType(rrs []dns.RR, subtype uint16) []dns.RR { return sigs } -// wildcardReplace replaces the ownername with the original query name. -func wildcardReplace(qname, ce string, rrs []dns.RR) []dns.RR { - // need to copy here, otherwise we change in zone stuff - ret := make([]dns.RR, len(rrs)) - for i, r := range rrs { - ret[i] = dns.Copy(r) - ret[i].Header().Name = qname +// Glue returns any potential glue records for nsrrs. +func (z *Zone) Glue(nsrrs []dns.RR) []dns.RR { + glue := []dns.RR{} + for _, ns := range nsrrs { + if dns.IsSubDomain(ns.Header().Name, ns.(*dns.NS).Ns) { + glue = append(glue, z.searchGlue(ns.(*dns.NS).Ns)...) + } } - return ret + return glue +} + +// searchGlue looks up A and AAAA for name. +func (z *Zone) searchGlue(name string) []dns.RR { + glue := []dns.RR{} + + // A + if elem, found := z.Tree.Search(name); found { + glue = append(glue, elem.Types(dns.TypeA)...) + } + + // AAAA + if elem, found := z.Tree.Search(name); found { + glue = append(glue, elem.Types(dns.TypeAAAA)...) + } + return glue } diff --git a/middleware/file/tree/elem.go b/middleware/file/tree/elem.go index 234e2c848..6317cc912 100644 --- a/middleware/file/tree/elem.go +++ b/middleware/file/tree/elem.go @@ -52,13 +52,10 @@ func (e *Elem) Name() string { return "" } -// IsWildcard returns true if this name starts with a wildcard label (*.) -func (e *Elem) IsWildcard() bool { - n := e.Name() - if len(n) < 2 { - return false - } - return n[0] == '*' && n[1] == '.' +// 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. diff --git a/middleware/file/tree/less.go b/middleware/file/tree/less.go index 595dc9213..3b8340088 100644 --- a/middleware/file/tree/less.go +++ b/middleware/file/tree/less.go @@ -11,7 +11,7 @@ import ( // 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. +// 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 { @@ -24,6 +24,7 @@ func less(a, b string) int { 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]) diff --git a/middleware/file/tree/less_test.go b/middleware/file/tree/less_test.go index 419b75c55..ed021b66f 100644 --- a/middleware/file/tree/less_test.go +++ b/middleware/file/tree/less_test.go @@ -65,7 +65,7 @@ Tests: 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]) + 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) { diff --git a/middleware/file/tree/print.go b/middleware/file/tree/print.go new file mode 100644 index 000000000..f098e56c7 --- /dev/null +++ b/middleware/file/tree/print.go @@ -0,0 +1,58 @@ +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 + +func NewQueue() queue { + q := queue([]*Node{}) + return q +} + +func (q *queue) Push(n *Node) { + *q = append(*q, n) +} + +func (q *queue) Pop() *Node { + n := (*q)[0] + *q = (*q)[1:] + return n +} + +func (q *queue) Empty() bool { + return len(*q) == 0 +} diff --git a/middleware/file/tree/tree.go b/middleware/file/tree/tree.go index 668c19734..ed33c09a4 100644 --- a/middleware/file/tree/tree.go +++ b/middleware/file/tree/tree.go @@ -13,9 +13,6 @@ // Heavily modified by Miek Gieben for use in DNS zones. package tree -// TODO(miek): locking? lockfree would be nice. Will probably go for fine grained locking on the name level. -// TODO(miek): fix docs - import "github.com/miekg/dns" const ( @@ -23,17 +20,6 @@ const ( bu23 ) -// Result is a result of a Search. -type Result int - -// Various constants that indicated the type a resource returned. -const ( - Found Result = iota - NameError - EmptyNonTerminal - Delegation -) - // Operation mode of the LLRB tree. const mode = bu23 @@ -151,77 +137,32 @@ func (t *Tree) Len() int { return t.Count } -// Search returns the first match of qname/qtype in the Tree. -func (t *Tree) Search(qname string, qtype uint16) (*Elem, Result) { - if t.Root == nil { - return nil, NameError - } - n, res := t.Root.search(qname, qtype, false) - if n == nil { - return nil, res - } - return n.Elem, res -} - -// SearchGlue returns the first match of qname/(A/AAAA) in the Tree. -func (t *Tree) SearchGlue(qname string) (*Elem, Result) { - // TODO(miek): shouldn't need this, because when we *find* the delegation, we - // know for sure that any glue is under it. Should change the return values - // to return the node, so we can resume from those. +// Search returns the first match of qname in the Tree. +func (t *Tree) Search(qname string) (*Elem, bool) { if t.Root == nil { - return nil, NameError + return nil, false } - n, res := t.Root.search(qname, dns.TypeA, true) + n, res := t.Root.search(qname) if n == nil { return nil, res } return n.Elem, res } -// search searches the tree for qname and type. If glue is true the search *does* not -// stop when hitting NS records, but descends in search of glue. The qtype for this -// kind of search can only be AAAA or A. -func (n *Node) search(qname string, qtype uint16, glue bool) (*Node, Result) { - old := n - - var wild *Node - +// search searches the tree for qname and type. +func (n *Node) search(qname string) (*Node, bool) { for n != nil { - - // Is this a wildcard that applies to us - if n.Elem.IsWildcard() { - if dns.IsSubDomain(n.Elem.Name()[2:], qname) { - wild = n - } - } - switch c := Less(n.Elem, qname); { case c == 0: - return n, Found + return n, true case c < 0: - old = n n = n.Left default: - if !glue && n.Elem.Types(dns.TypeNS) != nil { - return n, Delegation - - } - old = n n = n.Right } } - // If we have seen a wildcard "on-the-way-to-here", we should return this wildcard - // instead. This is to be able to have a more specific RR defined *under* the wildcard. - if wild != nil { - return wild, Found - } - - if dns.CountLabel(qname) < dns.CountLabel(old.Elem.Name()) { - return n, EmptyNonTerminal - } - - return n, NameError + return n, false } // Insert inserts rr into the Tree at the first match found @@ -233,6 +174,7 @@ func (t *Tree) Insert(rr dns.RR) { 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 @@ -339,21 +281,21 @@ func (t *Tree) Delete(rr dns.RR) { return } - el, _ := t.Search(rr.Header().Name, rr.Header().Rrtype) + el, _ := t.Search(rr.Header().Name) if el == nil { - t.DeleteNode(rr) + t.deleteNode(rr) return } // Delete from this element. empty := el.Delete(rr) if empty { - t.DeleteNode(rr) + t.deleteNode(rr) return } } // DeleteNode deletes the node that matches rr according to Less(). -func (t *Tree) DeleteNode(rr dns.RR) { +func (t *Tree) deleteNode(rr dns.RR) { if t.Root == nil { return } @@ -427,15 +369,16 @@ func (n *Node) max() *Node { } // Prev returns the greatest value equal to or less than the qname according to Less(). -func (t *Tree) Prev(qname string) *Elem { +func (t *Tree) Prev(qname string) (*Elem, bool) { if t.Root == nil { - return nil + return nil, false } + n := t.Root.floor(qname) if n == nil { - return nil + return nil, false } - return n.Elem + return n.Elem, true } func (n *Node) floor(qname string) *Node { @@ -445,7 +388,7 @@ func (n *Node) floor(qname string) *Node { switch c := Less(n.Elem, qname); { case c == 0: return n - case c < 0: + case c <= 0: return n.Left.floor(qname) default: if r := n.Right.floor(qname); r != nil { @@ -456,15 +399,15 @@ func (n *Node) floor(qname string) *Node { } // Next returns the smallest value equal to or greater than the qname according to Less(). -func (t *Tree) Next(qname string) *Elem { +func (t *Tree) Next(qname string) (*Elem, bool) { if t.Root == nil { - return nil + return nil, false } n := t.Root.ceil(qname) if n == nil { - return nil + return nil, false } - return n.Elem + return n.Elem, true } func (n *Node) ceil(qname string) *Node { diff --git a/middleware/file/wildcard.go b/middleware/file/wildcard.go new file mode 100644 index 000000000..9526cb53f --- /dev/null +++ b/middleware/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/middleware/file/wildcard_test.go b/middleware/file/wildcard_test.go index 5e7251d88..b3bd5915b 100644 --- a/middleware/file/wildcard_test.go +++ b/middleware/file/wildcard_test.go @@ -51,6 +51,7 @@ var wildcardTestCases = []test.Case{ { 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. os6INm6q2eXknD5z8TpfbK00uxVbQefMvHcR/RNX/kh0xXvzAaaDOV+Ge/Ko+2dXnKP+J1LYG9ffXNpdbaQy5ygzH5F041GJst4566GdG/jt7Z7vLHYxEBTpZfxo+PLsXQXH3VTemZyuWyDfqJzafXJVH1F0nDrcXmMlR6jlBHA=`), test.RRSIG(`dnssex.nl. 1800 IN RRSIG SOA 8 2 1800 20160428190224 20160329190224 14460 dnssex.nl. CA/Y3m9hCOiKC/8ieSOv8SeP964BUdG/8MC3WtKljUosK9Z9bBGrVizDjjqgq++lyH8BZJcTaabAsERs4xj5PRtcxicwQXZACX5VYjXHQeZmCyytFU5wq2gcXSmvUH86zZzftx3RGPvn1aOoTlcvoC3iF8fYUCpROlUS0YR8Cdw=`), @@ -63,7 +64,7 @@ var wildcardTestCases = []test.Case{ func TestLookupWildcard(t *testing.T) { zone, err := Parse(strings.NewReader(dbDnssexNLSigned), testzone1, "stdin") if err != nil { - t.Fatalf("expect no error when reading zone, got %q", err) + 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}}} @@ -75,7 +76,7 @@ func TestLookupWildcard(t *testing.T) { rec := dnsrecorder.New(&test.ResponseWriter{}) _, err := fm.ServeDNS(ctx, rec, m) if err != nil { - t.Errorf("expected no error, got %v\n", err) + t.Errorf("Expected no error, got %v\n", err) return } @@ -131,7 +132,7 @@ var wildcardDoubleTestCases = []test.Case{ func TestLookupDoubleWildcard(t *testing.T) { zone, err := Parse(strings.NewReader(exampleOrg), "example.org.", "stdin") if err != nil { - t.Fatalf("expect no error when reading zone, got %q", err) + 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."}}} @@ -143,7 +144,7 @@ func TestLookupDoubleWildcard(t *testing.T) { rec := dnsrecorder.New(&test.ResponseWriter{}) _, err := fm.ServeDNS(ctx, rec, m) if err != nil { - t.Errorf("expected no error, got %v\n", err) + t.Errorf("Expected no error, got %v\n", err) return } @@ -168,6 +169,23 @@ func TestLookupDoubleWildcard(t *testing.T) { } } +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) + } + } +} + 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. diff --git a/middleware/file/zone.go b/middleware/file/zone.go index ddedc6725..3636c04bd 100644 --- a/middleware/file/zone.go +++ b/middleware/file/zone.go @@ -17,8 +17,9 @@ import ( // Zone defines a structure that contains all data related to a DNS zone. type Zone struct { - origin string - file string + origin string + origLen int + file string *tree.Tree Apex Apex @@ -44,12 +45,14 @@ type Apex struct { 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 } @@ -102,6 +105,7 @@ func (z *Zone) Insert(r dns.RR) error { case dns.TypeSRV: r.(*dns.SRV).Target = strings.ToLower(r.(*dns.SRV).Target) } + z.Tree.Insert(r) return nil } @@ -165,7 +169,7 @@ func (z *Zone) Reload() error { select { case event := <-watcher.Events: // Looks for Write and Create events. Write is obvious, Create is used when - // a file in mv-ed into this place. + // a file is mv-ed into this place. if (event.Op == fsnotify.Write || event.Op == fsnotify.Create) && path.Clean(event.Name) == z.file { reader, err := os.Open(z.file) @@ -196,3 +200,33 @@ func (z *Zone) Reload() error { }() return nil } + +// 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/middleware/file/zone_test.go b/middleware/file/zone_test.go index e7eaef4b8..c9ff174db 100644 --- a/middleware/file/zone_test.go +++ b/middleware/file/zone_test.go @@ -1,4 +1,30 @@ package file -// TODO tests here. -// see secondary_test.go for some infrastructure stuff. +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) + } + } +} diff --git a/middleware/test/helpers.go b/middleware/test/helpers.go index 66fc3c50c..339385a23 100644 --- a/middleware/test/helpers.go +++ b/middleware/test/helpers.go @@ -88,6 +88,9 @@ func NSEC(rr string) *dns.NSEC { r, _ := dns.NewRR(rr); return r.(*dns.NSEC) } // DNSKEY returns a DNSKEY record from rr. It panics on errors. func DNSKEY(rr string) *dns.DNSKEY { r, _ := dns.NewRR(rr); return r.(*dns.DNSKEY) } +// DS returns a DS record from rr. It panics on errors. +func DS(rr string) *dns.DS { r, _ := dns.NewRR(rr); return r.(*dns.DS) } + // OPT returns an OPT record with UDP buffer size set to bufsize and the DO bit set to do. func OPT(bufsize int, do bool) *dns.OPT { o := new(dns.OPT) diff --git a/test/wildcard_test.go b/test/wildcard_test.go index 3a66dd7ca..57cfdc75a 100644 --- a/test/wildcard_test.go +++ b/test/wildcard_test.go @@ -40,7 +40,7 @@ func TestLookupWildcard(t *testing.T) { p := proxy.New([]string{udp}) state := request.Request{W: &test.ResponseWriter{}, Req: new(dns.Msg)} - for _, lookup := range []string{"w.example.org.", "a.w.example.org.", "a.a.w.example.org."} { + for _, lookup := range []string{"a.w.example.org.", "a.a.w.example.org."} { resp, err := p.Lookup(state, lookup, dns.TypeTXT) if err != nil || resp == nil { t.Fatalf("Expected to receive reply, but didn't for %s", lookup) |