如何锁死 Linux 出口,让 VPN 断开时什么都不漏
路由和过滤是两个决定。先锁住 output 钩子,再收紧隧道重连所需的端点、DNS 与管理例外。
设想一台接在家庭宽带上的媒体盒子,它只应该通过隧道访问互联网。隧道断开时,应用应当卡住,而不是悄悄改走物理网卡。要做到这一点,靠的是防火墙而不是路由,因为路由会被守护进程重启改写,而过滤器留在你放的位置。下面先说数据包路径究竟如何判断,再说如何收紧那些让隧道得以重连的例外。
本机出站与转发是两个不同的钩子
应用从本机发出的数据包走 output 钩子;本机为其他主机转发的数据包走 forward。只提到 forward 的策略只约束后者,把前者完全敞开,这正是许多出口锁看起来正确却毫无作用的原因。接口匹配要写进同一条规则:oifname "tun0" accept 放行已经从隧道出去过的流量。路由和过滤也依然是两个决定,因此上行路由存在时,过滤器仍可拒绝数据包。你依赖的正是这种分离。
为其他设备转发的网关两个钩子都需要。只锁 output 保护了本机,却让下游每个客户端都能自由泄漏。
从端点开始向外写例外清单
先写隧道端点:一个目的地址或一小组已批准的地址、传输协议和目的端口,例如 udp dport 1194 ip daddr 203.0.113.7 accept。如果客户端要解析主机名,引导阶段的 DNS 交换需要单独规则,只放行到解析器和 53 端口,而不是对该解析器地址的全面许可。每条例外都应写明它存在的理由。放行到 VPN 端口的所有 UDP 流量,覆盖面远大于你心里那一个已认证服务。
引导解析正是锁最容易腐烂的地方。按定时器刷新的地址集合,必须在刷新失败时有明确行为,而这个行为不能是一直打开上行直到成功。隧道在时通过隧道刷新,否则回退到最后一次已知可用的端点。
本机管理也需要同样处理。管理网段不会因为地址是私有地址就值得信任,信任它的规则等于给锁开了第二条路。
接口匹配并不证明隧道健康
凡是接受从 tun0 出去的一切的规则,都假定该接口上的数据包来自隧道。接口名会被重建,而名字不是密码学身份。对端也可能已经消失而接口仍然存在,这让规则变成非常可信的空操作。
连接跟踪会带来相关陷阱。宽泛的已建立连接规则会重放某个流早先取得的许可,于是隧道断开之前开始的会话在断开之后继续可用。把有状态放行限制在你真正指明的接口和地址上,否则你造出的锁会自己打开。输入与输出规则回答的也是不同问题,只加固一边而忽略另一边,机器仍会在你没考虑的方向上可达。
什么能在守护进程崩溃后幸存
隧道起来之后才运行的脚本所装的过滤器,会随隧道一起消失,而那正是最需要它的时刻。锁要在启动时加载,让守护进程只管理它自己的例外。
删掉默认路由也不是替代方案。路由是守护进程拥有并会重写的状态,删掉它可能连重连所需的路径一起拿走。IPv6 需要单独计划,因为邻居发现和路由器通告是链路服务,在普通流量被阻断时必须继续工作。DHCP 属于同一类。把这些写成有目的的例外,而不是事后当作故障去发现。
上线前的恢复通路
按它将要失败的方式来测试:无隧道启动、杀掉守护进程、更换端点、重新连接。每种情况都应该让应用卡住,同时恢复通道仍然可用。请从控制台做,因为远程 shell 才是最可能被你自己的规则锁在外面的会话。
然后检查普通应用还能到达哪里,用一个无害的目的地和一条可以验证的请求。你要的结果是上行被拒绝、经隧道成功,而不是一个两种含义都可能的模糊超时。规则集要和防火墙其余部分交给同一个所有者,而不是用 shell 脚本整表替换。一套每条放行都能自证其理由的策略,比一份谁也无法解释的长规则集更有价值。