隐私说明:这是一篇脱敏后的故障复盘。文中使用 node.example.com192.0.2.10198.51.100.2020001 这类示例占位符;真实服务器地址、账户邮箱、API Key、登录密码、客户 UUID 和真实节点端口均不公开。

这次故障一开始看起来像是 XUI 面板里的节点全部失效:客户用 Shadowrocket 做连通性测试,结果大面积超时。第一反应很容易是“是不是 XUI 崩了”“是不是 Xray 配置错了”“是不是防火墙把客户端口关了”。

我没有先重装,也没有直接改客户配置,而是先做了几件确认:XUI 服务还在运行,Xray 配置测试通过,原来的客户节点端口在服务器本机仍然监听,从海外网络到主 VPS 的节点端口可以连通,但从国内网络访问同一批端口则超时。

这些信号合在一起,基本可以判断:问题不在 XUI 配置本身,而是主 VPS 的公网 IP 在部分地区的链路上出了问题。我的目标也很明确:尽快恢复客户使用,并且尽量不让客户修改 Shadowrocket 配置。

为什么不能简单套 Cloudflare 橙云

我第一时间也考虑过把域名套到 Cloudflare 上。但这里有一个容易踩坑的地方:普通 Cloudflare 橙云代理并不能接管任意随机 TCP 端口。

Cloudflare 端口限制随机端口不是普通橙云的强项

Cloudflare 官方文档说明,默认代理的是 HTTP/HTTPS 的指定端口。如果业务使用其他随机端口,要么保持 DNS-only 灰云直连,要么使用 Spectrum。

DNS-only灰云不是保护,但能保持直连

灰云状态下,DNS 返回源站真实 IP,流量不经过 Cloudflare 网络。对于普通随机 TCP 节点,这通常比强行橙云更安全。

所以,如果客户原来的配置是:

node.example.com:随机端口

普通橙云并不能直接让这些随机端口通过 Cloudflare。强行把域名橙云,反而可能导致所有随机端口节点都彻底不可用。这也是我最后选择“另一台 VPS 做跳板”的原因。

参考资料:Cloudflare Network portsCloudflare Proxy status

方案:用备用 VPS 接住原来的域名和端口

这次能做到客户无感切换,有一个前提:客户配置里的服务器地址是域名,而不是裸 IP。

原路径:客户 -> node.example.com -> 主 VPS
临时路径:客户 -> node.example.com -> 备用 VPS -> 主 VPS
恢复路径:客户 -> node.example.com -> 主 VPS

如果客户配置是域名加原端口,我可以把 node.example.com 的 A 记录从主 VPS 改到备用 VPS。备用 VPS 再把这些原端口转发回主 VPS。客户不需要改端口、不需要改 UUID、不需要改 WebSocket Path,也不需要重新导入订阅。

如果客户配置里写的是裸 IP,例如 192.0.2.10:20001,DNS 切换不会影响它。这个场景下,要么客户改配置,要么只能等原 IP 恢复。

第一步:确认主 VPS 的节点端口还活着

我先在主 VPS 上确认服务状态:

systemctl status x-ui --no-pager
ss -lntp
/usr/local/x-ui/bin/xray-linux-amd64 -test -c /usr/local/x-ui/bin/config.json

然后从备用 VPS 上测试主 VPS 的节点端口:

ORIGIN_IP="192.0.2.10"

for p in 20001 20002 20003 20004; do
  nc -z -w 3 "$ORIGIN_IP" "$p" && echo "$p open" || echo "$p closed"
done

如果备用 VPS 到主 VPS 的这些端口是 open,说明主 VPS 的 Xray 入口还在,备用 VPS 可以作为中转。

第二步:在备用 VPS 上安装并配置转发

我用 socat 做用户态 TCP 转发。它的好处是直观:每个原端口在备用 VPS 上真实监听,再把连接转发到主 VPS 的同端口。

apt update
apt install -y socat

创建转发脚本:

cat > /usr/local/sbin/xui-origin-forward.sh <<'EOF'
#!/bin/sh
set -eu

ORIGIN_IP="192.0.2.10"
PORTS_LIST="20001 20002 20003 20004"
PORTS_CSV="20001,20002,20003,20004"

iptables -C INPUT -p tcp -m multiport --dports "$PORTS_CSV" -j ACCEPT 2>/dev/null || \
  iptables -I INPUT 1 -p tcp -m multiport --dports "$PORTS_CSV" -j ACCEPT

iptables -C ufw-user-input -p tcp -m multiport --dports "$PORTS_CSV" -j ACCEPT 2>/dev/null || \
  iptables -I ufw-user-input 1 -p tcp -m multiport --dports "$PORTS_CSV" -j ACCEPT

iptables -C IN_BT -p tcp -m multiport --dports "$PORTS_CSV" -j ACCEPT 2>/dev/null || \
  iptables -I IN_BT 1 -p tcp -m multiport --dports "$PORTS_CSV" -j ACCEPT

iptables -C IN_BT_user_port -p tcp -m multiport --dports "$PORTS_CSV" -j ACCEPT 2>/dev/null || \
  iptables -I IN_BT_user_port 1 -p tcp -m multiport --dports "$PORTS_CSV" -j ACCEPT

if command -v iptables-save >/dev/null 2>&1; then
  mkdir -p /etc/iptables
  iptables-save > /etc/iptables/rules.v4
fi

for port in $PORTS_LIST; do
  /usr/bin/socat TCP-LISTEN:${port},reuseaddr,fork,keepalive TCP:${ORIGIN_IP}:${port} &
done

wait
EOF

chmod 0755 /usr/local/sbin/xui-origin-forward.sh

这里我同时处理了几层防火墙:INPUTufw-user-inputIN_BTIN_BT_user_port。只启动 socat 不够,端口还必须真的能从公网打进来。

第三步:用 systemd 保持转发长期运行

cat > /etc/systemd/system/xui-origin-forward.service <<'EOF'
[Unit]
Description=Temporary XUI port forwarder
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=/usr/local/sbin/xui-origin-forward.sh
Restart=always
RestartSec=3
KillMode=control-group

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable xui-origin-forward.service
systemctl restart xui-origin-forward.service

确认状态:

systemctl is-enabled xui-origin-forward.service
systemctl is-active xui-origin-forward.service
ss -lntp | grep socat

我期望看到 enabledactive,并且每个原节点端口都有一个 socat 监听。

第四步:从外部验证备用 VPS 端口

在改 DNS 之前,我先从外部测试备用 VPS 的端口:

JUMP_IP="198.51.100.20"

for p in 20001 20002 20003 20004; do
  nc -vz -w 3 "$JUMP_IP" "$p"
done

如果节点是 TLS + WebSocket,还可以做一个粗略的 HTTP 探测:

curl -skI \
  --connect-timeout 5 \
  --max-time 8 \
  --resolve node.example.com:20001:198.51.100.20 \
  https://node.example.com:20001/

如果返回 400404 或类似响应,并不一定是坏事。普通 HTTP 请求不是完整的 WebSocket 握手,Xray 返回非业务响应很正常。关键是它不是连接失败、不是超时。

第五步:切 DNS 到备用 VPS

确认备用 VPS 端口可达后,我才切 DNS。如果在 Cloudflare 里管理 DNS,可以用 API。下面是模板,所有 ID 和 Key 都用占位符:

read -rs CF_KEY

curl -sS -X PUT \
  -H "X-Auth-Email: [email protected]" \
  -H "X-Auth-Key: $CF_KEY" \
  -H "Content-Type: application/json" \
  "https://api.cloudflare.com/client/v4/zones/<ZONE_ID>/dns_records/<RECORD_ID>" \
  --data '{"type":"A","name":"node.example.com","content":"198.51.100.20","ttl":1,"proxied":false}'

注意这里的 proxiedfalse。这不是套橙云,而是 DNS-only 灰云直连。原因很简单:节点端口是随机 TCP 端口,普通橙云不能代理这些端口。

第六步:客户侧恢复

DNS 切到备用 VPS 后,客户如果原配置是域名,就可以直接重连。我当时的验证顺序是:

  • 备用 VPS 上确认转发服务仍然 active。
  • 外部抽测几个原端口全部 open。
  • 用原域名作为 SNI 强制走备用 VPS,确认能穿到主 VPS 的 Xray。
  • 让客户重新连接 Shadowrocket。

最重要的是,不要一边改 DNS 一边又改客户节点内容。故障恢复时变量越少越好。能不改客户端,就不要改客户端。

三天后:等主 VPS 的 IP 恢复后如何切回去

这类 IP 链路问题有时不是永久的。我给自己留了一个恢复顺序:先切回 DNS,确认稳定,再清理备用 VPS。

1. 先确认主 VPS 真的恢复

ORIGIN_IP="192.0.2.10"

for p in 20001 20002 20003 20004; do
  nc -vz -w 3 "$ORIGIN_IP" "$p"
done

还可以用原域名强制解析到主 VPS:

curl -skI \
  --connect-timeout 5 \
  --max-time 8 \
  --resolve node.example.com:20001:192.0.2.10 \
  https://node.example.com:20001/

2. 先只把 DNS 切回主 VPS

read -rs CF_KEY

curl -sS -X PUT \
  -H "X-Auth-Email: [email protected]" \
  -H "X-Auth-Key: $CF_KEY" \
  -H "Content-Type: application/json" \
  "https://api.cloudflare.com/client/v4/zones/<ZONE_ID>/dns_records/<RECORD_ID>" \
  --data '{"type":"A","name":"node.example.com","content":"192.0.2.10","ttl":1,"proxied":false}'

这一步完成后,我不会马上停备用 VPS 的转发。因为有些客户 DNS 缓存还没刷新,或者某些运营商递归 DNS 更新慢。备用 VPS 多保留几个小时甚至一天,成本很低,但能避免恢复窗口里出现二次故障。

3. 确认客户稳定后,再清理备用 VPS

systemctl disable --now xui-origin-forward.service
rm -f /etc/systemd/system/xui-origin-forward.service
rm -f /usr/local/sbin/xui-origin-forward.sh
systemctl daemon-reload

删除临时防火墙规则:

PORTS_CSV="20001,20002,20003,20004"

while iptables -C INPUT -p tcp -m multiport --dports "$PORTS_CSV" -j ACCEPT 2>/dev/null; do
  iptables -D INPUT -p tcp -m multiport --dports "$PORTS_CSV" -j ACCEPT
done

while iptables -C ufw-user-input -p tcp -m multiport --dports "$PORTS_CSV" -j ACCEPT 2>/dev/null; do
  iptables -D ufw-user-input -p tcp -m multiport --dports "$PORTS_CSV" -j ACCEPT
done

while iptables -C IN_BT -p tcp -m multiport --dports "$PORTS_CSV" -j ACCEPT 2>/dev/null; do
  iptables -D IN_BT -p tcp -m multiport --dports "$PORTS_CSV" -j ACCEPT
done

while iptables -C IN_BT_user_port -p tcp -m multiport --dports "$PORTS_CSV" -j ACCEPT 2>/dev/null; do
  iptables -D IN_BT_user_port -p tcp -m multiport --dports "$PORTS_CSV" -j ACCEPT
done

iptables-save > /etc/iptables/rules.v4

我这次学到的几个教训

客户配置尽量使用域名

如果节点地址写的是裸 IP,主 IP 一旦不可达,就很难无感切走。域名不是为了好看,而是为了故障切换。

普通橙云不是万能 TCP 代理

随机端口的 Xray/XUI 节点不能指望普通橙云直接救回来。除非迁移到 Cloudflare 支持的标准 HTTPS 端口,或者使用 Spectrum。

先验证再切 DNS

备用 VPS 上监听了端口,不代表外部能进来。必须验证本机防火墙、云安全组、外部端口和 SNI 穿透。

恢复时不要急着删中转

切回主 VPS 后,中转最好多保留一段时间。DNS 缓存不是所有客户端同时刷新,太早删除可能造成二次掉线。

最后的恢复清单

  • 确认主 VPS 服务未崩,只是部分地区不可达。
  • 从备用 VPS 测主 VPS 原端口是否可达。
  • 在备用 VPS 上用 socat 监听原端口并转发到主 VPS。
  • 在备用 VPS 的 INPUT、UFW、宝塔链、安全组里放行原端口。
  • 外部验证备用 VPS 原端口全部 open。
  • 将业务域名 A 记录切到备用 VPS,保持 DNS-only。
  • 客户恢复后保持观察。
  • 等主 VPS IP 恢复,先把 DNS 切回主 VPS。
  • 客户稳定后,再清理备用 VPS 中转服务和防火墙规则。

这套方法不优雅,但很实用。它最大的价值是:在主 VPS 的 IP 临时不可达时,只要主 VPS 本身还能从备用 VPS 访问,就可以用备用 VPS 把原域名和原端口接住,为客户争取恢复时间。

最后修改:2026 年 06 月 03 日
Thanks for reading.