同一台机器上的浏览器打开毫无怨言,你的 Compose 服务却对那个 API 超时。这个差异就是线索:成功的请求从一个带 VPN 隧道的宿主机发出,而卡住的请求来自一个拥有自己网络栈的容器。容器内部没有任何东西知道隧道的路由,所以无论名字怎么写,私有目标都不可达。解决办法是刻意经由宿主机跳一跳,而不是更聪明的路由表。

先弄清两条路径为何不同,再决定隧道应该住在哪里。

为什么宿主机能通而容器不通

把隧道建起来会给宿主机一个接口、一组路由,通常还有一条由 VPN 提供的解析器记录。容器这些一样也继承不到:它启动时只有引擎网桥上的一个接口和指向该网桥的默认路由。装在宿主机上的路由,在该命名空间内运行的任何东西都看不见。

所以症状看起来不是单纯坏掉,而是前后矛盾。宿主机能解析私有名字并连接,因为名字和路由在那里都存在。容器把数据包发往一个网桥路径上谁都到不了的私有地址,请求于是超时。把名字加进容器的 hosts 文件毫无用处,因为从容器所在的位置根本到不了那个地址。

从容器内部访问宿主机

容器里的进程去拨 localhost,拨的其实是容器自己。Docker Desktop 会发布 host.docker.internal 这个名字,指向运行引擎的那台机器;Linux 引擎除非服务主动要求,否则不会定义它,下面的映射正是做了这个要求。这样一来,应用瞄准的就是一个名字和一个端口,而不是私有地址。

services:
  app:
    extra_hosts:
      - "host.docker.internal:host-gateway"
    environment:
      INTERNAL_API_HOST: "host.docker.internal:8666"

隧道留在这跳的宿主机一侧,那个变量里的端口就是代理要监听的端口。丢了隧道的容器现在会在一个已知的跳点上失败,而不是去尝试一条从来就不存在的路由。

选择隧道住在哪里

需要私有 API 的 Compose 服务有三种形态,区别在于哪个进程拥有隧道。VPN 客户端可以跑在容器镜像里,这需要暴露隧道设备并授予随之而来的网络管理能力。应用也可以共享另一个隧道容器的网络命名空间,这样两个进程看到同一个网络栈和同一张路由表。或者隧道留在宿主机上,由宿主机向容器内做一条窄范围的转发。

前两种会把配置文件、重连行为和解析器策略统统拉进项目,并让文件里每个服务都依赖隧道的健康。为了一个 API 调用,这样的耦合太多了。对于一个只读单个内部端点的开发栈,宿主机侧的转发是改动最小的做法,同时仍然把凭据留在平台团队管理的地方。

在宿主机上发布一条窄路径

从转发器继承到的东西入手:用拥有隧道的那个账户来运行它,它的出站套接字就会沿用宿主机的路由和宿主机的解析器。一个 nginx location 块对应一条路径。

server {
    listen 8666;
    location /search/ {
        proxy_pass http://10.20.30.40:8090/;
        proxy_set_header Host internal-api.example;
        proxy_read_timeout 60s;
    }
}

把地址和端口当作占位符,换成 API 真正监听的组合。每多一个上游就值得单独一个 location 块,而且每个块只应放行本服务会调用的路径,这样代理就不会变成通往私有网络的一扇大门。监听的绑定地址要选对:经 host-gateway 到达的客户端绑网桥地址,只有客户端共享宿主机网络命名空间时才绑 loopback,绝不要绑到 0.0.0.0。端口也要挑一个还空着的:已被占用的号码会让代理没在监听,而容器继续超时,这和你最初遇到的网络故障无法区分。

端口、名字与 TLS 的抉择

名字解析的分裂方式和路由一样。容器的公开查询走引擎的解析器,所以在宿主机上能解析的私有名字,在容器里解析不了。真正解析私有名字的是代理,因为它继承了宿主机的解析器。

TLS 是个会被一拖再拖、然后解释下一次故障的决定。如果代理原样转发字节,容器看到的就是 API 自己的证书,需要把内部 CA 放进它的信任库。如果代理终结 TLS,这份信任就转移到宿主机,应用开始用收到的 host 头来拼接链接。选定一种安排并应用到所有资源,因为另一种安排发出的重定向会指向容器无法解析的主机名。

从服务内部验证

请求要从容器发,而不是从宿主机发:先对代理路径来个普通的 curl,再跑应用自己的健康检查。如果代理有回应而应用仍然失败,就对比三样东西:它拼出来的基础 URL、环境里任何代理变量,以及它信任的 CA。这些都在容器的环境和日志里,修好其中一样,VPN 依旧不受打扰。

趁记忆还新,把选定的安排写进项目的 README。否则下一个撞上这个超时的人,会花掉同一个下午,重新发现隧道原来住在宿主机上。

相关文章