Files
drouter/_dev/live-apply-probe.py
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

100 lines
3.7 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""真机探测:每个页面的「应用」按钮到底能不能跑通。
用法:必须拷到目标机上执行(BASE 是 127.0.0.1)。
python3 live-apply-probe.py
只发 check_only=true,不写盘、不启服务、不动网络 —— 纯粹验证
「前端提交的 module 名 → 后端 APPLY_SPEC / render 渲染器」这条链是否闭合。
"""
import json
import ssl
import sys
import urllib.request
BASE = 'https://127.0.0.1:8443'
CTX = ssl._create_unverified_context()
# 页面 key → 该页「应用」要渲染的模块(与 app.js 的 PAGE_MODULES.apply 保持一致)
CASES = [
('iface', []),
('lan', []),
('wan', ['pppoe']),
('dhcp', ['dnsmasq']),
('dns', ['dnsmasq']),
('dhcpv6', ['dhcpv6']),
('ipv6', ['radvd', 'dhcpv6']),
('fw4', ['nft_v4']),
('fw6', ['nft_v6']),
('portfwd', ['nft_v4', 'nft_v6']),
('upnp', ['upnp']),
('ntp', ['ntp']),
]
# 纯配置模块:不会再被前端送去 apply,但后端仍要给出明确提示而不是报错
CONFIG_ONLY = ['system', 'portfwd']
def post(path, body, tk):
req = urllib.request.Request(BASE + path, data=json.dumps(body).encode(),
headers={'Content-Type': 'application/json',
'X-Token': tk},
method='POST')
try:
with urllib.request.urlopen(req, context=CTX, timeout=60) as r:
return json.loads(r.read().decode('utf-8'))
except urllib.error.HTTPError as e:
return json.loads(e.read().decode('utf-8'))
except Exception as e:
return {'ok': False, 'msg_cn': '请求异常:%s' % e}
def main():
req = urllib.request.Request(BASE + '/api/login',
data=json.dumps({'username': 'admin',
'password': 'admin123'}).encode(),
headers={'Content-Type': 'application/json'},
method='POST')
with urllib.request.urlopen(req, context=CTX, timeout=30) as r:
j = json.loads(r.read().decode('utf-8'))
if not j.get('ok'):
print('登录失败:%s' % j.get('msg_cn'))
return 1
tk = j['data']['token']
bad = 0
print('%-10s %-10s %-6s %s' % ('页面', 'module', '结果', '后端提示'))
print('-' * 90)
for page, mods in CASES:
if not mods:
print('%-10s %-10s %-6s %s' % (page, '(无)', '—', '该页只保存,不渲染配置文件'))
continue
for m in mods:
# 不传 data,避免 merge_cfg 改动任何已保存配置
r = post('/api/apply', {'module': m, 'check_only': True}, tk)
msg = r.get('msg_cn', '')
if r.get('ok'):
flag = 'OK'
elif msg.startswith('配置校验失败'):
# 渲染器接住了、只是配置内容还没填(例如没配 PPPoE 网卡)。
# 这是「填内容」的问题,不是「接线」的问题,不计入失败。
flag = 'CFG'
else:
flag = 'FAIL'
bad += 1
print('%-10s %-10s %-6s %s' % (page, m, flag, msg))
print('\n纯配置模块(防御性检查:不应再报「未知的模块」)')
for m in CONFIG_ONLY:
r = post('/api/apply', {'module': m, 'check_only': True}, tk)
flag = 'OK' if r.get('ok') else 'FAIL'
if not r.get('ok'):
bad += 1
print('%-10s %-10s %-6s %s' % ('-', m, flag, r.get('msg_cn', '')))
print('\n失败项:%d' % bad)
return 1 if bad else 0
if __name__ == '__main__':
sys.exit(main())