Files
coredns/plugin/tsig/tsig_test.go
Seena Fallah 471d62926d plugin/tsig: add require_opcode directive for opcode-based TSIG (#7828)
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>
2026-03-27 21:05:49 +02:00

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 }