Skip to content

Instantly share code, notes, and snippets.

@inevity
Last active August 9, 2020 08:40
Show Gist options
  • Save inevity/18dde844ddf14ed561905210999d57cb to your computer and use it in GitHub Desktop.
Save inevity/18dde844ddf14ed561905210999d57cb to your computer and use it in GitHub Desktop.
基于协议数据的简易分流工具

一、摘要

基于协议数据的统一的代理分流工具。nignx 作为前端,tls 终止并基于 tls 承载的数据进行分流。简单易安装,仅仅添加 21 行代码。简化官方 TCP + TLS + Web 方案;去掉了 / 替换 tls 分流器;也可以分流到 trojan;让 nginx 可以路由到 v2 的 http2 方案。

二、目的

看到有人根据 trojan 原理基于 v2ray 做了个类似功能的定制即 TCP + TLS + Web,就是在 TLS 层上传输 vmess 或者其他比如 http web 流量。
本人好奇,遂群里有如下互动:

Q:TCP + TLS + Web 为啥需要 web 前需要 hproxy 啊,nginx 也有这种功能啊 非要前面 hproxy,后面再弄个 nginx/httpd。 对于个人使用没必要吧。 当然你搭建商业的除外
A: 不要再问这种问题了,你觉得可以就自己搭,搭成了可以写给教程 pr
Q:nginx 的 stream 块不行吗
A: 不要再问这种问题了,你觉得可以就自己搭,搭成了可以写给教程 pr

所以目的很简单,就是去掉那个 haproxy, 只用 nginx 来分流,这样更适用于个人 vps 的搭建。
另外的一个目的,也作为 trojan 的前端,即 nginx 也可以分流到 trojan 后端。

三、方案的可行性分析

所有的 tls 在 nginx 终止,后端代理只进行明文协议的解析和处理,不做 tls 处理。

基于 SNI

有些域名既可以放我们的博客或网站,也可以放到我们的代理客户端的 SNI 里,所以不考虑 SNI 的分流。

基于 SSL 握手后的 app 数据分流

根据 TSL 握手后的数据进行分流,这里从 tcp/udp 层面考虑,tls 之上的数据都是需要我们分流的。所以对于 nginx,我们采用 stream 的配置方式。
如果是 HTTP1.1,就让直接到某个正常的网站,这里要求我们没有把代理协议承载在 https 上。
如果是 HTTP2, 就让路由到 h2 代理后端比如 v2ray 或者 trojan. 虽然 trojan-go 支持 http2,但考虑到 trojangfw 反对 http2 的引入,所以我们的实现也不考虑 trojan-go 的 http2. 这里要求 v 端的实现可以考虑到正常 http2 req 的网站的返回即 failback(目前没有实现)。
如果是裸的 vmess 协议数据即常说的 TCP+TLS+VMESS+WEB,直接转发到 v 后端,这里要求 v 后端在解析失败的情况下可以返回错误页面以便伪装,也是另一种 failback(目前 v 官方没有实现,倒是有个开源实现 )
如果是裸的 trojan 协议数据,直接转发到 tj 后端。 这里 trojan 实现了 failback。

四、此方案与 trojan 的区别

  • Active Detection
    对于 tj,All connection without correct structure and password will be redirected to a preset endpoint,including normal https traffic and faked traffic。 但是对于 v2ray 却没有这样的功能,即上面所提的 failback 功能。当然社区有人在做了。
  • Passive Detection
    Tj 的被动检测方法是 if you are not visiting an HTTP site, then the traffic looks the same as HTTPS kept alive or WebSocket. Because of this, trojan can also bypass ISP QoS limitations. 对于我们这么部署 v2ray 的方式,也是具有这样的功能的。
    这样部署的 trojgan-go 只不过把所有 tls 的功能放到 nginx 上,其他功能还在后端 tj 上。

五、实现

install nginx distribution Openresty

我们采用 luajit 来实现。Openresty 安装参考官方指导.
Debian 上的安装方法:

sudo systemctl disable nginx
sudo systemctl stop nginx
sudo apt-get -y install --no-install-recommends wget gnupg ca-certificates
wget -O - https://openresty.org/package/pubkey.gpg | sudo apt-key add -

# add this to /etc/apt/sources.list
codename=`grep -Po 'VERSION="[0-9]+ \(\K[^)]+' /etc/os-release`

echo "deb http://openresty.org/package/debian $codename openresty" \
    | sudo tee /etc/apt/sources.list.d/openresty.list

# end
sudo apt-get update
sudo apt-get -y install openresty

nginx config file

{
worker_processes  auto;
error_log  logs/error.log  info;
events {
    worker_connections  1024;
    use epoll;
    multi_accept on;
}
http {
    server {
        server_name build.approachai.com;

        # Enable QUIC and HTTP/3.
        listen 127.0.0.1:444 quic reuseport;
        # must set !!! dup error
        http3_early_data on;

      #  # Enable HTTP/2 (optional).
      #  listen 127.0.0.1:444 ssl http2;
        listen 444 http2;

        #listen 444 http2;

      #  # todo route the http3 traffic (vmess or normal traffic)

        ssl_early_data on;
        ssl_certificate      fullchain.pem;
        ssl_certificate_key  privatekey.pem;

        # Enable all TLS versions (TLSv1.3 is required for QUIC).
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;

        # Request buffering in not currently supported for HTTP/3.
        proxy_request_buffering off;

        # Add Alt-Svc header to negotiate HTTP/3.
        add_header alt-svc 'h3-27=":443"; ma=86400';
    }

    server {
        server_name xx.com;
        listen 127.0.0.1:445;
        location /yyyy {
             if ($http_upgrade != "websocket") {
                return 404;
             }
             add_header Cache-Control no-store;
             proxy_set_header Cache-Control no-cache;
             proxy_set_header Connection Keep-Alive;

             proxy_redirect off;
             proxy_pass http://127.0.0.1:10006;
             proxy_http_version 1.1;
             proxy_set_header Upgrade $http_upgrade;
             proxy_set_header Connection "upgrade";
             proxy_set_header Host $host;
             proxy_set_header X-Real-IP $remote_addr;

             proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
             proxy_read_timeout 300s;

        }
        location / {
             proxy_set_header X-Real-IP $remote_addr;
             proxy_set_header HOST $http_host;
             proxy_set_header X-NginX-Proxy true;

             proxy_pass http://127.0.0.1:444;
             proxy_redirect off;
        }
        location ~ /\.well-known/acme-challenge/ {
            root /;
            allow all;
            try_files $uri =404;
            break;
        }

  }



}

stream {

    resolver 127.0.0.1;
    lua_add_variable $vmess;
    server {
        listen           443 ssl reuseport backlog=4096;
        listen [::]:443 ssl reuseport;
        listen           443 udp reuseport;
        listen [::]:443 udp reuseport;

        ssl_certificate      fullchain.pem;
        ssl_certificate_key  privatekey.pem;


        ssl_session_timeout 1d;
        ssl_session_cache shared:SSL:20m;


        ssl_protocols TLSv1.1 TLSv1 TLSv1.2 TLSV1.3;
       # ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv3:+EXP;
        ssl_prefer_server_ciphers on;
        # 16k
        proxy_buffer_size          256k;
        # 16k
       # preread_buffer_size 4k;
        preread_buffer_size 58;
        preread_by_lua_block {
               if ngx.var.protocol == "TCP" then
                      local n = ngx.var.ssl_server_name
                      ngx.log(ngx.DEBUG, "$ssl_server_name = ", n )

                   local sock, err = ngx.req.socket()
                   if not sock then
                         ngx.say("500 Internal Server Error")
                         return ngx.exit(500)
                         -- or we forward to an http server return 500
                         -- or use the 50x html from nginx
                   end

                   local data, err = sock:peek(16)
                   local datal, err = sock:peek(58)
                   ngx.log(ngx.DEBUG, "The stream = ", datal)
                   ngx.log(ngx.DEBUG, "ssl_server_name = ", n )
                   if string.byte(datal:sub(57), 1, 2) == 13 then
                       -- for Trojan
                       ngx.var.vmess = "453"
                   elseif (string.match(datal, "HTTP/2.0") and n == "xx.xx.com") then
                       -- for V2Ray's tcp +TLS +h2c ok
                       ngx.var.vmess = "10008"

                       -- maybe faked http2 to detect us ,so need parse the body to failback to normal url
                       -- or by vmess
                       -- maybe we use trojan-go http2,but now giveup

                       -- will filter by path and sni name above  to v2ray h2 and other to normal h2 site or 404.
                       -- but need parse the http2, so we only filter by sni


                   elseif (string.match(datal, "HTTP/2.0") and n ~= "xx.xx.com") then
                       -- for normal http2 traffic
                       ngx.var.vmess = "444"

                   elseif (string.match(datal, "GET /yyyyy HTTP/1.1") and n == "xx.xx.com") then
                      -- for v2 ws traffic
                      -- route ws data by sni and path or just by header upgrade.
                      -- note: should have not plain ws., now the http2 cannot share with http1 listening.
                      ngx.var.vmess = "445"

                   -- elseif (string.match(data, "GET /yyyyy HTTP/1.1") and n == "xx.xx.com") then
                   elseif string.match(datal, "GET /yyyyy HTTP/1.1")  then
                      -- for tj ws
                      ngx.var.vmess = "458"
                   elseif string.match(datal, "HTTP/1.1") then
                       -- for normal http1.1 traffic, http1.1 not to endure any proxy data
                       -- for normal http req
                       ngx.var.vmess = "444"

                   -- todo: route mtproto
                   -- todo: route ss
                   else
                   -- for V2Ray's tcp+TLS +web ok
                       ngx.var.vmess = "10007"
                       ngx.log(ngx.INFO, "The last")
                   end
              else -- udp
                      -- route quic data
                      -- route http3 data
                      -- router accoridng the protocol character such dns/quic/http3/other
                      -- for quic, to carry http3 or vmess, need route here
                      --           for http3, need diff normal http3 traffic and vmess
                      --           now v2ray only support quic transport,not http3 transport.


                     -- impl quic parse module or just judge same as belowe
                     -- lua get_body_data or peek impl
                     -- js/rust
                     -- local sock, err = ngx.req.socket()
                     -- if not sock then
                     --   ngx.log(ngx.INFO, "The UDP socket nil")
                     -- end

                    --  local data, err = sock:peek(1)
                    --  no impl for udp
                    --  local data, err = sock:receive()
                    --  ngx.log(ngx.INFO, "The UDP stream = ", data)
                      ngx.var.vmess = "444"
             end --for the udp/tcp
        }
         proxy_pass 127.0.0.1:$vmess;
  }# server block
}


创建 nginx 目录 a

rsync -av /usr/local/openresty/nginx/[conf,html,logs] a
cd a
修改 a 下 conf/nginx.conf 为如上配置。
sudo openresty -p .
sudo openresty -p . -s reload
这样可以不污染 /usr/local/openresty/nginx 下的配置。

v's tcp +tls +h2c config

HTTP/2+TLS+WEB
注意这里 h2 的 tls 在 nginx 实现,即 h2c 配置。
对此参考的改进是采用了 nginx 作为前端。
android v2rayng version 1.2.6 有个 bug: 设置 allowsecure=true 不起作用,当然如果你的服务器 cert 正常,一般就设置为 false 就好。

v's tcp+tls +web config

可以参考 TCP + TLS + Web 的服务器配置。
与此参考不同的是去掉 haproxy。并且基于协议内容分流而不是 SNI。相对 TCP+TLS 分流器 , 这个方案更简单了。

trojan-go

禁止 tls 的处理的 trojan-go 版本目前处于 github 的 dev 分支. 需要自行编译相应架构的客户端或者服务器端。
server config

{
    "run_type": "server",
    "local_addr": "127.0.0.1",
    "local_port": 453,
    "remote_addr": "127.0.0.1",
    "remote_port": 80,
    "password": [
        "xxxx"
    ],
    "ssl": {
        "serve_plain_text": true
    }
}

client config 如正常的 tj 配置。

六、性能

  • vmessping 延迟 benchmark
tcp+tls+web
--- vmess ping statistics ---
10 requests made, 10 success, total time 26.515340739s
rtt min/avg/max = 993/1748/2544 ms

tls+ws+cdn
--- vmess ping statistics ---
10 requests made, 10 success, total time 30.647379082s
rtt min/avg/max = 1215/2161/7160 ms

tls+h2c
--- vmess ping statistics ---
10 requests made, 10 success, total time 20.977310488s
rtt min/avg/max = 616/1194/3263 ms

最好的延迟是 tls+ h2c .
按理说 tcp+tls (即 tcp+tls+web,没有 http 开销) 应该比 tls+h2c 好点吧,这里为什么性能还弱呢?

  • 浏览器 client 性能 benchmark
    todo

七、todo 和讨论

ssl 的 nginx 配置

不要用提供的,请尽量使用推荐的配置

可否做不同后端的负载均衡

v2ray 的 failback 实现

不合适在 nginx 上实现,因为需要解析对应的协议。
只能在 v2ray 上实现。

八、参考

trojan-gfw/trojan#14
https://trojan-gfw.github.io/trojan/protocol
Vmess + TCP + TLS 方式的 HTTP 分流和网站伪装
Vmess Fail Redirect 简单实现

九、历史

20200611 把tcp+tls+trojan流量、v的tcp +TLS +h2c、normal http2 traffic、v2 ws traffic、 tj ws 流量、normal http1.1 traffic、tcp+TLS +web)即tcp承载vmess流量、UDP的http3流量全部通过nginx路由。

@ju0594
Copy link

ju0594 commented May 15, 2020

感谢,这东西太好用了!之前一直在找怎么用nginx分流tcp和http,没找到,最后只好用sni分流,没想到有openresty这么神奇的东西!

@ju0594
Copy link

ju0594 commented May 15, 2020

贴一下我的配置 之前尝试过sni分流 这次尝试了全部整合在一起
mtproto开在30000端口 v2ray使用tcp+vmess开在10000端口
都通过openresty来自动分流 对外只开放一个443端口

点击展开配置
stream {
    map $ssl_preread_server_name $name {
        www.cloudflare.com mtg;
        your_v2ray_domain.com lua_check;
    }
    upstream mtg {
        server 127.0.0.1:30000;
    }
    upstream lua_check {
        server 127.0.0.1:8443;
    }
    server {
        listen 443 reuseport;
        proxy_pass $name;
        ssl_preread on;
    }

    resolver 127.0.0.1;
    lua_add_variable $vmess;

    server {
        listen  127.0.0.1:8443 ssl reuseport backlog=4096;

        ssl_certificate_key   /key.pem;
        ssl_certificate       /full.pem;


        ssl_session_timeout 1d;
        ssl_session_cache shared:SSL:20m;
        ssl_protocols TLSv1.1 TLSv1 TLSv1.2;
        ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv3:+EXP;
        ssl_prefer_server_ciphers on;


        proxy_buffer_size          256k;

        preread_buffer_size 58;

        preread_by_lua_block {
            local sock, err = ngx.req.socket()
            if sock then
               -- ngx.say("got the request socket")
            else
                ngx.say("failed to get the request socket: ", err)
            end

            local data, err = sock:peek(16)
            local datal, err = sock:peek(58)
            if string.match(data, "HTTP") then
                ngx.var.vmess = "80"
            else
                ngx.var.vmess = "10000"
            end
        }
         proxy_pass 127.0.0.1:$vmess;
  }
}

@inevity
Copy link
Author

inevity commented Jun 5, 2020

        elseif string.match(data, "HTTP") then

在这里也可以细分websocket和不同http1.1

@inevity
Copy link
Author

inevity commented Jun 11, 2020

通过stream 路由ws流量时不需要单独设置http server,可以直接路由到后端。所以上面445 的listening端口的http server block可以直接删掉。

@zbttl
Copy link

zbttl commented Jul 26, 2020

h2c+tls 的配置似乎在 v4.25.0 后失效。
v2ray 会报错 v2ray.com/core/transport/internet/http: http2: unexpected ALPN protocol ; want qh2,无法连通。
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment