土法炼钢兴趣小组的算法知识备份

【网络工程】DNS 协议解剖:查询格式、记录类型与响应码

文章导航

分类入口
network
标签入口
#dns#protocol#record-types#dig#edns0

目录

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 记录通常设置较长的 TTL

2.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)的缓存时间
#          注意: 这不是正常记录的默认 TTL

2.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 Section

4.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-5ms

4.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 msec

4.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 排查问题时知道每个选项的工程意义。

几个核心要点:

  1. RCODE 是 DNS 故障排查的第一站。 NXDOMAIN 是”域名不存在”,SERVFAIL 是”服务器内部错误”——两者的排查方向完全不同。NXDOMAIN 去检查域名配置,SERVFAIL 去检查递归解析链路和 DNSSEC。

  2. 记录类型的选择影响系统架构。 A 记录做 DNS 轮询不感知健康状态,SRV 记录可以带端口和权重,CNAME 不能在根域名使用。选错记录类型会在生产环境中造成意想不到的问题。

  3. EDNS0 不是可选的。 现代 DNS(DNSSEC、大量 TXT 记录)经常产生超过 512 字节的响应。如果你的网络设备或防火墙不支持大于 512 字节的 DNS UDP 报文,DNS 查询会频繁截断和 TCP 回退,严重影响性能。

  4. TTL 是 DNS 最重要的工程参数。 太长则故障切换慢,太短则解析延迟高。没有”最佳 TTL”——只有适合你场景的 TTL 策略。

  5. dig +trace 是排查 DNS 链路问题的终极工具。 它从根域名开始完整追踪解析过程,让你看到每一跳的延迟和响应,精确定位问题出在哪一层。

下一篇我们深入 DNS 解析链路——从浏览器的 Stub Resolver 到递归解析器再到权威服务器,每一层缓存的行为和 TTL 的工程影响。


上一篇:传输层选型决策:TCP vs UDP vs QUIC vs SCTP

下一篇:DNS 解析链路:递归、迭代与缓存层级

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2025-08-01 · network

【网络工程】DNS 故障排查实战:从超时到劫持

DNS 故障是最常见也最难排查的网络问题之一。本文系统性地覆盖 DNS 超时、NXDOMAIN、SERVFAIL、劫持四大故障类型的诊断方法,详解 dig、nslookup、drill 的高级用法,提供 DNS 故障的 SRE 应急手册和常见配置错误汇总。

2026-04-22 · network

网络工程索引

汇总本站网络工程系列文章,覆盖分层模型、以太网、IP、TCP、DNS、TLS、HTTP/2/3、CDN、BGP 与故障诊断。

2025-08-22 · network

【网络工程】全局负载均衡:GSLB、DNS 调度与 Anycast

系统讲解全局负载均衡的工程实现:GeoDNS 的原理与精度问题、Anycast 的路由收敛与故障转移、BGP 社区在流量调度中的应用、多活架构的流量切换策略,建立跨地域流量调度的完整知识体系。


By .