bigywater.tech
网络协议 · 66

DNS 报文格式:头部、问题与资源记录,逐字节解析

DNS 是互联网的电话簿,但它的报文只有区区几十个字节:12 字节头、问题区、几条资源记录。这篇手工拼一个查询、逐字节解析 1.1.1.1 的响应,看标志位、RCODE、名字压缩指针,并对照递归解析器与权威服务器的行为。

29 → 61 字节
查 example.com 的 A 记录:查询与响应的报文大小
c0 0c
响应里记录名的 2 字节压缩指针,指向报文偏移 12 的问题区名字
20% ~ 41%
名字压缩让真实响应省下的字节比例(按展开估算)

查询报文

实测 · 本机执行输出
    0000  1a 2b 01 00 00 01 00 00 00 00 00 00 07 65 78 61  .+...........exa
    0010  6d 70 6c 65 03 63 6f 6d 00 00 01 00 01           mple.com.....
  共 29 字节 = 12 字节头 + 问题区。头部:ID=1a2b  标志=0100(0x0100:QR=0 查询,Opcode=0,RD=1 期望递归)  QDCOUNT=1 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0
  问题区:07 "example" 03 "com" 00 = 13 字节的名字(每个标签前一个长度字节,以长度 0 结束),后面 QTYPE=1(A)、QCLASS=1(IN)各 2 字节
  对照 dnspython 生成的报文:29 字节,与手工拼的逐字节相同

12 字节头(ID、标志、QDCOUNT 等 4 个计数)加问题区;名字是“长度 + 标签”序列,以 0 结束。

响应与压缩指针

实测 · 本机执行输出
    0000  1a 2b 81 80 00 01 00 02 00 00 00 00 07 65 78 61  .+...........exa
    0010  6d 70 6c 65 03 63 6f 6d 00 00 01 00 01 c0 0c 00  mple.com........
    0020  01 00 01 00 00 01 10 00 04 68 14 17 9a c0 0c 00  .........h......
    0030  01 00 01 00 00 01 10 00 04 ac 42 93 f3           ..........B..
  响应 61 字节,往返 290.3 ms;ID 0x1a2b(与查询相同:True),标志 0x8180 = QR RD RA,RCODE = NOERROR;问题 1 / 回答 2 / 授权 0 / 附加 0
  [an] example.com  类型 A  类 1  TTL 272  RDLENGTH 4  RDATA 104.20.23.154
  [an] example.com  类型 A  类 1  TTL 272  RDLENGTH 4  RDATA 172.66.147.243
  回答区里记录的名字写成 2 字节的指针 c00c:高两位 11 表示“这是指针”,低 14 位 = 12 是报文内的偏移(从 ID 开头算),指向第 12 字节——问题区里的 example.com
  报文里共有 2 个记录名使用了压缩指针

响应里的记录名写成 c0 0c:指针指向报文内偏移 12。TTL、RDLENGTH、RDATA 紧随其后。

标志位

实测 · 本机执行输出
  递归解析器,RD=1,存在的名字                            :标志 QR RD RA;RCODE NOERROR;回答 2 授权 0 附加 0;61 字节
  递归解析器,RD=0(不要求递归)                           :标志 QR RA;RCODE NOERROR;回答 2 授权 0 附加 0;61 字节
  递归解析器,example.com 下不存在的子名字                  :标志 QR RD RA;RCODE NOERROR;回答 0 授权 1 附加 0;108 字节
  递归解析器,不存在的顶级域 .invalid                      :标志 QR RD RA;RCODE NXDOMAIN;回答 0 授权 1 附加 0;117 字节
    NXDOMAIN 的授权区带 SOA(1 条):其中的最小 TTL 字段决定这个否定答案能缓存多久(负缓存,下一篇展开)
  example.com 的 NS 记录:['hera.ns.cloudflare.com', 'elliott.ns.cloudflare.com']
  其中 hera.ns.cloudflare.com 的地址 108.162.192.162(公网地址)
  权威服务器,RD=1(权威服务器不递归)                        :标志 QR AA RD;RCODE NOERROR;回答 2 授权 0 附加 0;61 字节
  权威服务器,RD=0                                  :标志 QR AA;RCODE NOERROR;回答 2 授权 0 附加 0;61 字节
  权威服务器,不是它托管的名字 www.kernel.org               :标志 QR RD;RCODE REFUSED;回答 0 授权 0 附加 0;32 字节

递归解析器 QR RD RA;权威服务器 QR AA,没有 RA;不托管的名字得 REFUSED;不存在的顶级域得 NXDOMAIN + SOA。

名字压缩

实测 · 本机执行输出
  example.com NS:实际响应 84 字节(回答 2 / 授权 0 / 附加 0);全部展开的估算 126 字节,压缩省了 42 字节(33%)
  example.com MX:实际响应 44 字节(回答 1 / 授权 0 / 附加 0);全部展开的估算 55 字节,压缩省了 11 字节(20%)
  www.iana.org A:实际响应 107 字节(回答 3 / 授权 0 / 附加 0);全部展开的估算 181 字节,压缩省了 74 字节(41%)
  iana.org NS:实际响应 113 字节(回答 4 / 授权 0 / 附加 0);全部展开的估算 180 字节,压缩省了 67 字节(37%)
  iana.org SOA:实际响应 80 字节(回答 1 / 授权 0 / 附加 0);全部展开的估算 104 字节,压缩省了 24 字节(23%)

真实响应里压缩省了 20%~41% 的字节,多记录响应省得更多。

回显与畸形输入

实测 · 本机执行输出
  发 eXaMpLe.CoM → 响应问题区里写的是 eXaMpLe.CoM;回答里的记录名 ['eXaMpLe.CoM']
  发 EXAMPLE.COM → 响应问题区里写的是 EXAMPLE.COM;回答里的记录名 ['EXAMPLE.COM']
  发一个长度字节为 64 的名字 → 3 次都没有响应(服务器直接丢弃)
  发一个长度字节为 0x80 的名字 → 3 次都没有响应(服务器直接丢弃)

服务器原样回显大小写;长度字节 64 和 0x80 的畸形名字被 1.1.1.1 丢弃,没有回 FORMERR。

分步演示

DNS 报文
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64(Apple 芯片)/ Python 3.14.8 / tcpdump(libpcap)/ scapy 2.8 / dpkt / dnspython / h2 / aioquic / OpenSSL / curl 8.7.1;抓包在 lo0(环回)上做,少数实验用真实网卡 en0(这台机器上 en0 是 Wi-Fi,tcpdump 看到的是系统转成以太网格式的帧,所以文中的“以太网头”都指这个 14 字节的头)或访问公网(会在文章里注明);需要丢包 / 延迟 / 限速时,用一个只作用于 lo0 端口 9400-9499 的 dummynet 辅助脚本(netlab);所有服务端都是本机自己起的进程;时间戳取自 tcpdump 的 -tt,数字每次运行会有小幅抖动。向 1.1.1.1 与 example.com 的权威服务器(Cloudflare 的 NS)发送普通 DNS 查询,另有两个畸形名字只发给 1.1.1.1;往返约 290 ms;“展开估算”是我按名字长度计算的,不是实测;TTL 每次运行不同
    • 没有验证:压缩指针的循环或超界(服务器如何处理恶意报文);EDNS0 的 OPT 记录(下面 DNS over TCP 与 EDNS0 一篇展开);IPv6 传输的 DNS;不同递归解析器(8.8.8.8、本机配置的解析器)对 RD=0 的处理差别;为什么 1.1.1.1 对畸形名字不回 FORMERR(只看到“无响应”)
    京ICP备2026067502号-1京公网安备11010502063487号