EDNS Client Subnet (ECS) 对跨国 CDN 调度与出口路由判定的影响与泄漏排查
跨国访问一个托管在 CloudFront 或 Akamai 上的静态资源站,浏览器 DevTools 里 TTFB 稳定在 400ms 以上,而同一台机器 curl 一个位于法兰克福的裸 IP 只需要 90ms。traceroute 显示流量绕去了洛杉矶再折回欧洲。这类故障里,TCP 握手、TLS 协商、BBR 拥塞控制全都没问题,问题出在解析阶段:权威 DNS 拿到的是递归解析器的出口地址,而不是你的真实出口地址,于是它把一个物理上离你 8000 公里的边缘节点写进了 A 记录。ECS(EDNS Client Subnet,RFC 7871)就是用来修补这个信息断层的机制,而它本身又会引入新的泄漏面与调度误判。这篇文档从报文层拆到调度决策层,再落到排查命令。
一、问题域:权威 DNS 为什么需要知道你的子网
1.1 递归解析天然抹掉了客户端位置
标准 DNS 解析(RFC 1034 / RFC 1035)里,客户端只向递归解析器发一个查询,递归器代替客户端完成整条迭代链。权威服务器在 UDP 层看到的源地址是递归器的出口 IP。对 1.1.1.1 这类全球 Anycast 递归器,同一个逻辑 IP 在不同地区会落到不同的 PoP,但权威侧只能看到"某个 Cloudflare 出口",无法还原终端用户的 /24 或 /56 前缀。
对传统单机房权威服务器,这无所谓,返回的 A 记录固定。对 CDN 权威(Akamai EdgeScape、CloudFront、Fastly、Cloudflare 的 GeoDNS)就致命了:它们依赖源地址做地理映射,源地址是递归器地址时,映射结果等于"递归器的位置",而不是"用户的位置"。
1.2 ECS 的报文契约
RFC 7871 在 EDNS0 OPT 伪记录(RFC 6891)的 RDATA 里塞进一个 option code 8 的字段,结构如下:
+0: OPTION-CODE = 8 (2 octets)
+2: OPTION-LENGTH (2 octets)
+4: FAMILY (2 octets) 1 = IPv4, 2 = IPv6
+6: SOURCE PREFIX-LENGTH (1 octet)
+7: SCOPE PREFIX-LENGTH (1 octet)
+8: ADDRESS (变长,向上取整到 octet)2
3
4
5
6
客户端(或递归器)在查询里带上自己的地址前缀,SOURCE PREFIX-LENGTH 表示"我提供多少位",SCOPE PREFIX-LENGTH 在查询里必须置 0,由权威服务器在响应里回填,表示"我实际用了多少位做决策"。这个 SCOPE 字段是整个机制里最容易被忽视的部分,后面调度误判的分析全靠它。
一个 IPv4 /24 的 ECS 查询在 Wireshark 里长这样:
Additional records
OPT
Name: <Root>
Type: OPT (41)
UDP payload size: 1232
EDNS0 option: Client Subnet
Option Code: 8
Option Length: 7
Family: IPv4 (1)
Source Netmask: 24
Scope Netmask: 0
Client Subnet: 203.0.113.02
3
4
5
6
7
8
9
10
11
12
注意 UDP payload size 是 1232,这是 DNS Flag Day 2020 之后的主流值,避免 IP 分片。ECS 字段本身只占 11 字节,对放大攻击面影响很小,但它是可被伪造的,这是后文泄漏分析的起点。
1.3 谁在填充 ECS
填充方只有两类。第一类是"感知 ECS 的递归器",比如 Google Public DNS 8.8.8.8 会主动把用户子网(通常 /24)附在发往权威的查询上;Cloudflare 1.1.1.1 默认不附加 ECS,这是它隐私策略的一部分,也是大量跨国调度错配的根因。第二类是"直连权威的 stub",某些代理客户端或自建 dnsmasq 在 --add-subnet 配置下会直接带 ECS 查询。
递归器是否转发 ECS,还受"上游是否支持"影响。递归器对某个权威域名的 NS 做能力探测(缓存 TTL 内),若权威对带 ECS 的查询返回 FORMERR 或忽略 ECS,递归器会回退到不带 ECS 的模式。这条回退路径在跨国场景里经常静默发生,抓包才能看见。
二、公共 DNS 的 ECS 行为差异与调度错配的量化
2.1 三家公共 DNS 的实测行为
在法兰克福一台 VPS 上对 dsn.cloudfront.net 这类强地理调度域名做对照,命令统一为:
dig @1.1.1.1 d1234.cloudfront.net A +short
dig @8.8.8.8 d1234.cloudfront.net A +short
dig @9.9.9.9 d1234.cloudfront.net A +short2
3
典型结果(IP 已脱敏,保留 AS 归属):
| 递归器 | 是否带 ECS | 返回节点 AS | 实测 RTT |
|---|---|---|---|
| 1.1.1.1 | 否 | AS16509 美西 | 168ms |
| 8.8.8.8 | 是(/24) | AS16509 法兰克福 | 11ms |
| 9.9.9.9 | 否 | AS16509 美东 | 152ms |
1.1.1.1 和 9.9.9.9 返回的节点在物理上离用户很远,因为权威只能看到递归器的 Anycast 出口位置。Cloudflare 的 1.1.1.1 在法兰克福有 PoP,但其到 CloudFront 权威的出口可能落在阿姆斯特丹或更远,权威按出口地址做 GeoIP 映射,映射到美西并不奇怪。
2.2 时延数学:一次错配值多少毫秒
假设用户到正确边缘节点 RTT 为 11ms,到错配节点 RTT 为 168ms。一个 HTTPS 首屏加载包含:DNS 解析(缓存命中约 0ms)、TCP 三次握手 1 RTT、TLS 1.3 握手 1 RTT、HTTP 请求响应 1 RTT。仅握手阶段就多出 3 × (168 - 11) = 471ms。若页面引用 6 个同源子资源且未复用连接,每个子资源多 1 RTT 建连,累计额外 6 × 157 = 942ms。这就是"网站没坏但就是慢"的典型来源。
更隐蔽的是 TCP 层。跨大西洋链路的基础 RTT 在 80ms 以上,BBR 的带宽探测在长 RTT 下收敛更慢,net.ipv4.tcp_rmem 默认的 4096 87380 6291456 在 157ms 额外 RTT 下需要更多窗口才能跑满带宽。丢包 0.1% 时,CUBIC 的吞吐退化与 RTT 成反比,错配节点的有效吞吐可能只有正确节点的 1/5。
2.3 SCOPE PREFIX-LENGTH 的调度语义
权威响应里的 SCOPE 字段告诉递归器"这个答案对多大的子网有效"。若权威回填 SCOPE=24,意味着"我这个答案只对 /24 内所有地址成立",递归器可以把该答案缓存给同 /24 的其他客户端。若 SCOPE=0,表示"答案与客户端子网无关",递归器可以全局缓存。
调度错配常发生在 SCOPE 被权威回填得过大时。某 CDN 权威对欧洲用户回填 SCOPE=0(认为全欧一个答案),递归器就把法兰克福的答案缓存给了整个欧洲,包括一个实际在伊斯坦布尔的用户,后者拿到法兰克福节点,多绕 40ms。这是权威侧的策略问题,客户端无法直接修正,只能通过更换递归器或强制直连权威绕过。
三、代理客户端分流不严导致的 ECS 泄漏与本地 DNS 污染
3.1 泄漏的两条路径
路径一,DNS 查询走了本地运营商递归器。代理客户端(Clash、sing-box、v2ray)若分流规则把某域名判给直连,DNS 查询也会走系统默认解析器,即运营商 DNS。运营商递归器把你的真实 /24 附在 ECS 里发给权威,权威据此返回本地节点,但你的 TCP 流量实际走了代理出口(比如新加坡),于是"DNS 解析位置"与"TCP 出口位置"分裂,拿到本地节点却从新加坡连,RTT 反而更高。
路径二,代理客户端自身带 ECS 直连权威。部分客户端在 dns 配置里启用 respect-rules 但未隔离 ECS,查询直接发往权威并携带本机地址,权威返回本地节点。同时 TCP 走代理,分裂同上。
3.2 报文时序:分裂是怎么形成的
Client(北京) Proxy(新加坡) Local DNS(北京) CDN Auth
| | | |
|-- TCP SYN ------>| | |
| |-- TCP SYN ------>| (到边缘节点) |
| | | |
|-- DNS query ---------------------->| |
| | |-- ECS 203.0.113.0/24 -->|
| | |<-- A 北京节点 ----|
|<-- A 北京节点 ----------------------| |
| | |
|-- HTTP GET (经代理到北京节点) ------>| |
| | 新加坡 -> 北京 8000km |2
3
4
5
6
7
8
9
10
11
12
客户端拿到北京节点,但 TCP 从新加坡出发连北京节点,物理绕行 8000 公里以上。正确行为应该是 DNS 查询也走代理出口(新加坡),权威返回新加坡节点。
3.3 本地 DNS 污染的识别
运营商递归器对某些域名返回被劫持的 A 记录(指向广告页或黑洞 IP),这类记录通常 TTL 异常短(30s 或 60s)或指向保留地址段。用 dig +trace 对比权威直查与递归查结果即可定位:
dig @223.5.5.5 example.com A +short
dig @a.iana-servers.net example.com A +short2
两者不一致且递归结果指向 0.0.0.0 或 127.0.0.1 段,基本可判定污染。ECS 泄漏与污染常同时出现,因为都源于"DNS 走了不该走的路径"。
四、DoH 与 DoT 的部署边界
4.1 加密 DNS 解决了什么、没解决什么
DoH(RFC 8484)把 DNS 查询封装进 HTTPS,DoT(RFC 7858)封装进 TLS。两者都解决"链路上明文可篡改、可监听"的问题,对本地 DNS 污染有效。但它们不改变 ECS 语义:DoH 服务器收到查询后是否附加 ECS,取决于服务器策略。Cloudflare DoH(https://cloudflare-dns.com/dns-query)默认不带 ECS,Google DoH(https://dns.google/dns-query)默认带 /24 ECS。换 DoH 端点等于换递归器,ECS 行为随之改变。
4.2 部署边界
DoH 走 443 端口,与正常 HTTPS 流量混流,抗封锁能力强,但企业网关难以按 SNI 区分 DNS 与其他 HTTPS,审计困难。DoT 走 853 端口,端口特征明显,易被针对性阻断,但流量特征干净,适合自建。
代理场景里,正确做法是让 DNS 查询与 TCP 流量走同一出口。Clash 的 dns.enhanced-mode: fake-ip 配合 nameserver 指向代理隧道内的 DoH,可保证查询与流量同源。sing-box 的 dns.strategy: prefer_ipv4 与 outbound: proxy 绑定同理。关键约束是:DNS 出站必须绑定到与业务流量相同的 outbound,否则 ECS 携带的地址与 TCP 源地址不一致,调度必然错配。
4.3 自建递归器的 ECS 控制
自建 Unbound 时,send-client-subnet 与 client-subnet-always-forward 两个参数控制 ECS 行为。默认 Unbound 不发送 ECS。若上游权威强依赖 ECS 做调度,需要显式配置:
server:
send-client-subnet: 0.0.0.0/0
client-subnet-always-forward: yes
client-subnet-zone: example.cloudfront.net2
3
4
client-subnet-zone 限定只对特定域名发送 ECS,避免全局泄漏。这是隐私与调度精度的权衡点:全局发 ECS 精度最高但泄漏最广,按域白发最保守但配置量大。
五、实操:用 dig +subnet 与 ConnProof 验证权威出口分配
5.1 dig +subnet 探测权威调度
dig 的 +subnet 参数直接构造 ECS 查询,绕过递归器的 ECS 策略,直连权威观察其调度决策:
dig @ns1.example-cdn.net www.example.com A +subnet=203.0.113.0/24 +short
dig @ns1.example-cdn.net www.example.com A +subnet=198.51.100.0/24 +short2
两个不同子网应返回不同节点。若返回相同节点,说明权威未启用 ECS 调度或该域名未配置地理策略。加 +bufsize=1232 避免分片,加 +norecurse 模拟权威直查。
观察 SCOPE 回填需要看完整响应:
dig @ns1.example-cdn.net www.example.com A +subnet=203.0.113.0/24 +noall +additional输出里 CLIENT-SUBNET: 203.0.113.0/24/0 三段分别是地址、源前缀、作用域前缀。作用域为 0 表示权威未采纳 ECS,为 24 表示按 /24 调度。
5.2 ConnProof dns 命令的对照检测
ConnProof 的 dns 子命令用于在指定出口上执行解析并对比结果,定位"解析位置与出口位置分裂":
connproof dns example.com --verbose
connproof dns example.com --ipv4 --format json
connproof dns example.com --timeout 3 --verbose2
3
--verbose 输出递归链每一跳的响应时间与权威地址,--format json 便于脚本比对。典型排查流程是先在直连出口跑一次,再在代理出口跑一次,比对返回的 A 记录 AS 归属。若两次结果不同且代理出口返回的节点离代理位置更近,说明 ECS 与出口匹配正常;若代理出口仍返回本地节点,说明 DNS 查询未走代理,存在泄漏。
配合 diagnose 做端到端确认:
connproof diagnose example.com --verbose
connproof tcp example.com --port 443 --timeout 5
connproof tls example.com --port 443 --verbose2
3
diagnose 串联 DNS、TCP、TLS 三段,输出每段的耗时与目标 IP。若 DNS 段返回的 IP 与 TCP 段实际连接的 IP 不一致,说明中间有 DNS 重绑定或分流规则二次解析,需要检查代理客户端的 fake-ip 与 respect-rules 配置。
5.3 抓包验证 ECS 字段
在递归器出口抓包确认 ECS 是否真的发出:
tcpdump -i eth0 -n -s 0 'udp port 53' -w dns.pcap
tshark -r dns.pcap -Y 'dns.opt.client_subnet' -T fields \
-e ip.src -e dns.qry.name -e dns.opt.client_subnet_family \
-e dns.opt.client_subnet_source_mask -e dns.opt.client_subnet_address2
3
4
若过滤结果为空,说明该递归器未发送 ECS。对 1.1.1.1 这是预期行为,对 8.8.8.8 则说明查询命中了不支持 ECS 的路径或缓存。
Wireshark 显示过滤器 dns.opt.client_subnet 可直接高亮带 ECS 的查询。对比请求与响应的 SCOPE 字段,可判断权威是否采纳。
六、调度错配的判定矩阵与修复路径
6.1 判定矩阵
| DNS 出口 | TCP 出口 | 权威 ECS 行为 | 结果 | 修复方向 |
|---|---|---|---|---|
| 本地 | 本地 | 采纳 | 正常 | 无 |
| 本地 | 代理 | 采纳 | 错配,连本地节点 | DNS 走代理 |
| 代理 | 代理 | 采纳 | 正常 | 无 |
| 代理 | 代理 | 忽略 | 按递归器位置调度 | 换递归器或直连权威 |
| 本地 | 代理 | 忽略 | 按递归器位置调度 | DNS 走代理且换支持 ECS 的递归器 |
6.2 修复路径
第一优先级是统一 DNS 与 TCP 出口。代理客户端配置里把 DNS 的 outbound 绑定到与业务相同的代理组,禁用系统 DNS 回退。
第二优先级是选择 ECS 行为匹配的递归器。需要地理精度时用 8.8.8.8 或支持 ECS 的自建 Unbound;需要隐私时用 1.1.1.1 但接受调度精度损失,并通过 dig +subnet 手工验证权威是否仍能给出合理节点。
第三优先级是权威侧策略核查。若权威对某子网回填 SCOPE=0,说明它放弃了该子网的精度,客户端无法修正,只能换 CDN 或接受。这类问题常见于成本敏感的中小 CDN,其 GeoIP 库粒度只到国家。
6.3 失败边界
ECS 不是万能的。权威的 GeoIP 库若把某个 /24 映射错(比如把某企业专线段标成另一个城市),ECS 携带正确子网也返回错误节点。递归器到权威的路径若经过 NAT64 或 CGNAT,ECS 携带的地址可能是转换前的私网段,权威无法映射。IPv6 场景下 ECS 的 FAMILY=2,前缀长度常用 /56,部分权威对 IPv6 ECS 支持不完整,回退到无 ECS 调度。这些边界需要在排查时逐一排除,不能默认 ECS 生效就等于调度正确。
七、与站内其他故障域的关联
ECS 错配的表象常被误判为线路问题。若发现不同域名出口线路不一致,先排除 ECS 与递归器差异,再查 BGP 与代理分流,参见不同域名出口线路不一致。若解析直接失败而非错配,按DNS 查询失败解决顺序逐层排查,ECS 字段异常(如 OPTION-LENGTH 与实际地址长度不符)会导致权威返回 FORMERR,表现为解析超时。DNS 基础报文结构与 EDNS0 的完整语义见DNS 解析基础。
八、结论性工程清单
排查跨国 CDN 调度问题,按以下顺序执行,每步都有明确的观测点。
- 用
connproof dns example.com --verbose在直连与代理两个出口分别解析,记录返回 IP 的 AS 归属。 - 用
dig +subnet=<真实子网>/24 @<权威NS>直查权威,观察返回节点是否随子网变化,确认权威是否采纳 ECS。 - 用
dig +noall +additional查看 SCOPE 回填,SCOPE=0 表示权威放弃精度。 - 用
tshark -Y 'dns.opt.client_subnet'抓包确认递归器是否真的发送 ECS。 - 用
connproof diagnose example.com --verbose串联 DNS、TCP、TLS,确认三段目标一致。 - 检查代理客户端 DNS outbound 是否与业务 outbound 绑定,禁用系统 DNS 回退。
- 根据判定矩阵选择修复方向,优先统一出口,其次换递归器,最后接受权威侧限制。
ECS 的价值在于把"递归器位置"还原成"用户子网位置",它的风险在于这个还原过程依赖一条可伪造、可省略、可回填错误作用域的字段链。工程上能控制的是前两步:让 DNS 查询与 TCP 流量走同一出口,让递归器的 ECS 策略与权威的调度策略匹配。权威侧的 GeoIP 精度与 SCOPE 策略不在控制范围内,只能观测并接受。把这两步做扎实,跨国 CDN 的时延异常能消掉大部分。