隐藏

完美和网站共存使用Nginx进行SNI分流

发布:2023/1/10 17:53:08作者:管理员 来源:本站 浏览次数:1014

众所周知,Nginx是一个轻量小巧,功能强大,占用资源低的Web服务器。但Nginx不只是Web服务器,用Nginx进行反向代理,做集群负载均衡也是常用的操作;还可以编写Lua脚本,将其嵌入到Nginx中完成更为复杂的操作。除了这些在HTTP层面上的应用,就没有其他的了吗?


当然不!Nginx不止可以在工作在OSI协议中的第七层应用层上,它还可以直接工作在第四层传输层上,直接将第四层的TCP流量进行转发。本文就将探讨使用Nginx进行SNI分流并完美和网站共存的技术实现。


传输层安全性协议(即大名鼎鼎的 TLS)是一个工作在传输层上的重要安全协议,它可以为互联网通信提供安全及数据完整性保障,像HTTPS等安全传输都是基于TLS所进行的。服务器名称指示(SNI)是TLS的一个扩展协议,在该协议下,在握手过程开始时客户端告诉它正在连接的服务器要连接的主机名称。Nginx就可以利用stream模块,基于SNI,对进入同一端口、不同主机名的TLS流量进行分流。如果你有一个基于TLS的应用,想要运行在443端口;而443端口已经被Nginx监听用作Web运行网站,你就可以使用Nginx的SNI分流,将443端口复用,把使用不同的域名(主机名)的TLS流量分开,互不干扰,完美共存。如果你的这个应用比较害怕主动嗅探,那也不要紧,挡在最前面的是Nginx;另外你也可以配置你的应用进行回落,来规避主动嗅探。


主要的应用场景:基于TLS,直接获取TCP流的应用,如XTLS等。懂得自然懂。


目标:在443端口上,使用Nginx进行SNI分流并使得TCP应用完美和网站共存。示例中的网站后端是PHP,但其他后端原理也是类似的。

准备工作


因为我们转发的是TCP流,因此nginx需要安装ngx_stream_core_module模块(以下简称stream模块);我们还需要做一个SSL证书前置,需要ngx_stream_ssl_preread_module 模块。要查看这些模块是否被编译进了Nginx,可以使用Nginx -V命令进行查看。


如果返回的结果中含有 --with-stream和--with-stream_ssl_preread_module,就说明这两个模块已经被编译进了Nginx;否则则需要自己重新编译Nginx。

对Nginx的配置&原理

SNI分流


为了说明清楚我们最终配置文件的原理,这一小节的前半部分的配置文件并不是最佳的配置,在一步步的思考后,我们得出最终完美的配置文件在本小节的最下方。如果你对原理并不感兴趣,可以直接去看完美的最终版配置文件。


因为Nginx要对通过443端口的TLS流量进行SNI分流,因此Nginx的stream模块需要监听服务器公网IP的443端口,也因此Nginx的Web服务器配置文件中就不能监听0.0.0.0的443端口了,否则端口就会冲突。或许你从网络上能找到和下面类似的配置文件:


# stream模块设置


stream {


   # SNI识别,将一个个域名映射成一个配置名


   map $ssl_preread_server_name $stream_map {


       website.example.com web;


       xtls.example.com xtls;


   }




   # upstream,也就是流量上游的配置


   upstream xtls {


       server 127.0.0.1:9000;


   }


   upstream web {


       server 127.0.0.1:8000;


   }


   # stream模块监听443端口,并进行端口复用


   server {


       listen 443 reuseport;


       proxy_pass $stream_map;


       ssl_preread on;


   }


}




# Web服务器的配置


server {


   listen 80;# 我们只对443端口进行SNI分流,80端口依旧做Web服务;SNI分流也只能在443端口上跑TLS流量才能分流


   listen 8000 ssl http2;# 监听8000端口,要和上面的stream模块配置中的upstream配置对的上


   ......


   if ($ssl_protocol = "") {


       return 301 https://$host$request_uri;


   }


   index index.html index.htm index.php;


   try_files $uri $uri/ /index.php?$args;


   ......


}






TCP流量是这样流动的:



解决路径被自动添加端口号 无法访问的问题


经过简单测试,你好像觉得完全OK,网站和TCP应用都工作正常,以为大功告成了。


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


为什么会这样呢?


因为之所以访问https://website.example.com/php 能跳转到https://website.example.com/php/ ,是因为Nginx中配置了try_files,在我们配置完成后,当这个/php这个文件不存在时,Nginx就会尝试重定向到/php/这个目录。现在Web服务器监听在8000端口,我们自然也会被重定向到8000端口上。怎么解决这个问题呢?


有两个解决方案。第一种比较麻烦,是本文最初写成时找到的解决方案。在本文初稿完成半年后,我找到了更好的解决方案。因此推荐直接看方案二。

方案一 443监听本地法


把本地的Web应用监听在443端口上不就解决这个问题了吗?但0.0.0.0的443端口已经被stream模块占用了,443端口好像用不了。这个Web服务器是藏在stream模块后的,换言之,它只需要运行在本地(127.0.0.1),跟stream模块交互即可。stream模块也完全不用监听0.0.0.0,因为它是直接与公网交道的,只需要监听服务器公网IP即可。这样不就不冲突了吗?就能完美解决这个问题了。


经过第一次改进的Nginx配置文件如下:


# stream模块设置


stream {


   # SNI识别,将一个个域名映射成一个配置名


   map $ssl_preread_server_name $stream_map {


       website.example.com web;


       xtls.example.com xtls;


   }




   # upstream,也就是流量上游的配置


   upstream xtls {


       server 127.0.0.1:9000;


   }


   upstream web {


       server 127.0.0.1:443;


   }


   # stream模块监听服务器公网IP443端口,并进行端口复用


   server {


       listen [服务器公网IP]:443 reuseport;


       proxy_pass $stream_map;


       ssl_preread on;


   }


}




# Web服务器的配置


server {


   listen 80;# 我们只对443端口进行SNI分流,80端口依旧做Web服务;SNI分流也只能在443端口上跑TLS流量才能分流


   listen 127.0.0.1:443 ssl http2;# 监听本地443端口,要和上面的stream模块配置中的upstream配置对的上


   ......


   if ($ssl_protocol = "") {


       return 301 https://$host$request_uri;


   }


   index index.html index.htm index.php;


   try_files $uri $uri/ /index.php?$args;


   ......


}






注意:像阿里云、腾讯云这类使用VPC网络的大厂,有的时候需要监听到控制台中以10开头的内网地址,而不是服务器的公网IP。

方案二 port_in_redirect法


虽然方案一可以完美达到我们想要的效果,但是配置起来非常容易出错,既要设置端口号,又要设置监听的IP地址。监听的IP地址有的监听到127.0.0.1,有的监听到公网IP地址。如果公网IP地址换了,这里就要跟着换。而这里提到的port_in_redirect法,只要配置端口号就行,不用管监听到公网IP还是127.0.0.1了。


这个方法非常简单:将vhost中原来监听443的端口监听到一个其他的端口,比如8443端口;然后在vhost配置文件中的server块中添加port_in_redirect off;即可。非常简单不是?


port_in_redirect off的含义是禁用Nginx反代中重定向到绝对端口。默认值为on。


使用方案二的Nginx配置文件示例如下:


# stream模块设置


stream {


   # SNI识别,将一个个域名映射成一个配置名


   map $ssl_preread_server_name $stream_map {


       website.example.com web;


       xtls.example.com xtls;


   }




   # upstream,也就是流量上游的配置


   upstream xtls {


       server 127.0.0.1:9000;


   }


   upstream web {


       server 127.0.0.1:8443;#注意这里改到了8443


   }


   # stream模块监听服务器公网IP443端口,并进行端口复用


   server {


       listen 443 reuseport;


       proxy_pass $stream_map;


       ssl_preread on;


   }


}




# Web服务器的配置


server {


   listen 80;# 我们只对443端口进行SNI分流,80端口依旧做Web服务;SNI分流也只能在443端口上跑TLS流量才能分流


   listen 8443 ssl http2;# 监听本地443端口,要和上面的stream模块配置中的upstream配置对的上


   port_in_redirect off;


   ......


   if ($ssl_protocol = "") {


       return 301 https://$host$request_uri;


   }


   index index.html index.htm index.php;


   try_files $uri $uri/ /index.php?$args;


   ......


}






注意:但是方案二有一个缺点,兼容性不好。一些应用读取到的端口号是我们"Web服务器的配置"中配置的8443端口,而不是实际对外提供服务的443端口。例如,Wordpress的jetpack插件在使用方案二就可能会出现工作不正常的情况,这时候使用方法一更好一些。

解决无法获取访客IP的问题


解决了上一个问题后,还剩下一个无法获取正确访客IP的问题,获取到的访客IP全部都是127.0.0.1。


这里的Web服务器和Nginx stream模块有点像反向代理(划掉有点,是就是反向代理,不过它运行在OSI模型的第四层而不是第七层)。


那有没有用作4层代理的协议呢?有!那就是代理协议(Proxy protocol),是HAProxy的作者Willy Tarreau在2010年开发和设计的一个Internet协议,通过为tcp添加一个很小的头信息,来方便的传递客户端信息(协议栈、源IP、目的IP、源端口、目的端口等),在网络情况复杂又需要获取用户真实IP时非常有用。Nginx也支持Proxy protocol。我们接下来就用Proxy protocol来解决拿不到访客IP的问题,可以说这个协议就是为我们现在遇到的这个问题而生的。


但是Proxy protocol要求代理服务器和被代理的服务器都支持这一协议,如果服务器接收到的第一个数据包不符合 Proxy Protocol 的格式,那么服务器会直接终止连接。stream模块和Web服务器都是Nginx,Nginx是支持Proxy protocol的,这个好说。但是,普通的基于TCP的应用很可能就不支持这个协议。新的问题又来了,怎么破局?

提示

Note: Xray-Core目前已经支持Proxy protocol了,不需要stream模块再次转发。下面的解决方案仅仅是一个example,仅供学习参考,并非Xray-Core实践最佳方案。现在仅需在tcpSettings项中添加`"acceptProxyProtocol": true`即可启用Proxy protocol。


答案就是让Nginx的stream模块再充当一次和TCP应用交流的媒人,再TCP应用前面用stream模块做一层转发,将Proxy protocol这层外衣给去掉,传递给TCP应用的还是最原始的TCP流。这么说可能有些抽象,下面的流量图或许能帮助你理解。


最终的Nginx配置文件如下(添加端口号部分的解决使用的是方案一,可自行改成方案二):


# stream模块设置


stream {


   # SNI识别,将一个个域名映射成一个配置名


   map $ssl_preread_server_name $stream_map {


       website.example.com web;


       xtls.example.com beforextls;# 注意这里修改了


   }




   # upstream,也就是流量上游的配置


   upstream beforextls {


       server 127.0.0.1:7999;


   }


   upstream xtls {


       server 127.0.0.1:9000;


   }


   upstream web {


       server 127.0.0.1:443;


   }


   # stream模块监听服务器公网IP443端口,并进行端口复用


   server {


       listen [服务器公网IP]:443 reuseport;


       proxy_pass $stream_map;


       ssl_preread on;


       proxy_protocol on; # 开启Proxy protocol


   }


   server {


       listen 127.0.0.1:7999 proxy_protocol;# 开启Proxy protocol


       proxy_pass xtls; # 以真实的XTLS作为上游,这一层是与XTLS交互的“媒人”


   }


}




# Web服务器的配置


server {


   listen 80;# 我们只对443端口进行SNI分流,80端口依旧做Web服务;SNI分流也只能在443端口上跑TLS流量才能分流


   listen 127.0.0.1:443 ssl http2 proxy_protocol;# 监听本地443端口,要和上面的stream模块配置中的upstream配置对的上,开启Proxy protocol


   ......


   if ($ssl_protocol = "") {


       return 301 https://$host$request_uri;


   }


   index index.html index.htm index.php;


   try_files $uri $uri/ /index.php?$args;




   set_real_ip_from 127.0.0.1;# 从Proxy protocol获取真实IP


   real_ip_header proxy_protocol;


   ......


}






经过改进,最终完美版的TCP流量是这样流动的:



现在,你的TCP应用和网站就能在443端口完美共存啦!

对应用的配置


本例中使用的TCP应用范例是XTLS,XTLS的相关配置文件如下,仅供参考:


{


   "listen": "127.0.0.1",


   "port": 9000,


   "protocol": "vless",


   "settings": {


       "clients": [


           {


               "id": "YOUR UUID",


               "flow": "xtls-rprx-direct",


               "level": 0


           }


       ],


       "decryption": "none",


       "fallbacks": [


           {


               "dest": "80"


           }


       ]


   },


   "streamSettings": {


       "network": "tcp",


       "security": "xtls",


       "xtlsSettings": {


           "alpn": [


               "http/1.1"


           ],


           "certificates": [


               {


                   "certificateFile": "certificateFile PATH",


                   "keyFile": "keyFile PATH"


               }


           ]


       }


   }


}


其他TCP应用也类似,请参照使用手册或需求编写配置文件。

参考资料


这篇文章一直想写,自从XTLS产生起我就一直想要研究下让它和网站共存。先是这篇文章启发了我:


Xray+VLESS+XTLS+NginxSNI分流/443端口复用-荒岛


但照着这篇文章做,一直存在些问题,直到我看到了下面这篇文章,结合下面这篇文章的评论我完美的将Nginx进行SNI转发并和网站共存实现在了生产环境:


Trojan 共用 443 端口方案 - 程小白


再次感谢上面的两位开拓者!