From 6662dd58eab8e32c58174051215555b496b55836 Mon Sep 17 00:00:00 2001 From: houyuwushang Date: Mon, 6 Jul 2026 13:48:51 +0800 Subject: [PATCH] plugin/pkg/response: classify nxdomain without soa as denial (#8199) Signed-off-by: houyuwushang --- plugin/cache/cache.go | 13 +++++++ plugin/cache/cache_test.go | 10 ++++++ plugin/log/log_test.go | 54 ++++++++++++++++++++++++++++++ plugin/pkg/response/typify.go | 7 ++-- plugin/pkg/response/typify_test.go | 11 ++++++ 5 files changed, 93 insertions(+), 2 deletions(-) diff --git a/plugin/cache/cache.go b/plugin/cache/cache.go index fba713576..cc96162a4 100644 --- a/plugin/cache/cache.go +++ b/plugin/cache/cache.go @@ -90,10 +90,23 @@ func key(qname string, m *dns.Msg, t response.Type, do, cd bool) (bool, uint64) if t == response.OtherError || t == response.Meta || t == response.Update { return false, 0 } + // Negative caching requires an SOA record to determine the denial TTL. + if t == response.NameError && !hasSOA(m) { + return false, 0 + } return true, hash(qname, m.Question[0].Qtype, do, cd) } +func hasSOA(m *dns.Msg) bool { + for _, r := range m.Ns { + if r.Header().Rrtype == dns.TypeSOA { + return true + } + } + return false +} + var one = []byte("1") var zero = []byte("0") diff --git a/plugin/cache/cache_test.go b/plugin/cache/cache_test.go index 0a11f9737..da4c7d7b1 100644 --- a/plugin/cache/cache_test.go +++ b/plugin/cache/cache_test.go @@ -122,6 +122,16 @@ func TestCacheInsertion(t *testing.T) { }, shouldCache: true, }, + { + name: "test dns.RcodeNameError without SOA does not cache", + in: test.Case{ + Rcode: dns.RcodeNameError, + Qname: "1.1.168.192.in-addr.arpa.", + Qtype: dns.TypePTR, + RecursionAvailable: true, + }, + shouldCache: false, + }, { name: "test dns.RcodeServerFailure cache", out: test.Case{ diff --git a/plugin/log/log_test.go b/plugin/log/log_test.go index 015dd599d..f4e9a4e2b 100644 --- a/plugin/log/log_test.go +++ b/plugin/log/log_test.go @@ -8,6 +8,7 @@ import ( "strings" "testing" + "github.com/coredns/coredns/plugin" "github.com/coredns/coredns/plugin/pkg/dnstest" clog "github.com/coredns/coredns/plugin/pkg/log" "github.com/coredns/coredns/plugin/pkg/replacer" @@ -186,6 +187,59 @@ func TestLoggedSynthesizesDeferredRefused(t *testing.T) { } } +func TestLoggedClassNameErrorWithoutSOA(t *testing.T) { + tests := []struct { + name string + class response.Class + shouldLog bool + }{ + {name: "error", class: response.Error, shouldLog: false}, + {name: "denial", class: response.Denial, shouldLog: true}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + rule := Rule{ + NameScope: ".", + Format: DefaultLogFormat, + Class: map[response.Class]struct{}{tc.class: {}}, + } + + var f bytes.Buffer + log.SetOutput(&f) + + logger := Logger{ + Rules: []Rule{rule}, + Next: plugin.HandlerFunc(func(_ context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + m := new(dns.Msg) + m.SetRcode(r, dns.RcodeNameError) + return dns.RcodeNameError, w.WriteMsg(m) + }), + repl: replacer.New(), + } + + ctx := context.TODO() + r := new(dns.Msg) + r.SetQuestion("1.1.168.192.in-addr.arpa.", dns.TypePTR) + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + + _, err := logger.ServeDNS(ctx, rec, r) + if err != nil { + t.Fatal(err) + } + + logged := f.String() + if !tc.shouldLog && len(logged) != 0 { + t.Errorf("Expected it not to be logged, but got string: %s", logged) + } + if tc.shouldLog && !strings.Contains(logged, "NXDOMAIN") { + t.Errorf("Expected it to be logged as NXDOMAIN. Logged string: %s", logged) + } + }) + } +} + func TestLogged(t *testing.T) { tests := []struct { Rules []Rule diff --git a/plugin/pkg/response/typify.go b/plugin/pkg/response/typify.go index df314d41a..80a12a17a 100644 --- a/plugin/pkg/response/typify.go +++ b/plugin/pkg/response/typify.go @@ -13,7 +13,7 @@ type Type int const ( // NoError indicates a positive reply NoError Type = iota - // NameError is a NXDOMAIN in header, SOA in auth. + // NameError is an NXDOMAIN in the header. NameError // ServerError is a set of errors we want to cache, for now it contains SERVFAIL and NOTIMPL. ServerError @@ -89,6 +89,9 @@ func Typify(m *dns.Msg, t time.Time) (Type, *dns.OPT) { if len(m.Answer) > 0 && m.Rcode == dns.RcodeSuccess { return NoError, opt } + if len(m.Answer) > 0 && m.Rcode == dns.RcodeNameError { + return OtherError, opt + } soa := false ns := 0 @@ -105,7 +108,7 @@ func Typify(m *dns.Msg, t time.Time) (Type, *dns.OPT) { if soa && m.Rcode == dns.RcodeSuccess { return NoData, opt } - if soa && m.Rcode == dns.RcodeNameError { + if m.Rcode == dns.RcodeNameError { return NameError, opt } diff --git a/plugin/pkg/response/typify_test.go b/plugin/pkg/response/typify_test.go index 3d9abdf7c..62197ae3b 100644 --- a/plugin/pkg/response/typify_test.go +++ b/plugin/pkg/response/typify_test.go @@ -70,6 +70,17 @@ func TestTypifyRefused(t *testing.T) { } } +func TestTypifyNameErrorWithoutSOA(t *testing.T) { + m := new(dns.Msg) + m.SetQuestion("1.1.168.192.in-addr.arpa.", dns.TypePTR) + m.Rcode = dns.RcodeNameError + + mt, _ := Typify(m, time.Now().UTC()) + if mt != NameError { + t.Errorf("NXDOMAIN message not typified as NameError, got %s", mt) + } +} + func delegationMsg() *dns.Msg { return &dns.Msg{ Ns: []dns.RR{