无界面的机器上没有浏览器可打开,也没有锁形图标可看,所以核验必须是一个能读懂应答的请求。这听起来不值一提,直到你注意到有人用单个出口地址结果去为一台运行十几项服务、两个地址族和一个代理的设备作认证。下面这套流程每次只建立一小块有边界的证据,而且就在工作负载真正运行的上下文里进行。

先决定测试要证明什么

在运行任何东西之前,把预期写成一行:哪条流、经哪个接口、到哪个目的地、在另一端应当看到什么。没有明说预期的测试,只会给出一个谁也无法解释的数字。

最有用的写法是点名一个经批准的端点,由它报告所看到的源地址。

这个端点是你唯一需要安排的部分。其余都是客户端侧的观察,而客户端侧的观察会以有趣的方式出错。

在正确的上下文里执行观察

管理员的 shell 不是工作负载的环境。它可能带着另一个代理变量、另一个命名空间里的另一张路由表,或者另一个用户的环境文件。要在与被测对象相同的主机、容器、命名空间或服务上下文里运行测试。否则你测的只是自己的 shell。

在普通主机上,这意味着同一个用户和同一个单元。在容器环境里,这意味着进入容器的网络命名空间,而不是从主机上测试。在 systemd 服务上,这可能意味着使用配置相同的临时单元。

只有身处正确的上下文,接下来的请求才有意义。

分别检查两个地址族

要显式地请求每一个地址族,因为一次成功的 IPv4 请求根本不会走 IPv6 路径。--ipv4 和 --ipv6 选择器会强制做出选择,而两个结果的差异本身就是信息。

curl --ipv4 https://probe.example/observed-source
curl --ipv6 https://probe.example/observed-source

IPv6 请求失败并不能证明阻断有效。它可能意味着端点没有 IPv6 地址、DNS 没有返回任何可用的结果,或者设备根本没有 IPv6 路由。下结论之前先读懂失败。只有经隧道成功的 IPv6 请求,才能证明 IPv6 路径已被覆盖。

把两个答案并排记下来。只看其中一个,就只有一半事实。

补上出口检查漏掉的四件事

出口地址只回答一个问题。另外四件事才决定隧道是否按设计工作。

CheckWhat it provesWhat it does not
Included and excluded destinationsThe split behaves as designed for those flowsCoverage of any other destination
Resolver pathWhere a fresh lookup actually travelledWhere a cached answer came from
Tunnel-down behaviourThe failure policy for a new requestWhat an already open connection does
Reconnect and restartThe requirement survives the lifecycle eventBehaviour under load or partial failure

目的地策略排在第一位:如果设计里有分流,就分别测一条应当被包含的流和一条应当被排除的流。然后是 DNS,这里要看的是真实的解析路径,而不是配置值,因为缓存里的答案或打印出来的设置,说明不了新的查询去了哪里。

故障行为需要预案,也需要保留一条恢复路径。在隧道不可用时观察一个新请求,并记住:一条早已打开的连接不能作为故障策略的证据。生命周期放在最后:如果重连或重启服务会影响这项要求,就在这些事件之后重复观察。

记录证据,而不是结论

对每个结果,保留进程上下文、目的地、地址族、时间以及观察到的源。这组信息是关于你所跑过的流的有边界证据,它之所以站得住脚,正是因为它说明了覆盖了什么。

它不是对 VPN 运营商记录的审计,也不是对设备上未来每个应用的证明。

把边界写在结果旁边。说明自己没覆盖什么的测试能通过评审,而声称设备已验证的测试不能。把 curl 手册放在所用地址族选择器旁边,并把那张四行表与结果放在一起。

相关文章