Add optimistic DNS cache

This commit is contained in:
世界
2026-08-30 17:41:39 +08:00
parent 5cce5454e1
commit c322ad5d33
30 changed files with 1215 additions and 363 deletions
+21 -11
View File
@@ -3,6 +3,7 @@ package adapter
import ( import (
"context" "context"
"net/netip" "net/netip"
"time"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
@@ -31,12 +32,13 @@ type DNSClient interface {
} }
type DNSQueryOptions struct { type DNSQueryOptions struct {
Transport DNSTransport Transport DNSTransport
Strategy C.DomainStrategy Strategy C.DomainStrategy
LookupStrategy C.DomainStrategy LookupStrategy C.DomainStrategy
DisableCache bool DisableCache bool
RewriteTTL *uint32 DisableOptimisticCache bool
ClientSubnet netip.Prefix RewriteTTL *uint32
ClientSubnet netip.Prefix
} }
func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (*DNSQueryOptions, error) { 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 nil, E.New("domain resolver not found: " + options.Server)
} }
return &DNSQueryOptions{ return &DNSQueryOptions{
Transport: transport, Transport: transport,
Strategy: C.DomainStrategy(options.Strategy), Strategy: C.DomainStrategy(options.Strategy),
DisableCache: options.DisableCache, DisableCache: options.DisableCache,
RewriteTTL: options.RewriteTTL, DisableOptimisticCache: options.DisableOptimisticCache,
ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}), RewriteTTL: options.RewriteTTL,
ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
}, nil }, nil
} }
@@ -63,6 +66,13 @@ type RDRCStore interface {
SaveRDRCAsync(transportName string, qName string, qType uint16, logger logger.Logger) 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 { type DNSTransport interface {
Lifecycle Lifecycle
Type() string Type() string
+6
View File
@@ -48,6 +48,12 @@ type CacheFile interface {
StoreRDRC() bool StoreRDRC() bool
RDRCStore RDRCStore
StoreDNS() bool
DNSCacheStore
SetDisableExpire(disableExpire bool)
SetOptimisticTimeout(timeout time.Duration)
LoadMode() string LoadMode() string
StoreMode(mode string) error StoreMode(mode string) error
LoadSelected(group string) string LoadSelected(group string) string
+5 -2
View File
@@ -196,7 +196,10 @@ func New(options Options) (*Box, error) {
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager) service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
service.MustRegister[adapter.ServiceManager](ctx, serviceManager) service.MustRegister[adapter.ServiceManager](ctx, serviceManager)
service.MustRegister[adapter.CertificateProviderManager](ctx, certificateProviderManager) 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.DNSRouter](ctx, dnsRouter)
service.MustRegister[adapter.DNSRuleSetUpdateValidator](ctx, dnsRouter) service.MustRegister[adapter.DNSRuleSetUpdateValidator](ctx, dnsRouter)
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions, dnsOptions) networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions, dnsOptions)
@@ -372,7 +375,7 @@ func New(options Options) (*Box, error) {
} }
} }
if needCacheFile { 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) service.MustRegister[adapter.CacheFile](ctx, cacheFile)
internalServices = append(internalServices, cacheFile) internalServices = append(internalServices, cacheFile)
} }
+6 -5
View File
@@ -87,11 +87,12 @@ func NewWithOptions(options Options) (N.Dialer, error) {
} }
server = dialOptions.DomainResolver.Server server = dialOptions.DomainResolver.Server
dnsQueryOptions = adapter.DNSQueryOptions{ dnsQueryOptions = adapter.DNSQueryOptions{
Transport: transport, Transport: transport,
Strategy: strategy, Strategy: strategy,
DisableCache: dialOptions.DomainResolver.DisableCache, DisableCache: dialOptions.DomainResolver.DisableCache,
RewriteTTL: dialOptions.DomainResolver.RewriteTTL, DisableOptimisticCache: dialOptions.DomainResolver.DisableOptimisticCache,
ClientSubnet: dialOptions.DomainResolver.ClientSubnet.Build(netip.Prefix{}), RewriteTTL: dialOptions.DomainResolver.RewriteTTL,
ClientSubnet: dialOptions.DomainResolver.ClientSubnet.Build(netip.Prefix{}),
} }
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay) resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
} else if options.DirectResolver { } else if options.DirectResolver {
+298 -270
View File
@@ -30,56 +30,63 @@ var (
var _ adapter.DNSClient = (*Client)(nil) var _ adapter.DNSClient = (*Client)(nil)
type Client struct { type Client struct {
timeout time.Duration ctx context.Context
disableCache bool timeout time.Duration
disableExpire bool disableCache bool
independentCache bool disableExpire bool
clientSubnet netip.Prefix optimisticTimeout time.Duration
rdrc adapter.RDRCStore cacheCapacity uint32
initRDRCFunc func() adapter.RDRCStore clientSubnet netip.Prefix
logger logger.ContextLogger rdrc adapter.RDRCStore
cache freelru.Cache[dns.Question, *dns.Msg] initRDRCFunc func() adapter.RDRCStore
cacheLock compatible.Map[transportCacheKey, chan struct{}] dnsCache adapter.DNSCacheStore
transportCache freelru.Cache[transportCacheKey, *dns.Msg] initDNSCacheFunc func() adapter.DNSCacheStore
transportCacheLock compatible.Map[transportCacheKey, chan struct{}] logger logger.ContextLogger
cache freelru.Cache[dnsCacheKey, *dns.Msg]
cacheLock compatible.Map[dnsCacheKey, chan struct{}]
backgroundRefresh compatible.Map[dnsCacheKey, struct{}]
} }
type ClientOptions struct { type ClientOptions struct {
Timeout time.Duration Context context.Context
DisableCache bool Timeout time.Duration
DisableExpire bool DisableCache bool
IndependentCache bool DisableExpire bool
CacheCapacity uint32 OptimisticTimeout time.Duration
ClientSubnet netip.Prefix CacheCapacity uint32
RDRC func() adapter.RDRCStore ClientSubnet netip.Prefix
Logger logger.ContextLogger RDRC func() adapter.RDRCStore
DNSCache func() adapter.DNSCacheStore
Logger logger.ContextLogger
} }
func NewClient(options ClientOptions) *Client { func NewClient(options ClientOptions) *Client {
cacheCapacity := options.CacheCapacity
if cacheCapacity < 1024 {
cacheCapacity = 1024
}
client := &Client{ client := &Client{
timeout: options.Timeout, ctx: options.Context,
disableCache: options.DisableCache, timeout: options.Timeout,
disableExpire: options.DisableExpire, disableCache: options.DisableCache,
independentCache: options.IndependentCache, disableExpire: options.DisableExpire,
clientSubnet: options.ClientSubnet, optimisticTimeout: options.OptimisticTimeout,
initRDRCFunc: options.RDRC, cacheCapacity: cacheCapacity,
logger: options.Logger, clientSubnet: options.ClientSubnet,
initRDRCFunc: options.RDRC,
initDNSCacheFunc: options.DNSCache,
logger: options.Logger,
} }
if client.timeout == 0 { if client.timeout == 0 {
client.timeout = C.DNSTimeout client.timeout = C.DNSTimeout
} }
cacheCapacity := max(options.CacheCapacity, 1024) if !client.disableCache && client.initDNSCacheFunc == nil {
if !client.disableCache { client.initializeMemoryCache()
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))
}
} }
return client return client
} }
type transportCacheKey struct { type dnsCacheKey struct {
dns.Question dns.Question
transportTag string transportTag string
} }
@@ -88,6 +95,19 @@ func (c *Client) Start() {
if c.initRDRCFunc != nil { if c.initRDRCFunc != nil {
c.rdrc = c.initRDRCFunc() 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) { func extractNegativeTTL(response *dns.Msg) (uint32, bool) {
@@ -104,6 +124,37 @@ func extractNegativeTTL(response *dns.Msg) (uint32, bool) {
return 0, false 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) { 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 len(message.Question) == 0 {
if c.logger != nil { 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 return FixedResponseStatus(message, dns.RcodeSuccess), nil
} }
clientSubnet := options.ClientSubnet message = c.prepareExchangeMessage(message, options)
if !clientSubnet.IsValid() {
clientSubnet = c.clientSubnet
}
if clientSubnet.IsValid() {
message = SetClientSubnet(message, clientSubnet)
}
isSimpleRequest := len(message.Question) == 1 && isSimpleRequest := len(message.Question) == 1 &&
len(message.Ns) == 0 && len(message.Ns) == 0 &&
@@ -136,41 +181,32 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
!options.ClientSubnet.IsValid() !options.ClientSubnet.IsValid()
disableCache := !isSimpleRequest || c.disableCache || options.DisableCache disableCache := !isSimpleRequest || c.disableCache || options.DisableCache
if !disableCache { if !disableCache {
cacheKey := transportCacheKey{Question: question, transportTag: transport.Tag()} cacheKey := dnsCacheKey{Question: question, transportTag: transport.Tag()}
if c.cache != nil { cond, loaded := c.cacheLock.LoadOrStore(cacheKey, make(chan struct{}))
cond, loaded := c.cacheLock.LoadOrStore(cacheKey, make(chan struct{})) if loaded {
if loaded { select {
select { case <-cond:
case <-cond: case <-ctx.Done():
case <-ctx.Done(): return nil, ctx.Err()
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)
}()
} }
} 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 { if response != nil {
logCachedResponse(c.logger, ctx, response, ttl) if isStale && !options.DisableOptimisticCache {
response.Id = message.Id c.backgroundRefreshDNS(transport, question, message.Copy(), options, responseChecker)
return response, nil 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 return nil, ErrResponseRejectedCached
} }
} }
ctx, cancel := context.WithTimeout(ctx, c.timeout) response, err := c.exchangeToTransport(ctx, transport, message)
response, err := transport.Exchange(ctx, message)
cancel()
if err != nil { if err != nil {
var rcodeError RcodeError return nil, err
if errors.As(err, &rcodeError) {
response = FixedResponseStatus(message, int(rcodeError))
} else {
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) disableCache = disableCache || (response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError)
if responseChecker != nil { if responseChecker != nil {
var rejected bool var rejected bool
@@ -248,54 +242,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
return response, ErrResponseRejected return response, ErrResponseRejected
} }
} }
if question.Qtype == dns.TypeHTTPS { timeToLive := applyResponseOptions(question, response, options)
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
}
}
if !disableCache { if !disableCache {
c.storeCache(transport, question, response, timeToLive) 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() { func (c *Client) ClearCache() {
if c.cache != nil { if c.cache != nil {
c.cache.Purge() 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 { if timeToLive == 0 {
return 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.disableExpire {
if !c.independentCache { c.cache.Add(key, message.Copy())
c.cache.Add(question, message.Copy())
} else {
c.transportCache.Add(transportCacheKey{
Question: question,
transportTag: transport.Tag(),
}, message.Copy())
}
} else { } else {
if !c.independentCache { c.cache.AddWithLifetime(key, message.Copy(), time.Second*time.Duration(timeToLive))
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))
}
} }
} }
@@ -406,19 +355,19 @@ func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTran
Qtype: qType, Qtype: qType,
Qclass: dns.ClassINET, 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{ message := dns.Msg{
MsgHdr: dns.MsgHdr{ MsgHdr: dns.MsgHdr{
RecursionDesired: true, RecursionDesired: true,
}, },
Question: []dns.Question{question}, 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) response, err := c.Exchange(ctx, transport, &message, options, responseChecker)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -429,95 +378,174 @@ func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTran
return MessageToAddresses(response), nil return MessageToAddresses(response), nil
} }
func (c *Client) questionCache(question dns.Question, transport adapter.DNSTransport) ([]netip.Addr, error) { 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) {
response, _ := c.loadResponse(question, transport) question := message.Question[0]
response, _, isStale := c.loadResponse(question, transport)
if response == nil { if response == nil {
return nil, ErrNotCached 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 { if response.Rcode != dns.RcodeSuccess {
return nil, RcodeError(response.Rcode) return nil, RcodeError(response.Rcode)
} }
return MessageToAddresses(response), nil return MessageToAddresses(response), nil
} }
func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransport) (*dns.Msg, int) { func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransport) (*dns.Msg, int, bool) {
var ( if c.dnsCache != nil {
response *dns.Msg return c.loadPersistentResponse(question, transport)
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
} }
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 { func MessageToAddresses(response *dns.Msg) []netip.Addr {
+13
View File
@@ -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) { func logExchangedResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg, ttl uint32) {
if logger == nil || len(response.Question) == 0 { if logger == nil || len(response.Question) == 0 {
return return
+55 -9
View File
@@ -51,7 +51,7 @@ type Router struct {
closing bool 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{ router := &Router{
ctx: ctx, ctx: ctx,
logger: logFactory.NewLogger("dns"), 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)), rules: make([]adapter.DNSRule, 0, len(options.Rules)),
defaultDomainStrategy: C.DomainStrategy(options.Strategy), 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{ router.client = NewClient(ClientOptions{
DisableCache: options.DNSClientOptions.DisableCache, Context: ctx,
DisableExpire: options.DNSClientOptions.DisableExpire, DisableCache: options.DNSClientOptions.DisableCache,
IndependentCache: options.DNSClientOptions.IndependentCache, DisableExpire: options.DNSClientOptions.DisableExpire,
CacheCapacity: options.DNSClientOptions.CacheCapacity, OptimisticTimeout: optimisticTimeout,
ClientSubnet: options.DNSClientOptions.ClientSubnet.Build(netip.Prefix{}), CacheCapacity: options.DNSClientOptions.CacheCapacity,
ClientSubnet: options.DNSClientOptions.ClientSubnet.Build(netip.Prefix{}),
RDRC: func() adapter.RDRCStore { RDRC: func() adapter.RDRCStore {
cacheFile := service.FromContext[adapter.CacheFile](ctx) cacheFile := service.FromContext[adapter.CacheFile](ctx)
if cacheFile == nil { if cacheFile == nil {
@@ -77,12 +95,24 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOp
} }
return cacheFile 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, Logger: router.logger,
}) })
if options.ReverseMapping { if options.ReverseMapping {
router.dnsReverseMapping = common.Must1(freelru.NewSharded[netip.Addr, string](1024, maphash.NewHasher[netip.Addr]().Hash32)) 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 { func (r *Router) Initialize(rules []option.DNSRule) error {
@@ -319,6 +349,9 @@ func (r *Router) applyDNSRouteOptions(options *adapter.DNSQueryOptions, routeOpt
if routeOptions.DisableCache { if routeOptions.DisableCache {
options.DisableCache = true options.DisableCache = true
} }
if routeOptions.DisableOptimisticCache {
options.DisableOptimisticCache = true
}
if routeOptions.RewriteTTL != nil { if routeOptions.RewriteTTL != nil {
options.RewriteTTL = routeOptions.RewriteTTL options.RewriteTTL = routeOptions.RewriteTTL
} }
@@ -907,7 +940,9 @@ func dnsRuleModeRequirementsInRule(router adapter.Router, rule option.DNSRule, m
return dnsRuleModeRequirementsInDefaultRule(router, rule.DefaultOptions, metadataOverrides) return dnsRuleModeRequirementsInDefaultRule(router, rule.DefaultOptions, metadataOverrides)
case C.RuleTypeLogical: case C.RuleTypeLogical:
flags := dnsRuleModeFlags{ 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), neededFromStrategy: dnsRuleActionHasStrategy(rule.LogicalOptions.DNSRuleAction),
} }
flags.needed = flags.neededFromStrategy 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) { func dnsRuleModeRequirementsInDefaultRule(router adapter.Router, rule option.DefaultDNSRule, metadataOverrides map[string]adapter.RuleSetMetadata) (dnsRuleModeFlags, error) {
flags := dnsRuleModeFlags{ flags := dnsRuleModeFlags{
disabled: defaultRuleDisablesLegacyDNSMode(rule), disabled: defaultRuleDisablesLegacyDNSMode(rule) || dnsRuleActionDisablesLegacyDNSMode(rule.DNSRuleAction),
neededFromStrategy: dnsRuleActionHasStrategy(rule.DNSRuleAction), neededFromStrategy: dnsRuleActionHasStrategy(rule.DNSRuleAction),
} }
flags.needed = defaultRuleNeedsLegacyDNSModeFromAddressFilter(rule) || flags.neededFromStrategy flags.needed = defaultRuleNeedsLegacyDNSModeFromAddressFilter(rule) || flags.neededFromStrategy
@@ -1063,6 +1098,17 @@ func validateLegacyDNSModeDisabledDefaultRule(rule option.DefaultDNSRule) (bool,
return rule.MatchResponse || rule.Action == C.RuleActionTypeRespond, nil 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 { func dnsRuleActionHasStrategy(action option.DNSRuleAction) bool {
switch action.Action { switch action.Action {
case "", C.RuleActionTypeRoute, C.RuleActionTypeEvaluate: case "", C.RuleActionTypeRoute, C.RuleActionTypeEvaluate:
+42
View File
@@ -2,6 +2,11 @@
icon: material/alert-decagram 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" !!! quote "Changes in sing-box 1.12.0"
:material-decagram: [servers](#servers) :material-decagram: [servers](#servers)
@@ -25,6 +30,7 @@ icon: material/alert-decagram
"disable_expire": false, "disable_expire": false,
"independent_cache": false, "independent_cache": false,
"cache_capacity": 0, "cache_capacity": 0,
"optimistic": false, // or {}
"reverse_mapping": false, "reverse_mapping": false,
"client_subnet": "", "client_subnet": "",
"fakeip": {} "fakeip": {}
@@ -57,12 +63,20 @@ One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
Disable dns cache. Disable dns cache.
Conflict with `optimistic`.
#### disable_expire #### disable_expire
Disable dns cache expire. Disable dns cache expire.
Conflict with `optimistic`.
#### independent_cache #### 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. Make each DNS server's cache independent for special purposes. If enabled, will slightly degrade performance.
#### cache_capacity #### cache_capacity
@@ -73,6 +87,34 @@ LRU cache capacity.
Value less than 1024 will be ignored. 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 #### reverse_mapping
Stores a reverse mapping of IP addresses after responding to a DNS query in order to provide domain names when routing. Stores a reverse mapping of IP addresses after responding to a DNS query in order to provide domain names when routing.
+42
View File
@@ -2,6 +2,11 @@
icon: material/alert-decagram 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 中的更改" !!! quote "sing-box 1.12.0 中的更改"
:material-decagram: [servers](#servers) :material-decagram: [servers](#servers)
@@ -25,6 +30,7 @@ icon: material/alert-decagram
"disable_expire": false, "disable_expire": false,
"independent_cache": false, "independent_cache": false,
"cache_capacity": 0, "cache_capacity": 0,
"optimistic": false, // or {}
"reverse_mapping": false, "reverse_mapping": false,
"client_subnet": "", "client_subnet": "",
"fakeip": {} "fakeip": {}
@@ -56,12 +62,20 @@ icon: material/alert-decagram
禁用 DNS 缓存。 禁用 DNS 缓存。
`optimistic` 冲突。
#### disable_expire #### disable_expire
禁用 DNS 缓存过期。 禁用 DNS 缓存过期。
`optimistic` 冲突。
#### independent_cache #### 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 服务器的缓存独立,以满足特殊目的。如果启用,将轻微降低性能。 使每个 DNS 服务器的缓存独立,以满足特殊目的。如果启用,将轻微降低性能。
#### cache_capacity #### cache_capacity
@@ -72,6 +86,34 @@ LRU 缓存容量。
小于 1024 的值将被忽略。 小于 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 #### reverse_mapping
在响应 DNS 查询后存储 IP 地址的反向映射以为路由目的提供域名。 在响应 DNS 查询后存储 IP 地址的反向映射以为路由目的提供域名。
+17 -1
View File
@@ -6,7 +6,8 @@ icon: material/new-box
:material-delete-clock: [strategy](#strategy) :material-delete-clock: [strategy](#strategy)
:material-plus: [evaluate](#evaluate) :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" !!! quote "Changes in sing-box 1.12.0"
@@ -23,6 +24,7 @@ icon: material/new-box
"server": "", "server": "",
"strategy": "", "strategy": "",
"disable_cache": false, "disable_cache": false,
"disable_optimistic_cache": false,
"rewrite_ttl": null, "rewrite_ttl": null,
"client_subnet": 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 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
Rewrite TTL in DNS responses. Rewrite TTL in DNS responses.
@@ -73,6 +81,7 @@ Will override `dns.client_subnet`.
"action": "evaluate", "action": "evaluate",
"server": "", "server": "",
"disable_cache": false, "disable_cache": false,
"disable_optimistic_cache": false,
"rewrite_ttl": null, "rewrite_ttl": null,
"client_subnet": null "client_subnet": null
} }
@@ -97,6 +106,12 @@ Tag of target server.
Disable cache and save cache in this query. 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
Rewrite TTL in DNS responses. 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", "action": "route-options",
"disable_cache": false, "disable_cache": false,
"disable_optimistic_cache": false,
"rewrite_ttl": null, "rewrite_ttl": null,
"client_subnet": null "client_subnet": null
} }
+17 -1
View File
@@ -6,7 +6,8 @@ icon: material/new-box
:material-delete-clock: [strategy](#strategy) :material-delete-clock: [strategy](#strategy)
:material-plus: [evaluate](#evaluate) :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 中的更改" !!! quote "sing-box 1.12.0 中的更改"
@@ -23,6 +24,7 @@ icon: material/new-box
"server": "", "server": "",
"strategy": "", "strategy": "",
"disable_cache": false, "disable_cache": false,
"disable_optimistic_cache": false,
"rewrite_ttl": null, "rewrite_ttl": null,
"client_subnet": null "client_subnet": null
} }
@@ -52,6 +54,12 @@ icon: material/new-box
在此查询中禁用缓存。 在此查询中禁用缓存。
#### disable_optimistic_cache
!!! question "自 sing-box 1.14.0 起"
在此查询中禁用乐观 DNS 缓存。
#### rewrite_ttl #### rewrite_ttl
重写 DNS 回应中的 TTL。 重写 DNS 回应中的 TTL。
@@ -73,6 +81,7 @@ icon: material/new-box
"action": "evaluate", "action": "evaluate",
"server": "", "server": "",
"disable_cache": false, "disable_cache": false,
"disable_optimistic_cache": false,
"rewrite_ttl": null, "rewrite_ttl": null,
"client_subnet": null "client_subnet": null
} }
@@ -95,6 +104,12 @@ icon: material/new-box
在此查询中禁用缓存。 在此查询中禁用缓存。
#### disable_optimistic_cache
!!! question "自 sing-box 1.14.0 起"
在此查询中禁用乐观 DNS 缓存。
#### rewrite_ttl #### rewrite_ttl
重写 DNS 回应中的 TTL。 重写 DNS 回应中的 TTL。
@@ -129,6 +144,7 @@ icon: material/new-box
{ {
"action": "route-options", "action": "route-options",
"disable_cache": false, "disable_cache": false,
"disable_optimistic_cache": false,
"rewrite_ttl": null, "rewrite_ttl": null,
"client_subnet": null "client_subnet": null
} }
+17 -1
View File
@@ -1,5 +1,10 @@
!!! question "Since sing-box 1.8.0" !!! 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" !!! quote "Changes in sing-box 1.9.0"
:material-plus: [store_rdrc](#store_rdrc) :material-plus: [store_rdrc](#store_rdrc)
@@ -14,7 +19,8 @@
"cache_id": "", "cache_id": "",
"store_fakeip": false, "store_fakeip": false,
"store_rdrc": false, "store_rdrc": false,
"rdrc_timeout": "" "rdrc_timeout": "",
"store_dns": false
} }
``` ```
@@ -42,6 +48,10 @@ Store fakeip in the cache file
#### store_rdrc #### 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 Store rejected DNS response cache in the cache file
The check results of [Legacy Address Filter Fields](/configuration/dns/rule/#legacy-address-filter-fields) 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. Timeout of rejected DNS response cache.
`7d` is used by default. `7d` is used by default.
#### store_dns
!!! question "Since sing-box 1.14.0"
Store DNS cache in the cache file.
@@ -1,5 +1,10 @@
!!! question "自 sing-box 1.8.0 起" !!! 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 中的更改" !!! quote "sing-box 1.9.0 中的更改"
:material-plus: [store_rdrc](#store_rdrc) :material-plus: [store_rdrc](#store_rdrc)
@@ -14,7 +19,8 @@
"cache_id": "", "cache_id": "",
"store_fakeip": false, "store_fakeip": false,
"store_rdrc": false, "store_rdrc": false,
"rdrc_timeout": "" "rdrc_timeout": "",
"store_dns": false
} }
``` ```
@@ -40,6 +46,10 @@
#### store_rdrc #### 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 响应缓存存储在缓存文件中。 将拒绝的 DNS 响应缓存存储在缓存文件中。
[旧版地址筛选字段](/zh/configuration/dns/rule/#旧版地址筛选字段) 的检查结果将被缓存至过期。 [旧版地址筛选字段](/zh/configuration/dns/rule/#旧版地址筛选字段) 的检查结果将被缓存至过期。
@@ -49,3 +59,9 @@
拒绝的 DNS 响应缓存超时。 拒绝的 DNS 响应缓存超时。
默认使用 `7d` 默认使用 `7d`
#### store_dns
!!! question "自 sing-box 1.14.0 起"
将 DNS 缓存存储在缓存文件中。
+11
View File
@@ -7,6 +7,10 @@ icon: material/new-box
:material-plus: [bypass](#bypass) :material-plus: [bypass](#bypass)
:material-alert: [reject](#reject) :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" !!! quote "Changes in sing-box 1.12.0"
:material-plus: [tls_fragment](#tls_fragment) :material-plus: [tls_fragment](#tls_fragment)
@@ -279,6 +283,7 @@ Timeout for sniffing.
"server": "", "server": "",
"strategy": "", "strategy": "",
"disable_cache": false, "disable_cache": false,
"disable_optimistic_cache": false,
"rewrite_ttl": null, "rewrite_ttl": null,
"client_subnet": 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 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
!!! question "Since sing-box 1.12.0" !!! question "Since sing-box 1.12.0"
@@ -7,6 +7,10 @@ icon: material/new-box
:material-plus: [bypass](#bypass) :material-plus: [bypass](#bypass)
:material-alert: [reject](#reject) :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 中的更改" !!! quote "sing-box 1.12.0 中的更改"
:material-plus: [tls_fragment](#tls_fragment) :material-plus: [tls_fragment](#tls_fragment)
@@ -268,6 +272,7 @@ UDP 连接超时时间。
"server": "", "server": "",
"strategy": "", "strategy": "",
"disable_cache": false, "disable_cache": false,
"disable_optimistic_cache": false,
"rewrite_ttl": null, "rewrite_ttl": null,
"client_subnet": 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 #### rewrite_ttl
!!! question "自 sing-box 1.12.0 起" !!! question "自 sing-box 1.12.0 起"
+15
View File
@@ -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. 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 in DNS rules
Legacy Address Filter Fields (`ip_cidr`, `ip_is_private` without `match_response`) Legacy Address Filter Fields (`ip_cidr`, `ip_is_private` without `match_response`)
+15
View File
@@ -27,6 +27,21 @@ TLS 中的内联 ACME 选项(`tls.acme`)已废弃,
旧字段将在 sing-box 1.16.0 中被移除。 旧字段将在 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 规则) #### 旧版地址筛选字段 (DNS 规则)
旧版地址筛选字段(不使用 `match_response``ip_cidr``ip_is_private`)已废弃, 旧版地址筛选字段(不使用 `match_response``ip_cidr``ip_is_private`)已废弃,
+62
View File
@@ -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 ### ip_version and query_type behavior changes in DNS rules
In sing-box 1.14.0, the behavior of In sing-box 1.14.0, the behavior of
+62
View File
@@ -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 行为更改 ### DNS 规则中的 ip_version 和 query_type 行为更改
在 sing-box 1.14.0 中,DNS 规则中的 在 sing-box 1.14.0 中,DNS 规则中的
+62 -6
View File
@@ -12,9 +12,11 @@ import (
"github.com/sagernet/bbolt" "github.com/sagernet/bbolt"
bboltErrors "github.com/sagernet/bbolt/errors" bboltErrors "github.com/sagernet/bbolt/errors"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/experimental/deprecated"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"github.com/sagernet/sing/service/filemanager" "github.com/sagernet/sing/service/filemanager"
) )
@@ -30,6 +32,7 @@ var (
string(bucketMode), string(bucketMode),
string(bucketRuleSet), string(bucketRuleSet),
string(bucketRDRC), string(bucketRDRC),
string(bucketDNSCache),
} }
cacheIDDefault = []byte("default") cacheIDDefault = []byte("default")
@@ -39,11 +42,15 @@ var _ adapter.CacheFile = (*CacheFile)(nil)
type CacheFile struct { type CacheFile struct {
ctx context.Context ctx context.Context
logger logger.Logger
path string path string
cacheID []byte cacheID []byte
storeFakeIP bool storeFakeIP bool
storeRDRC bool storeRDRC bool
storeDNS bool
disableExpire bool
rdrcTimeout time.Duration rdrcTimeout time.Duration
optimisticTimeout time.Duration
DB *bbolt.DB DB *bbolt.DB
resetAccess sync.Mutex resetAccess sync.Mutex
saveMetadataAccess sync.Mutex saveMetadataAccess sync.Mutex
@@ -54,16 +61,25 @@ type CacheFile struct {
saveAddress4 map[string]netip.Addr saveAddress4 map[string]netip.Addr
saveAddress6 map[string]netip.Addr saveAddress6 map[string]netip.Addr
saveRDRCAccess sync.RWMutex saveRDRCAccess sync.RWMutex
saveRDRC map[saveRDRCCacheKey]bool saveRDRC map[saveCacheKey]bool
saveDNSCacheAccess sync.RWMutex
saveDNSCache map[saveCacheKey]saveDNSCacheEntry
} }
type saveRDRCCacheKey struct { type saveCacheKey struct {
TransportName string TransportName string
QuestionName string QuestionName string
QType uint16 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 var path string
if options.Path != "" { if options.Path != "" {
path = options.Path path = options.Path
@@ -74,6 +90,9 @@ func New(ctx context.Context, options option.CacheFileOptions) *CacheFile {
if options.CacheID != "" { if options.CacheID != "" {
cacheIDBytes = append([]byte{0}, []byte(options.CacheID)...) cacheIDBytes = append([]byte{0}, []byte(options.CacheID)...)
} }
if options.StoreRDRC {
deprecated.Report(ctx, deprecated.OptionStoreRDRC)
}
var rdrcTimeout time.Duration var rdrcTimeout time.Duration
if options.StoreRDRC { if options.StoreRDRC {
if options.RDRCTimeout > 0 { if options.RDRCTimeout > 0 {
@@ -84,15 +103,18 @@ func New(ctx context.Context, options option.CacheFileOptions) *CacheFile {
} }
return &CacheFile{ return &CacheFile{
ctx: ctx, ctx: ctx,
logger: logger,
path: filemanager.BasePath(ctx, path), path: filemanager.BasePath(ctx, path),
cacheID: cacheIDBytes, cacheID: cacheIDBytes,
storeFakeIP: options.StoreFakeIP, storeFakeIP: options.StoreFakeIP,
storeRDRC: options.StoreRDRC, storeRDRC: options.StoreRDRC,
storeDNS: options.StoreDNS,
rdrcTimeout: rdrcTimeout, rdrcTimeout: rdrcTimeout,
saveDomain: make(map[netip.Addr]string), saveDomain: make(map[netip.Addr]string),
saveAddress4: make(map[string]netip.Addr), saveAddress4: make(map[string]netip.Addr),
saveAddress6: 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),
} }
} }
@@ -104,10 +126,44 @@ func (c *CacheFile) Dependencies() []string {
return nil 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 { func (c *CacheFile) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateInitialize { switch stage {
return nil 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 const fileMode = 0o666
options := bbolt.Options{Timeout: time.Second} options := bbolt.Options{Timeout: time.Second}
var ( var (
+299
View File
@@ -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)
}
}
+2 -2
View File
@@ -21,7 +21,7 @@ func (c *CacheFile) RDRCTimeout() time.Duration {
func (c *CacheFile) LoadRDRC(transportName string, qName string, qType uint16) (rejected bool) { func (c *CacheFile) LoadRDRC(transportName string, qName string, qType uint16) (rejected bool) {
c.saveRDRCAccess.RLock() c.saveRDRCAccess.RLock()
rejected, cached := c.saveRDRC[saveRDRCCacheKey{transportName, qName, qType}] rejected, cached := c.saveRDRC[saveCacheKey{transportName, qName, qType}]
c.saveRDRCAccess.RUnlock() c.saveRDRCAccess.RUnlock()
if cached { if cached {
return 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) { 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.saveRDRCAccess.Lock()
c.saveRDRC[saveKey] = true c.saveRDRC[saveKey] = true
c.saveRDRCAccess.Unlock() c.saveRDRCAccess.Unlock()
+20
View File
@@ -120,6 +120,24 @@ var OptionLegacyDNSRuleStrategy = Note{
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-dns-rule-action-strategy-to-rule-items", 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{ var Options = []Note{
OptionOutboundDNSRuleItem, OptionOutboundDNSRuleItem,
OptionMissingDomainResolver, OptionMissingDomainResolver,
@@ -128,4 +146,6 @@ var Options = []Note{
OptionRuleSetIPCIDRAcceptEmpty, OptionRuleSetIPCIDRAcceptEmpty,
OptionLegacyDNSAddressFilter, OptionLegacyDNSAddressFilter,
OptionLegacyDNSRuleStrategy, OptionLegacyDNSRuleStrategy,
OptionIndependentDNSCache,
OptionStoreRDRC,
} }
+23
View File
@@ -52,9 +52,32 @@ type DNSClientOptions struct {
DisableExpire bool `json:"disable_expire,omitempty"` DisableExpire bool `json:"disable_expire,omitempty"`
IndependentCache bool `json:"independent_cache,omitempty"` IndependentCache bool `json:"independent_cache,omitempty"`
CacheCapacity uint32 `json:"cache_capacity,omitempty"` CacheCapacity uint32 `json:"cache_capacity,omitempty"`
Optimistic *OptimisticDNSOptions `json:"optimistic,omitempty"`
ClientSubnet *badoption.Prefixable `json:"client_subnet,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 { type DNSTransportOptionsRegistry interface {
CreateOptions(transportType string) (any, bool) CreateOptions(transportType string) (any, bool)
} }
+1
View File
@@ -16,6 +16,7 @@ type CacheFileOptions struct {
StoreFakeIP bool `json:"store_fakeip,omitempty"` StoreFakeIP bool `json:"store_fakeip,omitempty"`
StoreRDRC bool `json:"store_rdrc,omitempty"` StoreRDRC bool `json:"store_rdrc,omitempty"`
RDRCTimeout badoption.Duration `json:"rdrc_timeout,omitempty"` RDRCTimeout badoption.Duration `json:"rdrc_timeout,omitempty"`
StoreDNS bool `json:"store_dns,omitempty"`
} }
type ClashAPIOptions struct { type ClashAPIOptions struct {
+7 -5
View File
@@ -93,11 +93,12 @@ type DialerOptions struct {
} }
type _DomainResolveOptions struct { type _DomainResolveOptions struct {
Server string `json:"server"` Server string `json:"server"`
Strategy DomainStrategy `json:"strategy,omitempty"` Strategy DomainStrategy `json:"strategy,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"` DisableCache bool `json:"disable_cache,omitempty"`
RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` DisableOptimisticCache bool `json:"disable_optimistic_cache,omitempty"`
ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"`
ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"`
} }
type DomainResolveOptions _DomainResolveOptions type DomainResolveOptions _DomainResolveOptions
@@ -107,6 +108,7 @@ func (o DomainResolveOptions) MarshalJSON() ([]byte, error) {
return []byte("{}"), nil return []byte("{}"), nil
} else if o.Strategy == DomainStrategy(C.DomainStrategyAsIS) && } else if o.Strategy == DomainStrategy(C.DomainStrategyAsIS) &&
!o.DisableCache && !o.DisableCache &&
!o.DisableOptimisticCache &&
o.RewriteTTL == nil && o.RewriteTTL == nil &&
o.ClientSubnet == nil { o.ClientSubnet == nil {
return json.Marshal(o.Server) return json.Marshal(o.Server)
+17 -14
View File
@@ -201,18 +201,20 @@ func (r *RouteOptionsActionOptions) UnmarshalJSON(data []byte) error {
} }
type DNSRouteActionOptions struct { type DNSRouteActionOptions struct {
Server string `json:"server,omitempty"` Server string `json:"server,omitempty"`
Strategy DomainStrategy `json:"strategy,omitempty"` Strategy DomainStrategy `json:"strategy,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"` DisableCache bool `json:"disable_cache,omitempty"`
RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` DisableOptimisticCache bool `json:"disable_optimistic_cache,omitempty"`
ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"`
ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"`
} }
type _DNSRouteOptionsActionOptions struct { type _DNSRouteOptionsActionOptions struct {
Strategy DomainStrategy `json:"strategy,omitempty"` Strategy DomainStrategy `json:"strategy,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"` DisableCache bool `json:"disable_cache,omitempty"`
RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` DisableOptimisticCache bool `json:"disable_optimistic_cache,omitempty"`
ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"`
ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"`
} }
type DNSRouteOptionsActionOptions _DNSRouteOptionsActionOptions type DNSRouteOptionsActionOptions _DNSRouteOptionsActionOptions
@@ -321,11 +323,12 @@ type RouteActionSniff struct {
} }
type RouteActionResolve struct { type RouteActionResolve struct {
Server string `json:"server,omitempty"` Server string `json:"server,omitempty"`
Strategy DomainStrategy `json:"strategy,omitempty"` Strategy DomainStrategy `json:"strategy,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"` DisableCache bool `json:"disable_cache,omitempty"`
RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` DisableOptimisticCache bool `json:"disable_optimistic_cache,omitempty"`
ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"`
ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"`
} }
type DNSRouteActionPredefined struct { type DNSRouteActionPredefined struct {
+5 -4
View File
@@ -80,10 +80,11 @@ func NewNetworkManager(ctx context.Context, logger logger.ContextLogger, options
RoutingMark: uint32(options.DefaultMark), RoutingMark: uint32(options.DefaultMark),
DomainResolver: defaultDomainResolver.Server, DomainResolver: defaultDomainResolver.Server,
DomainResolveOptions: adapter.DNSQueryOptions{ DomainResolveOptions: adapter.DNSQueryOptions{
Strategy: C.DomainStrategy(defaultDomainResolver.Strategy), Strategy: C.DomainStrategy(defaultDomainResolver.Strategy),
DisableCache: defaultDomainResolver.DisableCache, DisableCache: defaultDomainResolver.DisableCache,
RewriteTTL: defaultDomainResolver.RewriteTTL, DisableOptimisticCache: defaultDomainResolver.DisableOptimisticCache,
ClientSubnet: defaultDomainResolver.ClientSubnet.Build(netip.Prefix{}), RewriteTTL: defaultDomainResolver.RewriteTTL,
ClientSubnet: defaultDomainResolver.ClientSubnet.Build(netip.Prefix{}),
}, },
NetworkStrategy: (*C.NetworkStrategy)(options.DefaultNetworkStrategy), NetworkStrategy: (*C.NetworkStrategy)(options.DefaultNetworkStrategy),
NetworkType: common.Map(options.DefaultNetworkType, option.InterfaceType.Build), NetworkType: common.Map(options.DefaultNetworkType, option.InterfaceType.Build),
+6 -5
View File
@@ -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{ addresses, err := r.dns.Lookup(adapter.WithContext(ctx, metadata), metadata.Destination.Fqdn, adapter.DNSQueryOptions{
Transport: transport, Transport: transport,
Strategy: action.Strategy, Strategy: action.Strategy,
DisableCache: action.DisableCache, DisableCache: action.DisableCache,
RewriteTTL: action.RewriteTTL, DisableOptimisticCache: action.DisableOptimisticCache,
ClientSubnet: action.ClientSubnet, RewriteTTL: action.RewriteTTL,
ClientSubnet: action.ClientSubnet,
}) })
if err != nil { if err != nil {
return err return err
+41 -26
View File
@@ -107,11 +107,12 @@ func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action opti
return sniffAction, sniffAction.build() return sniffAction, sniffAction.build()
case C.RuleActionTypeResolve: case C.RuleActionTypeResolve:
return &RuleActionResolve{ return &RuleActionResolve{
Server: action.ResolveOptions.Server, Server: action.ResolveOptions.Server,
Strategy: C.DomainStrategy(action.ResolveOptions.Strategy), Strategy: C.DomainStrategy(action.ResolveOptions.Strategy),
DisableCache: action.ResolveOptions.DisableCache, DisableCache: action.ResolveOptions.DisableCache,
RewriteTTL: action.ResolveOptions.RewriteTTL, DisableOptimisticCache: action.ResolveOptions.DisableOptimisticCache,
ClientSubnet: action.ResolveOptions.ClientSubnet.Build(netip.Prefix{}), RewriteTTL: action.ResolveOptions.RewriteTTL,
ClientSubnet: action.ResolveOptions.ClientSubnet.Build(netip.Prefix{}),
}, nil }, nil
default: default:
panic(F.ToString("unknown rule action: ", action.Action)) panic(F.ToString("unknown rule action: ", action.Action))
@@ -126,30 +127,33 @@ func NewDNSRuleAction(logger logger.ContextLogger, action option.DNSRuleAction)
return &RuleActionDNSRoute{ return &RuleActionDNSRoute{
Server: action.RouteOptions.Server, Server: action.RouteOptions.Server,
RuleActionDNSRouteOptions: RuleActionDNSRouteOptions{ RuleActionDNSRouteOptions: RuleActionDNSRouteOptions{
Strategy: C.DomainStrategy(action.RouteOptions.Strategy), Strategy: C.DomainStrategy(action.RouteOptions.Strategy),
DisableCache: action.RouteOptions.DisableCache, DisableCache: action.RouteOptions.DisableCache,
RewriteTTL: action.RouteOptions.RewriteTTL, DisableOptimisticCache: action.RouteOptions.DisableOptimisticCache,
ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptions.ClientSubnet)), RewriteTTL: action.RouteOptions.RewriteTTL,
ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptions.ClientSubnet)),
}, },
} }
case C.RuleActionTypeEvaluate: case C.RuleActionTypeEvaluate:
return &RuleActionEvaluate{ return &RuleActionEvaluate{
Server: action.RouteOptions.Server, Server: action.RouteOptions.Server,
RuleActionDNSRouteOptions: RuleActionDNSRouteOptions{ RuleActionDNSRouteOptions: RuleActionDNSRouteOptions{
Strategy: C.DomainStrategy(action.RouteOptions.Strategy), Strategy: C.DomainStrategy(action.RouteOptions.Strategy),
DisableCache: action.RouteOptions.DisableCache, DisableCache: action.RouteOptions.DisableCache,
RewriteTTL: action.RouteOptions.RewriteTTL, DisableOptimisticCache: action.RouteOptions.DisableOptimisticCache,
ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptions.ClientSubnet)), RewriteTTL: action.RouteOptions.RewriteTTL,
ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptions.ClientSubnet)),
}, },
} }
case C.RuleActionTypeRespond: case C.RuleActionTypeRespond:
return &RuleActionRespond{} return &RuleActionRespond{}
case C.RuleActionTypeRouteOptions: case C.RuleActionTypeRouteOptions:
return &RuleActionDNSRouteOptions{ return &RuleActionDNSRouteOptions{
Strategy: C.DomainStrategy(action.RouteOptionsOptions.Strategy), Strategy: C.DomainStrategy(action.RouteOptionsOptions.Strategy),
DisableCache: action.RouteOptionsOptions.DisableCache, DisableCache: action.RouteOptionsOptions.DisableCache,
RewriteTTL: action.RouteOptionsOptions.RewriteTTL, DisableOptimisticCache: action.RouteOptionsOptions.DisableOptimisticCache,
ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptionsOptions.ClientSubnet)), RewriteTTL: action.RouteOptionsOptions.RewriteTTL,
ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptionsOptions.ClientSubnet)),
} }
case C.RuleActionTypeReject: case C.RuleActionTypeReject:
return &RuleActionReject{ return &RuleActionReject{
@@ -310,6 +314,9 @@ func formatDNSRouteAction(action string, server string, options RuleActionDNSRou
if options.DisableCache { if options.DisableCache {
descriptions = append(descriptions, "disable-cache") descriptions = append(descriptions, "disable-cache")
} }
if options.DisableOptimisticCache {
descriptions = append(descriptions, "disable-optimistic-cache")
}
if options.RewriteTTL != nil { if options.RewriteTTL != nil {
descriptions = append(descriptions, F.ToString("rewrite-ttl=", *options.RewriteTTL)) descriptions = append(descriptions, F.ToString("rewrite-ttl=", *options.RewriteTTL))
} }
@@ -320,10 +327,11 @@ func formatDNSRouteAction(action string, server string, options RuleActionDNSRou
} }
type RuleActionDNSRouteOptions struct { type RuleActionDNSRouteOptions struct {
Strategy C.DomainStrategy Strategy C.DomainStrategy
DisableCache bool DisableCache bool
RewriteTTL *uint32 DisableOptimisticCache bool
ClientSubnet netip.Prefix RewriteTTL *uint32
ClientSubnet netip.Prefix
} }
func (r *RuleActionDNSRouteOptions) Type() string { func (r *RuleActionDNSRouteOptions) Type() string {
@@ -335,6 +343,9 @@ func (r *RuleActionDNSRouteOptions) String() string {
if r.DisableCache { if r.DisableCache {
descriptions = append(descriptions, "disable-cache") descriptions = append(descriptions, "disable-cache")
} }
if r.DisableOptimisticCache {
descriptions = append(descriptions, "disable-optimistic-cache")
}
if r.RewriteTTL != nil { if r.RewriteTTL != nil {
descriptions = append(descriptions, F.ToString("rewrite-ttl=", *r.RewriteTTL)) descriptions = append(descriptions, F.ToString("rewrite-ttl=", *r.RewriteTTL))
} }
@@ -510,11 +521,12 @@ func (r *RuleActionSniff) String() string {
} }
type RuleActionResolve struct { type RuleActionResolve struct {
Server string Server string
Strategy C.DomainStrategy Strategy C.DomainStrategy
DisableCache bool DisableCache bool
RewriteTTL *uint32 DisableOptimisticCache bool
ClientSubnet netip.Prefix RewriteTTL *uint32
ClientSubnet netip.Prefix
} }
func (r *RuleActionResolve) Type() string { func (r *RuleActionResolve) Type() string {
@@ -532,6 +544,9 @@ func (r *RuleActionResolve) String() string {
if r.DisableCache { if r.DisableCache {
options = append(options, "disable_cache") options = append(options, "disable_cache")
} }
if r.DisableOptimisticCache {
options = append(options, "disable_optimistic_cache")
}
if r.RewriteTTL != nil { if r.RewriteTTL != nil {
options = append(options, F.ToString("rewrite_ttl=", *r.RewriteTTL)) options = append(options, F.ToString("rewrite_ttl=", *r.RewriteTTL))
} }