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>
336 lines
7.8 KiB
Go
336 lines
7.8 KiB
Go
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
|
|
reqOpCodes opCodes
|
|
qType uint16
|
|
opcode int
|
|
qTsig bool
|
|
allTypes bool
|
|
allOpcodes bool
|
|
expectRcode int
|
|
expectTsig bool
|
|
statusError bool
|
|
}{
|
|
{
|
|
zones: []string{"."},
|
|
allTypes: true,
|
|
qType: dns.TypeA,
|
|
opcode: dns.OpcodeQuery,
|
|
qTsig: true,
|
|
expectRcode: dns.RcodeSuccess,
|
|
expectTsig: true,
|
|
},
|
|
{
|
|
zones: []string{"."},
|
|
allTypes: true,
|
|
qType: dns.TypeA,
|
|
opcode: dns.OpcodeQuery,
|
|
qTsig: false,
|
|
expectRcode: dns.RcodeRefused,
|
|
expectTsig: false,
|
|
},
|
|
{
|
|
zones: []string{"another.domain."},
|
|
allTypes: true,
|
|
qType: dns.TypeA,
|
|
opcode: dns.OpcodeQuery,
|
|
qTsig: false,
|
|
expectRcode: dns.RcodeSuccess,
|
|
expectTsig: false,
|
|
},
|
|
{
|
|
zones: []string{"another.domain."},
|
|
allTypes: true,
|
|
qType: dns.TypeA,
|
|
opcode: dns.OpcodeQuery,
|
|
qTsig: true,
|
|
expectRcode: dns.RcodeSuccess,
|
|
expectTsig: false,
|
|
},
|
|
{
|
|
zones: []string{"."},
|
|
reqTypes: qTypes{dns.TypeAXFR: {}},
|
|
qType: dns.TypeAXFR,
|
|
opcode: dns.OpcodeQuery,
|
|
qTsig: true,
|
|
expectRcode: dns.RcodeSuccess,
|
|
expectTsig: true,
|
|
},
|
|
{
|
|
zones: []string{"."},
|
|
reqTypes: qTypes{},
|
|
qType: dns.TypeA,
|
|
opcode: dns.OpcodeQuery,
|
|
qTsig: false,
|
|
expectRcode: dns.RcodeSuccess,
|
|
expectTsig: false,
|
|
},
|
|
{
|
|
zones: []string{"."},
|
|
reqTypes: qTypes{},
|
|
qType: dns.TypeA,
|
|
opcode: dns.OpcodeQuery,
|
|
qTsig: true,
|
|
expectRcode: dns.RcodeSuccess,
|
|
expectTsig: true,
|
|
},
|
|
{
|
|
zones: []string{"."},
|
|
allTypes: true,
|
|
qType: dns.TypeA,
|
|
opcode: dns.OpcodeQuery,
|
|
qTsig: true,
|
|
expectRcode: dns.RcodeNotAuth,
|
|
expectTsig: true,
|
|
statusError: true,
|
|
},
|
|
// Opcode-based tests
|
|
{
|
|
zones: []string{"."},
|
|
reqOpCodes: opCodes{dns.OpcodeUpdate: {}},
|
|
qType: dns.TypeSOA,
|
|
opcode: dns.OpcodeUpdate,
|
|
qTsig: true,
|
|
expectRcode: dns.RcodeSuccess,
|
|
expectTsig: true,
|
|
},
|
|
{
|
|
zones: []string{"."},
|
|
reqOpCodes: opCodes{dns.OpcodeUpdate: {}},
|
|
qType: dns.TypeSOA,
|
|
opcode: dns.OpcodeUpdate,
|
|
qTsig: false,
|
|
expectRcode: dns.RcodeRefused,
|
|
expectTsig: false,
|
|
},
|
|
{
|
|
zones: []string{"."},
|
|
reqOpCodes: opCodes{dns.OpcodeUpdate: {}},
|
|
qType: dns.TypeA,
|
|
opcode: dns.OpcodeQuery,
|
|
qTsig: false,
|
|
expectRcode: dns.RcodeSuccess,
|
|
expectTsig: false,
|
|
},
|
|
{
|
|
zones: []string{"."},
|
|
reqOpCodes: opCodes{dns.OpcodeNotify: {}},
|
|
qType: dns.TypeSOA,
|
|
opcode: dns.OpcodeNotify,
|
|
qTsig: true,
|
|
expectRcode: dns.RcodeSuccess,
|
|
expectTsig: true,
|
|
},
|
|
// Combined qtype and opcode requirement
|
|
{
|
|
zones: []string{"."},
|
|
reqTypes: qTypes{dns.TypeAXFR: {}},
|
|
reqOpCodes: opCodes{dns.OpcodeUpdate: {}},
|
|
qType: dns.TypeA,
|
|
opcode: dns.OpcodeUpdate,
|
|
qTsig: true,
|
|
expectRcode: dns.RcodeSuccess,
|
|
expectTsig: true,
|
|
},
|
|
// allOpcodes test
|
|
{
|
|
zones: []string{"."},
|
|
allOpcodes: true,
|
|
qType: dns.TypeA,
|
|
opcode: dns.OpcodeQuery,
|
|
qTsig: true,
|
|
expectRcode: dns.RcodeSuccess,
|
|
expectTsig: true,
|
|
},
|
|
{
|
|
zones: []string{"."},
|
|
allOpcodes: true,
|
|
qType: dns.TypeA,
|
|
opcode: dns.OpcodeQuery,
|
|
qTsig: false,
|
|
expectRcode: dns.RcodeRefused,
|
|
expectTsig: false,
|
|
},
|
|
}
|
|
|
|
for i, tc := range cases {
|
|
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
|
tsig := TSIGServer{
|
|
Zones: tc.zones,
|
|
allTypes: tc.allTypes,
|
|
allOpcodes: tc.allOpcodes,
|
|
types: tc.reqTypes,
|
|
opcodes: tc.reqOpCodes,
|
|
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)
|
|
r.Opcode = tc.opcode
|
|
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{"."},
|
|
allTypes: true,
|
|
Next: testHandler(),
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.desc, func(t *testing.T) {
|
|
ctx := context.TODO()
|
|
|
|
var 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 }
|