Nginx 之四层负载模块 ngx_stream_ssl_preread_module
ngx_stream_ssl_preread_module 是 Nginx 的一个 stream 模块,用于在不终止 SSL/TLS 连接的情况下从 ClientHello 消息中提取信息,比如 SNI(服务器名称指示)、ALPN(应用层协议协商)和支持的最高协议版本。
该模块默认不构建,需要通过 --with-stream_ssl_preread_module 配置参数启用。主要指令是 ssl_preread,变量包括 $ssl_preread_server_name、$ssl_preread_alpn_protocols、$ssl_preread_protocol 等。
模块编译与启用
该模块默认不构建,需要在编译 Nginx 时显式启用:
./configure --with-stream --with-stream_ssl_preread_module
make && make install
编译后可通过 nginx -V 2>&1 | grep stream_ssl_preread 验证模块是否已包含。
核心指令与变量
指令:
ssl_preread on | off; 在 stream { server { ... } } 上下文中使用,开启后 Nginx 会在收到 ClientHello 后提取 SNI、ALPN 等信息。
内置变量:
| 变量名 | 含义 | 引入版本 |
|---|
$ssl_preread_server_name | 从 SNI 扩展中提取的服务器域名(空串表示未提供) | 1.11.5 |
$ssl_preread_alpn_protocols | 客户端在 ALPN 扩展中声明的、逗号分隔的协议列表(如 h2,http/1.1) | 1.13.10 |
$ssl_preread_protocol | 客户端支持的最高 TLS/SSL 版本(如 TLSv1.3;空串表示无 SSL 握手信息) | 1.15.2 |
为编译安装的 Nginx 配置四层负载均衡,可以通过子配置文件的方式实现模块化管理。以下是完整配置方案:我们可以通过 include 指令引入子配置文件。创建子配置文件,例如在 /usr/local/nginx/stream/conf.d/stream.conf,并在该文件中配置四层负载均衡。具体配置示例:主配置文件(nginx.conf)中,在 http 配置块之外,添加 stream 配置块的引入:注意:stream 配置块和 http 配置块是平级的。
mkdir -p /usr/local/nginx/stream/conf.d/
编辑主配置文件 nginx.conf,在 http 块同级添加 stream 配置:
user nginx;
worker_processes auto;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
}
stream {
include /usr/local/nginx/stream/conf.d/*.conf;
log_format basic '$remote_addr [$time_local] "$protocol $status $bytes_sent $bytes_received $session_time $ssl_preread_server_name"';
access_log logs/stream_access.log basic;
error_log logs/stream_error.log;
preread_buffer_size 32k;
preread_timeout 30s;
proxy_connect_timeout 5s;
proxy_timeout 60s;
}
创建存放四层负载均衡的日志目录:
mkdir /usr/local/nginx/logs
/usr/local/nginx/sbin/nginx -t
/usr/local/nginx/sbin/nginx -s reload
配置示例
1. 基于 SNI 的分流
将不同域名的请求路由到不同的上游服务器:
基于 TLS SNI (Server Name Indication) 的四层 TCP/UDP 代理配置,用于 透明地转发 HTTPS 流量,而无需解密其内容。它利用了 Nginx 的 stream 模块(常用于代理 TCP/UDP 流量)和 ssl_preread 指令。
该配置的核心是:根据客户端 TLS 握手时发出的 SNI 主机名,将加密的 HTTPS 流量转发到不同的后端服务器。
1. 配置结构 (map 块):
map $ssl_preread_server_name $backend {
www.test1.com backend1;
www.test2.com backend2;
default backend_default;
}
- 目的:建立一个映射表。
- 变量:
$ssl_preread_server_name 是 ssl_preread 模块提取出的 SNI 主机名。
- 逻辑:检查 SNI 值。如果是
www.test1.com,则将变量 $backend 的值设置为 backend1;如果是 www.test2.com,则设置为 backend2;其他所有情况(即 SNI 不匹配或为空),则设置为 backend_default。
2. 上游定义 (upstream 块):
upstream backend1 {
server www.test1.com:443;
}
upstream backend2 {
server www.test2.com:443;
}
upstream backend_default {
server 10.0.0.3:443;
}
- 定义了三个上游服务器组。当流量需要被转发时,Nginx 会连接到这里定义的地址。
backend1 和 backend2 直接代理到了互联网上的真实服务。
backend_default 是一个默认的或内部的后端,例如一个默认的网站、一个错误页面服务器,或者一个用于处理的中间服务(如 WAF)。
3. 服务器块 (server 块):
server {
listen 443;
proxy_pass $backend;
ssl_preread on;
}
listen 443;:在 TCP 443 端口上监听(这是 HTTPS 的标准端口)。
ssl_preread on;:关键指令。它启用了一个'预读'机制。Nginx 会在不进行 TLS 解密的情况下,从客户端发来的第一个数据包(Client Hello)中提取 SNI 信息。这个过程不会破坏 TLS 的加密性。
proxy_pass $backend;:将接收到的所有流量转发到 $backend 变量所指向的上游服务器组。这个变量的值由第一步的 map 规则根据 SNI 决定。
工作原理与数据流
- 客户端连接:一个客户端(如浏览器)尝试连接到
www.test1.com 的 443 端口。
- 发送 Client Hello:客户端发起 TLS 握手,在
Client Hello 消息中携带 SNI 字段,明文指出它想要连接的主机名是 www.test1.com。
- Nginx 预读 SNI:Nginx 在
stream 层(四层)接收到这个 TCP 数据包。ssl_preread on; 指令使其能够解析 Client Hello 并提取出 www.test1.com。
- 映射决策:提取出的 SNI (
www.test1.com) 被赋值给变量 $ssl_preread_server_name。map 规则根据这个值进行匹配,决定 $backend 的值为 backend1。
- 透明转发:
proxy_pass $backend; 将整个 TCP 流(包含后续加密的 TLS 应用数据)转发给 upstream backend1 中定义的服务器 www.test1.com:443。
- 完成连接:客户端与真正的
www.test1.com 服务器完成后续的 TLS 握手和加密通信,而 Nginx 在整个过程中只是作为透明的'管道',无法看到解密后的 HTTP 内容。
配置用途与场景
- SSL 卸载网关/反向代理:可以作为多个后端 HTTPS 服务的统一入口,根据域名将请求分发到不同的服务器,而无需在每个后端服务器上配置 SSL 证书。
- 隧道或透明代理:常用于需要根据域名路由加密流量的场景,如在企业内部网络中,将特定域名的流量引导至安全审查设备或代理服务器。
- 负载均衡的前置路由:可以先根据 SNI 进行粗略路由,再在每个
upstream 块内定义多个 server 实现更细粒度的负载均衡。
重要特性
- 工作在四层 (Transport Layer):这是在 TCP 层面进行的代理,不涉及七层(HTTP)协议。
- 不解密 TLS:
ssl_preread 只读取握手初期的明文部分,全程不解密应用层数据,因此无需配置 SSL 证书和私钥。
完整配置
cd /usr/local/nginx/stream/conf.d/
vim server_proxy.conf
# SNI 到后端映射
map $ssl_preread_server_name $backend {
www.test1.com backend1;
www.test1.com backend2;
default backend_default;
}
upstream backend1 {
server www.test1.com:443;
}
upstream backend2 {
server www.test2.com:443;
}
upstream backend_default {
server 10.0.0.3:443;
}
server {
listen 443;
proxy_pass $backend;
ssl_preread on;
}
/usr/local/nginx/sbin/nginx -s reload
客户端验证:
curl -v --resolve www.test1.com:443:172.16.229.44 https://www.baidu.com
curl -v --resolve www.test2.com:443:172.16.229.44 https://www.jd.com/
2. 基于 ALPN 协议的分流
根据客户端声明的应用层协议(如 HTTP/2、HTTP/1.1)进行路由:
stream {
map $ssl_preread_alpn_protocols $proxy {
~\bh2\b 127.0.0.1:8001; # HTTP/2
~\bhttp/1.1\b 127.0.0.1:8002; # HTTP/1.1
~\bxmpp-client\b 127.0.0.1:8003; # XMPP
}
server {
listen 9000;
proxy_pass $proxy;
ssl_preread on;
}
}
3. 单端口同时支持 HTTP 与 HTTPS
通过检测是否包含 SSL 握手信息,将流量分别导向 HTTP 或 HTTPS 后端:
map 块:
map $ssl_preread_protocol $back {
"" http_backend;
default https_backend;
}
这里定义了一个映射,根据变量 $ssl_preread_protocol 的值来决定 $back 变量的值。 $ssl_preread_protocol 是 ssl_preread 模块提供的变量,用于获取客户端 TLS 握手中 ClientHello 消息的协议版本(如 TLSv1.2、TLSv1.3 等)。如果 $ssl_preread_protocol 的值为空(即非 TLS 流量),则 $back 的值为 http_backend。如果 $ssl_preread_protocol 的值非空(即 TLS 流量),则 $back 的值为 https_backend。
upstream 块:
upstream http_backend {
server www.testpm.cn:80;
}
upstream https_backend {
server www.test1.com:443;
}
定义了两个上游服务器组。 http_backend:指向一个 HTTP 服务器(www.testpm.cn 的 80 端口)。 https_backend:指向一个 HTTPS 服务器(www.test1.com 的 443 端口)。
server 块:
server {
listen 8081;
proxy_pass $back;
ssl_preread on;
}
监听 8081 端口。 ssl_preread on;:启用 ssl_preread 功能,允许 Nginx 在转发之前读取 TLS 握手的初始信息(如协议版本、SNI 等),而不进行解密。 proxy_pass $back;:将流量转发到由 $back 变量指定的上游服务器组。
完整配置:
cd /usr/local/nginx/stream/conf.d/
vim server_proxy.conf
map $ssl_preread_protocol $back {
"" http_backend;
default https_backend;
}
upstream http_backend {
server www.testpm.cn:80;
}
upstream https_backend {
server www.test1.com:443;
}
server {
listen 8081;
proxy_pass $back;
ssl_preread on;
}
vi /etc/hosts
客户端验证:
curl -vl http://172.16.229.44:8081
curl -vlk https://172.16.229.44:8081
4. TCP/UDP 混合协议分流
stream {
map $ssl_preread_protocol $upstream {
"" tcp_backend; # 非 SSL 流量
"TLSv1.2" tls_backend; # TLS 1.2 流量
"TLSv1.3" tls_backend; # TLS 1.3 流量
default default_backend;
}
upstream tcp_backend {
server 10.0.1.1:22; # SSH
}
upstream tls_backend {
server 10.0.1.2:443; # HTTPS
}
upstream default_backend {
server 10.0.1.3:8080;
}
server {
listen 8443;
proxy_pass $upstream;
ssl_preread on;
}
}
5. 灰度发布/金丝雀发布
stream {
# 按域名权重分流
split_clients "${ssl_preread_server_name}${remote_addr}" $backend {
10% canary_backend;
* production_backend;
}
upstream canary_backend {
server 10.0.2.1:443;
}
upstream production_backend {
server 10.0.2.2:443;
}
server {
listen 443 reuseport; # 支持端口复用,前提是在 stream 块中已经开启
proxy_pass $backend;
ssl_preread on;
}
}
注意事项
- 该模块工作在 stream 上下文(四层代理),与 http 模块的 SSL 处理无关。
- 启用
ssl_preread on 后,Nginx 不会解密 SSL 流量,仅提取 ClientHello 中的信息,因此无法获取 HTTP 头部等应用层数据。
- 若配置后出现
unknown directive 'ssl_preread' 错误,说明模块未正确编译,需按上述步骤重新编译 Nginx。
通过该模块,可以在保持 SSL 加密完整性的前提下,实现基于域名、协议版本等信息的灵活路由,适用于网关、负载均衡等场景。