blog

feassh

用 Nginx 的 stream 模块做基于 SNI 的端口转发

发布于 # Nginx # 网络

最近在折腾服务器的时候遇到一个场景:手头只有一个公网 IP,但有多台后端服务器需要分别提供 HTTPS 服务。想让用户通过不同域名访问时,自动转发到对应的后端服务器,但又不想在每个后端服务器上都单独绑定公网 IP,也不想用七层的反向代理来解密 HTTPS(那样证书配置和性能开销都比较麻烦)。

后来发现 Nginx 的 stream 模块配合 ssl_preread 可以很优雅地解决这个问题。简单来说就是在四层(TCP)层面根据 TLS 握手时的 SNI(Server Name Indication)信息来做路由转发。下面记录一下具体的配置方法和一些需要注意的点。

核心配置

我最终的配置大概是这样的:

events {
    # 即使为空,也必须保留 events 块
}

stream {
    map $ssl_preread_server_name $backend {
        example.com      104.19.143.56:443;
        api.example.com  104.19.143.56:443;
        # default        1.1.1.1:443;  # 默认后端,处理未匹配的域名
    }

    server {
        listen 443;
        proxy_pass $backend;
        ssl_preread on;
    }
}

关键点解读

1. events 块不能省略

即使里面什么都不写,events {} 这个块也必须有。这是 Nginx 的语法要求,少了它启动时会报错。

2. stream 上下文

stream 是 Nginx 用来处理 TCP/UDP 流量的模块,和常见的 http 是平级的。它工作在四层,不会解析 HTTP 协议的内容,所以效率比七层代理高,很适合做端口转发。

3. ssl_preread 的作用

ssl_preread on; 这行是关键。它会让 Nginx 在建立 TCP 连接后,先 peek(偷看)一下客户端发来的 TLS ClientHello 数据包,从中读取 SNI 字段(即客户端请求的域名),但不进行解密。读取完 SNI 后,Nginx 再根据 map 里定义好的规则,将整个 TCP 连接透明地转发给后端。

4. 动态选择后端

map $ssl_preread_server_name $backend 这行定义了一个变量 $backend,它的值会根据 $ssl_preread_server_name(SNI 域名)来变化。这样就可以实现不同域名指向不同的后端地址。

如果访问的域名不在列表中,可以用 default 指定一个兜底的后端,避免连接被拒绝。

一些实际操作时的注意事项

收尾

这种方案比较适合资源有限、需要在单一 IP 上暴露多个 HTTPS 服务的场景。比如在一台低配 VPS 上统一接收流量,再根据域名分发给内网的多台机器。配置起来也不复杂,只要理清 streammap 的逻辑,再注意一下 events 块的存在,基本就能跑起来。