git » dnss » commit 78b7406

dnsserver: Limit the size of answers we cache

author Alberto Bertogli
2026-10-23 18:33:46 UTC
committer Alberto Bertogli
2026-10-23 18:51:09 UTC
parent a8b9d894d5e1df258466ded86e78d87cafe3ec47

dnsserver: Limit the size of answers we cache

Today we cache up to 2k answers, but each answer can potentially get
quite large. This is very unlikely under normal conditions, but it could
end up causing the memory usage to blow up quite significantly.

That would also cause cache maintenance to hold the lock for longer,
stalling replies.

To prevent that, this patch makes dnss only cache answers <= 4 KiB
(uncompressed).

internal/dnsserver/caching_test.go +7 -0
internal/dnsserver/resolver.go +18 -0

diff --git a/internal/dnsserver/caching_test.go b/internal/dnsserver/caching_test.go
index 522efe6..00e2b69 100644
--- a/internal/dnsserver/caching_test.go
+++ b/internal/dnsserver/caching_test.go
@@ -276,6 +276,13 @@ func TestWantToCache(t *testing.T) {
 	r.Question = []dns.Question{
 		{Name: "other.", Qtype: dns.TypeMX, Qclass: dns.ClassINET}}
 	checkWantToCache(t, q, r, "reply question does not match")
+
+	r = reply.Copy()
+	r.Answer = nil
+	for i := 0; i < 300; i++ {
+		r.Answer = append(r.Answer, mustNewRR(t, "test. A 1.2.3.4"))
+	}
+	checkWantToCache(t, q, r, "answer too large")
 }
 
 // Test that we handle the cache filling up.
diff --git a/internal/dnsserver/resolver.go b/internal/dnsserver/resolver.go
index bc60008..708b009 100644
--- a/internal/dnsserver/resolver.go
+++ b/internal/dnsserver/resolver.go
@@ -98,6 +98,12 @@ var (
 	// with Maintain().
 	maxCacheSize = 2000
 
+	// Maximum size of an answer we will cache, in bytes (uncompressed).
+	// Larger answers are rare, and in memory they can be much bigger than
+	// on the wire (each RR gets its own copy of the name), so caching them
+	// could use a lot of memory.
+	maxAnswerSize = 4 * 1024
+
 	// Minimum TTL for entries we consider for the cache.
 	minTTL = 2 * time.Minute
 
@@ -248,11 +254,23 @@ func wantToCache(question dns.Question, reply *dns.Msg) error {
 		return fmt.Errorf(
 			"reply question does not match: asked %v, got %v",
 			question, reply.Question[0])
+	} else if size := answerSize(reply.Answer); size > maxAnswerSize {
+		return fmt.Errorf("answer too large (%d bytes)", size)
 	}
 
 	return nil
 }
 
+// answerSize returns the uncompressed size of the answer, which is a
+// reasonable approximation of how much memory it takes.
+func answerSize(answer []dns.RR) int {
+	size := 0
+	for _, rr := range answer {
+		size += dns.Len(rr)
+	}
+	return size
+}
+
 func limitTTL(answer []dns.RR) time.Duration {
 	// Use the minimum TTL of all the RRs, as the answer is only valid as
 	// long as all of them are. For example, a long-lived CNAME pointing to a