git » dnss » commit 029fc47

dnsserver: Improve how we construct a cached reply

author Alberto Bertogli
2026-10-23 18:42:17 UTC
committer Alberto Bertogli
2026-10-23 18:59:12 UTC
parent 786704dfee7a4ccf2bc2f5c64d93a80d70e471e3

dnsserver: Improve how we construct a cached reply

Today when we reply from the cache, we construct a reply that is missing
some values. For example we never set RA, and also don't handle OPT
records for EDNS0 queries.

To fix that, this patch uses SetReply instead of open-coding the
construction of the reply message, which properly copies RD and CD from
the request, and explicitly sets RA.

It also adds logic to correctly handle OPT records in EDNS0 queries.

internal/dnsserver/caching_test.go +55 -0
internal/dnsserver/resolver.go +10 -9

diff --git a/internal/dnsserver/caching_test.go b/internal/dnsserver/caching_test.go
index 9256461..d4a10a3 100644
--- a/internal/dnsserver/caching_test.go
+++ b/internal/dnsserver/caching_test.go
@@ -51,6 +51,61 @@ func TestBasic(t *testing.T) {
 	}
 }
 
+// Test the headers and OPT record of cache hit replies.
+func TestHitReply(t *testing.T) {
+	r := testutil.NewTestResolver()
+	c := NewCachingResolver(r)
+	c.Init()
+	resetStats()
+
+	r.Response = newReply(mustNewRR(t, "test. A 1.2.3.4"))
+
+	tr := trace.New("test", "TestHitReply")
+	defer tr.Finish()
+
+	// Plain query: no OPT in the reply.
+	req := newQuery("test.", dns.TypeA)
+	req.RecursionDesired = false
+	c.Query(req, tr)
+	resp, err := c.Query(req, tr)
+	if err != nil {
+		t.Fatalf("query failed: %v", err)
+	}
+	if !statsEquals(2, 1, 1) {
+		t.Errorf("bad stats: %v", dumpStats())
+	}
+	if resp.Id != req.Id || !resp.Response || !resp.RecursionAvailable ||
+		resp.RecursionDesired || resp.Authoritative ||
+		resp.Rcode != dns.RcodeSuccess {
+		t.Errorf("bad reply header: %v", resp.MsgHdr)
+	}
+	if opt := resp.IsEdns0(); opt != nil {
+		t.Errorf("unexpected OPT in reply: %v", opt)
+	}
+
+	// EDNS0 query with DO: the reply should have an OPT, with DO set.
+	req = newQuery("test.", dns.TypeA)
+	req.SetEdns0(1232, true)
+	c.Query(req, tr)
+	resp, err = c.Query(req, tr)
+	if err != nil {
+		t.Fatalf("query failed: %v", err)
+	}
+	if !statsEquals(4, 2, 2) {
+		t.Errorf("bad stats: %v", dumpStats())
+	}
+	if !resp.RecursionDesired {
+		t.Errorf("RD not copied from the request: %v", resp.MsgHdr)
+	}
+	opt := resp.IsEdns0()
+	if opt == nil {
+		t.Fatalf("no OPT in reply: %v", resp)
+	}
+	if opt.UDPSize() != 1232 || !opt.Do() {
+		t.Errorf("bad OPT in reply: %v", opt)
+	}
+}
+
 // Test TTL handling.
 func TestTTL(t *testing.T) {
 	r := testutil.NewTestResolver()
diff --git a/internal/dnsserver/resolver.go b/internal/dnsserver/resolver.go
index eed5726..936f3ec 100644
--- a/internal/dnsserver/resolver.go
+++ b/internal/dnsserver/resolver.go
@@ -361,15 +361,16 @@ func (c *cachingResolver) Query(r *dns.Msg, tr *trace.Trace) (*dns.Msg, error) {
 		tr.Printf("cache hit")
 		stats.cacheHits.Add(1)
 
-		reply := &dns.Msg{
-			MsgHdr: dns.MsgHdr{
-				Id:            r.Id,
-				Response:      true,
-				Authoritative: false,
-				Rcode:         dns.RcodeSuccess,
-			},
-			Question: r.Question,
-			Answer:   answer,
+		reply := new(dns.Msg)
+		reply.SetReply(r)
+		reply.RecursionAvailable = true
+		reply.Answer = answer
+
+		// If the client used EDNS0, it expects an OPT record in the reply.
+		// We mirror its UDP size, and the DO bit (which is in the key, so
+		// the answer matches it).
+		if opt := r.IsEdns0(); opt != nil {
+			reply.SetEdns0(opt.UDPSize(), opt.Do())
 		}
 
 		return reply, nil