SNI(Server Name Indication)是一项 TLS 扩展,允许客户端在 TLS 握手过程中指明自己要访问的主机名,从而使代理能够为托管在同一个 IP 地址上的多个域名正确路由或终止 TLS 连接。
在 SNI 出现之前,如果同一台服务器上托管多个 HTTPS 网站,那么每张 SSL 证书都需要一个独立的 IP 地址。这个限制的根源在于:TLS 握手发生在 HTTP Host 头发送之前,服务器无从得知该出示哪张证书。RFC 6066 定义的 SNI 通过在 ClientHello 消息中携带目标主机名解决了这一问题,使服务器(或代理)能够选择正确的证书和配置。
SNI 的工作原理
客户端把目标主机名放在 ClientHello 消息内 server_name 扩展的 extension_data 字段中。这发生在任何加密数据交换之前,也在服务器出示证书之前。
ClientHello
Version: TLS 1.2
Random: ...
Session ID: ...
Cipher Suites: ...
Extensions:
...
Server Name (SNI)
Server Name Type: host_name (0)
Server Name: example.com
...
服务器或中间代理收到这个 ClientHello 后即可提取出 example.com 主机名。随后该信息被用于决定出示哪张 TLS 证书,以及如何路由或处理后续连接。
代理对 SNI 的处理
代理与 SNI 的交互方式取决于其类型、运行模式,以及它执行的是 TLS 终止还是透传。
透明代理
透明代理在客户端未显式配置使用它的情况下拦截流量。对于 TLS 流量,透明代理通常工作在第 4 层(TCP)。它一般把原始 TCP 流(包括携带 SNI 的 ClientHello 消息)直接转发给目标服务器。
- SNI 可见性:透明代理可以检查
ClientHello消息以提取 SNI 主机名。即使负载本身仍然加密,这也能实现基于目标域名的基本路由、日志记录或过滤规则。 - TLS 终止:除非专门为深度包检测(DPI)或中间人(MITM)操作配置——这要求客户端信任代理的根 CA——透明代理通常不会为客户端终止 TLS。在标准透明部署中,代理把
ClientHello传给源站服务器,由源站直接与客户端完成 TLS 握手。
正向代理
正向代理作为客户端向外部服务器请求资源时的中介。客户端会被显式配置为使用正向代理。
CONNECT 方法
对于 HTTPS 流量,客户端通常使用 HTTP CONNECT 方法,指示正向代理建立一条通往目标服务器的 TCP 隧道。
CONNECT example.com:443 HTTP/1.1
Host: example.com:443
Proxy-Connection: Keep-Alive
CONNECT 请求成功后(代理返回 HTTP/1.1 200 Connection established),客户端通过已建立的隧道直接与目标服务器发起 TLS 握手。
- SNI 可见性(无拦截时):在这种模式下,正向代理通常不会处理
ClientHello中的 SNI 值,因为它只是在转发原始的加密 TCP 流。目标主机名由CONNECT请求提供,代理据此进行路由。 - TLS 终止:代理不终止 TLS;TLS 握手在客户端与源站服务器之间端到端完成。
TLS 拦截(MITM)
一些正向代理被配置为执行 TLS 拦截(也称 SSL 检查或 MITM 代理)。其做法是代理终止客户端的 TLS 连接,并向源站服务器另建一条新的 TLS 连接。
- 客户端与代理发起 TLS 握手:客户端向代理发送
ClientHello。 - 代理提取 SNI:代理从
ClientHello中读取 SNI 主机名。 - 代理生成证书:代理使用自己的 CA 证书(客户端已信任),为该 SNI 主机名动态生成一张服务器证书。
- 代理与客户端完成握手:代理把生成的证书出示给客户端。
- 代理与源站建立新的 TLS 连接:代理在自己的
ClientHello中使用提取到的 SNI 主机名,与真实源站服务器发起新的 TLS 握手。 - 代理解密/重新加密流量:代理解密客户端流量、进行检查,然后在发往源站前重新加密,反向亦然。
- SNI 的用途:SNI 对 TLS 拦截至关重要。代理使用 SNI 值来:
- 确定对客户端冒充哪个主机名。
- 确定向源站服务器请求哪个主机名。
- 安全影响:要求客户端信任代理的根 CA。该模式让代理对加密流量拥有完全可见性。
反向代理
反向代理位于一台或多台 Web 服务器之前,把客户端请求导向合适的后端服务器。客户端连接的是反向代理,并不了解后端架构。
- SNI 的用途:当客户端与反向代理发起 TLS 握手时,代理会收到包含 SNI 主机名的
ClientHello消息。反向代理使用该 SNI 值来:- 在托管多个域名时,选择要出示给客户端的正确服务器证书。
- 把请求路由到合适的后端服务器或应用池。
- 应用域名专属策略(例如 WAF 规则、缓存、负载均衡)。
- TLS 终止:反向代理几乎总是终止来自客户端的 TLS 连接。它们解密流量、进行处理,随后可能为与后端服务器通信而重新加密(重新加密,即后端 TLS)。
示例:带 SNI 的 Nginx 反向代理
http {
# Default server for requests without SNI or for unknown SNI
server {
listen 443 ssl default_server;
server_name _; # Catch-all
ssl_certificate /etc/nginx/ssl/default.crt;
ssl_certificate_key /etc/nginx/ssl/default.key;
return 404;
}
# Backend for example.com
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/nginx/ssl/example.com.crt;
ssl_certificate_key /etc/nginx/ssl/example.com.key;
location / {
proxy_pass https://backend_example_com;
proxy_ssl_server_name on; # Pass SNI to backend
}
}
# Backend for api.example.com
server {
listen 443 ssl;
server_name api.example.com;
ssl_certificate /etc/nginx/ssl/api.example.com.crt;
ssl_certificate_key /etc/nginx/ssl/api.example.com.key;
location / {
proxy_pass https://backend_api_example_com;
proxy_ssl_server_name on; # Pass SNI to backend
}
}
}
在这份 Nginx 配置中,server_name 指令与客户端提交的 SNI 相匹配。Nginx 随后使用对应的 ssl_certificate,并把请求路由到指定的 proxy_pass 后端。proxy_ssl_server_name on 确保 Nginx 在与后端建立新的 TLS 连接时携带 SNI 主机名。
SNI 与四层、七层代理
第 4 层(TCP)代理与第 7 层(应用层)代理的区别也会影响 SNI 的处理方式。
-
四层代理:工作在 TCP 层。它们可以检查
ClientHello数据包以提取 SNI,而无需解密整条 TLS 流。这使得在 TLS 握手完成之前即可基于 SNI 做路由或过滤,且无需终止 TLS。TCP 模式下的 HAProxy 可以做到这一点。listen https_frontend bind *:443 mode tcp tcp-request inspect-delay 5s tcp-request content accept if { req_ssl_hello_type 1 } tcp-request content track-sc0 src # Route based on SNI use_backend backend_example_com if { req_ssl_sni -i example.com } use_backend backend_api_example_com if { req_ssl_sni -i api.example.com } default_backend backend_default
这份 HAProxy 配置使用req_ssl_sni检查ClientHello中的 SNI,并据此路由 TCP 连接,自身并不终止 TLS。 -
七层代理:工作在应用层(例如 HTTP/HTTPS)。它们必须终止 TLS 才能访问应用层头部(如 HTTP Host、URL 路径等)。终止 TLS 时,它们使用最初
ClientHello中的 SNI 选择正确的服务器证书,然后继续处理解密后的 HTTP 请求。反向代理通常是 L7 代理,执行 TLS 拦截的正向代理同样是 L7。
对比:代理的 SNI 处理模式
| 特性 | 透明代理(L4 透传) | 正向代理(CONNECT,无 MITM) | 正向代理(TLS 拦截/MITM) | 反向代理(TLS 终止) |
|---|---|---|---|---|
| 客户端 TLS 终止于 | 源站服务器 | 源站服务器 | 代理 | 代理 |
| 代理 TLS 终止于 | 不适用(透传) | 不适用(透传) | 源站服务器 | 不适用(若使用后端 TLS 则另建连接) |
| 代理可见 SNI | 是(ClientHello 中明文) | 否(CONNECT 之后) | 是(ClientHello 中明文) | 是(ClientHello 中明文) |
| 代理对 SNI 的用途 | 路由、日志、基础过滤 | 不直接来自 ClientHello | 生成证书、选择源站 | 选择证书、路由、执行策略 |
| 代理是否解密流量 | 否 | 否 | 是 | 是 |
| 客户端信任要求 | 无 | 无 | 代理的根 CA | 源站证书(由代理出示) |
安全与隐私考量
SNI 在设计上以明文形式包含在 ClientHello 消息中传输。这意味着包括代理和 ISP 在内的网络中间方,即使在 TLS 通信其余部分已加密的情况下,仍能观察到客户端试图连接哪个主机名。
这种明文暴露带来隐私影响。为此,IETF 开发了 Encrypted SNI(ESNI),并正演进为 Encrypted Client Hello(ECH)。ESNI/ECH 会加密 ClientHello 消息中的 Server Name 扩展,使其对被动观察者不可读。
- 对代理的影响:ESNI/ECH 会显著改变代理(尤其是 L4 代理,或在不完全终止 TLS 的情况下做 SNI 路由的代理)的工作方式。如果 SNI 被加密,代理就无法用它做明文路由决策。支持 ESNI/ECH 的代理需要参与密钥交换,或依赖其他机制(例如 IP 地址、DNS)完成初始路由;若自身就是目标 ECH 端点,则可解密 ECH。对于终止 TLS 的代理(如反向代理或 MITM 正向代理),它们仍需解密 ECH 才能获知目标主机名,用于选择证书和路由。
