你把隧道建起来,以隧道为源接口去 ping fe80::1,却什么也没回来。这不是你忘了配置防火墙规则。链路本地地址只在它形成的那条链路上有意义,而路由式 VPN 并不是那条链路。远端路由器的 fe80::1 位于远端 LAN 网段上;你在另一网段上隔了一跳,而 IPv6 根本没有让路由器转发链路本地报文的机制。

fe80 前缀描述的是作用域,不是范围

链路本地单播是 fe80::/10,其定义性属性不是数字而是作用域。地址只在一条链路上有效,每个接口都用自己的标识符生成一个,所以两套互不相关的网络上的两台互不相关的机器可以同时应答 fe80::1,丝毫不冲突。这个前缀基本上只是在说“在某条链路上”,链路本身来自上下文。

在主机上,这个上下文就是你在百分号后追加的区域,即 scope ID:Linux 上是 fe80::1%eth0,Windows 以及会暴露接口索引的工具里是 fe80::1%3 或 fe80::1%12。内核会连同接口索引一起解析地址,然后再发送。去掉区域,目的地址就有歧义,协议栈无从选择网段。

路由式隧道为何送不到

路由式 VPN 是承载自身地址的点对点接口,通常是隧道内的 /64 唯一本地或私有 IPv6 空间。远端 LAN 是另一条链路,由隧道端点替你路由和转发才能到达。这是两个不同的网段,所以一个网段上的地址在另一个网段上不是有效目的地址。

转发规则在标准里写得很明确,并非你客户端的小毛病:路由器不转发源或目的为链路本地的报文。这就是路由表看起来正确、报文却仍然夭折的原因。为 fe80::/64 加一条指向隧道的路由,符合协议栈的实现要么拒绝,要么接受后丢弃所有匹配的报文:目的地址的作用域说“仅限本链路”,而出接口并不是那条链路。

邻居发现进一步强化了同一条边界。主机解析链路本地下一跳时,会把邻居请求发往链路范围的多播,跳数上限为 255,因此路由器可以拒绝任何被转发的报文。要让它在隧道上工作,就等于为远端网段代理发现,那是换了个帽子的Layer 2 桥接。

还有第二种更安静的故障,专治那些仍想强行加路由的人:区域。你在本地输入的任何区域都指向隧道接口,而你想要的那个地址绑定在远端 LAN 接口上。两者永远对不上,所以报文在远端无法被引导到正确的接口。

故障在实际中是什么样

作用域出错时,工具输出读起来像路由或防火墙错误,这正是人们把一下午浪费在错误层次上的原因。

What you runWhat you seeWhat it means
ping fe80::1 with no zoneInvalid argument on Linux, or a transmit failure on WindowsThe stack cannot pick an interface for an ambiguous destination
ping -6 -I tun0 fe80::1Destination unreachable, or a silent timeoutThe address is not on the tunnel link, so there is nothing on-link to answer
ssh user@fe80::1 after the tunnel is upHangs, then no route to hostThe SYN never left the machine in a deliverable form; the server logs nothing
ip -6 neigh show dev tun0Empty tableNo neighbor entry can exist for a link-local host that is not on this link
fe80::/64 inside AllowedIPs or route-ipv6Config accepted, traffic still droppedThe tunnel carried the packet; the receiving stack refused to forward it

第三行最费时间,因为没有任何输出的 ssh 客户端看起来像监听服务的问题。如果你有办法进去,就去远端确认 sshd 绑定的是所有地址还是只有一个。

改用本来就用于路由的地址

凡是需要经隧道到达的服务,都使用当初为这个用途分配给它的地址:全局单播地址,或者 fc00::/7 中的唯一本地地址,这个范围正是为这种内部场景保留的。确认监听器确实绑定了它,并检查回程路径和主机上的任何防火墙规则。

与其猜测,不如查看远端主机。ip -6 addr show 会列出存在的地址,ss -6 -ltn 会列出守护进程正在监听什么。只绑定 fe80:: 的服务只能从它自己的网段访问,这通常是没人有意选择的默认值,经管理员同意可以修改。

当服务确实无法重新绑定时,把桥接上移到应用层而不是网络层。在远端主机上运行一个监听可路由地址、并连接链路本地地址的中继,就能在不扩展任何网段的情况下解决问题:

socat TCP6-LISTEN:8080,bind=[fd00:1:2::10] TCP6:[fe80::1%eth0]:80

这里的区域在正确的一侧,位于真正在那条链路上拥有 eth0 的主机。你的客户端与 fd00:1:2::10 通信,这个地址能正常路由,中继则在本地完成链路本地这一跳。同样的办法也适用于设备的 Web UI,或只通告链路本地地址的老旧 BMC。

当桥接确实是答案时,该检查什么

有些部署确实需要共享一个网段:只讲链路本地发现的设备、依赖多播的专用设备、复现扁平 LAN 的实验环境。这是Layer 2 问题,需要Layer 2 答案,例如桥接到远端接口的 tap 设备,或带显式桥接模式的客户端产品。代价是穿越隧道的广播和多播流量,以及必须端到端可用的邻居发现。

不要靠给路由式配置加 fe80::/64 来凑合。那会产生最糟的状态:配置看起来完成了,路由表里也有条目,而每个报文仍然在两个都行为正确的协议栈之间某处被丢弃。

用接口而不是 ping 来验证。ip -6 addr show tun0 告诉你隧道链路是否形成了自己的链路本地地址,ip -6 route get fe80::1%tun0 告诉你内核对带作用域的目的地址实际会怎么做。当内核说该接口上目的不可达时,任何隧道设置都改变不了,该停止改配置了。

一套能定论的最短流程:

  • 读取远端主机上的地址,记下它是 fe80:: 还是可路由前缀。
  • 如果是 fe80::,就重新绑定服务、在该主机上做中继,或桥接该网段;如果可路由,就修正路由、防火墙或绑定地址。
  • 每次重测都要重新核对区域,因为复制来的命令通常保留着错误的接口。

来源:RFC 4291,IPv6 地址架构。

相关文章