一套跑在 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
98 lines
3.9 KiB
Python
98 lines
3.9 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""本轮 4 项改动的真机验收:累计流量 / 退出按钮 / 刷新按钮 / ifb 归类。"""
|
|
import json
|
|
import ssl
|
|
import urllib.request
|
|
|
|
BASE = 'https://127.0.0.1:8443'
|
|
CTX = ssl._create_unverified_context()
|
|
TOK = None
|
|
|
|
|
|
def call(path, payload=None):
|
|
data = json.dumps(payload or {}).encode('utf-8') if payload is not None else None
|
|
req = urllib.request.Request(BASE + path, data=data,
|
|
method='POST' if data else 'GET')
|
|
req.add_header('Content-Type', 'application/json')
|
|
if TOK:
|
|
req.add_header('X-Token', TOK)
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=30, context=CTX) as r:
|
|
return json.loads(r.read().decode('utf-8'))
|
|
except Exception as e:
|
|
return {'ok': False, 'msg_cn': str(e)}
|
|
|
|
|
|
def fetch(path):
|
|
req = urllib.request.Request(BASE + path)
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=30, context=CTX) as r:
|
|
return r.getcode(), r.read().decode('utf-8', 'replace')
|
|
except Exception as e:
|
|
return 0, str(e)
|
|
|
|
|
|
fails = []
|
|
|
|
|
|
def ck(name, cond, extra=''):
|
|
print(' %-40s %s %s' % (name, 'OK ' if cond else 'FAIL', extra))
|
|
if not cond:
|
|
fails.append(name)
|
|
|
|
|
|
TOK = call('/api/login', {'username': 'admin', 'password': 'admin123'})['data']['token']
|
|
|
|
print('=' * 62)
|
|
print('#1 历史累计流量')
|
|
si = call('/api/sysinfo')
|
|
nt = (si.get('data') or {}).get('net_total') or {}
|
|
ck('sysinfo 返回 net_total', bool(nt), 'keys=%s' % sorted(nt)[:6])
|
|
ck('含可读的累计下载量 rx_h', bool(nt.get('rx_h')), str(nt.get('rx_h')))
|
|
ck('含可读的累计上传量 tx_h', bool(nt.get('tx_h')), str(nt.get('tx_h')))
|
|
ck('含统计起始时间 since', bool(nt.get('since')), str(nt.get('since')))
|
|
mt = call('/api/metrics')
|
|
mnt = (mt.get('data') or {}).get('net_total') or {}
|
|
ck('metrics 也返回 net_total', bool(mnt), '%s / %s' % (mnt.get('rx_h'), mnt.get('tx_h')))
|
|
|
|
c, js = fetch('/app.js')
|
|
ck('前端不再把累计块叫「实时上行 / 下行」', '实时上行 / 下行' not in js)
|
|
ck('前端改叫「累计已下载 / 已上传」', '累计已下载 / 已上传' in js)
|
|
ck('实时速率只在「网络实时质量」里', 'q-rx' in js and '网络实时质量' in js)
|
|
|
|
print('\n#2 左下角退出系统按钮')
|
|
c, html = fetch('/')
|
|
ck('入口页有 btn-logout', 'id="btn-logout"' in html)
|
|
ck('按钮文案是「退出系统」', '退出系统' in html)
|
|
ck('在侧栏底部 side-foot 内', html.index('side-foot') < html.index('btn-logout') < html.index('</aside>'))
|
|
ck('app.js 绑定了登出逻辑', 'btn-logout' in js and 'logout(false)' in js)
|
|
|
|
print('\n#3 右上角刷新按钮')
|
|
ck('入口页有 btn-refresh', 'id="btn-refresh"' in html)
|
|
ck('在顶栏 tb-right 内', html.index('tb-right') < html.index('btn-refresh') < html.index('</header>'))
|
|
ck('app.js 绑定了刷新逻辑', 'btn-refresh' in js and "btnRefresh.classList.add('spinning')" in js)
|
|
|
|
print('\n#4 新网卡提示 / ifb-drouter 归类')
|
|
ifs = (call('/api/ifaces').get('data') or [])
|
|
ifb = [i for i in ifs if (i.get('name') or '').startswith('ifb')]
|
|
ck('目标机上存在 ifb-* 网卡', bool(ifb),
|
|
'、'.join(i['name'] for i in ifb) or '(无)')
|
|
ck('ifb-* 已被标记为虚拟设备', all(i.get('is_virtual') for i in ifb))
|
|
ck('ifb-* 标注了 managed_by', all(i.get('managed_by') for i in ifb),
|
|
'、'.join(str(i.get('managed_by')) for i in ifb))
|
|
ck('前端提示已排除虚拟设备', '!i.is_virtual && !i.managed_by' in js)
|
|
ck('提示带 X 关闭按钮', 'newiface-x' in js)
|
|
ck('关闭状态写入 localStorage', 'drouter_newiface_dismissed' in js)
|
|
|
|
c, css = fetch('/app.css')
|
|
ck('CSS 有 .tip-x 样式', '.tip-x' in css)
|
|
ck('CSS 有 .side-logout 样式', '.side-logout' in css)
|
|
ck('CSS 有刷新旋转动画', 'spinning' in css and '@keyframes spin' in css)
|
|
|
|
print('\n' + '=' * 62)
|
|
print('结果:失败 %d 项' % len(fails))
|
|
for f in fails:
|
|
print(' ✘ ' + f)
|
|
raise SystemExit(1 if fails else 0)
|