容器仍然在控制网桥上应答,但每个出站数据包都从办公区 macvlan 出去。没有任何东西故障。两个接入各提供一个网关,而唯一的默认路由落在了你没有打算用的那张网上,于是服务照常运行,只是流量绕了远路。等到有人察觉时,证据是一张路由表,而不是一条报错。

常见的解释是 Docker 碰巧选错了网络。实际上旧逻辑按所连网络名称的字母顺序打破平局,等于让路由成了标签的副作用。gw_priority 用一个你主动选择的数字取代它,方案写清楚之后,切换大约只要一分钟。

默认路由为什么会落到错误的网络

服务加入的每张网络都能提供自己的网关,而容器最终仍然只有一条默认路由。在 Compose 2.33.1 之前,平局靠网络名称排序决定,所以像 control 这样排在 office 前面的标签就决定了流量去向。容器内部没有任何地方记录这个决定是随意的,因此它能通过代码评审,日后才以一条奇怪的路径暴露出来。把策略写进 compose 文件,是让这个选择可被检查的唯一办法。

真正能改变哪张网胜出的有两件事:你赋予的 gw_priority 数值,以及究竟接入了哪些网络。加第三张网,候选集合就变大;去掉 macvlan,就没有东西再和网桥竞争。这两步都不会出现在应用日志里,因为错误的默认路由很少把连接彻底弄断。它只是让回应来自对端可能没料到的地址。

用 gw_priority 固定网关

从 2.33.1 版本起,Compose 在每个服务到网络的接入上暴露 gw_priority,数值最大的胜出。服务级没有单独的网关键,所以这个值属于服务的 networks 块。下面的例子把默认路由交给 control,同时保持 office 对它直连的 LAN 仍然可达。

services:
  vpn:
    networks:
      control:
        gw_priority: 1
      office:
        gw_priority: 0

networks:
  control:
    driver: bridge
    ipam:
      config:
        - subnet: 172.30.50.0/24
  office:
    driver: macvlan
    driver_opts:
      parent: eno1.120
      macvlan_mode: bridge
    ipam:
      config:
        - subnet: 192.168.120.0/24
          ip_range: 192.168.120.192/27
          gateway: 192.168.120.1

把这个块读成两个独立控制:gw_priority 决定默认路由,ipam 段决定地址分配。只改 subnet 不会触动出口选择,而这也是这类修复最常被做了一半的地方。把优先级放在它所治理的网络旁边,下一位读者就不必再去找这个决定。

不要把 gw_priority 和旁边那个普通的 priority 属性混为一谈。后者只决定连接创建顺序,对路由只字未提,所以它无法选择默认网关,也修不好接口命名。如果有人建议提高 priority 来引导流量,这个建议是错的,即使结果顺序碰巧看起来正确。

确认究竟哪张网络在承载默认路由

要在运行中的容器内部检查,因为宿主机的路由表描述的是宿主机,而不是服务所用的命名空间。先跑配置检查,让拼写错误在重启之前就失败,然后读取容器实际安装的路由。

docker compose config --quiet
docker exec vpn ip -4 route show default
docker exec vpn cat /proc/net/route

正确的结果会写出网桥设备和 172.30.50.1;错误的结果会写出 192.168.120.1 和 macvlan 设备。如果镜像里没有 iproute2 包,/proc 文件仍然会报告默认路由,目标为 00000000,网关字节是反序的。设备名不清楚时,把接口那一列和 docker inspect 对照。

macvlan 是物理端口,不是网桥的克隆

macvlan 让容器在线上拥有第二个身份,这正是访问本地设备能成功的原因。它也意味着交换机端口和虚拟化平台必须接受额外的 MAC 地址,而很多云网络会默默拒绝。先拿一个容器试,再规划整批,因为被拒的 MAC 通常表现为链路是通的,却什么都过不去。

macvlan_mode: bridge 这个设置是 macvlan 驱动的一种模式,不是 Docker 的软件网桥;passthru 是另一个专门模式,而不是默认值。Linux 也会阻止 macvlan 容器访问自己的宿主机 经同一个父接口,所以保留控制网桥,而不是指望一个接入能同时干两件事。

在 Compose 看到它之前先给 VLAN 父接口命名

Docker 会把 eno1.120 这种带点的父接口当作 802.1Q 子接口,并自行创建 VLAN 设备。这份便利有边界:派生出来的名字取决于物理接口名,过长的名字会把结果推到周边工具预期之外。在宿主机的网络配置里建一个命名 VLAN 接口,再把 parent 指向它,两头都可预测。

记住 networks 下面的键是 Compose 名字,不是 Linux 设备。在宿主机上找名为 office 的接口会失败,因为内核只看到父接口和容器背后的 veth 对。

在上面给 VPN 留出位置

VPN 客户端在这些网络存在之后才接入,所以它可以不受 gw_priority 影响地添加更具体的路由、策略规则和过滤。规划远端前缀时要同时对照两个本地 subnet,也包括控制路径所需的回程。当远端前缀和办公 LAN 重叠时,去修地址规划,而不是把一整段私有地址从隧道里剥掉。

VPN 启动之后、以及重连之后再检查一遍地址、路由和服务路径,因为重连正是手工添加的路由消失的地方。第一次测试之前先把期望的表写下来,这样之后的比较才是一份 diff,而不是靠回忆。把网络的长期归属留在 Compose 里,让 VPN 配置只管它自己的策略。

相关文章