踩坑实录:云服务器 Docker + Mihomo TUN 全局代理导致外部端口无法访问的终极解法

9 阅读6分钟

踩坑实录:云服务器 Docker + Mihomo TUN 全局代理导致外部端口无法访问的终极解法

1. 背景与现象

最近在云服务器上折腾大模型应用,使用 Docker Compose 部署了 LobeHub,对外暴漏了 3210 等端口。为了让 LobeHub 能够顺畅访问国外的 OpenAI/Google 等大模型 API,我在服务器上部署了 mihomo (Clash Meta) 代理,并开启了 TUN 全局路由模式 (auto-route: true)

诡异的现象出现了:

  • 只要启动 mihomo 的 TUN 模式,外部浏览器就无法访问服务器的 3210 端口(页面一直转圈直到超时)。
  • 一旦关闭 mihomo,或者停用 TUN 模式,外部访问瞬间恢复正常。
  • 服务器内部通过 curl 测试,网络一切正常。

显然,这是 TUN 模式劫持了全局网络导致的问题。

2. 核心原因剖析:非对称路由与 NAT 的多重魔法

在云服务器上开启 TUN 模式,与在个人电脑上完全不同。它会引发一个经典的**“非对称路由(Asymmetric Routing)”**问题,且由于云服务商的 NAT 机制和 Docker 的虚拟网桥,情况变得格外复杂。

  1. 请求入站 (Inbound): 外部用户的请求通过物理网卡(如 eth0)正常进入服务器,并被转发到 Docker 容器。
  2. TUN 劫持 (Hijack): 容器处理完毕准备回包。由于 mihomo 开启了全局路由接管,系统默认将所有外部出站流量塞进 tun0 虚拟网卡。
  3. 连接迷失: mihomo 没有主动发起过这个连接,收到回包后一脸懵,直接丢弃或代理到了国外。客户端永远等不到服务器的 [ACK],连接卡死。

更致命的是,云服务器通常处于 NAT 网络下(公网 IP 并不直接绑在物理网卡上,而是绑在网关上),这意味着我们不能简单粗暴地根据“源公网 IP”来做策略路由。

3. 破案过程:tcpdump 抓包发现的盲点

起初,我尝试使用 iptables 给外部进入的新连接打上防火墙标记 (CONNMARK),并配置策略路由(ip rule),强制带有标记的回包走物理网卡 eth0 返回。

配置后依然不通。通过开两个终端使用 tcpdump 分别监听物理网卡和 TUN 网卡:

Bash

# 监听物理网卡
sudo tcpdump -i eth0 -n port 3210
# 监听 TUN 网卡
sudo tcpdump -i tun0 -n port 3210

抓包日志揭示了残酷的真相:

服务端确实向外发送了回包 [S.](证明 iptables 标记生效,没进 TUN),客户端也发来了确认包 [P.]。但是,服务端却在疯狂重发初始包。

原因在于策略路由的优先级盲区:

当外部的后续数据包进入服务器时,iptables 会将其目标 IP 转换为 Docker 容器的 IP(如 172.18.0.4)。接着,Linux 去查我们自定义的回程路由表(只包含一条指向外网的默认路由),该路由表根本不知道 172.18.x.x 在哪! 于是,Linux 把本该发给 Docker 的数据包,又通过 eth0 扔回了互联网。

4. 终极解决方案

要彻底解决这个问题,我们需要同时利用 iptables 连接标记机制,以及带有高优先级的精细化策略路由

核心逻辑:

  1. 告诉系统:发往本地局域网和 Docker 网段(10.x, 172.16.x, 192.168.x)的流量,最高优先级查询系统主路由表(main)。
  2. 对于从物理网卡 (eth0) 进来的外部请求,打上 0x100 标记。
  3. 对于带有 0x100 标记的外部回包,强制走自定义的 100 号路由表(从 eth0 原路返回)。

具体实施步骤

为了保证每次重启服务器或重启网络后规则依然生效,并且避免规则重复叠加,我们直接编写一个 Shell 脚本并配置为 systemd 开机服务。

第一步:修改 mihomo 配置放行内网

mihomoconfig.yaml 中,确保排除了本地和 Docker 网段:

YAML

tun:
  enable: true
  auto-route: true
  route-exclude-address:
    - 10.0.0.0/8
    - 172.16.0.0/12
    - 192.168.0.0/16
第二步:获取网卡与网关信息

执行 ip route show default,找到你的物理网卡名称和网关 IP。

输出示例:default via 10.1.12.1 dev eth0 ...

这里网关是 10.1.12.1,网卡是 eth0

第三步:创建修复脚本

创建脚本文件:sudo nano /usr/local/bin/fix-tun-route.sh

填入以下内容(注意将 10.1.12.1eth0 替换为你自己的网关和网卡名):

Bash

#!/bin/bash
# 等待网络完全初始化
sleep 5

# ================= 1. 清理旧规则 (保证脚本的幂等性) =================
# 清理 IP Rule (2>/dev/null 用于屏蔽规则不存在时的报错)
ip rule del to 10.0.0.0/8 table main pref 100 2>/dev/null
ip rule del to 172.16.0.0/12 table main pref 100 2>/dev/null
ip rule del to 192.168.0.0/16 table main pref 100 2>/dev/null
ip rule del fwmark 0x100 table 100 pref 200 2>/dev/null
ip rule del fwmark 0x100 table 100 2>/dev/null
ip route del default table 100 2>/dev/null

# 清理 iptables
iptables -t mangle -D PREROUTING -i eth0 -m conntrack --ctstate NEW -j CONNMARK --set-mark 0x100 2>/dev/null
iptables -t mangle -D PREROUTING -m conntrack --ctstate ESTABLISHED,RELATED -j CONNMARK --restore-mark 2>/dev/null
iptables -t mangle -D OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j CONNMARK --restore-mark 2>/dev/null

# ================= 2. 设置路由表与策略 =================
# 创建 100 路由表,指定出站网关
ip route add default via 10.1.12.1 dev eth0 table 100

# 关键修复:设置最高优先级 (pref 100),本地/Docker 网段必须查主表!
ip rule add to 10.0.0.0/8 table main pref 100
ip rule add to 172.16.0.0/12 table main pref 100
ip rule add to 192.168.0.0/16 table main pref 100

# 优先级 200:带有 0x100 标记的回包走 100 路由表
ip rule add fwmark 0x100 table 100 pref 200

# ================= 3. 设置 iptables 标记 =================
# 新连接打上 0x100 标签
iptables -t mangle -A PREROUTING -i eth0 -m conntrack --ctstate NEW -j CONNMARK --set-mark 0x100
# 转发过程与出站过程恢复标签
iptables -t mangle -A PREROUTING -m conntrack --ctstate ESTABLISHED,RELATED -j CONNMARK --restore-mark
iptables -t mangle -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j CONNMARK --restore-mark

赋予执行权限:sudo chmod +x /usr/local/bin/fix-tun-route.sh

第四步:配置 Systemd 开机自启

创建服务文件:sudo nano /etc/systemd/system/fix-tun-route.service

Ini, TOML

[Unit]
Description=Fix TUN Asymmetric Routing with IPTables
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/fix-tun-route.sh
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target

启用并启动服务:

Bash

sudo systemctl daemon-reload
sudo systemctl enable fix-tun-route.service
sudo systemctl start fix-tun-route.service

5. 总结

在云服务器上强开 TUN 模式不可避免会遇到流量冲突。遇到此类网络不通的问题,不要盲目猜疑,直接上 tcpdump 抓包看 [S], [P], [ACK] 的走向,往往能拨云见日。

这套 iptables 标记 + ip rule 优先级策略的组合拳,不仅适用于 Mihomo/Clash,同样适用于所有在 Linux 云服务器上运行全局 VPN(如 WireGuard, Tailscale 的 exit node 等)导致的外部端口失联问题。

希望这篇文章能帮你少走弯路!