安全:
- chpasswd 换行注入可改 root 密码:用户名/密码拒绝换行与控制字符
- 删用户保护从写死两个名字改为 UID<1000 系统账号
- WOL 接口名白名单 + 目标地址 IPv4 校验,去 sh -c 管道
- ACL hosts 改 ipaddress 严格校验;存储挂载点限定 /mnt|/media|/srv
- 文件管理器拒绝清单扩到目录级 + 按文件名拦截(authorized_keys/shadow 等)
- 前端 201 处 value="${...}" 补 esc();href 只放行 http/https;
主题导出注释同时剥 /* 与 */
丢数据/假功能:
- ACL/文件共享/DDNS 三页切走再回来草稿被静默覆盖:新增 PAGE_DIRTY 脏页登记
- LAN 页网关/域名写进 system 死键,改绑 dnsmasq 同源
- UPnP 映射表按钮不再顺手起服务,走只读接口 read:upnpmap
资源:
- PTY 缓冲 8MB 封顶 + 会话信号量 + 后台空闲回收 + 离开页面主动断开
- Web 工作线程 48 封顶、请求体 16MB 封顶、会话 500 封顶改单调时钟、
审计 2 万行裁剪、helpd 并发 6 封顶
- helper 日志 32MB 轮转;快照前查磁盘余量;fs 下载 512MB→20MB
(通道单条响应 32MB,超限只会静默失败)
- 11 处配置写入改原子写;rescue/snapshot 单元补 MemoryMax(三处同步)
性能:
- /api/config 16 次开库 → 1 次 IN 查询
- cleanup 状态扫描 30s 缓存;gzip 缓存加 24MB 字节闸
- 后台标签页心跳暂停、终端轮询降频
杂项:DHCPv6 停用只留注释;租约回收用 dhcp_release(DEPS 可选登记);
VLAN 只删自建接口;公网探测清残留结果文件;快照根白名单;
心跳定时器防重复;keep_days/keep_rows 不吞 0
158 lines
5.5 KiB
Python
158 lines
5.5 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', '_atomic_write'):
|
||
exec(grab(fn), NS)
|
||
NS['tempfile'] = tempfile
|
||
|
||
# _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)
|