Nginx 不只是 HTTP 服务器
很多人对 Nginx 的印象停留在「网站服务器 + 反向代理」,其实它内置的 stream 模块是一个完整的四层(TCP/UDP)代理。这个模块的能力经常被忽略:它能把 MySQL 的 3306、Redis 的 6379、SSH 的 22 端口收口到一台入口机后面,做统一转发、健康检查和访问控制,而不需要额外部署 HAProxy 或 frp。本文讲清楚 stream 模块怎么开、怎么写、怎么和 proxy_protocol 配合拿真实来源 IP,以及 TLS 透传和终结两种形态的取舍。
先确认 stream 模块在不在
stream 模块不是所有 Nginx 编译版本都带。先查:
nginx -V 2>&1 | tr ' ' '\n' | grep -E 'stream|with-stream'
# 或者看已加载模块
nginx -V 2>&1 | grep -o 'with-stream[^ ]*'如果输出里没有 --with-stream,说明这个版本没编译进去。发行版自带的 nginx 包通常默认开启,源码编译需要显式加 --with-stream --with-stream_ssl_module --with-stream_ssl_preread_module。缺了 stream_ssl_preread,就没法做后面讲的按 SNI 分流。
基本结构:stream 与 http 平级
stream 块写在最外层,和 http 平级,不能嵌套进 http 里:
# /etc/nginx/nginx.conf 结构示意
user www-data;
worker_processes auto;
events { worker_connections 10240; }
http { ... 网站配置 ... }
stream {
upstream mysql_backend {
server 10.0.0.11:3306 max_fails=3 fail_timeout=10s;
server 10.0.0.12:3306 backup; # 备用节点
}
server {
listen 33060; # 对外暴露的端口
proxy_pass mysql_backend;
proxy_connect_timeout 5s;
proxy_timeout 600s; # 数据库长连接,超时要给大
}
}这样外部应用连入口机的 33060,就被透明转发到内网真实的 MySQL 上。proxy_timeout 对数据库、Redis 这类长连接服务必须给大,默认 10 分钟,小于业务空闲连接存活时间会被莫名切断。
TLS 透传 vs TLS 终结:两种形态别搞混
四层代理处理加密流量有两种方式,选错了会出大问题:
- TLS 透传(passthrough):Nginx 只转发 TCP 字节流,不解密。证书和加密全在后端服务器上,Nginx 看不到明文。配置直接
proxy_pass即可,用ssl_preread可以在不解密的情况下读取 SNI 做分流。 - TLS 终结(termination):Nginx 自己持有证书、解密后再以明文或重新加密转给后端。适用于你想在入口统一管理证书的场景。
按 SNI 分流的透传配置(一个 IP 一个端口承载多个 TLS 服务):
stream {
map $ssl_preread_server_name $backend {
db.example.com mysql_tls; # 各 upstream 自行定义
cache.example.com redis_tls;
default reject_backend;
}
upstream mysql_tls { server 10.0.0.11:3306; }
upstream redis_tls { server 10.0.0.12:6379; }
server {
listen 8443;
proxy_pass $backend;
ssl_preread on; # 关键:不开读不到 SNI
}
}ssl_preread 只在透传模式下有意义;终结模式下 Nginx 已经解密,直接看 $ssl_server_name 即可。
proxy_protocol:把真实来源 IP 带过去
四层转发后,后端服务器看到的所有连接来源都变成了 Nginx 入口机的 IP,日志和 IP 白名单全失效。proxy_protocol 就是为解决这个问题的:Nginx 在转发的数据流前面插入一小段「原始来源 IP 和端口」的信息。
# 入口机:开启了 proxy_protocol 的转发
stream {
server {
listen 33060 proxy_protocol; # 声明本端口接收 proxy_protocol
proxy_pass mysql_backend;
proxy_protocol on; # 转发时也带上
}
}注意发送和接收两端必须一致:入口 proxy_protocol on 表示「转出时附带」,listen ... proxy_protocol 表示「接收时解析」。后端如果是 Nginx,还要在 listen 上开 proxy_protocol 并用 set_real_ip_from 取真实 IP;如果是 MySQL,它原生不支持 proxy_protocol,就得靠后端自己解析或改用别的方式。⚠️ 这个特性两端必须都开,只开一端会导致连接直接失败——这是最常见的翻车点。
健康检查与访问控制
stream 模块只有被动健康检查(max_fails + fail_timeout),没有 http 那种主动探测。配合 allow/deny 做来源限制:
stream {
server {
listen 33060;
allow 10.0.0.0/24; # 只允许内网段
allow 203.0.113.5; # 允许某个固定办公 IP
deny all;
proxy_pass mysql_backend;
}
}把数据库收口到入口机、只对内网开放,是个人站安全加固里很划算的一步:既能隐藏真实 IP,又能在入口做统一的白名单,后端数据库甚至不用暴露任何公网端口。
常见坑速查
- stream 写进 http 块报错:
stream必须与http平级,不能嵌套。 - 模块没编译:
nginx -V确认--with-stream,缺了要重编译(用nginx -V原始参数)。 - 长连接被切断:
proxy_timeout太小,数据库/Redis 调到几百秒。 - 后端看不到真实 IP:没开
proxy_protocol,或只开了一端。 - SNI 分流不生效:忘了
ssl_preread on,或客户端用 IP 而非域名发起连接(IP 连接没有 SNI)。 - 改完不生效:
stream的改动需要nginx -t后reload,reload 对 stream 连接是新建连接才生效。
UDP 转发:DNS 与游戏加速场景
stream 模块同样支持 UDP,这对自建 DNS、游戏服务这类基于 UDP 的应用很有用。UDP 的配置和 TCP 类似,但要注意 UDP 是无连接的,健康检查只能靠超时判断:
stream {
# 自建递归 DNS 对外转发
upstream dns_backend {
server 10.0.0.20:53;
server 10.0.0.21:53;
}
server {
listen 53 udp;
listen 53; # 同时监听 TCP(DNS 大响应会走 TCP)
proxy_pass dns_backend;
proxy_timeout 3s; # UDP 超时要短,否则客户端等太久
proxy_responses 1; # 期望 1 个响应,收到即结束会话
}
}proxy_responses 1 是 UDP 转发里容易被忽略的参数:它告诉 Nginx 这个会话期望收到几个响应包,收到就可以释放会话资源。DNS 查询通常是「一来一回」,设成 1 能避免会话表被无谓占用。proxy_timeout 3s 也要比 TCP 场景小很多,因为 UDP 客户端不会等待太久。
把 stream 当 TLS 终结器统一管理证书
另一种常见用法是用 stream 做 TLS 终结,把所有证书集中在一台入口机管理,后端内网用明文传输,省去每台机器都配证书的麻烦:
stream {
server {
listen 443 ssl;
ssl_certificate /etc/nginx/certs/example.com.crt;
ssl_certificate_key /etc/nginx/certs/example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
proxy_pass 10.0.0.30:8080; # 后端明文
}
}⚠️ 这样做的前提是入口机到后端的内网链路可信。如果后端在后端机房、跨公网,明文就不可接受了,必须让后端自己持有证书并改用透传模式。选择终结还是透传,本质是在「证书管理便利」和「链路加密」之间做权衡:内网可信就终结,跨不可信网络就透传。
性能与选型:什么时候该用 stream,什么时候不该
stream 模块的性能在中低流量下完全够用,但它是七层服务器顺带做的四层代理,在极端高并发、超低延迟的场景里不如专门的 LVS 或 HAProxy。判断标准很简单:
- 转发量在每秒几千连接以内、个人站或中小业务:
stream足够,且省去额外组件。 - 需要主动健康检查、连接队列精细控制、极高 QPS:用 HAProxy 或 LVS。
- 只是想把内网端口收口、加白名单:
stream是最轻的选择。
另外 stream 的配置不能复用 http 里的 map、upstream 定义——它们是两套独立的命名空间,同名变量和 upstream 互不相干。写配置时别把 http 的 upstream 名字拿到 stream 里用,会报 no such upstream。
用 stream 做端口收口的安全收益
把数据库、Redis、管理面板这些「本不该暴露公网」的服务全部收到入口机的 stream 后面,是安全加固里立竿见影的一步。收益有三层:一是隐藏真实拓扑,扫描者只能看到入口机开放的少数端口,看不到后端内网的真实分布;二是集中访问控制,白名单只在入口配一次,后端无需各自维护;三是便于审计,所有连接都要经过入口,日志集中、时序清晰。
# 入口机:只放行固定来源,其余拒绝,并把连接日志记下来
stream {
log_format stream_log '$remote_addr [$time_local] $server_port -> $upstream_addr $status';
access_log /var/log/nginx/stream.log stream_log;
server {
listen 33060;
allow 10.0.0.0/24;
deny all;
proxy_pass mysql_backend;
# 连接建立失败时记录原因
proxy_next_upstream on;
}
}配合 fail2ban 或简单的 limit_conn 思路,可以在入口挡住针对数据库端口的暴力破解扫描。⚠️ 但要注意:stream 层的 access_log 记录的是 TCP 连接级信息,不是应用层的 SQL 或命令,所以「谁连上了」能查清,「连上后干了什么」还得靠后端应用自己的日志。做完收口后,务必再确认后端服务已经不再监听公网地址(ss -lntp 看是 0.0.0.0 还是 127.0.0.1),否则等于门关了、窗户还开着。
改配置时的保命习惯
stream 常常承载着数据库、SSH 这类「断了就进不去」的关键服务,改配置不慎会把管理通道也切断。养成两个习惯:一是改前先 nginx -t 测语法,reload 而非 restart;二是永远保留一个不经过 stream 的直连通道,比如保留一个内网 IP 直连 22 端口,或者用云厂商的 VNC/控制台兜底。历史上不少事故就是「把 SSH 也收进了 stream,结果配置一错整台机器失联」。先测试、再 reload、留后路,这三步能让你在改四层代理时心里有底。
结语:一台 Nginx 收住所有入口
stream 模块让个人站长不必为了「转发一个端口」就额外引入一个代理软件。数据库、缓存、内网服务的入口统一收到一台 Nginx 上,用白名单和 proxy_protocol 兼顾安全与可追溯,配置风格还和 http 保持一致,学习成本几乎为零。下次遇到「想让内网服务不暴露公网、又想有个统一入口」的需求,先想想是不是 stream 块就能搞定,而不是又多装一个组件。