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 | query.log | ||||||
| Corefile | Corefile | ||||||
|  | *.swp | ||||||
|  | coredns | ||||||
|   | |||||||
| @@ -60,6 +60,7 @@ var directiveOrder = []directive{ | |||||||
| 	{"rewrite", setup.Rewrite}, | 	{"rewrite", setup.Rewrite}, | ||||||
| 	{"loadbalance", setup.Loadbalance}, | 	{"loadbalance", setup.Loadbalance}, | ||||||
| 	{"cache", setup.Cache}, | 	{"cache", setup.Cache}, | ||||||
|  | 	{"dnssec", setup.Dnssec}, | ||||||
| 	{"file", setup.File}, | 	{"file", setup.File}, | ||||||
| 	{"secondary", setup.Secondary}, | 	{"secondary", setup.Secondary}, | ||||||
| 	{"etcd", setup.Etcd}, | 	{"etcd", setup.Etcd}, | ||||||
|   | |||||||
| @@ -27,8 +27,7 @@ func cacheParse(c *Controller) (int, []string, error) { | |||||||
| 	for c.Next() { | 	for c.Next() { | ||||||
| 		if c.Val() == "cache" { | 		if c.Val() == "cache" { | ||||||
| 			// cache [ttl] [zones..] | 			// cache [ttl] [zones..] | ||||||
|  | 			origins := c.ServerBlockHosts | ||||||
| 			origins := []string{c.ServerBlockHosts[c.ServerBlockHostIndex]} |  | ||||||
| 			args := c.RemainingArgs() | 			args := c.RemainingArgs() | ||||||
| 			if len(args) > 0 { | 			if len(args) > 0 { | ||||||
| 				origins = args | 				origins = args | ||||||
| @@ -39,7 +38,7 @@ func cacheParse(c *Controller) (int, []string, error) { | |||||||
| 					origins = origins[1:] | 					origins = origins[1:] | ||||||
| 					if len(origins) == 0 { | 					if len(origins) == 0 { | ||||||
| 						// There was *only* the ttl, revert back to server block | 						// 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 { | 	tests := []struct { | ||||||
| 		input              string | 		input              string | ||||||
| 		shouldErr          bool | 		shouldErr          bool | ||||||
| 		expectedVersion    string // expected veresion. | 		expectedVersion    string // expected version. | ||||||
| 		expectedAuthor     string // expected author (string, although we get a map). | 		expectedAuthor     string // expected author (string, although we get a map). | ||||||
| 		expectedErrContent string // substring from the expected error. Empty for positive cases. | 		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" | ||||||
| 	"github.com/miekg/coredns/middleware/etcd" | 	"github.com/miekg/coredns/middleware/etcd" | ||||||
| 	"github.com/miekg/coredns/middleware/etcd/singleflight" |  | ||||||
| 	"github.com/miekg/coredns/middleware/proxy" | 	"github.com/miekg/coredns/middleware/proxy" | ||||||
|  | 	"github.com/miekg/coredns/middleware/singleflight" | ||||||
|  |  | ||||||
| 	etcdc "github.com/coreos/etcd/client" | 	etcdc "github.com/coreos/etcd/client" | ||||||
| 	"golang.org/x/net/context" | 	"golang.org/x/net/context" | ||||||
|   | |||||||
| @@ -46,7 +46,7 @@ func fileParse(c *Controller) (file.Zones, error) { | |||||||
| 			} | 			} | ||||||
| 			fileName := c.Val() | 			fileName := c.Val() | ||||||
|  |  | ||||||
| 			origins := []string{c.ServerBlockHosts[c.ServerBlockHostIndex]} | 			origins := c.ServerBlockHosts | ||||||
| 			args := c.RemainingArgs() | 			args := c.RemainingArgs() | ||||||
| 			if len(args) > 0 { | 			if len(args) > 0 { | ||||||
| 				origins = args | 				origins = args | ||||||
| @@ -54,7 +54,7 @@ func fileParse(c *Controller) (file.Zones, error) { | |||||||
|  |  | ||||||
| 			reader, err := os.Open(fileName) | 			reader, err := os.Open(fileName) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return file.Zones{}, err | 				continue | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			for i, _ := range origins { | 			for i, _ := range origins { | ||||||
| @@ -68,7 +68,7 @@ func fileParse(c *Controller) (file.Zones, error) { | |||||||
|  |  | ||||||
| 			noReload := false | 			noReload := false | ||||||
| 			for c.NextBlock() { | 			for c.NextBlock() { | ||||||
| 				t, _, e := parseTransfer(c) | 				t, _, e := transferParse(c) | ||||||
| 				if e != nil { | 				if e != nil { | ||||||
| 					return file.Zones{}, e | 					return file.Zones{}, e | ||||||
| 				} | 				} | ||||||
| @@ -89,8 +89,8 @@ func fileParse(c *Controller) (file.Zones, error) { | |||||||
| 	return file.Zones{Z: z, Names: names}, nil | 	return file.Zones{Z: z, Names: names}, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // transfer to [address...] | // transferParse parses transfer statements: 'transfer to [address...]'. | ||||||
| func parseTransfer(c *Controller) (tos, froms []string, err error) { | func transferParse(c *Controller) (tos, froms []string, err error) { | ||||||
| 	what := c.Val() | 	what := c.Val() | ||||||
| 	if !c.NextArg() { | 	if !c.NextArg() { | ||||||
| 		return nil, nil, c.ArgErr() | 		return nil, nil, c.ArgErr() | ||||||
|   | |||||||
| @@ -7,10 +7,7 @@ import ( | |||||||
| 	"github.com/miekg/coredns/middleware/metrics" | 	"github.com/miekg/coredns/middleware/metrics" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| const ( | const addr = "localhost:9135" // 9153 is occupied by bind_exporter | ||||||
| 	path = "/metrics" |  | ||||||
| 	addr = "localhost:9135" // 9153 is occupied by bind_exporter |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| var once sync.Once | var once sync.Once | ||||||
|  |  | ||||||
|   | |||||||
| @@ -40,7 +40,7 @@ func secondaryParse(c *Controller) (file.Zones, error) { | |||||||
| 	for c.Next() { | 	for c.Next() { | ||||||
| 		if c.Val() == "secondary" { | 		if c.Val() == "secondary" { | ||||||
| 			// secondary [origin] | 			// secondary [origin] | ||||||
| 			origins := []string{c.ServerBlockHosts[c.ServerBlockHostIndex]} | 			origins := c.ServerBlockHosts | ||||||
| 			args := c.RemainingArgs() | 			args := c.RemainingArgs() | ||||||
| 			if len(args) > 0 { | 			if len(args) > 0 { | ||||||
| 				origins = args | 				origins = args | ||||||
| @@ -52,7 +52,7 @@ func secondaryParse(c *Controller) (file.Zones, error) { | |||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			for c.NextBlock() { | 			for c.NextBlock() { | ||||||
| 				t, f, e := parseTransfer(c) | 				t, f, e := transferParse(c) | ||||||
| 				if e != nil { | 				if e != nil { | ||||||
| 					return file.Zones{}, e | 					return file.Zones{}, e | ||||||
| 				} | 				} | ||||||
|   | |||||||
							
								
								
									
										92
									
								
								middleware/cache/cache.go
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										92
									
								
								middleware/cache/cache.go
									
									
									
									
										vendored
									
									
								
							| @@ -1,33 +1,5 @@ | |||||||
| package cache | 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 ( | import ( | ||||||
| 	"log" | 	"log" | ||||||
| 	"time" | 	"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} | 	return Cache{Next: next, Zones: zones, cache: gcache.New(defaultDuration, purgeDuration), cap: time.Duration(ttl) * time.Second} | ||||||
| } | } | ||||||
|  |  | ||||||
| type messageType int | func cacheKey(m *dns.Msg, t middleware.MsgType, do bool) string { | ||||||
|  |  | ||||||
| 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 { |  | ||||||
| 	if m.Truncated { | 	if m.Truncated { | ||||||
| 		return "" | 		return "" | ||||||
| 	} | 	} | ||||||
| @@ -92,13 +30,15 @@ func cacheKey(m *dns.Msg, t messageType, do bool) string { | |||||||
| 	qtype := m.Question[0].Qtype | 	qtype := m.Question[0].Qtype | ||||||
| 	qname := middleware.Name(m.Question[0].Name).Normalize() | 	qname := middleware.Name(m.Question[0].Name).Normalize() | ||||||
| 	switch t { | 	switch t { | ||||||
| 	case success: | 	case middleware.Success: | ||||||
|  | 		fallthrough | ||||||
|  | 	case middleware.Delegation: | ||||||
| 		return successKey(qname, qtype, do) | 		return successKey(qname, qtype, do) | ||||||
| 	case nameError: | 	case middleware.NameError: | ||||||
| 		return nameErrorKey(qname, do) | 		return nameErrorKey(qname, do) | ||||||
| 	case noData: | 	case middleware.NoData: | ||||||
| 		return noDataKey(qname, qtype, do) | 		return noDataKey(qname, qtype, do) | ||||||
| 	case otherError: | 	case middleware.OtherError: | ||||||
| 		return "" | 		return "" | ||||||
| 	} | 	} | ||||||
| 	return "" | 	return "" | ||||||
| @@ -116,13 +56,13 @@ func NewCachingResponseWriter(w dns.ResponseWriter, cache *gcache.Cache, cap tim | |||||||
|  |  | ||||||
| func (c *CachingResponseWriter) WriteMsg(res *dns.Msg) error { | func (c *CachingResponseWriter) WriteMsg(res *dns.Msg) error { | ||||||
| 	do := false | 	do := false | ||||||
| 	mt, opt := classify(res) | 	mt, opt := middleware.Classify(res) | ||||||
| 	if opt != nil { | 	if opt != nil { | ||||||
| 		do = opt.Do() | 		do = opt.Do() | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	key := cacheKey(res, mt, do) | 	key := cacheKey(res, mt, do) | ||||||
| 	c.Set(res, key, mt) | 	c.set(res, key, mt) | ||||||
|  |  | ||||||
| 	if c.cap != 0 { | 	if c.cap != 0 { | ||||||
| 		setCap(res, uint32(c.cap.Seconds())) | 		setCap(res, uint32(c.cap.Seconds())) | ||||||
| @@ -131,7 +71,7 @@ func (c *CachingResponseWriter) WriteMsg(res *dns.Msg) error { | |||||||
| 	return c.ResponseWriter.WriteMsg(res) | 	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 == "" { | 	if key == "" { | ||||||
| 		// logger the log? TODO(miek) | 		// logger the log? TODO(miek) | ||||||
| 		return | 		return | ||||||
| @@ -139,14 +79,14 @@ func (c *CachingResponseWriter) Set(m *dns.Msg, key string, mt messageType) { | |||||||
|  |  | ||||||
| 	duration := c.cap | 	duration := c.cap | ||||||
| 	switch mt { | 	switch mt { | ||||||
| 	case success: | 	case middleware.Success, middleware.Delegation: | ||||||
| 		if c.cap == 0 { | 		if c.cap == 0 { | ||||||
| 			duration = minTtl(m.Answer, mt) | 			duration = minTtl(m.Answer, mt) | ||||||
| 		} | 		} | ||||||
| 		i := newItem(m, duration) | 		i := newItem(m, duration) | ||||||
|  |  | ||||||
| 		c.cache.Set(key, i, duration) | 		c.cache.Set(key, i, duration) | ||||||
| 	case nameError, noData: | 	case middleware.NameError, middleware.NoData: | ||||||
| 		if c.cap == 0 { | 		if c.cap == 0 { | ||||||
| 			duration = minTtl(m.Ns, mt) | 			duration = minTtl(m.Ns, mt) | ||||||
| 		} | 		} | ||||||
| @@ -167,19 +107,19 @@ func (c *CachingResponseWriter) Hijack() { | |||||||
| 	return | 	return | ||||||
| } | } | ||||||
|  |  | ||||||
| func minTtl(rrs []dns.RR, mt messageType) time.Duration { | func minTtl(rrs []dns.RR, mt middleware.MsgType) time.Duration { | ||||||
| 	if mt != success && mt != nameError && mt != noData { | 	if mt != middleware.Success && mt != middleware.NameError && mt != middleware.NoData { | ||||||
| 		return 0 | 		return 0 | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	minTtl := maxTtl | 	minTtl := maxTtl | ||||||
| 	for _, r := range rrs { | 	for _, r := range rrs { | ||||||
| 		switch mt { | 		switch mt { | ||||||
| 		case nameError, noData: | 		case middleware.NameError, middleware.NoData: | ||||||
| 			if r.Header().Rrtype == dns.TypeSOA { | 			if r.Header().Rrtype == dns.TypeSOA { | ||||||
| 				return time.Duration(r.(*dns.SOA).Minttl) * time.Second | 				return time.Duration(r.(*dns.SOA).Minttl) * time.Second | ||||||
| 			} | 			} | ||||||
| 		case success: | 		case middleware.Success, middleware.Delegation: | ||||||
| 			if r.Header().Ttl < minTtl { | 			if r.Header().Ttl < minTtl { | ||||||
| 				minTtl = r.Header().Ttl | 				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) | 		m = cacheMsg(m, tc) | ||||||
| 		do := tc.in.Do | 		do := tc.in.Do | ||||||
|  |  | ||||||
| 		mt, _ := classify(m) | 		mt, _ := middleware.Classify(m) | ||||||
| 		key := cacheKey(m, mt, do) | 		key := cacheKey(m, mt, do) | ||||||
| 		crr.Set(m, key, mt) | 		crr.set(m, key, mt) | ||||||
|  |  | ||||||
| 		name := middleware.Name(m.Question[0].Name).Normalize() | 		name := middleware.Name(m.Question[0].Name).Normalize() | ||||||
| 		qtype := m.Question[0].Qtype | 		qtype := m.Question[0].Qtype | ||||||
| 		i, ok := c.Get(name, qtype, do) | 		i, ok := c.get(name, qtype, do) | ||||||
| 		if !ok && !m.Truncated { | 		if !ok && !m.Truncated { | ||||||
| 			t.Errorf("Truncated message should not have been cached") | 			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? | 	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) | 		resp := i.toMsg(r) | ||||||
| 		state.SizeAndDo(resp) | 		state.SizeAndDo(resp) | ||||||
| 		w.WriteMsg(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) | 	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) | 	nxdomain := nameErrorKey(qname, do) | ||||||
| 	if i, ok := c.cache.Get(nxdomain); ok { | 	if i, ok := c.cache.Get(nxdomain); ok { | ||||||
| 		return i.(*item), true | 		return i.(*item), true | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// TODO(miek): delegation was added double check | ||||||
| 	successOrNoData := successKey(qname, qtype, do) | 	successOrNoData := successKey(qname, qtype, do) | ||||||
| 	if i, ok := c.cache.Get(successOrNoData); ok { | 	if i, ok := c.cache.Get(successOrNoData); ok { | ||||||
| 		return i.(*item), true | 		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" | ||||||
| 	"github.com/miekg/coredns/middleware/etcd/msg" | 	"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/proxy" | ||||||
|  | 	"github.com/miekg/coredns/middleware/singleflight" | ||||||
|  |  | ||||||
| 	etcdc "github.com/coreos/etcd/client" | 	etcdc "github.com/coreos/etcd/client" | ||||||
| 	"golang.org/x/net/context" | 	"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. | 	// NS record for this zone live in a special place, ns.dns.<zone>. Fake our lookup. | ||||||
| 	// only a tad bit fishy... | 	// only a tad bit fishy... | ||||||
| 	old := state.QName() | 	old := state.QName() | ||||||
|  |  | ||||||
|  | 	state.Clear() | ||||||
| 	state.Req.Question[0].Name = "ns.dns." + zone | 	state.Req.Question[0].Name = "ns.dns." + zone | ||||||
| 	services, err := e.records(state, false) | 	services, err := e.records(state, false) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, nil, err | 		return nil, nil, err | ||||||
| 	} | 	} | ||||||
|  | 	// ... and reset | ||||||
| 	state.Req.Question[0].Name = old | 	state.Req.Question[0].Name = old | ||||||
|  |  | ||||||
| 	for _, serv := range services { | 	for _, serv := range services { | ||||||
|   | |||||||
| @@ -10,8 +10,8 @@ import ( | |||||||
|  |  | ||||||
| 	"github.com/miekg/coredns/middleware" | 	"github.com/miekg/coredns/middleware" | ||||||
| 	"github.com/miekg/coredns/middleware/etcd/msg" | 	"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/proxy" | ||||||
|  | 	"github.com/miekg/coredns/middleware/singleflight" | ||||||
| 	"github.com/miekg/coredns/middleware/test" | 	"github.com/miekg/coredns/middleware/test" | ||||||
| 	"github.com/miekg/dns" | 	"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. | 	// Check for duplicate NSEC. | ||||||
| 	if nsec[nsecIndex].Header().Name == nsec1[nsec1Index].Header().Name && | 	if nsec[nsecIndex].Header().Name == nsec1[nsec1Index].Header().Name && | ||||||
| 		nsec[nsecIndex].(*dns.NSEC).NextDomain == nsec1[nsec1Index].(*dns.NSEC).NextDomain { | 		nsec[nsecIndex].(*dns.NSEC).NextDomain == nsec1[nsec1Index].(*dns.NSEC).NextDomain { | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| package file | package file | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"errors" | ||||||
| 	"io" | 	"io" | ||||||
| 	"log" | 	"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} | 	state := middleware.State{W: w, Req: r} | ||||||
|  |  | ||||||
| 	if state.QClass() != dns.ClassINET { | 	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() | 	qname := state.Name() | ||||||
| 	zone := middleware.Zones(f.Zones.Names).Matches(qname) | 	zone := middleware.Zones(f.Zones.Names).Matches(qname) | ||||||
| 	if zone == "" { | 	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] | 	z, ok := f.Zones.Z[zone] | ||||||
| 	if !ok { | 	if !ok { | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| func TestZoneReload(t *testing.T) { | func TestZoneReload(t *testing.T) { | ||||||
| 	fileName, rm, err := test.Zone(t, ".", reloadZoneTest) | 	fileName, rm, err := test.TempFile(t, ".", reloadZoneTest) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("failed to create zone: %s", err) | 		t.Fatalf("failed to create zone: %s", err) | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -15,9 +15,13 @@ type State struct { | |||||||
| 	Req *dns.Msg | 	Req *dns.Msg | ||||||
| 	W   dns.ResponseWriter | 	W   dns.ResponseWriter | ||||||
|  |  | ||||||
| 	// Cache size after first call to Size or Do | 	// Cache size after first call to Size or Do. | ||||||
| 	size int | 	size int | ||||||
| 	do   int // 0: not, 1: true: 2: false | 	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. | // 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. | // NowDate returns the current date/time that can be used in other time functions. | ||||||
| func (s *State) NowDate() time.Time { return time.Now() } | 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. | // IP gets the (remote) IP address of the client making the request. | ||||||
| func (s *State) IP() string { | func (s *State) IP() string { | ||||||
| 	ip, _, err := net.SplitHostPort(s.W.RemoteAddr().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 | // Name returns the name of the question in the request. Note | ||||||
| // this name will always have a closing dot and will be lower cased. | // 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. | // QName returns the name of the question in the request. | ||||||
| func (s *State) QName() string { return dns.Name(s.Req.Question[0].Name).String() } | 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 | 	return m | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Clear clears all caching from State s. | ||||||
|  | func (s *State) Clear() { | ||||||
|  | 	s.name = "" | ||||||
|  | } | ||||||
|  |  | ||||||
| const ( | const ( | ||||||
| 	doTrue  = 1 | 	doTrue  = 1 | ||||||
| 	doFalse = 2 | 	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 | ||||||
|  | } | ||||||
| @@ -45,17 +45,18 @@ func (c Case) Msg() *dns.Msg { | |||||||
| 	return m | 	return m | ||||||
| } | } | ||||||
|  |  | ||||||
| func A(rr string) *dns.A         { r, _ := dns.NewRR(rr); return r.(*dns.A) } | 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 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 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 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 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 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 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 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 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 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 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 { | func OPT(bufsize int, do bool) *dns.OPT { | ||||||
| 	o := new(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) { | func TestLookupProxy(t *testing.T) { | ||||||
| 	name, rm, err := test.Zone(t, ".", exampleOrg) | 	name, rm, err := test.TempFile(t, ".", exampleOrg) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("failed to created zone: %s", err) | 		t.Fatalf("failed to created zone: %s", err) | ||||||
| 	} | 	} | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user