diff --git a/adapter/dns.go b/adapter/dns.go index f527e7ccd..7545f1663 100644 --- a/adapter/dns.go +++ b/adapter/dns.go @@ -3,6 +3,7 @@ package adapter import ( "context" "net/netip" + "time" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" @@ -31,12 +32,13 @@ type DNSClient interface { } type DNSQueryOptions struct { - Transport DNSTransport - Strategy C.DomainStrategy - LookupStrategy C.DomainStrategy - DisableCache bool - RewriteTTL *uint32 - ClientSubnet netip.Prefix + Transport DNSTransport + Strategy C.DomainStrategy + LookupStrategy C.DomainStrategy + DisableCache bool + DisableOptimisticCache bool + RewriteTTL *uint32 + ClientSubnet netip.Prefix } func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (*DNSQueryOptions, error) { @@ -49,11 +51,12 @@ func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptio return nil, E.New("domain resolver not found: " + options.Server) } return &DNSQueryOptions{ - Transport: transport, - Strategy: C.DomainStrategy(options.Strategy), - DisableCache: options.DisableCache, - RewriteTTL: options.RewriteTTL, - ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}), + Transport: transport, + Strategy: C.DomainStrategy(options.Strategy), + DisableCache: options.DisableCache, + DisableOptimisticCache: options.DisableOptimisticCache, + RewriteTTL: options.RewriteTTL, + ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}), }, nil } @@ -63,6 +66,13 @@ type RDRCStore interface { SaveRDRCAsync(transportName string, qName string, qType uint16, logger logger.Logger) } +type DNSCacheStore interface { + LoadDNSCache(transportName string, qName string, qType uint16) (rawMessage []byte, expireAt time.Time, loaded bool) + SaveDNSCache(transportName string, qName string, qType uint16, rawMessage []byte, expireAt time.Time) error + SaveDNSCacheAsync(transportName string, qName string, qType uint16, rawMessage []byte, expireAt time.Time, logger logger.Logger) + ClearDNSCache() error +} + type DNSTransport interface { Lifecycle Type() string diff --git a/adapter/experimental.go b/adapter/experimental.go index 1bd8d2d92..49fd2bd31 100644 --- a/adapter/experimental.go +++ b/adapter/experimental.go @@ -47,6 +47,12 @@ type CacheFile interface { StoreRDRC() bool RDRCStore + StoreDNS() bool + DNSCacheStore + + SetDisableExpire(disableExpire bool) + SetOptimisticTimeout(timeout time.Duration) + LoadMode() string StoreMode(mode string) error LoadSelected(group string) string diff --git a/box.go b/box.go index 619b05bba..c5b3fc68c 100644 --- a/box.go +++ b/box.go @@ -196,7 +196,10 @@ func New(options Options) (*Box, error) { service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager) service.MustRegister[adapter.ServiceManager](ctx, serviceManager) service.MustRegister[adapter.CertificateProviderManager](ctx, certificateProviderManager) - dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions) + dnsRouter, err := dns.NewRouter(ctx, logFactory, dnsOptions) + if err != nil { + return nil, E.Cause(err, "initialize DNS router") + } service.MustRegister[adapter.DNSRouter](ctx, dnsRouter) service.MustRegister[adapter.DNSRuleSetUpdateValidator](ctx, dnsRouter) networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions, dnsOptions) @@ -372,7 +375,7 @@ func New(options Options) (*Box, error) { } } if needCacheFile { - cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile)) + cacheFile := cachefile.New(ctx, logFactory.NewLogger("cache-file"), common.PtrValueOrDefault(experimentalOptions.CacheFile)) service.MustRegister[adapter.CacheFile](ctx, cacheFile) internalServices = append(internalServices, cacheFile) } diff --git a/common/dialer/dialer.go b/common/dialer/dialer.go index 2ba559f9e..ca6f905fe 100644 --- a/common/dialer/dialer.go +++ b/common/dialer/dialer.go @@ -87,11 +87,12 @@ func NewWithOptions(options Options) (N.Dialer, error) { } server = dialOptions.DomainResolver.Server dnsQueryOptions = adapter.DNSQueryOptions{ - Transport: transport, - Strategy: strategy, - DisableCache: dialOptions.DomainResolver.DisableCache, - RewriteTTL: dialOptions.DomainResolver.RewriteTTL, - ClientSubnet: dialOptions.DomainResolver.ClientSubnet.Build(netip.Prefix{}), + Transport: transport, + Strategy: strategy, + DisableCache: dialOptions.DomainResolver.DisableCache, + DisableOptimisticCache: dialOptions.DomainResolver.DisableOptimisticCache, + RewriteTTL: dialOptions.DomainResolver.RewriteTTL, + ClientSubnet: dialOptions.DomainResolver.ClientSubnet.Build(netip.Prefix{}), } resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay) } else if options.DirectResolver { diff --git a/dns/client.go b/dns/client.go index 35eb9d607..a257fca64 100644 --- a/dns/client.go +++ b/dns/client.go @@ -30,56 +30,63 @@ var ( var _ adapter.DNSClient = (*Client)(nil) type Client struct { - timeout time.Duration - disableCache bool - disableExpire bool - independentCache bool - clientSubnet netip.Prefix - rdrc adapter.RDRCStore - initRDRCFunc func() adapter.RDRCStore - logger logger.ContextLogger - cache freelru.Cache[dns.Question, *dns.Msg] - cacheLock compatible.Map[transportCacheKey, chan struct{}] - transportCache freelru.Cache[transportCacheKey, *dns.Msg] - transportCacheLock compatible.Map[transportCacheKey, chan struct{}] + ctx context.Context + timeout time.Duration + disableCache bool + disableExpire bool + optimisticTimeout time.Duration + cacheCapacity uint32 + clientSubnet netip.Prefix + rdrc adapter.RDRCStore + initRDRCFunc func() adapter.RDRCStore + dnsCache adapter.DNSCacheStore + initDNSCacheFunc func() adapter.DNSCacheStore + logger logger.ContextLogger + cache freelru.Cache[dnsCacheKey, *dns.Msg] + cacheLock compatible.Map[dnsCacheKey, chan struct{}] + backgroundRefresh compatible.Map[dnsCacheKey, struct{}] } type ClientOptions struct { - Timeout time.Duration - DisableCache bool - DisableExpire bool - IndependentCache bool - CacheCapacity uint32 - ClientSubnet netip.Prefix - RDRC func() adapter.RDRCStore - Logger logger.ContextLogger + Context context.Context + Timeout time.Duration + DisableCache bool + DisableExpire bool + OptimisticTimeout time.Duration + CacheCapacity uint32 + ClientSubnet netip.Prefix + RDRC func() adapter.RDRCStore + DNSCache func() adapter.DNSCacheStore + Logger logger.ContextLogger } func NewClient(options ClientOptions) *Client { + cacheCapacity := options.CacheCapacity + if cacheCapacity < 1024 { + cacheCapacity = 1024 + } client := &Client{ - timeout: options.Timeout, - disableCache: options.DisableCache, - disableExpire: options.DisableExpire, - independentCache: options.IndependentCache, - clientSubnet: options.ClientSubnet, - initRDRCFunc: options.RDRC, - logger: options.Logger, + ctx: options.Context, + timeout: options.Timeout, + disableCache: options.DisableCache, + disableExpire: options.DisableExpire, + optimisticTimeout: options.OptimisticTimeout, + cacheCapacity: cacheCapacity, + clientSubnet: options.ClientSubnet, + initRDRCFunc: options.RDRC, + initDNSCacheFunc: options.DNSCache, + logger: options.Logger, } if client.timeout == 0 { client.timeout = C.DNSTimeout } - cacheCapacity := max(options.CacheCapacity, 1024) - if !client.disableCache { - if !client.independentCache { - client.cache = common.Must1(freelru.NewSharded[dns.Question, *dns.Msg](cacheCapacity, maphash.NewHasher[dns.Question]().Hash32)) - } else { - client.transportCache = common.Must1(freelru.NewSharded[transportCacheKey, *dns.Msg](cacheCapacity, maphash.NewHasher[transportCacheKey]().Hash32)) - } + if !client.disableCache && client.initDNSCacheFunc == nil { + client.initializeMemoryCache() } return client } -type transportCacheKey struct { +type dnsCacheKey struct { dns.Question transportTag string } @@ -88,6 +95,19 @@ func (c *Client) Start() { if c.initRDRCFunc != nil { c.rdrc = c.initRDRCFunc() } + if c.initDNSCacheFunc != nil { + c.dnsCache = c.initDNSCacheFunc() + } + if c.dnsCache == nil { + c.initializeMemoryCache() + } +} + +func (c *Client) initializeMemoryCache() { + if c.disableCache || c.cache != nil { + return + } + c.cache = common.Must1(freelru.NewSharded[dnsCacheKey, *dns.Msg](c.cacheCapacity, maphash.NewHasher[dnsCacheKey]().Hash32)) } func extractNegativeTTL(response *dns.Msg) (uint32, bool) { @@ -104,6 +124,37 @@ func extractNegativeTTL(response *dns.Msg) (uint32, bool) { return 0, false } +func computeTimeToLive(response *dns.Msg) uint32 { + var timeToLive uint32 + if len(response.Answer) == 0 { + if soaTTL, hasSOA := extractNegativeTTL(response); hasSOA { + return soaTTL + } + } + for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { + for _, record := range recordList { + if record.Header().Rrtype == dns.TypeOPT { + continue + } + if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive { + timeToLive = record.Header().Ttl + } + } + } + return timeToLive +} + +func normalizeTTL(response *dns.Msg, timeToLive uint32) { + for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { + for _, record := range recordList { + if record.Header().Rrtype == dns.TypeOPT { + continue + } + record.Header().Ttl = timeToLive + } + } +} + func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) (*dns.Msg, error) { if len(message.Question) == 0 { if c.logger != nil { @@ -118,13 +169,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m } return FixedResponseStatus(message, dns.RcodeSuccess), nil } - clientSubnet := options.ClientSubnet - if !clientSubnet.IsValid() { - clientSubnet = c.clientSubnet - } - if clientSubnet.IsValid() { - message = SetClientSubnet(message, clientSubnet) - } + message = c.prepareExchangeMessage(message, options) isSimpleRequest := len(message.Question) == 1 && len(message.Ns) == 0 && @@ -136,41 +181,32 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m !options.ClientSubnet.IsValid() disableCache := !isSimpleRequest || c.disableCache || options.DisableCache if !disableCache { - cacheKey := transportCacheKey{Question: question, transportTag: transport.Tag()} - if c.cache != nil { - cond, loaded := c.cacheLock.LoadOrStore(cacheKey, make(chan struct{})) - if loaded { - select { - case <-cond: - case <-ctx.Done(): - return nil, ctx.Err() - } - } else { - defer func() { - c.cacheLock.Delete(cacheKey) - close(cond) - }() - } - } else if c.transportCache != nil { - cond, loaded := c.transportCacheLock.LoadOrStore(cacheKey, make(chan struct{})) - if loaded { - select { - case <-cond: - case <-ctx.Done(): - return nil, ctx.Err() - } - } else { - defer func() { - c.transportCacheLock.Delete(cacheKey) - close(cond) - }() + cacheKey := dnsCacheKey{Question: question, transportTag: transport.Tag()} + cond, loaded := c.cacheLock.LoadOrStore(cacheKey, make(chan struct{})) + if loaded { + select { + case <-cond: + case <-ctx.Done(): + return nil, ctx.Err() } + } else { + defer func() { + c.cacheLock.Delete(cacheKey) + close(cond) + }() } - response, ttl := c.loadResponse(question, transport) + response, ttl, isStale := c.loadResponse(question, transport) if response != nil { - logCachedResponse(c.logger, ctx, response, ttl) - response.Id = message.Id - return response, nil + if isStale && !options.DisableOptimisticCache { + c.backgroundRefreshDNS(transport, question, message.Copy(), options, responseChecker) + logOptimisticResponse(c.logger, ctx, response) + response.Id = message.Id + return response, nil + } else if !isStale { + logCachedResponse(c.logger, ctx, response, ttl) + response.Id = message.Id + return response, nil + } } } @@ -186,52 +222,10 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m return nil, ErrResponseRejectedCached } } - ctx, cancel := context.WithTimeout(ctx, c.timeout) - response, err := transport.Exchange(ctx, message) - cancel() + response, err := c.exchangeToTransport(ctx, transport, message) if err != nil { - var rcodeError RcodeError - if errors.As(err, &rcodeError) { - response = FixedResponseStatus(message, int(rcodeError)) - } else { - return nil, err - } + return nil, err } - /*if question.Qtype == dns.TypeA || question.Qtype == dns.TypeAAAA { - validResponse := response - loop: - for { - var ( - addresses int - queryCNAME string - ) - for _, rawRR := range validResponse.Answer { - switch rr := rawRR.(type) { - case *dns.A: - break loop - case *dns.AAAA: - break loop - case *dns.CNAME: - queryCNAME = rr.Target - } - } - if queryCNAME == "" { - break - } - exMessage := *message - exMessage.Question = []dns.Question{{ - Name: queryCNAME, - Qtype: question.Qtype, - }} - validResponse, err = c.Exchange(ctx, transport, &exMessage, options, responseChecker) - if err != nil { - return nil, err - } - } - if validResponse != response { - response.Answer = append(response.Answer, validResponse.Answer...) - } - }*/ disableCache = disableCache || (response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError) if responseChecker != nil { var rejected bool @@ -248,54 +242,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m return response, ErrResponseRejected } } - if question.Qtype == dns.TypeHTTPS { - if options.Strategy == C.DomainStrategyIPv4Only || options.Strategy == C.DomainStrategyIPv6Only { - for _, rr := range response.Answer { - https, isHTTPS := rr.(*dns.HTTPS) - if !isHTTPS { - continue - } - content := https.SVCB - content.Value = common.Filter(content.Value, func(it dns.SVCBKeyValue) bool { - if options.Strategy == C.DomainStrategyIPv4Only { - return it.Key() != dns.SVCB_IPV6HINT - } else { - return it.Key() != dns.SVCB_IPV4HINT - } - }) - https.SVCB = content - } - } - } - var timeToLive uint32 - if len(response.Answer) == 0 { - if soaTTL, hasSOA := extractNegativeTTL(response); hasSOA { - timeToLive = soaTTL - } - } - if timeToLive == 0 { - for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { - for _, record := range recordList { - if record.Header().Rrtype == dns.TypeOPT { - continue - } - if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive { - timeToLive = record.Header().Ttl - } - } - } - } - if options.RewriteTTL != nil { - timeToLive = *options.RewriteTTL - } - for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { - for _, record := range recordList { - if record.Header().Rrtype == dns.TypeOPT { - continue - } - record.Header().Ttl = timeToLive - } - } + timeToLive := applyResponseOptions(question, response, options) if !disableCache { c.storeCache(transport, question, response, timeToLive) } @@ -362,8 +309,12 @@ func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, dom func (c *Client) ClearCache() { if c.cache != nil { c.cache.Purge() - } else if c.transportCache != nil { - c.transportCache.Purge() + } + if c.dnsCache != nil { + err := c.dnsCache.ClearDNSCache() + if err != nil && c.logger != nil { + c.logger.Warn("clear DNS cache: ", err) + } } } @@ -379,24 +330,22 @@ func (c *Client) storeCache(transport adapter.DNSTransport, question dns.Questio if timeToLive == 0 { return } + if c.dnsCache != nil { + packed, err := message.Pack() + if err == nil { + expireAt := time.Now().Add(time.Second * time.Duration(timeToLive)) + c.dnsCache.SaveDNSCacheAsync(transport.Tag(), question.Name, question.Qtype, packed, expireAt, c.logger) + } + return + } + if c.cache == nil { + return + } + key := dnsCacheKey{Question: question, transportTag: transport.Tag()} if c.disableExpire { - if !c.independentCache { - c.cache.Add(question, message.Copy()) - } else { - c.transportCache.Add(transportCacheKey{ - Question: question, - transportTag: transport.Tag(), - }, message.Copy()) - } + c.cache.Add(key, message.Copy()) } else { - if !c.independentCache { - c.cache.AddWithLifetime(question, message.Copy(), time.Second*time.Duration(timeToLive)) - } else { - c.transportCache.AddWithLifetime(transportCacheKey{ - Question: question, - transportTag: transport.Tag(), - }, message.Copy(), time.Second*time.Duration(timeToLive)) - } + c.cache.AddWithLifetime(key, message.Copy(), time.Second*time.Duration(timeToLive)) } } @@ -406,19 +355,19 @@ func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTran Qtype: qType, Qclass: dns.ClassINET, } - disableCache := c.disableCache || options.DisableCache - if !disableCache { - cachedAddresses, err := c.questionCache(question, transport) - if err != ErrNotCached { - return cachedAddresses, err - } - } message := dns.Msg{ MsgHdr: dns.MsgHdr{ RecursionDesired: true, }, Question: []dns.Question{question}, } + disableCache := c.disableCache || options.DisableCache + if !disableCache { + cachedAddresses, err := c.questionCache(ctx, transport, &message, options, responseChecker) + if err != ErrNotCached { + return cachedAddresses, err + } + } response, err := c.Exchange(ctx, transport, &message, options, responseChecker) if err != nil { return nil, err @@ -429,95 +378,174 @@ func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTran return MessageToAddresses(response), nil } -func (c *Client) questionCache(question dns.Question, transport adapter.DNSTransport) ([]netip.Addr, error) { - response, _ := c.loadResponse(question, transport) +func (c *Client) questionCache(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error) { + question := message.Question[0] + response, _, isStale := c.loadResponse(question, transport) if response == nil { return nil, ErrNotCached } + if isStale { + if options.DisableOptimisticCache { + return nil, ErrNotCached + } + c.backgroundRefreshDNS(transport, question, c.prepareExchangeMessage(message.Copy(), options), options, responseChecker) + logOptimisticResponse(c.logger, ctx, response) + } if response.Rcode != dns.RcodeSuccess { return nil, RcodeError(response.Rcode) } return MessageToAddresses(response), nil } -func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransport) (*dns.Msg, int) { - var ( - response *dns.Msg - loaded bool - ) - if c.disableExpire { - if !c.independentCache { - response, loaded = c.cache.Get(question) - } else { - response, loaded = c.transportCache.Get(transportCacheKey{ - Question: question, - transportTag: transport.Tag(), - }) - } - if !loaded { - return nil, 0 - } - return response.Copy(), 0 - } else { - var expireAt time.Time - if !c.independentCache { - response, expireAt, loaded = c.cache.GetWithLifetime(question) - } else { - response, expireAt, loaded = c.transportCache.GetWithLifetime(transportCacheKey{ - Question: question, - transportTag: transport.Tag(), - }) - } - if !loaded { - return nil, 0 - } - timeNow := time.Now() - if timeNow.After(expireAt) { - if !c.independentCache { - c.cache.Remove(question) - } else { - c.transportCache.Remove(transportCacheKey{ - Question: question, - transportTag: transport.Tag(), - }) - } - return nil, 0 - } - var originTTL int - for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { - for _, record := range recordList { - if record.Header().Rrtype == dns.TypeOPT { - continue - } - if originTTL == 0 || record.Header().Ttl > 0 && int(record.Header().Ttl) < originTTL { - originTTL = int(record.Header().Ttl) - } - } - } - nowTTL := max(int(expireAt.Sub(timeNow).Seconds()), 0) - response = response.Copy() - if originTTL > 0 { - duration := uint32(originTTL - nowTTL) - for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { - for _, record := range recordList { - if record.Header().Rrtype == dns.TypeOPT { - continue - } - record.Header().Ttl = record.Header().Ttl - duration - } - } - } else { - for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { - for _, record := range recordList { - if record.Header().Rrtype == dns.TypeOPT { - continue - } - record.Header().Ttl = uint32(nowTTL) - } - } - } - return response, nowTTL +func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransport) (*dns.Msg, int, bool) { + if c.dnsCache != nil { + return c.loadPersistentResponse(question, transport) } + if c.cache == nil { + return nil, 0, false + } + key := dnsCacheKey{Question: question, transportTag: transport.Tag()} + if c.disableExpire { + response, loaded := c.cache.Get(key) + if !loaded { + return nil, 0, false + } + return response.Copy(), 0, false + } + response, expireAt, loaded := c.cache.GetWithLifetimeNoExpire(key) + if !loaded { + return nil, 0, false + } + timeNow := time.Now() + if timeNow.After(expireAt) { + if c.optimisticTimeout > 0 && timeNow.Before(expireAt.Add(c.optimisticTimeout)) { + response = response.Copy() + normalizeTTL(response, 1) + return response, 0, true + } + c.cache.Remove(key) + return nil, 0, false + } + nowTTL := int(expireAt.Sub(timeNow).Seconds()) + if nowTTL < 0 { + nowTTL = 0 + } + response = response.Copy() + normalizeTTL(response, uint32(nowTTL)) + return response, nowTTL, false +} + +func (c *Client) loadPersistentResponse(question dns.Question, transport adapter.DNSTransport) (*dns.Msg, int, bool) { + rawMessage, expireAt, loaded := c.dnsCache.LoadDNSCache(transport.Tag(), question.Name, question.Qtype) + if !loaded { + return nil, 0, false + } + response := new(dns.Msg) + err := response.Unpack(rawMessage) + if err != nil { + return nil, 0, false + } + if c.disableExpire { + return response, 0, false + } + timeNow := time.Now() + if timeNow.After(expireAt) { + if c.optimisticTimeout > 0 && timeNow.Before(expireAt.Add(c.optimisticTimeout)) { + normalizeTTL(response, 1) + return response, 0, true + } + return nil, 0, false + } + nowTTL := max(int(expireAt.Sub(timeNow).Seconds()), 0) + normalizeTTL(response, uint32(nowTTL)) + return response, nowTTL, false +} + +func applyResponseOptions(question dns.Question, response *dns.Msg, options adapter.DNSQueryOptions) uint32 { + if question.Qtype == dns.TypeHTTPS && (options.Strategy == C.DomainStrategyIPv4Only || options.Strategy == C.DomainStrategyIPv6Only) { + for _, rr := range response.Answer { + https, isHTTPS := rr.(*dns.HTTPS) + if !isHTTPS { + continue + } + content := https.SVCB + content.Value = common.Filter(content.Value, func(it dns.SVCBKeyValue) bool { + if options.Strategy == C.DomainStrategyIPv4Only { + return it.Key() != dns.SVCB_IPV6HINT + } + return it.Key() != dns.SVCB_IPV4HINT + }) + https.SVCB = content + } + } + timeToLive := computeTimeToLive(response) + if options.RewriteTTL != nil { + timeToLive = *options.RewriteTTL + } + normalizeTTL(response, timeToLive) + return timeToLive +} + +func (c *Client) backgroundRefreshDNS(transport adapter.DNSTransport, question dns.Question, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) { + key := dnsCacheKey{Question: question, transportTag: transport.Tag()} + _, loaded := c.backgroundRefresh.LoadOrStore(key, struct{}{}) + if loaded { + return + } + go func() { + defer c.backgroundRefresh.Delete(key) + ctx := contextWithTransportTag(c.ctx, transport.Tag()) + response, err := c.exchangeToTransport(ctx, transport, message) + if err != nil { + if c.logger != nil { + c.logger.Debug("optimistic refresh failed for ", FqdnToDomain(question.Name), ": ", err) + } + return + } + if responseChecker != nil { + var rejected bool + if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError { + rejected = true + } else { + rejected = !responseChecker(response) + } + if rejected { + if c.rdrc != nil { + c.rdrc.SaveRDRCAsync(transport.Tag(), question.Name, question.Qtype, c.logger) + } + return + } + } else if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError { + return + } + timeToLive := applyResponseOptions(question, response, options) + c.storeCache(transport, question, response, timeToLive) + }() +} + +func (c *Client) prepareExchangeMessage(message *dns.Msg, options adapter.DNSQueryOptions) *dns.Msg { + clientSubnet := options.ClientSubnet + if !clientSubnet.IsValid() { + clientSubnet = c.clientSubnet + } + if clientSubnet.IsValid() { + message = SetClientSubnet(message, clientSubnet) + } + return message +} + +func (c *Client) exchangeToTransport(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg) (*dns.Msg, error) { + ctx, cancel := context.WithTimeout(ctx, c.timeout) + defer cancel() + response, err := transport.Exchange(ctx, message) + if err == nil { + return response, nil + } + var rcodeError RcodeError + if errors.As(err, &rcodeError) { + return FixedResponseStatus(message, int(rcodeError)), nil + } + return nil, err } func MessageToAddresses(response *dns.Msg) []netip.Addr { diff --git a/dns/client_log.go b/dns/client_log.go index 67d007084..129e273c4 100644 --- a/dns/client_log.go +++ b/dns/client_log.go @@ -22,6 +22,19 @@ func logCachedResponse(logger logger.ContextLogger, ctx context.Context, respons } } +func logOptimisticResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg) { + if logger == nil || len(response.Question) == 0 { + return + } + domain := FqdnToDomain(response.Question[0].Name) + logger.DebugContext(ctx, "optimistic ", domain, " ", dns.RcodeToString[response.Rcode]) + for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { + for _, record := range recordList { + logger.InfoContext(ctx, "optimistic ", dns.Type(record.Header().Rrtype).String(), " ", FormatQuestion(record.String())) + } + } +} + func logExchangedResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg, ttl uint32) { if logger == nil || len(response.Question) == 0 { return diff --git a/dns/router.go b/dns/router.go index a14cecd0e..b9fc8f977 100644 --- a/dns/router.go +++ b/dns/router.go @@ -51,7 +51,7 @@ type Router struct { closing bool } -func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOptions) *Router { +func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOptions) (*Router, error) { router := &Router{ ctx: ctx, logger: logFactory.NewLogger("dns"), @@ -61,12 +61,30 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOp rules: make([]adapter.DNSRule, 0, len(options.Rules)), defaultDomainStrategy: C.DomainStrategy(options.Strategy), } + if options.DNSClientOptions.IndependentCache { + deprecated.Report(ctx, deprecated.OptionIndependentDNSCache) + } + var optimisticTimeout time.Duration + optimisticOptions := common.PtrValueOrDefault(options.DNSClientOptions.Optimistic) + if optimisticOptions.Enabled { + if options.DNSClientOptions.DisableCache { + return nil, E.New("`optimistic` is conflict with `disable_cache`") + } + if options.DNSClientOptions.DisableExpire { + return nil, E.New("`optimistic` is conflict with `disable_expire`") + } + optimisticTimeout = time.Duration(optimisticOptions.Timeout) + if optimisticTimeout == 0 { + optimisticTimeout = 3 * 24 * time.Hour + } + } router.client = NewClient(ClientOptions{ - DisableCache: options.DNSClientOptions.DisableCache, - DisableExpire: options.DNSClientOptions.DisableExpire, - IndependentCache: options.DNSClientOptions.IndependentCache, - CacheCapacity: options.DNSClientOptions.CacheCapacity, - ClientSubnet: options.DNSClientOptions.ClientSubnet.Build(netip.Prefix{}), + Context: ctx, + DisableCache: options.DNSClientOptions.DisableCache, + DisableExpire: options.DNSClientOptions.DisableExpire, + OptimisticTimeout: optimisticTimeout, + CacheCapacity: options.DNSClientOptions.CacheCapacity, + ClientSubnet: options.DNSClientOptions.ClientSubnet.Build(netip.Prefix{}), RDRC: func() adapter.RDRCStore { cacheFile := service.FromContext[adapter.CacheFile](ctx) if cacheFile == nil { @@ -77,12 +95,24 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOp } return cacheFile }, + DNSCache: func() adapter.DNSCacheStore { + cacheFile := service.FromContext[adapter.CacheFile](ctx) + if cacheFile == nil { + return nil + } + if !cacheFile.StoreDNS() { + return nil + } + cacheFile.SetDisableExpire(options.DNSClientOptions.DisableExpire) + cacheFile.SetOptimisticTimeout(optimisticTimeout) + return cacheFile + }, Logger: router.logger, }) if options.ReverseMapping { router.dnsReverseMapping = common.Must1(freelru.NewSharded[netip.Addr, string](1024, maphash.NewHasher[netip.Addr]().Hash32)) } - return router + return router, nil } func (r *Router) Initialize(rules []option.DNSRule) error { @@ -319,6 +349,9 @@ func (r *Router) applyDNSRouteOptions(options *adapter.DNSQueryOptions, routeOpt if routeOptions.DisableCache { options.DisableCache = true } + if routeOptions.DisableOptimisticCache { + options.DisableOptimisticCache = true + } if routeOptions.RewriteTTL != nil { options.RewriteTTL = routeOptions.RewriteTTL } @@ -907,7 +940,9 @@ func dnsRuleModeRequirementsInRule(router adapter.Router, rule option.DNSRule, m return dnsRuleModeRequirementsInDefaultRule(router, rule.DefaultOptions, metadataOverrides) case C.RuleTypeLogical: flags := dnsRuleModeFlags{ - disabled: dnsRuleActionType(rule) == C.RuleActionTypeEvaluate || dnsRuleActionType(rule) == C.RuleActionTypeRespond, + disabled: dnsRuleActionType(rule) == C.RuleActionTypeEvaluate || + dnsRuleActionType(rule) == C.RuleActionTypeRespond || + dnsRuleActionDisablesLegacyDNSMode(rule.LogicalOptions.DNSRuleAction), neededFromStrategy: dnsRuleActionHasStrategy(rule.LogicalOptions.DNSRuleAction), } flags.needed = flags.neededFromStrategy @@ -926,7 +961,7 @@ func dnsRuleModeRequirementsInRule(router adapter.Router, rule option.DNSRule, m func dnsRuleModeRequirementsInDefaultRule(router adapter.Router, rule option.DefaultDNSRule, metadataOverrides map[string]adapter.RuleSetMetadata) (dnsRuleModeFlags, error) { flags := dnsRuleModeFlags{ - disabled: defaultRuleDisablesLegacyDNSMode(rule), + disabled: defaultRuleDisablesLegacyDNSMode(rule) || dnsRuleActionDisablesLegacyDNSMode(rule.DNSRuleAction), neededFromStrategy: dnsRuleActionHasStrategy(rule.DNSRuleAction), } flags.needed = defaultRuleNeedsLegacyDNSModeFromAddressFilter(rule) || flags.neededFromStrategy @@ -1063,6 +1098,17 @@ func validateLegacyDNSModeDisabledDefaultRule(rule option.DefaultDNSRule) (bool, return rule.MatchResponse || rule.Action == C.RuleActionTypeRespond, nil } +func dnsRuleActionDisablesLegacyDNSMode(action option.DNSRuleAction) bool { + switch action.Action { + case "", C.RuleActionTypeRoute, C.RuleActionTypeEvaluate: + return action.RouteOptions.DisableOptimisticCache + case C.RuleActionTypeRouteOptions: + return action.RouteOptionsOptions.DisableOptimisticCache + default: + return false + } +} + func dnsRuleActionHasStrategy(action option.DNSRuleAction) bool { switch action.Action { case "", C.RuleActionTypeRoute, C.RuleActionTypeEvaluate: diff --git a/docs/configuration/dns/index.md b/docs/configuration/dns/index.md index cbb58906f..b78a49e7a 100644 --- a/docs/configuration/dns/index.md +++ b/docs/configuration/dns/index.md @@ -2,6 +2,11 @@ icon: material/alert-decagram --- +!!! quote "Changes in sing-box 1.14.0" + + :material-delete-clock: [independent_cache](#independent_cache) + :material-plus: [optimistic](#optimistic) + !!! quote "Changes in sing-box 1.12.0" :material-decagram: [servers](#servers) @@ -25,6 +30,7 @@ icon: material/alert-decagram "disable_expire": false, "independent_cache": false, "cache_capacity": 0, + "optimistic": false, // or {} "reverse_mapping": false, "client_subnet": "", "fakeip": {} @@ -57,12 +63,20 @@ One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`. Disable dns cache. +Conflict with `optimistic`. + #### disable_expire Disable dns cache expire. +Conflict with `optimistic`. + #### independent_cache +!!! failure "Deprecated in sing-box 1.14.0" + + `independent_cache` is deprecated and will be removed in sing-box 1.14.0, check [Migration](/migration/#migrate-independent-dns-cache). + Make each DNS server's cache independent for special purposes. If enabled, will slightly degrade performance. #### cache_capacity @@ -73,6 +87,34 @@ LRU cache capacity. Value less than 1024 will be ignored. +#### optimistic + +!!! question "Since sing-box 1.14.0" + +Enable optimistic DNS caching. When a cached DNS entry has expired but is still within the timeout window, +the stale response is returned immediately while a background refresh is triggered. + +Conflict with `disable_cache` and `disable_expire`. + +Accepts a boolean or an object. When set to `true`, the default timeout of `3d` is used. + +```json +{ + "enabled": true, + "timeout": "3d" +} +``` + +##### enabled + +Enable optimistic DNS caching. + +##### timeout + +The maximum time an expired cache entry can be served optimistically. + +`3d` is used by default. + #### reverse_mapping Stores a reverse mapping of IP addresses after responding to a DNS query in order to provide domain names when routing. diff --git a/docs/configuration/dns/index.zh.md b/docs/configuration/dns/index.zh.md index cd2518107..ae06b8ab6 100644 --- a/docs/configuration/dns/index.zh.md +++ b/docs/configuration/dns/index.zh.md @@ -2,6 +2,11 @@ icon: material/alert-decagram --- +!!! quote "sing-box 1.14.0 中的更改" + + :material-delete-clock: [independent_cache](#independent_cache) + :material-plus: [optimistic](#optimistic) + !!! quote "sing-box 1.12.0 中的更改" :material-decagram: [servers](#servers) @@ -25,6 +30,7 @@ icon: material/alert-decagram "disable_expire": false, "independent_cache": false, "cache_capacity": 0, + "optimistic": false, // or {} "reverse_mapping": false, "client_subnet": "", "fakeip": {} @@ -56,12 +62,20 @@ icon: material/alert-decagram 禁用 DNS 缓存。 +与 `optimistic` 冲突。 + #### disable_expire 禁用 DNS 缓存过期。 +与 `optimistic` 冲突。 + #### independent_cache +!!! failure "已在 sing-box 1.14.0 废弃" + + `independent_cache` 已在 sing-box 1.14.0 废弃,且将在 sing-box 1.16.0 中被移除,参阅[迁移指南](/zh/migration/#迁移-independent-dns-cache)。 + 使每个 DNS 服务器的缓存独立,以满足特殊目的。如果启用,将轻微降低性能。 #### cache_capacity @@ -72,6 +86,34 @@ LRU 缓存容量。 小于 1024 的值将被忽略。 +#### optimistic + +!!! question "自 sing-box 1.14.0 起" + +启用乐观 DNS 缓存。当缓存的 DNS 条目已过期但仍在超时窗口内时, +立即返回过期的响应,同时在后台触发刷新。 + +与 `disable_cache` 和 `disable_expire` 冲突。 + +接受布尔值或对象。当设置为 `true` 时,使用默认超时 `3d`。 + +```json +{ + "enabled": true, + "timeout": "3d" +} +``` + +##### enabled + +启用乐观 DNS 缓存。 + +##### timeout + +过期缓存条目可被乐观提供的最长时间。 + +默认使用 `3d`。 + #### reverse_mapping 在响应 DNS 查询后存储 IP 地址的反向映射以为路由目的提供域名。 diff --git a/docs/configuration/dns/rule_action.md b/docs/configuration/dns/rule_action.md index dfc72dc76..e5a99be3c 100644 --- a/docs/configuration/dns/rule_action.md +++ b/docs/configuration/dns/rule_action.md @@ -6,7 +6,8 @@ icon: material/new-box :material-delete-clock: [strategy](#strategy) :material-plus: [evaluate](#evaluate) - :material-plus: [respond](#respond) + :material-plus: [respond](#respond) + :material-plus: [disable_optimistic_cache](#disable_optimistic_cache) !!! quote "Changes in sing-box 1.12.0" @@ -23,6 +24,7 @@ icon: material/new-box "server": "", "strategy": "", "disable_cache": false, + "disable_optimistic_cache": false, "rewrite_ttl": null, "client_subnet": null } @@ -52,6 +54,12 @@ One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`. Disable cache and save cache in this query. +#### disable_optimistic_cache + +!!! question "Since sing-box 1.14.0" + +Disable optimistic DNS caching in this query. + #### rewrite_ttl Rewrite TTL in DNS responses. @@ -73,6 +81,7 @@ Will override `dns.client_subnet`. "action": "evaluate", "server": "", "disable_cache": false, + "disable_optimistic_cache": false, "rewrite_ttl": null, "client_subnet": null } @@ -97,6 +106,12 @@ Tag of target server. Disable cache and save cache in this query. +#### disable_optimistic_cache + +!!! question "Since sing-box 1.14.0" + +Disable optimistic DNS caching in this query. + #### rewrite_ttl Rewrite TTL in DNS responses. @@ -131,6 +146,7 @@ Only allowed after a preceding top-level `evaluate` rule. If the action is reach { "action": "route-options", "disable_cache": false, + "disable_optimistic_cache": false, "rewrite_ttl": null, "client_subnet": null } diff --git a/docs/configuration/dns/rule_action.zh.md b/docs/configuration/dns/rule_action.zh.md index 36c8111ce..24179977f 100644 --- a/docs/configuration/dns/rule_action.zh.md +++ b/docs/configuration/dns/rule_action.zh.md @@ -6,7 +6,8 @@ icon: material/new-box :material-delete-clock: [strategy](#strategy) :material-plus: [evaluate](#evaluate) - :material-plus: [respond](#respond) + :material-plus: [respond](#respond) + :material-plus: [disable_optimistic_cache](#disable_optimistic_cache) !!! quote "sing-box 1.12.0 中的更改" @@ -23,6 +24,7 @@ icon: material/new-box "server": "", "strategy": "", "disable_cache": false, + "disable_optimistic_cache": false, "rewrite_ttl": null, "client_subnet": null } @@ -52,6 +54,12 @@ icon: material/new-box 在此查询中禁用缓存。 +#### disable_optimistic_cache + +!!! question "自 sing-box 1.14.0 起" + +在此查询中禁用乐观 DNS 缓存。 + #### rewrite_ttl 重写 DNS 回应中的 TTL。 @@ -73,6 +81,7 @@ icon: material/new-box "action": "evaluate", "server": "", "disable_cache": false, + "disable_optimistic_cache": false, "rewrite_ttl": null, "client_subnet": null } @@ -95,6 +104,12 @@ icon: material/new-box 在此查询中禁用缓存。 +#### disable_optimistic_cache + +!!! question "自 sing-box 1.14.0 起" + +在此查询中禁用乐观 DNS 缓存。 + #### rewrite_ttl 重写 DNS 回应中的 TTL。 @@ -129,6 +144,7 @@ icon: material/new-box { "action": "route-options", "disable_cache": false, + "disable_optimistic_cache": false, "rewrite_ttl": null, "client_subnet": null } diff --git a/docs/configuration/experimental/cache-file.md b/docs/configuration/experimental/cache-file.md index f91ee50fd..b93aa1906 100644 --- a/docs/configuration/experimental/cache-file.md +++ b/docs/configuration/experimental/cache-file.md @@ -1,5 +1,10 @@ !!! question "Since sing-box 1.8.0" +!!! quote "Changes in sing-box 1.14.0" + + :material-delete-clock: [store_rdrc](#store_rdrc) + :material-plus: [store_dns](#store_dns) + !!! quote "Changes in sing-box 1.9.0" :material-plus: [store_rdrc](#store_rdrc) @@ -14,7 +19,8 @@ "cache_id": "", "store_fakeip": false, "store_rdrc": false, - "rdrc_timeout": "" + "rdrc_timeout": "", + "store_dns": false } ``` @@ -42,6 +48,10 @@ Store fakeip in the cache file #### store_rdrc +!!! failure "Deprecated in sing-box 1.14.0" + + `store_rdrc` is deprecated and will be removed in sing-box 1.16.0, check [Migration](/migration/#migrate-store-rdrc). + Store rejected DNS response cache in the cache file The check results of [Legacy Address Filter Fields](/configuration/dns/rule/#legacy-address-filter-fields) @@ -52,3 +62,9 @@ will be cached until expiration. Timeout of rejected DNS response cache. `7d` is used by default. + +#### store_dns + +!!! question "Since sing-box 1.14.0" + +Store DNS cache in the cache file. diff --git a/docs/configuration/experimental/cache-file.zh.md b/docs/configuration/experimental/cache-file.zh.md index a998aa773..5382f3a18 100644 --- a/docs/configuration/experimental/cache-file.zh.md +++ b/docs/configuration/experimental/cache-file.zh.md @@ -1,5 +1,10 @@ !!! question "自 sing-box 1.8.0 起" +!!! quote "sing-box 1.14.0 中的更改" + + :material-delete-clock: [store_rdrc](#store_rdrc) + :material-plus: [store_dns](#store_dns) + !!! quote "sing-box 1.9.0 中的更改" :material-plus: [store_rdrc](#store_rdrc) @@ -14,7 +19,8 @@ "cache_id": "", "store_fakeip": false, "store_rdrc": false, - "rdrc_timeout": "" + "rdrc_timeout": "", + "store_dns": false } ``` @@ -40,6 +46,10 @@ #### store_rdrc +!!! failure "已在 sing-box 1.14.0 废弃" + + `store_rdrc` 已在 sing-box 1.14.0 废弃,且将在 sing-box 1.16.0 中被移除,参阅[迁移指南](/zh/migration/#迁移-store_rdrc)。 + 将拒绝的 DNS 响应缓存存储在缓存文件中。 [旧版地址筛选字段](/zh/configuration/dns/rule/#旧版地址筛选字段) 的检查结果将被缓存至过期。 @@ -49,3 +59,9 @@ 拒绝的 DNS 响应缓存超时。 默认使用 `7d`。 + +#### store_dns + +!!! question "自 sing-box 1.14.0 起" + +将 DNS 缓存存储在缓存文件中。 diff --git a/docs/configuration/route/rule_action.md b/docs/configuration/route/rule_action.md index 4f2a35cbd..1ba690398 100644 --- a/docs/configuration/route/rule_action.md +++ b/docs/configuration/route/rule_action.md @@ -7,6 +7,10 @@ icon: material/new-box :material-plus: [bypass](#bypass) :material-alert: [reject](#reject) +!!! quote "Changes in sing-box 1.14.0" + + :material-plus: [resolve.disable_optimistic_cache](#disable_optimistic_cache) + !!! quote "Changes in sing-box 1.12.0" :material-plus: [tls_fragment](#tls_fragment) @@ -279,6 +283,7 @@ Timeout for sniffing. "server": "", "strategy": "", "disable_cache": false, + "disable_optimistic_cache": false, "rewrite_ttl": null, "client_subnet": null } @@ -302,6 +307,12 @@ DNS resolution strategy, available values are: `prefer_ipv4`, `prefer_ipv6`, `ip Disable cache and save cache in this query. +#### disable_optimistic_cache + +!!! question "Since sing-box 1.14.0" + +Disable optimistic DNS caching in this query. + #### rewrite_ttl !!! question "Since sing-box 1.12.0" diff --git a/docs/configuration/route/rule_action.zh.md b/docs/configuration/route/rule_action.zh.md index 16e161808..5b13219b2 100644 --- a/docs/configuration/route/rule_action.zh.md +++ b/docs/configuration/route/rule_action.zh.md @@ -7,6 +7,10 @@ icon: material/new-box :material-plus: [bypass](#bypass) :material-alert: [reject](#reject) +!!! quote "sing-box 1.14.0 中的更改" + + :material-plus: [resolve.disable_optimistic_cache](#disable_optimistic_cache) + !!! quote "sing-box 1.12.0 中的更改" :material-plus: [tls_fragment](#tls_fragment) @@ -268,6 +272,7 @@ UDP 连接超时时间。 "server": "", "strategy": "", "disable_cache": false, + "disable_optimistic_cache": false, "rewrite_ttl": null, "client_subnet": null } @@ -291,6 +296,12 @@ DNS 解析策略,可用值有:`prefer_ipv4`、`prefer_ipv6`、`ipv4_only`、 在此查询中禁用缓存。 +#### disable_optimistic_cache + +!!! question "自 sing-box 1.14.0 起" + +在此查询中禁用乐观 DNS 缓存。 + #### rewrite_ttl !!! question "自 sing-box 1.12.0 起" diff --git a/docs/deprecated.md b/docs/deprecated.md index 47a9bffdd..1eeab10d3 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -27,6 +27,21 @@ check [Migration](../migration/#migrate-address-filter-fields-to-response-matchi Old fields will be removed in sing-box 1.16.0. +#### `independent_cache` DNS option + +`independent_cache` DNS option is deprecated. +The DNS cache now always keys by transport, making this option unnecessary, +check [Migration](../migration/#migrate-independent-dns-cache). + +Old fields will be removed in sing-box 1.16.0. + +#### `store_rdrc` cache file option + +`store_rdrc` cache file option is deprecated, +check [Migration](../migration/#migrate-store-rdrc). + +Old fields will be removed in sing-box 1.16.0. + #### Legacy Address Filter Fields in DNS rules Legacy Address Filter Fields (`ip_cidr`, `ip_is_private` without `match_response`) diff --git a/docs/deprecated.zh.md b/docs/deprecated.zh.md index a4995b568..5dabd69fb 100644 --- a/docs/deprecated.zh.md +++ b/docs/deprecated.zh.md @@ -27,6 +27,21 @@ TLS 中的内联 ACME 选项(`tls.acme`)已废弃, 旧字段将在 sing-box 1.16.0 中被移除。 +#### `independent_cache` DNS 选项 + +`independent_cache` DNS 选项已废弃。 +DNS 缓存现在始终按传输分离,使此选项不再需要, +参阅[迁移指南](/zh/migration/#迁移-independent-dns-cache)。 + +旧字段将在 sing-box 1.16.0 中被移除。 + +#### `store_rdrc` 缓存文件选项 + +`store_rdrc` 缓存文件选项已废弃, +参阅[迁移指南](/zh/migration/#迁移-store_rdrc)。 + +旧字段将在 sing-box 1.16.0 中被移除。 + #### 旧版地址筛选字段 (DNS 规则) 旧版地址筛选字段(不使用 `match_response` 的 `ip_cidr`、`ip_is_private`)已废弃, diff --git a/docs/migration.md b/docs/migration.md index 129f387fa..867f903b6 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -137,6 +137,68 @@ to fetch a DNS response, then match against it explicitly with `match_response`. } ``` +### Migrate independent DNS cache + +The DNS cache now always keys by transport name, making `independent_cache` unnecessary. +Simply remove the field. + +!!! info "References" + + [DNS](/configuration/dns/) + +=== ":material-card-remove: Deprecated" + + ```json + { + "dns": { + "independent_cache": true + } + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "dns": {} + } + ``` + +### Migrate store_rdrc + +`store_rdrc` is deprecated and can be replaced by `store_dns`, +which persists the full DNS cache to the cache file. + +!!! info "References" + + [Cache File](/configuration/experimental/cache-file/) + +=== ":material-card-remove: Deprecated" + + ```json + { + "experimental": { + "cache_file": { + "enabled": true, + "store_rdrc": true + } + } + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "experimental": { + "cache_file": { + "enabled": true, + "store_dns": true + } + } + } + ``` + ### ip_version and query_type behavior changes in DNS rules In sing-box 1.14.0, the behavior of diff --git a/docs/migration.zh.md b/docs/migration.zh.md index 995b0d941..54dec47e4 100644 --- a/docs/migration.zh.md +++ b/docs/migration.zh.md @@ -137,6 +137,68 @@ sing-box 1.14.0 新增字段参阅 [ACME](/zh/configuration/shared/certificate-p } ``` +### 迁移 independent DNS cache + +DNS 缓存现在始终按传输名称分离,使 `independent_cache` 不再需要。 +直接移除该字段即可。 + +!!! info "参考" + + [DNS](/zh/configuration/dns/) + +=== ":material-card-remove: 弃用的" + + ```json + { + "dns": { + "independent_cache": true + } + } + ``` + +=== ":material-card-multiple: 新的" + + ```json + { + "dns": {} + } + ``` + +### 迁移 store_rdrc + +`store_rdrc` 已废弃,且可以被 `store_dns` 替代, +后者将完整的 DNS 缓存持久化到缓存文件中。 + +!!! info "参考" + + [缓存文件](/zh/configuration/experimental/cache-file/) + +=== ":material-card-remove: 弃用的" + + ```json + { + "experimental": { + "cache_file": { + "enabled": true, + "store_rdrc": true + } + } + } + ``` + +=== ":material-card-multiple: 新的" + + ```json + { + "experimental": { + "cache_file": { + "enabled": true, + "store_dns": true + } + } + } + ``` + ### DNS 规则中的 ip_version 和 query_type 行为更改 在 sing-box 1.14.0 中,DNS 规则中的 diff --git a/experimental/cachefile/cache.go b/experimental/cachefile/cache.go index e52dd3ed2..372e3938e 100644 --- a/experimental/cachefile/cache.go +++ b/experimental/cachefile/cache.go @@ -12,9 +12,11 @@ import ( "github.com/sagernet/bbolt" bboltErrors "github.com/sagernet/bbolt/errors" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/experimental/deprecated" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/service/filemanager" ) @@ -30,6 +32,7 @@ var ( string(bucketMode), string(bucketRuleSet), string(bucketRDRC), + string(bucketDNSCache), } cacheIDDefault = []byte("default") @@ -38,30 +41,43 @@ var ( var _ adapter.CacheFile = (*CacheFile)(nil) type CacheFile struct { - ctx context.Context - path string - cacheID []byte - storeFakeIP bool - storeRDRC bool - rdrcTimeout time.Duration - DB *bbolt.DB - resetAccess sync.Mutex - saveMetadataTimer *time.Timer - saveFakeIPAccess sync.RWMutex - saveDomain map[netip.Addr]string - saveAddress4 map[string]netip.Addr - saveAddress6 map[string]netip.Addr - saveRDRCAccess sync.RWMutex - saveRDRC map[saveRDRCCacheKey]bool + ctx context.Context + logger logger.Logger + path string + cacheID []byte + storeFakeIP bool + storeRDRC bool + storeDNS bool + disableExpire bool + rdrcTimeout time.Duration + optimisticTimeout time.Duration + DB *bbolt.DB + resetAccess sync.Mutex + saveMetadataTimer *time.Timer + saveFakeIPAccess sync.RWMutex + saveDomain map[netip.Addr]string + saveAddress4 map[string]netip.Addr + saveAddress6 map[string]netip.Addr + saveRDRCAccess sync.RWMutex + saveRDRC map[saveCacheKey]bool + saveDNSCacheAccess sync.RWMutex + saveDNSCache map[saveCacheKey]saveDNSCacheEntry } -type saveRDRCCacheKey struct { +type saveCacheKey struct { TransportName string QuestionName string QType uint16 } -func New(ctx context.Context, options option.CacheFileOptions) *CacheFile { +type saveDNSCacheEntry struct { + rawMessage []byte + expireAt time.Time + sequence uint64 + saving bool +} + +func New(ctx context.Context, logger logger.Logger, options option.CacheFileOptions) *CacheFile { var path string if options.Path != "" { path = options.Path @@ -72,6 +88,9 @@ func New(ctx context.Context, options option.CacheFileOptions) *CacheFile { if options.CacheID != "" { cacheIDBytes = append([]byte{0}, []byte(options.CacheID)...) } + if options.StoreRDRC { + deprecated.Report(ctx, deprecated.OptionStoreRDRC) + } var rdrcTimeout time.Duration if options.StoreRDRC { if options.RDRCTimeout > 0 { @@ -82,15 +101,18 @@ func New(ctx context.Context, options option.CacheFileOptions) *CacheFile { } return &CacheFile{ ctx: ctx, + logger: logger, path: filemanager.BasePath(ctx, path), cacheID: cacheIDBytes, storeFakeIP: options.StoreFakeIP, storeRDRC: options.StoreRDRC, + storeDNS: options.StoreDNS, rdrcTimeout: rdrcTimeout, saveDomain: make(map[netip.Addr]string), saveAddress4: make(map[string]netip.Addr), saveAddress6: make(map[string]netip.Addr), - saveRDRC: make(map[saveRDRCCacheKey]bool), + saveRDRC: make(map[saveCacheKey]bool), + saveDNSCache: make(map[saveCacheKey]saveDNSCacheEntry), } } @@ -102,10 +124,44 @@ func (c *CacheFile) Dependencies() []string { return nil } +func (c *CacheFile) SetOptimisticTimeout(timeout time.Duration) { + c.optimisticTimeout = timeout +} + +func (c *CacheFile) SetDisableExpire(disableExpire bool) { + c.disableExpire = disableExpire +} + func (c *CacheFile) Start(stage adapter.StartStage) error { - if stage != adapter.StartStateInitialize { - return nil + switch stage { + case adapter.StartStateInitialize: + return c.start() + case adapter.StartStateStart: + c.startCacheCleanup() } + return nil +} + +func (c *CacheFile) startCacheCleanup() { + if c.storeDNS { + c.clearRDRC() + c.cleanupDNSCache() + interval := c.optimisticTimeout / 2 + if interval <= 0 { + interval = time.Hour + } + go c.loopCacheCleanup(interval, c.cleanupDNSCache) + } else if c.storeRDRC { + c.cleanupRDRC() + interval := c.rdrcTimeout / 2 + if interval <= 0 { + interval = time.Hour + } + go c.loopCacheCleanup(interval, c.cleanupRDRC) + } +} + +func (c *CacheFile) start() error { const fileMode = 0o666 options := bbolt.Options{Timeout: time.Second} var ( diff --git a/experimental/cachefile/dns_cache.go b/experimental/cachefile/dns_cache.go new file mode 100644 index 000000000..914c7e5ad --- /dev/null +++ b/experimental/cachefile/dns_cache.go @@ -0,0 +1,299 @@ +package cachefile + +import ( + "encoding/binary" + "time" + + "github.com/sagernet/bbolt" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/logger" +) + +var bucketDNSCache = []byte("dns_cache") + +func (c *CacheFile) StoreDNS() bool { + return c.storeDNS +} + +func (c *CacheFile) LoadDNSCache(transportName string, qName string, qType uint16) (rawMessage []byte, expireAt time.Time, loaded bool) { + c.saveDNSCacheAccess.RLock() + entry, cached := c.saveDNSCache[saveCacheKey{transportName, qName, qType}] + c.saveDNSCacheAccess.RUnlock() + if cached { + return entry.rawMessage, entry.expireAt, true + } + key := buf.Get(2 + len(qName)) + binary.BigEndian.PutUint16(key, qType) + copy(key[2:], qName) + defer buf.Put(key) + err := c.view(func(tx *bbolt.Tx) error { + bucket := c.bucket(tx, bucketDNSCache) + if bucket == nil { + return nil + } + bucket = bucket.Bucket([]byte(transportName)) + if bucket == nil { + return nil + } + content := bucket.Get(key) + if len(content) < 8 { + return nil + } + expireAt = time.Unix(int64(binary.BigEndian.Uint64(content[:8])), 0) + rawMessage = make([]byte, len(content)-8) + copy(rawMessage, content[8:]) + loaded = true + return nil + }) + if err != nil { + return nil, time.Time{}, false + } + return +} + +func (c *CacheFile) SaveDNSCache(transportName string, qName string, qType uint16, rawMessage []byte, expireAt time.Time) error { + return c.batch(func(tx *bbolt.Tx) error { + bucket, err := c.createBucket(tx, bucketDNSCache) + if err != nil { + return err + } + bucket, err = bucket.CreateBucketIfNotExists([]byte(transportName)) + if err != nil { + return err + } + key := buf.Get(2 + len(qName)) + binary.BigEndian.PutUint16(key, qType) + copy(key[2:], qName) + defer buf.Put(key) + value := buf.Get(8 + len(rawMessage)) + defer buf.Put(value) + binary.BigEndian.PutUint64(value[:8], uint64(expireAt.Unix())) + copy(value[8:], rawMessage) + return bucket.Put(key, value) + }) +} + +func (c *CacheFile) SaveDNSCacheAsync(transportName string, qName string, qType uint16, rawMessage []byte, expireAt time.Time, logger logger.Logger) { + saveKey := saveCacheKey{transportName, qName, qType} + if !c.queueDNSCacheSave(saveKey, rawMessage, expireAt) { + return + } + go c.flushPendingDNSCache(saveKey, logger) +} + +func (c *CacheFile) queueDNSCacheSave(saveKey saveCacheKey, rawMessage []byte, expireAt time.Time) bool { + c.saveDNSCacheAccess.Lock() + defer c.saveDNSCacheAccess.Unlock() + entry := c.saveDNSCache[saveKey] + entry.rawMessage = append([]byte(nil), rawMessage...) + entry.expireAt = expireAt + entry.sequence++ + startFlush := !entry.saving + entry.saving = true + c.saveDNSCache[saveKey] = entry + return startFlush +} + +func (c *CacheFile) flushPendingDNSCache(saveKey saveCacheKey, logger logger.Logger) { + c.flushPendingDNSCacheWith(saveKey, logger, func(entry saveDNSCacheEntry) error { + return c.SaveDNSCache(saveKey.TransportName, saveKey.QuestionName, saveKey.QType, entry.rawMessage, entry.expireAt) + }) +} + +func (c *CacheFile) flushPendingDNSCacheWith(saveKey saveCacheKey, logger logger.Logger, save func(saveDNSCacheEntry) error) { + for { + c.saveDNSCacheAccess.RLock() + entry, loaded := c.saveDNSCache[saveKey] + c.saveDNSCacheAccess.RUnlock() + if !loaded { + return + } + err := save(entry) + if err != nil { + logger.Warn("save DNS cache: ", err) + } + c.saveDNSCacheAccess.Lock() + currentEntry, loaded := c.saveDNSCache[saveKey] + if !loaded { + c.saveDNSCacheAccess.Unlock() + return + } + if currentEntry.sequence != entry.sequence { + c.saveDNSCacheAccess.Unlock() + continue + } + delete(c.saveDNSCache, saveKey) + c.saveDNSCacheAccess.Unlock() + return + } +} + +func (c *CacheFile) ClearDNSCache() error { + c.saveDNSCacheAccess.Lock() + clear(c.saveDNSCache) + c.saveDNSCacheAccess.Unlock() + return c.batch(func(tx *bbolt.Tx) error { + if c.cacheID == nil { + bucket := tx.Bucket(bucketDNSCache) + if bucket == nil { + return nil + } + return tx.DeleteBucket(bucketDNSCache) + } + bucket := tx.Bucket(c.cacheID) + if bucket == nil || bucket.Bucket(bucketDNSCache) == nil { + return nil + } + return bucket.DeleteBucket(bucketDNSCache) + }) +} + +func (c *CacheFile) loopCacheCleanup(interval time.Duration, cleanupFunc func()) { + ticker := time.NewTicker(interval) + defer ticker.Stop() + for { + select { + case <-c.ctx.Done(): + return + case <-ticker.C: + cleanupFunc() + } + } +} + +func (c *CacheFile) cleanupDNSCache() { + now := time.Now() + err := c.batch(func(tx *bbolt.Tx) error { + bucket := c.bucket(tx, bucketDNSCache) + if bucket == nil { + return nil + } + var emptyTransports [][]byte + err := bucket.ForEachBucket(func(transportName []byte) error { + transportBucket := bucket.Bucket(transportName) + if transportBucket == nil { + return nil + } + var expiredKeys [][]byte + err := transportBucket.ForEach(func(key, value []byte) error { + if len(value) < 8 { + expiredKeys = append(expiredKeys, append([]byte(nil), key...)) + return nil + } + if c.disableExpire { + return nil + } + expireAt := time.Unix(int64(binary.BigEndian.Uint64(value[:8])), 0) + if now.After(expireAt.Add(c.optimisticTimeout)) { + expiredKeys = append(expiredKeys, append([]byte(nil), key...)) + } + return nil + }) + if err != nil { + return err + } + for _, key := range expiredKeys { + err = transportBucket.Delete(key) + if err != nil { + return err + } + } + first, _ := transportBucket.Cursor().First() + if first == nil { + emptyTransports = append(emptyTransports, append([]byte(nil), transportName...)) + } + return nil + }) + if err != nil { + return err + } + for _, name := range emptyTransports { + err = bucket.DeleteBucket(name) + if err != nil { + return err + } + } + return nil + }) + if err != nil { + c.logger.Warn("cleanup DNS cache: ", err) + } +} + +func (c *CacheFile) clearRDRC() { + c.saveRDRCAccess.Lock() + clear(c.saveRDRC) + c.saveRDRCAccess.Unlock() + err := c.batch(func(tx *bbolt.Tx) error { + if c.cacheID == nil { + if tx.Bucket(bucketRDRC) == nil { + return nil + } + return tx.DeleteBucket(bucketRDRC) + } + bucket := tx.Bucket(c.cacheID) + if bucket == nil || bucket.Bucket(bucketRDRC) == nil { + return nil + } + return bucket.DeleteBucket(bucketRDRC) + }) + if err != nil { + c.logger.Warn("clear RDRC: ", err) + } +} + +func (c *CacheFile) cleanupRDRC() { + now := time.Now() + err := c.batch(func(tx *bbolt.Tx) error { + bucket := c.bucket(tx, bucketRDRC) + if bucket == nil { + return nil + } + var emptyTransports [][]byte + err := bucket.ForEachBucket(func(transportName []byte) error { + transportBucket := bucket.Bucket(transportName) + if transportBucket == nil { + return nil + } + var expiredKeys [][]byte + err := transportBucket.ForEach(func(key, value []byte) error { + if len(value) < 8 { + expiredKeys = append(expiredKeys, append([]byte(nil), key...)) + return nil + } + expiresAt := time.Unix(int64(binary.BigEndian.Uint64(value)), 0) + if now.After(expiresAt) { + expiredKeys = append(expiredKeys, append([]byte(nil), key...)) + } + return nil + }) + if err != nil { + return err + } + for _, key := range expiredKeys { + err = transportBucket.Delete(key) + if err != nil { + return err + } + } + first, _ := transportBucket.Cursor().First() + if first == nil { + emptyTransports = append(emptyTransports, append([]byte(nil), transportName...)) + } + return nil + }) + if err != nil { + return err + } + for _, name := range emptyTransports { + err = bucket.DeleteBucket(name) + if err != nil { + return err + } + } + return nil + }) + if err != nil { + c.logger.Warn("cleanup RDRC: ", err) + } +} diff --git a/experimental/cachefile/rdrc.go b/experimental/cachefile/rdrc.go index dde324c3c..c02259c38 100644 --- a/experimental/cachefile/rdrc.go +++ b/experimental/cachefile/rdrc.go @@ -21,7 +21,7 @@ func (c *CacheFile) RDRCTimeout() time.Duration { func (c *CacheFile) LoadRDRC(transportName string, qName string, qType uint16) (rejected bool) { c.saveRDRCAccess.RLock() - rejected, cached := c.saveRDRC[saveRDRCCacheKey{transportName, qName, qType}] + rejected, cached := c.saveRDRC[saveCacheKey{transportName, qName, qType}] c.saveRDRCAccess.RUnlock() if cached { return @@ -93,7 +93,7 @@ func (c *CacheFile) SaveRDRC(transportName string, qName string, qType uint16) e } func (c *CacheFile) SaveRDRCAsync(transportName string, qName string, qType uint16, logger logger.Logger) { - saveKey := saveRDRCCacheKey{transportName, qName, qType} + saveKey := saveCacheKey{transportName, qName, qType} c.saveRDRCAccess.Lock() c.saveRDRC[saveKey] = true c.saveRDRCAccess.Unlock() diff --git a/experimental/deprecated/constants.go b/experimental/deprecated/constants.go index afe5c021a..108eba575 100644 --- a/experimental/deprecated/constants.go +++ b/experimental/deprecated/constants.go @@ -120,6 +120,24 @@ var OptionLegacyDNSRuleStrategy = Note{ MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-dns-rule-action-strategy-to-rule-items", } +var OptionIndependentDNSCache = Note{ + Name: "independent-dns-cache", + Description: "`independent_cache` DNS option", + DeprecatedVersion: "1.14.0", + ScheduledVersion: "1.16.0", + EnvName: "INDEPENDENT_DNS_CACHE", + MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-independent-dns-cache", +} + +var OptionStoreRDRC = Note{ + Name: "store-rdrc", + Description: "`store_rdrc` cache file option", + DeprecatedVersion: "1.14.0", + ScheduledVersion: "1.16.0", + EnvName: "STORE_RDRC", + MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-store-rdrc", +} + var Options = []Note{ OptionOutboundDNSRuleItem, OptionMissingDomainResolver, @@ -128,4 +146,6 @@ var Options = []Note{ OptionRuleSetIPCIDRAcceptEmpty, OptionLegacyDNSAddressFilter, OptionLegacyDNSRuleStrategy, + OptionIndependentDNSCache, + OptionStoreRDRC, } diff --git a/option/dns.go b/option/dns.go index ee29ce096..c09b3d5f3 100644 --- a/option/dns.go +++ b/option/dns.go @@ -52,9 +52,32 @@ type DNSClientOptions struct { DisableExpire bool `json:"disable_expire,omitempty"` IndependentCache bool `json:"independent_cache,omitempty"` CacheCapacity uint32 `json:"cache_capacity,omitempty"` + Optimistic *OptimisticDNSOptions `json:"optimistic,omitempty"` ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` } +type _OptimisticDNSOptions struct { + Enabled bool `json:"enabled,omitempty"` + Timeout badoption.Duration `json:"timeout,omitempty"` +} + +type OptimisticDNSOptions _OptimisticDNSOptions + +func (o OptimisticDNSOptions) MarshalJSON() ([]byte, error) { + if o.Timeout == 0 { + return json.Marshal(o.Enabled) + } + return json.Marshal((_OptimisticDNSOptions)(o)) +} + +func (o *OptimisticDNSOptions) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, &o.Enabled) + if err == nil { + return nil + } + return json.UnmarshalDisallowUnknownFields(bytes, (*_OptimisticDNSOptions)(o)) +} + type DNSTransportOptionsRegistry interface { CreateOptions(transportType string) (any, bool) } diff --git a/option/experimental.go b/option/experimental.go index bf0df9e78..2f00decfe 100644 --- a/option/experimental.go +++ b/option/experimental.go @@ -16,6 +16,7 @@ type CacheFileOptions struct { StoreFakeIP bool `json:"store_fakeip,omitempty"` StoreRDRC bool `json:"store_rdrc,omitempty"` RDRCTimeout badoption.Duration `json:"rdrc_timeout,omitempty"` + StoreDNS bool `json:"store_dns,omitempty"` } type ClashAPIOptions struct { diff --git a/option/outbound.go b/option/outbound.go index 6676a3e92..d8fcb8221 100644 --- a/option/outbound.go +++ b/option/outbound.go @@ -93,11 +93,12 @@ type DialerOptions struct { } type _DomainResolveOptions struct { - Server string `json:"server"` - Strategy DomainStrategy `json:"strategy,omitempty"` - DisableCache bool `json:"disable_cache,omitempty"` - RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` - ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` + Server string `json:"server"` + Strategy DomainStrategy `json:"strategy,omitempty"` + DisableCache bool `json:"disable_cache,omitempty"` + DisableOptimisticCache bool `json:"disable_optimistic_cache,omitempty"` + RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` + ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` } type DomainResolveOptions _DomainResolveOptions @@ -107,6 +108,7 @@ func (o DomainResolveOptions) MarshalJSON() ([]byte, error) { return []byte("{}"), nil } else if o.Strategy == DomainStrategy(C.DomainStrategyAsIS) && !o.DisableCache && + !o.DisableOptimisticCache && o.RewriteTTL == nil && o.ClientSubnet == nil { return json.Marshal(o.Server) diff --git a/option/rule_action.go b/option/rule_action.go index 212396b7b..c369cfeb3 100644 --- a/option/rule_action.go +++ b/option/rule_action.go @@ -201,18 +201,20 @@ func (r *RouteOptionsActionOptions) UnmarshalJSON(data []byte) error { } type DNSRouteActionOptions struct { - Server string `json:"server,omitempty"` - Strategy DomainStrategy `json:"strategy,omitempty"` - DisableCache bool `json:"disable_cache,omitempty"` - RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` - ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` + Server string `json:"server,omitempty"` + Strategy DomainStrategy `json:"strategy,omitempty"` + DisableCache bool `json:"disable_cache,omitempty"` + DisableOptimisticCache bool `json:"disable_optimistic_cache,omitempty"` + RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` + ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` } type _DNSRouteOptionsActionOptions struct { - Strategy DomainStrategy `json:"strategy,omitempty"` - DisableCache bool `json:"disable_cache,omitempty"` - RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` - ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` + Strategy DomainStrategy `json:"strategy,omitempty"` + DisableCache bool `json:"disable_cache,omitempty"` + DisableOptimisticCache bool `json:"disable_optimistic_cache,omitempty"` + RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` + ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` } type DNSRouteOptionsActionOptions _DNSRouteOptionsActionOptions @@ -321,11 +323,12 @@ type RouteActionSniff struct { } type RouteActionResolve struct { - Server string `json:"server,omitempty"` - Strategy DomainStrategy `json:"strategy,omitempty"` - DisableCache bool `json:"disable_cache,omitempty"` - RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` - ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` + Server string `json:"server,omitempty"` + Strategy DomainStrategy `json:"strategy,omitempty"` + DisableCache bool `json:"disable_cache,omitempty"` + DisableOptimisticCache bool `json:"disable_optimistic_cache,omitempty"` + RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` + ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` } type DNSRouteActionPredefined struct { diff --git a/route/network.go b/route/network.go index 03e94879b..858ea3b24 100644 --- a/route/network.go +++ b/route/network.go @@ -78,10 +78,11 @@ func NewNetworkManager(ctx context.Context, logger logger.ContextLogger, options RoutingMark: uint32(options.DefaultMark), DomainResolver: defaultDomainResolver.Server, DomainResolveOptions: adapter.DNSQueryOptions{ - Strategy: C.DomainStrategy(defaultDomainResolver.Strategy), - DisableCache: defaultDomainResolver.DisableCache, - RewriteTTL: defaultDomainResolver.RewriteTTL, - ClientSubnet: defaultDomainResolver.ClientSubnet.Build(netip.Prefix{}), + Strategy: C.DomainStrategy(defaultDomainResolver.Strategy), + DisableCache: defaultDomainResolver.DisableCache, + DisableOptimisticCache: defaultDomainResolver.DisableOptimisticCache, + RewriteTTL: defaultDomainResolver.RewriteTTL, + ClientSubnet: defaultDomainResolver.ClientSubnet.Build(netip.Prefix{}), }, NetworkStrategy: (*C.NetworkStrategy)(options.DefaultNetworkStrategy), NetworkType: common.Map(options.DefaultNetworkType, option.InterfaceType.Build), diff --git a/route/route.go b/route/route.go index f07f119db..412c3af1d 100644 --- a/route/route.go +++ b/route/route.go @@ -786,11 +786,12 @@ func (r *Router) actionResolve(ctx context.Context, metadata *adapter.InboundCon } } addresses, err := r.dns.Lookup(adapter.WithContext(ctx, metadata), metadata.Destination.Fqdn, adapter.DNSQueryOptions{ - Transport: transport, - Strategy: action.Strategy, - DisableCache: action.DisableCache, - RewriteTTL: action.RewriteTTL, - ClientSubnet: action.ClientSubnet, + Transport: transport, + Strategy: action.Strategy, + DisableCache: action.DisableCache, + DisableOptimisticCache: action.DisableOptimisticCache, + RewriteTTL: action.RewriteTTL, + ClientSubnet: action.ClientSubnet, }) if err != nil { return err diff --git a/route/rule/rule_action.go b/route/rule/rule_action.go index 2fe6ba98a..ea239b68c 100644 --- a/route/rule/rule_action.go +++ b/route/rule/rule_action.go @@ -107,11 +107,12 @@ func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action opti return sniffAction, sniffAction.build() case C.RuleActionTypeResolve: return &RuleActionResolve{ - Server: action.ResolveOptions.Server, - Strategy: C.DomainStrategy(action.ResolveOptions.Strategy), - DisableCache: action.ResolveOptions.DisableCache, - RewriteTTL: action.ResolveOptions.RewriteTTL, - ClientSubnet: action.ResolveOptions.ClientSubnet.Build(netip.Prefix{}), + Server: action.ResolveOptions.Server, + Strategy: C.DomainStrategy(action.ResolveOptions.Strategy), + DisableCache: action.ResolveOptions.DisableCache, + DisableOptimisticCache: action.ResolveOptions.DisableOptimisticCache, + RewriteTTL: action.ResolveOptions.RewriteTTL, + ClientSubnet: action.ResolveOptions.ClientSubnet.Build(netip.Prefix{}), }, nil default: panic(F.ToString("unknown rule action: ", action.Action)) @@ -126,30 +127,33 @@ func NewDNSRuleAction(logger logger.ContextLogger, action option.DNSRuleAction) return &RuleActionDNSRoute{ Server: action.RouteOptions.Server, RuleActionDNSRouteOptions: RuleActionDNSRouteOptions{ - Strategy: C.DomainStrategy(action.RouteOptions.Strategy), - DisableCache: action.RouteOptions.DisableCache, - RewriteTTL: action.RouteOptions.RewriteTTL, - ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptions.ClientSubnet)), + Strategy: C.DomainStrategy(action.RouteOptions.Strategy), + DisableCache: action.RouteOptions.DisableCache, + DisableOptimisticCache: action.RouteOptions.DisableOptimisticCache, + RewriteTTL: action.RouteOptions.RewriteTTL, + ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptions.ClientSubnet)), }, } case C.RuleActionTypeEvaluate: return &RuleActionEvaluate{ Server: action.RouteOptions.Server, RuleActionDNSRouteOptions: RuleActionDNSRouteOptions{ - Strategy: C.DomainStrategy(action.RouteOptions.Strategy), - DisableCache: action.RouteOptions.DisableCache, - RewriteTTL: action.RouteOptions.RewriteTTL, - ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptions.ClientSubnet)), + Strategy: C.DomainStrategy(action.RouteOptions.Strategy), + DisableCache: action.RouteOptions.DisableCache, + DisableOptimisticCache: action.RouteOptions.DisableOptimisticCache, + RewriteTTL: action.RouteOptions.RewriteTTL, + ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptions.ClientSubnet)), }, } case C.RuleActionTypeRespond: return &RuleActionRespond{} case C.RuleActionTypeRouteOptions: return &RuleActionDNSRouteOptions{ - Strategy: C.DomainStrategy(action.RouteOptionsOptions.Strategy), - DisableCache: action.RouteOptionsOptions.DisableCache, - RewriteTTL: action.RouteOptionsOptions.RewriteTTL, - ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptionsOptions.ClientSubnet)), + Strategy: C.DomainStrategy(action.RouteOptionsOptions.Strategy), + DisableCache: action.RouteOptionsOptions.DisableCache, + DisableOptimisticCache: action.RouteOptionsOptions.DisableOptimisticCache, + RewriteTTL: action.RouteOptionsOptions.RewriteTTL, + ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptionsOptions.ClientSubnet)), } case C.RuleActionTypeReject: return &RuleActionReject{ @@ -310,6 +314,9 @@ func formatDNSRouteAction(action string, server string, options RuleActionDNSRou if options.DisableCache { descriptions = append(descriptions, "disable-cache") } + if options.DisableOptimisticCache { + descriptions = append(descriptions, "disable-optimistic-cache") + } if options.RewriteTTL != nil { descriptions = append(descriptions, F.ToString("rewrite-ttl=", *options.RewriteTTL)) } @@ -320,10 +327,11 @@ func formatDNSRouteAction(action string, server string, options RuleActionDNSRou } type RuleActionDNSRouteOptions struct { - Strategy C.DomainStrategy - DisableCache bool - RewriteTTL *uint32 - ClientSubnet netip.Prefix + Strategy C.DomainStrategy + DisableCache bool + DisableOptimisticCache bool + RewriteTTL *uint32 + ClientSubnet netip.Prefix } func (r *RuleActionDNSRouteOptions) Type() string { @@ -335,6 +343,9 @@ func (r *RuleActionDNSRouteOptions) String() string { if r.DisableCache { descriptions = append(descriptions, "disable-cache") } + if r.DisableOptimisticCache { + descriptions = append(descriptions, "disable-optimistic-cache") + } if r.RewriteTTL != nil { descriptions = append(descriptions, F.ToString("rewrite-ttl=", *r.RewriteTTL)) } @@ -510,11 +521,12 @@ func (r *RuleActionSniff) String() string { } type RuleActionResolve struct { - Server string - Strategy C.DomainStrategy - DisableCache bool - RewriteTTL *uint32 - ClientSubnet netip.Prefix + Server string + Strategy C.DomainStrategy + DisableCache bool + DisableOptimisticCache bool + RewriteTTL *uint32 + ClientSubnet netip.Prefix } func (r *RuleActionResolve) Type() string { @@ -532,6 +544,9 @@ func (r *RuleActionResolve) String() string { if r.DisableCache { options = append(options, "disable_cache") } + if r.DisableOptimisticCache { + options = append(options, "disable_optimistic_cache") + } if r.RewriteTTL != nil { options = append(options, F.ToString("rewrite_ttl=", *r.RewriteTTL)) }