2026年1月8日,Cloudflare 对其公共 DNS 服务 1.1.1.1 的一次例行更新,意外地改变了 DNS 响应中记录的顺序,将 CNAME 记录放在了 A 记录之后。这一微小调整触发了全球范围的 DNS 解析故障。问题根源在于一个存在了近 40 年的协议模糊性:RFC 1034 对 CNAME 记录是否必须排在最前面没有做出强制性规定。然而,一些广泛使用的 DNS 客户端(如 Linux 系统中的 glibc 库和部分思科交换机)恰恰依赖这一顺序。Cloudflare 迅速回滚了更新,并着手推动 IETF 制定更明确的标准,以防止未来再次发生类似问题。
一次旨在优化的代码变更
事件的起因是一项旨在降低 1.1.1.1 缓存实现内存占用的代码优化。在处理部分过期的 CNAME 解析链时,旧代码会先将缓存中有效的 CNAME 记录放入一个新列表,再追加新解析到的 A/AAAA 记录,确保 CNAME 在前。
为了节省内存分配,新代码简化了这一过程,直接将缓存的 CNAME 记录追加到了已有 A/AAAA 记录列表的末尾。
- 旧逻辑: CNAME 记录 + A/AAAA 记录
- 新逻辑: A/AAAA 记录 + CNAME 记录
这个顺序的颠倒,成为了问题的导火索。
为什么顺序很重要?
DNS 客户端在收到包含 CNAME 别名的响应时,需要沿着这个别名链找到最终的 IP 地址。一些客户端,特别是某些旧的或特定的实现,采用了一种简单的 “顺序解析” 逻辑。
它们按顺序读取响应记录,并期望首先看到 CNAME 记录,以便知道接下来应该寻找哪个域名对应的 IP 地址。
当顺序改变后,这些客户端的解析过程被打乱:
- 客户端请求
www.example.com的 A 记录。 - 它先收到了
cdn.example.com的 A 记录(IP 地址),但这个域名与它请求的www.example.com不匹配,于是 忽略 这条记录。 - 接着它才收到
www.example.comCNAMEcdn.example.com这条记录。 - 此时,客户端知道要去查找
cdn.example.com,但后面的记录已经处理完毕,它找不到对应的 IP 地址,最终导致 解析失败。
受影响最典型的两个例子是:
- glibc 的
getaddrinfo函数: 这是 Linux 系统上广泛用于 DNS 解析的基础库函数。 - 部分思科交换机: 这些设备在收到顺序颠倒的响应后,甚至会陷入反复重启的循环。
相比之下,许多现代 DNS 客户端(如 systemd-resolved)会先将所有收到的记录解析并存入一个集合,然后再进行逻辑处理,因此不受记录顺序的影响。
根源:RFC 1034 的模糊性
DNS 协议的核心规范 RFC 1034 发布于 1987 年,其中一段描述是问题的关键:
递归响应…可能是查询的答案,可能以一个或多个 CNAME RR(资源记录)作为前序…
这里的“可能作为前序 (possibly preface)”这一措辞非常模糊。它并未像现代 RFC 那样使用 MUST (必须) 或 SHOULD (应该) 等强制性关键词。这种模糊性导致了不同的实现方式:
- 一些开发者 将其理解为一项强制要求,编写了依赖 CNAME 在前的代码。
- 另一些开发者 则认为记录顺序无关紧要。
此外,RFC 1034 明确指出 RRset (相同名称、类型和类别的记录集合) 内部的记录顺序不重要,但没有明确规定不同 RRset 之间(比如一个 CNAME 记录和一个 A 记录)的相对顺序。这种长达数十年的协议空白地带,为这次故障埋下了伏笔。
结论与行动
尽管从协议文本来看,CNAME 记录的顺序并非强制要求,但现实情况是,大量正在运行的系统确实依赖于 “CNAME 在前” 这一约定俗成的顺序。
考虑到修复这些客户端的难度和漫长周期,最务实的选择是让 DNS 服务器遵守这一隐性规则。
Cloudflare 已经恢复了原有的记录顺序,并承诺未来将始终保持 CNAME 记录在前。更重要的是,为了彻底解决这一历史遗留问题,他们已向 IETF (互联网工程任务组) 提交了一份互联网草案,旨在:
- 明确 DNS 响应中 CNAME 记录的正确处理方式。
- 将这一模糊的约定升级为清晰的行业标准。
此举将帮助整个 DNS 社区避免未来因同样的歧义而引发故障。