#!/bin/bash # ════════════════════════════════════════════════════════════════ # ykxmz 服务器端 · 一键部署 # ════════════════════════════════════════════════════════════════ # # 把「服务器端」这个文件夹拷到一台能上外网的 Linux 小机器上,然后: # # bash deploy.sh # # 它会自己把下面这些全部弄好,不用你一步步敲: # # 1. 检查并安装 cloudflared(已装就跳过) # 2. 把中转程序装到 /root/lanremote-relay/ # 3. 网络调优(keepalive、MSS、MTU、开机加速) # 4. 注册四个 systemd 服务并设成开机自启 # 5. 配好 Cloudflare 隧道(具名隧道,地址固定不变) # 6. 启动、自检、把公网地址打印出来 # # ──────────────────────────────────────────────────────────────── # 怎么用 # ──────────────────────────────────────────────────────────────── # # 最省事(机器上已经有隧道凭据,直接复用): # bash deploy.sh # # 用 Cloudflare 给的隧道 Token(在 Zero Trust 后台复制来的那串): # TUNNEL_TOKEN=eyJhIjoi... bash deploy.sh # (也可以写成 --token eyJhIjoi...,但**不推荐** —— # Token 会留在 shell 历史里。用环境变量更干净。) # # 改域名 / 改隧道名 / 只装中转不要隧道: # bash deploy.sh --hostname relay.你的域名 # bash deploy.sh --tunnel-name myserver # bash deploy.sh --skip-tunnel # # 看全部参数: # bash deploy.sh --help # # ──────────────────────────────────────────────────────────────── # 关于「重跑」 # ──────────────────────────────────────────────────────────────── # 这个脚本是**幂等**的:已经装过的机器再跑一遍 = 升级。 # * 程序文件覆盖成新的 # * systemd 服务重写一遍(内容一样就没变化) # * 隧道凭据 / 已有 config.yml / 管理密码 / 设备数据 **都不动** # * iptables 规则先清后加,不会越积越多 # 所以放心重跑,不会把已有配置冲掉。 # # ──────────────────────────────────────────────────────────────── # 安全 # ──────────────────────────────────────────────────────────────── # 脚本里**不含任何密码或 Token**。 # Token 只从环境变量 / 命令行读,落到机器上时写进 # /root/.cloudflared/tunnel.env 并设成 600(只有 root 能看)。 # ════════════════════════════════════════════════════════════════ set -u # ──────────────────────────────────────────────────────────────── # 版本号:从 relay.py 里读,避免两处不一致 # ──────────────────────────────────────────────────────────────── SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) VERSION="unknown" if [ -f "$SCRIPT_DIR/relay.py" ]; then VERSION=$(grep -m1 '^VERSION = ' "$SCRIPT_DIR/relay.py" 2>/dev/null \ | sed 's/.*"\(.*\)".*/\1/') [ -z "$VERSION" ] && VERSION="unknown" fi # ──────────────────────────────────────────────────────────────── # 默认值 # ──────────────────────────────────────────────────────────────── PREFIX="${PREFIX:-}" # 安装根,默认 /(测试时可指到临时目录) TOKEN="${TUNNEL_TOKEN:-}" # 隧道 Token,默认空 HOSTNAME_ARG="" # 域名,默认沿用已有 config.yml TUNNEL_NAME="${TUNNEL_NAME:-lanremote}" # 具名隧道名 PROTOCOL="${PROTOCOL:-quic}" # quic 或 http2 SKIP_TUNNEL=0 DOWNLOADS_SRC="" # 可选:把发布产物拷进下载目录 PORT="${RELAY_PORT:-8080}" usage() { cat </ —— 就是官网 查状态 lanremote-status 看公网地址 lanremote-url EOF } # ──────────────────────────────────────────────────────────────── # 解析参数 # ──────────────────────────────────────────────────────────────── while [ $# -gt 0 ]; do case "$1" in -h|--help) usage; exit 0 ;; --token) TOKEN="${2:-}"; shift 2 ;; --hostname) HOSTNAME_ARG="${2:-}"; shift 2 ;; --tunnel-name) TUNNEL_NAME="${2:-}"; shift 2 ;; --protocol) PROTOCOL="${2:-}"; shift 2 ;; --port) PORT="${2:-}"; shift 2 ;; --downloads) DOWNLOADS_SRC="${2:-}"; shift 2 ;; --skip-tunnel) SKIP_TUNNEL=1; shift ;; --prefix) PREFIX="${2:-}"; shift 2 ;; *) echo "未知参数: $1" >&2 echo "用 bash deploy.sh --help 看用法" >&2 exit 2 ;; esac done if [ "$PROTOCOL" != "quic" ] && [ "$PROTOCOL" != "http2" ]; then echo "协议只能是 quic 或 http2,收到的是: $PROTOCOL" >&2 exit 2 fi # ──────────────────────────────────────────────────────────────── # 路径:全部挂在 $PREFIX 下面 # 真实部署时 PREFIX 是空的,于是这些就是标准的绝对路径; # 测试时 --prefix /tmp/xxx,所有写入都关在沙箱里,不碰真系统。 # ──────────────────────────────────────────────────────────────── RELAY_DIR="$PREFIX/root/lanremote-relay" CF_DIR="$PREFIX/root/.cloudflared" UNIT_DIR="$PREFIX/etc/systemd/system" BIN_DIR="$PREFIX/usr/local/bin" SYSCTL_DIR="$PREFIX/etc/sysctl.d" say() { echo "$@"; } step() { echo; echo "════════ $* ════════"; } warn() { echo " ⚠ $*"; } die() { echo; echo " ✗ $*" >&2; exit 1; } say "════════════════════════════════════════════════════════" say " ykxmz 服务器端 · 一键部署 v$VERSION" say "════════════════════════════════════════════════════════" say " 安装根目录: ${PREFIX:-/}" say " 中转端口: $PORT" say " 隧道协议: $PROTOCOL" if [ "$SKIP_TUNNEL" = "1" ]; then say " 隧道: 跳过(--skip-tunnel)" elif [ -n "$TOKEN" ]; then say " 隧道: Token 模式" else say " 隧道: 凭据文件模式" fi # ════════════════════════════════════════════════════════════════ # 1/7 环境检查 # ════════════════════════════════════════════════════════════════ step "1/7 环境检查" # 沙箱模式(--prefix)不需要 root,因为写的都是临时目录 if [ -z "$PREFIX" ] && [ "$(id -u 2>/dev/null || echo 1)" != "0" ]; then die "需要 root 权限运行(前面加 sudo,或者直接以 root 登录)" fi if ! command -v python3 >/dev/null 2>&1; then die "没有 python3。先装:apt update && apt install -y python3" fi say " python3 $(python3 -V 2>&1 | awk '{print $2}')" if ! command -v systemctl >/dev/null 2>&1; then warn "没有 systemctl —— 装完之后要自己手动启动程序" fi # 核心程序必须齐全,缺一个都别往下走 MISSING="" for f in relay.py admin.py landing.py showcase.py favicon.ico; do [ -f "$SCRIPT_DIR/$f" ] || MISSING="$MISSING $f" done if [ -n "$MISSING" ]; then die "程序文件不全,缺:$MISSING (确认你是从 ykxmz-server-*.zip 解压出来的整个文件夹里跑的)" fi say " 程序文件齐全(5 个文件)" # 产品截图缺失只降级成警告 —— 官网上那几张图会 404, # 但中转、配对、远控这些核心功能完全不受影响。 # 打包脚本漏收 site_shots/ 目录这种事真的发生过, # 不该因为这个就整个部署失败。 if [ -d "$SCRIPT_DIR/site_shots" ]; then say " 产品截图齐全($(ls -1 "$SCRIPT_DIR/site_shots" 2>/dev/null | wc -l) 张)" else warn "没有 site_shots/ 目录 —— 官网上那几张产品截图会显示不出来," warn "其他功能都正常。" fi say " 内存: $(free -m 2>/dev/null | awk 'NR==2{print $2" MB 总量 / "$7" MB 可用"}' || echo '读不到')" # ════════════════════════════════════════════════════════════════ # 2/7 安装 cloudflared # ════════════════════════════════════════════════════════════════ step "2/7 cloudflared" CF_BIN="$BIN_DIR/cloudflared" # 已经在 PATH 里能用的,直接用,不重复下载 CF_READY=0 if command -v cloudflared >/dev/null 2>&1 \ && cloudflared --version >/dev/null 2>&1; then CF_BIN=$(command -v cloudflared) say " 已安装: $(cloudflared --version 2>&1 | head -1)" say " 位置: $CF_BIN" CF_READY=1 elif [ -x "$CF_BIN" ] && "$CF_BIN" --version >/dev/null 2>&1; then say " 已安装: $("$CF_BIN" --version 2>&1 | head -1)" CF_READY=1 fi if [ "$CF_READY" = "0" ]; then if [ "$SKIP_TUNNEL" = "1" ]; then say " [跳过] --skip-tunnel,不装 cloudflared" else ARCH=$(uname -m) case "$ARCH" in aarch64|arm64) CF_PKG=cloudflared-linux-arm64 ;; armv7l|armv6l) CF_PKG=cloudflared-linux-arm ;; x86_64|amd64) CF_PKG=cloudflared-linux-amd64 ;; *) die "不认识的 CPU 架构: $ARCH(自己去 Cloudflare 官网下对应版本放到 $CF_BIN)" ;; esac say " 没装,开始下载($ARCH -> $CF_PKG)" mkdir -p "$BIN_DIR" CF_URL="https://github.com/cloudflare/cloudflared/releases/latest/download/$CF_PKG" # --http1.1:这条线路对 HTTP/2 下载不稳;--retry:偶发失败自动重试 if curl -fsSL --http1.1 --retry 3 --connect-timeout 20 --max-time 300 \ -o "$CF_BIN.tmp" "$CF_URL" 2>/dev/null; then chmod +x "$CF_BIN.tmp" # 下载完必须真的能跑 —— 拿到一个 404 页面 / 半截文件也要能发现 if "$CF_BIN.tmp" --version >/dev/null 2>&1; then mv -f "$CF_BIN.tmp" "$CF_BIN" say " 装好了: $("$CF_BIN" --version 2>&1 | head -1)" say " 位置: $CF_BIN" CF_READY=1 else rm -f "$CF_BIN.tmp" warn "下载到的文件跑不起来(可能是网络劫持返回了错误页)" fi else warn "下载失败: $CF_URL" fi if [ "$CF_READY" = "0" ]; then warn "cloudflared 没装上 —— 中转服务照常能用(局域网内)," warn "但**跨网络**要等装好 cloudflared 才能用。" warn "可以手动装:" warn " curl -fsSL -o $CF_BIN $CF_URL && chmod +x $CF_BIN" fi fi fi # ════════════════════════════════════════════════════════════════ # 3/7 部署程序文件 # ════════════════════════════════════════════════════════════════ step "3/7 部署程序文件到 $RELAY_DIR" mkdir -p "$RELAY_DIR/data" mkdir -p "$RELAY_DIR/downloads" # 判断源和目标是不是同一个文件 —— 脚本有可能就在 RELAY_DIR 里跑, # 那样 cp 自己到自己会报 "are the same file"。 same_file() { [ -f "$1" ] && [ -f "$2" ] || return 1 [ "$(readlink -f "$1" 2>/dev/null)" = "$(readlink -f "$2" 2>/dev/null)" ] } # ⚠ 用户会在**已经跑着的机器上反复执行**这个脚本(当升级用)。 # 如果每次都不管三七二十一重启服务,正在服务的连接就被白打断一次。 # 所以这里记录"这次到底改动了什么",后面按需重启。 RELAY_CHANGED=0 SHOTS_CHANGED=0 CHANGED_LIST="" # 内容比较:一样就不动。 # cmp 不存在时保守地当成"变了"(多重启一次也好过改了没生效)。 differs() { [ -f "$1" ] || return 0 if command -v cmp >/dev/null 2>&1; then cmp -s "$1" "$2" && return 1 || return 0 fi return 0 } copy_one() { local name="$1" local src="$SCRIPT_DIR/$name" local dst="$RELAY_DIR/$name" [ -f "$src" ] || return 0 if same_file "$src" "$dst"; then return 0 fi if ! differs "$src" "$dst"; then return 0 # 内容一样,不碰时间戳 fi if cp -f "$src" "$dst"; then RELAY_CHANGED=1 CHANGED_LIST="$CHANGED_LIST $name" else warn "复制 $name 失败" fi } COPIED=0 # 程序本体 + 运维脚本。清单写死容易漏(watchdog.sh 就漏过一次), # 所以这里把 relay 目录里所有 .py / .sh / .ico 都带上。 for f in "$SCRIPT_DIR"/*.py "$SCRIPT_DIR"/*.sh "$SCRIPT_DIR"/*.ico; do [ -f "$f" ] || continue COPIED=$((COPIED + 1)) copy_one "$(basename "$f")" done # 产品截图(官网要显示) if [ -d "$SCRIPT_DIR/site_shots" ] \ && ! same_file "$SCRIPT_DIR/site_shots" "$RELAY_DIR/site_shots"; then mkdir -p "$RELAY_DIR/site_shots" for shot in "$SCRIPT_DIR/site_shots"/*; do [ -f "$shot" ] || continue base=$(basename "$shot") if differs "$shot" "$RELAY_DIR/site_shots/$base"; then cp -f "$shot" "$RELAY_DIR/site_shots/$base" 2>/dev/null \ && SHOTS_CHANGED=1 fi done fi chmod +x "$RELAY_DIR"/*.sh 2>/dev/null || true # 语法检查:装上去之前先确认能编译,别把服务弄成起不来 if python3 -m py_compile "$RELAY_DIR/relay.py" 2>/dev/null; then say " relay.py 语法检查通过" else die "relay.py 编译不过 —— 文件可能坏了,先重新解压一份" fi say " 已部署 $(ls -1 "$RELAY_DIR"/*.py "$RELAY_DIR"/*.sh 2>/dev/null | wc -l) 个脚本" say " 数据目录 $RELAY_DIR/data 保持不动(设备、配对码、管理密码都在里面)" # 可选:把发布产物拷进下载目录(官网的下载按钮读这里) if [ -n "$DOWNLOADS_SRC" ]; then if [ -d "$DOWNLOADS_SRC" ]; then n=$(ls -1 "$DOWNLOADS_SRC" 2>/dev/null | wc -l) cp -f "$DOWNLOADS_SRC"/* "$RELAY_DIR/downloads/" 2>/dev/null || true say " 已把 $n 个文件拷进下载目录" else warn "--downloads 给的目录不存在: $DOWNLOADS_SRC" fi fi # ════════════════════════════════════════════════════════════════ # 4/7 网络调优 # ════════════════════════════════════════════════════════════════ step "4/7 网络调优" if [ -f "$RELAY_DIR/deploy-tune.sh" ]; then # shellcheck disable=SC1090 if [ -n "$PREFIX" ]; then bash "$RELAY_DIR/deploy-tune.sh" --prefix "$PREFIX" || warn "网络调优有报错,继续" else bash "$RELAY_DIR/deploy-tune.sh" || warn "网络调优有报错,继续" fi else warn "找不到 deploy-tune.sh,跳过网络调优" fi # ════════════════════════════════════════════════════════════════ # 5/7 写 systemd 服务 # ════════════════════════════════════════════════════════════════ step "5/7 注册 systemd 服务" mkdir -p "$UNIT_DIR" # ──────────────────────────────────────────────────────────────── # 写 unit 文件的统一入口 # ──────────────────────────────────────────────────────────────── # 为什么要包一层:用户会在已经跑着的机器上反复执行本脚本(升级用)。 # 如果每次都把 unit 重写一遍,mtime 就变了 —— 于是每次都得 # daemon-reload + 重启服务,正在服务的连接被白打断。 # 内容真正变了才落盘,并置上对应的标志,后面按标志决定要不要重启。 UNITS_CHANGED=0 RELAY_UNIT_CHANGED=0 TUNNEL_UNIT_CHANGED=0 write_unit() { local path="$1" local tmp="$path.new.$$" cat > "$tmp" if [ -f "$path" ] && command -v cmp >/dev/null 2>&1 \ && cmp -s "$tmp" "$path"; then rm -f "$tmp" return 1 # 内容一样,没动 fi mv -f "$tmp" "$path" UNITS_CHANGED=1 return 0 # 真的改了 } # ---- 中转服务 ---- cat > "$UNIT_DIR/lanremote-relay.service" < "$UNIT_DIR/lanremote-nettune.service" <.service」找文件的, # `systemctl enable lanremote-tunnel` 找的是 lanremote-tunnel.service。 # 少这个后缀的话文件是写下去了,但 systemctl 完全看不见它 —— # 而且不会有任何报错(enable 会安静地失败),非常难查。实测踩过。 cat > "$UNIT_DIR/$unit.service" </dev/null 2>&1; then systemctl daemon-reload >/dev/null 2>&1 || true say " 已写入 4 个服务单元到 $UNIT_DIR" else say " [跳过] 没有 systemctl,服务单元已写好但没加载" fi # ════════════════════════════════════════════════════════════════ # 6/7 配置隧道 # ════════════════════════════════════════════════════════════════ step "6/7 配置隧道" TUNNEL_OK=0 PUBLIC_URL="" if [ "$SKIP_TUNNEL" = "1" ]; then say " --skip-tunnel:不配置隧道" elif [ "$CF_READY" = "0" ]; then warn "cloudflared 没装好,隧道配不了" else mkdir -p "$CF_DIR" chmod 700 "$CF_DIR" 2>/dev/null || true if [ -n "$TOKEN" ]; then # ---------- Token 模式 ---------- say " Token 模式" # 单独存文件 + 600 权限:unit 文件是 644 的,Token 不能写在里面 umask 077 printf 'TUNNEL_TOKEN=%s\n' "$TOKEN" > "$CF_DIR/tunnel.env" umask 022 chmod 600 "$CF_DIR/tunnel.env" say " Token 已写入 $CF_DIR/tunnel.env(权限 600,只有 root 能看)" TUNNEL_OK=1 if [ -n "$HOSTNAME_ARG" ]; then PUBLIC_URL="https://$HOSTNAME_ARG" fi say " 提示:Token 模式的入口域名在 Cloudflare 后台配,本地看不到。" say " 如果给了 --hostname,就按它当公网地址。" else # ---------- 凭据文件模式 ---------- CRED="" for f in "$CF_DIR"/*.json; do [ -f "$f" ] && CRED="$f" && break done if [ -z "$CRED" ]; then warn "没有找到隧道凭据($CF_DIR/*.json),也没给 TUNNEL_TOKEN" warn "两种办法,任选一种:" warn " A. 在 Cloudflare Zero Trust 后台建隧道,复制 Token,然后:" warn " TUNNEL_TOKEN=<粘贴> bash $0" warn " B. 在这台机器上登录并创建具名隧道(需要自己的域名):" warn " $CF_BIN tunnel login" warn " $CF_BIN tunnel create $TUNNEL_NAME" warn " $CF_BIN tunnel route dns $TUNNEL_NAME relay.你的域名" warn " 然后重跑本脚本。" warn "中转本身已经装好了 —— 局域网内可以直接用,只是跨网络还不行。" else TUNNEL_ID=$(basename "$CRED" .json) say " 找到凭据: $CRED" say " 隧道 ID: $TUNNEL_ID" # 域名:命令行 > 已有 config.yml > 猜不出来就报错 CUR_HOST="" if [ -f "$CF_DIR/config.yml" ]; then CUR_HOST=$(grep -m1 '^ *- hostname:' "$CF_DIR/config.yml" 2>/dev/null \ | sed 's/.*hostname: *//' | tr -d '"'"'"' ') fi HOST="$HOSTNAME_ARG" [ -z "$HOST" ] && HOST="$CUR_HOST" if [ -z "$HOST" ]; then warn "认不出公网域名(既没给 --hostname,config.yml 里也没有)" warn "补上域名再跑一次:" warn " bash $0 --hostname relay.你的域名" warn "如果这个域名还没指向隧道,先执行:" warn " $CF_BIN tunnel route dns $TUNNEL_NAME relay.你的域名" else # config.yml 只在「不存在」或「用户明确给了 --hostname」时才重写, # 免得把别人手工调过的配置冲掉。 if [ ! -f "$CF_DIR/config.yml" ] || [ -n "$HOSTNAME_ARG" ]; then cat > "$CF_DIR/config.yml" </dev/null 2>&1; then systemctl enable lanremote-relay >/dev/null 2>&1 || true systemctl enable lanremote-nettune >/dev/null 2>&1 || true systemctl start lanremote-nettune >/dev/null 2>&1 || true say " 启动中转服务" systemctl restart lanremote-relay >/dev/null 2>&1 || true sleep 3 say " lanremote-relay: $(systemctl is-active lanremote-relay 2>&1)" if [ "$TUNNEL_OK" = "1" ]; then say " 启动隧道服务" systemctl enable lanremote-tunnel >/dev/null 2>&1 || true # 备用隧道不自动启用 —— 两个同时跑会互相争抢 systemctl disable lanremote-tunnel-h2 >/dev/null 2>&1 || true systemctl restart lanremote-tunnel >/dev/null 2>&1 || true say " lanremote-tunnel: $(systemctl is-active lanremote-tunnel 2>&1)" elif [ "$SKIP_TUNNEL" != "1" ]; then say " 隧道没配好,先不启动" fi # 看门狗(如果包里有) if [ -f "$RELAY_DIR/watchdog.sh" ]; then cat > "$UNIT_DIR/lanremote-watchdog.service" < "$UNIT_DIR/lanremote-watchdog.timer" <<'EOF' [Unit] Description=定期检查隧道 [Timer] OnBootSec=45s OnUnitActiveSec=20s AccuracySec=5s Unit=lanremote-watchdog.service [Install] WantedBy=timers.target EOF systemctl daemon-reload >/dev/null 2>&1 || true systemctl enable lanremote-watchdog.timer >/dev/null 2>&1 || true systemctl start lanremote-watchdog.timer >/dev/null 2>&1 || true say " 看门狗: $(systemctl is-active lanremote-watchdog.timer 2>&1)" fi else say " [跳过] 没有 systemctl,服务没启动" fi # 便捷命令 mkdir -p "$BIN_DIR" cat > "$BIN_DIR/lanremote-url" <<'EOF' #!/bin/bash # 打印当前公网地址 cat /root/lanremote-relay/public_url.txt 2>/dev/null \ || grep -oE 'https://[a-z0-9.-]+' /root/lanremote-relay/tunnel.log 2>/dev/null | tail -1 \ || echo "(还不知道,隧道可能还没连上)" EOF cat > "$BIN_DIR/lanremote-key" <<'EOF' #!/bin/bash # 打印管理页钥匙 cat /root/lanremote-relay/data/admin_key.txt 2>/dev/null || echo "(还没有)" EOF cat > "$BIN_DIR/lanremote-status" <<'EOF' #!/bin/bash # 一眼看清状态 echo "=== 服务 ===" for s in lanremote-relay lanremote-tunnel lanremote-tunnel-h2 lanremote-nettune lanremote-watchdog.timer; do printf " %-26s %s\n" "$s" "$(systemctl is-active "$s" 2>/dev/null)" done echo echo "=== 端口 ===" (curl -s --max-time 5 http://127.0.0.1:8080/v1/health 2>/dev/null | head -c 120) || echo " 本机 8080 没响应" echo echo "=== 公网 ===" cat /root/lanremote-relay/public_url.txt 2>/dev/null || echo " (还不知道)" echo echo "=== 内存 ===" free -m 2>/dev/null | awk 'NR==2{printf " 已用 %s MB / 可用 %s MB\n", $3, $7}' EOF chmod +x "$BIN_DIR"/lanremote-url "$BIN_DIR"/lanremote-key \ "$BIN_DIR"/lanremote-status 2>/dev/null || true # 把公网地址写下来(relay 的 /v1/url 和客户端都读这个文件) if [ -n "$PUBLIC_URL" ]; then printf '%s\n' "$PUBLIC_URL" > "$RELAY_DIR/public_url.txt" fi # ---- 自检 ---- say say " ── 自检 ──" sleep 2 LOCAL_OK=0 for i in 1 2 3 4 5; do if curl -s --max-time 8 "http://127.0.0.1:$PORT/v1/health" 2>/dev/null \ | grep -q '"ok"'; then LOCAL_OK=1 break fi sleep 2 done if [ "$LOCAL_OK" = "1" ]; then say " 本机中转: ✓ 通(http://127.0.0.1:$PORT/v1/health)" else warn "本机中转: ✗ 不通" if command -v systemctl >/dev/null 2>&1; then systemctl status lanremote-relay --no-pager -l 2>/dev/null | tail -8 | sed 's/^/ /' fi say " 看日志: tail -30 $RELAY_DIR/relay.log" fi PUB_OK=0 if [ -n "$PUBLIC_URL" ]; then say " 等隧道连上(最多 90 秒)…" for i in $(seq 1 30); do if curl -s --max-time 12 "$PUBLIC_URL/v1/health" 2>/dev/null \ | grep -q '"ok"'; then PUB_OK=1 break fi sleep 3 done if [ "$PUB_OK" = "1" ]; then say " 公网访问: ✓ 通($PUBLIC_URL)" else warn "公网访问: ✗ 还没通" say " 看一眼隧道日志最后几行:" tail -6 "$RELAY_DIR/tunnel.log" 2>/dev/null | sed 's/^/ /' say " 常见原因:域名 DNS 还没指向隧道、或者这条线路连不上边缘" fi fi # ════════════════════════════════════════════════════════════════ # 收尾 # ════════════════════════════════════════════════════════════════ KEY=$(cat "$RELAY_DIR/data/admin_key.txt" 2>/dev/null || echo "") say say "════════════════════════════════════════════════════════" if [ "$LOCAL_OK" = "1" ] && { [ "$TUNNEL_OK" = "0" ] || [ "$PUB_OK" = "1" ]; }; then say " 装好了 ✓" else say " 装完了,但上面有 ✗ 的项目要看一眼" fi say "════════════════════════════════════════════════════════" say if [ -n "$PUBLIC_URL" ]; then say " 公网地址 $PUBLIC_URL" else say " 公网地址 (还没配上)" fi say " 官网 ${PUBLIC_URL:-http://<本机IP>:$PORT}/" [ -n "$KEY" ] && say " 管理页 ${PUBLIC_URL:-http://<本机IP>:$PORT}/?key=$KEY" say say " 常用命令" say " lanremote-status 看整体状态" say " lanremote-url 看公网地址" say " systemctl status lanremote-relay 看中转服务" say " systemctl status lanremote-tunnel 看隧道" say " journalctl -u lanremote-relay -f 跟日志" say say " 换隧道协议(quic 慢一点但尾延迟好 / http2 建连更稳)" say " systemctl disable --now lanremote-tunnel" say " systemctl enable --now lanremote-tunnel-h2" say say " 重跑本脚本 = 升级,已有配置和数据都不会动" say exit 0