Files
drouter/_dev/t-helpd.py
T
liuzhuohua 64df3cf331 feat: Drouter 1.0.0 —— Debian 13 软路由管理系统
一套跑在 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
2026-09-30 19:26:24 +08:00

111 lines
5.2 KiB
Python
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.
#!/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)