云计算百科
云计算领域专业知识百科平台

Nginx四层代理通过SNI实现不同的域名请求到不同的上游服务器 nginx之ngx_stream_ssl_preread_module模块使用

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验证模块是否已包含

[root@nginx01 ~]# /usr/local/nginx/sbin/nginx -V 2>&1 | grep stream_ssl_preread
configure arguments: –prefix=/usr/local/nginx –group=nginx –user=nginx –sbin-path=/usr/local/nginx/sbin/nginx –conf-path=/etc/nginx/nginx.conf –error-log-path=/var/log/nginx/error.log –http-log-path=/var/log/nginx/access.log –http-client-body-temp-path=/tmp/nginx/client_body –http-proxy-temp-path=/tmp/nginx/proxy –http-fastcgi-temp-path=/tmp/nginx/fastcgi –pid-path=/var/run/nginx.pid –lock-path=/var/lock/nginx –with-compat –with-file-aio –with-threads –with-http_addition_module –with-http_auth_request_module –with-http_dav_module –with-http_flv_module –with-http_gunzip_module –with-http_gzip_static_module –with-http_mp4_module –with-http_random_index_module –with-http_realip_module –with-http_secure_link_module –with-http_slice_module –with-http_ssl_module –with-http_stub_status_module –with-http_sub_module –with-http_v2_module –with-mail –with-mail_ssl_module –with-stream –with-stream_realip_module –with-stream_ssl_module –with-stream_ssl_preread_module –with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong –param=ssp-buffer-size=4 -grecord-gcc-switches -m64 -mtune=generic -fPIC' –with-ld-opt='-Wl,-z,relro -Wl,-z,now -pie'

核心指令与变量

指令:

  • 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配置块是平级的

[root@nginx01 ~]# mkdir -p /usr/local/nginx/stream/conf.d/ #用于存放四层负载均衡的子配置文件目录

编辑主配置文件 nginx.conf,在 http块同级添加 stream配置:

代理nginx-172.16.229.44
[root@nginx01 ~]# vim /etc/nginx/nginx.conf
user nginx;
worker_processes auto;
events {
worker_connections 1024;
}

# HTTP 层配置
http {
include mime.types;
default_type application/octet-stream;
# … 其他 http 配置
}
# 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;

#性能优化建议
# 启用端口复用(Linux 3.9+)如果需要
listen 443 reuseport;

# 调整预读缓冲区大小
preread_buffer_size 32k;
preread_timeout 30s;

# 连接优化
proxy_connect_timeout 5s;
proxy_timeout 60s;

}

#创建存放四层负载均衡的日志目录
[root@nginx01 ~]# mkdir /usr/local/nginx/logs

[root@nginx01 ~]# /usr/local/nginx/sbin/nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
[root@nginx01 ~]# /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 证书和私钥。
    完整配置

    [root@nginx01 ~]# cd /usr/local/nginx/stream/conf.d/
    [root@nginx01 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;
    }

    [root@nginx01 ~]# /usr/local/nginx/sbin/nginx -s reload

    客户端验证
    [root@web01 ~]# curl -v –resolve www.test1.com:443:172.16.229.44 https://www.baidu.com
    [root@web01 ~]# 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变量指定的上游服务器组。

    完整配置

    [root@nginx01 ~]# cd /usr/local/nginx/stream/conf.d/
    [root@nginx01 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;
    }
    [root@nginx01 ~]# vi /etc/hosts
    172.16.229.47 www.testpm.cn #做本地解析域名到测试虚拟机ip

    客户端验证
    [root@web01 ~]# curl -vl http://172.16.229.44:8081
    [root@web01 ~]# 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;
    }
    }

  • 灰度发布/金丝雀发布
  • 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 加密完整性的前提下,实现基于域名、协议版本等信息的灵活路由,适用于网关、负载均衡等场景。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Nginx四层代理通过SNI实现不同的域名请求到不同的上游服务器 nginx之ngx_stream_ssl_preread_module模块使用
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!