DNS over TLS(DoT)
DoT 的设计几乎没有新东西:把 DNS over TCP 放进 TLS,换一个端口。正因为简单,它是最容易实现、也最容易理解的加密 DNS。
基本做法#
客户端与服务器在 TCP 853 端口建立连接,完成 TLS 握手,之后的内容与 DNS over TCP 完全一样:每个报文前加 2 字节长度,一条连接上可以连续发多个查询。RFC 7858 于 2016 年发布,2018 年的 RFC 8310 补充了怎样认证服务器、怎样使用填充。
DoT 选择独立端口而不是在 53 端口上协商升级,是为了避免降级:如果同一个端口既能明文又能加密,中间人只要拦掉升级请求就能迫使双方退回明文。
服务器认证#
客户端连的是一个 IP 地址,但要验证的是“这个地址后面确实是我想用的那个解析器”。RFC 8310 提供两种依据:
- 认证域名(ADN):客户端知道解析器的名字(如
dns.example.net),用常规的 PKIX 证书校验确认对方持有这个名字的证书。TLS 握手里的 SNI 就是这个名字。 - SPKI 指纹:客户端事先记下服务器公钥的散列,直接比对。不依赖证书机构,但换密钥时客户端必须同步更新。
只给了 IP 地址、没给名字时,客户端多半只能做机会式加密。有的公共解析器把 IP 地址也写进了证书(例如 1.1.1.1),这种情况下按 IP 验证也可行。
填充#
TLS 加密后,报文长度依然可见。查询 a.example 和查询一个长名字,密文长度不同,观察者可以据此猜测内容。RFC 8467(实验性)给出的策略是:查询填充到 128 字节的整数倍,应答填充到 468 字节的整数倍。填充通过 EDNS 的 Padding 选项(代码 12,RFC 7830)实现,只应在加密通道上使用——明文 DNS 里填充毫无意义,还会浪费带宽。
性能:贵在连接,不在加密#
一次全新的 DoT 查询要先完成 TCP 握手和 TLS 握手,比 UDP 的一问一答多出一到两个往返。所以 DoT 的性能关键是复用连接:
- 连接建立后保持一段时间,后续查询直接发送。服务器可以用 edns-tcp-keepalive 选项(RFC 7828)告诉客户端它愿意保持多久。
- TLS 1.3 的会话恢复让重连只需一个往返;带 0-RTT 时可以更快,但 0-RTT 数据可能被重放,只应用于可以安全重复执行的普通查询。
- 同一连接上的多个查询可以乱序应答(RFC 7766),一个慢查询不会卡住后面已经有答案的查询——但 TCP 层的丢包重传仍会卡住所有查询,这是 DoQ 想解决的问题。
服务器侧要注意每个连接都占内存,空闲连接要有超时,单个来源的连接数要有上限。
试一试#
用 Knot DNS 的 kdig,加 -d 能看到 TLS 握手细节:
$ kdig -d @9.9.9.9 +tls-ca +tls-host=dns.quad9.net example.com ABIND 9.18 起的 dig 也支持 DoT:
$ dig +tls @1.1.1.1 example.com A注意 dig 的 +tls 默认不校验证书,只证明“能建立 TLS”;需要校验时加 +tls-ca(使用系统信任库)和 +tls-hostname=…。要看服务器给出的证书链,openssl 最直接:
$ openssl s_client -connect 9.9.9.9:853 -servername dns.quad9.net -brief </dev/null部署时会遇到的事#
- 853 端口可能不通。一些企业网、酒店和公共 Wi-Fi 只放行少数端口,DoT 会失败。客户端应当在严格模式下明确报错,而不是悄悄退回明文。
- 识别很容易。“去往 853 端口的 TLS 连接”本身就说明了这是 DoT,网络可以选择性地拦截它。如果这一点重要,考虑 DoH。
- 中间设备。有的防火墙会对 853 端口做 TLS 检查或直接重置连接。排错时先确认 TCP 能否连通,再看 TLS 握手在哪一步失败。