权威服务器上的地址已经改了,客户端为什么还会得到旧地址?一个刚创建的名字,为什么仍被回答为不存在?这两种现象都可能来自缓存,但需要不同的记录和返回码来证明,不能统一归结为“DNS 尚未传播完”。

前置是第 13 篇 UDP 与第 16 篇连接状态。本篇用独立 Unbound 进程查询自建权威服务,比较权威数据、递归缓存和客户端实际结果。实验不修改操作系统 DNS 配置,也不请求公共域名。

查询的是某个名字的某类数据

DNS 不是只保存“域名到一个 IP”的表。一次问题包含名字、类型和类;例如 www.lab.test. IN A 与同名的 AAAA 是两个问题。A 提供 IPv4 地址,RFC 3596 §2定义的 AAAA 提供 IPv6 地址,NS 描述名称服务器,SOA 携带一个区的管理信息。一个名字可以有多种记录,同一类型也可能有多条记录。

名字末尾的点表示从根开始的完整名称。www.lab.test. 的标签按层次组织,但每一级标签不必对应独立服务器或独立管理区。区(zone)是权威管理与数据提供的边界;委派把一部分名字空间交给下一级,父区保留通向下一级的指引。RFC 1034 §4.2说明了区与域的差别。

因此,得到一个 A 记录只能证明此次 DNS 交换返回了该地址,不能证明目标地址可路由、端口在监听、TLS 身份匹配或 HTTP 请求成功。相同名字的地址还可能在不同查询时间、解析器配置与服务策略下不同,本篇只固定一个自建区研究缓存行为。

递归服务与权威回答承担不同职责

客户端可以把获得最终答案的工作交给递归解析器。解析器命中有效缓存时可直接回答;没有可用结果时,则根据配置和已经掌握的信息继续查询。权威服务器提供所管理区的数据,不因为能回答某个名字就必须替客户端解析所有其他名字。

RFC 1034 §4.3.1区分递归请求与非递归查询。RD 表示此次请求希望递归,RA 表示服务器提供递归能力。RA 不是“这一包刚访问了权威服务器”的证据,缓存回答也可以携带 RD、RA。

AA 的含义又不同:RFC 1035 §4.1.1用它表示回答中的权威属性。向权威直查和向递归器查询同一 A 记录,数据相同而标志不同是合理现象。判断来自哪里需要结合目标端口、标志和实际查询日志,不能只读 Answer 一栏。

在迭代查找中,一个服务器可以返回 referral,指出更接近目标名字的权威服务器。解析器据此继续查询,不要求每个上游都替它完成递归。也不要求每次都从根重新开始:已经缓存的委派与地址可能让查找从更接近目标的位置继续。

委派为什么还需要名称服务器的地址

test.lab.test. 委派给 ns.lab.test.。解析器知道下一站的名字,却还没有它的 IP;而查询这个 IP 本身又可能需要访问 lab.test. 的权威服务器,形成启动依赖。父区附带名称服务器地址的 glue 可以提供访问入口。

1
2
3
4
5
6
委派的教学关系,不是本篇实测流量:
test. 的 referral
Authority: lab.test. NS ns.lab.test.
Additional: ns.lab.test. A 192.0.2.53

解析器访问给定的 NS 地址,继续问 www.lab.test. A

glue 中的地址属于名称服务器,不是 www.lab.test. 的最终地址。RFC 9471 §3.1进一步明确,referral 应提供全部可用的 in-domain NS glue;如果报文尺寸限制导致装不下,应设置 TC。它没有把所有 Additional 数据都变成目标名字的权威回答。

本篇的真实实验直接配置 lab.test. 的权威入口,省去根与顶级区的查找。这样可以单独观察缓存变化,但没有验证上述 referral/glue 链。术语中的“递归解析器”描述使用的组件和对客户端提供的服务,不表示本次必然经过三层委派。

TTL 约束缓存,不广播修改事件

TTL 的单位是秒。普通缓存复用期间,剩余 TTL 随已保存时间减少;权威区的修改并不会同时撤销各递归器中已经取得的旧答案。若要解释某次修改的可见时间,至少要知道旧答案何时进入缓存、当时 TTL、解析器缓存政策,以及实际查询了哪个组件。

设教学缓存于 t=10 保存 TTL 为 30 秒的 A 记录,在 t=20 权威数据改成新地址。若没有其他刷新、失效或例外政策,t=25 仍可使用旧记录,剩余 15 秒;到 t=40 才到这份缓存的过期点。把权威中的新 TTL 改成 1 秒,不会追溯改写缓存已经记录的旧过期点。

1
2
3
4
stored_at, ttl = 10, 30
assert stored_at + ttl == 40
assert max(0, stored_at + ttl - 25) == 15
assert max(0, stored_at + ttl - 41) == 0

这个手算固定了无预取、无主动清理、无过期服务等前提。不能由它推出“DNS TTL 一到,所有实现都绝不返回旧数据”。RFC 8767 §4–5允许在无法权威刷新等条件下提供过期数据,并规定对应处理。本次显式关闭该功能和预取,使实验只研究基础缓存复用及到期重查。

不存在的答案也可以被缓存

NXDOMAIN 表示名字不存在;NODATA 描述名字存在、但没有所问类型数据的情况。NODATA 没有独立的 RCODE,通常表现为 NOERROR 且没有相关答案,必须结合其他区段判断;referral 的 Answer 也可能为空。因此,dig +short 没有输出不足以区分这几种情况。RFC 2308 §2给出了区分方法。

权威负回答中的 SOA 为负缓存提供时限。按 RFC 2308 §3–5,负 TTL 取 SOA 记录 TTL 与 SOA.MINIMUM 的较小值;不能继续把 MINIMUM 当作区内所有记录的统一最小 TTL。NXDOMAIN 的基础缓存键按名字和类区分,NODATA 还需要区分查询类型,避免把“没有 AAAA”误当作“连 A 也不存在”。

如果某名字在负缓存有效期内才被创建,递归器仍可能返回已经缓存的 NXDOMAIN。此时直查权威得到 A,而查询递归器得到 NXDOMAIN,两者反映不同状态。必须等相关负缓存失效或明确清理相应缓存,才能在这组基础配置下触发重新取数;频繁重复相同查询不必产生新的上游请求。

独立解析器和本地权威区

实验采用已安装的 Unbound,另用 Python 标准库实现受限权威服务,只服务合成的 lab.test. 区。RFC 2606 §2保留 .test 用于测试。返回的合成地址只作为 DNS 数据比较,不向这些地址建立业务连接。

本次使用 Unbound 1.24.0。脚本生成临时配置,为解析器和权威服务分别选择回环高端口,启用 module-config: "iterator"do-not-query-localhost: no,关闭 prefetchserve-expired。权威 A 记录 TTL 为 3 秒,SOA TTL 和 MINIMUM 也均为 3 秒,解析器负缓存上限显式设为 3 秒。

Unbound 对 .test 有默认本地区处理。本实验使用 test.nodefault 和更具体的 lab.test. 透明区,允许这个自建区进入 stub 查询路径;根本地区配置为 refuse,越界查询另做拒绝对照。stub-prime: nostub-first: no 禁用 stub 初始化探查与失败后改走普通解析的后备路径,出站接口限定回环。完整配置与 unbound-checkconf 结果保存在记录中。

Unbound 官方配置手册区分 stub-zoneforward-zone:前者指定权威入口,后者把后续解析交给另一个递归器。这里配置的是 stub,权威服务不承担额外递归。手册中的选项还与本机版本和实际配置检查结果对照,不从网站“最新版本”推断本机功能。

所有客户端命令显式指定回环地址和实验端口。新的独立 Unbound 进程没有继承系统 DNS 缓存,权威端记录收到的问题与返回值。缓存命中判断采用“结果保持且该查询没有新增上游请求”的联合证据,不以查询耗时接近零代替。

权威已修改,缓存仍可回答旧结果

2026 年 9 月 20 日的记录中,权威端口为 65500,递归器端口为 64686。端口由当次运行选择,复现时应读取新的配置,不能照抄这两个数。下表计数包含所有权威请求,包括额外的直接权威检查;每行只比较该次递归查询前后,不能把行间增加全算作缓存未命中。

阶段 递归器返回 权威请求数:前 → 后
正缓存首次查询 192.0.2.1 1 → 2
立即重复 192.0.2.1 2 → 2
权威改为 .2 后、缓存仍有效 192.0.2.1 3 → 3
正缓存到期后 192.0.2.2 3 → 4
不存在名字首次查询 NXDOMAIN 4 → 5
立即重复不存在名字 NXDOMAIN 5 → 5
权威创建 .3 后、负缓存仍有效 NXDOMAIN 6 → 6
负缓存到期后 192.0.2.3 6 → 7

修改正记录后,脚本先直接查询权威,已经得到 192.0.2.2,才查询递归器并得到旧的 .1。创建 missing.lab.test. 后也先直查得到 .3,再观察递归器继续回答 NXDOMAIN。这两次直接查询分别使计数增加到 3 和 6,排除了“权威数据其实还没改”的解释。

正负缓存各自的到期查询均在首次查询完成后等待超过 4.1 秒再执行。立即重复查询的 TTL 仍显示 3,本次没有按秒采样观察 TTL 的逐步递减;实测证据是到期前保持旧结果且未新增权威请求,到期后新增请求并取得新结果。命令中的毫秒耗时不用于性能比较。

对已有 A 的 www.lab.test. 查询 AAAA,得到 NOERROR、零条 Answer 和一条 SOA,符合这组权威数据的 NODATA 情况。随后查询 A 仍得到 .2。原始日志在 AAAA 阶段还记录了一次额外 A 查询,随后的 A 查询也再次到达权威;这里不推断额外查询的具体实现原因,也不把这个对照写成“A 缓存一直未变”的证明。

直查权威的正常回答带 AA,递归器的正常缓存及回源回答带 RD、RA 而不带 AA。初始 TCP 直查保留了 dig 默认的 RD,权威未提供 RA,因此出现“请求递归但不可用”的提示;其 A 回答仍完整返回。后续直查显式使用 +norecurse。越界查询由递归器直接回答 REFUSED,权威计数保持 10;直接向权威查询越界名字也得到 REFUSED。

附件保留权威端收到和发送的 DNS 消息十六进制,以及 dig 完整输出。这是应用消息日志,不是 IP 抓包。父级独立复跑也观察到上述缓存转换;退出记录确认实验进程、线程与临时目录已清理。

复现与传输边界

附件提供完整脚本查询与权威日志环境和限制。在已有 Unbound、配置检查工具与 dig 的机器上运行,脚本不安装依赖、不修改系统 DNS,也不复用常驻解析服务的缓存。

1
2
3
python3 lab.py --help
python3 lab.py --self-check
python3 lab.py > local-run.jsonl

脚本每次启动新的进程和临时目录,运行完后停止自己创建的服务并清理临时配置。客户端调用的是 /usr/bin/dig,Unbound 与检查工具通过当前 PATH 查找;命令不存在会失败,不会自动下载替代品。TTL 等待使用单调时钟,三秒 TTL 后额外留出余量再查询,不能把这些人为等待写成解析性能。

DNS 不能概括为“只使用 UDP”。RFC 7766 §5要求通用实现支持 UDP 和 TCP,允许根据运行条件直接使用 TCP;TCP 不只是区域传送专用协议。DNS over TCP 另外使用两字节消息长度,第 02 篇的字节流分帧问题仍然适用。

本篇只验证实际执行的短响应和查询模式,不据此声称覆盖截断后回退、连接复用、流水线、EDNS 或完整权威服务器功能。DNSSEC 的数据真实性、DoT/DoH 的传输加密及递归端可见范围留到选修 E03;当前实验关闭 DNSSEC 验证,不能把成功返回 A 记录说成身份已经可信。

练习与证据边界

某正记录 TTL 为 60 秒,于 t=100 进入缓存,权威在 t=110 修改。按本篇无预取、无过期服务的教学条件,t=130 能否仍返回旧数据?可以,此时剩余 30 秒。若连缓存写入时刻也不知道,就不能仅凭权威修改时间计算所有客户端的可见时刻。

SOA 记录 TTL 为 20 秒、MINIMUM 为 5 秒,基础负 TTL 应是多少?取较小值得 5 秒。若对已有 A 的名字查询 AAAA 得到 NOERROR、无相关答案且包含 SOA,不能报告为 NXDOMAIN,更不能据此删除 A 类型的有效缓存。

查询返回旧地址且耗时很短,是否足以证明命中实验 Unbound?不足。还要排除查询了别的端口、客户端自有缓存及权威仍返回旧值。固定目标、直查对照、权威请求日志和递归回答共同限定了本次结论。

这里完成的是本机独立解析器与受限权威区之间的真实 DNS 交换。没有真实根/TLD 委派链、区域传送、RFC 2136 动态更新、DNSSEC 验证、公共解析器或生产变更传播实验;权威数据由测试程序直接修改。构建与附件检查仅证明文章交付可用。

一手资料

本篇一手资料在写作时核查。基础查询与权威语义依据 RFC 1034/1035,IPv6 地址记录依据 RFC 3596,负缓存依据 RFC 2308。RFC 9471、8767、7766 分别补充 glue、过期服务和传输要求,Unbound 配置另查官方手册。链接位于对应段落;实际版本、完整配置与原始命令输出保存在附件。