Synth Daily

先有 CNAME 还是先有 A 记录?

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 地址。

当顺序改变后,这些客户端的解析过程被打乱:

  1. 客户端请求 www.example.com 的 A 记录。
  2. 它先收到了 cdn.example.com 的 A 记录(IP 地址),但这个域名与它请求的 www.example.com 不匹配,于是 忽略 这条记录。
  3. 接着它才收到 www.example.com CNAME cdn.example.com 这条记录。
  4. 此时,客户端知道要去查找 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 社区避免未来因同样的歧义而引发故障。