把 0.0.0.0/0, ::/0 写进对端的 AllowedIPs,是每个人都记得的一步,而它只决定对端被允许承载哪些流量。这些前缀不创建任何路由,不在远端转发任何东西,也不会碰任何应用的解析器。要让全隧道真正完整,四个方面必须同时成立,最好按下面的顺序检查,因为后一项检查救不了前一项。下面每一项检查都是能从机器上读到的事实,而不是你希望已经生效的设置。

对端前缀选了什么,又没选什么

AllowedIPs 既是选择器,也是授权。它告诉本机侧哪些目的地可以发给这个对端,也告诉接收侧这个对端可以出示哪些源地址。它的全部作用就在这里。这段描述里没有任何内容会在主机上建立路由、在远端开启转发,或者触及域名解析。

这就是为什么 一份配置看起来完整,行为却像拆分隧道。前缀只是说对端愿意接受一切;真正决定什么能到达那里的是主机的路由表,决定是否有流量出去的则是网关。这三个决定没有哪一个住在对端配置段里。

为客户端加路由,而不只是列对端

像 wg-quick 这样的管理器会替你把主机的路由表写好,所以两份相同的配置文件,会因接口上线的方式不同而表现不同。最小配置短到可以一口气读完:

[Interface]
Address = 10.8.0.2/32
DNS = 10.8.0.1

[Peer]
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = vpn.example.net:51820

真正干活的是两个细节。其中Address 字段是 单个主机前缀,而不是客户端自以为拥有的子网,而把默认 AllowedIPs 变成真正默认路由的正是管理器。用 ip link 和 wg setconf 拉起同一个文件,则完全不会出现路由,看起来像隧道坏了,其实是一条健康隧道只是跳过了路由这一步。wg-quick 的手册记录了这些管理器所添加的路由与 Table 行为。

读结果,而不是读文件:ip route get 1.1.1.1 应当指出隧道接口,wg show 应当报告一次最近的握手,且传输计数在增长。没有路由的握手意味着管理器从未运行过,再改一遍配置也修不好。要按用户实际会用的方式启动接口,否则测的是你的 shell,而不是部署。

把端点留在隧道之外

全隧道会抓走每一个目的地,包括它为了存在而必须通信的那个。管理器通过为端点添加一条走原网关的主机路由来处理这件事,所以正确的配置几乎总含一个刻意的例外。一半交给管理器、一半手工应用,是这个例外最常丢失的方式。

如果端点是一个解析到多个地址的主机名,或解析到你不承载的地址族,这个例外就会老化得很糟,隧道可能在一次看似平常的重连后失败。把端点固定在你控制的地址上,或确认管理器会重新求值,并在每次网络变化后重读主机路由。

同一套机制也解释了全隧道为什么会和强制门户打架。门户期望拦截你的隧道如今抓走的流量,所以在酒店和机场网络里,要在门户登录之后再拉起隧道,绝不要提前。

网关上的转发与回程

在网关上,为你承载的地址族开启转发、做地址转换时在两个方向上放行 forward 链,并用按源地址的查询确认回程。这些都不是成功握手能推出来的,因为握手只证明两个对端能交换 UDP。它对解封装之后的包发生了什么一无所知。

sysctl net.ipv4.ip_forward net.ipv6.conf.all.forwarding
ip route get 1.1.1.1 from 10.8.0.2

查询要在网关上跑,而不是在客户端,否则读到的是另一张路由表。如果它指向错误的接口,或报告没有路由,那么客户端侧的改动都不会产生互联网出口。当网关在出方向转换客户端地址时,回程取决于那份转换状态,所以规则必须同时覆盖两个方向,而不只是出方向。

DNS 与失败状态,要分开测

协议本身不配置任何应用的解析器。wg-quick 通过发行版的解析器集成来应用 DNS 这一行,而自带协议栈的容器和浏览器可能根本不理会它,因此名字可能在隧道完美承载数据包时照样失败。要用主机名而不是地址来测,并且记下到底哪个服务器应答,而不是想当然地认为配置文件里那个应答了。

地址族是另一项测试,也要另给答案。一条 v4 默认路由并不说明某个应用的 v6 连接会怎样,而静默泄漏第二族地址的隧道,比大声失败的隧道更糟。分别用每个地址族查询一个名字,读出每次请求走了哪个接口,并在前后检查转换计数,因为什么都没匹配上的规则,看起来和缺失的规则一模一样。

最后,要测隧道停止时你想要的状态。全隧道应当对它声称承载的流量失败关闭:如果接口掉线而应用悄悄改走物理链路继续跑,这个设计就是多绕了几步的拆分隧道。把预期中的失败行为写下来,刻意检查它,最后把四个答案记录下来——由哪个管理器拉起接口、哪条路由承载默认、谁在网关上转发并转换、哪个解析器在应答。

相关文章