Add rejected DNS response cache support

This commit is contained in:
世界 2024-02-14 20:42:58 +08:00
parent f24a2aed7d
commit 93ae3f7a1e
No known key found for this signature in database
GPG key ID: CD109927C34A63C4
12 changed files with 253 additions and 36 deletions

View file

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

View file

@ -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. 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. `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 #### geoip
!!! question "Since sing-box 1.9.0" !!! question "Since sing-box 1.9.0"

View file

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

View file

@ -1,5 +1,14 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.8.0" !!! 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 ### Structure
```json ```json
@ -7,7 +16,9 @@
"enabled": true, "enabled": true,
"path": "", "path": "",
"cache_id": "", "cache_id": "",
"store_fakeip": false "store_fakeip": false,
"store_rdrc": false,
"rdrc_timeout": ""
} }
``` ```
@ -25,6 +36,23 @@ Path to the cache file.
#### cache_id #### 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. 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.

View file

@ -1,5 +1,14 @@
---
icon: material/new-box
---
!!! question "自 sing-box 1.8.0 起" !!! 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 ```json
@ -7,7 +16,9 @@
"enabled": true, "enabled": true,
"path": "", "path": "",
"cache_id": "", "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`

View file

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

View file

@ -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) { 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 oldDomain, loaded := c.saveDomain[address]; loaded {
if address.Is4() { if address.Is4() {
delete(c.saveAddress4, oldDomain) delete(c.saveAddress4, oldDomain)
@ -111,27 +111,27 @@ func (c *CacheFile) FakeIPStoreAsync(address netip.Addr, domain string, logger l
} else { } else {
c.saveAddress6[domain] = address c.saveAddress6[domain] = address
} }
c.saveAccess.Unlock() c.saveFakeIPAccess.Unlock()
go func() { go func() {
err := c.FakeIPStore(address, domain) err := c.FakeIPStore(address, domain)
if err != nil { 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) delete(c.saveDomain, address)
if address.Is4() { if address.Is4() {
delete(c.saveAddress4, domain) delete(c.saveAddress4, domain)
} else { } else {
delete(c.saveAddress6, domain) delete(c.saveAddress6, domain)
} }
c.saveAccess.Unlock() c.saveFakeIPAccess.Unlock()
}() }()
} }
func (c *CacheFile) FakeIPLoad(address netip.Addr) (string, bool) { func (c *CacheFile) FakeIPLoad(address netip.Addr) (string, bool) {
c.saveAccess.RLock() c.saveFakeIPAccess.RLock()
cachedDomain, cached := c.saveDomain[address] cachedDomain, cached := c.saveDomain[address]
c.saveAccess.RUnlock() c.saveFakeIPAccess.RUnlock()
if cached { if cached {
return cachedDomain, true return cachedDomain, true
} }
@ -152,13 +152,13 @@ func (c *CacheFile) FakeIPLoadDomain(domain string, isIPv6 bool) (netip.Addr, bo
cachedAddress netip.Addr cachedAddress netip.Addr
cached bool cached bool
) )
c.saveAccess.RLock() c.saveFakeIPAccess.RLock()
if !isIPv6 { if !isIPv6 {
cachedAddress, cached = c.saveAddress4[domain] cachedAddress, cached = c.saveAddress4[domain]
} else { } else {
cachedAddress, cached = c.saveAddress6[domain] cachedAddress, cached = c.saveAddress6[domain]
} }
c.saveAccess.RUnlock() c.saveFakeIPAccess.RUnlock()
if cached { if cached {
return cachedAddress, true return cachedAddress, true
} }

View file

@ -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()
}()
}

View file

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

View file

@ -139,7 +139,17 @@ func NewRouter(
DisableCache: dnsOptions.DNSClientOptions.DisableCache, DisableCache: dnsOptions.DNSClientOptions.DisableCache,
DisableExpire: dnsOptions.DNSClientOptions.DisableExpire, DisableExpire: dnsOptions.DNSClientOptions.DisableExpire,
IndependentCache: dnsOptions.DNSClientOptions.IndependentCache, 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 { for i, ruleOptions := range options.Rules {
routeRule, err := NewRule(router, router.logger, ruleOptions, true) routeRule, err := NewRule(router, router.logger, ruleOptions, true)
@ -625,6 +635,11 @@ func (r *Router) Start() error {
return E.Cause(err, "initialize rule[", i, "]") return E.Cause(err, "initialize rule[", i, "]")
} }
} }
monitor.Start("initialize DNS client")
r.dnsClient.Start()
monitor.Finish()
for i, rule := range r.dnsRules { for i, rule := range r.dnsRules {
monitor.Start("initialize DNS rule[", i, "]") monitor.Start("initialize DNS rule[", i, "]")
err := rule.Start() err := rule.Start()

View file

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

View file

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