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

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

众所周知,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的配置&原理

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

因为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端口上。怎么解决这个问题呢?

把本地的Web应用监听在443端口上不就解决这个问题了吗?但0.0.0.0的443端口已经被stream模块占用了,443端口好像用不了。这个Web服务器是藏在stream模块后的,换言之,它只需要运行在本地,跟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:8000;
  }
  # 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;
  ......
}

解决了上一个问题后,还剩下一个无法获取正确访客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的应用可能就不支持这个协议,像我们例子中的XTLS就不支持。新的问题又来了,怎么破局?

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

# stream模块设置
stream {
  # SNI识别,将一个个域名映射成一个配置名
  map $ssl_preread_server_name $stream_map {
    website.example.com web;
    xtls.example.com beforextls;# 注意这里修改了
  }

  # upstream,也就是流量上游的配置
  upstream beforextls { # 在流量到达XTLS之前,先用stream模块将Proxy protocol的外衣扒掉
    server 127.0.0.1:7999;
  }
  upstream xtls {
    server 127.0.0.1:9000;
  }
  upstream web {
    server 127.0.0.1:8000;
  }
  # 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 端口方案 - 程小白

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

本文永久链接:https://blog.xmgspace.me/archives/nginx-sni-dispatcher.html
本文文章标题:使用Nginx进行SNI分流并完美和网站共存
如文章内无特殊说明,只要您标明转载/引用自Xiaomage's Blog,您就可以自由的转载/引用文章。禁止CSDN/采集站采集转载。
授权协议:署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)

评论

  1. 清雨
    Windows Edge
    已编辑
    5天前
    2021-3-01 23:35:55

    参考博主的设置,先行测试了 VLESS + WS + TLS + CDN 组合下利用 NGX 的 stream 模块复用 443 端口(仅仅是为了测试,毕竟此方案本身是不需要 stream 模块来复用 443 端口的)。
    现在有个问题,用于伪装的站点,没过 CDN 的情况下,可以正常获取到客户端真实 IP,但是过 CDN 的好像不行(已经测试过配置成 CDN 厂商提供的 real_ip_header,还是只能获取到 CDN 的 IP)。
    科学组件倒是都能正常获取客户端真实 IP,但是加上 stream 模块后,链路带宽急剧下降,不知博主 stream 复用端口配合 XTLS 协议使用的情况下,链路带宽如何?

    • Xiaomage 博主
      Windows Edge
      4天前
      2021-3-02 21:35:20

      我这边自己测试的话,使用Cloudflare的CDN是能正常获取到访客IP的,Cloudflare向服务器传送访客IP的字段是x-forwarded-for,一般CDN也都是这个字段,可以尝试用"real_ip_header X-Forwarded-For"接收一下,我自己也没有用其他手段获取访客IP的,直接就可以获取到。
      链路带宽急剧下降的这个问题我没有遇到过,stream 复用端口配合 XTLS降低了延迟,速度起飞,配置正确的话带宽应该是不会急剧下降的。

  2. 58.212.43.*
    Windows Opera
    1周前
    2021-2-26 16:32:12

    太难了 不会分流

  3. hunter
    Android Chrome
    1月前
    2021-2-03 11:52:08

    大佬牛批

发送评论 编辑评论


|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇