#!/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}" # 具名隧道名 # 隧道协议。默认 both = 两条隧道一起跑(实测最优,见下面的说明)。 # 传 quic 或 http2 才是"只跑那一种"。 PROTOCOL="${PROTOCOL:-both}" 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" ] \ && [ "$PROTOCOL" != "both" ]; then echo "协议只能是 quic / http2 / both,收到的是: $PROTOCOL" >&2 echo "(不传这个参数就是 both —— 两条隧道一起跑)" >&2 exit 2 fi # ──────────────────────────────────────────────────────────────── # 两条隧道怎么分工(默认两条都跑) # ──────────────────────────────────────────────────────────────── # 为什么**两条一起跑**才是最优(真机 40 次/组的受控对照,MTU 1400): # # quic 5 + h2 5 可用率 100% 中位 1490 P90 2346 P99 3295 # quic 3 + h2 7 可用率 97% 中位 1271 P90 2558 P99 13934 # quic 8 + h2 2 可用率 100% 中位 1250 P90 1715 P99 2632 <- 采用 # # 根因也查清了:**QUIC 握手对丢包敏感**。 # # QUIC 注册 525 失败 197 掉线 48 拨号成功率 72% # HTTP/2 注册 13 失败 0 掉线 7 拨号成功率 100% # # QUIC 建连要 6+ 个包的往返,而 TCP 的 SYN 只有 3 个包、 # 内核还会按 1s/2s/4s 激进重传。约 5.4%/包的丢包下: # # (1 - 0.054)^6 ≈ 72% <- 和实测的 72% 对得上 # # ⚠ 注意:**这不是「运营商封了 UDP」。** 曾经这么以为过 —— # 但那个判断来自一个格式不对的手搓 UDP 探测包, # Cloudflare 本来就不该回应它,"0/12 无回应"什么都证明不了。 # 用 cloudflared --loglevel debug 实测真相是: # # INF | UDP Connectivity region1.v2.argotunnel.com PASS # INF | UDP Connectivity region2.v2.argotunnel.com PASS # INF | SUMMARY: Environment is healthy ... # INF Registered tunnel connection (握手 1 秒) # # UDP 是通的。QUIC 的弱点在「连不上」,不在「连上之后不好」—— # 一旦连上,它没有 TCP 的跨流队头阻塞,反而更稳。 # # 于是:单独用 QUIC,会有近三成的时候连不上; # 单独用 HTTP/2,队头阻塞在高丢包下会拖长尾延迟。 # 两个一起跑,HTTP/2 正好兜住 QUIC 拨号失败的那部分 —— # 这就是**协议级故障隔离**,比在同一个协议上堆连接数有效得多。 # # (早先这里写的是"两条同时跑会互相争抢",那个结论是错的,已作废。 # 再早还写过"运营商封 UDP",那个测量方法本身是错的,也作废。) MAIN_PROTO=quic MAIN_CONNS="${QUIC_CONNS:-8}" ALT_PROTO=http2 ALT_CONNS="${H2_CONNS:-2}" # metrics 端口必须**显式钉死**。 # cloudflared 默认都用 20241:正常启动顺序下看着像"主 20241 / 副 20242", # 但实测**停掉主实例再重启副实例,副实例会抢到 20241**。 # 于是 watchdog 和所有按端口读指标的脚本全部读错实例,还不会有任何报错。 MAIN_METRICS="127.0.0.1:20241" ALT_METRICS="127.0.0.1:20242" # --protocol 用来强制只跑一种(默认 both) MAIN_ENABLED=1 ALT_ENABLED=1 if [ "$PROTOCOL" = "quic" ]; then # 只跑 QUIC(它拨号失败率约三成,只在明确知道 UDP 通的时候用) MAIN_ENABLED=1 ALT_ENABLED=0 elif [ "$PROTOCOL" = "http2" ]; then # 只跑 HTTP/2(UDP 被封的环境) MAIN_ENABLED=0 ALT_ENABLED=1 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 # 真的改了 } # ---- 中转服务 ---- write_unit "$UNIT_DIR/lanremote-relay.service" <.service」找文件的, # `systemctl enable lanremote-tunnel` 找的是 lanremote-tunnel.service。 # 少这个后缀的话文件是写下去了,但 systemctl 完全看不见它 —— # 而且不会有任何报错(enable 会安静地失败),非常难查。实测踩过。 # # ⚠ --metrics 必须显式写死端口: # cloudflared 的默认 metrics 端口两个实例都是 20241。 # 正常启动顺序下看着像"主 20241 / 副 20242"(后启动的发现端口被占 # 就自己往后挪),但实测**停掉主实例再重启副实例时, # 副实例会抢到 20241** —— 于是 watchdog、看连接数的脚本、 # 以及所有按端口读指标的排查动作全部读错实例,而且不报任何错。 write_unit "$UNIT_DIR/$unit.service" </dev/null 2>&1; then # 只有 unit 真的变了才 daemon-reload —— # 每次都 reload 会顺带触发 systemd 重新评估依赖,没必要。 if [ "$UNITS_CHANGED" = "1" ]; then systemctl daemon-reload >/dev/null 2>&1 || true say " 已写入 4 个服务单元到 $UNIT_DIR(有变化,已 reload)" else say " 4 个服务单元内容没变,跳过 reload" fi else say " [跳过] 没有 systemctl,服务单元已写好但没加载" fi # ════════════════════════════════════════════════════════════════ # 6/7 配置隧道 # ════════════════════════════════════════════════════════════════ step "6/7 配置隧道" TUNNEL_OK=0 PUBLIC_URL="" # config.yml 这次是不是真的被改写了 —— 隧道不吃旧配置, # 改了就必须重启隧道,没改就别去动它。 CONFIG_CHANGED=0 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="" CUR_PORT="" if [ -f "$CF_DIR/config.yml" ]; then CUR_HOST=$(grep -m1 '^ *- hostname:' "$CF_DIR/config.yml" 2>/dev/null \ | sed 's/.*hostname: *//' | tr -d '"'"'"' ') # 已有配置里的回源端口 —— 单独用 --port 换端口时, # config.yml 还指着老端口的话隧道就回源到没人听的端口, # 现象是"隧道连上了但一访问就 502",很难查。 CUR_PORT=$(grep -m1 'service: *http://127\.0\.0\.1:' "$CF_DIR/config.yml" 2>/dev/null \ | sed 's|.*127\.0\.0\.1:||' | tr -dc '0-9') fi HOST="$HOSTNAME_ARG" [ -z "$HOST" ] && HOST="$CUR_HOST" if [ -z "$HOST" ]; then # ⚠ 绝不猜域名、也绝不内置任何默认值。 # 这个中转是自用的,域名只有机器主人知道。 # 脚本里写死一个域名的话,别人照抄就指到你的机器上去了 —— # 所以这里**明确停下来要输入**,而不是随便挑一个继续。 say say " ┌────────────────────────────────────────────────────┐" say " │ 需要你提供公网域名(脚本不会替你猜,也没有默认值) │" say " └────────────────────────────────────────────────────┘" say say " 机器上有隧道凭据,但是:" say " · 命令行没给 --hostname" say " · 已有的 config.yml 里也没有 hostname 行" say " 所以定不下来公网地址。" say say " 补上域名再跑一次:" say " bash $0 --hostname relay.你的域名" say say " 如果这个域名还没指向隧道,先执行一次:" say " $CF_BIN tunnel route dns $TUNNEL_NAME relay.你的域名" say say " 或者改用 Token 模式(域名信息在 Token 里,不用手填):" say " TUNNEL_TOKEN= bash $0" say say " ── 本次结果 ──" say " 中转已经装好并启动了 —— **局域网内现在就能用**。" say " 只是跨网络(走公网)还差这一步域名。" else # config.yml 的重写条件,三种情况: # 1. 本来就没有 —— 得生成一份 # 2. 用户明确给了 --hostname —— 要按新域名改 # 3. 已有配置里的回源端口和当前 $PORT 不一致 # (单独跑 --port 9090 时,若不改这一行, # 隧道还是回源到 8080,现象是"隧道通了但一访问就 502") # 其他情况一律沿用,免得把别人手工调过的配置冲掉。 NEED_WRITE=0 [ ! -f "$CF_DIR/config.yml" ] && NEED_WRITE=1 [ -n "$HOSTNAME_ARG" ] && NEED_WRITE=1 if [ -n "$CUR_PORT" ] && [ "$CUR_PORT" != "$PORT" ]; then NEED_WRITE=1 say " 已有 config.yml 指向端口 $CUR_PORT,当前是 $PORT —— 会一并更新" fi if [ "$NEED_WRITE" = "1" ]; 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 # ── 中转服务 ── # ⚠ 只在「程序变了」「unit 变了」「本来就没在跑」这三种情况下才重启。 # 用户在**已经跑着的机器上**重跑本脚本是为了升级, # 如果每次都不管三七二十一 restart,正在服务的连接就被白打断一次 —— # 重启会清掉内存里的设备注册和会话,客户端要几秒才重新接上。 RELAY_NEED_RESTART=0 [ "$RELAY_CHANGED" = "1" ] && RELAY_NEED_RESTART=1 [ "$RELAY_UNIT_CHANGED" = "1" ] && RELAY_NEED_RESTART=1 systemctl is-active --quiet lanremote-relay 2>/dev/null \ || RELAY_NEED_RESTART=1 if [ "$RELAY_NEED_RESTART" = "1" ]; then if [ "$RELAY_CHANGED" = "1" ] || [ "$RELAY_UNIT_CHANGED" = "1" ]; then say " 程序或配置有更新,重启中转服务" else say " 中转服务没在跑,启动它" fi systemctl restart lanremote-relay >/dev/null 2>&1 || true sleep 3 else say " 程序和配置都没变,中转服务保持运行(不打断在线连接)" fi say " lanremote-relay: $(systemctl is-active lanremote-relay 2>&1)" if [ "$TUNNEL_OK" = "1" ]; then # ── 按开关启用/停用两个单元 ── # 默认两个都启用(协议级故障隔离,见文件前面的实测对照)。 # 只有用户用 --protocol 明确指定单一协议时才关掉另一个。 if [ "$MAIN_ENABLED" = "1" ]; then systemctl enable lanremote-tunnel >/dev/null 2>&1 || true else systemctl disable --now lanremote-tunnel >/dev/null 2>&1 || true say " --protocol 指定只跑 $ALT_PROTO,已停用主隧道" fi if [ "$ALT_ENABLED" = "1" ]; then systemctl enable lanremote-tunnel-h2 >/dev/null 2>&1 || true else systemctl disable --now lanremote-tunnel-h2 >/dev/null 2>&1 || true say " --protocol 指定只跑 $MAIN_PROTO,已停用副本隧道" fi TUNNEL_NEED_RESTART=0 [ "$TUNNEL_UNIT_CHANGED" = "1" ] && TUNNEL_NEED_RESTART=1 # 新写的 config.yml 也算配置变更 —— 隧道不吃旧配置 [ "${CONFIG_CHANGED:-0}" = "1" ] && TUNNEL_NEED_RESTART=1 # 启用的那几个单元,只要有一个没在跑就得拉起来 for svc in lanremote-tunnel lanremote-tunnel-h2; do case "$svc" in lanremote-tunnel) [ "$MAIN_ENABLED" = "1" ] || continue ;; lanremote-tunnel-h2) [ "$ALT_ENABLED" = "1" ] || continue ;; esac systemctl is-active --quiet "$svc" 2>/dev/null \ || TUNNEL_NEED_RESTART=1 done if [ "$TUNNEL_NEED_RESTART" = "1" ]; then if [ "$TUNNEL_UNIT_CHANGED" = "1" ] \ || [ "${CONFIG_CHANGED:-0}" = "1" ]; then say " 隧道配置有更新,重启隧道服务" else say " 隧道服务没在跑,启动它" fi [ "$MAIN_ENABLED" = "1" ] \ && systemctl restart lanremote-tunnel >/dev/null 2>&1 [ "$ALT_ENABLED" = "1" ] \ && systemctl restart lanremote-tunnel-h2 >/dev/null 2>&1 else say " 隧道配置没变,保持运行(不重建隧道连接)" fi [ "$MAIN_ENABLED" = "1" ] && say " lanremote-tunnel: $(systemctl is-active lanremote-tunnel 2>&1)" [ "$ALT_ENABLED" = "1" ] && say " lanremote-tunnel-h2: $(systemctl is-active lanremote-tunnel-h2 2>&1)" elif [ "$SKIP_TUNNEL" != "1" ]; then say " 隧道没配好,先不启动" fi # 看门狗(如果包里有) if [ -f "$RELAY_DIR/watchdog.sh" ]; then write_unit "$UNIT_DIR/lanremote-watchdog.service" </dev/null 2>&1 || true fi 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 看主隧道(quic)" say " systemctl status lanremote-tunnel-h2 看副本隧道(http2)" say " journalctl -u lanremote-relay -f 跟日志" say say " 两个隧道服务默认**都开着**(协议级故障隔离,实测最优配比 quic 8 + h2 2)" say " 看各自的连接数和实时指标:" say " curl -s http://127.0.0.1:20241/metrics | grep ha_connections # 主" say " curl -s http://127.0.0.1:20242/metrics | grep ha_connections # 副本" say " (两个实例的 metrics 端口是分开钉死的,不会互相抢)" say say " 只想跑一种(比如确认这台机器 UDP 被封):" say " bash deploy.sh --protocol http2 # 只跑 HTTP/2" say " bash deploy.sh --protocol quic # 只跑 QUIC" say " bash deploy.sh # 不传就是两条一起跑" say say " 重跑本脚本 = 升级,已有配置和数据都不会动" say exit 0