mirror of
https://github.com/coredns/coredns.git
synced 2025-10-27 16:24:19 -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:
@@ -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 {
|
||||
|
||||
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 }
|
||||
Reference in New Issue
Block a user