package proxy import ( "context" "net" "slices" ) // cacheForContext returns cache object for the given context. d must not be nil. 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. d // must not be nil. func (p *Proxy) replyFromCache(ctx context.Context, 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.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.DebugContext( ctx, "replying from cache", "source", cacheSource, "ecs_enabled", p.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(ctx, 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. d must not be nil. 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") }