Allow customizing TUN DNS mode and hijack interface DNS by default

This commit is contained in:
世界 2026-05-02 18:36:58 +08:00
parent ce360eece8
commit e8643485d2
No known key found for this signature in database
GPG key ID: CD109927C34A63C4
15 changed files with 184 additions and 60 deletions

View file

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

View file

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

View file

@ -89,13 +89,9 @@ Example:
],
"rules": [
{
"action": "evaluate",
"preferred_by": "hosts",
"action": "route",
"server": "hosts"
},
{
"match_response": true,
"ip_accept_any": true,
"action": "respond"
}
]
}

View file

@ -89,13 +89,9 @@ hosts 文件路径列表。
],
"rules": [
{
"action": "evaluate",
"preferred_by": "hosts",
"action": "route",
"server": "hosts"
},
{
"match_response": true,
"ip_accept_any": true,
"action": "respond"
}
]
}

View file

@ -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"
}
]
}

View file

@ -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"
}
]
}

View file

@ -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"
}
]
}

View file

@ -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"
}
]
}

View file

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

View file

@ -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` | 尽可能设置平台的原生接口 DNSWindows 与 Apple 上的接口 DNSLinux 上的 `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 废弃"

View file

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

View file

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

View file

@ -1,3 +0,0 @@
package tun
var HookBeforeCreatePlatformInterface func()

View file

@ -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()
@ -491,9 +497,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)
}
@ -504,9 +518,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)
}
@ -549,9 +571,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)
}

View file

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