一套跑在 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
105 lines
5.1 KiB
Python
105 lines
5.1 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""访问控制 / 家长时间组 纯逻辑测试。"""
|
||
import re, sys, io
|
||
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('NOT FOUND: ' + name)
|
||
return m.group(0)
|
||
|
||
def grab_var(name, kind='['):
|
||
m = re.search(r'^%s = \%s.*?^\%s' % (re.escape(name), kind, ']' if kind == '[' else '}'), src, re.S | re.M)
|
||
if not m: raise SystemExit('NOT FOUND VAR: ' + name)
|
||
return m.group(0)
|
||
|
||
NS = {'re': re, 'json': __import__('json'), 'os': __import__('os')}
|
||
class ValidateError(Exception):
|
||
def __init__(self, msg_cn, field=None):
|
||
super().__init__(msg_cn); self.msg_cn = msg_cn; self.field = field
|
||
NS['ValidateError'] = ValidateError
|
||
NS['QOS_MARK_BASE'] = 0x1000
|
||
NS['TZ_OFFSET_H'] = 8
|
||
NS['ACL_TABLE'] = 'drouter_acl'
|
||
for v in ('ACL_TIME_PRESETS', 'ACL_APP_GROUPS', 'ACL_GROUP_TEMPLATES', 'ACL_ACTIONS'):
|
||
exec(grab_var(v), NS)
|
||
for f in ('_acl_hhmm_min', '_acl_min_hhmm', '_acl_localize_hhmm', '_acl_day_bit',
|
||
'_acl_shift_days', '_acl_time_windows', '_acl_time_expr', '_acl_render_nft'):
|
||
exec(grab(f), NS)
|
||
|
||
L, D, R = NS['_acl_localize_hhmm'], NS['_acl_day_bit'], NS['_acl_render_nft']
|
||
fails = 0
|
||
def chk(label, got, want):
|
||
global fails
|
||
ok = got == want
|
||
if not ok: fails += 1
|
||
print('[%s] %-46s got=%-22s want=%s' % ('OK' if ok else 'FAIL', label, str(got), str(want)))
|
||
|
||
print('=== 本地时间 → UTC 换算(UTC+8)===')
|
||
chk('07:00 → 23:00 前一日', L('07:00'), '23:00')
|
||
chk('17:00 → 09:00', L('17:00'), '09:00')
|
||
chk('22:00 → 14:00', L('22:00'), '14:00')
|
||
chk('06:30 → 22:30', L('06:30'), '22:30')
|
||
chk('00:00 → 16:00', L('00:00'), '16:00')
|
||
chk('08:00 → 00:00', L('08:00'), '00:00')
|
||
chk('23:59 → 15:59', L('23:59'), '15:59')
|
||
|
||
print('\n=== 星期集合 ===')
|
||
chk('工作日', D([1,2,3,4,5]), ['Mon','Tue','Wed','Thu','Fri'])
|
||
chk('周末', D([0,6]), ['Sun','Sat'])
|
||
chk('每天', D([0,1,2,3,4,5,6]), ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'])
|
||
|
||
print('\n=== 时间表达式(含跨零点)===')
|
||
NS['_acl_time_expr'] = NS['_acl_time_expr']
|
||
E = NS['_acl_time_expr']
|
||
chk('上学日白天', 'meta day' in E({'days':[1,2,3,4,5],'start':'07:00','end':'17:00'}), True)
|
||
e_night = E({'days':[0,1,2,3,4,5,6],'start':'22:00','end':'06:30'})
|
||
# 跨零点预览应拆成两段(段间用「或」连接),且带本地时间说明
|
||
chk('夜间跨零点拆两段', ' 或 ' in e_night, True)
|
||
chk('夜间预览含本地时间说明', '本地 22:00–06:30' in e_night, True)
|
||
print('\n=== 渲染:停用状态 ===')
|
||
off = R({'enable': False, 'time_groups': [], 'groups': [], 'rules': []})
|
||
chk('停用时无 drop', 'drop' not in off, True)
|
||
chk('停用时有计数说明', '访问控制已停用' in off, True)
|
||
|
||
print('\n=== 渲染:完整规则 ===')
|
||
d = {
|
||
'enable': True,
|
||
'time_groups': [
|
||
{'id':'night','name':'夜间睡眠','days':[0,1,2,3,4,5,6],'start':'22:00','end':'06:30'},
|
||
{'id':'school','name':'上学日','days':[1,2,3,4,5],'start':'07:00','end':'17:00'},
|
||
],
|
||
'groups': [
|
||
{'id':'kids','name':'孩子的设备','hosts':['192.168.7.20','192.168.7.21','2408:8207:1234::20']},
|
||
],
|
||
'rules': [
|
||
{'name':'夜间断娱乐','enable':True,'action':'block','time_group':'night','group':'kids','apps':['social','game','video']},
|
||
{'name':'上学日禁游戏','enable':True,'action':'block','time_group':'school','group':'kids','apps':['game']},
|
||
{'name':'只记录','enable':True,'action':'log','time_group':'school','group':'kids','apps':['video']},
|
||
{'name':'停用规则','enable':False,'action':'block','time_group':'night','group':'kids','apps':['p2p']},
|
||
],
|
||
}
|
||
out = R(d)
|
||
print(out)
|
||
chk('生成 inet 表', 'table inet drouter_acl' in out, True)
|
||
chk('forward 链优先级 -10', 'priority -10' in out, True)
|
||
chk('含 IPv4 组地址', '192.168.7.20' in out, True)
|
||
chk('含 IPv6 组地址', '2408:8207:1234::20' in out, True)
|
||
chk('含 drop 动作', 'counter drop' in out, True)
|
||
chk('含应用分类注释', '社交娱乐' in out and '游戏' in out, True)
|
||
chk('非跨零点规则未误判', out.count('本地时间跨零点') == 1, True)
|
||
chk('本地时间显示正确', '本地 07:00–17:00' in out, True)
|
||
# 上学日本地 07:00-17:00 → UTC 23:00-23:59(前一日,delta=-1)+ 00:00-09:00(当日,delta=0)
|
||
chk('上学日窗口 23:00-23:59', 'meta hour "23:00"-"23:59"' in out, True)
|
||
chk('上学日窗口 00:00-09:00', 'meta hour "00:00"-"09:00"' in out, True)
|
||
# 夜间本地 22:00-06:30 跨零点 → 本地 22:00-23:59=UTC 14:00-15:59;本地 00:00-06:30=UTC 16:00-22:30
|
||
chk('夜间窗口拆两段', 'meta hour "14:00"-"15:59"' in out and 'meta hour "16:00"-"22:30"' in out, True)
|
||
chk('停用规则不生成', 'P2P 下载' not in out, True)
|
||
chk('跨零点已拆分提示', '时间跨零点,已自动拆分' in out, True)
|
||
# 2 条 block 规则各拆 2 段 → 4 次 drop;log 规则不带 drop
|
||
chk('仅记录规则无 drop', out.count('counter drop') == 4, True)
|
||
|
||
print('\n结果: %s' % ('全部通过' if fails == 0 else '%d 项失败' % fails))
|
||
sys.exit(1 if fails else 0)
|