Old-School Networking / 2026 ・ No.09
古法网络入门
第9讲 DNS ── 从名字查地址,互联网的电话簿
发布
到第 8 讲为止,我们都用 --resolve 告诉 curl“expenses.example 就是 203.0.113.20”。这一讲不再这样做,而是让真正的机制 DNS 来查名字。
为了练习,我们搭建了根、.example、expenses.example 三层 DNS 服务器,以及扮演运营商的解析器。沿名字树追溯的过程、记住答案的时间(TTL)、与 hosts 的不一致,以及 DNS 停止时的症状,都用实测确认。
名字这一层
新井上一讲的 --resolve,实际上是怎么从名字知道地址的?
古井并没有一台机器知道全世界的名字。名字按树的形状划分,每个分支有各自的负责人。不知道就介绍“去问那边”,一路追下去。
新井这不是踢皮球吗?
古井对,是设计得很好的踢皮球。而且踢过一次的答案,会记住一段时间。
全 12 讲路线图(当前:第 9 讲)
- 名字树 ── 根、.example、expenses.example,每层都有负责人
- 解析器 ── 代替电脑沿树追溯。第一次用 0.166 秒(实测)
- TTL 与缓存 ── 记住期间 0 毫秒,过期就重新问(实测)
- 开发者的陷阱 ── hosts 与 dig 的不一致,DNS 停止时的症状(实测)
按 TCP/IP 的分层,DNS 是第 7 层、应用层的机制。不过它总是先于应用的通信运行,所以放在这里讲。练习网络把电脑和运营商的解析器(Unbound)放在线的这一侧,把三层 DNS 服务器(NSD)和申请服务器放在单程 20 毫秒的捣乱线那一侧。
名字树
名字按树的形状划分,每个分支有负责的 DNS 服务器
www.expenses.example 要从右往左读。最右边看不见的点是根,下面是 example,再下面是 expenses,最后是 www。根的 DNS 服务器知道 example 的负责人,example 的负责人知道 expenses 的负责人。这种“知道负责人”的关系叫委派。
在真实的互联网上,根有从 a 到 m 的 13 个名字,由 12 个组织运营。实际上,内容相同的副本在全世界放了一千台以上,由就近的一台回答。本讲的练习中,根也是笔者自己做的(a.root.lab,203.0.113.1)。
解析器走的路
电脑自己并不沿树追溯。它只向家用路由器或运营商准备的解析器(缓存 DNS 服务器)问一次“expenses.example 的地址是?”。我们在解析器一侧做了记录。
# 在解析器上记录:为了电脑的一个查询,解析器依次问了 3 个地方(在单程 20 ms 的线那边) 0.000 PC → resolver A expenses.example 0.000 resolver → root NS <Root> 0.041 root → resolver NS <Root> NS a.root.lab A 203.0.113.1 0.042 resolver → root A expenses.example 0.082 root → resolver A expenses.example NS ns1.example A 203.0.113.2 0.083 resolver → .example A expenses.example 0.124 .example → resolver A expenses.example NS ns1.expenses.example A 203.0.113.3 0.124 resolver → expenses.example A expenses.example 0.165 expenses.example → resolver A expenses.example A 203.0.113.20 NS ns1.expenses.example A 203.0.113.3 0.166 resolver → PC A expenses.example A 203.0.113.20
电脑只问一次,追溯的是解析器(实测时刻)
解析器先确认根是否在线(根的 NS),再问根。根的回答是介绍:“不知道。.example 去问 ns1.example(203.0.113.2)”(封面照片)。.example 同样介绍了 expenses.example,最后 expenses.example 的 DNS 服务器回答 203.0.113.20。在线上走了 4 个往返,用时 0.166 秒。
把同样的路从电脑开始依次走给我们看的,是 dig +trace。
# dig +trace:电脑自己从根开始依次追溯(← 是回答的 DNS 服务器)
pc$ dig +trace expenses.example
. 86368 IN NS a.root.lab.
← 198.51.100.53 (198.51.100.53)
example. 86400 IN NS ns1.example.
← a.root.lab (203.0.113.1)
expenses.example. 3600 IN NS ns1.expenses.example.
← ns1.example (203.0.113.2)
expenses.example. 30 IN A 203.0.113.20
expenses.example. 3600 IN NS ns1.expenses.example.
← ns1.expenses.example (203.0.113.3)
很多真正的解析器只向根透露“example”、只向 .example 透露“expenses.example”,按需露出名字来询问(QNAME minimisation)。本讲为了让流程更清楚,关掉了这个设置。
记住:TTL 与缓存
如果每次都花 0.166 秒沿树追溯,网页就慢得受不了。所以解析器会记住答案。能记多久,由答案附带的 TTL(秒)决定。为了练习,expenses.example 的地址设为 30 秒。
# 电脑向解析器(198.51.100.53)查询。答案的 TTL(还能记住几秒)与耗时
pc$ dig expenses.example @198.51.100.53
expenses.example. 30 IN A 203.0.113.20 168 ms ← 第一次
expenses.example. 29 IN A 203.0.113.20 0 ms ← 1 秒后
expenses.example. 19 IN A 203.0.113.20 0 ms ← 10 秒后
expenses.example. 10 IN A 203.0.113.20 0 ms ← 20 秒后
expenses.example. 30 IN A 203.0.113.20 44 ms ← 30 秒后(过期)
TTL 逐渐减少,过期就重新问(实测)
记住期间是 0 毫秒,TTL 依次减为 29、19、10。30 秒过期后解析器重新查询,但根和 .example 的介绍还记得(TTL 分别是 1 天和 1 小时),所以只问了 expenses.example,用时 0.044 秒。
服务器搬家换地址时,旧答案会留在全世界的解析器里直到 TTL 过期。俗称的“等 DNS 生效”,等的就是这个过期。常规做法是在搬家前几天先把 TTL 调短。
各种记录
DNS 回答的不只是地址。和名字绑定的每一行叫记录,有不同的类型。
# expenses.example 的各种记录(数字是 TTL,秒)
expenses.example. 29 IN A 203.0.113.20
expenses.example. 30 IN AAAA 2001:db8::20
www.expenses.example. 300 IN CNAME expenses.example.
expenses.example. 3600 IN MX 10 mail.expenses.example.
expenses.example. 3600 IN TXT "kiji practice zone for part 9"
expenses.example. 3600 IN NS ns1.expenses.example.
| 类型 | 含义 | 开发者遇到的场景 |
|---|---|---|
| A / AAAA | IPv4 / IPv6 地址 | 登记服务器的地址 |
| CNAME | 别名。“这个名字和那个名字相同” | 把 www 指向域名顶端,指向云服务 |
| MX | 接收邮件的服务器(数字是优先级) | 用自有域名收邮件 |
| TXT | 自由的字符串 | 确认域名所有者、确认发件来源(SPF 等) |
| NS | 负责这个区域的 DNS 服务器 | 买域名后指定使用的 DNS 服务 |
| SOA | 区域的基本信息(“不存在”答案的有效期等) | 下一章 |
CNAME 的名字上不能放其他记录。域名顶端(这里就是 expenses.example 本身)一定有 NS 和 SOA,所以顶端不能设为 CNAME,这条规则最好记住。
不存在的名字
# 查询不存在的名字
pc$ dig nosuch.expenses.example
status: NXDOMAIN
expenses.example. 60 IN SOA ns1.expenses.example. admin.expenses.example. 2026102501 1800 900 604800 60
# “不存在”这个答案,也会在 SOA 最后一个数(60 秒)内被记住
第 1 次: 40 ms 第 2 次: 0 ms
查询不存在的名字,答案是 NXDOMAIN(没有这个名字)。答案附有 SOA,它最后一个数(60 秒)就是“不存在”这个答案可以记住的时间。第 2 次是 0 毫秒。即使反复访问打错的名字,也不必每次沿树追溯。反过来,如果在登记新名字之前不小心查了一次,“不存在”就会被记住一段时间。
应用查名字的时候
终于可以不用 --resolve,让 curl 发送申请了。清空解析器的缓存后连续发送两次。
# 用 curl 发送 HTTPS 申请(不用 --resolve)。清空解析器缓存后连续 2 次
第 1 次 namelookup 0.1667 connect 0.208 tls 0.252 total 0.294 (201)
第 2 次 namelookup 0.0006 connect 0.041 tls 0.087 total 0.144 (201)
第 1 次查名字用了 0.167 秒,之后接着是第 6 讲的 TCP 和第 8 讲的 TLS。第 2 次只用 0.0006 秒,总时间也从 0.294 秒变成 0.144 秒。第一次访问的网站稍微慢一点,就是这个原因。
名字解析从近处依次查找
应用(curl 和浏览器)在问 DNS 之前,会先在近处查找。其中 hosts 文件会先于 DNS 被查看。开发时写的一行留下来,就会这样:
# 如果电脑的 hosts 里残留着旧地址 203.0.113.21
/etc/hosts: 203.0.113.21 expenses.example
# 操作系统的名字解析(先看 hosts)
pc$ getent ahostsv4 expenses.example
203.0.113.21 STREAM expenses.example
# dig(只问 DNS)
pc$ dig +short expenses.example
203.0.113.20
# curl(使用操作系统的名字解析)
curl: (28) Failed to connect to expenses.example port 3443 after 3002 ms: Timeout was reached
dig 正确地回答 203.0.113.20,curl 却去连接 hosts 里的旧地址 203.0.113.21,3 秒后超时。dig 只问 DNS,不看 hosts。遇到“dig 是对的,只有应用连不上”时,请怀疑 hosts 以及操作系统、浏览器的缓存。
DNS 停了的话
# 解析器的程序停了(返回 ICMP“那里没人”)
curl: (6) Could not resolve host: expenses.example
total 0.0001 s
# 到不了解析器(线断了)
curl: (6) Could not resolve host: expenses.example
total 6.023 s
只是解析器的程序停了的话,操作系统会通知“那里没人”(第 7 讲的 ICMP),curl 立刻就报 Could not resolve host。可是如果到不了解析器,就要等 6 秒才报同样的错误。这里是电脑用 ARP 询问了 6 次解析器的 MAC。在途中消失时,按照 Linux 的默认值(resolv.conf 的 timeout 5 秒、attempts 2 次),有时要等更久。
DNS 一停,用地址写的还能用,用名字写的全部停摆。在用户看来就是“上不了网”“很慢”。排查的第一步,是用名字和地址都试一试(第 12 讲)。
隐藏 DNS 的内容
本讲的记录全都是 UDP 53 号、查询的名字原样可读的 DNS。第 8 讲说过,即使用了 HTTPS,名字(SNI)仍看得见;而从 DNS 查询里同样能知道名字。
包裹 DNS 查询的方法
因此出现了用 TLS 包裹 DNS 查询本身的 DoT(853 号)和用 HTTPS 包裹的 DoH(443 号)。现在主流的浏览器和操作系统都可以设置使用 DoH。不过,解析器本身看得到名字,信任哪个解析器仍是另一个问题。另外,答案太大时还有从 UDP 切换到 TCP 重新查询的机制(第 7 讲的 1232 字节),TCP 53 号也是 DNS 的端口。
在自己的电脑上确认
| 想查的内容 | Mac | Windows | Linux |
|---|---|---|---|
| 查名字 | dig example.com | Resolve-DnsName example.com | dig example.com |
| 追溯路径 | dig +trace example.com | (用 nslookup 依次查) | dig +trace example.com |
| 操作系统的答案(含 hosts) | dscacheutil -q host -a name example.com | ping -n 1 example.com | getent ahosts example.com |
| 正在使用的解析器 | scutil --dns | ipconfig /all | resolvectl status |
| 清除操作系统缓存 | sudo killall -HUP mDNSResponder | ipconfig /flushdns | resolvectl flush-caches |
dig 答案的第 2 列就是 TTL。多查几次同一个名字,看看数字是不是在减少。
结 — 一页总结与下一讲
- 名字是从右往左读的树。根 → .example → expenses.example,每层都有负责人(委派)。
- 电脑只问解析器一次。解析器沿介绍追溯,第一次在线上走 4 个往返,用时 0.166 秒。
- 答案在 TTL 期间被记住。记住期间 0 毫秒,过期后只问需要的地方,用时 0.044 秒。
- A、AAAA、CNAME、MX、TXT、NS、SOA。顶端不能设为 CNAME。
- 不存在的名字(NXDOMAIN)也按 SOA 的数字记住。
- 应用先于 DNS 查看 hosts。dig 不看 hosts,所以可能不一致。
- 到不了解析器时,用名字写的全部在等待后停摆。DoT、DoH 可以隐藏查询。
新井知道了名字,连上了,也上了锁。终于轮到申请的内容了吧。
古井下一讲是 HTTP。把从第 1 讲一直在发送的 POST /api/expenses 一行一行地读。
参考
- RFC 1034 — Domain Names: Concepts and Facilities
- RFC 1035 — Domain Names: Implementation and Specification
- RFC 2308 — Negative Caching of DNS Queries
- RFC 9156 — DNS Query Name Minimisation to Improve Privacy
- RFC 7858 — DNS over Transport Layer Security (TLS)
- RFC 8484 — DNS Queries over HTTPS (DoH)
- RFC 2606 — Reserved Top Level DNS Names(.example)
- Root Server Technical Operations Association — root-servers.org
- IANA — Root Zone Database
- resolv.conf(5) — timeout と attempts の既定値
- NLnet Labs — Unbound
- NLnet Labs — NSD
记录来自笔者在云端 Linux(Ubuntu 24.04,内核 6.18)上用网络命名空间搭建的练习网络,于 2026 年 10 月 25 日抓取。解析器是 Unbound 1.19.2(关闭了 DNSSEC 验证和 QNAME minimisation),根、.example、expenses.example 的 DNS 服务器是 NSD 4.8.0,名字和地址都是为说明而保留的或为练习而制作的。解析器和 DNS 服务器之间是第 6 讲的捣乱线(单程 20 毫秒)。表中的数字都是从这些记录和当时命令的输出读出的实际数值。根服务器的数量依据 root-servers.org。登场人物均为虚构。
古法网络入门 第9讲 — DNS / 2026.10