一套跑在 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
101 lines
3.7 KiB
Python
101 lines
3.7 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""环境契约测试:守护进程 PATH 归一化 + 虚拟网卡归类。
|
||
|
||
这两条都是「接口照常返回 ok,但功能悄悄不工作」的隐形缺陷,必须常驻回归:
|
||
1) systemd 默认 PATH 不含 /usr/sbin,nft/dnsmasq 会「未找到命令」;
|
||
2) 本机自建的虚拟设备(QoS 的 ifb-*)若被当成物理网卡,界面会一直提示指派角色。
|
||
"""
|
||
import ast
|
||
import os
|
||
import sys
|
||
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
ROOT = os.path.join(HERE, '..')
|
||
BACKEND = os.path.join(ROOT, 'backend')
|
||
|
||
GUARDS = ['drouter-web.py', 'drouter-helper.py', 'drouter-logd.py',
|
||
'drouter-snapshotd.py', 'drouter-rescue.py']
|
||
UNITS = [os.path.join(ROOT, 'scripts', 'deploy.sh')]
|
||
|
||
fails = []
|
||
|
||
|
||
def ck(name, cond, extra=''):
|
||
print(' %-46s %s %s' % (name, 'OK ' if cond else 'FAIL', extra))
|
||
if not cond:
|
||
fails.append(name)
|
||
|
||
|
||
print('一、守护进程 PATH 归一化')
|
||
for f in GUARDS:
|
||
src = open(os.path.join(BACKEND, f), encoding='utf-8').read()
|
||
ck('%s 定义了 _ensure_sbin_path' % f, '_ensure_sbin_path' in src)
|
||
ck('%s 启动时调用了它' % f, '_ensure_sbin_path()' in src)
|
||
|
||
# 真正执行一遍,确认 PATH 里确实被补进 sbin
|
||
src = open(os.path.join(BACKEND, 'drouter-helper.py'), encoding='utf-8').read()
|
||
tree = ast.parse(src)
|
||
picked = [n for n in tree.body
|
||
if isinstance(n, ast.FunctionDef) and n.name == '_ensure_sbin_path']
|
||
picked += [n for n in tree.body
|
||
if isinstance(n, ast.Assign)
|
||
and any(isinstance(t, ast.Name) and t.id == '_SBIN_DIRS' for t in n.targets)]
|
||
# 注:本机(Windows)上没有 /usr/sbin,_ensure_sbin_path 里的 os.path.isdir 守卫
|
||
# 会正确地跳过它们。测试要验证的是「在 Debian 上的行为」,所以这里伪造 isdir。
|
||
_real_isdir = os.path.isdir
|
||
|
||
|
||
class FakePath:
|
||
def __init__(self):
|
||
self.isdir = self._isdir
|
||
self.sep = os.path.sep
|
||
|
||
@staticmethod
|
||
def _isdir(p):
|
||
return True if p in ('/usr/sbin', '/sbin', '/usr/local/sbin') else _real_isdir(p)
|
||
|
||
|
||
class FakeOs:
|
||
environ = os.environ
|
||
path = FakePath()
|
||
pathsep = os.pathsep
|
||
sep = os.path.sep
|
||
|
||
|
||
ns = {'os': FakeOs()}
|
||
exec(compile(ast.Module(body=picked, type_ignores=[]), 'helper', 'exec'), ns)
|
||
ns['_ensure_sbin_path']()
|
||
parts = os.environ['PATH'].split(os.pathsep)
|
||
ck('调用后 PATH 含 /usr/sbin', '/usr/sbin' in parts)
|
||
ck('调用后 PATH 含 /sbin', '/sbin' in parts)
|
||
ck('sbin 排在前面(优先命中)',
|
||
parts.index('/usr/sbin') < len(parts) - 1)
|
||
ck('原有 PATH 未被清空', len(parts) >= 2, '%d 段' % len(parts))
|
||
# 幂等:再调一次不应重复插入
|
||
before = os.environ['PATH']
|
||
ns['_ensure_sbin_path']()
|
||
ck('重复调用幂等(不重复插入)', os.environ['PATH'] == before)
|
||
|
||
print('\n二、systemd 单元显式声明 PATH')
|
||
for p in UNITS:
|
||
txt = open(p, encoding='utf-8').read()
|
||
n = txt.count('Environment=PATH=/usr/local/sbin')
|
||
cnt = txt.count('[Service]')
|
||
ck('deploy.sh 每个 [Service] 都带 PATH(%d/%d)' % (n, cnt), n == cnt, '%d 个单元' % cnt)
|
||
|
||
print('\n三、虚拟网卡归类(ifb-* 不该要求指派角色)')
|
||
h = open(os.path.join(BACKEND, 'drouter-helper.py'), encoding='utf-8').read()
|
||
ck('is_virtual 前缀含 ifb', "'ifb'" in h and 'is_virtual' in h)
|
||
ck('read_ifaces 标注 managed_by', 'managed_by' in h)
|
||
|
||
js = open(os.path.join(ROOT, 'web', 'app.js'), encoding='utf-8').read()
|
||
ck('前端新网卡提示排除虚拟设备', 'managed_by' in js)
|
||
ck('前端提示带关闭按钮', 'newiface-x' in js)
|
||
ck('关闭状态持久化', 'drouter_newiface_dismissed' in js)
|
||
|
||
print('\n结果:失败 %d 项' % len(fails))
|
||
for f in fails:
|
||
print(' ✘ ' + f)
|
||
sys.exit(1 if fails else 0)
|