一套跑在 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
141 lines
6.0 KiB
Python
141 lines
6.0 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""验证 SMB / NFS 渲染器(#9)。纯逻辑,可直接 import render.py。"""
|
||
import sys, io, os
|
||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||
'..', 'backend'))
|
||
import render as R # noqa: E402
|
||
|
||
fails = 0
|
||
|
||
|
||
def chk(name, got, want):
|
||
global fails
|
||
ok = (got == want)
|
||
if not ok:
|
||
fails += 1
|
||
print('[%s] %-40s got=%-24s want=%s' % ('OK' if ok else 'FAIL', name, repr(got), repr(want)))
|
||
|
||
|
||
def has(name, needle, hay, want=True):
|
||
chk(name, needle in hay, want)
|
||
|
||
|
||
print('=== SMB:基础全局段 ===')
|
||
cfg = {'samba': {
|
||
'workgroup': 'workgroup', 'server_string': 'drouter 文件共享',
|
||
'disable_netbios': True, 'interfaces': 'br0',
|
||
'shares': [
|
||
{'name': 'Public', 'path': '/srv/share/public', 'enable': True, 'mode': 'public',
|
||
'writable': True, 'comment': '公共盘'},
|
||
{'name': 'Media', 'path': '/srv/share/media', 'enable': True, 'mode': 'auth',
|
||
'writable': False, 'users': 'drouter, alice',
|
||
'allow_hosts': '192.168.7.0/24'},
|
||
]}}
|
||
conf = R.render_samba(cfg)
|
||
has('工作组大写化', 'workgroup = WORKGROUP', conf)
|
||
has('最小协议 SMB2', 'server min protocol = SMB2', conf)
|
||
has('最大协议 SMB3', 'server max protocol = SMB3', conf)
|
||
has('UTF-8 字符集', 'unix charset = UTF-8', conf)
|
||
has('关闭 NetBIOS', 'disable netbios = yes', conf)
|
||
has('绑定网卡', 'bind interfaces only = yes', conf)
|
||
has('公共盘 guest ok', 'guest ok = yes', conf)
|
||
has('公共盘可写', 'read only = no', conf)
|
||
has('公共盘 force user', 'force user = nobody', conf)
|
||
has('认证盘只读', '[Media]', conf)
|
||
has('认证盘 valid users', 'valid users = drouter, alice', conf)
|
||
has('认证盘 hosts allow', 'hosts allow = 192.168.7.0/24', conf)
|
||
has('macOS 支持 fruit', 'vfs objects = catia fruit streams_xattr', conf)
|
||
|
||
print('\n=== SMB:校验失败场景 ===')
|
||
for label, bad, kw in [
|
||
('空共享名', {'samba': {'shares': [{'name': '', 'path': '/srv/x'}]}}, '共享缺少名称'),
|
||
('非法共享名', {'samba': {'shares': [{'name': 'a/b', 'path': '/srv/x'}]}}, '不合法'),
|
||
('相对路径', {'samba': {'shares': [{'name': 'ok', 'path': 'srv/x'}]}}, '绝对路径'),
|
||
('目录穿越', {'samba': {'shares': [{'name': 'ok', 'path': '/srv/../etc'}]}}, '..'),
|
||
('非法工作组', {'samba': {'workgroup': '中文组', 'shares': []}}, '工作组名不合法'),
|
||
('非法用户名', {'samba': {'shares': [{'name': 'ok', 'path': '/srv/x', 'mode': 'auth',
|
||
'users': 'ROOT'}]}}, '不合规'),
|
||
('非法网段', {'samba': {'shares': [{'name': 'ok', 'path': '/srv/x',
|
||
'allow_hosts': '999.1.1.0/24'}]}}, '允许网段不合法'),
|
||
]:
|
||
try:
|
||
R.render_samba(bad)
|
||
chk(label, '未抛错', '应抛 ValidateError')
|
||
except R.ValidateError as e:
|
||
chk(label, kw in e.msg_cn, True)
|
||
|
||
print('\n=== SMB:共享名去重 ===')
|
||
try:
|
||
R.render_samba({'samba': {'shares': [
|
||
{'name': 'Same', 'path': '/srv/a'}, {'name': 'same', 'path': '/srv/b'}]}})
|
||
chk('共享名重复', '未抛错', '应抛 ValidateError')
|
||
except R.ValidateError as e:
|
||
chk('共享名重复', '重复' in e.msg_cn, True)
|
||
|
||
print('\n=== NFS:多平台模板 ===')
|
||
ncfg = {'nfs': {'threads': 16, 'exports': [
|
||
{'path': '/srv/share/public', 'enable': True, 'comment': '公共盘', 'clients': [
|
||
{'net': '192.168.7.0/24', 'preset': 'linux'},
|
||
{'net': '192.168.7.128/25', 'preset': 'macos'},
|
||
]},
|
||
{'path': '/srv/share/ro', 'enable': True, 'clients': [
|
||
{'net': '192.168.7.0/24', 'preset': 'readonly'}]},
|
||
]}}
|
||
exp = R.render_nfs(ncfg)
|
||
has('导出公共盘', '/srv/share/public 192.168.7.0/24(', exp)
|
||
has('linux 模板 root_squash', 'root_squash', exp)
|
||
has('macos 模板 all_squash', 'all_squash', exp)
|
||
has('macos 模板 insecure', 'insecure', exp)
|
||
has('只读模板 ro', 'ro,', exp)
|
||
|
||
nsconf = R.render_nfs_conf(ncfg)
|
||
has('线程数生效', 'threads=16', nsconf)
|
||
has('固定 2049 端口', 'port=2049', nsconf)
|
||
has('mountd 固定端口', 'port=20048', nsconf)
|
||
has('NFSv4.2 开启', 'vers4.2=y', nsconf)
|
||
|
||
print('\n=== NFS:IPv6 客户端 ===')
|
||
exp6 = R.render_nfs({'nfs': {'exports': [
|
||
{'path': '/srv/v6', 'clients': [{'net': '2408:8207:1234::/64', 'preset': 'linux'}]}]}})
|
||
has('IPv6 CIDR 导出', '2408:8207:1234::/64(', exp6)
|
||
|
||
print('\n=== NFS:通配符与校验 ===')
|
||
expw = R.render_nfs({'nfs': {'exports': [
|
||
{'path': '/srv/all', 'clients': [{'net': '*', 'preset': 'readonly'}]}]}})
|
||
has('通配符 *', '*(ro,', expw)
|
||
|
||
for label, bad, kw in [
|
||
('无客户端', {'nfs': {'exports': [{'path': '/srv/x', 'clients': []}]}}, '至少需要一个客户端'),
|
||
('非法客户端', {'nfs': {'exports': [{'path': '/srv/x',
|
||
'clients': [{'net': 'abc'}]}]}}, '客户端不合法'),
|
||
('非法线程数', {'nfs': {'threads': 999, 'exports': []}}, '1–128'),
|
||
('危险 no_root_squash', {'nfs': {'exports': [{'path': '/srv/x', 'clients': [
|
||
{'net': '192.168.7.0/24', 'preset': 'linux',
|
||
'options': 'rw,no_root_squash'}]}]}}, 'no_root_squash'),
|
||
]:
|
||
try:
|
||
if 'threads' in bad.get('nfs', {}):
|
||
R.render_nfs_conf(bad)
|
||
else:
|
||
R.render_nfs(bad)
|
||
chk(label, '未抛错', '应抛 ValidateError')
|
||
except R.ValidateError as e:
|
||
chk(label, kw in e.msg_cn, True)
|
||
|
||
print('\n=== 空导出 ===')
|
||
empty = R.render_nfs({'nfs': {'exports': []}})
|
||
has('空导出有说明', '未配置任何导出', empty)
|
||
|
||
print('\n=== 渲染入口注册 ===')
|
||
chk('samba 已注册', 'samba' in R.RENDERERS, True)
|
||
chk('nfs 已注册', 'nfs' in R.RENDERERS, True)
|
||
paths = [p for p, _ in R.render('smb', {'samba': {'shares': [
|
||
{'name': 'T', 'path': '/srv/t'}]}})]
|
||
chk('smb 别名 → 落盘路径', paths[0], '/etc/samba/drouter.conf')
|
||
npaths = [p for p, _ in R.render('nfs', ncfg)]
|
||
chk('nfs 落盘两处', len(npaths), 2)
|
||
chk('nfs exports 路径', npaths[0], '/etc/exports')
|
||
|
||
print('\n结果: %s' % ('全部通过' if fails == 0 else '%d 项失败' % fails))
|
||
sys.exit(1 if fails else 0)
|