前言
众所周知,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的应用很可能就不支持这个协议。新的问题又来了,怎么破局?
答案就是让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转发并和网站共存实现在了生产环境:
再次感谢上面的两位开拓者!
你好请问这个分流只能走tls吗 如果是多节点不是443端口的节点请问怎么配置 谢谢
要SNI分流就必须要用到TLS,因为SNI是TLS的一个扩展。
要想用回落的话最好使用443端口,回落到一个正常网站上;否则回落到不是443的https网站上,特征太明显了,正常网站肯定不是这样的。不想用回落功能,也不用443端口的话完全不需要参照这篇文章,xray直接监听端口号即可。
你好请问电报怎么联系
谢谢博主 照着样子配置成功了 但是想问下 现在xray多了一个用户
这种情况下nginx的upstream要怎么配置?
这样好像不行
不同用户用不同 UUID 区分即可,没必要另开一个监听端口来区分吧。
大佬好,搜到您的博文,仔细按文步骤配置。现在出现这样的情况:nginx和xray都正常运行,但是wordpress网站打不开,xray也是没有流量。我是在宝塔面板下安装的。最开始安装的是wordpress,能正常配置。等安装完xray,再改配置,却怎么也不行了。折腾了好几天了,一直解决不了,真搞累死了。我是真小白,对代码含义基本不懂,只会依样画葫芦,现把我的配置表附上,望赐教。等候您的回复,谢谢🙏
配置文件:https://pastebin.ubuntu.com/p/xQcN2ZGT2h/
http://www.xxxx.ml wordpress; 改成 http://www.xxxx.ml wordpress;
xray没流量可能是你客户的没有升级到最新版,客户端要最新版,客户端里面的Xray内核也要最新的。
不懂Nginx配置的建议去看荒岛的那篇,这篇文章本来就不是教你搭建的,搭建看博主感谢的那个荒岛的文章。
http://www.xxxx.ml wordpress 这个是博客显示问题。客户端是v2rayN,这个更新了又更新的。相关的几篇文章我也是看了又看了。我现在试的VPS是Orcale的Ubuntu20,装了宝塔面板。不知道跟系统有没有关系。
从配置文件上面看不出大的问题,listen [::]:443 reuseport;这里的[::]改成服务器的IPv4的公网IP试试。比较有价值的信息是nginx和xray的报错信息,用service nginx status和service xray status看一看有没有报错信息,然后对照着报错的信息修改。或者贴图到图床发上来,我有时间的时候试着看一下。
谢谢博主回复。改公网IP我也试过,结果nginx报错。用用service nginx status和service xray status都没报错的。我是没头绪了才了询问的。谢谢博主。我就是自己玩下,花的太多的时间了,我今天换了xray分流了,已经正常了。等下次再试这个问题吧。再次感谢!
谢谢楼主, 总算配出来了,对于多subdomain的情况建议以下配置,已有的website*.example.com server块就只需要加入"listen [port] ssl http2 proxy_protocol"就可以了。
nginx配置方面,直接复制配置到一个文件里nginx会报错。建议注释nginx的配置方法,我的方案是把stream{}写到/etc/nginx/stream.d/sni,把web服务器部分写到/etc/nginx/conf.d/web.conf,然后在/etc/nginx.conf加入
include /etc/nginx/stream.d/sni;
(需要在http{}外部)。这样配置结构比较简洁。你试试这样多配置几个站点,我是用的二级域名,就会出现这问题!
按照大佬的配置已经完成设置。
但发现一个问题,会出现先访问哪个域名,另一个域名也会访问同样的页面。
例如:打开 1.domain.com,然后新标签页打开 2.domain.com 后,他们的内容是一样的。同样的步骤,反过来也是,不是1变成2,就是2变成1。
大佬不敢当😂前面几位为我开路的才是大佬
理论上不会出现这种问题的,SNI会让Nginx把流量转发到正确的位置上去的,出现这个情况应该是配置问题,或者可能是配置了Nginx层面上的缓存(比如PageSpeed模块)引起的。
1.domain.com和2.domain.com的Server配置中是都需要配置listen 127.0.0.1:443 ssl http2 proxy_protocol的,其他能不变的参数尽量保持相同。
另外如果你的站点启用了QUIC,尽量每个vhost都要启用,不要有的vhost启用,有的不启用,否则是有可能出现跟你类似情况的问题的。
我也遇见了同样的问题,多个网站显示一样的页面。
我觉得可能是你们配置文件stream模块中的的 $ssl_preread_server_name 没有配置好,有多少个网站域名都要添加到里面的,比如website1.example.com和website2.example.com,配置文件就要这样写:
配置好了应该就问题不大了,还不行的话可以用pastebin贴一些配置文件上来,或者私发给我看看。
我也碰到这问题了,不管是Nginx还是xray自己的SNI都是这样,我试了chrome内核的浏览器都这尿性,iphone safari没这问题,Nginx配置也没有任何缓存设置,nginx日志我是按站点分开的,里面能看到其他域名请求。。。
有时会不同域名输出内容一模一样,有时候输入不同域名直接301跳转到刚刚开打过的域名(域名都是这台服务器的),不知道什么原因,网上找了半天也没什么头绪!
server_name应该无效了,都是通过端口分流。
xray SNI看日志经常获取不到realName,也就是回落域名。
很多人遇到了相同的问题,我仔细检查了一下我博客中的配置文件,确实有一个错误,最后Nginx配置文件中的upstream配置应该是这样的,之前把端口号写错成8000了,应该是443,现在已经修正:
但是这个错误应该会使服务器上所有的网站无法打开,浏览器报错:似乎 xxx.com 关闭了连接。很多人出现的网站域名对不上的情况应该还是配置文件的问题,本站所用的服务器都是这样配置的,并没有因此遇到过这类问题...拿不到有问题的配置文件,只靠描述我也无法确定问题在哪里...
我觉得应该不是你配置的问题,因为使用Xray自己的SNI也是这样,应该是Nginx问题,可能缺少什么配置还是怎么,要好好研究一下!
我也遇到了,最后发现是浏览器缓存问题。开匿名浏览正常访问的话就是缓存问题,清缓存就好了。表扬一下edge,不需要清缓存也可以正常访问。
贴一个大佬的链接,他有讲这个问题。
https://www.orzlee.com/proxy/2021/04/13/nginx-sni-offload-port-multiplexing-uses-xray-vless-xtls.html
感谢!此链中的博文内容完美解决了此问题。
参考博主的设置,先行测试了 VLESS + WS + TLS + CDN 组合下利用 NGX 的 stream 模块复用 443 端口(仅仅是为了测试,毕竟此方案本身是不需要 stream 模块来复用 443 端口的)。
现在有个问题,用于伪装的站点,没过 CDN 的情况下,可以正常获取到客户端真实 IP,但是过 CDN 的好像不行(已经测试过配置成 CDN 厂商提供的 real_ip_header,还是只能获取到 CDN 的 IP)。
科学组件倒是都能正常获取客户端真实 IP,但是加上 stream 模块后,链路带宽急剧下降,不知博主 stream 复用端口配合 XTLS 协议使用的情况下,链路带宽如何?
我这边自己测试的话,使用Cloudflare的CDN是能正常获取到访客IP的,Cloudflare向服务器传送访客IP的字段是x-forwarded-for,一般CDN也都是这个字段,可以尝试用"real_ip_header X-Forwarded-For"接收一下,我自己也没有用其他手段获取访客IP的,直接就可以获取到。
链路带宽急剧下降的这个问题我没有遇到过,stream 复用端口配合 XTLS降低了延迟,速度起飞,配置正确的话带宽应该是不会急剧下降的。
兄弟你好,这样得话,xtls得日志是不是就不能够获取用户端得ip
刚刚看了一下 确实获取不到IP 这篇文章写的时候xray还不支持proxy protocol,没法比较好的传递IP
过一段时间有空我再看看 这一块配置有些乱,有段时间没看了,我也有点头疼 得慢慢来😂
太难了 不会分流
大佬牛批