一套跑在 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
89 lines
3.2 KiB
Python
89 lines
3.2 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""统一日志性能契约(#7)—— 防止「每次查询固定白等 2 秒」回归。
|
|
|
|
踩过的坑:_ulog_from_conntrack 里固定跑
|
|
timeout 2 conntrack -E -o extended
|
|
`conntrack -E` 是阻塞的事件流,只能靠 timeout 掐断,于是**每查一次统一日志
|
|
都要白等 2 秒**。在这台还没接管转发的机器上连接跟踪表是空的,2 秒换来 0 条
|
|
记录 —— 实测 /api/ulog 的 2109ms 里 2019ms 花在这上面。
|
|
|
|
修法:实时事件订阅改成按需(live=True 才跑),默认查询走快路径。
|
|
本测试锁住这个约定。
|
|
"""
|
|
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()
|
|
|
|
|
|
HELPER = read('backend/drouter-helper.py')
|
|
APP = read('web/app.js')
|
|
TREE = ast.parse(HELPER)
|
|
|
|
fn = None
|
|
for n in TREE.body:
|
|
if isinstance(n, ast.FunctionDef) and n.name == '_ulog_from_conntrack':
|
|
fn = n
|
|
chk('存在 _ulog_from_conntrack', fn is not None)
|
|
if fn is None:
|
|
sys.exit(1)
|
|
|
|
src = ast.get_source_segment(HELPER, fn) or ''
|
|
|
|
# 1) 阻塞订阅必须被 live 开关包住,不能无条件执行
|
|
chk('conntrack -E 只在 live 分支里执行',
|
|
"'conntrack', '-E'" in src and 'if live:' in src)
|
|
# 无条件调用的特征:函数体里出现不在 if 内的 timeout 调用。
|
|
# 简单可靠的判断:'-E' 那行前面必须能找到 'if live:'
|
|
lines = src.splitlines()
|
|
e_idx = [i for i, l in enumerate(lines) if "'-E'" in l]
|
|
live_idx = [i for i, l in enumerate(lines) if 'if live:' in l]
|
|
chk('-E 调用位于 if live: 之后',
|
|
bool(e_idx) and bool(live_idx) and min(e_idx) > min(live_idx),
|
|
'if live 行=%s, -E 行=%s' % (live_idx[:1], e_idx[:1]))
|
|
# 2) 绝对不能出现写死的 timeout 2
|
|
chk('不再有写死的 timeout 2 阻塞订阅',
|
|
not re.search(r"'timeout',\s*'2'", src))
|
|
chk('订阅秒数由参数控制且上限 10 秒',
|
|
'live_sec' in src and 'min(int(live_sec), 10)' in src)
|
|
# 3) 默认关闭
|
|
chk('live 默认 False', 'live=False' in src)
|
|
chk('默认仍走内核日志回退(能力不丢)', 'journalctl' in src)
|
|
|
|
# 4) 采集层把 live 传下去
|
|
collect = None
|
|
for n in TREE.body:
|
|
if isinstance(n, ast.FunctionDef) and n.name == '_ulog_collect':
|
|
collect = n
|
|
csrc = ast.get_source_segment(HELPER, collect) or ''
|
|
chk('_ulog_collect 读取 live 参数',
|
|
"p.get('live')" in csrc and 'live=live' in csrc)
|
|
chk('_ulog_collect 把 live 状态回给前端', "'live': live" in csrc)
|
|
|
|
# 5) 前端有开关,且默认关
|
|
chk('前端 ulog 状态默认不订阅实时事件', 'live: false' in APP)
|
|
chk('前端查询带上 live 参数', 'live: !!u.live' in APP)
|
|
chk('前端提供实时开关控件', 'id="ul-live"' in APP)
|
|
chk('开关只在「统一日志」视图出现(当前连接视图不查事件流)',
|
|
"${isFlow ? '' : `" in APP and 'ul-live' in APP)
|
|
|
|
print('\n结果: %s' % ('全部通过' if fails == 0 else '%d 项失败' % fails))
|
|
sys.exit(1 if fails else 0)
|