Skip to content

Commit f214e79

Browse files
committed
Pull request #434: 8384-fix-do-cache
Merge in GO/dnsproxy from 8384-fix-do-cache to master Updates AdguardTeam/AdGuardHome#8384. Squashed commit of the following: commit e36ac40 Merge: 29d546b bd64c3a Author: Eugene Burkov <E.Burkov@AdGuard.COM> Date: Thu May 21 18:40:11 2026 +0300 Merge branch 'master' into 8384-fix-do-cache commit 29d546b Author: Eugene Burkov <E.Burkov@AdGuard.COM> Date: Thu May 21 17:09:12 2026 +0300 proxy: add do bit to cache key commit e6245e9 Author: Eugene Burkov <E.Burkov@AdGuard.COM> Date: Wed May 20 22:17:17 2026 +0300 proxy: imp doc commit 58b5643 Author: Eugene Burkov <E.Burkov@AdGuard.COM> Date: Wed May 20 21:04:01 2026 +0300 proxy: fix caching without do
1 parent bd64c3a commit f214e79

7 files changed

Lines changed: 93 additions & 94 deletions

File tree

proxy/cache.go

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -326,14 +326,15 @@ func createCache(cacheSize int) (glc glcache.Cache) {
326326
return glcache.New(conf)
327327
}
328328

329-
// set stores response and upstream in the cache. l must not be nil.
330-
func (c *cache) set(m *dns.Msg, u upstream.Upstream, l *slog.Logger) {
329+
// set stores response and upstream for req in the cache. u and l must not be
330+
// nil.
331+
func (c *cache) set(req, m *dns.Msg, u upstream.Upstream, l *slog.Logger) {
331332
item := c.respToItem(m, u, l)
332333
if item == nil {
333334
return
334335
}
335336

336-
key := msgToKey(m)
337+
key := msgToKey(req)
337338
packed := item.pack()
338339

339340
c.itemsLock.Lock()
@@ -343,16 +344,16 @@ func (c *cache) set(m *dns.Msg, u upstream.Upstream, l *slog.Logger) {
343344
}
344345

345346
// setWithSubnet stores response and upstream with subnet in the cache. The
346-
// given subnet mask and IP address are used to calculate the cache key. l must
347-
// not be nil.
348-
func (c *cache) setWithSubnet(m *dns.Msg, u upstream.Upstream, subnet *net.IPNet, l *slog.Logger) {
347+
// given subnet mask and IP address are used to calculate the cache key. u, n,
348+
// and l must not be nil.
349+
func (c *cache) setWithSubnet(req, m *dns.Msg, u upstream.Upstream, n *net.IPNet, l *slog.Logger) {
349350
item := c.respToItem(m, u, l)
350351
if item == nil {
351352
return
352353
}
353354

354-
pref, _ := subnet.Mask.Size()
355-
key := msgToKeyWithSubnet(m, subnet.IP.Mask(subnet.Mask), pref)
355+
pref, _ := n.Mask.Size()
356+
key := msgToKeyWithSubnet(req, n.IP.Mask(n.Mask), pref)
356357
packed := item.pack()
357358

358359
c.itemsWithSubnetLock.Lock()
@@ -536,12 +537,16 @@ func respectTTLOverrides(ttl, cacheMinTTL, cacheMaxTTL uint32) uint32 {
536537
func msgToKey(m *dns.Msg) (b []byte) {
537538
q := m.Question[0]
538539
name := q.Name
539-
b = make([]byte, packedMsgLenSz+packedMsgLenSz+len(name))
540+
b = make([]byte, 1+packedMsgLenSz+packedMsgLenSz+len(name))
541+
542+
// Put the DO flag.
543+
opt := m.IsEdns0()
544+
b[0] = mathutil.BoolToNumber[byte](opt != nil && opt.Do())
540545

541546
// Put QTYPE, QCLASS, and QNAME.
542-
binary.BigEndian.PutUint16(b, q.Qtype)
543-
binary.BigEndian.PutUint16(b[packedMsgLenSz:], q.Qclass)
544-
copy(b[2*packedMsgLenSz:], strings.ToLower(name))
547+
binary.BigEndian.PutUint16(b[1:], q.Qtype)
548+
binary.BigEndian.PutUint16(b[1+packedMsgLenSz:], q.Qclass)
549+
copy(b[1+2*packedMsgLenSz:], strings.ToLower(name))
545550

546551
return b
547552
}
@@ -573,9 +578,7 @@ func msgToKeyWithSubnet(m *dns.Msg, ecsIP net.IP, mask int) (key []byte) {
573578
key[0] = mathutil.BoolToNumber[byte](opt != nil && opt.Do())
574579

575580
// Put Qtype.
576-
//
577-
// TODO(d.kolyshev): We should put Qtype in key[1:].
578-
binary.BigEndian.PutUint16(key[:], q.Qtype)
581+
binary.BigEndian.PutUint16(key[1:], q.Qtype)
579582

580583
// Put Qclass.
581584
binary.BigEndian.PutUint16(key[1+packedMsgLenSz:], q.Qclass)

proxy/cache_internal_test.go

Lines changed: 54 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -57,23 +57,24 @@ func TestServeCached(t *testing.T) {
5757
TCPListenAddr: []*net.TCPAddr{net.TCPAddrFromAddrPort(localhostAnyPort)},
5858
UpstreamConfig: newTestUpstreamConfig(t, defaultTimeout, testDefaultUpstreamAddr),
5959
TrustedProxies: defaultTrustedProxies,
60-
DNSSECEnabled: true,
60+
DNSSECEnabled: false,
6161
CacheEnabled: true,
6262
})
6363

6464
// Start listening.
6565
servicetest.RequireRun(t, dnsProxy, testTimeout)
6666

67+
// Create a DNS request.
68+
request := (&dns.Msg{}).SetQuestion("google.com.", dns.TypeA)
69+
request.SetEdns0(defaultUDPBufSize, false)
70+
6771
// Fill the cache.
6872
reply := (&dns.Msg{
69-
MsgHdr: dns.MsgHdr{
70-
Response: true,
71-
},
7273
Answer: []dns.RR{newRR(t, "google.com.", dns.TypeA, 3600, net.IP{8, 8, 8, 8})},
73-
}).SetQuestion("google.com.", dns.TypeA)
74+
}).SetReply(request)
7475
reply.SetEdns0(defaultUDPBufSize, false)
7576

76-
dnsProxy.cache.set(reply, upstreamWithAddr, testLogger)
77+
dnsProxy.cache.set(request, reply, upstreamWithAddr, testLogger)
7778

7879
// Create a DNS-over-UDP client connection.
7980
addr := dnsProxy.Addr(ProtoUDP)
@@ -82,10 +83,6 @@ func TestServeCached(t *testing.T) {
8283
Timeout: testTimeout,
8384
}
8485

85-
// Create a DNS request.
86-
request := (&dns.Msg{}).SetQuestion("google.com.", dns.TypeA)
87-
request.SetEdns0(defaultUDPBufSize, false)
88-
8986
r, _, err := client.Exchange(request, addr.String())
9087
require.NoErrorf(t, err, "error in the first request: %s", err)
9188

@@ -187,25 +184,24 @@ func TestCache_expired(t *testing.T) {
187184
func TestCacheDO(t *testing.T) {
188185
testCache := newTestCache(t, nil)
189186

187+
// Make a request.
188+
request := (&dns.Msg{}).SetQuestion("google.com.", dns.TypeA)
189+
190190
// Fill the cache.
191191
reply := (&dns.Msg{
192192
MsgHdr: dns.MsgHdr{
193193
Response: true,
194194
},
195195
Answer: []dns.RR{newRR(t, "google.com.", dns.TypeA, 3600, net.IP{8, 8, 8, 8})},
196196
}).SetQuestion("google.com.", dns.TypeA)
197-
reply.SetEdns0(4096, true)
197+
reply.SetEdns0(4096, false)
198198

199199
// Store in cache.
200-
testCache.set(reply, upstreamWithAddr, testLogger)
201-
202-
// Make a request.
203-
request := (&dns.Msg{}).SetQuestion("google.com.", dns.TypeA)
200+
testCache.set(request, reply, upstreamWithAddr, testLogger)
204201

205202
t.Run("without_do", func(t *testing.T) {
206-
ci, expired, key := testCache.get(request)
203+
ci, expired, _ := testCache.get(request)
207204
assert.False(t, expired)
208-
assert.Equal(t, msgToKey(request), key)
209205
assert.NotNil(t, ci)
210206
})
211207

@@ -217,30 +213,26 @@ func TestCacheDO(t *testing.T) {
217213

218214
request.SetEdns0(4096, true)
219215

220-
ci, expired, key := testCache.get(request)
216+
ci, expired, _ := testCache.get(request)
217+
require.Nil(t, ci)
221218
assert.False(t, expired)
222-
assert.Equal(t, msgToKey(request), key)
223-
224-
require.NotNil(t, ci)
225-
226-
assert.Equal(t, testUpsAddr, ci.u)
227219
})
228220
}
229221

230222
func TestCacheCNAME(t *testing.T) {
231223
testCache := newTestCache(t, nil)
232224

225+
// Create a DNS request.
226+
request := (&dns.Msg{}).SetQuestion("google.com.", dns.TypeA)
227+
233228
// Fill the cache
234229
reply := (&dns.Msg{
235230
MsgHdr: dns.MsgHdr{
236231
Response: true,
237232
},
238233
Answer: []dns.RR{newRR(t, "google.com.", dns.TypeCNAME, 3600, "test.google.com.")},
239234
}).SetQuestion("google.com.", dns.TypeA)
240-
testCache.set(reply, upstreamWithAddr, testLogger)
241-
242-
// Create a DNS request.
243-
request := (&dns.Msg{}).SetQuestion("google.com.", dns.TypeA)
235+
testCache.set(request, reply, upstreamWithAddr, testLogger)
244236

245237
t.Run("no_cnames", func(t *testing.T) {
246238
r, expired, _ := testCache.get(request)
@@ -250,7 +242,7 @@ func TestCacheCNAME(t *testing.T) {
250242

251243
// Now fill the cache with a cacheable CNAME response.
252244
reply.Answer = append(reply.Answer, newRR(t, "google.com.", dns.TypeA, 3600, net.IP{8, 8, 8, 8}))
253-
testCache.set(reply, upstreamWithAddr, testLogger)
245+
testCache.set(request, reply, upstreamWithAddr, testLogger)
254246

255247
// We are testing that a proper CNAME response gets cached
256248
t.Run("cnames_exist", func(t *testing.T) {
@@ -273,7 +265,7 @@ func TestCache_uncacheable(t *testing.T) {
273265
reply := (&dns.Msg{}).SetRcode(request, dns.RcodeBadAlg)
274266

275267
// We are testing that SERVFAIL responses aren't cached
276-
testCache.set(reply, upstreamWithAddr, testLogger)
268+
testCache.set(request, reply, upstreamWithAddr, testLogger)
277269

278270
r, expired, _ := testCache.get(request)
279271
assert.Nil(t, r)
@@ -330,30 +322,35 @@ func TestCacheExpiration(t *testing.T) {
330322
newRR(t, "google.com.", dns.TypeA, 1, net.IP{8, 8, 8, 8}),
331323
newRR(t, "yandex.com.", dns.TypeA, 1, net.IP{213, 180, 204, 62}),
332324
}
333-
replies := make([]*dns.Msg, len(rrs))
334-
for i, rr := range rrs {
335-
rep := (&dns.Msg{
325+
requests := make([]*dns.Msg, 0, len(rrs))
326+
responses := make([]*dns.Msg, 0, len(rrs))
327+
for _, rr := range rrs {
328+
req := (&dns.Msg{
336329
MsgHdr: dns.MsgHdr{
337-
Response: true,
330+
Id: dns.Id(),
338331
},
339-
Answer: []dns.RR{dns.Copy(rr)},
340332
}).SetQuestion(rr.Header().Name, dns.TypeA)
341-
dnsProxy.cache.set(rep, upstreamWithAddr, testLogger)
342-
replies[i] = rep
333+
requests = append(requests, req)
334+
335+
resp := (&dns.Msg{}).SetReply(req)
336+
resp.Answer = append(resp.Answer, rr)
337+
responses = append(responses, resp)
338+
339+
dnsProxy.cache.set(req, resp, upstreamWithAddr, testLogger)
343340
}
344341

345-
for _, r := range replies {
342+
for i, r := range requests {
346343
ci, expired, key := dnsProxy.cache.get(r)
347344
require.NotNil(t, ci)
348345

349346
assert.False(t, expired)
350347
assert.Equal(t, msgToKey(ci.m), key)
351348

352-
requireEqualMsgs(t, ci.m, r)
349+
requireEqualMsgs(t, ci.m, responses[i])
353350
}
354351

355352
assert.Eventually(t, func() bool {
356-
for _, r := range replies {
353+
for _, r := range requests {
357354
if ci, _, _ := dnsProxy.cache.get(r); ci != nil {
358355
return false
359356
}
@@ -550,13 +547,15 @@ func (tests testCases) run(t *testing.T) {
550547
testCache := newTestCache(t, nil)
551548

552549
for _, res := range tests.cache {
550+
request := (&dns.Msg{}).SetQuestion(res.q, res.t)
551+
553552
reply := (&dns.Msg{
554553
MsgHdr: dns.MsgHdr{
555554
Response: true,
556555
},
557556
Answer: res.a,
558-
}).SetQuestion(res.q, res.t)
559-
testCache.set(reply, upstreamWithAddr, testLogger)
557+
}).SetReply(request)
558+
testCache.set(request, reply, upstreamWithAddr, testLogger)
560559
}
561560

562561
for _, tc := range tests.cases {
@@ -579,7 +578,7 @@ func (tests testCases) run(t *testing.T) {
579578
Answer: tc.a,
580579
}).SetQuestion(tc.q, tc.t)
581580

582-
testCache.set(reply, upstreamWithAddr, testLogger)
581+
testCache.set(request, reply, upstreamWithAddr, testLogger)
583582

584583
requireEqualMsgs(t, ci.m, reply)
585584
}
@@ -619,27 +618,33 @@ func setAndGetCache(t *testing.T, c *cache, g *sync.WaitGroup, host, ip string)
619618

620619
ipAddr := net.ParseIP(ip)
621620

621+
req := (&dns.Msg{
622+
MsgHdr: dns.MsgHdr{
623+
Id: dns.Id(),
624+
},
625+
}).SetQuestion(host, dns.TypeA)
626+
622627
dnsMsg := (&dns.Msg{
623628
MsgHdr: dns.MsgHdr{
624629
Response: true,
625630
},
626631
Answer: []dns.RR{newRR(t, host, dns.TypeA, 1, ipAddr)},
627632
}).SetQuestion(host, dns.TypeA)
628633

629-
c.set(dnsMsg, upstreamWithAddr, testLogger)
634+
c.set(req, dnsMsg, upstreamWithAddr, testLogger)
630635

631636
for range 2 {
632637
ci, expired, key := c.get(dnsMsg)
633638
require.NotNilf(t, ci, "no cache found for %s", host)
634639

635640
assert.False(t, expired)
636-
assert.Equal(t, msgToKey(dnsMsg), key)
641+
assert.Equal(t, msgToKey(req), key)
637642

638643
requireEqualMsgs(t, ci.m, dnsMsg)
639644
}
640645

641646
assert.Eventuallyf(t, func() bool {
642-
ci, _, _ := c.get(dnsMsg)
647+
ci, _, _ := c.get(req)
643648

644649
return ci == nil
645650
}, cacheTimeout, cacheTick, "cache for %s should already be removed", host)
@@ -665,7 +670,7 @@ func TestCache_getWithSubnet(t *testing.T) {
665670
resp := (&dns.Msg{
666671
Answer: []dns.RR{newRR(t, testFQDN, dns.TypeA, 1, net.IP{1, 1, 1, 1})},
667672
}).SetReply(req)
668-
c.setWithSubnet(resp, upstreamWithAddr, &net.IPNet{IP: ip1234, Mask: mask16}, testLogger)
673+
c.setWithSubnet(req, resp, upstreamWithAddr, &net.IPNet{IP: ip1234, Mask: mask16}, testLogger)
669674

670675
t.Run("different_ip", func(t *testing.T) {
671676
ci, expired, key := c.getWithSubnet(req, &net.IPNet{IP: ip2234, Mask: mask24})
@@ -678,13 +683,13 @@ func TestCache_getWithSubnet(t *testing.T) {
678683
resp = (&dns.Msg{
679684
Answer: []dns.RR{newRR(t, testFQDN, dns.TypeA, 1, net.IP{2, 2, 2, 2})},
680685
}).SetReply(req)
681-
c.setWithSubnet(resp, upstreamWithAddr, &net.IPNet{IP: ip2234, Mask: mask16}, testLogger)
686+
c.setWithSubnet(req, resp, upstreamWithAddr, &net.IPNet{IP: ip2234, Mask: mask16}, testLogger)
682687

683688
// Add a response entry without subnet.
684689
resp = (&dns.Msg{
685690
Answer: []dns.RR{newRR(t, testFQDN, dns.TypeA, 1, net.IP{3, 3, 3, 3})},
686691
}).SetReply(req)
687-
c.setWithSubnet(resp, upstreamWithAddr, &net.IPNet{IP: nil, Mask: nil}, testLogger)
692+
c.setWithSubnet(req, resp, upstreamWithAddr, &net.IPNet{IP: nil, Mask: nil}, testLogger)
688693

689694
t.Run("with_subnet_1", func(t *testing.T) {
690695
ci, expired, key := c.getWithSubnet(req, &net.IPNet{IP: ip1234, Mask: mask24})
@@ -751,6 +756,7 @@ func TestCache_getWithSubnet_mask(t *testing.T) {
751756

752757
// Cache IP network that contains the testIP.
753758
c.setWithSubnet(
759+
req,
754760
resp,
755761
upstreamWithAddr,
756762
&net.IPNet{IP: cachedIP, Mask: cidrMask},

proxy/dnscontext.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,9 @@ func (dctx *DNSContext) scrub() {
165165
}
166166

167167
// We should guarantee that all the values we need are calculated.
168+
//
169+
// TODO(e.burkov): It's theoretically guaranteed already, investigate the
170+
// need to this call.
168171
dctx.calcFlagsAndSize()
169172

170173
// RFC-6891 (https://tools.ietf.org/html/rfc6891) states that response

proxy/proxy.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,15 @@ func (p *Proxy) Resolve(ctx context.Context, dctx *DNSContext) (err error) {
701701

702702
cacheWorks := p.cacheWorks(dctx)
703703
if cacheWorks {
704+
// Request for DNSSEC from the upstream to cache the
705+
// DNSSEC resource records as well. In case of disabled DNSSEC,
706+
// requesting and therefore caching of DNSSEC resource records depends
707+
// on the DO bit of the initiating query.
708+
//
709+
// See https://datatracker.ietf.org/doc/html/rfc4035#section-4.5 and
710+
// https://datatracker.ietf.org/doc/html/rfc3225#section-3.
711+
p.addDO(dctx.Req)
712+
704713
// Only add pending requests if the cache is enabled, since this is a
705714
// mitigation against cache poisoning.
706715
//
@@ -714,14 +723,11 @@ func (p *Proxy) Resolve(ctx context.Context, dctx *DNSContext) (err error) {
714723

715724
if p.replyFromCache(dctx) {
716725
// Complete the response from cache.
726+
filterMsg(dctx.Res, dctx.Res, dctx.adBit, dctx.doBit, 0)
717727
dctx.scrub()
718728

719729
return nil
720730
}
721-
722-
// On cache miss request for DNSSEC from the upstream to cache it
723-
// afterwards.
724-
p.addDO(dctx.Req)
725731
}
726732

727733
var ok bool
@@ -800,12 +806,6 @@ func (p *Proxy) cacheWorks(dctx *DNSContext) (ok bool) {
800806
// Don't cache the requests intended for local upstream servers, those
801807
// should be fast enough as is.
802808
reason = "requested address is private"
803-
case !p.DNSSECEnabled && !dctx.doBit:
804-
// Don't cache the responses without DNSSEC RRs if DNSSEC is disabled
805-
// and DO bit is not set, since those responses may differ from the ones
806-
// with DNSSEC RRs and thus may be not the desired result for user. In
807-
// case DNSSEC is enabled in the proxy, the DO bit will be enforced.
808-
reason = "dnssec disabled"
809809
case dctx.Req.CheckingDisabled:
810810
// Also don't lookup the cache for responses with DNSSEC checking
811811
// disabled since only validated responses are cached and those may be

0 commit comments

Comments
 (0)