摘要

  • 有限作用域 IPv6 地址可以在不同 zone 中重复,因此 128 位地址本身不总能说明本机应使用哪条链路。zone index 是节点本地的选择信息,不属于地址位,也不得作为数据包的一部分发送。
  • 2013 年的 RFC 6874 曾尝试把 ZoneID 放进 URI;2025 年的 RFC 9844 认为浏览器实现路线不可行并将其废止。现行办法要求用户界面接收、校验并本地映射 zone identifier,而不是把接口名变成可携带的地址身份。

字符串完整,不等于语境完整

一台同时接入两条链路的主机,可能在两边都能看见 fe80::1。这并非地址冲突的自动证据。链路本地地址的意义本来就被限制在一条链路内;另一条链路可以独立重复同一个值。2006 年的 RFC 4291 规定,路由器不得把源或目的为链路本地地址的包转发到其他链路。

问题出现在主机拥有多条这样的链路时。应用只拿到 128 位地址,无法知道用户指的是哪一个 fe80::1。RFC 4007 在 2005 年把两个容易混淆的概念分开:scope 是拓扑区域的大小,zone 则是该大小下某个区域的具体实例。两条物理链路可以是两个 link-local zone,却共享“链路本地”这个 scope。

zone index 是节点给这些实例分配的本地编号。它在节点内部必须足以消除歧义,但不需要与邻居一致。即使两台机器接在同一条线缆上,它们给这条链路的编号也可以不同。因而,复制地址字符串时保留下来的 %eth0 不是远端可查的公共坐标,而是发送端操作系统的一条索引提示。

这条边界解释了一个看似反直觉的事实:最方便人阅读的完整表示,恰恰不是最适合跨机器搬运的表示。字符串可以正确记录某次本地操作,却不能凭自身重建另一台主机的接口命名和拓扑位置。

输入校验保护的是本地控制边界

RFC 4007 没有规定 zone identifier 的统一字符集和最大长度。保留实现自由的代价,是每个接收界面都必须面对恶意或异常输入。RFC 9844 建议设置适当长度,并拒绝 NUL 等危险内容。RFC 4007 的 held erratum 8553 也把缺失的字符与长度限制指向这份新安全指导。

风险不仅是解析器崩溃。zone 字符串可能经过日志、配置文件、shell、URI 控件和系统 API;不同层若在不同时间解码、截断或规范化,就可能选择错误接口或让审计记录与实际调用不一致。把输入拆成地址字段和接口选择字段,可以让每层明确验证自己的数据类型。

一次通过 %eth0 的成功连接只能证明:在当时那台主机上,该名称成功映射到一个接口,内核用它完成了那次操作。它不能证明远端身份,不能证明名称在其他主机可移植,不能证明全局可达,更不能证明地址所有权。把一次本地路径成功升级为身份或授权证据,会越过标准提供的事实。

规范化地址没有规范化本地注脚

IPv6 文本有许多等价写法。2010 年的 RFC 5952 通过压缩零、字母大小写等规则减少差异,并说明地址与端口组合时使用方括号。这样的规范化有助于日志比较、搜索和配置一致性。

但 RFC 5952 规范的是地址文字,不负责 RFC 4007 的 zone 后缀。2025 年的 RFC 9844 特别提醒,两者不能混为一个全局 canonical string。即使地址部分已经完全规范化,%eth0、%7 或某个平台的其他名称仍属于本地映射。

默认 zone 也不能救场。RFC 4007 建议节点可为某些 scope 维护默认 zone,通常用零表示默认选择;然而 RFC 9844 指出,并非所有操作系统都提供可用默认值,Linux 被明确列为例子。应用省略 selector 后在一台机器成功,不能成为跨平台省略它的依据。

因此,运维记录至少要区分三层:原始地址位、用户输入的 zone identifier、系统解析后的接口索引。只保存最终数字会失去人的意图,只保存接口名会失去当时的系统映射,只保存地址则会丢掉最关键的出口选择。

地址与出口从一开始就是两个字段

这种分离并非后来为命令行临时添加。2003 年的 RFC 3493 在 IPv6 基本套接字接口中,让 sockaddr_in6 分别保存 sin6_addr 和 sin6_scope_id。一个字段承载 128 位地址,另一个字段承载 32 位 scope selector。调用者必须在本机把二者组合,内核不需要假装 selector 已编码进地址。

同一文档还给出接口名称与索引之间的映射接口。if_nametoindex() 把名称交给本机系统;若找不到该名称,就失败。这里没有一个互联网范围的 eth0 注册表,也没有向链路另一端询问“你把接口叫什么”的协议。名称只是本地人机界面的入口,数字索引才进入本机套接字结构。

RFC 4007 推荐文本写法 <address>%<zone_id>。实现应接受非负十进制标识,也可以支持接口名等自定义字符串。地址已经提供 scope,后缀只需在该 scope 的 zone 之间区分,因此它未必等同于完整的内部 zone index。

标准同时给出禁区:这种写法只对非全局作用域地址有意义;用于全局地址没有意义,也不应施加在回环地址上。更重要的是,它只能在一个节点内部使用。除非所有解释者事先约定相同语义,否则 zone identifier 不得被送上网络。

这不是“格式细节”。它把责任放在最有知识的一侧:本机知道自己有哪些接口、索引如何分配、哪一个 zone 可达。远端既不需要也无法可靠解释这套名字。

2025 年的修正不是换一个分隔符

RFC 9844 在 2025 年完全废止 RFC 6874,并说明浏览器实现者认为原路线不可行。它还撤销 RFC 6874 对 RFC 3986 的更新。经核实的 RFC 6874 erratum 8552 记录了这一状态变化。

替代方案把问题退回到正确的控制面:凡是接收非全局单播地址的用户界面,都必须让用户输入或选择 zone identifier。完整的 % 写法仍是首选,但实现也可使用独立字段、下拉列表、另一种分隔形式或单独参数。要求的是能力和语义,不是强迫所有界面把本地信息塞进 URI。

界面应把地址和 zone identifier 分开保存,再把后者转换为本机接口索引。错误的名称应该作为错误报告,而不是静默猜测。POSIX inet_pton() 单独不能解析 fe80::1%eth0;程序可以使用 getaddrinfo(),也可以先拆开字符串,再把 inet_pton() 与 if_nametoindex() 组合。

RFC 9844 明确排除浏览器获取 URI 的语义。它解决的是用户如何向本地应用表达 zone,不是重新定义 HTTP origin。zone identifier 仍不得上网。标准从“让 URI 携带后缀”转为“让 UI 负责接收和验证”,并不是功能倒退,而是把局部知识归还给拥有它的节点。

URI 把一条本地规则推向了全球语法

URI 带来了更尖锐的冲突。2005 年的 RFC 3986 把 URI 设计为具有全球作用域的标识语法,同时保留某些动作依赖终端用户语境的可能性。它还把 % 定义为百分号编码的引导字符;若一个字面百分号本身作为数据出现,就要写成 %25。

2013 年的 RFC 6874 尝试把 zone identifier 安放在 IPv6 字面量 URI 中;HTTP URI 的方括号部分可写成 [fe80::a%25en1]。%25 表示分隔用的字面 %,后面才是本地 ZoneID。方案希望浏览器和其他 URI 使用者能够打开链路本地设备。

该文档其实已经承认核心矛盾:ZoneID 只对发起节点有意义,放入外发 HTTP 请求之前应被移除。一个看似属于 URL 的组成部分,在真正跨网络传递前又必须消失。编码层次还增加了重复编码或错误解码的机会;同一串 %25 在不同解析阶段可能被赋予不同含义。

困难不只是选择哪个转义字符。浏览器需要决定 zone 后缀是否参与 origin、安全策略、历史记录、书签、代理请求和地址栏规范化。若把它当成稳定 URI 身份,就会携带主机内部名字;若完全忽略它,本地链路选择又无法完成。局部执行参数与全球标识语法被迫争用同一个字符串。

最小的共享协议,保留最大的本地诚实

zone identifier 的历史展示了互联网协议常见的一种克制。共享标准规定 scope、地址行为、套接字字段和输入义务;节点保留接口命名、索引分配与实际 zone 选择。数据包只携带跨节点有共同意义的部分,本地 selector 在出发前完成工作并停留在本地。

这种安排并不让操作变得自动。相反,它迫使工具承认自己缺少上下文。界面必须询问用户,应用必须调用正确 API,审计系统必须保留映射,自动化必须在目标主机上解析名称。代价是多一步显式选择,收益是不把偶然的本地名字伪装成网络身份。

真正完整的地址记录,不是一条能到处粘贴的字符串,而是一组边界清楚的事实:哪些位属于 IPv6 地址,哪个选择只在本机成立,哪次映射已经验证,以及哪些结论仍未获得证据。那条本地注脚有价值,正因为它从未被允许冒充正文。

来源