VPN 上的 IPv6 link-local 不通:fe80 为何止步于链路
fe80:: 地址只在一个链路上有意义。了解路由式 VPN 为何无法承载它、故障表现为何,以及该改用哪个地址。
你把隧道建起来,以隧道为源接口去 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 run | What you see | What it means |
|---|---|---|
| ping fe80::1 with no zone | Invalid argument on Linux, or a transmit failure on Windows | The stack cannot pick an interface for an ambiguous destination |
| ping -6 -I tun0 fe80::1 | Destination unreachable, or a silent timeout | The address is not on the tunnel link, so there is nothing on-link to answer |
| ssh user@fe80::1 after the tunnel is up | Hangs, then no route to host | The SYN never left the machine in a deliverable form; the server logs nothing |
| ip -6 neigh show dev tun0 | Empty table | No neighbor entry can exist for a link-local host that is not on this link |
| fe80::/64 inside AllowedIPs or route-ipv6 | Config accepted, traffic still dropped | The 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::,就重新绑定服务、在该主机上做中继,或桥接该网段;如果可路由,就修正路由、防火墙或绑定地址。
- 每次重测都要重新核对区域,因为复制来的命令通常保留着错误的接口。