DNS 手册域名系统参考

报文格式与 EDNS(0)

不管走 UDP、TCP、TLS、HTTPS 还是 QUIC,里面装的都是同一种报文。它的格式从 1987 年的 RFC 1035 起基本没变,后来的扩展都塞进了一个“伪记录”里。

最后修订 · 约 7 分钟读完

整体结构#

一个 DNS 报文依次是:12 字节的头部,然后是问题、回答、授权、附加四个节。查询和应答格式相同,靠头部的 QR 位区分。

部分内容
头部ID、标志位、四个节各自的记录数
问题节要问的名字、类型、类别。实际上几乎总是恰好一个问题
回答节直接回答问题的记录(包括途经的 CNAME)
授权节转介时下一层的 NS;否定应答时的 SOA;DNSSEC 的不存在证明
附加节可能有用的附带数据,例如胶水地址;EDNS 的 OPT 伪记录也在这里

下图按 RFC 1035 的画法,一行 16 位。AD 与 CD 两位来自 DNSSEC(RFC 4035),占用了原先保留的 Z 字段中的两位。

头部布局
                                1  1  1  1  1  1
  0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                      ID                       |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|QR|   Opcode  |AA|TC|RD|RA| Z|AD|CD|   RCODE   |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    QDCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    ANCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    NSCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    ARCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
字段含义
ID16 位标识,应答原样带回,用来配对。在 UDP 上它和源端口一起构成防伪的随机性;DoH 建议填 0 以便 HTTP 缓存,DoQ 要求必须填 0(加密通道已经解决了配对和防伪)。
QR0 为查询,1 为应答。
Opcode0 QUERY;2 STATUS;4 NOTIFY(RFC 1996);5 UPDATE(RFC 2136);6 DSO(RFC 8490)。1 IQUERY 已被 RFC 3425 废弃。
AA权威应答。
TC截断:应答放不下,客户端应改用 TCP。
RD / RA请求递归 / 支持递归。stub 发查询时置 RD;递归解析器在应答里置 RA。
Z保留,必须为 0。
AD已认证数据:验证型解析器确认数据通过了 DNSSEC 验证。只有在你信任这个解析器、且到它的链路安全时,这一位才有意义。
CD禁止检查:请求解析器不做验证,把数据原样交回,让客户端自己验。
RCODE4 位结果码;EDNS 再扩展出 8 位,合计 12 位。常见值见排错页的对照表。

问题与资源记录的编码#

问题由名字、2 字节类型(QTYPE)、2 字节类别(QCLASS,几乎总是 1,即 IN)组成。资源记录在此基础上再加 4 字节 TTL、2 字节数据长度(RDLENGTH)和数据本身(RDATA)。

名字的编码与压缩#

名字在线上写成一串“长度 + 内容”的标签,以长度为 0 的根标签结束。www.example.com. 编码为:

十六进制
03 77 77 77  07 65 78 61 6d 70 6c 65  03 63 6f 6d  00
 3  w  w  w   7  e  x  a  m  p  l  e   3  c  o  m  (根)

同一个报文里常常重复出现相同的后缀,所以 RFC 1035 定义了压缩:长度字节的高两位为 11 时,它和后面一个字节组成 14 位的指针,指向报文中更早出现的一段名字。应答里的第一条回答通常写成 c0 0c,意思是“同问题节里那个名字”(问题节从偏移 12 开始)。

解析器实现要当心

指针可以指向指针,恶意报文能借此构造环路或越界读取。历史上不少 DNS 漏洞都出在名字解压上。稳妥的实现会限制跳转次数、要求指针只能向前(指向更早的位置),并检查名字总长度。

EDNS(0)#

RFC 1035 的头部没有空余位置可以扩展。EDNS(0)(RFC 6891)的办法是在附加节放一条类型为 OPT(41)的伪记录,借用它的字段:

字段在 OPT 里的含义
名字必须是根(单个 00)
CLASS发送方能接收的 UDP 报文大小
TTL 的高 8 位扩展 RCODE 的高位
TTL 的次 8 位EDNS 版本,目前只有 0
TTL 的低 16 位最高位是 DO(DNSSEC OK),表示“请把签名一起给我”;其余为保留位
RDATA若干个“代码 + 长度 + 数据”的选项

常见选项:

代码选项作用RFC
3NSID让服务器报出自己是哪台实例,任播排错很有用5001
8Client Subnet(ECS)把客户端所在网段告诉权威服务器,见隐私页7871
9EXPIRE区传送时告知剩余过期时间7314
10COOKIE轻量的源地址验证,见攻击与防护7873,9018
11edns-tcp-keepalive协商 TCP/TLS 连接的空闲超时7828
12Padding填充,掩盖加密报文的长度7830,8467
13CHAIN一次请求整条 DNSSEC 链7901
14edns-key-tag报告解析器信任的根密钥标签8145
15Extended DNS Error附在应答上的详细错误原因8914

一个只懂 RFC 1035 的老服务器收到带 OPT 的查询,可能回 FORMERR,也可能干脆不回。解析器过去为此准备了各种“去掉 EDNS 再试一次”的变通办法,2019 年的 DNS Flag Day 各大解析器统一取消了这些变通,不再迁就不合规的权威服务器。

UDP 大小与分片#

EDNS 让客户端可以声明“我能收 4096 字节的 UDP 报文”。问题在于大报文会在 IP 层被分片,而分片在今天的网络里经常丢失,还会被用来做投毒(伪造第二个分片比猜 ID 容易得多)。

2020 年的 DNS Flag Day 建议把 EDNS 缓冲区默认设为 1232 字节:这是 IPv6 保证能通过的最小 MTU 1280 字节减去 40 字节 IPv6 头和 8 字节 UDP 头。放不下就截断、改走 TCP,用一点延迟换可靠性。主流解析器和权威软件此后都把默认值降了下来。

截断与 TCP#

应答超过对方声明的大小时,服务器只发能放下的部分并置 TC=1(实践中常常是只带问题节的空应答)。客户端看到 TC 就改用 TCP 重发同一个问题。

在 TCP(以及其上的 DoT)里,每个报文前面加 2 字节的长度字段,所以单个报文最大 65535 字节。RFC 7766 要求客户端和服务器支持在一条连接上连续发送多个查询(管线化),服务器可以按完成顺序乱序应答,客户端靠 ID 配对。DoQ 同样使用 2 字节长度前缀,但每个查询独占一条 QUIC 流。

动手解析一个报文#

把抓包得到的十六进制粘贴进来(空格、冒号、0x 都可以带着),或者直接粘贴 DoH GET 请求里 dns= 后面的那一段。解析全部在浏览器里完成,不会发出任何网络请求。

想反过来从名字生成报文,可以用 DoH 页的请求构造器,它的输出可以直接贴回这里核对。

DNS 手册 · 内容依据 IETF RFC 与各运营方公开资料整理,出现分歧时以原文为准。回到顶部