Docker 容器流量绕过主机 VPN:如何核查
主机 VPN 只覆盖主机的网络命名空间。用这些检查判断桥接容器流量是进入隧道还是直接流出。
主机上的浏览器显示 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 see | Usual cause | First check |
|---|---|---|
| Host exits through the VPN, container shows the ISP address | The container never entered the tunnel | Default route inside the container |
| Container works until the daemon restarts, then stops | Rules outside the supported hook, or a changed backend | Daemon's reported firewall backend and DOCKER-USER |
| Container loses internal hosts after the tunnel comes up | VPN prefix overlaps the Docker bridge subnet | ip route on the host, then docker network inspect |
| Small replies arrive, large ones hang | Tunnel MTU or missing MSS clamping | Link MTU on both ends of the tunnel |
| Container falls back to the ISP address when the tunnel drops | Nothing enforces the kill switch for forwarded traffic | Stop the tunnel with a request in flight |
重叠那一行值得单独说一句。VPN 配置若下发 172.17.0.0/16 的路由,或下发恰好包含你网桥前缀的 10.0.0.0/8,隧道就会声称拥有容器子网,发往被转换地址的回包会走错方向或消失。重新编号 Docker 网络,通常比不断堆例外更干净。
把覆盖范围写清楚,然后故意打破它
两种设计经得起评审。共享 VPN 容器的命名空间,所有被覆盖的工作负载共用一条隧道、一个重启边界,这很简单,直到某个服务需要不同出口或自己的 DNS 路径。或者 把容器网络路由到主机上的 VPN 网关:开启转发,把网桥前缀指向隧道,显式处理回程流量。这样换来按网络的控制力,代价是一套要跨越守护进程与客户端升级来维护的规则。
无论哪种,真正要紧的测试是故障场景。在容器请求还在途中时停掉隧道,看数据包去了哪里。如果它退回直连路径,说明覆盖从未被强制执行,因为客户端的 kill switch 只管客户端自己的流量,不管来自网桥的转发包。之后再重启守护进程和主机,第二次确认结果;手写规则消失的正是那一刻。
在宣布完成之前,确认四件事:隧道接口与流量处于同一命名空间,容器测试是在真实容器里跑的,域名解析与两个地址族都检查过,隧道故障测试仍然保持关闭。然后把防火墙后端、客户端版本和每条规则的负责人记下来。正是这份记录,让下一次升级不会留下一堆没人写下来的旧假设。