本轮按「每个菜单、每个功能」逐项排查,重点是家庭宽带上网必经的
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 回收、虚拟地址回收、服务停。
483 lines
21 KiB
Bash
483 lines
21 KiB
Bash
#!/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"
|