一套跑在 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
170 lines
7.0 KiB
Python
170 lines
7.0 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""真机总对账:在 192.168.7.3 上登录,逐项验证各功能页面依赖的接口与前端标记。
|
||
|
||
用法:scp 到目标机后 `python3 /tmp/live-check.py`(在目标机本地跑,走 127.0.0.1)。
|
||
"""
|
||
import json
|
||
import ssl
|
||
import sys
|
||
import urllib.error
|
||
import urllib.request
|
||
|
||
BASE = 'https://127.0.0.1:8443'
|
||
CTX = ssl._create_unverified_context()
|
||
|
||
TOK = None
|
||
|
||
|
||
def call(path, payload=None, method=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=method or ('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=25, context=CTX) as r:
|
||
return json.loads(r.read().decode('utf-8'))
|
||
except urllib.error.HTTPError as e:
|
||
try:
|
||
return json.loads(e.read().decode('utf-8'))
|
||
except Exception:
|
||
return {'ok': False, 'msg_cn': 'HTTP %s' % e.code}
|
||
except Exception as e:
|
||
return {'ok': False, 'msg_cn': str(e)}
|
||
|
||
|
||
r = call('/api/login', {'username': 'admin', 'password': 'admin123'})
|
||
if not r.get('ok'):
|
||
print('登录失败:%s' % r.get('msg_cn'))
|
||
sys.exit(1)
|
||
TOK = r['data']['token']
|
||
|
||
fails = []
|
||
|
||
|
||
def ck(name, cond, extra=''):
|
||
print(' %-28s %s %s' % (name, 'OK ' if cond else 'FAIL', extra))
|
||
if not cond:
|
||
fails.append(name)
|
||
|
||
|
||
print('=' * 60)
|
||
print('一、入口与鉴权')
|
||
ck('健康检查 /api/health', call('/api/health').get('ok'))
|
||
ck('登录并取 token', len(TOK) > 10, 'len=%d' % len(TOK))
|
||
ck('当前用户 /api/me', call('/api/me').get('ok'))
|
||
sysinfo = call('/api/sysinfo')
|
||
d = sysinfo.get('data') or {}
|
||
ck('系统信息含主机名', bool(d.get('host') or d.get('hostname')),
|
||
str(d.get('host') or d.get('hostname'))[:30])
|
||
ck('概览仪表盘有 CPU 数据', 'cpu' in d or 'cpu_pct' in d)
|
||
|
||
print('\n二、配置模块(统一 /api/config)')
|
||
cfg = (call('/api/config').get('data') or {})
|
||
ck('配置模块数 = 16', len(cfg) == 16, '实际 %d' % len(cfg))
|
||
need = ['dnsmasq', 'nft_v4', 'nft_v6', 'radvd', 'dhcpv6', 'upnp', 'ntp',
|
||
'system', 'pppoe', 'ddns', 'portfwd', 'acl', 'share', 'docker',
|
||
'ulog', 'theme']
|
||
miss = [k for k in need if k not in cfg]
|
||
ck('16 个模块齐全', not miss, '缺 %s' % miss if miss else '')
|
||
|
||
print('\n三、网络 / 网卡 / WAN')
|
||
ck('网卡列表 /api/ifaces', call('/api/ifaces').get('ok'))
|
||
ck('网卡配置方式 /api/iface_method', call('/api/iface_method').get('ok'))
|
||
ck('路由表 /api/routes', call('/api/routes').get('ok'))
|
||
ck('IPv6 状态 /api/ipv6', call('/api/ipv6').get('ok'))
|
||
ck('VLAN /api/vlan', call('/api/vlan').get('ok'))
|
||
ck('WOL /api/wol', call('/api/wol').get('ok'))
|
||
ck('PPPoE 多拨 /api/pppoe-multi', call('/api/pppoe-multi').get('ok'))
|
||
ck('PPPoE 拨号日志 /api/ppp/log', call('/api/ppp/log').get('ok'))
|
||
|
||
print('\n四、防火墙 / 日志')
|
||
fw = call('/api/fwlog')
|
||
ck('防火墙日志 /api/fwlog', fw.get('ok'))
|
||
dd = fw.get('data') or {}
|
||
# 实际字段:items(日志行数组)/ enabled(开关)/ stat(统计)/ since
|
||
ck('防火墙日志有 items 内容', isinstance(dd.get('items'), list),
|
||
'items=%d 条' % len(dd.get('items') or []))
|
||
ck('防火墙日志有开关字段 enabled', 'enabled' in dd, 'enabled=%s' % dd.get('enabled'))
|
||
ck('nft 规则集 /api/nft', call('/api/nft').get('ok'))
|
||
ck('WAN 日志 /api/wan/log', call('/api/wan/log').get('ok'))
|
||
ck('统一日志 /api/ulog', call('/api/ulog').get('ok'))
|
||
ck('日志守护配置 /api/ulog/conf', call('/api/ulog/conf').get('ok'))
|
||
ck('日志守护状态 /api/ulog/daemon', call('/api/ulog/daemon').get('ok'))
|
||
ck('流量日志 /api/ulog/flow', call('/api/ulog/flow').get('ok'))
|
||
ck('系统日志 /api/journal', call('/api/journal').get('ok'))
|
||
ck('实时指标 /api/metrics', call('/api/metrics').get('ok'))
|
||
|
||
print('\n五、服务功能')
|
||
ck('DHCP 租约 /api/leases', call('/api/leases').get('ok'))
|
||
ck('DDNS /api/ddns', call('/api/ddns').get('ok'))
|
||
ck('ACL /api/acl', call('/api/acl').get('ok'))
|
||
ck('共享 SMB/NFS /api/share', call('/api/share').get('ok'))
|
||
ck('Docker /api/docker', call('/api/docker').get('ok'))
|
||
ck('QoS /api/qos', call('/api/qos').get('ok'))
|
||
ck('DPI /api/dpi', call('/api/dpi').get('ok'))
|
||
ck('流加速 /api/accel', call('/api/accel').get('ok'))
|
||
ck('服务管理 /api/services', call('/api/services').get('ok'))
|
||
ck('NTP /api/ntp', call('/api/ntp').get('ok'))
|
||
ck('NAT 检测 /api/nat/check', call('/api/nat/check').get('ok'))
|
||
# /api/diag 是 POST,参数名是 tool(不是 kind);GET 会落进 NOTFOUND
|
||
dg = call('/api/diag', {'tool': 'ping', 'target': '127.0.0.1', 'count': 2})
|
||
ck('诊断 /api/diag (POST+tool)', dg.get('ok'), str(dg.get('msg_cn'))[:40])
|
||
|
||
print('\n六、主题之家')
|
||
ck('主题列表 /api/themes', call('/api/themes').get('ok'))
|
||
th = call('/api/themes')
|
||
td = th.get('data') or {}
|
||
lst = td.get('list') or td.get('themes') or (td if isinstance(td, list) else [])
|
||
ck('内置主题 >= 5 个', len(lst) >= 5, '实际 %d' % len(lst))
|
||
ck('当前生效主题可读', bool((cfg.get('theme') or {}).get('active')),
|
||
str((cfg.get('theme') or {}).get('active')))
|
||
|
||
print('\n七、快照 / 救援 / 依赖')
|
||
ck('快照列表 /api/snapshot/list', call('/api/snapshot/list').get('ok'))
|
||
ck('自动快照 /api/snapshot/auto', call('/api/snapshot/auto').get('ok'))
|
||
ck('救援通道 /api/rescue', call('/api/rescue').get('ok'))
|
||
ck('依赖自检 /api/depcheck', call('/api/depcheck').get('ok'))
|
||
ck('构建保护模式 /api/buildmode', call('/api/buildmode').get('ok'))
|
||
ck('Web 端口 /api/webport', call('/api/webport').get('ok'))
|
||
|
||
print('\n八、前端资源(浏览器实际拿到的)')
|
||
|
||
|
||
def fetch(path):
|
||
req = urllib.request.Request(BASE + path)
|
||
try:
|
||
with urllib.request.urlopen(req, timeout=25, context=CTX) as r:
|
||
return r.getcode(), r.read().decode('utf-8', 'replace'), dict(r.headers)
|
||
except urllib.error.HTTPError as e:
|
||
return e.code, e.read().decode('utf-8', 'replace'), {}
|
||
except Exception as e:
|
||
return 0, str(e), {}
|
||
|
||
|
||
c, html, hdr = fetch('/')
|
||
ck('入口页 200', c == 200)
|
||
ck('入口页带资源版本号', 'app.js?v=' in html and 'app.css?v=' in html)
|
||
ck('入口页 no-store', 'no-store' in (hdr.get('Cache-Control') or ''),
|
||
hdr.get('Cache-Control') or '(无)')
|
||
ck('入口页有手机遮罩 m-mask', 'm-mask' in html)
|
||
c, js, _ = fetch('/app.js')
|
||
ck('app.js 可取且 >300KB', c == 200 and len(js) > 300000, '%d bytes' % len(js))
|
||
ck('app.js 含主题之家', '主题之家' in js)
|
||
ck('app.js 含手机抽屉', 'm-drawer' in js)
|
||
ck('app.js 含 35 个视图映射', js.count('view') > 30)
|
||
c, css, _ = fetch('/app.css')
|
||
ck('app.css 含手机媒体查询', 'max-width:760px' in css)
|
||
ck('app.css 含表格滚动容器 .tw', '.tw' in css)
|
||
c, tcss, _ = fetch('/theme.css')
|
||
ck('主题样式 /theme.css 可取', c == 200)
|
||
|
||
print('\n' + '=' * 60)
|
||
print('结果:失败 %d 项' % len(fails))
|
||
if fails:
|
||
for f in fails:
|
||
print(' ✘ ' + f)
|
||
sys.exit(1 if fails else 0)
|