DNS(Domain Name System,域名系统)可能是互联网上使用频率最高、但被理解最少的协议。每一次 HTTP 请求、每一次邮件发送、每一次 API 调用——背后都有一次或多次 DNS 查询。但大多数工程师对 DNS 的理解停留在”域名转 IP”这个层面。
实际上,DNS 是一个复杂的分布式数据库系统,支持十几种记录类型、多层缓存、权威/递归分离架构、动态更新和安全扩展。理解 DNS 的报文结构和记录类型,是排查 DNS 故障、优化解析性能、配置 DNS 记录的基础。
本文从最底层的 wire format 开始,逐字段解析 DNS 报文,然后系统介绍每种记录类型的工程用途。不讲”DNS 是什么”——讲”DNS 的包长什么样、每个字段什么意思、每种记录怎么用”。
一、DNS 报文结构
DNS 使用一种紧凑的二进制报文格式(RFC 1035),无论是查询(Query)还是响应(Response),都使用相同的结构:
DNS 报文总体结构:
┌──────────────────────────────────────┐
│ Header (12 bytes, 固定) │
├──────────────────────────────────────┤
│ Question Section (变长) │
│ 查询的域名和类型 │
├──────────────────────────────────────┤
│ Answer Section (变长) │
│ 查询结果(响应时填充) │
├──────────────────────────────────────┤
│ Authority Section (变长) │
│ 权威 NS 记录(指向权威服务器) │
├──────────────────────────────────────┤
│ Additional Section (变长) │
│ 额外信息(如 NS 对应的 A 记录) │
└──────────────────────────────────────┘
1.1 Header 结构(12 字节)
Header 是 DNS 报文中唯一固定长度的部分,包含了控制整个 DNS 事务的关键信息:
DNS Header (12 bytes):
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ID | 2 bytes
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|QR| Opcode |AA|TC|RD|RA| Z|AD|CD| RCODE | 2 bytes
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QDCOUNT | 2 bytes
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ANCOUNT | 2 bytes
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| NSCOUNT | 2 bytes
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ARCOUNT | 2 bytes
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
字段说明:
ID (16-bit): 事务标识,查询和响应的 ID 必须一致
用于匹配请求和响应
应该是随机生成的(防止 DNS 缓存投毒)
QR (1-bit): 0=查询, 1=响应
Opcode (4-bit): 0=标准查询, 1=反向查询(已废弃), 2=服务器状态
AA (1-bit): Authoritative Answer — 响应来自权威服务器
TC (1-bit): TrunCation — 响应被截断(超过 UDP 大小限制)
RD (1-bit): Recursion Desired — 客户端请求递归查询
RA (1-bit): Recursion Available — 服务器支持递归
Z (1-bit): 保留,必须为 0
AD (1-bit): Authentic Data — DNSSEC 验证通过
CD (1-bit): Checking Disabled — 禁用 DNSSEC 验证
RCODE (4-bit): Response Code — 响应状态码
QDCOUNT (16-bit): Question Section 中的条目数(通常为 1)
ANCOUNT (16-bit): Answer Section 中的 RR 数
NSCOUNT (16-bit): Authority Section 中的 RR 数
ARCOUNT (16-bit): Additional Section 中的 RR 数
# 用 dig 查看 DNS Header 字段
dig example.com A +noall +comments
# ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 42871
# ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
# 解读:
# opcode: QUERY → Opcode=0, 标准查询
# status: NOERROR → RCODE=0, 查询成功
# id: 42871 → 事务 ID
# qr → QR=1, 这是响应
# rd → RD=1, 请求了递归
# ra → RA=1, 服务器支持递归
# 没有 aa → AA=0, 响应不是来自权威服务器(来自缓存)
# QUERY: 1 → QDCOUNT=1
# ANSWER: 1 → ANCOUNT=1
# AUTHORITY: 0 → NSCOUNT=0
# ADDITIONAL: 1 → ARCOUNT=1 (EDNS0 OPT 记录)1.2 RCODE(响应状态码)
RCODE 是 DNS 故障排查最重要的字段之一:
常见 RCODE:
RCODE=0 NOERROR 查询成功
域名存在,有对应的记录
RCODE=1 FORMERR 格式错误
DNS 服务器无法解析请求的格式
常见原因: 客户端发送了畸形报文
RCODE=2 SERVFAIL 服务器失败
DNS 服务器在处理请求时遇到内部错误
常见原因:
- 递归查询超时(上游权威服务器不可达)
- DNSSEC 验证失败
- 服务器配置错误
RCODE=3 NXDOMAIN 域名不存在
查询的域名在 DNS 中不存在
注意: NXDOMAIN 表示域名不存在,不是记录不存在
域名存在但没有请求类型的记录 → NOERROR + 空 Answer
RCODE=5 REFUSED 拒绝
DNS 服务器拒绝处理请求
常见原因:
- 递归服务器的 ACL 拒绝了客户端 IP
- 权威服务器拒绝了区域传输请求
RCODE=9 NOTAUTH 不是该区域的权威
服务器不负责该域名的区域
# 触发不同 RCODE 的实验
# NOERROR — 正常查询
dig example.com A +short
# 93.184.216.34
# NXDOMAIN — 域名不存在
dig thisdomaindoesnotexist12345.com A +noall +comments
# status: NXDOMAIN
# SERVFAIL — 通常是递归查询失败
# 模拟: 指定一个不可达的 DNS 服务器
dig @192.0.2.1 example.com A +timeout=2 +noall +comments
# ;; connection timed out; no servers could be reached
# REFUSED — 被 ACL 拒绝
# 某些递归服务器只服务特定 IP 段
dig @ns1.example.com example.com A +noall +comments
# status: REFUSED (如果 ns1 只接受授权的递归查询)
# NOERROR + 空 Answer — 域名存在但没有 AAAA 记录
dig example.com AAAA +noall +answer
# (空输出,但 status 是 NOERROR)1.3 Question Section
Question 格式:
┌────────────────────────────────────┐
│ QNAME (变长) │
│ 域名,使用标签序列编码 │
├────────────────────────────────────┤
│ QTYPE (2 bytes) │
│ 查询类型 (A=1, AAAA=28, ...) │
├────────────────────────────────────┤
│ QCLASS (2 bytes) │
│ 查询类别 (IN=1, 几乎总是 1) │
└────────────────────────────────────┘
域名编码 (QNAME):
www.example.com 编码为:
03 77 77 77 → 长度 3, "www"
07 65 78 61 6D 70 6C 65 → 长度 7, "example"
03 63 6F 6D → 长度 3, "com"
00 → 根域名标签(长度 0)
每个标签: [长度(1B)] [标签内容(N B)]
域名以 0x00(根域名)结束
单个标签最大 63 字节
整个域名最大 253 字节
1.4 Resource Record(资源记录)格式
Answer、Authority、Additional 三个 Section 都使用相同的 RR 格式:
Resource Record (RR) 格式:
┌────────────────────────────────────┐
│ NAME (变长) │
│ 域名(可能使用指针压缩) │
├────────────────────────────────────┤
│ TYPE (2 bytes) │
│ 记录类型 (A=1, NS=2, ...) │
├────────────────────────────────────┤
│ CLASS (2 bytes) │
│ 记录类别 (IN=1) │
├────────────────────────────────────┤
│ TTL (4 bytes) │
│ 缓存有效期(秒) │
├────────────────────────────────────┤
│ RDLENGTH (2 bytes) │
│ RDATA 的长度 │
├────────────────────────────────────┤
│ RDATA (变长) │
│ 记录数据(格式取决于 TYPE) │
└────────────────────────────────────┘
NAME 的指针压缩:
为了减小报文大小,NAME 字段可以使用指针引用报文中之前出现的域名
指针格式: 前两 bit 为 11(0xC0),后 14 bit 是偏移量
例: C0 0C → 指向偏移 12 处的域名(通常是 Question 中的域名)
这种压缩对性能影响不大,但在解析 DNS 报文时必须正确处理
1.5 用 Python 解析 DNS 报文
#!/usr/bin/env python3
"""解析 DNS 报文的 wire format"""
import struct
import socket
def parse_dns_header(data):
"""解析 12 字节的 DNS Header"""
fields = struct.unpack('!HHHHHH', data[:12])
header = {
'id': fields[0],
'flags': fields[1],
'qr': (fields[1] >> 15) & 1,
'opcode': (fields[1] >> 11) & 0xF,
'aa': (fields[1] >> 10) & 1,
'tc': (fields[1] >> 9) & 1,
'rd': (fields[1] >> 8) & 1,
'ra': (fields[1] >> 7) & 1,
'rcode': fields[1] & 0xF,
'qdcount': fields[2],
'ancount': fields[3],
'nscount': fields[4],
'arcount': fields[5],
}
return header
def parse_domain_name(data, offset):
"""解析域名(处理指针压缩)"""
labels = []
original_offset = offset
jumped = False
while True:
length = data[offset]
if length == 0:
offset += 1
break
if (length & 0xC0) == 0xC0:
# 指针压缩
pointer = struct.unpack('!H', data[offset:offset+2])[0] & 0x3FFF
if not jumped:
original_offset = offset + 2
offset = pointer
jumped = True
continue
offset += 1
labels.append(data[offset:offset+length].decode('ascii'))
offset += length
name = '.'.join(labels)
return name, (original_offset if jumped else offset)
RCODE_NAMES = {
0: 'NOERROR', 1: 'FORMERR', 2: 'SERVFAIL',
3: 'NXDOMAIN', 4: 'NOTIMP', 5: 'REFUSED',
}
TYPE_NAMES = {
1: 'A', 2: 'NS', 5: 'CNAME', 6: 'SOA',
15: 'MX', 16: 'TXT', 28: 'AAAA', 33: 'SRV',
41: 'OPT', 43: 'DS', 46: 'RRSIG', 48: 'DNSKEY',
}
# 发送 DNS 查询并解析响应
def dns_query(domain, qtype=1, server='8.8.8.8'):
# 构建查询报文
query = struct.pack('!HHHHHH', 0x1234, 0x0100, 1, 0, 0, 0)
for label in domain.split('.'):
query += bytes([len(label)]) + label.encode()
query += b'\x00' + struct.pack('!HH', qtype, 1)
# 发送 UDP 查询
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(5)
sock.sendto(query, (server, 53))
response, _ = sock.recvfrom(4096)
sock.close()
# 解析 Header
header = parse_dns_header(response)
print(f"ID: {header['id']:#06x}")
print(f"QR: {'Response' if header['qr'] else 'Query'}")
print(f"RCODE: {RCODE_NAMES.get(header['rcode'], header['rcode'])}")
print(f"Answer RRs: {header['ancount']}")
return response, header
if __name__ == '__main__':
dns_query('example.com')二、DNS 记录类型详解
DNS 支持多种资源记录(Resource Record, RR)类型,每种类型有不同的用途和 RDATA 格式。
2.1 A 和 AAAA 记录
A 记录 (TYPE=1):
将域名映射到 IPv4 地址
RDATA: 4 字节 IPv4 地址
AAAA 记录 (TYPE=28):
将域名映射到 IPv6 地址
RDATA: 16 字节 IPv6 地址
# A 记录查询
dig example.com A +short
# 93.184.216.34
# AAAA 记录查询
dig example.com AAAA +short
# 2606:2800:220:1:248:1893:25c8:1946
# 多个 A 记录 — DNS 轮询负载均衡
dig google.com A +short
# 142.250.80.46
# 142.250.80.78
# 142.250.80.110
# ...
# 工程注意事项:
# 1. 多个 A 记录的顺序在每次查询时可能不同(DNS Round Robin)
# 2. 客户端通常使用第一个返回的地址
# 3. DNS 轮询不是真正的负载均衡(不感知服务器健康状态)
# 4. TTL 影响切换速度 — 故障转移时 TTL 过长导致切换慢2.2 CNAME 记录
CNAME 记录 (TYPE=5):
将一个域名别名指向另一个域名(Canonical Name)
RDATA: 目标域名
www.example.com CNAME example.com
blog.example.com CNAME example-blog.netlify.app
核心规则:
1. CNAME 不能和其他记录共存于同一域名
错误: example.com CNAME other.com
example.com MX mail.example.com
正确: www.example.com CNAME example.com (子域名可以)
2. 根域名(Zone Apex)不能设置 CNAME
错误: example.com CNAME cdn.example.net
原因: 根域名必须有 SOA 和 NS 记录,与 CNAME 冲突
替代: 使用 ALIAS/ANAME 记录(非标准但多数 DNS 提供商支持)
或使用 A 记录直接指向 IP
3. CNAME 链最多跟随 8 次(防止循环)
# CNAME 查询
dig www.github.com CNAME +short
# github.com.
# 完整的 CNAME 链解析
dig www.example.com +trace +all
# 跟踪从根域名到最终 A 记录的完整路径
# CNAME + A 的解析过程:
dig www.netlify.com A
# ;; ANSWER SECTION:
# www.netlify.com. 3600 IN CNAME netlify.com.
# netlify.com. 20 IN A 75.2.60.5
# 递归解析器自动跟随 CNAME 并返回最终的 A 记录2.3 MX 记录
MX 记录 (TYPE=15):
指定接收邮件的服务器
RDATA: 优先级(2B) + 邮件服务器域名
example.com MX 10 mail1.example.com
example.com MX 20 mail2.example.com
example.com MX 30 backup-mail.example.com
优先级:
- 数字越小优先级越高
- 发送方先尝试优先级最高的服务器
- 相同优先级的服务器之间随机选择(负载均衡)
- 高优先级服务器不可达时尝试次高优先级
工程要点:
- MX 记录必须指向域名,不能指向 IP
- MX 指向的域名不应该是 CNAME(RFC 2181)
- 没有 MX 记录时,发送方会尝试 A/AAAA 记录
- SPF/DKIM/DMARC 与 MX 配合实现邮件安全
# 查看域名的 MX 记录
dig google.com MX +short
# 10 smtp.google.com.
# 20 smtp2.google.com.
# 30 smtp3.google.com.
# 40 smtp4.google.com.
# 邮件投递的 DNS 查询链:
# 1. 查 MX: dig example.com MX
# 2. 查 A: dig mail1.example.com A
# 3. 连接 SMTP: telnet <IP> 25
# 4. 验证 SPF: dig example.com TXT (查 SPF 记录)2.4 NS 记录
NS 记录 (TYPE=2):
指定域名的权威 DNS 服务器
RDATA: 权威服务器域名
example.com NS ns1.example.com
example.com NS ns2.example.com
两种位置:
1. 在父域(com)的区域文件中 — 委派记录(Delegation)
告诉递归解析器: "去找 ns1.example.com 查 example.com 的记录"
2. 在自己(example.com)的区域文件中 — 权威记录
告诉查询者: "我就是 example.com 的权威服务器"
胶水记录(Glue Record):
如果 NS 记录指向自己域名下的服务器:
example.com NS ns1.example.com
要查 ns1.example.com 的 IP 需要先查 example.com 的 NS...循环了
解决: 在父域 com 中添加胶水记录:
ns1.example.com A 192.0.2.1 ← 在 com 的区域文件中
# 查看域名的 NS 记录
dig example.com NS +short
# a.iana-servers.net.
# b.iana-servers.net.
# 直接向权威服务器查询(+norecurse)
dig @a.iana-servers.net example.com A +norecurse
# AA 标志会被设置(Authoritative Answer)
# 查看 NS 记录的 TTL
dig example.com NS
# example.com. 86400 IN NS a.iana-servers.net.
# TTL=86400 (24 小时) — NS 记录通常设置较长的 TTL2.5 SOA 记录
SOA 记录 (TYPE=6):
Start of Authority — 区域的管理信息
每个 DNS 区域必须有且仅有一个 SOA 记录
RDATA:
MNAME: 主要权威服务器域名
RNAME: 区域管理员邮箱(@ 用 . 替代)
SERIAL: 区域序列号(用于判断区域是否更新)
REFRESH: 从服务器检查更新的间隔(秒)
RETRY: REFRESH 失败后的重试间隔
EXPIRE: 从服务器在无法联系主服务器后停止服务的时间
MINIMUM: 否定缓存(Negative Caching)TTL
dig example.com SOA +noall +answer
# example.com. 3600 IN SOA ns.icann.org. noc.dns.icann.org. (
# 2024010101 ; SERIAL
# 7200 ; REFRESH (2 hours)
# 3600 ; RETRY (1 hour)
# 1209600 ; EXPIRE (14 days)
# 3600 ; MINIMUM / Negative TTL
# )
# SOA 字段的工程含义:
# SERIAL: 每次修改区域文件后必须递增,从服务器靠它判断是否需要同步
# 常用格式: YYYYMMDDNN (如 2024010101)
# REFRESH: 从服务器多久检查一次主服务器的 SERIAL
# RETRY: REFRESH 失败后多久重试(应 < REFRESH)
# EXPIRE: 从服务器多久没联系上主服务器后停止回答查询
# MINIMUM: 否定响应(NXDOMAIN)的缓存时间
# 注意: 这不是正常记录的默认 TTL2.6 SRV 记录
SRV 记录 (TYPE=33):
Service Record — 指定服务的位置(主机和端口)
RDATA: 优先级(2B) + 权重(2B) + 端口(2B) + 目标域名
格式: _service._proto.name SRV priority weight port target
_http._tcp.example.com SRV 10 60 80 web1.example.com
_http._tcp.example.com SRV 10 40 80 web2.example.com
_http._tcp.example.com SRV 20 0 80 backup.example.com
优先级: 数字越小越优先(类似 MX)
权重: 相同优先级内的流量分配比例
上例: web1 获得 60/(60+40)=60% 流量
web2 获得 40/(60+40)=40% 流量
端口: 服务监听的端口号
目标: 服务所在的主机域名
典型应用:
- Kubernetes 中的 Headless Service 使用 SRV 记录
- SIP (VoIP) 使用 SRV 发现 SIP Proxy
- LDAP、XMPP、CalDAV 等协议的服务发现
- Microsoft Active Directory 的 DC 定位
# 查询 SRV 记录
dig _sip._tcp.example.com SRV +short
# 10 60 5060 sip1.example.com.
# 10 40 5060 sip2.example.com.
# 20 0 5060 sip-backup.example.com.
# Kubernetes Headless Service 的 SRV 记录
# 在 Pod 内:
dig _http._tcp.my-service.default.svc.cluster.local SRV +short
# 0 33 80 my-service-0.my-service.default.svc.cluster.local.
# 0 33 80 my-service-1.my-service.default.svc.cluster.local.
# 0 33 80 my-service-2.my-service.default.svc.cluster.local.2.7 TXT 记录
TXT 记录 (TYPE=16):
存储任意文本数据
RDATA: 一个或多个字符字符串
原始用途: 人类可读的描述
现代用途: 机器可读的配置/验证信息
常见 TXT 记录应用:
1. SPF (Sender Policy Framework) — 邮件发送者验证
example.com TXT "v=spf1 ip4:192.0.2.0/24 include:_spf.google.com -all"
含义: 只有 192.0.2.0/24 和 Google 的邮件服务器可以代表 example.com 发邮件
2. DKIM (DomainKeys Identified Mail) — 邮件签名
selector._domainkey.example.com TXT "v=DKIM1; k=rsa; p=MIGf..."
含义: 邮件接收方用这个公钥验证邮件签名
3. DMARC (Domain-based Message Authentication) — 邮件策略
_dmarc.example.com TXT "v=DMARC1; p=reject; rua=mailto:dmarc@example.com"
含义: 不通过 SPF/DKIM 验证的邮件直接拒绝
4. 域名所有权验证
example.com TXT "google-site-verification=abc123..."
含义: 证明你控制这个域名(Google/AWS/Azure 等验证)
5. Let's Encrypt DNS-01 挑战
_acme-challenge.example.com TXT "token-value..."
含义: 证明你控制该域名以获取 TLS 证书
# 查看域名的所有 TXT 记录
dig example.com TXT +short
# "v=spf1 -all"
# 查看 Google 的 SPF 记录
dig google.com TXT +short | grep spf
# "v=spf1 include:_spf.google.com ~all"
# 展开 SPF include
dig _spf.google.com TXT +short
# "v=spf1 include:_netblocks.google.com include:_netblocks2.google.com ..."
# 查看 DMARC 策略
dig _dmarc.google.com TXT +short
# "v=DMARC1; p=reject; rua=mailto:mailauth-reports@google.com"2.8 PTR 记录
PTR 记录 (TYPE=12):
反向 DNS — 将 IP 地址映射回域名
用于 IP → 域名 的反向查找
IPv4 反向域名格式:
IP 地址 1.2.3.4 → 4.3.2.1.in-addr.arpa
(IP 地址倒过来 + .in-addr.arpa)
IPv6 反向域名格式:
每个十六进制数字用 . 分隔,倒序 + .ip6.arpa
2001:db8::1 → 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa
主要用途:
- 邮件服务器反向验证(很多 SMTP 服务器要求发送方 IP 有 PTR 记录)
- 日志中的 IP 地址反解析
- 安全审计和溯源
# IPv4 反向查询
dig -x 8.8.8.8 +short
# dns.google.
# IPv6 反向查询
dig -x 2001:4860:4860::8888 +short
# dns.google.
# 使用 host 命令(更简洁)
host 8.8.8.8
# 8.8.8.8.in-addr.arpa domain name pointer dns.google.
# 邮件服务器反向 DNS 验证
# 发送邮件时,接收方 SMTP 服务器会:
# 1. 看到连接来自 IP 203.0.113.1
# 2. 查 PTR: dig -x 203.0.113.1 → mail.example.com
# 3. 查 A: dig mail.example.com A → 203.0.113.1
# 4. 两者一致 → 通过反向验证
# 5. 不一致或没有 PTR → 可能被标记为垃圾邮件2.9 记录类型速查表
┌──────┬──────┬──────────────────────────────────────────┐
│ TYPE │ 编号 │ 用途 │
├──────┼──────┼──────────────────────────────────────────┤
│ A │ 1 │ 域名 → IPv4 地址 │
│ NS │ 2 │ 域名的权威 DNS 服务器 │
│ CNAME│ 5 │ 域名别名 → 规范域名 │
│ SOA │ 6 │ 区域管理信息 │
│ PTR │ 12 │ IP → 域名(反向 DNS) │
│ MX │ 15 │ 邮件服务器(带优先级) │
│ TXT │ 16 │ 任意文本(SPF/DKIM/验证) │
│ AAAA │ 28 │ 域名 → IPv6 地址 │
│ SRV │ 33 │ 服务发现(带端口和权重) │
│ OPT │ 41 │ EDNS0 伪记录(扩展功能) │
│ DS │ 43 │ DNSSEC 委派签名者 │
│ RRSIG│ 46 │ DNSSEC 资源记录签名 │
│ DNSKEY│48 │ DNSSEC 区域密钥 │
│ CAA │ 257 │ CA 授权(限制哪些 CA 可以颁发证书) │
│ HTTPS│ 65 │ HTTPS 服务绑定(含 ALPN、ECH 等参数) │
└──────┴──────┴──────────────────────────────────────────┘
三、EDNS0 扩展
原始 DNS 协议(RFC 1035)将 UDP 报文大小限制在 512 字节。随着 DNSSEC 和 IPv6 的出现,512 字节已经严重不足。EDNS0(Extension Mechanisms for DNS, RFC 6891)通过在 Additional Section 中添加一个 OPT 伪记录来扩展 DNS 协议。
3.1 OPT 记录结构
OPT 伪记录 (TYPE=41):
不是真正的 DNS 记录——它利用 RR 格式的字段携带扩展信息
字段复用:
NAME: 必须为空(根域名, 0x00)
TYPE: 41 (OPT)
CLASS: 被复用为 UDP Payload Size(客户端接受的最大 UDP 响应大小)
TTL: 被复用为扩展标志
高 8 位: Extended RCODE
8-15 位: VERSION
16 位: DO (DNSSEC OK) 标志
17-31: 保留
RDLENGTH: RDATA 长度
RDATA: EDNS0 选项(可选)
常见选项:
Option Code 8: EDNS Client Subnet (ECS)
允许递归解析器将客户端子网信息传递给权威服务器
用于地理位置感知的 DNS 响应(CDN 调度)
Option Code 10: COOKIE
DNS Cookie — 轻量级防 DNS 缓存投毒
客户端发送随机 Cookie,服务器返回 Server Cookie
后续查询带上两个 Cookie 以验证身份
Option Code 15: Extended DNS Error (EDE)
详细的错误信息(RFC 8914)
比 RCODE 提供更具体的错误原因
# 查看 EDNS0 信息
dig example.com A +bufsize=4096
# ;; OPT PSEUDOSECTION:
# ; EDNS: version: 0, flags:; udp: 4096
# udp: 4096 → 客户端声明接受最大 4096 字节的 UDP 响应
# 启用 DNSSEC 查询(设置 DO 标志)
dig example.com A +dnssec
# ;; OPT PSEUDOSECTION:
# ; EDNS: version: 0, flags: do; udp: 4096
# flags: do → DNSSEC OK,请求包含 DNSSEC 记录
# 查看 EDNS Client Subnet
dig @8.8.8.8 example.com A +subnet=203.0.113.0/24
# 通知 Google DNS: 客户端来自 203.0.113.0/24 网段
# 权威服务器可以返回地理位置最近的 IP
# 禁用 EDNS0
dig example.com A +noedns
# 使用原始 DNS 格式(最大 512 字节 UDP)3.2 UDP 大小限制与截断
DNS 报文大小的演进:
1987 (RFC 1035): 最大 512 字节 UDP
原因: 当时的网络 MTU 普遍 ≥ 576 字节
512 + IP 头(20) + UDP 头(8) = 540 < 576
问题: DNSSEC 签名经常超过 512 字节
1999 (RFC 2671): EDNS0 — 可声明更大的 UDP 缓冲区
客户端通过 OPT 记录声明可接受的最大 UDP 大小
常见值: 1232, 4096 字节
推荐: 1232 字节(避免 IP 分片, 1280 - IP/UDP 头)
TCP 回退:
当 DNS 响应超过 UDP 缓冲区大小时:
1. 服务器设置 TC (Truncation) 标志
2. 返回截断的响应
3. 客户端收到 TC=1 后,用 TCP 重新查询
4. TCP 没有大小限制(最大 65535 字节)
DNS over TCP 的触发条件:
1. UDP 响应被截断(TC=1)
2. 区域传输(AXFR/IXFR)— 始终使用 TCP
3. 大型 DNSSEC 响应
4. 某些查询工具强制 TCP(dig +tcp)
# 强制使用 TCP 查询
dig example.com A +tcp
# ;; SERVER: 8.8.8.8#53(8.8.8.8) (TCP)
# 设置较小的 UDP 缓冲区触发截断
dig example.com ANY +bufsize=128
# ;; Truncated, retrying in TCP mode.
# 响应超过 128 字节 → TC=1 → 自动 TCP 重试
# 查看 DNSSEC 签名导致的大响应
dig cloudflare.com DNSKEY +dnssec +bufsize=4096
# 响应通常 > 1000 字节(包含 DNSKEY 和 RRSIG)
# 如果 UDP 缓冲区太小,就会触发 TCP 回退
# 区域传输(必须 TCP)
dig @ns1.example.com example.com AXFR
# AXFR 始终使用 TCP,传输整个区域文件四、dig 完整实操
dig(Domain Information Groper)是 DNS
诊断的首选工具。掌握 dig 的高级用法是 DNS
故障排查的基础。
4.1 基本用法
# 基本查询
dig example.com # 默认查 A 记录
dig example.com AAAA # 查 AAAA 记录
dig example.com MX # 查 MX 记录
dig example.com ANY # 查所有记录(某些服务器不支持)
# 指定 DNS 服务器
dig @8.8.8.8 example.com # 使用 Google DNS
dig @1.1.1.1 example.com # 使用 Cloudflare DNS
dig @ns1.example.com example.com # 直接查权威服务器
# 输出控制
dig example.com +short # 只显示结果
dig example.com +noall +answer # 只显示 Answer Section
dig example.com +noall +comments # 只显示 Header
dig example.com +noall +authority # 只显示 Authority Section
dig example.com +noall +additional # 只显示 Additional Section4.2 dig +trace:完整递归追踪
# 从根域名开始追踪解析过程
dig www.example.com +trace
# 输出解读:
# . 518400 IN NS a.root-servers.net.
# . 518400 IN NS b.root-servers.net.
# ...
# ;; Received 525 bytes from 127.0.0.1#53 in 1 ms
#
# com. 172800 IN NS a.gtld-servers.net.
# com. 172800 IN NS b.gtld-servers.net.
# ...
# ;; Received 1170 bytes from 198.41.0.4#53(a.root-servers.net) in 20 ms
#
# example.com. 172800 IN NS a.iana-servers.net.
# example.com. 172800 IN NS b.iana-servers.net.
# ;; Received 266 bytes from 192.5.6.30#53(a.gtld-servers.net) in 15 ms
#
# www.example.com. 86400 IN A 93.184.216.34
# ;; Received 56 bytes from 199.43.135.53#53(a.iana-servers.net) in 25 ms
# 解析过程:
# 1. 问本地 DNS (127.0.0.1): . 的 NS 是谁?→ 根服务器列表
# 2. 问根服务器 (a.root-servers.net): com. 的 NS 是谁?
# → a.gtld-servers.net 等
# 3. 问 com TLD (a.gtld-servers.net): example.com. 的 NS 是谁?
# → a.iana-servers.net 等
# 4. 问权威服务器 (a.iana-servers.net): www.example.com 的 A 记录?
# → 93.184.216.34
# 总延迟: 1 + 20 + 15 + 25 = 61ms(无缓存的冷查询)
# 有缓存时,递归解析器只需要 1-5ms4.3 高级诊断
# 查看完整响应(包括所有 Section)
dig example.com A +noall +answer +authority +additional +comments
# 同时看到 Answer, Authority, Additional Section 和 Header
# 比较不同 DNS 服务器的响应
for ns in 8.8.8.8 1.1.1.1 9.9.9.9 223.5.5.5; do
echo "=== $ns ==="
dig @$ns example.com A +short +time=2
done
# 检查 TTL 递减
dig @8.8.8.8 example.com A | grep -E "^example"
# example.com. 21423 IN A 93.184.216.34
sleep 10
dig @8.8.8.8 example.com A | grep -E "^example"
# example.com. 21413 IN A 93.184.216.34
# TTL 减少了 10 秒 → 说明结果来自缓存
# 批量查询(从文件读取域名)
# domains.txt:
# example.com
# google.com
# github.com
dig -f domains.txt +short
# 反向查询
dig -x 8.8.8.8 +short
# dns.google.
# 查看 NSID(Name Server ID)— 识别回答你的是哪台服务器
dig example.com A +nsid
# NSID 选项会返回服务器标识(如果服务器支持)
# 测量查询时间
dig example.com A | grep "Query time"
# ;; Query time: 3 msec4.4 CAA 记录
# CAA (Certification Authority Authorization) — 限制哪些 CA 可以颁发证书
dig example.com CAA +short
# 0 issue "letsencrypt.org"
# 0 issuewild "letsencrypt.org"
# 0 iodef "mailto:security@example.com"
# CAA 字段含义:
# issue: 允许颁发普通证书的 CA
# issuewild: 允许颁发通配符证书的 CA
# iodef: 证书颁发异常时的通知方式
# 没有 CAA 记录 → 任何 CA 都可以颁发证书
# 有 CAA 且不匹配 → CA 必须拒绝颁发
# CA 在颁发证书前会检查 CAA 记录(RFC 8659 要求)五、DNS 报文的 wire format 实测
# 用 tcpdump 抓取 DNS 报文
tcpdump -i eth0 port 53 -vvv -X -c 5
# 典型输出:
# 08:30:01.123456 IP client.54321 > 8.8.8.8.53: 42871+ A? example.com. (29)
# 0x0000: 4500 0039 ... ← IP Header
# 0x0014: ... a7 69 00 35 ... ← UDP Header (54321 → 53)
# 0x001c: a7 a7 01 00 00 01 00 00 00 00 00 00 ← DNS Header
# │ │ │ │ │ │
# ID Flags QD=1 AN=0 NS=0 AR=0
# 0x0028: 07 65 78 61 6d 70 6c 65 ← "example" (7 bytes)
# 0x0030: 03 63 6f 6d 00 ← "com" + root
# 0x0035: 00 01 00 01 ← QTYPE=1(A), QCLASS=1(IN)
# 用 Wireshark 打开 pcap 文件进行深度分析
tcpdump -i eth0 port 53 -w dns.pcap -c 100
# Wireshark 会自动解析 DNS 报文的每个字段# 用 Python 构造并发送原始 DNS 查询
python3 -c "
import socket, struct
# 构造 DNS 查询: example.com A
query = b''
query += struct.pack('!H', 0xABCD) # ID
query += struct.pack('!H', 0x0100) # Flags: RD=1
query += struct.pack('!HHHH', 1,0,0,0) # QDCOUNT=1
# QNAME: example.com
query += b'\x07example\x03com\x00'
query += struct.pack('!HH', 1, 1) # QTYPE=A, QCLASS=IN
# 发送
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(5)
sock.sendto(query, ('8.8.8.8', 53))
resp, _ = sock.recvfrom(4096)
# 解析响应
resp_id = struct.unpack('!H', resp[0:2])[0]
flags = struct.unpack('!H', resp[2:4])[0]
rcode = flags & 0xF
ancount = struct.unpack('!H', resp[6:8])[0]
print(f'Response ID: {resp_id:#06x}')
print(f'RCODE: {rcode} (NOERROR)' if rcode == 0 else f'RCODE: {rcode}')
print(f'Answer count: {ancount}')
print(f'Total response size: {len(resp)} bytes')
sock.close()
"
# 输出:
# Response ID: 0xabcd
# RCODE: 0 (NOERROR)
# Answer count: 1
# Total response size: 56 bytes六、DNS 工程实践要点
6.1 TTL 策略
TTL (Time To Live) 的工程权衡:
TTL 太长 (如 86400 = 24h):
✓ 减少 DNS 查询次数,降低解析延迟
✓ 减轻权威服务器负载
✗ 变更后生效慢(用户看到旧 IP 长达 24 小时)
✗ 故障转移缓慢
适用: NS 记录、不经常变更的记录
TTL 太短 (如 30s):
✓ 变更快速生效
✓ 故障转移快
✗ DNS 查询量大幅增加
✗ 每次访问都要解析 DNS,增加延迟
适用: 需要快速切换的 A/AAAA 记录
推荐策略:
正常运行期: TTL = 300-3600 (5min - 1h)
计划变更前: 提前将 TTL 降至 60-300
故障恢复时: 使用低 TTL 直到稳定
CDN 记录: TTL = 60-300 (CDN 通常管理自己的 TTL)
NS 记录: TTL = 86400-172800 (1-2 天)
6.2 常见 DNS 配置错误
# 错误 1: CNAME 与其他记录共存
# 错误配置:
# example.com CNAME other.com
# example.com MX mail.example.com
# 正确: 根域名不能用 CNAME,改用 A 记录
# 错误 2: MX 指向 CNAME
# 错误配置:
# example.com MX 10 mail.example.com
# mail.example.com CNAME actual-mail.provider.com
# 正确: MX 应该直接指向 A/AAAA 记录的域名
# 错误 3: 缺少反向 DNS (PTR)
# 邮件服务器没有 PTR 记录 → 邮件被大量拒绝
# 检查:
dig -x YOUR_MAIL_SERVER_IP +short
# 应该返回你的邮件服务器域名
# 错误 4: SOA SERIAL 没有递增
# 修改区域文件后忘记更新 SERIAL
# 从服务器看到 SERIAL 没变,不会同步更新
# 检查: dig example.com SOA +short | awk '{print $3}'
# 错误 5: 胶水记录缺失
# NS 指向自己域名下的服务器但没有胶水记录
# 导致解析死循环
# 检查: dig example.com NS +trace
# 如果 trace 中断或超时 → 可能是胶水记录问题
# 错误 6: TTL 不一致
# 同一域名的多个 A 记录 TTL 不同
# 可能导致缓存行为不一致
dig example.com A | grep "IN.*A" | awk '{print $2}'
# 所有 A 记录的 TTL 应该一致6.3 DNS 查询性能优化
# 测量 DNS 解析延迟
# 冷查询(无缓存)
dig @8.8.8.8 rarely-queried-domain.com A | grep "Query time"
# ;; Query time: 45 msec
# 热查询(有缓存)
dig @8.8.8.8 rarely-queried-domain.com A | grep "Query time"
# ;; Query time: 1 msec
# 使用本地 DNS 缓存减少延迟
# systemd-resolved (Ubuntu):
resolvectl statistics
# Current DNS Servers: 8.8.8.8
# Cache: yes (entries: 234)
# Cache Hits: 1523
# Cache Misses: 456
# 使用 nscd 或 dnsmasq 作为本地缓存
# dnsmasq 配置 (/etc/dnsmasq.conf):
# cache-size=10000 # 缓存 10000 条记录
# no-negcache # 不缓存否定响应(可选)
# server=8.8.8.8 # 上游 DNS
# server=1.1.1.1 # 备用上游 DNS
# 预加载常用域名
# 在应用启动时预热 DNS 缓存:
for domain in api.example.com db.example.com cache.example.com; do
dig +short $domain > /dev/null &
done
wait七、结论
DNS 的报文结构看似简单——12 字节 Header 加上几个变长 Section——但它承载了互联网上最关键的名称解析服务。理解 wire format 的价值不在于手工解析报文,而在于你读 tcpdump 输出时能立刻看懂每个字段的含义,用 dig 排查问题时知道每个选项的工程意义。
几个核心要点:
RCODE 是 DNS 故障排查的第一站。 NXDOMAIN 是”域名不存在”,SERVFAIL 是”服务器内部错误”——两者的排查方向完全不同。NXDOMAIN 去检查域名配置,SERVFAIL 去检查递归解析链路和 DNSSEC。
记录类型的选择影响系统架构。 A 记录做 DNS 轮询不感知健康状态,SRV 记录可以带端口和权重,CNAME 不能在根域名使用。选错记录类型会在生产环境中造成意想不到的问题。
EDNS0 不是可选的。 现代 DNS(DNSSEC、大量 TXT 记录)经常产生超过 512 字节的响应。如果你的网络设备或防火墙不支持大于 512 字节的 DNS UDP 报文,DNS 查询会频繁截断和 TCP 回退,严重影响性能。
TTL 是 DNS 最重要的工程参数。 太长则故障切换慢,太短则解析延迟高。没有”最佳 TTL”——只有适合你场景的 TTL 策略。
dig +trace 是排查 DNS 链路问题的终极工具。 它从根域名开始完整追踪解析过程,让你看到每一跳的延迟和响应,精确定位问题出在哪一层。
下一篇我们深入 DNS 解析链路——从浏览器的 Stub Resolver 到递归解析器再到权威服务器,每一层缓存的行为和 TTL 的工程影响。
上一篇:传输层选型决策:TCP vs UDP vs QUIC vs SCTP
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【网络工程】DNS 故障排查实战:从超时到劫持
DNS 故障是最常见也最难排查的网络问题之一。本文系统性地覆盖 DNS 超时、NXDOMAIN、SERVFAIL、劫持四大故障类型的诊断方法,详解 dig、nslookup、drill 的高级用法,提供 DNS 故障的 SRE 应急手册和常见配置错误汇总。
网络工程索引
汇总本站网络工程系列文章,覆盖分层模型、以太网、IP、TCP、DNS、TLS、HTTP/2/3、CDN、BGP 与故障诊断。
【网络工程】CDN 架构原理:PoP、边缘节点与 Origin Shield
系统解剖 CDN 的多层缓存架构——从 DNS 调度到 PoP 内部结构、Origin Shield 回源保护、多 CDN 部署策略。结合实际配置和响应头分析,给出 CDN 架构的工程理解。
【网络工程】全局负载均衡:GSLB、DNS 调度与 Anycast
系统讲解全局负载均衡的工程实现:GeoDNS 的原理与精度问题、Anycast 的路由收敛与故障转移、BGP 社区在流量调度中的应用、多活架构的流量切换策略,建立跨地域流量调度的完整知识体系。