Trojan 共用 443 端口方案
HTTPS 已经是互联网服务准入的基本门槛了,同时 443 端口作为 HTTPS 请求的默认端口,在虚拟主机服务的支持下,搭配的天衣无缝。
但是 Trojan 比较特殊,因为它的工作方式导致了其必须直接对接流量入口,否则其协议无法被服务端正常识别,同时为了增加服务的隐蔽性,一般会把它配置在 443 端口。但是 443 端口只有一个,虽然 Trojan 提供了「非标请求」的转发功能,但是毕竟是一个新生事物,所有流量都过它手,在稳定、性能、灵活等等方面都不够好,而且还不支持 TLS 转发。
在此背景下,我设计了一套 Trojan 和 Nginx 公用 443 端口的方案,同时也支持 Docker 部署。
首页说一下最后的成品架构:
主要的工作就是在 dispatch 阶段的流量分发。因为都是 HTTPS 流量,所以在确定分发策略前得有一点相关知识。
在虚拟主机流行前,一个服务 IP 只会有一个 TLS 服务站点,只会有一张 SSL 证书,所以请求来了就只有一张证书,没得选直接用就好了。
后面虚拟主机流行起来,一个服务 IP 可以有多个 TLS 服务站点,那就有多个 SSL 证书,那怎么明确这次请求用哪张证书呢?
于是就有了 SNI(TLS 服务器名称指示),它要求在一个 IP 有多个 TLS 服务站点的情况下,客户端在初始 TLS 握手期间指定要连接到哪个站点,数据上的实现就是在 Client Hello 阶段里面新增一个 server_name
字段。
所以它是现在支持同一个 IP 上配置多个 HTTPS 主机的基础:
server {
listen 443 ssl;
server_name www.chengxiaobai.cn;
ssl_certificate www.chengxiaobai.cn.crt;
}
server {
listen 443 ssl;
server_name nothing.chengxiaobai.cn;
ssl_certificate nothing.chengxiaobai.cn.crt;
}
我们的流量分发策略也是基于这个数据来的。
首先得明确,Trojan 是无法通过 Nginx 在 7 层进行代理的,所以它设定必须在流量入口,Nginx 都只能挂在它的后面。
但是我仍然有个疑惑:为什么不能在 http 模块下使用
proxy_pass
进行 HTTPS 反向代理 Trojan?我尝试了一下,发现是建立连接失败了,我怀疑是 Trojan 本身对建立连接的过程做了数据校验或者本身没有完全实现 HTTP 协议(HTTP CONNECT 部分)?
http 模块的
proxy_pass
是在七层做的转发,可能需要挂载证书进行流量的解密识别,再加密请求,相当于做了一次中间人(MITM, Man-in-the-Middle)代理,又或者因为使用 HTTP CONNECT 来建立隧道而导致的链接失败?里面涉及到的东西就太多了,有空翻源码看看吧。
那无法在 7 层转发,就在 4 层转发吧。
Nginx 支持基于 SNI 的 4 层转发。简单说就是:识别 SNI 信息,然后直接转发 TCP/UDP 数据流。这个可以比 7 层的虚拟主机转发厉害太多了,该功能由 ngx_stream_ssl_preread_module
模块提供,但是 Nginx 默认不启用该模块,配置起来也很简单,需要注意的是该模块属于 stream
,不是大家常用的 http
。
Nginx 配置:
user nginx;
pid /var/run/nginx.pid;
# 其他配置保持默认即可
# 流量转发核心配置
stream {
# 这里就是 SNI 识别,将域名映射成一个配置名
map $ssl_preread_server_name $backend_name {
web.cn.chengxiaobai web;
vmess.cn.chengxiaobai vmess;
trojan.cn.chengxiaobai trojan;
# 域名都不匹配情况下的默认值
default web;
}
# web,配置转发详情
upstream web {
server 127.0.0.1:10240;
}
# trojan,配置转发详情
upstream trojan {
server 127.0.0.1:10241;
}
# vmess,配置转发详情
upstream vmess {
server 127.0.0.1:10242;
}
# 监听 443 并开启 ssl_preread
server {
listen 443 reuseport;
listen [::]:443 reuseport;
proxy_pass $backend_name;
ssl_preread on;
}
}
http {
# 这块保持不变即可
}
简简单单几行配置,就完成了流量分发,最后将 Trojan 和 V2Ray 的配置端口修改一下和上面的配置保持一致即可。
V2Ray 搭配的 Nginx 配置,用于解密 TSL 加密信息提升整体链路性能,并且实现站点伪装:
server {
# 开启 HTTP2 支持
listen 10242 ssl http2;
server_name vmess.cn.chengxiaobai;
gzip on;
gzip_http_version 1.1;
gzip_vary on;
gzip_comp_level 6;
gzip_proxied any;
gzip_types text/plain text/css application/json application/javascript application/x-javascript text/javascript;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_certificate /etc/ssl/vmess.cn.chengxiaobai.crt;
ssl_certificate_key /etc/ssl/private/vmess.cn.chengxiaobai.key;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# WS 协议转发
location /fTY9Bx7c {
proxy_redirect off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $http_host;
proxy_pass http://127.0.0.1:16881;
}
# 非标请求转发伪装
location / {
proxy_pass http://127.0.0.1:80;
}
}
V2Ray 配置:
{
"api": {
"tag": "api"
},
"dns": {
"servers": [
"https://1.1.1.1/dns-query",
"https://8.8.8.8/dns-query",
"localhost"
]
},
"inbounds": [
{
"allocate": {
"strategy": "always"
},
"port": 16881,
"protocol": "vmess",
"settings": {
"clients": [
{
"alterId": 64,
"id": "唯一ID",
"level": 1
}
]
},
"sniffing": {
"enabled": false
},
"streamSettings": {
"network": "ws",
"security": "none", // 注意这里没有 TLS,因为 TSL 已经交给 Nginx 处理
"wsSettings": {
"path": "/fTY9Bx7c" // 可以复杂一点,避免嗅探
}
}
}
],
"log": {
"access": "/var/log/v2ray/access.log",
"error": "/var/log/v2ray/error.log",
"loglevel": "error"
},
"outbounds": [
{
"protocol": "freedom",
"settings": {
"domainStrategy": "AsIs"
},
"tag": "direct"
},
{
"protocol": "blackhole",
"settings": {
"response": {
"type": "none"
}
},
"tag": "blocked"
}
],
"policy": {},
"reverse": {},
"routing": {
"domainStrategy": "IPIfNonMatch",
"rules": [
{
"ip": [
"0.0.0.0/8",
"10.0.0.0/8",
"100.64.0.0/10",
"127.0.0.0/8",
"169.254.0.0/16",
"172.16.0.0/12",
"192.0.0.0/24",
"192.0.2.0/24",
"192.168.0.0/16",
"198.18.0.0/15",
"198.51.100.0/24",
"203.0.113.0/24",
"::1/128",
"fc00::/7",
"fe80::/10"
],
"outboundTag": "blocked",
"type": "field"
}
]
},
"stats": {},
"transport": {}
}
Trojan 配置:
{
"run_type": "server",
"local_addr": "127.0.0.1",
"local_port": 10241,
"remote_addr": "127.0.0.1",//转发回 nginx 默认页面
"remote_port": 80,
"password": [
"密码"
],
"log_level": 3,
"ssl": {
"cert": "证书地址.crt",
"key": "证书地址.key",
"key_password": "",
"cipher": "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384",
"cipher_tls13": "TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_256_GCM_SHA384",
"prefer_server_cipher": true,
"alpn": [
"http/1.1"
],
"alpn_port_override": {
"h2": 81
},
"reuse_session": true,
"session_ticket": false,
"session_timeout": 600,
"plain_http_response": "",
"curves": "",
"dhparam": ""
},
"tcp": {
"prefer_ipv4": false,
"no_delay": true,
"keep_alive": true,
"reuse_port": false,
"fast_open": false,
"fast_open_qlen": 20
},
"mysql": {
"enabled": false,
"server_addr": "127.0.0.1",
"server_port": 3306,
"database": "trojan",
"username": "trojan",
"password": "",
"cafile": ""
}
}
其他服务按照之前使用正常配置即可,毕竟 Nginx 可以转发到 Trojan,还可以转发到 Nginx 自身从而实现其他服务的反向代理,当前笔者使用的流量分发架构:
Nginx 层统一管理收敛流量入口,整个主机只用开启 443 和 SSH 端口即可,同时各个模块都做了伪装,「非标请求」看到的都是正常的页面,而且 WS 协议的 CDN 大法完美支持。
同时得益于 Nginx 的优秀性能和对 HTTP 协议的支持力度,全部请求都可以 HTTP2。
Youtube 速度相比之前 Trojan 直连没有明显差异。
把专业的事交给专业的人 😎 。
最后补充一个「获取 real client IP 为 127.0.0.1 」问题的解决方案,其实这个问题已经完全可以独立一篇文章来说明了,为了方便大家阅读,就放在一起了。
这个是我疏忽了,因为我的 Web 服务就是本博客,最前面是 CDN,它已经在 header 里面包含了 real client IP ,而我针对这种情况做了适配,所以这个问题没有暴露出来。
我去掉 CDN 后发现获取的 real client IP 确实是 127.0.0.1。这是为什么呢?
首先得介绍下俩个知识点:
第一,一般获取 real client IP 都是通过 header 里面的 remote_addr
或者 http_x_forwarded_for
。
先说下 remote_addr
的来源,它不是 HTTP 特有的,是在建立 TCP 链接的时候就确定的,所以它不容易伪造。
而 http_x_forwarded_for
是 HTTP 特有的,每经过一次七层代理就会往后 append 一次代理服务器 IP,所以的格式为: 用户真实IP(remote_addr
),代理服务器 1 的 IP,代理服务器 2 的 IP ......
第二,4 层 TCP 负载均衡/代理的机制。
Nginx 直接转发 TCP 请求,实际上是针对各个后端服务各新起了一个 TCP 链接,直接在 TCP 层面转发数据,最大层度保证了数据完整性并且性能更高。但是因为前面介绍过的 remote_addr
的确定时机导致各个后端拿到的数据其实是 Nginx 的 IP。
这是一个客观存在的问题,各个 4 层代理软件都有这个问题。
我查了下 Nginx 的官方文档,Nginx Plus 很早就给了方案,但是那是付费版,现在也免费了,就是两种方案。
方案一:使用 Proxy Protocol 协议进行通信。
前面说过各个 4 层代理软件都 real client IP 问题,所以针对这种情况专门设计了一种新的通信协议:代理协议(Proxy Protocol) 用来传递用户真实 IP 。
代理协议(Proxy Protocol),是 HAProxy 的作者 Willy Tarreau 于2010年开发和设计的一个 Internet 协议,通过为 TCP 添加一个很小的头信息,来方便的传递客户端信息(协议栈、源IP、目的IP、源端口、目的端口等),在网络情况复杂又需要获取用户真实 IP 时非常有用。其本质是在三次握手结束后由代理在连接中插入了一个携带了原始连接四元组信息的数据包。
Nginx 也支持了 Proxy Protocol,所以只用在转发端和接受端都配置上代理协议支持即可。
在转发层新增 proxy_protocol
配置。
stream {
server {
listen 443 reuseport;
listen [::]:443 reuseport;
proxy_pass $backend_name;
proxy_protocol on;
ssl_preread on;
}
}
同时后端服务配套也要在 listen
规则里面配置接受 proxy_protocol
。
server {
listen 10242 ssl proxy_protocol;# 注意这里新增了协议类型
server_name vmess.cn.chengxiaobai;
# ...
# 省略一些配置示例
# ...
}
但是该方案也有弊端,就是一旦接受端配置了代理协议,那就不再支持非代理协议的请求。
Proxy Protocol 的接收端必须在接收到完整有效的 Proxy Protocol 头部后才能开始处理连接数据。因此对于服务器的同一个监听端口,不兼容非 Proxy Protocol 的连接。如果服务器接收到的第一个数据包不符合 Proxy Protocol 的格式,那么服务器会直接终止连接。
方案二:使用 proxy_bind
语法。
user root; # 注意这里的权限要求
#requires additional OS-level configuration!
#https://WWW.kernel.org/doc/Documentation/networking/tproxy.txt
stream {
server {
listen 443 reuseport;
listen [::]:443 reuseport;
proxy_bind $remote_addr transparent; # 此次新增
proxy_pass $backend_name;
ssl_preread on;
}
}
这个方案不修改通信协议,但是 Nginx 本身需要 root 用户权限来运行,并且需要配置后端服务机器上的本地路由表来起到 IP 重定向的作用。
真实的运转流程是这样的:这个配置修改了 TCP 连接的源地址为 real client IP 而非 Nginx 自己的 IP,但是这样后端服务返回数据的时候会有异常,因为真实的 client 并没有和后端服务建立起通信链接,所以必须在后端服务上配置路由把请求响应流量转发到 Nginx 机器上,同时 Nginx 机器本地配置 IP 拦截规则,将改流量重定向到 Nginx 服务上,完成一次「流量劫持」。
类比下大家常知的的中间人(MITM, Man-in-the-Middle)攻击更容易理解,相当于 Nginx 做了一次中间人,但是因为是 4 层 TCP 协议,所以比 7 层的 HTTP 协议用到的工具更底层一点。
小结一下,不建议大家使用方案二,毕竟安全性和操作性都不符合最佳实践。目前 V2Ray 和 Trojan 对用户的真实 IP 其实没有要求,如果你强依赖用户的真实 IP(更多的依赖场景是在 Web 服务上?),建议使用方案一。
参考资料:
TCP/UDP Load Balancing with NGINX: Overview, Tips, and Tricks
Nginx real client IP to TCP stream backend
七层LB-NGINX 客户端获取协议Proxy Protocol介绍
Nginx基于TCP/UDP端口的四层负载均衡(stream模块)配置梳理
本作品由 程小白 创作,采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可,可自由转载、引用但需署名作者且注明文章出处。
原文地址:https://www.chengxiaobai.cn/record/trojan-shared-443-port-scheme.html
大佬咨询下,按照这样的流量分发架构,v2ray侧的配置文件模板有么, 嫁接v2ray始终不成功.trojan和web这两个路径的流量分发都正常了.
已更新 V2Ray 相关的配置
非常感谢,已经搞定!
老大,有没有完整的nginx的配置啊?看不懂不会用呢?谢谢。
已更新
老大,现在很多人在用Trojan+Caddy,能不能写个关于Caddy基于sni转发的教程?这样用Caddy当前端,Trojan和v2ray在后端,只暴露443一个端口。多谢。
目前 Nginx 已经实现你的期望实现。
Caddy 目前不支持四层流量转发,所以目前所有 Trojan + Caddy 组合都是把 Trojan 做为流量入口,和 Nginx 方案的对比已在文章开头说明。
Trojan nginx 的配置,是不是就是按照默认的 80 端口就可以了,还是需要改为 listen Nginx 配置里面的端口?
我来替老大回答,如果错误请指出来。
Nginx对外要配置80/443两个端口,80是伪装网站,443则是进一步检查是不是Trojan和v2ray的流量的端口。Trojan nginx的端口配置,要改为 listen Nginx 配置里面的端口。前面配置了10241,则这里也要配置10241。
流量流程:如果你用Trojan,例如域名是trojan.mydomain.com,Nginx在443端口收到请求后,会根据这个sni,将所有的含此域名的流量(含正常WEB访问以及Trojan翻墙流量)都转发给本地的Trojan服务监听端口10241。Trojan服务会判检查该流量,如果是翻墙流量(协议正确,密码正确),就提供翻墙服务;如果不是,则转发到本地的80端口,也就是Nginx的伪装网站的网页界面。
如果在同一 IP 还有其他 SSL 服务,也应该需要在 Nginx 配置里加上吗?之后把原来 listen 443 的都要改成相应的端口吗?估计等有空去试一下。
stream 和 http 工作在 Nginx 不同的网络层级上,所以 443 并不冲突。
按照本文的例子,你可以把以前 listen 443 的 http server 改成 listen 10240,因为默认服务走的就是 10240 端口,然后 Nginx 会按照 server_name 进行流量分发,stream 配置无需额外修改。
v2ray 和几个同一 IP 的 SSL 服务都没有问题了,就是 Trojan 搞不定,能否把 Trojan 搭配的 Nginx 配置发一下,麻烦了。
好像是 Trojan 的 docker 没有读取到 Trojan 配置, --net host 运行后用 docker ps -a 看不到 PORTS 有内容,如果指定端口号运行又会断断续续,不知道哪里出错?
如果你用的是
trojangfw/trojan:latest
这个 image 的话,Trojan 的配置的路径应该是/config/config.json
说的很正确,看来你已经完全理解整个运作流程了👍
请问将Ningx换成Caddy的话,应该如何来配置?
谢谢博主前面已经回答了。
深入探讨:现在很多人从v2ray转到Trojan,因为v2ray多了一层ws转发,造成速度下降。如果将你Nginx第七层配置 # WS 协议转发部分的“location /fTY9Bx7c”删掉,然后改动“# 非标请求转发伪装”里的80端口为后台v2ray的侦听端口,可以这样实现v2ay+tls+tcp吗?如果可以,博主再出个教程,那就完美了。
V2Ray + TLS + TCP 模式的实现和 Trojan 的实现是一样的,直接在 Nginx 层转发,然后由 V2Ray 自己负责 TLS 和协议解码处理。
补充下个人看法:了解了 TCP 模式的工作流程就能发现它其实没有太大优势,文中的配置介绍了:Trojan、V2Ray + WS,两者的协议都是真实的 HTTP 协议,安全性高,并且前者速度快,后者支持 CDN,隐蔽性好。最后生态上这俩种协议也是被 UI 工具支持最多的(ClashX 就不支持 V2Ray 的 TCP 😂)。
我现在是用tls-shunt-proxy,在前台负责TLS解码以及https站点伪装,然后将明文流量发往后台的v2ray,目的就是减少一次v2ray的ws工作,提升速度。希望能将大佬你的Nginx改动一下,也能达到同样的效果。
首先我觉得这是一项很有必要且对很多人有意义的工作,但是对一般人而言,比如我,小鸡直接trojan裸奔,现在很多人过多的追求web伪装了,如果只是学习工作娱乐上上网不为非作歹我觉得毫无必要过度追求web伪装,tls1.3保证墙不知道你的流量干什么就够了,另外一般配置的小鸡装了这么多服务后性能上吃得开吗,很多人真的就是跟风!
明确自己的目标,做对自己有价值的事情
大佬可以把Trojan 搭配的 Nginx 配置发一下吗,我好排查错误,谢谢了
大佬,问一下,如果我trojan、v2ray、mtg(go实现的telegram代理)各一个域名,但web有n个域名,该怎么配置?你这边nginx的stream模块示例只有一个web呀。
stream 和 http 工作在 Nginx 不同的网络层级上,所以会先走 stream 逻辑,没有匹配的域名会走到 default 逻辑,转发到 default 配置的地址(address 和 port)上,你的 web 域名都 listen default 里面的 port,然后 Nginx 会按照 server_name 进行流量分发,就和正常虚拟主机配置一样了。
对的,确实是这样,但是我突然有一个疑问,你的vmess和web为什么是分开的?按道理,假设我不用trojan的情况下,vmess(即v2ray)不是跟web使用相同域名,然后在web的http server里用一段location /xxxx 这样来识别并转发到v2ray吗?(就是所谓的分流),现在只是在前面加了一个stream,但后面的还是不变的呀也就是说,不管是web还是vmess都应该走web,然后由web里的location区分出ws请求,再转给v2ray呀
你这样也能实现,我这么做时因为我个人实际需求,俩者的 CDN 分流策略不同,所以 vmess 和 web 分开了。
感谢大佬,确实是这样