能 ping 通网关,却访问不了它后面的任何东西,这是转发问题而不是隧道问题的形态。本地送达网关只能证明隧道已建立、地址分配已生效;它完全不能说明内核是否会把数据包从一个接口转发到另一个接口。在改动任何 sysctl 之前,先确认这台机器本来就该为这些流量做路由,并找到承载客户端侧接口的网络上下文。然后在那里读取这个值,而不是在上次读到的指南碰巧查看的位置。

能到达网关不等于会转发

内核内部会做两个彼此独立的判断。送往本地地址会终止数据包的旅程;转发则是把它从另一个接口再发出去,而这第二个判断按网络命名空间分别管理。网关完全可以被访问到,同时转发是关闭的,而在 ping 提示符下这两种故障看起来一模一样。

记下客户端数据包从哪个接口进来、后续路径从哪个接口出去。两者属于同一个命名空间,真正重要的设置就在那个命名空间里。

到数据包实际所在的位置去读这个值

在主机命名空间中做路由的网关上,一条命令就能回答这个问题:sysctl net.ipv4.ip_forward。当入口路径和后续路径位于一个有名字的命名空间时,就问那个命名空间:ip netns exec vpn-gw sysctl net.ipv4.ip_forward。主机的值不能代替命名空间里的值,主机上处于启用状态也证明不了什么。

并不是每个路由上下文都有可以传给 ip netns 的标签。容器运行时和服务会创建从未以友好名称注册的命名空间,对这些要用运行时提供的检查工具。把 vpn-gw 换成真实的名字,或者换成能到达同一上下文的运行时机制,不要在主机层面猜。

让这个设置只有一个所有者

在常规的 systemd 主机网关上,持久化的形式是一个由管理员自选名称的 drop-in 文件,例如 /etc/sysctl.d/60-vpn-forwarding.conf,内容为 net.ipv4.ip_forward = 1。写好文件后,加载这个特定文件,让运行中的状态也随之改变:sudo sysctl -p /etc/sysctl.d/60-vpn-forwarding.conf,然后再读回来核对。文件名只是示例,不是保留的 VPN 路径;开头的数字很重要,因为排序靠后的文件会覆盖前面的。

持久性往往在优先级这里悄悄失效。既要检查发行版的加载器,也要检查你自己的 drop-in,因为 procps 的 sysctl --system 在最后还会读 /etc/sysctl.conf,而 systemd 的 sysctl.d 顺序完全是另一套序列。后面的赋值可以把同一个键立刻改回去,让你的文件本身正确、实际生效的值却错了。不要为了赢这场争论去覆盖已有的管理员文件;找到所有者,在那里改。

启动之后创建的命名空间完全不受主机 drop-in 管辖。它的设置属于创建它的东西——可能是容器运行时,也可能是该命名空间的置备单元。为了避免这个问题就给容器开放宽泛的主机权限,比花十分钟找出正确的所有者要糟得多。

在拨动这个开关之前,先弄清它的代价

ip_forward 不是一个无害的布尔值。内核文档指出,改动它会把另外几个 IPv4 参数重置为主机或路由器的默认值,所以一次诊断性的拨动可能把别处做过的调优丢掉,而且完全不提。请把它当成带副作用的配置变更,而不是可以随手跑的探针。

这也排除了每次客户端到不了主机就把它关掉再打开的做法。如果值本来就是启用状态,故障在路径的别处,把同一个键写第三遍也不会让它移动。

先证明持久性,再测试路径的其余部分

在四个时刻读回这个值:改动刚完成时、VPN 接口出现后、重新连接后,以及一次计划内重启之后。在其中任何一个时刻回退的值都有第二个所有者,再补一次一次性写入只会把它藏到下次重启。

转发已启用,仍有三件事需要核实。客户端要有一条通往目的地的路由,网关要有一条后续路由以及使用它的权限,目的地要有一条回到客户端隧道地址的回程路径。私有路由的 LAN 并不自动需要 NAT,所以在弄清三者缺哪一个之前不要加 masquerading;而且要从预期的客户端测试,而不是从网关自身测试,因为发端在网关上的请求永远不会走到你刚启用的那条转发路径。IPv6 有自己的转发和路由策略,所以 IPv4 测试通过并不能说明它的情况。

把 drop-in 路径、命名空间的所有者和读回命令一起写进 runbook。下一个接手的人应该能读出这个值、看出谁拥有它,并在一分钟内判断问题出在持久性还是路径的其余部分。

相关文章