系统设计 / DNS · 层级、委任、缓存,以及不存在的「浸透」 / 资源记录与 zone 里的四类数据 待审核 2 / 11
RR · RRset · glue · 遮蔽

资源记录与 zone 里的四类数据

zone 里的数据全部是同一种东西:资源记录(resource record,简称 RR)。一棵树,每个节点上挂着若干条带类型的记录,就是 DNS 的全部数据模型。

1 · 一条记录的五段

mail.vega.dev.        3600      IN      A       203.0.113.40
└─ owner ────┘        └TTL┘     └class┘ └type┘  └─ RDATA ──┘

owner 是这条记录挂在哪个名字上。TTL 是缓存方可以留存它多少秒。class 几乎总是 IN(Internet)——CHHS 是历史遗留,CH 至今仍有一处实用价值:许多权威服务器实现用 version.bind CH TXT 报告自己的版本号。type 决定 RDATA 怎么解读。

写在 zone file 里时,这五段有一套省略规则:owner 省略即继承上一条(缩进在此有语义)、TTL 省略即取 $TTL、class 与 TTL 的先后可以互换、相对名会补上 $ORIGIN@ 代表 $ORIGIN 自身、括号之间可以跨行。这些省略叠起来,使得同一份数据的两种写法在字面上可以毫无共同点。

图 1-1 · 常见类型与它们的 16 位类型码。可筛选查看;末列标出本页编解码器是按类型解析 RDATA 还是按 RFC 3597 透传。

2 · 缓存与签名的单位是 RRset

owner、class、type 三者相同的一组 RR 合称一个 RRset。它不是一个方便的说法,而是协议里的实际单位:缓存以 RRset 为单位存取,DNSSEC 以 RRset 为单位签名,应答里也不允许只给一个 RRset 的一部分。

由此推出一条硬约束:

定理 2.1 同一个 RRset 内所有 RR 的 TTL 必须相同(RFC 2181 §5.2)。

证明 反设 RRset 内有两条 TTL 不同的 RR。缓存方按 RRset 整体存取,到期时也整体丢弃,故必须为整个集合选定一个寿命;若取较大者,较短那条的作者意图被违反;若取较小者,则应答里出现过的一条记录会在其自身 TTL 未尽时消失。两种选择都使 TTL 失去「这条记录可以缓存多久」的含义。∎

现实中这条约束经常被违反(多人分别编辑 zone file 时尤甚),而各实现的收敛行为不一致——有的取最小值,有的取第一条,有的报错拒绝加载。本页的解析器取最小值并报出冲突。

3 · zone file 里的四类数据

一份 zone file 里的行,权威性并不一致。RFC 1034 §4.2.1 把它们分作四类:

第一类是本 zone 的权威数据——顶点以下、未被委任出去的一切。它们进应答的 ANSWER 段,带 AA 标志,会被 DNSSEC 签名。

第二类是顶点自身的 SOA 与 NS,表明「本服务器对此 zone 有权威」。

第三类是委任:指向下级 zone 的 NS 记录。它们写在本 zone 的文件里,却不带权威——同一组 NS 在子 zone 的顶点也有一份,那一份才是权威的。委任应答里它们进 AUTHORITY 段,AA=0。

第四类是 glue:委任目标主机的地址记录,同样不带权威,进 ADDITIONAL 段。

图 3-1 · zone file 的解析与逐行身份判定。可编辑左栏观察分类变化;「故意写坏的」那份示例含遮蔽记录、CNAME 冲突、RRset 内 TTL 不一致三类问题。

glue 的必要性来自一个循环。设 vega.dev 的 NS 是 ns1.vega.dev——要问 ns1.vega.dev 的地址,得先找到 vega.dev 的权威服务器,而那正是 ns1.vega.dev 自己。父 zone 提供 glue 就是为了打破这个循环,所以 glue 只在 NS 目标落在被委任子树内(术语叫 in-domain)时才是必需的。

注 · NS 完全可以指向别处:图 3-1 的根 zone 里,dev. 的 NS 就落在另一个 TLD 之下。这类地址记录叫 sibling glue,父 zone 没有提供的义务,因为它不构成循环。但实践中根 zone 提供了,各家权威实现也都会在 ADDITIONAL 段附上手头有的地址——省一轮往返是实打实的。写本系列的解析模拟器时,起初只按「严格 glue」填 ADDITIONAL 段,结果模拟出的根不给 TLD 服务器的地址,与真机 dig 的输出对不上;改成「手上有就附上」才吻合。

4 · 被切断遮蔽的记录

委任点之下唯一还能留在父 zone 里的数据是 glue 与 DS/NSEC。其余全部被遮蔽(occluded):不会被应答,也不会被 AXFR 传送(RFC 5936 §3.2)。

这是 zone file 里最隐蔽的一类错误,因为它完全沉默:语法无误、named-checkzone 通过、记录明明写在文件里,就是永远查不到。图 3-1 的「故意写坏的」示例里那条 hidden.lab.vega.dev. TXT 正是如此——lab.vega.dev 已经委任出去,它以下的整棵子树就不再属于 vega.dev 这个 zone 了,哪怕文件里还写着。

5 · 通配与空节点

*.cdn.vega.dev 这样的 owner 是通配(wildcard)。它的展开规则比看起来窄,三条都容易记错:

通配只在 closest encloser 之下生效——先找 qname 最长的、在 zone 里确实存在的祖先,再看它下面有没有 *。所以更近的实名会挡住更远的通配。

合成出来的记录,owner 是被问的那个名字,不是 *.…。通配是合成,不是别名,应答里不出现星号。

通配只能给它自己有的类型。问 img.cdn.vega.dev 的 AAAA 而通配只有 A,得到的是 NODATA,不是 NXDOMAIN。

图 5-1 · 同一台权威服务器对十种查询的应答。可逐条切换观察 rcode、AA 位与三个段的内容,含通配、空节点、CNAME、委任四种特殊情形。

与通配相关的是空节点(empty non-terminal):cdn.vega.dev 自己没有任何记录,但因为 *.cdn.vega.dev 存在,它是一个存在但无数据的节点。问它得到 NODATA 而非 NXDOMAIN。这一条是实现里最容易写错的地方,而错的后果会在 DNSSEC 与 QNAME minimisation 两处放大(见第 7、8 讲)。

6 · CNAME 的两条限制

CNAME 把一个名字指向另一个名字,解析器拿到它之后换名字继续查。两条限制:

同一个名字上只能有一条 CNAME,且它不得与其他类型共存(RFC 1034 §3.6.2)。原因是解析语义会矛盾:CNAME 的含义是「这个名字的数据在别处」,而并存的记录说数据就在本名字上。

因此 zone 顶点不能是 CNAME——顶点必须有 SOA 与 NS。这正是「根域名(example.com 而非 www.example.com)不能 CNAME 到 CDN」这个常见困境的来源,各家 DNS 服务商用 ALIAS / ANAME / CNAME flattening 之类的非标准记录绕开它:在权威服务器内部解析目标名,然后以 A/AAAA 的形式应答。RFC 9460 之后更规范的出路是 HTTPS 记录,它允许在顶点做服务绑定而不违反上述限制。

7 · 参考文献

  1. Mockapetris, P. (1987). Domain Names — Concepts and Facilities (RFC 1034). IETF. §3.6 是 RR 的构成与 CNAME 限制,§4.2.1 是 zone 内四类数据的划分。
  2. Elz, R., & Bush, R. (1997). Clarifications to the DNS Specification (RFC 2181). IETF. §5 定义 RRset 与 TTL 一致性要求。
  3. Lewis, E. (2006). The Role of Wildcards in the Domain Name System (RFC 4592). IETF.
  4. Gustafsson, A. (2003). Handling of Unknown DNS Resource Record (RR) Types (RFC 3597). IETF.
  5. Lewis, E., & Hoenes, A. (Eds.). (2010). DNS Zone Transfer Protocol (AXFR) (RFC 5936). IETF. §3.2 说明委任点之下的数据为何不被传送。