Commit 68e141ef authored by Chris O'Haver's avatar Chris O'Haver Committed by GitHub

plugin/tsig: new plugin TSIG (#4957)

* expose tsig secrets via dnsserver.Config
* add tsig plugin
Signed-off-by: default avatarChris O'Haver <cohaver@infoblox.com>
parent 64885950
......@@ -43,6 +43,9 @@ type Config struct {
// TLSConfig when listening for encrypted connections (gRPC, DNS-over-TLS).
TLSConfig *tls.Config
// TSIG secrets, [name]key.
TsigSecret map[string]string
// Plugin stack.
Plugin []plugin.Plugin
......
......@@ -156,6 +156,7 @@ func (h *dnsContext) MakeServers() ([]caddy.Server, error) {
c.Debug = c.firstConfigInBlock.Debug
c.Stacktrace = c.firstConfigInBlock.Stacktrace
c.TLSConfig = c.firstConfigInBlock.TLSConfig
c.TsigSecret = c.firstConfigInBlock.TsigSecret
}
// we must map (group) each config to a bind address
......
......@@ -44,6 +44,8 @@ type Server struct {
debug bool // disable recover()
stacktrace bool // enable stacktrace in recover error log
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
......@@ -54,6 +56,7 @@ func NewServer(addr string, group []*Config) (*Server, error) {
Addr: addr,
zones: make(map[string]*Config),
graceTimeout: 5 * time.Second,
tsigSecret: make(map[string]string),
}
// 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
s.zones[site.Zone] = site
// copy tsig secrets
for key, secret := range site.TsigSecret {
s.tsigSecret[key] = secret
}
// compile custom plugin for everything
var stack plugin.Handler
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(ctx, LoopKey{}, 0)
s.ServeDNS(ctx, w, r)
})}
}), TsigSecret: s.tsigSecret}
s.m.Unlock()
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(ctx, LoopKey{}, 0)
s.ServeDNS(ctx, w, r)
})}
}), TsigSecret: s.tsigSecret}
s.m.Unlock()
return s.server[udp].ActivateAndServe()
......
......@@ -34,6 +34,7 @@ var Directives = []string{
"any",
"chaos",
"loadbalance",
"tsig",
"cache",
"rewrite",
"header",
......
......@@ -52,5 +52,6 @@ import (
_ "github.com/coredns/coredns/plugin/tls"
_ "github.com/coredns/coredns/plugin/trace"
_ "github.com/coredns/coredns/plugin/transfer"
_ "github.com/coredns/coredns/plugin/tsig"
_ "github.com/coredns/coredns/plugin/whoami"
)
......@@ -43,6 +43,7 @@ acl:acl
any:any
chaos:chaos
loadbalance:loadbalance
tsig:tsig
cache:cache
rewrite:rewrite
header:header
......
......@@ -28,8 +28,10 @@ func setup(c *caddy.Controller) error {
})
c.OnStartup(func() error {
config := dnsserver.GetConfig(c)
t.tsigSecret = config.TsigSecret
// find all plugins that implement Transferer and add them to Transferers
plugins := dnsserver.GetConfig(c).Handlers()
plugins := config.Handlers()
for _, pl := range plugins {
tr, ok := pl.(Transferer)
if !ok {
......
......@@ -18,6 +18,7 @@ var log = clog.NewWithPlugin("transfer")
type Transfer struct {
Transferers []Transferer // List of plugins that implement Transferer
xfrs []*xfr
tsigSecret map[string]string
Next plugin.Handler
}
......@@ -110,6 +111,9 @@ func (t *Transfer) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Ms
// Send response to client
ch := make(chan *dns.Envelope)
tr := new(dns.Transfer)
if r.IsTsig() != nil {
tr.TsigSecret = t.tsigSecret
}
errCh := make(chan error)
go func() {
if err := tr.Out(w, r, ch); err != nil {
......
# 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.
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{}
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())
}
}
}
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"
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 }
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")
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment