使用 nginx 作为 sniproxy
把多个 HTTPS 服务部署在同一个 IP 上时,需要把传入连接根据 SNI 转发到不同的服务上。 Nginx 可以在不解密 TLS 的情况下,进行 SNI 代理。

把多个 HTTPS 服务部署在同一个 IP 上时,需要把传入连接根据 SNI 转发到不同的服务上。 Nginx 可以在不解密 TLS 的情况下,进行 SNI 代理。

思路

由于我们不想在 nginx 里解密 TLS 流量,只是单纯进行 TCP 连接转发,所以这里需要用到的是 nginx_stream_proxy_module. 为了能够根据 SNI 转发到不同后端,需要 ngx_stream_ssl_preread_module 来解析 TLS 协议头里的 SNI 字段。

配置

模块安装和激活

发行版打包的 nginx 可能是分模块、动态加载,并且拆包的。以 debian 为例,在 debian 12 里需要安装 nginx-full 包,并且在 nginx.conf 开头加入

load_module /usr/lib/nginx/modules/ngx_stream_module.so;

才能用上四层转发功能。

转发配置

首先,这里所有的内容都是属于 stream 模块的配置,需要写在 stream {} 块里面。这里的关键配置包含两个部分:

  1. server 块中指定代理监听的地址、端口,打开 ssl_preread 功能,并把转发目标指定为一个变量 $upstream.
  2. 通过 map 块,根据 stream 模块预读提供的 $ssl_pread_server_name 变量, 设置 $upstream 的值。

例如:

stream {
    map $ssl_preread_server_name $upstream {
        foo.example.com  [::1]:2001;
        bar.example.com  unix:/run/bar/bar.socket;
        default unix:/nonexist;
    }

    server {
      listen        443;
      listen        [::]:443;
      proxy_pass    $upstream;
      ssl_preread   on;

      proxy_connect_timeout 15s;
      proxy_half_close      on;
    }
}

在这里,我们设置了两个转发的主机名,转发目标可以是 IP:port, 也可以是 unix domain socket. 并且在最后的 default 目标设置成了一个不存在的 unix domain socket, 让它能在收到无效的主机名时关闭连接。(网络上各种随机的扫描很多的)

其余的参数可以按需添加,如 proxy_half_close on 让这个代理对应用层看起来更加透明。

完整配置

发行版的 nginx 默认配置可能很复杂。因为我们只用 sni proxy 功能,里面大部分都是我们不需要的。因此可以单独写一份最小配置来启动 nginx. Stream 转发功能是很简单的,我们需要关注的参数基本上就只有以下 3 个:

worker_processes auto;        # worker 进程数, auto 为根据 CPU 数自动配置
worker_cpu_affinity auto;     # worker 的 CPU 亲和性,自动配置就可以了

events {
  worker_connections 1024;    # 每个 worker 的并发连接数,根据你的工作负载,和服务器性能选择一个合适的
}

合并在一起就是:

load_module /usr/lib/nginx/modules/ngx_stream_module.so;

worker_processes auto;
worker_cpu_affinity auto;

events {
  worker_connections 1024;
}

stream {
    ...
}

最后修改于 2025-08-03