mirror of
				https://github.com/coredns/coredns.git
				synced 2025-10-30 17:53:21 -04:00 
			
		
		
		
	Add middleware/dnssec (#133)
This adds an online dnssec middleware. The middleware will sign responses on the fly. Negative responses are signed with NSEC black lies.
This commit is contained in:
		
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,2 +1,4 @@ | ||||
| query.log | ||||
| Corefile | ||||
| *.swp | ||||
| coredns | ||||
|   | ||||
| @@ -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}, | ||||
|   | ||||
| @@ -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 | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
|   | ||||
| @@ -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. | ||||
| 	}{ | ||||
|   | ||||
							
								
								
									
										79
									
								
								core/setup/dnssec.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								core/setup/dnssec.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| } | ||||
							
								
								
									
										54
									
								
								core/setup/dnssec_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								core/setup/dnssec_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -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" | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
| 				} | ||||
|   | ||||
							
								
								
									
										92
									
								
								middleware/cache/cache.go
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										92
									
								
								middleware/cache/cache.go
									
									
									
									
										vendored
									
									
								
							| @@ -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 <blaat.miek.nl> | ||||
| 	Item: | ||||
| 		Ns: <miek.nl> 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 | ||||
| 			} | ||||
|   | ||||
							
								
								
									
										6
									
								
								middleware/cache/cache_test.go
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								middleware/cache/cache_test.go
									
									
									
									
										vendored
									
									
								
							| @@ -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") | ||||
| 		} | ||||
|   | ||||
							
								
								
									
										5
									
								
								middleware/cache/handler.go
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								middleware/cache/handler.go
									
									
									
									
										vendored
									
									
								
							| @@ -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 | ||||
|   | ||||
							
								
								
									
										52
									
								
								middleware/classify.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								middleware/classify.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| } | ||||
							
								
								
									
										31
									
								
								middleware/classify_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								middleware/classify_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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"), | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										35
									
								
								middleware/dnssec/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								middleware/dnssec/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -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 <zonename>`. A key created for zone *A* can be safely used for zone *B*. | ||||
|  | ||||
| ## Examples | ||||
							
								
								
									
										24
									
								
								middleware/dnssec/black_lies.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								middleware/dnssec/black_lies.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| } | ||||
							
								
								
									
										50
									
								
								middleware/dnssec/black_lies_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								middleware/dnssec/black_lies_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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")}, | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										23
									
								
								middleware/dnssec/cache.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								middleware/dnssec/cache.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
| } | ||||
							
								
								
									
										32
									
								
								middleware/dnssec/cache_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								middleware/dnssec/cache_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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") | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										71
									
								
								middleware/dnssec/dnskey.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								middleware/dnssec/dnskey.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| } | ||||
							
								
								
									
										127
									
								
								middleware/dnssec/dnssec.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								middleware/dnssec/dnssec.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| ) | ||||
							
								
								
									
										193
									
								
								middleware/dnssec/dnssec_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										193
									
								
								middleware/dnssec/dnssec_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| ` | ||||
| ) | ||||
							
								
								
									
										61
									
								
								middleware/dnssec/handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								middleware/dnssec/handler.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
| } | ||||
							
								
								
									
										170
									
								
								middleware/dnssec/handler_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								middleware/dnssec/handler_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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` | ||||
							
								
								
									
										48
									
								
								middleware/dnssec/responsewriter.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								middleware/dnssec/responsewriter.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| } | ||||
							
								
								
									
										53
									
								
								middleware/dnssec/rrsig.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								middleware/dnssec/rrsig.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| @@ -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" | ||||
|   | ||||
| @@ -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.<zone>. 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 { | ||||
|   | ||||
| @@ -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" | ||||
|  | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| package file | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"errors" | ||||
| 	"io" | ||||
| 	"log" | ||||
|  | ||||
| @@ -27,13 +27,16 @@ 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 == "" { | ||||
| 		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 { | ||||
| 		return f.Next.ServeDNS(ctx, w, r) | ||||
|   | ||||
| @@ -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) | ||||
| 	} | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										20
									
								
								middleware/test/file.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								middleware/test/file.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| } | ||||
| @@ -56,6 +56,7 @@ 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) | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
| @@ -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) | ||||
| 	} | ||||
|   | ||||
		Reference in New Issue
	
	Block a user