公网 IPv4 地址稀缺,一个 IP 的 443 却要塞下博客、DoH API、后台……SNI 分流让它们各走各的后端,互不打架。
SNI(Server Name Indication)是 TLS 握手时客户端在 ClientHello 报文里携带的 server_name 扩展(见 RFC 6066),告诉服务器"我要访问哪个域名"。用 nginx 的 stream 模块 + ssl_preread on;,可以在不解密 TLS 的前提下,仅凭 ClientHello 里的域名把 443 端口流量转发到不同后端:博客主站走默认后端,dns.23-4.cn 的 DoH 服务走本机 1443 端口。分流层全程不碰证书,证书由各后端自己持有。
没有 SNI 之前,一个 IP:443 只能对应一个证书、一个网站
TLS 握手发生在 HTTP 请求之前,服务器在看到 Host 头之前就要选择用哪张证书。如果一个 IP 上只绑一个 443,就只能服务一个 HTTPS 站点——IPv4 地址根本不够用。
客户端在最开始的 ClientHello 里就把目标域名写进 server_name 扩展。服务器(或前置的分流层)据此选择对应证书与后端,于是一个 IP:443 可以同时服务多个域名。
DoH 服务本身也是 HTTPS。当 dns.23-4.cn/dns-query 与博客共用一个 IP 的 443 时,就是靠 SNI 告诉 nginx"这条流量属于 DoH,转给 1443 后端"。
关键:在 stream 块里读 $ssl_preread_server_name,按它 proxy_pass 到不同后端
stream {
# 按 SNI 把流量映射到不同后端
map $ssl_preread_server_name $sni_backend {
default 127.0.0.1:8443; # 默认:博客主站
dns.23-4.cn 127.0.0.1:1443; # DoH 服务
}
server {
listen 443;
ssl_preread on; # 只预判 ClientHello,不解密
proxy_pass $sni_backend;
proxy_timeout 300s;
}
}
说明:ssl_preread 来自 ngx_stream_ssl_preread_module(官方文档见 nginx.org),它解析 ClientHello 里的 server_name 赋给 $ssl_preread_server_name;proxy_pass 行为见 ngx_stream_proxy_module。注意 stream 与 http 是 nginx 两个独立上下文,stream 配置通常放在 nginx.conf 顶层。
| 客户端访问 | SNI (server_name) | 分流后到达的后端 |
|---|---|---|
| https://23-4.cn 博客 | 23-4.cn | 127.0.0.1:8443(博客) |
| https://dns.23-4.cn/dns-query | dns.23-4.cn | 127.0.0.1:1443(DoH) |
| 其他未知域名 | 任意 | 127.0.0.1:8443(默认兜底) |
这是 ssl_preread 方案最省心的一点
它只做"看一眼 ClientHello、转发 TCP 流",不终止 TLS,因此不需要任何证书,也不用管证书过期续签。
自己持有 23-4.cn 证书(Let's Encrypt 等),在自己的 https server 块里 ssl_certificate 配置。
自己持有 dns.23-4.cn 证书。因为客户端是按 SNI 找到它的,它出示对应域名证书即可通过校验。
不猜,用工具看握手
openssl s_client -connect 服务器IP:443 -servername dns.23-4.cn,看返回的证书 CN/SAN 是不是 dns 子域。若不是,说明 SNI 没路由到 DoH 后端。
nginx -t 验证语法;确认 stream 块已被加载(部分发行版需 --with-stream 或动态模块)。重载 systemctl reload nginx。
访问一次目标域名,看 1443 / 8443 后端的访问日志有没有收到记录。收到 = 分流正确;没收到 = map 规则或 proxy_pass 写错。
极少数老客户端/调试工具不带 SNI,会落到 default 后端。排障时优先用现代浏览器或带 -servername 的 openssl。
server_name 扩展(RFC 6066 定义),告诉服务器自己想访问哪个域名。服务器据此在同一个 IP:443 上选择对应证书与后端。openssl s_client -connect 服务器IP:443 -servername 测试域名,看握手返回的证书是不是该域名的;或看 nginx error.log / 后端访问日志确认流量被转发到了预期后端。