git » dnss » commit 90bda13

dnsserver: Don't cache queries with EDNS client subnet

author Alberto Bertogli
2026-10-23 19:26:19 UTC
committer Alberto Bertogli
2026-10-23 19:28:59 UTC
parent 1416dd920cad0e8d286a9df4d7d744cec1d188cd

dnsserver: Don't cache queries with EDNS client subnet

The EDNS client subnet option lets servers adjust the response to a
specific subnet. Today, we don't use that client subnet in the cache
key, so a client could get a response for a different (or no) subnet.

This patch makes those queries avoid the cache. Most client resolvers
don't use EDNS client subnet options, so introducing that as a cache key
is not worth the complexity.

internal/dnsserver/caching_test.go +11 -1
internal/dnsserver/resolver.go +18 -0

diff --git a/internal/dnsserver/caching_test.go b/internal/dnsserver/caching_test.go
index d4a10a3..168163a 100644
--- a/internal/dnsserver/caching_test.go
+++ b/internal/dnsserver/caching_test.go
@@ -4,6 +4,7 @@ package dnsserver
 
 import (
 	"fmt"
+	"net"
 	"reflect"
 	"strconv"
 	"strings"
@@ -188,10 +189,19 @@ func TestCacheBypass(t *testing.T) {
 	ednsV1.SetEdns0(4096, false)
 	ednsV1.IsEdns0().SetVersion(1)
 
+	ecs := newQuery("test.", dns.TypeA)
+	ecs.SetEdns0(4096, false)
+	ecs.IsEdns0().Option = append(ecs.IsEdns0().Option, &dns.EDNS0_SUBNET{
+		Code:          dns.EDNS0SUBNET,
+		Family:        1,
+		SourceNetmask: 24,
+		Address:       net.ParseIP("192.0.2.0").To4(),
+	})
+
 	tr := trace.New("test", "TestCacheBypass")
 	defer tr.Finish()
 
-	for _, req := range []*dns.Msg{multi, notify, ednsV1} {
+	for _, req := range []*dns.Msg{multi, notify, ednsV1, ecs} {
 		resetStats()
 		r.LastQuery = nil
 		if _, err := c.Query(req, tr); err != nil {
diff --git a/internal/dnsserver/resolver.go b/internal/dnsserver/resolver.go
index 936f3ec..2087c5c 100644
--- a/internal/dnsserver/resolver.go
+++ b/internal/dnsserver/resolver.go
@@ -256,11 +256,29 @@ func canUseCache(r *dns.Msg) error {
 		// We only support EDNS version 0 in our replies. Let the upstream
 		// handle other versions (it should reply with BADVERS).
 		return fmt.Errorf("EDNS version %d != 0", opt.Version())
+	} else if hasClientSubnet(opt) {
+		// The answer may be specific to the client's chosen subnet, so it
+		// should not be served to others.
+		return fmt.Errorf("has EDNS client subnet")
 	}
 
 	return nil
 }
 
+// hasClientSubnet returns true if the OPT record (which may be nil) has an
+// EDNS Client Subnet option (RFC 7871).
+func hasClientSubnet(opt *dns.OPT) bool {
+	if opt == nil {
+		return false
+	}
+	for _, o := range opt.Option {
+		if o.Option() == dns.EDNS0SUBNET {
+			return true
+		}
+	}
+	return false
+}
+
 func wantToCache(question dns.Question, reply *dns.Msg) error {
 	if reply.Rcode != dns.RcodeSuccess {
 		return fmt.Errorf("unsuccessful query")