diff --git a/.gitignore b/.gitignore index 486c8230f..fe121cbce 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ query.log Corefile +*.swp +coredns diff --git a/core/directives.go b/core/directives.go index c6cb81592..862e87b68 100644 --- a/core/directives.go +++ b/core/directives.go @@ -60,6 +60,7 @@ var directiveOrder = []directive{ {"rewrite", setup.Rewrite}, {"loadbalance", setup.Loadbalance}, {"cache", setup.Cache}, + {"dnssec", setup.Dnssec}, {"file", setup.File}, {"secondary", setup.Secondary}, {"etcd", setup.Etcd}, diff --git a/core/setup/cache.go b/core/setup/cache.go index 0d2440cfc..f5a1cf0d9 100644 --- a/core/setup/cache.go +++ b/core/setup/cache.go @@ -27,8 +27,7 @@ func cacheParse(c *Controller) (int, []string, error) { for c.Next() { if c.Val() == "cache" { // cache [ttl] [zones..] - - origins := []string{c.ServerBlockHosts[c.ServerBlockHostIndex]} + origins := c.ServerBlockHosts args := c.RemainingArgs() if len(args) > 0 { origins = args @@ -39,7 +38,7 @@ func cacheParse(c *Controller) (int, []string, error) { origins = origins[1:] if len(origins) == 0 { // There was *only* the ttl, revert back to server block - origins = []string{c.ServerBlockHosts[c.ServerBlockHostIndex]} + origins = c.ServerBlockHosts } } } diff --git a/core/setup/chaos_test.go b/core/setup/chaos_test.go index a745545fc..8431cecef 100644 --- a/core/setup/chaos_test.go +++ b/core/setup/chaos_test.go @@ -10,7 +10,7 @@ func TestChaos(t *testing.T) { tests := []struct { input string shouldErr bool - expectedVersion string // expected veresion. + expectedVersion string // expected version. expectedAuthor string // expected author (string, although we get a map). expectedErrContent string // substring from the expected error. Empty for positive cases. }{ diff --git a/core/setup/dnssec.go b/core/setup/dnssec.go new file mode 100644 index 000000000..523f6e659 --- /dev/null +++ b/core/setup/dnssec.go @@ -0,0 +1,79 @@ +package setup + +import ( + "path" + + "github.com/miekg/coredns/middleware" + "github.com/miekg/coredns/middleware/dnssec" +) + +// Dnssec sets up the dnssec middleware. +func Dnssec(c *Controller) (middleware.Middleware, error) { + zones, keys, err := dnssecParse(c) + if err != nil { + return nil, err + } + + return func(next middleware.Handler) middleware.Handler { + return dnssec.NewDnssec(zones, keys, next) + }, nil +} + +func dnssecParse(c *Controller) ([]string, []*dnssec.DNSKEY, error) { + zones := []string{} + + keys := []*dnssec.DNSKEY{} + for c.Next() { + if c.Val() == "dnssec" { + // dnssec [zones...] + zones = c.ServerBlockHosts + args := c.RemainingArgs() + if len(args) > 0 { + zones = args + } + + for c.NextBlock() { + k, e := keyParse(c) + if e != nil { + // TODO(miek): Log and drop or something? stop startup? + continue + } + keys = append(keys, k...) + } + } + } + for i, _ := range zones { + zones[i] = middleware.Host(zones[i]).Normalize() + } + return zones, keys, nil +} + +func keyParse(c *Controller) ([]*dnssec.DNSKEY, error) { + keys := []*dnssec.DNSKEY{} + + what := c.Val() + if !c.NextArg() { + return nil, c.ArgErr() + } + value := c.Val() + switch what { + case "key": + if value == "file" { + ks := c.RemainingArgs() + for _, k := range ks { + // Kmiek.nl.+013+26205.key, handle .private or without extension: Kmiek.nl.+013+26205 + ext := path.Ext(k) // TODO(miek): test things like .key + base := k + if len(ext) > 0 { + base = k[:len(k)-len(ext)] + } + k, err := dnssec.ParseKeyFile(base+".key", base+".private") + if err != nil { + return nil, err + } + keys = append(keys, k) + } + } + } + return keys, nil +} diff --git a/core/setup/dnssec_test.go b/core/setup/dnssec_test.go new file mode 100644 index 000000000..364a363bd --- /dev/null +++ b/core/setup/dnssec_test.go @@ -0,0 +1,54 @@ +package setup + +import ( + "strings" + "testing" +) + +func TestDnssec(t *testing.T) { + tests := []struct { + input string + shouldErr bool + expectedZones []string + expectedKeys []string + expectedErrContent string + }{ + { + `dnssec`, false, nil, nil, "", + }, + { + `dnssec miek.nl`, false, []string{"miek.nl."}, nil, "", + }, + } + + for i, test := range tests { + c := NewTestController(test.input) + zones, keys, err := dnssecParse(c) + + if test.shouldErr && err == nil { + t.Errorf("Test %d: Expected error but found %s for input %s", i, err, test.input) + } + + if err != nil { + if !test.shouldErr { + t.Errorf("Test %d: Expected no error but found one for input %s. Error was: %v", i, test.input, err) + } + + if !strings.Contains(err.Error(), test.expectedErrContent) { + t.Errorf("Test %d: Expected error to contain: %v, found error: %v, input: %s", i, test.expectedErrContent, err, test.input) + } + } + if !test.shouldErr { + for i, z := range test.expectedZones { + if zones[i] != z { + t.Errorf("Dnssec not correctly set for input %s. Expected: %s, actual: %s", test.input, z, zones[i]) + } + } + for i, k := range test.expectedKeys { + if k != keys[i].K.Header().Name { + t.Errorf("Dnssec not correctly set for input %s. Expected: '%s', actual: '%s'", test.input, k, keys[i].K.Header().Name) + } + } + } + } +} diff --git a/core/setup/etcd.go b/core/setup/etcd.go index 228829008..8e7740e12 100644 --- a/core/setup/etcd.go +++ b/core/setup/etcd.go @@ -10,8 +10,8 @@ import ( "github.com/miekg/coredns/middleware" "github.com/miekg/coredns/middleware/etcd" - "github.com/miekg/coredns/middleware/etcd/singleflight" "github.com/miekg/coredns/middleware/proxy" + "github.com/miekg/coredns/middleware/singleflight" etcdc "github.com/coreos/etcd/client" "golang.org/x/net/context" diff --git a/core/setup/file.go b/core/setup/file.go index 0b33fe47f..b535332a7 100644 --- a/core/setup/file.go +++ b/core/setup/file.go @@ -46,7 +46,7 @@ func fileParse(c *Controller) (file.Zones, error) { } fileName := c.Val() - origins := []string{c.ServerBlockHosts[c.ServerBlockHostIndex]} + origins := c.ServerBlockHosts args := c.RemainingArgs() if len(args) > 0 { origins = args @@ -54,7 +54,7 @@ func fileParse(c *Controller) (file.Zones, error) { reader, err := os.Open(fileName) if err != nil { - return file.Zones{}, err + continue } for i, _ := range origins { @@ -68,7 +68,7 @@ func fileParse(c *Controller) (file.Zones, error) { noReload := false for c.NextBlock() { - t, _, e := parseTransfer(c) + t, _, e := transferParse(c) if e != nil { return file.Zones{}, e } @@ -89,8 +89,8 @@ func fileParse(c *Controller) (file.Zones, error) { return file.Zones{Z: z, Names: names}, nil } -// transfer to [address...] -func parseTransfer(c *Controller) (tos, froms []string, err error) { +// transferParse parses transfer statements: 'transfer to [address...]'. +func transferParse(c *Controller) (tos, froms []string, err error) { what := c.Val() if !c.NextArg() { return nil, nil, c.ArgErr() diff --git a/core/setup/metrics.go b/core/setup/metrics.go index 84fdadb29..262550a90 100644 --- a/core/setup/metrics.go +++ b/core/setup/metrics.go @@ -7,10 +7,7 @@ import ( "github.com/miekg/coredns/middleware/metrics" ) -const ( - path = "/metrics" - addr = "localhost:9135" // 9153 is occupied by bind_exporter -) +const addr = "localhost:9135" // 9153 is occupied by bind_exporter var once sync.Once diff --git a/core/setup/secondary.go b/core/setup/secondary.go index 0abf82c04..e1f54a651 100644 --- a/core/setup/secondary.go +++ b/core/setup/secondary.go @@ -40,7 +40,7 @@ func secondaryParse(c *Controller) (file.Zones, error) { for c.Next() { if c.Val() == "secondary" { // secondary [origin] - origins := []string{c.ServerBlockHosts[c.ServerBlockHostIndex]} + origins := c.ServerBlockHosts args := c.RemainingArgs() if len(args) > 0 { origins = args @@ -52,7 +52,7 @@ func secondaryParse(c *Controller) (file.Zones, error) { } for c.NextBlock() { - t, f, e := parseTransfer(c) + t, f, e := transferParse(c) if e != nil { return file.Zones{}, e } diff --git a/middleware/cache/cache.go b/middleware/cache/cache.go index 1ec71b047..71df68fdd 100644 --- a/middleware/cache/cache.go +++ b/middleware/cache/cache.go @@ -1,33 +1,5 @@ package cache -/* -The idea behind this implementation is as follows. We have a cache that is index -by a couple different keys, which allows use to have: - -- negative cache: qname only for NXDOMAIN responses -- negative cache: qname + qtype for NODATA responses -- positive cache: qname + qtype for succesful responses. - -We track DNSSEC responses separately, i.e. under a different cache key. -Each Item stored contains the message split up in the different sections -and a few bits of the msg header. - -For instance an NXDOMAIN for blaat.miek.nl will create the -following negative cache entry (do signal state of DO (do off, DO on)). - - ncache: do - Item: - Ns: SOA RR - -If found a return packet is assembled and returned to the client. Taking size and EDNS0 -constraints into account. - -We also need to track if the answer received was an authoritative answer, ad bit and other -setting, for this we also store a few header bits. - -For the positive cache we use the same idea. Truncated responses are never stored. -*/ - import ( "log" "time" @@ -50,41 +22,7 @@ func NewCache(ttl int, zones []string, next middleware.Handler) Cache { return Cache{Next: next, Zones: zones, cache: gcache.New(defaultDuration, purgeDuration), cap: time.Duration(ttl) * time.Second} } -type messageType int - -const ( - success messageType = iota - nameError // NXDOMAIN in header, SOA in auth. - noData // NOERROR in header, SOA in auth. - otherError // Don't cache these. -) - -// classify classifies a message, it returns the MessageType. -func classify(m *dns.Msg) (messageType, *dns.OPT) { - opt := m.IsEdns0() - soa := false - if m.Rcode == dns.RcodeSuccess { - return success, opt - } - for _, r := range m.Ns { - if r.Header().Rrtype == dns.TypeSOA { - soa = true - break - } - } - - // Check length of different section, and drop stuff that is just to large. - if soa && m.Rcode == dns.RcodeSuccess { - return noData, opt - } - if soa && m.Rcode == dns.RcodeNameError { - return nameError, opt - } - - return otherError, opt -} - -func cacheKey(m *dns.Msg, t messageType, do bool) string { +func cacheKey(m *dns.Msg, t middleware.MsgType, do bool) string { if m.Truncated { return "" } @@ -92,13 +30,15 @@ func cacheKey(m *dns.Msg, t messageType, do bool) string { qtype := m.Question[0].Qtype qname := middleware.Name(m.Question[0].Name).Normalize() switch t { - case success: + case middleware.Success: + fallthrough + case middleware.Delegation: return successKey(qname, qtype, do) - case nameError: + case middleware.NameError: return nameErrorKey(qname, do) - case noData: + case middleware.NoData: return noDataKey(qname, qtype, do) - case otherError: + case middleware.OtherError: return "" } return "" @@ -116,13 +56,13 @@ func NewCachingResponseWriter(w dns.ResponseWriter, cache *gcache.Cache, cap tim func (c *CachingResponseWriter) WriteMsg(res *dns.Msg) error { do := false - mt, opt := classify(res) + mt, opt := middleware.Classify(res) if opt != nil { do = opt.Do() } key := cacheKey(res, mt, do) - c.Set(res, key, mt) + c.set(res, key, mt) if c.cap != 0 { setCap(res, uint32(c.cap.Seconds())) @@ -131,7 +71,7 @@ func (c *CachingResponseWriter) WriteMsg(res *dns.Msg) error { return c.ResponseWriter.WriteMsg(res) } -func (c *CachingResponseWriter) Set(m *dns.Msg, key string, mt messageType) { +func (c *CachingResponseWriter) set(m *dns.Msg, key string, mt middleware.MsgType) { if key == "" { // logger the log? TODO(miek) return @@ -139,14 +79,14 @@ func (c *CachingResponseWriter) Set(m *dns.Msg, key string, mt messageType) { duration := c.cap switch mt { - case success: + case middleware.Success, middleware.Delegation: if c.cap == 0 { duration = minTtl(m.Answer, mt) } i := newItem(m, duration) c.cache.Set(key, i, duration) - case nameError, noData: + case middleware.NameError, middleware.NoData: if c.cap == 0 { duration = minTtl(m.Ns, mt) } @@ -167,19 +107,19 @@ func (c *CachingResponseWriter) Hijack() { return } -func minTtl(rrs []dns.RR, mt messageType) time.Duration { - if mt != success && mt != nameError && mt != noData { +func minTtl(rrs []dns.RR, mt middleware.MsgType) time.Duration { + if mt != middleware.Success && mt != middleware.NameError && mt != middleware.NoData { return 0 } minTtl := maxTtl for _, r := range rrs { switch mt { - case nameError, noData: + case middleware.NameError, middleware.NoData: if r.Header().Rrtype == dns.TypeSOA { return time.Duration(r.(*dns.SOA).Minttl) * time.Second } - case success: + case middleware.Success, middleware.Delegation: if r.Header().Ttl < minTtl { minTtl = r.Header().Ttl } diff --git a/middleware/cache/cache_test.go b/middleware/cache/cache_test.go index 310a1164e..452831082 100644 --- a/middleware/cache/cache_test.go +++ b/middleware/cache/cache_test.go @@ -78,13 +78,13 @@ func TestCache(t *testing.T) { m = cacheMsg(m, tc) do := tc.in.Do - mt, _ := classify(m) + mt, _ := middleware.Classify(m) key := cacheKey(m, mt, do) - crr.Set(m, key, mt) + crr.set(m, key, mt) name := middleware.Name(m.Question[0].Name).Normalize() qtype := m.Question[0].Qtype - i, ok := c.Get(name, qtype, do) + i, ok := c.get(name, qtype, do) if !ok && !m.Truncated { t.Errorf("Truncated message should not have been cached") } diff --git a/middleware/cache/handler.go b/middleware/cache/handler.go index 35443fa1d..b891d7278 100644 --- a/middleware/cache/handler.go +++ b/middleware/cache/handler.go @@ -21,7 +21,7 @@ func (c Cache) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) ( do := state.Do() // might need more from OPT record? - if i, ok := c.Get(qname, qtype, do); ok { + if i, ok := c.get(qname, qtype, do); ok { resp := i.toMsg(r) state.SizeAndDo(resp) w.WriteMsg(resp) @@ -35,12 +35,13 @@ func (c Cache) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) ( return c.Next.ServeDNS(ctx, crr, r) } -func (c Cache) Get(qname string, qtype uint16, do bool) (*item, bool) { +func (c Cache) get(qname string, qtype uint16, do bool) (*item, bool) { nxdomain := nameErrorKey(qname, do) if i, ok := c.cache.Get(nxdomain); ok { return i.(*item), true } + // TODO(miek): delegation was added double check successOrNoData := successKey(qname, qtype, do) if i, ok := c.cache.Get(successOrNoData); ok { return i.(*item), true diff --git a/middleware/classify.go b/middleware/classify.go new file mode 100644 index 000000000..72c131157 --- /dev/null +++ b/middleware/classify.go @@ -0,0 +1,52 @@ +package middleware + +import "github.com/miekg/dns" + +type MsgType int + +const ( + Success MsgType = iota + NameError // NXDOMAIN in header, SOA in auth. + NoData // NOERROR in header, SOA in auth. + Delegation // NOERROR in header, NS in auth, optionally fluff in additional (not checked). + OtherError // Don't cache these. +) + +// Classify classifies a message, it returns the MessageType. +func Classify(m *dns.Msg) (MsgType, *dns.OPT) { + opt := m.IsEdns0() + + if len(m.Answer) > 0 && m.Rcode == dns.RcodeSuccess { + return Success, opt + } + + soa := false + ns := 0 + for _, r := range m.Ns { + if r.Header().Rrtype == dns.TypeSOA { + soa = true + continue + } + if r.Header().Rrtype == dns.TypeNS { + ns++ + } + } + + // Check length of different sections, and drop stuff that is just to large? TODO(miek). + if soa && m.Rcode == dns.RcodeSuccess { + return NoData, opt + } + if soa && m.Rcode == dns.RcodeNameError { + return NameError, opt + } + + if ns > 0 && ns == len(m.Ns) && m.Rcode == dns.RcodeSuccess { + return Delegation, opt + } + + if m.Rcode == dns.RcodeSuccess { + return Success, opt + } + + return OtherError, opt +} diff --git a/middleware/classify_test.go b/middleware/classify_test.go new file mode 100644 index 000000000..26c52db55 --- /dev/null +++ b/middleware/classify_test.go @@ -0,0 +1,31 @@ +package middleware + +import ( + "testing" + + "github.com/miekg/coredns/middleware/test" + + "github.com/miekg/dns" +) + +func TestClassifyDelegation(t *testing.T) { + m := delegationMsg() + mt, _ := Classify(m) + if mt != Delegation { + t.Errorf("message is wrongly classified, expected delegation, got %d", mt) + } +} + +func delegationMsg() *dns.Msg { + return &dns.Msg{ + Ns: []dns.RR{ + test.NS("miek.nl. 3600 IN NS linode.atoom.net."), + test.NS("miek.nl. 3600 IN NS ns-ext.nlnetlabs.nl."), + test.NS("miek.nl. 3600 IN NS omval.tednet.nl."), + }, + Extra: []dns.RR{ + test.A("omval.tednet.nl. 3600 IN A 185.49.141.42"), + test.AAAA("omval.tednet.nl. 3600 IN AAAA 2a04:b900:0:100::42"), + }, + } +} diff --git a/middleware/dnssec/README.md b/middleware/dnssec/README.md new file mode 100644 index 000000000..df00866cf --- /dev/null +++ b/middleware/dnssec/README.md @@ -0,0 +1,35 @@ +# dnssec + +`dnssec` enables on-the-fly DNSSEC signing of served data. + +## Syntax + +~~~ +dnssec [zones...] +~~~ + +* `zones` zones that should be signed. If empty the zones from the configuration block + are used. + +If keys are not specified (see below) a key is generated and used for all signing operations. The +DNSSEC signing will treat this key a CSK (common signing key) forgoing the ZSK/KSK split. All +signing operations are done online. Authenticated denial of existence is implemented with NSEC black +lies. Using ECDSA as an algorithm is preferred as this leads to smaller signatures (compared to +RSA). + +A signing key can be specified by using the `key` directive. + +TODO(miek): think about key rollovers. + + +~~~ +dnssec [zones... ] { + key file [key...] +} +~~~ + +* `key file` indicates key file(s) should be read from disk. When multiple keys are specified, RRset + will be signed with all keys. Generating a key can be done with `dnssec-keygen`: `dnssec-keygen -a + ECDSAP256SHA256 `. A key created for zone *A* can be safely used for zone *B*. + +## Examples diff --git a/middleware/dnssec/black_lies.go b/middleware/dnssec/black_lies.go new file mode 100644 index 000000000..527b2fc3e --- /dev/null +++ b/middleware/dnssec/black_lies.go @@ -0,0 +1,24 @@ +package dnssec + +import "github.com/miekg/dns" + +// nsec returns an NSEC useful for NXDOMAIN respsones. +// See https://tools.ietf.org/html/draft-valsorda-dnsop-black-lies-00 +// For example, a request for the non-existing name a.example.com would +// cause the following NSEC record to be generated: +// a.example.com. 3600 IN NSEC \000.a.example.com. ( RRSIG NSEC ) +// This inturn makes every NXDOMAIN answer a NODATA one, don't forget to flip +// the header rcode to NOERROR. +func (d Dnssec) nsec(name, zone string, ttl, incep, expir uint32) ([]dns.RR, error) { + nsec := &dns.NSEC{} + nsec.Hdr = dns.RR_Header{Name: name, Ttl: ttl, Class: dns.ClassINET, Rrtype: dns.TypeNSEC} + nsec.NextDomain = "\\000." + name + nsec.TypeBitMap = []uint16{dns.TypeRRSIG, dns.TypeNSEC} + + sigs, err := d.sign([]dns.RR{nsec}, zone, ttl, incep, expir) + if err != nil { + return nil, err + } + + return append(sigs, nsec), nil +} diff --git a/middleware/dnssec/black_lies_test.go b/middleware/dnssec/black_lies_test.go new file mode 100644 index 000000000..951e8952e --- /dev/null +++ b/middleware/dnssec/black_lies_test.go @@ -0,0 +1,50 @@ +package dnssec + +import ( + "testing" + "time" + + "github.com/miekg/coredns/middleware" + "github.com/miekg/coredns/middleware/test" + + "github.com/miekg/dns" +) + +func TestZoneSigningBlackLies(t *testing.T) { + d, rm1, rm2 := newDnssec(t, []string{"miek.nl."}) + defer rm1() + defer rm2() + + m := testNxdomainMsg() + state := middleware.State{Req: m} + m = d.Sign(state, "miek.nl.", time.Now().UTC()) + if !section(m.Ns, 2) { + t.Errorf("authority section should have 2 sig") + } + var nsec *dns.NSEC + for _, r := range m.Ns { + if r.Header().Rrtype == dns.TypeNSEC { + nsec = r.(*dns.NSEC) + } + } + if m.Rcode != dns.RcodeSuccess { + t.Errorf("expected rcode %d, got %d", dns.RcodeSuccess, m.Rcode) + } + if nsec == nil { + t.Fatalf("expected NSEC, got none") + } + if nsec.Hdr.Name != "ww.miek.nl." { + t.Errorf("expected %s, got %s", "ww.miek.nl.", nsec.Hdr.Name) + } + if nsec.NextDomain != "\\000.ww.miek.nl." { + t.Errorf("expected %s, got %s", "\\000.ww.miek.nl.", nsec.NextDomain) + } + t.Logf("%+v\n", m) +} + +func testNxdomainMsg() *dns.Msg { + return &dns.Msg{MsgHdr: dns.MsgHdr{Rcode: dns.RcodeNameError}, + Question: []dns.Question{dns.Question{Name: "ww.miek.nl.", Qclass: dns.ClassINET, Qtype: dns.TypeTXT}}, + Ns: []dns.RR{test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1461471181 14400 3600 604800 14400")}, + } +} diff --git a/middleware/dnssec/cache.go b/middleware/dnssec/cache.go new file mode 100644 index 000000000..2153c84cb --- /dev/null +++ b/middleware/dnssec/cache.go @@ -0,0 +1,23 @@ +package dnssec + +import ( + "hash/fnv" + "strconv" + + "github.com/miekg/dns" +) + +// Key serializes the RRset and return a signature cache key. +func key(rrs []dns.RR) string { + h := fnv.New64() + buf := make([]byte, 256) + for _, r := range rrs { + off, err := dns.PackRR(r, buf, 0, nil, false) + if err == nil { + h.Write(buf[:off]) + } + } + + i := h.Sum64() + return strconv.FormatUint(i, 10) +} diff --git a/middleware/dnssec/cache_test.go b/middleware/dnssec/cache_test.go new file mode 100644 index 000000000..0039586d5 --- /dev/null +++ b/middleware/dnssec/cache_test.go @@ -0,0 +1,32 @@ +package dnssec + +import ( + "testing" + "time" + + "github.com/miekg/coredns/middleware" + "github.com/miekg/coredns/middleware/test" +) + +func TestCacheSet(t *testing.T) { + fPriv, rmPriv, _ := test.TempFile(t, ".", privKey) + fPub, rmPub, _ := test.TempFile(t, ".", pubKey) + defer rmPriv() + defer rmPub() + + dnskey, err := ParseKeyFile(fPub, fPriv) + if err != nil { + t.Fatalf("failed to parse key: %v\n", err) + } + + m := testMsg() + state := middleware.State{Req: m} + k := key(m.Answer) // calculate *before* we add the sig + d := NewDnssec([]string{"miek.nl."}, []*DNSKEY{dnskey}, nil) + m = d.Sign(state, "miek.nl.", time.Now().UTC()) + + _, ok := d.get(k) + if !ok { + t.Errorf("signature was not added to the cache") + } +} diff --git a/middleware/dnssec/dnskey.go b/middleware/dnssec/dnskey.go new file mode 100644 index 000000000..9ae437c54 --- /dev/null +++ b/middleware/dnssec/dnskey.go @@ -0,0 +1,71 @@ +package dnssec + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "errors" + "os" + "time" + + "github.com/miekg/coredns/middleware" + + "github.com/miekg/dns" +) + +type DNSKEY struct { + K *dns.DNSKEY + s crypto.Signer + keytag uint16 +} + +// ParseKeyFile read a DNSSEC keyfile as generated by dnssec-keygen or other +// utilities. It adds ".key" for the public key and ".private" for the private key. +func ParseKeyFile(pubFile, privFile string) (*DNSKEY, error) { + f, e := os.Open(pubFile) + if e != nil { + return nil, e + } + k, e := dns.ReadRR(f, pubFile) + if e != nil { + return nil, e + } + + f, e = os.Open(privFile) + if e != nil { + return nil, e + } + p, e := k.(*dns.DNSKEY).ReadPrivateKey(f, privFile) + if e != nil { + return nil, e + } + + if v, ok := p.(*rsa.PrivateKey); ok { + return &DNSKEY{k.(*dns.DNSKEY), v, k.(*dns.DNSKEY).KeyTag()}, nil + } + if v, ok := p.(*ecdsa.PrivateKey); ok { + return &DNSKEY{k.(*dns.DNSKEY), v, k.(*dns.DNSKEY).KeyTag()}, nil + } + return &DNSKEY{k.(*dns.DNSKEY), nil, 0}, errors.New("no known? private key found") +} + +// getDNSKEY returns the correct DNSKEY to the client. Signatures are added when do is true. +func (d Dnssec) getDNSKEY(state middleware.State, zone string, do bool) *dns.Msg { + keys := make([]dns.RR, len(d.keys)) + for i, k := range d.keys { + keys[i] = dns.Copy(k.K) + keys[i].Header().Name = zone + } + m := new(dns.Msg) + m.SetReply(state.Req) + m.Answer = keys + if !do { + return m + } + + incep, expir := incepExpir(time.Now().UTC()) + if sigs, err := d.sign(keys, zone, 3600, incep, expir); err == nil { + m.Answer = append(m.Answer, sigs...) + } + return m +} diff --git a/middleware/dnssec/dnssec.go b/middleware/dnssec/dnssec.go new file mode 100644 index 000000000..b0a328bee --- /dev/null +++ b/middleware/dnssec/dnssec.go @@ -0,0 +1,127 @@ +package dnssec + +import ( + "time" + + "github.com/miekg/coredns/middleware" + "github.com/miekg/coredns/middleware/singleflight" + + "github.com/miekg/dns" + gcache "github.com/patrickmn/go-cache" +) + +type Dnssec struct { + Next middleware.Handler + zones []string + keys []*DNSKEY + inflight *singleflight.Group + cache *gcache.Cache +} + +func NewDnssec(zones []string, keys []*DNSKEY, next middleware.Handler) Dnssec { + return Dnssec{Next: next, + zones: zones, + keys: keys, + cache: gcache.New(defaultDuration, purgeDuration), + inflight: new(singleflight.Group), + } +} + +// Sign signs the message m. it takes care of negative or nodata responses. It +// uses NSEC black lies for authenticated denial of existence. Signatures +// creates will be cached for a short while. By default we sign for 8 days, +// starting 3 hours ago. +func (d Dnssec) Sign(state middleware.State, zone string, now time.Time) *dns.Msg { + req := state.Req + mt, _ := middleware.Classify(req) // TODO(miek): need opt record here? + if mt == middleware.Delegation { + return req + } + + incep, expir := incepExpir(now) + + if mt == middleware.NameError { + if req.Ns[0].Header().Rrtype != dns.TypeSOA || len(req.Ns) > 1 { + return req + } + + ttl := req.Ns[0].Header().Ttl + + if sigs, err := d.sign(req.Ns, zone, ttl, incep, expir); err == nil { + req.Ns = append(req.Ns, sigs...) + } + if sigs, err := d.nsec(state.Name(), zone, ttl, incep, expir); err == nil { + req.Ns = append(req.Ns, sigs...) + } + if len(req.Ns) > 1 { // actually added nsec and sigs, reset the rcode + req.Rcode = dns.RcodeSuccess + } + return req + } + + for _, r := range rrSets(req.Answer) { + ttl := r[0].Header().Ttl + if sigs, err := d.sign(r, zone, ttl, incep, expir); err == nil { + req.Answer = append(req.Answer, sigs...) + } + } + for _, r := range rrSets(req.Ns) { + ttl := r[0].Header().Ttl + if sigs, err := d.sign(r, zone, ttl, incep, expir); err == nil { + req.Ns = append(req.Ns, sigs...) + } + } + for _, r := range rrSets(req.Extra) { + ttl := r[0].Header().Ttl + if sigs, err := d.sign(r, zone, ttl, incep, expir); err == nil { + req.Extra = append(req.Extra, sigs...) + } + } + return req +} + +func (d Dnssec) sign(rrs []dns.RR, signerName string, ttl, incep, expir uint32) ([]dns.RR, error) { + k := key(rrs) + sgs, ok := d.get(k) + if ok { + return sgs, nil + } + + sigs, err := d.inflight.Do(k, func() (interface{}, error) { + sigs := make([]dns.RR, len(d.keys)) + var e error + for i, k := range d.keys { + sig := k.NewRRSIG(signerName, ttl, incep, expir) + e = sig.Sign(k.s, rrs) + sigs[i] = sig + } + d.set(k, sigs) + return sigs, e + }) + return sigs.([]dns.RR), err +} + +func (d Dnssec) set(key string, sigs []dns.RR) { + // we insert the sigs with a duration that is 24 hours less then the expiration, as these + // sigs have *just* been made the duration is 7 days. + d.cache.Set(key, sigs, eightDays-24*time.Hour) +} + +func (d Dnssec) get(key string) ([]dns.RR, bool) { + if s, ok := d.cache.Get(key); ok { + return s.([]dns.RR), true + } + return nil, false +} + +func incepExpir(now time.Time) (uint32, uint32) { + incep := uint32(now.Add(-3 * time.Hour).Unix()) // -(2+1) hours, be sure to catch daylight saving time and such + expir := uint32(now.Add(eightDays).Unix()) // sign for 8 days + return incep, expir +} + +const ( + purgeDuration = 3 * time.Hour + defaultDuration = 24 * time.Hour + eightDays = 8 * 24 * time.Hour +) diff --git a/middleware/dnssec/dnssec_test.go b/middleware/dnssec/dnssec_test.go new file mode 100644 index 000000000..49b0d5d3a --- /dev/null +++ b/middleware/dnssec/dnssec_test.go @@ -0,0 +1,193 @@ +package dnssec + +import ( + "testing" + "time" + + "github.com/miekg/coredns/middleware" + "github.com/miekg/coredns/middleware/test" + + "github.com/miekg/dns" +) + +func TestZoneSigning(t *testing.T) { + d, rm1, rm2 := newDnssec(t, []string{"miek.nl."}) + defer rm1() + defer rm2() + + m := testMsg() + state := middleware.State{Req: m} + + m = d.Sign(state, "miek.nl.", time.Now().UTC()) + if !section(m.Answer, 1) { + t.Errorf("answer section should have 1 sig") + } + if !section(m.Ns, 1) { + t.Errorf("authority section should have 1 sig") + } +} + +func TestZoneSigningDouble(t *testing.T) { + d, rm1, rm2 := newDnssec(t, []string{"miek.nl."}) + defer rm1() + defer rm2() + + fPriv1, rmPriv1, _ := test.TempFile(t, ".", privKey1) + fPub1, rmPub1, _ := test.TempFile(t, ".", pubKey1) + defer rmPriv1() + defer rmPub1() + + key1, err := ParseKeyFile(fPub1, fPriv1) + if err != nil { + t.Fatalf("failed to parse key: %v\n", err) + } + d.keys = append(d.keys, key1) + + m := testMsg() + state := middleware.State{Req: m} + m = d.Sign(state, "miek.nl.", time.Now().UTC()) + if !section(m.Answer, 2) { + t.Errorf("answer section should have 1 sig") + } + if !section(m.Ns, 2) { + t.Errorf("authority section should have 1 sig") + } + t.Logf("%+v\n", m) +} + +// TestSigningDifferentZone tests if a key for miek.nl and be used for example.org. +func TestSigningDifferentZone(t *testing.T) { + fPriv, rmPriv, _ := test.TempFile(t, ".", privKey) + fPub, rmPub, _ := test.TempFile(t, ".", pubKey) + defer rmPriv() + defer rmPub() + + key, err := ParseKeyFile(fPub, fPriv) + if err != nil { + t.Fatalf("failed to parse key: %v\n", err) + } + + m := testMsgEx() + state := middleware.State{Req: m} + d := NewDnssec([]string{"example.org."}, []*DNSKEY{key}, nil) + m = d.Sign(state, "example.org.", time.Now().UTC()) + if !section(m.Answer, 1) { + t.Errorf("answer section should have 1 sig") + } + if !section(m.Ns, 1) { + t.Errorf("authority section should have 1 sig") + } + t.Logf("%+v\n", m) +} + +func TestSigningCname(t *testing.T) { + d, rm1, rm2 := newDnssec(t, []string{"miek.nl."}) + defer rm1() + defer rm2() + + m := testMsgCname() + state := middleware.State{Req: m} + m = d.Sign(state, "miek.nl.", time.Now().UTC()) + if !section(m.Answer, 1) { + t.Errorf("answer section should have 1 sig") + } + t.Logf("%+v\n", m) +} + +func TestZoneSigningDelegation(t *testing.T) { + d, rm1, rm2 := newDnssec(t, []string{"miek.nl."}) + defer rm1() + defer rm2() + + m := testDelegationMsg() + state := middleware.State{Req: m} + m = d.Sign(state, "miek.nl.", time.Now().UTC()) + if !section(m.Ns, 0) { + t.Errorf("authority section should have 0 sig") + t.Logf("%v\n", m) + } + if !section(m.Extra, 0) { + t.Errorf("answer section should have 0 sig") + t.Logf("%v\n", m) + } +} + +func section(rss []dns.RR, nrSigs int) bool { + i := 0 + for _, r := range rss { + if r.Header().Rrtype == dns.TypeRRSIG { + i++ + } + } + return nrSigs == i +} + +func testMsg() *dns.Msg { + // don't care about the message header + return &dns.Msg{ + Answer: []dns.RR{test.MX("miek.nl. 1703 IN MX 1 aspmx.l.google.com.")}, + Ns: []dns.RR{test.NS("miek.nl. 1703 IN NS omval.tednet.nl.")}, + } +} +func testMsgEx() *dns.Msg { + return &dns.Msg{ + Answer: []dns.RR{test.MX("example.org. 1703 IN MX 1 aspmx.l.google.com.")}, + Ns: []dns.RR{test.NS("example.org. 1703 IN NS omval.tednet.nl.")}, + } +} + +func testMsgCname() *dns.Msg { + return &dns.Msg{ + Answer: []dns.RR{test.CNAME("www.miek.nl. 1800 IN CNAME a.miek.nl.")}, + } +} + +func testDelegationMsg() *dns.Msg { + return &dns.Msg{ + Ns: []dns.RR{ + test.NS("miek.nl. 3600 IN NS linode.atoom.net."), + test.NS("miek.nl. 3600 IN NS ns-ext.nlnetlabs.nl."), + test.NS("miek.nl. 3600 IN NS omval.tednet.nl."), + }, + Extra: []dns.RR{ + test.A("omval.tednet.nl. 3600 IN A 185.49.141.42"), + test.AAAA("omval.tednet.nl. 3600 IN AAAA 2a04:b900:0:100::42"), + }, + } +} + +func newDnssec(t *testing.T, zones []string) (Dnssec, func(), func()) { + k, rm1, rm2 := newKey(t) + d := NewDnssec(zones, []*DNSKEY{k}, nil) + return d, rm1, rm2 +} + +func newKey(t *testing.T) (*DNSKEY, func(), func()) { + fPriv, rmPriv, _ := test.TempFile(t, ".", privKey) + fPub, rmPub, _ := test.TempFile(t, ".", pubKey) + + key, err := ParseKeyFile(fPub, fPriv) + if err != nil { + t.Fatalf("failed to parse key: %v\n", err) + } + return key, rmPriv, rmPub +} + +const ( + pubKey = `miek.nl. IN DNSKEY 257 3 13 0J8u0XJ9GNGFEBXuAmLu04taHG4BXPP3gwhetiOUMnGA+x09nqzgF5IY OyjWB7N3rXqQbnOSILhH1hnuyh7mmA==` + privKey = `Private-key-format: v1.3 +Algorithm: 13 (ECDSAP256SHA256) +PrivateKey: /4BZk8AFvyW5hL3cOLSVxIp1RTqHSAEloWUxj86p3gs= +Created: 20160423195532 +Publish: 20160423195532 +Activate: 20160423195532 +` + pubKey1 = `example.org. IN DNSKEY 257 3 13 tVRWNSGpHZbCi7Pr7OmbADVUO3MxJ0Lb8Lk3o/HBHqCxf5K/J50lFqRa 98lkdAIiFOVRy8LyMvjwmxZKwB5MNw==` + privKey1 = `Private-key-format: v1.3 +Algorithm: 13 (ECDSAP256SHA256) +PrivateKey: i8j4OfDGT8CQt24SDwLz2hg9yx4qKOEOh1LvbAuSp1c= +Created: 20160423211746 +Publish: 20160423211746 +Activate: 20160423211746 +` +) diff --git a/middleware/dnssec/handler.go b/middleware/dnssec/handler.go new file mode 100644 index 000000000..0c0f4a61d --- /dev/null +++ b/middleware/dnssec/handler.go @@ -0,0 +1,61 @@ +package dnssec + +import ( + "github.com/miekg/coredns/middleware" + + "github.com/miekg/dns" + "github.com/prometheus/client_golang/prometheus" + "golang.org/x/net/context" +) + +// ServeDNS implements the middleware.Handler interface. +func (d Dnssec) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + state := middleware.State{W: w, Req: r} + + do := state.Do() + qname := state.Name() + qtype := state.QType() + zone := middleware.Zones(d.zones).Matches(qname) + if zone == "" { + return d.Next.ServeDNS(ctx, w, r) + } + + // Intercept queries for DNSKEY, but only if one of the zones matches the qname, otherwise we let + // the query through. + if qtype == dns.TypeDNSKEY { + for _, z := range d.zones { + if qname == z { + resp := d.getDNSKEY(state, z, do) + state.SizeAndDo(resp) + w.WriteMsg(resp) + return dns.RcodeSuccess, nil + } + } + } + + drr := NewDnssecResponseWriter(w, d) + return d.Next.ServeDNS(ctx, drr, r) +} + +var ( + cacheHitCount = prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: middleware.Namespace, + Subsystem: subsystem, + Name: "hit_count_total", + Help: "Counter of signatures that were found in the cache.", + }, []string{"zone"}) + + cacheMissCount = prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: middleware.Namespace, + Subsystem: subsystem, + Name: "miss_count_total", + Help: "Counter of signatures that were not found in the cache.", + }, []string{"zone"}) +) + +const subsystem = "dnssec" + +func init() { + prometheus.MustRegister(cacheHitCount) + prometheus.MustRegister(cacheMissCount) +} diff --git a/middleware/dnssec/handler_test.go b/middleware/dnssec/handler_test.go new file mode 100644 index 000000000..9e6cedf8a --- /dev/null +++ b/middleware/dnssec/handler_test.go @@ -0,0 +1,170 @@ +package dnssec + +import ( + "sort" + "strings" + "testing" + + "github.com/miekg/coredns/middleware" + "github.com/miekg/coredns/middleware/file" + "github.com/miekg/coredns/middleware/test" + + "github.com/miekg/dns" + "golang.org/x/net/context" +) + +var dnssecTestCases = []test.Case{ + { + Qname: "miek.nl.", Qtype: dns.TypeDNSKEY, + Answer: []dns.RR{ + test.DNSKEY("miek.nl. 3600 IN DNSKEY 257 3 13 0J8u0XJ9GNGFEBXuAmLu04taHG4"), + }, + }, + { + Qname: "miek.nl.", Qtype: dns.TypeDNSKEY, Do: true, + Answer: []dns.RR{ + test.DNSKEY("miek.nl. 3600 IN DNSKEY 257 3 13 0J8u0XJ9GNGFEBXuAmLu04taHG4"), + test.RRSIG("miek.nl. 3600 IN RRSIG DNSKEY 13 2 3600 20160503150844 20160425120844 18512 miek.nl. Iw/kNOyM"), + }, + Extra: []dns.RR{test.OPT(4096, true)}, + }, +} + +var dnsTestCases = []test.Case{ + { + Qname: "miek.nl.", Qtype: dns.TypeDNSKEY, + Answer: []dns.RR{ + test.DNSKEY("miek.nl. 3600 IN DNSKEY 257 3 13 0J8u0XJ9GNGFEBXuAmLu04taHG4"), + }, + }, + { + Qname: "miek.nl.", Qtype: dns.TypeMX, + Answer: []dns.RR{ + test.MX("miek.nl. 1800 IN MX 1 aspmx.l.google.com."), + }, + }, + { + Qname: "miek.nl.", Qtype: dns.TypeMX, Do: true, + Answer: []dns.RR{ + test.MX("miek.nl. 1800 IN MX 1 aspmx.l.google.com."), + test.RRSIG("miek.nl. 1800 IN RRSIG MX 13 2 3600 20160503192428 20160425162428 18512 miek.nl. 4nxuGKitXjPVA9zP1JIUvA09"), + }, + Extra: []dns.RR{test.OPT(4096, true)}, + }, + { + Qname: "www.miek.nl.", Qtype: dns.TypeAAAA, Do: true, + Answer: []dns.RR{ + test.AAAA("a.miek.nl. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"), + test.RRSIG("a.miek.nl. 1800 IN RRSIG AAAA 13 3 3600 20160503193047 20160425163047 18512 miek.nl. UAyMG+gcnoXW3"), + test.CNAME("www.miek.nl. 1800 IN CNAME a.miek.nl."), + test.RRSIG("www.miek.nl. 1800 IN RRSIG CNAME 13 3 3600 20160503193047 20160425163047 18512 miek.nl. E3qGZn"), + }, + Extra: []dns.RR{test.OPT(4096, true)}, + }, + { + Qname: "www.example.org.", Qtype: dns.TypeAAAA, Do: true, + Rcode: dns.RcodeServerFailure, + // Extra: []dns.RR{test.OPT(4096, true)}, // test.ErrorHandler is a simple handler that does not do EDNS. + }, +} + +func TestLookupZone(t *testing.T) { + zone, err := file.Parse(strings.NewReader(dbMiekNL), "miek.nl.", "stdin") + if err != nil { + return + } + fm := file.File{Next: test.ErrorHandler(), Zones: file.Zones{Z: map[string]*file.Zone{"miek.nl.": zone}, Names: []string{"miek.nl."}}} + dnskey, rm1, rm2 := newKey(t) + defer rm1() + defer rm2() + dh := NewDnssec([]string{"miek.nl."}, []*DNSKEY{dnskey}, fm) + ctx := context.TODO() + + for _, tc := range dnsTestCases { + m := tc.Msg() + + rec := middleware.NewResponseRecorder(&test.ResponseWriter{}) + _, err := dh.ServeDNS(ctx, rec, m) + if err != nil { + t.Errorf("expected no error, got %v\n", err) + return + } + resp := rec.Msg() + + sort.Sort(test.RRSet(resp.Answer)) + sort.Sort(test.RRSet(resp.Ns)) + sort.Sort(test.RRSet(resp.Extra)) + + if !test.Header(t, tc, resp) { + t.Logf("%v\n", resp) + continue + } + if !test.Section(t, tc, test.Answer, resp.Answer) { + t.Logf("%v\n", resp) + } + if !test.Section(t, tc, test.Ns, resp.Ns) { + t.Logf("%v\n", resp) + } + if !test.Section(t, tc, test.Extra, resp.Extra) { + t.Logf("%v\n", resp) + } + } +} + +func TestLookupDNSKEY(t *testing.T) { + dnskey, rm1, rm2 := newKey(t) + defer rm1() + defer rm2() + dh := NewDnssec([]string{"miek.nl."}, []*DNSKEY{dnskey}, test.ErrorHandler()) + ctx := context.TODO() + + for _, tc := range dnssecTestCases { + m := tc.Msg() + + rec := middleware.NewResponseRecorder(&test.ResponseWriter{}) + _, err := dh.ServeDNS(ctx, rec, m) + if err != nil { + t.Errorf("expected no error, got %v\n", err) + return + } + resp := rec.Msg() + + sort.Sort(test.RRSet(resp.Answer)) + sort.Sort(test.RRSet(resp.Ns)) + sort.Sort(test.RRSet(resp.Extra)) + + if !test.Header(t, tc, resp) { + t.Logf("%v\n", resp) + continue + } + if !test.Section(t, tc, test.Answer, resp.Answer) { + t.Logf("%v\n", resp) + } + if !test.Section(t, tc, test.Ns, resp.Ns) { + t.Logf("%v\n", resp) + } + if !test.Section(t, tc, test.Extra, resp.Extra) { + t.Logf("%v\n", resp) + } + } +} + +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 MX 1 aspmx.l.google.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` diff --git a/middleware/dnssec/responsewriter.go b/middleware/dnssec/responsewriter.go new file mode 100644 index 000000000..2a7cbb972 --- /dev/null +++ b/middleware/dnssec/responsewriter.go @@ -0,0 +1,48 @@ +package dnssec + +import ( + "log" + "time" + + "github.com/miekg/coredns/middleware" + "github.com/miekg/dns" +) + +type DnssecResponseWriter struct { + dns.ResponseWriter + d Dnssec +} + +func NewDnssecResponseWriter(w dns.ResponseWriter, d Dnssec) *DnssecResponseWriter { + return &DnssecResponseWriter{w, d} +} + +func (d *DnssecResponseWriter) WriteMsg(res *dns.Msg) error { + // By definition we should sign anything that comes back, we should still figure out for + // which zone it should be. + state := middleware.State{W: d.ResponseWriter, Req: res} + + qname := state.Name() + zone := middleware.Zones(d.d.zones).Matches(qname) + if zone == "" { + return d.ResponseWriter.WriteMsg(res) + } + + if state.Do() { + res = d.d.Sign(state, zone, time.Now().UTC()) + } + state.SizeAndDo(res) + + return d.ResponseWriter.WriteMsg(res) +} + +func (d *DnssecResponseWriter) Write(buf []byte) (int, error) { + log.Printf("[WARNING] Dnssec called with Write: not signing reply") + n, err := d.ResponseWriter.Write(buf) + return n, err +} + +func (d *DnssecResponseWriter) Hijack() { + d.ResponseWriter.Hijack() + return +} diff --git a/middleware/dnssec/rrsig.go b/middleware/dnssec/rrsig.go new file mode 100644 index 000000000..17bc1195b --- /dev/null +++ b/middleware/dnssec/rrsig.go @@ -0,0 +1,53 @@ +package dnssec + +import "github.com/miekg/dns" + +// newRRSIG return a new RRSIG, with all fields filled out, except the signed data. +func (k *DNSKEY) NewRRSIG(signerName string, ttl, incep, expir uint32) *dns.RRSIG { + sig := new(dns.RRSIG) + + sig.Hdr.Rrtype = dns.TypeRRSIG + sig.Algorithm = k.K.Algorithm + sig.KeyTag = k.keytag + sig.SignerName = signerName + sig.Hdr.Ttl = ttl + sig.OrigTtl = origTtl + + sig.Inception = incep + sig.Expiration = expir + + return sig +} + +type rrset struct { + qname string + qtype uint16 +} + +// rrSets returns rrs as a map of RRsets. It skips RRSIG and OPT records as those don't need to be signed. +func rrSets(rrs []dns.RR) map[rrset][]dns.RR { + m := make(map[rrset][]dns.RR) + + for _, r := range rrs { + if r.Header().Rrtype == dns.TypeRRSIG || r.Header().Rrtype == dns.TypeOPT { + continue + } + + if s, ok := m[rrset{r.Header().Name, r.Header().Rrtype}]; ok { + s = append(s, r) + m[rrset{r.Header().Name, r.Header().Rrtype}] = s + continue + } + + s := make([]dns.RR, 1, 3) + s[0] = r + m[rrset{r.Header().Name, r.Header().Rrtype}] = s + } + + if len(m) > 0 { + return m + } + return nil +} + +const origTtl = 3600 diff --git a/middleware/etcd/etcd.go b/middleware/etcd/etcd.go index 4eb55f7ed..38eac7ab3 100644 --- a/middleware/etcd/etcd.go +++ b/middleware/etcd/etcd.go @@ -8,8 +8,8 @@ import ( "github.com/miekg/coredns/middleware" "github.com/miekg/coredns/middleware/etcd/msg" - "github.com/miekg/coredns/middleware/etcd/singleflight" "github.com/miekg/coredns/middleware/proxy" + "github.com/miekg/coredns/middleware/singleflight" etcdc "github.com/coreos/etcd/client" "golang.org/x/net/context" diff --git a/middleware/etcd/lookup.go b/middleware/etcd/lookup.go index 88f362fc7..35d4b7226 100644 --- a/middleware/etcd/lookup.go +++ b/middleware/etcd/lookup.go @@ -317,11 +317,14 @@ func (e Etcd) NS(zone string, state middleware.State) (records, extra []dns.RR, // NS record for this zone live in a special place, ns.dns.. Fake our lookup. // only a tad bit fishy... old := state.QName() + + state.Clear() state.Req.Question[0].Name = "ns.dns." + zone services, err := e.records(state, false) if err != nil { return nil, nil, err } + // ... and reset state.Req.Question[0].Name = old for _, serv := range services { diff --git a/middleware/etcd/setup_test.go b/middleware/etcd/setup_test.go index b28602122..a695d43ce 100644 --- a/middleware/etcd/setup_test.go +++ b/middleware/etcd/setup_test.go @@ -10,8 +10,8 @@ import ( "github.com/miekg/coredns/middleware" "github.com/miekg/coredns/middleware/etcd/msg" - "github.com/miekg/coredns/middleware/etcd/singleflight" "github.com/miekg/coredns/middleware/proxy" + "github.com/miekg/coredns/middleware/singleflight" "github.com/miekg/coredns/middleware/test" "github.com/miekg/dns" diff --git a/middleware/file/closest.go b/middleware/file/closest.go index 6af033e64..741327fde 100644 --- a/middleware/file/closest.go +++ b/middleware/file/closest.go @@ -52,6 +52,10 @@ func (z *Zone) nameErrorProof(qname string, qtype uint16) []dns.RR { } } + 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 { diff --git a/middleware/file/file.go b/middleware/file/file.go index 441e8b94d..a99a64d6f 100644 --- a/middleware/file/file.go +++ b/middleware/file/file.go @@ -1,7 +1,7 @@ package file import ( - "fmt" + "errors" "io" "log" @@ -27,12 +27,15 @@ func (f File) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (i state := middleware.State{W: w, Req: r} if state.QClass() != dns.ClassINET { - return dns.RcodeServerFailure, fmt.Errorf("can only deal with ClassINET") + return dns.RcodeServerFailure, errors.New("can only deal with ClassINET") } qname := state.Name() zone := middleware.Zones(f.Zones.Names).Matches(qname) if zone == "" { - return f.Next.ServeDNS(ctx, w, r) + if f.Next != nil { + return f.Next.ServeDNS(ctx, w, r) + } + return dns.RcodeServerFailure, errors.New("no next middleware found") } z, ok := f.Zones.Z[zone] if !ok { diff --git a/middleware/file/reload_test.go b/middleware/file/reload_test.go index 1769c701e..1ba9f4bcf 100644 --- a/middleware/file/reload_test.go +++ b/middleware/file/reload_test.go @@ -11,7 +11,7 @@ import ( ) func TestZoneReload(t *testing.T) { - fileName, rm, err := test.Zone(t, ".", reloadZoneTest) + fileName, rm, err := test.TempFile(t, ".", reloadZoneTest) if err != nil { t.Fatalf("failed to create zone: %s", err) } diff --git a/middleware/etcd/singleflight/singleflight.go b/middleware/singleflight/singleflight.go similarity index 100% rename from middleware/etcd/singleflight/singleflight.go rename to middleware/singleflight/singleflight.go diff --git a/middleware/state.go b/middleware/state.go index ec2b618a6..34e6cdfc3 100644 --- a/middleware/state.go +++ b/middleware/state.go @@ -15,9 +15,13 @@ type State struct { Req *dns.Msg W dns.ResponseWriter - // Cache size after first call to Size or Do + // Cache size after first call to Size or Do. size int do int // 0: not, 1: true: 2: false + // TODO(miek): opt record itself as well. + + // Cache name as (lowercase) well + name string } // Now returns the current timestamp in the specified format. @@ -26,12 +30,6 @@ func (s *State) Now(format string) string { return time.Now().Format(format) } // NowDate returns the current date/time that can be used in other time functions. func (s *State) NowDate() time.Time { return time.Now() } -// Header gets the heaser of the request in State. -func (s *State) Header() *dns.RR_Header { - // TODO(miek) - return nil -} - // IP gets the (remote) IP address of the client making the request. func (s *State) IP() string { ip, _, err := net.SplitHostPort(s.W.RemoteAddr().String()) @@ -191,7 +189,13 @@ func (s *State) QType() uint16 { return s.Req.Question[0].Qtype } // Name returns the name of the question in the request. Note // this name will always have a closing dot and will be lower cased. -func (s *State) Name() string { return strings.ToLower(dns.Name(s.Req.Question[0].Name).String()) } +func (s *State) Name() string { + if s.name != "" { + return s.name + } + s.name = strings.ToLower(dns.Name(s.Req.Question[0].Name).String()) + return s.name +} // QName returns the name of the question in the request. func (s *State) QName() string { return dns.Name(s.Req.Question[0].Name).String() } @@ -210,6 +214,11 @@ func (s *State) ErrorMessage(rcode int) *dns.Msg { return m } +// Clear clears all caching from State s. +func (s *State) Clear() { + s.name = "" +} + const ( doTrue = 1 doFalse = 2 diff --git a/middleware/test/file.go b/middleware/test/file.go new file mode 100644 index 000000000..b6068a32b --- /dev/null +++ b/middleware/test/file.go @@ -0,0 +1,20 @@ +package test + +import ( + "io/ioutil" + "os" + "testing" +) + +// TempFile will create a temporary file on disk and returns the name and a cleanup function to remove it later. +func TempFile(t *testing.T, dir, content string) (string, func(), error) { + f, err := ioutil.TempFile(dir, "go-test-tmpfile") + if err != nil { + return "", nil, err + } + if err := ioutil.WriteFile(f.Name(), []byte(content), 0644); err != nil { + return "", nil, err + } + rmFunc := func() { os.Remove(f.Name()) } + return f.Name(), rmFunc, nil +} diff --git a/middleware/test/helpers.go b/middleware/test/helpers.go index 3096f126b..a01d7a306 100644 --- a/middleware/test/helpers.go +++ b/middleware/test/helpers.go @@ -45,17 +45,18 @@ func (c Case) Msg() *dns.Msg { return m } -func A(rr string) *dns.A { r, _ := dns.NewRR(rr); return r.(*dns.A) } -func AAAA(rr string) *dns.AAAA { r, _ := dns.NewRR(rr); return r.(*dns.AAAA) } -func CNAME(rr string) *dns.CNAME { r, _ := dns.NewRR(rr); return r.(*dns.CNAME) } -func SRV(rr string) *dns.SRV { r, _ := dns.NewRR(rr); return r.(*dns.SRV) } -func SOA(rr string) *dns.SOA { r, _ := dns.NewRR(rr); return r.(*dns.SOA) } -func NS(rr string) *dns.NS { r, _ := dns.NewRR(rr); return r.(*dns.NS) } -func PTR(rr string) *dns.PTR { r, _ := dns.NewRR(rr); return r.(*dns.PTR) } -func TXT(rr string) *dns.TXT { r, _ := dns.NewRR(rr); return r.(*dns.TXT) } -func MX(rr string) *dns.MX { r, _ := dns.NewRR(rr); return r.(*dns.MX) } -func RRSIG(rr string) *dns.RRSIG { r, _ := dns.NewRR(rr); return r.(*dns.RRSIG) } -func NSEC(rr string) *dns.NSEC { r, _ := dns.NewRR(rr); return r.(*dns.NSEC) } +func A(rr string) *dns.A { r, _ := dns.NewRR(rr); return r.(*dns.A) } +func AAAA(rr string) *dns.AAAA { r, _ := dns.NewRR(rr); return r.(*dns.AAAA) } +func CNAME(rr string) *dns.CNAME { r, _ := dns.NewRR(rr); return r.(*dns.CNAME) } +func SRV(rr string) *dns.SRV { r, _ := dns.NewRR(rr); return r.(*dns.SRV) } +func SOA(rr string) *dns.SOA { r, _ := dns.NewRR(rr); return r.(*dns.SOA) } +func NS(rr string) *dns.NS { r, _ := dns.NewRR(rr); return r.(*dns.NS) } +func PTR(rr string) *dns.PTR { r, _ := dns.NewRR(rr); return r.(*dns.PTR) } +func TXT(rr string) *dns.TXT { r, _ := dns.NewRR(rr); return r.(*dns.TXT) } +func MX(rr string) *dns.MX { r, _ := dns.NewRR(rr); return r.(*dns.MX) } +func RRSIG(rr string) *dns.RRSIG { r, _ := dns.NewRR(rr); return r.(*dns.RRSIG) } +func NSEC(rr string) *dns.NSEC { r, _ := dns.NewRR(rr); return r.(*dns.NSEC) } +func DNSKEY(rr string) *dns.DNSKEY { r, _ := dns.NewRR(rr); return r.(*dns.DNSKEY) } func OPT(bufsize int, do bool) *dns.OPT { o := new(dns.OPT) diff --git a/middleware/test/zone.go b/middleware/test/zone.go deleted file mode 100644 index 490280a7a..000000000 --- a/middleware/test/zone.go +++ /dev/null @@ -1,21 +0,0 @@ -package test - -import ( - "io/ioutil" - "os" - "testing" -) - -// Zone will create a temporary file on disk and returns the name and -// cleanup function to remove it later. -func Zone(t *testing.T, dir, zonefile string) (string, func(), error) { - f, err := ioutil.TempFile(dir, "go-test-zone") - if err != nil { - return "", nil, err - } - if err := ioutil.WriteFile(f.Name(), []byte(zonefile), 0644); err != nil { - return "", nil, err - } - rmFunc := func() { os.Remove(f.Name()) } - return f.Name(), rmFunc, nil -} diff --git a/test/proxy_test.go b/test/proxy_test.go index 9365570b6..74e60148a 100644 --- a/test/proxy_test.go +++ b/test/proxy_test.go @@ -21,7 +21,7 @@ example.org. IN A 127.0.0.1 ` func TestLookupProxy(t *testing.T) { - name, rm, err := test.Zone(t, ".", exampleOrg) + name, rm, err := test.TempFile(t, ".", exampleOrg) if err != nil { t.Fatalf("failed to created zone: %s", err) }