Files
drouter/_dev/t-packaging.py
T
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

176 lines
7.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""打包产物静态自检:deb 元数据 / 维护脚本 / Dockerfile。
真机构建由 packaging/build-deb.sh 负责(需要 dpkg-deb),本脚本负责在任何机器上
都能跑的「结构正确性」检查,把「打出来的包不能用」挡在构建之前。
"""
import os
import subprocess
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.normpath(os.path.join(HERE, '..'))
fails = []
def ck(name, cond, extra=''):
print(' %-46s %s %s' % (name, 'OK ' if cond else 'FAIL', extra))
if not cond:
fails.append(name)
def read(rel):
with open(os.path.join(ROOT, rel), encoding='utf-8') as f:
return f.read()
def no_crlf(rel):
with open(os.path.join(ROOT, rel), 'rb') as f:
return b'\r' not in f.read()
def bash_ok(rel):
"""能用 bash 就做真语法检查;Windows 上没有 bash 就跳过(不算失败)。"""
exe = subprocess.run(['bash', '-n', os.path.join(ROOT, rel)],
capture_output=True)
if exe.returncode == 127 or (b'not found' in exe.stderr.lower()):
return None
return exe.returncode == 0
print('一、deb 元数据(packaging/deb/control)')
ctl = read('packaging/deb/control')
ck('含 Package', 'Package: drouter' in ctl)
ck('Version 是占位符(构建时替换)', '__VERSION__' in ctl)
ck('Architecture: all', 'Architecture: all' in ctl)
ck('Depends 非空', 'Depends:' in ctl and len(ctl.split('Depends:')[1].split('\n\n')) > 0)
for pkg in ('python3', 'nftables', 'dnsmasq', 'radvd', 'ppp', 'systemd', 'sudo'):
ck('Depends 含 %s' % pkg, pkg in ctl.split('Recommends:')[0])
# smartmontools 装完会自动起 smartd,不该出现在 apt 默认安装的 Recommends 里
rec = ctl.split('Recommends:')[1].split('\n')[0] if 'Recommends:' in ctl else ''
ck('Recommends 不含 smartmontools(会自启守护)', 'smartmontools' not in rec, rec.strip()[:50])
ck('smartmontools 放进了 Suggests',
'smartmontools' in (ctl.split('Suggests:')[1] if 'Suggests:' in ctl else ''))
ck('control 无 CRLF', no_crlf('packaging/deb/control'))
# DEBIAN/control 是 deb822 格式,官方并不保证支持 "#" 注释 —— 别往里写注释
ck('control 不含 # 注释行(deb822 不保证支持)',
not [l for l in ctl.split('\n') if l.lstrip().startswith('#')])
# 每一行必须是「空行」或「字段名: 值」或「以空格/制表符开头的续行」
bad_cont = []
for l in ctl.split('\n'):
if not l.strip():
continue
if l[0] in (' ', '\t'):
continue
if ':' not in l:
bad_cont.append(l)
ck('每行都是「字段: 值」或续行', not bad_cont, str(bad_cont[:2]))
# 依赖策略的理由写在 build-deb.sh 里(control 不能写注释)
ck('依赖策略说明在 build-deb.sh 里',
'smartmontools' in read('packaging/build-deb.sh'))
print('\n二、维护脚本(postinst / prerm / postrm)')
for m in ('postinst', 'prerm', 'postrm'):
rel = 'packaging/deb/' + m
src = read(rel)
ck('%s 存在' % m, bool(src))
ck('%s 无 CRLF' % m, no_crlf(rel))
ck('%s 有 shebang' % m, src.startswith('#!/bin/bash'))
ck('%s 处理了所有 dpkg 动作' % m, 'case "$1" in' in src and '*)' in src)
ck('%s 以 exit 0 收尾' % m, 'exit 0' in src)
r = bash_ok(rel)
if r is not None:
ck('%s bash 语法' % m, r)
# postinst 的安全红线:绝不能 enable/start DHCP、DNS、UPnP
pi = read('packaging/deb/postinst')
for svc in ('dnsmasq', 'radvd', 'miniupnpd'):
ck('postinst 不启动 %s' % svc,
('systemctl enable %s' % svc) not in pi and ('systemctl start %s' % svc) not in pi)
ck('postinst 用构建保护模式', 'BUILD_MODE' in pi)
ck('postinst 会装 drouter-ctl', 'drouter-ctl' in pi)
ck('postinst 会跑 deploy.sh', 'deploy.sh' in pi)
ck('postinst 用 DROUTER_SRC 指向 /opt/drouter', 'DROUTER_SRC=/opt/drouter' in pi)
prm = read('packaging/deb/postrm')
ck('postrm purge 清 /etc/drouter', 'rm -rf /etc/drouter' in prm)
ck('postrm purge 清 /var/lib/drouter(累计流量)', '/var/lib/drouter' in prm)
ck('postrm remove 保留配置', 'remove)' in prm)
print('\n三、Dockerfile')
df = read('packaging/docker/Dockerfile')
ck('无 CRLF', no_crlf('packaging/docker/Dockerfile'))
INSTR = ('FROM', 'LABEL', 'ENV', 'RUN', 'COPY', 'EXPOSE', 'VOLUME',
'HEALTHCHECK', 'ENTRYPOINT', 'CMD', 'WORKDIR', 'ARG', 'USER', 'SHELL')
lines = df.split('\n')
bad = []
for i, l in enumerate(lines):
s = l.strip()
if not s or s.startswith('#'):
continue
if s.split()[0].startswith(INSTR):
continue
# 上一行以 \ 结尾则是续行,合法
if i > 0 and lines[i - 1].rstrip().endswith('\\'):
continue
bad.append((i + 1, s))
ck('所有指令合法', not bad, str(bad[:2]))
ck('有 FROM', df.startswith('FROM ') or '\nFROM ' in df)
ck('有 ENTRYPOINT', 'ENTRYPOINT' in df)
ck('有 CMD', 'CMD' in df)
ck('有 HEALTHCHECK', 'HEALTHCHECK' in df)
# COPY 的源必须真实存在,否则 docker build 才失败就太晚了
missing = []
for l in lines:
if l.strip().startswith('COPY'):
parts = l.split()
if len(parts) >= 2:
src = parts[1]
if '*' not in src and not os.path.exists(os.path.join(ROOT, src.rstrip('/'))):
missing.append(src)
ck('COPY 源路径都存在', not missing, str(missing))
# HEALTHCHECK / 端口不应写死,否则用户改端口后永远 unhealthy
hc = [l for l in lines if 'HEALTHCHECK' in l or 'curl -sk' in l]
ck('健康检查读取实际端口(不写死 8443)',
any('web-port' in x for x in hc), ''.join(hc)[:80])
ini = read('packaging/docker/docker-init.sh')
ck('docker-init 无 CRLF', no_crlf('packaging/docker/docker-init.sh'))
ck('docker-init 以 exec 结尾(信号可传递)', 'exec "$@"' in ini)
ck('docker-init 播种端口后 unset(避免压住界面改端口)',
'unset DROUTER_WEB_PORT' in ini)
r = bash_ok('packaging/docker/docker-init.sh')
if r is not None:
ck('docker-init bash 语法', r)
print('\n四、构建脚本')
for rel in ('packaging/build-deb.sh', 'packaging/build-docker.sh',
'scripts/drouter-ctl.sh', 'scripts/install-base.sh'):
ck('%s 存在' % rel, os.path.exists(os.path.join(ROOT, rel)))
ck('%s 无 CRLF' % rel, no_crlf(rel))
r = bash_ok(rel)
if r is not None:
ck('%s bash 语法' % rel, r)
bd = read('packaging/build-deb.sh')
ck('build-deb 先校验后端再打包', 'ast.parse' in bd)
ck('build-deb 生成 md5sums', 'md5sums' in bd)
# 逐个点名,别再用「数 .py 出现次数」这种会被注释里的文件名骗过的写法。
# drouter-shelld.py 是 Web 终端的 PTY 守护,漏了它终端就打不开。
BACKEND_MODULES = ['render.py', 'drouter-helper.py', 'drouter-web.py',
'drouter-logd.py', 'drouter-snapshotd.py',
'drouter-rescue.py', 'drouter-shelld.py',
'drouter-helpd.py', 'theme.py']
miss_mod = [m for m in BACKEND_MODULES if m not in bd]
ck('build-deb 逐个校验 %d 个后端模块' % len(BACKEND_MODULES), not miss_mod,
'→ 缺 %s' % miss_mod if miss_mod else '')
print('\n结果:失败 %d 项' % len(fails))
for f in fails:
print(' ✘ ' + f)
sys.exit(1 if fails else 0)