主机上的浏览器显示 VPN 出口地址,容器却报告你的家庭宽带,于是有人说这是泄漏。其实什么都没泄漏。主机隧道是归属于主机网络命名空间的一组接口、路由和规则,而桥接网络上的容器是另一个命名空间,有自己的默认路由。我遇到过这样的 Compose 栈:VPN 客户端连了几周,却没有任何一个容器数据包进去过。

先看命名空间,再看托盘图标

命名空间就是全部理由。Docker 在桥接网络上启动容器时,会创建一个网络命名空间,把 veth 对的一端接进去,并给这个命名空间分配地址和默认路由。隧道接口无论叫什么名字,都活在主机的命名空间里。容器内的流量看不到它,任何客户端设置也无法从外部改变这一点。

有两种受支持的做法可以走出隔离。--network host 把容器放进主机命名空间,于是它继承主机的路由,也就继承了隧道。--network container:vpn 共享另一个容器的命名空间,多数边车设计正是这样让工作负载经由 VPN 容器出网。两者都是明确的选择;通常只有一个是部署者真正想要的。

主机模式并不是免费的升级。容器不再拥有自己的地址,端口发布不再生效,持有 NET_ADMIN 的进程可以在里面改写主机路由。接受这一点可能是对的。假定它已经生效,才是人们花一晚上去调试一条从来不在路径上的隧道的原因。

跟着数据包离开网桥

桥接容器把数据包发给自己的默认网关,也就是主机上的网桥接口。随后主机转发该数据包,并在默认配置下于离开之前把源地址转换成自己的地址。正是这一步转换悄悄让策略失效。

等数据包到达隧道端点时,它携带的是主机地址。任何以容器原始地址为依据的规则、策略路由或分流决定都不再匹配,两个本应从不同出口离开的容器也无法仅凭源地址区分。如果需要按容器分流,决策必须发生在转换之前,或者发生在你掌控的命名空间之内。

Docker 自己维护这些规则,取决于守护进程配置了哪个防火墙后端。写在受支持的转发流量钩子 DOCKER-USER 之外的手工规则,会在守护进程重启或主机升级时被重排或丢弃。Docker 的数据包过滤文档给出了链的顺序;实务要点是每条规则都需要一个负责人,以及一条你真正验证过的重载路径。

把两个系统分开测试

一个命名空间里的证据说明不了另一个。在 VPN 容器里发起的请求只能证明隧道对该容器有效。它不是关于主机的证据,而同一条命令在 --network host 容器里执行,也不是关于网桥的证据。

# host namespace
ip route get 1.1.1.1
ip -brief addr show

# a fresh container on the same bridge network
docker run --rm --network appnet alpine sh -c 'ip route; cat /etc/resolv.conf; ip -6 route'

然后在真实应用容器里重做测试,而不是在相似的镜像里。应用可能带有多余路由、代理环境变量或另一个解析器,这些都会改变答案。每次都检查域名解析和第二个地址族:用户自定义网桥上的容器通过 Docker 内嵌解析器在 127.0.0.11 上解析,而网桥上的 IPv6 默认路由可能在你忙于测试纯 IPv4 隧道时把流量带出去。

先读模式,再动凭据

主机正常而容器不正常时,原因几乎总是结构性的,轮换密钥或重装客户端只会浪费一下午。先把症状分类,修法通常自己会说出名字。

What you seeUsual causeFirst check
Host exits through the VPN, container shows the ISP addressThe container never entered the tunnelDefault route inside the container
Container works until the daemon restarts, then stopsRules outside the supported hook, or a changed backendDaemon's reported firewall backend and DOCKER-USER
Container loses internal hosts after the tunnel comes upVPN prefix overlaps the Docker bridge subnetip route on the host, then docker network inspect
Small replies arrive, large ones hangTunnel MTU or missing MSS clampingLink MTU on both ends of the tunnel
Container falls back to the ISP address when the tunnel dropsNothing enforces the kill switch for forwarded trafficStop the tunnel with a request in flight

重叠那一行值得单独说一句。VPN 配置若下发 172.17.0.0/16 的路由,或下发恰好包含你网桥前缀的 10.0.0.0/8,隧道就会声称拥有容器子网,发往被转换地址的回包会走错方向或消失。重新编号 Docker 网络,通常比不断堆例外更干净。

把覆盖范围写清楚,然后故意打破它

两种设计经得起评审。共享 VPN 容器的命名空间,所有被覆盖的工作负载共用一条隧道、一个重启边界,这很简单,直到某个服务需要不同出口或自己的 DNS 路径。或者 把容器网络路由到主机上的 VPN 网关:开启转发,把网桥前缀指向隧道,显式处理回程流量。这样换来按网络的控制力,代价是一套要跨越守护进程与客户端升级来维护的规则。

无论哪种,真正要紧的测试是故障场景。在容器请求还在途中时停掉隧道,看数据包去了哪里。如果它退回直连路径,说明覆盖从未被强制执行,因为客户端的 kill switch 只管客户端自己的流量,不管来自网桥的转发包。之后再重启守护进程和主机,第二次确认结果;手写规则消失的正是那一刻。

在宣布完成之前,确认四件事:隧道接口与流量处于同一命名空间,容器测试是在真实容器里跑的,域名解析与两个地址族都检查过,隧道故障测试仍然保持关闭。然后把防火墙后端、客户端版本和每条规则的负责人记下来。正是这份记录,让下一次升级不会留下一堆没人写下来的旧假设。

相关文章