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

170 lines
7.0 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.
#!/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)