关闭脚本还是防火墙规则:谁能真正堵住泄漏?
断线时把网卡全部关掉很粗暴,而且常常没有权限执行。把它与一条在断线后仍然生效、能补上缺口的防火墙规则对比一下。
两种设计承诺的是同一件事:隧道断开时,不会有任何流量明文外泄。一种在拆除时执行脚本,把网卡全部切掉。另一种保留一条防火墙规则,完全不向 VPN 客户端提出任何要求。它们的差别在于谁有权执行动作、缺口期间会发生什么,以及之后机器如何重新上线。在这三点上比较,比争论谁更严格更有用。
同一件事,两种机制
只要配置文件允许执行脚本,客户端就能在隧道建立和拆除时运行钩子。这个钩子跑在客户端的生命周期内,也就是连接正在关闭的那一刻。防火墙规则则属于系统,不关心客户端是在运行、崩溃还是被杀掉。
| Property | Teardown script | Firewall unit |
|---|---|---|
| Runs when the client is killed | No | Yes |
| Needs privilege at teardown time | Usually yes | Configured once, at boot |
| Acts on the whole machine | Often, including recovery paths | Only on the flows it matches |
那张表就是全部论点。只在正常退出时运行的脚本,覆盖的恰恰是你最不担心的情况;真正要紧的是崩溃或网络突然消失,而这两种都不会产生干净的拆除。
拆除钩子能做和不能做的事
文档里的做法是遍历网络接口,把除 loopback 之外的一切都关掉。物理设备一关,就没有东西能跑到隧道外面,这正是本意。
#!/bin/sh
for dev in /sys/class/net/*; do
name=$(basename "$dev")
[ "$name" = lo ] || ip link set dev "$name" down
done
它同时会关掉本地网络、机器上托管的任何服务,以及你用来修问题的那条路径。在笔记本上,无线设备事后有时还得手动切回来,于是一次短暂的故障变成一场维修。更糟的是,钩子可能根本没有权限做这些:客户端一旦放弃权限,拆除脚本就以非特权账户运行,每一次接口变更都因权限错误而失败,而这条错误恰好写在一个没人看日志的时刻。
在信任钩子之前,先确认客户端是如何启动的。以 root 手动执行时能用的脚本,并不能证明开机时启动的那个服务也能用。
防火墙规则的不同之处
只放行隧道流量的规则,在客户端进程消失后仍然有效,因为内核并不依赖那个进程。规则也更精准:它只拦它匹配的那些流,对 loopback、本地子网和恢复路径不闻不问,所以不会把机器困死。
代价是这条规则必须由拥有主机防火墙的人来编写和维护,而且它必须 指名客户端真正创建的隧道设备。这意味着规则和配置文件要一起维护;接口一改名,留下的防火墙就会什么都拦、什么都不解释。
两种机制谁也不替代谁。一条改完却从不重新加载的防火墙规则,和一个从不运行的脚本一样是空话。
有没有缺口,由 persist-tun 决定
隧道设备在断开后仍然存在时,指向它的路由也仍然存在。此时流量是被丢弃而不是被改道,所以默认路由不会回落到物理网关。正是这个行为,让泄漏在任何防火墙规则介入之前就很难发生。
没有这个设置,设备会被移除,路由随之消失,默认路由就可能重新指向物理网关。会不会这样取决于平台和客户端,这也正是必须观察而不是假设状态的原因。把这项设置当作 kill switch 的一部分,而不是性能选项。
保住重连通道,然后测试四种状态
严格封锁也可能让客户端无法重连,因为在其他一切都被拒绝时,客户端仍然需要一条经物理网关到服务器的路由。要在隧道建立之前为那一个地址装好主机路由,并先解析名字,因为等你需要它的时候,DNS 可能已经被封了。
ip route add 198.51.100.20/32 via 192.168.0.1 dev wlan0
然后验证状态,而不是验证标签。有意识地走一遍四个时刻:没有隧道的冷启动、健康会话、没人要求的断线,以及恢复之后的会话。每个时刻都读取默认路由,并发出一个能看到出口地址的请求。一个在隧道断开时放弃的应用程序,说明不了邮件客户端或更新程序的情况,所以再用一个后台程序重复最后一项检查。把规则、接口名和恢复步骤写下来,因为没人能验证的 kill switch 只是传闻。