一套跑在 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
100 lines
3.7 KiB
Python
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())
|