跳到内容

第 4 卷 / 第 7 章 / 第 1 课

7.1 DNS 解析、记录与缓存边界 ​

DNS 是分层的分布式数据库,不只把域名换成 IP。一个名字可以有多组资源记录集(RRset),分别用于地址、邮件路由、委派、服务发现或域名控制权验证。

1. Stub、递归解析器与权威服务器 ​

典型客户端路径:

text
application
    |
OS / runtime stub resolver
    |
recursive resolver
    |-- root server: referral to a TLD
    |-- TLD server: referral to an authoritative zone
    `-- authoritative server: answer or authoritative negative answer

应用通常调用系统或运行时的名字服务接口。该接口可能按平台配置查询 hosts、本地缓存或企业名字服务,再把 DNS 请求交给递归解析器;这些步骤不一定全由 DNS stub 自己完成。递归解析器根据缓存决定是否还需询问根、顶级域和权威服务器。

根和顶级域服务器通常返回委派线索,不会代递归解析器继续查到底。应答中的 NS 指向下一组名字服务器,必要的 glue 地址帮助打破循环:例如 example.test 的名字服务器是 ns.example.test,不能先要求完整解析这个区,才知道去哪里查询它。glue 是启动查询的地址线索,不等于该地址的 DNSSEC 验证结果。

“13 个根服务器”指 A 至 M 这 13 个根服务标识,实际由多地点的任播实例提供,并非全球只有 13 台机器。

2. 递归请求与迭代解析 ​

客户端通常向递归解析器要最终结果或错误。递归解析器则逐次询问各级权威服务器,收到委派后继续查询下一组服务器。它也可能把请求转给另一递归解析器,具体看配置。

bash
# 询问当前配置的递归解析器
dig www.example.com A

# 由 dig 从根开始跟随委派,演示迭代解析
dig +trace www.example.com A

# 指定递归解析器
dig @1.1.1.1 www.example.com AAAA

dig +trace 不是用户上一条查询的回放。它先获取根服务器信息,再由本机跟随委派;@server 在 trace 模式下只影响最初的根 NS 查询。真实递归解析器可能已命中缓存,或采用转发、QNAME 最小化和不同路径。上面命令会发起实际 DNS 查询;企业网络中应选择获准使用的解析器。

3. 记录集与别名约束 ​

Type所属名字下的数据常见用途
AIPv4 地址地址解析
AAAAIPv6 地址地址解析
CNAME规范名字别名
MX优先级与邮件交换器名字邮件路由
NS权威名字服务器名区委派与权威服务
TXT一个或多个字符串域名验证、SPF 等
SOA主名字服务器、序列号与区定时参数区管理与负缓存参数
CAACA 授权策略约束允许签发该域名证书的 CA

RRset 由相同 owner name、class 和 type 的记录组成。一个 A RRset 可以有多个 IPv4 地址,AAAA 则是另一组;不要把每个地址当成单独一组,也不要把应答顺序当作稳定的优先级。连接库还可能交错尝试 IPv6 与 IPv4。

CNAME 所属名字不能再带其他普通数据,DNSSEC 相关记录有特定例外。因此普通区顶点必需的 SOA/NS 与 CNAME 冲突。提供商的 ALIAS、ANAME 或 flattening 是扩展能力,通常通过合成地址答案处理,不能把它们当作标准 CNAME。

CNAME 链增加解析工作,但不一定多一次往返:缓存或权威应答中的附带数据可能省去查询。

4. TTL 与迁移时序 ​

权威记录携带 TTL,解析器缓存后递减剩余时间。TTL 到期表示需要刷新,不是向全球缓存广播删除。RFC 8767 允许配置了 serve-stale 的解析器在无法刷新时受控返回过期数据,并限制保留时间和返回的 TTL;它不会把旧记录重新变成权威最新值。

TTL 的取舍包括:

  • 长 TTL 减少权威查询和解析延迟,但变更前的答案也保留更久;
  • 短 TTL 让新查询更快看到变更,但增加对 DNS 服务的依赖与查询负载;
  • 解析器和应用可能另设缓存上限或本地保留策略,不能只看权威 TTL 推测实际行为;
  • 已建立的 TCP/TLS 连接不会因 DNS TTL 到期自动迁移。

计划迁移时先降低 TTL,并确认权威服务器已发布这个值,再等待至少一个旧 TTL 窗口,最后切换地址。比如从 3600 秒降为 60 秒,降值前刚缓存的记录仍可能保留近一小时;等 60 秒就切换并不能加速那份旧缓存。切换后还要保留旧端点并观察流量,考虑应用缓存、长连接和 serve-stale。

5. 负缓存与新建名字 ​

NXDOMAIN 与 NODATA 都可能进入负缓存。按 RFC 2308,权威负应答中 SOA 的 TTL 取 min(SOA 自身 TTL, SOA.MINIMUM),接下来像普通 TTL 一样递减。下面只计算正常负缓存的剩余时间,不实现解析器或 serve-stale:

python
soa_ttl = 3600
soa_minimum = 300
negative_ttl = min(soa_ttl, soa_minimum)
for elapsed in [0, 120, 300]:
    remaining = max(0, negative_ttl - elapsed)
    print(elapsed, remaining)
# 0 300
# 120 180
# 300 0

新建记录不会抹掉此前缓存的不存在应答。排查时要区分:

  • NXDOMAIN:该名字不存在;
  • NODATA:名字存在,但没有所查询类型的记录集;
  • SERVFAIL:解析器未能得到可用答案,可能涉及 DNSSEC 验证、上游故障或协议错误。

NODATA 不是独立 RCODE,通常由 NOERROR、缺少所查询类型的答案及权威区信息判断,不能把所有空答案都认作 NODATA。NXDOMAIN 的负缓存按名字与 class 生效,NODATA 还需区分查询类型;存在 A 而没有 AAAA,不能推导整个名字不存在。

6. 枚举系统解析的 IPv4 与 IPv6 地址 ​

Python getaddrinfo() 使用平台名字服务配置,结果未必来自 DNS,socket API 也不暴露权威 TTL。下面按 IPv4/IPv6 去重,并保留 IPv6 链路本地地址的 scope ID,避免把不同接口上的同一地址合并:

python
#!/usr/bin/env python3
import socket
import sys

def resolve_stream_addresses(host: str, service: str = "443") -> list[str]:
    results = socket.getaddrinfo(
        host, service, family=socket.AF_UNSPEC,
        type=socket.SOCK_STREAM, proto=socket.IPPROTO_TCP,
    )
    addresses: list[str] = []
    seen: set[tuple[int, str, int]] = set()
    for family, _type, _proto, _canonname, sockaddr in results:
        address = sockaddr[0]
        scope = sockaddr[3] if family == socket.AF_INET6 else 0
        key = (family, address, scope)
        if key not in seen:
            seen.add(key)
            label = "IPv6" if family == socket.AF_INET6 else "IPv4"
            suffix = f"%{scope}" if scope and "%" not in address else ""
            addresses.append(f"{label} {address}{suffix}")
    return addresses

if __name__ == "__main__":
    host = sys.argv[1] if len(sys.argv) > 1 else "localhost"
    try:
        for item in resolve_stream_addresses(host):
            print(item)
    except socket.gaierror as error:
        raise SystemExit(f"resolution failed: {error}")

默认查询 localhost,地址种类和顺序取决于机器配置;使用 127.0.0.1 等数字地址可检查输出格式而不依赖公共 DNS。socket.setdefaulttimeout() 既不清缓存,也不为 getaddrinfo() 设置解析截止时间。重复调用不能稳定测量冷、热 DNS 缓存;观察 TTL 或指定解析器时应使用 dig 或 DNS 库,并记录环境。

7. DNS 传输与安全边界 ​

传统 DNS 同时支持 UDP 和 TCP,通用实现需要支持 TCP。EDNS(0) 通告可接收的 UDP 负载大小,但不保证路径能无分片传送那么大的报文。UDP 应答的 TC 位表示截断,客户端通常重试 TCP;丢包也可能直接表现为超时。TCP 承载的每条 DNS 消息还有 2 字节大端长度前缀。

DNSSEC 认证 DNS 数据来源并检查完整性,不隐藏查询名字。DoT、DoH 加密客户端到解析器这一段,解析器仍可看到查询,也不会因为传输加密就自动验证权威数据。应分别检查:

  • 客户端到解析器的传输隐私;
  • 解析器的日志与信任策略;
  • DNS 数据的真实性;
  • 应用协议自己的 TLS 认证。

8. 诊断顺序 ​

  1. 查当前解析器,不要默认所有主机都用同一公共 DNS。
  2. 分别查 A 和 AAAA,记录状态、答案、权威区信息和 TTL。
  3. 直接问权威服务器,与递归应答对比。
  4. 检查 CNAME 链、委派 NS 与 glue。
  5. 区分正缓存、负缓存与已有连接复用。
  6. 有 DNSSEC 时查验证失败,别把 SERVFAIL 直接当作没有记录。

9. 验收问题 ​

  1. stub、递归解析器和权威服务器分别负责什么?
  2. 为什么 CNAME 不适合直接放在普通区顶点?
  3. 为一次 DNS 切换写出降 TTL、等待、切换、观测和下线旧端点的顺序。
  4. 区分 NXDOMAIN、NODATA 和 SERVFAIL。
  5. 为什么 DNSSEC 不能替代 HTTPS?

下一课进入7.2 CDN 缓存、回源与流量导向。

参考 ​

Built with VitePress | Software Systems Atlas