一套跑在 Debian 13 (trixie) 上的软路由管理系统,提供 43 个功能页面的 Web 管理面板:WAN 接入、PPPoE 多拨、DHCP/DNS、IPv6、防火墙、QoS 限速、 访问控制、DDNS、文件共享、Docker 管理等。 核心设计原则:不重新实现任何协议 PPPoE 交给 ppp,DHCP 交给 dnsmasq,IPv6 RA 交给 radvd,NAT 交给 nftables。 Web 面板只做三件事:渲染原生配置文件 → 语法预检 → 原子写入并 reload。 面板删掉后,系统仍是一台配置完好的 Debian 路由器。 安全默认值 · 安装后不接管网络:不改默认路由、不开 IP 转发、不启动 DHCP 服务 · 所有改网络的动作都需要用户在面板上亲手点「应用」 · 避开 5900 端口,不触碰 VNC / X 会话 · 配置写入前先做语法预检,失败则回滚 架构 · backend/ Python 3 标准库,零第三方依赖(9 个模块) · web/ 原生 JS/CSS,无构建步骤 · 常驻 helper 通过 Unix socket 提供 75 个动作,socket 不可用时回退 subprocess · 鉴权走 X-Token 请求头 打包 · packaging/build-deb.sh → drouter_1.0.0_all.deb(含 6 个 systemd 单元) · scripts/make-offline-deps.sh → 236 个依赖 .deb + 本地仓库索引 · packaging/build-offline-bundle.sh → 自包含离线包,目标机零外网可装 · packaging/build-docker.sh → 容器镜像(体验用) 测试 40 套单元测试 + 3 个前端检查脚本(菜单映射 / 离线渲染 / 手机适配), 全部挂在 devtools/sync.sh 的预检链里。 许可:MIT
64 lines
2.2 KiB
Python
64 lines
2.2 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
drouter-web.py 内部契约检查(不需要目标机、不需要起服务):
|
|
|
|
1) GET /api/config 返回的模块清单 == save_config 允许保存的模块集合
|
|
两者一旦走偏,就会出现「能保存但读不回来」的模块(theme 曾漏过)。
|
|
2) 所有 settings 默认键(cfg_defaults)都应在上面两份清单里,
|
|
否则新加的配置模块会永远读不到。
|
|
"""
|
|
import io
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
BACKEND = os.path.join(os.path.dirname(HERE), 'backend')
|
|
WEB = os.path.join(BACKEND, 'drouter-web.py')
|
|
|
|
src = io.open(WEB, encoding='utf-8').read()
|
|
|
|
PASS = FAIL = 0
|
|
MSG = []
|
|
|
|
|
|
def has(name, cond, extra=''):
|
|
global PASS, FAIL
|
|
if cond:
|
|
PASS += 1
|
|
else:
|
|
FAIL += 1
|
|
MSG.append(' ✗ %s%s' % (name, (' → ' + str(extra)) if extra else ''))
|
|
|
|
|
|
# ---- 1. GET /api/config 的 keys ----
|
|
m = re.search(r"p == '/api/config' and method == 'GET':.*?keys = \[(.*?)\]", src, re.S)
|
|
has('能定位 GET /api/config 的 keys', bool(m))
|
|
get_keys = set(re.findall(r"'([a-z0-9_]+)'", m.group(1))) if m else set()
|
|
|
|
# ---- 2. save_config 的 allowed ----
|
|
m2 = re.search(r"def save_config\(self\):.*?allowed = \{(.*?)\}", src, re.S)
|
|
has('能定位 save_config 的 allowed', bool(m2))
|
|
post_keys = set(re.findall(r"'([a-z0-9_]+)'", m2.group(1))) if m2 else set()
|
|
|
|
has('GET 与 POST 模块清单完全一致', get_keys == post_keys,
|
|
'GET 多: %s / POST 多: %s' % (sorted(get_keys - post_keys), sorted(post_keys - get_keys)))
|
|
|
|
# ---- 3. cfg_defaults 的键都应被覆盖 ----
|
|
m3 = re.search(r'cfg_defaults = \{(.*?)\n \}', src, re.S)
|
|
has('能定位 cfg_defaults', bool(m3))
|
|
# 只取顶层键(cfg_defaults 里缩进 8 空格);嵌套的如 share.samba / share.nfs 不算模块
|
|
default_keys = set(re.findall(r"^ {8}'([a-z0-9_]+)':\s*\{", m3.group(1), re.M)) if m3 else set()
|
|
has('cfg_defaults 非空', len(default_keys) > 0, len(default_keys))
|
|
missing = sorted(default_keys - get_keys)
|
|
has('默认配置模块都在 GET 清单里', not missing, missing)
|
|
|
|
print('=' * 60)
|
|
print('通过 %d 项,失败 %d 项' % (PASS, FAIL))
|
|
if FAIL:
|
|
print('-' * 60)
|
|
for x in MSG:
|
|
print(x)
|
|
sys.exit(1 if FAIL else 0)
|