宿主机通过隧道上网,旁边的容器却什么都到不了。从自带 VPN 客户端的容器发布端口更糟:请求进来了,响应却永远出不去。两者都是宿主机上的路由决策,Docker 只是传话的。

三个问题就能判定你遇到的是哪一种,整个过程大约一分钟。

两种表现相同的故障

第一种是回程问题。发布的端口被转换进容器,请求到达,响应却按宿主机的默认路由发出,而这条默认路由已被 OpenVPN 客户端指向 tun0。响应从远端不期望的地址发出,于是被丢弃。第二种是前缀冲突:Docker 默认网桥占用 172.17.0.0/16,而公司隧道下发的正是同一网段。

一种靠路由表修复,另一种靠给网桥重编号。还没弄清是哪种就动手,正是把本来能用的容器弄坏的方式。

用三个问题来判断

问题一:停掉 VPN 后故障是否消失?如果隧道停掉一切正常,就怀疑回程。问题二:ip route show table all 是否两次列出同一前缀,一次在网桥上、一次在 tun0 上?那就是冲突。问题三:受影响的地址是容器、NAT 来宾,还是桥接来宾?

第三个问题很关键,因为三种情况的正确改法不同,把容器的改法套到 libvirt 来宾上会白白耗掉一个晚上。

ip route
ip route show table all | grep -E '172\.1[7-9]|10\.8'
docker network inspect bridge --format '{{.IPAM.Config}}'

回程:为单个源地址建一张表

当容器流量要走物理网卡出门,而宿主机自己走隧道时,基于源地址的路由 是改动最小的方案。加一条匹配容器地址的规则,把它送进专用路由表,宿主机上其他东西都不动。

ip rule add from 172.22.0.3 lookup 10
ip route add default via 172.22.0.1 table 10

凡是源地址为该地址的包都先查表 10,表 10 把它送往网桥网关,而不是隧道。

用 ip route get 1.1.1.1 from 172.22.0.3 验证。输出里应当是网桥接口,而不是 tun0。这样加的规则重启就没了,所以要写进管辖该容器的 unit 或网络钩子。

同样的做法也适用于 WireGuard 容器,它的回程同样需要一条显式规则。

前缀冲突:给网桥重编号

一张路由表把一个前缀送到一个地方。如果 VPN 下发 172.17.0.0/16,而 Docker 网桥已经占着它,那么度量值小的赢:Docker 以度量 0 装上直连路由,下发的路由以 50 进来。于是 Docker 照常工作,隧道却像坏了,或者反过来,取决于安装顺序。与其跟度量值较劲,不如给网桥重编号。

{
  "bip": "10.200.0.1/24",
  "default-address-pools": [{"base": "10.201.0.0/16", "size": 24}]
}

改完重启 Docker,并用旧地址池建的网段要重新创建。只改度量值,只是把冲突推迟到下次启动。

虚拟机的答案又不一样

默认 NAT 网络里的 libvirt 来宾继承宿主机的路由,一旦宿主机隧道占住默认路由,它就出不去。把来宾换成桥接或 macvtap 设备,它就能在物理网络上走自己的路。在 virt-manager 里添加硬件,选 Network,把来源设为 macvtap 宿主设备,型号保持 virtio,然后停用旧的 NAT 接口。

macvtap 来宾通常无法访问自己的宿主机。这是预期行为而非故障,应当写进你留下的备注。

如果来宾只需要宿主机的局域网,厂商客户端里也许有那个缺失的开关:用 Mullvad 时,mullvad lan set allow 就能恢复本地可达性而不必动 libvirt。而当容器只应通过隧道访问时,把发布的端口绑到隧道地址上,-p 10.8.0.5:8080:8080,而不是在所有地址上发布。

相关文章