sing-box 客户端 TUN 落地:配置、分流验证与 1.14 拒收的旧写法
客户端上的 TUN 模式和「代理模式」的差别在于接管范围:代理模式只服务主动配置了代理的应用,TUN 模式把整机流量都交给 sing-box。代价是必须处理路由、DNS 与分流,而且这三个东西经常出问题,所以验证方法比配置本身更重要。
这篇在一台一次性 Ubuntu 虚拟机(arm64)上用 sing-box 1.14.1 跑完整套接管:TUN 接口与路由的变化、DNS 劫持、用一条reject规则证明分流生效、退出后的自动回收,以及 1.14 会直接拒绝启动的三处旧写法原文报错。
1. 便宜的说法与真实的代价
服务端那篇讲的是「在一台服务器上暴露代理」;客户端这边解决的是另一个问题:本机有一堆程序,浏览器、终端、Docker、后台服务,逐个配代理既麻烦又会漏。TUN 的做法是造一个虚拟网卡,把默认路由指过去,让内核把流量交给 sing-box 决定怎么走。
听上去只是「接管」,但要同时成立四件事:
- 路由要从物理网卡切到 TUN,且不能把 sing-box 自己的出站流量也绕回去(回环)
- DNS 要一起接管,否则域名解析仍然暴露在明网,分流规则也无从匹配
- 分流规则要能验证:不能只靠「能上网」判断,因为这既可能是直连成功,也可能是代理成功
- 退出时要能干净回收,否则你会在没有代理的情况下继续用一条指向已消失网卡的路由
2. 一份能通过 1.14 校验的客户端配置
1 | { |
1 | $ sing-box check -c /etc/sing-box/config.json |
几个要点:
- DNS 服务端写法换了。1.12 起是
{"type": "...", "tag": "...", "server": "..."},address: "https://1.1.1.1/dns-query"那种老格式在 1.14 已经被移除(下一节有原文报错)。规则里也不再写"server"直接匹配,而是"action": "route", "server": "local"。 route.default_domain_resolver是必填项。不写它,1.14 会直接拒绝启动;它的值就是上面某个 DNS 服务端的tag。这个字段决定「出站需要解析域名时用哪个 DNS」,不写等于让 sing-box 猜。strategy: "ipv4_only"会主动拒绝 AAAA 查询。日志里会看到dns: strategy rejected,这是预期行为:客户端通常不需要 IPv6 出口,主动拒绝能省掉一类「解析出 IPv6 但没有出口」的故障。- 规则集先落到本地。远端
rule_set要在启动阶段完成首次下载,而那一刻 DNS 还没接管完成,容易卡在自己身上(实测:远端规则集 + 本地 DNS 服务端不可用时,启动直接失败在initialize rule-set[0])。把.srs下到本地、用"type": "local",启动就不受网络影响;要动态更新再换回远端。
3. 接管之后系统里发生了什么
启动前后的差异如下(同一台机器,只启动 sing-box):
1 | === 接口 === |
三件事同时发生了:多出一个 tun0;auto_route 把默认路由写进独立的表 2022,再用 ip rule 把普通流量导过去;DNS 的 53 端口被单独放行(dport 53 那条规则),交给 sing-box 的 DNS 模块处理。
规则表逐条看更清楚,这是接管态的完整输出:
1 | 0: from all lookup local |
9002 ... iif tun0 goto 9010 那一条就是防回环的关键:sing-box 自己发出去的包不会再被导回表 2022,否则代理的上游连接会绕回自己。
strict_route: true 的作用在服务端也常见:它会让 sing-box 补上更严格的规则,避免「一部分流量没被接管」的灰区;关掉它时,某些从本机其它网卡进出的流量会绕过 TUN。
退出后的回收同样重要,实测是干净的:
1 | tun0 接口: 已移除 |
但异常退出是另一个结果。用 kill -9 打死进程之后:
1 | tun0: 已移除 # 设备随进程的文件描述符关闭而消失 |
也就是说,SIGKILL 之后系统还能正常上网(残留规则指向一张空表,会继续落到 main 表),但那些规则不会自己消失。反复崩溃几次就会累积一堆;更麻烦的是它们引用的表号下次可能被新进程用上。所以:别用 kill -9 停 sing-box,正常退出(SIGTERM)才会自动回收;已经留下的用 ip rule delete 逐条删掉即可。
4. 分流到底生效了没有:用 reject 做探针
「能不能上网」证明不了任何事。要在客户端验证分流,最省事的办法是故意写一条会拒绝的规则,再拿一个真实域名去撞它:
1 | { "domain_suffix": ["example.org"], "action": "reject" } |
1 | $ curl -s -o /dev/null -w "%{http_code}\n" https://example.org |
而 sing-box 的 debug 日志会把你需要的信息全部交代清楚:
1 | router: sniffed protocol: tls, domain: example.org |
match[2] 里的数字是 route.rules 里的下标(从 0 开始),sniffed 说明 TLS 的 SNI 被读出来用于匹配。把探针域名换成你自己的业务域名,就能确认规则集与分流是否按预想命中——这比看流量图可靠得多。
5. 1.14 直接拒绝启动的三处旧写法
客户端配置最容易过期的就是这三处,报错原文如下(sing-box 1.14.1):
1 | # ① 路由规则里的 geoip(1.12 移除) |
三处的替代写法:geoip → 用 rule_set(二进制 .srs 更小,加载也快);sniff / sniff_override_destination → 用 {"action": "sniff"} 路由规则;DNS → 上面第 2 节的新格式。
两条警告级的变化同样值得先改:download_detour 在 1.14 弃用、1.16 移除;TUN 的 stack 字段在 1.15 弃用、1.17 移除(新版本用自有协议栈,删掉即可)。
还有一个容易忽略的必填项错误,1.14 的报错如下:
1 | ERROR missing `route.default_domain_resolver` or `domain_resolver` in dial fields is deprecated in sing-box 1.12.0 and will be removed in sing-box 1.14.0 |
6. 容器流量会不会跟着走
如果你在跑 Docker,TUN 接管之后容器出网走不走代理?
在 Linux 上实测(同一台机器,宿主 TUN 已开启,规则里有 example.org => reject):
1 | host https://example.org : 000(被拒) |
结论:会跟着走。容器的转发流量同样命中 ip rule 与表 2022,被送进 TUN,因此规则对容器内也生效。日志里看不到 172.17.0.x,是因为 Docker 自己的 POSTROUTING ... -j MASQUERADE 在流量进入 TUN 前改了源地址,sing-box 侧只能看到 tun 的地址——这一点在排查「谁发起的请求」时会误导你,值得记住。
7. 客户端形态:GUI 与 CLI
macOS 上的 GUI 客户端(SFM 这类,用 NetworkExtension 实现)和你自己用 CLI 起 TUN 的区别只在「谁来创建网卡与路由」:GUI 把 tun inbound 与路由接管做进了系统扩展,因此不需要 root,退出时由系统回收;CLI 模式下 auto_route 就是上面那套 ip rule 操作,需要 root,退出时自己回收。
配置本身是同一套字段,所以上面的配置可以整段搬进 GUI 客户端。一个迁移注意点来自官方 migration 文档:macOS 独立客户端在 1.14 换了开发者账号,bundle 从 …io.nekohasekai.sfavt 变成 …io.nekohasekai.sfamt,配置不会自动继承,需要按文档手动迁移 Group Container 目录。升级客户端版本前,先确认自己在用的是哪一个应用。
macOS 上连接前后实测到的三件事
在同一台 Mac 上用 GUI 客户端(NetworkExtension 形态)连接前后各取一次,得到三条可复现的观察:
1 | # 1. 多出一个 utun 接口 |
第二条尤其值得对照配置看:172.19.0.2 正是配置里 address: ["172.19.0.1/30"] 的下一个地址——官方文档描述的 dns_address 默认推导规则(取第一个 IPv4 条目的下一个 IP)在这里被完整复现了。也就是说 GUI 客户端连 DNS 都不需要你手配,它按同一套规则生成。
容器侧在同一时间窗内的出口 IP 与宿主同步变化(宿主与容器同为同网段的出口地址,断开后一起复原),说明容器的出网经由宿主网络栈;但 macOS 上做不出 Linux 那种 reject 探针的决定性证据,所以「macOS 容器是否被隧道接管」这一条只能算观察一致、未证伪。
8. 坑清单
- 远端规则集卡启动:首次启动要下载
.srs,而 DNS 可能还没接管好,先落地到本地更稳。 detour指向空 direct 出站:只有direct出站时给 DNS 写detour会被拒——start service: start dns/udp[local]: detour to an empty direct outbound makes no sense。- DNS 服务端不可用会以「启动失败」的形式出现,而不是「解析慢」:
initialize rule-set[0]: ... lookup ... connection refused。 reject与block不是一回事:新写法用action,block出站属于被淘汰的旧写法。- 用日志验证,不要用连通性验证:看
match[n]与sniffed两行。
总结
- TUN 接管 = 虚拟网卡 + 独立路由表(
auto_route用 2022 表 + 9000 段规则)+ DNS 劫持,退出时自动回收 - 1.14 拒收三处旧写法:
geoip路由规则、inbound 的sniff字段、DNS 服务端旧格式;route.default_domain_resolver已成必填 - 分流验证的正确姿势是写一条
reject探针规则,看match[n] ... => reject与sniffed protocol日志 - Linux 上容器流量会被宿主 TUN 一并接管(Docker 的 MASQUERADE 会改写源地址,日志里看不到容器网段)
参考资料
系列索引:网络与自建服务






