mirror of
				https://github.com/coredns/coredns.git
				synced 2025-10-28 16:54:15 -04:00 
			
		
		
		
	plugin/tsig: new plugin TSIG (#4957)
* expose tsig secrets via dnsserver.Config * add tsig plugin Signed-off-by: Chris O'Haver <cohaver@infoblox.com>
This commit is contained in:
		| @@ -43,6 +43,9 @@ type Config struct { | |||||||
| 	// TLSConfig when listening for encrypted connections (gRPC, DNS-over-TLS). | 	// TLSConfig when listening for encrypted connections (gRPC, DNS-over-TLS). | ||||||
| 	TLSConfig *tls.Config | 	TLSConfig *tls.Config | ||||||
|  |  | ||||||
|  | 	// TSIG secrets, [name]key. | ||||||
|  | 	TsigSecret map[string]string | ||||||
|  |  | ||||||
| 	// Plugin stack. | 	// Plugin stack. | ||||||
| 	Plugin []plugin.Plugin | 	Plugin []plugin.Plugin | ||||||
|  |  | ||||||
|   | |||||||
| @@ -156,6 +156,7 @@ func (h *dnsContext) MakeServers() ([]caddy.Server, error) { | |||||||
| 		c.Debug = c.firstConfigInBlock.Debug | 		c.Debug = c.firstConfigInBlock.Debug | ||||||
| 		c.Stacktrace = c.firstConfigInBlock.Stacktrace | 		c.Stacktrace = c.firstConfigInBlock.Stacktrace | ||||||
| 		c.TLSConfig = c.firstConfigInBlock.TLSConfig | 		c.TLSConfig = c.firstConfigInBlock.TLSConfig | ||||||
|  | 		c.TsigSecret = c.firstConfigInBlock.TsigSecret | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// we must map (group) each config to a bind address | 	// we must map (group) each config to a bind address | ||||||
|   | |||||||
| @@ -44,6 +44,8 @@ type Server struct { | |||||||
| 	debug        bool               // disable recover() | 	debug        bool               // disable recover() | ||||||
| 	stacktrace   bool               // enable stacktrace in recover error log | 	stacktrace   bool               // enable stacktrace in recover error log | ||||||
| 	classChaos   bool               // allow non-INET class queries | 	classChaos   bool               // allow non-INET class queries | ||||||
|  |  | ||||||
|  | 	tsigSecret map[string]string | ||||||
| } | } | ||||||
|  |  | ||||||
| // NewServer returns a new CoreDNS server and compiles all plugins in to it. By default CH class | // NewServer returns a new CoreDNS server and compiles all plugins in to it. By default CH class | ||||||
| @@ -54,6 +56,7 @@ func NewServer(addr string, group []*Config) (*Server, error) { | |||||||
| 		Addr:         addr, | 		Addr:         addr, | ||||||
| 		zones:        make(map[string]*Config), | 		zones:        make(map[string]*Config), | ||||||
| 		graceTimeout: 5 * time.Second, | 		graceTimeout: 5 * time.Second, | ||||||
|  | 		tsigSecret: make(map[string]string), | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// We have to bound our wg with one increment | 	// We have to bound our wg with one increment | ||||||
| @@ -73,6 +76,11 @@ func NewServer(addr string, group []*Config) (*Server, error) { | |||||||
| 		// set the config per zone | 		// set the config per zone | ||||||
| 		s.zones[site.Zone] = site | 		s.zones[site.Zone] = site | ||||||
|  |  | ||||||
|  | 		// copy tsig secrets | ||||||
|  | 		for key, secret := range site.TsigSecret { | ||||||
|  | 			s.tsigSecret[key] = secret | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		// compile custom plugin for everything | 		// compile custom plugin for everything | ||||||
| 		var stack plugin.Handler | 		var stack plugin.Handler | ||||||
| 		for i := len(site.Plugin) - 1; i >= 0; i-- { | 		for i := len(site.Plugin) - 1; i >= 0; i-- { | ||||||
| @@ -115,7 +123,7 @@ func (s *Server) Serve(l net.Listener) error { | |||||||
| 		ctx := context.WithValue(context.Background(), Key{}, s) | 		ctx := context.WithValue(context.Background(), Key{}, s) | ||||||
| 		ctx = context.WithValue(ctx, LoopKey{}, 0) | 		ctx = context.WithValue(ctx, LoopKey{}, 0) | ||||||
| 		s.ServeDNS(ctx, w, r) | 		s.ServeDNS(ctx, w, r) | ||||||
| 	})} | 	}), TsigSecret: s.tsigSecret} | ||||||
| 	s.m.Unlock() | 	s.m.Unlock() | ||||||
|  |  | ||||||
| 	return s.server[tcp].ActivateAndServe() | 	return s.server[tcp].ActivateAndServe() | ||||||
| @@ -129,7 +137,7 @@ func (s *Server) ServePacket(p net.PacketConn) error { | |||||||
| 		ctx := context.WithValue(context.Background(), Key{}, s) | 		ctx := context.WithValue(context.Background(), Key{}, s) | ||||||
| 		ctx = context.WithValue(ctx, LoopKey{}, 0) | 		ctx = context.WithValue(ctx, LoopKey{}, 0) | ||||||
| 		s.ServeDNS(ctx, w, r) | 		s.ServeDNS(ctx, w, r) | ||||||
| 	})} | 	}), TsigSecret: s.tsigSecret} | ||||||
| 	s.m.Unlock() | 	s.m.Unlock() | ||||||
|  |  | ||||||
| 	return s.server[udp].ActivateAndServe() | 	return s.server[udp].ActivateAndServe() | ||||||
|   | |||||||
| @@ -34,6 +34,7 @@ var Directives = []string{ | |||||||
| 	"any", | 	"any", | ||||||
| 	"chaos", | 	"chaos", | ||||||
| 	"loadbalance", | 	"loadbalance", | ||||||
|  | 	"tsig", | ||||||
| 	"cache", | 	"cache", | ||||||
| 	"rewrite", | 	"rewrite", | ||||||
| 	"header", | 	"header", | ||||||
|   | |||||||
| @@ -52,5 +52,6 @@ import ( | |||||||
| 	_ "github.com/coredns/coredns/plugin/tls" | 	_ "github.com/coredns/coredns/plugin/tls" | ||||||
| 	_ "github.com/coredns/coredns/plugin/trace" | 	_ "github.com/coredns/coredns/plugin/trace" | ||||||
| 	_ "github.com/coredns/coredns/plugin/transfer" | 	_ "github.com/coredns/coredns/plugin/transfer" | ||||||
|  | 	_ "github.com/coredns/coredns/plugin/tsig" | ||||||
| 	_ "github.com/coredns/coredns/plugin/whoami" | 	_ "github.com/coredns/coredns/plugin/whoami" | ||||||
| ) | ) | ||||||
|   | |||||||
| @@ -43,6 +43,7 @@ acl:acl | |||||||
| any:any | any:any | ||||||
| chaos:chaos | chaos:chaos | ||||||
| loadbalance:loadbalance | loadbalance:loadbalance | ||||||
|  | tsig:tsig | ||||||
| cache:cache | cache:cache | ||||||
| rewrite:rewrite | rewrite:rewrite | ||||||
| header:header | header:header | ||||||
|   | |||||||
| @@ -28,8 +28,10 @@ func setup(c *caddy.Controller) error { | |||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
| 	c.OnStartup(func() error { | 	c.OnStartup(func() error { | ||||||
|  | 		config := dnsserver.GetConfig(c) | ||||||
|  | 		t.tsigSecret = config.TsigSecret | ||||||
| 		// find all plugins that implement Transferer and add them to Transferers | 		// find all plugins that implement Transferer and add them to Transferers | ||||||
| 		plugins := dnsserver.GetConfig(c).Handlers() | 		plugins := config.Handlers() | ||||||
| 		for _, pl := range plugins { | 		for _, pl := range plugins { | ||||||
| 			tr, ok := pl.(Transferer) | 			tr, ok := pl.(Transferer) | ||||||
| 			if !ok { | 			if !ok { | ||||||
|   | |||||||
| @@ -18,6 +18,7 @@ var log = clog.NewWithPlugin("transfer") | |||||||
| type Transfer struct { | type Transfer struct { | ||||||
| 	Transferers []Transferer // List of plugins that implement Transferer | 	Transferers []Transferer // List of plugins that implement Transferer | ||||||
| 	xfrs        []*xfr | 	xfrs        []*xfr | ||||||
|  | 	tsigSecret  map[string]string | ||||||
| 	Next        plugin.Handler | 	Next        plugin.Handler | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -110,6 +111,9 @@ func (t *Transfer) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Ms | |||||||
| 	// Send response to client | 	// Send response to client | ||||||
| 	ch := make(chan *dns.Envelope) | 	ch := make(chan *dns.Envelope) | ||||||
| 	tr := new(dns.Transfer) | 	tr := new(dns.Transfer) | ||||||
|  | 	if r.IsTsig() != nil { | ||||||
|  | 		tr.TsigSecret = t.tsigSecret | ||||||
|  | 	} | ||||||
| 	errCh := make(chan error) | 	errCh := make(chan error) | ||||||
| 	go func() { | 	go func() { | ||||||
| 		if err := tr.Out(w, r, ch); err != nil { | 		if err := tr.Out(w, r, ch); err != nil { | ||||||
|   | |||||||
							
								
								
									
										111
									
								
								plugin/tsig/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								plugin/tsig/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,111 @@ | |||||||
|  | # tsig | ||||||
|  |  | ||||||
|  | ## Name | ||||||
|  |  | ||||||
|  | *tsig* - validate TSIG requests and sign responses. | ||||||
|  |  | ||||||
|  | ## Description | ||||||
|  |  | ||||||
|  | With *tsig*, you can define a set of TSIG secret keys for validating incoming TSIG requests and signing | ||||||
|  | responses. It can also require TSIG for certain query types, refusing requests that do not comply. | ||||||
|  |  | ||||||
|  | ## Syntax | ||||||
|  |  | ||||||
|  | ~~~ | ||||||
|  | tsig [ZONE...] { | ||||||
|  |   secret NAME KEY | ||||||
|  |   secrets FILE | ||||||
|  |   require [QTYPE...] | ||||||
|  | } | ||||||
|  | ~~~ | ||||||
|  |  | ||||||
|  |    * **ZONE** - the zones *tsig* will TSIG.  By default, the zones from the server block are used. | ||||||
|  |  | ||||||
|  |    * `secret` **NAME** **KEY** - specifies a TSIG secret for **NAME** with **KEY**. Use this option more than once | ||||||
|  |    to define multiple secrets. Secrets are global to the server instance, not just for the enclosing **ZONE**. | ||||||
|  |  | ||||||
|  |    * `secrets` **FILE** - same as `secret`, but load the secrets from a file. The file may define any number | ||||||
|  |      of unique keys, each in the following `named.conf` format: | ||||||
|  |      ```cgo | ||||||
|  |      key "example." { | ||||||
|  |          secret "X28hl0BOfAL5G0jsmJWSacrwn7YRm2f6U5brnzwWEus="; | ||||||
|  |      }; | ||||||
|  |      ``` | ||||||
|  |      Each key may also specify an `algorithm` e.g. `algorithm hmac-sha256;`, but this is currently ignored by the plugin. | ||||||
|  |  | ||||||
|  |      * `require` **QTYPE...** - the query types that must be TSIG'd. Requests of the specified types | ||||||
|  |    will be `REFUSED` if they are not signed.`require all` will require requests of all types to be | ||||||
|  |    signed. `require none` will not require requests any types to be signed. Default behavior is to not require. | ||||||
|  |  | ||||||
|  | ## Examples | ||||||
|  |  | ||||||
|  | Require TSIG signed transactions for transfer requests to `example.zone`. | ||||||
|  |   | ||||||
|  | ``` | ||||||
|  | example.zone { | ||||||
|  |   tsig { | ||||||
|  |     secret example.zone.key. NoTCJU+DMqFWywaPyxSijrDEA/eC3nK0xi3AMEZuPVk= | ||||||
|  |     require AXFR IXFR | ||||||
|  |   } | ||||||
|  |   transfer { | ||||||
|  |     to * | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | Require TSIG signed transactions for all requests to `auth.zone`. | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | auth.zone { | ||||||
|  |   tsig { | ||||||
|  |     secret auth.zone.key. NoTCJU+DMqFWywaPyxSijrDEA/eC3nK0xi3AMEZuPVk= | ||||||
|  |     require all | ||||||
|  |   } | ||||||
|  |   forward . 10.1.0.2 | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Bugs | ||||||
|  |  | ||||||
|  | ### Zone Transfer Notifies | ||||||
|  |  | ||||||
|  | With the transfer plugin, zone transfer notifications from CoreDNS are not TSIG signed.  | ||||||
|  |  | ||||||
|  | ### Special Considerations for Forwarding Servers (RFC 8945 5.5) | ||||||
|  |  | ||||||
|  | https://datatracker.ietf.org/doc/html/rfc8945#section-5.5 | ||||||
|  |  | ||||||
|  | CoreDNS does not implement this section as follows ... | ||||||
|  |  | ||||||
|  | * RFC requirement: | ||||||
|  |   > If the name on the TSIG is not | ||||||
|  | of a secret that the server shares with the originator, the server | ||||||
|  | MUST forward the message unchanged including the TSIG. | ||||||
|  |  | ||||||
|  |   CoreDNS behavior: | ||||||
|  | If ths zone of the request matches the _tsig_ plugin zones, then the TSIG record | ||||||
|  | is always stripped. But even when the _tsig_ plugin is not involved, the _forward_ plugin | ||||||
|  | may alter the message with compression, which would cause validation failure | ||||||
|  | at the destination. | ||||||
|  |  | ||||||
|  |  | ||||||
|  | * RFC requirement: | ||||||
|  |   > If the TSIG passes all checks, the forwarding | ||||||
|  | server MUST, if possible, include a TSIG of its own to the | ||||||
|  | destination or the next forwarder. | ||||||
|  |  | ||||||
|  |   CoreDNS behavior: | ||||||
|  | If ths zone of the request matches the _tsig_ plugin zones, _forward_ plugin will | ||||||
|  | proxy the request upstream without TSIG. | ||||||
|  |  | ||||||
|  |  | ||||||
|  | * RFC requirement: | ||||||
|  |   > If no transaction security is | ||||||
|  | available to the destination and the message is a query, and if the | ||||||
|  | corresponding response has the AD flag (see RFC4035) set, the | ||||||
|  | forwarder MUST clear the AD flag before adding the TSIG to the | ||||||
|  | response and returning the result to the system from which it | ||||||
|  | received the query. | ||||||
|  |  | ||||||
|  |   CoreDNS behavior: | ||||||
|  | The AD flag is not cleared. | ||||||
							
								
								
									
										168
									
								
								plugin/tsig/setup.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										168
									
								
								plugin/tsig/setup.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,168 @@ | |||||||
|  | package tsig | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"bufio" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	"os" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"github.com/coredns/caddy" | ||||||
|  | 	"github.com/coredns/coredns/core/dnsserver" | ||||||
|  | 	"github.com/coredns/coredns/plugin" | ||||||
|  |  | ||||||
|  | 	"github.com/miekg/dns" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	caddy.RegisterPlugin(pluginName, caddy.Plugin{ | ||||||
|  | 		ServerType: "dns", | ||||||
|  | 		Action:     setup, | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func setup(c *caddy.Controller) error { | ||||||
|  | 	t, err := parse(c) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return plugin.Error(pluginName, c.ArgErr()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	config := dnsserver.GetConfig(c) | ||||||
|  |  | ||||||
|  | 	config.TsigSecret = t.secrets | ||||||
|  |  | ||||||
|  | 	config.AddPlugin(func(next plugin.Handler) plugin.Handler { | ||||||
|  | 		t.Next = next | ||||||
|  | 		return t | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func parse(c *caddy.Controller) (*TSIGServer, error) { | ||||||
|  | 	t := &TSIGServer{ | ||||||
|  | 		secrets: make(map[string]string), | ||||||
|  | 		types:   defaultQTypes, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for i := 0; c.Next(); i++ { | ||||||
|  | 		if i > 0 { | ||||||
|  | 			return nil, plugin.ErrOnce | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		t.Zones = plugin.OriginsFromArgsOrServerBlock(c.RemainingArgs(), c.ServerBlockKeys) | ||||||
|  | 		for c.NextBlock() { | ||||||
|  | 			switch c.Val() { | ||||||
|  | 			case "secret": | ||||||
|  | 				args := c.RemainingArgs() | ||||||
|  | 				if len(args) != 2 { | ||||||
|  | 					return nil, c.ArgErr() | ||||||
|  | 				} | ||||||
|  | 				k := plugin.Name(args[0]).Normalize() | ||||||
|  | 				if _, exists := t.secrets[k]; exists { | ||||||
|  | 					return nil, fmt.Errorf("key %q redefined", k) | ||||||
|  | 				} | ||||||
|  | 				t.secrets[k] = args[1] | ||||||
|  | 			case "secrets": | ||||||
|  | 				args := c.RemainingArgs() | ||||||
|  | 				if len(args) != 1 { | ||||||
|  | 					return nil, c.ArgErr() | ||||||
|  | 				} | ||||||
|  | 				f, err := os.Open(args[0]) | ||||||
|  | 				if err != nil { | ||||||
|  | 					return nil, err | ||||||
|  | 				} | ||||||
|  | 				secrets, err := parseKeyFile(f) | ||||||
|  | 				if err != nil { | ||||||
|  | 					return nil, err | ||||||
|  | 				} | ||||||
|  | 				for k, s := range secrets { | ||||||
|  | 					if _, exists := t.secrets[k]; exists { | ||||||
|  | 						return nil, fmt.Errorf("key %q redefined", k) | ||||||
|  | 					} | ||||||
|  | 					t.secrets[k] = s | ||||||
|  | 				} | ||||||
|  | 			case "require": | ||||||
|  | 				t.types = qTypes{} | ||||||
|  | 				args := c.RemainingArgs() | ||||||
|  | 				if len(args) == 0 { | ||||||
|  | 					return nil, c.ArgErr() | ||||||
|  | 				} | ||||||
|  | 				if args[0] == "all" { | ||||||
|  | 					t.all = true | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  | 				if args[0] == "none" { | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  | 				for _, str := range args { | ||||||
|  | 					qt, ok := dns.StringToType[str] | ||||||
|  | 					if !ok { | ||||||
|  | 						return nil, c.Errf("unknown query type '%s'", str) | ||||||
|  | 					} | ||||||
|  | 					t.types[qt] = struct{}{} | ||||||
|  | 				} | ||||||
|  | 			default: | ||||||
|  | 				return nil, c.Errf("unknown property '%s'", c.Val()) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return t, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func parseKeyFile(f io.Reader) (map[string]string, error) { | ||||||
|  | 	secrets := make(map[string]string) | ||||||
|  | 	s := bufio.NewScanner(f) | ||||||
|  | 	for s.Scan() { | ||||||
|  | 		fields := strings.Fields(s.Text()) | ||||||
|  | 		if len(fields) == 0 { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		if fields[0] != "key" { | ||||||
|  | 			return nil, fmt.Errorf("unexpected token %q", fields[0]) | ||||||
|  | 		} | ||||||
|  | 		if len(fields) < 2 { | ||||||
|  | 			return nil, fmt.Errorf("expected key name %q", s.Text()) | ||||||
|  | 		} | ||||||
|  | 		key := strings.Trim(fields[1], "\"{") | ||||||
|  | 		if len(key) == 0 { | ||||||
|  | 			return nil, fmt.Errorf("expected key name %q", s.Text()) | ||||||
|  | 		} | ||||||
|  | 		key = plugin.Name(key).Normalize() | ||||||
|  | 		if _, ok := secrets[key]; ok { | ||||||
|  | 			return nil, fmt.Errorf("key %q redefined", key) | ||||||
|  | 		} | ||||||
|  | 	key: | ||||||
|  | 		for s.Scan() { | ||||||
|  | 			fields := strings.Fields(s.Text()) | ||||||
|  | 			if len(fields) == 0 { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			switch fields[0] { | ||||||
|  | 			case "algorithm": | ||||||
|  | 				continue | ||||||
|  | 			case "secret": | ||||||
|  | 				if len(fields) < 2 { | ||||||
|  | 					return nil, fmt.Errorf("expected secret key %q", s.Text()) | ||||||
|  | 				} | ||||||
|  | 				secret := strings.Trim(fields[1], "\";") | ||||||
|  | 				if len(secret) == 0 { | ||||||
|  | 					return nil, fmt.Errorf("expected secret key %q", s.Text()) | ||||||
|  | 				} | ||||||
|  | 				secrets[key] = secret | ||||||
|  | 			case "}": | ||||||
|  | 				fallthrough | ||||||
|  | 			case "};": | ||||||
|  | 				break key | ||||||
|  | 			default: | ||||||
|  | 				return nil, fmt.Errorf("unexpected token %q", fields[0]) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		if _, ok := secrets[key]; !ok { | ||||||
|  | 			return nil, fmt.Errorf("expected secret for key %q", key) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return secrets, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var defaultQTypes = qTypes{} | ||||||
							
								
								
									
										248
									
								
								plugin/tsig/setup_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										248
									
								
								plugin/tsig/setup_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,248 @@ | |||||||
|  | package tsig | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"strings" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"github.com/coredns/caddy" | ||||||
|  | 	"github.com/coredns/coredns/plugin/test" | ||||||
|  |  | ||||||
|  | 	"github.com/miekg/dns" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestParse(t *testing.T) { | ||||||
|  |  | ||||||
|  | 	secrets := map[string]string{ | ||||||
|  | 		"name.key.":  "test-key", | ||||||
|  | 		"name2.key.": "test-key-2", | ||||||
|  | 	} | ||||||
|  | 	secretConfig := "" | ||||||
|  | 	for k, s := range secrets { | ||||||
|  | 		secretConfig += fmt.Sprintf("secret %s %s\n", k, s) | ||||||
|  | 	} | ||||||
|  | 	secretsFile, cleanup, err := test.TempFile(".", `key "name.key." { | ||||||
|  | 	secret "test-key"; | ||||||
|  | }; | ||||||
|  | key "name2.key." { | ||||||
|  | 	secret "test-key2"; | ||||||
|  | };`) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("failed to create temp file: %v", err) | ||||||
|  | 	} | ||||||
|  | 	defer cleanup() | ||||||
|  |  | ||||||
|  | 	tests := []struct { | ||||||
|  | 		input           string | ||||||
|  | 		shouldErr       bool | ||||||
|  | 		expectedZones   []string | ||||||
|  | 		expectedQTypes  qTypes | ||||||
|  | 		expectedSecrets map[string]string | ||||||
|  | 		expectedAll     bool | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			input:           "tsig {\n " + secretConfig + "}", | ||||||
|  | 			expectedZones:   []string{"."}, | ||||||
|  | 			expectedQTypes:  defaultQTypes, | ||||||
|  | 			expectedSecrets: secrets, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input:           "tsig {\n secrets " + secretsFile + "\n}", | ||||||
|  | 			expectedZones:   []string{"."}, | ||||||
|  | 			expectedQTypes:  defaultQTypes, | ||||||
|  | 			expectedSecrets: secrets, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input:           "tsig example.com {\n " + secretConfig + "}", | ||||||
|  | 			expectedZones:   []string{"example.com."}, | ||||||
|  | 			expectedQTypes:  defaultQTypes, | ||||||
|  | 			expectedSecrets: secrets, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input:           "tsig {\n " + secretConfig + " require all \n}", | ||||||
|  | 			expectedZones:   []string{"."}, | ||||||
|  | 			expectedQTypes:  qTypes{}, | ||||||
|  | 			expectedAll:     true, | ||||||
|  | 			expectedSecrets: secrets, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input:           "tsig {\n " + secretConfig + " require none \n}", | ||||||
|  | 			expectedZones:   []string{"."}, | ||||||
|  | 			expectedQTypes:  qTypes{}, | ||||||
|  | 			expectedAll:     false, | ||||||
|  | 			expectedSecrets: secrets, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input:           "tsig {\n " + secretConfig + " \n require A AAAA \n}", | ||||||
|  | 			expectedZones:   []string{"."}, | ||||||
|  | 			expectedQTypes:  qTypes{dns.TypeA: {}, dns.TypeAAAA: {}}, | ||||||
|  | 			expectedSecrets: secrets, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input:     "tsig {\n blah \n}", | ||||||
|  | 			shouldErr: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input:     "tsig {\n secret name. too many parameters \n}", | ||||||
|  | 			shouldErr: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input:     "tsig {\n require \n}", | ||||||
|  | 			shouldErr: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			input:     "tsig {\n require invalid-qtype \n}", | ||||||
|  | 			shouldErr: true, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	serverBlockKeys := []string{"."} | ||||||
|  | 	for i, test := range tests { | ||||||
|  | 		c := caddy.NewTestController("dns", test.input) | ||||||
|  | 		c.ServerBlockKeys = serverBlockKeys | ||||||
|  | 		ts, err := parse(c) | ||||||
|  |  | ||||||
|  | 		if err == nil && test.shouldErr { | ||||||
|  | 			t.Fatalf("Test %d expected errors, but got no error.", i) | ||||||
|  | 		} else if err != nil && !test.shouldErr { | ||||||
|  | 			t.Fatalf("Test %d expected no errors, but got '%v'", i, err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if test.shouldErr { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if len(test.expectedZones) != len(ts.Zones) { | ||||||
|  | 			t.Fatalf("Test %d expected zones '%v', but got '%v'.", i, test.expectedZones, ts.Zones) | ||||||
|  | 		} | ||||||
|  | 		for j := range test.expectedZones { | ||||||
|  | 			if test.expectedZones[j] != ts.Zones[j] { | ||||||
|  | 				t.Errorf("Test %d expected zones '%v', but got '%v'.", i, test.expectedZones, ts.Zones) | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if test.expectedAll != ts.all { | ||||||
|  | 			t.Errorf("Test %d expected require all to be '%v', but got '%v'.", i, test.expectedAll, ts.all) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if len(test.expectedQTypes) != len(ts.types) { | ||||||
|  | 			t.Fatalf("Test %d expected required types '%v', but got '%v'.", i, test.expectedQTypes, ts.types) | ||||||
|  | 		} | ||||||
|  | 		for qt := range test.expectedQTypes { | ||||||
|  | 			if _, ok := ts.types[qt]; !ok { | ||||||
|  | 				t.Errorf("Test %d required types '%v', but got '%v'.", i, test.expectedQTypes, ts.types) | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if len(test.expectedSecrets) != len(ts.secrets) { | ||||||
|  | 			t.Fatalf("Test %d expected secrets '%v', but got '%v'.", i, test.expectedSecrets, ts.secrets) | ||||||
|  | 		} | ||||||
|  | 		for qt := range test.expectedSecrets { | ||||||
|  | 			secret, ok := ts.secrets[qt] | ||||||
|  | 			if !ok { | ||||||
|  | 				t.Errorf("Test %d required secrets '%v', but got '%v'.", i, test.expectedSecrets, ts.secrets) | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 			if secret != ts.secrets[qt] { | ||||||
|  | 				t.Errorf("Test %d required secrets '%v', but got '%v'.", i, test.expectedSecrets, ts.secrets) | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestParseKeyFile(t *testing.T) { | ||||||
|  | 	var reader = strings.NewReader(`key "foo" { | ||||||
|  | 	algorithm hmac-sha256; | ||||||
|  | 	secret "36eowrtmxceNA3T5AdE+JNUOWFCw3amtcyHACnrDVgQ="; | ||||||
|  | }; | ||||||
|  | key "bar" { | ||||||
|  | 	algorithm hmac-sha256; | ||||||
|  | 	secret "X28hl0BOfAL5G0jsmJWSacrwn7YRm2f6U5brnzwWEus="; | ||||||
|  | }; | ||||||
|  | key "baz" { | ||||||
|  | 	secret "BycDPXSx/5YCD44Q4g5Nd2QNxNRDKwWTXddrU/zpIQM="; | ||||||
|  | };`) | ||||||
|  |  | ||||||
|  | 	secrets, err := parseKeyFile(reader) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Unexpected error: %q", err) | ||||||
|  | 	} | ||||||
|  | 	expectedSecrets := map[string]string{ | ||||||
|  | 		"foo.": "36eowrtmxceNA3T5AdE+JNUOWFCw3amtcyHACnrDVgQ=", | ||||||
|  | 		"bar.": "X28hl0BOfAL5G0jsmJWSacrwn7YRm2f6U5brnzwWEus=", | ||||||
|  | 		"baz.": "BycDPXSx/5YCD44Q4g5Nd2QNxNRDKwWTXddrU/zpIQM=", | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if len(secrets) != len(expectedSecrets) { | ||||||
|  | 		t.Fatalf("result has %d keys. expected %d", len(secrets), len(expectedSecrets)) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for k, sec := range secrets { | ||||||
|  | 		expectedSec, ok := expectedSecrets[k] | ||||||
|  | 		if !ok { | ||||||
|  | 			t.Errorf("unexpected key in result. %q", k) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		if sec != expectedSec { | ||||||
|  | 			t.Errorf("incorrect secret in result for key %q. expected %q got %q ", k, expectedSec, sec) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestParseKeyFileErrors(t *testing.T) { | ||||||
|  | 	tests := []struct { | ||||||
|  | 		in  string | ||||||
|  | 		err string | ||||||
|  | 	}{ | ||||||
|  | 		{in: `key {`, err: "expected key name \"key {\""}, | ||||||
|  | 		{in: `foo "key" {`, err: "unexpected token \"foo\""}, | ||||||
|  | 		{ | ||||||
|  | 			in: `key "foo" { | ||||||
|  | 		secret "36eowrtmxceNA3T5AdE+JNUOWFCw3amtcyHACnrDVgQ="; | ||||||
|  | 	}; | ||||||
|  | 		key "foo" { | ||||||
|  | 		secret "X28hl0BOfAL5G0jsmJWSacrwn7YRm2f6U5brnzwWEus="; | ||||||
|  | 	}; `, | ||||||
|  | 			err: "key \"foo.\" redefined", | ||||||
|  | 		}, | ||||||
|  | 		{in: `key "foo" { | ||||||
|  | 	schmalgorithm hmac-sha256;`, | ||||||
|  | 			err: "unexpected token \"schmalgorithm\"", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			in: `key "foo" { | ||||||
|  | 	schmecret "36eowrtmxceNA3T5AdE+JNUOWFCw3amtcyHACnrDVgQ=";`, | ||||||
|  | 			err: "unexpected token \"schmecret\"", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			in: `key "foo" { | ||||||
|  | 	secret`, | ||||||
|  | 			err: "expected secret key \"\\tsecret\"", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			in: `key "foo" { | ||||||
|  | 	secret ;`, | ||||||
|  | 			err: "expected secret key \"\\tsecret ;\"", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			in: `key "foo" { | ||||||
|  | 	};`, | ||||||
|  | 			err: "expected secret for key \"foo.\"", | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	for i, testcase := range tests { | ||||||
|  | 		_, err := parseKeyFile(strings.NewReader(testcase.in)) | ||||||
|  | 		if err == nil { | ||||||
|  | 			t.Errorf("Test %d: expected error, got no error", i) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		if err.Error() != testcase.err { | ||||||
|  | 			t.Errorf("Test %d: Expected error: %q, got %q", i, testcase.err, err.Error()) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										140
									
								
								plugin/tsig/tsig.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								plugin/tsig/tsig.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,140 @@ | |||||||
|  | package tsig | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"encoding/binary" | ||||||
|  | 	"encoding/hex" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/coredns/coredns/plugin" | ||||||
|  | 	"github.com/coredns/coredns/plugin/pkg/log" | ||||||
|  | 	"github.com/coredns/coredns/request" | ||||||
|  |  | ||||||
|  | 	"github.com/miekg/dns" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // TSIGServer verifies tsig status and adds tsig to responses | ||||||
|  | type TSIGServer struct { | ||||||
|  | 	Zones   []string | ||||||
|  | 	secrets map[string]string // [key-name]secret | ||||||
|  | 	types   qTypes | ||||||
|  | 	all     bool | ||||||
|  | 	Next    plugin.Handler | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type qTypes map[uint16]struct{} | ||||||
|  |  | ||||||
|  | // Name implements plugin.Handler | ||||||
|  | func (t TSIGServer) Name() string { return pluginName } | ||||||
|  |  | ||||||
|  | // ServeDNS implements plugin.Handler | ||||||
|  | func (t *TSIGServer) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { | ||||||
|  | 	var err error | ||||||
|  | 	state := request.Request{Req: r, W: w} | ||||||
|  | 	if z := plugin.Zones(t.Zones).Matches(state.Name()); z == "" { | ||||||
|  | 		return plugin.NextOrFailure(t.Name(), t.Next, ctx, w, r) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var tsigRR = r.IsTsig() | ||||||
|  | 	rcode := dns.RcodeSuccess | ||||||
|  | 	if !t.tsigRequired(state.QType()) && tsigRR == nil { | ||||||
|  | 		return plugin.NextOrFailure(t.Name(), t.Next, ctx, w, r) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if tsigRR == nil { | ||||||
|  | 		log.Debugf("rejecting '%s' request without TSIG\n", dns.TypeToString[state.QType()]) | ||||||
|  | 		rcode = dns.RcodeRefused | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// wrap the response writer so the response will be TSIG signed. | ||||||
|  | 	w = &restoreTsigWriter{w, r, tsigRR} | ||||||
|  |  | ||||||
|  | 	tsigStatus := w.TsigStatus() | ||||||
|  | 	if tsigStatus != nil { | ||||||
|  | 		log.Debugf("TSIG validation failed: %v %v", dns.TypeToString[state.QType()], tsigStatus) | ||||||
|  | 		rcode = dns.RcodeNotAuth | ||||||
|  | 		switch tsigStatus { | ||||||
|  | 		case dns.ErrSecret: | ||||||
|  | 			tsigRR.Error = dns.RcodeBadKey | ||||||
|  | 		case dns.ErrTime: | ||||||
|  | 			tsigRR.Error = dns.RcodeBadTime | ||||||
|  | 		default: | ||||||
|  | 			tsigRR.Error = dns.RcodeBadSig | ||||||
|  | 		} | ||||||
|  | 		resp := new(dns.Msg).SetRcode(r, rcode) | ||||||
|  | 		w.WriteMsg(resp) | ||||||
|  | 		return dns.RcodeSuccess, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// strip the TSIG RR. Next, and subsequent plugins will not see the TSIG RRs. | ||||||
|  | 	// This violates forwarding cases (RFC 8945 5.5). See README.md Bugs | ||||||
|  | 	if len(r.Extra) > 1 { | ||||||
|  | 		r.Extra = r.Extra[0 : len(r.Extra)-1] | ||||||
|  | 	} else { | ||||||
|  | 		r.Extra = []dns.RR{} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if rcode == dns.RcodeSuccess { | ||||||
|  | 		rcode, err = plugin.NextOrFailure(t.Name(), t.Next, ctx, w, r) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Errorf("request handler returned an error: %v\n", err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	// If the plugin chain result was not an error, restore the TSIG and write the response. | ||||||
|  | 	if !plugin.ClientWrite(rcode) { | ||||||
|  | 		resp := new(dns.Msg).SetRcode(r, rcode) | ||||||
|  | 		w.WriteMsg(resp) | ||||||
|  | 	} | ||||||
|  | 	return dns.RcodeSuccess, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t *TSIGServer) tsigRequired(qtype uint16) bool { | ||||||
|  | 	if t.all { | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  | 	if _, ok := t.types[qtype]; ok { | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // restoreTsigWriter Implement Response Writer, and adds a TSIG RR to a response | ||||||
|  | type restoreTsigWriter struct { | ||||||
|  | 	dns.ResponseWriter | ||||||
|  | 	req     *dns.Msg  // original request excluding TSIG if it has one | ||||||
|  | 	reqTSIG *dns.TSIG // original TSIG | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // WriteMsg adds a TSIG RR to the response | ||||||
|  | func (r *restoreTsigWriter) WriteMsg(m *dns.Msg) error { | ||||||
|  | 	// Make sure the response has an EDNS OPT RR if the request had it. | ||||||
|  | 	// Otherwise ScrubWriter would append it *after* TSIG, making it a non-compliant DNS message. | ||||||
|  | 	state := request.Request{Req: r.req, W: r.ResponseWriter} | ||||||
|  | 	state.SizeAndDo(m) | ||||||
|  |  | ||||||
|  | 	repTSIG := m.IsTsig() | ||||||
|  | 	if r.reqTSIG != nil && repTSIG == nil { | ||||||
|  | 		repTSIG = new(dns.TSIG) | ||||||
|  | 		repTSIG.Hdr = dns.RR_Header{Name: r.reqTSIG.Hdr.Name, Rrtype: dns.TypeTSIG, Class: dns.ClassANY} | ||||||
|  | 		repTSIG.Algorithm = r.reqTSIG.Algorithm | ||||||
|  | 		repTSIG.OrigId = m.MsgHdr.Id | ||||||
|  | 		repTSIG.Error = r.reqTSIG.Error | ||||||
|  | 		repTSIG.MAC = r.reqTSIG.MAC | ||||||
|  | 		repTSIG.MACSize = r.reqTSIG.MACSize | ||||||
|  | 		if repTSIG.Error == dns.RcodeBadTime { | ||||||
|  | 			// per RFC 8945 5.2.3. client time goes into TimeSigned, server time in OtherData, OtherLen = 6 ... | ||||||
|  | 			repTSIG.TimeSigned = r.reqTSIG.TimeSigned | ||||||
|  | 			b := make([]byte, 8) | ||||||
|  | 			// TimeSigned is network byte order. | ||||||
|  | 			binary.BigEndian.PutUint64(b, uint64(time.Now().Unix())) | ||||||
|  | 			// truncate to 48 least significant bits (network order 6 rightmost bytes) | ||||||
|  | 			repTSIG.OtherData = hex.EncodeToString(b[2:]) | ||||||
|  | 			repTSIG.OtherLen = 6 | ||||||
|  | 		} | ||||||
|  | 		m.Extra = append(m.Extra, repTSIG) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return r.ResponseWriter.WriteMsg(m) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const pluginName = "tsig" | ||||||
							
								
								
									
										255
									
								
								plugin/tsig/tsig_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										255
									
								
								plugin/tsig/tsig_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,255 @@ | |||||||
|  | package tsig | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  | 	"testing" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/coredns/coredns/plugin/pkg/dnstest" | ||||||
|  | 	"github.com/coredns/coredns/plugin/test" | ||||||
|  | 	"github.com/coredns/coredns/request" | ||||||
|  |  | ||||||
|  | 	"github.com/miekg/dns" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestServeDNS(t *testing.T) { | ||||||
|  | 	cases := []struct { | ||||||
|  | 		zones       []string | ||||||
|  | 		reqTypes    qTypes | ||||||
|  | 		qType       uint16 | ||||||
|  | 		qTsig, all  bool | ||||||
|  | 		expectRcode int | ||||||
|  | 		expectTsig  bool | ||||||
|  | 		statusError bool | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			zones:       []string{"."}, | ||||||
|  | 			all:         true, | ||||||
|  | 			qType:       dns.TypeA, | ||||||
|  | 			qTsig:       true, | ||||||
|  | 			expectRcode: dns.RcodeSuccess, | ||||||
|  | 			expectTsig:  true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			zones:       []string{"."}, | ||||||
|  | 			all:         true, | ||||||
|  | 			qType:       dns.TypeA, | ||||||
|  | 			qTsig:       false, | ||||||
|  | 			expectRcode: dns.RcodeRefused, | ||||||
|  | 			expectTsig:  false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			zones:       []string{"another.domain."}, | ||||||
|  | 			all:         true, | ||||||
|  | 			qType:       dns.TypeA, | ||||||
|  | 			qTsig:       false, | ||||||
|  | 			expectRcode: dns.RcodeSuccess, | ||||||
|  | 			expectTsig:  false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			zones:       []string{"another.domain."}, | ||||||
|  | 			all:         true, | ||||||
|  | 			qType:       dns.TypeA, | ||||||
|  | 			qTsig:       true, | ||||||
|  | 			expectRcode: dns.RcodeSuccess, | ||||||
|  | 			expectTsig:  false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			zones:       []string{"."}, | ||||||
|  | 			reqTypes:    qTypes{dns.TypeAXFR: {}}, | ||||||
|  | 			qType:       dns.TypeAXFR, | ||||||
|  | 			qTsig:       true, | ||||||
|  | 			expectRcode: dns.RcodeSuccess, | ||||||
|  | 			expectTsig:  true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			zones:       []string{"."}, | ||||||
|  | 			reqTypes:    qTypes{}, | ||||||
|  | 			qType:       dns.TypeA, | ||||||
|  | 			qTsig:       false, | ||||||
|  | 			expectRcode: dns.RcodeSuccess, | ||||||
|  | 			expectTsig:  false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			zones:       []string{"."}, | ||||||
|  | 			reqTypes:    qTypes{}, | ||||||
|  | 			qType:       dns.TypeA, | ||||||
|  | 			qTsig:       true, | ||||||
|  | 			expectRcode: dns.RcodeSuccess, | ||||||
|  | 			expectTsig:  true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			zones:       []string{"."}, | ||||||
|  | 			all:         true, | ||||||
|  | 			qType:       dns.TypeA, | ||||||
|  | 			qTsig:       true, | ||||||
|  | 			expectRcode: dns.RcodeNotAuth, | ||||||
|  | 			expectTsig:  true, | ||||||
|  | 			statusError: true, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for i, tc := range cases { | ||||||
|  | 		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { | ||||||
|  | 			tsig := TSIGServer{ | ||||||
|  | 				Zones: tc.zones, | ||||||
|  | 				all:   tc.all, | ||||||
|  | 				types: tc.reqTypes, | ||||||
|  | 				Next:  testHandler(), | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			ctx := context.TODO() | ||||||
|  |  | ||||||
|  | 			var w *dnstest.Recorder | ||||||
|  | 			if tc.statusError { | ||||||
|  | 				w = dnstest.NewRecorder(&ErrWriter{err: dns.ErrSig}) | ||||||
|  | 			} else { | ||||||
|  | 				w = dnstest.NewRecorder(&test.ResponseWriter{}) | ||||||
|  | 			} | ||||||
|  | 			r := new(dns.Msg) | ||||||
|  | 			r.SetQuestion("test.example.", tc.qType) | ||||||
|  | 			if tc.qTsig { | ||||||
|  | 				r.SetTsig("test.key.", dns.HmacSHA256, 300, time.Now().Unix()) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			_, err := tsig.ServeDNS(ctx, w, r) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if w.Msg.Rcode != tc.expectRcode { | ||||||
|  | 				t.Fatalf("expected rcode %v, got %v", tc.expectRcode, w.Msg.Rcode) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if ts := w.Msg.IsTsig(); ts == nil && tc.expectTsig { | ||||||
|  | 				t.Fatal("expected TSIG in response") | ||||||
|  | 			} | ||||||
|  | 			if ts := w.Msg.IsTsig(); ts != nil && !tc.expectTsig { | ||||||
|  | 				t.Fatal("expected no TSIG in response") | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestServeDNSTsigErrors(t *testing.T) { | ||||||
|  | 	clientNow := time.Now().Unix() | ||||||
|  |  | ||||||
|  | 	cases := []struct { | ||||||
|  | 		desc              string | ||||||
|  | 		tsigErr           error | ||||||
|  | 		expectRcode       int | ||||||
|  | 		expectError       int | ||||||
|  | 		expectOtherLength int | ||||||
|  | 		expectTimeSigned  int64 | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			desc:              "Unknown Key", | ||||||
|  | 			tsigErr:           dns.ErrSecret, | ||||||
|  | 			expectRcode:       dns.RcodeNotAuth, | ||||||
|  | 			expectError:       dns.RcodeBadKey, | ||||||
|  | 			expectOtherLength: 0, | ||||||
|  | 			expectTimeSigned:  0, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			desc:              "Bad Signature", | ||||||
|  | 			tsigErr:           dns.ErrSig, | ||||||
|  | 			expectRcode:       dns.RcodeNotAuth, | ||||||
|  | 			expectError:       dns.RcodeBadSig, | ||||||
|  | 			expectOtherLength: 0, | ||||||
|  | 			expectTimeSigned:  0, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			desc:              "Bad Time", | ||||||
|  | 			tsigErr:           dns.ErrTime, | ||||||
|  | 			expectRcode:       dns.RcodeNotAuth, | ||||||
|  | 			expectError:       dns.RcodeBadTime, | ||||||
|  | 			expectOtherLength: 6, | ||||||
|  | 			expectTimeSigned:  clientNow, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	tsig := TSIGServer{ | ||||||
|  | 		Zones: []string{"."}, | ||||||
|  | 		all:   true, | ||||||
|  | 		Next:  testHandler(), | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, tc := range cases { | ||||||
|  | 		t.Run(tc.desc, func(t *testing.T) { | ||||||
|  | 			ctx := context.TODO() | ||||||
|  |  | ||||||
|  | 			var w *dnstest.Recorder | ||||||
|  |  | ||||||
|  | 			w = dnstest.NewRecorder(&ErrWriter{err: tc.tsigErr}) | ||||||
|  |  | ||||||
|  | 			r := new(dns.Msg) | ||||||
|  | 			r.SetQuestion("test.example.", dns.TypeA) | ||||||
|  | 			r.SetTsig("test.key.", dns.HmacSHA256, 300, clientNow) | ||||||
|  |  | ||||||
|  | 			// set a fake MAC and Size in request | ||||||
|  | 			rtsig := r.IsTsig() | ||||||
|  | 			rtsig.MAC = "0123456789012345678901234567890101234567890123456789012345678901" | ||||||
|  | 			rtsig.MACSize = 32 | ||||||
|  |  | ||||||
|  | 			_, err := tsig.ServeDNS(ctx, w, r) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatal(err) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if w.Msg.Rcode != tc.expectRcode { | ||||||
|  | 				t.Fatalf("expected rcode %v, got %v", tc.expectRcode, w.Msg.Rcode) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			ts := w.Msg.IsTsig() | ||||||
|  |  | ||||||
|  | 			if ts == nil { | ||||||
|  | 				t.Fatal("expected TSIG in response") | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if int(ts.Error) != tc.expectError { | ||||||
|  | 				t.Errorf("expected TSIG error code %v, got %v", tc.expectError, ts.Error) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if len(ts.OtherData)/2 != tc.expectOtherLength { | ||||||
|  | 				t.Errorf("expected Other of length %v, got %v", tc.expectOtherLength, len(ts.OtherData)) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if int(ts.OtherLen) != tc.expectOtherLength { | ||||||
|  | 				t.Errorf("expected OtherLen %v, got %v", tc.expectOtherLength, ts.OtherLen) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if ts.TimeSigned != uint64(tc.expectTimeSigned) { | ||||||
|  | 				t.Errorf("expected TimeSigned to be %v, got %v", tc.expectTimeSigned, ts.TimeSigned) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func testHandler() test.HandlerFunc { | ||||||
|  | 	return func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { | ||||||
|  | 		state := request.Request{W: w, Req: r} | ||||||
|  | 		qname := state.Name() | ||||||
|  | 		m := new(dns.Msg) | ||||||
|  | 		rcode := dns.RcodeServerFailure | ||||||
|  | 		if qname == "test.example." { | ||||||
|  | 			m.SetReply(r) | ||||||
|  | 			rr := test.A("test.example.  300  IN  A  1.2.3.48") | ||||||
|  | 			m.Answer = []dns.RR{rr} | ||||||
|  | 			m.Authoritative = true | ||||||
|  | 			rcode = dns.RcodeSuccess | ||||||
|  | 		} | ||||||
|  | 		m.SetRcode(r, rcode) | ||||||
|  | 		w.WriteMsg(m) | ||||||
|  | 		return rcode, nil | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // a test.ResponseWriter that always returns err as the TSIG status error | ||||||
|  | type ErrWriter struct { | ||||||
|  | 	err error | ||||||
|  | 	test.ResponseWriter | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // TsigStatus always returns an error. | ||||||
|  | func (t *ErrWriter) TsigStatus() error { return t.err } | ||||||
							
								
								
									
										166
									
								
								test/tsig_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								test/tsig_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,166 @@ | |||||||
|  | package test | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"testing" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/miekg/dns" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var tsigKey = "tsig.key." | ||||||
|  | var tsigSecret = "i9M+00yrECfVZG2qCjr4mPpaGim/Bq+IWMiNrLjUO4Y=" | ||||||
|  |  | ||||||
|  | var corefile = `.:0 { | ||||||
|  | 		tsig { | ||||||
|  |     		secret ` + tsigKey + ` ` + tsigSecret + ` | ||||||
|  | 		} | ||||||
|  | 		hosts { | ||||||
|  | 			1.2.3.4 test | ||||||
|  | 		} | ||||||
|  | 	}` | ||||||
|  |  | ||||||
|  | func TestTsig(t *testing.T) { | ||||||
|  | 	i, udp, _, err := CoreDNSServerAndPorts(corefile) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Could not get CoreDNS serving instance: %s", err) | ||||||
|  | 	} | ||||||
|  | 	defer i.Stop() | ||||||
|  |  | ||||||
|  | 	m := new(dns.Msg) | ||||||
|  | 	m.SetQuestion("test.", dns.TypeA) | ||||||
|  | 	m.SetTsig(tsigKey, dns.HmacSHA256, 300, time.Now().Unix()) | ||||||
|  |  | ||||||
|  | 	client := dns.Client{Net: "udp", TsigSecret: map[string]string{tsigKey: tsigSecret}} | ||||||
|  | 	r, _, err := client.Exchange(m, udp) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Could not send msg: %s", err) | ||||||
|  | 	} | ||||||
|  | 	if r.Rcode != dns.RcodeSuccess { | ||||||
|  | 		t.Fatalf("Rcode should be dns.RcodeSuccess") | ||||||
|  | 	} | ||||||
|  | 	tsig := r.IsTsig() | ||||||
|  | 	if tsig == nil { | ||||||
|  | 		t.Fatalf("Respose was not TSIG") | ||||||
|  | 	} | ||||||
|  | 	if tsig.Error != dns.RcodeSuccess { | ||||||
|  | 		t.Fatalf("TSIG Error code should be dns.RcodeSuccess") | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestTsigBadKey(t *testing.T) { | ||||||
|  | 	i, udp, _, err := CoreDNSServerAndPorts(corefile) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Could not get CoreDNS serving instance: %s", err) | ||||||
|  | 	} | ||||||
|  | 	defer i.Stop() | ||||||
|  |  | ||||||
|  | 	m := new(dns.Msg) | ||||||
|  | 	m.SetQuestion("test.", dns.TypeA) | ||||||
|  | 	m.SetTsig("bad.key.", dns.HmacSHA256, 300, time.Now().Unix()) | ||||||
|  |  | ||||||
|  | 	// rename client key to a key name the server doesnt have | ||||||
|  | 	client := dns.Client{Net: "udp", TsigSecret: map[string]string{"bad.key.": tsigSecret}} | ||||||
|  | 	r, _, err := client.Exchange(m, udp) | ||||||
|  |  | ||||||
|  | 	if err != dns.ErrAuth { | ||||||
|  | 		t.Fatalf("Expected \"dns: bad authentication\" error, got: %s", err) | ||||||
|  | 	} | ||||||
|  | 	if r.Rcode != dns.RcodeNotAuth { | ||||||
|  | 		t.Fatalf("Rcode should be dns.RcodeNotAuth") | ||||||
|  | 	} | ||||||
|  | 	tsig := r.IsTsig() | ||||||
|  | 	if tsig == nil { | ||||||
|  | 		t.Fatalf("Respose was not TSIG") | ||||||
|  | 	} | ||||||
|  | 	if tsig.Error != dns.RcodeBadKey { | ||||||
|  | 		t.Fatalf("TSIG Error code should be dns.RcodeBadKey") | ||||||
|  | 	} | ||||||
|  | 	if tsig.MAC != "" { | ||||||
|  | 		t.Fatalf("TSIG MAC should be empty") | ||||||
|  | 	} | ||||||
|  | 	if tsig.MACSize != 0 { | ||||||
|  | 		t.Fatalf("TSIG MACSize should be 0") | ||||||
|  | 	} | ||||||
|  | 	if tsig.TimeSigned != 0 { | ||||||
|  | 		t.Fatalf("TSIG TimeSigned should be 0") | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestTsigBadSig(t *testing.T) { | ||||||
|  | 	i, udp, _, err := CoreDNSServerAndPorts(corefile) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Could not get CoreDNS serving instance: %s", err) | ||||||
|  | 	} | ||||||
|  | 	defer i.Stop() | ||||||
|  |  | ||||||
|  | 	m := new(dns.Msg) | ||||||
|  | 	m.SetQuestion("test.", dns.TypeA) | ||||||
|  | 	m.SetTsig(tsigKey, dns.HmacSHA256, 300, time.Now().Unix()) | ||||||
|  |  | ||||||
|  | 	// mangle the client secret so the sig wont match the server sig | ||||||
|  | 	client := dns.Client{Net: "udp", TsigSecret: map[string]string{tsigKey: "BADSIG00ECfVZG2qCjr4mPpaGim/Bq+IWMiNrLjUO4Y="}} | ||||||
|  | 	r, _, err := client.Exchange(m, udp) | ||||||
|  |  | ||||||
|  | 	if err != dns.ErrAuth { | ||||||
|  | 		t.Fatalf("Expected \"dns: bad authentication\" error, got: %s", err) | ||||||
|  | 	} | ||||||
|  | 	if r.Rcode != dns.RcodeNotAuth { | ||||||
|  | 		t.Fatalf("Rcode should be dns.RcodeNotAuth") | ||||||
|  | 	} | ||||||
|  | 	tsig := r.IsTsig() | ||||||
|  | 	if tsig == nil { | ||||||
|  | 		t.Fatalf("Respose was not TSIG") | ||||||
|  | 	} | ||||||
|  | 	if tsig.Error != dns.RcodeBadSig { | ||||||
|  | 		t.Fatalf("TSIG Error code should be dns.RcodeBadSig") | ||||||
|  | 	} | ||||||
|  | 	if tsig.MAC != "" { | ||||||
|  | 		t.Fatalf("TSIG MAC should be empty") | ||||||
|  | 	} | ||||||
|  | 	if tsig.MACSize != 0 { | ||||||
|  | 		t.Fatalf("TSIG MACSize should be 0") | ||||||
|  | 	} | ||||||
|  | 	if tsig.TimeSigned != 0 { | ||||||
|  | 		t.Fatalf("TSIG TimeSigned should be 0") | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestTsigBadTime(t *testing.T) { | ||||||
|  | 	i, udp, _, err := CoreDNSServerAndPorts(corefile) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Could not get CoreDNS serving instance: %s", err) | ||||||
|  | 	} | ||||||
|  | 	defer i.Stop() | ||||||
|  |  | ||||||
|  | 	m := new(dns.Msg) | ||||||
|  | 	m.SetQuestion("test.", dns.TypeA) | ||||||
|  |  | ||||||
|  | 	// set time to be older by > fudge seconds | ||||||
|  | 	m.SetTsig(tsigKey, dns.HmacSHA256, 300, time.Now().Unix()-600) | ||||||
|  |  | ||||||
|  | 	client := dns.Client{Net: "udp", TsigSecret: map[string]string{tsigKey: tsigSecret}} | ||||||
|  | 	r, _, err := client.Exchange(m, udp) | ||||||
|  |  | ||||||
|  | 	if err != dns.ErrAuth { | ||||||
|  | 		t.Fatalf("Expected \"dns: bad authentication\" error, got: %s", err) | ||||||
|  | 	} | ||||||
|  | 	if r.Rcode != dns.RcodeNotAuth { | ||||||
|  | 		t.Fatalf("Rcode should be dns.RcodeNotAuth") | ||||||
|  | 	} | ||||||
|  | 	tsig := r.IsTsig() | ||||||
|  | 	if tsig == nil { | ||||||
|  | 		t.Fatalf("Respose was not TSIG") | ||||||
|  | 	} | ||||||
|  | 	if tsig.Error != dns.RcodeBadTime { | ||||||
|  | 		t.Fatalf("TSIG Error code should be dns.RcodeBadTime") | ||||||
|  | 	} | ||||||
|  | 	if tsig.MAC == "" { | ||||||
|  | 		t.Fatalf("TSIG MAC should not be empty") | ||||||
|  | 	} | ||||||
|  | 	if tsig.MACSize != 32 { | ||||||
|  | 		t.Fatalf("TSIG MACSize should be 32") | ||||||
|  | 	} | ||||||
|  | 	if tsig.TimeSigned == 0 { | ||||||
|  | 		t.Fatalf("TSIG TimeSigned should not be 0") | ||||||
|  | 	} | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user