bigywater.tech
网络协议 · 92

SNI 与 ALPN:同一个 IP 上的多个站点与协议协商

同一个 IP 上藏着成千上万个网站,TLS 靠 SNI 找到该给哪张证书,靠 ALPN 决定接着说 HTTP/1.1 还是 HTTP/2。这两个小小的扩展还各有隐私和安全的含义:SNI 是明文泄露你要访问的站点,SNI 与 Host 不一致会被拒绝,ECH 想把 SNI 也加密。这篇逐项实测。

偏移 130
明文 SNI "example.com" 在 ClientHello 里的位置;其余 9 个有载荷的包都找不到它
403
SNI 是 example.com、HTTP Host 写 example.org / www.iana.org 时 Cloudflare 的回应
选定 h2
客户端把 http/1.1 写在 ALPN 列表前面时服务器仍选的协议

明文 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 与 Host

实测 · 本机执行输出
  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,域前置行不通。

ALPN

实测 · 本机执行输出
  客户端提供 ['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(看上面前两行)

服务器按自己的偏好选协议;没有共同协议时握手不报错、只是不选。

ECH

实测 · 本机执行输出
  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 参数,这几个站点没有发布。

分步演示

SNI 与 ALPN
本步的对象

    本文用到的工具与边界

    • 本机 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,数字每次运行会有小幅抖动。对 example.com:443 的真实连接(抓包只在第一节);第二节我第一次跑时请求格式写错得到 400,文章用的是修正后的结果;SNI 与 Host 不一致被拒是这一个服务器的行为;本机 Python 不支持 ECH
    • 没有验证:ECH 本身(没有支持它的客户端);SNI 为空或为 IP 地址时的服务器行为(只测了无 SNI 在 w91 里);其它 CDN 的域前置;ALPN 的 h2c、acme-tls/1 等其它协议标识;QUIC 里 ALPN 的作用(w86 只提到 h3)
    京ICP备2026067502号-1京公网安备11010502063487号