ソースを参照

Add rejected DNS response cache support

世界 1 年間 前
コミット
93ae3f7a1e

+ 4 - 0
adapter/experimental.go

@@ -9,6 +9,7 @@ import (
 	"time"
 
 	"github.com/sagernet/sing-box/common/urltest"
+	"github.com/sagernet/sing-dns"
 	N "github.com/sagernet/sing/common/network"
 	"github.com/sagernet/sing/common/rw"
 )
@@ -30,6 +31,9 @@ type CacheFile interface {
 	StoreFakeIP() bool
 	FakeIPStorage
 
+	StoreRDRC() bool
+	dns.RDRCStore
+
 	LoadMode() string
 	StoreMode(mode string) error
 	LoadSelected(group string) string

+ 5 - 1
docs/configuration/dns/rule.md

@@ -339,10 +339,14 @@ Will overrides `dns.client_subnet` and `servers.[].client_subnet`.
 
 Only takes effect for IP address requests. When the query results do not match the address filtering rule items, the current rule will be skipped.
 
-!!! note ""
+!!! info ""
 
     `ip_cidr` items in included rule sets also takes effect as an address filtering field.
 
+!!! note ""
+
+    Enable `experimental.cache_file.store_rdrc` to cache results.
+
 #### geoip
 
 !!! question "Since sing-box 1.9.0"

+ 5 - 1
docs/configuration/dns/rule.zh.md

@@ -337,10 +337,14 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
 
 仅对IP地址请求生效。 当查询结果与地址筛选规则项不匹配时,将跳过当前规则。
 
-!!! note ""
+!!! info ""
 
     引用的规则集中的 `ip_cidr` 项也作为地址筛选字段生效。
 
+!!! note ""
+
+    启用 `experimental.cache_file.store_rdrc` 以缓存结果。
+
 #### geoip
 
 !!! question "自 sing-box 1.9.0 起"

+ 30 - 2
docs/configuration/experimental/cache-file.md

@@ -1,5 +1,14 @@
+---
+icon: material/new-box
+---
+
 !!! question "Since sing-box 1.8.0"
 
+!!! quote "Changes in sing-box 1.9.0"
+
+    :material-plus: [store_rdrc](#store_rdrc)  
+    :material-plus: [rdrc_timeout](#rdrc_timeout)  
+
 ### Structure
 
 ```json
@@ -7,7 +16,9 @@
   "enabled": true,
   "path": "",
   "cache_id": "",
-  "store_fakeip": false
+  "store_fakeip": false,
+  "store_rdrc": false,
+  "rdrc_timeout": ""
 }
 ```
 
@@ -25,6 +36,23 @@ Path to the cache file.
 
 #### cache_id
 
-Identifier in cache file.
+Identifier in the cache file
 
 If not empty, configuration specified data will use a separate store keyed by it.
+
+#### store_fakeip
+
+Store fakeip in the cache file
+
+#### store_rdrc
+
+Store rejected DNS response cache in the cache file
+
+The check results of [Address filter DNS rule items](/configuration/dns/rule/#address-filter-fields)
+will be cached until expiration.
+
+#### rdrc_timeout
+
+Timeout of rejected DNS response cache.
+
+`7d` is used by default.

+ 28 - 1
docs/configuration/experimental/cache-file.zh.md

@@ -1,5 +1,14 @@
+---
+icon: material/new-box
+---
+
 !!! question "自 sing-box 1.8.0 起"
 
+!!! quote "sing-box 1.9.0 中的更改"
+
+    :material-plus: [store_rdrc](#store_rdrc)  
+    :material-plus: [rdrc_timeout](#rdrc_timeout)  
+
 ### 结构
 
 ```json
@@ -7,7 +16,9 @@
   "enabled": true,
   "path": "",
   "cache_id": "",
-  "store_fakeip": false
+  "store_fakeip": false,
+  "store_rdrc": false,
+  "rdrc_timeout": ""
 }
 ```
 
@@ -26,3 +37,19 @@
 缓存文件中的标识符。
 
 如果不为空,配置特定的数据将使用由其键控的单独存储。
+
+#### store_fakeip
+
+将 fakeip 存储在缓存文件中。
+
+#### store_rdrc
+
+将拒绝的 DNS 响应缓存存储在缓存文件中。
+
+[地址筛选 DNS 规则项](/zh/configuration/dns/rule/#_3) 的检查结果将被缓存至过期。
+
+#### rdrc_timeout
+
+拒绝的 DNS 响应缓存超时。
+
+默认使用 `7d`。

+ 27 - 7
experimental/cachefile/cache.go

@@ -29,6 +29,7 @@ var (
 		string(bucketExpand),
 		string(bucketMode),
 		string(bucketRuleSet),
+		string(bucketRDRC),
 	}
 
 	cacheIDDefault = []byte("default")
@@ -37,17 +38,25 @@ var (
 var _ adapter.CacheFile = (*CacheFile)(nil)
 
 type CacheFile struct {
-	ctx         context.Context
-	path        string
-	cacheID     []byte
-	storeFakeIP bool
-
+	ctx               context.Context
+	path              string
+	cacheID           []byte
+	storeFakeIP       bool
+	storeRDRC         bool
+	rdrcTimeout       time.Duration
 	DB                *bbolt.DB
-	saveAccess        sync.RWMutex
+	saveMetadataTimer *time.Timer
+	saveFakeIPAccess  sync.RWMutex
 	saveDomain        map[netip.Addr]string
 	saveAddress4      map[string]netip.Addr
 	saveAddress6      map[string]netip.Addr
-	saveMetadataTimer *time.Timer
+	saveRDRCAccess    sync.RWMutex
+	saveRDRC          map[saveRDRCCacheKey]bool
+}
+
+type saveRDRCCacheKey struct {
+	TransportName string
+	QuestionName  string
 }
 
 func New(ctx context.Context, options option.CacheFileOptions) *CacheFile {
@@ -61,14 +70,25 @@ func New(ctx context.Context, options option.CacheFileOptions) *CacheFile {
 	if options.CacheID != "" {
 		cacheIDBytes = append([]byte{0}, []byte(options.CacheID)...)
 	}
+	var rdrcTimeout time.Duration
+	if options.StoreRDRC {
+		if options.RDRCTimeout > 0 {
+			rdrcTimeout = time.Duration(options.RDRCTimeout)
+		} else {
+			rdrcTimeout = 7 * 24 * time.Hour
+		}
+	}
 	return &CacheFile{
 		ctx:          ctx,
 		path:         filemanager.BasePath(ctx, path),
 		cacheID:      cacheIDBytes,
 		storeFakeIP:  options.StoreFakeIP,
+		storeRDRC:    options.StoreRDRC,
+		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),
 	}
 }
 

+ 9 - 9
experimental/cachefile/fakeip.go

@@ -97,7 +97,7 @@ func (c *CacheFile) FakeIPStore(address netip.Addr, domain string) error {
 }
 
 func (c *CacheFile) FakeIPStoreAsync(address netip.Addr, domain string, logger logger.Logger) {
-	c.saveAccess.Lock()
+	c.saveFakeIPAccess.Lock()
 	if oldDomain, loaded := c.saveDomain[address]; loaded {
 		if address.Is4() {
 			delete(c.saveAddress4, oldDomain)
@@ -111,27 +111,27 @@ func (c *CacheFile) FakeIPStoreAsync(address netip.Addr, domain string, logger l
 	} else {
 		c.saveAddress6[domain] = address
 	}
-	c.saveAccess.Unlock()
+	c.saveFakeIPAccess.Unlock()
 	go func() {
 		err := c.FakeIPStore(address, domain)
 		if err != nil {
-			logger.Warn("save FakeIP address pair: ", err)
+			logger.Warn("save FakeIP cache: ", err)
 		}
-		c.saveAccess.Lock()
+		c.saveFakeIPAccess.Lock()
 		delete(c.saveDomain, address)
 		if address.Is4() {
 			delete(c.saveAddress4, domain)
 		} else {
 			delete(c.saveAddress6, domain)
 		}
-		c.saveAccess.Unlock()
+		c.saveFakeIPAccess.Unlock()
 	}()
 }
 
 func (c *CacheFile) FakeIPLoad(address netip.Addr) (string, bool) {
-	c.saveAccess.RLock()
+	c.saveFakeIPAccess.RLock()
 	cachedDomain, cached := c.saveDomain[address]
-	c.saveAccess.RUnlock()
+	c.saveFakeIPAccess.RUnlock()
 	if cached {
 		return cachedDomain, true
 	}
@@ -152,13 +152,13 @@ func (c *CacheFile) FakeIPLoadDomain(domain string, isIPv6 bool) (netip.Addr, bo
 		cachedAddress netip.Addr
 		cached        bool
 	)
-	c.saveAccess.RLock()
+	c.saveFakeIPAccess.RLock()
 	if !isIPv6 {
 		cachedAddress, cached = c.saveAddress4[domain]
 	} else {
 		cachedAddress, cached = c.saveAddress6[domain]
 	}
-	c.saveAccess.RUnlock()
+	c.saveFakeIPAccess.RUnlock()
 	if cached {
 		return cachedAddress, true
 	}

+ 101 - 0
experimental/cachefile/rdrc.go

@@ -0,0 +1,101 @@
+package cachefile
+
+import (
+	"encoding/binary"
+	"time"
+
+	"github.com/sagernet/bbolt"
+	"github.com/sagernet/sing/common/buf"
+	"github.com/sagernet/sing/common/logger"
+)
+
+var bucketRDRC = []byte("rdrc")
+
+func (c *CacheFile) StoreRDRC() bool {
+	return c.storeRDRC
+}
+
+func (c *CacheFile) RDRCTimeout() time.Duration {
+	return c.rdrcTimeout
+}
+
+func (c *CacheFile) LoadRDRC(transportName string, qName string) (rejected bool) {
+	c.saveRDRCAccess.RLock()
+	rejected, cached := c.saveRDRC[saveRDRCCacheKey{transportName, qName}]
+	c.saveRDRCAccess.RUnlock()
+	if cached {
+		return
+	}
+	var deleteCache bool
+	err := c.DB.View(func(tx *bbolt.Tx) error {
+		bucket := c.bucket(tx, bucketRDRC)
+		if bucket == nil {
+			return nil
+		}
+		bucket = bucket.Bucket([]byte(transportName))
+		if bucket == nil {
+			return nil
+		}
+		content := bucket.Get([]byte(qName))
+		if content == nil {
+			return nil
+		}
+		expiresAt := time.Unix(int64(binary.BigEndian.Uint64(content)), 0)
+		if time.Now().After(expiresAt) {
+			deleteCache = true
+			return nil
+		}
+		rejected = true
+		return nil
+	})
+	if err != nil {
+		return
+	}
+	if deleteCache {
+		c.DB.Update(func(tx *bbolt.Tx) error {
+			bucket := c.bucket(tx, bucketRDRC)
+			if bucket == nil {
+				return nil
+			}
+			bucket = bucket.Bucket([]byte(transportName))
+			if bucket == nil {
+				return nil
+			}
+			return bucket.Delete([]byte(qName))
+		})
+	}
+	return
+}
+
+func (c *CacheFile) SaveRDRC(transportName string, qName string) error {
+	return c.DB.Batch(func(tx *bbolt.Tx) error {
+		bucket, err := c.createBucket(tx, bucketRDRC)
+		if err != nil {
+			return err
+		}
+		bucket, err = bucket.CreateBucketIfNotExists([]byte(transportName))
+		if err != nil {
+			return err
+		}
+		expiresAt := buf.Get(8)
+		defer buf.Put(expiresAt)
+		binary.BigEndian.PutUint64(expiresAt, uint64(time.Now().Add(c.rdrcTimeout).Unix()))
+		return bucket.Put([]byte(qName), expiresAt)
+	})
+}
+
+func (c *CacheFile) SaveRDRCAsync(transportName string, qName string, logger logger.Logger) {
+	saveKey := saveRDRCCacheKey{transportName, qName}
+	c.saveRDRCAccess.Lock()
+	c.saveRDRC[saveKey] = true
+	c.saveRDRCAccess.Unlock()
+	go func() {
+		err := c.SaveRDRC(transportName, qName)
+		if err != nil {
+			logger.Warn("save RDRC: ", err)
+		}
+		c.saveRDRCAccess.Lock()
+		delete(c.saveRDRC, saveKey)
+		c.saveRDRCAccess.Unlock()
+	}()
+}

+ 6 - 4
option/experimental.go

@@ -8,10 +8,12 @@ type ExperimentalOptions struct {
 }
 
 type CacheFileOptions struct {
-	Enabled     bool   `json:"enabled,omitempty"`
-	Path        string `json:"path,omitempty"`
-	CacheID     string `json:"cache_id,omitempty"`
-	StoreFakeIP bool   `json:"store_fakeip,omitempty"`
+	Enabled     bool     `json:"enabled,omitempty"`
+	Path        string   `json:"path,omitempty"`
+	CacheID     string   `json:"cache_id,omitempty"`
+	StoreFakeIP bool     `json:"store_fakeip,omitempty"`
+	StoreRDRC   bool     `json:"store_rdrc,omitempty"`
+	RDRCTimeout Duration `json:"rdrc_timeout,omitempty"`
 }
 
 type ClashAPIOptions struct {

+ 16 - 1
route/router.go

@@ -139,7 +139,17 @@ func NewRouter(
 		DisableCache:     dnsOptions.DNSClientOptions.DisableCache,
 		DisableExpire:    dnsOptions.DNSClientOptions.DisableExpire,
 		IndependentCache: dnsOptions.DNSClientOptions.IndependentCache,
-		Logger:           router.dnsLogger,
+		RDRC: func() dns.RDRCStore {
+			cacheFile := service.FromContext[adapter.CacheFile](ctx)
+			if cacheFile == nil {
+				return nil
+			}
+			if !cacheFile.StoreRDRC() {
+				return nil
+			}
+			return cacheFile
+		},
+		Logger: router.dnsLogger,
 	})
 	for i, ruleOptions := range options.Rules {
 		routeRule, err := NewRule(router, router.logger, ruleOptions, true)
@@ -625,6 +635,11 @@ func (r *Router) Start() error {
 			return E.Cause(err, "initialize rule[", i, "]")
 		}
 	}
+
+	monitor.Start("initialize DNS client")
+	r.dnsClient.Start()
+	monitor.Finish()
+
 	for i, rule := range r.dnsRules {
 		monitor.Start("initialize DNS rule[", i, "]")
 		err := rule.Start()

+ 21 - 10
route/router_dns.go

@@ -139,7 +139,9 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, er
 			}
 			cancel()
 			if err != nil {
-				if errors.Is(err, dns.ErrResponseRejected) {
+				if errors.Is(err, dns.ErrResponseRejectedCached) {
+					r.dnsLogger.DebugContext(ctx, E.Cause(err, "response rejected for ", formatQuestion(message.Question[0].String())), " (cached)")
+				} else if errors.Is(err, dns.ErrResponseRejected) {
 					r.dnsLogger.DebugContext(ctx, E.Cause(err, "response rejected for ", formatQuestion(message.Question[0].String())))
 				} else if len(message.Question) > 0 {
 					r.dnsLogger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", formatQuestion(message.Question[0].String())))
@@ -166,6 +168,15 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, er
 }
 
 func (r *Router) Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error) {
+	var (
+		responseAddrs []netip.Addr
+		cached        bool
+		err           error
+	)
+	responseAddrs, cached = r.dnsClient.LookupCache(ctx, domain, strategy)
+	if cached {
+		return responseAddrs, nil
+	}
 	r.dnsLogger.DebugContext(ctx, "lookup domain ", domain)
 	ctx, metadata := adapter.AppendContext(ctx)
 	metadata.Domain = domain
@@ -174,8 +185,6 @@ func (r *Router) Lookup(ctx context.Context, domain string, strategy dns.DomainS
 		transportStrategy dns.DomainStrategy
 		rule              adapter.DNSRule
 		ruleIndex         int
-		resultAddrs       []netip.Addr
-		err               error
 	)
 	ruleIndex = -1
 	for {
@@ -193,22 +202,24 @@ func (r *Router) Lookup(ctx context.Context, domain string, strategy dns.DomainS
 		dnsCtx, cancel = context.WithTimeout(dnsCtx, C.DNSTimeout)
 		if rule != nil && rule.WithAddressLimit() {
 			addressLimit = true
-			resultAddrs, err = r.dnsClient.LookupWithResponseCheck(dnsCtx, transport, domain, strategy, func(responseAddrs []netip.Addr) bool {
+			responseAddrs, err = r.dnsClient.LookupWithResponseCheck(dnsCtx, transport, domain, strategy, func(responseAddrs []netip.Addr) bool {
 				metadata.DestinationAddresses = responseAddrs
 				return rule.MatchAddressLimit(metadata)
 			})
 		} else {
 			addressLimit = false
-			resultAddrs, err = r.dnsClient.Lookup(dnsCtx, transport, domain, strategy)
+			responseAddrs, err = r.dnsClient.Lookup(dnsCtx, transport, domain, strategy)
 		}
 		cancel()
 		if err != nil {
-			if errors.Is(err, dns.ErrResponseRejected) {
+			if errors.Is(err, dns.ErrResponseRejectedCached) {
+				r.dnsLogger.DebugContext(ctx, "response rejected for ", domain, " (cached)")
+			} else if errors.Is(err, dns.ErrResponseRejected) {
 				r.dnsLogger.DebugContext(ctx, "response rejected for ", domain)
 			} else {
 				r.dnsLogger.ErrorContext(ctx, E.Cause(err, "lookup failed for ", domain))
 			}
-		} else if len(resultAddrs) == 0 {
+		} else if len(responseAddrs) == 0 {
 			r.dnsLogger.ErrorContext(ctx, "lookup failed for ", domain, ": empty result")
 			err = dns.RCodeNameError
 		}
@@ -216,10 +227,10 @@ func (r *Router) Lookup(ctx context.Context, domain string, strategy dns.DomainS
 			break
 		}
 	}
-	if len(resultAddrs) > 0 {
-		r.dnsLogger.InfoContext(ctx, "lookup succeed for ", domain, ": ", strings.Join(F.MapToString(resultAddrs), " "))
+	if len(responseAddrs) > 0 {
+		r.dnsLogger.InfoContext(ctx, "lookup succeed for ", domain, ": ", strings.Join(F.MapToString(responseAddrs), " "))
 	}
-	return resultAddrs, err
+	return responseAddrs, err
 }
 
 func (r *Router) LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error) {

+ 1 - 0
transport/dhcp/server.go

@@ -58,6 +58,7 @@ func NewTransport(options dns.TransportOptions) (*Transport, error) {
 		return nil, E.New("missing router in context")
 	}
 	transport := &Transport{
+		options:       options,
 		router:        router,
 		interfaceName: linkURL.Host,
 		autoInterface: linkURL.Host == "auto",