Kubernetes 里的 OpenVPN Pod 什么都不转发?检查 ip_forward
客户端能 ping 通 VPN Pod,再往后就什么都不通。命名空间里的转发是关着的,而修复必须经得起 admission、重启和回程路由的考验。
笔记本上隧道是绿的,客户端日志说路由已经推送,而唯一能回应的地址是 VPN 服务器自己的隧道 IP。再往后不管是 ClusterIP、另一个 Pod 的地址还是公网主机名,ping 一律石沉大海。这个组合并不含糊,而是很有诊断性:解封装是通的,第二跳不通。
我是在一个用 chart 安装 OpenVPN 镜像的 Debian 节点上遇到这事的,隧道反而是最容易的部分。客户端拿到了 10.8.0.x 的租约,10.8.0.1 有回应,而 10.96.0.10 这个集群 DNS 服务超时了。服务端日志里没有任何异常,正因如此,人们才开始重写那些根本不需要碰的 iptables 规则。
VPN Pod 就是一台路由器,这话值得直说。加密包到达后,在 Pod 自己的网络命名空间里被拆开,然后必须从另一个接口出去。除非那个命名空间内部启用了转发,否则 Linux 会拒绝这第二步。
客户端能到达什么、不能到达什么
能回应的地址属于隧道端点,由 VPN 进程在本地持有,所以完全不涉及转发。其余一切,包括另一个 Pod、服务 VIP 或公网,都需要内核把包从 tun0 移到 eth0。当这次移动被拒绝时,症状是整齐的沉默而不是拒绝,因为没有任何东西会沿着隧道回送 ICMP 错误。你得到的是超时,而不是不可达消息,这正是没人做路由的线索。
两分钟的归类就能把层次缩小。如果 DNS 名字解析失败,而直接 ping 服务 IP 成功,问题就在 DNS 管道,而不是转发。如果对端地址之外的所有目的地都超时,包括裸 IP,那就从命名空间查起。
在命名空间内部读取这项设置
每个 Pod 都有自己的网络命名空间,而 net.ipv4.ip_forward 是按命名空间生效的 sysctl。在节点上改它对容器毫无意义,这正是让人在错误的机器上耗掉一下午的可靠办法。
kubectl exec -it deploy/openvpn -- sysctl net.ipv4.ip_forward
# net.ipv4.ip_forward = 0
零证实了这个判断。这个值也不会从节点的运行时配置继承:每次创建容器,它都从镜像默认值开始。所以用 kubectl exec ... sysctl -w 手工改的东西会在下次重启后消失,并以一个看起来时有时无的故障回来。
让它生效,好让清单把它保留下来
持久做法是在同一个 Pod sandbox 里运行一个 init 容器,在 VPN 容器启动之前写入该值。Pod 里的容器共享同一个网络命名空间,所以设置会带进主容器。
initContainers:
- name: enable-forwarding
image: busybox:1.36
command: ["sysctl", "-w", "net.ipv4.ip_forward=1"]
securityContext:
privileged: true
正是 privileged: true 这一行让评审者反对,而他们是对的。写 sysctl 要么需要特权,要么需要范围很窄的能力,而运行受限 Pod Security Standard 的集群可能直接拒绝特权 init 容器。如果 admission 拒绝了这个 Pod,你在创建时就会知道,至少这很诚实,也好读。
更干净的选择是 Pod 级的 securityContext.sysctls 列表,它让 kubelet 在不给任何容器特权的情况下应用带命名空间的 sysctl。它只覆盖集群白名单上的名字。net.ipv4.ip_forward 在不在名单上取决于 kubelet 是怎么配置的,而请求名单外 sysctl 的 Pod 会在 admission 被拒,或者在重试 sandbox 时卡在 CreateContainerConfigError。在断定这个字段坏了之前,先查白名单。
在手工实现这两种方案之前,先看看 chart 是不是已经暴露了开关,通常形如 ipForwardInitContainer: true。用 chart 的值能让你的 override 文件保持很小,也能扛过下一次升级,而手工打补丁的 Deployment 改动做不到。
后半段:回程路由与策略
打开转发是必要条件,不是充分条件。如果客户端使用集群网络没有路由的私有地址池,回包就找不到进隧道的路,Pod 必须把这份流量伪装到目的地。
策略叠在上面。NetworkPolicy 对象和云防火墙规则是按包经过转换后携带的源地址来评估的,所以为客户端地址池写的规则一旦开启伪装就可能悄悄失配。
当客户端只需要访问集群内的服务时,让 VPN 终止在服务 VIP 上,比路由进 Pod 网络的设计更小。它还让客户端地址池完全远离 CNI 的路由表。
把出错的那一跳从客户端隔离出来
由内向外做,在第一个沉默的环节停下。先测 VPN 对端,再试某个 Pod 的地址,然后试服务 VIP,最后才试公网目的地。每成功一步,边界就外移一跳;每失败一次,就点出值得检查的那一层。任何发布之后都要 exec 进去再读一次 sysctl,因为被替换的 Pod 会从镜像默认值开始,而从未写进清单的修复早就不在了。
Kubernetes 关于在集群中使用 sysctl 的文档列出了哪些名字可以通过 Pod 安全上下文设置,当 admission 拒绝这项改动时,那里是第一个该查的地方。