DNS 报文结构解析:从一次 dig 查询说起
用 dig 发起一次查询,结合 RFC 1035 逐字段拆解 DNS 头部、问题区与资源记录,顺便搞懂域名压缩指针 0xC00C。
- DATE
- LEN
- 848 字
- TTL
- 2 min
- LAYER
- L7 · 应用层
DNS 是我们每天用得最多、却最少认真看过的协议。它的报文格式非常紧凑,很适合作为“逐字节分析”的入门练习。
一次查询
$ dig www.example.com A
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 41870
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;www.example.com. IN A
;; ANSWER SECTION:
www.example.com. 300 IN A 203.0.113.80
;; SERVER: 192.0.2.53#53(192.0.2.53) (UDP)
;; MSG SIZE rcvd: 60
笔记
示例中的 IP 均替换成了 RFC 5737 规定的文档保留地址,你自己实验时会看到真实地址。
dig 的输出其实就是把报文的各个部分“翻译”成了人话。下面对照报文格式一一拆开。
整体结构
一个 DNS 报文由五部分组成:
| 区段 | 内容 |
|---|---|
| Header | 固定 12 字节 |
| Question | 要查什么(域名 + 类型 + 类) |
| Answer | 回答的资源记录(RR) |
| Authority | 权威服务器的 RR |
| Additional | 附加 RR(如 EDNS 的 OPT 伪记录) |
Header:12 字节
按 RFC 1035 的习惯,DNS 头部画成 16 位宽:
对照 dig 输出:
id: 41870→ ID,客户端随机生成,用来匹配请求和响应;flags: qr rd ra:- QR=1:这是响应(请求为 0);
- RD=1:客户端希望递归查询(Recursion Desired);
- RA=1:服务器支持递归(Recursion Available);
- 没有 AA,说明这不是权威服务器给的答案,而是递归解析器的缓存或转发结果;
status: NOERROR→ RCODE=0;常见的还有NXDOMAIN(3)、SERVFAIL(2)、REFUSED(5);QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1→ 四个计数字段。
Z 位的历史
RFC 1035 中 Z 是 3 个保留位。后来 DNSSEC(RFC 4035)从中拿走了两位:AD(Authentic Data)和 CD(Checking Disabled),所以现在只剩 1 位 Z。看老资料时注意区分。
Question:域名是怎么编码的
域名不是以点分隔的字符串,而是一串“长度 + 内容”的标签,以 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 .
后面紧跟 2 字节 QTYPE(A=1、NS=2、CNAME=5、MX=15、TXT=16、AAAA=28)和 2 字节 QCLASS(IN=1)。
所以这个问题区共 17 + 4 = 21 字节。
Answer:资源记录与压缩指针
每条资源记录(RR)的格式是:
如果每条记录都完整写一遍域名,报文会很浪费。DNS 用压缩指针解决:当一个 16 位值的最高两位是 11 时,剩下 14 位表示“这个名字在报文中的偏移量”。
最常见的指针是 c0 0c:
0xC00C = 1100 0000 0000 1100
^^ 指针标志 偏移 = 0x0C = 12
偏移 12 正好是紧跟在 12 字节头部之后的位置,也就是问题区里的第一个域名。所以 Answer 中的 NAME 只需要 2 字节。
于是这条 A 记录的长度是:2(NAME)+ 2(TYPE)+ 2(CLASS)+ 4(TTL)+ 2(RDLENGTH)+ 4(IPv4 地址)= 16 字节。
验证一下报文长度
dig 显示 MSG SIZE rcvd: 60,我们来算算:
| 部分 | 字节数 |
|---|---|
| Header | 12 |
| Question(域名 17 + 类型 2 + 类 2) | 21 |
| Answer(A 记录) | 16 |
| Additional(EDNS OPT 伪记录:根域名 1 + 类型 2 + 类 2 + TTL 4 + 长度 2) | 11 |
| 合计 | 60 ✓ |
能把报文长度一字节不差地算出来,说明格式真的看懂了。
一次完整的解析过程
dig 问的只是本地的递归解析器。解析器背后还要做一系列迭代查询:
用 Scapy 自己构造一个查询
from scapy.all import IP, UDP, DNS, DNSQR, sr1
q = IP(dst="192.0.2.53") / UDP(dport=53) / DNS(rd=1, qd=DNSQR(qname="www.example.com", qtype="A"))
ans = sr1(q, timeout=2, verbose=0)
if ans:
ans[DNS].show()
Wireshark 过滤器
dns # 所有 DNS
dns.flags.response == 0 # 只看查询
dns.qry.name == "www.example.com" # 指定域名
dns.flags.rcode != 0 # 出错的响应
dns.qry.type == 28 # AAAA 查询
小结
- DNS 默认走 UDP 53;响应被截断(TC=1)时会改用 TCP 重试,区域传送(AXFR)也走 TCP;
- 头部固定 12 字节,四个计数字段决定后面有几条记录;
- 域名用“长度 + 标签”编码,靠
0xC0开头的压缩指针复用; - 现代 DNS 几乎都带 EDNS(0) 的 OPT 伪记录,用来声明更大的 UDP 报文尺寸等能力。
下一步想看看 DoH / DoT 加密之后,流量里还能看出什么。