浏览代码

Add rejected DNS response cache support

世界 1 年之前
父节点
当前提交
93ae3f7a1e

+ 4 - 0
adapter/experimental.go

@@ -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

+ 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.
 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"

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

@@ -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 起"

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

@@ -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.

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

@@ -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`。

+ 27 - 7
experimental/cachefile/cache.go

@@ -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
-	path        string
-	cacheID     []byte
-	storeFakeIP bool
-
+	ctx               context.Context
+	path              string
+	cacheID           []byte
+	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),
 	}
 	}
 }
 }
 
 

+ 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) {
 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
 	}
 	}

+ 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 {
 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 {
 type ClashAPIOptions struct {

+ 16 - 1
route/router.go

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

+ 21 - 10
route/router_dns.go

@@ -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 {
-		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) {
 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")
 		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",