目标:在一台海外 VPS 上,搭出一条能在小火箭里用的「某国家出口」线路。
你几乎不需要懂原理,按步骤复制粘贴就行。
所有 IP、密码、密钥都用<尖括号>表示,换成你自己的值,尖括号本身也要删掉。
一条可以导入小火箭的链接,大概长这样:
vless://……@你的VPS的IP:2087?……#Home-US
连上之后,Safari 打开 https://ipinfo.io,看到的国家会是你选的那个国家(例如美国),而不是 VPS 所在地。
| 东西 | 怎么拿 | 大概多少钱 |
|---|---|---|
| 一台海外 VPS,系统选 Ubuntu 22.04 | 甲骨文免费套餐,或其他便宜 VPS | 0~10 美元/月 |
| 能 SSH 登录这台机器 | Windows 用 Termius;Mac 用「终端」 | 免费 |
一个空闲的 TCP 端口,例如 2087 |
后面脚本会检查是否空着 | — |
| iPhone + Shadowrocket(小火箭) | 非国区 Apple ID 购买 | 约 2.99 美元 |
| 云控制台能登录(甲骨文 / 阿里云国际等) | 用来放行端口 | — |
重要: 如果这台 VPS 上已经有别人帮你装过面板(比如 3x-ui、x-ui),本教程用的是另一套程序、另一个端口,不会去改原来的面板。但请你先确认自己要用的端口没被占用。
在电脑上打开终端(或 Termius),输入(把 IP 换成你的):
ssh ubuntu@<你的VPS_IP>
你应该看到: 提示输入密码或直接进去,最后出现类似 ubuntu@某个名字:~$ 的提示符。
如果提示 Are you sure you want to continue connecting,输入 yes 回车。
脚本文件是本教程同目录的 install-home-line.sh。二选一:
方法 A:从电脑上传(推荐)。 在电脑上另开一个终端(不是 VPS 里),进入脚本所在文件夹后运行:
scp install-home-line.sh ubuntu@<你的VPS_IP>:~/
方法 B:直接粘贴。 在 VPS 终端里运行 nano install-home-line.sh,用记事本打开脚本文件,全选复制,粘贴进 nano,然后按 Ctrl+O、回车保存,再按 Ctrl+X 退出。
然后在 VPS 里改最上面 4 个变量:
nano install-home-line.sh
COUNTRY="US" # 国家:JP日本 KR韩国 US美国 GB英国 RU俄罗斯 TH泰国 VN越南 ID印尼
NAME="us" # 线路名字,用小写字母,别和已有的重复
PORT="2087" # 对外端口,云控制台也要放行这个
SOCKS_PORT="8085" # 本机内部用,多条线路时不能重复
改完按 Ctrl+O、回车保存,再按 Ctrl+X 退出,然后运行:
sudo bash install-home-line.sh
你应该看到(大约 5~10 分钟):
== 0. 检查端口是否空闲== 1. 安装依赖和 Docker(第一次会下载不少东西)== 2. 写网关和管理器文件== 3. 启动容器== 4. 安装独立 Xray== 5. 生成密钥== 6. systemd 服务 ……== 7. 放行本机防火墙端口 ……== 8. 等待线路连通 ……==============================================
线路已连通,出口 IP:x.x.x.x
别忘了:去云控制台安全列表放行 TCP 2087
小火箭链接(已保存到 /etc/xray-home-us/client.txt):
vless://……
==============================================
把最后那行 vless:// 开头的链接整行复制下来(或之后用 sudo cat /etc/xray-home-us/client.txt 再看)。
如果提示「端口已被占用」:换一个 PORT / SOCKS_PORT / NAME 再跑。
如果提示「线路暂未连通」:这个国家现在可能没有可用节点,等几十分钟再试;日本、韩国一般更容易成功。
脚本只放行了 VPS 自己的防火墙。云厂商外面还有一道门,要你手动开。
甲骨文云(界面可能更新,以你看到的为准):
0.0.0.0/0 PORT(例如 2087) 其他云厂商: 到「安全组 / 防火墙规则」里加一条「入站 TCP 你的端口,来源 0.0.0.0/0」。
你应该看到: 规则列表里多了一行 TCP 2087(或你选的端口)。
在自己电脑上测一下(Mac / Linux):
nc -vz <你的VPS_IP> 2087
成功时大致显示: Connection to … port 2087 [tcp/*] succeeded!
如果一直卡住或 timed out:安全列表还没放对,再检查一遍。
vless://…… 链接 +,手动粘贴,或用左上角扫码导入 你应该看到: 顶部状态变成已连接(或显示节点名称)。
手机浏览器打开:
https://ipinfo.io
成功: 页面上的国家 / 城市是你选的国家(例如 United States),IP 不是 你的 VPS IP。
失败: 还是 VPS 所在地,或网页打不开 → 看下面「出问题了怎么办」。
| 你看到的现象 | 最常见原因 | 你可以做什么 |
|---|---|---|
| 小火箭一直转圈 / 超时 | 云控制台没放行端口 | 回去做第 3 步;用 nc -vz 再测 |
| 能连上但网页打不开 | 这个国家暂时没有可用节点 | 换日本或韩国再装一条;或等几小时 |
| 脚本说端口被占用 | 这台机器上已有服务占用了 | 换一组 PORT / SOCKS_PORT / NAME |
AUTH_FAILED 刷屏 |
志愿者节点拒绝连接 | 脚本里的管理器会自动换点,等即可 |
| 怕搞坏原来的 3x-ui | — | 本教程不碰面板;只要别用面板占用的端口就行 |
需要把错误信息发给会折腾的朋友或 AI 时,把下面两条命令的输出一起发(不要发 .env、私钥、整条 vless 链接):
docker logs --tail 40 vpngw-us-manager
systemctl status xray-home-us --no-pager
(把名字里的 us 换成你设的 NAME。)
再跑一遍脚本,只改最上面 4 个变量,例如:
COUNTRY="JP"
NAME="jp"
PORT="2083"
SOCKS_PORT="8083"
四条都不能和已有线路重复。 每条线路都要在云控制台单独放行对应的 PORT。
vless:// 链接发到群里。 脚本文件:与本教程同目录的 install-home-line.sh。云控制台界面以实际上看到的为准。
先修改顶部的 4 个变量,再保存为 install-home-line.sh,然后运行 sudo bash install-home-line.sh。
#!/bin/bash
# 家宽出口一键安装脚本:网关 + 管理器 + 独立 Xray + systemd,最后打印 vless 链接
# 只改下面 4 个变量即可。每条线路的 NAME / PORT / SOCKS_PORT 都不能和已有的重复。
COUNTRY="US" # VPN Gate 国家代码:JP KR US GB RU TH VN ID ...
NAME="us" # 线路名(小写字母),决定目录和服务名
PORT="2087" # 对外 Reality 端口(要在云安全列表放行)
SOCKS_PORT="8085" # 本机 SOCKS 端口(只监听 127.0.0.1)
SNI="www.apple.com"
set -euo pipefail
[ "$(id -u)" -eq 0 ] || { echo "请用 sudo 运行:sudo bash $0"; exit 1; }
REAL_USER="${SUDO_USER:-root}"
HOME_DIR=$(getent passwd "$REAL_USER" | cut -d: -f6)
DIR="$HOME_DIR/vpngw-$NAME"
XDIR="/etc/xray-home-$NAME"
SVC="xray-home-$NAME"
echo "== 0. 检查端口是否空闲"
for p in "$PORT" "$SOCKS_PORT"; do
if ss -lnt | awk '{print $4}' | grep -Eq "[:.]$p\$"; then echo "端口 $p 已被占用,请换一个后重试"; exit 1; fi
done
[ -e "$DIR" ] && { echo "$DIR 已存在,请换 NAME"; exit 1; }
[ -e "$XDIR" ] && { echo "$XDIR 已存在,请换 NAME"; exit 1; }
echo "== 1. 安装依赖和 Docker"
apt-get update -y
apt-get install -y curl unzip openssl iptables
command -v docker >/dev/null || curl -fsSL https://get.docker.com | sh
usermod -aG docker "$REAL_USER" || true
echo "== 2. 写网关和管理器文件"
mkdir -p "$DIR/gateway" "$DIR/manager" "$DIR/ovpn" "$DIR/state"
cat > "$DIR/gateway/Dockerfile" <<'EOF'
FROM alpine:3.20 AS builder
RUN apk add --no-cache build-base git \
&& git clone --depth 1 https://github.com/rofl0r/microsocks /tmp/microsocks \
&& make -C /tmp/microsocks
FROM alpine:3.20
COPY --from=builder /tmp/microsocks/microsocks /usr/local/bin/microsocks
RUN apk add --no-cache openvpn iptables iproute2 su-exec bash curl \
&& adduser -D -s /bin/sh dante \
&& mkdir -p /scripts /vpn
COPY entrypoint.sh /entrypoint.sh
COPY up.sh /scripts/up.sh
COPY down.sh /scripts/down.sh
RUN chmod +x /entrypoint.sh /scripts/up.sh /scripts/down.sh
ENTRYPOINT ["/entrypoint.sh"]
EOF
cat > "$DIR/gateway/entrypoint.sh" <<'EOF'
#!/bin/bash
set -e
: "${GATE_USER:?need GATE_USER}"
: "${GATE_PASS:?need GATE_PASS}"
# openvpn 的 --up 脚本拿不到容器环境变量,所以先把密码写到文件里
printf 'GATE_USER=%s\nGATE_PASS=%s\n' "$GATE_USER" "$GATE_PASS" > /etc/socks.auth
chmod 600 /etc/socks.auth
echo "[entrypoint] waiting for /vpn/client.ovpn ..."
while [ ! -f /vpn/client.ovpn ]; do sleep 5; done
while true; do
openvpn --config /vpn/client.ovpn \
--script-security 2 \
--up /scripts/up.sh \
--down /scripts/down.sh \
--up-restart \
|| true
echo "[entrypoint] openvpn exited, retry in 10s..."
sleep 10
done
EOF
cat > "$DIR/gateway/up.sh" <<'EOF'
#!/bin/bash
TUN="$1"
set -e
. /etc/socks.auth
# dante 用户的新连接打 mark 0x1,走 table 100 → tun0
ip rule add fwmark 0x1 table 100 2>/dev/null || true
ip route replace default dev "$TUN" table 100
iptables -t mangle -C OUTPUT -m owner --uid-owner dante -j CONNMARK --restore-mark 2>/dev/null \
|| iptables -t mangle -A OUTPUT -m owner --uid-owner dante -j CONNMARK --restore-mark
iptables -t mangle -C OUTPUT -m owner --uid-owner dante -m mark --mark 0 -m conntrack --ctstate NEW -j MARK --set-mark 0x1 2>/dev/null \
|| iptables -t mangle -A OUTPUT -m owner --uid-owner dante -m mark --mark 0 -m conntrack --ctstate NEW -j MARK --set-mark 0x1
iptables -t mangle -C OUTPUT -m owner --uid-owner dante -m mark --mark 0x1 -j CONNMARK --save-mark 2>/dev/null \
|| iptables -t mangle -A OUTPUT -m owner --uid-owner dante -m mark --mark 0x1 -j CONNMARK --save-mark
# 修复非对称路由:连进 1080 的回包打 0x2,走 table 101 → eth0
GW4=$(ip route show default dev eth0 2>/dev/null | awk '/^default/ {print $3; exit}')
NET4=$(ip route show dev eth0 scope link 2>/dev/null | awk '/^[0-9]+\./ {print $1; exit}')
if [ -n "$GW4" ]; then
ip rule add fwmark 0x2 table 101 2>/dev/null || true
[ -n "$NET4" ] && ip route replace "$NET4" dev eth0 scope link table 101 2>/dev/null || true
ip route replace default via "$GW4" dev eth0 table 101
fi
iptables -t mangle -C PREROUTING -i eth0 -p tcp --dport 1080 -m conntrack --ctstate NEW -j CONNMARK --set-mark 0x2 2>/dev/null \
|| iptables -t mangle -A PREROUTING -i eth0 -p tcp --dport 1080 -m conntrack --ctstate NEW -j CONNMARK --set-mark 0x2
# 启动带认证的 SOCKS5(容器内 1080;宿主机只映射到 127.0.0.1)
if ! pgrep -x microsocks >/dev/null 2>&1; then
su-exec dante microsocks -i 0.0.0.0 -p 1080 -u "${GATE_USER}" -P "${GATE_PASS}" &
fi
exit 0
EOF
cat > "$DIR/gateway/down.sh" <<'EOF'
#!/bin/bash
pkill -x microsocks 2>/dev/null || true
iptables -t mangle -D OUTPUT -m owner --uid-owner dante -j CONNMARK --restore-mark 2>/dev/null || true
iptables -t mangle -D OUTPUT -m owner --uid-owner dante -m mark --mark 0 -m conntrack --ctstate NEW -j MARK --set-mark 0x1 2>/dev/null || true
iptables -t mangle -D OUTPUT -m owner --uid-owner dante -m mark --mark 0x1 -j CONNMARK --save-mark 2>/dev/null || true
ip rule del fwmark 0x1 table 100 2>/dev/null || true
iptables -t mangle -D PREROUTING -i eth0 -p tcp --dport 1080 -m conntrack --ctstate NEW -j CONNMARK --set-mark 0x2 2>/dev/null || true
ip rule del fwmark 0x2 table 101 2>/dev/null || true
ip route flush table 101 2>/dev/null || true
exit 0
EOF
cat > "$DIR/manager/Dockerfile" <<'EOF'
FROM python:3.12-alpine
RUN apk add --no-cache docker-cli curl
COPY vpngate.py /vpngate.py
CMD ["python", "/vpngate.py"]
EOF
cat > "$DIR/manager/vpngate.py" <<'EOF'
#!/usr/bin/env python3
import os, sys, re, json, base64, time, socket, random, subprocess, urllib.request
DATA_URLS = [
"https://www.vpngate.net/api/iphone/",
"https://cdn.jsdelivr.net/gh/GeorgeXie2333/vpngate-list-mirror@latest/data/vpngate.csv",
]
COUNTRIES = [c.strip().upper() for c in os.environ.get("COUNTRIES", "JP").split(",") if c.strip()]
GATEWAY_NAME = os.environ.get("GATEWAY_NAME", "vpngw-us-gateway")
GATE_USER = os.environ.get("GATE_USER", "")
GATE_PASS = os.environ.get("GATE_PASS", "")
OVPN_PATH = "/ovpn/client.ovpn"
STATE_PATH = "/state/current.json"
BL_PATH = "/state/blacklist.json"
CHECK_INTERVAL = int(os.environ.get("CHECK_INTERVAL", "60"))
ROTATE_MINUTES = int(os.environ.get("ROTATE_MINUTES", "60"))
BL_SECONDS = int(os.environ.get("BL_SECONDS", "21600"))
FAIL_THRESHOLD = 3
IP_ECHO_URLS = ["https://api.ipify.org", "https://ifconfig.me",
"https://icanhazip.com", "https://checkip.amazonaws.com"]
def log(*a):
print(f"[manager {time.strftime('%H:%M:%S')}]", *a, flush=True)
def sh(cmd, timeout=30):
try:
r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout)
return r.returncode, r.stdout.strip(), r.stderr.strip()
except Exception as e:
return 1, "", str(e)
def gateway_cid():
rc, out, _ = sh(f'docker ps -q --filter "name={GATEWAY_NAME}" | head -1')
return out.strip() if rc == 0 else ""
def fetch_text(url):
req = urllib.request.Request(url, headers={"User-Agent": "vpngw-manager/1.0"})
with urllib.request.urlopen(req, timeout=60) as r:
return r.read().decode("utf-8", "ignore")
def parse_csv(text):
nodes = []
for line in text.splitlines():
line = line.strip()
if not line or line.startswith("*") or line.startswith("#"):
continue
p = line.split(",", 14) # 最后一列可能含逗号
if len(p) < 15:
continue
try:
nodes.append({"host": p[0], "ip": p[1],
"score": int(p[2] or 0),
"ping": int(p[3]) if p[3].isdigit() else 9999, # ping 为 "-" 的修复
"cc": (p[6] or "").upper(), "config_b64": p[14]})
except ValueError:
continue
return nodes
def load_nodes():
for u in DATA_URLS:
try:
log("fetching", u)
nodes = parse_csv(fetch_text(u))
if nodes:
log(f"got {len(nodes)} nodes")
return nodes
except Exception as e:
log("fetch failed:", u, str(e)[:120])
raise RuntimeError("all sources failed")
def candidates(nodes, exclude):
out = []
for n in nodes:
if n["cc"] not in COUNTRIES or n["ip"] in exclude:
continue
try:
cfg = base64.b64decode(n["config_b64"]).decode("utf-8", "ignore")
except Exception:
continue
if "proto tcp" not in cfg.lower():
continue
m = re.search(r"^remote (\S+) (\d+)", cfg, re.M)
if not m:
continue
try: # TCP 探测:连不上的直接跳过
socket.create_connection((m.group(1), int(m.group(2))), timeout=4).close()
except Exception:
continue
out.append((n, cfg))
out.sort(key=lambda x: (-x[0]["score"], x[0]["ping"]))
return out
def build_ovpn(node, cfg):
lines = [l for l in cfg.splitlines() if not l.strip().lower().startswith("redirect-gateway")]
lines.append(f"route {node['ip']} 255.255.255.255 net_gateway")
lines.append("<auth-user-pass>\nvpn\nvpn\n</auth-user-pass>") # VPN Gate 公共账号
lines += ["script-security 2", "up /scripts/up.sh", "down /scripts/down.sh"]
return "\n".join(lines) + "\n"
def save_state(node):
with open(STATE_PATH, "w") as f:
json.dump({"ip": node["ip"], "host": node["host"], "cc": node["cc"],
"since": time.time()}, f)
def load_state():
try:
return json.load(open(STATE_PATH))
except Exception:
return {}
def load_bl():
try:
bl = json.load(open(BL_PATH))
except Exception:
bl = {}
now = time.time()
return {k: v for k, v in bl.items() if v > now}
def blacklist(ip, why):
bl = load_bl(); bl[ip] = time.time() + BL_SECONDS
json.dump(bl, open(BL_PATH, "w"))
log(f"blacklisted {ip} ({why})")
def check_auth_failed(ip):
cid = gateway_cid()
if ip and cid:
_, out, _ = sh(f"docker logs --since 5m {cid} 2>&1 | grep -c AUTH_FAILED")
if out.strip().isdigit() and int(out.strip()) > 0:
blacklist(ip, "AUTH_FAILED")
def rotate(extra=()):
st = load_state()
check_auth_failed(st.get("ip", ""))
bl = set(load_bl())
exclude = set(extra) | bl | ({st["ip"]} if st.get("ip") else set())
try:
nodes = load_nodes()
except RuntimeError as e:
log("rotate aborted:", e); return False
cands = candidates(nodes, exclude) or candidates(nodes, bl)
if not cands:
log(f"no usable {COUNTRIES} TCP nodes"); return False
node, cfg = random.choice(cands[:8])
with open(OVPN_PATH, "w") as f:
f.write(build_ovpn(node, cfg))
cid = gateway_cid()
if not cid:
log("gateway container not found!"); return False
log(f"switching to {node['host']} ({node['ip']}, {node['cc']})")
sh(f"docker restart {cid}", timeout=60)
save_state(node)
return True
def exit_ip_via_socks():
for url in IP_ECHO_URLS:
rc, out, _ = sh(f'curl -s -m 15 --socks5-hostname gateway:1080 '
f'--proxy-user "{GATE_USER}:{GATE_PASS}" {url}', timeout=20)
ip = out.split()[0] if out else ""
if rc == 0 and re.fullmatch(r"\d+\.\d+\.\d+\.\d+", ip):
return ip
return None
def main():
log(f"start countries={COUNTRIES}")
if not os.path.exists(OVPN_PATH):
rotate()
_, vps_ip, _ = sh("curl -s -m 15 https://ifconfig.me", timeout=25)
fails = 0
while True:
time.sleep(CHECK_INTERVAL)
st = load_state()
got = exit_ip_via_socks()
if got and got == vps_ip:
log("LEAK! exit == vps ip, rotating"); rotate([st.get("ip", "")]); fails = 0
elif got:
fails = 0; log(f"ok exit={got}")
else:
fails += 1; log(f"health fail {fails}/{FAIL_THRESHOLD}")
if fails >= FAIL_THRESHOLD:
rotate([st.get("ip", "")]); fails = 0; continue
st = load_state()
if st.get("since") and time.time() - st["since"] > ROTATE_MINUTES * 60:
log("proactive rotation"); rotate([st.get("ip", "")])
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
sys.exit(0)
EOF
GATE_USER="gw$(openssl rand -hex 3)"
GATE_PASS="$(openssl rand -hex 16)"
printf 'GATE_USER=%s\nGATE_PASS=%s\n' "$GATE_USER" "$GATE_PASS" > "$DIR/.env"
chmod 600 "$DIR/.env"
cat > "$DIR/docker-compose.yml" <<EOF
services:
gateway:
build: ./gateway
container_name: vpngw-$NAME-gateway
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun
sysctls:
net.ipv4.ip_forward: "1"
ports:
- "127.0.0.1:$SOCKS_PORT:1080"
volumes:
- ./ovpn:/vpn
- ./state:/state
environment:
GATE_USER: \${GATE_USER}
GATE_PASS: \${GATE_PASS}
restart: unless-stopped
manager:
build: ./manager
container_name: vpngw-$NAME-manager
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./ovpn:/ovpn
- ./state:/state
environment:
- COUNTRIES=$COUNTRY
- GATEWAY_NAME=vpngw-$NAME-gateway
- GATE_USER=\${GATE_USER}
- GATE_PASS=\${GATE_PASS}
restart: unless-stopped
EOF
chown -R "$REAL_USER:" "$DIR"
echo "== 3. 启动容器(第一次构建要几分钟)"
(cd "$DIR" && docker compose up -d --build)
echo "== 4. 安装独立 Xray 到 $XDIR"
case "$(uname -m)" in
aarch64|arm64) PKG="Xray-linux-arm64-v8a.zip" ;;
x86_64|amd64) PKG="Xray-linux-64.zip" ;;
*) echo "不支持的架构 $(uname -m)"; exit 1 ;;
esac
mkdir -p "$XDIR"
curl -fL -o /tmp/xray-$NAME.zip "https://github.com/XTLS/Xray-core/releases/latest/download/$PKG"
unzip -o /tmp/xray-$NAME.zip xray -d "$XDIR/"
chmod +x "$XDIR/xray"
echo "== 5. 生成密钥"
KEYS=$("$XDIR/xray" x25519)
PRIV=$(echo "$KEYS" | awk -F': *' '/^Private ?[Kk]ey/ {print $2; exit}')
PUB=$(echo "$KEYS" | awk -F': *' '/^(Public ?[Kk]ey|Password)/ {print $2; exit}')
UUID=$("$XDIR/xray" uuid)
SID=$(openssl rand -hex 8)
[ -n "$PRIV" ] && [ -n "$PUB" ] || { echo "密钥解析失败,xray x25519 输出:"; echo "$KEYS"; exit 1; }
cat > "$XDIR/config.json" <<EOF
{
"log": {"loglevel": "warning"},
"inbounds": [{
"port": $PORT, "protocol": "vless",
"settings": {"clients": [{"id": "$UUID", "flow": "xtls-rprx-vision"}], "decryption": "none"},
"streamSettings": {"network": "tcp", "security": "reality",
"realitySettings": {"dest": "$SNI:443", "serverNames": ["$SNI"],
"privateKey": "$PRIV", "shortIds": ["$SID"]}}
}],
"outbounds": [
{"protocol": "socks", "settings": {"servers": [{"address": "127.0.0.1", "port": $SOCKS_PORT,
"users": [{"user": "$GATE_USER", "pass": "$GATE_PASS"}]}]}},
{"tag": "block", "protocol": "blackhole"}
],
"routing": {"rules": [{"type": "field", "network": "udp", "outboundTag": "block"}]}
}
EOF
chmod 600 "$XDIR/config.json"
"$XDIR/xray" run -test -c "$XDIR/config.json"
echo "== 6. systemd 服务 $SVC"
cat > "/etc/systemd/system/$SVC.service" <<EOF
[Unit]
Description=Standalone Xray $SVC (VLESS-Reality :$PORT -> socks 127.0.0.1:$SOCKS_PORT)
After=network-online.target docker.service
[Service]
ExecStart=$XDIR/xray run -c $XDIR/config.json
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now "$SVC"
echo "== 7. 放行本机防火墙端口 $PORT"
iptables -C INPUT -p tcp --dport "$PORT" -j ACCEPT 2>/dev/null || iptables -I INPUT -p tcp --dport "$PORT" -j ACCEPT
command -v netfilter-persistent >/dev/null && netfilter-persistent save || true
command -v ufw >/dev/null && ufw status | grep -q active && ufw allow "$PORT/tcp" || true
echo "== 8. 等待线路连通(最多约 5 分钟)"
EXIT_IP=""
for i in $(seq 1 30); do
EXIT_IP=$(curl -s -m 10 --socks5-hostname "127.0.0.1:$SOCKS_PORT" --proxy-user "$GATE_USER:$GATE_PASS" https://api.ipify.org || true)
[ -n "$EXIT_IP" ] && break
sleep 10
done
VPS_IP=$(curl -s -m 10 https://api.ipify.org || echo "<你的VPS_IP>")
LINK="vless://$UUID@$VPS_IP:$PORT?encryption=none&flow=xtls-rprx-vision&security=reality&sni=$SNI&fp=chrome&pbk=$PUB&sid=$SID&type=tcp#Home-$COUNTRY"
echo "$LINK" > "$XDIR/client.txt"; chmod 600 "$XDIR/client.txt"
echo
echo "=============================================="
if [ -n "$EXIT_IP" ]; then echo "线路已连通,出口 IP:$EXIT_IP"; else echo "线路暂未连通(节点可能暂时没有),管理器会自动重试;可稍后再试"; fi
echo "别忘了:去云控制台安全列表放行 TCP $PORT"
echo "小火箭链接(已保存到 $XDIR/client.txt):"
echo "$LINK"
echo "=============================================="