系统设计 / DNS · 层级、委任、缓存,以及不存在的「浸透」 / 报文的字节:12 字节头与名字压缩 待审核 5 / 11
flag 位 · 压缩指针 · EDNS(0)

报文的字节:12 字节头与名字压缩

前一页把解析讲成「问谁、得到哪种应答」。这一页下沉一层:那些应答在线路上是什么样的字节。

报文的骨架自 1987 年的 RFC 1035 §4 起没有变过:12 字节定长头,加四个变长段(question、answer、authority、additional)。

1 · 头部的 16 位控制字段

12 字节头里有一个 16 位的 flag 字段,它是整套协议的控制面。其中七位有名字:QR(查询还是应答)、AA(应答者对该名字有权威)、TC(应答被截断)、RD(请对方代为递归)、RA(本服务器提供递归服务)、AD 与 CD(DNSSEC 用,见第 7 讲)。剩下的是 opcode(4 位)、rcode(4 位)与 1 位保留位 Z(恒为 0)。

图 1-1 · 一次查询的完整字节。可改 QNAME / QTYPE、开关各 flag 与 EDNS(0),悬停任一字节看它属于哪个字段;勾选「当作应答」可见 ANSWER 与 AUTHORITY 两段如何填入。

图 1-1 能看清一件事:一次委任应答与一次权威应答在报文层面的区别极小——AA 这一位,加上 ANCOUNT 与 NSCOUNT 两个计数。没有专门的「referral」报文类型;「该去问别人」这个语义完全由「AA=0 且 ANSWER 空而 AUTHORITY 里有 NS」这个组合表达。

另一件是 QNAME 的形状:线路上不出现点。每个 label 前置一个长度字节,末尾一个 0 字节表示根。所以字节数 = 各 label 字节数之和 + label 数 + 1,mail.vega.dev. 占 15 字节。255 字节的名字上限管的是这个数,不是屏幕上的字符数。

2 · 名字压缩

DNS 应答里同一个后缀会反复出现:一次委任应答的每条 NS、每条 glue 都带着同一个 zone 名。RFC 1035 §4.1.4 为此定义了压缩指针——一个长度字节若高两位为 11,它与下一字节合成 14 位偏移,指向报文中先前出现过的同名后缀。

图 2-1 · 压缩省下的字节数随 NS 条数上升。可拖动条数观察比例变化;下半部分是解码器对三种坏报文的处置。

14 位这个宽度带来一条至今仍在咬人的限制:可寻址范围只到 16383 字节,超出该偏移的名字无法被指向。在 DNSSEC 把应答撑大之后,这个上限偶尔会真的碰到。

压缩还有一条容易被忽略的规则:只有 RFC 1035 本身定义的类型允许在 RDATA 内使用压缩。之后新增的类型(SRV、HTTPS 等)一律不许。理由是中间设备——不认识某个类型的转发器如果重写了报文,RDATA 里那个指针就会指向错误的位置。

警示 · 压缩指针给解码器带来一类必须处理的输入:环。两个指针可以互指,一串合法的、逐次前进的指针也可以把解码器拖到二次开销。写本页的解码器时,最初的防护是「指针只许指向更小的偏移」——看起来合理,因为编码器写出的指针总是向后指。但这条规则会把一类合法的报文误判:跳回一个较早的名字之后继续向前读,其后遇到的指针目标完全可能大于第一次跳转的目标。改成「每个指针目标只许访问一次」加「名字总长不超过 255 字节」两道闸才既终止又不误判——前者保证终止(偏移是有限集),后者拦不带指针、纯靠 label 堆出来的超长名。

3 · 512 字节与 EDNS(0)

RFC 1035 给 UDP 报文定了 512 字节上限。超出就置 TC 位,提问方改用 TCP 重问一次——多一个往返,还多一次三次握手。

这个上限在 1987 年是保守而合理的(当时的路径 MTU 假设),但它挡住了后来的一切扩展:DNSSEC 的签名、NSEC 的位图、以及带上 AAAA glue 之后的根 NS 应答都装不下(实测今日 dig +tcp . NS @198.41.0.4 为 811 字节,核对于 2026-08)。

EDNS(0)(RFC 6891)的解法是一个伪记录:在 additional 段放一条 type 为 OPT 的记录,借它的 CLASS 字段位置声明自己能接收的 UDP 载荷大小,借 TTL 字段位置放扩展 rcode 与 flag 位(其中 DO 位表示「我要 DNSSEC 数据」)。

这个设计的巧处在于它没有改动 12 字节头——不支持 EDNS 的老实现看到 additional 段里一条不认识的记录,按 RFC 3597 当不透明数据处理即可,不会崩。整个 DNS 的可扩展性都建立在「不认识也要能存能传」这一条上。

注 · OPT 与 TSIG、TKEY 同属只存在于报文、不出现在任何 zone 的 meta-RR,其中只有它承担协议扩展。它的 owner 恒为根、TTL 字段不是 TTL、CLASS 字段不是 class——三个字段全部被重新赋义。这在 DNS 里是孤例,代价是它无法被缓存也无法被签名,好处是不必动头部。

声明多大的载荷曾有过一段反复。早期常见 4096,后来发现大 UDP 报文在 IPv6 分片与部分中间设备上丢包率显著,DNS Flag Day 2020 之后的共识是 1232(IPv6 最小 MTU 1280 减去 IPv6 与 UDP 头)。这不是协议规定,而是一次跨实现的运维协调。

4 · 参考文献

  1. Mockapetris, P. (1987). Domain Names — Implementation and Specification (RFC 1035). IETF. §4 是报文格式,§4.1.4 是名字压缩,§2.3.4 是 512 / 255 / 63 三条上限。
  2. Damas, J., Graff, M., & Vixie, P. (2013). Extension Mechanisms for DNS (EDNS(0)) (RFC 6891). IETF.
  3. Gustafsson, A. (2003). Handling of Unknown DNS Resource Record (RR) Types (RFC 3597). IETF.
  4. DNS Flag Day contributors. (2020). DNS Flag Day 2020. https://dnsflagday.net/2020/(1232 这个数字的来历。)