报文格式与 EDNS(0)
不管走 UDP、TCP、TLS、HTTPS 还是 QUIC,里面装的都是同一种报文。它的格式从 1987 年的 RFC 1035 起基本没变,后来的扩展都塞进了一个“伪记录”里。
整体结构#
一个 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 |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+| 字段 | 含义 |
|---|---|
ID | 16 位标识,应答原样带回,用来配对。在 UDP 上它和源端口一起构成防伪的随机性;DoH 建议填 0 以便 HTTP 缓存,DoQ 要求必须填 0(加密通道已经解决了配对和防伪)。 |
QR | 0 为查询,1 为应答。 |
Opcode | 0 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 | 禁止检查:请求解析器不做验证,把数据原样交回,让客户端自己验。 |
RCODE | 4 位结果码;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 |
|---|---|---|---|
| 3 | NSID | 让服务器报出自己是哪台实例,任播排错很有用 | 5001 |
| 8 | Client Subnet(ECS) | 把客户端所在网段告诉权威服务器,见隐私页 | 7871 |
| 9 | EXPIRE | 区传送时告知剩余过期时间 | 7314 |
| 10 | COOKIE | 轻量的源地址验证,见攻击与防护 | 7873,9018 |
| 11 | edns-tcp-keepalive | 协商 TCP/TLS 连接的空闲超时 | 7828 |
| 12 | Padding | 填充,掩盖加密报文的长度 | 7830,8467 |
| 13 | CHAIN | 一次请求整条 DNSSEC 链 | 7901 |
| 14 | edns-key-tag | 报告解析器信任的根密钥标签 | 8145 |
| 15 | Extended 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 页的请求构造器,它的输出可以直接贴回这里核对。