Files
drouter/packaging/offline-install.sh
liuzhuohua 2f8f09604b release: 1.0.8 — VPN 页把「模块没加载」误报成「内核不支持 WireGuard」
版本号 1.0.7 → 1.0.8(19 个文件 55 处,逐条过;历史陈述不动)。
顺带补上1.0.7 漏掉的更新日志小节(那一版只改了版本号)。

## 本轮修的缺陷

**先说结论:Debian 13 虚拟机上跑 drouter,KVM 不是容器,
内核里一直有 WireGuard —— 是面板自己判错了。**

原 _vpn_installed() 只看两件事:/sys/module/wireguard 在不在(模块当前
已加载)、/usr/bin/wg 在不在(工具已安装)。**从没查过 /lib/modules 下
有没有模块文件** → 「模块文件在但没加载」被压进「内核真没这个模块」。

而 Debian 走 udev 按需加载,/etc/modules 与 /etc/modules-load.d 默认都
不写 wireguard,模块在磁盘上但没进内存。本机实测:内核 6.12.107+deb13-amd64,
wireguard.ko.xz 在,未加载;wireguard-tools 也没装。

前端还配了一句写死的文案把它坐实:「通常说明跑在精简容器里」——
而这台是 PVE 上的 KVM 虚拟机。排查方向被这句话整个带偏。

修法:
- 判据扩到四态(ready / need_module / need_tool / unsupported),
  每态给可执行的下一步。unsupported 明确说装不了且不给一键修复。
- 模块文件名覆盖 .ko / .ko.xz / .ko.zst(Debian 默认压成 .ko.xz,
  只认 .ko 会把正常机器也判成 unsupported)
- 新增「一键修复」:modprobe → 写 /etc/modules-load.d/ 开机自启 →
  缺工具则 apt-get install,确认弹窗写清三步并告知不动网络
- apply 改为先自愈再报错,不再直接弹「内核不支持」把人挡在门外
- 老后端兜底 unknown 而非把旧的 no 猜成 unsupported(猜错=复活本 bug)
- 页面新增「运行环境」行;改掉「不需要装任何第三方软件」这句误导

## 测试基建:这轮抓出 3 条恒绿断言 + 1 处死分支

反向验证(把 bug 注入回去看它变红)逮到三条永远不会红的断言:
把探测路径改成 /nonexistent 仍全绿(docstring 里就写着那句话);
把 'env': _vpn_env() 注释掉仍全绿(我写的是 #注释,正则照样匹配);
把 if (!confirm( 改成 if (false && confirm( 仍全绿(提示文案里有那句话)。

根因:习惯在注释里引用被删掉的旧代码来解释 bug 来历,这些字面量让
「这段代码还在吗」类的判据永远成立。修法:t-107 统一走 py_code_only() /
js_code_only()(先按ast 判语义剥 docstring,再剥行注释),16 个注入全部被抓住。

死分支:用小锚点 Edit 只替换 if (d.installed === 'no') { 这一行时,
旧 body 与它的 } else if (!d.has_systemd) { 原样留在原地,拼出一个
「没有 systemd → 报内核不支持」的假分支——语法合法、静态检查也过。
防线:断言 } else if ( 的数量恰好等于分支数。

另有 4 处判据写错导致假红,共同形态是「脚本假设了一个修好之后就不成立
的前提」——比如「模块必须未加载」这条,fix 一跑完就假红。

## 验证

- t-107:126 → 167 项
- 反向验证 16/16 被抓住
- 新增 devtools/check-vpn-render.js 36 项(挂进 sync.sh 预检链)
- 新增 _dev/live-108-wg.py 真机验收 43 项;真机 _vpn_env 从 need_module
  修到 ready,wg 装上(v1.0.20210914),modules-load.d 已写入
- t-*.py 51 个全绿 / 静态检查器 39 个全绿 / 前端四查全绿 / md 0 死链
- 容器冒烟新增第 14 节:镜像里真跑 _vpn_env() 验四态判定
  (不 grep 字面量——注释里也会写这些词)
- deb 内 helper 与 app.js sha256 与本地逐字节一致
2026-10-03 12:28:21 +08:00

234 lines
10 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# =============================================================================
# Drouter 自包含离线安装脚本
#
# sudo bash install.sh [drouter_x.y.z_all.deb]
#
# 设计目标:**目标机零外网**也要能一次装完。
# 做法是先把本地 debs/ 目录注册成一个 file: 源,让 apt 自己去算依赖顺序 ——
# 比 `dpkg -i debs/*.deb` 一通乱装可靠得多(dpkg 不看顺序,装到一半就会
# 因为依赖没满足而大面积失败)。
#
# 会自动 sudo:不加 sudo 直接跑也能用。
# =============================================================================
set -u
HERE="$(cd "$(dirname "$0")" && pwd)"
# ---- 提权 ----
if [ "$(id -u)" -ne 0 ]; then
echo "需要 root 权限,正在通过 sudo 重新执行…"
exec sudo -E bash "$0" "$@"
fi
step() { printf '\n\033[1;36m== %s ==\033[0m\n' "$*"; }
warn() { printf '\033[1;33m ⚠ %s\033[0m\n' "$*"; }
die() { printf '\033[1;31m ✘ %s\033[0m\n' "$*"; exit 1; }
# ---------------------------------------------------------------------------
# 根前缀(仅 chroot 验收用,真实目标机上为空、行为完全不变)。
#
# 为什么需要:在不设根前缀的 chroot 里,apt/dpkg 读的是**宿主机**的
# /var/lib/dpkg/status,于是会出现「apt 说 hostname 已经是最新版、chroot 里
# 其实没有」这种鬼打墙;最后 drouter 本体因为
# `Package hostname is not installed` 装不上,看起来像离线包缺包。
#
# 用法(验收脚本):DROUTER_ROOT=/tmp/xxx bash install.sh
# 真实目标机:不设该变量 → ROOT_PREFIX 为空 → 走原路径,零差异。
# ---------------------------------------------------------------------------
ROOT_PREFIX=""
APT_ROOT_OPTS=()
# 注意 `${X%/}` 对 "/" 会得到空串,会被误判成「没设置」。
# chroot 内的正确用法恰恰就是 DROUTER_ROOT=/(此时 chroot 的 / 已是目标根),
# 所以要单独判一次原值,而不是判剥掉斜杠后的结果。
if [ -n "${DROUTER_ROOT:-}" ]; then
if [ "$DROUTER_ROOT" = "/" ] || [ "$DROUTER_ROOT" = "" ]; then
ROOT_PREFIX=""
echo " 根前缀:/(chroot 内,直接使用当前根)"
else
ROOT_PREFIX="${DROUTER_ROOT%/}"
[ -d "$ROOT_PREFIX/var/lib/dpkg" ] || die "DROUTER_ROOT 指向的目录不是有效根:$ROOT_PREFIX"
APT_ROOT_OPTS=(
-o "Dir=$ROOT_PREFIX/"
-o "Dir::State=$ROOT_PREFIX/var/lib/apt"
-o "Dir::State::status=$ROOT_PREFIX/var/lib/dpkg/status"
-o "Dir::Cache=$ROOT_PREFIX/var/cache/apt"
)
echo " 根前缀:$ROOT_PREFIX(chroot 模式,apt/dpkg 库都指向该根)"
fi
fi
# ---------------------------------------------------------------------------
step "0/4 环境自检"
# ---------------------------------------------------------------------------
[ -d "$HERE/debs" ] || die "找不到 debs/ 目录 —— 解包不完整?"
NDEB=$(ls -1 "$HERE"/debs/*.deb 2>/dev/null | wc -l)
[ "$NDEB" -gt 0 ] || die "debs/ 里一个 .deb 都没有"
echo " 依赖包:$NDEB 个"
# 架构必须在下载依赖时那台机器一致,否则装了也起不来
if command -v dpkg >/dev/null 2>&1; then
MYARCH=$(dpkg --print-architecture)
case "$HERE" in
*-"$MYARCH") : ;;
*) warn "本机架构是 $MYARCH,但包名里写的是别的 —— 可能不匹配" ;;
esac
fi
if [ -f /etc/os-release ]; then
. /etc/os-release
echo " 系统:${PRETTY_NAME:-unknown}"
case "${VERSION_CODENAME:-}" in
trixie) : ;;
'') warn "识别不出 VERSION_CODENAME,跳过版本核对" ;;
*) warn "依赖是按 Debian 13 (trixie) 下载的,本机是 ${VERSION_CODENAME},"
warn "版本对不上的包可能装不上。继续尝试…" ;;
esac
fi
# 磁盘检查:200+ 个 deb 解包后要占几百 MB
# 用 df -Pk + cut 而不是 awk —— awk 不是 Essential 包,极简系统上可能没有,
# 装包脚本自己先因为「找不到 awk」而报错就太讽刺了。
AVAIL_KB=$(df -Pk /var 2>/dev/null | tail -1 | tr -s ' ' | cut -d' ' -f4)
if [ -n "${AVAIL_KB:-}" ] && [ "$AVAIL_KB" -lt 1500000 ] 2>/dev/null; then
warn "根分区可用空间不足 1.5G(当前 $((AVAIL_KB/1024))M),安装可能失败"
fi
# ---------------------------------------------------------------------------
step "1/4 找安装包"
# ---------------------------------------------------------------------------
PKG="${1:-}"
if [ -z "$PKG" ]; then
PKG=$(ls -1 "$HERE"/drouter_*_all.deb 2>/dev/null | head -1 || true)
fi
if [ -z "$PKG" ]; then
die "目录里找不到 drouter_*_all.deb(用法:bash install.sh drouter_1.0.8_all.deb)"
fi
[ -f "$PKG" ] || PKG="$HERE/$PKG"
[ -f "$PKG" ] || die "找不到安装包:$PKG"
echo " 本体:$(basename "$PKG")"
# ---------------------------------------------------------------------------
step "2/4 安装依赖(走本地 file: 源,零外网)"
# ---------------------------------------------------------------------------
# 关键点:用一份**只指向本地 debs/** 的临时 sourcelist,
# 并同时把 Dir::Etc::sourceparts 指到 /dev/null —— 否则系统里原有的
# 外网源还会被读进来,apt 会去联网找"更新的版本",无外网时就卡住/报错。
TMPLIST=$(mktemp /tmp/drouter-src.XXXXXX.list)
trap 'rm -f "$TMPLIST"' EXIT
cat > "$TMPLIST" <<EOF
deb [trusted=yes] file:$HERE/debs ./
EOF
APT_OPTS=(
-o "Dir::Etc::sourcelist=$TMPLIST"
-o "Dir::Etc::sourceparts=/dev/null"
-o "APT::Get::List-Cleanup=0"
-o "Acquire::Languages=none"
-o "Acquire::Retries=0"
)
# chroot 模式下必须把整个 apt/dpkg 状态根也搬过去,
# 否则 apt 会去读宿主机的 status,判断「已装」和「未装」全是错的。
if [ -n "$ROOT_PREFIX" ]; then
APT_OPTS+=("${APT_ROOT_OPTS[@]}")
else
APT_OPTS+=( -o "Dir::State::lists=$HERE/.aptlists" )
fi
if [ -n "$ROOT_PREFIX" ]; then
mkdir -p "$ROOT_PREFIX/var/lib/apt/lists/partial" "$ROOT_PREFIX/var/cache/apt/archives/partial" 2>/dev/null || true
else
mkdir -p "$HERE/.aptlists/partial"
fi
# 索引自检:debs/ 里必须有 Packages 或 Packages.gz,否则 apt 会报
# `E: Failed to fetch file:.../debs/./Packages File not found`
# 然后「本地仓库」这条链路就静默退化成 dpkg -i 乱装(顺序不对就装不上)。
# 老版本的 make-offline-deps.sh 把索引生成到了上一级目录,正是这个症状;
# 这里顺手自愈一下,兼容用户手上可能存在的旧包。
if [ ! -f "$HERE/debs/Packages" ] && [ ! -f "$HERE/debs/Packages.gz" ]; then
if [ -f "$HERE/Packages.gz" ]; then
echo " (索引在上级目录,已自动搬到 debs/ 下)"
mv -f "$HERE/Packages.gz" "$HERE/debs/Packages.gz"
elif [ -f "$HERE/Packages" ]; then
echo " (索引在上级目录,已自动搬到 debs/ 下)"
mv -f "$HERE/Packages" "$HERE/debs/Packages"
else
warn "debs/ 里没有 Packages 索引,将退化为 dpkg -i 逐个安装"
fi
fi
echo " 建立本地仓库索引…"
if ! apt-get "${APT_OPTS[@]}" update -qq 2>&1 | tail -3; then
warn "本地索引建立有问题,继续尝试(下面的 install 会再校验一次)"
fi
# 把 debs/ 里所有包名拿去 install —— apt 会自动只选本地源里有的,
# 且按依赖顺序装。已经装过且版本相同的会跳过。
DEBLIST=$(cd "$HERE/debs" && ls -1 *.deb 2>/dev/null | sed 's/_.*//' | sort -u | tr '\n' ' ')
echo " 从本地源安装 $NDEB 个依赖包(已装过的会跳过)…"
# dpkg 也要跟着根前缀走。chroot 下不传 --root 会把包装到宿主机上(灾难)。
DPKG_ROOT_OPTS=()
[ -n "$ROOT_PREFIX" ] && DPKG_ROOT_OPTS=( --root="$ROOT_PREFIX" --force-not-root )
if ! DEBIAN_FRONTEND=noninteractive apt-get "${APT_OPTS[@]}" \
--no-install-recommends -y install $DEBLIST 2>&1 | tail -8; then
warn "apt 批量安装报错,改用 dpkg 逐个兜底"
dpkg "${DPKG_ROOT_OPTS[@]}" -i "$HERE"/debs/*.deb 2>&1 | tail -5 || true
# 再让 apt 用本地源把没配好的补一遍
apt-get "${APT_OPTS[@]}" -y -f install 2>&1 | tail -5 || true
fi
# ---------------------------------------------------------------------------
step "3/4 安装 drouter 本体"
# ---------------------------------------------------------------------------
# postinst 里可能调用 systemctl,无外网不影响,但如果目标机没有 systemd
# 会报错,所以失败不致命 —— 最后单独报告。
if ! DEBIAN_FRONTEND=noninteractive dpkg "${DPKG_ROOT_OPTS[@]}" -i "$PKG" 2>&1 | tail -10; then
warn "本体安装过程有报错,尝试用本地源补依赖"
apt-get "${APT_OPTS[@]}" -y -f install 2>&1 | tail -8 || true
fi
# ---------------------------------------------------------------------------
step "4/4 结果"
# ---------------------------------------------------------------------------
OPT_ROOT="${ROOT_PREFIX}/opt/drouter"
if [ -f "$OPT_ROOT/backend/drouter-helper.py" ]; then
MODS=$(ls -1 "$OPT_ROOT"/backend/*.py 2>/dev/null | wc -l)
printf '\033[1;32m ✅ 安装成功\033[0m(后端模块 %s 个)\n' "$MODS"
# 服务状态(没 systemd 就跳过)
if command -v systemctl >/dev/null 2>&1 && systemctl is-system-running >/dev/null 2>&1; then
ST=$(systemctl is-active drouter-web 2>/dev/null || echo unknown)
echo " drouter-web 服务:$ST"
fi
# 取本机 IP。优先 iproute2;没有就退回 hostname -I(更老但到处都有)。
# 两者都不可用时留空,不影响安装结果。
IP=""
if command -v ip >/dev/null 2>&1; then
IP=$(ip -4 -o addr show scope global 2>/dev/null | tr -s ' ' | cut -d' ' -f4 | cut -d/ -f1 | head -1)
fi
if [ -z "$IP" ] && command -v hostname >/dev/null 2>&1; then
IP=$(hostname -I 2>/dev/null | tr ' ' '\n' | head -1)
fi
PORT=$(cat /etc/drouter/web-port 2>/dev/null || echo 8443)
cat <<EOF
下一步:
1) 浏览器访问 https://${IP:-<本机IP>}:${PORT}/
(自签证书,浏览器会提示不安全,点"继续访问")
2) 用 admin / admin123 登录,**第一件事改密码**
3) 左侧菜单第一项「新手向导」会带你配好外网/内网/DNS/IPv6
命令行工具:
sudo drouter-ctl status 查看状态
sudo drouter-ctl webport 8443 改 Web 端口
EOF
else
die "没装上 —— 看上面的报错。完整日志:bash install.sh 2>&1 | tee /tmp/install.log"
fi