mirror of
https://github.com/coredns/coredns.git
synced 2026-04-05 11:45:33 -04:00
Extend the tsig plugin to require TSIG signatures based on DNS opcodes,
similar to the existing qtype-based requirement.
The new require_opcode directive accepts opcode names (QUERY, IQUERY,
STATUS, NOTIFY, UPDATE) or the special values "all" and "none".
This is useful for requiring TSIG on dynamic update (UPDATE) or zone
transfer notification (NOTIFY) requests while allowing unsigned queries.
Example:
```
tsig {
secret key. NoTCJU+DMqFWywaPyxSijrDEA/eC3nK0xi3AMEZuPVk=
require_opcode UPDATE NOTIFY
}
```
Signed-off-by: Seena Fallah <seenafallah@gmail.com>
304 lines
8.0 KiB
Go
304 lines
8.0 KiB
Go
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
|
|
expectedOpCodes opCodes
|
|
expectedSecrets map[string]string
|
|
expectedAllTypes bool
|
|
expectedAllOpcodes bool
|
|
}{
|
|
{
|
|
input: "tsig {\n " + secretConfig + "}",
|
|
expectedZones: []string{"."},
|
|
expectedQTypes: defaultQTypes,
|
|
expectedOpCodes: defaultOpCodes,
|
|
expectedSecrets: secrets,
|
|
},
|
|
{
|
|
input: "tsig {\n secrets " + secretsFile + "\n}",
|
|
expectedZones: []string{"."},
|
|
expectedQTypes: defaultQTypes,
|
|
expectedOpCodes: defaultOpCodes,
|
|
expectedSecrets: secrets,
|
|
},
|
|
{
|
|
input: "tsig example.com {\n " + secretConfig + "}",
|
|
expectedZones: []string{"example.com."},
|
|
expectedQTypes: defaultQTypes,
|
|
expectedOpCodes: defaultOpCodes,
|
|
expectedSecrets: secrets,
|
|
},
|
|
{
|
|
input: "tsig {\n " + secretConfig + " require all \n}",
|
|
expectedZones: []string{"."},
|
|
expectedQTypes: qTypes{},
|
|
expectedOpCodes: defaultOpCodes,
|
|
expectedAllTypes: true,
|
|
expectedSecrets: secrets,
|
|
},
|
|
{
|
|
input: "tsig {\n " + secretConfig + " require none \n}",
|
|
expectedZones: []string{"."},
|
|
expectedQTypes: qTypes{},
|
|
expectedOpCodes: defaultOpCodes,
|
|
expectedSecrets: secrets,
|
|
},
|
|
{
|
|
input: "tsig {\n " + secretConfig + " \n require A AAAA \n}",
|
|
expectedZones: []string{"."},
|
|
expectedQTypes: qTypes{dns.TypeA: {}, dns.TypeAAAA: {}},
|
|
expectedOpCodes: defaultOpCodes,
|
|
expectedSecrets: secrets,
|
|
},
|
|
{
|
|
input: "tsig {\n " + secretConfig + " \n require_opcode UPDATE NOTIFY \n}",
|
|
expectedZones: []string{"."},
|
|
expectedQTypes: defaultQTypes,
|
|
expectedOpCodes: opCodes{dns.OpcodeUpdate: {}, dns.OpcodeNotify: {}},
|
|
expectedSecrets: secrets,
|
|
},
|
|
{
|
|
input: "tsig {\n " + secretConfig + " \n require_opcode all \n}",
|
|
expectedZones: []string{"."},
|
|
expectedQTypes: defaultQTypes,
|
|
expectedOpCodes: opCodes{},
|
|
expectedAllOpcodes: true,
|
|
expectedSecrets: secrets,
|
|
},
|
|
{
|
|
input: "tsig {\n " + secretConfig + " \n require_opcode none \n}",
|
|
expectedZones: []string{"."},
|
|
expectedQTypes: defaultQTypes,
|
|
expectedOpCodes: opCodes{},
|
|
expectedSecrets: secrets,
|
|
},
|
|
{
|
|
input: "tsig {\n " + secretConfig + " \n require AXFR \n require_opcode UPDATE \n}",
|
|
expectedZones: []string{"."},
|
|
expectedQTypes: qTypes{dns.TypeAXFR: {}},
|
|
expectedOpCodes: opCodes{dns.OpcodeUpdate: {}},
|
|
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,
|
|
},
|
|
{
|
|
input: "tsig {\n require_opcode \n}",
|
|
shouldErr: true,
|
|
},
|
|
{
|
|
input: "tsig {\n require_opcode INVALID \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.expectedAllTypes != ts.allTypes {
|
|
t.Errorf("Test %d expected require all types to be '%v', but got '%v'.", i, test.expectedAllTypes, ts.allTypes)
|
|
}
|
|
|
|
if test.expectedAllOpcodes != ts.allOpcodes {
|
|
t.Errorf("Test %d expected require all opcodes to be '%v', but got '%v'.", i, test.expectedAllOpcodes, ts.allOpcodes)
|
|
}
|
|
|
|
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.expectedOpCodes) != len(ts.opcodes) {
|
|
t.Fatalf("Test %d expected required opcodes '%v', but got '%v'.", i, test.expectedOpCodes, ts.opcodes)
|
|
}
|
|
for op := range test.expectedOpCodes {
|
|
if _, ok := ts.opcodes[op]; !ok {
|
|
t.Errorf("Test %d required opcodes '%v', but got '%v'.", i, test.expectedOpCodes, ts.opcodes)
|
|
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())
|
|
}
|
|
}
|
|
}
|