三段 WireGuard 跳点连接到没有公网地址的网络,随后中间那台机器不再回应它本应监听的端口。再往前的隧道仍然工作,远端主机仍有响应,只有中间服务器自己从外部变得悄无声息。这个组合之所以令人困惑,正是因为它由自己造成:中间跳遵守自己的路由表,而路由表把它的应答送向了错误的方向。

在你还不能与中间盒子对话之前一直有效的链条

取一条示例链:客户端 A 通过 UDP 500 到达服务器 B,B 再通过 UDP 500 继续到服务器 C。C 只接受来自其唯一对端地址的流量,这使它成为叶子节点,行为也保持简单。B 不是叶子,而这个故障里所有有意思的地方都发生在那里。

这个区别很重要,因为叶子和中间角色的拉扯方向相反。叶子可以把所有流量都送进一条隧道而不承担后果,因为没有别的东西依赖它。中间跳既是对客户端的终点,又是朝远端方向的客户端,而这两项工作想要不同的路由。

为什么应答会从隧道出去

在 B 上,朝向 C 的接口带着 AllowedIPs = 0.0.0.0/0 以及匹配的默认路由。因此 B 产生的每个数据包都会进入那条隧道,除非有更具体的东西取得优先。

来自 A、发往 B 的数据包到达并被接受,因为 B 就是目的地,本地投递先发生。应答是一个全新的本地生成数据包,目标指向 A 的公网地址,而该目的地与进入隧道的默认路由相符。C 收到它,并不拥有那个地址,于是将它丢弃。

症状读起来像防火墙问题或密钥不匹配,而这里两者都不是。在通往 C 的隧道上抓包会显示应答走错了方向。仅仅这一项观察,就能比读任何数量的配置更省时间,所以在开始更换密钥之前先把它记下来。没见过这次抓包的人会以为隧道本身坏了。

窄修法:匹配源端口的策略规则

来自 B 监听套接字的应答以源端口 500 发出。策略规则可以匹配这一点,把这些数据包送回普通路由表:

ip rule add sport 500 table main priority 100
ip rule add sport 8080 table main priority 101

策略规则按优先级顺序被查询,而 main 表位于 32766,所以 100 处的规则会在隧道默认路由发话之前被问到。对外必须保持可达的端口,每个只加一条,别的都不要加,这样被转发的流量就继续使用它为之前构建的那条隧道。

要了解这个技巧的边界。sport 选择器匹配的是本地生成的数据包,这恰好覆盖了应答的情形,并且刻意不覆盖 B 只是替别人转发的流量。位于 B 之后的托管服务仍然需要自己的应答,假装不是这样,只会得到一个对第一个服务有效、对第二个服务失效的修法。

给规则找一个归属

在提示符里敲下的规则,会在下次重建接口时消失。在 WireGuard 配置里,把它放到接口被创建和删除的地方:

PostUp = ip rule add sport 500 table main priority 100
PostDown = ip rule del sport 500 table main priority 100

如果接口归 NetworkManager、发行版钩子或容器入口点所有,这对规则就该放在那个归属处,而不是放进手工编辑的文件。要在重新连接之后检查生效的规则,而不仅仅是在第一次启动之后,因为第一次启动总是能成功的那种情况。

诚实地比较各种替代方案

源端口规则是四种出路之一,也是其中范围最窄的一种。它的优点是完全不碰隧道的策略,因此往往能在后来的改动中存活下来。窄修法在失效时也会大声失败,而宽泛的例外会在理由消失很久之后仍悄悄继续工作。两年后当盒子归别人所有时,这个差别比优雅更重要。

ApproachWhat it costsWhere it breaks
Source-port policy ruleOne rule per exposed portAnything B forwards rather than originates
Destination exception routeA route pinned to specific client addressesClients whose address changes
Stop full-tunnelling on BLoses the catch-all path toward CDesigns that depend on it
Split into two tunnelsMore interfaces and more policyNothing, at the cost of complexity

选定之后要从两端验证。从 A 发起一条到 B 端口的连接,并让它忙上一阵。在 B 上,让内核用类似 ip route get <address-of-A> sport 500 的东西来说明应答路径,确认它给出的是 WAN 接口而不是隧道。之后,检查 C 后面的东西是否仍能通过这条链应答,这证明例外保持了窄的范围,而没有变成绕行。最后,把端口记进设计笔记,否则下一位运维会在另一个服务上重新发现这件事。

相关文章