Files
drouter/scripts/deploy.sh
T
liuzhuohua f602e69f1b 全面审计:修 30 处静默失效/安全/资源缺陷,含 4 个 P0
本轮按「每个菜单、每个功能」逐项排查,重点是家庭宽带上网必经的
IP / DHCP / DNS / PPPoE 链路,外加日志、监控、依赖三类基础能力。
所有缺陷的共同特征是「静默」:界面提示成功、配置写盘成功、服务
照常运行,但实际什么都没发生 —— 这类 bug 功能测试抓不到。

P0(安全 / 完全失效)
- readings 遮蔽 6 个模块的 POST 写分支:readings 字典同时匹配 GET 与
  POST 且位于所有写路由之前,/api/{acl,share,docker,ddns,theme,ulog}
  的保存操作永远走不到,还返回 ok:true + 读数据,前端照样提示
  「保存完成」。这六个模块从 1.0.5 之前就一直是废的。
- PPPoE 缺 ip-up 钩子:usepeerdns 只把运营商 DNS 写进
  /etc/ppp/resolv.conf,而 dnsmasq 读的是 resolv-file 指向的
  generated/isp-dns.conf。dns_mode=isp/both 时上游全空,全家断网。
  补 render_ppp 的 ip-up/ip-down-script + 新增 sync-isp-dns.sh。
- 紧急救援通道零凭据:开启时 169.254.0.1/16 挂到每一张物理网卡,
  「救援网段内」等于局域网任意主机,而确认短语是页面源码常量 ——
  等于任何人都能回滚配置并重启全机。改为开启即生成 6 位 token,
  所有写操作强制校验,失败 10 次锁 60 秒;老配置无 token 时拒绝
  放行(不退化成无凭据)。
- SNMP 注入:community 有 [\s#] 校验,sysname/location/contact 只截
  长度。塞一个换行就能凭空插出 rwcommunity,把只读团体名升级成可写。
  三个字段补齐换行与 # 过滤。

P1(静默失效)
- 静态租约写入 host/无 enabled,渲染器读 name/enabled → 整条被
  v_bool 过滤,「转静态」提示成功但 dnsmasq 里一条 dhcp-host 都没有。
- UPnP 网段永远退回 192.168.0.0/16:render_miniupnpd 读
  system.lan_address,但 apply 与 render 两条路径都只注入 ext_iface。
- 防火墙「仅语法检查」会写配置库:live=False 只代表写盘不生效,
  真正不写盘必须传 check_only。前端漏传,后端 merge_cfg 也照写。
- 构建保护模式下 rollback+reload 绕过守卫:会 restart dnsmasq /
  radvd / dhcpcd —— 正是这个模式要防的污染源。
- sys 页改了 access_scope 却提示「点击保存并应用」,而 PAGE_MODULES
  里没有 sys → 保存按钮根本不存在。
- DHCP 页 rebuild() 整体重渲染,新闭包覆盖 stopLeaseTimer,旧 interval
  句柄永久丢失 → 每加 10 条自定义 Option 泄漏一个 10 秒轮询。
- DHCPv6 prefix_len 在 IPv6 页写数字、DHCPv6 页写字符串 → 下拉框全不
  选中,动别的字段还会反向污染。
- 端口转发草稿:编辑只写 S.pfRules,重进从 c.rules 重建 → 切页丢草稿。
- 通用保存/应用成功后从不 pageClean(S.page):20 多个页面的脏标记
  保存后永久残留,之后每次进页面都用旧草稿盖掉库里读回的新值。
- DHCP 池三项校验只在向导路径,直存路径绕过 → 池含本机 LAN 地址时
  表现为「网络时通时不通」且无任何日志。校验提到 render_dnsmasq。
- 多拨 service_name 无白名单,含引号即可闭合引号续写 pppd 指令。
- 救援放行规则只 add 不 delete,改端口 + 关闭后旧 accept 永久残留。
  改独立 chain drouter_rescue,关闭时按 handle 删 jump 再删 chain。

P2(资源 / 性能 / 一致性)
- _ulog_prune 用 f.read().splitlines() + 第二个 kept 列表,keep_rows
  默认 20 万行 ≈ 160MB 峰值,而它每 60 秒跑一次 → 4GB 机器上的
  OOM 杀手。改流式:先只数行,未超上限零重写,超了才滑窗 + os.replace。
- /api/metrics 每 2 秒轮询,而 _route_latency 在 _metric_lock() 内跑
  ping(timeout 10s)→ 目标不可达时整个仪表盘阻塞十几秒。移出锁。
- br0 的 RX/TX 是成员口之和,与成员口一起累加使总流量约翻倍。
- dnsmasq cache-size=0(禁用缓存)被 `or 1000` 吞成 1000。
- radvd pool_enabled / dhcpv6 enabled 用 `is not False`,把字符串
  'false'/'0' 当 True。改 v_bool。
- Number('') === 0,清空的 MTU/MRU/租期/端口被提交成 0。新增 numOr。
- drouter-logd 缺磁盘将满保护(snapshotd 早有),根分区将满时归档
  失败被 except 吞掉,日志静默停更。
- multiupnpd local 域写死 /lan/,不跟随用户域名。
- 多拨 MRU 独立取值并钳 576–1500(原来直接写 mtu,界面填的 MRU
  被静默忽略)。
- rescue.conf 注释说 0600 实际 0640,改注释与实现对齐。

测试
- 新增 _dev/t-apicheck.py:前后端 API 契约 + 分支可达性检查器
  (169 项)。静态比对「路径在文件里出现过」查不出遮蔽,必须检查
  「谁先 return」。
- 新增 _dev/t-audit106.py:83 项,覆盖本轮全部修复。
- 新增 _dev/live-audit106.py:真机验收,重点验 6 个曾失效模块的写
  分支可达、check_only 不写库、静态租约字段、SNMP 净化、多拨白名单。
- t-fefix 的 value="${...}" 断言过严:map 回调形参取自源码字面量
  数组,零风险却一直误报。改为只对含点/调用/拼接的插值强制 esc()。
- 离线回归 35 个测试全绿;真机 192.168.7.3 验收全通过。

真机验证(192.168.7.3)
- 6 个模块 POST 均走到专属分支(用不存在 op 打出模块级 BAD_OP,
  不再是读数据 + ok:true);主题切换 active 读回一致、ulog save_conf
  成功、DDNS 非法 provider 被拒。
- check_only 预检前后配置库逐字节一致。
- 静态绑定写库为 {enabled,mac,ip,name},与渲染器读取字段一致。
- SNMP 库里 sysname 无换行、无 rwcommunity。
- 救援通道:无/错 token 403,正确 token 200,正确 token + 错短语被
  拒不执行,错 10 次 429 锁 60 秒,rescue.conf 0640 root:drouter;
  关闭后 token 清空、nft chain 回收、虚拟地址回收、服务停。
2026-10-02 18:58:23 +08:00

483 lines
21 KiB
Bash
Raw 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
# 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 > active-theme` —— 每次部署都把用户自己
# 挑的主题打回默认,用户只会看到「部署完样式莫名其妙变回去了」。
# 容器版 docker-init.sh 一直是「首次为空时才写」,两处行为保持一致。
if [ ! -f /etc/drouter/active-theme ]; then
echo "default" > /etc/drouter/active-theme
fi
chmod 644 /etc/drouter/active-theme
# 记录的生效主题如果指向一个已不存在的主题(自定义主题目录被删/id 写错),
# 界面会一直挂着一个「不存在」的生效主题,且只能手改文件才出得来。这里兜底回退。
python3 - <<'PY' || true
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)
tid = h._theme_read_active()
if h._theme_find(tid) is None:
h._theme_write_active('default')
print('生效主题 %r 已不存在,已回退 default' % tid)
PY
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
# oneshot 打完包就退出,但 tar 大目录时也会有内存尖峰,给个上限兜底
MemoryMax=300M
[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
# 救援通道是「最后的入口」:本身只是个小 HTTP 服务,几十 MB 足够。
# 给它也装上内存闸 —— 真到内存泄漏那天,不能让它把本就紧张的机器拖死
MemoryMax=150M
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 引用)==="
# 只在缺失时补占位。这个文件是 pppd 拨号成功后经 bin/sync-isp-dns.sh 写入的
# 真实运营商 DNS;无条件覆盖会把已经拿到的 DNS 擦掉,在那之后 dnsmasq 的
# 上游解析一直空着,直到下一次拨号才会恢复(表现为「重新部署后网页解析变慢」)。
# 容器版 docker-init.sh 同样是「缺失才写」。
if [ ! -f /etc/drouter/generated/isp-dns.conf ]; then
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
fi
cat > $OPT/bin/sync-isp-dns.sh <<'SYNC'
#!/bin/bash
# 将 pppd 获取到的运营商 DNS 同步给 dnsmasq
# 由 pppd 的 ip-up-script / ip-down-script 调用(见 render.py 的 render_ppp)。
# 内容没变就不 reload:拨号/断线事件很频繁,每次都重启 dnsmasq 会把
# DNS 缓存全清掉,表现为「网页时快时慢」。
SRC=/etc/ppp/resolv.conf
DST=/etc/drouter/generated/isp-dns.conf
TMP=$DST.tmp.$$
{
echo "# 由 drouter 自动生成:运营商下发 DNS(来源 $SRC)"
if [ -f "$SRC" ]; then
grep -E '^nameserver' "$SRC" || echo "# 尚未获取到运营商 DNS"
else
echo "# $SRC 不存在(尚未拨号成功)"
fi
} > "$TMP"
# 归一化注释行后再比较,避免「只有时间戳/注释差异」被当成变化
if [ -f "$DST" ] && diff -q <(grep -v '^#' "$DST") <(grep -v '^#' "$TMP") >/dev/null 2>&1; then
rm -f "$TMP"
exit 0
fi
mv -f "$TMP" "$DST"
chmod 644 "$DST"
# DNS 模式为「仅运营商 / 两者合并」时 dnsmasq 依赖这个文件,必须重载
if grep -qs 'resolv-file=/etc/drouter/generated/isp-dns.conf' /etc/dnsmasq.d/drouter.conf 2>/dev/null; then
systemctl reload dnsmasq 2>/dev/null || systemctl restart dnsmasq 2>/dev/null || true
fi
exit 0
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 动作生成(默认参数即推荐值)。
# 关键点:**只在新机器上才传 enabled=true**。
# _clean_save_op 是合并式保存,无条件传 enabled=true 会把用户在界面上明确
# 关掉的自动清理重新打开 —— 而清理是本项目唯一会真删文件的模块,
# 静默恢复一个「自动删文件」的策略是所有副作用里最不该发生的。
if [ ! -f /etc/drouter/cleanup.conf ]; then
/usr/bin/python3 $OPT/backend/drouter-helper.py cleanup '{"op":"save","enabled":true}' \
>/dev/null 2>&1 && echo "清理策略已按推荐值初始化" || echo "⚠ 清理策略初始化失败(界面保存一次即可恢复)"
else
# 已有策略:只重建单元文件,enabled / trigger / items 全部沿用用户设置
/usr/bin/python3 $OPT/backend/drouter-helper.py cleanup '{"op":"save"}' \
>/dev/null 2>&1 && echo "清理策略沿用已有设置(未改动用户开关)" || echo "⚠ 清理单元重建失败(界面保存一次即可恢复)"
fi
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"