一套跑在 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
111 lines
5.2 KiB
Python
111 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""常驻执行守护(#7 性能优化)契约测试。
|
||
|
||
核心契约只有两条,任一条破了都会在生产环境出事:
|
||
1. 有快路径:drouter-web 会先连 Unix socket;
|
||
2. 有回退:连不上时必须退回原来的 subprocess 方式。
|
||
—— 只有快路径没有回退,守护一崩整个面板就废了;
|
||
只有回退没有快路径,性能优化等于没做。
|
||
|
||
另外几条是「上线后最难发现的坑」:
|
||
* 守护把 helper 常驻内存,部署时不 restart 就跑的是旧代码;
|
||
* 两个守护共用 /run/drouter,RuntimeDirectoryMode 不一致会让 socket
|
||
权限随启动顺序时好时坏;
|
||
* 守护必须复用 helper.run_action,不能另写一套分发(否则构建保护熔断
|
||
就可能在守护路径上失效)。
|
||
"""
|
||
import ast
|
||
import io
|
||
import os
|
||
import re
|
||
import sys
|
||
|
||
ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..')
|
||
fails = 0
|
||
|
||
|
||
def chk(label, cond, extra=''):
|
||
global fails
|
||
if not cond:
|
||
fails += 1
|
||
print('[%s] %s %s' % ('OK' if cond else 'FAIL', label, extra))
|
||
|
||
|
||
def read(rel):
|
||
with io.open(os.path.join(ROOT, rel), encoding='utf-8') as f:
|
||
return f.read()
|
||
|
||
|
||
HELPD = read('backend/drouter-helpd.py')
|
||
WEB = read('backend/drouter-web.py')
|
||
HELPER = read('backend/drouter-helper.py')
|
||
DEPLOY = read('scripts/deploy.sh')
|
||
|
||
# ---------------------------------------------------------------- 1. 快路径
|
||
chk('web.py 定义了 socket 路径', "HELPD_SOCK = '/run/drouter/helper.sock'" in WEB)
|
||
chk('web.py 实现了 _helpd_call', 'def _helpd_call(' in WEB)
|
||
chk('web.py 的 helper() 先走守护', '_helpd_call(action, payload, timeout)' in WEB)
|
||
chk('协议是换行分隔的 JSON', "'\\n').encode('utf-8')" in WEB)
|
||
# ---------------------------------------------------------------- 2. 回退
|
||
chk('web.py 保留了 subprocess 回退',
|
||
"['sudo', '-n', '/usr/bin/python3', HELPER, action" in WEB)
|
||
chk('回退前有「守护不可用」冷却期,避免每请求白等',
|
||
'_helpd_down_until' in WEB and '_helpd_down_until[0] = time.time() + 30' in WEB)
|
||
chk('守护不可用时返回 None 触发回退', 'return None' in WEB.split('def _helpd_call')[1][:2000])
|
||
chk('回退分支有注释说明为什么不能只有快路径',
|
||
'守护挂掉只会变慢' in WEB or '不会让面板不可用' in WEB)
|
||
|
||
# ---------------------------------------------------------------- 3. 守护本身
|
||
chk('守护复用 helper.run_action(不另写分发)', 'HELPER.run_action(action, payload)' in HELPD)
|
||
chk('守护 socket 权限 0660', 'os.chmod(SOCK_PATH, 0o660)' in HELPD)
|
||
chk('守护把 socket 属组设为 drouter(否则 drouter-web 连不上)',
|
||
"SOCK_GROUP = 'drouter'" in HELPD and 'grp.getgrnam(SOCK_GROUP)' in HELPD)
|
||
chk('守护启动前清理陈旧 socket', 'os.unlink(SOCK_PATH)' in HELPD)
|
||
chk('守护用多线程(并发请求不排队)', 'ThreadingUnixStreamServer' in HELPD)
|
||
chk('客户端断开时工作线程不能崩', '_write(self, obj)' in HELPD)
|
||
chk('守护不监听任何 TCP 端口', 'AF_INET' not in HELPD)
|
||
|
||
# ---------------------------------------------------------------- 4. helper 改造
|
||
chk('helper 暴露 run_action', 'def run_action(' in HELPER)
|
||
h_tree = ast.parse(HELPER)
|
||
has_guard = False
|
||
for n in ast.walk(h_tree):
|
||
if isinstance(n, ast.FunctionDef) and n.name == 'run_action':
|
||
src = ast.get_source_segment(HELPER, n) or ''
|
||
has_guard = 'check_build_guard' in src
|
||
chk('run_action 保留了构建保护模式熔断', has_guard)
|
||
chk('main() 改为调用 run_action(两条路径同一套逻辑)',
|
||
'res = run_action(action, payload)' in HELPER)
|
||
|
||
# ---------------------------------------------------------------- 5. 部署
|
||
chk('部署脚本安装了 drouter-helpd.py', 'backend/drouter-helpd.py' in DEPLOY)
|
||
chk('部署脚本 restart 守护(否则旧代码常驻不更新)',
|
||
'systemctl restart drouter-helpd' in DEPLOY)
|
||
# 共用 /run/drouter 时绝不能用 systemd 的 RuntimeDirectory:
|
||
# 一个守护重启,systemd 就重建目录,把另一个守护的 socket 删掉。
|
||
chk('两个守护都不用 RuntimeDirectory(改用各自 mkdir)',
|
||
not re.search(r'^RuntimeDirectory=', DEPLOY, re.M))
|
||
chk('守护自己建目录并设 0755',
|
||
"SOCK_DIR = '/run/drouter'" in HELPD and 'os.chmod(SOCK_DIR, 0o755)' in HELPD)
|
||
# helpd 单元段:从 'drouter-helpd.service' 到下一个 'cat > /etc/systemd/system'
|
||
_seg = DEPLOY.split('drouter-helpd.service')[1].split('cat > /etc/systemd/system')[0]
|
||
# 注意只匹配真正的配置行:单元里那条「故意不设 CPUQuota」的说明文字里
|
||
# 也含 CPUQuota 字样,直接 `not in` 会误报。
|
||
chk('守护单元刻意不限 CPU(格式化大盘等子进程不能被拖慢)',
|
||
'MemoryMax=300M' in _seg
|
||
and not re.search(r'^CPUQuota=', _seg, re.M))
|
||
|
||
# ---------------------------------------------------------------- 6. 语法
|
||
for rel, src in (('backend/drouter-helpd.py', HELPD),
|
||
('backend/drouter-web.py', WEB),
|
||
('backend/drouter-helper.py', HELPER)):
|
||
try:
|
||
ast.parse(src)
|
||
chk('%s 语法正确' % rel, True)
|
||
except SyntaxError as e:
|
||
chk('%s 语法正确' % rel, False, str(e))
|
||
|
||
print('\n结果: %s' % ('全部通过' if fails == 0 else '%d 项失败' % fails))
|
||
sys.exit(1 if fails else 0)
|