Skip to content

Releases: liuzhuohua/drouter

v1.0.11 — arm64(Debian 13)适配 + 产物重打

Choose a tag to compare

@liuzhuohua liuzhuohua released this 16 Oct 03:17

Drouter 1.0.11

Debian 13 (trixie) 软路由管理系统 —— 47 个功能页面的 Web 管理面板。

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


🧭 这个版本是什么

arm64(Debian 13)适配的产物重打 + arm64 产物首发。

上一版把 arm64 适配的代码推了上去,但没有重打产物 —— Release 上挂的三件套
仍是旧的,不含那次修复。这一版把产物补齐,并首次发布 arm64 的离线包与镜像。

实测机型(全部取自机器真实输出,见 docs/07-arm64-适配说明.md):

项 值
发行版 Armbian 26.8.1 trixie(ID=debian、VERSION_ID=13、DEBIAN_VERSION_FULL=13.6)
内核 6.18.35-ophub
CPU ARM Cortex-A53 × 8,aarch64(dpkg 架构 arm64)
板卡 设备树 Octopus Planet,compat amlogic,q200(Armbian 板型 S912 / meson-gxm)
启动方式 无 EFI、无 DMI,U-Boot + 设备树

⚠️ 只对这个发行版做过实测。 别的 arm64 Debian 衍生版(Ubuntu / Raspberry Pi OS /
其他 Armbian 板型)未验证,请先按适配说明自检再上生产。


🔧 代码上改了什么(v1.0.10 → v1.0.11)

只有版本号变了。 arm64 适配的代码本身是上一版(6df3a94)推的:

  1. _read_bios() 不再把 ARM 板误报成「传统 BIOS」
    ARM 板既无 /sys/firmware/efi 也无 /sys/class/dmi,原代码一律落到 else 分支,
    系统概览显示「传统 BIOS」—— 这类机器上根本没有 BIOS。新增设备树分支,
    判据顺序 EFI → 设备树 → BIOS(树莓派 4 这类两可的机器要优先认 EFI);
    compatible 按 NUL 切开取第一条;没有 DMI 厂商时拿设备树板名兜底。

  2. 更新模块不再给 arm64 下发 x86 离线包
    离线包资源名原先写死 -offline-amd64,arm64 机器点一键更新会下载一份 x86 包,
    传几十 MB 才在装包阶段炸依赖。改为按 dpkg --print-architecture 拼名,
    并对带架构后缀的附件在下载前先拿官方 SHA256SUMS 核对存在性。

  3. 离线包说明里两处写死的 amd64 换成 $ARCH;build-docker.sh 新增可选第 2 参数
    [arch](不传时行为与改动前逐字节一致)。

关于「amd64 有没有实质更新」

代码逻辑上没有:设备树分支在 x86 上不触发(有 EFI/DMI 时走原分支,结果逐字节一致,
只是多返回 fw_type / is_dt / compat 三个字段);_deb_arch() 在 amd64 上返回
amd64,拼出的离线包名与原写死的完全相同。

但「只重打 arm」这条路走不通,两个硬约束:

  1. deb 是 Architecture: all 的单包 —— 不存在「arm 版 deb」;
  2. 离线包里内含本体 deb —— 本体一变,amd64 那份离线包装出来的就是旧 deb。

所以这次是全量重打:deb 1 + 离线包 2 + 镜像 2,外加 compose / guide 两个模板附件。


📦 附件

文件 大小 说明
drouter_1.0.11_all.deb 753,804 字节 本体,Architecture: all,各 CPU 通用
drouter-1.0.11-offline-amd64.tar.gz 87,469,655 字节 x86_64 离线包(断网可装,含 237 个依赖)
drouter-1.0.11-offline-arm64.tar.gz 83,559,313 字节 aarch64 离线包
drouter-1.0.11-docker.tar 178,616,320 字节 amd64 镜像(drouter:1.0.11)
drouter-1.0.11-arm64-docker.tar 70,662,656 字节 arm64 镜像(drouter:1.0.11-arm64)
docker-compose.yml 5,257 字节 Compose 编排
DOCKER-GUIDE.md 13,291 字节 Docker 使用说明

镜像 tar 里同时打了 drouter:<版本> 与 liuzhuohua/drouter:<版本> 两个标签,
docker load 后可直接用。

🔐 SHA256

e70ddec36ec7a4c988bf67d79ad80c4c7da2fff49264c50e48644d37e8a6903a  drouter_1.0.11_all.deb
15ef9401e5be91634950f7cd1b40a2e2eafb44fb5c3becaba2f0739002c36e3e  drouter-1.0.11-offline-amd64.tar.gz
454b3c6be736747cce57510622aeae7018df39d987a7e12d237ce67cf7b51c39  drouter-1.0.11-offline-arm64.tar.gz
71f07e6cfca360810e37fa8f0b05b470dc2db5d359346678a634ecf1be15190e  drouter-1.0.11-docker.tar
1c2afc62ed69f070446023c611cc0dec8cb7ac139d5e7040fb535e64bff89be8  drouter-1.0.11-arm64-docker.tar
76ba22a79c3c8097905ca6928fdad1dc4deb2ed72834afce86db0f5db142dc68  docker-compose.yml
64e4d53e0e6895c26170edbf66be066543aff6a126e7ae8634a347fc6112a33b  DOCKER-GUIDE.md

⬆️ 从 1.0.10 升级

配置存储格式没变,老配置直接可用。

  • deb 安装的用户:装新 deb 即可(apt install ./drouter_1.0.11_all.deb),
    也可以直接在面板「系统 → 更新」里点升级。
  • 离线包用户:注意按自己机器的架构选包,dpkg --print-architecture
    要与包名里的后缀对上。
  • Docker 用户:重新 docker load 新 tar 并重建容器。

📄 相关

  • 适配说明与实测数据:docs/07-arm64-适配说明.md
  • 想在自己的板子上复验:python3 devtools/verify-arm64.py(只读,不改系统)

Drouter 1.0.10

Choose a tag to compare

@liuzhuohua liuzhuohua released this 10 Oct 07:35

Drouter 1.0.10

Debian 13 (trixie) 软路由管理系统 —— 47 个功能页面的 Web 管理面板。

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


🔧 这个版本修了什么

全站中英双语 + 四轮排版修复的收官 + 一个真缺陷。

英文界面彻底去掉中文残留;「文字竖着排 / 错位」的排版问题在多分辨率下
反复暴露、逐层定位,最终连根修掉;顺带补上「已连接时长重启后算负数」的缺陷。

🌐 一、界面中英双语,英文界面不再残留中文

给整个面板加了一套零依赖、零构建的 i18n 框架,核心是新增的 web/i18n.js
(约 4200 条词条):

  • 菜单、页面标题、状态、日志、提示语全部走 t() / T() 查词;
  • 后端下发的中文数据(30+ 张表:加速影响、依赖清单、告警规则、上游协议、
    DHCPv6 策略、审计日志……)经 bt4() 查表翻译,英文界面查不到才回落后端原文;
  • 独立前端模块(upstream / netdetail / realtime / update)纳入翻译,
    并注册切语言后的重绘钩子,切换后异步子区块也即时刷新;
  • 右上角语言切换按钮,选择持久化,刷新不丢。

翻译不是简单抽字面量。这一路踩过并处理掉的坑:

  • 动态拼接的 key(nav.n. + 序号)静态检查覆盖不到;
  • 导航持久化键用了中文菜单名 → 切语言后展开状态全丢,改成语言无关的 g0 序号;
  • 顶层 const 不挂 window → 独立脚本里的 esc 静默退化成语零转义,
    是一个真实 XSS,已修;
  • T() 查不到词条返回 key 原文(非空),T('x') || fallback 的兜底永不触发;
  • 后端「状态/聚合」接口一律只回稳定枚举 + 原始数值,不下发中文文案,
    从根上掐断「后端下发中文」这条债。

📐 二、修掉「文字竖着排 / 错位」——四轮排版修复的收官

用户在多分辨率下反复反馈「字是竖着的」。根因不止一处,逐层定位后统一修掉:

根因 表现 修法
单元格 min-content 只有 1 字宽(CJK 任意两字间断行) 表格列被压到 ~25px,一列一个字 th,td{word-break:keep-all} + 表头 nowrap
依赖卡片正文被标签/按钮挤成 27px 描述折成 23 行 × 5 字(屏幕越宽越明显) .dep-body{flex:1 1 200px} + 卡片 flex-wrap:wrap
裸 flex:1(= 1 1 0%,下限为 0)给正文 正文被固定宽兄弟压成细柱 一律改 flex:1 1 <px>
.dep-item 横向容器直接塞块级子项 告警规则名被压到 26px 该容器退回块级
徽标 / 表单标签 / 开关名称被压 一字一行 keep-all + 不收缩 + 允许换行

这类 bug 有个共性陷阱:窄屏反而看不出来。
.dep-grid 用 repeat(auto-fill, minmax(300px,1fr)) —— 屏幕越宽列数越多、
单卡越窄,所以在 800px 窗口下是好的,到 1920/2560 反而崩。
回归守卫 _dev/t-layout.py 从 16 条扩到 35 条判据,真机审计推进到
「逐视图点按钮渲染真数据 + 1920/2560 宽视口 + 中英双语各跑一遍」,
全部 12 处反向验证注入都按预期变红。

🐛 三、顺带修掉的真缺陷:重启后「已连接时长」可能算成负数

_link_since() 里的重启检测在一次「无关注释清理」中被误改成了 if False:
(注释里还写着「已修」)—— 于是机器重启后 uptime 归零,状态文件里的
start_uptime 还是上一次启动的值,相减得到负数。恢复为比对 boot_id
(/proc/sys/kernel/random/boot_id),重启后重新记起点。


📦 附件

文件 大小 说明
drouter_1.0.10_all.deb 730 KB Debian 13 一键安装包(推荐)
drouter-1.0.10-offline-amd64.tar.gz 83 MB 完全离线安装包,237 个依赖全带
drouter-1.0.10-docker.tar 170 MB Docker 镜像(179 MB 解压)
docker-compose.yml 5.1 KB Compose 编排
DOCKER-GUIDE.md 13 KB Docker 使用说明

SHA256

1ac52a1ce3935595a272ae3666f30fc7219ceea384ffdbd990568562cfe19237  drouter_1.0.10_all.deb
eea16b750146e651f7c2fbe6d15aa399cf0b616c70cbbd6c50110471fab5f07b  drouter-1.0.10-offline-amd64.tar.gz
74ba6511bfe44c517f72422c846b184b4fb18414f38b42d846f50543312c1251  drouter-1.0.10-docker.tar
ac4632416bd45d4cde2ece834746d1a364fcbae3dd1d79e9086d1fdc893be4c6  docker-compose.yml
656bf55fa8fc193aaf9d98698a2471e397d6c18a6a94af506cddb688c8757a9d  DOCKER-GUIDE.md

镜像 tar 在构建时有两个 sha256:buildah 直接导出的原始 tar 标签是
localhost/drouter:1.0.10;上面这份是上传到 Release 的那份 ——
已重标记为 drouter:1.0.10 + liuzhuohua/drouter:1.0.10,并补上了
HEALTHCHECK(/api/health,自签证书走 curl -k)。
两者的 layer 逐字节相同(已校验),差别只在三个元数据小文件。

装法

Debian 13(推荐)

sudo apt install ./drouter_1.0.10_all.deb

完全离线(目标机不通外网)

tar xzf drouter-1.0.10-offline-amd64.tar.gz
cd drouter-1.0.10-offline-amd64
sudo bash install.sh

Docker

docker load -i drouter-1.0.10-docker.tar
docker compose up -d

浏览器打开 https://<宿主IP>:8443/,默认账号 admin / admin123,登录后请立即修改。


⬆️ 从 1.0.9 升级

sudo apt install ./drouter_1.0.10_all.deb

配置文件全部保留,升级后建议硬刷新一次(Ctrl+F5)——
浏览器可能缓存着旧的 app.js。


🔗 仓库

https://github.com/liuzhuohua/drouter · MIT License

Drouter 1.0.9 — 首屏 200KB、页脚版本号、logo 回概述、修 VPN 四态缺态

Choose a tag to compare

@liuzhuohua liuzhuohua released this 04 Oct 06:09

Drouter 1.0.9

Debian 13 (trixie) 软路由管理系统 —— 47 个功能页面的 Web 管理面板。

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


🔧 这个版本修了什么

一次传输层优化 + 两个小功能 + 一个真缺陷。

首屏从 690 KB 裸传降到 200 KB,第二次打开直接 0 字节。
页脚现在显示当前版本号,左上角 logo 点一下回系统概览。

顺带修掉一个 v1.0.8 就存在、但一直没被发现的缺陷:VPN 页面的 WireGuard 四态诊断
少了一态,在 PVE 的 KVM 虚机上明明有 wireguard.ko 却报「不支持」。

⚡ 一、首屏资源:690 KB → 200 KB,第二次打开 0 字节

之前每个静态资源都是「原样传输 + 重新校验」,app.js 一个文件就有 690 KB。
现在做了三件事:

场景 1.0.8 1.0.9
首次打开 app.js 690,482 字节裸传 200,137 字节(gzip)
第二次打开 又传 200 KB 0 字节(304 + immutable)
index.html 重新下载 no-store,但 ETag 含版本号

具体是三处改动:

① 压缩按客户端能力逐级降级。 优先 brotli,不可用退 gzip,再不行原样输出。
这里有个容易踩的坑:目标机没装 python3-brotli(只有 C 库 libbrotli1),
最初写的降级逻辑在模块缺失时返回「压不动」,调用方理解成「原样输出」——
结果 583 KB 裸传,比优化前更慢。现在明确区分「这条路不通」和「压不动」。

② 缓存策略按「有没有指纹」分,不是按扩展名。
带 ?v=<mtime> 的(index.html 注入的)→ immutable 长缓存,浏览器第二次打开本地命中;
没指纹的(你手输 /app.js、从收藏夹进)→ no-cache,否则部署新版后你拿到的是旧文件。

③ 协商要认 q=0。 Accept-Encoding: br;q=0 的意思是「明确不要 brotli」。
子串匹配会照样发给解不开的客户端,满屏乱码。

为什么不拆包? 测了:app.js 语法编译 18 ms、顶层执行 18 ms(393 个顶层函数 / 13401 行),
JS 执行根本不是瓶颈。拆包会引入首屏 waterfall,风险大于收益,所以没动。

🆕 二、页脚显示当前版本号

页面底部现在有一行 v1.0.9。

版本号的唯一真源是 packaging/VERSION 一个文件,构建时随包落到 /opt/drouter/VERSION。
之前版本号散在 4 个构建脚本各写一个默认值 + openapi 里硬编码一处,
漏改一处就会产出「文件名是 1.0.9、control 里是 1.0.8」的包 ——
apt 按 control 认版本,表现为 dpkg -l 永远显示旧版,而且升不上去。

有个细节值得说:index.html 的 ETag 现在额外并入了版本号。
它发出去的不是文件原样,而是注入过资源指纹和版本号的产物;
只按 mtime + size 算的话,光改 VERSION 文件(index.html 一个字节都没动)时 ETag 不变,
客户端拿 If-None-Match 换到 304 就继续用旧副本 —— 页面版本号永远停在旧版本,
恰好是最该更新的地方不更新。

🆕 三、左上角 logo 点一下回系统概览

之前 logo 只是个摆设。

用的是 <a href="#dash"> 而不是 <div onclick>:Tab 能聚焦、Enter 能触发、
读屏会念「链接」。role="button" + tabindex 只能骗过一部分辅助工具,语义标签是免费的。

已经停在概览页时不重复跳转 —— go() 会停掉页面轮询再重新拉数据,
在概览页上连点 logo 会白白清掉轮询再重建,看着像「卡了一下」。概览页自己有刷新按钮。

🐛 四、VPN 四态诊断少了一态(v1.0.8 遗留)

WireGuard 状态有四态,判定顺序必须逐级降级:

已加载 + 有工具    → ready
已加载 + 没工具    → need_tool
没加载 + 有模块文件 → need_module   ← 这一级以前整个漏掉了
两头都没有         → unsupported

need_module 整级消失,后果不是「少一个状态」,而是:
模块文件明明在(/lib/modules/<kver>/wireguard.ko.xz),状态却被判成 unsupported,
前端只能配一句写死的「通常说明跑在精简容器里」——
而 PVE 的 KVM 虚机内核是有这个模块的,那句话纯误导。

根因是一次反向验证(注入 bug 看判据是否变红)被 SIGTERM 打断,
finally 里的还原没执行,注入留在了源码里,一路带进已发布的 v1.0.8。

这次一并加了防线:t-worktree-clean.py 现在会扫两种残留形态 ——
① 假路径哨兵(/nonexistent 那种),② 被注释掉的整行代码(不含任何哨兵字样,
上一轮就抓不到它)。新判据做了双向验证:4 种注入形态全部精确报到行号,
基线零误报。


📦 附件

文件 大小 说明
drouter_1.0.9_all.deb 523 KB Debian 13 一键安装包(推荐)
drouter-1.0.9-offline-amd64.tar.gz 83 MB 完全离线安装包,237 个依赖全带
drouter-1.0.9-docker.tar 169 MB Docker 镜像(177 MB 解压)
docker-compose.yml 5.2 KB Compose 编排
DOCKER-GUIDE.md 13 KB Docker 使用说明

SHA256

5eebf63edeaffebeb079106f4df2b0faed8e72c13ea38467d2c48dee12507dc6  drouter_1.0.9_all.deb
fa2affe861f6fec54d7465f22b0fcc5ae1198afdfba3b5592c21203fa26e468a  drouter-1.0.9-offline-amd64.tar.gz
c7f34f7f6c117df53b73ea9bbecb14cf54e89fe2efd0b1c114b98d31dfc73ff5  drouter-1.0.9-docker.tar
32abf4344b28c27b94a5df317139ff131224327335f0f9be39c266dad4d72d53  docker-compose.yml
941446b84f5e049d659906604d9c5fbf8b08b8e2256fad432d2aa13a50c81963  DOCKER-GUIDE.md

镜像 tar 在构建时有两个 sha256:buildah 直接导出的原始 tar 是
e565ce63…,标签是 localhost/drouter:1.0.9;上面这个 c7f34f7f… 是
上传到 Release 的那份 —— 已重标记为 drouter:1.0.9 + liuzhuohua/drouter:1.0.9,
并补上了 HEALTHCHECK(/api/health,自签证书走 curl -k)。
两者的 3 个 layer 逐字节相同(已校验),差别只在三个元数据小文件。

装法

Debian 13(推荐)

sudo apt install ./drouter_1.0.9_all.deb

完全离线(目标机不通外网)

tar xzf drouter-1.0.9-offline-amd64.tar.gz
cd drouter-1.0.9-offline-amd64
sudo bash install.sh

Docker

docker load -i drouter-1.0.9-docker.tar
docker compose up -d

浏览器打开 https://<宿主IP>:8443/,默认账号 admin / admin123,登录后请立即修改。


⬆️ 从 1.0.8 升级

sudo apt install ./drouter_1.0.9_all.deb

配置文件全部保留,升级后建议硬刷新一次(Ctrl+F5)——
浏览器可能缓存着旧的 app.js。


🔗 仓库

https://github.com/liuzhuohua/drouter · MIT License

Drouter 1.0.8 — DDNS 真实下发、页面载入亚秒级、快照可单独上锁

Choose a tag to compare

@liuzhuohua liuzhuohua released this 03 Oct 17:46

Drouter 1.0.8

Debian 13 (trixie) 软路由管理系统 —— 47 个功能页面的 Web 管理面板。

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


🔧 这个版本修了什么

6 项新功能 + 1 个真缺陷 + 3 个接口的性能优化。
快照四项都并进了「紧急救援通道」页;性能部分解决「页面载入慢」;
DDNS 那一项是把一个假的按钮和一个纯装饰的字段变成真功能。

🆕 一、快照可以单独上锁,自动清理一律跳过

面板一直有个「手动快照不参与自动清理」的全局开关。它按创建方式自动识别快照来源
(tag 以 auto 开头就算自动拍的一律保留)。但这只能整体开关,没法表达
「34 份里我要保住的就那一份」—— 于是只能全局关掉清理,或者把重要快照混在一堆里赌它不被删。

现在每一份快照右上角有个独立开关。上锁的那一份,在任何自动清理下都不会被删:

「手动快照不参与自动清理」 上锁(新)
粒度 全局,所有手动快照一起 单份,一份一份来
判据 tag 是否以 auto 开头(猜来源) 你对这一份的显式操作
生效范围 只挡按天/按数量的自动清理 同左,且跨策略生效
占「最多保留份数」配额 不占(只算自动快照) 不占,且受保护的自动快照也不占

最后一行是刻意设计的:受保护的自动快照如果照常参与 keep_count 计数,
用户设「保留 5 份」却只剩 4 份,而且没有任何提示——那几份差额去哪了没人知道。
上锁的既然反正删不掉,让它占配额等于悄悄削掉用户的一个名额。

上锁 ≠ 删不掉。 上锁挡的是自动清理,不是把快照变成只读。
但如果点一下普通删除就能把特意保下来的那份弄掉,那这个开关就是个骗人的东西。
所以删除受保护快照时后端会回一个 needs_force 标记,前端弹一次二次确认
(按钮文案是「仍要删除」而不是「确定」),确认后才带 force 重试。
审计日志里记的是「删除快照(受保护,已强制删除)」——不然事后只看得到「删了快照」,
看不出删的是你特意保下来的那一份。

兜底:快照的 _meta.json 用「先写 .tmp 再 os.replace」原子落盘。
meta 是清理逻辑唯一的判据来源,写到一半被打断会留下一个解析不了的残文件,
之后那份快照既不会被清理也上不了锁(读不到就当没保护,宁可多留一份)。

🆕 二、快照备注事后可以改

备注字段本来就有,但只能在创建时填 —— 存下来之后就只能看不能改。
于是「先随手拍一张,回头再补上说明」这个最自然的用法走不通。

现在每行备注是一个输入框,改完点「存备注」生效(刻意不做「每键输入就发请求」:
那是把一次编辑变成几十次 HTTP,而且用户按 Esc 想放弃都来不及)。
备注会进日志消息、导出文件名和 shell 片段,所以控制字符在入口就掐掉、
截断到 60 字符。

一个真实踩坑:控制字符原先是删掉而不是换成空格 ——
'a\nb\tc' 变成了 'ab c',相邻两个词被粘成一坨。
本地判据只写了「不含换行和制表符」,ab c 当然不含,照绿。
「没有危险字符」和「内容没被偷偷改掉」是两件事,
真机验收时才抓出来,已改成断言具体结果。

🆕 三、快照列表搬进「紧急救援通道」,回滚入口不再弹残缺弹窗

网络已经打崩的时候,最怕的不是没有快照,是不知道该回滚哪一份。
所以列表直接放进救援通道卡片:看得清每份的备注、体积、是否受保护,回滚按钮就在手边。

原先 listSnapshots() 有个分叉:系统设置页内嵌、其他页弹 modal。
同一个渲染函数走两条路,结果是只有系统设置页能改备注、能上锁 ——
防火墙页点「回滚」弹出来的窗里只有三个按钮,回滚哪一份只能靠猜时间戳。
现在统一内嵌,modal 分支删掉;防火墙页的「回滚」改为带你去救援通道页。

页面里也把三件容易混的事讲清楚了:手动快照(全局策略,在「自动快照」卡片里可关)、
已上锁(你对单份的决定)、以及为什么上锁的那份不占保留份数额度。

🆕 四、创建快照时可以顺手勾「创建后上锁」

留一手。切换配置前拍一张并锁上,比事后再从列表里找出来锁上可靠得多。
建完会自动清空输入框并取消勾选 —— 留着旧备注会让下一张快照顶着同一个名字,
列表里两行长得一模一样。

🔴 五、VPN 页把「模块没加载」误报成「内核不支持 WireGuard」

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

原判据 _vpn_installed() 只看两件事:

查了什么 含义
/sys/module/wireguard 存在 模块当前已加载
/usr/bin/wg 存在 工具已安装

从没查过 /lib/modules/ 下有没有模块文件。 于是「模块文件在、但没加载」这一态
被压进了 no,与「内核真没这个模块」混为一谈。

而 Debian 走 udev 按需加载 —— /etc/modules 和 /etc/modules-load.d/
默认都不写 wireguard,模块在磁盘上但没进内存。所以这台机器一直落在
「文件在、没加载」这一档,被判成「内核不支持」。

前端还配了一句写死的文案把这事坐实了:

通常说明跑在精简容器里

而这台是 PVE 上的 KVM 虚拟机。 排查方向被这句话整个带偏 ——
用户会去查容器配置,而真正该做的是 modprobe wireguard。

真实环境:内核 6.12.107+deb13-amd64,/lib/modules/.../wireguard.ko.xz 在,
未加载;wireguard-tools 也没装。

修法 —— 判据从两态扩到四态,每一态给可执行的下一步:

状态 含义 页面给出的动作
ready 模块已加载 + 工具已装 正常;缺开机自启就补上
need_module 模块文件在但没加载 一键修复(modprobe + 写开机自启)
need_tool 模块在,只缺 wireguard-tools 一键修复(apt-get install)
unsupported 内核真没有这个模块 明确说「装不了」,不给一键修复(不设套路的死路)

配套三件事:

  1. 模块文件名覆盖 .ko / .ko.xz / .ko.zst —— Debian 默认压成 .ko.xz,
    只认 .ko 会把正常机器也判成 unsupported。
  2. 新增「一键修复」按钮:确认弹窗写清三步(modprobe → 写
    /etc/modules-load.d/drouter-wireguard.conf → 装 wireguard-tools),
    明确告知「不会动网络接口、防火墙和现有 VPN 配置」。
  3. apply 改为先自愈再报错 —— 以前直接弹「内核不支持」把用户挡在门外,
    现在先尝试 modprobe,成功就直接继续。
  4. 老后端兜底为 unknown,不是把旧的 no 猜成 unsupported ——
    旧判据分不清这两种情况,猜错就等于把这个 bug 复活一次。
  5. 页面新增「运行环境」行(内核版本 / 虚拟化类型 / 是否已配开机自启),
    顺手改掉那句「不需要装任何第三方软件」→ 「不需要装任何第三方内核模块」。

验证:真机 _vpn_env() 从 need_module 修到 ready,
/usr/bin/wg 装上(v1.0.20210914),
/etc/modules-load.d/drouter-wireguard.conf 已写入 → 重启后不用再手动 modprobe。


🧪 这轮怎么验的

这一版的测试基建比功能本身更值得说。

抓出 3 条恒绿断言(判据喂了带注释的原文)

新增 41 条静态断言后做反向验证(把 bug 注入回去看它变红),逮到三条永远不会红的:

注入的 bug 为什么仍报「通过」
探测路径改成 /nonexistent _vpn_env 的 docstring 里就写着「/lib/modules 下的模块文件」
'env': _vpn_env(), 注释掉 注释写成 # 'env': _vpn_env(),,正则照样匹配
if (!confirm( → if (false && confirm( confirm 的提示文案字符串里就含 apt-get install

根因:我们习惯在注释里引用被删掉的旧代码来解释 bug 来历,
这些字面量会让「这段代码还在吗」类的判据永远成立。

修法:t-107.py 里所有静态断言统一改用 py_code_only() / js_code_only()
(先按 ast 判语义剥掉 docstring,再剥行注释)。
16 个注入用例全部被抓住。

顺带记一笔:实现 strip_docstrings() 时自己踩了两个坑 ——
用「前一个 token 是 INDENT」判docstring 会把 return {...} 里的
'virt': virt 这类普通键值也剥掉;用 tokenize.untokenize() 会把被剥处
填成续行,行结构全乱,钉死「多分支判定顺序」的跨行正则全部失配。
正解是用 ast 判语义 + 按行号区间删除,不重排不填充。

抓出一处死分支(语法完全合法、静态检查也过)

改文案时只替换了 if (d.installed === 'no') { 这一行,
旧分支的 body 与它的 } else if (!d.has_systemd) { 原样留在原地,
拼出一个「没有 systemd → 报内核不支持」的假分支。
只有对着文件读才发现。防线:断言 } else if ( 的数量恰好等于分支数。

本轮验证全表

项目 结果
_dev/t-107.py 126 → 167 项,全绿
反向验证(16 个注入) 16/16 全部被断言抓住
新增 devtools/check-vpn-render.js 36/0 —— 离线把 vpnRenderTop 按 6 种输入各渲一遍,验 HTML 与转义
前端三查 47 视图 0 异常 / 渲染 0 异常 / 手机 26-0
静态检查器 38 个分 3 批全绿
真机验收 _dev/live-108-wg.py 43/0 —— 含修好后 apply 端到端、wg0.conf 权限 0600、nft 段渲染、清理不留痕
回归 live-107.py / live-107-api.py 29/0、26/0
真机部署后基线 网关 192.168.7.2 / drouter 的 nft 表 0 张 / ip_forward=1 / dnsmasq inactive+disabled ✔

快照这轮怎么验的

新增三个检查器 + 一个真机验收脚本,全表在下面。写它们的过程比功能本身更值得记一笔。

项目 结果
_dev/t-108-snaplock.py(新增) 53 项,含 6 条反向验证(注入后必须观察到红)
_dev/t-109-node-front.py(新增) 54 项 —— 把两个 node 检查器纳入 python 回归
devtools/check-jsstrip.js(新增) 10/0 —— 剥完的 app.js 交给 V8 解析
devtools/check-snap-render.js(新增) 44/0 —— 离线渲染快照列表,验交互链路
真机验收 _dev/t-snaplock-live.sh 40/0 —— 走真实 HTTP 接口,跑完整体还原用户原有快照
全量静态回归 38 → 41 个检查器,失败 0

真机验收当场抓到 2 个本地回归漏掉的真 bug:

bug 本地为什么没抓到
备注消毒把制表符删掉而不是换空格(a\nb\tc → ab c) 判据只写「不含 \n \t」,ab c 当然不含 → 照绿
web 层无条件写 'protected': b.get('protected'),值可能是 None,于是 payload 里这个键永远在,helper 的 if 'protected' in p 恒成立,「不传就取反」从 HTTP 路上彻底失效 判据只验「透传了 protected」,无条件透传也满足 → 照绿

两条的共同形态值得记:「没有 X」和「做对了 Y」不是同一件事。
判据只验前者,绿灯就没有意义。两个都补了反向验证。

顺带修掉一个一直在骗人的既存缺陷

t-107.py 里用来剥注释的 js_code_only() 一直在吃 app.js 的真代码。
根因是它「先剥块注释、后剥行注释」,而 app.js 第 8222 行有一句讲
「怎么防止 CSS 注释被提前闭合」的行注释,注释里写着块注释的起止符字面量——
剥块注释时那句行注释还在原文里,于是被当成真注释开头,一路吃到几千行之后。

证据很硬:app.js 里块注释起始出现 169 次而结束 170 次,数量不等就是配对错位一格,
而错位会传播到文件末尾。判据读的是一份被掏空的文件,绿灯全是假的。
HEAD 版同样中招(163 / 164),所以不是本轮引入的。

换成 _dev/jsstrip.py 里的词法扫描版(正确处理模板串的插值、正则字面量、
标识符/关键字上下文),并加 node 侧把产物交给 V8 解析做交叉验证。

这一版还有 4 处判据写错导致假红,形态都是「脚本假设了一个修好之后
就不成立的前提」。最典型的一条:判「旧的按页分叉已删」时写成
「整个 app.js 里不许出现 S.page === 'sys'」—— 可这文件里
S.page === 'xxx' 有几十处(菜单高亮、切页清理)。数出 1 处后,
判据既不能说明是哪儿,也没法告诉人该改什么。
教训:判据要指向具体那一段代码,不是扫全文。

⚡ 六、页面载入慢:三个接口从「十秒级」降到「亚秒级」

用户反馈「DDNS、打印服务、页面载入缓慢」。查下来是同一个病:
慢查询串成一条线,且探测结果不缓存。

接口 改之前 改之后(热缓存) 改之后(冷/强刷)
/api/pubip 10.2 s 0.002 s 5.5 s
/api/ddns 7.1 s 0.003 s —
/api/print 4.3 s 0.08–0.25 s 4.3 s

(真机 192.168.7.3 实测。前端切页是并发的,页面载入时间约等于最慢的那个接口,
所以「页面卡 10 秒」就是 /api/pubip 这一个接口造成的。)

三处根因:

  1. 回显服务串行探测。_echo_public_ip 原来逐个试 5 个 IPv4 + 4 个 IPv6
    回显服务,每个 timeout=6。全部失败时就是 30 秒起步。
    这台机器上实测 api.ipify.org 立即失败、cloudflare 满 3 秒才超时 ——
    串行下这些白花的时间全是白等。改成线程池并发、谁快用谁。
    traceroute 与 mtr 同理(原来是「先 traceroute 25 秒,失败再 mtr 25 秒」)。

  2. 进程内缓存在这个架构下等于没写。helper 的每个动作都是 fork 出去的一次性
    进程
    ,_PUBIP_CACHE 这种模块级变量每次都是空的,20 秒的进程内缓存
    在这里一点用都没有。三个缓存全部落盘到 /run/drouter/:
    公网 IP(TTL 5 分钟)、两块诊断证据(30 分钟)、lpinfo 设备列表(2 分钟)。

  3. lpinfo -v 是打印页载入慢的全部原因。实测单项 4.21 秒
    (lpstat -p 只要 0.05 秒)—— lpinfo 是 cups 的网络发现扫描,
    它会真的去扫局域网里的 631 端口,慢是它的固有成本,不是异常。
    现在五路查询(dpkg-query / lpstat -p / lpinfo -v / lsusb / ss)一起并发。

⚠️ 有一条判据是这一轮才补的,因为它对应的 bug 骗了我很久:
/api/pubip 缓存命中时返回了 2 元组,调用方按 3 元组解包 → ValueError
被 except 吞掉 → 设备列表变空、devices_timed_out=true。

而耗时只有 0.08 秒 —— 「快」和「对」是两件事,只看耗时永远发现不了。
现在判据会真的调一次缓存命中分支,断言设备数来自缓存内容。

另外「打印页只慢在 lpinfo」这个判断也是错的:_print_usb_printers() 和
_print_listen_now() 串在它后面就是 4.2s + 0.2s。优化只做一半最容易发生,
因为「看起来慢的那一条」通常只有一条。

还有一条:判断「命中缓存后更快」这种判据在单测里必然恒绿
(替身都是本地 sleep,缓存省的是外部网络往返,量不出来)。
已改成只判「探测函数有没有被调用」,耗时收益由真机测速覆盖。

🆕 七、DDNS 真的会更新了,「检测间隔」也真的在跑

这一条是用户报出来的:「检测间隔(秒)」这个字段,全仓 grep 不到任何定时器消费它。
纯装饰。设了不会自动更新,不设也照样不更新。

排查下去发现不止是字段的问题 —— 「立即更新」按钮也是假的。
它不调任何服务商接口,只打印一份「更新计划」就结束了。
用户的 DNS 记录纹丝不动,而页面显示一切正常。

这一版把整条链路做通了:

1.0.7 1.0.8
「立即更新」 打印一份更新计划 真的向服务商发请求,逐条显示成功/失败原因
「检测间隔」 纯装饰,无人消费 驱动 systemd timer,改完立刻重写生效
地址没变时 — 不下发(IP 没变不敲 API,避免触发服务商频率限制)
服务商失败时 — 3 倍退避递增、上限 1 小时;退避期内秒退不探测外网

接入 7 家:自定义 URL(模板占位符 {ip} {ipv6} {domain} {sub} {token} {user} {pass})、
DNSPod、阿里云、Cloudflare、No-IP、DynDNS、DynDNS2。

华为云 / 腾讯云如实报「暂未接入」,并建议改用阿里云 / DNSPod / Cloudflare / 自定义 URL。
这是刻意的:签名算法没被真机验证过,写一份「看起来对」的实现比不写更坏 ——
用户会以为配好了,实际每次都失败,而失败原因还藏在看不懂的地方。

这轮最关键的技术点:这类接口的共同点是用 HTTP 200 回一句 error 来表达失败。
只判 HTTP 层的话,页面显示「更新成功」,DNS 记录纹丝不动,
用户只能一次次检查自己的密钥,而明明是请求被拒了。

所以每家都要单独判业务成功:DNSPod 看 status.code == '1'、
阿里云看 Code == '200'、Cloudflare 看 success。
顺带把「部分成功」从「全成功」里拆出来 ——
IPv4 通了 IPv6 没通,显示成「更新成功」是骗人。

还有个真实的坑:Cloudflare 返回 81057(记录已存在)不是失败,
当成失败会让用户以为出事了。

真机验收(Debian 13,192.168.7.3):

  • timer 正常生成/启用/运行,OnUnitActiveSec 跟随用户设定的秒数
  • 地址未变 → unchanged,不下发(不白敲服务商接口)
  • 人为制造「地址变了」→ 真的下发 → 失败原因如实记录并进入退避
  • 退避期内第二次执行 0.136 秒返回(首次 4.3 秒),确认没有白探测外网
  • 验收后已恢复出厂默认,临时单元与状态文件已清理

产物校验(发版前最后一道关,1.0.6 靠它揪出 PPPoE 那三个问题):

检查 结果
t-*.py 全套 53 个全绿 ✔(新增 t-111-ddns-push.py)
静态检查器 43 个全绿 ✔
前端四查 47 视图 0 异常 / 渲染 0 异常 / 手机 26 通过 / VPN 渲染 36 通过 ✔
markdown 死链 178 个本地引用 0 死链 ✔
deb 解包:本轮改动是否进包 _vpn_env / WG_AUTOLOAD / _vpn_fix_op / 前端 vp-fix / unknown 兜底 全部命中 ✔
deb 内 helper + app.js sha256 与本地...
Read more

Drouter 1.0.7

Choose a tag to compare

@liuzhuohua liuzhuohua released this 02 Oct 18:58

Drouter 1.0.7

Debian 13 (trixie) 软路由管理系统 —— 47 个功能页面的 Web 管理面板。

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


🆕 这版本加了什么

三个新模块,全部带定时器守护(drouter-backupd / drouter-alertd / drouter-quotad):

页面 做什么
配置备份与还原 把全部设置导成一个能下载、能换机还原的包:带清单与逐文件 sha256 校验,还原前先预检(dry_run),敏感项默认排除
告警与通知 掉线 / 磁盘满 / 温度高主动推送:Bark · 邮件 · 群机器人,带冷却期与免打扰时段
用量与账单 按设备与服务统计每月流量,工作室可按比例分摊话费;后台增量聚合,不拖慢机器

⚠️ 三个守护的 systemd timer 不在 deb 里,是首次保存配置后由 helper
动态写出来的(/etc/systemd/system/drouter-backup.timer 等 6 个单元)。
这是有意设计 —— 没开的功能不该有定时器。看到timer「不存在」是正常的,
进对应页面保存一次配置就会生成并 enable。


🔧 这个版本修了什么

本轮共修 10 个真缺陷。前 7 个是自查抓的,后 3 个是真机验收才暴露的 ——
静态检查全绿、函数级测试 29/29 全过,接口一打就红了。

🔴 一、默认备份静默丢失大半配置(最严重)

_bk_files() 里写了 sensitive=(d == '/etc/drouter'),把整个 /etc/drouter
标成敏感 → 默认导出时整目录被排除。而 BACKUP_SCOPE 恰恰宣称备份含
dcfg.json / kern.conf / generated/vlans.json / Docker 栈文件 / 自定义主题。

界面全程显示「已导出」,零提示。用户导出 → 换机 → 还原 → 才发现大半配置没了。

修法:敏感判定改成逐文件属性(_bk_is_sensitive() 按 BK_SENSITIVE
精确判定),目录只是容器。

教训:目录级一刀切是最省事的写法,也是最贵的写法。

🔴 二、备份包名校验比命名规则窄 → 导出的包自己都认不出

命名带主机名后缀(工作室里常有好几台设备,包名分不清是谁的):

命名:re.sub(r'[^A-Za-z0-9_-]', '', hostname)[:24].lower()
校验:^drouter-backup-\d{8}-\d{6}(-[a-z0-9]+)?\.tar\.gz$

Debian 默认主机名是 debian-primaryrouter —— 带连字符,而校验正则
既不认 - 也不认 _。于是导出的包在列表里看不到、永远还原不了,
界面还显示「已导出」。

修完才发现 _bk_list_entries() 里还有第二份同样的正则 —— 改了一处、漏了另一处。
现已让它直接调用 _bk_safe_name(),并加断言「这份正则字面在 helper 里只允许存在一份」。

教训:同一份正则只允许存在一份,第二处必须是调用。
另:测试用例要照真实世界的值造 —— 我原来只用了 debian(无连字符),
凭想象的最简单值恰恰最容易漏掉真实形态。

🔴 三、包内容校验拿不到 sha256 → 所有文件都被标成「已损坏」

_bk_inspect() 把 manifest 条目重建成 row(给前端渲染用)时漏了 sha256,
下一行又拿 row.get('sha256') 去比实际哈希 → 恒为 None ≠ 实际哈希 →
20 项全部 state='已损坏',还原被「内容与清单不符」挡死。
刚导出来的新包立刻报损坏。

教训:数据要从来源一路带到使用点,中间任何一次「重新构造字典」都会悄悄丢字段。

🟠 四、清单重叠 → 同一个文件进包两次

SNAPSHOT_ETC_FILES 含 /etc/drouter/rescue.conf,SNAPSHOT_ETC_DIRS 含
/etc/drouter 整目录 → 展开后同一个 arcname 写两遍 tar。
后者覆盖前者且 tarfile 不报错,表现是 file_count / total_bytes
虚高一倍、inspect 里同一文件两行。修法:_bk_create() 里按 arcname 去重。
真机证据:修复前 22 项、修复后 20 项。

🟠 五、VPN 私钥通过状态接口外泄

_vpn_status() 的返回字典里带了 'raw': d —— 而 d 是完整配置,
含 WireGuard 私钥。界面上「私钥」这个框在接口响应里是明文。
现已把 raw 从返回载荷去掉,另有「私钥只在本机文件里、接口只回 has_key 标志」
的第二道防线。

🟠 六、ip_network 在 Python 3.13 判定变了 → IPv6 网段算错

3.12 与 3.13 对某些 ip_network 入参的严格性不同,导致网段合法性判定
在 3.13 上误判。镜像里必须跑 3.13 才验得住(现在 smoke 脚本会检查镜像 Python 版本)。

🟠 七、wireguard-tools 没登记进依赖清单

helper 里会调 wg 命令,但 DEPS 三处(helper / make-offline-deps.sh / deb control)
都漏了它 → 一键安装后 VPN 页「看起来能配,一用就报错」。已补齐。

🟠 八~十、本轮真机新抓的三条

  • /etc/ppp/peers/drouter-wan 是否敏感:查证后确认不是(它只有 user
    与拨号参数,密码由 render 单独写进 chap-secrets / pap-secrets,两者都已排除,
    并配 hide-password)。这里修正的是验收判据而不是产品 ——
    判据按目录一刀切会误报,按文件才准确。
  • 包内清单位于 inspect 接口,不在 list 的每个 pack 上(那里只有
    name/size/mtime/note 四个字段,是给列表页渲染用的)。
  • 另修正验收脚本自身的一批问题(挪开配置会改变被测对象、op 名与字段名要照真机源码抄、
    token走 X-Token 请求头不是 Cookie、提示字段是 msg_cn)——
    这些全是「测试的错」不是「产品的错」,但不改就验不出真问题。

🧪 这轮怎么验的

  • 新增 _dev/t-107.py(126 项) —— 备份 / 告警 / 配额三模块的纯逻辑深测:
    路径穿越、tar 炸弹上限、敏感排除、配额聚合幂等、VPN 私钥生命周期。
    本轮新增的 7 条断言全部做过反向验证(把 bug 注入回去看它变红,还原后变绿)——
    没反向验证过的断言可能因为抽取失败而恒绿。
  • 新增 _dev/live-107.py(29 项) —— 真机函数级验收。
  • 新增 _dev/live-107-api.py(26 项) —— 真机 HTTP 接口层验收:
    真的登录拿 token、真的 POST、真的看返回体字段。后 3 个缺陷就是它抓出来的。
  • 静态 42 个检查器全绿(分批跑,本机一口气跑 40+ 会被 SIGTERM)。
  • 前端三查:47 视图 / 0 异常 / 手机适配 26 通过 0 失败。

产物校验(发版前最后一道关,1.0.6 就是靠它揪出 PPPoE 那三个问题):

检查 结果
deb 解包抽查:三个新守护在 backend/ 且字节数与源一致 ✔
deb 内 helper sha256 与本地一致 86524421… ✔
deb 内含本轮两处修复(a-z0-9_- 校验正则 / seen_arc 去重) 各 11 / 3 处 ✔
离线包 sha256sum -c(238 项) 238/238 通过 ✔
三件产物远端↔本地 sha256 逐一对齐 ✔
真机部署后基线零变化 网关 192.168.7.2 / nft 0 行 / dnsmasq inactive+disabled / rescue inactive / ip_forward=1 ✔
podman 构建留下的 table inet netavark 已 dump 后删除,还原基线 ✔

📦 下载

文件 大小 用途
drouter_1.0.7_all.deb 约 500 KB 本体,装到已有 Debian 13 上
drouter-1.0.7-offline-amd64.tar.gz 约 83 MB 自包含离线包(237 个依赖),断网也能装
drouter-1.0.7-docker.tar 约 169 MB 容器镜像,一条 docker load 就能跑
docker-compose.yml 约 5 KB 容器编排模板
DOCKER-GUIDE.md 约 13 KB 容器形态使用说明
# 离线包(推荐:目标机断网也能装)
tar xzf drouter-1.0.7-offline-amd64.tar.gz
cd drouter-1.0.7-offline-amd64
bash install.sh

# 或者只装本体
sudo apt install ./drouter_1.0.7_all.deb

⚠️ 升级前请看

  1. 配置格式没变,升级后原有配置、主题、快照、证书全部保留。
  2. 本次不改动任何网络配置,升级过程中面板会短暂 502 几秒(重启 Web 服务)。
  3. 三个新模块首次使用请先保存一次配置,对应的 timer 才会生成并 enable。
  4. 备份包的默认不含 CA 私钥、WireGuard 私钥、宽带密码(chap-secrets)。
    换机还原后这三样需要重新生成或重填 —— 这是有意的默认值,
    界面上会明确列出被排除了哪些项。

🔗 仓库

https://github.com/liuzhuohua/drouter(MIT)
有问题请开 issue,遇到具体故障请附上「日志」页的相关条目 ——
这一版修的绝大多数问题在日志里都是能找到线索的。

v1.0.6 静默失效专项:六个模块的保存按钮从来没生效过

Choose a tag to compare

@liuzhuohua liuzhuohua released this 02 Oct 14:00

Drouter 1.0.6

Debian 13 (trixie) 软路由管理系统 —— 43 个功能页面的 Web 管理面板。

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


🔧 这个版本修了什么

1.0.6 是一次「静默失效」专项排查。没有新增功能,没有改动任何配置文件
格式,升级后你原有的配置、主题、快照、证书全部原样保留。

这一轮的主题是一类特定 bug:界面提示成功、配置写盘成功、服务照常运行,
但实际什么都没发生。
它们比崩溃更难发现 —— 崩溃你会看到报错,
静默失效你只会觉得「这软件真不靠谱」,却永远查不到原因。

排查方式是逐个菜单、逐个功能过一遍,重点是家庭宽带上网必经的
IP / DHCP / DNS / PPPoE 链路,外加日志、监控、依赖三类基础能力。
共修掉 30 处,其中 4 个 P0。

🔴 一、六个模块的「保存」按钮从来就没生效过(最严重)

访问控制、文件共享、Docker、动态域名、主题、流日志 —— 这六个页面的
保存操作,从 1.0.5 之前就一直是废的。

原因是路由分发顺序:readings 字典同时匹配 GET 与 POST,而它位于所有写
路由之前。结果是 POST 请求被它先吃掉,直接返回了读出来的数据 + ok:true。
前端看到 ok:true 就弹「保存完成」—— 看着一切正常,配置文件一个字节没动。

这类 bug 用「路径在文件里出现过吗」这种静态比对永远查不出来,
因为每个模块的代码都在、逻辑都对,只是永远执行不到。
所以这轮专门写了一个检查器(_dev/t-apicheck.py,169 项),
除了比对字段名,还要检查**「谁先 return」**。

🔴 二、PPPoE 拨上号了但全家断网

usepeerdns 只把运营商下发的 DNS 写进 /etc/ppp/resolv.conf,
而 dnsmasq 读的是 resolv-file 指向的另一个文件
(/etc/drouter/generated/isp-dns.conf)。两者之间没有桥。

结果:DNS 模式选「仅运营商」或「两者合并」时,dnsmasq 的上游是空的 ——
PPPoE 状态栏显示已连接、网页打不开、ping 得通域名解析不了,
而且日志里什么都没有。

修法是给 pppd 挂 ip-up-script / ip-down-script 钩子,新增
/opt/drouter/scripts/sync-isp-dns.sh 把运营商 DNS 同步过去;
内容没变就不 reload(拨号/断线事件很频繁,每次都重启 dnsmasq 会把
DNS 缓存全清掉,表现为「网页时快时慢」)。

打包校验时又揪出这个修复自己的两个问题,一并修掉:

  • 脚本装错位置:初版由 deploy.sh 用 heredoc 写进 $OPT/bin/,
    而 deb 与镜像都只把 scripts/*.sh 装到 $OPT/scripts/ ——
    结果装 deb、跑容器的机器上 ip-up-script 指向一个不存在的文件,
    断网照旧。现在脚本是 scripts/ 下的实体文件,三条部署路径共用一份。
  • 容器里重载不掉:脚本只 systemctl reload dnsmasq,
    容器形态没有 systemd,等于什么都不做。改为检测不到 systemd 时
    回退 kill -HUP $(pidof dnsmasq)。
  • 写盘失败仍返回 0:pppd 只看 ip-up-script 的退出码来决定要不要
    记日志。原来目录不可写时脚本照样 exit 0,等于把「DNS 没同步」
    这件事彻底藏起来。现在写临时文件或 mv 失败一律非零退出。

真机实测四条路径:拨号成功 → 两条 nameserver 落盘 rc=0;重复执行 → 内容
不变 rc=0(不重载 dnsmasq);断线 → 写占位注释 rc=0;目录不可写 → rc=1。

🔴 三、紧急救援通道等于零凭据

救援通道的用途是「主面板锁死时唯一的救命入口」。它开启时会把
169.254.0.1/16 挂到每一张物理网卡上 —— 于是「救援网段内的主机」
实际上等于你局域网里插着网线的任意一台机器。
而还原操作的确认短语是硬编码在页面源码里的常量。

也就是说:局域网里任何一台机器,打开一个网址、输入一个页面上看得见的短语,
就能回滚你的全部配置并重启路由器。而这条通道存在的意义恰恰是
「主面板已经锁死」的时候。

现在改成:开启即生成一个 6 位数字令牌(每次开启都换),页面上所有写操作
必须带上它;令牌猜错 10 次锁 60 秒。令牌存在 0640 root:drouter 的
rescue.conf 里。

⚠️ 升级注意:如果你之前开过救援通道,旧配置里没有令牌,
现在会被直接拒绝(这是有意的 —— 不能让「升级」本身变成开后门)。
请到主面板把救援关掉再打开一次,记下新令牌。

🔴 四、SNMP 字段能注入,把只读团体名升级成可写

SNMP 的 community 字段有 [\s#] 校验,但同一处的
sysname / location / contact 只截了长度。这三个字段被原样拼进
snmpd.conf 的 sysName %s 之类的指令行 —— 在 sysname 里塞一个换行,
就能凭空插出一条 rwcommunity 指令,把只读团体名变成可写,
之后就能改路由、改接口、重启设备。

三个字段现在一律过滤换行、回车与 #,截长 128。

🟠 五、「仅语法检查」其实偷偷保存了

防火墙页的「仅语法检查」,前端只传了 live: false。但 live=false
只代表写盘但不生效,不代表不写盘 —— 点一次预检,
配置库就真的被改了,而你以为只是看看。

现在前端显式传 check_only: true,后端也加了对应的分支:
预检只拿「这次提交的这份」去渲染,一个字节都不写进配置库。
真机上验过:预检前后配置库逐字节一致。

🟠 六、其余 26 处

展开查看

静默失效类

  • 静态租约转了等于没转:写入时用 host 字段且没有 enabled,
    而渲染器读的是 name + enabled —— 整条被 v_bool 过滤掉,
    dnsmasq 里一条 dhcp-host 都没有,但界面提示「已转为静态绑定」。
  • UPnP 网段永远是 192.168.0.0/16:渲染器从 system.lan_address
    取网段,而 apply 与 preview 两条路径都只注入了 ext_iface。
  • 构建保护模式能被回滚绕过:回滚与其它只读操作一起无条件放行,
    于是在保护模式下回滚会 restart dnsmasq / radvd / dhcpcd ——
    正是这个模式要防的污染源。现在只放行「不重启服务」的那一半。
  • 「系统」页改了访问范围却找不到保存按钮:该页提示
    「请点击保存并应用」,但它没登记在 PAGE_MODULES 里,按钮压根不存在。
  • 通用保存成功后从不清除脏标记:20 多个页面保存后 PAGE_DIRTY 永久
    残留,之后每次进页面都拿旧草稿盖掉刚从库里读回来的新值。
  • 端口转发切页丢草稿:编辑只写 S.pfRules,重进页面从配置重建。
  • DHCP 池三项校验只在向导路径有,从页面直接存能绕过 ——
    池里圈进本机 LAN 地址时表现为「网络时通时不通」,且没有任何日志。
    校验已提到渲染层,两条路径都拦。
  • 多拨的服务名没白名单,含一个引号就能闭合引号续写 pppd 指令。
  • 多拨的 MRU 被静默忽略,界面填的值根本不生效(部分线路确实需要 mru < mtu)。
  • 救援放行规则只加不删,改端口再关闭后旧的 accept 规则永久残留。
    现在走独立 chain,关闭时按 handle 删干净。
  • 网卡的 local= 域写死 /lan/,不跟随你设置的域名。

资源与性能类

  • 流日志清理是 OOM 杀手:原来 f.read().splitlines() 一次性读进内存,
    再建第二个列表。保留 20 万行 × 约 400 字节 ≈ 160MB 峰值,
    而这个函数每 60 秒跑一次。4GB 的小机器上这是致命的。
    改成流式:先只数行(不驻留内容),没超上限一个字节都不用改,
    超了才逐行写临时文件再原子替换。
  • 仪表盘卡死十几秒:/api/metrics 每 2 秒轮询一次,
    而延迟探测在指标锁内跑 ping(超时 10 秒)—— 目标不可达时
    所有指标请求全部堆着。改成锁内只读缓存,ping 锁外跑。
  • 总流量大约翻倍:br0 的 RX/TX 是成员口之和,
    和成员口一起累加就重了。网桥口现在跳过。
  • 日志采集缺磁盘将满保护(快照守护早就有,日志守护漏了):
    根分区将满时归档失败被 except 吞掉,日志静默停更。
  • DHCP 页定时器泄漏:整体重渲染时新闭包覆盖了 stopLeaseTimer,
    旧 interval 的句柄永久丢失 —— 每加 10 条自定义 Option 就泄漏一个
    10 秒轮询,持续空打后端。
  • 清空输入框会提交 0:Number('') === 0,所以清空 MTU / MRU / 租期
    再保存会写成 0。新增转换器:空串一律当「没填」,让后端默认值生效。
  • 禁用 DNS 缓存的开关失效:cache-size=0 被 or 1000 吞成 1000。
  • 字符串 'false' 被当成 True:is not False 对字符串一律真,
    改成统一的开关判定函数。

🧪 这轮怎么验的

  • 新增 _dev/t-apicheck.py(169 项) —— 前后端 API 契约 + 分支可达性。
    除了比对字段名,还会解析 p.startswith() 前缀分发、元组多路径路由、
    整包透传,并检查分支的到达顺序(就是靠它抓住了六个模块的遮蔽)。
  • 新增 _dev/t-audit106.py(83 项) —— 覆盖本轮全部修复。
  • 新增 _dev/live-audit106.py —— 真机验收,重点验六个曾失效模块的
    写分支是否真的可达、check_only 是否真的不写库、救援令牌是否真的拦得住。
  • 离线回归 35 个测试全绿;真机(192.168.7.3)验收全通过。

真机上另外手工验了救援通道的完整生命周期:
开启拿到令牌 → 无/错令牌 403 → 正确令牌 200 → 正确令牌 + 错确认短语被拒不执行
→ 连错 10 次锁 60 秒 → 关闭后令牌清空、nft chain 回收、虚拟地址回收、服务停。

产物校验(这三件是发版前最后一道关,本次就是靠它揪出上面 PPPoE 那三个问题):

检查 结果
deb 内 scripts/sync-isp-dns.sh 位置与权限 ./opt/drouter/scripts/ 0755 ✔
离线包 sha256sum -c(237 项) 237/237 通过,退出码 0 ✔
包内 deb 与独立产物 cmp 完全一致 ✔
镜像 layer 逐层 diff_ids(3 层) 逐层不变(流式重写未损坏层) ✔
镜像新标签 drouter:1.0.6 + liuzhuohua/drouter:1.0.6 ✔
镜像 license 标签 MIT ✔ / healthcheck 已补 ✔
三件产物远端↔本地 sha256 逐一对齐 ✔
容器冒烟(--network none) 24/0 全绿 ✔
宿主安全基线 ip_forward=1、默认路由不变、5900 未动、podman 的 table inet netavark 已回收 ✔

📦 下载

文件 大小 用途
drouter_1.0.6_all.deb 约 453 KB 本体,装到已有 Debian 13 上
drouter-1.0.6-offline-amd64.tar.gz 约 83 MB 自包含离线包(237 个依赖),断网也能装
drouter-1.0.6-docker.tar 约 169 MB 容器镜像,一条 docker load 就能跑
docker-compose.yml 约 5 KB 容器编排模板
DOCKER-GUIDE.md 约 12 KB 容器形态使用说明
# 离线包(推荐:目标机断网也能装)
tar xzf drouter-1.0.6-offline-amd64.tar.gz
cd drouter-1.0.6-offline-amd64
bash install.sh

# 或者只装本体
sudo apt install ./drouter_1.0.6_all.deb

⚠️ 升级前请看

  1. 救援通道令牌:开过救援的老配置需要关掉再打开一次生成新令牌,
    否则救援页会拒绝一切写操作(有意为之)。
  2. 配置格式没变,升级后原有配置、主题、快照、证书全部保留。
  3. 本次不改动任何网络配置,升级过程中面板会短暂 502 几秒(重启 Web 服务)。

🔗 仓库

https://github.com/liuzhuohua/drouter(MIT)
有问题请开 issue,遇到具体故障请附上「日志」页的相关条目 ——
这一版修的绝大多数问题在日志里都是能找到线索的。

Drouter 1.0.5 — 堵提权/注入、资源收口、低配置性能水位

Choose a tag to compare

@liuzhuohua liuzhuohua released this 02 Oct 02:26

Drouter 1.0.5

Debian 13 (trixie) 软路由管理系统 —— 43 个功能页面的 Web 管理面板。

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


🔧 这个版本修了什么

1.0.5 是纯修复与加固版本:没有新增功能,没有改动任何配置文件格式,
升级后你原有的配置、主题、快照、证书全部原样保留。

1.0.4 清完了「假输入框 / 隐身字段」。这一轮把前后端再过一遍,主题是三类:
能被利用的安全缺陷、会慢慢吃掉资源的生命周期问题、低配置机器(2C/4G)上的性能水位。

🔐 一、改个密码,就能顺手拿到 root(本轮最严重)

chpasswd 是按行读输入的:面板把「用户名:密码」喂给它时,密码里只要
夹一个换行,第二行就会被当成另一条记录执行 —— 写一行 root:新密码,
root 就被改密了。现在用户名与密码一律拒绝换行、回车、NUL 与控制字符。

同一批收口的提权/注入面:

  • 删除用户:从「写死保护 root/ajeef 两个名字」改成「保护全部 UID<1000
    的系统账号」—— 原先可以把 daemon、www-data 删掉。
  • WOL 唤醒:接口名加白名单、目标地址强制 IPv4 校验,广播地址的获取
    不再经过 sh -c 管道(原先接口名原样进 shell)。
  • ACL 的 hosts 字段:从「含冒号就算 IPv6」这种弱判断改成 ipaddress
    严格校验 —— malformed 的地址会直接变成 nftables 规则里的一行。
  • 存储挂载点:限定在 /mnt / /media / /srv 之下、拒绝空格与特殊
    字符。原先 target=/etc 会把 U 盘直接盖到 /etc 上,系统立刻不可用。
  • 文件管理器:拒绝清单从文件级扩到目录级(.ssh、/etc/sudoers.d、
    /etc/pam.d …),再加一道按文件名的拦截(authorized_keys、shadow、
    sudoers …)。原先 .. 绕一层就能把公钥写进 root 的 authorized_keys,
    等于给自己开 SSH 后门。

🕳️ 二、三个页面:切走再回来,未保存的改动被静默冲掉

访问控制、文件共享、DDNS 三个页面,进入时都会无条件用服务端数据
覆盖整个表单状态。你改了一半切去看别的页,再切回来 —— 草稿没了,
没有任何提示。这是全站唯一真正不可逆的丢数据路径。

现在页内任何改动都会登记为「脏页」:再进页面时保留你的编辑并在动作条
明确提示,保存成功后才清除。DDNS 页的做法更细:表单用草稿渲染,
公网检测等状态区仍然用实时数据。

🧩 三、两个「改了等于没改 / 点了不是那回事」

  • LAN 页的「网关地址 / 域名后缀」:这两个值实际由 dnsmasq 下发给客户端,
    页面却把输入写进了谁也不会读的 system 模块死键 —— 能填、能保存、
    永远不生效。现在与 DHCP 页的 Option 3 同源同写。
  • UPnP 的「刷新映射表」按钮:原先它会顺手把 miniupnpd 服务启动
    (一个「查询」按钮产生「启动服务」的副作用),然后展示一句静态提示,
    映射表本身从来不会出现。现在走真正的只读接口 read:upnpmap:
    服务没开就如实告诉你没开,开了就列出真实的映射表。

🧵 四、PTY 终端的三条命:缓冲、会话数、空闲回收

Web 终端是长期挂着的会话,三个慢漏一次性修掉:

  • 缓冲区无上限:ping -f 或刷屏命令挂一晚,内存就涨一晚。
    现在每个会话 8MB 封顶,超出丢最旧(并统计丢弃量)。
  • 会话数「先查再用」:检查与创建之间有个窗口,并发连接能越过上限。
    现在用信号量把创建路径真正卡住,满了直接回 BUSY。
  • 空闲回收看运气:原先只在「新连接发现满了」那一刻才顺手清空闲会话。
    现在有独立的后台线程每 60 秒扫一次;离开终端页面时前端也会主动断开
    会话,不再白占 30 分钟。

顺带把超时的计时时钟从 time.time() 换成 time.monotonic() ——
系统时间被 NTP 校准跳一下,原先的空闲判定会集体错乱。

📊 五、Web 服务的并发与内存水位

daemon_threads=True 的意思只是「主进程退出时不用等它们」,不是限流 ——
原先每个请求一条线程、没有上限。现在:

  • 工作线程 48 封顶,超出排队;监听 backlog 显式调到 64;
  • 请求体 16MB 封顶(原先一个巨大的 POST 就能把内存顶爆);
  • 登录会话表 500 封顶,过期判断改用单调时钟(系统时间一回调,
    原先的会话会集体「永生」或集体过期);GC 遍历加了快照与异常兜底,
    不会再因为一个 KeyError 整条 GC 线程死掉;
  • 审计表 2 万行封顶自动裁剪;登录失败计数表 2000 条封顶;
  • helpd 并发动作 6 个封顶,状态统计表定期清理。

💾 六、日志、快照与下载:不再能撑爆磁盘/内存

  • helper 自身的运行日志 32MB 封顶自动轮转(原先无界增长);
  • 自动快照执行前检查剩余空间,不足 1GB 直接跳过并写清原因
    (原先磁盘快满时照打,把最后一点空间也吃掉);
  • 文件管理器的下载封顶从 512MB 收到 20MB —— 512MB 本来就超过
    下载通道 32MB 的单条响应上限:大文件只会静默失败,而守护不在时
    的回退路径又会把约 700MB 的 base64 输出整个读进内存。现在如实报错
    并指引用 SFTP/SCP。

⚡ 七、低配置机器上的性能水位

  • /api/config:进任何页面都要调一次,原先逐个模块开一次 SQLite
    (16 次开库/PRAGMA/关库),现在一次连接取回全部;
  • 磁盘清理状态页:原先每进一次页面就把 /var/log 等目录全量 os.walk
    一遍
    ,机械盘上秒级。现在 30 秒内复用上次扫描,保存配置或执行清理
    后立即失效;
  • gzip 压缩缓存从「只限条目数」加上 24MB 字节闸,16 个大响应不再能
    占掉上百 MB 常驻内存;
  • 浏览器标签页在后台时:心跳暂停、终端轮询降频到 1.5 秒;
  • 日志归档去重改成逐行流式比对(原先整个读进内存),并加了文件锁,
    定时任务和手动触发不会再互相踩。

🧱 八、断电安全与 systemd 资源闸

  • 11 处「直接 open(w) 写配置」全部改成原子写(同目录临时文件 +
    os.replace):ACL、共享、ddns 预配置、打印、opensoho、快照策略、
    救援通道、networkd 迁移…… 断电或被 kill 的瞬间不会再留下空文件;
  • drouter-rescue 与 drouter-snapshot 两个单元补上 MemoryMax
    (150M / 300M)—— 连同 helper 保存策略时重建单元的那处模板,
    三处同步,不会再出现「deb 里有闸、deploy.sh 装的没闸」。

🩹 九、杂项逻辑修复

  • DHCPv6「停用」不停用:enabled=false 时 dhcpcd 配置里仍然输出
    实际配置行,现在停用了就只留注释占位;
  • 租约回收改用 dhcp_release(dnsmasq-utils,已登记进依赖自检清单;
    未安装时自动退化为删租约文件 + 重载,并如实告诉你是哪种方式);
  • VLAN 只删自己创建的接口:原先按名字匹配,apply 可能把用户
    手工建的同名 VLAN 删掉;现在自建清单落盘,删之前先核对,跳过的
    会逐个列出;
  • 公网探测启动前清掉上次的 .hits/.icmp/.port 结果文件 —— 原先
    一次探测成功后结果文件永久残留,页面一直显示「外网可达」;
  • 快照根目录限定白名单前缀(env / 数据库 / 默认值三条路径来源都查);
  • 前端 201 处未转义的 value="${...}" 统一补 esc();href 一律只放行
    http/https;主题导出注释同时剥 /* 与 */(1.0.4 只剥了 */,
    用 /* 照样能闭合注入);
  • 重新登录后心跳定时器不再叠加(原先登录几次就有几个 60 秒心跳);
  • keep_days / keep_rows 不再被 || 吞掉 0。

📦 下载哪个?

文件 大小 用途
drouter-1.0.5-offline-amd64.tar.gz 83 MB ⭐ 推荐。目标机完全不需要外网,一个包装完
drouter_1.0.5_all.deb 445 KB 目标机有网(或内网 apt 镜像)时用,依赖交给 apt 去下
drouter-1.0.5-docker.tar 169 MB 想快速体验界面、不想装系统时用(不建议当主路由)
docker-compose.yml 5 KB Docker 编排文件,配合上面那个 tar 用
DOCKER-GUIDE.md 13 KB Docker 形态的完整说明(含常见问题排查)

从 1.0.0 / 1.0.1 / 1.0.2 / 1.0.3 升级:sudo apt install ./drouter_1.0.5_all.deb
直接覆盖安装,配置全部保留;或者用离线包解压后跑 sudo bash install.sh。


🐳 Docker 快速体验

# 1) 加载镜像(得到 drouter:1.0.5)
docker load -i drouter-1.0.5-docker.tar

# 2) 用附带的 compose 文件起
mkdir -p ~/drouter && cd ~/drouter
#    把 docker-compose.yml 放到这里
docker compose up -d

# 3) 等健康检查通过
docker compose ps

访问 https://<宿主IP>:8443/,账号 admin / admin123。

不想用 compose 的话,等价的 docker run:

docker run -d --name drouter --restart unless-stopped \
  --network host \
  --cap-add NET_ADMIN --cap-add NET_RAW \
  -e TZ=Asia/Shanghai \
  -v drouter-etc:/etc/drouter \
  -v drouter-data:/opt/drouter/data \
  -v drouter-snapshots:/opt/drouter/snapshots \
  -v drouter-log:/var/log/drouter \
  drouter:1.0.5

三条必须知道的

--network host 是硬要求 容器要直接读写宿主网卡和 nftables。bridge 模式下只能看到 eth0,读不到你的 ens18/ens19,也管不了宿主 NAT
NET_ADMIN 不能省 少了它 nft 报 Operation not permitted —— 而且「防火墙」页会显示成空的,看起来像"没有规则"而不是"没有权限",误导性很强
不需要 --privileged 网上教程爱图省事写它,但 Drouter 只要 NET_ADMIN + NET_RAW。给 privileged 等于把宿主完全敞开

⚠️ Docker 形态的定位

这是「体验 / 调试 / CI 渲染校验」用的,不建议把容器当主路由长期跑。

容器里没有 systemd,所以「服务管理」页会显示 active: unknown ——
这是预期内的优雅降级,不是故障。Web 终端不受此影响:
1.0.1 起守护会绕开 init 系统直接启动,容器里也能正常开终端。
真正当路由器用,请走上面的离线包装到 Debian 13 物理机/虚拟机上。

详细说明(含「防火墙页空白」「一直 unhealthy」「改端口没生效」「配置丢失」
等问题的排查步骤)见附件 DOCKER-GUIDE.md。


⭐ 离线自包含安装(目标机零外网)

这是给路由器场景准备的默认交付形态。 路由器常常待在机房、弱电箱、
隔离网段,或者一台你根本不想让它联网的机器上 —— 所以这个包里
带了 Drouter 本体 + 它跑起来需要的全部 236 个 Debian 依赖。

# 1) 把 tar.gz 拷到目标机(U 盘 / scp / 内网共享都行)
# 2) 解包
tar xzf drouter-1.0.5-offline-amd64.tar.gz
cd drouter-1.0.5-offline-amd64

# 3) 一条命令装完(会自动 sudo)
sudo bash install.sh

装完访问 https://<目标机IP>:8443/,账号 admin / admin123。

全程不发起任何网络请求。

这个包里有什么

drouter-1.0.5-offline-amd64/
├── install.sh                 安装脚本(自动提权 + 本地 file: 源,零外网)
├── README-离线安装.md          给不熟悉 Linux 的人看的逐步说明
├── SHA256SUMS                 237 条校验和(236 依赖 + 本体)
├── drouter_1.0.5_all.deb      本体(含 6 个 systemd 单元)
└── debs/                      236 个依赖 .deb
    ├── nftables / dnsmasq / radvd / ppp / chrony ...   ← 直接依赖
    ├── libc6 / zlib1g / dash / diffutils / libc-bin ... ← 最底层传递依赖
    ├── Packages                  本地仓库索引
    └── Packages.gz               (同上,gzip 版)

离线校验完整性:

sha256sum -c SHA256SUMS

已实测验证

在干净 chroot + 清空 sources.list + DNS 指向黑洞的环境下跑通全流程:

  • 236 个依赖全部装上
  • drouter 本体 postinst 正常执行
  • dpkg 记录包数 237 个(236 依赖 + 本体)
  • 16 个关键依赖均为 install ok installed
  • 6 个 systemd 单元全部到位

必须知道的三条约束

约束 说明
架构必须匹配 包名带 -amd64,内含 x86_64 二进制。arm64 请自行重建
发行版必须匹配 依赖来自 trixie 仓库,跨版本(如 bookworm)会出现 libc6 冲突
磁盘 ≥ 1.5 GB 236 个 deb 解包 + 安装要占几百 MB,脚本会在不足时告警

🛡️ 装完之后,它不会做什么

这点很重要 —— 整个项目最核心的设计原则:

install.sh 只安装软件,不会:

  • ❌ 启动 dnsmasq / radvd / kea 等会发 DHCP 的服务
  • ❌ 修改默认路由或打开 IP 转发
  • ❌ 改动任何现有网络配置

装完之后这台机器仍然是一台普通主机,直到你打开面板、亲手点「应用」。
如果你是把它装在一台正在承担路由的机器上,不会造成任何网络中断。


✨ 1.0.x 有什么

  • 43 个功能页面,分 8 组:概览 / 接口 / 寻址与路由 / 安全 / 服务 / 工具 / 日志与审计 / 系统
  • 后端纯 Python 3 标准库,零第三方依赖(9 个模块)
  • 前端原生 JS/CSS,无构建步骤、无 npm
  • 常驻 helper 通过 Unix socket 提供 75 个动作,socket 不可用时自动回退 subprocess
  • 三个层次的救命机制:配置快照 → 语法预检回滚 → 紧急救援通道
  • 主题系统:Web 主题设计与离线预览,支持 ZIP 导入导出

1.0.4 修掉的关键问题(留档)

  • 点了「迁移到 systemd-networkd」会生成一堆垃圾文件 —— 渲染层约定返回
    「(路径, 内容)」二元组列表,唯独 networkd 返回字典;元组解包不报错,
    把两个键名当成了路径和内容,认真写出一堆文件名叫 name 的文件。
  • DMZ 规则方向写反 —— 本应只 DNAT 外部流量,实际连管理流量也一并转走。
  • 一批「填了等于没填」的输入框(5 处):WAN 的 MTU/接受上游 DNS、
    DHCPv6 子网 ID、Samba WINS、网桥 STP、主题「按主色生成配色」按钮。
  • 迁移预览永远报「WAN 网卡不能为空」 —— 预览只提交了 LAN 段。
  • 一批校验缺失:dnsmasq 域名/日志路径、miniupnpd 租约文件/UUID、
    PPPoE 运营商名、NFS 导出选项等从表单直接拼进配置;chrony 误杀 IPv6 服务器;
    Unicode 上标数字过 isdigit() 却让 int() 抛异常(页面 500)。
  • 自签证书 SAN 写死开发者内网 IP —— 现从本机实际地址推导。
  • DDNS 面板 XSS + 主题 CSS 注释注入 —— 统一转义与剥离。

1.0.3 修掉的关键问题(留档)

  • IPv6 路由通告里填了 IPv4 DNS,会生成一份 radvd 起不来的配置 —— RDNSS 字段
    原先完全不校验。现在按 IPv6 地址逐项校验并给中文提示,且支持逗号/空格两种多值写法。
  • 自动清理的「每天 00:00」存不下来,实际跑在 04:00 —— 前后端都把 0 当成「没填」。
  • 重新部署会静默回退用户设置(3 处) —— 生效主题、运营商 DNS、自动清理开关
    都被无条件覆盖;现改为「只在不存在时初始化」。
  • 文件名里残留 \r 的脏条目 —— 随包附 devtools/fix-crlf-names.py 保守清理。

1.0.2 修掉的关键问题(留档)

  • DHCP 页把 option 3 / option 6 渲染成重复指令 —— 专属字段与「自定义 Options」
    表都能写同一个 code,而 dnsmasq 对列表型 option 是追加语义,客户端因此收到
    重复网关、以及公网 DNS 排在本机前面的乱序列表。现按 code 合并成一行并顺序去重。
  • code 6 的占位符写成了上游 DNS —— 让人把上游填进「下发给客户端」的字段,
    已改为提示填本机。
  • /dev 设备节点缺失时 dnsmasq 抛内核英文天书 —— 现在分诊成系统层面的中文提示,
    并随包附 devtools/fix-dev-nodes.sh 幂等修复。

1.0.1 修掉的关键问题(留档)

  • Web SSH 终端任何账号都连不上(out of pty devices) —— 真因与 PTY 数量无关,
    是 /dev/ptmx 这个符号链接丢失;守护进程启动时与每次失败时都会自愈。
  • 终端并发满员后整个服务被冻死 —— 会话锁不可重入,「满员 → 回收空闲会话」
    在持锁状态下二次取同一把锁,同线程永久阻塞且不报错、不写日志。
  • Docker 形态下 Web 终端完全打不开 —— 容器里没有 systemd,而拉起守护
    只有 systemctl 一条路;现在会回退为直接以子进程启动。
  • 页脚协议标注写成了 GNU GPL v3 —— 与仓库实际的 MIT 不符,已改正。

1.0.0 修掉的关键问题(留档)

  • .deb 里原本一个 systemd 单元都没有 —— 单元只存在于部署脚本里,
    导致「用 deploy.sh 部署」能用、「用 .deb 装」的用户装完没服务。
  • 离线包索引生成到了错误目录 —— apt 找不到 debs/Packages 会静默退化
    成 dpkg -i 乱装。
  • 依赖闭包用宿主机状态求解导致漏包 —— 开发机上恰好装过的包会被 apt 判为
    "已满足"而不列出,到干净目标机就变成 Depends: xxx but it is not installable。
  • apt-cache depends --recurse 收进冲突替代品 —— systemd /
    systemd-standalone-sysusers / opensysusers 三者互相 Conflicts,
    文本展开会把三家全下下来导致 E: Unmet dependencies。已改用 apt 求解器。

📋 推荐配置

最低 推荐
CPU 1 核 2 核
内存 512 MB 1 GB+
磁盘 2 GB 8 GB+
网卡 1(单臂路由) 2(WAN + LAN)
系统 Debian 13 (trixie) amd64 同左

实测环境就是 *...

Read more

Drouter 1.0.4

Choose a tag to compare

@liuzhuohua liuzhuohua released this 01 Oct 16:37

Drouter 1.0.4

Debian 13 (trixie) 软路由管理系统 —— 43 个功能页面的 Web 管理面板。

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


🔧 这个版本修了什么

1.0.4 是纯修复版本:没有新增功能,没有改动任何配置文件格式,
升级后你原有的配置、主题、快照、证书全部原样保留。

这一版是把全量代码(后端 9 个模块 + 前端约 1.6 万行的单文件 SPA,合计约 4.5 万行)
从头过了一遍,专门找两类最伤人的问题:

  • 「假输入框」 —— 页面上有这个框、能填、能保存,后端却从来不读,等于白填;
  • 「隐身字段」 —— 后端真的会用它,页面上却没有入口,永远走默认值。

排查方式不是「看一遍」,而是用脚本把两边对撞:拿 AST 把渲染器里读取的每一个
配置键抽出来,再和页面上所有输入框绑定的字段做双向差集。

🐛 一、点了「迁移到 systemd-networkd」,会生成一堆垃圾文件

这是本轮唯一会直接写坏系统的缺陷,也是最值得单独说的一条。

渲染层有一条不成文的铁律:每个渲染器都返回「(绝对路径, 内容)」的二元组列表,
调用方按这个约定去落盘。唯独 networkd 这个返回的是字典。而字典恰好也有两个元素 ——
Python 的元组解包不会报错,它会把字典的两个键当成路径和内容:

for path, content in files:      # files 里是 {'name': ..., 'content': ...}
    write(path, content)         # path 拿到的是字符串 'name'

于是「迁移」既不会失败、也不会报错,它认认真真地生成了一堆文件名叫 name、
内容写着 content 的文件
。现在返回值改成真正的二元组,并且这条契约已经写进
回归测试 —— 每个渲染器的返回值都会被逐个断言。

🔀 二、DMZ 规则方向写反

注释写的是「排除到本机自身的流量」,条件却写成了它的反面。
本来只有外部流量才该被 DNAT 进内网主机,写反之后连管理流量也一并转走 ——
你在面板上已经配好了 DMZ 主机,结果连管理自己的连接都被送去了别处。

🕳️ 三、一批「填了等于没填」的输入框(5 处)

这是「假输入框」这一类的直接战果。它们的共同点是:没有一个会报错,
波浪线没有、校验没有、保存还成功 —— 只是你的设置从来没生效过。

页面 现象 真因
WAN / DHCP 模式 MTU、「是否接受上游 DNS」填多少都不影响 后端压根没读这两个键,现在真正写进 systemd-networkd 的 MTUBytes= / UseDNS=
DHCPv6 「子网 ID」改了没反应 输入框绑的是一个根本不存在的键,真正生效的前缀委派 ID 无入口 —— 现在绑对了
Samba 「WINS 支持」勾了永不生效 渲染时要求另一个从不回填的字段同时为真,这个条件永远为假
网桥 「启用 STP」勾完保存不进去 复选框没有绑定事件,值从来没有被收集过
主题设计器 「按主色生成整套配色」点了没反应 按钮是从 <input> 升级的,但没接处理函数

⏳ 四、IPv6 路由通告页少了两个输入框

1.0.3 修了「DNSSL 生命期实际读的是 RDNSS 生命期」这个张冠李戴,但页面上
一直没有这两个生命期的入口 —— 修好了错位却没给输入框。现在补齐。

🧩 五、迁移预览永远报「WAN 网卡不能为空」

预览请求只提交了 LAN 段的配置。后端要求 WAN 网卡必填,于是预览必定报错,
而且网桥配置从来没在预览里出现过。现在 WAN(含模式、MTU、是否接受上游 DNS、
静态地址)与网桥一起提交。

🧪 六、一批校验缺失导致的「保存完服务起不来 / 页面 500」

这些字段过去是从表单直接拼进配置文件的 —— 用户填什么就写什么:

  • dnsmasq 的域名、日志文件路径、LAN 侧 DNS
  • miniupnpd 的租约文件路径、UUID
  • PPPoE 的运营商名、服务名
  • IPv6 前缀委派 ID、路由通告的地址池(名称 / 起止 / 前缀 / 策略五项)
  • NFS 的导出预设、导出选项、注释、线程数

现在全部按各自的语法校验并给中文提示(比如 NFS 线程数只接受 1–128)。
另外 chrony 的服务器地址校验只认域名和 IPv4 —— 填一个 IPv6 的 NTP 服务器
会被判成非法,这个是旧代码里就存在的漏网之鱼。

顺带修了一个很刁钻的崩溃:数值解析前用 str.isdigit() 判断,而 Unicode 的
上标数字(比如「²」)能通过 isdigit() 却会让 int() 抛异常 —— 页面直接 500。

🔐 七、自签证书的 SAN 里写死了开发者的内网 IP

面板首次启动会签一张自签证书。过去这张证书的主题别名里必定带着 192.168.7.3
(开发机地址),跟你的网络一点关系都没有 —— 有的客户端/浏览器会因为它校验不过而报警。

现在改为从本机实际地址 + 默认路由的源地址推导,并保底保留 127.0.0.1 /
localhost。同时补了 openssl 失败检测:过去 openssl 执行失败时不会报错,
只是继续返回一个并不存在的证书路径,报错链条完全错位。

🛡️ 八、两处模板注入

  • DDNS 面板的 XSS:记录名、服务商是拼进 HTML 的。在这两个框里填一段带
    尖括号的内容,就能在面板页面里执行自己的脚本。现在统一转义。
  • 导出的主题 CSS 注释注入:导出的 CSS 里有一行注释写着主题名和版本。
    把主题名起成带 */ 的样子(注释结束符),就能提前闭合注释、往文件里塞任意 CSS ——
    这份 CSS 是设计给别人导出分享后再导入的 —— 注入的东西会跟着你分享出去。
    现在前端转义、后端清理注释结束符与换行,两头都堵住。

📦 下载哪个?

文件 大小 用途
drouter-1.0.4-offline-amd64.tar.gz 83 MB ⭐ 推荐。目标机完全不需要外网,一个包装完
drouter_1.0.4_all.deb 435 KB 目标机有网(或内网 apt 镜像)时用,依赖交给 apt 去下
drouter-1.0.4-docker.tar 169 MB 想快速体验界面、不想装系统时用(不建议当主路由)
docker-compose.yml 5 KB Docker 编排文件,配合上面那个 tar 用
DOCKER-GUIDE.md 13 KB Docker 形态的完整说明(含常见问题排查)

从 1.0.0 / 1.0.1 / 1.0.2 / 1.0.3 升级:sudo apt install ./drouter_1.0.4_all.deb
直接覆盖安装,配置全部保留;或者用离线包解压后跑 sudo bash install.sh。


🐳 Docker 快速体验

# 1) 加载镜像(得到 drouter:1.0.4)
docker load -i drouter-1.0.4-docker.tar

# 2) 用附带的 compose 文件起
mkdir -p ~/drouter && cd ~/drouter
#    把 docker-compose.yml 放到这里
docker compose up -d

# 3) 等健康检查通过
docker compose ps

访问 https://<宿主IP>:8443/,账号 admin / admin123。

不想用 compose 的话,等价的 docker run:

docker run -d --name drouter --restart unless-stopped \
  --network host \
  --cap-add NET_ADMIN --cap-add NET_RAW \
  -e TZ=Asia/Shanghai \
  -v drouter-etc:/etc/drouter \
  -v drouter-data:/opt/drouter/data \
  -v drouter-snapshots:/opt/drouter/snapshots \
  -v drouter-log:/var/log/drouter \
  drouter:1.0.4

三条必须知道的

--network host 是硬要求 容器要直接读写宿主网卡和 nftables。bridge 模式下只能看到 eth0,读不到你的 ens18/ens19,也管不了宿主 NAT
NET_ADMIN 不能省 少了它 nft 报 Operation not permitted —— 而且「防火墙」页会显示成空的,看起来像"没有规则"而不是"没有权限",误导性很强
不需要 --privileged 网上教程爱图省事写它,但 Drouter 只要 NET_ADMIN + NET_RAW。给 privileged 等于把宿主完全敞开

⚠️ Docker 形态的定位

这是「体验 / 调试 / CI 渲染校验」用的,不建议把容器当主路由长期跑。

容器里没有 systemd,所以「服务管理」页会显示 active: unknown ——
这是预期内的优雅降级,不是故障。Web 终端不受此影响:
1.0.1 起守护会绕开 init 系统直接启动,容器里也能正常开终端。
真正当路由器用,请走上面的离线包装到 Debian 13 物理机/虚拟机上。

详细说明(含「防火墙页空白」「一直 unhealthy」「改端口没生效」「配置丢失」
等问题的排查步骤)见附件 DOCKER-GUIDE.md。


⭐ 离线自包含安装(目标机零外网)

这是给路由器场景准备的默认交付形态。 路由器常常待在机房、弱电箱、
隔离网段,或者一台你根本不想让它联网的机器上 —— 所以这个包里
带了 Drouter 本体 + 它跑起来需要的全部 236 个 Debian 依赖。

# 1) 把 tar.gz 拷到目标机(U 盘 / scp / 内网共享都行)
# 2) 解包
tar xzf drouter-1.0.4-offline-amd64.tar.gz
cd drouter-1.0.4-offline-amd64

# 3) 一条命令装完(会自动 sudo)
sudo bash install.sh

装完访问 https://<目标机IP>:8443/,账号 admin / admin123。

全程不发起任何网络请求。

这个包里有什么

drouter-1.0.4-offline-amd64/
├── install.sh                 安装脚本(自动提权 + 本地 file: 源,零外网)
├── README-离线安装.md          给不熟悉 Linux 的人看的逐步说明
├── SHA256SUMS                 237 条校验和(236 依赖 + 本体)
├── drouter_1.0.4_all.deb      本体(含 6 个 systemd 单元)
└── debs/                      236 个依赖 .deb
    ├── nftables / dnsmasq / radvd / ppp / chrony ...   ← 直接依赖
    ├── libc6 / zlib1g / dash / diffutils / libc-bin ... ← 最底层传递依赖
    ├── Packages                  本地仓库索引
    └── Packages.gz               (同上,gzip 版)

离线校验完整性:

sha256sum -c SHA256SUMS

已实测验证

在干净 chroot + 清空 sources.list + DNS 指向黑洞的环境下跑通全流程:

  • 236 个依赖全部装上
  • drouter 本体 postinst 正常执行
  • dpkg 记录包数 237 个(236 依赖 + 本体)
  • 16 个关键依赖均为 install ok installed
  • 6 个 systemd 单元全部到位

必须知道的三条约束

约束 说明
架构必须匹配 包名带 -amd64,内含 x86_64 二进制。arm64 请自行重建
发行版必须匹配 依赖来自 trixie 仓库,跨版本(如 bookworm)会出现 libc6 冲突
磁盘 ≥ 1.5 GB 236 个 deb 解包 + 安装要占几百 MB,脚本会在不足时告警

🛡️ 装完之后,它不会做什么

这点很重要 —— 整个项目最核心的设计原则:

install.sh 只安装软件,不会:

  • ❌ 启动 dnsmasq / radvd / kea 等会发 DHCP 的服务
  • ❌ 修改默认路由或打开 IP 转发
  • ❌ 改动任何现有网络配置

装完之后这台机器仍然是一台普通主机,直到你打开面板、亲手点「应用」。
如果你是把它装在一台正在承担路由的机器上,不会造成任何网络中断。


✨ 1.0.x 有什么

  • 43 个功能页面,分 8 组:概览 / 接口 / 寻址与路由 / 安全 / 服务 / 工具 / 日志与审计 / 系统
  • 后端纯 Python 3 标准库,零第三方依赖(9 个模块)
  • 前端原生 JS/CSS,无构建步骤、无 npm
  • 常驻 helper 通过 Unix socket 提供 75 个动作,socket 不可用时自动回退 subprocess
  • 三个层次的救命机制:配置快照 → 语法预检回滚 → 紧急救援通道
  • 主题系统:Web 主题设计与离线预览,支持 ZIP 导入导出

1.0.3 修掉的关键问题(留档)

  • IPv6 路由通告里填了 IPv4 DNS,会生成一份 radvd 起不来的配置 —— RDNSS 字段
    原先完全不校验。现在按 IPv6 地址逐项校验并给中文提示,且支持逗号/空格两种多值写法。
  • 自动清理的「每天 00:00」存不下来,实际跑在 04:00 —— 前后端都把 0 当成「没填」。
  • 重新部署会静默回退用户设置(3 处) —— 生效主题、运营商 DNS、自动清理开关
    都被无条件覆盖;现改为「只在不存在时初始化」。
  • 文件名里残留 \r 的脏条目 —— 随包附 devtools/fix-crlf-names.py 保守清理。

1.0.2 修掉的关键问题(留档)

  • DHCP 页把 option 3 / option 6 渲染成重复指令 —— 专属字段与「自定义 Options」
    表都能写同一个 code,而 dnsmasq 对列表型 option 是追加语义,客户端因此收到
    重复网关、以及公网 DNS 排在本机前面的乱序列表。现按 code 合并成一行并顺序去重。
  • code 6 的占位符写成了上游 DNS —— 让人把上游填进「下发给客户端」的字段,
    已改为提示填本机。
  • /dev 设备节点缺失时 dnsmasq 抛内核英文天书 —— 现在分诊成系统层面的中文提示,
    并随包附 devtools/fix-dev-nodes.sh 幂等修复。

1.0.1 修掉的关键问题(留档)

  • Web SSH 终端任何账号都连不上(out of pty devices) —— 真因与 PTY 数量无关,
    是 /dev/ptmx 这个符号链接丢失;守护进程启动时与每次失败时都会自愈。
  • 终端并发满员后整个服务被冻死 —— 会话锁不可重入,「满员 → 回收空闲会话」
    在持锁状态下二次取同一把锁,同线程永久阻塞且不报错、不写日志。
  • Docker 形态下 Web 终端完全打不开 —— 容器里没有 systemd,而拉起守护
    只有 systemctl 一条路;现在会回退为直接以子进程启动。
  • 页脚协议标注写成了 GNU GPL v3 —— 与仓库实际的 MIT 不符,已改正。

1.0.0 修掉的关键问题(留档)

  • .deb 里原本一个 systemd 单元都没有 —— 单元只存在于部署脚本里,
    导致「用 deploy.sh 部署」能用、「用 .deb 装」的用户装完没服务。
  • 离线包索引生成到了错误目录 —— apt 找不到 debs/Packages 会静默退化
    成 dpkg -i 乱装。
  • 依赖闭包用宿主机状态求解导致漏包 —— 开发机上恰好装过的包会被 apt 判为
    "已满足"而不列出,到干净目标机就变成 Depends: xxx but it is not installable。
  • apt-cache depends --recurse 收进冲突替代品 —— systemd /
    systemd-standalone-sysusers / opensysusers 三者互相 Conflicts,
    文本展开会把三家全下下来导致 E: Unmet dependencies。已改用 apt 求解器。

📋 推荐配置

最低 推荐
CPU 1 核 2 核
内存 512 MB 1 GB+
磁盘 2 GB 8 GB+
网卡 1(单臂路由) 2(WAN + LAN)
系统 Debian 13 (trixie) amd64 同左

实测环境就是 2 核 / 4 GB 内存 的虚拟机,跑满 43 个功能页无压力。
这是参考环境,不是准入门槛 —— 最低 1 核 1 GB 就能起来。


📸 关于截图

README 里的 78 张截图全部是真实运行界面,取自一台 2 核 4 GB 的 Debian 13 虚拟机,
按功能分组整理在 docs/screenshots/(ASCII 文件名,02-overview.jpg 这种)。

想看效果不必装 —— 直接翻 README 的截图章节。


📄 许可

MIT

Drouter 1.0.3 · 部署静默回退与 radvd RDNSS 校验

Choose a tag to compare

@liuzhuohua liuzhuohua released this 01 Oct 10:03

Drouter 1.0.3

Debian 13 (trixie) 软路由管理系统 —— 43 个功能页面的 Web 管理面板。

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


🔧 这个版本修了什么

1.0.3 是纯修复版本:没有新增功能,没有改动任何配置文件格式,
升级后你原有的配置、主题、快照、证书全部原样保留。

这一版的五个问题全部是在真机(192.168.7.3)上做了一轮深度排查挖出来的,
排查手段是:翻 journal 与面板自己的统一日志里的 error 级记录、全盘扫描文件名里的
控制字符、把现行配置库里每个模块的渲染结果重新交给对应的真守护进程做语法校验。

🐛 一、IPv6 路由通告里填了 IPv4 DNS,会生成一份 radvd 起不来的配置

RDNSS 字段(「下发 DNS」)原先完全不校验 —— 同一段代码里 prefix 会校验
IPv6 网段、DNSSL 会校验域名,唯独漏了它。

而 223.5.5.5 恰恰是最顺手的写法。于是它会原样写进 radvd.conf:

    RDNSS 223.5.5.5 {
        AdvRDNSSLifetime 600;
    };

radvd 读到这一行直接 syntax error 起不来,而界面上只回一句英文
radvd.conf:18 error: syntax error,完全指不到是哪个字段。
真机日志里就留着这么一条:

2026-09-30T15:44:49 [error] IPv6 路由通告:RA 配置语法检查未通过:
/run/drouter-verify/radvd.conf:18 error: syntax error

现在会逐项按 IPv6 地址校验,并给出中文提示:

RDNSS 只能是 IPv6 地址:223.5.5.5(RA 通告无法下发 IPv4 DNS,IPv4 请填在 DHCP 的 option 6 里)

顺带修了两件事:

  • 多值支持逗号与空格两种写法(统一按空格输出 —— 逗号形式 radvd 本来就不认,
    之前会直接 syntax error);DNSSL 同样支持多域名
  • 「DNSSL 生命期」实际读的是 rdnss_life —— 改一次 RDNSS 生命期会连带改掉 DNSSL 的,
    这个张冠李戴已改正(没有 dnssl_life 时仍会退回 rdnss_life)

⏰ 二、自动清理的「每天 00:00」根本存不下来,实际跑在 04:00

前端 Number(el.value) || 4 和后端 int(cfg.get('hour') or 4) 都把 0 当成「没填」。
两处叠加的结果是:你在界面上选「00:00」,保存后回显成「04:00」,怎么改都改不过来。

现在两处都改成显式判断 None / '',0 点是 0 点。

另外执行时刻的判定从「正好等于」放宽成「不早于」:
定时器虽然每小时醒一次,但机器可能在 04:00 那一刻正好在关机或重启 ——
原来的 == 会让这一天彻底跳过(要等第二天)。
现在当天晚些时候会补做;hour 的语义相应变成「不早于这个点」。

🔁 三、重新部署会静默回退用户设置(3 处)

这是最难被归因的一类问题 —— 用户只会觉得「部署完之后我的设置莫名其妙变回去了」。
scripts/deploy.sh 里三处无条件写入已全部改成「只在不存在时初始化」:

原来 后果
echo default > /etc/drouter/active-theme 在「主题之家」挑的自定义主题,每次部署都被打回默认
cat > /etc/drouter/generated/isp-dns.conf PPPoE 拨号拿到的运营商 DNS 被擦掉,上游解析变空直到下次拨号
cleanup '{"op":"save","enabled":true}' 你在界面上明确关掉的「自动清理」被重新打开

第三条最要紧:磁盘清理是本项目唯一会真删文件的模块,
静默恢复一个「自动删除文件」的策略,是所有副作用里最不该发生的。
现在已有的策略文件会被完整尊重(enabled / trigger / 保留天数一律不动),
只重建 systemd 单元。

顺带补了一个兜底:生效主题如果指向一个已经不存在的主题(自定义主题目录被删掉),
部署时会回退到 default 并打印一行说明 —— 否则界面会一直挂着一个无解的失效主题。

🧹 四、浏览器切页/关标签会往 journal 里刷一整段 Python traceback

socketserver 默认把任何异常连整段 traceback 打到 stderr(也就是 journal)。
而浏览器切页、关标签、取消长请求抛出的 BrokenPipeError / ConnectionResetError,
本质上都是「对面先挂了」,不是服务端的错:

drouter-web[185391]: Exception occurred during processing of request from ('127.0.0.1', 42700)
drouter-web[185391]: Traceback (most recent call last):
drouter-web[185391]: BrokenPipeError: [Errno 32] Broken pipe

真机上 3 天攒了几十条,足以把真正的报错淹掉。现在只吞这几类「对面先挂了」的异常,
其余异常照旧往外抛(不放过真问题)。

🗑️ 五、/etc、/opt、/var/log 里残留着名字带 \r 的脏条目

早先把 Windows 上编辑的部署脚本以 CRLF 传到 Linux 直接执行时,
每一行结尾的 \r 被当成了参数的一部分 —— mkdir -p /etc/dnsmasq.d 真的建出了
一个名字叫 dnsmasq.d\r 的目录,echo default > /etc/drouter/active-theme
真的写出了一个名字带 \r 的文件。

真机上清出来 7 个:

/etc/dnsmasq.d\r        /etc/nftables.d\r       /var/log/drouter\r
/etc/drouter/active-theme\r   /etc/drouter/generated\r
/etc/drouter/themes\r   /opt/drouter\r

它们功能上无害(正常名字的那份也在),但会:让 ls /etc 出现同名条目两遍的
重影(\r 让终端光标回到行首)、被配置快照反复打包、干扰排查。

随包附 devtools/fix-crlf-names.py,保守到极点:

  • 默认只预演,加 --apply 才真删
  • 只删名字去掉控制字符后存在同名兄弟的条目(证明它是重复品)
  • 目录会先比较相对文件集合,只要里面有兄弟目录没有的文件就拒绝删
  • 明确跳过 snapshots/(快照是某个时刻的备份,必须保持原样)

同时把「文件名不得含控制字符」加进发布前预检(原来只查文件内容里的 CRLF,
查不到这个)。项目仓库里目前是干净的。


📦 下载哪个?

文件 大小 用途
drouter-1.0.3-offline-amd64.tar.gz 83 MB ⭐ 推荐。目标机完全不需要外网,一个包装完
drouter_1.0.3_all.deb 430 KB 目标机有网(或内网 apt 镜像)时用,依赖交给 apt 去下
drouter-1.0.3-docker.tar 169 MB 想快速体验界面、不想装系统时用(不建议当主路由)
docker-compose.yml 5 KB Docker 编排文件,配合上面那个 tar 用
DOCKER-GUIDE.md 13 KB Docker 形态的完整说明(含常见问题排查)

从 1.0.0 / 1.0.1 / 1.0.2 升级:sudo apt install ./drouter_1.0.3_all.deb
直接覆盖安装,配置全部保留;或者用离线包解压后跑 sudo bash install.sh。


🐳 Docker 快速体验

# 1) 加载镜像(得到 drouter:1.0.3)
docker load -i drouter-1.0.3-docker.tar

# 2) 用附带的 compose 文件起
mkdir -p ~/drouter && cd ~/drouter
#    把 docker-compose.yml 放到这里
docker compose up -d

# 3) 等健康检查通过
docker compose ps

访问 https://<宿主IP>:8443/,账号 admin / admin123。

不想用 compose 的话,等价的 docker run:

docker run -d --name drouter --restart unless-stopped \
  --network host \
  --cap-add NET_ADMIN --cap-add NET_RAW \
  -e TZ=Asia/Shanghai \
  -v drouter-etc:/etc/drouter \
  -v drouter-data:/opt/drouter/data \
  -v drouter-snapshots:/opt/drouter/snapshots \
  -v drouter-log:/var/log/drouter \
  drouter:1.0.3

三条必须知道的

--network host 是硬要求 容器要直接读写宿主网卡和 nftables。bridge 模式下只能看到 eth0,读不到你的 ens18/ens19,也管不了宿主 NAT
NET_ADMIN 不能省 少了它 nft 报 Operation not permitted —— 而且「防火墙」页会显示成空的,看起来像"没有规则"而不是"没有权限",误导性很强
不需要 --privileged 网上教程爱图省事写它,但 Drouter 只要 NET_ADMIN + NET_RAW。给 privileged 等于把宿主完全敞开

⚠️ Docker 形态的定位

这是「体验 / 调试 / CI 渲染校验」用的,不建议把容器当主路由长期跑。

容器里没有 systemd,所以「服务管理」页会显示 active: unknown ——
这是预期内的优雅降级,不是故障。Web 终端不受此影响:
1.0.1 起守护会绕开 init 系统直接启动,容器里也能正常开终端。
真正当路由器用,请走上面的离线包装到 Debian 13 物理机/虚拟机上。

详细说明(含「防火墙页空白」「一直 unhealthy」「改端口没生效」「配置丢失」
等问题的排查步骤)见附件 DOCKER-GUIDE.md。


⭐ 离线自包含安装(目标机零外网)

这是给路由器场景准备的默认交付形态。 路由器常常待在机房、弱电箱、
隔离网段,或者一台你根本不想让它联网的机器上 —— 所以这个包里
带了 Drouter 本体 + 它跑起来需要的全部 236 个 Debian 依赖。

# 1) 把 tar.gz 拷到目标机(U 盘 / scp / 内网共享都行)
# 2) 解包
tar xzf drouter-1.0.3-offline-amd64.tar.gz
cd drouter-1.0.3-offline-amd64

# 3) 一条命令装完(会自动 sudo)
sudo bash install.sh

装完访问 https://<目标机IP>:8443/,账号 admin / admin123。

全程不发起任何网络请求。

这个包里有什么

drouter-1.0.3-offline-amd64/
├── install.sh                 安装脚本(自动提权 + 本地 file: 源,零外网)
├── README-离线安装.md          给不熟悉 Linux 的人看的逐步说明
├── SHA256SUMS                 237 条校验和(236 依赖 + 本体)
├── drouter_1.0.3_all.deb      本体(含 6 个 systemd 单元)
└── debs/                      236 个依赖 .deb
    ├── nftables / dnsmasq / radvd / ppp / chrony ...   ← 直接依赖
    ├── libc6 / zlib1g / dash / diffutils / libc-bin ... ← 最底层传递依赖
    ├── Packages                  本地仓库索引
    └── Packages.gz               (同上,gzip 版)

离线校验完整性:

sha256sum -c SHA256SUMS

已实测验证

在干净 chroot + 清空 sources.list + DNS 指向黑洞的环境下跑通全流程:

  • 236 个依赖全部装上
  • drouter 本体 postinst 正常执行
  • dpkg 记录包数 237 个(236 依赖 + 本体)
  • 16 个关键依赖均为 install ok installed
  • 6 个 systemd 单元全部到位

必须知道的三条约束

约束 说明
架构必须匹配 包名带 -amd64,内含 x86_64 二进制。arm64 请自行重建
发行版必须匹配 依赖来自 trixie 仓库,跨版本(如 bookworm)会出现 libc6 冲突
磁盘 ≥ 1.5 GB 236 个 deb 解包 + 安装要占几百 MB,脚本会在不足时告警

🛡️ 装完之后,它不会做什么

这点很重要 —— 整个项目最核心的设计原则:

install.sh 只安装软件,不会:

  • ❌ 启动 dnsmasq / radvd / kea 等会发 DHCP 的服务
  • ❌ 修改默认路由或打开 IP 转发
  • ❌ 改动任何现有网络配置

装完之后这台机器仍然是一台普通主机,直到你打开面板、亲手点「应用」。
如果你是把它装在一台正在承担路由的机器上,不会造成任何网络中断。


✨ 1.0.x 有什么

  • 43 个功能页面,分 8 组:概览 / 接口 / 寻址与路由 / 安全 / 服务 / 工具 / 日志与审计 / 系统
  • 后端纯 Python 3 标准库,零第三方依赖(9 个模块)
  • 前端原生 JS/CSS,无构建步骤、无 npm
  • 常驻 helper 通过 Unix socket 提供 75 个动作,socket 不可用时自动回退 subprocess
  • 三个层次的救命机制:配置快照 → 语法预检回滚 → 紧急救援通道
  • 主题系统:Web 主题设计与离线预览,支持 ZIP 导入导出

1.0.2 修掉的关键问题(留档)

  • DHCP 页把 option 3 / option 6 渲染成重复指令 —— 专属字段与「自定义 Options」
    表都能写同一个 code,而 dnsmasq 对列表型 option 是追加语义,客户端因此收到
    重复网关、以及公网 DNS 排在本机前面的乱序列表。现按 code 合并成一行并顺序去重。
  • code 6 的占位符写成了上游 DNS —— 让人把上游填进「下发给客户端」的字段,
    已改为提示填本机。
  • /dev 设备节点缺失时 dnsmasq 抛内核英文天书 —— 现在分诊成系统层面的中文提示,
    并随包附 devtools/fix-dev-nodes.sh 幂等修复。

1.0.1 修掉的关键问题(留档)

  • Web SSH 终端任何账号都连不上(out of pty devices) —— 真因与 PTY 数量无关,
    是 /dev/ptmx 这个符号链接丢失;守护进程启动时与每次失败时都会自愈。
  • 终端并发满员后整个服务被冻死 —— 会话锁不可重入,「满员 → 回收空闲会话」
    在持锁状态下二次取同一把锁,同线程永久阻塞且不报错、不写日志。
  • Docker 形态下 Web 终端完全打不开 —— 容器里没有 systemd,而拉起守护
    只有 systemctl 一条路;现在会回退为直接以子进程启动。
  • 页脚协议标注写成了 GNU GPL v3 —— 与仓库实际的 MIT 不符,已改正。

1.0.0 修掉的关键问题(留档)

  • .deb 里原本一个 systemd 单元都没有 —— 单元只存在于部署脚本里,
    导致「用 deploy.sh 部署」能用、「用 .deb 装」的用户装完没服务。
  • 离线包索引生成到了错误目录 —— apt 找不到 debs/Packages 会静默退化
    成 dpkg -i 乱装。
  • 依赖闭包用宿主机状态求解导致漏包 —— 开发机上恰好装过的包会被 apt 判为
    "已满足"而不列出,到干净目标机就变成 Depends: xxx but it is not installable。
  • apt-cache depends --recurse 收进冲突替代品 —— systemd /
    systemd-standalone-sysusers / opensysusers 三者互相 Conflicts,
    文本展开会把三家全下下来导致 E: Unmet dependencies。已改用 apt 求解器。

📋 推荐配置

最低 推荐
CPU 1 核 2 核
内存 512 MB 1 GB+
磁盘 2 GB 8 GB+
网卡 1(单臂路由) 2(WAN + LAN)
系统 Debian 13 (trixie) amd64 同左

实测环境就是 2 核 / 4 GB 内存 的虚拟机,跑满 43 个功能页无压力。
这是参考环境,不是准入门槛 —— 最低 1 核 1 GB 就能起来。


📸 关于截图

README 里的 78 张截图全部是真实运行界面,取自一台 2 核 4 GB 的 Debian 13 虚拟机,
按功能分组整理在 docs/screenshots/(ASCII 文件名,02-overview.jpg 这种)。

想看效果不必装 —— 直接翻 README 的截图章节。


📄 许可

MIT

Drouter 1.0.2 · DHCP 渲染去重 + 预检诊断

Choose a tag to compare

@liuzhuohua liuzhuohua released this 01 Oct 08:57

Drouter 1.0.2

Debian 13 (trixie) 软路由管理系统 —— 43 个功能页面的 Web 管理面板。

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


🔧 这个版本修了什么

1.0.2 是纯修复版本:没有新增功能,没有改动任何配置文件格式,
升级后你原有的配置、主题、快照、证书全部原样保留。

🐛 一、DHCP 页会把 option 3 / option 6 渲染成重复指令

DHCP 页上有两处能写同一个 option code:

  • 上方的专属字段 Option code 3 · 网关 / Option code 6 · DNS
  • 下方的「自定义 Options」表(本来就是给 43、121 这类自定义 code 用的)

两处都填了同一个 code,就会各自输出一遍:

dhcp-option=3,192.168.7.3              ← 专属字段
dhcp-option=6,223.5.5.5,119.29.29.29   ← 专属字段
dhcp-option=6,192.168.7.3              ← 自定义表
dhcp-option=3,192.168.7.3              ← 自定义表

而 dnsmasq 对 3/6 这类列表型 option 是追加语义 —— 于是客户端收到的
默认网关是 192.168.7.3 两遍,DNS 列表则变成
223.5.5.5, 119.29.29.29, 192.168.7.3:公网 DNS 排在前面,本机 dnsmasq
等于形同虚设
(以后想在本机做过滤、分流、查询日志都会失效)。

现在按 code 合并成一行,并做顺序去重:

dhcp-option=3,192.168.7.3
dhcp-option=6,223.5.5.5,119.29.29.29,192.168.7.3

几个边角都照顾到了:

  • 「强制」的 3/6 行语义不同(客户端没请求也要下发),不合并,仍单独输出为 dhcp-option-force
  • 专属字段留空时,自定义表里的 3/6 不会被吞掉
  • 其它 code(43、121……)完全不受影响,重复出现也照旧
  • 新建配置不再预置与专属字段重复的 code 3/6 行(老配置不受影响,渲染时自动合并)

⚠️ 把 DHCP 打开之前,先确认两件事:局域网里不能还有另一台 DHCP 服务器;
地址池别把在用的网关和本机地址圈进去。否则会出现同一网段两个 DHCP
服务器互相抢答,客户端还可能拿到与现役网关冲突的地址。

💡 二、code 6 的填写提示会把人带偏

Option code 6 · DNS 的占位符原本写着 223.5.5.5,119.29.29.29,
很容易让人把上游 DNS 填进去。但 option 6 是下发给客户端的
「你去哪台机器查 DNS」,正常应该填本机地址,由本机再转发到上游。

占位符与说明文字已改正,并把「填本机、不是填上游」写在了字段旁边。

🔧 三、dnsmasq 预检失败时不再只丢一句天书

预检不通过时,界面原本直接把子进程的英文原文抛出来,看不出问题出在哪一层。

典型例子:保存 DHCP 时提示

配置语法检查未通过:dnsmasq: failed to seed the random number generator: 没有那个文件或目录

这句话跟你的配置毫无关系 —— 它是宿主机的 /dev/urandom 这个设备节点
不存在(/dev 曾被清空过,而 devtmpfs 不会自动重建已删除的节点)。
现在这类报错会被分诊,明确告诉你是系统层面的问题,并给出补救方向。

随包附 devtools/fix-dev-nodes.sh:幂等地补齐缺失的 /dev 设备节点,
设备号取自内核 devices.txt、/proc/partitions 与 sysfs(不硬编码)。


📦 下载哪个?

文件 大小 用途
drouter-1.0.2-offline-amd64.tar.gz 83 MB ⭐ 推荐。目标机完全不需要外网,一个包装完
drouter_1.0.2_all.deb 428 KB 目标机有网(或内网 apt 镜像)时用,依赖交给 apt 去下
drouter-1.0.2-docker.tar 169 MB 想快速体验界面、不想装系统时用(不建议当主路由)
docker-compose.yml 5 KB Docker 编排文件,配合上面那个 tar 用
DOCKER-GUIDE.md 12 KB Docker 形态的完整说明(含常见问题排查)

从 1.0.0 / 1.0.1 升级:sudo apt install ./drouter_1.0.2_all.deb 直接覆盖安装,
配置全部保留;或者用离线包解压后跑 sudo bash install.sh。


🐳 Docker 快速体验

# 1) 加载镜像(得到 drouter:1.0.2)
docker load -i drouter-1.0.2-docker.tar

# 2) 用附带的 compose 文件起
mkdir -p ~/drouter && cd ~/drouter
#    把 docker-compose.yml 放到这里
docker compose up -d

# 3) 等健康检查通过
docker compose ps

访问 https://<宿主IP>:8443/,账号 admin / admin123。

不想用 compose 的话,等价的 docker run:

docker run -d --name drouter --restart unless-stopped \
  --network host \
  --cap-add NET_ADMIN --cap-add NET_RAW \
  -e TZ=Asia/Shanghai \
  -v drouter-etc:/etc/drouter \
  -v drouter-data:/opt/drouter/data \
  -v drouter-snapshots:/opt/drouter/snapshots \
  -v drouter-log:/var/log/drouter \
  drouter:1.0.2

三条必须知道的

--network host 是硬要求 容器要直接读写宿主网卡和 nftables。bridge 模式下只能看到 eth0,读不到你的 ens18/ens19,也管不了宿主 NAT
NET_ADMIN 不能省 少了它 nft 报 Operation not permitted —— 而且「防火墙」页会显示成空的,看起来像"没有规则"而不是"没有权限",误导性很强
不需要 --privileged 网上教程爱图省事写它,但 Drouter 只要 NET_ADMIN + NET_RAW。给 privileged 等于把宿主完全敞开

⚠️ Docker 形态的定位

这是「体验 / 调试 / CI 渲染校验」用的,不建议把容器当主路由长期跑。

容器里没有 systemd,所以「服务管理」页会显示 active: unknown ——
这是预期内的优雅降级,不是故障。Web 终端不受此影响:
1.0.1 起守护会绕开 init 系统直接启动,容器里也能正常开终端。
真正当路由器用,请走上面的离线包装到 Debian 13 物理机/虚拟机上。

详细说明(含「防火墙页空白」「一直 unhealthy」「改端口没生效」「配置丢失」
等问题的排查步骤)见附件 DOCKER-GUIDE.md。


⭐ 离线自包含安装(目标机零外网)

这是给路由器场景准备的默认交付形态。 路由器常常待在机房、弱电箱、
隔离网段,或者一台你根本不想让它联网的机器上 —— 所以这个包里
带了 Drouter 本体 + 它跑起来需要的全部 236 个 Debian 依赖。

# 1) 把 tar.gz 拷到目标机(U 盘 / scp / 内网共享都行)
# 2) 解包
tar xzf drouter-1.0.2-offline-amd64.tar.gz
cd drouter-1.0.2-offline-amd64

# 3) 一条命令装完(会自动 sudo)
sudo bash install.sh

装完访问 https://<目标机IP>:8443/,账号 admin / admin123。

全程不发起任何网络请求。

这个包里有什么

drouter-1.0.2-offline-amd64/
├── install.sh                 安装脚本(自动提权 + 本地 file: 源,零外网)
├── README-离线安装.md          给不熟悉 Linux 的人看的逐步说明
├── SHA256SUMS                 237 条校验和(236 依赖 + 本体)
├── drouter_1.0.2_all.deb      本体(含 6 个 systemd 单元)
└── debs/                      236 个依赖 .deb
    ├── nftables / dnsmasq / radvd / ppp / chrony ...   ← 直接依赖
    ├── libc6 / zlib1g / dash / diffutils / libc-bin ... ← 最底层传递依赖
    ├── Packages                  本地仓库索引
    └── Packages.gz               (同上,gzip 版)

离线校验完整性:

sha256sum -c SHA256SUMS

已实测验证

在干净 chroot + 清空 sources.list + DNS 指向黑洞的环境下跑通全流程:

  • 236 个依赖全部装上
  • drouter 本体 postinst 正常执行
  • dpkg 记录包数 237 个(236 依赖 + 本体)
  • 16 个关键依赖均为 install ok installed
  • 6 个 systemd 单元全部到位

必须知道的三条约束

约束 说明
架构必须匹配 包名带 -amd64,内含 x86_64 二进制。arm64 请自行重建
发行版必须匹配 依赖来自 trixie 仓库,跨版本(如 bookworm)会出现 libc6 冲突
磁盘 ≥ 1.5 GB 236 个 deb 解包 + 安装要占几百 MB,脚本会在不足时告警

🛡️ 装完之后,它不会做什么

这点很重要 —— 整个项目最核心的设计原则:

install.sh 只安装软件,不会:

  • ❌ 启动 dnsmasq / radvd / kea 等会发 DHCP 的服务
  • ❌ 修改默认路由或打开 IP 转发
  • ❌ 改动任何现有网络配置

装完之后这台机器仍然是一台普通主机,直到你打开面板、亲手点「应用」。
如果你是把它装在一台正在承担路由的机器上,不会造成任何网络中断。


✨ 1.0.x 有什么

  • 43 个功能页面,分 8 组:概览 / 接口 / 寻址与路由 / 安全 / 服务 / 工具 / 日志与审计 / 系统
  • 后端纯 Python 3 标准库,零第三方依赖(9 个模块)
  • 前端原生 JS/CSS,无构建步骤、无 npm
  • 常驻 helper 通过 Unix socket 提供 75 个动作,socket 不可用时自动回退 subprocess
  • 三个层次的救命机制:配置快照 → 语法预检回滚 → 紧急救援通道
  • 主题系统:Web 主题设计与离线预览,支持 ZIP 导入导出

1.0.1 修掉的关键问题(留档)

  • Web SSH 终端任何账号都连不上(out of pty devices) —— 真因与 PTY 数量无关,
    是 /dev/ptmx 这个符号链接丢失;守护进程启动时与每次失败时都会自愈。
  • 终端并发满员后整个服务被冻死 —— 会话锁不可重入,「满员 → 回收空闲会话」
    在持锁状态下二次取同一把锁,同线程永久阻塞且不报错、不写日志。
  • Docker 形态下 Web 终端完全打不开 —— 容器里没有 systemd,而拉起守护
    只有 systemctl 一条路;现在会回退为直接以子进程启动。
  • 页脚协议标注写成了 GNU GPL v3 —— 与仓库实际的 MIT 不符,已改正。

1.0.0 修掉的关键问题(留档)

  • .deb 里原本一个 systemd 单元都没有 —— 单元只存在于部署脚本里,
    导致「用 deploy.sh 部署」能用、「用 .deb 装」的用户装完没服务。
  • 离线包索引生成到了错误目录 —— apt 找不到 debs/Packages 会静默退化
    成 dpkg -i 乱装。
  • 依赖闭包用宿主机状态求解导致漏包 —— 开发机上恰好装过的包会被 apt 判为
    "已满足"而不列出,到干净目标机就变成 Depends: xxx but it is not installable。
  • apt-cache depends --recurse 收进冲突替代品 —— systemd /
    systemd-standalone-sysusers / opensysusers 三者互相 Conflicts,
    文本展开会把三家全下下来导致 E: Unmet dependencies。已改用 apt 求解器。

📋 推荐配置

最低 推荐
CPU 1 核 2 核
内存 512 MB 1 GB+
磁盘 2 GB 8 GB+
网卡 1(单臂路由) 2(WAN + LAN)
系统 Debian 13 (trixie) amd64 同左

实测环境就是 2 核 / 4 GB 内存 的虚拟机,跑满 43 个功能页无压力。
这是参考环境,不是准入门槛 —— 最低 1 核 1 GB 就能起来。


📸 关于截图

README 里的 78 张截图全部是真实运行界面,取自一台 2 核 4 GB 的 Debian 13 虚拟机,
按功能分组整理在 docs/screenshots/(ASCII 文件名,02-overview.jpg 这种)。

想看效果不必装 —— 直接翻 README 的截图章节。


📄 许可

MIT