系统设计 / DNS · 层级、委任、缓存,以及不存在的「浸透」 / 权威之间的一致性:SOA 与 zone transfer 待审核 3 / 11
SOA · SERIAL · AXFR / IXFR

权威之间的一致性:SOA 与 zone transfer

一个 zone 通常有两台以上权威服务器,理由是可用性:单点故障不该让整个域名消失。而协议同时要求它们的应答完全一致(RFC 2181 §4)——同一个查询问哪台都得到同样的记录。

这两个要求之间需要一套同步机制。DNS 给的是:一个版本号,加一个拉取协议。

1 · SOA:zone 的元数据

zone 顶点上那条 SOA(Start of Authority)记录,RDATA 有七个字段。前两个是名字,后五个是数字,各管一件事:

图 1-1 · SOA 七字段的作用与它们决定的时间界限。可调各计时器观察否定应答 TTL 的实际生效值,以及计时器之间的合理关系检查。

其中 MINIMUM 这个字段名是历史包袱。RFC 1035 定义它为「本 zone 所有 RR 的 TTL 下限」,而 RFC 2308 §4 明确废弃了那个含义——原话是那个用法「在实践中从未被使用,据此废弃」——只留下「否定应答的 TTL」这一条。

警示 · 即便只按新含义读,生效值也不是 MINIMUM 本身。RFC 2308 §3 规定否定应答的 TTL 取 MINIMUM 与否定应答里那条 SOA 记录自身 TTL 的较小者。于是把 SOA 的 TTL 设成 60 秒,否定缓存就是 60 秒,MINIMUM 写 3600 也不起作用。查「为什么写了记录还是查不到、要等多久」这类问题时,两个数都得看。

MNAME 指向 primary。它的实际用途有两处:NOTIFY 的发出方按它判断该通知谁(自己不通知自己),dynamic update 的请求也按它转发。RNAME 是管理员邮箱写成域名形式,dnsadm.vega.dev.dnsadm@vega.dev——local part 里若有点,得写成 \.,否则会被当成 label 分隔符。

2 · SERIAL 是环上的位置

SERIAL 是 32 位无符号数,secondary 靠比较它决定是否需要拉取。但它不是计数器:从 4294967295 加 1 回到 0,而 secondary 必须仍然认为这是「更新了」。

RFC 1982 因此给出一套序列号空间的算术:

s1<s2    s1s2[(i1<i2i2i1<231)(i1>i2i1i2>231)]s_1 < s_2 \iff s_1 \ne s_2 \land \big[(i_1 < i_2 \land i_2 - i_1 < 2^{31}) \lor (i_1 > i_2 \land i_1 - i_2 > 2^{31})\big]

这个定义有一处刻意留白:当 i1i2=231|i_1 - i_2| = 2^{31} 时,两个值既不大于也不小于对方。RFC 1982 §3.2 明确列出这种情形,理由不是实现偷懒,而是若强行给它定序,就会出现 s1<s2s_1 < s_2s1+1>s2+1s_1 + 1 > s_2 + 1——「序」这个概念自身就不自洽了。

图 2-1 · 序列号比较的六种情形与 secondary 的判定。可直接改两个数值;「差恰为 2³¹」那一档给出的是「无定义」而非「相等」。

把无定义实现成「相等」是个有后果的错误。相等的含义是「数据一致,不必传送」,而无从比较的含义是「状态已经坏了」——后者必须人工把 serial 拉到一个明确更大的值上,前者什么都不用做。

由此还推出一条运维约束:单次(或 EXPIRE 期间累计)的增量不得超过 23112^{31} - 1。日常写法 YYYYMMDDnn 离这个上限很远,真正会越界的是误写——手抖多打一位数字,serial 被顶到环的另一侧,此后所有比较都要靠上面这套算术才不出错。这个写法自身的边界是每天 100 次:序号用到 99 就只能借第二天的日期。

3 · 触发路径与传送方式

图 3-1 · 一次 zone transfer 的完整时序。可切换触发方式与传法,观察 IXFR 相对 AXFR 省下的数据量。

触发有两条路径。REFRESH 到期是 secondary 主动轮询,这是基线机制,不依赖 primary 做任何事。NOTIFY(RFC 1996)是 primary 在数据变更后主动通知各 secondary。

两种传法:AXFR(RFC 5936)送整个 zone,IXFR(RFC 1995)只送两个版本之间的差分。IXFR 请求可以得到 AXFR 应答——这是合法的退化,发生在 primary 没保留足够历史时。

注 · NOTIFY 是整套 DNS 里唯一带 push 味道的机制,但它只是一个提示,不携带任何数据。收到之后 secondary 仍要自己去查 SOA、自己比 serial、自己发起传送;而且它不被信任——伪造一个 NOTIFY 最多让 secondary 白跑一趟去问 primary,问不出更新就什么也不做。所以即便把 NOTIFY 算作 push,数据的流向仍然是 pull。这一点在第 9 讲还要用到。

传送走 TCP。原因很直接:一个 zone 装不进单个 UDP 报文,而 AXFR 需要一个有序、可靠、可以持续多个报文的流。这也是 DNS 从一开始就同时用 53/udp 与 53/tcp 的原因之一。

4 · 谁能拉走整个 zone

AXFR 把 zone 的全部内容交出去,所以它必须限制来源。默认对全世界开放 AXFR 曾是常见配置错误,后果是把内部主机名清单、命名规律、乃至未公开的服务端点整份泄露出去。

现在的做法有三层:按 IP 限制(allow-transfer)、用 TSIG(RFC 8945)做共享密钥认证、以及在网络层限制 53/tcp 的可达范围。TSIG 是三者中唯一提供认证的手段——按 IP 限制拦不住源地址伪造,尽管 TCP 的三次握手已经让伪造 AXFR 请求比伪造 UDP 查询困难得多。

值得一提的是这与第 7 讲的 DNSSEC 是两件不同的事,且互不替代:DNSSEC 让应答可验证,但它不提供机密性,也不管谁有权拉走 zone。RFC 4033 §4 把这一点写得很明白。

5 · 一处实测

写本页的模拟器时踩到一个与直觉相反的地方:递归服务器缓存的委任 NS,TTL 取自父 zone 那一份,不是子 zone 顶点那一份。

本系列的 fixture 里,vega.dev 顶点自己的 NS 写着 TTL 600,而父 zone dev 里那组委任 NS 写着 10800。测试原本断言「600 秒后委任过期、解析要退回上一级」,实测没有——缓存里那份是 10800 的。

这件事的运维含义比它看起来重要:第 9 讲会讲到「换权威服务器前先把 NS 的 TTL 调短」,而要调的是父 zone / 委任元那一份。只把子 zone 顶点的 NS TTL 改小是不够的:在解析器只走过委任、尚未从子 zone 取回权威 NS 之前,缓存里用的一直是父那份 TTL。稳妥的做法是父子两份都提前调短。

6 · 参考文献

  1. Elz, R., & Bush, R. (1997). Clarifications to the DNS Specification (RFC 2181). IETF. §4 要求同一 zone 的各权威服务器应答一致。
  2. Elz, R., & Bush, R. (1996). Serial Number Arithmetic (RFC 1982). IETF. §3.2 是比较的定义与「无定义」那一档。
  3. Andrews, M. (1998). Negative Caching of DNS Queries (DNS NCACHE) (RFC 2308). IETF. §3 是否定应答 TTL 的取值,§4 废弃 MINIMUM 的原义。
  4. Vixie, P. (1996). A Mechanism for Prompt Notification of Zone Changes (DNS NOTIFY) (RFC 1996). IETF.
  5. Ohta, M. (1996). Incremental Zone Transfer in DNS (RFC 1995). IETF.
  6. Lewis, E., & Hoenes, A. (Eds.). (2010). DNS Zone Transfer Protocol (AXFR) (RFC 5936). IETF.
  7. Barr, D. (1996). Common DNS Operational and Configuration Errors (RFC 1912). IETF. §2.2 是 YYYYMMDDnn 写法与各计时器取值的建议。