工程
CNAME 让 Chrome 用不上 DNS 里的 h3
HTTPS 记录里明明有 alpn=h3,Chrome 却显示 No alternative endpoints。排查发现,CNAME 让 HTTPS 元数据与地址分属不同名字;用 clean-dns 压平后,Reddit 无需代理也能通过 HTTP/3 访问。
HTTPS 记录里明明有 alpn=h3,Chrome 却始终显示 No alternative endpoints。
问题出在记录的名字上:Chrome 按名字把 HTTPS 记录的元数据(metadata)与 A/AAAA 地址配对,
这次 CNAME 恰好让两者落在不同名字下,无法组装出可用的替代端点(alternative endpoint)。
用 clean-dns 把记录压平到同一个名字后,https://www.reddit.com 在我的网络环境里无需代理就能打开,走的还是 HTTP/3。
如果想先了解自建 DoH、本机 DNS 接入和 DNS 污染验证,可以从 《自建 DoH,顺便把三家公共 DoH 实测了一遍》 读起; 本文继续追查 Chrome 收到 HTTPS 记录后,为什么仍然用不上 h3。
一、现象
本机的 DoH 由 clean-dns 提供,只监听回环地址,支持 h2 和 h3。Chrome 配好 DoH 后,
仍然打不开 Reddit:A 记录解析正常,拿到了 Fastly 的 IP,但 Chrome 没有可用的
HTTP/3 端点,只能回落到 TCP,而当前网络到这个地址的 TCP 443 连接被阻断。
chrome://net-internals/#dns 里是这样:
Resolved IP addresses of "www.reddit.com": ["199.232.149.140"].
No alternative endpoints
用 doggo 直接查询同一个解析器,却能拿到 HTTPS 记录:
$ doggo @127.0.0.1 www.reddit.com HTTPS
NAME TYPE CLASS TTL ADDRESS NAMESERVER
www.reddit.com. HTTPS IN 60s 1 . alpn="h3" 127.0.0.1
解析器返回了 alpn=h3,Chrome 却说没有可用端点。接下来要确认的是:Chrome 收到了什么,又是怎么处理的。
这里的 HTTPS 指 DNS 的 HTTPS 记录,类型编号为 65;与它结构相同的 SVCB 记录编号为 64。Chrome 查询的是前者。
二、从 Chrome 日志追到源码
用 chrome://net-export 抓一份 netlog,重点看两个事件。
HOST_RESOLVER_DNS_TASK_EXTRACTION_RESULTS:
metadata: domain_name=www.reddit.com
supported_protocol_alpns=["h3","http/1.1"]
target_name=www.reddit.com
A: alias www.reddit.com -> reddit.map.fastly.net
data domain_name=reddit.map.fastly.net, address=199.232.149.140
HOST_RESOLVER_SERVICE_ENDPOINTS_UPDATED 出现 0 次,日志中没有端点更新事件。
记录确实收到了,alpn 也解析出来了,但地址挂在 reddit.map.fastly.net 名下,
metadata 挂在 www.reddit.com 名下。Chromium 拼装 endpoint 时按名字分组
(net/dns/dns_task_results_manager.cc):
GetOrCreatePerDomainResult(result->domain_name()); // 地址:reddit.map.fastly.net
GetOrCreatePerDomainResult(metadata.target_name); // metadata:www.reddit.com
两组对不上,www.reddit.com 这一组没有地址,就被跳过了。源码中已有对应的 TODO:
// TODO(crbug.com/41493696): Just adding per-domain metadata does not work
// properly when the target name of HTTPS is an alias, e.g:
// example.com. 60 IN CNAME svc.example.com.
另外,extractHttpsResults 的注释也提到:Chrome does not yet support followup queries。
这里的 follow-up 指 HTTPS/SVCB 服务发现过程中,对目标名字发起的后续查询,
不能与递归解析器处理普通 CNAME 的过程混为一谈。相关实现历史见第六节。
三、三种行为
当前配置:在原始域名下合成 HTTPS 记录,配对失败
这里把浏览器最初查询的域名称为原始域名(origin)。问题出在两类应答的名字不一致:
sequenceDiagram
participant C as Chrome
participant R as 解析器
C->>R: A www.reddit.com
R-->>C: CNAME reddit.map.fastly.net + A 199.232.149.140
C->>R: HTTPS www.reddit.com
R-->>C: HTTPS 1 . alpn=h3,owner 为 www.reddit.com
Note over C: metadata 叫 www.reddit.com<br/>地址叫 reddit.map.fastly.net<br/>配对失败
标准解析路径:递归器带回 CNAME 链与链尾记录
按 CNAME 的基本规则,存在 CNAME 的名字不能同时拥有普通的其他类型记录(RFC 1034 §3.6.2)。 在这条解析链中,HTTPS 记录应放在链尾。递归解析器沿 CNAME 链继续查询, 将别名链和最终结果一起返回(RFC 1034 §4.3.2);如果应答已经包含所需记录,客户端就不必追加查询。
sequenceDiagram
participant C as RFC 客户端
participant R as 递归解析器
participant A as 权威
C->>R: HTTPS www.reddit.com
R->>A: HTTPS www.reddit.com
A-->>R: CNAME 指向 reddit.map.fastly.net
R->>A: HTTPS reddit.map.fastly.net
A-->>R: HTTPS 1 . alpn=h3
R-->>C: CNAME 链 + 链尾 HTTPS 记录
Note over C: metadata 与地址都叫 reddit.map.fastly.net<br/>配对成功
但这条路径首先要求链尾实际存在 HTTPS 记录。这次查询中,reddit.map.fastly.net 并没有发布它。
即使浏览器支持追加查询,也不能凭空得到一条不存在的记录。这里的 alpn=h3 来自 clean-dns 的本地合成,
要解决的是如何让这条元数据与已有地址配对。
我的做法:clean-dns 把记录压平到 origin
既然这里的配对依赖名字一致,就在 clean-dns 中把相关应答统一到原始域名:
sequenceDiagram
participant C as Chrome
participant D as clean-dns
participant U as 上游
C->>D: A www.reddit.com
D->>U: A www.reddit.com
U-->>D: CNAME reddit.map.fastly.net + A 199.232.149.140
D-->>C: A 199.232.149.140,owner 改写为 www.reddit.com
C->>D: HTTPS www.reddit.com
D->>U: CNAME www.reddit.com
U-->>D: CNAME reddit.map.fastly.net
Note over D: records 命中链尾的 +.fastly.net
D-->>C: HTTPS 1 . alpn=h3,owner 改写为 www.reddit.com
C->>D: CNAME www.reddit.com
D-->>C: NOERROR + 空 answer
Note over C: metadata 与地址都叫 www.reddit.com<br/>配对成功,首次访问即走 h3
具体处理分为三类:
| 查询 | 处理 |
|---|---|
| A / AAAA | 转发回来后丢掉 CNAME/RRSIG,地址 owner 改成 origin,TTL 取链上最小 |
| HTTPS / SVCB | origin 未在 -records 命中时逐级查 CNAME,链尾记录(records 或链尾自己的上游记录)压平到 origin |
| CNAME | 链尾有记录时回 NODATA,对外不再暴露别名 |
四、-records 是什么
-records 用于指定一份纯文本静态记录表,记录写法类似 zone file,域名匹配支持 mihomo 通配符。
查询命中配置的域名与类型时,直接按表中内容应答,不查上游;文件每 2 秒检查一次变更,无需重启。
# h3 白名单:裸域与所有子域都回一条 HTTPS 记录
+.reddit.com HTTPS 1 . alpn=h3
+.fastly.net HTTPS 1 . alpn=h3
# 名字只在某个类型上接管,其余类型继续走上游
doh.example.com HTTPS 1 . alpn=h3
HTTPS 1 . alpn=h3 的读法:priority=1(ServiceMode)、target 是 .(就是域名自己)、
支持的 ALPN 为 h3。它向客户端声明:这个域名提供 HTTP/3 服务,可以尝试通过 QUIC 连接。
启动 clean-dns 时带上它:
clean-dns -flatten-cname -records dns-records.txt \
-upstream <上游 DNS> -clean-upstream <干净上游> \
-listen 127.0.0.1:<port> -https-listen 127.0.0.1:<port> --http3 \
-tls-crt <cert.pem> -tls-key <key.pem>
五、结果
清掉 Chrome 的主机缓存后,再访问 Reddit,net-internals 的结果变成:
Resolved IP addresses of "www.reddit.com": ["199.232.149.140"].
Alternative endpoint: {"alpns":["h3","http/1.1"],"ip_endpoints":["199.232.149.140"]}
DevTools 的 Protocol 列也显示 h3。无需代理,https://www.reddit.com 就能直接打开。
在这次测试的网络环境中,到 Fastly 边缘的 UDP 443 可用,浏览器成功建立了 HTTP/3 连接。
DNS 只负责告诉 Chrome「这里支持 h3」,真正的 QUIC 连接仍由浏览器直接发起。
顺带验证了几件事:
- 本次测试中,没有 IPv6 地址的 AAAA 查询在压平后返回空应答,不再透传 CNAME
- 压平结果进独立缓存,第二次查询不再打上游
六、这个 TODO 背后,究竟跟进了什么
顺着源码里的编号追下去,会遇到几条范围不同的工单。截至 2026 年 11 月 9 日, 底层端点 API 一直有人维护,完整 HTTPS-RR 支持也已有外部实现,但后者仍未获得明确的评审与交付承诺。 把它概括成「官方明确不修」并不准确;更贴近公开记录的说法是:是否支持仍未决定,目前不承诺投入。
41493696:端点 API 的改造任务,不是单独的 CNAME 修复单
41493696:Implement DNS ServiceEndpoint Request APIs
创建于 2024 年 1 月 23 日,旧编号是 1520720。它的目标是改变 DNS 解析结果的交付方式:
旧的 ResolveHostRequest 等待全部查询结束或超时;新的 ServiceEndpointRequest
在部分结果已经足够建立连接时,就向调用方提供候选端点,后续再持续更新。
这也是它与 Happy Eyeballs、连接调度有关的原因:A、AAAA、HTTPS 的返回时间不同, 浏览器需要决定什么时候尝试连接、如何排序,以及何时认为加密握手所需的信息已经齐全。 工单中的主要节点如下,日期按香港时区整理:
| 时间 | 进展 | 与本文的关系 |
|---|---|---|
| 2024-01-23 | 增加逐次 DNS 响应提取结果的 NetLog 事件 | 本文使用的 HOST_RESOLVER_DNS_TASK_EXTRACTION_RESULTS 就来自这项工作 |
| 2024-03-15 | 引入 DnsTaskResultsManager,聚合中间结果,并协调 AAAA 的等待时机 |
本文追到的按域名分组与端点组装逻辑在这里 |
| 2024-04-15~16 | 加入 ServiceEndpointRequest,并向公共 HostResolver API 暴露创建接口 |
让上层连接逻辑消费逐步更新的端点 |
| 2024-05~09 | 补充错误信息、优先级调整、IPv6 可达性检查和任务中止处理 | 完善 API 生命周期和兼容行为 |
| 2025-04-21 | 修正没有最终端点时的 EndpointsCryptoReady() 返回值 |
修复握手就绪状态判断 |
| 2026-07-08~10 | 修复异步 IPv6 检查后的完成通知、上下文关闭处理,以及同步错误码归一化 | 继续修复请求时序与生命周期问题 |
| 2026-09-04 | 调整为逐个 DNS 事务排序,再向调用方交付中间结果 | 改进地址优先级,不等于补齐别名处理 |
所以,这条工单有实质进展,而且持续了两年多。可它推进的是端点解析基础设施, 不能把其中任何一个提交当作「CNAME 与 HTTPS 元数据配对问题已修复」。
截至查阅时,主干源码中的 TODO 仍然存在: 当 HTTPS 的目标名字本身是别名时,正确处理需要沿别名树寻找对应关系。 这比单纯按字符串把 metadata 和地址放进同一个桶更复杂。 源码中的例子是在链尾发布 HTTPS、再将 TargetName 指向别名;本文则是在原始域名合成 HTTPS, 同时保留地址查询中的 CNAME 链。两者的记录形状不同,但都触及名字与地址之间的别名映射。
40257146:完整 HTTPS-RR 支持,有实现,尚无落地承诺
更直接跟踪协议能力的是 40257146:Fully implement HTTPS-RR。 这条工单至少从 2023 年 2 月就在跟进,截至查阅时为 P3 Feature Request、New,未分配负责人,也没有目标里程碑。
| 时间 | 公开记录 |
|---|---|
| 2023-02-17 | 从 Bug 调整为 Feature;2 月 21 日曾被提交信息误关,随后立即重新打开 |
| 2023-03-09 | Eric Orth 表示此前已在缓慢推进必要的 HostCache 改动 |
| 2025-09~2026-06 | 外部贡献者多次接手;2026 年 6 月 22 日表示新的缓存解析器已就位,准备再次尝试 |
| 2026-08-11 | 社区要求提高优先级,并列举 TargetName、AliasMode、替代端口与后续地址查询等缺失能力;贡献者表示已准备新一轮 CL |
| 2026-08-14 | 贡献者报告端到端实现可用,拆成五个 CL,提供公开 DNS 测试和预编译包 |
| 2026-08-16~17 | 补充原始域名完全没有 A/AAAA、仅靠地址提示连接,以及 IPv6 测试案例 |
| 2026-08-20 | bashi 澄清支持范围仍未决定,目前无法承诺承担实现或评审这些补丁 |
| 2026-08-30 | 贡献者表示暂缓推进,等待 bashi 的工作落地、架构和可贡献范围明朗 |
| 2026-11-03 | 批量清理长期未更新的赏金工单负责人,状态回到 New;这是管理操作,不是新的技术裁决 |
五个 CL 分别处理地址提示、AliasMode、不同的 TargetName、后续查询和替代端口: 7984025、 7984026、 7984027、 7984028、 8259314。 工单仍将这五项列在待处理代码变更中;贡献者报告测试成功,不能据此认为正式版已经具备这些能力。
这轮讨论的关键是 bashi 的措辞:AliasMode、后续查询和不匹配的 TargetName 属于 「support is still undecided」,并非仅仅被连接层重写阻塞。 团队需要承担内部评审、安全验证和长期维护成本,因此要求直接产品需求,或足够强的生态必要性证据, 才可能为这项工作安排资源。他同时保留了未来重新评估、或由其他 Chromium 团队承接的可能。 这是一项尚未获准投入的功能提案,不能写成永久拒绝支持。原话见 2026 年 8 月 20 日回复。
我的理解是,当前缺口已经不只是「有没有人会写」:外部贡献者愿意拆分、适配架构、补测试并继续维护, 但仍需要项目内部确认支持范围、负责人和评审资源。公开记录没有给出发布时间,不能承诺短期修复, 也不能从中推导出永远不会修复。
相关工单分别说明了什么
| 工单 | 范围与可确认的结论 |
|---|---|
| 40156536 | 2020 年的「复杂情况下未提取 HTTPS 记录」。曾误标 Fixed 后重新打开;公开历史有多次关联变更,但不足以认定本文场景已修复 |
| 388545139 | 「实现仍需要 A 或 AAAA」。2025 年 1 月,报告者反馈其他浏览器可用;bashi 确认嵌套 HTTPS RR 与不同 TargetName 尚不支持,且当时不在优先事项中 |
| 40222704 | 用 HTTPS DNS 记录发现并尝试 HTTP/3 的功能本体。2022 年已有实现提交,2024 年 2 月标记 Fixed;它完成不代表完整 HTTPS-RR 服务发现已经完成 |
| 501792454 | Windows 异步地址排序中的迭代器失效 UAF,2026 年 4 月报告,5 月并入 497588858,8 月解除访问限制。它属于排序与取消操作的生命周期问题 |
最后一条尤其容易看错时间。报告者在 501792454 的评论 2 将引入迭代器捕获的代码追溯到 2024 年 1 月的事务排序改动;41493696 中上述排序策略调整则发生在 2026 年 9 月。 因此不能把它写成「2026 年 8 月,41493696 的那次修复带出了 UAF」。 它确实体现了异步排序、取消和回调之间的维护难度,但公开记录不足以证明它导致团队暂缓完整 HTTPS-RR 支持。
回到本文:压平绕过了哪一层限制
普通 CNAME、HTTPS AliasMode 和 ServiceMode 的 TargetName 是三种不同的机制: CNAME 由 DNS 解析过程追踪;AliasMode 用优先级为 0 的 HTTPS 记录指向下一处服务绑定; ServiceMode 则在优先级大于 0 的记录里描述服务目标与参数。后两者的规则见 RFC 9460 §2.4。 支持普通 CNAME,不代表已经支持完整的 HTTPS-RR 别名与目标解析。
本文遇到的是本地合成 HTTPS 记录与上游地址记录之间的名字不一致。 clean-dns 通过统一 owner,让 Chrome 可以沿现有的同名配对路径得到 HTTP/3 端点; 它没有替浏览器实现 AliasMode、任意 TargetName、替代端口或完整的后续查询。 即使 40257146 将来有补丁落地,也仍要单独验证本文这组「合成 HTTPS + 地址 CNAME」应答, 不能只看工单变成 Fixed 就关闭压平。
七、收尾
这次排查碰到了 Chromium 源码已经注明的别名处理限制,也暴露了本地合成记录与上游 CNAME 链之间的不一致。 暂时无法修改浏览器时,在解析器一侧把名字对齐,是一个直接可用的绕行办法。 代价是客户端不再看到 CNAME 链:本次浏览器访问已验证可用,依赖别名做后续解析的场景则需要单独评估。
等 Chrome 修复相关的别名处理逻辑后,再验证能否关闭压平开关。