🌊 第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 反代,好处在伪装和复用上:

  1. 443 端口被 Nginx 统一管理,同一台机器可以同时跑"正常网站"和"伪装代理",互相不冲突;
  2. TLS 证书用 Certbot 自动签发与续期,Xray 完全不用碰证书这类麻烦事;
  3. 他人扫描你的 443 端口时,看到的是一个正经 HTTPS 网站,而不是一个代理服务——这正是"伪装"的价值;
  4. 你能同时复用这个域名下的多个业务(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 全站文件查一遍是否有引用。