跳转到内容

TUN 流量如何进入 Mihomo

解释没有 TUN 时系统如何转发真实流量,以及 TUN、auto-route、auto-detect-interface 如何改变流量入口。

TUN 不是“更强的系统代理”,而是另一种入口模型。

系统代理的入口是应用层协议:应用主动连接 127.0.0.1:7890,再通过 HTTP CONNECT 或 SOCKS 把目标地址告诉 mihomo。TUN 的入口是网络层路由:操作系统把一部分 IP 包送进虚拟网卡,mihomo 从虚拟网卡里读包,再把它们转成自己的 TCP / UDP 入站连接。

所以两者的核心差异是:

系统代理:
应用先连本地 mihomo。
mihomo 从代理协议里拿到目标。
TUN:
应用仍然以为自己在直连目标。
操作系统按路由表把 IP 包交给虚拟网卡。
mihomo 从虚拟网卡里接住这些包。

TUN 能覆盖很多不遵循系统代理的程序,但它也引入了新的问题:谁来改路由表,DNS 如何不泄漏,mihomo 自己的出站连接如何避免绕回 TUN,以及哪些流量应该明确绕过 TUN。

没有 TUN 时,系统怎么转发真实流量

Section titled “没有 TUN 时,系统怎么转发真实流量”

先看最普通的网络路径。

当一个应用访问:

https://api.openai.com

它通常会做两件事:

  1. 解析 api.openai.com,拿到一个目标 IP。
  2. 调用 connect(目标 IP:443),让操作系统建立连接。

应用并不直接决定包从 Wi-Fi、以太网、VPN 还是其他接口出去。它把目标交给内核网络栈,内核查路由表。

路由表可以理解成操作系统的“出门地图”:

目标 IP 属于哪段地址?
这段地址该走哪个网关?
应该从哪张网卡发出去?

例如一台 Mac 可能有这样的普通路径:

应用
-> DNS 解析 api.openai.com
-> connect(真实 IP:443)
macOS 内核网络栈
-> 查路由表
-> 默认出口是 Wi-Fi 网卡 en1
-> 下一跳网关是 192.168.31.1
真实网卡 en1
-> 局域网路由器
-> 互联网
-> api.openai.com

这时没有 mihomo 参与。应用产生的包直接走真实网卡。

系统代理也不会改变这条底层事实。系统代理只是给遵循代理设置的应用一张“便签”:

HTTP Proxy -> 127.0.0.1:7890
HTTPS Proxy -> 127.0.0.1:7890
SOCKS Proxy -> 127.0.0.1:7890

如果应用愿意读这张便签,它会主动连接 mihomo;如果应用不读,还是会照常 connect(目标 IP:443),然后按路由表从真实网卡出去。

TUN 是一张虚拟网卡。

它看起来像网卡,但不是物理设备。真实网卡负责把数据发到 Wi-Fi、以太网或其他网络介质;TUN 网卡负责在内核网络栈和用户态程序之间传递 IP 包。

在 macOS 上,这类接口通常叫 utun*。本机曾经可以查到一个典型例子:

Terminal window
ifconfig utun1500

关键输出是:

utun1500: flags=8051<UP,POINTOPOINT,RUNNING,MULTICAST> mtu 1500
inet 198.18.0.1 --> 198.18.0.1 netmask 0xfffffffc

198.18.0.1 是 mihomo / fake-ip / TUN 场景里常见的保留地址段。接口存在只说明虚拟网卡创建好了,还不等于应用流量一定会进入它。

要让流量进入 TUN,还需要路由表把目标流量指向这张虚拟网卡。开启 TUN 和自动路由后,路径会变成:

应用
-> 正常 connect(目标 IP:443)
macOS 内核网络栈
-> 查路由表
-> 命中 TUN 相关路由
-> 把 IP 包写进 utun1500
mihomo
-> 从 utun1500 读到 IP 包
-> 转成 TCP / UDP 入站连接
-> 生成 metadata
-> 按规则选择 DIRECT / 代理节点 / REJECT
-> 自己再从真实网卡向外拨号

对应用来说,它仍然像是在直连目标。改变的是操作系统内部的出门路径。

对 mihomo 来说,入口不再是 HTTP proxy、SOCKS 或 mixed-port,而是 TUN stack 交上来的连接和数据包。进入 tunnel 之后,TUN 流量会和其他入口重新汇合,继续走同一套规则匹配和出站转发流程。

路由表是一组“目标地址到出口”的规则。

可以用 macOS 命令查看:

Terminal window
netstat -rn -f inet
route -n get 8.8.8.8
route -n get default

一条普通路由大概包含三类信息:

Destination Gateway Interface
default 192.168.31.1 en1
192.168.31.0/24 link#15 en1
198.18.0.1 198.18.0.1 utun1500

含义是:

访问局域网 192.168.31.0/24 -> 直接走 en1
访问其他默认地址 -> 发给网关 192.168.31.1,从 en1 出去
访问 TUN 相关地址 -> 走 utun1500

路由匹配通常遵循“最长前缀优先”:越具体的规则优先级越高。192.168.31.0/24default 更具体,所以访问局域网设备时不会被默认路由抢走。

TUN 模式利用的就是这个机制。它不是让应用改代码,也不是让应用连接本地代理端口,而是让系统路由把一部分包交给虚拟网卡。

tun.enable: true 只表示启用 TUN 能力。真正让应用流量进 TUN 的,是路由。

如果只创建一张虚拟网卡,但没有对应路由,普通应用流量还是会走真实网卡:

应用 -> en1 -> 路由器 -> 互联网

auto-route 的作用是自动配置路由表,让目标流量先进入 TUN:

应用 -> utun1500 -> mihomo -> en1 -> 互联网

本机曾经能看到类似这样的路由结果:

1 198.18.0.1 utun1500
2/7 198.18.0.1 utun1500
4/6 198.18.0.1 utun1500
8/5 198.18.0.1 utun1500
16/4 198.18.0.1 utun1500
32/3 198.18.0.1 utun1500
64/2 198.18.0.1 utun1500
128.0/1 198.18.0.1 utun1500

这些看起来不像常见的 0.0.0.0/0,但本质上是在把大范围 IPv4 流量拆成多段导向 TUN,同时给局域网、网关、保留地址和程序自身出站留下排除空间。

所以可以这样记:

tun.enable:
创建和启动虚拟网卡入口。
auto-route:
自动改路由表,让应用流量真的进虚拟网卡。

TUN 接住应用流量后,mihomo 还要自己向外建立连接。

例如应用访问 api.openai.com,规则命中 US-Proxy。mihomo 接下来要连接代理节点:

mihomo -> 代理节点 IP:端口

这条出站连接不能再进入 TUN,否则会形成回环:

mihomo 出站连接
-> 命中 TUN 路由
-> 被 mihomo 接住
-> mihomo 再处理自己的连接
-> 再次命中 TUN

auto-detect-interface 解决的是“真实出口是谁”的问题。

它会帮助 mihomo 识别当前默认出口网卡,例如 Wi-Fi 是 en1。然后 mihomo 在需要出站、排除自身连接或处理直连路径时,知道真实网络应该从哪张网卡出去。

可以把它和 auto-route 分工记成:

auto-route:
把应用流量导进 TUN。
auto-detect-interface:
找到 mihomo 自己最终应该使用的真实出口网卡。

这也是 TUN 客户端必须处理的基本问题:既要尽量接住应用流量,又不能把自己的出站连接也吞回去。

为什么 TUN 经常和 DNS 劫持一起出现

Section titled “为什么 TUN 经常和 DNS 劫持一起出现”

系统代理模式下,目标域名通常直接出现在代理协议里:

CONNECT api.openai.com:443 HTTP/1.1
Host: api.openai.com:443

mihomo 从这条请求里就知道目标是 api.openai.com

TUN 模式下,mihomo 从虚拟网卡里看到的是 IP 包。IP 包里天然只有源地址、目标地址、协议和端口,不一定有原始域名。

如果应用已经把域名解析成真实 IP,mihomo 可能只看到:

目标 = 104.x.x.x:443

这时 DOMAIN-SUFFIX,openai.com 这类规则就缺少域名输入。

因此 TUN 常常配合 DNS 劫持和 fake-ip:

tun:
enable: true
dns-hijack:
- 0.0.0.0:53

大致流程是:

应用查询 api.openai.com
-> DNS 请求被导入 mihomo
mihomo DNS
-> 返回一个 fake-ip,例如 198.18.0.10
-> 记录 198.18.0.10 = api.openai.com
应用连接 198.18.0.10:443
-> 流量进入 TUN
mihomo
-> 看到目标 198.18.0.10:443
-> 通过 fake-ip 映射还原 api.openai.com
-> 继续按域名规则匹配

这就是为什么 TUN、dns-hijack、fake-ip 经常一起出现。TUN 负责接住包,DNS/fake-ip 负责让 mihomo 仍然知道“这个 IP 原本代表哪个域名”。

假设配置里有:

tun:
enable: true
stack: system
dns-hijack:
- 0.0.0.0:53
auto-route: true
auto-detect-interface: true
mode: rule
rules:
- DOMAIN-SUFFIX,openai.com,US-Proxy
- MATCH,DIRECT

应用访问:

https://api.openai.com/v1/models

路径会变成:

应用
-> 查询 api.openai.com
DNS 劫持
-> mihomo 接住 DNS 请求
-> 返回 fake-ip 198.18.0.10
-> 保存 fake-ip 到域名的映射
应用
-> connect(198.18.0.10:443)
操作系统
-> 查路由表
-> auto-route 添加的路由命中
-> 把 IP 包送进 utun
mihomo TUN
-> 从虚拟网卡读取 TCP 流量
-> 还原目标域名 api.openai.com
-> 生成 metadata: host=api.openai.com, port=443, network=tcp
mihomo tunnel
-> rule 模式匹配 DOMAIN-SUFFIX,openai.com
-> 选择 US-Proxy
mihomo outbound
-> 通过真实出口网卡连接代理节点
-> 让代理节点访问 api.openai.com:443
之后
-> 应用和 api.openai.com 继续 TLS 握手
-> mihomo 转发加密字节流

TUN 不等于 HTTPS 解密。它接住的是网络层流量;默认情况下,应用和目标网站之间的 TLS 仍然是加密的。

本节源码基于 MetaCubeX/mihomo commit 5e22035118d13fa609164670111cc674906bb2a4

配置层先把 YAML 里的 TUN 字段读进 RawTun。这里可以看到 enabledevicestackdns-hijackauto-routeauto-detect-interfacestrict-routeroute-exclude-address 等字段都属于 TUN 配置的一部分。

RawTun
-> parseTun
-> general.Tun

配置加载后,executor 会单独更新 TUN:

executor.updateTun
-> listener.ReCreateTun
-> sing_tun.New

ReCreateTun 会先关闭旧 TUN listener;如果 tun.enable 为 false,就直接返回;如果启用,就创建新的 sing_tun listener。

sing_tun.New 才是真正把 TUN 配置转成运行时选项的地方。它会确定设备名、MTU、TUN 地址、AutoRoute、路由排除、接口包含/排除、UID/端口过滤、StrictRouteAutoRedirect 等选项,然后调用底层 sing-tun 创建接口和 stack。

这条主线可以概括成:

YAML tun 配置
-> RawTun
-> LC.Tun
-> listener.ReCreateTun
-> sing_tun.New
-> tun.Options
-> tun.New(...)
-> tun.NewStack(...)
-> tunStack.Start()

进入 TUN stack 后,TCP 和 UDP 会走到通用的 sing listener handler:

TUN TCP
-> NewConnection
-> C.Metadata
-> Tunnel.HandleTCPConn
TUN UDP
-> NewPacket / NewPacketConnection
-> C.Metadata
-> Tunnel.HandleUDPPacket

到这里,TUN 入口已经完成了“把系统网络包变成 mihomo metadata”的工作。后面的规则匹配、代理选择和出站拨号,就和系统代理入口重新汇合。

  • config.RawTun:说明 TUN 是顶层配置能力,包含 enabledns-hijackauto-routeauto-detect-interface、路由排除和过滤条件。 源码:config/config.go

  • parseTun:把原始 YAML 配置转换成运行时 LC.Tun,并在没有有效 fake-ip 网段时使用 198.18.0.1/16 派生 TUN IPv4 地址。 源码:config/config.go

  • executor.updateTun:配置更新时单独重建 TUN,入口是 listener.ReCreateTun(general.Tun, tunnel.Tunnel)。 源码:hub/executor/executor.gohub/executor/executor.go

  • listener.ReCreateTun:如果 tun.enable 为 false 就不创建 listener;启用时调用 sing_tun.New 并输出 TUN adapter 地址。 源码:listener/listener.go

  • sing_tun.New:把配置转换成 tun.Options,包括 AutoRouteStrictRoute、路由包含/排除、接口/UID/端口过滤等,再创建 TUN interface 和 stack。 源码:listener/sing_tun/server.golistener/sing_tun/server.go

  • sing_tun/dns.go:TUN 入口先判断目标是否命中 dns-hijack,命中后把 TCP/UDP DNS 请求交给 mihomo resolver 处理。 源码:listener/sing_tun/dns.go

  • sing.ListenerHandler.NewConnection / NewPacket:TUN stack 交上来的 TCP / UDP 流量会被转换成 C.Metadata,再交给 Tunnel.HandleTCPConnTunnel.HandleUDPPacket。 源码:listener/sing/sing.golistener/sing/sing.go

在 macOS 上,可以用这些只读命令观察 TUN 是否存在,以及路由是否导向它:

Terminal window
ifconfig | rg "^utun|inet "
ifconfig utun1500
netstat -rn -f inet | rg "utun|198\\.18"
route -n get 8.8.8.8
route -n get default
scutil --dns | rg -n "nameserver|if_index|utun|resolver"

判断时不要只看有没有 utun。macOS 自己、VPN、网络扩展和 Apple 系统服务都可能创建 utun*。更可靠的判断是同时看:

  1. 是否有 198.18.0.1 这类 mihomo/fake-ip/TUN 地址。
  2. 路由表里是否有大范围目标指向这个 utun
  3. 当前是否有 mihomo core 进程在运行。

这篇只讲 TUN 的主路径:虚拟网卡、路由表、auto-routeauto-detect-interface、DNS/fake-ip,以及 mihomo 如何把 TUN 流量送进 tunnel

TUN 覆盖不到或可能被绕过的情况适合单独写一篇,包括:

  • 局域网、组播、系统服务和 IPv6 为什么可能绕过 TUN。
  • 普通 App 能否绑定真实接口或源地址。
  • 高权限程序、PF、Network Extension、per-app VPN 能做到什么。
  • “阻断非 TUN 出口”和“无感改回 TUN”为什么不是同一件事。