什么是 SNI?

SNI(Server Name Indication,服务名称指示) 是 TLS 协议的一个扩展,允许客户端在 TLS 握手开始时提前声明要访问的服务器域名。这一看似简单的机制,彻底解决了 HTTPS 早期的核心痛点——单 IP 多域名 问题。

为什么需要 SNI?

在 SNI 出现之前,TLS 握手存在一个困境:

  1. 客户端发送 ClientHello 消息时,还没有指定要访问的具体域名
  2. 服务器收到 ClientHello 后,无法确定客户端想要哪个虚拟主机
  3. 服务器只能返回一个"默认"证书,无法为每个域名提供正确证书

这意味着:

  • 一个 IP 地址只能部署一个 HTTPS 网站
  • 要托管多个 HTTPS 网站,必须为每个网站分配独立 IP

SNI 的出现改变了这一局面。客户端在 ClientHello 中直接携带 server_name 扩展,声明目标域名,服务器据此返回对应证书。

工作原理

TLS 握手中的 SNI

SNI 作为 TLS 扩展(Extension),在 ClientHello 消息中发送:

1
2
3
4
5
6
7
8
9
ClientHello
├── Version: TLS 1.2 / TLS 1.3
├── Random: ...
├── Session ID: ...
├── Cipher Suites: ...
├── Extensions: ...
│   ├── server_name: example.com    ← SNI 扩展
│   ├── supported_versions: TLS 1.3
│   └── ...

服务器收到后,匹配 server_name 值,返回对应的证书链。

实际验证

使用 OpenSSL 可以查看 SNI 在握手中的应用:

1
2
3
4
5
# 指定 SNI 访问目标域名
openssl s_client -connect www.example.com:443 -servername www.example.com

# 不带 SNI(-noservername 参数)
openssl s_client -connect www.example.com:443 -noservername

对比两种方式的握手结果,观察证书返回的差异。

SNI 的格式

SNI 在 TLS 扩展中采用特定编码:

字段
Extension Type 0x0000 (host_name)
Server Name Type 0x00 (DNS hostname)
Server Name 目标域名(如 www.example.com

域名以 ASCII 编码传输,这意味着任何观察网络流量的第三方都能轻易得知用户访问的网站。

SNI 与隐私问题

隐私泄露风险

SNI 是明文传输的。 这带来一个重要隐私问题:

  • 网络观察者(如 ISP、WiFi 热点运营者、政府监控)可以轻易看到用户访问的域名
  • 即使流量本身已加密,SNI 仍然以明文形式暴露在网络层
  • 这对于追求极致隐私的用户来说是一个明显漏洞

这与 HTTPS 加密整个连接的目的形成了微妙矛盾——用户访问的域名本身被暴露。

解决方案:加密 SNI (ESNI/ECH)

针对 SNI 隐私问题,业界提出了两种方案:

1. ESNI (Encrypted SNI)

ESNI 将 SNI 加密后再传输,密钥从 DNS TXT 记录中获取:

  • 服务器发布公钥到 DNS
  • 客户端从 DNS 获取公钥,加密 SNI 后发送
  • 只有目标服务器能解密 SNI

但 ESNI 存在一个问题:DNS 查询仍然是明文的,攻击者仍可通过 DNS 看到目标域名。

2. ECH (Encrypted Client Hello)

ECH 是 ESNI 的升级版,被纳入 TLS 1.3 规范(RFC 8446):

  • SNI 被加密,且加密密钥由服务器通过 DNS 记录发布
  • 客户端使用服务器公钥加密完整的 ClientHello(包括 SNI)
  • 即使 DNS 查询被监听,也无法得知目标域名

ECH 正在逐步部署,是隐私保护的推荐方案。

OpenSSL 中的 SNI 使用

基本用法

1
2
3
4
5
6
7
8
# 使用 SNI 连接
openssl s_client -connect example.com:443 -servername example.com

# 查看连接详情
openssl s_client -connect example.com:443 -servername example.com -showcerts

# 调试模式,查看完整握手
openssl s_client -connect example.com:443 -servername example.com -debug

脚本化检测

批量检测多个域名的 SNI 支持:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#!/bin/bash
# 检查域名是否正确返回证书

check_sni() {
    local domain=$1
    local cert=$(echo | openssl s_client -connect "$domain:443" -servername "$domain" 2>/dev/null | openssl x509 -noout -subject 2>/dev/null)
    echo "$domain: $cert"
}

# 使用示例
check_sni "www.example.com"
check_sni "api.example.com"

常见问题排查

问题:连接返回错误的证书

1
2
# 检查 SNI 是否正确发送
openssl s_client -connect $IP:443 -servername $DOMAIN -connect $IP:443

确保 -servername 参数与实际访问域名一致。

SNI 配置注意事项

服务器端

  1. 确保配置完整:Web 服务器(Nginx/Apache)需正确配置 SNI
  2. 注意顺序:默认证书应放在配置首位
  3. 兼容性考虑:旧版客户端可能不支持 SNI,需保留 fallback 方案

客户端

  1. 正确设置 SNI:程序化访问 HTTPS 时,确保发送正确的 server_name
  2. 不要禁用 SNI:除非有特殊需求,否则会影响大多数现代网站的访问

总结

SNI 是现代 HTTPS 不可或缺的基础设施,解决了单 IP 多域名问题,使 HTTPS 的大规模部署成为可能。然而,其明文传输特性带来了隐私风险。

随着 ECH(Encrypted Client Hello)的逐步推广,SNI 的隐私问题将得到根本性解决。但在当前阶段,部署 SNI 仍是 HTTPS 服务的必修课。


参考来源