使用Nginx进行SNI分流

前言

众所周知,Nginx 是一款轻量小巧、功能强大、占用资源低的 WEB 服务器,但 Nginx 不只是 WEB 服务器,还可进行反向代理、负责集群负载均衡、更甚可以编写 Lua 脚本,将其嵌入到 Nginx 中完成更为复杂的操作。除了这些在 Http 层面上的应用,就没有其它了吗? 当然不!Nginx 不止可以在 OSI 协议中的第七层应用层上,还可以直接工作在第四层传输层上,直接将第四层的 TCP 流量进行转发。

传输层中的安全行协议( 即 TLS )是一款工作在传输层上重要的安全协议,它可以为互联网通信提供安全及数据完整性保障,HTTPS等安全传输就是基于 TLS 进行。

服务器名称指示 ( 即 SNI )是 TLS 的一个扩展协议,在该协议下,握手过程开始时候客户端告诉它正在连接的服务器要连接的主机名称。Nginx 就可以利用 stream 模块基于 SNI 在同一端口不同主机名的 TLS 流量进行分流。如果您有一款基于 TLS 的应用,想运行在 443 端口上,而 443 端口已被 Nginx 用作监听 WEB 网站,您就可以使用 Nginx 的 SNI 分流,将 443 端口复用。

准备工作

检查 Nginx 是否已经编译安装所需模块。
ngx_stream_core_module
ngx_stream_ssl_preread_module
运行 Nginx -V 命令查看,如返回结果中含有 --with-stream--with-stream_ssl_preread_module,说明这两个模块已经被编译,否则需要您重新编译 Nginx。

Nginx

SNI分流

为了说明如何达到最终配置文件的原理,这里开始一步步解析,如果您对原理不感兴趣,可以直接到文章最后查看最终配置文件信息。

当前 Nginx 要对 443 端口的 TLS 流量进行 SNI 分流,因此 Nginx 的 Stream 模块需要监听服务器公网 IP 的 443 端口。也因此 Nginx 的 WEB 服务器配置文件中不能监听 0.0.0.0 的 443 端口,否则端口会被占用而产生冲突。下面这段代码是来自 Nginx 的主配置文件中的片段。

Nginx主配置原版代码

user  www www;
worker_processes auto;
error_log  /www/wwwlogs/nginx_error.log  crit;
pid        /www/server/nginx/logs/nginx.pid;
worker_rlimit_nofile 51200;

stream {
log_format tcp_format '$time_local|$remote_addr|
$protocol|$status|$bytes_sent|$bytes_received| 
$session_time|$upstream_addr|$upstream_bytes_sent|
$upstream_bytes_received|$upstream_connect_time';
access_log /www/wwwlogs/tcp-access.log tcp_format;
error_log /www/wwwlogs/tcp-error.log;
include /www/server/panel/vhost/nginx/tcp/*.conf;
}

Nginx主配置分流代码

stream {
log_format tcp_format '$time_local|$remote_addr|
$protocol|$status|$bytes_sent|$bytes_received|
$session_time|$upstream_addr|$upstream_bytes_sent|
$upstream_bytes_received|$upstream_connect_time';

access_log /www/wwwlogs/tcp-access.log tcp_format;
error_log /www/wwwlogs/tcp-error.log;
include /www/server/panel/vhost/nginx/tcp/*.conf;

map $ssl_preread_server_name $stream_map {
    website.example.com reality;
    website.example.com reality1;
    website.example.com www1;
    website.example.com www2;
    website.example.com www3;
    website.example.com www4;
    website.example.com www5;
    website.example.com www6;
}
upstream reality {
    server 127.0.0.1:1443;
}
upstream reality1 {
    server 127.0.0.1:2443;
}
upstream www1 {
    server 127.0.0.1:3443;
}
upstream www2 {
    server 127.0.0.1:4443;
}
upstream www3 {
    server 127.0.0.1:5443;
}
upstream www4 {
    server 127.0.0.1:6443;
}
upstream www5 {
    server 127.0.0.1:7443;
}
upstream www6 {
    server 127.0.0.1:8443;
}
server {
    listen 443 reuseport;
    proxy_pass $stream_map;
    ssl_preread on;
}

nginx-sni-1.png

解决路径地址被自动添加端口号及无法访问

经过简单测试,网站 与 TCP 应用都工作正常。但当深入测试后,如果您的网站后端使用的是 PHP ,您想要访问 https://website.example.com/php ,理论上会直接跳转到 https://website.example.com/php/ 并显示出 php 目录下 index.php 的内容。但实际上并没有,等了大一小会,您会发现浏览器报错,提示 https://website.example.com/php/ 响应时间过长。另外一情况,即使是正常显示网页,获取访客的 IP 地址全都是127.0.0.1,并不是真是访客 IP。

Nginx WEB 配置代码
port_in_redirect off
set_real_ip_from 127.0.0.1
real_ip_header proxy_protocol

结尾

Nginx 主配置代码

stream {
log_format tcp_format '$time_local|$remote_addr|
$protocol|$status|$bytes_sent|$bytes_received|
$session_time|$upstream_addr|$upstream_bytes_sent|
$upstream_bytes_received|$upstream_connect_time';

access_log /www/wwwlogs/tcp-access.log tcp_format;
error_log /www/wwwlogs/tcp-error.log;
include /www/server/panel/vhost/nginx/tcp/*.conf;

map $ssl_preread_server_name $stream_map {
    website.example.com reality;
    website.example.com reality1;
    website.example.com www1;
    website.example.com www2;
    website.example.com www3;
    website.example.com www4;
    website.example.com www5;
    website.example.com www6;
}
upstream reality {
    server 127.0.0.1:1443;
}
upstream reality1 {
    server 127.0.0.1:2443;
}
upstream www1 {
    server 127.0.0.1:3443;
}
upstream www2 {
    server 127.0.0.1:4443;
}
upstream www3 {
    server 127.0.0.1:5443;
}
upstream www4 {
    server 127.0.0.1:6443;
}
upstream www5 {
    server 127.0.0.1:7443;
}
upstream www6 {
    server 127.0.0.1:8443;
}
server {
    listen 443 reuseport;
    proxy_pass $stream_map;
    ssl_preread on;
    proxy_protocol on;
}
server {
    listen 443 udp;
    proxy_pass 127.0.0.1:443;
}

Nginx WEB 配置代码

server {
listen 80 default_server;
listen 8443 ssl proxy_protocol default_server;
port_in_redirect off;
set_real_ip_from 127.0.0.1;
real_ip_header proxy_protocol;
}