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
实现非常简单,frps config 加个参数 vhost_https_port = 8443 (注:随意不冲突现有的)然后如博主所以那样加个 map 和 transmission 别名
本地就也装上 nginx 和 frpc。
本地 frpc 的配置定义客户端
本地nginx 配置代理指向你的服务:
请问开启了Trojan的mysql之后就会翻不了墙是什么原因?没开启的话是可以正常翻墙的.
建议查看下 Trojan 日志
[...]利用Nginx:https://www.chengxiaobai.cn/record/trojan-shared-443-port-scheme.html[...]
我想知道CDN是怎么实现header里面包含 real client IP的?貌似都没那么复杂,即不用改后端配置,也不需要对网站进行适配
CDN 回源的时候已经带上
http_x_forwarded_for
了评论有审核?还是压根没发出?
评论发出不报错即为成功,反垃圾、审核、CDN 等因素会导致评论展现延迟。
那么有什么办法跟CDN一样,不用改后端配置,只改反代配置也能带上http_x_forwarded_for 呢?
在宝塔面板添加proxy_protocol后trojan连接失效了
我观察到在宝塔nginx配置显示 proxy_pass 和 ssl_preread 是用紫色显示的,偏偏 proxy_protocol是黑色的(特意仔细检查了拼写是否有误),难道是宝塔安装的nginx1.18不支持吗?
按照本文教程实现v2ray+trojan+web,只有trojan有些小问题,只能访问google站,其他的都会报错(QQ浏览器):
无法显示此网页
错误代码 ERR_CONNECTION_RESET
刷新网页 | 查看解决办法 | 打开网址导航
您所遇到的问题可以通过清理缓存和Cookies来修复 立即修复
Trojan 不支持 Proxy Protocol, 作者也不愿意支持。
如果不想用方案二的话,建议使用 Haproxy 。
Haproxy 可以分别对待发往不同后端的流量,甚至可以 Trojan 走 4 层代理,其他走 7 层代理。通过让支持的后端才用 Proxy Protocol ,完美解决 Trojan 顽固不化的问题。
可以参考:
https://blog.nanpuyue.com/2017/038.html
采用方法1,Trojan无法连接
方法二的话,直接整个网站打不开了。有什么解决方案吗
方法一需要 Trojan 支持,方法二不止改 Nginx 配置,还得配合系统级别的流量转发
系统级别的流量转发要怎么操作呢
配置 iptables/firewalld 即可,可以搜索一下 "iptables forward traffic"