前言
对与流量是加密的,并且不符合标准的 HTTP 协议,Nginx 无法在 OSI协议中的第七层应用层上解析和处理这些流量。因此,必须通过 ssl_preread 模块在 第四层进行代理和转发TCP流量。这样才能根据 SNI 信息(即客户端在 TLS 握手过程中提供的域名)来决定将流量转发到哪个后端服务。
4 层代理(传输层代理):Nginx 只处理 TCP 连接,而不深入解析数据内容。它只能基于 IP、端口和 TCP 连接的基本属性来做转发。
7 层代理(应用层代理):Nginx 能解析和处理 HTTP/HTTPS 请求的内容,如 URL、头信息等。
ssl_preread
模块允许 Nginx 在 4 层代理中读取 SSL/TLS 握手的初始数据包,从中提取 SNI 信息(域名)。通过这个 SNI 信息,Nginx 可以决定将流量转发到哪个后端服务。
http模块以及stream模块
HTTPS等安全传输都是基于TLS所进行的。服务器名称指示(SNI)是TLS的一个扩展协议,在该协议下,在握手过程开始时客户端告诉它正在连接的服务器要连接的主机名称。Nginx就可以利用stream模块,基于SNI,对进入同一端口、不同主机名的TLS流量进行分流。如果你有一个基于TLS的应用,想要运行在443端口;而443端口已经被Nginx监听用作Web运行网站,你就可以使用Nginx的SNI分流,将443端口复用,把使用不同的域名(主机名)的TLS流量分开,互不干扰,完美共存。
重定向的问题
因为HTTP请求被发送到HTTPS端口,在Nginx既处理HTTP请求又处理HTTPS请求的时候会出现问题
正常80端口访问应该是:http://xxxx.test.com:80/login
正常开启HTTPS以后443端口访问应该是:https://xxxx.test.com:443/login
但是此时却变成了: http://xxxx.test.com:443/login,即HTTP请求被发送到HTTPS端口。
简单来说就是:当第一次请求试图通过HTTP访问网站xxxx.test.com,这个请求被重定向到HTTPS。于是Nginx预计使用SSL(443端口)交互,但原来的请求(通过端口80接收,即检查到未登录,需要从/跳转到/login)是普通的HTTP请求,于是会产生错误。
解决方法是在原来的配置上面加两个参数:
proxy_set_header X-Forwarded-Proto https; # X-Forwarded-Proto(XFP)报头是用于识别协议HTTP或HTTPS的,即用户客户端实际连接到代理或负载均衡的标准报头。
proxy_redirect http:// https:// # proxy_redirect 该指令用来修改被代理服务器返回的响应头中的Location头域和“refresh”头域,也就是把http协议改成https协议。
https访问80应该是不可行的,对于80默认http的情况下,个人暂未研究
本机服务且不同端口代理多个https网站
直接Nginx做映射,不同网站监听不同的端口就行
也可在默认的80,443进行设置,将不同的域名转发到不同的端口
不再赘述
同一个端口代理多个https网站
通过这种方式可以实现根据不同的sni返回对应的tls
要注意,stream跟http模块的server不能同时监听同一个端口
# 流量转发核心配置
stream {
# 这里就是 SNI 识别,将域名映射成一个配置名
map $ssl_preread_server_name $backend_name {
xxxx.com web;
xxxtest.com test;
# 域名都不匹配情况下的默认值
default web;
}
# web,配置转发详情
upstream web {
server 127.0.0.1:4443;
}
# trojan,配置转发详情
upstream test {
server 172.21.0.2:1443;
}
# 监听 443 并开启 ssl_preread
server {
listen 443 reuseport;
listen [::]:443 reuseport;
proxy_pass $backend_name;
ssl_preread on;
}
}
解决路径被自动添加端口号 无法访问的问题
把本地的Web应用监听在443端口,这个Web服务器是藏在stream模块后的,换言之,它只需要运行在本地(127.0.0.1),跟stream模块交互即可。stream模块也完全不用监听0.0.0.0,因为它是直接与公网交道的,只需要监听服务器公网IP即可。
# 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;
......
}
将vhost中原来监听443的端口监听到一个其他的端口,比如8443端口;然后在vhost配置文件中的server块中添加port_in_redirect off;
即可
port_in_redirect off
的含义是禁用Nginx反代中重定向到绝对端口。默认值为on。
# 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;
......
}
出现无法访问访客真实ip的情况
这里的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流。
# 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;
......
}
参考:https://blog.xmgspace.me/archives/nginx-sni-dispatcher.html