一套跑在 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
157 lines
5.4 KiB
Python
157 lines
5.4 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""验证 ACL 落盘部署逻辑(#8)。
|
||
|
||
helper 依赖 pwd/systemd,无法在 Windows 直接 import,因此这里用
|
||
正则从源码抽取所需片段,注入到隔离命名空间中运行,并用临时目录
|
||
替换 ACL_CONF / ACL_JSON / ACL_UNIT。
|
||
"""
|
||
import re, io, os, sys, tempfile, shutil, json
|
||
|
||
SRC = r"C:/Users/lyrz-pve-win10/WorkBuddy/2026-09-27-19-57-00/router-build/backend/drouter-helper.py"
|
||
src = io.open(SRC, encoding='utf-8').read()
|
||
|
||
|
||
def grab(name):
|
||
m = re.search(r'^def %s\(.*?(?=\n(?:def |[A-Z_]+ *=|# ---|class ))' % re.escape(name),
|
||
src, re.S | re.M)
|
||
if not m:
|
||
raise SystemExit('未找到函数:%s' % name)
|
||
return m.group(0)
|
||
|
||
|
||
class ValidateError(Exception):
|
||
def __init__(self, m, f=None):
|
||
self.msg_cn = m
|
||
|
||
|
||
def grab_const(name):
|
||
m = re.search(r'^%s = .*?$' % re.escape(name), src, re.M)
|
||
return m.group(0)
|
||
|
||
|
||
tmp = tempfile.mkdtemp(prefix='acl-deploy-')
|
||
NS = {'re': re, 'os': os, 'json': json, 'subprocess': _stub if False else None,
|
||
'ValidateError': ValidateError, 'TZ_OFFSET_H': 8,
|
||
'fail': lambda m, c=None, d=None: {'ok': False, 'msg_cn': m},
|
||
'ok': lambda d=None, m='', c=0: {'ok': True, 'data': d, 'msg_cn': m}}
|
||
|
||
|
||
class _Sub:
|
||
"""假的 subprocess,记录调用而不真的执行。"""
|
||
called = []
|
||
|
||
@staticmethod
|
||
def run(args, **kw):
|
||
_Sub.called.append(list(args))
|
||
|
||
class R:
|
||
returncode = 0
|
||
stderr = b''
|
||
return R()
|
||
|
||
|
||
NS['subprocess'] = _Sub
|
||
|
||
# 常量:把路径全部改到临时目录
|
||
NS['ACL_CONF'] = os.path.join(tmp, 'generated', 'acl.nft')
|
||
NS['ACL_JSON'] = os.path.join(tmp, 'generated', 'acl.json')
|
||
NS['ACL_UNIT'] = os.path.join(tmp, 'system', 'drouter-acl.service')
|
||
NS['ACL_TABLE'] = 'drouter_acl'
|
||
NS['BUILD_MODE_FILE'] = os.path.join(tmp, 'BUILD_MODE')
|
||
|
||
# 抽取模板
|
||
m = re.search(r'^ACL_UNIT_TMPL = """.*?"""', src, re.S | re.M)
|
||
if not m:
|
||
raise SystemExit('未找到 ACL_UNIT_TMPL')
|
||
exec(m.group(0), NS)
|
||
|
||
for fn in ('_acl_hhmm_min', '_acl_min_hhmm', '_acl_day_bit', '_acl_shift_days',
|
||
'_acl_time_windows', '_acl_time_expr', '_acl_load', '_acl_save',
|
||
'_acl_render_nft'):
|
||
exec(grab(fn), NS)
|
||
|
||
# _acl_deploy 里的 BUILD_MODE 路径改成临时文件(在命名空间内可见的全局名)
|
||
_src_deploy = grab('_acl_deploy').replace(
|
||
"os.path.isfile('/etc/drouter/BUILD_MODE')",
|
||
"os.path.isfile(BUILD_MODE_FILE)")
|
||
exec(_src_deploy, NS)
|
||
DEPLOY = NS['_acl_deploy']
|
||
|
||
# 需要 ACL_APP_GROUPS 供 _acl_render_nft 使用
|
||
mgrp = re.search(r'^ACL_APP_GROUPS = \[.*?\n\]', src, re.S | re.M)
|
||
exec(mgrp.group(0), NS)
|
||
mq = re.search(r'^QOS_MARK_BASE = .*?$', src, re.M)
|
||
if mq:
|
||
exec(mq.group(0), NS)
|
||
else:
|
||
NS['QOS_MARK_BASE'] = 0x100
|
||
|
||
fails = 0
|
||
|
||
|
||
def chk(name, got, want):
|
||
global fails
|
||
ok = (got == want)
|
||
if not ok:
|
||
fails += 1
|
||
print('[%s] %-34s got=%-22s want=%s' % ('OK' if ok else 'FAIL', name, got, want))
|
||
|
||
|
||
CFG = {
|
||
'enable': True,
|
||
'time_groups': [{'id': 'night', 'name': '夜间', 'days': [0, 1, 2, 3, 4, 5, 6],
|
||
'start': '22:00', 'end': '06:30'}],
|
||
'groups': [{'id': 'kids', 'name': '孩子设备', 'hosts': ['192.168.7.20']}],
|
||
'rules': [{'name': '夜间断网', 'enable': True, 'action': 'block',
|
||
'time_group': 'night', 'group': 'kids', 'apps': []}],
|
||
}
|
||
|
||
print('=== 落盘(live=False,只写文件)===')
|
||
r = DEPLOY(CFG, live=False)
|
||
chk('生成 nft 文件', os.path.isfile(NS['ACL_CONF']), True)
|
||
chk('返回 applied=False', r.get('applied'), False)
|
||
body = io.open(NS['ACL_CONF'], encoding='utf-8').read()
|
||
chk('文件含 inet 表', 'table inet drouter_acl' in body, True)
|
||
chk('文件含 drop', 'counter drop' in body, True)
|
||
chk('unit 文件已生成', os.path.isfile(NS['ACL_UNIT']), True)
|
||
unit = io.open(NS['ACL_UNIT'], encoding='utf-8').read()
|
||
chk('unit 指向 conf', NS['ACL_CONF'] in unit, True)
|
||
chk('unit 有 ExecStart', 'ExecStart=/usr/sbin/nft -f' in unit, True)
|
||
chk('未执行 nft(live=False)',
|
||
any(a[:1] == ['nft'] and '-f' in a for a in _Sub.called), False)
|
||
|
||
print('\n=== 构建保护模式(文件存在时不加载)===')
|
||
io.open(NS['BUILD_MODE_FILE'], 'w').write('1')
|
||
_Sub.called = []
|
||
r2 = DEPLOY(CFG, live=True)
|
||
chk('applied=False', r2.get('applied'), False)
|
||
chk('提示构建保护', '构建保护' in (r2.get('msg_cn') or ''), True)
|
||
chk('未调用 nft', any(a[:1] == ['nft'] for a in _Sub.called), False)
|
||
os.remove(NS['BUILD_MODE_FILE'])
|
||
|
||
print('\n=== 正常加载(live=True)===')
|
||
_Sub.called = []
|
||
r3 = DEPLOY(CFG, live=True)
|
||
chk('applied=True', r3.get('applied'), True)
|
||
calls = [a for a in _Sub.called if a[:1] == ['nft']]
|
||
chk('先删旧表', calls[0][:4] == ['nft', 'delete', 'table', 'inet'], True)
|
||
chk('再加载文件', calls[1][:2] == ['nft', '-f'], True)
|
||
chk('加载的是 ACL_CONF', calls[1][2] == NS['ACL_CONF'], True)
|
||
|
||
print('\n=== 停用状态(enable=False)===')
|
||
_Sub.called = []
|
||
r4 = DEPLOY(dict(CFG, enable=False), live=True)
|
||
chk('applied=True', r4.get('applied'), True)
|
||
body2 = io.open(NS['ACL_CONF'], encoding='utf-8').read()
|
||
chk('停用时无 drop', 'counter drop' in body2, False)
|
||
chk('有停用说明', '访问控制已停用' in body2, True)
|
||
|
||
print('\n=== 幂等:重复部署不重复写 unit ===')
|
||
mtime = os.path.getmtime(NS['ACL_UNIT'])
|
||
DEPLOY(CFG, live=False)
|
||
chk('unit 未重复写入', os.path.getmtime(NS['ACL_UNIT']), mtime)
|
||
|
||
shutil.rmtree(tmp, ignore_errors=True)
|
||
print('\n结果: %s' % ('全部通过' if fails == 0 else '%d 项失败' % fails))
|
||
sys.exit(1 if fails else 0)
|