使用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模块后的,换言之,它只需要运行在本地(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;
  ......
}

解决了上一个问题后,还剩下一个无法获取正确访客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: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 端口方案 - 程小白

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

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

评论

  1. wizard
    Windows Chrome
    已编辑
    2月前
    2021-4-21 9:44:19

    谢谢楼主, 总算配出来了,对于多subdomain的情况建议以下配置,已有的website*.example.com server块就只需要加入"listen [port] ssl http2 proxy_protocol"就可以了。
    stream {
    map $ssl_preread_server_name $stream_map {
    website1.example.com web1;
    website2.example.com web2;
    xtls.example.com beforextls;;
    }
    upstream beforextls {
    server 127.0.0.1:7999;
    }
    upstream xtls {
    server 127.0.0.1:9000;
    }
    upstream web1 {
    server 127.0.0.1:8443;
    }
    upstream web2 {
    server 127.0.0.1:8443;
    }
    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{}外部)。这样配置结构比较简洁。

  2. orzlee
    iPhone Safari
    2月前
    2021-4-12 14:53:04

    你试试这样多配置几个站点,我是用的二级域名,就会出现这问题!

  3. Eric
    Macintosh Safari
    已编辑
    3月前
    2021-4-03 19:02:34

    按照大佬的配置已经完成设置。
    但发现一个问题,会出现先访问哪个域名,另一个域名也会访问同样的页面。
    例如:打开 1.domain.com,然后新标签页打开 2.domain.com 后,他们的内容是一样的。同样的步骤,反过来也是,不是1变成2,就是2变成1。

    • Xiaomage 博主
      Windows Edge
      3月前
      2021-4-03 22:14:00

      大佬不敢当😂前面几位为我开路的才是大佬
      理论上不会出现这种问题的,SNI会让Nginx把流量转发到正确的位置上去的,出现这个情况应该是配置问题,或者可能是配置了Nginx层面上的缓存(比如PageSpeed模块)引起的。
      1.domain.com和2.domain.com的Server配置中是都需要配置listen 127.0.0.1:443 ssl http2 proxy_protocol的,其他能不变的参数尽量保持相同。
      另外如果你的站点启用了QUIC,尽量每个vhost都要启用,不要有的vhost启用,有的不启用,否则是有可能出现跟你类似情况的问题的。

      • 小白白
        Macintosh Chrome
        2月前
        2021-4-07 13:13:04

        我也遇见了同样的问题,多个网站显示一样的页面。

        • Xiaomage 博主
          Windows Edge
          2月前
          2021-4-09 10:31:29

          我觉得可能是你们配置文件stream模块中的的 $ssl_preread_server_name 没有配置好,有多少个网站域名都要添加到里面的,比如website1.example.com和website2.example.com,配置文件就要这样写:

          stream {
            map $ssl_preread_server_name $stream_map {
              website1.example.com web;
              website2.example.com web;
              default web;
            }

          配置好了应该就问题不大了,还不行的话可以用pastebin贴一些配置文件上来,或者私发给我看看。

        • orzlee
          Windows Chrome
          2月前
          2021-4-12 5:25:16

          我也碰到这问题了,不管是Nginx还是xray自己的SNI都是这样,我试了chrome内核的浏览器都这尿性,iphone safari没这问题,Nginx配置也没有任何缓存设置,nginx日志我是按站点分开的,里面能看到其他域名请求。。。
          有时会不同域名输出内容一模一样,有时候输入不同域名直接301跳转到刚刚开打过的域名(域名都是这台服务器的),不知道什么原因,网上找了半天也没什么头绪!
          server_name应该无效了,都是通过端口分流。
          xray SNI看日志经常获取不到realName,也就是回落域名。

        • Xiaomage 博主
          Windows Edge
          2月前
          2021-4-12 12:05:10

          很多人遇到了相同的问题,我仔细检查了一下我博客中的配置文件,确实有一个错误,最后Nginx配置文件中的upstream配置应该是这样的,之前把端口号写错成8000了,应该是443,现在已经修正:

            upstream web {
              server 127.0.0.1:443;
            }

          但是这个错误应该会使服务器上所有的网站无法打开,浏览器报错:似乎 xxx.com 关闭了连接。很多人出现的网站域名对不上的情况应该还是配置文件的问题,本站所用的服务器都是这样配置的,并没有因此遇到过这类问题...拿不到有问题的配置文件,只靠描述我也无法确定问题在哪里...

        • orzlee
          Windows Chrome
          2月前
          2021-4-12 18:30:01

          我觉得应该不是你配置的问题,因为使用Xray自己的SNI也是这样,应该是Nginx问题,可能缺少什么配置还是怎么,要好好研究一下!

  4. 清雨
    Windows Edge
    已编辑
    4月前
    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降低了延迟,速度起飞,配置正确的话带宽应该是不会急剧下降的。

  5. 58.212.43.*
    Windows Opera
    4月前
    2021-2-26 16:32:12

    太难了 不会分流

  6. hunter
    Android Chrome
    5月前
    2021-2-03 11:52:08

    大佬牛批

发送评论 编辑评论


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