Files
drouter/_dev/test-offline-install.sh
liuzhuohua 64df3cf331 feat: Drouter 1.0.0 —— Debian 13 软路由管理系统
一套跑在 Debian 13 (trixie) 上的软路由管理系统,提供 43 个功能页面的
Web 管理面板:WAN 接入、PPPoE 多拨、DHCP/DNS、IPv6、防火墙、QoS 限速、
访问控制、DDNS、文件共享、Docker 管理等。

核心设计原则:不重新实现任何协议
  PPPoE 交给 ppp,DHCP 交给 dnsmasq,IPv6 RA 交给 radvd,NAT 交给 nftables。
  Web 面板只做三件事:渲染原生配置文件 → 语法预检 → 原子写入并 reload。
  面板删掉后,系统仍是一台配置完好的 Debian 路由器。

安全默认值
  · 安装后不接管网络:不改默认路由、不开 IP 转发、不启动 DHCP 服务
  · 所有改网络的动作都需要用户在面板上亲手点「应用」
  · 避开 5900 端口,不触碰 VNC / X 会话
  · 配置写入前先做语法预检,失败则回滚

架构
  · backend/  Python 3 标准库,零第三方依赖(9 个模块)
  · web/      原生 JS/CSS,无构建步骤
  · 常驻 helper 通过 Unix socket 提供 75 个动作,socket 不可用时回退 subprocess
  · 鉴权走 X-Token 请求头

打包
  · packaging/build-deb.sh            → drouter_1.0.0_all.deb(含 6 个 systemd 单元)
  · scripts/make-offline-deps.sh      → 236 个依赖 .deb + 本地仓库索引
  · packaging/build-offline-bundle.sh → 自包含离线包,目标机零外网可装
  · packaging/build-docker.sh         → 容器镜像(体验用)

测试
  40 套单元测试 + 3 个前端检查脚本(菜单映射 / 离线渲染 / 手机适配),
  全部挂在 devtools/sync.sh 的预检链里。

许可:MIT
2026-09-30 19:26:24 +08:00

231 lines
12 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
# =============================================================================
# 离线安装验收:在一个一次性 chroot 里「断网」跑一遍 install.sh,
# 证明离线包确实自洽(不需要任何外网就能把 drouter 装起来)。
#
# sudo bash /tmp/test-offline-install.sh /tmp/drouter-offline
#
# 为什么要这么测:离线包最容易出的问题是**传递依赖没下全** —— 在装了
# 一堆东西的开发机上测不出来,因为缺的包恰好已经装着了。只有在干净
# 根目录 + 无外网的环境里,缺哪个包才会立刻暴露。
# =============================================================================
set -u
OFFLINE="${1:-/tmp/drouter-offline}"
CH=/tmp/drouter-chroot-test
DEBS="$OFFLINE/debs"
# 安全前置检查:CH 必须是个具体的 /tmp 子目录,绝不允许为空或落在 / 上,
# 否则后面的 rm -rf "$CH" 会变成删根。
case "$CH" in
/tmp/*) : ;;
*) echo "✘ 拒绝执行:CH 必须位于 /tmp 下(当前:$CH)"; exit 1 ;;
esac
[ -d "$DEBS" ] || { echo "✘ 找不到依赖目录:$DEBS"; exit 1; }
echo "=== 0/4 清理上次残留(含挂载点)==="
for m in "$CH/proc" "$CH/dev" "$CH/sys"; do
umount "$m" 2>/dev/null || true
done
# 顺带检查宿主机 /dev/null 是否被人误改成了普通文件(本脚本早期版本踩过)
if [ ! -c /dev/null ]; then
echo " ⚠ 检测到 /dev/null 不是字符设备,正在修复"
rm -f /dev/null && mknod -m 666 /dev/null c 1 3
fi
echo "=== 1/4 准备干净根目录 ==="
rm -rf "$CH"
mkdir -p "$CH"/{bin,dev,proc,sys,tmp,etc,var/lib/dpkg,usr/bin,usr/lib,var/log}
# 基础层:一个能跑 dpkg/apt 的最小 Debian 根。
# 注意 dpkg 启动时会硬性检查 PATH 里有没有 sh / diff / ldconfig,
# 缺任何一个都直接 `dpkg: error: N expected programs not found`,什么也装不了。
# 所以 dash(diffutils 的 diff)、libc-bin(ldconfig) 必须在。
# apt-get 也是必需的(install.sh 优先走本地仓库模式)。
BASE="base-files base-passwd libc6 libgcc-s1 gcc-14-base libselinux1 libpcre2-8-0
libacl1 libattr1 libcap2 libaudit1 libaudit-common dpkg tar gzip xz-utils
zlib1g liblzma5 libbz2-1.0 sed coreutils libc-bin bash libtinfo6 libreadline8t64 dash
diffutils grep findutils libstdc++6 libmd0 libgmp10 libnettle8t64 libhogweed6t64
libidn2-0 libunistring5 libtasn1-6 libp11-kit0 libffi8 libgnutls30t64 libseccomp2
libsystemd0 libzstd1 libudev1 libdb5.3t64 libgcrypt20 libgpg-error0 libmount1
libblkid1 libsmartcols1 libuuid1 libcap-ng0 libcrypt1 libedit2 libncursesw6
libfdisk1 libapparmor1 libxxhash0 libcom-err2 libext2fs2t64 libjansson4 libargon2-1
libapt-pkg7.0 libssl3t64 openssl openssl-provider-legacy"
# 说明两个刻意的例外(不是漏,是离线包里本来就没有):
# · apt / apt-utils:任何 Debian 都自带,没必要打进包;测试根从宿主机借。
# · dpkg-dev / lsof / libcbor0.10 / libfido2-1:本项目用不到,不进闭包。
# (lsof 只在诊断时手动用;libcbor/libfido2 是 openssh 的 FIDO 支持链。)
# 真实目标机是完整 Debian 13,这些一律不缺,只有「从零拼的测试根」才要借。
# ⚠️ 基础层的搭法(两种都试过,最后用这个组合):
#
# 方案 A:只用 `dpkg-deb -x` 摊文件 —— 文件对,但 **不在 status 里登记**,
# 后面装 drouter 时 dpkg 会报「Package tar is not installed」。
# 方案 B:只用 `dpkg -i --root/--instdir` —— 登记对,但 dpkg 会按
# 「已装好的根」前提去处理 usr-merge 与维护脚本,实测把 /bin 搞空
# (/bin/bash 消失),chroot 直接起不来。
#
# 最终方案:**dpkg-deb -x 摊文件(保证文件系统正确)**
# + **手工把 control 记录并进 status(保证依赖数据库正确)**。
# 这样既不依赖 dpkg 的重装逻辑,又能让依赖校验正常工作。
for p in $BASE; do
f=$(ls "$DEBS/${p}"_*.deb 2>/dev/null | head -1)
[ -n "$f" ] || continue
dpkg-deb -x "$f" "$CH" 2>/dev/null
done
# 把每个基础包的 control 段登记进 dpkg status,并把状态改成 installed。
# dpkg 的 status 是「空行分隔的记录集」,记录之间靠空行分界 ——
# **不能**写 `---` 分隔线(那是 .changes/changelog 的写法),
# dpkg 解析到 `---` 会直接报 `field name '---' cannot start with hyphen`。
# 只改 Status 字段:control 里的 Status 一般是 unknown,dpkg 会当成没装。
register_pkg() {
_f="$1"
[ -n "$_f" ] || return 0
_ctl=$(dpkg-deb -f "$_f" Package) || return 0
[ -n "$_ctl" ] || return 0
grep -q "^Package: $_ctl\$" "$CH/var/lib/dpkg/status" 2>/dev/null && return 0
# 保证前一条记录以空行收尾,否则会和新记录粘在一起
if [ -s "$CH/var/lib/dpkg/status" ]; then
printf '\n' >> "$CH/var/lib/dpkg/status"
fi
dpkg-deb -f "$_f" 2>/dev/null | grep -v '^Status:' >> "$CH/var/lib/dpkg/status"
echo "Status: install ok installed" >> "$CH/var/lib/dpkg/status"
mkdir -p "$CH/var/lib/dpkg/info"
dpkg-deb -e "$_f" "$CH/var/lib/dpkg/info/$_ctl" 2>/dev/null || true
dpkg-deb -c "$_f" 2>/dev/null | awk '{print $NF}' \
| sed 's|^\./||; s|^|/|' | grep -v '^/$' \
> "$CH/var/lib/dpkg/info/$_ctl.list" 2>/dev/null || true
}
for p in $BASE; do
register_pkg "$(ls "$DEBS/${p}"_*.deb 2>/dev/null | head -1)"
done
# usr-merge:Debian 13 的 /bin /sbin /lib 都是指向 usr/ 的符号链接。
# 从零拼根目录时必须手工补上,否则 chroot 里 /bin/bash 找不到。
for d in bin sbin lib lib64; do
if [ -d "$CH/usr/$d" ] && [ ! -e "$CH/$d" ]; then
ln -s "usr/$d" "$CH/$d" 2>/dev/null || true
fi
done
# 基础层登记结果自检:哪些包没进 dpkg 数据库就报出来。
# 不要吞掉这个信息 —— 基础层缺包会让后面「装 drouter 失败」被误判成
# 离线包有问题,白白浪费一轮排查。
BASE_MISS=""
for p in $BASE; do
grep -q "^Package: $p\$" "$CH/var/lib/dpkg/status" 2>/dev/null || BASE_MISS="$BASE_MISS $p"
done
if [ -n "$BASE_MISS" ]; then
echo " ⚠ 基础层未登记(离线包里可能没有):$BASE_MISS"
else
echo " 基础层全部已登记进 dpkg"
fi
# apt 本身不在离线包里(任何 Debian 都自带,没必要发),但测试用的干净根
# 是从零拼的,所以要从宿主机借一套 apt/dpkg 二进制过来,否则 install.sh
# 会退化成 `dpkg -i` 乱装那一支,测不到真实的「本地 file: 源」链路。
# 注意:真实目标机不可能缺这些,这里只是补全测试环境。
for b in /usr/bin/apt-get /usr/bin/apt /usr/bin/apt-cache /usr/bin/dpkg \
/usr/bin/dpkg-deb /usr/bin/dpkg-query /usr/bin/dpkg-split /usr/bin/dpkg-trigger; do
[ -e "$b" ] && cp -a "$b" "$CH/usr/bin/" 2>/dev/null
done
mkdir -p "$CH/usr/lib/apt" "$CH/usr/lib/dpkg" "$CH/usr/lib/x86_64-linux-gnu"
cp -a /usr/lib/x86_64-linux-gnu/libapt-pkg*.so* "$CH/usr/lib/x86_64-linux-gnu/" 2>/dev/null
cp -a /usr/lib/x86_64-linux-gnu/libapt-private*.so* "$CH/usr/lib/x86_64-linux-gnu/" 2>/dev/null
cp -a /usr/lib/apt/* "$CH/usr/lib/apt/" 2>/dev/null
# dpkg 的方法脚本(dpkg-deb 解 .deb 时要调 tar 等)
mkdir -p "$CH/usr/lib/dpkg/methods"
cp -a /usr/share/dpkg/* "$CH/usr/share/dpkg/" 2>/dev/null
cp -a /etc/dpkg/dpkg.cfg "$CH/etc/dpkg/dpkg.cfg" 2>/dev/null
# 补 apt/dpkg 的动态库依赖
# libssl3t64 必须在:apt-get / apt-cache 都链 libcrypto.so.3,
# 缺了会直接 `error while loading shared libraries: libcrypto.so.3`,
# install.sh 的「本地 file: 源」那一支就整条走不通。
for p in libstdc++6 libmd0 libpcre2-8-0 liblzma5 liblz4-1 libzstd1 libbz2-1.0 \
libselinux1 libacl1 libattr1 libcap2 libgcc-s1 libmount1 libblkid1 \
libsmartcols1 libuuid1 libsystemd0 libudev1 libaudit1 libcap-ng0 \
libseccomp2 libcrypt1 libedit2 libncursesw6 libfdisk1 libapparmor1 \
libxxhash0 libcom-err2 libext2fs2t64 libgpg-error0 libgcrypt20 libjansson4 \
libargon2-1 libdb5.3t64 mawk gawk libsigsegv2 libmpfr6 libmpc3 \
libtext-charwidth-perl libtext-wrapi18n-perl perl-base \
libssl3t64 openssl openssl-provider-legacy libreadline8t64 libnettle8t64 \
libhogweed6t64 libgnutls30t64; do
f=$(ls "$DEBS/${p}"_*.deb 2>/dev/null | head -1)
[ -n "$f" ] && dpkg-deb -x "$f" "$CH" 2>/dev/null
done
ldconfig -r "$CH" 2>/dev/null || true
echo " 基础层文件数:$(find "$CH" -type f | wc -l)"
echo "=== 2/4 把离线包搬进 chroot 并断网 ==="
mkdir -p "$CH/tmp/offline" "$CH/etc/apt/sources.list.d" "$CH/etc/apt/apt.conf.d" \
"$CH/var/lib/apt/lists/partial" "$CH/var/cache/apt/archives/partial" \
"$CH/usr/share/dpkg" "$CH/var/lib/dpkg/info" "$CH/var/lib/dpkg/updates"
cp -a "$OFFLINE"/. "$CH/tmp/offline/"
# 清空所有源:让 apt 除了本地 file: 仓库之外无处可取
: > "$CH/etc/apt/sources.list"
rm -f "$CH"/etc/apt/sources.list.d/* 2>/dev/null || true
touch "$CH/var/lib/dpkg/status"
# dpkg 需要这两个文件做三向合并,缺了会在安装时报错
[ -f "$CH/etc/dpkg/dpkg.cfg" ] || { mkdir -p "$CH/etc/dpkg"; touch "$CH/etc/dpkg/dpkg.cfg"; }
echo " chroot 内 sources.list 已清空、DNS 指向黑洞"
# ⚠️ 绝对不要 `mount --bind /dev $CH/dev` ——
# 实测踩过:如果 $CH 的路径写错(比如前面多 cd 了一层、或 CH 变量为空),
# bind 会把宿主机的 /dev 挂到宿主机自己的某个目录上,随后清理时的
# rm -f /dev/null 之类会把**宿主机的设备节点换成普通文件**,
# 结果是全系统所有 `> /dev/null` 都变成「权限不够」,各种服务莫名其妙挂掉。
# chroot 里装包根本用不到 /dev 的大部分节点,只补 /dev/null 即可。
mkdir -p "$CH/dev"
[ -e "$CH/dev/null" ] || mknod -m 666 "$CH/dev/null" c 1 3 2>/dev/null || \
{ : > "$CH/dev/null"; chmod 666 "$CH/dev/null"; }
[ -e "$CH/dev/urandom" ] || { : > "$CH/dev/urandom"; chmod 666 "$CH/dev/urandom"; }
echo " chroot 内 /dev/null 已就绪(未做危险 bind-mount)"
echo "=== 3/4 在无外网 chroot 里执行离线安装 ==="
# 优先用自包含包里的 install.sh(packaging/offline-install.sh,走本地 file: 源);
# 没有就退回 make-offline-deps.sh 生成的那个。
INSTALLER=""
for c in "$CH/tmp/offline/install.sh"; do
[ -f "$c" ] && INSTALLER="$c" && break
done
if [ -z "$INSTALLER" ]; then
echo " ✘ 离线包里没有 install.sh"; exit 1
fi
echo " 使用安装脚本:$(basename "$INSTALLER")"
# 关键:把 chroot 路径通过 DROUTER_ROOT 告诉 install.sh,
# 让 apt/dpkg 都去读这个根的数据库(不然读的是宿主机的,判断全错)。
chroot "$CH" /bin/bash -c '
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
export DEBIAN_FRONTEND=noninteractive
export DROUTER_ROOT=/
cd /tmp/offline
bash install.sh
' 2>&1 | tail -45
RC=${PIPESTATUS[0]}
echo ""
echo "=== 4/4 验收:drouter 是否真的装上了 ==="
if [ -f "$CH/opt/drouter/backend/drouter-helper.py" ]; then
echo " ✅ /opt/drouter/backend/drouter-helper.py 存在"
echo " 后端模块:$(ls -1 "$CH"/opt/drouter/backend/*.py 2>/dev/null | wc -l) 个"
echo " 前端文件:$(ls -1 "$CH"/opt/drouter/web/ 2>/dev/null | wc -l) 个"
echo " drouter-ctl:$([ -x "$CH/usr/local/bin/drouter-ctl" ] && echo 存在 || echo 缺失)"
N_DEP=$(ls -1 "$CH"/var/lib/dpkg/info/*.list 2>/dev/null | wc -l)
echo " dpkg 记录的已安装包:$N_DEP"
else
echo " ❌ 没装上 drouter 本体"
fi
# 清理挂载点,避免残留
umount "$CH/proc" 2>/dev/null || true
umount "$CH/dev" 2>/dev/null || true
# 再确认一次宿主机设备节点没被弄坏
[ -c /dev/null ] || { rm -f /dev/null && mknod -m 666 /dev/null c 1 3; echo " (已修复宿主 /dev/null)"; }
echo ""
echo "chroot 保留在 $CH(确认无误后可 rm -rf)"
exit 0