Files
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

105 lines
5.1 KiB
Python
Raw Permalink 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.
# -*- 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)