IPv6 隧道对端 ping 不通?检查 :: 地址
全零接口标识符是 subnet-router anycast,而不是主机。分配 ::1 与 ::2,路由该前缀,并在责怪路由之前检查 multicast。
你的一条 IPv6 隧道有一端能 ping 通另一端,却永远只有一个方向有回应。接口是 up 的,路由看起来也对,日志里也没有任何解释。你分配的地址是 subnet-router anycast 地址,根本不是主机地址;在两端点链路上,它只有一个投递目标:发送者自己。再大的超时值也修不好,因为这个地址做的并不是你以为的事。
指向该地址的症状
链路上每台主机都有回应,唯独接口标识符全为零的那一台没有。按定义,发往这种形式的包会交给子网上的路由器,所以在点对点隧道上它又回到发送它的那台机器。从远端看是不可达的对端,从近端看就是沉默。换用同一前缀下的另一个标识符,症状随即消失,其他什么都不用改——这才是动路由之前应当取得的确认。
全零标识符被保留为 anycast
RFC 4291 把全零接口标识符保留为 subnet-router anycast 地址,所以像 2001:db8:4a::/64 这样的 /64 中,2001:db8:4a:: 处没有可用主机。这项保留自有其道理:router advertisement 和 neighbour discovery 会发往该形式,路由器为它服务的那个前缀作答。把该值配给隧道端点,等于把路由器角色交给叶子,这正是没有任何回应回来的原因。前缀中任何其他标识符都能当主机用,所以修复通常只差一个数字。
要把这项保留当成一个你无权从隧道端点占据的角色,而不是绕路由就能避开的怪癖。
分配主机地址并路由该前缀
为两端选 ::1 和 ::2,用 ip -6 addr add 2001:db8:4a::2/64 dev tun0 添加其中一个,然后路由远端前缀,而不是试图给它配地址。为 2001:db8:4a::/64 建一条出隧道的路由,远端才可达;只配地址只是把本地接口配置好。
对全隧道而言还有第三步:为全局 unicast 经该设备配置默认路由,这是把所有流量送进隧道的 IPv6 等价物。跳过它,IPv6 会继续走物理链路,而 IPv4 在隧道里传输,于是主机恰好泄漏了你以为已被覆盖的流量。用 ip -6 route get 2001:4860:4860::8888 确认,并读它点名的设备,而不是想当然。
multicast 在路由之前就被拒绝
Android 通过 LinkAddress 校验 VpnService.Builder 的地址和路由,而该构造函数直接拒绝 multicast。把 multicast 前缀当路由交给它,得到的是 bad LinkAddress parameters 失败而不是一条路由,这是校验层面的回答,不是网络层面的。拒绝与介质相符:点对点 tun 设备没有链路层 multicast,因此其上的组路由没有可安装的投递模型。组地址由协议栈自己创建和消费,不靠手工配置。
IPv6 all-nodes 与 solicited-node 地址同受此规则约束。接口之所以带上它们,是协议栈放进去的,不是哪个策略有意为之。
隧道还能承载什么
要把 multicast 发现搬过隧道,需要一层懂组成员关系的东西:呈现一段以太网的 TAP 网桥、应用层中继,或明确被告知向哪些对端复制的路由协议。VpnService 模型里这些都不存在——应用只读写数据包,没有为隧道加入某个组的调用。要么补上懂组的组件,要么把这项需求从设计里删掉。
相比之下,unicast 在上述地址与路由之外不需要任何额外东西,所以隧道通常被认为就该承载它,然后在某个发现协议失败时被责怪。
动手分配前三项检查
确认地址不是 anycast 形式,确认它不是 multicast,确认该前缀是被路由而不只是配到设备上。然后把测试发往一个真正的主机,因为 ping anycast 地址测的是发送者,对路径什么也说明不了。把这三项检查写进变更记录,好让下一次部署不必重新发现。
平台从未装上的路由,等再久也调不出来;从来不是主机的地址,ping 得再好也修不好。