# 架构设计 — luci-app-substore OpenWrt / ImmortalWrt 原生 LuCI 机场订阅管理工具。参考 Sub-Store 功能体验,独立设计与实现,不复制上游代码。 ## 1. 目标与范围 原生 OpenWrt 软件包,不使用 Docker,不依赖外部云端服务。在低性能路由器(内存 ≤ 512MB,存储紧张)上运行。 功能范围: - 订阅管理:多订阅源增删改查、手动/定时更新、更新时间/节点数/状态展示 - 节点管理:解析常见代理协议、查看/搜索/筛选/排序/重命名/去重 - 订阅处理:按条件筛选、去重、重命名、排序、多订阅合并、生成客户端订阅 - 输出格式:15 种全量实现(Plain JSON / Stash / Clash.Meta / Clash 原版 / Surfboard / Surge / SurgeMac / Loon / Egern / Shadowrocket / QX / sing-box / V2Ray / V2Ray URI / WireGuard `.conf`) - 订阅链接:转换结果以订阅链接形式下发,供 Passwall / OpenClash 等拉取 - LuCI 界面:订阅列表、编辑更新、节点查看、规则配置、输出复制、状态日志 ## 2. 技术选型 | 层 | 选型 | 理由 | |----|------|------| | 后端核心 | **Lua 5.1 纯脚本库** | 用户选定;不依赖 luci.*,可独立测试;跨 LuCI 版本稳定 | | LuCI 前端 | **Lua 兼容模式 (luci-lua-runtime)** | 用户选定;在 23.05+/24.10+/25.12 以 Lua 兼容层运行 | | 数据存储 | 独立数据文件 `/etc/substore/*.json` | CLAUDE.md 要求订阅/状态用独立文件,不用 UCI 存节点数据 | | 配置 | UCI `/etc/config/substore` | 仅存订阅源元数据、处理规则、输出设置 | | 后台服务 | procd(仅当需要常驻/定时时) | 定时更新可走 LuCI 页面触发 + cron,避免常驻 | | 输出接口 | LuCI 路由 / CGI,带随机 Token 鉴权 | CLAUDE.md 安全要求 | ## 3. 模块划分 ``` luci-app-substore/ ├── Makefile # 软件包构建 ├── root/ │ ├── etc/config/substore # UCI 配置 │ ├── usr/lib/lua/luci/ # LuCI 前端(controller / model / view) │ └── usr/share/substore/ # 纯 Lua 核心库 + 数据目录 ├── src/ # 可选:需编译的辅助(暂无) └── docs/ # 文档 ``` 核心库(`/usr/share/substore/`,纯 Lua,不依赖 luci.*): - `core.lua` — 订阅源元数据读写、状态管理、订阅链接生成(token + generate_link) - `parser.lua` — 订阅格式解析(Base64/URI/Clash YAML/JSON) - `node.lua` — 节点模型、筛选、去重、重命名、排序 - `output.lua` — 统一格式分发(FORMAT_ALIASES 别名映射、content-type/扩展名) - `output_clash_meta.lua` — Clash.Meta / Mihomo / Stash YAML - `output_uri.lua` — 分享链接 URI / Shadowrocket / V2Ray URI - `output_singbox.lua` — sing-box JSON - `output_v2ray.lua` — V2Ray/Xray JSON - `output_formats.lua` — Surge 系 / Loon / QX / Egern / Plain JSON - `http.lua` — 订阅下载(SSRF 防护、超时、大小限制) - `util.lua` — 通用工具(原子写、时间、日志、Base64、JSON) LuCI 前端(`/usr/lib/lua/luci/`): - `controller/admin/substore.lua` — 路由注册 - `model/cbi/substore/*.lua` — 配置界面 - `view/substore/*.htm` — 页面模板 ## 4. 数据流 订阅 URL / 本地文本 → `http.lua` 下载(SSRF 防护)→ `parser.lua` 解析 → `node.lua` 统一节点模型 → 去重/筛选/重命名/排序 → `output.lua` 按目标格式生成 → LuCI 界面 / 订阅下载端点 **订阅链接下发流程**:每个订阅在创建时生成随机 16 位十六进制 `token`,存入元数据。客户端请求公开端点 `GET /substore/download?token=&target=`(如 `target=ClashMeta`),无需登录态、靠不可猜测的 token 鉴权, 按 `target` 实时转换节点并返回(由 `output.*` 按目标格式生成)。 > **历史说明**:早期设计里 `node_converter.lua` / `converter.lua` 负责「任意协议 → > 任意协议」的转换。实际实现中该转换入口从未接入(`output.*` 各模块直接消费统一 > 节点模型),两个模块只被彼此与单元测试引用,已于 `[2.6.11-r1]` 作为死代码删除。 节点统一内部模型(协议无关): ```lua { proto = "vmess"|"vless"|"trojan"|"shadowsocks"|"hysteria2"|"tuic"|"ss"|..., name = "节点名", server = "host", port = 443, uuid = "uuid", password = "...", cipher = "...", -- 协议特有字段 raw = "原始URI串" -- 保留原始,便于无损转发 } ``` ## 5. 安全设计(CLAUDE.md 第 6 节) - **SSRF 防护**:`http.lua` 下载前解析主机名,拒绝 localhost/内网/链路本地/保留地址;DNS 解析后二次校验;处理重定向 - **资源限制**:订阅响应体最大大小(默认 10MB 可配)、连接/总超时、并发限制 - **路径穿越**:订阅名/文件名做白名单校验,禁止 `..`、`/` - **命令注入**:不将用户输入拼入 shell;需要时用参数化执行 - **凭据保护**:订阅 URL 中的 token/密码不写入普通日志;输出接口用随机 Token 鉴权 - **导入校验**:解析时对结构/大小/嵌套深度做限制,防恶意 YAML/JSON 资源耗尽 - 所有安全策略结合 OpenWrt 实际网络环境实现并编写测试 ## 6. 兼容性约束 - 目标 LuCI 使用 Lua 兼容模式,需依赖 `luci-lua-runtime` + `luci-compat`(23.05+) - Lua 控制器沿用 `module()` + `entry()` 风格(24.10 有效,活跃插件仍在使用) - **25.12 风险**:LuCI 模板优先级已转向 `.ut`(ucode),`.htm` 为 Legacy。阶段4 已评估迁移方案,详见 `docs/UCODE_MIGRATION.md`(含 `.htm`→`.ut` 语法对照与逐模板映射;核心库纯 Lua 可通过 `include()` 复用,迁移成本集中在 5 个模板的胶水代码) - 不假设所有 OpenWrt 版本依赖一致,分版本适配 - 纯 Lua 核心库优先用 Lua 5.1 标准库(luci 自带 lua5.1),避免 luarocks 依赖 - 如需要 YAML 解析:优先评估纯 Lua 实现或 luci 内置,避免重运行时 - `/lib/functions/luci.sh` 已移除,postinst 用 `rm -f /tmp/luci-indexcache` + `/etc/init.d/luci reload` 刷新缓存 ## 7. 文档清单 README.md、docs/ARCHITECTURE.md、docs/BUILD.md、docs/INSTALL.md、docs/SECURITY.md、docs/TESTING.md、CHANGELOG.md