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
This commit is contained in:
commit
64df3cf331
135 files changed
+56059
No files matched your search
@@ -0,0 +1,46 @@
|
||||
#!/bin/bash
|
||||
# 交付前最终状态审计
|
||||
echo "===== 1. 后端服务 ====="
|
||||
systemctl is-active drouter-web; systemctl is-enabled drouter-web
|
||||
systemctl show drouter-web -p MemoryCurrent -p ProtectSystem -p PrivateTmp -p User 2>/dev/null
|
||||
|
||||
echo; echo "===== 2. 监听端口 ====="
|
||||
ss -lntup 2>/dev/null | awk 'NR==1 || /8080|8443|5900|:22/'
|
||||
|
||||
echo; echo "===== 3. 网络服务应全部 inactive+disabled ====="
|
||||
for s in dnsmasq radvd dhcpcd miniupnpd chrony nftables; do
|
||||
printf "%-12s active=%-10s enabled=%s\n" "$s" "$(systemctl is-active $s 2>&1)" "$(systemctl is-enabled $s 2>&1)"
|
||||
done
|
||||
|
||||
echo; echo "===== 4. 端口 53/67/68 占用(应为空) ====="
|
||||
ss -lnu 2>/dev/null | grep -E ':53 |:67 |:68 ' || echo " 53/67/68 空闲 ✔"
|
||||
|
||||
echo; echo "===== 5. nftables 规则(应为空) ====="
|
||||
nft list ruleset 2>/dev/null | wc -l
|
||||
|
||||
echo; echo "===== 6. 构建保护模式 ====="
|
||||
if [ -f /etc/drouter/BUILD_MODE ]; then echo " BUILD_MODE 存在 —— 保护开启 ✔"; cat /etc/drouter/BUILD_MODE; else echo " 未开启"; fi
|
||||
|
||||
echo; echo "===== 7. 已生成配置文件 ====="
|
||||
for f in /etc/dnsmasq.d/drouter.conf /etc/nftables.d/drouter-v4.nft /etc/nftables.d/drouter-v6.nft \
|
||||
/etc/radvd.conf /etc/dhcpcd.conf /etc/miniupnpd/miniupnpd.conf /etc/chrony/chrony.conf \
|
||||
/etc/ppp/peers/drouter-wan; do
|
||||
[ -f "$f" ] && printf " ✔ %-42s %s 字节\n" "$f" "$(stat -c%s "$f")" || printf " ✘ %-42s 缺失\n" "$f"
|
||||
done
|
||||
|
||||
echo; echo "===== 8. 快照数量 ====="
|
||||
ls -1 /opt/drouter/snapshots 2>/dev/null | wc -l
|
||||
|
||||
echo; echo "===== 9. 内存占用 ====="
|
||||
ps -o rss=,cmd= -C python3 2>/dev/null | awk '{printf " RSS=%.1f MB %s\n", $1/1024, $2}'
|
||||
|
||||
echo; echo "===== 10. 网卡 ====="
|
||||
ip -br -4 addr show ens18
|
||||
|
||||
echo; echo "===== 11. VNC 进程 ====="
|
||||
ps aux | grep -i '[v]nc' | awk '{print " "$11" "$12" "$13}'
|
||||
|
||||
echo; echo "===== 12. 审计日志尾部 ====="
|
||||
tail -5 /var/log/drouter/audit.jsonl 2>/dev/null || echo " (无)"
|
||||
|
||||
echo "AUDIT_DONE"
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
# 认领新网卡 ens19 为 WAN 口
|
||||
B=http://127.0.0.1:8080
|
||||
TOK=$(curl -s $B/api/login -X POST -H 'Content-Type: application/json' \
|
||||
-d '{"username":"admin","password":"admin123"}' \
|
||||
| python3 -c 'import sys,json;print((json.load(sys.stdin).get("data") or {}).get("token",""))')
|
||||
|
||||
curl -s $B/api/iface/meta -H "X-Token: $TOK" | python3 -m json.tool --no-ensure-ascii
|
||||
echo
|
||||
|
||||
echo "=== 认领 ens19 为 WAN ==="
|
||||
curl -s $B/api/iface/save -X POST -H "X-Token: $TOK" -H 'Content-Type: application/json' \
|
||||
-d '{"mac":"bc:24:11:f2:5a:47","name":"ens19","remark":"WAN口(PPPoE拨号)","role":"wan"}' \
|
||||
| python3 -m json.tool --no-ensure-ascii
|
||||
|
||||
echo
|
||||
echo "=== 确认 ens18 仍为 LAN ==="
|
||||
curl -s $B/api/iface/save -X POST -H "X-Token: $TOK" -H 'Content-Type: application/json' \
|
||||
-d '{"mac":"bc:24:11:1a:8f:43","name":"ens18","remark":"LAN管理口","role":"lan"}' \
|
||||
| python3 -m json.tool --no-ensure-ascii
|
||||
|
||||
echo
|
||||
echo "=== 结果 ==="
|
||||
sqlite3 -header -column /opt/drouter/data/drouter.db 'select mac,name,remark,role from ifaces;'
|
||||
echo
|
||||
echo "--- system 配置 ---"
|
||||
sqlite3 /opt/drouter/data/drouter.db "select value from settings where key='system';" | python3 -m json.tool --no-ensure-ascii
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
# 清理回归测试残留:PPPoE 测试账号(peers / chap-secrets / pap-secrets)
|
||||
set -u
|
||||
echo "=== 清理前 ==="
|
||||
grep -l "test12345" /etc/ppp/peers/* /etc/ppp/*-secrets 2>/dev/null
|
||||
|
||||
echo "=== 删除测试生成的 PPPoE 文件 ==="
|
||||
rm -f /etc/ppp/peers/drouter-wan
|
||||
# secrets 文件如果是本次测试生成的(仅含测试账号),恢复为空的模板
|
||||
for f in /etc/ppp/chap-secrets /etc/ppp/pap-secrets; do
|
||||
if [ -f "$f" ] && [ "$(wc -l < "$f")" -le 1 ] && grep -q "test12345" "$f" 2>/dev/null; then
|
||||
printf '# 由 drouter 自动生成:PPPoE 认证凭据\n# 在 WEB 界面填写 PPPoE 账号密码并保存后,此处会被写入\n' > "$f"
|
||||
chmod 600 "$f"; chown root:root "$f"
|
||||
echo "已重置 $f"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "=== 重置数据库 pppoe 配置块 ==="
|
||||
python3 - <<'PY'
|
||||
import sqlite3, json
|
||||
db = '/opt/drouter/data/drouter.db'
|
||||
c = sqlite3.connect(db); c.row_factory = sqlite3.Row
|
||||
row = c.execute("SELECT value FROM settings WHERE key='pppoe'").fetchone()
|
||||
cur = json.loads(row['value']) if row else {}
|
||||
cur.setdefault('isp', 'auto'); cur.setdefault('mtu', 1492); cur.setdefault('mru', 1492)
|
||||
cur.setdefault('persist', True); cur.setdefault('maxfail', 0); cur.setdefault('holdoff', 5)
|
||||
cur.setdefault('service_name', '')
|
||||
cur['iface'] = ''; cur['username'] = ''; cur['password'] = ''
|
||||
cur['saved_only'] = True; cur['note'] = '仅保存凭据,未执行拨号'
|
||||
c.execute("UPDATE settings SET value=? WHERE key='pppoe'", (json.dumps(cur, ensure_ascii=False),))
|
||||
c.commit()
|
||||
print('pppoe ->', json.dumps(cur, ensure_ascii=False))
|
||||
PY
|
||||
|
||||
echo "=== 清理后核对 ==="
|
||||
if grep -rq "test12345" /etc/ppp/ 2>/dev/null; then
|
||||
echo "仍有残留!"; grep -rn "test12345" /etc/ppp/ 2>/dev/null
|
||||
else
|
||||
echo "无测试账号残留 ✔"
|
||||
fi
|
||||
ls -la /etc/ppp/peers/ /etc/ppp/chap-secrets /etc/ppp/pap-secrets 2>&1
|
||||
echo "CLEANUP_DONE"
|
||||
@@ -0,0 +1,428 @@
|
||||
#!/bin/bash
|
||||
# Debian13 主路由系统 —— 部署脚本
|
||||
# 将后端、前端、systemd 服务、sudoers 白名单安装到位。
|
||||
# 不会启动任何网络服务,不会修改网络配置。
|
||||
set -e
|
||||
|
||||
# 源码目录可用 DROUTER_SRC 覆盖(一键脚本 drouter-ctl.sh 会传入实际路径)
|
||||
SRC="${DROUTER_SRC:-/tmp/drouter-src}"
|
||||
OPT=/opt/drouter
|
||||
UNIT_WEB=drouter-web
|
||||
|
||||
echo "=== 1. 创建目录结构 ==="
|
||||
mkdir -p $OPT/backend $OPT/web $OPT/data $OPT/snapshots $OPT/certs $OPT/bin $OPT/docs
|
||||
mkdir -p /etc/drouter/generated
|
||||
mkdir -p /etc/nftables.d
|
||||
mkdir -p /var/log/drouter
|
||||
mkdir -p /etc/dnsmasq.d
|
||||
mkdir -p /var/lib/drouter/trash # 文件管理器"回收站"
|
||||
mkdir -p $OPT/dpi # DPI 识别规则库与源码(#25)
|
||||
mkdir -p /etc/ppp/ip-up.d /etc/ppp/ip-down.d # PPPoE 多拨策略路由钩子(#23)
|
||||
# helper 私有临时目录:/run 为 tmpfs(重启自清),且不归低权用户所有,
|
||||
# 避开 /tmp、/var/tmp 的 sticky 位与 fs.protected_regular=2 写保护。
|
||||
mkdir -p /run/drouter-helper
|
||||
chmod 700 /run/drouter-helper
|
||||
|
||||
sha_of() { [ -f "$1" ] && sha1sum < "$1" | cut -d' ' -f1 || echo "-"; }
|
||||
# shelld 是常驻守护,代码在内存里,不重启就是旧行为。
|
||||
# 判断依据必须是「本次源码指纹」vs「上次真正重启过的指纹」(记在 mark 文件里),
|
||||
# 不能拿 $SRC 和 $OPT 比 —— install 之后两边同源,cmp 恒等,永远不重启。
|
||||
# 也不能拿 install 前的 $OPT 比 —— 若上一轮已经把新文件拷进去了但没重启成功,
|
||||
# 这一轮两边又相等,漏网。
|
||||
SHELLD_NEW=$(sha_of "$SRC/backend/drouter-shelld.py")
|
||||
SHELLD_MARK=/var/lib/drouter/shelld.deployed.sha
|
||||
|
||||
echo "=== 2. 安装后端 ==="
|
||||
install -m 0644 $SRC/backend/render.py $OPT/backend/render.py
|
||||
install -m 0644 $SRC/backend/drouter-helper.py $OPT/backend/drouter-helper.py
|
||||
install -m 0644 $SRC/backend/drouter-web.py $OPT/backend/drouter-web.py
|
||||
install -m 0644 $SRC/backend/theme.py $OPT/backend/theme.py
|
||||
# drouter-logd:统一日志采集守护。helper 写出的 drouter-logd.service 直接
|
||||
# ExecStart 这个文件,漏装会导致日志定时器起不来(此前确实漏了,务必保留)。
|
||||
install -m 0644 $SRC/backend/drouter-logd.py $OPT/backend/drouter-logd.py
|
||||
install -m 0644 $SRC/backend/drouter-snapshotd.py $OPT/backend/drouter-snapshotd.py
|
||||
install -m 0644 $SRC/backend/drouter-rescue.py $OPT/backend/drouter-rescue.py
|
||||
# drouter-shelld:Web 真·PTY 终端的会话守护。它必须由常驻进程持有 PTY
|
||||
# (helper 每次都是跑完就退出的新进程,PTY fd 跨不了请求),漏装则终端连不上。
|
||||
install -m 0644 $SRC/backend/drouter-shelld.py $OPT/backend/drouter-shelld.py
|
||||
install -m 0644 $SRC/backend/drouter-helpd.py $OPT/backend/drouter-helpd.py
|
||||
|
||||
# 主题之家(#12):自定义主题目录 + 生效主题标记
|
||||
mkdir -p /etc/drouter/themes
|
||||
echo "default" > /etc/drouter/active-theme
|
||||
chmod 644 /etc/drouter/active-theme
|
||||
|
||||
echo "=== 3. 安装前端 ==="
|
||||
install -m 0644 $SRC/web/index.html $OPT/web/index.html
|
||||
install -m 0644 $SRC/web/app.css $OPT/web/app.css
|
||||
install -m 0644 $SRC/web/app.js $OPT/web/app.js
|
||||
if [ -f "$SRC/web/logo.svg" ]; then
|
||||
install -m 0644 $SRC/web/logo.svg $OPT/web/logo.svg
|
||||
fi
|
||||
|
||||
echo "=== 3.5 安装运维脚本(自检/回归用)==="
|
||||
if [ -d "$SRC/scripts" ]; then
|
||||
install -d -m 0755 $OPT/scripts
|
||||
for s in "$SRC"/scripts/*.sh; do
|
||||
[ -f "$s" ] || continue
|
||||
install -m 0755 "$s" "$OPT/scripts/$(basename "$s")"
|
||||
done
|
||||
echo "已安装脚本:$(ls $OPT/scripts | tr '\n' ' ')"
|
||||
fi
|
||||
|
||||
echo "=== 3.6 安装文档(systemd unit 的 Documentation= 指向这里)==="
|
||||
if [ -d "$SRC/docs" ]; then
|
||||
for d in "$SRC"/docs/*; do
|
||||
[ -f "$d" ] || continue
|
||||
install -m 0644 "$d" "$OPT/docs/$(basename "$d")"
|
||||
done
|
||||
echo "已安装文档:$(ls $OPT/docs | tr '\n' ' ')"
|
||||
fi
|
||||
|
||||
echo "=== 4. 创建专用低权用户 drouter ==="
|
||||
if ! id drouter >/dev/null 2>&1; then
|
||||
useradd -r -s /usr/sbin/nologin -d $OPT -M drouter
|
||||
echo "已创建系统用户 drouter"
|
||||
else
|
||||
echo "用户 drouter 已存在"
|
||||
fi
|
||||
|
||||
echo "=== 5. 权限设置 ==="
|
||||
chown -R drouter:drouter $OPT/data $OPT/snapshots $OPT/certs
|
||||
chmod 750 $OPT/data $OPT/snapshots $OPT/certs
|
||||
chmod +x $OPT/backend/drouter-helper.py
|
||||
chmod +x $OPT/backend/drouter-snapshotd.py
|
||||
chmod +x $OPT/backend/drouter-rescue.py
|
||||
chown root:root $OPT/backend/drouter-helper.py
|
||||
chown -R root:root $OPT/backend
|
||||
chown -R root:root $OPT/web
|
||||
chmod 755 /var/log/drouter
|
||||
chmod 755 /etc/drouter/generated
|
||||
chown -R drouter:drouter /var/lib/drouter 2>/dev/null || true
|
||||
chmod -R 750 /var/lib/drouter 2>/dev/null || true
|
||||
|
||||
echo "=== 6. sudoers 白名单(仅放行 helper,且限定参数形态)==="
|
||||
cat > /etc/sudoers.d/drouter <<'SUDO'
|
||||
# drouter:后端仅可通过该脚本以 root 执行白名单操作
|
||||
# 参数被严格限制为 action + JSON 字符串
|
||||
drouter ALL=(root) NOPASSWD: /usr/bin/python3 /opt/drouter/backend/drouter-helper.py *
|
||||
SUDO
|
||||
chmod 0440 /etc/sudoers.d/drouter
|
||||
visudo -c -f /etc/sudoers.d/drouter >/dev/null && echo "sudoers 语法检查通过"
|
||||
|
||||
echo "=== 7. 日志轮转配置 ==="
|
||||
cat > /etc/logrotate.d/drouter <<'LR'
|
||||
/var/log/drouter/*.log /var/log/drouter/*.jsonl {
|
||||
daily
|
||||
rotate 7
|
||||
missingok
|
||||
notifempty
|
||||
compress
|
||||
delaycompress
|
||||
copytruncate
|
||||
create 0644 root root
|
||||
}
|
||||
LR
|
||||
|
||||
echo "=== 8. systemd 服务单元 ==="
|
||||
cat > /etc/systemd/system/drouter-web.service <<'UNIT'
|
||||
[Unit]
|
||||
Description=drouter 主路由管理后台
|
||||
Documentation=file:/opt/drouter/docs/
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
Before=shutdown.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=drouter
|
||||
Group=drouter
|
||||
WorkingDirectory=/opt/drouter
|
||||
# systemd 默认 PATH 不含 /usr/sbin,而 nft/dnsmasq/radvd 都在那儿。
|
||||
# 后端虽已自行归一化 PATH,这里再显式声明一次,避免任何遗漏路径静默失败。
|
||||
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
ExecStart=/usr/bin/python3 /opt/drouter/backend/drouter-web.py
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
# 资源限制:适配 2 核 / 3.9G 小内存环境
|
||||
MemoryMax=160M
|
||||
MemoryHigh=120M
|
||||
CPUQuota=40%
|
||||
Nice=5
|
||||
IOSchedulingClass=best-effort
|
||||
IOSchedulingPriority=5
|
||||
# 安全加固
|
||||
# 说明:后端以低权用户 drouter 运行,写 /etc 由 root helper 完成(白名单 action)。
|
||||
# 注意:这里必须 ProtectSystem=false —— 「依赖自检 / 一键安装」需要通过 apt/dpkg
|
||||
# 往 /usr/bin、/var/cache/apt、/var/lib/dpkg 写入;若把 /usr 挂为只读,
|
||||
# apt 会报 "只读文件系统" 而安装失败。安全边界由 helper 的 action 白名单保证:
|
||||
# 非白名单动作一律拒绝,且所有参数逐个强校验。
|
||||
NoNewPrivileges=false
|
||||
PrivateTmp=false
|
||||
ProtectSystem=false
|
||||
ProtectHome=read-only
|
||||
ReadWritePaths=/opt/drouter/data /opt/drouter/snapshots /opt/drouter/certs /var/log/drouter
|
||||
# 额外保护:这些路径不给写(即使 helper 被滥用也改不到)
|
||||
InaccessiblePaths=/boot
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=drouter-web
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
UNIT
|
||||
|
||||
echo "=== 8.5 自动快照定时器 + 紧急救援通道服务单元 ==="
|
||||
# 自动快照:默认「开启」,每 6 小时一次。真实参数由界面写入
|
||||
# /etc/drouter/snapshot.conf,helper 保存策略时会重建本单元。
|
||||
cat > /etc/systemd/system/drouter-snapshot.service <<'SVC'
|
||||
[Unit]
|
||||
Description=drouter 自动配置快照
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
Type=oneshot
|
||||
User=root
|
||||
ExecStart=/usr/bin/python3 /opt/drouter/backend/drouter-snapshotd.py
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
SVC
|
||||
|
||||
cat > /etc/systemd/system/drouter-snapshot.timer <<'TMR'
|
||||
[Unit]
|
||||
Description=drouter 自动快照定时器
|
||||
|
||||
[Timer]
|
||||
OnBootSec=10min
|
||||
OnUnitActiveSec=6h
|
||||
Persistent=true
|
||||
Unit=drouter-snapshot.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
TMR
|
||||
|
||||
# 紧急救援通道:默认「关闭」。只有用户在界面上勾选开启后,
|
||||
# helper 才会创建虚拟网卡别名并 enable/start 本服务。
|
||||
cat > /etc/systemd/system/drouter-rescue.service <<'RSC'
|
||||
[Unit]
|
||||
Description=drouter 紧急救援通道(独立于 WAN/LAN 的应急恢复入口)
|
||||
After=network.target
|
||||
Wants=network.target
|
||||
|
||||
[Service]
|
||||
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
Type=simple
|
||||
User=root
|
||||
ExecStart=/usr/bin/python3 /opt/drouter/backend/drouter-rescue.py
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
NoNewPrivileges=false
|
||||
ProtectSystem=false
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
RSC
|
||||
|
||||
# 救援通道默认关闭:不 enable、不 start(避免影响现有局域网)
|
||||
echo "救援通道单元已就位(默认关闭,需在界面显式开启)"
|
||||
|
||||
# 常驻执行守护(#7 性能优化):把「每个请求 spawn 一次 python3+sudo」的
|
||||
# 236ms 固定开销去掉。以 root 运行,但只监听 Unix socket(0660 root:drouter),
|
||||
# 不占任何 TCP 端口;drouter-web 连不上它时会自动回退到 subprocess,所以它
|
||||
# 挂掉只会变慢、不会让面板不可用。
|
||||
cat > /etc/systemd/system/drouter-helpd.service <<'HPD'
|
||||
[Unit]
|
||||
Description=drouter 特权动作常驻执行守护
|
||||
Documentation=file:/opt/drouter/docs/
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
Type=simple
|
||||
User=root
|
||||
Group=root
|
||||
# 刻意不用 RuntimeDirectory:见 drouter-shelld.service 里的说明。
|
||||
# /run/drouter 由守护自己 mkdir 并 chmod 0755(drouter-web 要能进目录连
|
||||
# helper.sock,而 socket 自身是 0660 root:drouter,权限并没有放宽)。
|
||||
ExecStart=/usr/bin/python3 /opt/drouter/backend/drouter-helpd.py
|
||||
Restart=on-failure
|
||||
RestartSec=2
|
||||
# 常驻 import 了整套 helper,比其它守护占得多;上限防内存泄漏拖垮路由器
|
||||
MemoryMax=300M
|
||||
# 故意不设 CPUQuota:它要跑格式化大硬盘这类子进程,限 CPU 会把这些操作拖慢
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=drouter-helpd
|
||||
ProtectHome=read-only
|
||||
PrivateTmp=false
|
||||
NoNewPrivileges=false
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
HPD
|
||||
echo "常驻执行守护单元已就位"
|
||||
|
||||
# Web 终端守护(#4 重做):真 PTY 会话必须由常驻进程持有。
|
||||
# 说明为什么不用 socket 激活:helper 在连不上时会自动 systemctl start 拉起,
|
||||
# 但那要等几秒;预启动能让用户点「连接」时立刻进入 shell。
|
||||
# 它以 root 运行(要开 root shell),但只监听 0600 的 Unix socket,不占任何端口。
|
||||
cat > /etc/systemd/system/drouter-shelld.service <<'SHD'
|
||||
[Unit]
|
||||
Description=drouter Web 终端 PTY 会话守护
|
||||
Documentation=file:/opt/drouter/docs/
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
Type=simple
|
||||
User=root
|
||||
Group=root
|
||||
# 刻意不用 RuntimeDirectory=drouter:systemd 会在「最后一个声明它的单元停止」
|
||||
# 时把整个目录删掉重建。两个守护共用 /run/drouter,于是 restart helpd 的瞬间
|
||||
# systemd 重建目录,把 shelld 的 shell.sock 一起删了 —— shelld 依旧 active,
|
||||
# socket 却没了,用户点终端只看到「守护未运行」。改成两个守护各自 mkdir。
|
||||
ExecStart=/usr/bin/python3 /opt/drouter/backend/drouter-shelld.py
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
# 只是转字节流,用不了多少内存;上限是防异常输出把内存吃光
|
||||
MemoryMax=80M
|
||||
CPUQuota=15%
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=drouter-shelld
|
||||
# 安全加固:不碰系统目录、不给新特权(它本身就以 root 跑,靠 socket 0600 收口)
|
||||
ProtectHome=read-only
|
||||
PrivateTmp=false
|
||||
NoNewPrivileges=false
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
SHD
|
||||
echo "Web 终端守护单元已就位"
|
||||
|
||||
echo "=== 9. 生成 isp-dns 占位文件(dnsmasq resolv-file 引用)==="
|
||||
cat > /etc/drouter/generated/isp-dns.conf <<'ISP'
|
||||
# 由 drouter 自动维护:运营商 PPPoE 下发的 DNS
|
||||
# 拨号成功后由 pppd 的 usepeerdns 写入 /etc/ppp/resolv.conf,
|
||||
# 可执行 /opt/drouter/bin/sync-isp-dns.sh 将其同步到此处。
|
||||
ISP
|
||||
|
||||
cat > $OPT/bin/sync-isp-dns.sh <<'SYNC'
|
||||
#!/bin/bash
|
||||
# 将 pppd 获取到的运营商 DNS 同步给 dnsmasq
|
||||
SRC=/etc/ppp/resolv.conf
|
||||
DST=/etc/drouter/generated/isp-dns.conf
|
||||
{
|
||||
echo "# 由 drouter 自动生成:运营商下发 DNS(来源 $SRC)"
|
||||
if [ -f "$SRC" ]; then
|
||||
grep -E '^nameserver' "$SRC" || echo "# 尚未获取到运营商 DNS"
|
||||
else
|
||||
echo "# $SRC 不存在(尚未拨号成功)"
|
||||
fi
|
||||
} > "$DST"
|
||||
chmod 644 "$DST"
|
||||
SYNC
|
||||
chmod +x $OPT/bin/sync-isp-dns.sh
|
||||
|
||||
# 存量机器迁移:/etc/drouter/rescue.conf 旧版本写成了 0600 属主 root,
|
||||
# 以 drouter 身份运行的快照读不到它,一个文件就让整个 /etc/drouter 目录
|
||||
# 进不了快照(界面却显示「已创建快照」)。这里把它放宽成 0640 属组 drouter。
|
||||
if [ -f /etc/drouter/rescue.conf ]; then
|
||||
chmod 0640 /etc/drouter/rescue.conf 2>/dev/null || true
|
||||
chgrp drouter /etc/drouter/rescue.conf 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo "=== 10. 校验 ==="
|
||||
# 逐个编译校验:缺文件直接中止部署(避免上线后才发现少装了一个守护进程)
|
||||
for f in render.py drouter-helper.py drouter-web.py drouter-logd.py \
|
||||
drouter-snapshotd.py drouter-rescue.py drouter-shelld.py \
|
||||
drouter-helpd.py theme.py; do
|
||||
p="$OPT/backend/$f"
|
||||
if [ ! -f "$p" ]; then echo "✘ 缺少文件: $p"; exit 1; fi
|
||||
python3 -c 'import ast,sys; ast.parse(open(sys.argv[1],encoding="utf-8").read())' "$p" \
|
||||
&& echo "语法OK: $p" || { echo "✘ 语法错误: $p"; exit 1; }
|
||||
done
|
||||
echo "--- 目录 ---"
|
||||
ls -la $OPT/
|
||||
echo "--- 服务文件 ---"
|
||||
systemctl daemon-reload
|
||||
systemctl is-enabled drouter-web 2>&1 || true
|
||||
echo "--- 自动快照定时器(默认开启)---"
|
||||
systemctl enable --now drouter-snapshot.timer 2>&1 | tail -2 || true
|
||||
systemctl is-active drouter-snapshot.timer 2>&1 || true
|
||||
echo "--- 紧急救援通道(默认关闭)---"
|
||||
systemctl is-active drouter-rescue 2>&1 || true
|
||||
echo "--- 磁盘与日志清理定时器(默认开启)---"
|
||||
# 单元与入口脚本由 helper 的 cleanup save 动作生成(默认参数即推荐值),
|
||||
# 这里只负责在部署时把它建立起来 —— 用户之后在界面改策略会重新生成。
|
||||
/usr/bin/python3 $OPT/backend/drouter-helper.py cleanup '{"op":"save","enabled":true}' \
|
||||
>/dev/null 2>&1 && echo "清理策略已初始化" || echo "⚠ 清理策略初始化失败(界面保存一次即可恢复)"
|
||||
systemctl is-active drouter-cleanup.timer 2>&1 || true
|
||||
echo "--- 常驻执行守护 ---"
|
||||
# 必须 restart 而不是 enable --now:守护把 helper 代码常驻在内存里,
|
||||
# 只 enable --now 的话它还在跑上一版代码,改的东西根本不生效。
|
||||
systemctl enable drouter-helpd 2>&1 | tail -1 || true
|
||||
systemctl restart drouter-helpd 2>&1 | tail -2 || true
|
||||
sleep 1
|
||||
systemctl is-active drouter-helpd 2>&1 || echo "⚠ 常驻执行守护未运行(面板会自动回退到慢路径)"
|
||||
echo "--- Web 终端守护 ---"
|
||||
# 重启 shelld 会断掉正在用的终端会话,所以默认只 ensure 起来。
|
||||
# 但两种情况必须重启:① socket 文件没了(多半是被 helpd 重启连带删掉的);
|
||||
# ② 代码确实变了 —— 不重启的话跑的还是旧代码。
|
||||
mkdir -p /var/lib/drouter
|
||||
SHELLD_SEEN=$(cat "$SHELLD_MARK" 2>/dev/null || echo "-")
|
||||
if [ ! -S /run/drouter/shell.sock ] || [ "$SHELLD_NEW" != "$SHELLD_SEEN" ]; then
|
||||
echo "终端守护代码有更新($SHELLD_SEEN → $SHELLD_NEW),重启以生效"
|
||||
systemctl restart drouter-shelld 2>&1 | tail -2 || true
|
||||
# 只有真的起来了才更新 mark,否则下一轮还会重试重启
|
||||
if systemctl is-active --quiet drouter-shelld; then
|
||||
echo "$SHELLD_NEW" > "$SHELLD_MARK"
|
||||
else
|
||||
echo "⚠ 终端守护未能启动,mark 不更新(下一轮部署会再试)"
|
||||
fi
|
||||
else
|
||||
systemctl enable --now drouter-shelld 2>&1 | tail -1 || true
|
||||
echo "$SHELLD_NEW" > "$SHELLD_MARK"
|
||||
fi
|
||||
systemctl is-active drouter-shelld 2>&1 || true
|
||||
# socket 必须真的存在:systemd 说 active 不代表 socket 已经在盘上
|
||||
# (restart 是异步的,守护起来后还要自己 mkdir + bind,得给它几秒)。
|
||||
for _i in 1 2 3 4 5 6 7 8 9 10; do
|
||||
[ -S /run/drouter/shell.sock ] && break
|
||||
sleep 1
|
||||
done
|
||||
[ -S /run/drouter/shell.sock ] && echo "终端 socket 就位" \
|
||||
|| echo "⚠ 终端 socket 缺失,Web 终端将不可用"
|
||||
echo "--- 网络与 VNC 复核 ---"
|
||||
ip -4 -o addr show ens18 2>/dev/null || echo "(ens18 不存在,跳过)"
|
||||
ss -lnt | grep 5900 || echo "5900 异常"
|
||||
|
||||
# 重启管理后台,让新代码真正生效。
|
||||
# 必须放在语法校验之后:一旦新代码有语法错误,宁可继续跑旧进程也不能把自己关在门外。
|
||||
echo "--- 重启管理后台 ---"
|
||||
systemctl restart $UNIT_WEB 2>&1 | tail -3 || echo "(重启失败,请手动检查)"
|
||||
sleep 2
|
||||
systemctl is-active $UNIT_WEB || echo "⚠ drouter-web 未在运行"
|
||||
|
||||
# 统一日志采集定时器:默认配置是 enabled=true,但 service/timer 单元只有
|
||||
# 在界面里点过一次「保存日志设置」才会被 helper 写出来。全新部署后防火墙日志
|
||||
# 页面会一直是空的,表现为「日志功能没做」。这里补一次同步,让默认开关真正生效。
|
||||
echo "--- 同步日志采集定时器 ---"
|
||||
python3 - <<'PY' || echo "(日志定时器同步失败,可在界面「日志设置」里保存一次重建)"
|
||||
# 文件名带连字符,不能用普通 import,走 importlib 按路径加载
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
'drouter_helper', '/opt/drouter/backend/drouter-helper.py')
|
||||
h = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(h)
|
||||
r = h.act_logd_sync({'interval_sec': 60})
|
||||
print(r.get('msg_cn') or r)
|
||||
PY
|
||||
systemctl is-active drouter-logd.timer 2>&1 || true
|
||||
|
||||
echo "DEPLOY_DONE"
|
||||
@@ -0,0 +1,424 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# drouter 一键运维脚本 —— 安装 / 卸载 / 修复 / 更新 / 状态
|
||||
#
|
||||
# drouter-ctl.sh install 交互式安装(询问网卡、LAN IP/网段、网关、DNS、管理员账号)
|
||||
# drouter-ctl.sh uninstall 卸载(保留配置与快照;加 --purge 彻底清除)
|
||||
# drouter-ctl.sh repair 修复(重部署文件 + 修权限 + 重建单元 + 校验)
|
||||
# drouter-ctl.sh update 更新(更新代码,保留配置与主题)
|
||||
# drouter-ctl.sh status 状态(服务/端口/版本/配置摘要/自检)
|
||||
#
|
||||
# 安全红线(任何时候都不得违反):
|
||||
# 1. 绝不启动/启用 dnsmasq、radvd、miniupnpd 等会抢端口或发 DHCP 的服务
|
||||
# 2. 绝不触碰 RealVNC(5900)
|
||||
# 3. /etc/drouter/BUILD_MODE 存在时(构建保护模式),任何写网络动作一律中止
|
||||
# 4. 卸载默认保留 /etc/drouter 配置与快照,避免用户配置被误删
|
||||
# =============================================================================
|
||||
set -u
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
OPT=/opt/drouter
|
||||
SRC_DEFAULT=/tmp/drouter-src
|
||||
DB=$OPT/data/drouter.db
|
||||
UNIT_WEB=drouter-web
|
||||
UNIT_SNAP=drouter-snapshot.timer
|
||||
UNIT_LOGD=drouter-logd.timer
|
||||
UNIT_RESCUE=drouter-rescue
|
||||
UNIT_SHELLD=drouter-shelld
|
||||
UNIT_HELPD=drouter-helpd
|
||||
|
||||
C_RESET=$'\033[0m'; C_RED=$'\033[31m'; C_GRN=$'\033[32m'; C_YEL=$'\033[33m'; C_DIM=$'\033[2m'
|
||||
say() { printf '%s\n' "$*"; }
|
||||
ok() { printf '%s✔%s %s\n' "$C_GRN" "$C_RESET" "$*"; }
|
||||
warn() { printf '%s⚠%s %s\n' "$C_YEL" "$C_RESET" "$*"; }
|
||||
err() { printf '%s✘%s %s\n' "$C_RED" "$C_RESET" "$*" >&2; }
|
||||
step() { printf '\n%s==>%s %s\n' "$C_DIM" "$C_RESET" "$*"; }
|
||||
die() { err "$*"; exit 1; }
|
||||
|
||||
# ---------------------------------------------------------------- 前置检查
|
||||
need_root() {
|
||||
[ "$(id -u)" = 0 ] || die "请以 root 运行(sudo bash $0 $1)"
|
||||
}
|
||||
# 构建保护模式:存在该文件时禁止一切可能改网络的动作
|
||||
guard_build_mode() {
|
||||
if [ -f /etc/drouter/BUILD_MODE ]; then
|
||||
die "检测到构建保护模式 /etc/drouter/BUILD_MODE,已中止。
|
||||
如需继续,请先确认当前不在构建阶段,然后:rm -f /etc/drouter/BUILD_MODE"
|
||||
fi
|
||||
}
|
||||
# 安全红线自检:本脚本绝不去动这些服务/端口
|
||||
assert_no_network_daemons() {
|
||||
local bad=""
|
||||
for s in dnsmasq radvd miniupnpd dhcpcd isc-dhcp-server; do
|
||||
if systemctl is-enabled "$s" 2>/dev/null | grep -qx enabled; then bad="$bad $s"; fi
|
||||
done
|
||||
[ -z "$bad" ] || warn "以下服务当前是 enabled 状态:$bad(本脚本不会启用它们,请自行确认)"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- 输入校验
|
||||
valid_ipv4() {
|
||||
python3 - "$1" <<'PY'
|
||||
import sys, ipaddress
|
||||
try:
|
||||
ipaddress.IPv4Address(sys.argv[1]); print('yes')
|
||||
except Exception:
|
||||
print('no')
|
||||
PY
|
||||
}
|
||||
valid_cidr() {
|
||||
python3 - "$1" <<'PY'
|
||||
import sys, ipaddress
|
||||
try:
|
||||
ipaddress.IPv4Network(sys.argv[1], strict=False); print('yes')
|
||||
except Exception:
|
||||
print('no')
|
||||
PY
|
||||
}
|
||||
# 由网段推导:网络地址、掩码、可用主机范围
|
||||
net_info() {
|
||||
python3 - "$1" <<'PY'
|
||||
import sys, ipaddress
|
||||
n = ipaddress.IPv4Network(sys.argv[1], strict=False)
|
||||
hosts = list(n.hosts())
|
||||
gw = str(hosts[0]) if hosts else str(n.network_address)
|
||||
p1 = str(hosts[min(10, len(hosts)-1)]) if hosts else ''
|
||||
p2 = str(hosts[-1]) if hosts else ''
|
||||
print(' '.join([str(n.network_address), str(n.netmask), str(n.broadcast_address), gw, p1, p2]))
|
||||
PY
|
||||
}
|
||||
ask() { # ask 变量名 提示 默认值
|
||||
local var="$1" prompt="$2" def="$3" val
|
||||
if [ -n "${NONINTERACTIVE:-}" ]; then val="$def"
|
||||
else
|
||||
read -r -p "$(printf '%s%s%s [%s]: ' "$C_DIM" "$prompt" "$C_RESET" "$def")" val
|
||||
val="${val:-$def}"
|
||||
fi
|
||||
printf -v "$var" '%s' "$val"
|
||||
}
|
||||
ask_secret() { # 密码类:不回显
|
||||
local var="$1" prompt="$2" val
|
||||
if [ -n "${NONINTERACTIVE:-}" ]; then val="${3:-}"; printf -v "$var" '%s' "$val"; return; fi
|
||||
read -r -s -p "$(printf '%s%s%s: ' "$C_DIM" "$prompt" "$C_RESET")" val; echo
|
||||
printf -v "$var" '%s' "$val"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
cmd_install() {
|
||||
need_root install
|
||||
guard_build_mode
|
||||
local SRC="${1:-$SRC_DEFAULT}"
|
||||
[ -d "$SRC/backend" ] || die "源码目录不存在:$SRC(请先同步源码到该目录)"
|
||||
|
||||
step "1/7 收集安装参数"
|
||||
# 自动探测候选网卡(排除 lo / 虚拟口)
|
||||
local ifaces
|
||||
ifaces=$(ls /sys/class/net 2>/dev/null | grep -vE '^(lo|docker|br-|veth|virbr|ppp)' | tr '\n' ' ')
|
||||
say " 可用网卡:$ifaces"
|
||||
ask LAN_IFACE "LAN 网卡(内网)" "$(echo "$ifaces" | awk '{print $1}')"
|
||||
ask WAN_IFACE "WAN 网卡(外网,可留空稍后在界面选)" ""
|
||||
ask LAN_CIDR "LAN 地址(IP/掩码位数)" "192.168.7.3/24"
|
||||
valid_cidr "$LAN_CIDR" | grep -qx yes || die "LAN 地址格式不对:$LAN_CIDR(示例 192.168.7.3/24)"
|
||||
|
||||
read -r _NET _MASK _BC _GW _P1 _P2 <<<"$(net_info "$LAN_CIDR")"
|
||||
say " ${C_DIM}网段 ${_NET} 掩码 ${_MASK} 可用 ${_P1} ~ ${_P2}${C_RESET}"
|
||||
ask GATEWAY "本机 LAN 网关(路由器自身地址,通常就是 LAN IP)" "${LAN_CIDR%%/*}"
|
||||
valid_ipv4 "$GATEWAY" | grep -qx yes || die "网关地址不合法:$GATEWAY"
|
||||
ask DNS_LIST "DNS(逗号分隔)" "223.5.5.5,119.29.29.29"
|
||||
ask DHCP_ON "是否开启 DHCP 地址池?(y/N)" "N"
|
||||
POOL_START=""; POOL_END=""
|
||||
if [ "${DHCP_ON}" = "y" ] || [ "${DHCP_ON}" = "Y" ]; then
|
||||
ask POOL_START "地址池起始" "${_P1:-192.168.7.100}"
|
||||
ask POOL_END "地址池结束" "${_P2:-192.168.7.200}"
|
||||
valid_ipv4 "$POOL_START" | grep -qx yes || die "地址池起始不合法"
|
||||
valid_ipv4 "$POOL_END" | grep -qx yes || die "地址池结束不合法"
|
||||
fi
|
||||
ask WEB_PORT "Web 管理端口" "8443"
|
||||
ask ADM_USER "Web 管理员用户名" "admin"
|
||||
ask_secret ADM_PASS "Web 管理员密码(至少 6 位,留空则沿用 admin123)" ""
|
||||
if [ -n "$ADM_PASS" ] && [ "${#ADM_PASS}" -lt 6 ]; then die "密码至少 6 位"; fi
|
||||
ask PPP_USER "PPPoE 宽带账号(可留空)" ""
|
||||
PPP_PASS=""
|
||||
if [ -n "$PPP_USER" ]; then ask_secret PPP_PASS "PPPoE 宽带密码" ""; fi
|
||||
|
||||
step "2/7 安装系统依赖(进入构建保护模式,不会启动任何服务)"
|
||||
bash "$SRC/scripts/install-base.sh" 2>&1 | tail -12
|
||||
|
||||
step "3/7 部署程序文件"
|
||||
DROUTER_SRC="$SRC" bash "$SRC/scripts/deploy.sh" 2>&1 | tail -20
|
||||
|
||||
step "4/7 启动管理后台(只启动 drouter-web,绝不启动 DHCP/DNS)"
|
||||
# 自定义端口必须写进 /etc/drouter/web-port 才会生效(后端启动时读取)
|
||||
if [ "$WEB_PORT" != "8443" ]; then
|
||||
mkdir -p /etc/drouter
|
||||
printf '%s\n' "$WEB_PORT" > /etc/drouter/web-port
|
||||
ok "Web 端口已设为 $WEB_PORT"
|
||||
fi
|
||||
systemctl daemon-reload
|
||||
systemctl enable "$UNIT_WEB" >/dev/null 2>&1 || true
|
||||
systemctl restart "$UNIT_WEB"
|
||||
# 等待端口就绪
|
||||
local i=0
|
||||
while [ $i -lt 30 ]; do
|
||||
if ss -lnt 2>/dev/null | grep -q ":${WEB_PORT}\b"; then break; fi
|
||||
sleep 1; i=$((i+1))
|
||||
done
|
||||
ss -lnt 2>/dev/null | grep -q ":${WEB_PORT}\b" \
|
||||
&& ok "管理后台已监听 :${WEB_PORT}" \
|
||||
|| { systemctl status "$UNIT_WEB" --no-pager -n 20; die "管理后台未能启动"; }
|
||||
|
||||
step "5/7 写入预置配置(走官方 API,保证与界面同一套校验)"
|
||||
local pw="${ADM_PASS:-admin123}"
|
||||
LOGIN_USER="$ADM_USER" LOGIN_PASS="$pw" PORT="$WEB_PORT" \
|
||||
LAN_IFACE="$LAN_IFACE" WAN_IFACE="$WAN_IFACE" LAN_CIDR="$LAN_CIDR" \
|
||||
GATEWAY="$GATEWAY" DNS_LIST="$DNS_LIST" POOL_START="$POOL_START" POOL_END="$POOL_END" \
|
||||
PPP_USER="$PPP_USER" PPP_PASS="$PPP_PASS" \
|
||||
python3 - <<'PY'
|
||||
import json, os, ssl, urllib.request, urllib.error, sys
|
||||
|
||||
CTX = ssl._create_unverified_context() # 自签证书,仅本机回环调用
|
||||
# 优先 HTTPS(自定义端口),失败回退 HTTP 8080(后端同时监听两者)
|
||||
CANDIDATES = ['https://127.0.0.1:%s' % os.environ['PORT'], 'http://127.0.0.1:8080']
|
||||
BASE = None
|
||||
|
||||
def call(path, payload=None, token=None):
|
||||
global BASE
|
||||
bases = [BASE] if BASE else CANDIDATES
|
||||
last = {'ok': False, 'msg_cn': '无可用地址'}
|
||||
for b in bases:
|
||||
data = json.dumps(payload or {}).encode('utf-8')
|
||||
req = urllib.request.Request(b + path, data=data, method='POST')
|
||||
req.add_header('Content-Type', 'application/json')
|
||||
if token: req.add_header('X-Token', token)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=20, context=CTX) as r:
|
||||
BASE = b
|
||||
return json.loads(r.read().decode('utf-8'))
|
||||
except urllib.error.HTTPError as e:
|
||||
try:
|
||||
BASE = b
|
||||
return json.loads(e.read().decode('utf-8'))
|
||||
except Exception:
|
||||
last = {'ok': False, 'msg_cn': 'HTTP %s' % e.code}
|
||||
except Exception as e:
|
||||
last = {'ok': False, 'msg_cn': str(e)}
|
||||
return last
|
||||
|
||||
r = call('/api/login', {'username': os.environ['LOGIN_USER'],
|
||||
'password': os.environ['LOGIN_PASS']})
|
||||
if not r.get('ok'):
|
||||
print(' 登录失败:%s' % r.get('msg_cn')); sys.exit(1)
|
||||
tok = (r.get('data') or {}).get('token')
|
||||
print(' 已登录(%s)' % BASE)
|
||||
|
||||
lan = os.environ['LAN_IFACE']; wan = os.environ['WAN_IFACE']
|
||||
patch = {
|
||||
'system': {'lan_iface': lan, 'lan_address': os.environ['LAN_CIDR'],
|
||||
'gateway': os.environ['GATEWAY'], 'wan_iface': wan,
|
||||
'wan_present': bool(wan)},
|
||||
'nft_v4': {'lan_ifaces': [lan], 'wan_iface': wan or 'ppp0'},
|
||||
'nft_v6': {'lan_ifaces': [lan], 'wan_iface': wan or 'ppp0'},
|
||||
'radvd': {'iface': lan},
|
||||
'dhcpv6': {'lan_iface': lan},
|
||||
'dnsmasq': {'option_gateway': os.environ['GATEWAY'],
|
||||
'option_dns': os.environ['GATEWAY'],
|
||||
'dns_custom': os.environ['DNS_LIST'],
|
||||
'dhcp_enabled': bool(os.environ.get('POOL_START'))},
|
||||
}
|
||||
if os.environ.get('POOL_START'):
|
||||
patch['dnsmasq'].update({'pool_start': os.environ['POOL_START'],
|
||||
'pool_end': os.environ['POOL_END'],
|
||||
'pool_netmask': '255.255.255.0'})
|
||||
if os.environ.get('PPP_USER'):
|
||||
patch['pppoe'] = {'username': os.environ['PPP_USER'],
|
||||
'password': os.environ.get('PPP_PASS', ''),
|
||||
'iface': wan or '', 'saved_only': True}
|
||||
for mod, data in patch.items():
|
||||
rr = call('/api/config', {'module': mod, 'data': data}, tok)
|
||||
print(' %-8s %s' % (mod, '已保存' if rr.get('ok') else '失败:%s' % rr.get('msg_cn')))
|
||||
PY
|
||||
|
||||
step "6/7 安全红线复核"
|
||||
assert_no_network_daemons
|
||||
if ss -lnt 2>/dev/null | grep -q ':5900\b'; then ok "RealVNC 5900 仍在监听(未被影响)"
|
||||
else warn "5900 未监听 —— 请确认 RealVNC 状态(本脚本未做任何操作)"; fi
|
||||
[ -f /etc/drouter/BUILD_MODE ] && rm -f /etc/drouter/BUILD_MODE && ok "已退出构建保护模式"
|
||||
|
||||
step "7/7 完成"
|
||||
local ip="${LAN_CIDR%%/*}"
|
||||
ok "安装完成"
|
||||
say " 管理地址:https://${ip}:${WEB_PORT}/"
|
||||
say " 管理员:${ADM_USER} / ${ADM_PASS:+(已设置)}${ADM_PASS:-admin123(请尽快修改)}"
|
||||
say ""
|
||||
say " ${C_DIM}提示:以上只写入了配置,尚未应用到系统。${C_RESET}"
|
||||
say " ${C_DIM}请在界面的「应用配置」里确认后再下发,避免影响现有网络。${C_RESET}"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
cmd_uninstall() {
|
||||
need_root uninstall
|
||||
guard_build_mode
|
||||
local PURGE=0
|
||||
[ "${1:-}" = "--purge" ] && PURGE=1
|
||||
|
||||
step "停止并禁用服务"
|
||||
for u in $UNIT_RESCUE $UNIT_WEB $UNIT_SNAP $UNIT_LOGD $UNIT_SHELLD $UNIT_HELPD; do
|
||||
systemctl stop "$u" 2>/dev/null || true
|
||||
systemctl disable "$u" 2>/dev/null || true
|
||||
done
|
||||
systemctl daemon-reload
|
||||
|
||||
step "移除 systemd 单元与系统配置"
|
||||
rm -f /etc/systemd/system/drouter-*.service /etc/systemd/system/drouter-*.timer
|
||||
rm -f /etc/sudoers.d/drouter /etc/logrotate.d/drouter
|
||||
systemctl daemon-reload
|
||||
|
||||
step "移除程序目录"
|
||||
rm -rf "$OPT"
|
||||
|
||||
if [ "$PURGE" = 1 ]; then
|
||||
warn "--purge:同时删除配置、快照与日志"
|
||||
rm -rf /etc/drouter /var/log/drouter /var/lib/drouter /run/drouter-helper
|
||||
rm -f /etc/drouter/generated/theme.css
|
||||
ok "已彻底清除(含用户配置与快照)"
|
||||
else
|
||||
ok "已卸载;保留 /etc/drouter(配置与主题)、/var/log/drouter(日志)"
|
||||
say " ${C_DIM}如需彻底清除:bash $0 uninstall --purge${C_RESET}"
|
||||
fi
|
||||
# 顺带处理 apt 装的包?不动 —— 卸载程序不应卸载用户可能共用的系统包
|
||||
say " ${C_DIM}注:系统软件包(dnsmasq/nftables 等)未被移除,避免影响其它服务。${C_RESET}"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
cmd_repair() {
|
||||
need_root repair
|
||||
guard_build_mode
|
||||
local SRC="${1:-$SRC_DEFAULT}"
|
||||
[ -d "$SRC/backend" ] || die "源码目录不存在:$SRC"
|
||||
|
||||
step "1/5 重新部署文件(覆盖,不影响已有配置)"
|
||||
DROUTER_SRC="$SRC" bash "$SRC/scripts/deploy.sh" 2>&1 | tail -12
|
||||
|
||||
step "2/5 修复权限与属主"
|
||||
id drouter >/dev/null 2>&1 || useradd -r -s /usr/sbin/nologin -d "$OPT" -M drouter
|
||||
chown -R root:root "$OPT/backend" "$OPT/web"
|
||||
chown -R drouter:drouter "$OPT/data" "$OPT/snapshots" "$OPT/certs" 2>/dev/null || true
|
||||
chmod 750 "$OPT/data" "$OPT/snapshots" "$OPT/certs" 2>/dev/null || true
|
||||
chmod 700 /run/drouter-helper 2>/dev/null || true
|
||||
chmod 0440 /etc/sudoers.d/drouter
|
||||
visudo -c -f /etc/sudoers.d/drouter >/dev/null && ok "sudoers 语法正确"
|
||||
|
||||
step "3/5 重建服务单元并重启管理后台"
|
||||
systemctl daemon-reload
|
||||
systemctl enable $UNIT_WEB >/dev/null 2>&1 || true
|
||||
systemctl restart $UNIT_WEB
|
||||
systemctl enable $UNIT_SNAP >/dev/null 2>&1 || true
|
||||
|
||||
step "4/5 校验关键文件"
|
||||
local bad=0
|
||||
for f in render.py drouter-helper.py drouter-web.py drouter-logd.py \
|
||||
drouter-snapshotd.py drouter-rescue.py drouter-shelld.py \
|
||||
drouter-helpd.py theme.py; do
|
||||
[ -f "$OPT/backend/$f" ] || { err "缺少 $f"; bad=1; }
|
||||
done
|
||||
[ -d /etc/drouter/themes ] || { mkdir -p /etc/drouter/themes; ok "已补建主题目录"; }
|
||||
[ "$bad" = 0 ] && ok "8 个后端模块齐全"
|
||||
|
||||
step "5/5 复核安全红线"
|
||||
assert_no_network_daemons
|
||||
ok "修复完成"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
cmd_update() {
|
||||
need_root update
|
||||
guard_build_mode
|
||||
local SRC="${1:-$SRC_DEFAULT}"
|
||||
[ -d "$SRC/backend" ] || die "源码目录不存在:$SRC"
|
||||
|
||||
step "1/4 更新前自动快照(便于回滚)"
|
||||
if [ -f "$OPT/backend/drouter-snapshotd.py" ]; then
|
||||
python3 "$OPT/backend/drouter-snapshotd.py" >/dev/null 2>&1 \
|
||||
&& ok "快照已生成" || warn "快照失败(继续更新)"
|
||||
fi
|
||||
|
||||
step "2/4 备份当前版本"
|
||||
local ts; ts=$(date +%Y%m%d-%H%M%S)
|
||||
local BAK="/opt/drouter-backup-$ts"
|
||||
mkdir -p "$BAK"
|
||||
cp -a "$OPT/backend" "$BAK/" 2>/dev/null || true
|
||||
cp -a "$OPT/web" "$BAK/" 2>/dev/null || true
|
||||
ok "已备份到 $BAK"
|
||||
|
||||
step "3/4 部署新版本(配置与主题不受影响)"
|
||||
DROUTER_SRC="$SRC" bash "$SRC/scripts/deploy.sh" 2>&1 | tail -12
|
||||
|
||||
step "4/4 重启并校验"
|
||||
systemctl daemon-reload
|
||||
systemctl restart $UNIT_WEB
|
||||
sleep 2
|
||||
systemctl is-active $UNIT_WEB | grep -qx active \
|
||||
&& ok "管理后台已重启" || { systemctl status $UNIT_WEB --no-pager -n 15; die "启动失败,可回滚:$BAK"; }
|
||||
ok "更新完成(回滚:把 $BAK 里的目录拷回 $OPT 后 restart)"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
cmd_status() {
|
||||
step "服务"
|
||||
printf ' %-24s %-10s %s\n' "单元" "活动" "开机自启"
|
||||
for u in $UNIT_WEB $UNIT_SNAP $UNIT_LOGD $UNIT_RESCUE $UNIT_SHELLD $UNIT_HELPD; do
|
||||
printf ' %-24s %-10s %s\n' "$u" \
|
||||
"$(systemctl is-active "$u" 2>/dev/null)" "$(systemctl is-enabled "$u" 2>/dev/null)"
|
||||
done
|
||||
|
||||
step "安装"
|
||||
if [ -d "$OPT" ]; then
|
||||
ok "程序目录:$OPT"
|
||||
local n=0
|
||||
for f in "$OPT"/backend/*.py; do [ -f "$f" ] && n=$((n+1)); done
|
||||
say " 后端模块:$n 个"
|
||||
[ -f "$DB" ] && say " 配置库:$(du -h "$DB" | cut -f1)" || warn "配置库不存在(可能尚未启动过)"
|
||||
else
|
||||
warn "未安装($OPT 不存在)"
|
||||
fi
|
||||
|
||||
step "端口"
|
||||
ss -lntup 2>/dev/null | grep -E ':(8443|53|67|546|5900)\b' | sed 's/^/ /' || say " (无相关端口)"
|
||||
|
||||
step "主题之家"
|
||||
if [ -f /etc/drouter/active-theme ]; then
|
||||
say " 当前主题:$(cat /etc/drouter/active-theme)"
|
||||
say " 自定义主题数:$(ls -1 /etc/drouter/themes 2>/dev/null | wc -l)"
|
||||
else warn " 主题状态文件不存在"; fi
|
||||
|
||||
step "安全红线"
|
||||
if [ -f /etc/drouter/BUILD_MODE ]; then warn "构建保护模式:开启(写网络操作被禁止)"
|
||||
else say " 构建保护模式:关闭"; fi
|
||||
ss -lnt 2>/dev/null | grep -q ':5900\b' && say " RealVNC 5900:正常监听" || say " RealVNC 5900:未监听"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
drouter 一键运维脚本
|
||||
|
||||
用法:drouter-ctl.sh <命令> [参数]
|
||||
|
||||
install [源码目录] 交互式安装(询问网卡 / LAN IP 网段 / 网关 / DNS / 管理员账号)
|
||||
非交互可用环境变量 NONINTERACTIVE=1 并配合默认值
|
||||
uninstall [--purge] 卸载。默认保留 /etc/drouter 配置与日志;--purge 彻底清除
|
||||
repair [源码目录] 修复:重部署 + 修权限 + 重建单元 + 校验
|
||||
update [源码目录] 更新:先快照备份,再部署新版本,配置与主题保留
|
||||
status 状态:服务 / 端口 / 版本 / 主题 / 安全红线
|
||||
|
||||
安全约束:本脚本绝不启用 dnsmasq/radvd 等会抢端口的服务,绝不触碰 RealVNC 5900。
|
||||
USAGE
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
install) shift; cmd_install "$@" ;;
|
||||
uninstall) shift; cmd_uninstall "$@" ;;
|
||||
repair) shift; cmd_repair "$@" ;;
|
||||
update) shift; cmd_update "$@" ;;
|
||||
status) shift; cmd_status "$@" ;;
|
||||
*) usage; exit 1 ;;
|
||||
esac
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/bin/bash
|
||||
# Debian13 主路由系统 —— 底座安装脚本 v2(干净环境版)
|
||||
#
|
||||
# 安全约束(重要):
|
||||
# 1. 安装期间禁止任何服务自动启动(policy-rc.d 返回 101)
|
||||
# 2. 不触碰 NetworkManager(ens18 的网络保持原样)
|
||||
# 3. 不触碰 RealVNC / LightDM / X 会话
|
||||
# 4. 不加载任何 nftables 生效规则
|
||||
set -e
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
MARK=/etc/drouter-build-phase
|
||||
echo "=== 阶段 1/6:进入构建保护模式 ==="
|
||||
mkdir -p "$(dirname "$MARK")"
|
||||
printf 'build' > "$MARK"
|
||||
|
||||
# 阻止一切服务在安装期间自动启动/重启
|
||||
printf '#!/bin/sh\nexit 101\n' > /usr/sbin/policy-rc.d
|
||||
chmod +x /usr/sbin/policy-rc.d
|
||||
|
||||
# 记录 RealVNC 状态用于事后校验
|
||||
echo "--- 安装前 RealVNC 状态 ---"
|
||||
systemctl is-active vncserver-x11-serviced 2>/dev/null || true
|
||||
ss -lnt 2>/dev/null | grep ':5900' || echo "5900 未监听"
|
||||
echo "--- 安装前网络 ---"
|
||||
ip -4 -o addr show ens18 2>/dev/null || echo "(ens18 不存在,跳过)"
|
||||
|
||||
echo "=== 阶段 2/6:apt update ==="
|
||||
apt-get update -qq 2>&1 | tail -3
|
||||
|
||||
echo "=== 阶段 3/6:安装软件包 ==="
|
||||
# 注意:这里刻意不含 samba / nfs-kernel-server / docker.io ——
|
||||
# 它们装完会自动拉起常驻服务,属于用户在界面上按需安装的组件,
|
||||
# 不应在「装底座」这一步就被带起来。
|
||||
PACKAGES="
|
||||
ppp pppoe pppoeconf
|
||||
dnsmasq radvd
|
||||
nftables
|
||||
miniupnpd chrony
|
||||
ethtool iperf3 mtr-tiny traceroute dnsutils conntrack
|
||||
bridge-utils vlan iproute2
|
||||
sqlite3 logrotate rsyslog
|
||||
curl wget
|
||||
python3
|
||||
exfatprogs ntfs-3g dosfstools xfsprogs btrfs-progs f2fs-tools
|
||||
etherwake zip unzip p7zip-full iputils-ping vim-tiny
|
||||
"
|
||||
apt-get install -y --no-install-recommends $PACKAGES 2>&1 | tail -25
|
||||
|
||||
echo "=== 阶段 4/6:确保新装服务处于停止+禁用 ==="
|
||||
for s in dnsmasq radvd dhcpcd miniupnpd chrony rsyslog; do
|
||||
systemctl stop "$s" 2>/dev/null || true
|
||||
systemctl disable "$s" 2>/dev/null || true
|
||||
done
|
||||
|
||||
echo "=== 阶段 5/6:退出保护模式 ==="
|
||||
rm -f /usr/sbin/policy-rc.d
|
||||
|
||||
echo "=== 阶段 6/6:校验 ==="
|
||||
echo "--- RealVNC 状态 ---"
|
||||
systemctl is-active vncserver-x11-serviced 2>/dev/null || true
|
||||
ss -lnt 2>/dev/null | grep ':5900' || echo "5900 未监听(异常!)"
|
||||
echo "--- 网络(应与安装前一致)---"
|
||||
ip -4 -o addr show ens18 2>/dev/null || echo "(ens18 不存在,跳过)"
|
||||
echo "--- 服务状态 ---"
|
||||
for s in dnsmasq radvd miniupnpd chrony NetworkManager; do
|
||||
printf "%-16s active=%-10s enabled=%s\n" "$s" \
|
||||
"$(systemctl is-active $s 2>/dev/null)" "$(systemctl is-enabled $s 2>/dev/null)"
|
||||
done
|
||||
echo "--- 端口占用 ---"
|
||||
ss -lntup 2>/dev/null | grep -E ':(53|67|546|5900)\b' || echo "(53/67/546 均未被占用,符合预期)"
|
||||
echo "INSTALL_DONE"
|
||||
@@ -0,0 +1,324 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# 离线依赖打包:把 drouter 的全部依赖(含传递依赖)预下载成一个自洽目录,
|
||||
# 供**没有外网**的目标机离线安装。
|
||||
#
|
||||
# bash scripts/make-offline-deps.sh [输出目录]
|
||||
#
|
||||
# 产出目录结构:
|
||||
# debs/ 所有依赖的 .deb(含 libc6 这类最底层传递依赖)
|
||||
# Packages.gz 由 dpkg-scanpackages 生成的本地索引(没有就退化为无索引模式)
|
||||
# install.sh 离线安装脚本(先装依赖,再装 drouter 本体)
|
||||
#
|
||||
# 为什么不能只 apt-get download 一串包名:
|
||||
# `apt-get download a b c` 只下载**指定**的那些包,不拉传递依赖。
|
||||
# 目标机上 dpkg -i *.deb 会因为 libxxx 没装而大面积失败。必须先把
|
||||
# apt-cache depends --recurse 展开成闭包,再整体下载。
|
||||
#
|
||||
# 为什么用 apt-cache depends 而不是 apt-rdepends:
|
||||
# apt-rdepends 不是 Debian 基础系统自带的(要额外装),而 apt-cache 一定有。
|
||||
# `apt-cache depends --recurse --no-recommends --no-suggests ...` 的输出
|
||||
# 已经能拿到完整闭包,不必引入外部依赖。
|
||||
#
|
||||
# 用法(目标机):
|
||||
# 把整个目录拷过去,执行
|
||||
# sudo bash install.sh drouter_1.0.0_all.deb
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
OUT="${1:-$HERE/dist/offline-deps}"
|
||||
|
||||
# 与 backend/drouter-helper.py 的 DEPS「必需项 + 常用可选项」保持一致。
|
||||
# 手动同步点:改了 DEPS 就要同步这里,_dev/t-deps.py 会核对关键包是否都在。
|
||||
# 注意:samba / nfs-kernel-server / docker.io / cups / snmpd / smartmontools / xfce4
|
||||
# 会拉起常驻服务或体积巨大,不进离线包,用户在界面上按需安装即可。
|
||||
DEPS="python3 nftables dnsmasq radvd dhcpcd-base ppp pppoe pppoeconf miniupnpd chrony
|
||||
ethtool iperf3 mtr-tiny traceroute dnsutils conntrack bridge-utils iproute2
|
||||
sqlite3 logrotate rsyslog curl wget procps sudo openssl ipset kmod systemd
|
||||
adduser ca-certificates tcpdump jq htop lm-sensors dmidecode
|
||||
e2fsprogs exfatprogs ntfs-3g dosfstools xfsprogs btrfs-progs f2fs-tools
|
||||
util-linux usbutils tar coreutils hostname udev
|
||||
etherwake zip unzip p7zip-full iputils-ping vim-tiny"
|
||||
|
||||
command -v apt-get >/dev/null 2>&1 || { echo "需要 apt-get(Debian/Ubuntu)"; exit 1; }
|
||||
|
||||
echo "== 1/4 准备目录 =="
|
||||
rm -rf "$OUT"
|
||||
mkdir -p "$OUT/debs"
|
||||
cd "$OUT/debs"
|
||||
|
||||
echo "== 2/4 展开传递依赖闭包 =="
|
||||
# --no-recommends / --no-suggests:把可选依赖排除掉,否则 4GB 机器光依赖就装爆。
|
||||
# 过滤规则:只保留顶格的包名行(依赖关系行前面有空格和「依赖:」等前缀)。
|
||||
#
|
||||
# ⚠️ 这里有一个必须显式补的坑:`Essential: yes` 的包**不会**出现在
|
||||
# apt-cache depends --recurse 的输出里 —— apt 认定「任何 Debian 系统都必有它们」,
|
||||
# 所以从不把它们列为别人 的依赖。实测缺失的有:dash / diffutils / libc-bin /
|
||||
# grep / findutils / base-files / base-passwd / dpkg / coreutils / sed / tar …
|
||||
# 在正常的 Debian 上装没问题(本机已有了),但如果目标机是**刚 debootstrap
|
||||
# 出来的极简根**或换了个发行版,缺了它们的表现是:
|
||||
# dpkg: error: 3 expected programs not found in PATH or not executable
|
||||
# (缺 sh / diff / ldconfig —— 分别来自 dash / diffutils / libc-bin)
|
||||
# 而且这个错在装 drouter 本体时才暴露,排查起来很费劲。所以显式补一份。
|
||||
ESSENTIAL="${DROUTER_ESSENTIAL:-base-files base-passwd bash coreutils dash diffutils
|
||||
dpkg grep gzip findutils hostname libc-bin libc6 libgcc-s1 libselinux1 libpcre2-8-0
|
||||
libacl1 libattr1 libcap2 libaudit1 libaudit-common sed tar zlib1g liblzma5 libbz2-1.0
|
||||
xz-utils libtinfo6 libreadline8 libmount1 libblkid1 libsmartcols1 libuuid1
|
||||
libsystemd0 libudev1 libzstd1 libcap-ng0 libcrypt1 libgmp10 libnettle8 libhogweed6
|
||||
libidn2-0 libunistring5 libtasn1-6 libp11-kit0 libffi8 libgnutls30 libgpg-error0
|
||||
libgcrypt20 libseccomp2 libstdc++6 libmd0 libedit2 libncursesw6 libfdisk1
|
||||
libapparmor1 libxxhash0 libcom-err2 libext2fs2 libjansson4 libargon2-1
|
||||
debianutils debconf init-system-helpers mount util-linux passwd login.defs
|
||||
sysvinit-utils mawk perl-base readline-common sensible-utils}"
|
||||
|
||||
# 再补一层「基线库」。踩过的坑:apt 自己的依赖链(libapt-pkg / liblz4-1 /
|
||||
# libgnutls30 / libnettle8 / libstdc++6 …)**不会**被 drouter 的依赖闭包带进来 ——
|
||||
# 因为 apt 在任何 Debian 上都已装好,它的依赖也被视为已满足。
|
||||
# 后果是:在**没有 apt 的干净根 / 换发行版**里跑 install.sh 时,
|
||||
# 「建立本地仓库索引」这一步会直接报
|
||||
# apt-get: error while loading shared libraries: liblz4.so.1
|
||||
# 然后整条安装链路退化成 dpkg 乱装。补上这几个库成本极小(几百 KB),
|
||||
# 换来的是「任何 Debian 用户态环境都能装」。
|
||||
BASELIBS="${DROUTER_BASELIBS:-liblz4-1 libapt-pkg7.0 libgnutls30 libnettle8
|
||||
libhogweed6 libstdc++6 libgmp10 libgnutls30 libunistring5 libidn2-0 libp11-kit0
|
||||
libffi8 libtasn1-6 libssl3t64 zlib1g liblzma5 libzstd1 libbz2-1.0 libpcre2-8-0
|
||||
libselinux1 libmount1 libblkid1 libuuid1 libcap2 libcap-ng0 libaudit1
|
||||
libsystemd0 libudev1 libseccomp2 libgpg-error0 libgcrypt20}"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 闭包求解:用 apt 自己算,不要用 `apt-cache depends --recurse` 做文本展开。
|
||||
#
|
||||
# 踩过的坑(很隐蔽,装到最后一步才炸):
|
||||
# `apt-cache depends --recurse` 会把**虚拟包的每一个提供者**都展开成一行。
|
||||
# systemd / systemd-standalone-sysusers / opensysusers 三者都 Provides
|
||||
# `systemd-sysusers`,同时又互相 Conflicts。文本展开会把三家**全**收进来,
|
||||
# 下载没问题;但装上时 apt 解冲突发现无解:
|
||||
# E: Unmet dependencies. Try 'apt --fix-broken install'
|
||||
# systemd : 预依赖 libsystemd-shared 但是它将不会被安装
|
||||
# 看起来像「离线包缺包」,实际是一堆互相冲突的替代品被硬凑在一起。
|
||||
#
|
||||
# `apt-get install --print-uris -y <目标>` 走的是**真正的依赖求解器**:
|
||||
# 它会按 Conflicts/Provides 选一套自洽的方案,只给出确实要装的包。
|
||||
# 这才是「离线包」该用的闭包来源。
|
||||
# ---------------------------------------------------------------------------
|
||||
CLOSURE_FILE="$OUT/.closure"
|
||||
: > "$CLOSURE_FILE"
|
||||
|
||||
# ⚠️ 关键:求解必须在一个**空的 dpkg status** 上做,不能直接用宿主机的状态。
|
||||
# 踩过的坑:在装了一堆东西的开发机上求解,apt 会认为「这些依赖已满足」而不列出来
|
||||
# (libicu76 / liburcu8t64 / python3-dbus 就是这么漏的 —— 宿主机上有,
|
||||
# 于是不进闭包;到了干净目标机上就变成
|
||||
# `Depends: libicu76 but it is not installable`,整包装不上)。
|
||||
# 所以先造一个只有「空 status」的临时 apt 根,让 apt 以为什么都没装。
|
||||
EMPTYROOT="$OUT/.solveroot"
|
||||
rm -rf "$EMPTYROOT"
|
||||
mkdir -p "$EMPTYROOT/var/lib/dpkg" "$EMPTYROOT/var/lib/apt/lists/partial" \
|
||||
"$EMPTYROOT/var/cache/apt/archives/partial" "$EMPTYROOT/etc/apt/apt.conf.d" \
|
||||
"$EMPTYROOT/etc/apt/preferences.d" "$EMPTYROOT/etc/apt/sources.list.d" \
|
||||
"$EMPTYROOT/etc/apt/trusted.gpg.d" "$EMPTYROOT/usr/share/keyrings" \
|
||||
"$EMPTYROOT/var/lib/dpkg/info" "$EMPTYROOT/var/lib/dpkg/updates"
|
||||
|
||||
# 签名校验要用到 keyring,否则 apt 会拒绝所有源:
|
||||
# E: 仓库 “…” 没有数字签名。 (trusted.gpg.d 不是目录)
|
||||
# 只借「公钥」目录即可,不复制 status —— status 必须是空的,那是本步的目的。
|
||||
cp -f /etc/apt/trusted.gpg.d/* "$EMPTYROOT/etc/apt/trusted.gpg.d/" 2>/dev/null || true
|
||||
cp -f /usr/share/keyrings/* "$EMPTYROOT/usr/share/keyrings/" 2>/dev/null || true
|
||||
# apt 自身仍从宿主机运行,签名校验走宿主 keyring 也通常可行,这里只是兜底。
|
||||
: > "$EMPTYROOT/var/lib/dpkg/status"
|
||||
touch "$EMPTYROOT/etc/apt/apt.conf.d/.keep"
|
||||
|
||||
# 复用宿主机的源列表(只是用来读 Packages 索引,不会真的下载)
|
||||
[ -f /etc/apt/sources.list ] && cp -f /etc/apt/sources.list "$EMPTYROOT/etc/apt/" 2>/dev/null || true
|
||||
mkdir -p "$EMPTYROOT/etc/apt/sources.list.d"
|
||||
cp -f /etc/apt/sources.list.d/*.list "$EMPTYROOT/etc/apt/sources.list.d/" 2>/dev/null || true
|
||||
cp -f /etc/apt/sources.list.d/*.sources "$EMPTYROOT/etc/apt/sources.list.d/" 2>/dev/null || true
|
||||
|
||||
SOLVER_OPTS=(
|
||||
-o "Dir=$EMPTYROOT/"
|
||||
-o "Dir::State=$EMPTYROOT/var/lib/apt"
|
||||
-o "Dir::State::status=$EMPTYROOT/var/lib/dpkg/status"
|
||||
-o "Dir::Cache=$EMPTYROOT/var/cache/apt"
|
||||
-o "Dir::Etc::sourcelist=$EMPTYROOT/etc/apt/sources.list"
|
||||
)
|
||||
echo " 在空 status 上求解依赖(模拟全新目标机)…"
|
||||
# update 失败不致命:说明宿主机源列表不可用,但本地 Packages 索引可能还在,
|
||||
# 后面的 -s 求解会自己报出真正的问题,这里不吞错也不中止。
|
||||
apt-get "${SOLVER_OPTS[@]}" update -qq >/dev/null 2>&1 || \
|
||||
echo " (索引刷新失败,继续用已有索引求解)"
|
||||
|
||||
# 求解:只列 Inst 行的包名。--no-install-recommends 与发布口径一致。
|
||||
if apt-get "${SOLVER_OPTS[@]}" -s --no-install-recommends -y install $DEPS \
|
||||
>"$OUT/.sim.txt" 2>"$OUT/.sim.err"; then
|
||||
sed -n 's/^Inst \([^ ]*\) .*/\1/p' "$OUT/.sim.txt" >> "$CLOSURE_FILE"
|
||||
echo " 依赖求解成功(apt 求解器 / 空 status)"
|
||||
else
|
||||
echo " ⚠ 空 status 求解失败,回退到宿主状态求解"
|
||||
if apt-get -s --no-install-recommends -y install $DEPS >"$OUT/.sim.txt" 2>/dev/null; then
|
||||
sed -n 's/^Inst \([^ ]*\) .*/\1/p' "$OUT/.sim.txt" >> "$CLOSURE_FILE"
|
||||
echo " 已用宿主状态求解"
|
||||
else
|
||||
echo " ⚠ 仍失败,回退到文本展开(可能含冲突替代品)"
|
||||
apt-cache depends --recurse --no-recommends --no-suggests \
|
||||
--no-conflicts --no-breaks --no-replaces --no-enhances \
|
||||
$DEPS 2>/dev/null \
|
||||
| grep -E '^[a-zA-Z0-9][a-zA-Z0-9.+_-]*$' >> "$CLOSURE_FILE"
|
||||
fi
|
||||
fi
|
||||
rm -f "$OUT/.sim.txt" "$OUT/.sim.err"
|
||||
|
||||
ALL=$(cat "$CLOSURE_FILE" | sort -u)
|
||||
ALL="$ALL $ESSENTIAL $BASELIBS"
|
||||
ALL=$(echo $ALL | tr ' ' '\n' | grep -v '^$' | sort -u | tr '\n' ' ')
|
||||
|
||||
# 显式排掉已知的「冲突替代品」:它们是 systemd-sysusers 的多余提供者,
|
||||
# 只要 systemd 在,就不能同时出现。留着必然导致上面那个 Unmet dependencies。
|
||||
for drop in systemd-standalone-sysusers opensysusers systemd-standalone-tmpfiles; do
|
||||
if [ -n "$(ls "$OUT"/debs/${drop}_*.deb 2>/dev/null)" ]; then
|
||||
echo " (移除冲突替代品:$drop)"
|
||||
rm -f "$OUT"/debs/${drop}_*.deb
|
||||
fi
|
||||
ALL=$(echo "$ALL" | tr ' ' '\n' | grep -vx "$drop" | tr '\n' ' ')
|
||||
done
|
||||
|
||||
# 过滤掉纯粹是虚拟/元包而下载不到实体的名字
|
||||
REAL=""
|
||||
for p in $ALL; do
|
||||
if apt-get download --print-uris "$p" >/dev/null 2>&1; then
|
||||
REAL="$REAL $p"
|
||||
fi
|
||||
done
|
||||
ALL="$REAL"
|
||||
echo " 闭包内可下载的包:$(echo $ALL | wc -w) 个"
|
||||
|
||||
echo "== 3/4 下载(只下不装)=="
|
||||
# 分批下载:命令行过长会在老 shell 上踩 ARG_MAX,且失败时更容易定位是哪个包。
|
||||
# 陷阱:apt-get download 的落地目录取自**进程的当前目录**,不是 -o 参数;
|
||||
# 早期写法用 `xargs -a partfile apt-get download`——xargs 在部分实现下会以
|
||||
# 自己的 CWD 起子进程,结果 apt 把 .deb 往 / 根目录写,全部「权限不够」。
|
||||
# 改成显式 for 循环 + 显式 cd,且每批都重新确认 CWD,绝不让 xargs 起进程。
|
||||
DEBDIR="$OUT/debs"
|
||||
echo "$ALL" | tr ' ' '\n' | grep -v '^$' > "$OUT/.allpkgs"
|
||||
BATCH=40
|
||||
total=$(wc -l < "$OUT/.allpkgs")
|
||||
i=0
|
||||
while [ "$i" -lt "$total" ]; do
|
||||
batch=$(sed -n "$((i+1)),$((i+BATCH))p" "$OUT/.allpkgs")
|
||||
# shellcheck disable=SC2086
|
||||
( cd "$DEBDIR" && apt-get download $batch 2>&1 | grep -E '^E:' | head -3 ) || true
|
||||
i=$((i + BATCH))
|
||||
done
|
||||
rm -f "$OUT/.allpkgs"
|
||||
N=$(ls -1 "$DEBDIR"/*.deb 2>/dev/null | wc -l)
|
||||
echo " 已下载 $N 个 .deb(约 $(du -sh "$DEBDIR" | cut -f1))"
|
||||
[ "$N" -gt 0 ] || { echo "✘ 一个都没下到,检查网络/源"; exit 1; }
|
||||
|
||||
echo "== 4/4 生成本地索引与安装脚本 =="
|
||||
# 有 dpkg-scanpackages(dpkg-dev 提供)就生成真正的本地仓库索引,
|
||||
# 这样目标机可以用 `apt-get install -o Dir::Etc::sourcelist=... drouter` 一次装齐。
|
||||
cd "$OUT"
|
||||
if command -v dpkg-scanpackages >/dev/null 2>&1; then
|
||||
# ⚠️ 索引必须落在 **debs/** 里,和 install.sh 里那条
|
||||
# `deb [trusted=yes] file:$HERE/debs ./` 对上。
|
||||
# 踩过的坑:原来生成到 $OUT/Packages.gz,而 apt 去 $OUT/debs/ 找 Packages,
|
||||
# 结果 `E: Failed to fetch file:.../debs/./Packages File not found`,
|
||||
# 整条「本地仓库」链路静默退化成 dpkg -i 乱装。
|
||||
( cd debs && dpkg-scanpackages -m . /dev/null 2>/dev/null > Packages )
|
||||
# 同时给一份 gzip 版:apt 优先用 Packages.gz,没有才用 Packages。
|
||||
# 两个都放,避免不同 apt 版本偏好不一致又踩一次。
|
||||
[ -s debs/Packages ] && gzip -9c debs/Packages > debs/Packages.gz
|
||||
echo " 已生成 debs/Packages(+.gz)(本地仓库索引)"
|
||||
HAS_IDX=1
|
||||
else
|
||||
echo " 未装 dpkg-dev,跳过 Packages,改用 dpkg -i 逐个安装"
|
||||
HAS_IDX=0
|
||||
fi
|
||||
|
||||
cat > "$OUT/install.sh" <<'EOS'
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# drouter 离线安装脚本
|
||||
#
|
||||
# sudo bash install.sh [drouter_x.y.z_all.deb]
|
||||
#
|
||||
# 不传参数则在当前目录里自动找 drouter_*.deb。
|
||||
# 做三件事:① 装齐全部依赖;② 装 drouter 本体;③ 打印后续步骤。
|
||||
# =============================================================================
|
||||
set -e
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
PKG="${1:-}"
|
||||
|
||||
yesno() { printf ' → %s' "$1"; }
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
# 先切到 sudo 再回来执行自己,免得用户忘了加 sudo 跑到一半才失败
|
||||
exec sudo -E bash "$0" "$@"
|
||||
fi
|
||||
|
||||
# 1) 找本体
|
||||
if [ -z "$PKG" ]; then
|
||||
PKG=$(ls -1 "$HERE"/drouter_*_all.deb "$HERE"/*/drouter_*_all.deb 2>/dev/null | head -1 || true)
|
||||
fi
|
||||
[ -n "$PKG" ] && [ -f "$PKG" ] || PKG="$HERE/$PKG"
|
||||
[ -f "$PKG" ] || { echo "✘ 找不到 drouter 安装包(用法:bash install.sh drouter_1.0.0_all.deb)"; exit 1; }
|
||||
echo "安装包:$PKG"
|
||||
|
||||
echo ""
|
||||
echo "== 1/3 安装离线依赖 =="
|
||||
# 优先走本地仓库模式:让 apt 自己算依赖顺序,比 dpkg -i 一通乱装可靠得多。
|
||||
if [ -f "$HERE/Packages.gz" ] && command -v apt-get >/dev/null 2>&1; then
|
||||
echo " 使用本地仓库索引安装(推荐)"
|
||||
tmp_list=$(mktemp)
|
||||
trap 'rm -f "$tmp_list"' EXIT
|
||||
cat > "$tmp_list" <<EOF
|
||||
deb [trusted=yes] file:$HERE/debs ./
|
||||
EOF
|
||||
apt-get -o Dir::Etc::sourcelist="$tmp_list" \
|
||||
-o Dir::Etc::sourceparts=/dev/null \
|
||||
-o APT::Get::List-Cleanup=0 \
|
||||
-o Acquire::Languages=none \
|
||||
update -qq 2>&1 | tail -2 || true
|
||||
# 先把闭包里的包整体装掉(--no-install-recommends 避免又去网上拉推荐包)
|
||||
DEBLIST=$(ls -1 "$HERE"/debs/*.deb 2>/dev/null | sed 's|.*/||; s|_.*||' | sort -u)
|
||||
# shellcheck disable=SC2086
|
||||
apt-get -o Dir::Etc::sourcelist="$tmp_list" \
|
||||
-o Dir::Etc::sourceparts=/dev/null \
|
||||
-o APT::Get::List-Cleanup=0 \
|
||||
-o Acquire::Languages=none \
|
||||
--no-install-recommends -y install $DEBLIST 2>&1 | tail -6 || true
|
||||
else
|
||||
echo " 无本地索引,逐个 dpkg -i(可能报依赖未满足,最后会用 -f 补齐)"
|
||||
dpkg -i "$HERE"/debs/*.deb 2>&1 | tail -4 || true
|
||||
apt-get --no-download -y -f install 2>&1 | tail -4 || true
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "== 2/3 安装 drouter 本体 =="
|
||||
dpkg -i "$PKG" 2>&1 | tail -6 || apt-get --no-download -y -f install 2>&1 | tail -6
|
||||
|
||||
echo ""
|
||||
echo "== 3/3 完成 =="
|
||||
echo " · 查看状态:sudo drouter-ctl status"
|
||||
echo " · 打开面板:https://<本机IP>:8443/ (默认账号 admin / admin123,请尽快改掉)"
|
||||
echo " · 首次部署建议在界面里先走「新手向导」"
|
||||
EOS
|
||||
chmod +x "$OUT/install.sh"
|
||||
|
||||
# 把本体也放进离线目录,方便整包分发
|
||||
if ls "$HERE"/dist/drouter_*.deb >/dev/null 2>&1; then
|
||||
cp -f "$HERE"/dist/drouter_*.deb "$OUT/" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# 清掉构建期的中间产物。踩过的坑:`.closure`(包名列表)和 `.solveroot/`
|
||||
# (空 status 求解用的临时 apt 根)会被原样打进 tar 包,用户解包后会看到两个
|
||||
# 莫名其妙的隐藏目录 —— 既占地方又让人困惑「这是不是漏了什么」。
|
||||
rm -rf "$OUT/.solveroot" "$OUT/.closure" "$OUT/.allpkgs" \
|
||||
"$OUT/.sim.txt" "$OUT/.sim.err" 2>/dev/null || true
|
||||
|
||||
echo ""
|
||||
echo "======================================================================"
|
||||
echo "离线包已生成:$OUT"
|
||||
echo " 体积:$(du -sh "$OUT" | cut -f1) (依赖 $(ls -1 "$OUT"/debs/*.deb 2>/dev/null | wc -l) 个 deb)"
|
||||
echo " 分发:把整个目录拷到目标机,执行"
|
||||
echo " sudo bash install.sh drouter_1.0.0_all.deb"
|
||||
echo "======================================================================"
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/bin/bash
|
||||
# drouter 八模块回归自检(构建保护模式下应全部写成到磁盘,不启动服务)
|
||||
B=http://127.0.0.1:8080
|
||||
C="curl -s"
|
||||
TOK=$($C $B/api/login -X POST -H 'Content-Type: application/json' \
|
||||
-d '{"username":"admin","password":"admin123"}' \
|
||||
| python3 -c 'import sys,json;d=json.load(sys.stdin);print((d.get("data") or {}).get("token",""))')
|
||||
if [ -z "$TOK" ]; then echo "登录失败"; exit 1; fi
|
||||
echo "token ok: ${TOK:0:12}..."
|
||||
|
||||
A(){ $C $B/api/apply -X POST -H "X-Token: $TOK" -H 'Content-Type: application/json' -d "$1" \
|
||||
| python3 -c 'import sys,json;d=json.load(sys.stdin);print("ok=",d.get("ok"),"|",d.get("msg_cn") or d.get("code") or "")'; }
|
||||
|
||||
echo "--- 1 dnsmasq ---"
|
||||
A '{"module":"dnsmasq","data":{"lan_iface":"ens18","lan_ip":"192.168.7.3","dhcp_enabled":false,"pool_start":"192.168.7.100","pool_end":"192.168.7.200","lease_time":43200,"dns_mode":"custom","dns_custom":"223.5.5.5,119.29.29.29","options":[{"enabled":true,"code":"6","value":"192.168.7.3"},{"enabled":true,"code":"3","value":"192.168.7.3"}]}}'
|
||||
echo "--- 2 nft_v4 ---"
|
||||
A '{"module":"nft_v4","data":{"wan_iface":"","lan_iface":"ens18","allow_estab":true,"icmp":true,"input_policy":"drop"}}'
|
||||
echo "--- 3 nft_v6 ---"
|
||||
A '{"module":"nft_v6","data":{"wan_iface":"","lan_iface":"ens18","allow_estab":true,"icmpv6":true,"input_policy":"drop"}}'
|
||||
echo "--- 4 radvd ---"
|
||||
A '{"module":"radvd","data":{"lan_iface":"ens18","prefix":"fd00:7::/64","adv_link_mtu":1500}}'
|
||||
echo "--- 5 dhcpv6 ---"
|
||||
A '{"module":"dhcpv6","data":{"wan_iface":"","lan_iface":"ens18","pd_len":60}}'
|
||||
echo "--- 6 upnp ---"
|
||||
A '{"module":"upnp","data":{"enable":false,"lan_iface":"ens18","wan_iface":""}}'
|
||||
echo "--- 7 ntp ---"
|
||||
A '{"module":"ntp","data":{"servers":["ntp.aliyun.com","time.cloudflare.com"]}}'
|
||||
echo "--- 8 ppp ---"
|
||||
A '{"module":"ppp","data":{"iface":"ens18","username":"test12345","password":"pass67890","isp":"ct","mtu":1492}}'
|
||||
|
||||
echo "=== 生成文件落盘检查 ==="
|
||||
ls -la /etc/dnsmasq.d/drouter.conf /etc/nftables.d/drouter-v4.nft /etc/nftables.d/drouter-v6.nft \
|
||||
/etc/radvd.conf /etc/dhcpcd.conf /etc/miniupnpd/miniupnpd.conf /etc/chrony/chrony.conf \
|
||||
/etc/ppp/peers/drouter-wan 2>&1
|
||||
|
||||
echo "=== 安全不变量 ==="
|
||||
echo -n "dnsmasq: "; systemctl is-active dnsmasq 2>&1
|
||||
echo -n "radvd: "; systemctl is-active radvd 2>&1
|
||||
echo -n "dhcpcd: "; systemctl is-active dhcpcd 2>&1
|
||||
echo -n "miniupnpd:"; systemctl is-active miniupnpd 2>&1
|
||||
echo -n "chrony: "; systemctl is-active chrony 2>&1
|
||||
echo -n "53端口: "; ss -lnu | grep -c ':53 ' || true
|
||||
echo -n "67端口: "; ss -lnu | grep -c ':67 ' || true
|
||||
echo -n "VNC5900: "; ss -lnt | grep -c ':5900'
|
||||
echo -n "BUILD_MODE: "; test -f /etc/drouter/BUILD_MODE && echo "存在(保护中)" || echo "不存在"
|
||||
echo -n "ens18: "; ip -4 -o addr show ens18 | awk '{print $4}'
|
||||
echo "=== nft 规则 ==="
|
||||
nft list ruleset 2>/dev/null | head -5 || echo "nftables 表为空"
|
||||
echo "REGRESSION_DONE"
|
||||
@@ -0,0 +1,84 @@
|
||||
#!/bin/bash
|
||||
# 前端契约回归:验证登录能拿到 token,且每个前端页面依赖的接口都返回 200
|
||||
B=http://127.0.0.1:8080
|
||||
echo "=== 1. 登录应返回真实 token ==="
|
||||
RESP=$(curl -s $B/api/login -X POST -H 'Content-Type: application/json' \
|
||||
-d '{"username":"admin","password":"admin123"}')
|
||||
echo "$RESP" | python3 -m json.tool --no-ensure-ascii
|
||||
TOK=$(echo "$RESP" | python3 -c 'import sys,json;print((json.load(sys.stdin).get("data") or {}).get("token") or "")')
|
||||
if [ -z "$TOK" ]; then echo "❌ token 为空 —— 登录仍坏"; exit 1; fi
|
||||
echo "✅ token = ${TOK:0:16}..."
|
||||
|
||||
echo
|
||||
echo "=== 2. 用该 token 请求各页面依赖接口 ==="
|
||||
FAIL=0
|
||||
check() {
|
||||
local ep="$1" m="${2:-GET}" body="${3:-}"
|
||||
local args=(-s -o /tmp/o.json -w '%{http_code}' "$B$ep" -H "X-Token: $TOK")
|
||||
[ "$m" = POST ] && args+=(-X POST -H 'Content-Type: application/json' -d "${body:-{\}}")
|
||||
local code=$(curl "${args[@]}")
|
||||
local ok=$(python3 -c 'import json;print(json.load(open("/tmp/o.json")).get("ok"))' 2>/dev/null)
|
||||
printf " %-22s %-5s HTTP=%-4s ok=%-6s %s\n" "$ep" "$m" "$code" "$ok" \
|
||||
"$( [ "$code" = 200 ] && [ "$ok" = True ] && echo ✔ || { echo ✘; FAIL=1; } )"
|
||||
}
|
||||
# 概览页
|
||||
check /api/sysinfo; check /api/services; check /api/ifaces
|
||||
check /api/ipv6; check /api/routes
|
||||
# 网卡/WAN/LAN
|
||||
check /api/iface/meta; check /api/config
|
||||
# 防火墙/日志/诊断/电源/用户/升级
|
||||
check /api/nft; check /api/leases; check /api/users; check /api/ntp
|
||||
check /api/logs; check /api/journal; check /api/audit
|
||||
check /api/snapshot/list; check /api/upgrade/info
|
||||
# 构建保护模式(前端用 GET)
|
||||
check /api/buildmode GET
|
||||
check /api/pppoe/saved
|
||||
# 新增:Web 端口 / 网卡配置方式 / 拨号实时日志
|
||||
check /api/webport
|
||||
check /api/iface_method
|
||||
check /api/ppp/log
|
||||
# 新增:IPv6 连通性测试工具
|
||||
check /api/diag POST '{"tool":"ipv6test","which":"cn"}'
|
||||
# 新增:自动快照策略 / 紧急救援通道
|
||||
check /api/snapshot/auto
|
||||
check /api/rescue
|
||||
# 新增批次(#17–#22):通用 API / 依赖自检 / NAT / 软加速 / VLAN / WOL / 文件管理
|
||||
check /api/openapi
|
||||
check /api/examples
|
||||
check /api/deps/check
|
||||
check /api/vlan
|
||||
check /api/wol
|
||||
check /api/nat/last
|
||||
check /api/accel POST '{"op":"get"}'
|
||||
# 新增批次(#23–#25):PPPoE 多拨 / QoS 智能限速 / DPI 应用识别
|
||||
check /api/pppoe-multi
|
||||
check /api/pppoe-multi POST '{"op":"get"}'
|
||||
check /api/pppoe-multi/log POST '{"idx":1}'
|
||||
check /api/qos
|
||||
check /api/qos POST '{"op":"get"}'
|
||||
check /api/dpi
|
||||
check /api/dpi POST '{"op":"get"}'
|
||||
check /api/dpi POST '{"op":"plan","which":"apt"}'
|
||||
check /api/dpi/log
|
||||
check '/api/files/list?path=/etc/drouter'
|
||||
check '/api/files/read?path=/etc/drouter/snapshot.conf'
|
||||
# 写操作:只保存(不应用)
|
||||
check /api/config POST '{"module":"system","data":{"hostname":"debian-primaryrouter"}}'
|
||||
|
||||
echo
|
||||
echo "=== 3. 关键字段核对 ==="
|
||||
curl -s $B/api/buildmode -H "X-Token: $TOK" | python3 -c '
|
||||
import sys,json;d=json.load(sys.stdin)
|
||||
print(" buildmode.data.enabled =", d["data"]["enabled"], "(解除后应为 False)")'
|
||||
curl -s $B/api/ifaces -H "X-Token: $TOK" | python3 -c '
|
||||
import sys,json;d=json.load(sys.stdin)
|
||||
for i in d["data"]:
|
||||
if i["name"]=="lo": continue
|
||||
n=i.get("name"); m=i.get("mac"); st=i.get("oper") or i.get("operstate") or ""
|
||||
print(" %-8s mac=%s state=%s" % (n, m, st))'
|
||||
curl -s $B/api/routes -H "X-Token: $TOK" | python3 -c '
|
||||
import sys,json;d=json.load(sys.stdin)
|
||||
print(" 默认路由:", [(r["gateway"], r["dev"]) for r in d["data"]["v4"] if r["dst"]=="default"])'
|
||||
|
||||
echo
|
||||
[ $FAIL = 0 ] && echo "FRONTEND_CONTRACT_OK ✔ 全部接口正常" || echo "FRONTEND_CONTRACT_FAIL ❌ 存在异常接口"
|
||||
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
# 模拟“用户在 WEB 里填写 PPPoE 账号密码后点击保存并应用”的完整链路
|
||||
B=http://127.0.0.1:8080
|
||||
TOK=$(curl -s $B/api/login -X POST -H 'Content-Type: application/json' \
|
||||
-d '{"username":"admin","password":"admin123"}' \
|
||||
| python3 -c 'import sys,json;print((json.load(sys.stdin).get("data") or {}).get("token",""))')
|
||||
[ -z "$TOK" ] && { echo "登录失败"; exit 1; }
|
||||
|
||||
echo "=== A. 保存 pppoe 模块配置(仅保存)==="
|
||||
curl -s $B/api/save -X POST -H "X-Token: $TOK" -H 'Content-Type: application/json' \
|
||||
-d '{"module":"pppoe","data":{"iface":"ens18","username":"user@test","password":"secret999","isp":"ct","mtu":1492}}' \
|
||||
| python3 -m json.tool --no-ensure-ascii 2>/dev/null || echo "(接口不存在,尝试 apply)"
|
||||
|
||||
echo
|
||||
echo "=== B. 通过 apply 接口写盘(构建保护模式下 live=false)==="
|
||||
curl -s $B/api/apply -X POST -H "X-Token: $TOK" -H 'Content-Type: application/json' \
|
||||
-d '{"module":"ppp","data":{"iface":"ens18","username":"user@test","password":"secret999","isp":"ct","mtu":1492},"live":false}' \
|
||||
| python3 -m json.tool --no-ensure-ascii
|
||||
|
||||
echo
|
||||
echo "=== C. 落盘结果 ==="
|
||||
echo "--- /etc/ppp/peers/drouter-wan ---"; cat /etc/ppp/peers/drouter-wan 2>/dev/null | head -12
|
||||
echo "--- /etc/ppp/chap-secrets ---"; cat /etc/ppp/chap-secrets 2>/dev/null
|
||||
echo "--- 权限 ---"; ls -l /etc/ppp/peers/drouter-wan /etc/ppp/chap-secrets 2>/dev/null
|
||||
|
||||
echo
|
||||
echo "=== D. 服务是否仍未被启动(安全不变量)==="
|
||||
for s in pppd dnsmasq; do printf " %-8s %s\n" "$s" "$(systemctl is-active $s 2>&1)"; done
|
||||
ss -lnu | grep -E ':53 |:67 ' && echo " 端口被占用(异常)" || echo " 53/67 仍空闲 ✔"
|
||||
echo "PPPOE_SAVE_PATH_OK"
|
||||
Reference in new issue
Block a user