为什么 macvlan 容器无法回复 VPN 客户端
macvlan 容器无法与自己的宿主机通信,其回复也可能从错误的网关发出。先在宿主机侧添加 shim 路由,再选择源地址模型。
设想一台 Linux 宿主机:它同时终结 VPN,并在 macvlan 网络上运行一个容器。LAN 是 192.168.60.0/24,容器持有 192.168.60.210,VPN 客户端来自 10.95.0.0/24。客户端能到达服务,回复却始终不回来,而宿主机上的每条防火墙规则看起来都正确。这个症状由两个彼此独立的限制造成,修好其中一个,另一个仍然存在。
两个限制,一个症状
第一个限制是隔离:Docker 的 macvlan 驱动不允许容器通过父接口与自己的宿主机通信。宿主机协议栈与容器之间没有通路,宿主机上的路由怎么写都一样。
第二个限制是回程路径。容器的默认网关通常是 LAN 路由器,因此发给 VPN 客户端的回复从这个路由器出去。如果路由器没有回到 VPN 地址池的路由,回复就会在中间某处被丢弃。
从客户端看,这两种故障一模一样:请求到了,回复永远不来。它们需要不同的修复,而一条宽泛的 masquerade 规则可能掩盖其中一个仍然故障。所以第一个测试是在容器侧抓包,而不是再改一次防火墙。如果请求从未在那里出现,那起作用的就是隔离限制。
在宿主机侧把 shim 建窄
在宿主机上再加一个 macvlan 接口,并让它以桥接模式挂在同一个父接口上,宿主机协议栈就在该 LAN 网段上有了自己的身份。为它保留一个 DHCP 地址池之外、容器范围之外的地址,并且只把容器从它上面走。
ip link add macvlan-vpn link eth0 type macvlan mode bridge ip address add 192.168.60.250/32 dev macvlan-vpn ip link set macvlan-vpn up ip route add 192.168.60.210/32 dev macvlan-vpn src 192.168.60.250
第一条命令以桥接模式创建接口,这是该驱动文档记载的默认值,也是宿主机与容器之间通信所需的模式。第二条分配 /32,使该地址不会与 LAN 前缀冲突;第四条只把容器的地址送过 shim。
不要把整个 LAN 都改走新接口。原来的直连路由保持不变,宿主机路由故意做得很窄,这样只有一个容器获得通路,而该网段其余部分的行为不变。指向 shim 的默认路由会是另一种设计,后果也不同。保持具体,影响范围就会很小。
这些命令会改动正在运行的宿主机的网络配置。在专用设备或受管平台上,请使用该平台支持的配置机制,因为 root shell 里的交互式改动未必能挺过一次升级。
决定如何保留源地址
shim 就位后,容器可以到达宿主机,问题变成回复携带哪个源地址。保留原始 VPN 源地址是更干净的模型:容器看到真实的客户端地址,服务的日志和访问规则也能用上它。
要做到这一点,容器的网络配置必须把 10.95.0.0/24 的流量以 192.168.60.250 作为这条路径的网关发出去。这是容器自身配置中受支持的路由或网关设置,不是宿主机侧的技巧。要从容器内部确认,而不是假定设置已经生效。仍然指向 LAN 路由器的默认网关会继续把回复送进同一个洞。
如果获批的设计改为隐藏 VPN 源地址,就把翻译范围收窄:真实的 VPN 地址池、作为出接口的 shim,以及具体的容器和服务。
iptables -t nat -A POSTROUTING -s 10.95.0.0/24 -o macvlan-vpn -j SNAT --to-source 192.168.60.250
要把 shim 匹配为出接口,而不是隧道。把隧道指定为出接口的规则匹配不到发往容器的报文,这正是这类配置常犯的错误。规则要像上面那句话一样窄,并在地址池变化时复查。
从客户端验证,而不是从宿主机
从宿主机本身、绑定 shim 地址发出的请求,只能证明 shim 能到达容器。它不能证明容器能把流量回送给保留的 10.95.0.x 源地址,因为该测试中的源地址是 shim 自己的。
curl -sS --interface 192.168.60.250 -o /dev/null -w '%{http_code}\n' http://192.168.60.210:8080/health
sudo tcpdump -ni macvlan-vpn host 192.168.60.210第一条命令测试宿主机到容器这一段,第二条在真实 VPN 客户端发起请求时显示实际穿过 shim 的流量。要看入向的源地址,以及是否出现同一流的回复。
宿主机上必须启用转发,转发路径需要允许 VPN 地址池在服务端口上访问 shim,并允许已建立的回复。宿主机的 INPUT 规则并不授权转发流量,这正是路由看起来正确、报文却被丢弃的常见原因。
用真实凭据运行真实应用,然后确认同一容器上另一个无关服务仍然拒绝你。如果唯一让它可用的办法是把每条转发规则都打开,这个设计就没有完成。也要记下服务记录了哪个源地址,因为从今往后它自己的访问规则会用到那个地址。