DNS 协议与解析

本页目录

修改域名 A 记录后,有的用户立刻看到新地址,有的仍访问旧地址。通常不是 DNS 在所有服务器之间缓慢广播一次更新,而是权威数据、递归缓存、系统与应用缓存各有状态。理解一次解析,需要区分谁负责数据、谁代为查询、谁缓存了哪份结果,以及响应到底是“不存在”还是“查询失败”。

问题本篇入口
root、递归与权威各做什么委派和查询角色
有 NS 为何还要 glue解析权威服务器地址的启动依赖
DNS 包怎样读12 B 头、区段、标签和压缩指针
修改后何时生效RRset TTL、负缓存与分层缓存
大应答为什么需要 TCPEDNS 通告、路径 MTU 与截断
次级服务器怎样同步AXFR/IXFR 与递归缓存的区别

命名空间、域与区域

DNS 名称由层级标签组成,以根标签结束;www.example.com. 末尾的点明确表示绝对名称。域描述名字树中的子树,zone 则是某组权威服务器管理的数据范围;父区把子域委派出去后,父区仍保有委派信息,但不再权威管理子区全部记录。

DNS 也不只返回地址:MX 表达邮件交换目标,TXT 承载应用约定信息,SRV 表达服务位置等。每组相同 owner name、type、class 的记录构成 RRset,这比“一个域名对应一个 IP”的模型更适合解释缓存、签名和多地址。RFC 1034:DNS 概念

stub、转发器、递归器和权威

递归器分别访问权威层级;权威服务器不会逐层替它转发 客户端 / stub 请求最终答案 递归解析器 缓存、迭代、验证 根区服务器 返回 com. 委派 com. 服务器 返回 example.com. 委派 example.com. 权威 返回目标 RRset 冷缓存概念流程 有缓存时可跳过部分步骤 转发器则交给上游递归器 各条连线表示相应查询/响应事务;实际还可能需要解析 NS 地址、追踪别名或执行 DNSSEC 验证。

stub 通常把查询交给配置的解析服务。转发器将工作交给上游,递归器可自行追踪委派,也可在部分策略下转发;权威服务器回答自己负责的区。一个软件可以承担多个角色,不能把某产品名称直接等同于一种固定架构。

RD 表示客户端请求递归,RA 表示服务端支持递归,并不保证每条 RD=1 查询都被允许递归。冷缓存递归器可从根提示信息启动,再根据 referral 寻找下一组服务器;已有委派或答案缓存时,不必每次都问根。

权威层级之间不通常为本次查询逐层转发请求,实际发起多次查询的是递归器。它还可能因 NS 地址、别名和验证需要产生额外查询,不能固定说每次总共恰好三次,也不应把未经测量的“数十次”当普遍开销。

QNAME 最小化改变向上级暴露的信息

传统迭代可能把完整目标名称交给各级服务器;QNAME minimisation 尽量只向当前层级询问推进委派所需的名称部分。例如上级不一定需要知道完整的 www.example.com.。具体 QTYPE 和回退流程按算法处理,不能把一张简图中的 NS 查询当作全部实现唯一线上序列。RFC 9156:QNAME 最小化

glue 解开的是地址依赖

若 example.com. 的 NS 是 ns1.example.com.,解析器想问子区前先要找到这台 NS 的地址,而地址本身又在子区。父区可提供 glue 地址帮助启动。它与从真正权威处取得并验证的地址 RRset 并非同一信任层级。

Additional 区并不意味着内容可无条件缓存;解析器要做相关性与 bailiwick 等检查。NS 位于区外时,可能需要另行解析其地址;不是每次 referral 都带全部 NS 的 A/AAAA,也不是所有 Additional 地址都叫 glue。

报文结构:先定位区段,再解析记录

DNS 消息基本头为 12 B,含 ID、Flags 及 Question/Answer/Authority/Additional 四个计数。TCP 承载还在每条 DNS 消息前放两字节长度前缀,该前缀不计入 DNS 消息偏移;TCP 是字节流,一次读操作不保证正好得到一条完整消息。RFC 1035:消息格式、RFC 7766:DNS over TCP

Flags 掩码意义
0x8000QR:查询或响应
0x7800OPCODE 字段,需移位解释
0x0400AA:权威回答标志
0x0200TC:截断
0x0100 / 0x0080RD / RA
0x0020 / 0x0010AD / CD,DNSSEC 相关语义
0x000f基础 RCODE,EDNS 可扩展

历史图中的三位 Z 不能在现代报文中统一当作三个必须为零的位,AD/CD 已有用途。位图从左数与代码最低有效位编号也不同,使用掩码有助于避免颠倒。ID 匹配只是关联响应的条件之一,还需核对端点、问题及上下文。

Question 含 QNAME、QTYPE、QCLASS;普通资源记录含 NAME、TYPE、CLASS、TTL、RDLENGTH、RDATA。RDATA 按 TYPE 解释,不能将所有类型都当作一个字符串或一个可压缩域名。

标签与压缩:完整的 33 → 49 B 算例

www.example.com. 的线上标签是长度 3 + www、长度 7 + example、长度 3 + com,最后一个零长度根标签,共 17 B。加 12 B 头、2 B QTYPE、2 B QCLASS,单问题 A 查询为 33 B。

如果响应包含同名 A 记录,可用 c0 0c 指向 DNS 消息起点偏移 12 的 QNAME。记录自身需要 2 B NAME 指针、10 B 固定字段和 4 B IPv4 地址,共 16 B;保留同一问题后响应总计 49 B。不压缩 NAME 则需 17+10+4=31 B,此处实际节省 15 B,不能凭名称重复次数随意估算。RFC 1035:名称压缩

普通标签最长 63 B,完整线上名称有总长限制;压缩指针高两位为 11,低 14 位是相对整条 DNS 消息的偏移。解析器要检查越界、循环和展开长度,不能把攻击者给的指针无限追下去。TCP 长度前缀不改变 c00c 所指的 DNS 内部位置。

指针跳转后,记录的游标停在哪里?

c00c 从33跳到12去展开名称,但 Answer 的 TYPE 仍从35开始。切换为指向33自身的指针,读取位置进入已访问集合,解析必须拒绝;不能靠“再多读几步”碰运气结束。

正在呈现知识画面
指针跳转后,记录的游标停在哪里?

压缩指针占原位置两字节,但展开可读取远处标签。递归读取名称的位置不等于原记录后续字段的位置;异常指针必须终止。

记录类型与解析依赖

类型读法常见误区
A / AAAAIPv4 / IPv6 地址没有 AAAA 不代表整个名字不存在
CNAME当前 owner 是另一名字的别名通常不能在同 owner 同时配置普通 A/MX 等数据
NS指向权威服务器名字NS 目标是名字,不是直接写 IP 的位置
MXpreference 越低越优先,后跟邮件目标名不保证最低值目标永远可用
SOA区管理、序列号及同步/负缓存相关字段MINIMUM 不是所有正记录的统一 TTL
PTR在反向名称空间查询目标名与历史 IQUERY opcode 不是同一机制
SRVpriority、weight、port、targetpriority 与 weight 不可混作同一排序数字
TXT一个或多个有长度的字符串单个字符串限制不等于整个 RR 永远只能 255 B
CAA约束证书签发授权的 DNS 数据不加密 DNS,也不直接验证正在展示的证书

IPv4 反查如 192.0.2.10 查询 10.2.0.192.in-addr.arpa. 的 PTR,是普通标准查询。PTR 与正向 A/AAAA 没有自动一一对应保证,不能只靠反查名称认证主机身份。

CNAME 追踪可能再触发目标查询,别名与目标 RRset 各有 TTL;普通 CNAME 在区顶点会与必须存在的 SOA/NS 冲突。托管平台的 ALIAS/flattening 行为不是把普通线上 CNAME 共存规则取消了。

缓存:正结果、负结果与失败分开

TTL 是各缓存副本的剩余寿命,不是全球统一切换倒计时 t=0 t=100 t=200 t=300 s 递归器缓存旧 A 初始 TTL=300 s 权威改为新 A 旧缓存仍余 200 s 旧副本到期 下次请求可重新获取 其他缓存的取数时刻不同;降低新 TTL 不会追溯缩短已发出的旧 TTL,serve-stale 又有明确例外。

TTL 控制缓存副本能在相应规则下使用多久,不通知已有连接切换地址。递归器在 t=0 缓存 TTL=300 的旧 A,权威 t=100 改值,旧副本可能仍用到 t=300。此时才把新记录 TTL 改成 30,不会追溯修改已经发出的那份 300。

迁移前降低 TTL 需要留出旧缓存自然到期的时间;实际用户路径还可能有应用缓存、负缓存和不同视图。部分解析器按 serve-stale 机制在特定故障条件下提供过期数据,这需要明确策略,不能笼统视为所有 TTL 都失效。RFC 8767:serve-stale

响应含义缓存解释
NXDOMAIN,RCODE=3查询名称不存在名称层面的否定结果,需结合别名等上下文
NOERROR 但目标类型无数据常称 NODATA,不是独立 RCODE名称存在但该类型缺失,需结合 SOA 等判读
SERVFAIL,RCODE=2无法完成,例如上游/验证失败不能当作权威证明名称不存在
REFUSED,RCODE=5服务端策略拒绝不说明域名本身是否存在

常规负缓存 TTL 取响应 SOA 的 TTL 与 SOA.MINIMUM 的较小值,解析器还可能施加本地限制。若两者为 600 与 300,则负缓存基础值 300 s;刚创建原本不存在的记录,也可能被已有负缓存暂时遮住。RFC 2308:负缓存

权威更新,为什么缓存没有同步变化?

在 t=140,旧副本还有160秒,新副本只剩10秒;较新不意味着更晚过期。t=150之后第二份副本不可再按普通TTL规则使用,却也不代表第一份副本同时失效。

正在呈现知识画面
权威更新,为什么缓存没有同步变化?

TTL 附着在取得的副本上。权威修改内容和新 TTL,不会追溯重写旧缓存的到期时刻。

EDNS、截断与路径大小

传统无 EDNS 的 DNS/UDP 消息上限是 512 B。EDNS 使用 Additional 中的 OPT 伪记录表达能力:TYPE=41,CLASS 用于通告 UDP 载荷大小,原 TTL 位置承载扩展 RCODE、版本与标志。它不是普通可缓存的域名 RR,DO 也不等于验证成功。RFC 6891:EDNS

通告能接收 4096 B 不证明路径无分片地承载 4096 B。1232 B 的一个预算依据是 IPv6 最小 MTU 1280−40 B 基本头−8 B UDP 头;若有更多开销仍需考虑。响应超出适用 UDP 限额可截断并置 TC,客户端据此使用 TCP 等适当承载重新取得完整结果。

TCP 不只是区域传送的专用通道,也是普通查询必须支持的重要承载。过滤 TCP 53 可能让小查询看似正常、大应答失败;单纯调大 EDNS 值反而可能引入 IP 分片问题。RFC 7766

DNSSEC 验证数据来源与完整性,加密 DNS 保护所选传输链路,二者解决不同问题。AA 不是密码学认证,AD 的可信度也依赖本机与所用解析器的信任关系,下一篇继续展开。

区域传送:同步权威副本,不更新全球递归缓存

AXFR 是完整区传送,可跨多条 DNS 消息,通过 TCP 承载并由 SOA 等规则界定传送内容。IXFR 依据序列号请求增量,服务器没有合适差量时可能退回完整传送。序列号比较有自己的回绕规则,不能永远按普通整数大小判断新旧。RFC 5936:AXFR

primary/secondary 完成同步只说明权威副本更新,不能强制互联网递归器清空尚未过期缓存。NOTIFY 可提示相应次级服务器检查变化,也不等于全网缓存失效广播。

本地构造一问一答

import struct
from ipaddress import IPv4Address

def encode_name(name):
    labels = name.rstrip('.').split('.')
    result = b''.join(bytes([len(x.encode('ascii'))])+x.encode('ascii') for x in labels)+bytes(1)
    assert all(0<len(x.encode('ascii'))<=63 for x in labels)
    assert len(result)<=255
    return result
qname = encode_name('www.example.com.')
question = qname+struct.pack('!HH',1,1)
query = struct.pack('!6H',0x1234,0x0100,1,0,0,0)+question
assert len(qname)==17 and len(query)==33
answer = bytes.fromhex('c00c')+struct.pack('!HHIH',1,1,300,4)+IPv4Address('192.0.2.10').packed
response = struct.pack('!6H',0x1234,0x8180,1,1,0,0)+question+answer
assert len(answer)==16 and len(response)==49
pointer = int.from_bytes(answer[:2],'big')&0x3fff
assert pointer==12 and response[pointer:pointer+17]==qname
flags = struct.unpack('!H',response[2:4])[0]
assert flags&0x8000 and flags&0x0080 and not flags&0x0400
assert 1280-40-8==1232
assert min(600,300)==300
assert max(0,300-100)==200
assert IPv4Address('192.0.2.10').reverse_pointer=='10.2.0.192.in-addr.arpa'
print('DNS 查询/响应长度、压缩偏移、Flags、负缓存与 EDNS 预算通过')

例子生成固定教学 A 应答,不是完整 DNS 解析器或缓存实现;所有地址均用于文档示例,未查询或修改真实域名。

继续查阅