Răsfoiți Sursa

Add support for `client-subnet` DNS options

世界 1 an în urmă
părinte
comite
f24a2aed7d

+ 1 - 0
adapter/router.go

@@ -86,6 +86,7 @@ type DNSRule interface {
 	Rule
 	DisableCache() bool
 	RewriteTTL() *uint32
+	ClientSubnet() *netip.Addr
 	WithAddressLimit() bool
 	MatchAddressLimit(metadata *InboundContext) bool
 }

+ 15 - 2
docs/configuration/dns/index.md

@@ -1,3 +1,11 @@
+---
+icon: material/new-box
+---
+
+!!! quote "Changes in sing-box 1.9.0"
+
+    :material-plus: [client_subnet](#client_subnet)
+
 # DNS
 
 ### Structure
@@ -13,6 +21,7 @@
     "disable_expire": false,
     "independent_cache": false,
     "reverse_mapping": false,
+    "client_subnet": "",
     "fakeip": {}
   }
 }
@@ -60,6 +69,10 @@ Stores a reverse mapping of IP addresses after responding to a DNS query in orde
 Since this process relies on the act of resolving domain names by an application before making a request, it can be
 problematic in environments such as macOS, where DNS is proxied and cached by the system.
 
-#### fakeip
+#### client_subnet
+
+!!! question "Since sing-box 1.9.0"
+
+Append a `edns0-subnet` OPT extra record with the specified IP address to every query by default.
 
-[FakeIP](./fakeip/) settings.
+Can be overrides by `servers.[].client_subnet` or `rules.[].client_subnet`.

+ 17 - 0
docs/configuration/dns/index.zh.md

@@ -1,3 +1,11 @@
+---
+icon: material/new-box
+---
+
+!!! quote "sing-box 1.9.0 中的更改"
+
+    :material-plus: [client_subnet](#client_subnet)
+
 # DNS
 
 ### 结构
@@ -13,6 +21,7 @@
     "disable_expire": false,
     "independent_cache": false,
     "reverse_mapping": false,
+    "client_subnet": "",
     "fakeip": {}
   }
 }
@@ -58,6 +67,14 @@
 
 由于此过程依赖于应用程序在发出请求之前解析域名的行为,因此在 macOS 等 DNS 由系统代理和缓存的环境中可能会出现问题。
 
+#### client_subnet
+
+!!! question "自 sing-box 1.9.0 起"
+
+默认情况下,将带有指定 IP 地址的 `edns0-subnet` OPT 附加记录附加到每个查询。
+ 
+可以被 `servers.[].client_subnet` 或 `rules.[].client_subnet` 覆盖。
+
 #### fakeip
 
 [FakeIP](./fakeip/) 设置。

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

@@ -6,7 +6,8 @@ icon: material/new-box
 
     :material-plus: [geoip](#geoip)  
     :material-plus: [ip_cidr](#ip_cidr)  
-    :material-plus: [ip_is_private](#ip_is_private)
+    :material-plus: [ip_is_private](#ip_is_private)  
+    :material-plus: [client_subnet](#client_subnet)
 
 !!! quote "Changes in sing-box 1.8.0"
 
@@ -121,7 +122,8 @@ icon: material/new-box
         ],
         "server": "local",
         "disable_cache": false,
-        "rewrite_ttl": 100
+        "rewrite_ttl": 100,
+        "client_subnet": "127.0.0.1"
       },
       {
         "type": "logical",
@@ -129,7 +131,8 @@ icon: material/new-box
         "rules": [],
         "server": "local",
         "disable_cache": false,
-        "rewrite_ttl": 100
+        "rewrite_ttl": 100,
+        "client_subnet": "127.0.0.1"
       }
     ]
   }
@@ -280,8 +283,6 @@ Match Clash mode.
 
 #### wifi_ssid
 
-<!-- md:version 1.7.0-beta.4 -->
-
 !!! quote ""
 
     Only supported in graphical clients on Android and iOS.
@@ -326,6 +327,14 @@ Disable cache and save cache in this query.
 
 Rewrite TTL in DNS responses.
 
+#### client_subnet
+
+!!! question "Since sing-box 1.9.0"
+
+Append a `edns0-subnet` OPT extra record with the specified IP address to every query by default.
+
+Will overrides `dns.client_subnet` and `servers.[].client_subnet`.
+
 ### Address Filter Fields
 
 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.

+ 14 - 3
docs/configuration/dns/rule.zh.md

@@ -6,7 +6,8 @@ icon: material/new-box
 
     :material-plus: [geoip](#geoip)  
     :material-plus: [ip_cidr](#ip_cidr)  
-    :material-plus: [ip_is_private](#ip_is_private)
+    :material-plus: [ip_is_private](#ip_is_private)  
+    :material-plus: [client_subnet](#client_subnet)
 
 !!! quote "sing-box 1.8.0 中的更改"
 
@@ -120,14 +121,16 @@ icon: material/new-box
           "direct"
         ],
         "server": "local",
-        "disable_cache": false
+        "disable_cache": false,
+        "client_subnet": "127.0.0.1"
       },
       {
         "type": "logical",
         "mode": "and",
         "rules": [],
         "server": "local",
-        "disable_cache": false
+        "disable_cache": false,
+        "client_subnet": "127.0.0.1"
       }
     ]
   }
@@ -322,6 +325,14 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
 
 重写 DNS 回应中的 TTL。
 
+#### client_subnet
+
+!!! question "自 sing-box 1.9.0 起"
+
+默认情况下,将带有指定 IP 地址的 `edns0-subnet` OPT 附加记录附加到每个查询。
+
+将覆盖 `dns.client_subnet` 与 `servers.[].client_subnet`。
+
 ### 地址筛选字段
 
 仅对IP地址请求生效。 当查询结果与地址筛选规则项不匹配时,将跳过当前规则。

+ 26 - 8
docs/configuration/dns/server.md

@@ -1,3 +1,11 @@
+---
+icon: material/new-box
+---
+
+!!! quote "Changes in sing-box 1.9.0"
+
+    :material-plus: [client_subnet](#client_subnet)
+
 ### Structure
 
 ```json
@@ -5,17 +13,17 @@
   "dns": {
     "servers": [
       {
-        "tag": "google",
-        "address": "tls://dns.google",
-        "address_resolver": "local",
-        "address_strategy": "prefer_ipv4",
-        "strategy": "ipv4_only",
-        "detour": "direct"
+        "tag": "",
+        "address": "",
+        "address_resolver": "",
+        "address_strategy": "",
+        "strategy": "",
+        "detour": "",
+        "client_subnet": ""
       }
     ]
   }
 }
-
 ```
 
 ### Fields
@@ -80,10 +88,20 @@ Default domain strategy for resolving the domain names.
 
 One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
 
-Take no effect if override by other settings.
+Take no effect if overridden by other settings.
 
 #### detour
 
 Tag of an outbound for connecting to the dns server.
 
 Default outbound will be used if empty.
+
+#### client_subnet
+
+!!! question "Since sing-box 1.9.0"
+
+Append a `edns0-subnet` OPT extra record with the specified IP address to every query by default.
+
+Can be overrides by `rules.[].client_subnet`.
+
+Will overrides `dns.client_subnet`.

+ 25 - 7
docs/configuration/dns/server.zh.md

@@ -1,3 +1,11 @@
+---
+icon: material/new-box
+---
+
+!!! quote "sing-box 1.9.0 中的更改"
+
+    :material-plus: [client_subnet](#client_subnet)
+
 ### 结构
 
 ```json
@@ -5,17 +13,17 @@
   "dns": {
     "servers": [
       {
-        "tag": "google",
-        "address": "tls://dns.google",
-        "address_resolver": "local",
-        "address_strategy": "prefer_ipv4",
-        "strategy": "ipv4_only",
-        "detour": "direct"
+        "tag": "",
+        "address": "",
+        "address_resolver": "",
+        "address_strategy": "",
+        "strategy": "",
+        "detour": "",
+        "client_subnet": ""
       }
     ]
   }
 }
-
 ```
 
 ### 字段
@@ -87,3 +95,13 @@ DNS 服务器的地址。
 用于连接到 DNS 服务器的出站的标签。
 
 如果为空,将使用默认出站。
+
+#### client_subnet
+
+!!! question "自 sing-box 1.9.0 起"
+
+默认情况下,将带有指定 IP 地址的 `edns0-subnet` OPT 附加记录附加到每个查询。
+
+可以被 `rules.[].client_subnet` 覆盖。
+
+将覆盖 `dns.client_subnet`。

+ 119 - 62
docs/manual/proxy/client.md

@@ -338,74 +338,131 @@ flowchart TB
 
 === ":material-dns: DNS rules (1.9.0+)"
 
-    !!! warning "DNS leaks"
-
-        The new DNS feature allows you to more precisely bypass Chinese websites via **DNS leaks**. Do not use plain local DNS if using this method.
-
-    ```json
-    {
-      "dns": {
-        "servers": [
-          {
-            "tag": "google",
-            "address": "tls://8.8.8.8"
-          },
-          {
-            "tag": "local",
-            "address": "https://223.5.5.5/dns-query",
-            "detour": "direct"
-          }
-        ],
-        "rules": [
-          {
-            "outbound": "any",
-            "server": "local"
-          },
-          {
-            "clash_mode": "Direct",
-            "server": "local"
-          },
-          {
-            "clash_mode": "Global",
-            "server": "google"
-          },
-          {
-            "rule_set": "geosite-geolocation-cn",
-            "server": "local"
+    === ":material-shield-off: With DNS Leaks"
+    
+        ```json
+        {
+          "dns": {
+            "servers": [
+              {
+                "tag": "google",
+                "address": "tls://8.8.8.8"
+              },
+              {
+                "tag": "local",
+                "address": "https://223.5.5.5/dns-query",
+                "detour": "direct"
+              }
+            ],
+            "rules": [
+              {
+                "outbound": "any",
+                "server": "local"
+              },
+              {
+                "clash_mode": "Direct",
+                "server": "local"
+              },
+              {
+                "clash_mode": "Global",
+                "server": "google"
+              },
+              {
+                "rule_set": "geosite-geolocation-cn",
+                "server": "local"
+              },
+              {
+                "clash_mode": "Default",
+                "server": "google"
+              },
+              {
+                "rule_set": "geoip-cn",
+                "server": "local"
+              }
+            ]
           },
-          {
-            "clash_mode": "Default",
-            "server": "google"
+          "route": {
+            "rule_set": [
+              {
+                "type": "remote",
+                "tag": "geosite-geolocation-cn",
+                "format": "binary",
+                "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-cn.srs"
+              },
+              {
+                "type": "remote",
+                "tag": "geoip-cn",
+                "format": "binary",
+                "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs"
+              }
+            ]
           },
-          {
-            "rule_set": "geoip-cn",
-            "server": "local"
+          "experimental": {
+            "clash_api": {
+              "default_mode": "Leak"
+            }
           }
-        ]
-      },
-      "route": {
-        "rule_set": [
-          {
-            "type": "remote",
-            "tag": "geosite-geolocation-cn",
-            "format": "binary",
-            "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-cn.srs"
+        }
+        ```
+
+    === ":material-security: Without DNS Leaks (1.9.0-alpha.2+)"
+
+        ```json
+        {
+          "dns": {
+            "servers": [
+              {
+                "tag": "google",
+                "address": "tls://8.8.8.8"
+              },
+              {
+                "tag": "local",
+                "address": "https://223.5.5.5/dns-query",
+                "detour": "direct"
+              }
+            ],
+            "rules": [
+              {
+                "outbound": "any",
+                "server": "local"
+              },
+              {
+                "clash_mode": "Direct",
+                "server": "local"
+              },
+              {
+                "clash_mode": "Global",
+                "server": "google"
+              },
+              {
+                "rule_set": "geosite-geolocation-cn",
+                "server": "local"
+              },
+              {
+                "rule_set": "geoip-cn",
+                "server": "google",
+                "client_subnet": "114.114.114.114" // Any China client IP address
+              }
+            ]
           },
-          {
-            "type": "remote",
-            "tag": "geoip-cn",
-            "format": "binary",
-            "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs"
+          "route": {
+            "rule_set": [
+              {
+                "type": "remote",
+                "tag": "geosite-geolocation-cn",
+                "format": "binary",
+                "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-cn.srs"
+              },
+              {
+                "type": "remote",
+                "tag": "geoip-cn",
+                "format": "binary",
+                "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs"
+              }
+            ]
           }
-        ]
-      },
-      "experimental": {
-        "clash_api": {
-          "default_mode": "Leak"
         }
-      }
-    }
-    ```
+        ```
 
 === ":material-router-network: Route rules"
 

+ 4 - 4
experimental/libbox/dns.go

@@ -9,9 +9,7 @@ import (
 	"github.com/sagernet/sing-dns"
 	"github.com/sagernet/sing/common"
 	E "github.com/sagernet/sing/common/exceptions"
-	"github.com/sagernet/sing/common/logger"
 	M "github.com/sagernet/sing/common/metadata"
-	N "github.com/sagernet/sing/common/network"
 	"github.com/sagernet/sing/common/task"
 
 	mDNS "github.com/miekg/dns"
@@ -25,9 +23,11 @@ type LocalDNSTransport interface {
 
 func RegisterLocalDNSTransport(transport LocalDNSTransport) {
 	if transport == nil {
-		dns.RegisterTransport([]string{"local"}, dns.CreateLocalTransport)
+		dns.RegisterTransport([]string{"local"}, func(options dns.TransportOptions) (dns.Transport, error) {
+			return dns.NewLocalTransport(options), nil
+		})
 	} else {
-		dns.RegisterTransport([]string{"local"}, func(name string, ctx context.Context, logger logger.ContextLogger, dialer N.Dialer, link string) (dns.Transport, error) {
+		dns.RegisterTransport([]string{"local"}, func(options dns.TransportOptions) (dns.Transport, error) {
 			return &platformLocalDNSTransport{
 				iif: transport,
 			}, nil

+ 1 - 5
include/dhcp_stub.go

@@ -3,16 +3,12 @@
 package include
 
 import (
-	"context"
-
 	"github.com/sagernet/sing-dns"
 	E "github.com/sagernet/sing/common/exceptions"
-	"github.com/sagernet/sing/common/logger"
-	N "github.com/sagernet/sing/common/network"
 )
 
 func init() {
-	dns.RegisterTransport([]string{"dhcp"}, func(name string, ctx context.Context, logger logger.ContextLogger, dialer N.Dialer, link string) (dns.Transport, error) {
+	dns.RegisterTransport([]string{"dhcp"}, func(options dns.TransportOptions) (dns.Transport, error) {
 		return nil, E.New(`DHCP is not included in this build, rebuild with -tags with_dhcp`)
 	})
 }

+ 1 - 2
include/quic_stub.go

@@ -11,13 +11,12 @@ import (
 	"github.com/sagernet/sing-box/option"
 	"github.com/sagernet/sing-box/transport/v2ray"
 	"github.com/sagernet/sing-dns"
-	"github.com/sagernet/sing/common/logger"
 	M "github.com/sagernet/sing/common/metadata"
 	N "github.com/sagernet/sing/common/network"
 )
 
 func init() {
-	dns.RegisterTransport([]string{"quic", "h3"}, func(name string, ctx context.Context, logger logger.ContextLogger, dialer N.Dialer, link string) (dns.Transport, error) {
+	dns.RegisterTransport([]string{"quic", "h3"}, func(options dns.TransportOptions) (dns.Transport, error) {
 		return nil, C.ErrQUICNotIncluded
 	})
 	v2ray.RegisterQUICConstructor(

+ 2 - 0
option/dns.go

@@ -19,6 +19,7 @@ type DNSServerOptions struct {
 	AddressFallbackDelay Duration       `json:"address_fallback_delay,omitempty"`
 	Strategy             DomainStrategy `json:"strategy,omitempty"`
 	Detour               string         `json:"detour,omitempty"`
+	ClientSubnet         *ListenAddress `json:"client_subnet,omitempty"`
 }
 
 type DNSClientOptions struct {
@@ -26,6 +27,7 @@ type DNSClientOptions struct {
 	DisableCache     bool           `json:"disable_cache,omitempty"`
 	DisableExpire    bool           `json:"disable_expire,omitempty"`
 	IndependentCache bool           `json:"independent_cache,omitempty"`
+	ClientSubnet     *ListenAddress `json:"client_subnet,omitempty"`
 }
 
 type DNSFakeIPOptions struct {

+ 9 - 6
option/rule_dns.go

@@ -100,6 +100,7 @@ type DefaultDNSRule struct {
 	Server            string                 `json:"server,omitempty"`
 	DisableCache      bool                   `json:"disable_cache,omitempty"`
 	RewriteTTL        *uint32                `json:"rewrite_ttl,omitempty"`
+	ClientSubnet      *ListenAddress         `json:"client_subnet,omitempty"`
 }
 
 func (r DefaultDNSRule) IsValid() bool {
@@ -108,16 +109,18 @@ func (r DefaultDNSRule) IsValid() bool {
 	defaultValue.Server = r.Server
 	defaultValue.DisableCache = r.DisableCache
 	defaultValue.RewriteTTL = r.RewriteTTL
+	defaultValue.ClientSubnet = r.ClientSubnet
 	return !reflect.DeepEqual(r, defaultValue)
 }
 
 type LogicalDNSRule struct {
-	Mode         string    `json:"mode"`
-	Rules        []DNSRule `json:"rules,omitempty"`
-	Invert       bool      `json:"invert,omitempty"`
-	Server       string    `json:"server,omitempty"`
-	DisableCache bool      `json:"disable_cache,omitempty"`
-	RewriteTTL   *uint32   `json:"rewrite_ttl,omitempty"`
+	Mode         string         `json:"mode"`
+	Rules        []DNSRule      `json:"rules,omitempty"`
+	Invert       bool           `json:"invert,omitempty"`
+	Server       string         `json:"server,omitempty"`
+	DisableCache bool           `json:"disable_cache,omitempty"`
+	RewriteTTL   *uint32        `json:"rewrite_ttl,omitempty"`
+	ClientSubnet *ListenAddress `json:"client_subnet,omitempty"`
 }
 
 func (r LogicalDNSRule) IsValid() bool {

+ 20 - 2
route/router.go

@@ -225,7 +225,20 @@ func NewRouter(
 					return nil, E.New("parse dns server[", tag, "]: missing address_resolver")
 				}
 			}
-			transport, err := dns.CreateTransport(tag, ctx, logFactory.NewLogger(F.ToString("dns/transport[", tag, "]")), detour, server.Address)
+			var clientSubnet netip.Addr
+			if server.ClientSubnet != nil {
+				clientSubnet = server.ClientSubnet.Build()
+			} else if dnsOptions.ClientSubnet != nil {
+				clientSubnet = dnsOptions.ClientSubnet.Build()
+			}
+			transport, err := dns.CreateTransport(dns.TransportOptions{
+				Context:      ctx,
+				Logger:       logFactory.NewLogger(F.ToString("dns/transport[", tag, "]")),
+				Name:         tag,
+				Dialer:       detour,
+				Address:      server.Address,
+				ClientSubnet: clientSubnet,
+			})
 			if err != nil {
 				return nil, E.Cause(err, "parse dns server[", tag, "]")
 			}
@@ -265,7 +278,12 @@ func NewRouter(
 	}
 	if defaultTransport == nil {
 		if len(transports) == 0 {
-			transports = append(transports, dns.NewLocalTransport("local", N.SystemDialer))
+			transports = append(transports, common.Must1(dns.CreateTransport(dns.TransportOptions{
+				Context: ctx,
+				Name:    "local",
+				Address: "local",
+				Dialer:  common.Must1(dialer.NewDefault(router, option.DialerOptions{})),
+			})))
 		}
 		defaultTransport = transports[0]
 	}

+ 3 - 0
route/router_dns.go

@@ -70,6 +70,9 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, index int) (con
 				if rewriteTTL := rule.RewriteTTL(); rewriteTTL != nil {
 					ctx = dns.ContextWithRewriteTTL(ctx, *rewriteTTL)
 				}
+				if clientSubnet := rule.ClientSubnet(); clientSubnet != nil {
+					ctx = dns.ContextWithClientSubnet(ctx, *clientSubnet)
+				}
 				if domainStrategy, dsLoaded := r.transportDomainStrategy[transport]; dsLoaded {
 					return ctx, transport, domainStrategy, rule, ruleIndex
 				} else {

+ 14 - 0
route/rule_dns.go

@@ -1,6 +1,8 @@
 package route
 
 import (
+	"net/netip"
+
 	"github.com/sagernet/sing-box/adapter"
 	C "github.com/sagernet/sing-box/constant"
 	"github.com/sagernet/sing-box/log"
@@ -38,6 +40,7 @@ type DefaultDNSRule struct {
 	abstractDefaultRule
 	disableCache bool
 	rewriteTTL   *uint32
+	clientSubnet *netip.Addr
 }
 
 func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options option.DefaultDNSRule) (*DefaultDNSRule, error) {
@@ -48,6 +51,7 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options
 		},
 		disableCache: options.DisableCache,
 		rewriteTTL:   options.RewriteTTL,
+		clientSubnet: (*netip.Addr)(options.ClientSubnet),
 	}
 	if len(options.Inbound) > 0 {
 		item := NewInboundRule(options.Inbound)
@@ -230,6 +234,10 @@ func (r *DefaultDNSRule) RewriteTTL() *uint32 {
 	return r.rewriteTTL
 }
 
+func (r *DefaultDNSRule) ClientSubnet() *netip.Addr {
+	return r.clientSubnet
+}
+
 func (r *DefaultDNSRule) WithAddressLimit() bool {
 	if len(r.destinationIPCIDRItems) > 0 {
 		return true
@@ -264,6 +272,7 @@ type LogicalDNSRule struct {
 	abstractLogicalRule
 	disableCache bool
 	rewriteTTL   *uint32
+	clientSubnet *netip.Addr
 }
 
 func NewLogicalDNSRule(router adapter.Router, logger log.ContextLogger, options option.LogicalDNSRule) (*LogicalDNSRule, error) {
@@ -275,6 +284,7 @@ func NewLogicalDNSRule(router adapter.Router, logger log.ContextLogger, options
 		},
 		disableCache: options.DisableCache,
 		rewriteTTL:   options.RewriteTTL,
+		clientSubnet: (*netip.Addr)(options.ClientSubnet),
 	}
 	switch options.Mode {
 	case C.LogicalTypeAnd:
@@ -302,6 +312,10 @@ func (r *LogicalDNSRule) RewriteTTL() *uint32 {
 	return r.rewriteTTL
 }
 
+func (r *LogicalDNSRule) ClientSubnet() *netip.Addr {
+	return r.clientSubnet
+}
+
 func (r *LogicalDNSRule) WithAddressLimit() bool {
 	for _, rawRule := range r.rules {
 		switch rule := rawRule.(type) {

+ 21 - 25
transport/dhcp/server.go

@@ -21,9 +21,6 @@ import (
 	"github.com/sagernet/sing/common/buf"
 	"github.com/sagernet/sing/common/control"
 	E "github.com/sagernet/sing/common/exceptions"
-	"github.com/sagernet/sing/common/logger"
-	M "github.com/sagernet/sing/common/metadata"
-	N "github.com/sagernet/sing/common/network"
 	"github.com/sagernet/sing/common/task"
 	"github.com/sagernet/sing/common/x/list"
 
@@ -32,14 +29,14 @@ import (
 )
 
 func init() {
-	dns.RegisterTransport([]string{"dhcp"}, NewTransport)
+	dns.RegisterTransport([]string{"dhcp"}, func(options dns.TransportOptions) (dns.Transport, error) {
+		return NewTransport(options)
+	})
 }
 
 type Transport struct {
-	name              string
-	ctx               context.Context
+	options           dns.TransportOptions
 	router            adapter.Router
-	logger            logger.Logger
 	interfaceName     string
 	autoInterface     bool
 	interfaceCallback *list.Element[tun.DefaultInterfaceUpdateCallback]
@@ -48,23 +45,20 @@ type Transport struct {
 	updatedAt         time.Time
 }
 
-func NewTransport(name string, ctx context.Context, logger logger.ContextLogger, dialer N.Dialer, link string) (dns.Transport, error) {
-	linkURL, err := url.Parse(link)
+func NewTransport(options dns.TransportOptions) (*Transport, error) {
+	linkURL, err := url.Parse(options.Address)
 	if err != nil {
 		return nil, err
 	}
 	if linkURL.Host == "" {
 		return nil, E.New("missing interface name for DHCP")
 	}
-	router := adapter.RouterFromContext(ctx)
+	router := adapter.RouterFromContext(options.Context)
 	if router == nil {
 		return nil, E.New("missing router in context")
 	}
 	transport := &Transport{
-		name:          name,
-		ctx:           ctx,
 		router:        router,
-		logger:        logger,
 		interfaceName: linkURL.Host,
 		autoInterface: linkURL.Host == "auto",
 	}
@@ -72,7 +66,7 @@ func NewTransport(name string, ctx context.Context, logger logger.ContextLogger,
 }
 
 func (t *Transport) Name() string {
-	return t.name
+	return t.options.Name
 }
 
 func (t *Transport) Start() error {
@@ -158,8 +152,8 @@ func (t *Transport) updateServers() error {
 		return E.Cause(err, "dhcp: prepare interface")
 	}
 
-	t.logger.Info("dhcp: query DNS servers on ", iface.Name)
-	fetchCtx, cancel := context.WithTimeout(t.ctx, C.DHCPTimeout)
+	t.options.Logger.Info("dhcp: query DNS servers on ", iface.Name)
+	fetchCtx, cancel := context.WithTimeout(t.options.Context, C.DHCPTimeout)
 	err = t.fetchServers0(fetchCtx, iface)
 	cancel()
 	if err != nil {
@@ -175,7 +169,7 @@ func (t *Transport) updateServers() error {
 func (t *Transport) interfaceUpdated(int) {
 	err := t.updateServers()
 	if err != nil {
-		t.logger.Error("update servers: ", err)
+		t.options.Logger.Error("update servers: ", err)
 	}
 }
 
@@ -187,7 +181,7 @@ func (t *Transport) fetchServers0(ctx context.Context, iface *net.Interface) err
 	if runtime.GOOS == "linux" || runtime.GOOS == "android" {
 		listenAddr = "255.255.255.255:68"
 	}
-	packetConn, err := listener.ListenPacket(t.ctx, "udp4", listenAddr)
+	packetConn, err := listener.ListenPacket(t.options.Context, "udp4", listenAddr)
 	if err != nil {
 		return err
 	}
@@ -225,17 +219,17 @@ func (t *Transport) fetchServersResponse(iface *net.Interface, packetConn net.Pa
 
 		dhcpPacket, err := dhcpv4.FromBytes(buffer.Bytes())
 		if err != nil {
-			t.logger.Trace("dhcp: parse DHCP response: ", err)
+			t.options.Logger.Trace("dhcp: parse DHCP response: ", err)
 			return err
 		}
 
 		if dhcpPacket.MessageType() != dhcpv4.MessageTypeOffer {
-			t.logger.Trace("dhcp: expected OFFER response, but got ", dhcpPacket.MessageType())
+			t.options.Logger.Trace("dhcp: expected OFFER response, but got ", dhcpPacket.MessageType())
 			continue
 		}
 
 		if dhcpPacket.TransactionID != transactionID {
-			t.logger.Trace("dhcp: expected transaction ID ", transactionID, ", but got ", dhcpPacket.TransactionID)
+			t.options.Logger.Trace("dhcp: expected transaction ID ", transactionID, ", but got ", dhcpPacket.TransactionID)
 			continue
 		}
 
@@ -255,20 +249,22 @@ func (t *Transport) fetchServersResponse(iface *net.Interface, packetConn net.Pa
 
 func (t *Transport) recreateServers(iface *net.Interface, serverAddrs []netip.Addr) error {
 	if len(serverAddrs) > 0 {
-		t.logger.Info("dhcp: updated DNS servers from ", iface.Name, ": [", strings.Join(common.Map(serverAddrs, func(it netip.Addr) string {
+		t.options.Logger.Info("dhcp: updated DNS servers from ", iface.Name, ": [", strings.Join(common.Map(serverAddrs, func(it netip.Addr) string {
 			return it.String()
 		}), ","), "]")
 	}
-
 	serverDialer := common.Must1(dialer.NewDefault(t.router, option.DialerOptions{
 		BindInterface:      iface.Name,
 		UDPFragmentDefault: true,
 	}))
 	var transports []dns.Transport
 	for _, serverAddr := range serverAddrs {
-		serverTransport, err := dns.NewUDPTransport(t.name, t.ctx, serverDialer, M.Socksaddr{Addr: serverAddr, Port: 53})
+		newOptions := t.options
+		newOptions.Address = serverAddr.String()
+		newOptions.Dialer = serverDialer
+		serverTransport, err := dns.NewUDPTransport(newOptions)
 		if err != nil {
-			return err
+			return E.Cause(err, "create UDP transport from DHCP result: ", serverAddr)
 		}
 		transports = append(transports, serverTransport)
 	}

+ 7 - 6
transport/fakeip/server.go

@@ -9,7 +9,6 @@ import (
 	"github.com/sagernet/sing-dns"
 	E "github.com/sagernet/sing/common/exceptions"
 	"github.com/sagernet/sing/common/logger"
-	N "github.com/sagernet/sing/common/network"
 
 	mDNS "github.com/miekg/dns"
 )
@@ -20,7 +19,9 @@ var (
 )
 
 func init() {
-	dns.RegisterTransport([]string{"fakeip"}, NewTransport)
+	dns.RegisterTransport([]string{"fakeip"}, func(options dns.TransportOptions) (dns.Transport, error) {
+		return NewTransport(options)
+	})
 }
 
 type Transport struct {
@@ -30,15 +31,15 @@ type Transport struct {
 	logger logger.ContextLogger
 }
 
-func NewTransport(name string, ctx context.Context, logger logger.ContextLogger, dialer N.Dialer, link string) (dns.Transport, error) {
-	router := adapter.RouterFromContext(ctx)
+func NewTransport(options dns.TransportOptions) (*Transport, error) {
+	router := adapter.RouterFromContext(options.Context)
 	if router == nil {
 		return nil, E.New("missing router in context")
 	}
 	return &Transport{
-		name:   name,
+		name:   options.Name,
 		router: router,
-		logger: logger,
+		logger: options.Logger,
 	}, nil
 }