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

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)