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它通常会做两件事:
- 解析
api.openai.com,拿到一个目标 IP。 - 调用
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:7890HTTPS Proxy -> 127.0.0.1:7890SOCKS Proxy -> 127.0.0.1:7890如果应用愿意读这张便签,它会主动连接 mihomo;如果应用不读,还是会照常 connect(目标 IP:443),然后按路由表从真实网卡出去。
TUN 创建后,系统路径变成什么
Section titled “TUN 创建后,系统路径变成什么”TUN 是一张虚拟网卡。
它看起来像网卡,但不是物理设备。真实网卡负责把数据发到 Wi-Fi、以太网或其他网络介质;TUN 网卡负责在内核网络栈和用户态程序之间传递 IP 包。
在 macOS 上,这类接口通常叫 utun*。本机曾经可以查到一个典型例子:
ifconfig utun1500关键输出是:
utun1500: flags=8051<UP,POINTOPOINT,RUNNING,MULTICAST> mtu 1500 inet 198.18.0.1 --> 198.18.0.1 netmask 0xfffffffc198.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 流量会和其他入口重新汇合,继续走同一套规则匹配和出站转发流程。
路由表是什么
Section titled “路由表是什么”路由表是一组“目标地址到出口”的规则。
可以用 macOS 命令查看:
netstat -rn -f inetroute -n get 8.8.8.8route -n get default一条普通路由大概包含三类信息:
Destination Gateway Interfacedefault 192.168.31.1 en1192.168.31.0/24 link#15 en1198.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/24 比 default 更具体,所以访问局域网设备时不会被默认路由抢走。
TUN 模式利用的就是这个机制。它不是让应用改代码,也不是让应用连接本地代理端口,而是让系统路由把一部分包交给虚拟网卡。
auto-route 在做什么
Section titled “auto-route 在做什么”tun.enable: true 只表示启用 TUN 能力。真正让应用流量进 TUN 的,是路由。
如果只创建一张虚拟网卡,但没有对应路由,普通应用流量还是会走真实网卡:
应用 -> en1 -> 路由器 -> 互联网auto-route 的作用是自动配置路由表,让目标流量先进入 TUN:
应用 -> utun1500 -> mihomo -> en1 -> 互联网本机曾经能看到类似这样的路由结果:
1 198.18.0.1 utun15002/7 198.18.0.1 utun15004/6 198.18.0.1 utun15008/5 198.18.0.1 utun150016/4 198.18.0.1 utun150032/3 198.18.0.1 utun150064/2 198.18.0.1 utun1500128.0/1 198.18.0.1 utun1500这些看起来不像常见的 0.0.0.0/0,但本质上是在把大范围 IPv4 流量拆成多段导向 TUN,同时给局域网、网关、保留地址和程序自身出站留下排除空间。
所以可以这样记:
tun.enable: 创建和启动虚拟网卡入口。
auto-route: 自动改路由表,让应用流量真的进虚拟网卡。auto-detect-interface 在做什么
Section titled “auto-detect-interface 在做什么”TUN 接住应用流量后,mihomo 还要自己向外建立连接。
例如应用访问 api.openai.com,规则命中 US-Proxy。mihomo 接下来要连接代理节点:
mihomo -> 代理节点 IP:端口这条出站连接不能再进入 TUN,否则会形成回环:
mihomo 出站连接 -> 命中 TUN 路由 -> 被 mihomo 接住 -> mihomo 再处理自己的连接 -> 再次命中 TUNauto-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.1Host: api.openai.com:443mihomo 从这条请求里就知道目标是 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 原本代表哪个域名”。
一条完整路径
Section titled “一条完整路径”假设配置里有:
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 仍然是加密的。
Mihomo 源码里的 TUN 主线
Section titled “Mihomo 源码里的 TUN 主线”本节源码基于 MetaCubeX/mihomo commit 5e22035118d13fa609164670111cc674906bb2a4。
配置层先把 YAML 里的 TUN 字段读进 RawTun。这里可以看到 enable、device、stack、dns-hijack、auto-route、auto-detect-interface、strict-route、route-exclude-address 等字段都属于 TUN 配置的一部分。
RawTun -> parseTun -> general.Tun配置加载后,executor 会单独更新 TUN:
executor.updateTun -> listener.ReCreateTun -> sing_tun.NewReCreateTun 会先关闭旧 TUN listener;如果 tun.enable 为 false,就直接返回;如果启用,就创建新的 sing_tun listener。
sing_tun.New 才是真正把 TUN 配置转成运行时选项的地方。它会确定设备名、MTU、TUN 地址、AutoRoute、路由排除、接口包含/排除、UID/端口过滤、StrictRoute、AutoRedirect 等选项,然后调用底层 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 是顶层配置能力,包含enable、dns-hijack、auto-route、auto-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.go、hub/executor/executor.go -
listener.ReCreateTun:如果tun.enable为 false 就不创建 listener;启用时调用sing_tun.New并输出 TUN adapter 地址。 源码:listener/listener.go -
sing_tun.New:把配置转换成tun.Options,包括AutoRoute、StrictRoute、路由包含/排除、接口/UID/端口过滤等,再创建 TUN interface 和 stack。 源码:listener/sing_tun/server.go、listener/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.HandleTCPConn或Tunnel.HandleUDPPacket。 源码:listener/sing/sing.go、listener/sing/sing.go
常用观察命令
Section titled “常用观察命令”在 macOS 上,可以用这些只读命令观察 TUN 是否存在,以及路由是否导向它:
ifconfig | rg "^utun|inet "ifconfig utun1500netstat -rn -f inet | rg "utun|198\\.18"route -n get 8.8.8.8route -n get defaultscutil --dns | rg -n "nameserver|if_index|utun|resolver"判断时不要只看有没有 utun。macOS 自己、VPN、网络扩展和 Apple 系统服务都可能创建 utun*。更可靠的判断是同时看:
- 是否有
198.18.0.1这类 mihomo/fake-ip/TUN 地址。 - 路由表里是否有大范围目标指向这个
utun。 - 当前是否有 mihomo core 进程在运行。
这篇只讲 TUN 的主路径:虚拟网卡、路由表、auto-route、auto-detect-interface、DNS/fake-ip,以及 mihomo 如何把 TUN 流量送进 tunnel。
TUN 覆盖不到或可能被绕过的情况适合单独写一篇,包括:
- 局域网、组播、系统服务和 IPv6 为什么可能绕过 TUN。
- 普通 App 能否绑定真实接口或源地址。
- 高权限程序、PF、Network Extension、per-app VPN 能做到什么。
- “阻断非 TUN 出口”和“无感改回 TUN”为什么不是同一件事。