plugin/tsig: don't echo client's TSIG.Error if verification is successful (#8215)

This commit is contained in:
Ilya Kulakov
2026-07-09 15:04:23 -07:00
committed by GitHub
parent 0ebe4da2b0
commit 540e2f325f
2 changed files with 141 additions and 42 deletions

View File

@@ -32,30 +32,31 @@ func (t TSIGServer) Name() string { return pluginName }
// ServeDNS implements plugin.Handler // ServeDNS implements plugin.Handler
func (t *TSIGServer) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { func (t *TSIGServer) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
var err error var (
state := request.Request{Req: r, W: w} state = request.Request{Req: r, W: w}
if z := plugin.Zones(t.Zones).Matches(state.Name()); z == "" { tsigRR = r.IsTsig()
)
switch {
case tsigRR == nil && !t.tsigRequired(state.QType(), r.Opcode):
fallthrough
case plugin.Zones(t.Zones).Matches(state.Name()) == "":
return plugin.NextOrFailure(t.Name(), t.Next, ctx, w, r) return plugin.NextOrFailure(t.Name(), t.Next, ctx, w, r)
} case tsigRR == nil:
var tsigRR = r.IsTsig()
rcode := dns.RcodeSuccess
if !t.tsigRequired(state.QType(), r.Opcode) && 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()]) log.Debugf("rejecting '%s' request without TSIG\n", dns.TypeToString[state.QType()])
rcode = dns.RcodeRefused resp := new(dns.Msg).SetRcode(r, dns.RcodeRefused)
w.WriteMsg(resp)
return dns.RcodeSuccess, nil
} }
// wrap the response writer so the response will be TSIG signed. // 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
r.Extra = r.Extra[:len(r.Extra)-1]
// Wrap the response writer so the response will be TSIG signed.
w = &restoreTsigWriter{w, r, tsigRR} w = &restoreTsigWriter{w, r, tsigRR}
tsigStatus := w.TsigStatus() if tsigStatus := w.TsigStatus(); tsigStatus != nil {
if tsigStatus != nil {
log.Debugf("TSIG validation failed: %v %v", dns.TypeToString[state.QType()], tsigStatus) log.Debugf("TSIG validation failed: %v %v", dns.TypeToString[state.QType()], tsigStatus)
rcode = dns.RcodeNotAuth
switch tsigStatus { switch tsigStatus {
case dns.ErrSecret: case dns.ErrSecret:
tsigRR.Error = dns.RcodeBadKey tsigRR.Error = dns.RcodeBadKey
@@ -64,26 +65,18 @@ func (t *TSIGServer) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.
default: default:
tsigRR.Error = dns.RcodeBadSig tsigRR.Error = dns.RcodeBadSig
} }
resp := new(dns.Msg).SetRcode(r, rcode) resp := new(dns.Msg).SetRcode(r, dns.RcodeNotAuth)
w.WriteMsg(resp) w.WriteMsg(resp)
return dns.RcodeSuccess, nil return dns.RcodeSuccess, nil
} }
// strip the TSIG RR. Next, and subsequent plugins will not see the TSIG RRs. tsigRR.Error = dns.RcodeSuccess
// This violates forwarding cases (RFC 8945 5.5). See README.md Bugs rcode, err := plugin.NextOrFailure(t.Name(), t.Next, ctx, w, r)
if len(r.Extra) > 1 { if err != nil {
r.Extra = r.Extra[0 : len(r.Extra)-1] log.Errorf("request handler returned an error: %v\n", err)
} else {
r.Extra = []dns.RR{}
} }
// If the downstream plugin chain did not write, use custom ResponseWriter here
if rcode == dns.RcodeSuccess { // because [dnsserver.errorFunc] ignores TSIG.
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) { if !plugin.ClientWrite(rcode) {
resp := new(dns.Msg).SetRcode(r, rcode) resp := new(dns.Msg).SetRcode(r, rcode)
w.WriteMsg(resp) w.WriteMsg(resp)
@@ -105,22 +98,21 @@ func (t *TSIGServer) tsigRequired(qtype uint16, opcode int) bool {
return typeMatches || opcodeMatches return typeMatches || opcodeMatches
} }
// restoreTsigWriter Implement Response Writer, and adds a TSIG RR to a response // restoreTsigWriter implements [dns.ResponseWriter], and adds a [dns.TSIG] RR to a response.
type restoreTsigWriter struct { type restoreTsigWriter struct {
dns.ResponseWriter dns.ResponseWriter
req *dns.Msg // original request excluding TSIG if it has one req *dns.Msg // original request excluding TSIG
reqTSIG *dns.TSIG // original TSIG reqTSIG *dns.TSIG // original TSIG
} }
// WriteMsg adds a TSIG RR to the response // WriteMsg adds a TSIG RR to the response
func (r *restoreTsigWriter) WriteMsg(m *dns.Msg) error { func (r *restoreTsigWriter) WriteMsg(m *dns.Msg) error {
// Make sure the response has an EDNS OPT RR if the request had it. if repTSIG := m.IsTsig(); repTSIG == nil { // respect TSIG set downstream
// Otherwise ScrubWriter would append it *after* TSIG, making it a non-compliant DNS message. // Make sure the response has an EDNS OPT RR if the request had it.
state := request.Request{Req: r.req, W: r.ResponseWriter} // Otherwise [request.ScrubWriter] would append it *after* TSIG, making it a non-compliant DNS message.
state.SizeAndDo(m) 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 = new(dns.TSIG)
repTSIG.Hdr = dns.RR_Header{Name: r.reqTSIG.Hdr.Name, Rrtype: dns.TypeTSIG, Class: dns.ClassANY} repTSIG.Hdr = dns.RR_Header{Name: r.reqTSIG.Hdr.Name, Rrtype: dns.TypeTSIG, Class: dns.ClassANY}
repTSIG.Algorithm = r.reqTSIG.Algorithm repTSIG.Algorithm = r.reqTSIG.Algorithm
@@ -140,7 +132,6 @@ func (r *restoreTsigWriter) WriteMsg(m *dns.Msg) error {
} }
m.Extra = append(m.Extra, repTSIG) m.Extra = append(m.Extra, repTSIG)
} }
return r.ResponseWriter.WriteMsg(m) return r.ResponseWriter.WriteMsg(m)
} }

View File

@@ -3,6 +3,7 @@ package tsig
import ( import (
"context" "context"
"fmt" "fmt"
"slices"
"testing" "testing"
"time" "time"
@@ -20,6 +21,7 @@ func TestServeDNS(t *testing.T) {
reqOpCodes opCodes reqOpCodes opCodes
qType uint16 qType uint16
opcode int opcode int
extra []dns.RR
qTsig bool qTsig bool
allTypes bool allTypes bool
allOpcodes bool allOpcodes bool
@@ -220,6 +222,7 @@ func TestServeDNSTsigErrors(t *testing.T) {
cases := []struct { cases := []struct {
desc string desc string
tsigErr error tsigErr error
reqError int
expectRcode int expectRcode int
expectError int expectError int
expectOtherLength int expectOtherLength int
@@ -249,6 +252,15 @@ func TestServeDNSTsigErrors(t *testing.T) {
expectOtherLength: 6, expectOtherLength: 6,
expectTimeSigned: clientNow, expectTimeSigned: clientNow,
}, },
{
desc: "Client Set Error",
tsigErr: nil,
reqError: dns.RcodeBadKey,
expectRcode: dns.RcodeSuccess,
expectError: dns.RcodeSuccess,
expectOtherLength: 0,
expectTimeSigned: 0,
},
} }
tsig := TSIGServer{ tsig := TSIGServer{
@@ -267,8 +279,9 @@ func TestServeDNSTsigErrors(t *testing.T) {
r.SetQuestion("test.example.", dns.TypeA) r.SetQuestion("test.example.", dns.TypeA)
r.SetTsig("test.key.", dns.HmacSHA256, 300, clientNow) r.SetTsig("test.key.", dns.HmacSHA256, 300, clientNow)
// set a fake MAC and Size in request
rtsig := r.IsTsig() rtsig := r.IsTsig()
rtsig.Error = uint16(tc.reqError)
// set a fake MAC and Size in request
rtsig.MAC = "0123456789012345678901234567890101234567890123456789012345678901" rtsig.MAC = "0123456789012345678901234567890101234567890123456789012345678901"
rtsig.MACSize = 32 rtsig.MACSize = 32
@@ -306,6 +319,101 @@ func TestServeDNSTsigErrors(t *testing.T) {
} }
} }
func TestServeDNSTsigNext(t *testing.T) {
cases := []struct {
desc string
zones []string
tsigRequired bool
tsigStatus error
reqExtra []dns.RR
reqSigned bool
expectExtra []uint16
expectNext int
}{
{
desc: "Optional TSIG",
zones: []string{"."},
tsigRequired: false,
reqExtra: []dns.RR{test.OPT(42, true)},
reqSigned: false,
expectExtra: []uint16{dns.TypeOPT},
expectNext: 1,
},
{
desc: "Missing TSIG",
zones: []string{"."},
tsigRequired: true,
tsigStatus: nil,
reqExtra: []dns.RR{test.OPT(42, true)},
reqSigned: false,
expectNext: 0,
},
{
desc: "Bad Zone",
zones: []string{"another.domain."},
reqExtra: []dns.RR{test.OPT(42, true)},
reqSigned: true,
expectExtra: []uint16{dns.TypeOPT, dns.TypeTSIG},
expectNext: 1,
},
{
desc: "Bad Status",
zones: []string{"."},
tsigRequired: true,
tsigStatus: dns.ErrSig,
reqExtra: []dns.RR{test.OPT(42, true)},
reqSigned: true,
expectNext: 0,
},
{
desc: "Success",
zones: []string{"."},
tsigRequired: true,
reqExtra: []dns.RR{test.OPT(42, true)},
reqSigned: true,
expectExtra: []uint16{dns.TypeOPT},
expectNext: 1,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
h := testHandler()
var nextCalled int
tsig := TSIGServer{
Zones: tc.zones,
allTypes: tc.tsigRequired,
Next: test.HandlerFunc(func(_ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
nextCalled++
if !slices.EqualFunc(r.Extra, tc.expectExtra, func(rr dns.RR, t uint16) bool { return rr.Header().Rrtype == t }) {
t.Errorf("expected %v, got %v", tc.expectExtra, r.Extra)
}
return h(_ctx, w, r)
}),
}
ctx := context.TODO()
w := dnstest.NewRecorder(&ErrWriter{err: tc.tsigStatus})
r := new(dns.Msg)
r.SetQuestion("test.example.", dns.TypeA)
r.Extra = tc.reqExtra
if tc.reqSigned {
r.SetTsig("test.key.", dns.HmacSHA256, 300, time.Now().Unix())
}
_, err := tsig.ServeDNS(ctx, w, r)
if err != nil {
t.Fatal(err)
}
if nextCalled != tc.expectNext {
t.Errorf("expected next plugin called")
}
})
}
}
func testHandler() test.HandlerFunc { func testHandler() test.HandlerFunc {
return func(_ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { return func(_ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
state := request.Request{W: w, Req: r} state := request.Request{W: w, Req: r}