Files
dnsproxy/proxy/proxycache.go
T
Eugene Burkov f214e791e1 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 e36ac40f2f6dc3d6fd0d0ca1b61b15b2a83eb0de
Merge: 29d546b bd64c3a
Author: Eugene Burkov <[email protected]>
Date:   Thu May 21 18:40:11 2026 +0300

    Merge branch 'master' into 8384-fix-do-cache

commit 29d546b8b6a44d2b80bdab1ef40b5da729c07126
Author: Eugene Burkov <[email protected]>
Date:   Thu May 21 17:09:12 2026 +0300

    proxy: add do bit to cache key

commit e6245e9afd2813923f91b65e01bf96ab79fcddbf
Author: Eugene Burkov <[email protected]>
Date:   Wed May 20 22:17:17 2026 +0300

    proxy: imp doc

commit 58b5643fe5eefa5d2b05fffc427148b6cafb1dc1
Author: Eugene Burkov <[email protected]>
Date:   Wed May 20 21:04:01 2026 +0300

    proxy: fix caching without do
2026-05-21 15:55:57 +00:00

145 lines
3.7 KiB
Go

package proxy
import (
"net"
"slices"
)
// cacheForContext returns cache object for the given context.
func (p *Proxy) cacheForContext(d *DNSContext) (c *cache) {
if d.CustomUpstreamConfig != nil && d.CustomUpstreamConfig.cache != nil {
return d.CustomUpstreamConfig.cache
}
return p.cache
}
// replyFromCache tries to get the response from general or subnet cache. In
// case the cache is present in d, it's used first. Returns true on success.
func (p *Proxy) replyFromCache(d *DNSContext) (hit bool) {
dctxCache := p.cacheForContext(d)
var ci *cacheItem
var cacheSource string
var expired bool
var key []byte
// TODO(d.kolyshev): Use EnableEDNSClientSubnet from dctxCache.
if p.Config.EnableEDNSClientSubnet && d.ReqECS != nil {
ci, expired, key = dctxCache.getWithSubnet(d.Req, d.ReqECS)
cacheSource = "subnet cache"
} else {
ci, expired, key = dctxCache.get(d.Req)
cacheSource = "general cache"
}
if hit = ci != nil; !hit {
return hit
}
d.Res = ci.m
d.queryStatistics = cachedQueryStatistics(ci.u)
p.logger.Debug(
"replying from cache",
"source", cacheSource,
"ecs_enabled", p.Config.EnableEDNSClientSubnet,
)
if dctxCache.optimistic && expired {
// Build a reduced clone of the current context to avoid data race.
minCtxClone := &DNSContext{
// It is only read inside the optimistic resolver.
CustomUpstreamConfig: d.CustomUpstreamConfig,
ReqECS: cloneIPNet(d.ReqECS),
IsPrivateClient: d.IsPrivateClient,
}
if d.Req != nil {
minCtxClone.Req = d.Req.Copy()
}
go p.shortFlighter.resolveOnce(minCtxClone, key, p.logger)
}
return hit
}
// cloneIPNet returns a deep clone of n.
func cloneIPNet(n *net.IPNet) (clone *net.IPNet) {
if n == nil {
return nil
}
return &net.IPNet{
IP: slices.Clone(n.IP),
Mask: slices.Clone(n.Mask),
}
}
// cacheResp stores the response from d in general or subnet cache. In case the
// cache is present in d, it's used first.
func (p *Proxy) cacheResp(d *DNSContext) {
dctxCache := p.cacheForContext(d)
if !p.EnableEDNSClientSubnet {
dctxCache.set(d.Req, d.Res, d.Upstream, p.logger)
return
}
switch ecs, scope := ecsFromMsg(d.Res); {
case ecs != nil && d.ReqECS != nil:
ones, bits := ecs.Mask.Size()
reqOnes, _ := d.ReqECS.Mask.Size()
// If FAMILY, SOURCE PREFIX-LENGTH, and SOURCE PREFIX-LENGTH bits of
// ADDRESS in the response don't match the non-zero fields in the
// corresponding query, the full response MUST be dropped.
//
// See RFC 7871 Section 7.3.
//
// TODO(a.meshkov): The whole response MUST be dropped if ECS in it
// doesn't correspond.
if !ecs.IP.Mask(ecs.Mask).Equal(d.ReqECS.IP.Mask(d.ReqECS.Mask)) || ones != reqOnes {
p.logger.Debug(
"not caching response; subnet mismatch",
"ecs", ecs,
"req_ecs", d.ReqECS,
)
return
}
// If SCOPE PREFIX-LENGTH is not longer than SOURCE PREFIX-LENGTH, store
// SCOPE PREFIX-LENGTH bits of ADDRESS, and then mark the response as
// valid for all addresses that fall within that range.
//
// See RFC 7871 Section 7.3.1.
if scope < reqOnes {
ecs.Mask = net.CIDRMask(scope, bits)
ecs.IP = ecs.IP.Mask(ecs.Mask)
}
p.logger.Debug("caching response", "ecs", ecs)
dctxCache.setWithSubnet(d.Req, d.Res, d.Upstream, ecs, p.logger)
case d.ReqECS != nil:
// Cache the response for all subnets since the server doesn't support
// EDNS Client Subnet option.
dctxCache.setWithSubnet(d.Req, d.Res, d.Upstream, &net.IPNet{IP: nil, Mask: nil}, p.logger)
default:
dctxCache.set(d.Req, d.Res, d.Upstream, p.logger)
}
}
// ClearCache clears the DNS cache of p.
func (p *Proxy) ClearCache() {
if p.cache == nil {
return
}
p.cache.clearItems()
p.cache.clearItemsWithSubnet()
p.logger.Debug("cache cleared")
}