konakona

让 Tailscale Exit Node 的流量经过 Mihomo

救援通道、最小范围的 TUN 接管、DNS 污染排查、最终实现通过 Exit Node 使用 Mihomo 代理

我在家里的 NAS(下文称为 nasRemote)上运行了 Mihomo,也把这台机器配置成了 Tailscale Exit Node。最初,两者是完全独立的:手机可以把 nasRemote 当作出口节点,但离开 Tailscale 后,流量仍然直接从 NAS 的默认网络出口访问互联网,不会经过 Mihomo。

我想实现的链路是:

iPhone
  → Tailscale
  → nasRemote 上的 tailscaled Exit Node
  → Mihomo TUN
  → 复用现有 config.yaml 的规则
  → 代理节点或 DIRECT
  → Internet

本文记录实现这一链路的过程。

先解决失联风险

当时 Mac 只能通过 Tailscale 连接 nasRemote。如果 TUN 路由误伤了 Tailscale 自身,远程修复配置的入口也会一起消失。因此,我没有直接修改 Mihomo,而是先建立一条完全不依赖 Tailscale 的救援通道。

我的另一台服务器 serJP 同时能被 Mac 和 nasRemote 访问,所以可以让 NAS 主动向 serJP 建立反向 SSH 隧道:

Mac → serJP:22022 → reverse SSH tunnel → nasRemote:22

其核心等价于在 NAS 上运行:

ssh -N -R 127.0.0.1:22022:127.0.0.1:22 serJP

我把它做成了 nasRemote 上的 systemd 服务,并使用一把专用密钥。serJP 上对应的 authorized_keys 条目只允许端口转发,不允许获得 shell,同时将监听地址限制为 127.0.0.1:22022。这样,救援端口不会直接暴露到公网。

Mac 再通过 ProxyJump serJP 建立一个 nasRescue SSH 别名。这条救援链路平时也有用:即使 Tailscale 控制面、DNS 或路由出了问题,我仍然可以从 serJP 回到 NAS。

让用户态 Mihomo 使用 TUN

Mihomo 原本由普通用户 nas 运行。我不想为了 TUN 直接把整个进程改成 root,而是通过 systemd 只授予它创建和管理网络接口所需的能力:

[Service]
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW

服务仍然以 User=nas 运行,只额外获得 CAP_NET_ADMINCAP_NET_RAW

接着在现有 config.yaml 中加入 TUN 配置:

tun:
  enable: true
  stack: mixed
  auto-route: true
  auto-redirect: true
  auto-detect-interface: true
  include-interface:
    - tailscale0
  route-exclude-address:
    - 10.0.0.0/8
    - 100.64.0.0/10
    - 127.0.0.0/8
    - 169.254.0.0/16
    - 172.16.0.0/12
    - 192.168.0.0/16
    - ::1/128
    - fd7a:115c:a1e0::/48
    - fe80::/10

这里最重要的限制是 include-interface: tailscale0。它让 Mihomo 只接管从 Tailscale 接口进入的 Exit Node 流量,而不是把 NAS 自己的全部网络连接都塞进 TUN。私有地址、Tailscale CGNAT 地址和本地 IPv6 地址也被排除,避免管理流量和内部服务发生递归路由。

我保留了现有配置中的全部代理组和规则。换句话说,TUN 只改变“流量如何进入 Mihomo”,进入之后仍由原来的 config.yaml 决定走代理还是 DIRECT

我也刻意没有配置 dns-hijack。Tailscale 的 MagicDNS 和 tailscaled 自己仍然需要正常工作;在没有必要的情况下劫持 DNS,会扩大这次改动的影响范围。

用嗅探恢复域名规则

TUN 最初拿到的通常只是目标 IP,而现有规则中有大量域名规则。为了继续复用这些规则,我启用了 HTTP、TLS 和 QUIC 嗅探:

sniffer:
  enable: true
  parse-pure-ip: true
  override-destination: false
  sniff:
    HTTP:
      ports:
        - 80
        - 8080-8880
    TLS:
      ports:
        - 443
        - 8443
    QUIC:
      ports:
        - 443
        - 8443

一开始我把 override-destination 设为 false,想让 Mihomo 只用嗅探到的域名匹配规则,但不要改变客户端原本请求的目标地址。这个选择看起来更保守,后来却暴露了一些问题。

可回滚地启用配置

远程修改网络路径时,不能把“配置语法正确”等同于“网络一定不会断”。实际启用前的流程是:

  1. 备份当前 config.yaml、配置生成脚本和 systemd drop-in。
  2. 使用 mihomo -t 检查完整配置。
  3. 设置一个延迟执行的 systemd 回滚任务。
  4. 只重启 Mihomo,不重启 tailscaled
  5. 同时验证 Tailscale SSH 和 nasRescue
  6. 验证 MagicDNS、NAS 上的其他服务和 Mihomo 控制面。
  7. 确认一切正常后,取消回滚任务。

启用后,iPhone 通过 Exit Node 访问 ifconfig.me,看到的已经是 Mihomo 代理节点的公网 IP。这说明主链路确实变成了:

iPhone → Tailscale → nasRemote → Mihomo → 代理节点

但事情还没有结束。

出口 IP 正确,部分网站仍然打不开

实际测试中发现了如下现象:

  • x.com 可以正常访问。
  • ifconfig.me 显示的是代理出口 IP。
  • Google、YouTube 和 Instagram 无法访问。

第一反应很容易怀疑 GFW 是否在识别并干扰 Tailscale 的 UDP 流量。但这个解释与现象并不吻合:Tailscale 连接仍然活跃,iPhone 与 NAS 之间是直连 WireGuard 路径,而且大量其他流量可以正常传输。如果 Tailscale 的加密隧道本身遭到选择性阻断,很难解释为什么同一条隧道里的 x.com 和出口 IP 检测都完全正常。

真正的线索来自 Mihomo 日志。失败请求中出现了明显不合理的目标地址:

  • Google 域名被解析到 Meta/Facebook 的地址段。
  • YouTube 也被解析到 Meta 的地址。
  • 部分请求甚至得到与目标服务完全无关的 IPv4 或 IPv6 地址。

这是典型的 GFW DNS 污染现象:域名查询返回了错误的 IP,导致 Mihomo 连接到错误的服务器。由于这些 IP 并不提供预期的服务,连接会报 TLS 错误或超时。Mihomo 的 sniffer 已经从 TLS 或 QUIC 流量中识别出 googleyoutubeinstagram,因此规则匹配是正确的,流量也选择了预期的代理组。问题在于:

override-destination: false

这个设置只让嗅探到的域名参与规则判断,实际连接仍然使用客户端拿到的那个错误 IP。

DNS 污染发生在哪一层

Tailscale 负责把 iPhone 的数据加密传到 Exit Node,但加密隧道并不会自动保证所有外部域名都通过可信 DNS 解析。如果域名查询最终从 NAS 一侧发往普通明文 DNS 上游,返回结果仍然可能被注入或污染。随后,iPhone 会把错误的目标 IP 放进已经加密的 Tailscale 数据包中。

从外部看,GFW 看不到 WireGuard 包里的域名和目标地址;但它可以影响发生在隧道外、NAS 上游网络中的普通 DNS 请求。Tailscale 只是可靠地把这个错误结果对应的连接送到了 Exit Node。

override-destination 修复最终连接

修复只需要改一个选项:

sniffer:
  enable: true
  parse-pure-ip: true
  override-destination: true

开启后,Mihomo 不再只把嗅探到的域名用于规则匹配,还会用它替换错误的目标 IP。后续代理连接以真实域名为目标,由代理路径完成正确的解析和连接。

新的实际流程变成:

客户端 DNS 得到错误 IP
  → 请求通过 Tailscale 到达 nasRemote
  → Mihomo 从 TLS/QUIC 中识别真实域名
  → 现有域名规则选择代理组
  → 用真实域名替换错误 IP
  → 代理侧解析并连接真实服务

这个选项并非没有代价。对于故意让 SNI 与目标 IP 不一致、使用特殊域名前置方式,或无法被当前 sniffer 识别的连接,重写目标地址可能改变应用预期;ECH 等无法提供可见域名的流量也不能依赖这种方式修复。不过本次 TUN 只接管 tailscale0,影响范围限于 Exit Node 客户端,而不是 NAS 上的全部服务。

结论

让 Tailscale Exit Node 的流量经过 Mihomo 是可行的。最终方案的关键点有四个:

  1. 在改变远程网络路径前,先建立一条不依赖 Tailscale 的救援通道。
  2. 只给 Mihomo 必要的网络能力,并用 include-interface: tailscale0 限制 TUN 的接管范围。
  3. 不改动已有代理组和规则,让 TUN 流量继续复用原来的 config.yaml
  4. 当 DNS 已经给出错误 IP 时,仅用域名做规则匹配还不够;需要 override-destination: true 修正实际连接目标。

目录