重启 VPN 客户端只能证明隧道恢复了。它并不能说明那个被要求经由它出站的代理。Squid、tinyproxy 以及大多数其他转发器都会保留启动时解析出的本地源地址,直到有什么东西迫使它们重新加载。当重连分配了新的隧道地址时,转发器要么每次对外连接都失败,要么开始走你平常的路由出去。

第二种故障才危险:隧道是通的,页面能打开,只有出口地址是错的。

先看监听端,而不是隧道

这里涉及三个地址,而重连只挪动其中一部分。监听端是浏览器指向的地方,通常是 127.0.0.1 或某个局域网地址,它并不关心隧道变没变。出站本地源是转发器在拨下一跳时要求内核使用的地址,它绑定在隧道设备上。公网出口是 Web 服务器记录下来的地址,它可能是几跳之外经过转换的地址。

查公网 IP 只能回答最后一个问题。它无法告诉你转发器该绑定哪个本地地址,因为一个公网出口可能掩盖多个内部来源。要从拥有连接的一方读取地址:客户端的状态输出、接口本身,或者隧道所在的名字空间。抓第一个名字看起来像隧道的接口,一旦存在第二个配置文件就会给出错误答案。

容器与名字空间的布局会悄悄打破这个假设。一个网络名字空间里的转发器无法绑定存在于另一个名字空间里的隧道,改多少配置都修不好。

动手改之前先做四步检查

按顺序做。第一条失败的检查就是你要停下的地方,因为后面的检查都以它成立为前提。

  1. 确认正在使用的隧道是代理当初围绕的那个配置文件,而不是自己连上来的第二个配置文件。
  2. 打印该设备当前的地址,并与运行中的配置里的地址比较。
  3. 问转发器它解析到了哪个源,而不是磁盘上的文件声称什么。
  4. 确认来自转发器的流量确实受你所期望的路由与防火墙策略约束。

设备地址很好查:ip -4 addr show dev tun0 会打印当前地址,ip -4 route get 1.1.1.1 会显示内核对那个目的地会选择哪个接口。绑定精确地址的转发器在地址消失时会大声失败。绑定设备、或回退到默认源的转发器会继续工作并泄露流量,这就是安静的结局。

文档要对照你实际运行的版本,而不是搜索结果的第一条。Squid 的出站地址指令是为 v7 及更早版本写的文档,而 v8 文档明确说明该指令在那里不可用,所以围绕它构建的脚本过不了这道坎(参见 Squid tcp_outgoing_address 参考)。在重新加载任何东西之前,先用产品自带的解析器校验:squid -k parse 会报告错误,并在不触碰运行中进程的情况下退出。

给每条隧道编号,并丢弃过期的切换

重连是一次交接,而交接需要顺序。用每次成功建立都会递增的计数器或时间戳标记每一条观察到的连接,再把该标记带进候选配置里。当候选配置上的标记比当前标记更旧时,丢弃这个候选而不是应用它,否则为前一条隧道写的慢更新会覆盖当前隧道的设置。把事件突发合并起来,让一条抖动链路只产生一次切换,而不是十五次。

把工作排成四步:先作废旧出口状态,等连接所有者报告完成,只构建受管片段,然后通过服务管理器校验并激活。从两个地址值重新生成整个文件,正是监听限制、认证和目的地策略悄悄消失的原因。也要尊重有意的停止。用户已经关掉的隧道,助手还在不停拨号,那是缺陷而不是韧性。

已打开的套接字不会跟随新地址

重新配置转发器对已经建立的连接没有任何影响。它们是排空还是断开取决于产品本身,所以要观察行为,不要假设。

在切换之前打开的套接字上服务的请求,即使新配置正确,也仍会走旧路径出去。如果这很重要,就明确决定:允许一个短的排空窗口并停止接收新工作,或者终止旧套接字并接受错误。然后用新连接测试,并把该结果与排空检查分开保存。源地址绑定也不是安全边界。当流量绝不能碰普通路由时,用标记和防火墙规则强制执行,包括 DNS 和 IPv6,好让失败的重载让代理明确停掉而不是敞开。

用新请求证明新出口

重要的是激活之后打开的请求,不是 ping,也不是已经被缓存的页面。让代理取一个真实 URL,读取远端报告的出口地址,再在服务商提供相关信息时与客户端自己对隧道的看法比较。

把解析、重载和请求保持为三条独立命令,这样失败能指向单一步骤。前两条失败时不会改变任何东西,这正是它们要在重启之前运行的原因。

squid -k parse
sudo squid -k reconfigure
curl -x http://127.0.0.1:3128 -sS -o /dev/null -w '%{http_code} %{remote_ip}\n' https://example.com

如果重载失败,就停掉服务并修好片段;带着过期出口继续服务的代理比短暂中断更糟。如果重载成功但新请求仍从意料之外的地方出去,就回到路由检查,而不是再次改地址。记下你校验时所对应的隧道代数,好让下一次重连有具体的东西可比。

相关文章