一套跑在 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
167 lines
7.8 KiB
Python
167 lines
7.8 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""真机验收:内核转发与加速(#4)。
|
||
|
||
必须 scp 到目标机再跑(BASE 是 127.0.0.1:8443)。
|
||
用法: python3 /tmp/live-kern.py
|
||
|
||
会真正修改内核参数。之所以敢这么做:
|
||
* 开启 ip_forward 只是让本机「具备转发能力」,不改变默认网关、
|
||
不启动 DHCP、不加载 nft 规则集,不会产生网络风暴;
|
||
* IPv6 转发会同时设 accept_ra=2,保住本机靠 SLAAC 拿到的 IPv6;
|
||
* BBR 只影响本机自己发起的 TCP;
|
||
* SNMP 未安装,不会真的启动。
|
||
"""
|
||
import json
|
||
import ssl
|
||
import sys
|
||
import urllib.request
|
||
|
||
BASE = 'https://127.0.0.1:8443'
|
||
CTX = ssl._create_unverified_context()
|
||
TOK = None
|
||
PASS = FAIL = 0
|
||
|
||
|
||
def call(path, payload=None, timeout=60):
|
||
data = json.dumps(payload).encode('utf-8') if payload is not None else None
|
||
req = urllib.request.Request(BASE + path, data=data,
|
||
method='POST' if data is not None else 'GET')
|
||
req.add_header('Content-Type', 'application/json')
|
||
if TOK:
|
||
req.add_header('X-Token', TOK)
|
||
try:
|
||
with urllib.request.urlopen(req, timeout=timeout, context=CTX) as r:
|
||
return json.loads(r.read().decode('utf-8'))
|
||
except Exception as e:
|
||
return {'ok': False, 'msg_cn': 'HTTP 异常:%s' % e}
|
||
|
||
|
||
def chk(name, cond, extra=''):
|
||
global PASS, FAIL
|
||
if cond:
|
||
PASS += 1
|
||
print('[OK] %s %s' % (name, extra))
|
||
else:
|
||
FAIL += 1
|
||
print('[FAIL] %s %s' % (name, extra))
|
||
|
||
|
||
TOK = call('/api/login', {'username': 'admin', 'password': 'admin123'}).get('data', {}).get('token')
|
||
if not TOK:
|
||
print('登录失败,终止')
|
||
sys.exit(1)
|
||
|
||
# ---------------------------------------------------------------- 1. 读取
|
||
print('--- 状态读取 ---')
|
||
r = call('/api/kern', {'op': 'status'}, timeout=60)
|
||
chk('status 接口可用', r.get('ok') is True, '→ %s' % r.get('msg_cn'))
|
||
d = r.get('data') or {}
|
||
live0 = d.get('live') or {}
|
||
print(' 当前实际:ip_forward=%s ipv6_forwarding=%s 拥塞=%s 队列=%s'
|
||
% (live0.get('ip_forward'), live0.get('ipv6_forwarding'),
|
||
live0.get('congestion'), live0.get('qdisc')))
|
||
print(' 内核可用算法:%s' % live0.get('available'))
|
||
chk('返回实时内核参数', live0.get('ip_forward') is not None)
|
||
chk('返回可用拥塞算法', bool(live0.get('available')))
|
||
chk('返回 SNMP 安装状态', 'snmp_installed' in d, '→ %s' % d.get('snmp_installed'))
|
||
chk('返回持久化文件路径', '99-drouter.conf' in (d.get('sysctl_file') or ''))
|
||
|
||
items = d.get('items') or []
|
||
fws = d.get('fw_items') or []
|
||
chk('四项内核开关', len(items) == 4, '→ %d' % len(items))
|
||
chk('四项防火墙参数', len(fws) == 4, '→ %d' % len(fws))
|
||
for it in items + fws:
|
||
chk('%-14s 有说明与联动影响' % it.get('key'),
|
||
len(it.get('why') or '') >= 20 and len(it.get('impact') or []) >= 2)
|
||
|
||
fw0 = d.get('fw') or {}
|
||
print(' 防火墙参数:masq_v4=%s mss_v4=%s(%s/%s) masq_v6=%s mss_v6=%s'
|
||
% (fw0.get('masquerade_v4'), fw0.get('mss_v4'), fw0.get('mss_v4_mode'),
|
||
fw0.get('mss_v4_value'), fw0.get('masquerade_v6'), fw0.get('mss_v6')))
|
||
chk('默认 masquerade_v4 开', fw0.get('masquerade_v4') is True)
|
||
chk('默认 masquerade_v6 关', fw0.get('masquerade_v6') is False)
|
||
chk('默认 mss_v4 开', fw0.get('mss_v4') is True)
|
||
chk('默认 mss_v6 关', fw0.get('mss_v6') is False)
|
||
|
||
# ---------------------------------------------------------------- 2. 保存并生效
|
||
print('\n--- 保存并生效(转发 + BBR 全开)---')
|
||
r2 = call('/api/kern', {'op': 'save', 'fwd_v4': True, 'fwd_v6': True, 'bbr': True,
|
||
'fw': {'masquerade_v4': True, 'mss_v4': True,
|
||
'mss_v4_mode': 'clamp', 'mss_v4_value': 1452,
|
||
'masquerade_v6': False, 'mss_v6': False,
|
||
'mss_v6_mode': 'clamp', 'mss_v6_value': 1432},
|
||
'snmp': {'enabled': False}}, timeout=120)
|
||
chk('save 接口可用', r2.get('ok') is True, '→ %s' % r2.get('msg_cn'))
|
||
d2 = r2.get('data') or {}
|
||
print(' 已生效参数:%s' % ', '.join(d2.get('applied') or []))
|
||
if d2.get('errors'):
|
||
print(' 提示:%s' % ';'.join(d2['errors']))
|
||
chk('ip_forward 真的生效', 'net.ipv4.ip_forward' in (d2.get('applied') or []))
|
||
chk('ipv6 forwarding 真的生效', 'net.ipv6.conf.all.forwarding' in (d2.get('applied') or []))
|
||
chk('accept_ra=2 一起写了', 'net.ipv6.conf.all.accept_ra' in (d2.get('applied') or []))
|
||
chk('BBR 已生效', d2.get('bbr_active') is True)
|
||
|
||
r3 = call('/api/kern', {'op': 'status'}, timeout=60)
|
||
l3 = ((r3.get('data') or {}).get('live') or {})
|
||
print(' 生效后:ip_forward=%s ipv6_forwarding=%s 拥塞=%s 队列=%s'
|
||
% (l3.get('ip_forward'), l3.get('ipv6_forwarding'),
|
||
l3.get('congestion'), l3.get('qdisc')))
|
||
chk('ip_forward 已变为 1', l3.get('ip_forward') == '1')
|
||
chk('ipv6 forwarding 已变为 1', l3.get('ipv6_forwarding') == '1')
|
||
chk('拥塞控制已变为 bbr', l3.get('congestion') == 'bbr')
|
||
chk('队列已变为 fq', l3.get('qdisc') == 'fq')
|
||
|
||
# ---------------------------------------------------------------- 3. 渲染产物
|
||
print('\n--- 渲染产物核对 ---')
|
||
r4 = call('/api/render', {'module': 'nft_v4'}, timeout=60)
|
||
t4 = ((r4.get('data') or {}).get('text') or '')
|
||
chk('v4 规则集含 masquerade', 'masquerade' in t4)
|
||
chk('v4 规则集含 MSS 钳制', 'maxseg size set rt mtu' in t4)
|
||
r5 = call('/api/render', {'module': 'nft_v6'}, timeout=60)
|
||
t6 = ((r5.get('data') or {}).get('text') or '')
|
||
chk('v6 规则集默认无 NAT66', 'masquerade' not in t6)
|
||
chk('v6 规则集默认无 MSS', 'maxseg' not in t6)
|
||
|
||
# 打开 v6 的两项,再看渲染
|
||
call('/api/kern', {'op': 'save',
|
||
'fw': {'masquerade_v6': True, 'mss_v6': True}}, timeout=60)
|
||
r6 = call('/api/render', {'module': 'nft_v6'}, timeout=60)
|
||
t6b = ((r6.get('data') or {}).get('text') or '')
|
||
chk('v6 打开后出现 NAT66', 'masquerade' in t6b)
|
||
chk('v6 打开后出现 MSS 钳制', 'maxseg size set rt mtu' in t6b)
|
||
# 还原
|
||
call('/api/kern', {'op': 'save',
|
||
'fw': {'masquerade_v6': False, 'mss_v6': False}}, timeout=60)
|
||
print(' (v6 两项已还原为关闭)')
|
||
|
||
# ---------------------------------------------------------------- 4. 非法参数
|
||
print('\n--- 参数校验 ---')
|
||
bad = call('/api/kern', {'op': 'save', 'snmp': {'community': 'a b c'}}, timeout=60)
|
||
chk('团体名含空格被拒', bad.get('ok') is False, '→ %s' % bad.get('msg_cn'))
|
||
bad2 = call('/api/kern', {'op': 'save', 'snmp': {'port': 99999}}, timeout=60)
|
||
chk('端口越界被钳制而非报错', bad2.get('ok') is True)
|
||
bad3 = call('/api/kern', {'op': 'save', 'snmp': {'listen': '随便写'}}, timeout=60)
|
||
chk('非法监听地址被拒', bad3.get('ok') is False, '→ %s' % bad3.get('msg_cn'))
|
||
bad4 = call('/api/kern', {'op': 'bogus'}, timeout=60)
|
||
chk('未知 op 被拒', bad4.get('ok') is False)
|
||
|
||
# ---------------------------------------------------------------- 5. 关闭转发
|
||
print('\n--- 关闭转发(应立刻回到 0)---')
|
||
call('/api/kern', {'op': 'save', 'fwd_v4': False, 'fwd_v6': False}, timeout=120)
|
||
r7 = call('/api/kern', {'op': 'status'}, timeout=60)
|
||
l7 = ((r7.get('data') or {}).get('live') or {})
|
||
chk('ip_forward 已回到 0', l7.get('ip_forward') == '0', '→ %s' % l7.get('ip_forward'))
|
||
chk('ipv6 forwarding 已回到 0', l7.get('ipv6_forwarding') == '0')
|
||
# 恢复成推荐默认
|
||
call('/api/kern', {'op': 'save', 'fwd_v4': True, 'fwd_v6': True, 'bbr': True}, timeout=120)
|
||
r8 = call('/api/kern', {'op': 'status'}, timeout=60)
|
||
l8 = ((r8.get('data') or {}).get('live') or {})
|
||
chk('已恢复推荐默认(转发开 + BBR 开)',
|
||
l8.get('ip_forward') == '1' and l8.get('congestion') == 'bbr',
|
||
'fwd=%s cong=%s' % (l8.get('ip_forward'), l8.get('congestion')))
|
||
|
||
print('\n' + '=' * 60)
|
||
print('通过 %d 项,失败 %d 项' % (PASS, FAIL))
|
||
sys.exit(1 if FAIL else 0)
|