🌊 第03章 · Nginx WS 反代 Xray
反向代理 + 密钥路径
Nginx WS 反代 Xray¶
上一章我们把 Xray 装在了本机 127.0.0.1:10086,它只听本机、不对外。现在我们要给它装一个"前台门卫":Nginx。Nginx 在公网 443 端口接待所有访客,但只有访问那个隐藏路径的请求,才会被悄悄转进 Xray;其它请求一律按这个域名上的正常网站处理。
一句话:Nginx 是"大门",
/ws-xxx路径是"密道"。普通人走大门看的是正常网站;知道密道的人才能进到代理。
本章用本机的真实 Nginx 站点配置作案例。它对应的域名是 bangdream.xyz,这个站点既给线上一个 Web 应用用,也兼任 Xray 的伪装壳。
为什么必须是 Nginx 来做这层"伪装"¶
第 1 章讲过 Xray 自持 TLS 的缺点。用 Nginx 反代,好处在伪装和复用上:
- 443 端口被 Nginx 统一管理,同一台机器可以同时跑"正常网站"和"伪装代理",互相不冲突;
- TLS 证书用 Certbot 自动签发与续期,Xray 完全不用碰证书这类麻烦事;
- 他人扫描你的 443 端口时,看到的是一个正经 HTTPS 网站,而不是一个代理服务——这正是"伪装"的价值;
- 你能同时复用这个域名下的多个业务(Web 应用、GitLab、Xray),全走同一个 443。
关键:WebSocket 反向代理的"特殊头"¶
普通 HTTP 反代只需要 proxy_pass,但 WebSocket 反代必须额外处理握手升级。Nginx 必须显式传递两组头,否则 WebSocket 的 Upgrade 握手会失败:
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
先记住这个"四件套",原理后面解释。再看本机真实配置。
真实配置案例:bangdream.xyz¶
下面这段是从 /etc/nginx/sites-available/bangdream.xyz.conf 里摘出的完整 server 块。重点看 location /ws-f033eda368eb1b99f9d79a4f(密道)是怎么写的:
# BanG Dream! - 主域名 bangdream.xyz
server {
server_name bangdream.xyz www.bangdream.xyz;
access_log /var/log/nginx/bangdream.access.log;
error_log /var/log/nginx/bangdream.error.log;
# ============ 密道:VLESS+WS 伪装路径 (Xray 处理, 端口 10086) ============
location /ws-f033eda368eb1b99f9d79a4f {
proxy_pass http://127.0.0.1:10086;
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_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
# ============ 正常网站(防止敏感文件被代理访问) ============
location ~ ^/(node_modules|scripts)/ { return 403; }
location = /package.json { return 403; }
location = /package-lock.json { return 403; }
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
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_set_header X-Forwarded-Proto $scheme;
}
listen [::]:443 ssl ipv6only=on; # managed by Certbot
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/bangdream.xyz/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/bangdream.xyz/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
逐行拆解"密道" location¶
把最精华的 location /ws-f033eda368eb1b99f9d79a4f 拿出来逐个解释:
| 指令 | 值 | 作用 |
|---|---|---|
location |
/ws-f033eda368eb1b99f9d79a4f |
精确匹配这个隐藏路径——这是"密道钥匙" |
proxy_pass |
http://127.0.0.1:10086 |
转发到后端 Xray |
proxy_http_version |
1.1 |
WebSocket 握手必须用 HTTP/1.1 |
proxy_set_header Upgrade |
$http_upgrade |
透传客户端的 Upgrade 请求头 |
proxy_set_header Connection |
"upgrade" |
告诉后端"这是一条要升级的 WebSocket" |
proxy_set_header Host |
$host |
透传 Host,让 Xray 看到真实域名 |
proxy_set_header X-Real-IP |
$remote_addr |
把真实客户端 IP 传给后端 |
proxy_set_header X-Forwarded-For |
$proxy_add_x_forwarded_for |
追加代理链 IP |
proxy_set_header X-Forwarded-Proto |
$scheme |
标记原始协议(http/https) |
proxy_read_timeout |
3600s |
WebSocket 长连接,超时设大,防断开 |
proxy_send_timeout |
3600s |
同理,向客户端发送的超时 |
为什么
proxy_read_timeout要大? WebSocket 是一条长连接,Xray 代理期间可能很长时间没有数据流动。默认的 60 秒超时会因为"读超时"而强行掐断你的代理。设成3600s是为了让连接能长时间保持不中断。
理解那组"升级头"背后的机制¶
WebSocket 握手是这么工作的:客户端先发一个普通 GET,请求头里带着 Upgrade: websocket 和 Connection: Upgrade。后端要确认"同意升级",并返回 101 Switching Protocols。Nginx 默认会把这些头吃掉,所以必须显式转发:
客户端 ──→ Nginx ──→ Xray
GET /ws-xxx (Upgrade: websocket, Connection: upgrade)
│ Nginx 再用同样的头转发给后端 127.0.0.1:10086
▼
Xray → 101 Switching Protocols → 之后这条连接双向畅通
如果没有 proxy_set_header Upgrade $http_upgrade; 和 Connection "upgrade";,Nginx 会把后端当普通 HTTP,直接返回一个静态首页或 404,客户端就会收到一个"看起来被墙了"的错误。
路径要前后一致(最容易踩的坑)¶
三个地方必须完全一致:Xray 配置里的 wsSettings.path、Nginx 里的 location、以及客户端里的 path。本机三者的值都是:
/ws-f033eda368eb1b99f9d79a4f
| 文件 | 出现位置 | 值 |
|---|---|---|
/usr/local/etc/xray/config.json |
wsSettings.path |
/ws-f033eda368eb1b99f9d79a4f |
/etc/nginx/sites-available/bangdream.xyz.conf |
location |
/ws-f033eda368eb1b99f9d79a4f |
| 客户端配置 | wsSettings.path |
/ws-f033eda368eb1b99f9d79a4f |
若前后端路径不一致,Nginx 匹配不到
location,就会走location /把请求丢给那个 Web 应用(3000 端口),客户端收到的是 404 或一个普通网页,表现为"连上但没法翻墙"。排查时先对一下这三处路径是否一模一样。
让 Nginx 生效¶
改完配置后,必须让 Nginx 加载它:
# 1. 语法检查(必做,语法对了再 reload)
nginx -t
# 2. 若无报错,重新加载配置
systemctl reload nginx
# 3. 确认 443 正在监听
ss -tlnp | grep :443
# tcp LISTEN 0 511 0.0.0.0:443 ... users:(("nginx",...))
跨机器提醒:如果你的 Nginx 只在
sites-available/里写了配置却没建软链接到sites-enabled/,Nginx 不会加载它。本机是这样连接的:```bash ls -l /etc/nginx/sites-enabled/
bangdream.xyz.conf -> /etc/nginx/sites-available/bangdream.xyz.conf¶
ln -s /etc/nginx/sites-available/bangdream.xyz.conf \ /etc/nginx/sites-enabled/bangdream.xyz.conf ```
验证"密道"是否通¶
配置加载后,先用 curl 从本机探一下密道。注意:此时没有客户端,Xray 会返回一个非 200 的东西(比如 400 或连接被升级处理),这是正常的——重要的是"请求确实被送到了 Xray"而不是被 404 掉了。
# 带 Upgrade 头模拟 WebSocket 握手打密道
curl -s -H "Upgrade: websocket" -H "Connection: Upgrade" \
http://127.0.0.1/ws-f033eda368eb1b99f9d79a4f -I
# 顺便确认 HTTPS 也通(证书有效)
curl -sI https://bangdream.xyz | head -3
安全提示:
curl -I打密道不应返回正常的200 OK网页内容。如果你发现返回了200 OK且是正常的 HTML 页面,说明路径匹配失败,请求被location /接管走了——这是伪装壳在"错误地"兜底,不是密道走通了。
本章小结¶
| 关键点 | 说明 |
|---|---|
| 分层架构 | Nginx(443) → 密道 path → Xray(10086) |
| 升级四件套 | proxy_http_version 1.1 + Upgrade + Connection upgrade |
| 三处一致 | Xray wsSettings.path = Nginx location = 客户端 path |
| 长连接超时 | proxy_read_timeout 3600s 防 WebSocket 断链 |
| 生效命令 | nginx -t 检查 → systemctl reload nginx |
到这一步,你的服务器已经能正确接收"知道密道的人"的访问了。下一步就是让用户端(客户端)用你生成的 UUID + 路径 + 域名,正确地连上这台服务器——这是第 4 章的内容。
安全提示:隐藏路径
/ws-f033eda368eb1b99f9d79a4f是第二把钥匙。不要把location里的路径写平铺到任何公开的文档、教程、博客或群聊里,也不要让这个路径在你网站的正常页面中被任何链接引用到。 想确认它不被误暴露,可以grep -r全站文件查一遍是否有引用。