使用Nginx实现TCP四层反向代理
众所周知,海外高性能服务器加不错的线路等于贵得离谱,然而不看网络的话,价格就便宜不少,再选择一台低配的线路不错的VPS作为前端进行代理的就划算很多。做站的话一般通过nginx进行http或者tcp代理,以前一直都在使用http反向代理,好处是配置简单效果也不差,但如果进行https代理,前端也需要配置ssl证书,从安全的角度来说显然是不合理的。因此折腾了下通过Nginx Stream模块进行tcp代理,因为只是传输层协议,便可以不用配置ssl证书。
然而无论那种方式,对于做站来说,都有获取到真实ip的需求。如果是http代理的话,可以通过编译ngx_http_realip_module
模块并设置proxy_set_header
实现,这里不再讨论。这篇文章着重说下tcp代理的实现方法和过程。
要使用stream模块,编译Nginx的时候需要加上--with-stream
参数,然后再nginx.conf中配置,一个简单的代理模型如下:
stream{
...
upstream backend{
server your.domain.com:443
}
server{
listen 443 ssl;
proxy_pass backend;
...
}
}
而要实现代理多个后端还需要用到ngx_stream_ssl_preread_module
模块,需要自行编译,该模块可以在不解密的情况下,通过SNI获取到访问的域名,之后在server中加入ssl_preread on
,示例如下:
stream{
...
map $ssl_preread_server_name $backend {
server-1.com server_1;
server-2.com server_2;
...
default server_1;
}
upstream server_1{
server server-1.com:443;
}
upstream server_2{
server server-2.com:443;
}
...
server{
listen 443 ssl;
ssl_preread on;
proxy_pass backend;
...
}
}
那么如tcp代理如何获取到真实ip呢,这里就需要用到proxy_protocol
协议,该协议是HAProxy作者开发的一个开源协议,通过添加tcp头信息传递客户端的参数。具体的实现主要是参考了Nginx官方的文档Accepting the PROXY Protocol。具体的实现不算复杂,proxy_protocol
需要两个角色sender和receiver,都指定proxy_protocol
协议即可,示例如下:
stream{
...
# 这里修改记录格式为$proxy_protocol_addr
log_format basic '$proxy_protocol_addr - $remote_user [$time_local] '
'$protocol $status $bytes_sent $bytes_received '
'$session_time';
map $ssl_preread_server_name $backend {
server-1.com server_1;
server-2.com server_2;
...
default server_1;
}
upstream server_1{
server server-1.com:443;
}
upstream server_2{
server server-2.com:443;
}
...
server{
listen 443 ssl;
ssl_preread on;
proxy_protocol on; # 这里打开proxy_protocol协议
proxy_pass backend;
...
}
}
正确的接受信息,需要在server的listen端口后加上proxy_protocol,如listen 443 ssl http2 proxy_protocol;
并设置tcp代理的CIDR地址:
set_real_ip_from 192.168.1.0/24;
real_ip_header proxy_protocol;
对于http,记录真实ip需要设置proxy_set_header
:
http {
proxy_set_header X-Real-IP $proxy_protocol_addr;
proxy_set_header X-Forwarded-For $proxy_protocol_addr;
}
以上设置便可以通过tcp反向代理并获取到真实ip了,由于我是国内外分线路解析,国内走前端代理,国外直连,因此我的问题并没有解决。最开始发现前端能访问,后端无法直接访问,也就是国内能访问到,而国外则无法连接,查看error.log日志也发现乱码并提示broken header:
而后在这篇帖子中发现问题,研究发现proxy_protocol
的接收端必须在接收到完整有效的proxy_protocol
头部后才能开始处理连接数据,sender在与receiver之间建立连接后,会先发送一个带有客户信息的tcp header,因为更改了tcp协议,receiver也必须支持proxy_protocol
,否则不能识别tcp包头,导致无法成功建立连接。也就是如果服务器接收到的第一个数据包不符合proxy_protocol
的格式,服务器会直接终止连接。而我的策略是国外走的是直连,并不需要proxy_protocol
,添加上的话没有前端发送对应的协议,就会无法打开,而如果去掉的话,就会变成国外能访问,国内无法访问。
最终的解决方案与stackoverflow上的一篇帖子中提到的类似,国外保留443接口直连,使用传统协议,而前端将请求转发到后端的另一个端口,设置防火墙该端口只能通过前端的ip访问,并通过Nginx监听接受proxy_protocol
协议。这样就解决了在tcp代理的同时分线路的问题,最终实现了国内访问解析到前端,然后通过tcp四层反向代理到后端并传递真实ip,而国外访问直接解析到后端源站。
本文链接:https://www.zatp.com/post/nginx-tcp-proxy-forward-client-ip/
版权声明:本文为原创文章,版权归 ATP BLOG 所有,转载请注明本文链接(采用 CC BY-NC-SA 4.0 许可协议)
你好,我是用NGINX SNI分流时遇到和你差不多的问题,NGINX监听443,然后把流量按照SNI分流给A(某个程序)和B(web server),B倒是没问题,可以添加proxy_protocol参数,但是A无法解析来自NGINX转发过来的流量(原因正如你文章所提),想请教一下解决方法,文末哪里没怎么看懂,请问能否再讲一下处理方法,十分感谢。
nginx部分配置如下:stream {
}
server {
}
你好,可能是接收的程序不支持 proxy_protocol 协议,不妨尝试下增加一层转发,例如下面的例子,中间加了一层 server 用于处理 proxy_protocol,然后再转发到你需要的地址:
这也是一个解决方法,多谢了。