From a2df2cdc1f4f0fec39b456b67ceabdf235237394 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 24 Sep 2026 17:30:20 +0800 Subject: [PATCH] Add dns_server_address and dns_search_domain rule items --- adapter/dns.go | 6 + adapter/network.go | 11 +- dns/transport/dhcp/dhcp.go | 47 ++++++- dns/transport/local/local.go | 17 +++ dns/transport/local/local_resolved.go | 2 + dns/transport/local/local_resolved_linux.go | 16 ++- docs/configuration/dns/rule.md | 45 +++++++ docs/configuration/dns/rule.zh.md | 45 +++++++ docs/configuration/route/rule.md | 45 +++++++ docs/configuration/route/rule.zh.md | 45 +++++++ docs/schema.json | 128 ++++++++++++++++++++ experimental/libbox/dns.go | 30 +++++ experimental/libbox/platform.go | 9 +- experimental/libbox/service.go | 5 +- option/rule.go | 2 + option/rule_dns.go | 2 + protocol/openconnect/dns_transport.go | 22 ++++ protocol/openvpn/dns_transport.go | 24 ++++ protocol/tailscale/dns_transport.go | 29 +++++ route/rule/rule_default.go | 10 ++ route/rule/rule_dns.go | 10 ++ route/rule/rule_item_dns_search_domain.go | 72 +++++++++++ route/rule/rule_item_dns_server_address.go | 78 ++++++++++++ service/resolved/transport.go | 42 +++++++ 24 files changed, 722 insertions(+), 20 deletions(-) create mode 100644 route/rule/rule_item_dns_search_domain.go create mode 100644 route/rule/rule_item_dns_server_address.go diff --git a/adapter/dns.go b/adapter/dns.go index 8119097e8..9610e6c27 100644 --- a/adapter/dns.go +++ b/adapter/dns.go @@ -73,6 +73,12 @@ type DNSTransportWithPreferredDomain interface { PreferredDomain(domain string) bool } +type DNSTransportWithConfiguration interface { + DNSTransport + ServerAddresses() []netip.Addr + SearchDomains() []string +} + type DNSTransportWithEnvironment interface { DNSTransport Environment() []string diff --git a/adapter/network.go b/adapter/network.go index fc52bcd30..b8febf653 100644 --- a/adapter/network.go +++ b/adapter/network.go @@ -80,9 +80,10 @@ func NormalizeWIFIBSSID(bssid string) string { type NetworkInterface struct { control.Interface - Type C.InterfaceType - DNSServers []string - Gateways []netip.Addr - Expensive bool - Constrained bool + Type C.InterfaceType + DNSServers []string + DNSSearchDomains []string + Gateways []netip.Addr + Expensive bool + Constrained bool } diff --git a/dns/transport/dhcp/dhcp.go b/dns/transport/dhcp/dhcp.go index 329dc5375..da6ed3d2b 100644 --- a/dns/transport/dhcp/dhcp.go +++ b/dns/transport/dhcp/dhcp.go @@ -5,6 +5,7 @@ import ( "errors" "io" "net" + "net/netip" "runtime" "strings" "sync" @@ -38,8 +39,9 @@ func RegisterTransport(registry *dns.TransportRegistry) { } var ( - _ adapter.DNSTransport = (*Transport)(nil) - _ adapter.DNSTransportWithEnvironment = (*Transport)(nil) + _ adapter.DNSTransport = (*Transport)(nil) + _ adapter.DNSTransportWithConfiguration = (*Transport)(nil) + _ adapter.DNSTransportWithEnvironment = (*Transport)(nil) ) var errInterfaceIsCellular = E.New("interface is cellular") @@ -139,12 +141,37 @@ func (t *Transport) Start(stage adapter.StartStage, scope *adapter.Scope) error func (t *Transport) Reset() { t.refreshAccess.Lock() defer t.refreshAccess.Unlock() - state := t.savedState.Swap(nil) - if state != nil && state.serverScope != nil { + state := t.savedState.Load() + if state == nil { + return + } + t.savedState.Store(&transportState{ + search: state.search, + servers: state.servers, + }) + if state.serverScope != nil { state.serverScope.Close() } } +func (t *Transport) ServerAddresses() []netip.Addr { + state := t.loadState() + if state == nil { + return nil + } + return common.Map(state.servers, func(it M.Socksaddr) netip.Addr { + return it.Addr + }) +} + +func (t *Transport) SearchDomains() []string { + state := t.loadState() + if state == nil { + return nil + } + return state.search +} + func (t *Transport) Environment() []string { state := t.savedState.Load() if state == nil { @@ -207,14 +234,22 @@ func (t *Transport) exchangeCold(ctx context.Context, message *mDNS.Msg, callbac } func (t *Transport) Fetch() []M.Socksaddr { + state := t.loadState() + if state == nil { + return nil + } + return state.servers +} + +func (t *Transport) loadState() *transportState { state := t.savedState.Load() if state == nil || state.lastError != nil { return nil } - if len(state.servers) > 0 && time.Since(state.updatedAt) >= C.DHCPTTL { + if time.Since(state.updatedAt) >= C.DHCPTTL { t.startRefresh() } - return state.servers + return state } func (t *Transport) fetch(ctx context.Context) error { diff --git a/dns/transport/local/local.go b/dns/transport/local/local.go index 9c24768b4..ebc933af6 100644 --- a/dns/transport/local/local.go +++ b/dns/transport/local/local.go @@ -2,6 +2,7 @@ package local import ( "context" + "net/netip" "sync" "sync/atomic" @@ -12,8 +13,10 @@ import ( "github.com/sagernet/sing-box/dns/transport/mdns" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" + "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" mDNS "github.com/miekg/dns" @@ -26,6 +29,7 @@ func RegisterTransport(registry *dns.TransportRegistry) { var ( _ adapter.DNSTransport = (*Transport)(nil) _ adapter.DNSTransportWithPreferredDomain = (*Transport)(nil) + _ adapter.DNSTransportWithConfiguration = (*Transport)(nil) _ adapter.DNSTransportWithEnvironment = (*Transport)(nil) ) @@ -132,6 +136,19 @@ func (t *Transport) PreferredDomain(domain string) bool { return t.preferredResolver.PreferredDomain(domain) } +func (t *Transport) ServerAddresses() []netip.Addr { + if t.resolved != nil { + return t.resolved.ServerAddresses() + } + return common.Map(t.configSource.Configuration().Servers, func(it M.Socksaddr) netip.Addr { + return it.Addr + }) +} + +func (t *Transport) SearchDomains() []string { + return t.configSource.Configuration().Search +} + func (t *Transport) Environment() []string { if t.resolved != nil { return t.resolved.Environment() diff --git a/dns/transport/local/local_resolved.go b/dns/transport/local/local_resolved.go index efd5eabdb..c1bd92709 100644 --- a/dns/transport/local/local_resolved.go +++ b/dns/transport/local/local_resolved.go @@ -2,6 +2,7 @@ package local import ( "context" + "net/netip" mDNS "github.com/miekg/dns" ) @@ -11,5 +12,6 @@ type ResolvedResolver interface { Close() error Reset() Environment() []string + ServerAddresses() []netip.Addr ExchangeAsync(ctx context.Context, message *mDNS.Msg, callback func(response *mDNS.Msg, err error)) } diff --git a/dns/transport/local/local_resolved_linux.go b/dns/transport/local/local_resolved_linux.go index 0272854dc..82f04fd1f 100644 --- a/dns/transport/local/local_resolved_linux.go +++ b/dns/transport/local/local_resolved_linux.go @@ -69,9 +69,10 @@ type DBusResolvedResolver struct { } type resolvedServerSet struct { - scopes []*resolvedScope - signature []string - serverScope *adapter.Scope + scopes []*resolvedScope + serverAddresses []netip.Addr + signature []string + serverScope *adapter.Scope } // Match levels of dns_scope_good_domain() in systemd-resolved: a routing or search @@ -244,6 +245,14 @@ func (t *DBusResolvedResolver) Environment() []string { return serverSet.signature } +func (t *DBusResolvedResolver) ServerAddresses() []netip.Addr { + serverSet := t.savedServerSet.Load() + if serverSet == nil { + return nil + } + return serverSet.serverAddresses +} + func (t *DBusResolvedResolver) ExchangeAsync(ctx context.Context, message *mDNS.Msg, callback func(response *mDNS.Msg, err error)) { serverSet := t.savedServerSet.Load() if serverSet == nil { @@ -569,6 +578,7 @@ func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*resolvedServ } serverSet.scopes = append(serverSet.scopes, scope) for _, serverSpecification := range scopeSpecification.servers { + serverSet.serverAddresses = append(serverSet.serverAddresses, serverSpecification.address) serverSet.signature = append(serverSet.signature, scopeSpecification.interfaceName+"/"+M.SocksaddrFrom(serverSpecification.address, serverSpecification.port).String()) } } diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index 7a82f1888..fcb658ff4 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -2,6 +2,11 @@ icon: material/alert-decagram --- +!!! quote "Changes in sing-box 1.15.0" + + :material-plus: [dns_server_address](#dns_server_address) + :material-plus: [dns_search_domain](#dns_search_domain) + !!! quote "Changes in sing-box 1.14.0" :material-plus: [source_mac_address](#source_mac_address) @@ -178,6 +183,16 @@ icon: material/alert-decagram "local", "ts-dns" ], + "dns_server_address": { + "local": [ + "192.168.1.1/32" + ] + }, + "dns_search_domain": { + "ts-dns": [ + "example.ts.net" + ] + }, "wifi_ssid": [ "My WIFI" ], @@ -535,6 +550,36 @@ Match specified DNS servers' preferred domains. | `openconnect` | Match split DNS and search domains pushed by the VPN server | | `resolved` | Match split DNS and search domains from systemd-resolved links | +#### dns_server_address + +!!! question "Since sing-box 1.15.0" + +Match specified DNS servers' server addresses. + +| Type | Match | +|---------------|-----------------------------------------------| +| `local` | Match system DNS servers | +| `dhcp` | Match DNS servers from DHCP | +| `resolved` | Match DNS servers from systemd-resolved links | +| `tailscale` | Match DNS resolvers of the tailnet | +| `openvpn` | Match DNS servers pushed by the VPN server | +| `openconnect` | Match DNS servers pushed by the VPN server | + +#### dns_search_domain + +!!! question "Since sing-box 1.15.0" + +Match specified DNS servers' search domains. + +| Type | Match | +|---------------|--------------------------------------------------| +| `local` | Match system search domains | +| `dhcp` | Match search domains from DHCP | +| `resolved` | Match search domains from systemd-resolved links | +| `tailscale` | Match search domains of the tailnet | +| `openvpn` | Match search domains pushed by the VPN server | +| `openconnect` | Match search domains pushed by the VPN server | + #### wifi_ssid !!! quote "" diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md index 572f9d681..9fc6c31a7 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -2,6 +2,11 @@ icon: material/alert-decagram --- +!!! quote "sing-box 1.15.0 中的更改" + + :material-plus: [dns_server_address](#dns_server_address) + :material-plus: [dns_search_domain](#dns_search_domain) + !!! quote "sing-box 1.14.0 中的更改" :material-plus: [source_mac_address](#source_mac_address) @@ -178,6 +183,16 @@ icon: material/alert-decagram "local", "ts-dns" ], + "dns_server_address": { + "local": [ + "192.168.1.1/32" + ] + }, + "dns_search_domain": { + "ts-dns": [ + "example.ts.net" + ] + }, "wifi_ssid": [ "My WIFI" ], @@ -529,6 +544,36 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`. | `openconnect` | 匹配 VPN 服务器推送的分流 DNS 和搜索域 | | `resolved` | 匹配 systemd-resolved 链路中的分流域名和搜索域 | +#### dns_server_address + +!!! question "自 sing-box 1.15.0 起" + +匹配指定 DNS 服务器的服务器地址。 + +| 类型 | 匹配 | +|---------------|----------------------------------| +| `local` | 匹配系统 DNS 服务器 | +| `dhcp` | 匹配通过 DHCP 获取的 DNS 服务器 | +| `resolved` | 匹配 systemd-resolved 链路中的 DNS 服务器 | +| `tailscale` | 匹配 tailnet 的 DNS 解析器 | +| `openvpn` | 匹配 VPN 服务器推送的 DNS 服务器 | +| `openconnect` | 匹配 VPN 服务器推送的 DNS 服务器 | + +#### dns_search_domain + +!!! question "自 sing-box 1.15.0 起" + +匹配指定 DNS 服务器的搜索域。 + +| 类型 | 匹配 | +|---------------|-------------------------------| +| `local` | 匹配系统搜索域 | +| `dhcp` | 匹配通过 DHCP 获取的搜索域 | +| `resolved` | 匹配 systemd-resolved 链路中的搜索域 | +| `tailscale` | 匹配 tailnet 的搜索域 | +| `openvpn` | 匹配 VPN 服务器推送的搜索域 | +| `openconnect` | 匹配 VPN 服务器推送的搜索域 | + #### wifi_ssid !!! quote "" diff --git a/docs/configuration/route/rule.md b/docs/configuration/route/rule.md index 5e9909296..0a95a4fa6 100644 --- a/docs/configuration/route/rule.md +++ b/docs/configuration/route/rule.md @@ -2,6 +2,11 @@ icon: material/new-box --- +!!! quote "Changes in sing-box 1.15.0" + + :material-plus: [dns_server_address](#dns_server_address) + :material-plus: [dns_search_domain](#dns_search_domain) + !!! quote "Changes in sing-box 1.14.0" :material-plus: [source_mac_address](#source_mac_address) @@ -168,6 +173,16 @@ icon: material/new-box "tailscale", "wireguard" ], + "dns_server_address": { + "local": [ + "192.168.1.1/32" + ] + }, + "dns_search_domain": { + "ts-dns": [ + "example.ts.net" + ] + }, "source_mac_address": [ "00:11:22:33:44:55" ], @@ -471,6 +486,36 @@ Match specified outbounds' preferred routes. | `wireguard` | Match peers's allowed IPs | | `bridge` | Match all addresses except local addresses of the machine, only in [pre-match](/configuration/shared/pre-match/) | +#### dns_server_address + +!!! question "Since sing-box 1.15.0" + +Match specified DNS servers' server addresses. + +| Type | Match | +|---------------|-----------------------------------------------| +| `local` | Match system DNS servers | +| `dhcp` | Match DNS servers from DHCP | +| `resolved` | Match DNS servers from systemd-resolved links | +| `tailscale` | Match DNS resolvers of the tailnet | +| `openvpn` | Match DNS servers pushed by the VPN server | +| `openconnect` | Match DNS servers pushed by the VPN server | + +#### dns_search_domain + +!!! question "Since sing-box 1.15.0" + +Match specified DNS servers' search domains. + +| Type | Match | +|---------------|--------------------------------------------------| +| `local` | Match system search domains | +| `dhcp` | Match search domains from DHCP | +| `resolved` | Match search domains from systemd-resolved links | +| `tailscale` | Match search domains of the tailnet | +| `openvpn` | Match search domains pushed by the VPN server | +| `openconnect` | Match search domains pushed by the VPN server | + #### source_mac_address !!! question "Since sing-box 1.14.0" diff --git a/docs/configuration/route/rule.zh.md b/docs/configuration/route/rule.zh.md index c280d368e..83571f8c9 100644 --- a/docs/configuration/route/rule.zh.md +++ b/docs/configuration/route/rule.zh.md @@ -2,6 +2,11 @@ icon: material/new-box --- +!!! quote "sing-box 1.15.0 中的更改" + + :material-plus: [dns_server_address](#dns_server_address) + :material-plus: [dns_search_domain](#dns_search_domain) + !!! quote "sing-box 1.14.0 中的更改" :material-plus: [source_mac_address](#source_mac_address) @@ -166,6 +171,16 @@ icon: material/new-box "tailscale", "wireguard" ], + "dns_server_address": { + "local": [ + "192.168.1.1/32" + ] + }, + "dns_search_domain": { + "ts-dns": [ + "example.ts.net" + ] + }, "source_mac_address": [ "00:11:22:33:44:55" ], @@ -469,6 +484,36 @@ icon: material/new-box | `wireguard` | 匹配对端的 allowed IPs | | `bridge` | 匹配除本机本地地址外的所有地址,仅在[预匹配](/zh/configuration/shared/pre-match/)中 | +#### dns_server_address + +!!! question "自 sing-box 1.15.0 起" + +匹配指定 DNS 服务器的服务器地址。 + +| 类型 | 匹配 | +|---------------|----------------------------------| +| `local` | 匹配系统 DNS 服务器 | +| `dhcp` | 匹配通过 DHCP 获取的 DNS 服务器 | +| `resolved` | 匹配 systemd-resolved 链路中的 DNS 服务器 | +| `tailscale` | 匹配 tailnet 的 DNS 解析器 | +| `openvpn` | 匹配 VPN 服务器推送的 DNS 服务器 | +| `openconnect` | 匹配 VPN 服务器推送的 DNS 服务器 | + +#### dns_search_domain + +!!! question "自 sing-box 1.15.0 起" + +匹配指定 DNS 服务器的搜索域。 + +| 类型 | 匹配 | +|---------------|-------------------------------| +| `local` | 匹配系统搜索域 | +| `dhcp` | 匹配通过 DHCP 获取的搜索域 | +| `resolved` | 匹配 systemd-resolved 链路中的搜索域 | +| `tailscale` | 匹配 tailnet 的搜索域 | +| `openvpn` | 匹配 VPN 服务器推送的搜索域 | +| `openconnect` | 匹配 VPN 服务器推送的搜索域 | + #### source_mac_address !!! question "自 sing-box 1.14.0 起" diff --git a/docs/schema.json b/docs/schema.json index 81da83aaf..4e0f7eab7 100644 --- a/docs/schema.json +++ b/docs/schema.json @@ -1513,6 +1513,38 @@ } ] }, + "dns_server_address": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/IPAddressOrPrefix" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/IPAddressOrPrefix" + } + } + ] + } + }, + "dns_search_domain": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + }, "rule_set": { "anyOf": [ { @@ -9811,6 +9843,38 @@ } ] }, + "dns_server_address": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/IPAddressOrPrefix" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/IPAddressOrPrefix" + } + } + ] + } + }, + "dns_search_domain": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + }, "rule_set": { "anyOf": [ { @@ -10428,6 +10492,38 @@ } ] }, + "dns_server_address": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/IPAddressOrPrefix" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/IPAddressOrPrefix" + } + } + ] + } + }, + "dns_search_domain": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + }, "rule_set": { "anyOf": [ { @@ -15601,6 +15697,38 @@ } ] }, + "dns_server_address": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/IPAddressOrPrefix" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/IPAddressOrPrefix" + } + } + ] + } + }, + "dns_search_domain": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + }, "rule_set": { "anyOf": [ { diff --git a/experimental/libbox/dns.go b/experimental/libbox/dns.go index 3b96b621e..6a6ae0aef 100644 --- a/experimental/libbox/dns.go +++ b/experimental/libbox/dns.go @@ -58,6 +58,35 @@ func (p *platformTransport) PreferredDomain(domain string) bool { return p.preferredResolver.PreferredDomain(domain) } +func (p *platformTransport) ServerAddresses() []netip.Addr { + if p.networkManager == nil { + return nil + } + defaultInterface := p.networkManager.DefaultNetworkInterface() + if defaultInterface == nil { + return nil + } + var serverAddresses []netip.Addr + for _, server := range defaultInterface.DNSServers { + serverAddr, err := netip.ParseAddr(server) + if err == nil { + serverAddresses = append(serverAddresses, serverAddr) + } + } + return serverAddresses +} + +func (p *platformTransport) SearchDomains() []string { + if p.networkManager == nil { + return nil + } + defaultInterface := p.networkManager.DefaultNetworkInterface() + if defaultInterface == nil { + return nil + } + return defaultInterface.DNSSearchDomains +} + func (p *platformTransport) Environment() []string { if p.networkManager == nil { return nil @@ -180,5 +209,6 @@ func (c *ExchangeContext) ErrnoCode(code int32) { var ( _ adapter.DNSTransport = (*platformTransport)(nil) _ adapter.DNSTransportWithPreferredDomain = (*platformTransport)(nil) + _ adapter.DNSTransportWithConfiguration = (*platformTransport)(nil) _ adapter.DNSTransportWithEnvironment = (*platformTransport)(nil) ) diff --git a/experimental/libbox/platform.go b/experimental/libbox/platform.go index 19759b3c6..f0e465f9a 100644 --- a/experimental/libbox/platform.go +++ b/experimental/libbox/platform.go @@ -129,10 +129,11 @@ type NetworkInterface struct { Addresses StringIterator Flags int32 - Type int32 - DNSServer StringIterator - Gateway StringIterator - Metered bool + Type int32 + DNSServer StringIterator + DNSSearchDomain StringIterator + Gateway StringIterator + Metered bool } type WIFIState struct { diff --git a/experimental/libbox/service.go b/experimental/libbox/service.go index 628ab8da1..982e1a283 100644 --- a/experimental/libbox/service.go +++ b/experimental/libbox/service.go @@ -143,8 +143,9 @@ func (w *platformInterfaceWrapper) NetworkInterfaces() ([]adapter.NetworkInterfa Addresses: common.Map(iteratorToArray[string](netInterface.Addresses), netip.MustParsePrefix), Flags: linkFlags(uint32(netInterface.Flags)), }, - Type: C.InterfaceType(netInterface.Type), - DNSServers: iteratorToArray[string](netInterface.DNSServer), + Type: C.InterfaceType(netInterface.Type), + DNSServers: iteratorToArray[string](netInterface.DNSServer), + DNSSearchDomains: iteratorToArray[string](netInterface.DNSSearchDomain), Gateways: common.Filter(common.Map(iteratorToArray[string](netInterface.Gateway), func(it string) netip.Addr { gateway, _ := netip.ParseAddr(it) return gateway.Unmap().WithZone("") diff --git a/option/rule.go b/option/rule.go index a7e34c3fe..9af85fa94 100644 --- a/option/rule.go +++ b/option/rule.go @@ -178,6 +178,8 @@ type RawDefaultRule struct { SourceMACAddress badoption.Listable[string] `json:"source_mac_address,omitempty"` SourceHostname badoption.Listable[string] `json:"source_hostname,omitempty"` PreferredBy badoption.Listable[string] `json:"preferred_by,omitempty"` + DNSServerAddress *badjson.TypedMap[string, badoption.Listable[*badoption.Prefixable]] `json:"dns_server_address,omitempty"` + DNSSearchDomain *badjson.TypedMap[string, badoption.Listable[string]] `json:"dns_search_domain,omitempty"` RuleSet badoption.Listable[string] `json:"rule_set,omitempty" reference:"rule_set"` RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"` Invert bool `json:"invert,omitempty"` diff --git a/option/rule_dns.go b/option/rule_dns.go index ed7d982e1..841c82d9e 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -173,6 +173,8 @@ type RawDefaultDNSRule struct { SourceMACAddress badoption.Listable[string] `json:"source_mac_address,omitempty"` SourceHostname badoption.Listable[string] `json:"source_hostname,omitempty"` PreferredBy badoption.Listable[string] `json:"preferred_by,omitempty"` + DNSServerAddress *badjson.TypedMap[string, badoption.Listable[*badoption.Prefixable]] `json:"dns_server_address,omitempty"` + DNSSearchDomain *badjson.TypedMap[string, badoption.Listable[string]] `json:"dns_search_domain,omitempty"` RuleSet badoption.Listable[string] `json:"rule_set,omitempty" reference:"rule_set"` RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"` MatchResponse *DNSRuleMatchResponse `json:"match_response,omitempty"` diff --git a/protocol/openconnect/dns_transport.go b/protocol/openconnect/dns_transport.go index ae70ee232..3c791f6d3 100644 --- a/protocol/openconnect/dns_transport.go +++ b/protocol/openconnect/dns_transport.go @@ -41,6 +41,7 @@ type DNSTransport struct { closed bool routes []openConnectDNSRoute searchDomains []string + serverAddresses []netip.Addr defaultResolvers []adapter.DNSTransport resolverScope *adapter.Scope } @@ -83,6 +84,7 @@ func (t *DNSTransport) Start(stage adapter.StartStage, scope *adapter.Scope) err t.closed = true t.routes = nil t.searchDomains = nil + t.serverAddresses = nil t.defaultResolvers = nil t.resolverScope = nil t.access.Unlock() @@ -116,6 +118,7 @@ func (t *DNSTransport) Start(stage adapter.StartStage, scope *adapter.Scope) err func (t *DNSTransport) updateConfiguration(configuration openconnecttransport.Configuration) { resolverByAddress := make(map[netip.Addr]adapter.DNSTransport) + var serverAddresses []netip.Addr resolverFor := func(address netip.Addr) adapter.DNSTransport { if !address.IsValid() { return nil @@ -124,6 +127,7 @@ func (t *DNSTransport) updateConfiguration(configuration openconnecttransport.Co if loaded { return resolver } + serverAddresses = append(serverAddresses, address) resolver = transport.NewUDPRaw( t.logger, dns.NewTransportAdapter(C.DNSTypeUDP, t.Tag()+"/"+address.String(), nil), @@ -217,6 +221,7 @@ func (t *DNSTransport) updateConfiguration(configuration openconnecttransport.Co oldResolverScope := t.resolverScope t.routes = routes t.searchDomains = searchDomains + t.serverAddresses = serverAddresses t.defaultResolvers = defaultResolvers t.resolverScope = resolverScope t.access.Unlock() @@ -239,6 +244,18 @@ func (t *DNSTransport) Reset() { } } +func (t *DNSTransport) ServerAddresses() []netip.Addr { + t.access.RLock() + defer t.access.RUnlock() + return t.serverAddresses +} + +func (t *DNSTransport) SearchDomains() []string { + t.access.RLock() + defer t.access.RUnlock() + return t.searchDomains +} + func (t *DNSTransport) PreferredDomain(domain string) bool { canonicalDomain := mDNS.Fqdn(canonicalOpenConnectDomain(domain)) t.access.RLock() @@ -369,3 +386,8 @@ func (t *DNSTransport) collectResolversLocked() []adapter.DNSTransport { } return resolvers } + +var ( + _ adapter.DNSTransportWithPreferredDomain = (*DNSTransport)(nil) + _ adapter.DNSTransportWithConfiguration = (*DNSTransport)(nil) +) diff --git a/protocol/openvpn/dns_transport.go b/protocol/openvpn/dns_transport.go index 41461979d..a9649523c 100644 --- a/protocol/openvpn/dns_transport.go +++ b/protocol/openvpn/dns_transport.go @@ -49,6 +49,7 @@ type DNSTransport struct { closed bool routes map[string][]adapter.DNSTransport searchDomains []string + serverAddresses []netip.Addr defaultResolvers []adapter.DNSTransport resolverScope *adapter.Scope } @@ -88,6 +89,7 @@ func (t *DNSTransport) Start(stage adapter.StartStage, scope *adapter.Scope) err t.closed = true t.routes = nil t.searchDomains = nil + t.serverAddresses = nil t.defaultResolvers = nil t.resolverScope = nil t.access.Unlock() @@ -133,6 +135,7 @@ func (t *DNSTransport) updateResolvers(configuration ovpntransport.Configuration return left.Priority - right.Priority }) var selectedResolvers []adapter.DNSTransport + var serverAddresses []netip.Addr if len(servers) > 0 { server := servers[0] if server.DNSSEC == "yes" { @@ -148,6 +151,7 @@ func (t *DNSTransport) updateResolvers(configuration ovpntransport.Configuration return t.failResolverUpdate(resolverScope, err) } selectedResolvers = append(selectedResolvers, resolver) + serverAddresses = append(serverAddresses, address.Addr()) } if len(selectedResolvers) == 0 { return t.failResolverUpdate(resolverScope, E.New("DNS server ", server.Priority, " has no addresses")) @@ -170,6 +174,7 @@ func (t *DNSTransport) updateResolvers(configuration ovpntransport.Configuration return t.failResolverUpdate(resolverScope, err) } selectedResolvers = append(selectedResolvers, resolver) + serverAddresses = append(serverAddresses, address) } if len(configuration.DNSRoutes) > 0 { if len(selectedResolvers) == 0 { @@ -196,6 +201,7 @@ func (t *DNSTransport) updateResolvers(configuration ovpntransport.Configuration oldResolverScope := t.resolverScope t.routes = routes t.searchDomains = searchDomains + t.serverAddresses = serverAddresses t.defaultResolvers = defaultResolvers t.resolverScope = resolverScope t.access.Unlock() @@ -213,6 +219,7 @@ func (t *DNSTransport) failResolverUpdate(resolverScope *adapter.Scope, updateEr oldResolverScope := t.resolverScope t.routes = nil t.searchDomains = nil + t.serverAddresses = nil t.defaultResolvers = nil t.resolverScope = nil t.access.Unlock() @@ -290,6 +297,18 @@ func (t *DNSTransport) Raw() bool { return true } +func (t *DNSTransport) ServerAddresses() []netip.Addr { + t.access.RLock() + defer t.access.RUnlock() + return t.serverAddresses +} + +func (t *DNSTransport) SearchDomains() []string { + t.access.RLock() + defer t.access.RUnlock() + return t.searchDomains +} + func (t *DNSTransport) PreferredDomain(domain string) bool { t.access.RLock() defer t.access.RUnlock() @@ -428,3 +447,8 @@ func restoreOpenVPNOriginalQuestion(response *mDNS.Msg, expandedName string, ori } } } + +var ( + _ adapter.DNSTransportWithPreferredDomain = (*DNSTransport)(nil) + _ adapter.DNSTransportWithConfiguration = (*DNSTransport)(nil) +) diff --git a/protocol/tailscale/dns_transport.go b/protocol/tailscale/dns_transport.go index 10fde65fc..b9bb604ef 100644 --- a/protocol/tailscale/dns_transport.go +++ b/protocol/tailscale/dns_transport.go @@ -58,6 +58,7 @@ type DNSTransport struct { hosts map[string][]netip.Addr magicHosts nDNSResolver.MagicDNSHosts searchDomains []string + serverAddresses []netip.Addr defaultResolvers []adapter.DNSTransport resolverScope *adapter.Scope } @@ -102,6 +103,7 @@ func (t *DNSTransport) Start(stage adapter.StartStage, scope *adapter.Scope) err t.routes = nil t.hosts = nil t.magicHosts = nil + t.serverAddresses = nil t.defaultResolvers = nil t.resolverScope = nil t.access.Unlock() @@ -137,6 +139,7 @@ func (t *DNSTransport) updateDNSServers(routeConfig *router.Config, dnsConfig *n }) resolverScope := adapter.NewScope(t.ctx, t.logger) routes := make(map[string][]adapter.DNSTransport) + var serverAddresses []netip.Addr for domain, resolvers := range dnsConfig.Routes { var myResolvers []adapter.DNSTransport for _, resolver := range resolvers { @@ -145,6 +148,10 @@ func (t *DNSTransport) updateDNSServers(routeConfig *router.Config, dnsConfig *n return E.Errors(err, resolverScope.Close()) } myResolvers = append(myResolvers, myResolver) + serverAddrPort, isIPResolver := resolver.IPPort() + if isIPResolver { + serverAddresses = append(serverAddresses, serverAddrPort.Addr()) + } } routes[mDNS.CanonicalName(domain.WithTrailingDot())] = myResolvers } @@ -162,6 +169,10 @@ func (t *DNSTransport) updateDNSServers(routeConfig *router.Config, dnsConfig *n return E.Errors(err, resolverScope.Close()) } defaultResolvers = append(defaultResolvers, myResolver) + serverAddrPort, isIPResolver := resolver.IPPort() + if isIPResolver { + serverAddresses = append(serverAddresses, serverAddrPort.Addr()) + } } t.access.Lock() @@ -171,6 +182,7 @@ func (t *DNSTransport) updateDNSServers(routeConfig *router.Config, dnsConfig *n t.hosts = hosts t.magicHosts = t.endpoint.server.ExportLocalBackend().ExportMagicDNSHosts() t.searchDomains = searchDomains + t.serverAddresses = common.Uniq(serverAddresses) t.defaultResolvers = defaultResolvers t.resolverScope = resolverScope t.access.Unlock() @@ -285,6 +297,18 @@ func (t *DNSTransport) Raw() bool { return true } +func (t *DNSTransport) ServerAddresses() []netip.Addr { + t.access.RLock() + defer t.access.RUnlock() + return t.serverAddresses +} + +func (t *DNSTransport) SearchDomains() []string { + t.access.RLock() + defer t.access.RUnlock() + return t.searchDomains +} + func (t *DNSTransport) PreferredDomain(domain string) bool { t.access.RLock() hosts := t.hosts @@ -515,3 +539,8 @@ func (t *DNSTransport) routePrefixesSnapshot() []netip.Prefix { defer t.access.RUnlock() return append([]netip.Prefix(nil), t.routePrefixes...) } + +var ( + _ adapter.DNSTransportWithPreferredDomain = (*DNSTransport)(nil) + _ adapter.DNSTransportWithConfiguration = (*DNSTransport)(nil) +) diff --git a/route/rule/rule_default.go b/route/rule/rule_default.go index 3aef4a1c8..da6527d84 100644 --- a/route/rule/rule_default.go +++ b/route/rule/rule_default.go @@ -283,6 +283,16 @@ func NewDefaultRule(ctx context.Context, logger log.ContextLogger, options optio rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if options.DNSServerAddress != nil && options.DNSServerAddress.Size() > 0 { + item := NewDNSServerAddressItem(ctx, options.DNSServerAddress) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if options.DNSSearchDomain != nil && options.DNSSearchDomain.Size() > 0 { + item := NewDNSSearchDomainItem(ctx, options.DNSSearchDomain) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } if len(options.RuleSet) > 0 { //nolint:staticcheck if options.Deprecated_RulesetIPCIDRMatchSource { diff --git a/route/rule/rule_dns.go b/route/rule/rule_dns.go index a7f3bcf51..c7a8c216f 100644 --- a/route/rule/rule_dns.go +++ b/route/rule/rule_dns.go @@ -377,6 +377,16 @@ func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options op rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if options.DNSServerAddress != nil && options.DNSServerAddress.Size() > 0 { + item := NewDNSServerAddressItem(ctx, options.DNSServerAddress) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if options.DNSSearchDomain != nil && options.DNSSearchDomain.Size() > 0 { + item := NewDNSSearchDomainItem(ctx, options.DNSSearchDomain) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } if options.RuleSetIPCIDRAcceptEmpty { //nolint:staticcheck if legacyDNSMode { deprecated.Report(ctx, deprecated.OptionRuleSetIPCIDRAcceptEmpty) diff --git a/route/rule/rule_item_dns_search_domain.go b/route/rule/rule_item_dns_search_domain.go new file mode 100644 index 000000000..bb0ce64a9 --- /dev/null +++ b/route/rule/rule_item_dns_search_domain.go @@ -0,0 +1,72 @@ +package rule + +import ( + "context" + "slices" + "strings" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json/badjson" + "github.com/sagernet/sing/common/json/badoption" + "github.com/sagernet/sing/service" + + mDNS "github.com/miekg/dns" +) + +var _ RuleItem = (*DNSSearchDomainItem)(nil) + +type DNSSearchDomainItem struct { + ctx context.Context + transportTags []string + searchDomains [][]string + transports []adapter.DNSTransportWithConfiguration + description string +} + +func NewDNSSearchDomainItem(ctx context.Context, searchDomains *badjson.TypedMap[string, badoption.Listable[string]]) *DNSSearchDomainItem { + item := &DNSSearchDomainItem{ + ctx: ctx, + } + var entryDescriptions []string + for _, entry := range searchDomains.Entries() { + item.transportTags = append(item.transportTags, entry.Key) + item.searchDomains = append(item.searchDomains, common.Map(entry.Value, mDNS.CanonicalName)) + entryDescriptions = append(entryDescriptions, entry.Key+"="+strings.Join(entry.Value, ",")) + } + item.description = "dns_search_domain=[" + strings.Join(entryDescriptions, " ") + "]" + return item +} + +func (r *DNSSearchDomainItem) Start() error { + transportManager := service.FromContext[adapter.DNSTransportManager](r.ctx) + for _, transportTag := range r.transportTags { + rawTransport, loaded := transportManager.Transport(transportTag) + if !loaded { + return E.New("DNS server not found: ", transportTag) + } + transportWithConfiguration, withConfiguration := rawTransport.(adapter.DNSTransportWithConfiguration) + if !withConfiguration { + return E.New("DNS server type does not support dns_search_domain: ", rawTransport.Type()) + } + r.transports = append(r.transports, transportWithConfiguration) + } + return nil +} + +func (r *DNSSearchDomainItem) Match(metadata *adapter.InboundContext) bool { + for i, transport := range r.transports { + domains := r.searchDomains[i] + if !slices.ContainsFunc(transport.SearchDomains(), func(searchDomain string) bool { + return slices.Contains(domains, mDNS.CanonicalName(searchDomain)) + }) { + return false + } + } + return true +} + +func (r *DNSSearchDomainItem) String() string { + return r.description +} diff --git a/route/rule/rule_item_dns_server_address.go b/route/rule/rule_item_dns_server_address.go new file mode 100644 index 000000000..de2a90736 --- /dev/null +++ b/route/rule/rule_item_dns_server_address.go @@ -0,0 +1,78 @@ +package rule + +import ( + "context" + "net/netip" + "slices" + "strings" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json/badjson" + "github.com/sagernet/sing/common/json/badoption" + "github.com/sagernet/sing/service" +) + +var _ RuleItem = (*DNSServerAddressItem)(nil) + +type DNSServerAddressItem struct { + ctx context.Context + transportTags []string + serverAddresses [][]netip.Prefix + transports []adapter.DNSTransportWithConfiguration + description string +} + +func NewDNSServerAddressItem(ctx context.Context, serverAddresses *badjson.TypedMap[string, badoption.Listable[*badoption.Prefixable]]) *DNSServerAddressItem { + item := &DNSServerAddressItem{ + ctx: ctx, + } + var entryDescriptions []string + for _, entry := range serverAddresses.Entries() { + prefixes := make([]netip.Prefix, 0, len(entry.Value)) + for _, prefixable := range entry.Value { + prefixes = append(prefixes, prefixable.Build(netip.Prefix{})) + } + item.transportTags = append(item.transportTags, entry.Key) + item.serverAddresses = append(item.serverAddresses, prefixes) + entryDescriptions = append(entryDescriptions, entry.Key+"="+strings.Join(common.Map(prefixes, netip.Prefix.String), ",")) + } + item.description = "dns_server_address=[" + strings.Join(entryDescriptions, " ") + "]" + return item +} + +func (r *DNSServerAddressItem) Start() error { + transportManager := service.FromContext[adapter.DNSTransportManager](r.ctx) + for _, transportTag := range r.transportTags { + rawTransport, loaded := transportManager.Transport(transportTag) + if !loaded { + return E.New("DNS server not found: ", transportTag) + } + transportWithConfiguration, withConfiguration := rawTransport.(adapter.DNSTransportWithConfiguration) + if !withConfiguration { + return E.New("DNS server type does not support dns_server_address: ", rawTransport.Type()) + } + r.transports = append(r.transports, transportWithConfiguration) + } + return nil +} + +func (r *DNSServerAddressItem) Match(metadata *adapter.InboundContext) bool { + for i, transport := range r.transports { + prefixes := r.serverAddresses[i] + if !slices.ContainsFunc(transport.ServerAddresses(), func(address netip.Addr) bool { + unwrappedAddress := address.Unmap().WithZone("") + return slices.ContainsFunc(prefixes, func(prefix netip.Prefix) bool { + return prefix.Contains(unwrappedAddress) + }) + }) { + return false + } + } + return true +} + +func (r *DNSServerAddressItem) String() string { + return r.description +} diff --git a/service/resolved/transport.go b/service/resolved/transport.go index 34dfa3329..4c78eaf47 100644 --- a/service/resolved/transport.go +++ b/service/resolved/transport.go @@ -36,6 +36,7 @@ func RegisterTransport(registry *dns.TransportRegistry) { var ( _ adapter.DNSTransport = (*Transport)(nil) _ adapter.DNSTransportWithPreferredDomain = (*Transport)(nil) + _ adapter.DNSTransportWithConfiguration = (*Transport)(nil) _ adapter.DNSTransportWithEnvironment = (*Transport)(nil) ) @@ -165,6 +166,47 @@ func (t *Transport) Environment() []string { return environment } +func (t *Transport) ServerAddresses() []netip.Addr { + if t.service == nil { + return nil + } + t.service.linkAccess.RLock() + defer t.service.linkAccess.RUnlock() + var serverAddresses []netip.Addr + for _, link := range t.service.links { + for _, address := range link.address { + serverAddr, loaded := netip.AddrFromSlice(address.Address) + if loaded { + serverAddresses = append(serverAddresses, serverAddr) + } + } + for _, address := range link.addressEx { + serverAddr, loaded := netip.AddrFromSlice(address.Address) + if loaded { + serverAddresses = append(serverAddresses, serverAddr) + } + } + } + return serverAddresses +} + +func (t *Transport) SearchDomains() []string { + if t.service == nil { + return nil + } + t.service.linkAccess.RLock() + defer t.service.linkAccess.RUnlock() + var searchDomains []string + for _, link := range t.service.links { + for _, domain := range link.domain { + if !domain.RoutingOnly && domain.Domain != "." { + searchDomains = append(searchDomains, domain.Domain) + } + } + } + return searchDomains +} + func (t *Transport) updateTransports(link *TransportLink) error { t.linkAccess.Lock() defer t.linkAccess.Unlock()