终止开关只封锁 IPv4:为什么 IPv6 仍然溜出去
一套针对 IPv4 的防火墙规则并不涉及 IPv6。在双栈设备上,隧道可能断开,而应用仍然保留一条绕开它的可用路径。
隧道断开,终止开关报告已生效,而下载仍在继续。IPv4 规则没有任何问题。应用只是切换到了一条这些规则从未为之编写的路径。
双栈设备有两条独立的出口。只封锁其中一条,等于只封锁了一半。
两套规则,两套默认值
在 Linux 上,iptables 和 ip6tables 是两张独立的表,各有各的策略。把 IPv4 表的 INPUT 和 OUTPUT 链设为 DROP,不会改变 IPv6 表——那里的默认值通常是 ACCEPT。nftables 中也有同样的分裂:你可以检查一个族,发现只有其中一个承载了你的规则。这种不对称很正常,而整个问题就在这里。
这就是为什么终止开关可以看起来彻底,却只部分起作用。写 IPv4 规则的那个脚本运行了,计数器在增长,日志行出现了,而一个 IPv6 数据包仍然完全不受影响地从物理接口离开。
读一读你的客户端实际安装了什么,而不是营销页面承诺了什么。有些客户端把 IPv6 送进隧道,有些刻意封锁它,还有些任其自流,同时平台级锁定执行另一套规则。这里唯一值得信任的来源是厂商文档。
接口配置告诉你什么
wg-quick 的手册页在示例配置中包含了 IPv6 规则,原因正是这个缺口。那个示例不是装饰。它之所以存在,是因为只有 IPv4 规则会让第二个族保持敞开。
复制半个示例,就得到半套策略。这是这种泄漏最常见的来源。一篇博客文章为了篇幅删减了示例。删减后的版本随后成了所有人复用的模板。
# what is actually installed, per family?
sudo iptables -S OUTPUT | head -20
sudo ip6tables -S OUTPUT | head -20
# nftables: inspect each family separately
sudo nft list ruleset inet | head -40
# does this host even have a usable IPv6 path?
ip -6 route show default
如果第二条命令输出的是允许策略,而第一条输出 DROP,那么你还没碰客户端就找到了答案。这种不对称就是 bug。
为什么一次查询不是全部
一个名字可能同时返回 A 记录和 AAAA 记录,而应用并不都按同样的方式选择。浏览器可能优先使用 IPv6,失败时回退;而后台同步或更新程序则使用自己的解析器和自己的偏好。因此,一次通过 IPv4 的成功测试只能证明一条路径,不能证明策略。
网络切换也会改变答案。加入另一个 Wi-Fi 网络、从睡眠中恢复或切换到蜂窝网络,都可能给设备一个新的 IPv6 前缀,而那条在办公室 LAN 里显得够用的策略也许覆盖不到它。
测试隧道断开时的状态
在连接状态下测试只能说明隧道在工作。有意思的测试是不可用状态,因为那正是终止开关应当撑住的时候。故意停掉隧道,然后从你保护的应用去访问真正重要的目的地。
# with the tunnel stopped, does anything still leave?
ip -6 route get 2606:4700:4700::1111
curl -6 -sS -o /dev/null -w '%{http_code}\n' --max-time 5 https://ipv6.example.net/
curl -4 -sS -o /dev/null -w '%{http_code}\n' --max-time 5 https://ipv4.example.net/
两个族都应当以关闭方式失败。如果 IPv4 请求超时而 IPv6 请求返回状态码,策略就是不完整的,再怎么重测隧道也改变不了这一点。
也把有记录的例外保留在那个测试里。本地网络发现、DHCP 续租或通往 VPN 端点的引导路径可能需要保持可达,这些例外应当写在策略里,而不是在故障期间才发现。一个因为隧道无法恢复而失败的泄漏测试不是安全发现,而是连通性发现。