Old-School Networking / 2026 ・ No.09

古法网络入门

第9讲 DNS ── 从名字查地址,互联网的电话簿

发布

Wireshark 的界面。在解析器上记录的 DNS 列表,依次是电脑发来的 A expenses.example 查询、向根(203.0.113.1)的查询与回答、向 203.0.113.2 和 203.0.113.3 的查询与回答,以及给电脑的回答 A 203.0.113.20。选中第 5 个根的回答,详情的 Authoritative nameservers 里可以看到 example: type NS, ns1.example 和 Time to live: 86400 (1 day)
解析器的记录。第 5 个是根回答“不知道 expenses.example。.example 的事去问 ns1.example”,并写着这个介绍可以记住 1 天(86400 秒)(在笔者于云端搭建的练习用网络中,于 2026 年 10 月 25 日记录,用 Wireshark 4.2.2 打开后截图。界面保持英文。窗口上方的标题栏和小标题是为了说明而后加的)

到第 8 讲为止,我们都用 --resolve 告诉 curl“expenses.example 就是 203.0.113.20”。这一讲不再这样做,而是让真正的机制 DNS 来查名字。
为了练习,我们搭建了根、.example、expenses.example 三层 DNS 服务器,以及扮演运营商的解析器。沿名字树追溯的过程、记住答案的时间(TTL)、与 hosts 的不一致,以及 DNS 停止时的症状,都用实测确认。

一

名字这一层

新井上一讲的 --resolve,实际上是怎么从名字知道地址的?

古井并没有一台机器知道全世界的名字。名字按树的形状划分,每个分支有各自的负责人。不知道就介绍“去问那边”,一路追下去。

新井这不是踢皮球吗?

古井对,是设计得很好的踢皮球。而且踢过一次的答案,会记住一段时间。

全 12 讲路线图(当前:第 9 讲)

1 全景图 2 物理层 3 数据链路层 4 IP 地址 5 路由器 6 TCP 7 UDP・套接字 8 TLS 9 DNS 10 HTTP 11 开发者的网络 12 故障排查 当前位置 全景图 第1层 第2层 第3层 第4层 第5・6层 第7层 实战
  • 名字树 ── 根、.example、expenses.example,每层都有负责人
  • 解析器 ── 代替电脑沿树追溯。第一次用 0.166 秒(实测)
  • TTL 与缓存 ── 记住期间 0 毫秒,过期就重新问(实测)
  • 开发者的陷阱 ── hosts 与 dig 的不一致,DNS 停止时的症状(实测)

按 TCP/IP 的分层,DNS 是第 7 层、应用层的机制。不过它总是先于应用的通信运行,所以放在这里讲。练习网络把电脑和运营商的解析器(Unbound)放在线的这一侧,把三层 DNS 服务器(NSD)和申请服务器放在单程 20 毫秒的捣乱线那一侧。

二

名字树

名字按树的形状划分,每个分支有负责的 DNS 服务器

. 根 com jp example 本讲主角 net … expenses 报销系统的区域 www mail ns1 根的服务器 203.0.113.1 .example 的服务器 203.0.113.2 expenses.example 的 203.0.113.3 名字从右往左读:www . expenses . example . (最后的点是根)

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

电脑只问一次,追溯的是解析器(实测时刻)

电脑 解析器 根 .example expenses 确认根 去问 .example 去问 expenses 203.0.113.20 答案 0 166 电脑只问一次,逐级追溯是解析器的工作(单程 20 ms 的线 3 个往返+最初确认 1 个往返)

解析器先确认根是否在线(根的 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)
NOTE

很多真正的解析器只向根透露“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 s 10 s 20 s 30 s 168 ms TTL 30 0 ms TTL 19 0 ms TTL 10 44 ms TTL 30 蓝线=答案还能记住几秒(TTL) 红点是解析器重新查询的时候,蓝点是返回记住的答案的时候 过期后只问 expenses.example。根和 .example 的介绍分别记住 1 天和 1 小时
把两个 Wireshark 数据包列表上下并排。上面是第一次查询,解析器依次问了根、203.0.113.2、203.0.113.3,在 0.165845 秒回答电脑。下面是 30 秒后,解析器只问了 203.0.113.3,在 0.041326 秒回答
上面是第一次查询,下面是答案的有效期(30 秒)过后的查询。根和 .example 的介绍还记得,所以第 2 次只问一个地方就够了(在笔者于云端搭建的练习用网络中,于 2026 年 10 月 25 日记录,用 Wireshark 4.2.2 打开后截图。界面保持英文。窗口上方的标题栏和小标题是为了说明而后加的)

记住期间是 0 毫秒,TTL 依次减为 29、19、10。30 秒过期后解析器重新查询,但根和 .example 的介绍还记得(TTL 分别是 1 天和 1 小时),所以只问了 expenses.example,用时 0.044 秒。

POINT

服务器搬家换地址时,旧答案会留在全世界的解析器里直到 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 / AAAAIPv4 / 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)
把两个 Wireshark 数据包列表上下并排。两者都是在电脑上记录的,DNS 查询与回答之后,接着是发往 203.0.113.20 的 3443 号的 SYN 和 Client Hello。上面到回答用了 0.166132 秒,下面是 0.000145 秒
用 curl 发送两次 HTTPS 申请。第 1 次解析器沿名字树追溯,用了 0.166 秒;第 2 次返回记住的答案,只用 0.0001 秒,马上就去连接了(在笔者于云端搭建的练习用网络中,于 2026 年 10 月 25 日记录,用 Wireshark 4.2.2 打开后截图。界面保持英文。窗口上方的标题栏和小标题是为了说明而后加的)

第 1 次查名字用了 0.167 秒,之后接着是第 6 讲的 TCP 和第 8 讲的 TLS。第 2 次只用 0.0006 秒,总时间也从 0.294 秒变成 0.144 秒。第一次访问的网站稍微慢一点,就是这个原因。

名字解析从近处依次查找

浏览器 缓存 操作系统 缓存 hosts 文件 解析器 缓存 DNS 服务器 沿树追溯 从左往右依次查找,找到就停 自己的电脑里 网络的另一边 hosts 里残留旧地址时,会先于 DNS 被使用 dig 只问 DNS,不看 hosts,所以和 curl、浏览器的答案可能不一致 本讲 curl 的第 2 次,靠解析器缓存只用了 0.0006 秒

应用(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 次),有时要等更久。

POINT

DNS 一停,用地址写的还能用,用名字写的全部停摆。在用户看来就是“上不了网”“很慢”。排查的第一步,是用名字和地址都试一试(第 12 讲)。

九

隐藏 DNS 的内容

本讲的记录全都是 UDP 53 号、查询的名字原样可读的 DNS。第 8 讲说过,即使用了 HTTPS,名字(SNI)仍看得见;而从 DNS 查询里同样能知道名字。

包裹 DNS 查询的方法

PC 解析器 UDP 53 普通 DNS:名字一览无余 本讲的记录全是这种 PC 解析器 DoT 853 DNS over TLS 用第 8 讲的 TLS 包裹 PC 解析器 DoH 443 DNS over HTTPS 混在普通的网页通信里 即使隐藏了,解析器本身还是知道名字。问题在于信任哪个解析器 和第 8 讲的 ECH 一起,才能在途中隐藏“要去哪里”

因此出现了用 TLS 包裹 DNS 查询本身的 DoT(853 号)和用 HTTPS 包裹的 DoH(443 号)。现在主流的浏览器和操作系统都可以设置使用 DoH。不过,解析器本身看得到名字,信任哪个解析器仍是另一个问题。另外,答案太大时还有从 UDP 切换到 TCP 重新查询的机制(第 7 讲的 1232 字节),TCP 53 号也是 DNS 的端口。

十

在自己的电脑上确认

想查的内容MacWindowsLinux
查名字dig example.comResolve-DnsName example.comdig example.com
追溯路径dig +trace example.com(用 nslookup 依次查)dig +trace example.com
操作系统的答案(含 hosts)dscacheutil -q host -a name example.comping -n 1 example.comgetent ahosts example.com
正在使用的解析器scutil --dnsipconfig /allresolvectl status
清除操作系统缓存sudo killall -HUP mDNSResponderipconfig /flushdnsresolvectl flush-caches

dig 答案的第 2 列就是 TTL。多查几次同一个名字,看看数字是不是在减少。

结 — 一页总结与下一讲

  1. 名字是从右往左读的树。根 → .example → expenses.example,每层都有负责人(委派)。
  2. 电脑只问解析器一次。解析器沿介绍追溯,第一次在线上走 4 个往返,用时 0.166 秒。
  3. 答案在 TTL 期间被记住。记住期间 0 毫秒,过期后只问需要的地方,用时 0.044 秒。
  4. A、AAAA、CNAME、MX、TXT、NS、SOA。顶端不能设为 CNAME。
  5. 不存在的名字(NXDOMAIN)也按 SOA 的数字记住。
  6. 应用先于 DNS 查看 hosts。dig 不看 hosts,所以可能不一致。
  7. 到不了解析器时,用名字写的全部在等待后停摆。DoT、DoH 可以隐藏查询。

新井知道了名字,连上了,也上了锁。终于轮到申请的内容了吧。

古井下一讲是 HTTP。把从第 1 讲一直在发送的 POST /api/expenses 一行一行地读。

参考

记录来自笔者在云端 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

← 返回文章列表