隐私说明:这是一篇脱敏后的故障复盘。文中使用 node.example.com、192.0.2.10、198.51.100.20、20001 这类示例占位符;真实服务器地址、账户邮箱、API Key、登录密码、客户 UUID 和真实节点端口均不公开。
这次故障一开始看起来像是 XUI 面板里的节点全部失效:客户用 Shadowrocket 做连通性测试,结果大面积超时。第一反应很容易是“是不是 XUI 崩了”“是不是 Xray 配置错了”“是不是防火墙把客户端口关了”。
我没有先重装,也没有直接改客户配置,而是先做了几件确认:XUI 服务还在运行,Xray 配置测试通过,原来的客户节点端口在服务器本机仍然监听,从海外网络到主 VPS 的节点端口可以连通,但从国内网络访问同一批端口则超时。
这些信号合在一起,基本可以判断:问题不在 XUI 配置本身,而是主 VPS 的公网 IP 在部分地区的链路上出了问题。我的目标也很明确:尽快恢复客户使用,并且尽量不让客户修改 Shadowrocket 配置。
为什么不能简单套 Cloudflare 橙云
我第一时间也考虑过把域名套到 Cloudflare 上。但这里有一个容易踩坑的地方:普通 Cloudflare 橙云代理并不能接管任意随机 TCP 端口。
Cloudflare 官方文档说明,默认代理的是 HTTP/HTTPS 的指定端口。如果业务使用其他随机端口,要么保持 DNS-only 灰云直连,要么使用 Spectrum。
灰云状态下,DNS 返回源站真实 IP,流量不经过 Cloudflare 网络。对于普通随机 TCP 节点,这通常比强行橙云更安全。
所以,如果客户原来的配置是:
node.example.com:随机端口普通橙云并不能直接让这些随机端口通过 Cloudflare。强行把域名橙云,反而可能导致所有随机端口节点都彻底不可用。这也是我最后选择“另一台 VPS 做跳板”的原因。
参考资料:Cloudflare Network ports、Cloudflare Proxy status。
方案:用备用 VPS 接住原来的域名和端口
这次能做到客户无感切换,有一个前提:客户配置里的服务器地址是域名,而不是裸 IP。
如果客户配置是域名加原端口,我可以把 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这里我同时处理了几层防火墙:INPUT、ufw-user-input、IN_BT 和 IN_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我期望看到 enabled、active,并且每个原节点端口都有一个 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/如果返回 400、404 或类似响应,并不一定是坏事。普通 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}'注意这里的 proxied 是 false。这不是套橙云,而是 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 一旦不可达,就很难无感切走。域名不是为了好看,而是为了故障切换。
随机端口的 Xray/XUI 节点不能指望普通橙云直接救回来。除非迁移到 Cloudflare 支持的标准 HTTPS 端口,或者使用 Spectrum。
备用 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 把原域名和原端口接住,为客户争取恢复时间。
