开发容器连不上只走 VPN 的主机:该从哪里查
浏览器和容器拥有不同的解析器、代理和路由上下文。在改动任何 VPN 设置之前,先弄清出错的那个进程用的是哪一套。
私有 API 在你的桌面浏览器里能打开,在开发容器里却失败。这不是矛盾,也很少是 VPN 本身的问题。浏览器请求和失败的请求由不同的进程发出,解析器、代理和路由各不相同,而且往往来自完全不同的网络命名空间。"在我的笔记本上"可能指 Windows 主机、某个 WSL 发行版、Docker Desktop 虚拟机,以及那台虚拟机里的容器——这四种上下文对同一个主机名的回答可能各不相同。在动任何一个设置之前,先搞清楚出错的那个进程用的是哪一种。
先定位进程,再改设置
从容器引擎入手,而不是 VPN 客户端。先问命令行在和哪个上下文对话、这个引擎跑在什么操作系统上,因为由桌面虚拟机管理的 Linux 容器,和由本地守护进程管理的容器走的是两条不同的路:
docker context ls
docker info --format '{{.OperatingSystem}} / {{.Name}}'
docker run --rm alpine ip route
在 Windows 上,wsl -l -v 会告诉你该发行版是否运行在 WSL 2 之下,这会把它放到自己的虚拟交换机后面。这个细节决定了容器是共享主机的路由表,还是位于一个 NAT 之后、需要端口转发或主机名才能到达主机。
固定一个请求来做对比
在私有主机上挑一个无害的端点,把它用于每一次对比:从浏览器、从主机终端、从 WSL 发行版,以及从容器内部。每次尝试都记录四件事:解析到的地址、是否用了代理、失败类别和耗时。名称解析、建立连接、TLS 校验和 HTTP 状态是四种不同的问题,把它们混成一句"就是不通"会白白浪费一个上午。
在出错的容器内部做对比,并设置超时,免得沉默的防火墙把终端卡上几分钟。新建的容器能复现引擎的网络上下文,但复现不了应用容器的命名空间、代理变量或解析器,所以 docker exec 进真正那个容器才是权威测试,旁路容器只是用来隔离问题出在引擎还是容器上:
docker exec -it app /bin/sh -c "getent hosts api.internal; curl -v --max-time 8 https://api.internal/ 2>&1 | tail -20"
docker run --rm --network container:app curlimages/curl:latest -sv --max-time 8 https://api.internal/ 2>&1 | tail -20
alpine 基础镜像带 busybox,却没有 curl——正是这类细节让照抄的诊断命令块失败:挑一个带该客户端的镜像,或者在同一次运行里把它装上。拿无关的公共站点做对比什么都隔离不出来。如果 https://example.com 在容器里能通,你只学到容器能上网,对能否通过隧道到达私有地址一无所知。同理,开一个新终端只能证明新终端好用,证明不了它和失败的应用共享同一个运行时。
Docker Desktop 可以和 VPN 一起工作
Docker 自己关于 VPN 与主机连通性的文档把这种情况描述为受支持的配置,老实的总结是:它通常能用,而不是天生就坏。不要想当然地认为桌面虚拟机会在设计上绕过主机隧道;在很多配置里,流量是正确地跟着主机隧道走的,这正是"Docker 无视 VPN"这类传言既自信又错误的原因。
真正要紧的仍是所用的后端、客户端的分离隧道策略,以及虚拟机拿到的解析器配置。一个只装路由不装 DNS 的 VPN 客户端,或者推送了虚拟机无法访问的 DNS 服务器,会在主机正常时造成容器内部的名称解析失败,因为主机多一个解析器或有一份缓存答案。这是披着容器外衣的解析器问题。你每次重连 VPN 它都还在,因为隧道本来就在承载你让它承载的流量。
地址重叠和 MTU 是两种不同的故障
有两种故障值得单独诊断,因为它们常被误认为访问策略。第一种是地址重叠:如果 VPN 推送的网段与某个容器网络的默认网桥子网冲突,容器去该网段的路由就指向本地网桥,永远到不了隧道。与其猜,不如查实际子网:
docker network inspect bridge --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}'
ip route get 10.20.30.40
第二种是 MTU 问题。如果连接建立、TLS 也完成,但较大的响应卡住,那就是包大小问题,不是授权问题,换 DNS 记录永远治不好。动手之前先用一个小请求和一个大请求各测一次,把两个结果都记下来。
重叠的修法是换一个容器子网或加一条限定范围的路由,由地址规划的负责人来选。MTU 卡顿的修法是在你掌控的那一层,把隧道或接口的 MTU 钳制或调低到能走通这条路径的值。
代理配置是按进程算的,不是按机器算的
在容器内部,localhost 指的是这个容器自己的网络上下文,所以在那里用这个名字访问不到跑在主机上的代理。Docker Desktop 提供 host.docker.internal 用于访问主机,在 Linux 上你要用 --add-host=host.docker.internal:host-gateway 显式加上。无论哪种方式,这个名字给你的是一条通往主机的路;它不会创建代理服务,也不授权你去使用它。
使用为实际环境提供的代理地址和端口,并检查是谁在读它。在交互式 shell 里导出的变量,不会自动配置每个编辑器扩展、语言服务器、构建工具和后台守护进程。真正要紧的是发出失败请求的那个进程,所以要看它的环境,而不是你终端的环境。
最后,让修法自己把话说清楚。有用的解决方案会点名出错的那条边界、改动了它的那个设置,以及它现在影响的那批进程。依次重启 VPN、编辑器和引擎也许能改变症状,但不会给下一个人留下任何可用的东西。