同一个 IP 上藏着成千上万个网站,TLS 靠 SNI 找到该给哪张证书,靠 ALPN 决定接着说 HTTP/1.1 还是 HTTP/2。这两个小小的扩展还各有隐私和安全的含义:SNI 是明文泄露你要访问的站点,SNI 与 Host 不一致会被拒绝,ECH 想把 SNI 也加密。这篇逐项实测。
第 1 个有载荷的包 C→S 1388 字节:含明文 example.com,偏移 130 第 2 个有载荷的包 C→S 146 字节:没有明文 example.com 第 3 个有载荷的包 S→C 1440 字节:没有明文 example.com 第 4 个有载荷的包 S→C 1440 字节:没有明文 example.com 第 5 个有载荷的包 S→C 1440 字节:没有明文 example.com 第 6 个有载荷的包 S→C 760 字节:没有明文 example.com 第 7 个有载荷的包 C→S 80 字节:没有明文 example.com 第 8 个有载荷的包 S→C 544 字节:没有明文 example.com 第 9 个有载荷的包 C→S 78 字节:没有明文 example.com 第 10 个有载荷的包 S→C 39 字节:没有明文 example.com 整个连接里含明文 "example.com" 的包共 1 个(其中 C→S 1 个、S→C 0 个)——只有 ClientHello 一个;证书、Host 头、响应都在加密记录里 所以路径上的任何设备都能读出你要访问哪个站点,即使内容是加密的
ClientHello 里的 SNI 是明文,路径上的设备能读出你访问的站点。
SNI = example.com,Host: example.com → HTTP/1.1 200 OK;页面标题 Example Domain;928 字节 SNI = example.com,Host: example.org → HTTP/1.1 403 Forbidden;页面标题 403 Forbidden;329 字节 SNI = example.com,Host: www.iana.org → HTTP/1.1 403 Forbidden;页面标题 403 Forbidden;329 字节 SNI = example.com,Host: nonexistent-w92.example→ HTTP/1.1 403 Forbidden;页面标题 403 Forbidden;329 字节
Cloudflare 检查两者一致,不一致返回 403,域前置行不通。
客户端提供 ['h2', 'http/1.1'] → 选定 h2 客户端提供 ['http/1.1', 'h2'] → 选定 h2 客户端提供 ['h2'] → 选定 h2 客户端提供 ['http/1.1'] → 选定 http/1.1 客户端提供 ['h3'] → 选定 None 客户端提供 ['foo'] → 选定 None 客户端提供 ['foo', 'http/1.1'] → 选定 http/1.1 客户端提供 None → 选定 None ALPN 列表的顺序:规范里是服务器按自己的偏好在客户端列表里挑一个,所以客户端把 http/1.1 写在前面不一定得到 http/1.1(看上面前两行)
服务器按自己的偏好选协议;没有共同协议时握手不报错、只是不选。
example.com :HTTPS 记录参数 ['alpn', 'ipv4hint', 'ipv6hint'];没有 ech cloudflare.com :HTTPS 记录参数 ['alpn', 'ipv4hint', 'ipv6hint'];没有 ech www.python.org :没有 HTTPS 记录 github.com :没有 HTTPS 记录 本机 Python 的 ssl 不支持 ECH,所以上面第一节看到的 SNI 仍然是明文;浏览器支持时,会先查 HTTPS 记录里的 ech 配置,把真实的 SNI 放在加密的 ClientHello 内层
要靠 DNS 的 HTTPS 记录里的 ech 参数,这几个站点没有发布。