一套跑在 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
119 lines
4.1 KiB
Python
119 lines
4.1 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""终端手感体检(#10):量「敲下回车 → 屏幕上出现回显」到底要多久。
|
||
|
||
必须在目标机上跑(BASE = 127.0.0.1:8443)。
|
||
只读 + 只在一个临时会话里 echo,不动系统任何配置。
|
||
"""
|
||
import json
|
||
import ssl
|
||
import sys
|
||
import time
|
||
import urllib.request
|
||
|
||
BASE = 'https://127.0.0.1:8443'
|
||
CTX = ssl.create_default_context()
|
||
CTX.check_hostname = False
|
||
CTX.verify_mode = ssl.CERT_NONE
|
||
TK = None
|
||
|
||
|
||
def call(path, body=None, timeout=60):
|
||
hdrs = {'Content-Type': 'application/json'}
|
||
if TK:
|
||
hdrs['X-Token'] = TK
|
||
data = json.dumps(body or {}).encode() if body is not None else None
|
||
req = urllib.request.Request(BASE + path, data=data, headers=hdrs,
|
||
method='POST' if body is not None else 'GET')
|
||
t0 = time.time()
|
||
try:
|
||
with urllib.request.urlopen(req, context=CTX, timeout=timeout) as r:
|
||
j = json.loads(r.read().decode('utf-8'))
|
||
except Exception as e:
|
||
return {'ok': False, 'msg_cn': str(e)}, (time.time() - t0) * 1000
|
||
return j, (time.time() - t0) * 1000
|
||
|
||
|
||
def main():
|
||
global TK
|
||
j, _ = call('/api/login', {'username': 'admin', 'password': 'admin123'})
|
||
if not j.get('ok'):
|
||
print('登录失败:%s' % j.get('msg_cn'))
|
||
return 1
|
||
TK = j['data']['token']
|
||
|
||
j, dt = call('/api/webshell/connect', {'user': 'root', 'cols': 120, 'rows': 30})
|
||
if not j.get('ok'):
|
||
print('连接失败:%s' % j.get('msg_cn'))
|
||
return 1
|
||
sid = j['data']['sid']
|
||
print('连接耗时 %.0f ms,sid=%s\n' % (dt, sid))
|
||
|
||
# 1) 单次 read 往返耗时(空闲时)
|
||
ts = []
|
||
for _ in range(10):
|
||
_j, d = call('/api/webshell/read', {'sid': sid})
|
||
ts.append(d)
|
||
ts.sort()
|
||
print('空闲 read 往返(10 次):最快 %.0f ms 中位 %.0f ms 最慢 %.0f ms'
|
||
% (ts[0], ts[len(ts) // 2], ts[-1]))
|
||
|
||
# 2) 敲回车 → 首次看到回显,需要轮询几轮、多久
|
||
print('\n命令 → 首次回显延迟(模拟前端 60ms 轮询):')
|
||
for cmd in ('echo LAT_TEST_1', 'echo 中文延迟测试', 'uname -a'):
|
||
_j, dw = call('/api/webshell/write', {'sid': sid, 'data': cmd + '\n'})
|
||
t0 = time.time()
|
||
rounds = 0
|
||
got = ''
|
||
while time.time() - t0 < 8:
|
||
r, _d = call('/api/webshell/read', {'sid': sid})
|
||
rounds += 1
|
||
chunk = (r.get('data') or {}).get('data') or ''
|
||
if chunk:
|
||
got += chunk
|
||
if cmd.split()[1] in got or 'Linux' in got:
|
||
break
|
||
time.sleep(0.06)
|
||
dt = (time.time() - t0) * 1000
|
||
print(' %-16s 首字节 %.0f ms(轮询 %d 轮)' % (cmd, dt, rounds))
|
||
time.sleep(0.3)
|
||
|
||
# 3) 长输出的分帧完整性(会不会断字)
|
||
print('\n长输出分帧检查:')
|
||
call('/api/webshell/write', {'sid': sid,
|
||
'data': 'for i in $(seq 1 200); do echo "第 $i 行 中文混排 abcdefghij"; done\n'})
|
||
total = b''
|
||
t0 = time.time()
|
||
frames = 0
|
||
bad = 0
|
||
while time.time() - t0 < 15:
|
||
r, _d = call('/api/webshell/read', {'sid': sid})
|
||
chunk = (r.get('data') or {}).get('data') or ''
|
||
if chunk:
|
||
frames += 1
|
||
total += chunk.encode('utf-8', 'replace')
|
||
try:
|
||
total.decode('utf-8')
|
||
except UnicodeDecodeError:
|
||
bad += 1
|
||
elif frames and time.time() - t0 > 3:
|
||
break
|
||
time.sleep(0.06)
|
||
try:
|
||
text = total.decode('utf-8')
|
||
ok = True
|
||
except UnicodeDecodeError as e:
|
||
text = total.decode('utf-8', 'replace')
|
||
ok = False
|
||
print(' 收到 %d 帧 / %d 字节,UTF-8 解码 %s'
|
||
% (frames, len(total), '完整' if ok else '损坏(会显示黑菱形)'))
|
||
print(' 含 U+FFFD 替换字符:%d 个' % text.count('\ufffd'))
|
||
print(' 末行示例:%s' % (text.strip().splitlines()[-1][:60] if text.strip() else '(空)'))
|
||
|
||
call('/api/webshell/disconnect', {'sid': sid})
|
||
return 0
|
||
|
||
|
||
if __name__ == '__main__':
|
||
sys.exit(main())
|