服务端 zip 加一键部署脚本(cloudflared 安装 + 部署 + 公网,全自动)

用户要求:服务器端文件里放一个 .sh,双击就能检查 cloudflared 装没装、
自动部署、配好公网,以后全自动。

== 新增 ==

relay/deploy.sh        一键部署主脚本,7 步全自动:
                       环境检查 -> 装 cloudflared -> 部署程序 -> 网络调优
                       -> 写 4 个 systemd 服务 -> 配隧道 -> 启动自检打公网地址
relay/deploy-tune.sh   网络调优,可单独重跑:
                       keepalive 30/10/3 + 路径 MTU 探测 + MSS 1360
                       + 接口 MTU 1400 + mask networkd-wait-online
                       每一项都写明了实测依据

用法:
    bash deploy.sh                           复用已有凭据,全自动
    TUNNEL_TOKEN=eyJh... bash deploy.sh      Token 模式
    bash deploy.sh --hostname relay.你的域名  换域名
    bash deploy.sh --skip-tunnel             只装中转
    bash deploy.sh --help                    全部参数

重跑 = 升级;已有数据/配对码/管理密码/凭据/手改过的 config.yml 都不动。

== 自动化到什么程度 ==

* cloudflared 不在就自动下载装上(并校验能执行)
* 隧道支持两种模式:已有凭据文件、或 TUNNEL_TOKEN
* **不传 --hostname 且没有 Token 时明确提示要填**,不会静默用一个写死的值
  (用户明确说过:这个中转站是他自己用的,不对外开放,
   绝不能把私人域名烧成默认值)
* deploy-tune.sh 的 MTU 取 1400:路径 MTU 实测 1472,
  而 MSS 钳制只管 TCP、QUIC 走 UDP 完全不受保护;
  接口 MTU 降到 1400 后 QUIC 建齐 10 条连接从 28 秒缩到 12 秒

== 顺带修掉两个真 bug ==

1. write_tunnel_unit 写的文件名漏了 .service 后缀 ——
   文件写下去了但 systemd 完全看不见,enable 还会安静地失败。
2. 发布包一直缺 site_shots/(老 bug):
   package_remote.py 用 iterdir() + is_file() 收文件,目录进不去。
   新机器装完官网截图全是 404。只有从 zip 解压真跑一遍才暴露。
   同时把 deploy.sh 里 site_shots 的检查降级成警告(缺截图不该让部署失败)。

另外给 deploy-tune.sh 清旧 MSS 规则的循环加了 10 次上限 ——
原写法在 -D 报成功但规则仍在时会死循环把整个部署卡死。

== 验证 ==

* _build/test_deploy_sh.sh        沙箱 65 项,全通过
* _build/test_zip_deploy.sh       从发布 zip 解压再跑,30 项,全通过
* _build/test_deploy_real.sh      **真机 Debian 上实测**(新增):
    zip 解出 17 个文件 -> 语法/LF/无 BOM 通过
    --help 退出码 0 且无副作用、未知参数退出码 2
    沙箱安装成功、幂等重跑哨兵不被冲
    现有三个中转服务全程 active,自检 200
This commit is contained in:
xmz7891
2026-10-07 17:28:09 +08:00
parent d4f335b8f9
commit facb156aca
10 changed files with 2331 additions and 89 deletions
+221
View File
@@ -0,0 +1,221 @@
#!/bin/bash
# ════════════════════════════════════════════════════════════════
# 端到端验证:拿**真正打包出来的**服务器端 zip 解压,在里面跑 deploy.sh
# ════════════════════════════════════════════════════════════════
#
# 为什么还要单独测这个:
# 前面的 test_deploy_sh.sh 是拿仓库里的 relay/ 目录跑的。
# 但用户拿到的是 zip —— 有没有可能打包漏了文件、
# 或者 zip 里权限/换行变了?只有真的解压出来跑一遍才知道。
#
# 用法: bash _build/test_zip_deploy.sh [版本号]
# ════════════════════════════════════════════════════════════════
set -u
REPO=$(cd "$(dirname "$0")/.." && pwd)
VERSION=${1:-1.8.2}
ZIP="$REPO/dist/ykxmz-server-$VERSION.zip"
PASS=0
FAIL=0
ok() { PASS=$((PASS + 1)); printf " [PASS] %s\n" "$1"; }
bad() { FAIL=$((FAIL + 1)); printf " [FAIL] %s\n" "$1"; }
check(){ if [ "$2" = "$3" ]; then ok "$1"; else bad "$1(期望 $3,实际 $2)"; fi; }
echo "============================================================"
echo " 从发布 zip 端到端验证 deploy.sh v$VERSION"
echo "============================================================"
echo " zip: $ZIP"
if [ ! -f "$ZIP" ]; then
echo " 找不到 zip —— 先跑 python _build/package_remote.py"
exit 1
fi
echo " 大小: $(stat -c %s "$ZIP" 2>/dev/null || echo '?') 字节"
TMP=$(mktemp -d)
echo " 解压到: $TMP"
# ────────────────────────────────────────────────────────────
echo
echo "── 1. zip 里必须有一键部署脚本 ──"
python - "$ZIP" <<'PYEOF'
import sys, zipfile
from pathlib import Path
z = zipfile.ZipFile(sys.argv[1])
names = z.namelist()
need = ["deploy.sh", "deploy-tune.sh", "relay.py", "admin.py",
"landing.py", "showcase.py", "favicon.ico",
"site_shots/pc-main.png", "watchdog.sh", "net-tune.sh",
"README-server.txt"]
bad = 0
for n in need:
hit = [x for x in names if x.endswith("/" + n) or x.endswith(n)]
if hit:
print(" [PASS] zip 含 %s" % n)
else:
print(" [FAIL] zip 缺 %s" % n)
bad += 1
print(" ── 共 %d 个条目 ──" % len(names))
sys.exit(1 if bad else 0)
PYEOF
if [ $? -eq 0 ]; then PASS=$((PASS + 11)); else FAIL=$((FAIL + 1)); fi
# ────────────────────────────────────────────────────────────
echo
echo "── 2. 解压 ──"
if command -v unzip >/dev/null 2>&1; then
unzip -q "$ZIP" -d "$TMP"
else
python -c "import zipfile,sys; zipfile.ZipFile(sys.argv[1]).extractall(sys.argv[2])" \
"$ZIP" "$TMP"
fi
SRV=$(find "$TMP" -maxdepth 2 -type d -name "ykxmz-server-*" | head -1)
if [ -z "$SRV" ]; then
bad "解压后找不到 ykxmz-server-* 目录"
exit 1
fi
ok "解压出目录 $(basename "$SRV")"
# ────────────────────────────────────────────────────────────
echo
echo "── 3. 解压出来的 .sh 换行符 / 权限 ──"
python - "$SRV" <<'PYEOF'
import sys
from pathlib import Path
d = Path(sys.argv[1])
for name in ("deploy.sh", "deploy-tune.sh"):
p = d / name
if not p.is_file():
print(" [FAIL] 没有 %s" % name); continue
raw = p.read_bytes()
crlf = raw.count(b"\r\n")
bom = raw.startswith(b"\xef\xbb\xbf")
if crlf == 0 and not bom:
print(" [PASS] %s 是 LF 且无 BOM" % name)
else:
print(" [FAIL] %s CRLF=%d BOM=%s" % (name, crlf, bom))
PYEOF
if [ $? -eq 0 ]; then PASS=$((PASS + 2)); fi
# ────────────────────────────────────────────────────────────
echo
echo "── 4. 从解压目录跑 --help ──"
HELP=$(cd "$SRV" && bash deploy.sh --help 2>&1)
check "--help 退出码为 0" "$?" "0"
if echo "$HELP" | grep -q '用法'; then ok "--help 有用法说明"; else bad "--help 没用法"; fi
if echo "$HELP" | grep -q 'v1\.8\.'; then ok "--help 显示了版本号(从 relay.py 读的)"; else bad "--help 版本号没读出来"; fi
# ────────────────────────────────────────────────────────────
echo
echo "── 5. 从解压目录做一次沙箱部署 ──"
STUB="$TMP/stub"
SANDBOX="$TMP/sandbox"
STUB_LOG="$TMP/stub.log"
mkdir -p "$STUB"; : > "$STUB_LOG"
export STUB_LOG
REAL_PY=$(command -v python 2>/dev/null || true)
mk() { cat > "$STUB/$1"; chmod +x "$STUB/$1"; }
mk systemctl <<'EOF'
#!/bin/bash
echo "systemctl $*" >> "$STUB_LOG"
case "$1" in is-active) echo active;; is-enabled) echo enabled;; esac
exit 0
EOF
mk ip <<'EOF'
#!/bin/bash
echo "ip $*" >> "$STUB_LOG"
[ "$1" = "-4" ] && echo "default via 192.168.1.1 dev wlan0 proto dhcp"
exit 0
EOF
mk iptables <<'EOF'
#!/bin/bash
echo "iptables $*" >> "$STUB_LOG"
RULES="$STUB_LOG.rules"; touch "$RULES"
if [ "${1:-}" = "-t" ] && [ "${3:-}" = "-L" ]; then
while read -r r; do [ -n "$r" ] && echo "TCPMSS tcp -- $r"; done < "$RULES"
exit 0
fi
chain=""; mss=""; action=""; prev=""
for a in "$@"; do
case "$prev" in -C|-D|-A) chain="$a";; --set-mss) mss="$a";; esac
case "$a" in -C|-D|-A) [ -z "$action" ] && action="$a";; esac
prev="$a"
done
key="$chain:$mss"
case "$action" in
-C) grep -qxF "$key" "$RULES" 2>/dev/null && exit 0 || exit 1 ;;
-A) grep -qxF "$key" "$RULES" 2>/dev/null || echo "$key" >> "$RULES"; exit 0 ;;
-D) grep -qxF "$key" "$RULES" 2>/dev/null || exit 1
grep -vxF "$key" "$RULES" > "$RULES.t" 2>/dev/null; mv "$RULES.t" "$RULES"; exit 0 ;;
esac
exit 0
EOF
mk cloudflared <<'EOF'
#!/bin/bash
[ "$1" = "--version" ] && { echo "cloudflared version 2026.9.3 (stub)"; exit 0; }
echo "cloudflared $*" >> "$STUB_LOG"; exit 0
EOF
mk curl <<'EOF'
#!/bin/bash
for a in "$@"; do
case "$a" in *v1/health*) echo '{"ok": true}'; exit 0;; esac
done
exit 0
EOF
mk free <<'EOF'
#!/bin/bash
echo " total used free"
echo "Mem: 460 131 144"
EOF
mk nmcli <<'EOF'
#!/bin/bash
exit 0
EOF
mk getent <<'EOF'
#!/bin/bash
exit 0
EOF
[ -n "$REAL_PY" ] && mk python3 <<EOF
#!/bin/bash
exec "$REAL_PY" "\$@"
EOF
export PATH="$STUB:$PATH"
OUT="$TMP/run.log"
(cd "$SRV" && bash deploy.sh --prefix "$SANDBOX" --skip-tunnel) > "$OUT" 2>&1
RC=$?
check "沙箱部署退出码为 0" "$RC" "0"
if [ "$RC" != "0" ]; then tail -20 "$OUT" | sed 's/^/ /'; fi
REL="$SANDBOX/root/lanremote-relay"
UNIT="$SANDBOX/etc/systemd/system"
for f in relay.py admin.py landing.py showcase.py favicon.ico; do
if [ -f "$REL/$f" ]; then ok "部署了 $f"; else bad "没部署 $f"; fi
done
[ -f "$REL/site_shots/pc-main.png" ] && ok "部署了截图" || bad "没部署截图"
for u in lanremote-relay lanremote-nettune lanremote-tunnel lanremote-tunnel-h2; do
[ -f "$UNIT/$u.service" ] && ok "生成了 $u.service" || bad "缺 $u.service"
done
echo
echo "── 6. 再跑一遍(幂等)──"
echo "keep" > "$REL/data/sentinel.txt"
(cd "$SRV" && bash deploy.sh --prefix "$SANDBOX" --skip-tunnel) > "$TMP/run2.log" 2>&1
check "第二次退出码为 0" "$?" "0"
check "哨兵文件还在" "$(cat "$REL/data/sentinel.txt" 2>/dev/null)" "keep"
echo
echo "============================================================"
echo " 结果: 通过 $PASS 项,失败 $FAIL 项"
echo "============================================================"
if [ "$FAIL" = "0" ]; then
rm -rf "$TMP"
exit 0
else
echo " 临时目录保留: $TMP"
exit 1
fi