bigywater.tech
网络协议 · 70

DNS over TCP 与 EDNS0:什么时候不再用 UDP 512 字节

DNS 最早规定 UDP 响应不得超过 512 字节,今天的响应(DNSSEC 签名、一大串 TXT)早就装不下了。这篇看两种办法:EDNS0 让客户端告诉服务器自己能收多大的 UDP 包;或者干脆改用 TCP。再看 TCP 比 UDP 贵多少,以及公共解析器怎么避免产生 IP 分片。

17 字节
不带 EDNS0 的根 DNSKEY 查询,UDP 上收到的被截断响应(TC = 1)
285 → 612 ms
UDP 与新建 TCP 连接的一问一答耗时中位数,约 2 倍
0 个
1.1.1.1 对 2649 / 4689 字节的大响应在线上产生的 IP 分片

512 字节与 TC

实测 · 本机执行输出
  UDP(无 EDNS0):响应 17 字节,标志 QR TC RD RA,回答 0 条;TC 位 = True(被截断:服务器放不下就只回头部和问题,让客户端改用 TCP)
  TCP 重问:响应 842 字节,标志 QR RD RA,回答 3 条(DNSKEY);建立连接 270 ms + 查询往返 269 ms

不带 EDNS0 的 UDP 响应超过 512 字节时,服务器只回头部和问题并置 TC,客户端应改用 TCP。

OPT 记录

实测 · 本机执行输出
  查询 40 字节;末尾 11 字节就是 OPT:00 00 29 04 d0 00 00 80 00 00 00
    00 = 名字(根);41 = 类型 OPT(41);1232 = 通告的 UDP 载荷大小;00 = 扩展 RCODE,00 = EDNS 版本,8000 = 标志(最高位 DO = True);0 = RDLENGTH(选项总长)
  响应 179 字节;响应里的 OPT:有,服务器通告的 UDP 载荷大小 = 1232,DO 位回显 = True,选项 []
  通告 payload=512:响应 72 字节,服务器回的 payload = 1232
  通告 payload=1232:响应 72 字节,服务器回的 payload = 1232
  通告 payload=4096:响应 72 字节,服务器回的 payload = 1232

EDNS0 的 OPT 伪记录借用“类”字段放 UDP 载荷大小,借用“TTL”字段放扩展 RCODE、版本和 DO 标志,1.1.1.1 总是通告 1232。

TCP 的开销

实测 · 本机执行输出
  TCP 上发送的字节:0028 + 40 字节报文(前两个字节 0x0028 = 40 就是后面报文的长度)
  UDP                       :[272, 290, 288, 277, 282, 294] ms,中位 285 ms
  TCP 新连接(连接 + 查询)          :[599, 1630, 492, 576, 625, 2618] ms,中位 612 ms
  TCP 复用连接的第 2 问            :[1038, 294, 312, 216, 295, 282] ms,中位 295 ms
  到 1.1.1.1 的网络往返约 285 ms:UDP 一个往返;TCP 新连接多一个握手往返(约 2 倍);复用连接的后续查询回到一个往返

TCP 消息前加 2 字节长度;新建连接要多一个往返,约 2 倍耗时;复用同一连接则几乎没有额外代价。

线上包数

实测 · 本机执行输出
  UDP:4 个 IP 包,共 272 字节(IP 层);逐包(→ 发出 / ← 收到,数字是 IP 包长):['←52T', '→52T', '→68U', '←100U']
  TCP 新连接:10 个 IP 包,共 656 字节(IP 层);逐包:['→64T', '←60T', '→52T', '→94T', '←52T', '←126T', '→52T', '→52T', '←52T', '→52T']
  再重复 UDP 3 次的包数:[2, 2, 2](逐包 [['→68U', '←100U'], ['→68U', '←100U'], ['→68U', '←100U']])

一次 UDP 问答 2 包 / 168 字节;一次新建 TCP 连接的问答 10 包 / 656 字节。

避免分片

实测 · 本机执行输出
  cloudflare.com TXT,通告 payload=4096:应用层收到 43 字节,TC = True,回答 0 条;线上来自 1.1.1.1 的 IP 包 1 个:[('偏移0', '末片', 71)]
  cloudflare.com TXT,通告 payload=1232:应用层收到 43 字节,TC = True,回答 0 条;线上来自 1.1.1.1 的 IP 包 1 个:[('偏移0', '末片', 71)]
  microsoft.com TXT,通告 payload=4096:应用层收到 42 字节,TC = True,回答 0 条;线上来自 1.1.1.1 的 IP 包 1 个:[('偏移0', '末片', 70)]
  2020 年起的 “DNS flag day” 建议把 EDNS0 载荷大小设成 1232 左右(= IPv6 最小 MTU 1280 − 40 − 8),既避免分片,又足够容纳大多数响应;更大的交给 TCP

1.1.1.1 不管客户端通告多大都只发不超过 1232 字节的 UDP 响应,更大的置 TC,让客户端改走 TCP。

分步演示

DNS over TCP 与 EDNS0
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64(Apple 芯片)/ Python 3.14.8 / tcpdump(libpcap)/ scapy 2.8 / dpkt / dnspython / h2 / aioquic / OpenSSL / curl 8.7.1;抓包在 lo0(环回)上做,少数实验用真实网卡 en0(这台机器上 en0 是 Wi-Fi,tcpdump 看到的是系统转成以太网格式的帧,所以文中的“以太网头”都指这个 14 字节的头)或访问公网(会在文章里注明);需要丢包 / 延迟 / 限速时,用一个只作用于 lo0 端口 9400-9499 的 dummynet 辅助脚本(netlab);所有服务端都是本机自己起的进程;时间戳取自 tcpdump 的 -tt,数字每次运行会有小幅抖动。全部向 1.1.1.1 的 53 端口(UDP 和 TCP),en0 抓包;每种耗时只测 6 次,TCP 新连接出现过 1630 / 2618 ms 的异常慢样本(原因没有查);第四节第一次抓包混入 2 个上一条连接收尾的杂包,正文用的是重复 3 次的干净结果
    • 没有验证:真正发生 IP 分片的 DNS 响应(1.1.1.1 把它们全避开了,需要别的服务器;分片在网络上被丢弃的风险没有测);EDNS0 的其它选项(客户端子网 ECS、Cookie、Padding、NSID)在本篇都没有出现;TCP Fast Open、TCP 保活对 DNS over TCP 的影响;服务器发起关闭 TCP 连接的空闲超时
    京ICP备2026067502号-1京公网安备11010502063487号