diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index 897bb09b8..1fc4eafaf 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -513,6 +513,7 @@ Match specified DNS servers' preferred domains. | `local` | Match hosts entries, neighbor-resolved hosts, and mDNS local domains | | `mdns` | Match mDNS local domains (`*.local.` and IPv4/IPv6 link-local reverse zones) | | `tailscale` | Match MagicDNS hosts and DNS route suffixes | +| `resolved` | Match split DNS and search domains from systemd-resolved links | #### wifi_ssid diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md index 55e229805..c56e7f5bf 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -505,6 +505,7 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`. | `local` | 匹配 hosts 中的条目、邻居解析得到的主机名以及 mDNS 本地域名 | | `mdns` | 匹配 mDNS 本地域名(`*.local.` 以及 IPv4/IPv6 链路本地反向区域) | | `tailscale` | 匹配 MagicDNS 主机和 DNS 路由后缀 | +| `resolved` | 匹配 systemd-resolved 链路中的分流域名和搜索域 | #### wifi_ssid diff --git a/docs/configuration/dns/server/hosts.md b/docs/configuration/dns/server/hosts.md index da76f6192..2b2cdee63 100644 --- a/docs/configuration/dns/server/hosts.md +++ b/docs/configuration/dns/server/hosts.md @@ -89,13 +89,9 @@ Example: ], "rules": [ { - "action": "evaluate", + "preferred_by": "hosts", + "action": "route", "server": "hosts" - }, - { - "match_response": true, - "ip_accept_any": true, - "action": "respond" } ] } diff --git a/docs/configuration/dns/server/hosts.zh.md b/docs/configuration/dns/server/hosts.zh.md index 3384adc74..bd4ae7638 100644 --- a/docs/configuration/dns/server/hosts.zh.md +++ b/docs/configuration/dns/server/hosts.zh.md @@ -89,13 +89,9 @@ hosts 文件路径列表。 ], "rules": [ { - "action": "evaluate", + "preferred_by": "hosts", + "action": "route", "server": "hosts" - }, - { - "match_response": true, - "ip_accept_any": true, - "action": "respond" } ] } diff --git a/docs/configuration/dns/server/resolved.md b/docs/configuration/dns/server/resolved.md index 75835c6b8..44a4123c4 100644 --- a/docs/configuration/dns/server/resolved.md +++ b/docs/configuration/dns/server/resolved.md @@ -61,13 +61,9 @@ If not enabled, `NXDOMAIN` will be returned for requests that do not match searc ], "rules": [ { - "action": "evaluate", + "preferred_by": "resolved", + "action": "route", "server": "resolved" - }, - { - "match_response": true, - "ip_accept_any": true, - "action": "respond" } ] } diff --git a/docs/configuration/dns/server/resolved.zh.md b/docs/configuration/dns/server/resolved.zh.md index 8747e8313..6102aa809 100644 --- a/docs/configuration/dns/server/resolved.zh.md +++ b/docs/configuration/dns/server/resolved.zh.md @@ -60,13 +60,9 @@ icon: material/new-box ], "rules": [ { - "action": "evaluate", + "preferred_by": "resolved", + "action": "route", "server": "resolved" - }, - { - "match_response": true, - "ip_accept_any": true, - "action": "respond" } ] } diff --git a/docs/configuration/dns/server/tailscale.md b/docs/configuration/dns/server/tailscale.md index b2169ed38..d28a11c63 100644 --- a/docs/configuration/dns/server/tailscale.md +++ b/docs/configuration/dns/server/tailscale.md @@ -73,13 +73,9 @@ Default resolvers are not consulted for single-label queries regardless of `acce ], "rules": [ { - "action": "evaluate", + "preferred_by": "ts", + "action": "route", "server": "ts" - }, - { - "match_response": true, - "ip_accept_any": true, - "action": "respond" } ] } diff --git a/docs/configuration/dns/server/tailscale.zh.md b/docs/configuration/dns/server/tailscale.zh.md index e0086653d..40fae69cc 100644 --- a/docs/configuration/dns/server/tailscale.zh.md +++ b/docs/configuration/dns/server/tailscale.zh.md @@ -73,13 +73,9 @@ icon: material/new-box ], "rules": [ { - "action": "evaluate", + "preferred_by": "ts", + "action": "route", "server": "ts" - }, - { - "match_response": true, - "ip_accept_any": true, - "action": "respond" } ] } diff --git a/docs/configuration/inbound/tun.md b/docs/configuration/inbound/tun.md index 6dae06e18..9af118b68 100644 --- a/docs/configuration/inbound/tun.md +++ b/docs/configuration/inbound/tun.md @@ -5,7 +5,9 @@ icon: material/new-box !!! quote "Changes in sing-box 1.14.0" :material-plus: [include_mac_address](#include_mac_address) - :material-plus: [exclude_mac_address](#exclude_mac_address) + :material-plus: [exclude_mac_address](#exclude_mac_address) + :material-plus: [dns_mode](#dns_mode) + :material-plus: [dns_address](#dns_address) !!! quote "Changes in sing-box 1.13.3" @@ -73,6 +75,11 @@ icon: material/new-box "fdfe:dcba:9876::1/126" ], "mtu": 9000, + "dns_mode": "hijack", + "dns_address": [ + "172.18.0.2", + "fdfe:dcba:9876::2" + ], "auto_route": true, "iproute2_table_index": 2022, "iproute2_rule_index": 9000, @@ -216,6 +223,52 @@ IPv6 prefix for the tun interface. The maximum transmission unit. +#### dns_mode + +!!! question "Since sing-box 1.14.0" + +How DNS is handled on the TUN interface. + +| Mode | Description | +|------------|------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `disabled` | Do not configure native DNS and do not hijack DNS traffic. | +| `native` | Set the platform's native interface DNS where possible: per-interface DNS on Windows and Apple platforms, and `systemd-resolved` interface DNS on Linux. | +| `hijack` | Same as `native`, with additional port 53 hijacking described below. Used by default. | + +`hijack` adds the following on top of `native`: + +*On Linux*: only DNS sent to non-local destinations can be intercepted. +Traffic destined to addresses on the host's own interfaces (such as +`127.0.0.53` or the host's LAN-side IP) is delivered through the kernel +`local` routing table before any user rule applies, and `OUTPUT` NAT cannot +redirect packets going through `lo`. + +- Without `auto_redirect`, an `iproute2` rule makes port 53 skip the `main` + table's specific-route lookup, forcing DNS that would otherwise be + delivered through a directly-attached subnet through the TUN. Destination + addresses are not rewritten. +- With `auto_redirect`, an nftables rule DNATs port 53 traffic directly to + [`dns_address`](#dns_address). + +*On Windows with [`strict_route`](#strict_route)*: a WFP filter blocks port +53 traffic going through interfaces other than the TUN. + +#### dns_address + +!!! question "Since sing-box 1.14.0" + +List of DNS server addresses used by [`dns_mode`](#dns_mode). + +When unset, sing-box derives one address per family by taking the next IP after +the first IPv4/IPv6 entry in [`address`](#address). Connections toward those +derived addresses are additionally hijacked into the sing-box DNS module, +equivalent to a [`hijack-dns`](/configuration/route/rule_action/#hijack-dns) +route action; this preserves the behaviour from before this option was added. + +When set, this auto-hijack is not applied; configure an explicit +[`hijack-dns`](/configuration/route/rule_action/#hijack-dns) route rule if the +behaviour is still required. + #### gso !!! failure "Deprecated in sing-box 1.11.0" diff --git a/docs/configuration/inbound/tun.zh.md b/docs/configuration/inbound/tun.zh.md index a41e5ae9f..7471771ee 100644 --- a/docs/configuration/inbound/tun.zh.md +++ b/docs/configuration/inbound/tun.zh.md @@ -5,7 +5,9 @@ icon: material/new-box !!! quote "sing-box 1.14.0 中的更改" :material-plus: [include_mac_address](#include_mac_address) - :material-plus: [exclude_mac_address](#exclude_mac_address) + :material-plus: [exclude_mac_address](#exclude_mac_address) + :material-plus: [dns_mode](#dns_mode) + :material-plus: [dns_address](#dns_address) !!! quote "sing-box 1.13.3 中的更改" @@ -73,6 +75,11 @@ icon: material/new-box "fdfe:dcba:9876::1/126" ], "mtu": 9000, + "dns_mode": "hijack", + "dns_address": [ + "172.18.0.2", + "fdfe:dcba:9876::2" + ], "auto_route": true, "iproute2_table_index": 2022, "iproute2_rule_index": 9000, @@ -222,6 +229,46 @@ tun 接口的 IPv6 前缀。 最大传输单元。 +#### dns_mode + +!!! question "自 sing-box 1.14.0 起" + +TUN 接口上 DNS 的处理方式。 + +| 模式 | 描述 | +|------------|-------------------------------------------------------------------------------------------------------| +| `disabled` | 不设置原生 DNS,也不劫持 DNS 流量。 | +| `native` | 尽可能设置平台的原生接口 DNS:Windows 与 Apple 上的接口 DNS,Linux 上的 `systemd-resolved` 接口 DNS。 | +| `hijack` | 与 `native` 相同,并额外执行下文所述的 53 端口劫持。默认使用。 | + +`hijack` 在 `native` 之上额外执行: + +*Linux*:只能劫持发往非本机地址的 DNS。发往本机接口地址(如 `127.0.0.53` +或本机 LAN 接口 IP)的流量由内核 `local` 路由表在所有用户规则之前直接交付, +`OUTPUT` 链 NAT 也无法对走 `lo` 的包生效。 + +- 未启用 `auto_redirect` 时:通过 `iproute2` 规则让 53 端口跳过 `main` 表的 + 具体路由查找,把本来会经直连子网直接送达的 DNS 改走 TUN —— 不重写目的地址。 +- 启用 `auto_redirect` 时:通过 nftables 规则将 53 端口流量直接 DNAT 至 + [`dns_address`](#dns_address)。 + +*Windows 启用 [`strict_route`](#strict_route) 时*:通过 WFP 过滤器阻止经由非 +TUN 接口的 53 端口流量。 + +#### dns_address + +!!! question "自 sing-box 1.14.0 起" + +[`dns_mode`](#dns_mode) 使用的 DNS 服务器地址列表。 + +未设置时,sing-box 会按地址族在 [`address`](#address) 的第一个 IPv4/IPv6 +条目后面取下一个 IP 作为 DNS 服务器地址,并将流向这些推导地址的连接额外劫持到 +sing-box DNS 模块,等价于一条 +[`hijack-dns`](/zh/configuration/route/rule_action/#hijack-dns) 路由动作;这与此选项加入之前的行为一致。 + +设置后,将不再自动劫持;如仍需此行为,请显式配置 +[`hijack-dns`](/zh/configuration/route/rule_action/#hijack-dns) 路由规则。 + #### gso !!! failure "已在 sing-box 1.11.0 废弃" diff --git a/experimental/libbox/tun.go b/experimental/libbox/tun.go index 84c6372ac..fef66b91c 100644 --- a/experimental/libbox/tun.go +++ b/experimental/libbox/tun.go @@ -7,13 +7,19 @@ import ( "github.com/sagernet/sing-box/option" tun "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common" - E "github.com/sagernet/sing/common/exceptions" +) + +const ( + DNSModeDisabled = tun.DNSModeDisabled + DNSModeNative = tun.DNSModeNative + DNSModeHijack = tun.DNSModeHijack ) type TunOptions interface { GetInet4Address() RoutePrefixIterator GetInet6Address() RoutePrefixIterator - GetDNSServerAddress() (*StringBox, error) + GetDNSMode() *StringBox + GetDNSServerAddress() (StringIterator, error) GetMTU() int32 GetAutoRoute() bool GetStrictRoute() bool @@ -89,11 +95,16 @@ func (o *tunOptions) GetInet6Address() RoutePrefixIterator { return mapRoutePrefix(o.Inet6Address) } -func (o *tunOptions) GetDNSServerAddress() (*StringBox, error) { - if len(o.Inet4Address) == 0 || o.Inet4Address[0].Bits() == 32 { - return nil, E.New("need one more IPv4 address for DNS hijacking") +func (o *tunOptions) GetDNSMode() *StringBox { + return wrapString(o.Options.DNSMode) +} + +func (o *tunOptions) GetDNSServerAddress() (StringIterator, error) { + dnsServers, err := o.Options.DNSServerAddress() + if err != nil { + return nil, err } - return wrapString(o.Inet4Address[0].Addr().Next().String()), nil + return newIterator(common.Map(dnsServers, netip.Addr.String)), nil } func (o *tunOptions) GetMTU() int32 { diff --git a/go.mod b/go.mod index 15afa043f..13ed3ab3f 100644 --- a/go.mod +++ b/go.mod @@ -44,7 +44,7 @@ require ( github.com/sagernet/sing-shadowsocks v0.2.8 github.com/sagernet/sing-shadowsocks2 v0.2.1 github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 - github.com/sagernet/sing-tun v0.8.10-0.20260427231103-812b89ea042d + github.com/sagernet/sing-tun v0.8.10-0.20260502074200-87904db3a2c1 github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 github.com/sagernet/smux v1.5.50-sing-box-mod.1 github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7 diff --git a/go.sum b/go.sum index 1dd31b567..1fdaa4dd4 100644 --- a/go.sum +++ b/go.sum @@ -256,8 +256,8 @@ github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnq github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ= github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w= github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA= -github.com/sagernet/sing-tun v0.8.10-0.20260427231103-812b89ea042d h1:rcFzy3rMpx9M/Zel+YLd2iNGHl0ElH7T8Pl7Y6oxPOQ= -github.com/sagernet/sing-tun v0.8.10-0.20260427231103-812b89ea042d/go.mod h1:QvarqUtHfj1ULaRR+6kZOS/OoCE+pYGq67A5tyIy+dQ= +github.com/sagernet/sing-tun v0.8.10-0.20260502074200-87904db3a2c1 h1:JaW/aRriLE4fgCBLM6wFlpDcscJwRmAgHVRgN0ePOkA= +github.com/sagernet/sing-tun v0.8.10-0.20260502074200-87904db3a2c1/go.mod h1:QvarqUtHfj1ULaRR+6kZOS/OoCE+pYGq67A5tyIy+dQ= github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o= github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY= github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478= diff --git a/option/tun.go b/option/tun.go index fda028b69..379aa7c33 100644 --- a/option/tun.go +++ b/option/tun.go @@ -14,6 +14,8 @@ type TunInboundOptions struct { InterfaceName string `json:"interface_name,omitempty"` MTU uint32 `json:"mtu,omitempty"` Address badoption.Listable[netip.Prefix] `json:"address,omitempty"` + DNSMode string `json:"dns_mode,omitempty"` + DNSAddress badoption.Listable[netip.Addr] `json:"dns_address,omitempty"` AutoRoute bool `json:"auto_route,omitempty"` IPRoute2TableIndex int `json:"iproute2_table_index,omitempty"` IPRoute2RuleIndex int `json:"iproute2_rule_index,omitempty"` diff --git a/protocol/tun/hook.go b/protocol/tun/hook.go deleted file mode 100644 index 1afa643f2..000000000 --- a/protocol/tun/hook.go +++ /dev/null @@ -1,3 +0,0 @@ -package tun - -var HookBeforeCreatePlatformInterface func() diff --git a/protocol/tun/inbound.go b/protocol/tun/inbound.go index 4b113f4a7..65e87bfdc 100644 --- a/protocol/tun/inbound.go +++ b/protocol/tun/inbound.go @@ -42,6 +42,7 @@ type Inbound struct { logger log.ContextLogger tunOptions tun.Options udpTimeout time.Duration + dnsHijackAddress []netip.Addr stack string tunIf tun.Tun tunStack tun.Stack @@ -190,6 +191,8 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo GSO: enableGSO, Inet4Address: inet4Address, Inet6Address: inet6Address, + DNSMode: options.DNSMode, + DNSAddress: options.DNSAddress, AutoRoute: options.AutoRoute, IPRoute2TableIndex: tableIndex, IPRoute2RuleIndex: ruleIndex, @@ -310,6 +313,12 @@ func (t *Inbound) Tag() string { func (t *Inbound) Start(stage adapter.StartStage) error { switch stage { + case adapter.StartStateInitialize: + if t.tunOptions.DNSModeOrDefault() != tun.DNSModeDisabled && len(t.tunOptions.DNSAddress) == 0 { + inet4DNSAddress, _ := t.tunOptions.Inet4DNSAddress() + inet6DNSAddress, _ := t.tunOptions.Inet6DNSAddress() + t.dnsHijackAddress = append(inet4DNSAddress, inet6DNSAddress...) + } case adapter.StartStateStart: if C.IsAndroid && t.platformInterface == nil { t.tunOptions.BuildAndroidRules(t.networkManager.PackageManager()) @@ -373,9 +382,6 @@ func (t *Inbound) Start(stage adapter.StartStage) error { if t.platformInterface != nil && t.platformInterface.UsePlatformInterface() { tunInterface, err = t.platformInterface.OpenInterface(&tunOptions, t.platformOptions) } else { - if HookBeforeCreatePlatformInterface != nil { - HookBeforeCreatePlatformInterface() - } tunInterface, err = tun.New(tunOptions) } monitor.Finish() @@ -490,9 +496,17 @@ func (t *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, source M.S metadata.InboundType = C.TypeTun metadata.Source = source metadata.Destination = destination - - t.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) - t.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) + for _, dnsHijackAddress := range t.dnsHijackAddress { + if destination.Addr == dnsHijackAddress { + metadata.Protocol = C.ProtocolDNS + } + } + if metadata.Protocol == C.ProtocolDNS { + t.logger.InfoContext(ctx, "inbound DNS connection from ", metadata.Source) + } else { + t.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) + t.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) + } t.router.RouteConnectionEx(ctx, conn, metadata, onClose) } @@ -503,9 +517,17 @@ func (t *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata.InboundType = C.TypeTun metadata.Source = source metadata.Destination = destination - - t.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source) - t.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) + for _, dnsHijackAddress := range t.dnsHijackAddress { + if destination.Addr == dnsHijackAddress { + metadata.Protocol = C.ProtocolDNS + } + } + if metadata.Protocol == C.ProtocolDNS { + t.logger.InfoContext(ctx, "inbound DNS packet connection from ", metadata.Source) + } else { + t.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source) + t.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) + } t.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) } @@ -548,9 +570,17 @@ func (t *autoRedirectHandler) NewConnectionEx(ctx context.Context, conn net.Conn metadata.InboundType = C.TypeTun metadata.Source = source metadata.Destination = destination - - t.logger.InfoContext(ctx, "inbound redirect connection from ", metadata.Source) - t.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) + for _, dnsHijackAddress := range t.dnsHijackAddress { + if destination.Addr == dnsHijackAddress { + metadata.Protocol = C.ProtocolDNS + } + } + if metadata.Protocol == C.ProtocolDNS { + t.logger.InfoContext(ctx, "inbound redirect DNS connection from ", metadata.Source) + } else { + t.logger.InfoContext(ctx, "inbound redirect connection from ", metadata.Source) + t.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) + } t.router.RouteConnectionEx(ctx, conn, metadata, onClose) } diff --git a/route/route.go b/route/route.go index 1809971ed..bdc533932 100644 --- a/route/route.go +++ b/route/route.go @@ -88,6 +88,10 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad case uot.LegacyMagicAddress: return E.New("global UoT (legacy) not supported since sing-box v1.7.0.") } + if metadata.InboundType == C.TypeTun && metadata.Protocol == C.ProtocolDNS { + N.CloseOnHandshakeFailure(conn, onClose, r.hijackDNSStream(ctx, conn, metadata)) + return nil + } if deadline.NeedAdditionalReadDeadline(conn) { conn = deadline.NewConn(conn) } @@ -219,7 +223,9 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m /*if deadline.NeedAdditionalReadDeadline(conn) { conn = deadline.NewPacketConn(bufio.NewNetPacketConn(conn)) }*/ - + if metadata.InboundType == C.TypeTun && metadata.Protocol == C.ProtocolDNS { + return r.hijackDNSPacket(ctx, conn, nil, metadata, onClose) + } selectedRule, _, _, packetBuffers, err := r.matchRule(ctx, &metadata, false, false, nil, conn) if err != nil { return err