diff --git a/plugin/cache/ad_bit_test.go b/plugin/cache/ad_bit_test.go new file mode 100644 index 000000000..55286951b --- /dev/null +++ b/plugin/cache/ad_bit_test.go @@ -0,0 +1,79 @@ +package cache + +import ( + "context" + "testing" + + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +// TestCacheADBitNotPartitioned guards the fix for issue #6642. +// +// The cache key is hashed on qname, qtype, the DO bit and the CD bit, but +// deliberately NOT on the AD bit (AD must not partition the cache). This means +// a single cache entry serves both AD-requesting and non-AD-requesting clients, +// so the AD bit returned to a client must be derived per request from the +// authentication status of the cached answer, never frozen from whichever query +// happened to populate the entry first. +// +// Historically (reported on v1.11.1) the order mattered: a first query without +// the AD bit cached the answer with AD cleared, and a later query WITH the AD +// bit could no longer get AD=1 back. The reverse order worked. This test pins +// both directions so the asymmetry cannot regress. +func TestCacheADBitNotPartitioned(t *testing.T) { + // Forward order: +noad first (populates cache), then +ad on the same entry. + // The upstream answer is authenticated, so the +ad query must receive AD=1. + t.Run("noad_then_ad", func(t *testing.T) { + c := New() + c.Next = dnssecHandler() // sets AuthenticatedData=true on the reply + + // First query: AD not requested, DO not set -> miss, populates cache. + noad := new(dns.Msg) + noad.SetQuestion("invent.example.org.", dns.TypeA) + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + c.ServeDNS(context.TODO(), rec, noad) + if rec.Msg.AuthenticatedData { + t.Errorf("first query did not request AD, expected AuthenticatedData=false, got true") + } + + // Second query: AD requested, DO not set -> hit on the same key. + // Must reflect the authenticated answer with AD=1. + ad := new(dns.Msg) + ad.SetQuestion("invent.example.org.", dns.TypeA) + ad.AuthenticatedData = true + rec = dnstest.NewRecorder(&test.ResponseWriter{}) + c.ServeDNS(context.TODO(), rec, ad) + if !rec.Msg.AuthenticatedData { + t.Errorf("second query requested AD on an authenticated cached answer, expected AuthenticatedData=true, got false") + } + }) + + // Reverse order: +ad first, then +noad. This direction already worked; we + // pin it so a fix for the forward case never breaks it. + t.Run("ad_then_noad", func(t *testing.T) { + c := New() + c.Next = dnssecHandler() + + // First query: AD requested -> AD=1 expected. + ad := new(dns.Msg) + ad.SetQuestion("invent.example.org.", dns.TypeA) + ad.AuthenticatedData = true + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + c.ServeDNS(context.TODO(), rec, ad) + if !rec.Msg.AuthenticatedData { + t.Errorf("first query requested AD on an authenticated answer, expected AuthenticatedData=true, got false") + } + + // Second query: AD not requested -> AD must be cleared for this client. + noad := new(dns.Msg) + noad.SetQuestion("invent.example.org.", dns.TypeA) + rec = dnstest.NewRecorder(&test.ResponseWriter{}) + c.ServeDNS(context.TODO(), rec, noad) + if rec.Msg.AuthenticatedData { + t.Errorf("second query did not request AD, expected AuthenticatedData=false, got true") + } + }) +}