mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-04 00:53:56 +08:00
1596 lines
99 KiB
Markdown
1596 lines
99 KiB
Markdown
# 遗留缺陷与已知限制汇总
|
||
|
||
**决策状态**:本文件所列各项**均已决策并处置完毕**(`[2.6.16-r1]` 为最后一批,
|
||
见下文「六」)。仍留在这里是因为它们记录了「为什么是这个处置」—— 其中一部分是
|
||
**有意保留的已知限制**,不是待办;另有几项写明「待上游规范可核实后再定」。
|
||
本文件只记录**不在 P2 范围内**、需要另行决策的项。
|
||
|
||
**当前进度**:
|
||
|
||
| 批次 | 范围 | 状态 |
|
||
|---|---|---|
|
||
| P0 | 14 项高危(输出整份不可用 / 数据永久丢失 / 安全) | ✅ 已完成(4 次推送,见 CHANGELOG `[2.6.0-r2]`~`[2.6.0-r5]`) |
|
||
| P1 | 13 项中危 + 审计中实测确认的 4 项新缺陷 | ✅ 已完成(`[2.6.1-r1]`) |
|
||
| P2 | 其余 M / L 项 | ✅ 已完成(批次一 `[2.6.3-r1]`、批次二 `[2.6.4-r1]`、批次三 `[2.6.5-r1]`、批次四 `[2.6.6-r1]`、批次五 `[2.6.7-r1]`、批次六 `[2.6.8-r1]`、批次七 `[2.6.9-r1]`「P2 最后一批」)。范围定义里有 **M6 / L15 / L19** 三项无据可查,见文末「附」 |
|
||
| P3 | 输出层代码级复审(10 项)+ shadowsocks SIP003 插件全链路(1 项) | ✅ 已完成(`[2.6.10-r1]`,见下文「四」) |
|
||
| P4 | 遗留项决策后实施(1.1 / 1.3 / 1.4 / 1.5 / 2.5 五项,3.3 补文档) | ✅ 已完成(`[2.6.11-r1]`,见下文「五」) |
|
||
| P5 | 决策项实施(wget 路径 SSRF、删除部分失败漏写 cron、混合格式静默丢弃、写入失败被忽略、M28 token、M29 ACL;wget 体积/重定向记文档) | ✅ 已完成(`[2.6.16-r1]`,见下文「六」) |
|
||
|
||
**编写纪律**:只记录经**代码级审计或实测探针**确认的结论,不含推测。原始 70 项
|
||
审计表(高危 14 / 中危 30 / 低危 26)未落盘,本文件自本次起作为「未修复项」的
|
||
权威清单。已修复项见 [CHANGELOG.md](../CHANGELOG.md)。
|
||
|
||
---
|
||
|
||
# 一、本轮(P1 之后)确认的遗留项
|
||
|
||
## 1.1 H10 的「无锁」部分 —— `load()` → `save()` 丢更新(**已修复 `[2.6.11-r1]`**)
|
||
|
||
**已修的部分**:H10 的另一半(临时文件名固定为 `path..".tmp"` 被并发写者共用)
|
||
已在 P0 批次三修复,改为 `path..".tmp.<8 字节随机 hex>"`。
|
||
|
||
**未修的部分**:读-改-写全程仍无互斥。
|
||
|
||
**代码级依据**:
|
||
|
||
- `core.lua` 的 `local function load()` 读出整表,`local function save(seq, items)`
|
||
整表写回,中间无锁;`M.add` / `M.add_local` / `M.add_combo` / `M.save_meta` /
|
||
`M.save_combo` 均走这条路径。
|
||
- `util.lua` 中**不存在任何锁原语**(无 `flock`、无 `mkdir` 锁、无 `link()`)。
|
||
- `nixio` 只用于 `http.lua:208-210` 的 `getaddrinfo`(`util.try_require` 可选加载),
|
||
`Makefile` 的 `LUCI_DEPENDS` 未声明 `+nixio`。
|
||
|
||
**并发来源**:LuCI 页面保存订阅、cron 定时更新、组合订阅自动重算,三者可能同时发生。
|
||
|
||
**影响**:后写者整表覆盖先写者 → 订阅丢失或新增丢失。与已修复的 H8 同属「数据被
|
||
抹除」,但触发条件是并发而非文件损坏,H8 的修复**不覆盖**本项。
|
||
|
||
**候选方案**:
|
||
|
||
| 方案 | 做法 | 代价 / 风险 |
|
||
|---|---|---|
|
||
| A. 记为已知限制 | 文档说明「同一时间只应有一个写操作」 | 零成本;用户无从感知,仍会丢数据 |
|
||
| B. 引入 `nixio` flock | `Makefile` 加 `+nixio`,写路径加文件锁 | 最可靠;新增运行时依赖,本机**无法验证** |
|
||
| C. `mkdir` 锁 + 陈旧锁回收 | 目录创建做互斥,带超时与持有者 PID 回收 | 纯 Lua 无依赖;需正确处理崩溃残留锁,否则死锁 |
|
||
| D. 写路径单点串行化 | 所有写入收敛到同一入口 | 跨进程串行仍需 OS 原语,实际仍要 B 或 C |
|
||
|
||
## 1.2 M28 / M29 —— CSRF `post_ok()` 与 ACL
|
||
|
||
**状态**:**已修复(`[2.6.16-r1]`)**。原「跳过验证」的处置已作废 —— 两项依赖的
|
||
LuCI 框架行为本轮**已从上游源码逐条核实**(依据见下),不再是猜测,故按核实结果实施。
|
||
|
||
**M28(`post_ok()` 空 token 放行)—— 已修复**
|
||
|
||
原缺陷:`substore.lua` 的 `post_ok()` 只判 `formvalue("token") ~= nil`,**空串也通过**;
|
||
且所有 `entry` 均未声明 `post`,框架的 `test_post_security` 从未执行。
|
||
|
||
**框架行为核实**(上游 `luci/luci-base`,非推测):
|
||
|
||
- `luci/dispatcher.lua` 的 `test_post_security()` 只在 `entry.post` 为真时才被调用;
|
||
本控制器未声明 `post`,因此该函数**确实从未执行** —— 原判断成立。
|
||
- `luci/template.lua` 中 viewns 元表的 `token` 键返回 `disp.context.authtoken`,
|
||
即模板里的 `token` **就是** `luci.dispatcher.context.authtoken`。因此「把提交的 token
|
||
与 `context.authtoken` 比对」正是框架自身的判据,**不会误拒任何合法表单**。
|
||
|
||
**修复内容**:`post_ok()` 现在要求 token **存在且非空**,并在能取到 `authtoken` 时
|
||
要求两者**相等**;取不到 `authtoken`(非标准运行环境)时退回「非空即通过」,
|
||
不因取不到值而拒绝全部请求。失败时通过 `post_fail_msg` 把原因回显到列表页,
|
||
不再静默失败。同时补齐了原先 9 处**忽略返回值**的调用点(7 处 `back_to_list()`、
|
||
2 处 `back_to_nodes()`),使校验失败**必然**被用户看到。
|
||
|
||
**回归测试**:`tests/controller_robustness_test.lua` 的 M28 段(6 条断言),
|
||
覆盖「合法 token 通过 / 空 token 拒绝 / 不匹配拒绝 / create / update / node_save
|
||
四类写操作均受检 / 取不到 authtoken 时的回退」。反向验证:修复前 7 条断言失败。
|
||
|
||
**M29(无 ACL)—— 已修复**
|
||
|
||
原缺陷:`menu.d` 无 `depends.acl`、仓库内无 rpcd ACL 文件 → 任意已登录 LuCI 用户
|
||
可读写全部订阅(含凭据 URL)。
|
||
|
||
**ACL 结构核实**(上游 `luci-base` 与 `luci-app-commands`,非推测):acl.d 的顶层是
|
||
`{ "<组名>": { "description": …, "read": { "uci": [...] }, "write": { "uci": [...] } } }`;
|
||
menu.d 用 `"depends": { "acl": [ "<组名>" ] }` 引用。
|
||
|
||
**修复内容**:新增 `root/usr/share/rpcd/acl.d/luci-app-substore.json` 定义
|
||
`luci-app-substore` 组(读写 `uci: substore`),`menu.d` 两个条目均声明
|
||
`depends.acl` 引用它,`Makefile` 增加对应的安装规则。
|
||
|
||
**执行范围(已从上游源码核实,非推测)**:
|
||
|
||
`menu.d` 的 `depends.acl` 是**入口级**门禁,不只是「菜单里看不见」。核实过程:
|
||
|
||
- **ucode dispatcher(23.05+,现代目标机上实际运行的那套)**:
|
||
`modules/luci-base/ucode/dispatcher.uc` 的 `build_pagetree()` 把
|
||
`/usr/share/luci/menu.d/*.json` **与** Lua 控制器
|
||
(`/usr/lib/lua/luci/controller/*/*.lua`)glob 进**同一棵树**;
|
||
`dispatch()` 逐段走这棵树并调用 `ctx_append(ctx, name, node)`,而
|
||
`ctx_append` 里是 `push(ctx.acls, ...(node?.depends?.acl || []))` ——
|
||
**路径上每个节点**的 `depends.acl` 都会被累积。随后:
|
||
```js
|
||
if (length(resolved.ctx.acls)) {
|
||
let perm = check_acl_depends(resolved.ctx.acls, resolved.ctx.authacl?.['access-group']);
|
||
if (perm == null) { http.status(403, 'Forbidden'); return; }
|
||
}
|
||
```
|
||
即:缺少该组的用户**直接访问 URL 会拿到 403**。
|
||
- **ACL 沿路径累积**这一点还带来一个有用的推论:把 ACL 挂在父节点
|
||
`admin/services/substore` 上,就已覆盖它的**全部子路由**
|
||
(`form` / `nodes` / `delete` / `save` / `node_save` / `update` / `probe` …)。
|
||
本应用的所有动作都在该节点之下,因此这 18 个 `entry` 一并受保护。
|
||
- **旧版 Lua dispatcher(≤ 22.03)行为不同,但不在支持范围内**:`luasrc/dispatcher.lua`
|
||
里 `menu_json()` 把控制器树与 menu.d 树 merge 后 `apply_tree_acls`,那只影响
|
||
**菜单渲染**;`dispatch()` 用的是控制器树,其 `depends.acl` 只来自
|
||
`entry.acl_depends`。也就是说在 22.03 上仅靠 menu.d 只有「菜单看不见」。
|
||
**本项目最低支持 23.05**(用户 2026-10-02 确认),23.05 起用的是 ucode dispatcher,
|
||
即上面那套入口级 403 语义。因此**无需**为旧版补 `entry.acl_depends` —— 记录在此
|
||
只为说明「为什么 22.03 不在支持范围内时这个缺口可以不管」,不是待办。
|
||
|
||
**设备实测(已完成并通过,2026-10-02)**:以上结论最初来自**读上游源码**(本机无
|
||
LuCI 运行环境),随后按 `docs/TESTING.md` 第 9 项在目标设备复核,**三项断言全部通过**:
|
||
|
||
- 非 root 且不在 `luci-app-substore` 组内的 LuCI 用户:**看不到**本应用菜单入口
|
||
- 把该用户加入组后:入口**出现**
|
||
- 该用户**直接访问** `/cgi-bin/luci/admin/services/substore/list`:返回 **403 Forbidden**
|
||
|
||
即:入口级 403 的语义在真实设备上成立,与上文读源码得出的推断一致。
|
||
|
||
**回滚方式**:删除 `root/usr/share/rpcd/acl.d/luci-app-substore.json`(及 Makefile 中
|
||
对应的两行安装规则),并移除 `menu.d` 两个条目里的 `depends` 块。
|
||
|
||
**回归测试**:`tests/acl_menu_test.lua`(19 条断言)固化跨文件接线 —— ACL 组定义、
|
||
菜单引用**同一个组名**、菜单路径在控制器中确实注册、Makefile 确实安装这两个文件。
|
||
这条链路上任何一环写错都**不会报错**,只会静默失效(ACL 形同虚设),故必须由测试
|
||
锁住。反向验证:修复前 13 条断言失败。
|
||
|
||
## 1.3 sing-box 读取侧:transport 整层丢失(**已修复 `[2.6.11-r1]`**)
|
||
|
||
**现象**:sing-box 配置中 ws / grpc / h2 的传输参数在导入后**完全丢失**,节点退化
|
||
为 tcp 直连。
|
||
|
||
**实测探针**(`parser.parse`,vmess + `transport: {type: ws, path: /ws, headers.Host}`):
|
||
|
||
```
|
||
JSON -> proto=vmess net=tcp sni=a.com fp=nil path=nil host=nil
|
||
YAML -> proto=vmess net=tcp sni=a.com fp=chrome path=nil host=nil
|
||
```
|
||
|
||
**代码级依据**:
|
||
|
||
- `parser_json_config.lua`(`parse_singbox_json`,314 行)全文**不含**
|
||
`transport` / `path` / `host` / `utls` 任何一处。它读的是 `outbound.network`,
|
||
而 sing-box 表达传输方式用的是 `transport.type` —— 该字段在 sing-box 出站里
|
||
并不存在,因此 `net` 恒为默认值 `tcp`。
|
||
- 简易 YAML 解析器(`parser.lua`)只展开 `tls:` 子块,同样不处理 `transport`。
|
||
|
||
**影响**:ws / grpc / h2 节点在机场导出的 sing-box 配置里占相当比例,导入后全部按
|
||
tcp 直连,握手必然失败,且**不报错**。比原记录(「不读 path/host」)更严重:连传输
|
||
类型本身都没读进来。
|
||
|
||
**附带不一致**:`tls.utls.fingerprint` 在 YAML 侧**已读**(`fp=chrome`,P1 修复),
|
||
JSON 侧**不读**(`fp=nil`)。
|
||
|
||
**候选方案**:在 `parse_singbox_json` 中补 `transport.type` → `net`、
|
||
`transport.path` → `path`、`transport.headers.Host` → `host`、
|
||
`tls.utls.fingerprint` → `fp`;并让简易 YAML 侧同样处理 `transport`。
|
||
|
||
**权威字段名已核实(`[2.6.10-r1]`,取自上游
|
||
`sing-box.sagernet.org/configuration/shared/v2ray-transport/`,可直接照此实现,
|
||
无需再猜)**:
|
||
|
||
| `transport.type` | 本项目的 `net` | 字段映射 |
|
||
|---|---|---|
|
||
| `ws` | `ws` | `path` → `path`;`headers.Host` → `host` |
|
||
| `grpc` | `grpc` | `service_name` → `path` |
|
||
| `http` | `http` | `path` → `path`;`host` 是**数组**,取首个 → `host` |
|
||
| `httpupgrade` | `http` | `path` → `path`;`host` 是**单个字符串** → `host` |
|
||
| `quic` | 无对应 | 本项目模型没有 quic,按未知传输处理(回落 tcp 或丢弃) |
|
||
|
||
另:`tls.utls.fingerprint` → `fp`(YAML 侧已读,JSON 侧未读,需对齐);
|
||
`tls.enabled` 为 `false` 时不得置 `security`。
|
||
|
||
**修复建议:本轮修复**(`[2.6.10-r1]` 未实施,留待下一轮)。它与 F5 / F6 / F11
|
||
同属「静默丢传输层 → 节点连不上且不报错」,而 ws / grpc 节点在机场的 sing-box
|
||
配置里占比很高,影响面比 F5/F6 更大。实施时两侧(JSON 与简易 YAML)必须一起改,
|
||
否则同一条订阅走两条导入路径会得到不同的节点。
|
||
|
||
## 1.4 `converter.lua` / `node_converter.lua` 为死代码(**已删除 `[2.6.11-r1]`**)
|
||
|
||
(即审计表 L2)
|
||
|
||
**代码级依据**:
|
||
|
||
- `root/` 下**没有任何文件** `require` 这两个模块 —— 唯一的引用是
|
||
`converter.lua:7` 引用 `node_converter.lua`,以及 `tests/` 下的单元测试。
|
||
- LuCI controller 与前端 JS 中**不存在**「转换」相关的路由或 UI。
|
||
- 模块实现本身也有问题:禁止 shadowsocks 转换;`hysteria2/tuic/wireguard →
|
||
"sing-box"/"clash"` 产出**非法 proto**;`node_converter.convert` 因 `network` /
|
||
`fingerprint` 字段名不匹配丢 `net`/`path`/`host`/`fp`;`core.generate_link` 不传
|
||
opts → `options.proto` 永不被设置。
|
||
|
||
**README 声明**:`README.md:59`「协议转换:任意协议 → 任意协议」、
|
||
`README.en.md:68`「Protocol conversion: any node type → any other type」;
|
||
紧接其后的 `README.md:60-62` 又说明「SSR 不能与 vmess/vless 等其它协议互转」,
|
||
与上一行自相矛盾。
|
||
|
||
**候选方案**:A. 接入 UI(需先定义「哪些协议可互转」的权威矩阵并审计现有映射表);
|
||
B. 删除两个模块与测试,修正 README 两处声明。
|
||
|
||
---
|
||
|
||
## 1.5 Quantumult X:节点名含逗号时 `tag=` 字段本身被写坏(**已按方案 B 修复 `[2.6.11-r1]`**)
|
||
|
||
**发现于**:P2 批次三修复 L21 时顺带发现。L21 只处理了**成员列表**(Surge 家族
|
||
`[Proxy Group]` 与 QX `[policy]`),已修复;本条是同一根因在 QX **定义行**上的
|
||
另一处表现。
|
||
|
||
**实测(已验证的部分)**:`output_formats.lua` 的 `to_qx` 用
|
||
`string.format("trojan=%s, password=%s, over-tls=true, tag=%s", …)` 拼行,
|
||
`tag` 直接取自 `n.name`。节点名 `A,B` 经 `util.one_line` 后仍含逗号,
|
||
输出为:
|
||
|
||
```
|
||
trojan=1.2.3.4:443, password=p, over-tls=true, tag=A,B
|
||
```
|
||
|
||
`[server_local]` 行内是逗号分隔的 `key=value` 字段序列,名字里的逗号落在
|
||
`tag=` 的值里,按该语法会被读成字段分隔符。
|
||
|
||
**补充(`[2.6.10-r1]`)**:同一条行语法在**参数值**上的同类问题已修复 —— `to_qx`
|
||
现在先把具名字段攒成列表、逐字段判定值里是否含逗号,含逗号的整条丢弃(连同
|
||
`[policy]` 成员),不再出现 `password=pa,ss` 被静默截断成 `password=pa`。
|
||
本条讨论的 `tag=` **名字**部分已于 `[2.6.11-r1]` 按下面的方案 B 一并处理:
|
||
`to_qx` 现在对「one_line 之后仍含逗号」的节点整条丢弃(定义行与 `[policy]` 成员
|
||
一起),判定条件与 `names_of` 完全一致,两边不会再对不上。
|
||
|
||
**未验证的部分(不猜)**:QX 对这种「最后一个字段值里多出逗号」的行究竟是
|
||
报错、忽略多余片段,还是原样接受,**本机没有 Quantumult X 可供实测**。
|
||
因此本条按「待核实」记录,未作修改。
|
||
|
||
**候选方案**(三选一,需先核实 QX 实际行为再定):
|
||
|
||
- **A. 改名**:输出时把名字里的 `,` 替换为安全字符。节点不丢失,但导出名与
|
||
LuCI 中显示的名字不一致;且 `A,B` 与 `A_B` 两个节点会撞名(Surge / QX 对
|
||
重名代理的处理需另行核实)。
|
||
- **B. 丢弃**:把含逗号名字的节点从 QX 输出中整体剔除(连定义行一起)。
|
||
与「Surge 家族丢弃 wireguard / ssr、Clash 原版丢弃不支持的协议」的既有约定
|
||
一致,产出必定合法;代价是节点静默消失。
|
||
- **C. 维持现状**:仅在文档中说明「QX 输出请勿使用含逗号的节点名」。
|
||
|
||
**注**:Surge 家族的 `[Proxy]` 定义行是 `NAME = type, host, port, …`,
|
||
名字在 `=` **左侧**,逗号大概率不影响解析(未实测),故本条**只针对 QX**。
|
||
|
||
**推荐方案:B(丢弃)**。理由:
|
||
|
||
- A(改名)会引入新的重名风险,而「重名代理」在 Surge / QX 上的行为同样未核实 ——
|
||
等于用一个未核实的问题换掉另一个,不划算。
|
||
- C(维持现状)会让用户拿到一份静默损坏的配置,与本项目「宁可丢节点也不输出
|
||
损坏行」的既有约定(Surge 家族丢 wireguard / ssr、Clash 原版丢不支持的协议)相悖。
|
||
- B 与 F7 已落地的处置完全一致,实现上只是把 `names_of` 的排除条件也用到定义行上,
|
||
改动小、行为可预期。
|
||
|
||
实施前仍建议先核实 QX 对多余逗号片段是报错还是忽略 —— 若确认是「忽略多余片段、
|
||
`tag` 取第一段」,则 C 也可接受,届时再定。
|
||
|
||
---
|
||
|
||
# 二、P0 时代的遗留项(2.6.0 审计时记录)
|
||
|
||
来源:CHANGELOG `[2.6.0-r1]` 的「已知限制(本轮不修,均有明确原因)」。以下逐条
|
||
复核过当前代码,均**仍然成立**。
|
||
|
||
## 2.1 M11 混合格式文本导入不支持(**已按方案 B+ 修复 `[2.6.16-r1]`**)
|
||
|
||
**依据**:`parse_local → parse → detect` 只识别**一种**格式,其余部分被静默丢弃
|
||
(实测:「URI + WG conf」只剩 WG 节点;「URI + JSON」只剩 URI 节点)。
|
||
|
||
**性质**:**功能缺失**而非小缺陷,需要重新设计 parser 的分段架构(规格 §22 明确
|
||
警告不要直接采用未经验证的逐行算法)。
|
||
|
||
**候选方案**:A. 设计分段架构后实现(工作量大,属 minor 版本特性);
|
||
B. 在 README / UI 明确「一次只支持一种格式」(README 已如实说明,但 UI 未提示)。
|
||
|
||
**已实施方案 B+(拒绝而非静默丢弃)**:新增 `M.detect_all(content)` 收集文本中
|
||
**全部**出现的格式,本地文本导入(`parse_local` 文本模式)在多于一种时**明确报错**
|
||
并列出两种格式名,而不是交给 `detect()` 挑一种、悄悄丢掉其余。这比 B 更进一步:
|
||
B 只是提示,用户仍会拿到一份少了一半节点的配置;现在是**导入直接失败**,不存在
|
||
「以为成功」的中间态。
|
||
|
||
**实施中修正的两处误判**(若不修,会把合法配置拒之门外,属新 bug):
|
||
|
||
- Surge / Clash 的 INI 段头 `[Proxy]` 等会被朴素的 `^%s*%[` 规则误判为 JSON 数组,
|
||
于是一份合法 Surge 配置被判成 `surge + json` 而拒绝。已加 `is_ini_section_head()`
|
||
排除段头(段头内是标识符,不是引号字符串)。
|
||
- 「URI + JSON」这个方向起初漏检(原判据只看首个非空字符)。已补
|
||
`has_json_object_line()`(`^%s*{%s*$` 或 `^%s*{%s*"`)做行级判定,两个方向都能识别。
|
||
|
||
**刻意限定范围(如实记录)**:守卫**只作用于本地文本导入**。远程订阅走
|
||
`M.detect` / `M.parse`,**未改动** —— 改动它会让既有远程订阅的解析行为变化,
|
||
风险大于收益。因此**远程订阅内容若混用格式,仍然是静默丢弃**,与修复前一致。
|
||
|
||
**回归测试**:`tests/parser_mixed_format_test.lua`(70 条断言)—— A 组确认单一格式
|
||
(含带 `proxy-provider` 的 Clash YAML、带 `url` 规则的 sing-box JSON、YAML 流式映射、
|
||
`[Interface]` / `[Proxy]`)**不会被误判**;B 组确认 7 种混用组合都明确报错且错误信息
|
||
含两种格式名;C 组确认远程路径 `M.parse` 行为未变。反向验证:修复前探针显示
|
||
「URI + WG conf」得到 `nodes=1, err=nil`(静默丢弃)。
|
||
|
||
## 2.2 sing-box 的 hysteria(v1) 出站还要求 `up` / `down`(带宽)
|
||
|
||
**状态:确认为已知限制(`[2.6.16-r1]` 决策,方案 A)** —— 不改代码,写入用户文档
|
||
(README「已知限制」)。
|
||
|
||
**依据**:`output_singbox.lua:127` 有明确注释说明;本项目的节点模型不承载该字段,
|
||
凭空填默认值属于猜测,故不输出。
|
||
|
||
**影响**:导出到 sing-box 的 hysteria v1 节点需用户自行补 `up`/`down`。
|
||
|
||
**候选方案**:A. 保持现状(文档说明);B. 在节点模型中增加 `up`/`down` 字段并贯通
|
||
解析 / 表单 / 输出(属新功能)。
|
||
|
||
**决策理由**:选 A。B 需要先定字段名与单位(bps 还是 Mbps、是否成对、缺省值取多少),
|
||
这些都是**未经核实的规格问题**,按「不猜」纪律不能凭推测落码;且它属于新功能,
|
||
按版本约定应进 minor 版本,不适合混在本轮的缺陷修复里。
|
||
|
||
## 2.3 hysteria(v1) 的上游 URI 规范未能核实
|
||
|
||
**状态:确认为已知限制(`[2.6.16-r1]` 决策,方案 A)** —— 不改代码,写入用户文档
|
||
(README「已知限制」)。
|
||
|
||
**依据**:上游文档站点持续 404,无法取得权威定义。`parse_hysteria` 只保证解析本
|
||
项目 `output_uri` 自身生成的链接形态(回环已验证),认不出的查询参数一律忽略而不
|
||
报错,不臆造参数语义。
|
||
|
||
**候选方案**:待上游文档可访问后核实并补全;或维持「回环保证」的现状。
|
||
|
||
**决策理由**:选「维持现状」。规范拿不到就是拿不到 —— 此时任何「补全」都是编造
|
||
参数语义,正是「不猜」纪律要禁止的。已实测的回环保证(本包导出 → 本包导入不失真)
|
||
是本项目唯一能负责的边界,如实写进文档即可。
|
||
|
||
## 2.4 `http` 不加入任何 UI 协议列表
|
||
|
||
**状态:确认为已知限制(`[2.6.16-r1]` 决策,方案 A)** —— 不改代码,写入用户文档
|
||
(README「已知限制」)。
|
||
|
||
**依据**:规格 §25 的权威协议表恰好是 10 个协议、不含 `http`。`node.lua` 的
|
||
`M.PROTOS` 实测为 10 项(vmess / vless / trojan / shadowsocks / ssr / hysteria2 /
|
||
tuic / hysteria / wireguard / socks),确实不含 `http`。它可由 Clash YAML / JSON
|
||
配置导入并正常导出(凭据已修),但不作为表单可选项。
|
||
|
||
**候选方案**:A. 维持现状(`http` 属「可导入可导出但不可手录」);B. 加入 UI 列表
|
||
(需先确认规格 §25 是否有意排除)。
|
||
|
||
**决策理由**:选 A。规格 §25 是否有意排除 `http` **未核实**,在核实之前把它加进
|
||
UI 就是替规格做决定;而现状并无功能损失(能导入、能导出),只是不能手工新建,
|
||
写进文档说明即可。
|
||
|
||
## 2.5 `parser_yaml.lua` 为死代码(**已删除 `[2.6.11-r1]`**)
|
||
|
||
**依据**:`parser.lua:6` `require` 了它,但全文对 `parser_yaml.` 的调用次数实测为
|
||
**0**。其中留有同样未归一的 `socks5` 映射。
|
||
|
||
**影响**:不影响运行,属清理项。
|
||
|
||
**候选方案**:A. 删除文件与 `require`;B. 保留(无害)。
|
||
|
||
## 2.6 AmneziaWG 3.0 / 3.1 新增的 9 个字段不被 `.conf` 解析器接受
|
||
|
||
**字段**:`HeaderProtectionKey`、`ContentPaddingAddition`、`RekeyAfterTime`、
|
||
`RekeyTimeout`、`RejectAfterTime`、`KeepaliveTimeout`、`MaxHandshakeAttempts`、
|
||
`RandomTrailers`、`DisableCookies`。
|
||
|
||
**依据**:实测这 9 个名字在 `parser.lua` 中**一处都不出现** —— 即 `.conf` 解析器
|
||
按已知键白名单映射,这 9 个键落在白名单外,**导入即丢弃**;但它们会从 Clash / JSON /
|
||
`wireguard://` 导入路径原样透传。
|
||
|
||
**影响**:同一节点经 `.conf` 导入与经 JSON 导入,得到的字段集**不一致**。
|
||
|
||
**状态:已修复(`[2.6.10-r1]` 复核)**。`parser.lua` 的 `.conf` 键映射表已收录全部
|
||
9 个 v3.0 / 3.1 键(`headerprotectionkey` → `header-protection-key` …
|
||
`disablecookies` → `disable-cookies`),并补入了 v1.5 的 `s3/s4/i1..i5/j1..j3/itime`。
|
||
其中 `random-trailers` / `disable-cookies` 是布尔字段,按 amneziawg-tools 的
|
||
`parse_bool`(只认 `on`/`off` 或十进制数)解析,非法值丢弃而不是原样透传 ——
|
||
留着会让 mihomo 解析该字段时报错。**本条无需再决策。**
|
||
|
||
---
|
||
|
||
# 三、更早的遗留项(2.5.x 记录,一并归档)
|
||
|
||
来源:CHANGELOG `[2.5.1-r1]` / `[2.5.0-r1]` 的「已知限制」。均为**已确认、未修复**。
|
||
|
||
## 3.1 LuCI 里 `amnezia-wg-option` 仍是单个 JSON 文本框
|
||
|
||
**状态:确认为已知限制(`[2.6.16-r1]` 决策)** —— 不改代码,写入用户文档
|
||
(README「已知限制」)。
|
||
|
||
**依据**:`node.lua` 的 `PROTO_FIELDS.wireguard` 中 `amnezia-wg-option` 是**单个**
|
||
字段(实测该数组共 13 项,`amnezia-wg-option` 占其一),因此前端渲染成一个文本框,
|
||
需用户手写 JSON。数据本身能正确往返,但录入体验不佳。
|
||
|
||
**候选方案**:逐字段化(属新功能,按版本约定应进入 minor 版本)。
|
||
|
||
**决策理由**:数据往返正确、无功能损失,仅是录入体验;逐字段化属新功能,
|
||
不适合混在缺陷修复批次里。写进文档说明录入方式即可。
|
||
|
||
## 3.2 wget 后端的下载体积上限是**下载后**判断
|
||
|
||
**状态:确认为工具限制(`[2.6.16-r1]` 决策)** —— 无法在代码层消除,写入用户文档
|
||
(README「已知限制」)与本文。
|
||
|
||
**依据**:busybox wget 没有「下载前限流」的选项(已核对 busybox 1.37 `--help`,
|
||
不存在 `--max-filesize` 之类的等价物)。响应体虽被丢弃,但请求已经发出。
|
||
|
||
**实际影响有限的原因**:`fetch_wget` 在体积超限时会**丢弃响应体并报错**
|
||
(`http.lua`:`size > max` → `os.remove(tmp)` + 返回错误),因此超限内容不会进入
|
||
解析流程,也不会落盘留存。剩下的差异只是「流量已经消耗」——这属于工具能力问题。
|
||
|
||
**候选方案**:改用 curl(若有,curl 路径有 `--max-filesize`,是**下载中**截断)或
|
||
维持现状并在文档说明。
|
||
|
||
**决策理由**:不改代码。curl 后端本就已经用 `--max-filesize` 做了下载中限制;
|
||
wget 后端受 busybox 能力所限做不到,自实现 HTTP 客户端属大改动、收益与风险不成
|
||
比例。如实写进文档。
|
||
|
||
## 3.3 改名规则不支持括号内的「或」`(a|b)`
|
||
|
||
**依据**:Lua 模式无 alternation 语义,按字面处理。
|
||
|
||
**候选方案**:A. 维持现状并文档说明;B. 实现一个简单的 alternation 展开。
|
||
|
||
## 3.4 `check_public` 在无 DNS 解析能力时放行(fail-open)
|
||
|
||
**依据**:`http.lua` 实测代码为:
|
||
|
||
```lua
|
||
local ips = resolve(host)
|
||
if not ips then
|
||
-- 无 DNS 解析能力:放行,交由下载工具处理(尽力而为)
|
||
return true
|
||
end
|
||
```
|
||
|
||
即无 `nixio` 时任意主机名放行。另存在解析与下载之间的 TOCTOU 窗口。
|
||
|
||
**状态:已修复(`[2.6.8-r1]`),但该修复本身引入了回归,已于 `[2.6.12-r1]` 重做。**
|
||
|
||
`[2.6.8-r1]` 把「解析不出 IP」一律改成 fail-closed,前提是「本包依赖
|
||
luci-lua-runtime → 硬依赖 luci-lib-nixio → 解析失败只意味着真的解析不了」。
|
||
**这个前提在用户设备上不成立**:`resolve()` 当时只有 `nixio` 一条路,
|
||
nixio 取不到时恒返回 `nil`,于是所有域名订阅都被拒,报「无法解析目标主机名」
|
||
(用户实测 2.6.7-r1 正常,2.6.8-r1 起四个订阅全部失效)。根因是把
|
||
「本机没有解析手段」与「这个域名解析不出来」合并成了同一个 `nil`。
|
||
|
||
`[2.6.12-r1]` 的做法:
|
||
- `resolve()` 多级回退 `nixio` → busybox `nslookup`,并返回 `ips, have_resolver`;
|
||
- 有解析手段却解析不出来 → 仍 fail-closed(本条要堵的绕过口继续堵死);
|
||
- 完全没有解析手段 → 放行,但返回 `unverified = true`;
|
||
- 下载层用 curl 的 `%{remote_ip}` 在**连接建立后**复核实际对端地址,
|
||
并把 `unverified` 且拿不到对端 IP 视为拒绝。
|
||
|
||
TOCTOU 窗口(解析与下载之间)也由此关闭:`%{remote_ip}` 就是真正连上的那个 IP,
|
||
免疫「预检时解析到公网、连接时解析到内网」的 DNS rebinding。
|
||
|
||
**`[2.6.16-r1]` 补完:wget 后端此前仍是 fail-open。**
|
||
|
||
`[2.6.12-r1]` 的「连接后复核」只对 **curl** 后端成立 —— 它靠 `%{remote_ip}` 拿到
|
||
真实对端地址。**wget 后端没有任何等价物**:busybox wget 拿不到对端 IP,
|
||
`-S` 日志里的重定向链也只能**事后**看(拦不住已经发出去的请求)。于是在
|
||
「本机没有解析手段」这种设备上,wget 路径的「预检放行」之后**不存在任何一处校验**
|
||
—— 等于完全没有 SSRF 防护,而这条路径此前是**静默**的。
|
||
|
||
现已按与 `verify_peer_ip` 相同的处置收敛:**校验不了就拒绝**。
|
||
`fetch_wget` 在 `opts.unverified` 为真时直接返回错误
|
||
("本机无 DNS 解析能力,wget 后端无法校验目标地址,已拒绝下载(安装 curl 后重试)"),
|
||
不再尝试下载。
|
||
|
||
**影响面(如实记录)**:仅影响「本机无任何 DNS 解析手段(nixio 不可用且无 nslookup)
|
||
**且** 目标为域名 **且** 下载后端为 wget」的设备 —— 这类设备上的域名订阅会失败,
|
||
提示安装 curl。字面 IP 目标不受影响(`check_public` 对 IP 提前返回,不产生
|
||
`unverified`);有解析手段的设备也不受影响。**这是有意的取舍**:本包依赖
|
||
`luci-lua-runtime`,正常安装的设备上 `nslookup` 可用,属于罕见组合;而放行的代价是
|
||
一个**完全无防护**的 SSRF 入口。
|
||
|
||
**回归测试**:`tests/dns_fallback_test.lua` 用例 H(无解析器 + wget → 拒绝;桩会写入
|
||
`evilbody`,确保断言不是因「内容为空」而误过)与 H2(对照组:有解析器 → wget 正常
|
||
下载)。反向验证:修复前 H 有 3 条断言失败。
|
||
|
||
**本条已了结,无需再决策。**
|
||
|
||
## 3.5 wget 路径的重定向校验是**事后**的
|
||
|
||
**状态:确认为工具限制(`[2.6.16-r1]` 决策)** —— 无法在代码层消除,写入用户文档
|
||
(README「已知限制」)与本文。
|
||
|
||
**依据**:busybox wget 无 `--max-redirect`(已核对 busybox 1.37 `--help`);响应体虽
|
||
被丢弃但请求已经发出,且无法限制下载体积。
|
||
|
||
**已在代码层做到的部分(`[2.6.16-r1]` 复核)**:`M.validate_redirect_chain(url, log)`
|
||
会从 `-S` 日志里按出现顺序提取整条重定向链的 `Location`,对**每一跳**重新做
|
||
`check_public`,任一跳指向内网/保留地址即整体拒绝并丢弃响应体。因此
|
||
「重定向到内网」**不会**被放行 —— 缺的只是「在发出下一跳请求**之前**拦住它」。
|
||
|
||
**与 3.4 的区别**:3.4 是「根本没有校验」(已修,改为拒绝);本条是「校验存在但
|
||
发生在请求之后」。后者不会让内网内容进入解析流程,只是多发出了一跳请求。
|
||
|
||
**候选方案**:与 3.2 一并处理(改用 curl 或自实现 HTTP 客户端)。curl 路径用
|
||
`--max-redirs 0` 自行逐跳跟随,每跳都在**发出前**校验,不受此限制。
|
||
|
||
**决策理由**:不改代码。curl 后端已是「发出前校验」;wget 后端受 busybox 能力所限,
|
||
自实现 HTTP 客户端属大改动。如实写进文档。
|
||
|
||
---
|
||
|
||
# 四、P3:输出层代码级复审(`[2.6.10-r1]` 已修复)
|
||
|
||
对 8 个输出模块与 `output.lua` 做了逐行复审,按「生成的配置能否被目标客户端加载」
|
||
这一条标准筛出 11 项缺陷。**全部已修复、已补回归测试、并已对 `HEAD` 反向验证**
|
||
(`tests/output_layer_fixes_test.lua` 30 条断言、`tests/ss_plugin_test.lua` 25 条
|
||
断言在修复前失败,修复后全绿)。逐项记录如下。
|
||
|
||
| 编号 | 位置 | 缺陷 | 后果 |
|
||
|---|---|---|---|
|
||
| F1 | `output_clash_meta` | `ws-opts:` 写在 `if node.path` 内,只有 host 的 ws 节点输出悬空的 `headers:` | YAML 非法 / Host 被忽略,ws 握手被拒 |
|
||
| F2 | `output_v2ray` | 空传输层编码成 `[]` 而非 `{}` | Xray `UnmarshalTypeError`,拒绝启动 |
|
||
| F3 | `output_clash_meta` | 节点名与其它节点 / 策略组名 / 保留名冲突时不消解 | mihomo `proxies` 重名,拒绝加载整份配置 |
|
||
| F4 | `output_clash_meta` | `esc_yaml` 未引用以 `%` `!` `-` 开头的标量 | 非法 YAML,mihomo 解析失败 |
|
||
| F5 | `output_formats` | QX 的 vless 分支不输出 obfs / tls 参数 | 客户端按明文 tcp 连 ws 端口,必然失败且不报错 |
|
||
| F6 | `output_formats` | Surge 家族的 vless 分支不输出 ws 传输层 | 同上 |
|
||
| F7 | `output_formats` | 参数值里的逗号把凭据静默截断(`password=pa,ss` → `password=pa`) | 静默发错凭据 |
|
||
| F8 | `output_uri` | IPv6 字面量地址未加方括号 | authority 无法解析 |
|
||
| F9 | `output_clash_meta` | `amnezia-wg-option` 子键未转义 | 键名里的 `:` / 引号写坏 YAML 映射 |
|
||
| F10 | `output.lua` | `content_type_for` / `extension_for` 对空 target 落空(`""` 在 Lua 里是真值) | 正文与响应头 / 文件名后缀不一致 |
|
||
| F11 | 全链路 | shadowsocks 的 SIP003 `plugin` 被三个输出模块静默丢弃;表单保存时被清空 | 带 obfs / v2ray-plugin 的节点导出后连不上;界面上编辑一次即永久丢失插件配置 |
|
||
|
||
**F11 的处置依据**(均核对上游源码 / 文档,非推测):
|
||
|
||
- **SIP002**:`SS-URI = "ss://" userinfo "@" host ":" port [ "/" ] [ "?" plugin ] [ "#" tag ]`,
|
||
插件参数整体做百分号编码。
|
||
- **sing-box**:`shadowsocks` 出站只有 `plugin`(字符串,官方文档明确
|
||
*"Only two are supported: obfs-local and v2ray-plugin"*)与 `plugin_opts`
|
||
(SIP003 原始参数串,原样透传)。其余插件名会被拒绝,故按白名单过滤。
|
||
- **mihomo**:`plugin-opts` 是**映射**而非字符串,且名称与参数名都要翻译
|
||
(`obfs-local`/`simple-obfs` → `obfs`,参数 `obfs` → `mode`、`obfs-host` → `host`)。
|
||
`adapter/outbound/shadowsocks.go` 对这两类插件是**强校验**的:obfs 的 mode 不在
|
||
`{tls,http}` 里报 `"ss %s obfs mode error"`,v2ray-plugin 的 mode 不是 `websocket`
|
||
同样报错 —— 都会让整个 outbound 构造失败,进而**拒绝加载整份配置**。因此参数
|
||
不全时宁可整个不输出插件,也不能输出一个必然被拒绝的组合。
|
||
|
||
**顺带修正的既有测试**:`tests/output_formats_test.lua` 原断言
|
||
`tuic alpn array joined` 期望 `alpn=h3,h2`。多值 alpn 在 Surge 家族的行语法里
|
||
表达不了(逗号分隔的 `key=value`,无转义),F7 的通用逗号检查会因此丢掉整个节点。
|
||
处置:alpn 是**协商提示**而非凭据,缺省时客户端用服务端给出的列表 —— 所以只省略
|
||
该参数、保留节点。断言已按此契约更新。
|
||
|
||
---
|
||
|
||
# 五、其余遗留项的修复建议(按推荐优先级排序)
|
||
|
||
**实施记录(`[2.6.11-r1]`)**:下表中 1.1 / 1.3 / 1.4 / 1.5 / 2.5 五项已按推荐方案
|
||
实施完毕,3.3 按方案 A 补了用户文档说明。每项都补了自包含回归测试,并对 `HEAD`
|
||
做了反向验证(新断言在修复前失败、修复后全绿):
|
||
|
||
| 项 | 回归测试 | HEAD 上失败断言数 |
|
||
|---|---|---|
|
||
| 1.1 `mkdir` 锁 | `tests/list_lock_test.lua`(39 条) | 18 |
|
||
| 1.3 sing-box `transport` | `tests/singbox_transport_test.lua`(28 条) | 18 |
|
||
| 1.5 QX `tag=` 逗号 | `tests/qx_tag_comma_test.lua`(24 条) | 9 |
|
||
|
||
1.4 / 2.5 是删除死代码,无新增断言;1.4 顺带修复了 `tests/p2_batch7_test.lua`
|
||
中对已删模块的依赖(改为直接验证 `util.uuid`)。
|
||
|
||
实施中修正了两处**推荐方案本身**的疏漏,记录如下:
|
||
|
||
- **1.1**:`with_list_lock` 最初把锁目录写成常量 `M.LOCK_DIR`。测试普遍在
|
||
`require` 之后改写 `core.DATA_DIR`,常量不会跟着变 —— 于是测试会去锁真实的
|
||
`/etc/substore`。改为由 `M.DATA_DIR` 现算。另一处:全新安装时 `DATA_DIR` 尚不存在,
|
||
`mkdir <DATA_DIR>/.lock` 失败,而失败在 `lock_acquire` 眼里等同于「他人持锁」,
|
||
症状是第一次保存订阅就报「正被另一个进程修改」;已在取锁前先 `ensure_dirs()`。
|
||
- **1.1**:`M.merge` 是**只读**的(只读各订阅节点、过滤排序后返回数组),
|
||
包进锁后一旦取锁失败会返回 `nil` 顶掉原本的数组,把「拿不到锁」变成调用方眼里的
|
||
「没有数据」—— 比不加锁更糟。已移出包装列表。
|
||
|
||
| 项 | 推荐方案 | 理由 | 前置条件 |
|
||
|---|---|---|---|
|
||
| 1.1 H10 无锁 | ✅ **C**:`mkdir` 原子锁 + 陈旧锁回收 | 纯 Lua、无新依赖,本机**可测**(桩掉 `os.execute` / `os.time`);崩溃残留锁由锁目录 mtime 自愈 | 已定阈值 `util.LOCK_STALE = 60` 秒 |
|
||
| 1.3 sing-box 读取侧 transport 丢失 | ✅ **已修复**(补 `transport` 解析) | 与 F5/F6/F11 同类:静默丢传输层 → 节点连不上;影响面更大 | 字段映射**已核实**(见 1.3 正文) |
|
||
| 1.4 死代码 + README 声明不符 | ✅ **已删除**死代码(并已随 `[2.6.10-r1]` 精简 README 声明) | 删去了「协议转换:任意协议 → 任意协议」的过度声明 | 已确认无任何 `require` |
|
||
| 1.5 QX `tag=` 逗号 | ✅ **B**:整体丢弃 | 见上文 | 已实施;QX 实际行为仍未实测(无 QX 环境),按「宁可丢节点也不输出损坏行」处理 |
|
||
| 2.1 混合格式文本导入 | ✅ **B+**:本地导入时明确报错(`[2.6.16-r1]`) | 见 2.1 正文;远程订阅路径刻意不动 | 无 |
|
||
| 2.2 hysteria v1 `up`/`down` | ✅ **A**:确认为已知限制 + 文档(`[2.6.16-r1]`) | 补字段属新功能,且单位/缺省值规格未核实 | 无 |
|
||
| 2.3 hysteria v1 URI 规范 | ✅ **维持现状** + 文档(`[2.6.16-r1]`) | 上游规范无法核实,按「不猜」原则不动 | 找到权威规范后再定 |
|
||
| 2.4 `http` 不进 UI | ✅ **A**:确认为已知限制 + 文档(`[2.6.16-r1]`) | 规格 §25 是否有意排除未核实,现状无功能损失 | 无 |
|
||
| 2.5 `parser_yaml.lua` 死代码 | ✅ **已删除** | 死代码会被后续审计反复重新评估,成本高于收益 | 已确认只被 `parser.lua` 一行 `require` 引用、且从未使用 |
|
||
| 3.1 AWG 逐字段化 UI | ✅ **确认为已知限制** + 文档(`[2.6.16-r1]`) | 数据往返已正确,仅录入体验 | 无 |
|
||
| 3.2 / 3.5 wget 体积与重定向 | ✅ **确认为工具限制** + 文档(`[2.6.16-r1]`) | busybox wget 无对应选项;curl 路径已无此问题 | 无 |
|
||
| 3.3 改名规则 `(a\|b)` | ✅ **A**:维持现状 + 文档说明 | Lua 模式无 alternation;自实现展开要处理嵌套与字符类,收益低 | 已在 README.md / README.en.md 的「使用方法」补说明(顶层 `\|` 才是「或」,`(...)` 内按字面) |
|
||
|
||
---
|
||
|
||
# 六、本轮实施记录(`[2.6.16-r1]`)
|
||
|
||
本轮按「处置优先级 1–8」实施,逐项记录如下。**每项都补了自包含回归测试,并对
|
||
`HEAD` 做了反向验证**(新断言在修复前失败、修复后全绿)—— 反向验证是必须的:
|
||
只证明「修复后测试通过」无法排除「这条断言本来就不会失败」。
|
||
|
||
| 优先级 | 项 | 位置 | 回归测试 | HEAD 上失败断言数 |
|
||
|---|---|---|---|---|
|
||
| 1 | wget 后端 SSRF 缺口(见 3.4) | `http.lua` `fetch_wget` | `tests/dns_fallback_test.lua` 用例 H / H2 | 3 |
|
||
| 2 | 删除部分失败时漏写 cron | `controller/admin/substore.lua` `action_delete` | `tests/controller_robustness_test.lua` | 1 |
|
||
| 3 | 混合格式静默丢弃(见 2.1) | `parser.lua` `detect_all` + `parse_local` | `tests/parser_mixed_format_test.lua`(70 条) | 探针:`nodes=1, err=nil` |
|
||
| 4 | 写入失败被忽略 | `core.lua` `M.remove` / `M.ensure_token` | `tests/data_integrity_test.lua` | 5 |
|
||
| 5 | `post_ok()` 空 token 放行(见 1.2 M28) | `controller/admin/substore.lua` | `tests/controller_robustness_test.lua` M28 段 | 7 |
|
||
| 6 | 无 ACL(见 1.2 M29) | `rpcd/acl.d` + `menu.d` + `Makefile` | `tests/acl_menu_test.lua`(19 条) | 13 |
|
||
| 7 | A4–A7 记文档(2.2 / 2.3 / 2.4 / 3.1) | README / 本文件 | —(无代码改动) | — |
|
||
| 8 | wget 体积与重定向记文档(3.2 / 3.5) | README / 本文件 | —(无代码改动) | — |
|
||
|
||
**优先级 2 的详细依据**(该缺陷不在本文件原有清单内,是实施前代码级审计新发现的):
|
||
|
||
`action_delete` 原先只在**全部删除成功**时调用 `core.write_cron()`,部分失败时
|
||
直接 `return back_to_list(...)` 跳过了它。后果:被删掉的订阅的 cron 行仍留在
|
||
crontab 里,`substore-cron.sh` 会拿着已不存在的 id 反复执行,每次都以非 0 退出
|
||
(脚本末尾有 FAILED 判断),在日志里刷失败、并让监控误报。现改为
|
||
**只要有订阅真的被删掉(`removed > 0`)就重写 cron**,与成功/失败分支无关。
|
||
|
||
**优先级 4 的详细依据**(同样是本轮审计新发现):
|
||
|
||
`core.lua` 的 `save()` 返回 `util.atomic_write(...)` 的结果 —— 失败时是
|
||
`false, err`,而 `M.remove` / `M.ensure_token` 此前**丢弃了返回值**:写盘失败时
|
||
`M.remove` 仍报成功(订阅文件已删、索引没更新 → 索引指向不存在的订阅),
|
||
`M.ensure_token` 会把一个**没有落盘**的 token 返回给调用方(页面显示 token,
|
||
实际不存在)。现改为透传失败与原因。回归测试用 `util.atomic_write` 猴补丁注入
|
||
写失败,断言:`remove` 报失败**且保留** nodes 文件、`ensure_token` 返回 `nil, err`、
|
||
失败恢复后能正常写入。
|
||
|
||
---
|
||
|
||
# 七、2.7.2 审计新发现
|
||
|
||
本节是给 AnyTLS + Reality 支持做代码级审计时**顺带确认**的问题。
|
||
每条都给出**实测探针或上游文档依据**,无推测项。
|
||
|
||
**状态**(决策已定,按轮次实施):
|
||
|
||
| # | 决策 | 状态 |
|
||
|---|---|---|
|
||
| 7.1 | A:引入 `FAMILY_CAPS` 按客户端能力表过滤 | **已实施**(第二轮,见下) |
|
||
| 7.2 | A:真正实现 Egern YAML 生成器 | **已实施**(第三轮,见下) |
|
||
| 7.3 | C:只对带 `public-key` 的 vmess / vless 分叉 `qx_tls` | **已实施**(第二轮,见下) |
|
||
| 7.4 | A:Loon 位置参数化(并给 `public-key` 加双引号) | **已实施**(第二轮,见下) |
|
||
| 7.5 | 修复 | **已修复**(第一轮,见下) |
|
||
| 7.6 | 修复 | **已修复**(第一轮,见下) |
|
||
| 7.7 | A:后端错误串 msgid 化 | **已实施**(第四轮,见下) |
|
||
| 7.8 | A:删除 `age.lua` / `age_test.lua` | **已删除**(第一轮) |
|
||
| 7.9 | (a)(b)(c)(d) 全部实施;(e) 具名保持保守、Loon 位置参数放行(第八轮修订);(g)(h) 实施;(i) 保持现状;(j) 实施(Surge/SurgeMac) | **已实施**(第五~八轮,见下) |
|
||
|
||
7.5 / 7.6 的修复见本节末尾「7.5 / 7.6 修复记录」,
|
||
7.1 / 7.3 / 7.4 的实施见「第二轮修复记录」,
|
||
7.2 的实施见「第三轮修复记录」,
|
||
7.7 的实施见「第四轮修复记录」,
|
||
7.9 的 (a)–(e) 见「第五轮修复记录」,(g)(h) 见「第六轮修复记录」,(j) 见「第七轮修复记录」,(e) 的修订见「第八轮修复记录」。
|
||
|
||
| # | 问题 | 位置 | 依据 | 影响 |
|
||
|---|---|---|---|---|
|
||
| 7.1 | Surge 格式会为 VLESS 节点生成代理行,而 Surge 的协议清单里没有 VLESS | `output_formats.surge_config` | Surge 手册(`manual.nssurge.com`)协议清单无 VLESS;探针见下 | 节点必然不可用(Surge 对「不认识的代理行」的处置**未获官方证实** —— 官方只说明过无法识别的 *section* 会原样保留且不报错;此处按「不输出客户端读不懂的东西」处理,与丢弃 wireguard / ssr 同一约定)。**已按 A 实施**:新增 `FAMILY_CAPS`,Surge / Surfboard / SurgeMac 丢 vless。(第二轮时 Egern 也在表里丢 ssr;第三轮 7.2 实施后 Egern 有了自己的模块,能力判定随之搬进 `output_egern.lua` 的 `EGERN_KEY`。) |
|
||
| 7.2 | Egern 格式输出的是 Surge 逗号行,而 Egern 的配置是 YAML | `output_formats.to_egern`(**已删除**) | `egernapp.com/docs/configuration/example/` 与 `.../proxies/`;探针见下 | 选 Egern 格式导出的内容 Egern 读不了。**已按 A 实施**:新增 `output_egern.lua`(真正的 YAML 生成器),`output_formats.to_egern` 与其 `M.generate` 分支一并删除;后缀 `.conf` → `.yaml` |
|
||
| 7.3 | QX 的 vmess / vless 用 `tls-host=` + `tls-verification=true` 表示 TLS,官方 `sample.conf` 用 `obfs=over-tls` / `obfs=wss` + `obfs-host` | `output_formats.qx_tls` | `crossutility/Quantumult-X` 的 `sample.conf`;探针见下 | 见 7.3 的详细说明 —— **会让新加的 QX vmess/vless Reality 公钥不生效** |
|
||
| 7.4 | Loon 的 trojan / vmess / vless 凭据在官方文档里是**位置参数**,本生成器一律写具名参数(只有 anytls 按 flavor 分对了) | `output_formats.surge_line` | `nsloon.app/docs/Node/` 的示例行 | Loon 是否同时接受具名写法**无文档依据**;若不接受则这几类节点导出到 Loon 后连不上。**已按 A 实施**:Loon 改位置参数,Surfboard 的同类问题见 7.9 |
|
||
| 7.5 | ~~`parser_surge` 的行拆分不是引号感知的(`rest:gmatch("[^,]+")`)~~ **已修复** | `parser_surge.lua` 的 `split_fields` | 代码级:位置参数里含逗号的值会被切断 | 别人给的 Loon / QX 配置里带逗号的密码被**静默截断**(生成端已有「含逗号就整条丢弃」的防护,解析端没有对应防护) |
|
||
| 7.6 | ~~Loon 的 `transport=ws` 未映射到 `net`~~ **已修复** | `parser_surge.parse_surge_line` 的 `transport=` 分支 | 只认 `ws=true`(Surge 旧写法)与 `obfs=ws`(QX);`nsloon.app/docs/Node/` 用 `transport=ws` + `path=` + `host=` | Loon 的 ws 节点导入后 `net=tcp`,`path` / `host` 全丢 → 导出到任何格式都按 tcp 连,握手失败**且不报错** |
|
||
| 7.7 | ~~后端模块仍有 **113 处**硬编码中文字符串字面量(注释外)~~ **已实施** | `core.lua` 47 / `http.lua` 47 / `parser.lua` 9 / `util.lua` 5 / `output_wireguard_conf.lua` 3 / `node.lua` 2 | 扫描脚本(去注释后提取含 CJK / 全角的字符串字面量),见下 | 控制器文案已接入 i18n,但这些来自后端的失败原因经 `?err=` **原样**显示,英文界面下仍是中文。**已按 A 实施**:全部改为语言中立的英文 msgid,组合消息用 `msg.lua` 的分隔符机制,翻译只在显示边界发生(详见「第四轮修复记录」) |
|
||
| 7.8 | ~~`root/usr/share/substore/age.lua` 与 `tests/age_test.lua` 未被 git 跟踪,且 `age.lua` 未被任何模块 `require`~~ **已删除** | 仓库根 | `grep -rn require` 无引用 | 未随包发布;留在工作区会被后续审计反复重新评估 |
|
||
| 7.9 | ~~Loon 的节点行仍有**多处**与官方文档不一致~~ **已实施** | `output_formats.surge_line` / `parser_surge.parse_surge_line` | `nsloon.app/docs/Node/`;`manual.nssurge.com`;`getsurfboard.com` | 逐条见下「7.9 的明细」。**已按 (a)(b)(c)(d) 全部实施、(e) 具名保持保守而 Loon 位置参数于第八轮改为放行**:Loon 的 TLS 开关改 `over-tls`(Trojan 除外)、UDP 改 `udp`、Surge 家族补 `encrypt-method`、Loon 的 ss/ssr/hysteria2 改位置参数;解析端同步跟进以保住回环(详见「第五轮修复记录」)。实施中新发现三条待决策项,见下「第五轮新发现」;其中 (g)(h) 已于第六轮实施、(i) 决定保持现状,另发现 (j) Surge 家族同样没有 Hysteria v1,**已于第七轮实施**(Surge / SurgeMac 丢弃,Surfboard 因无证据保留) |
|
||
|
||
### 7.7 的统计口径与例外(修复前必读)
|
||
|
||
本节原先写的「约 103 处(`grep -c 'return nil, ".*[^ -~]'`)」**口径有误**:
|
||
那条 grep 按行匹配,既会把中文注释算进去,又会漏掉「只含全角标点」的字面量
|
||
(如 `","` 这类),逐文件数字也对不上。下面是重新核对的口径。
|
||
|
||
**统计方法**:先去掉注释(`--` 行注释与 `--[[ ]]` 块注释),再提取字符串字面量
|
||
(`"` 与 `'`),保留其中含 CJK / 全角字符的(UTF-8 首字节 `E3`–`E9` 或 `EF`)。
|
||
结果 **113 处**:`core.lua` 47 / `http.lua` 47 / `parser.lua` 9 / `util.lua` 5 /
|
||
`output_wireguard_conf.lua` 3 / `node.lua` 2。
|
||
|
||
其中 **76 处**是直接的 `return nil, "…"` / `return false, "…"` 形式;另外 37 处是
|
||
同一类文案经别的路径到达用户,例如:
|
||
|
||
* `core.lua` 的 `M.save_meta(id, { error = "…" })` —— 同一个串既写进 meta
|
||
(列表页用 `it.error` 显示)又被 `return` 出去,**两处必须用同一个 msgid**;
|
||
* `:format()` / `..` 拼接出来的串(如 `"HTTP 错误 "`、`"响应超过大小限制 ("`);
|
||
* 表项(`FORMAT_LABELS`)与视图直接渲染的标签(`human_duration`)。
|
||
|
||
**三处不能照搬 msgid 化**:
|
||
|
||
| 位置 | 内容 | 为什么特殊 |
|
||
|---|---|---|
|
||
| `node.lua:377` | `"[^,%s,]+"` | 这是**正则字符类**,全角逗号是模式的一部分,翻译会直接破坏关键词拆分 |
|
||
| `util.lua:658` `M.human_duration` | `"已过期"` / `"%d天"` / `"%d小时"` / `"%d分钟"` / `"不足1分钟"` | 由**视图** `view/substore/form.htm:39` 直接渲染,不走 `?err=`;应在视图侧翻译(或 msgid 化后在视图 `_()`) |
|
||
| `parser.lua:103` `FORMAT_LABELS` | `"URI 链接"` / `"Surge/Loon 配置"` | 是**格式显示名**,被插进「混用多种格式」的错误文案里 |
|
||
|
||
**关键设计点**:`meta.error` 会被 `it.error` 原样显示,所以 msgid 必须**语言中立**
|
||
(英文),翻译只发生在显示边界(视图 / 控制器),后端模块不得 `require("luci.i18n")`
|
||
—— `substore-cron.sh` 会在独立的 lua 进程里跑 `core.sync`,那里没有 LuCI 环境。
|
||
|
||
### 7.9 的明细(第二轮实施时新发现,**第五轮已实施**)
|
||
|
||
第二轮为实施 7.4-A 去核对 Loon 的节点行文档(`nsloon.app/docs/Node/`),顺带发现
|
||
Loon 与 Surge 家族的不一致远不止「凭据位置」。以下每条都对照官方文档原文,
|
||
**没有推测项**;本轮按用户的轮次安排只做决策里点名的 7.4-A,其余记录在此待决策。
|
||
**第五轮(2.7.2-r6)已按 (a)(b)(c)(d) 全部实施、(e) 保持保守**,各条下方的
|
||
「→ 实施」注明落点;**(e) 已于第八轮(2.7.2-r9)部分修订**(具名参数保持保守、
|
||
Loon 的引号包裹位置参数改为放行),见「第八轮修复记录」。
|
||
|
||
**(a) Loon 的 TLS 开关写作 `over-tls`,本生成器写 `tls`** —— 影响最大的一条。
|
||
Loon 文档的通用参数表与 Reality 示例都用 `over-tls=true`:
|
||
|
||
```
|
||
节点名称 = VMess,服务器,端口,加密方式,"UUID",transport=传输方式,可选参数
|
||
… ,public-key="…",short-id=…,over-tls=true
|
||
```
|
||
|
||
本生成器对 Loon 的 vmess / vless / trojan 一律写 `tls=true`(Surge 的写法)。
|
||
Loon 文档**只**为传输参数明说了旧写法别名(`ws=true` ↔ `transport=ws`、
|
||
`ws-path` ↔ `path`、`ws-headers=Host:域名` ↔ `host`),**没有**把 `tls` 列为
|
||
`over-tls` 的别名,也没有说它会被拒绝。所以「`tls=true` 在 Loon 里是否生效」
|
||
**无依据**。若被忽略,Loon 上的 vmess / vless / trojan 会按明文连 ——
|
||
**包括本轮新支持的 Reality 节点**(公钥写了、TLS 标志却没生效)。
|
||
这条不修,7.4-A 与 Reality 支持在真机 Loon 上的收益都要打折。
|
||
|
||
**→ 实施(第五轮)**:`surge_line` 引入 `tls_flag`(`is_loon and "over-tls=true"
|
||
or "tls=true"`),vmess / vless 用它。**Trojan 例外**:复核 Loon 文档的明文与
|
||
Reality 两条 Trojan 示例,**都没有任何 TLS 键** —— Trojan 本身就建立在 TLS 之上,
|
||
Loon 不再要这个开关,所以 Loon 的 Trojan 行整个不写 TLS 参数(Surge 家族的
|
||
`tls=true` 是文档要求的,保持不变)。解析端早已同时认 `tls` 与 `over-tls`
|
||
(`parser_surge.lua`),回环不受影响。
|
||
|
||
**(b) Loon 的 UDP 参数写作 `udp`,本生成器写 `udp-relay`** —— 同上,Loon 文档是
|
||
`udp=true`。影响较小(UDP 转发失效,节点本身还能用)。
|
||
|
||
**→ 实施(第五轮)**:`udp = is_loon and "udp=true" or "udp-relay=true"`。
|
||
|
||
**(c) Surge 家族的 vmess 不输出 `encrypt-method`** —— Surge 手册的 vmess 页有
|
||
`encrypt-method` 参数,取值 `aes-128-gcm`(默认)或 `chacha20-ietf-poly1305`。
|
||
本生成器只写 `username=`,所以 cipher 是 chacha20 的节点会被 Surge 按默认的
|
||
`aes-128-gcm` 去连 —— **静默用错算法**,与 F5/F6 同一类失效。注意拼写差异:
|
||
统一模型写 `chacha20-poly1305`,Surge / Loon 都写 `chacha20-ietf-poly1305`
|
||
(Loon 侧本轮已用 `LOON_VMESS_CIPHER` 映射,Surge 侧没有对应参数可写)。
|
||
|
||
**→ 实施(第五轮)**:新增 `SURGE_VMESS_CIPHER`(`aes-128-gcm` → `aes-128-gcm`、
|
||
`chacha20-poly1305` → `chacha20-ietf-poly1305`),在**非 Loon** 的 vmess 分支输出
|
||
`encrypt-method=`。取值不在 Surge 清单里时(`zero` / `none` / `auto` /
|
||
`aes-128-cfb`)不写该参数、节点保留 —— 写一个客户端读不懂的取值比不写更糟。
|
||
|
||
**(d) Loon 的 shadowsocks / ssr / hysteria2 凭据也是位置参数** —— Loon 文档:
|
||
|
||
```
|
||
节点名称 = Shadowsocks,服务器,端口,加密方式,"密码",可选参数
|
||
节点名称 = ShadowsocksR,服务器,端口,加密方式,"密码",protocol=协议,…
|
||
节点名称 = Hysteria2,服务器,端口,"密码",可选参数
|
||
```
|
||
|
||
本生成器对这些协议一律写具名参数(`encrypt-method=` / `password=`)。
|
||
|
||
**→ 实施(第五轮)**:Loon 分支下 ss / ssr 的加密方式与密码、hysteria2 的密码
|
||
一律改位置参数(密码走 `loon_positional` 加双引号);ssr 的 `protocol=` / `obfs=`
|
||
及其参数仍是具名。**解析端同步跟进**(`parser_surge.parse_surge_line`)——
|
||
其中 **SSR 此前根本没有取字段的分支**,任何写法导入后加密方式、密码、协议插件、
|
||
混淆方式全是 nil,是这一轮顺带修掉的既有缺陷。
|
||
|
||
**(e) Loon 的双引号确实能保住逗号** —— 文档原文「参数值中含有英文逗号时,
|
||
请使用双引号包裹」。本文件此前注释写的「Loon 的那对引号只是标记,值里的逗号
|
||
照样是分隔符」是**错的**,已在本轮更正。当前实现仍按「含逗号就整条丢弃」处理
|
||
(保守:本行的具名参数一律不带引号,两种约定混用会让行为依赖客户端实现),
|
||
代价是极少见的「密码里带逗号」的节点在 Loon 上被丢弃。
|
||
|
||
**→ 部分修订(第八轮,2.7.2-r9)**:既然引号**确实**能保住逗号,上述保守决策的
|
||
依据(「引号救不了逗号」)就不成立。第八轮只推翻**位置参数那一半** —— Loon 的
|
||
引号包裹位置参数放行含逗号的值;**具名参数仍按「含逗号就丢弃」**(Surge 家族的
|
||
引号语义未获文档证据,且解析端不对具名值 unquote)。判据是「值是否被引号完整
|
||
包裹」而非「是否 Loon」,Surfboard 的裸位置参数因此自动落在丢弃一侧。详见
|
||
「第八轮修复记录」。
|
||
|
||
**(f) 更正:Surfboard 的 trojan / vmess / vless 凭据是具名写法,不是位置参数** ——
|
||
本节 7.4 行原先把 Surfboard 与 Loon 并列,是**误判**。复核 `getsurfboard.com` 的
|
||
vmess 页:其 Format 模板写 `{username}` 位置,但**实际示例与参数表都是 `key=value`**
|
||
(`ProxyVMess = vmess, 1.2.3.4, 8000, username=0233d11c-…`);Surge 手册的 vmess /
|
||
trojan 页同样写 `username=` / `password=`。所以 Surge / Surfboard / SurgeMac 的
|
||
具名写法**是对的**,7.4 只该改 Loon —— 7.4 行已按此更正。
|
||
|
||
**(e) 实施说明(第五轮)**:保持不变。生成端仍按「位置参数或具名值里含英文逗号
|
||
就整条丢弃」处理,并在 `tests/output_formats_test.lua` 钉了一条断言防止日后被
|
||
无意改成半截输出。
|
||
|
||
**(e) 修订说明(第八轮)**:位置参数那一半被**部分推翻**。第五轮那条断言
|
||
(`loon comma password drops node`)已按新行为翻转成 `loon comma password kept
|
||
(quoted positional)`,并补了「导出 → 导入拿到同一个密码」的回环断言 —— 放行的
|
||
前提就是能原样回环。具名参数那一半保持不变,护栏断言仍在。
|
||
|
||
### 第五轮新发现((g)(h) 第六轮已实施,(i) 保持现状,(j) 第七轮已实施)
|
||
|
||
第五轮实施 (a)–(d) 时对照 `nsloon.app/docs/Node/` 全文复核,另发现三处不一致。
|
||
每条都只陈述**文档写了什么**与**本实现写了什么**,不推测客户端的实际行为。
|
||
第六轮按决策实施了 (g)(h),(i) 决定保持现状,实施过程中又发现 (j)(第七轮已实施)。
|
||
|
||
**(g) Loon 没有 Hysteria v1,本生成器会把 `hysteria` 行输出给它** —— Loon 文档的
|
||
节点类型清单里只有 **Hysteria2**,没有 Hysteria(v1)。而 `FAMILY_CAPS` 只按
|
||
vless / ssr 分客户端能力,`hysteria` 不在过滤表里,所以统一模型里的 hysteria v1
|
||
节点会生成一条 Loon 读不懂的行。
|
||
|
||
**→ 实施(第六轮)**:`FAMILY_CAPS.loon` 加 `hysteria = false`,整条丢弃
|
||
(与丢弃 wireguard / ssr 同一约定)。同时把 `surge_config` 的过滤从逐协议写死的
|
||
`elseif p == "ssr" and not caps.ssr` 改成数据驱动的 `elseif caps[p] == false` ——
|
||
每加一个维度就要再加一条分支,漏掉的那条默认是「保留」,正是本条缺陷的成因。
|
||
**注意 hysteria 2 是各家通用的**,被丢的只有上一代 v1。
|
||
|
||
**(h) `skip-cert-verify` 写成 `1`,两家文档都没写这个取值** —— Loon 文档的示例
|
||
一律写 `skip-cert-verify=false`;Surge 手册的 TLS 页只写「Optional, boolean,
|
||
default: false」,**没有**列出可接受的字面量。本生成器一律写
|
||
`skip-cert-verify=1`。`1` 是否被接受**无依据**。解析端两种都认(`parser_surge`
|
||
把非 `0` / 非 `false` 都当 true),所以回环不受影响。
|
||
|
||
**→ 实施(第六轮)**:改写成 `skip-cert-verify=true`。判据是「Loon 自己的示例写了
|
||
`false`,直接证明 `false` 是合法字面量、`true` 同理;而 `1` 在任何一家文档里都
|
||
找不到」。既有测试里没有任何一条断言过**生成端**的 `skip-cert-verify=1`
|
||
(`anytls_reality_test` 里那一处是把 `=1` 当**输入**喂给解析器,仍然有效)。
|
||
|
||
**(i) 协议名大小写:Loon 文档一律首字母大写,本生成器一律小写** —— Loon 文档写
|
||
`Hysteria2` / `VMess` / `VLESS` / `Shadowsocks` / `AnyTLS`;本生成器输出
|
||
`hysteria2` / `vmess` / `vless` / `shadowsocks` / `anytls`。Loon 官方定位是
|
||
「兼容 Surge 配置」,而 Surge 手册的语法行用的是小写(`Name = vmess, <host>, …`),
|
||
所以小写**很可能**被接受,但**没有文档明说**。
|
||
|
||
**→ 决定:保持现状(第六轮)**。没有任何证据显示小写会失败,而改动面覆盖全部
|
||
协议;若日后确认大小写敏感,应改的是 Loon 分支的类型名映射。
|
||
|
||
### 第六轮新发现((j) **第七轮已实施**)
|
||
|
||
**(j) Surge 家族同样没有 Hysteria v1,本生成器仍会把 `hysteria` 行输出给它们** ——
|
||
第五轮实施 (g) 时顺带复核了 Surge 的协议清单,发现与 Loon 是**同一个缺口**:
|
||
|
||
* `manual.nssurge.com` 的 Proxy Protocols 一节列的是 **Hysteria 2**,没有 Hysteria;
|
||
* 直接的页面探针:`manual.nssurge.com/policies/hysteria.html` → **HTTP 404**,
|
||
而 `manual.nssurge.com/policies/hysteria2.html` → **存在**(语法行
|
||
`Proxy-Hysteria = hysteria2, 192.168.20.6, 443, password=pwd, download-bandwidth=100`)。
|
||
|
||
也就是说 Surge / SurgeMac 的 `hysteria`(v1)行同样是一条客户端读不懂的行。
|
||
**(g) 的决策只点名了 Loon**,所以第六轮**没有**顺手改 Surge 家族 ——
|
||
`tests/output_formats_test.lua` 里有一条断言如实钉住了当前行为
|
||
(`surge still keeps hysteria v1 (finding j pending)`),实施 (j) 时要一并翻转。
|
||
|
||
Surfboard 未获证据:`getsurfboard.com/docs/profile-format/proxy/external-proxy/`
|
||
与其无斜杠形式本轮都返回 404,没能取到协议清单。
|
||
|
||
修法:给 `FAMILY_CAPS` 的 surge / surfboard / surgemac 三行都加 `hysteria = false`
|
||
(过滤逻辑第六轮已改成数据驱动,加一个键即可),或把 `hysteria` 与 `wireguard`
|
||
一样无条件丢弃。
|
||
|
||
**→ 实施(第七轮,2.7.2-r8)**:`FAMILY_CAPS.surge` / `FAMILY_CAPS.surgemac` 各加
|
||
`hysteria = false`,整条丢弃。被丢的仍然只有上一代 v1 —— Hysteria 2 三家都支持,
|
||
两者在 `surge_line` 里共用同一个输出分支,靠 `proto` 区分,容易误伤,测试里专门
|
||
断言了「丢 v1、留 v2」。
|
||
|
||
**没有**给 `surfboard` 加这一条:文档 404、证据不足,能力表对它保持沉默
|
||
(`caps[p] == nil` 一律保留)。上面那条「三家都加」的修法**没有**采纳 ——
|
||
第七轮的决策是「Surge + SurgeMac 一起丢」,Surfboard 维持现状。
|
||
`tests/output_formats_test.lua` 里留了一条 `surfboard keeps hysteria v1
|
||
(no evidence to drop)` 钉住这点,防止日后有人「顺手统一」。
|
||
第六轮那条 `finding j pending` 的占位断言已按计划翻转成 `surge drops hysteria v1`。
|
||
|
||
## 7.1 / 7.2 / 7.3 的实测探针
|
||
|
||
**修复前**(`[2.7.2-r2]` 及更早):
|
||
|
||
```
|
||
$ lua5.1 -e '... out.generate({vless节点}, "surge", {name="P"}) ...'
|
||
[Proxy]
|
||
V = vless, 1.2.3.4, 443, username=u, tls=true ← Surge 协议清单里没有 vless
|
||
|
||
$ lua5.1 -e '... out.generate({vmess节点}, "egern", {name="P"}) ...'
|
||
[Proxy]
|
||
M = vmess, 1.2.3.4, 443, username=u, tls=true ← Egern 的配置是 YAML,不是逗号行
|
||
|
||
$ lua5.1 -e '... out.generate({vmess+reality节点}, "qx", {name="P"}) ...'
|
||
[server_local]
|
||
vmess=1.2.3.4:443, method=none, password=u, tls-host=s.example.com,
|
||
tls-verification=true, reality-base64-pubkey=PBK, reality-hex-shortid=SID, tag=R
|
||
```
|
||
|
||
**修复后**(`[2.7.2-r4]`,7.1 / 7.2 / 7.3 均已实施):
|
||
|
||
```
|
||
$ ... out.generate({vless节点}, "surge", {name="P"}) ...
|
||
[Proxy]
|
||
|
||
[Proxy Group]
|
||
P = select, DIRECT
|
||
↑ [Proxy] 段为空:节点被 FAMILY_CAPS 整体丢弃(连成员列表也不引用它)
|
||
|
||
$ ... out.generate({vmess节点}, "egern", {name="P"}) ...
|
||
proxies:
|
||
- vmess:
|
||
name: M
|
||
server: 1.2.3.4
|
||
port: 443
|
||
user_id: u
|
||
security: auto
|
||
policy_groups:
|
||
- select:
|
||
name: P
|
||
policies:
|
||
- M
|
||
↑ 7.2-A:真正的 Egern YAML —— 协议名是映射键、字段 snake_case
|
||
(此前 `[2.7.2-r3]` 及更早输出的是 `M = vmess, 1.2.3.4, 443, username=u, tls=true`
|
||
这样的 Surge 逗号行,Egern 读不了)
|
||
|
||
$ ... out.generate({vless+reality节点}, "egern", {name="P"}) ...
|
||
proxies:
|
||
- vless:
|
||
name: V
|
||
server: 1.2.3.4
|
||
port: 443
|
||
user_id: u
|
||
transport:
|
||
tls:
|
||
sni: s.example.com
|
||
reality:
|
||
public_key: PBK
|
||
short_id: SID
|
||
↑ 7.2-A:Reality 在 transport.<类型>.reality 里,键名是 public_key / short_id
|
||
|
||
$ ... out.generate({wireguard节点}, "egern", {name="P"}) ...
|
||
proxies:
|
||
- wireguard:
|
||
name: W
|
||
server: 1.2.3.4
|
||
port: 51820
|
||
private_key: k
|
||
peer_public_key: k2
|
||
local_ipv4: 10.0.0.2/32
|
||
↑ 7.2-A 顺带补上的能力:Egern 的 WireGuard 有独立协议块
|
||
(此前走 surge_config 的单行 [Proxy],只能整条丢弃)
|
||
|
||
$ ... out.generate({ssr节点}, "egern", {name="P"}) ...
|
||
proxies: []
|
||
policy_groups:
|
||
- select:
|
||
name: P
|
||
policies:
|
||
- DIRECT
|
||
↑ Egern 的协议清单里没有 SSR(也没有 Hysteria v1):整条丢弃
|
||
|
||
$ ... out.generate({vmess+reality节点}, "qx", {name="P"}) ...
|
||
[server_local]
|
||
vmess=1.2.3.4:443, method=none, password=u, obfs=over-tls, obfs-host=s.example.com, reality-base64-pubkey=PBK, reality-hex-shortid=SID, tag=R
|
||
↑ 7.3-C:QX 只认这个形式的 TLS 标志
|
||
(此前写的是 tls-host= + tls-verification=)
|
||
|
||
$ ... out.generate({vmess+reality节点}, "loon", {name="P"}) ...
|
||
[Proxy]
|
||
R = vmess, 1.2.3.4, 443, auto, "u", tls=true, sni=s.example.com, public-key="PBK", short-id=SID
|
||
↑ 7.4-A:位置参数「加密方式, "UUID"」 ↑ 公钥按文档加双引号
|
||
```
|
||
|
||
## 7.3 详细说明(唯一一条会影响本次新功能的)
|
||
|
||
`sample.conf` 的说明原文(`crossutility/Quantumult-X` 仓库,`[server_local]` 前):
|
||
|
||
> …if the corresponding line (socks5: `over-tls=true`, http: `over-tls=true`,
|
||
> trojan: `over-tls=true` or `obfs=wss`, anytls: `over-tls=true`, … vmess:
|
||
> `obfs=over-tls` or `obfs=wss`, vless: `obfs=over-tls` or `obfs=wss`) contains
|
||
> the `reality-base64-pubkey` param, then the standard TLS will be replaced with
|
||
> the Reality.
|
||
|
||
即:QX 里 vmess / vless 的「TLS 标志」写作 `obfs=over-tls`(或 `obfs=wss`),
|
||
trojan / anytls 才写 `over-tls=true`。而 `qx_tls()` 对所有协议统一输出
|
||
`tls-host=` + `tls-verification=true`(探针第三段可见)—— 于是 vmess / vless 的
|
||
`reality-base64-pubkey` **很可能被 QX 忽略**,节点退回普通 TLS。
|
||
|
||
本次**没有**改 `qx_tls()`:它是既有实现,改动会让**所有** QX vmess / vless TLS
|
||
节点的输出形态变化(不只是 Reality 节点),需要单独决策与回归;而且
|
||
`tls-host` / `tls-verification` 在 QX 里是否对 vmess / vless 同样有效**没有找到
|
||
官方依据**(`sample.conf` 里只用 `obfs=` 形式,但「未出现」不等于「无效」)。
|
||
本次只保证:**只要 TLS 标志生效,公钥就已经写在那行上了**(`qx_reality()` 覆盖
|
||
vmess / vless / trojan / anytls 四类,与 `sample.conf` 的 Reality 条目一致)。
|
||
|
||
**处置候选**:A. 维持现状 + 本节记录;B. `qx_tls()` 按协议分叉(vmess / vless 走
|
||
`obfs=over-tls` + `obfs-host`,trojan / anytls 维持现状),需要同步更新
|
||
`parser_surge.parse_qx_line` 的读取端与 `tests/protocol_registry_test.lua`。
|
||
|
||
**已按候选 C 实施**(第二轮):分叉只针对**带 `public-key` 的** vmess / vless ——
|
||
新增 `qx_obfs_reality()`,`net == "ws"` 时写 `obfs=wss` + `obfs-uri` + `obfs-host`,
|
||
否则写 `obfs=over-tls` + `obfs-host`(取 sni)。不带公钥的 vmess / vless 完全不动
|
||
(仍走 `qx_transport` + `qx_tls`),所以既有节点的输出形态不变,风险面只落在
|
||
本次新加的 Reality 功能上。读取端不需要改:`parser_surge.parse_qx_line` 早就
|
||
按 sample.conf 实现了 `obfs=over-tls` / `obfs=wss` 的映射(`obfs-host` 在
|
||
`over-tls` 下落 `sni`、在 `wss` 下落 `host`),往返已在回归测试里断言。
|
||
|
||
## 第一轮修复记录(7.5 / 7.6 / 7.8)
|
||
|
||
**7.5 引号感知切分** —— 新增 `parser_surge.split_fields()`(`parser_surge.lua`),
|
||
`parse_surge_line` 与 `parse_qx_line` 的切分都改走它。规则:
|
||
|
||
* 先数引号个数,**奇数则退回旧的 `gmatch("[^,]+")`**。订阅内容不可信,落单的
|
||
引号若被当成「开引号」,会把后面的 `sni=` / `over-tls=` 全吞进同一个字段,
|
||
比按逗号切更糟。
|
||
* 偶数时按引号开合切分,引号内的逗号不再是分隔符;引号本身保留在字段里,
|
||
由既有的 `unquote()`(Loon 位置参数)或原样(QX kv 值)处理。
|
||
* 无引号的输入与旧实现**逐字符等价**:同样按逗号切、同样丢弃空字段、同样 trim。
|
||
|
||
**7.6 Loon 传输参数** —— `parse_surge_line` 增加 `transport=` / `path=` / `host=`
|
||
三个映射(`transport=http` 按 Loon 文档「会按 WebSocket 处理」落成 `ws`),放在
|
||
`ws=true` / `ws-path` / `ws-headers=Host:` 之后,同一行两种写法都出现时**以新写法为准**。
|
||
|
||
**回归测试**:新增 `tests/parser_surge_fields_test.lua`(36 条断言),覆盖两个
|
||
缺陷的修复点、旧写法回归、无引号输入的逐字符等价、空字段边界、奇数引号边界、
|
||
以及「新写法优先」。文件头写明「修复前应当失败」。
|
||
|
||
**反向验证**:把 `parser_surge.lua` stash 掉后跑该文件,得到 **12 条 FAIL**,症状
|
||
与预测完全一致(`"pa` / `"p` / `"uu` 被截断的凭据、`net=tcp`、`path=nil`、
|
||
`host=nil`、旧写法的 `/old` 与 `old.example.com` 胜出);`git stash pop` 后
|
||
**0 条 FAIL**。全套测试 58 个文件、0 失败。
|
||
|
||
**7.8 删除** —— `root/usr/share/substore/age.lua` 与 `tests/age_test.lua` 已从工作区
|
||
删除。审计复核发现它们并非「写完没人用」的死代码,而是**未完成的半成品**:
|
||
`age.lua` 文件头声称实现了 sha256 / hmac-sha256 / hkdf / chacha20 / poly1305 /
|
||
x25519 / bech32 / armor / STREAM,实际只写到 `M._hkdf_sha256` 就停了,末尾留着
|
||
`-- @@NEXT@@` 续写标记(全仓库仅此一处);`tests/age_test.lua` 引用的
|
||
`tests/age_vectors/README.md` 根本不存在。两者都**未被 git 跟踪**,删除即不可恢复,
|
||
因此删除前已在仓库外留了一份备份(`~/age-lua-wip-backup-2026-10-05/`,含
|
||
`age.lua` 与 `tests/age_test.lua` 两个原文件)。后续若要继续这条线,从该备份恢复即可。
|
||
|
||
## 第二轮修复记录(7.1 / 7.3 / 7.4)
|
||
|
||
全部改动集中在 `root/usr/share/substore/output_formats.lua`(输出侧),
|
||
解析侧**一行未改** —— 三处新写法都早就有对应的读取实现,往返由回归测试锁定。
|
||
|
||
**7.1 `FAMILY_CAPS`(按客户端能力表过滤)** —— 新增一张表,键是输出格式名,
|
||
值是 `{ vless = <bool>, ssr = <bool> }`,只记录各家**不一致**的两个协议
|
||
(其余协议各家都认):
|
||
|
||
| flavor | vless | ssr | 依据 |
|
||
|---|---|---|---|
|
||
| surge / surfboard / surgemac | ✗ | ✗ | `manual.nssurge.com` 的 Proxy Protocols 清单、`getsurfboard.com` 的 external-proxy 清单 |
|
||
| loon | ✓ | ✓ | `nsloon.app/docs/Node/` 有独立的 VLESS 与 ShadowsocksR 两节 |
|
||
| egern | ✓ | ✗ | `egernapp.com/docs/configuration/proxies/` 的协议清单有 Vless、无 ssr |
|
||
|
||
> 第三轮 7.2 实施后 **egern 已移出这张表** —— 它的配置是 YAML,改由
|
||
> `output_egern.lua` 的 `EGERN_KEY` 决定收哪些协议(上表这一行是第二轮当时的
|
||
> 真实状态,保留作记录)。
|
||
|
||
`surge_config(nodes, group_name, flavor)` 改成按这张表丢节点(此前是调用点各传一个
|
||
`supports_ssr` 布尔量 —— 加一个维度就要再加一个参数,调用点一多必然漏传,
|
||
而漏传的默认值是「支持」)。五个调用方(surge / surfboard / surgemac / loon / egern)
|
||
改为传格式名。
|
||
|
||
**7.3-C QX Reality 的 TLS 标志分叉** —— 新增 `qx_obfs_reality()`,**只对带
|
||
`public-key` 的** vmess / vless 生效:`net == "ws"` 写 `obfs=wss` + `obfs-uri` +
|
||
`obfs-host`,否则写 `obfs=over-tls` + `obfs-host`(取 `sni`)。不带公钥的
|
||
vmess / vless 完全不动,所以既有 QX 节点的输出形态不变 —— 风险面只落在本次新加的
|
||
Reality 功能上(这是选 C 而不是 B 的全部理由)。读取端 `parse_qx_line` 早就实现了
|
||
`obfs=over-tls` / `obfs=wss` 的映射,无需改动。
|
||
|
||
**7.4-A Loon 位置参数化 + 公钥加引号** —— 新增 `loon_positional()`,Loon 的
|
||
trojan / vmess / vless 凭据改写成端口之后的带引号位置参数(`Trojan,h,p,"密码"`、
|
||
`VLESS,h,p,"UUID"`、`VMess,h,p,加密方式,"UUID"`),Reality 的 `public-key` 按文档
|
||
加双引号(`short-id` 不加)。Surge / Surfboard / SurgeMac 维持具名写法 ——
|
||
复核确认**它们本来就是对的**(见 7.9 的 (f))。
|
||
|
||
实施中发现并一并修掉的两个**本轮改动自身**会引入的问题:
|
||
|
||
* **Loon 的 VMess 加密方式拼写不同** —— 统一模型的 `chacha20-poly1305` 在 Loon 里
|
||
写作 `chacha20-ietf-poly1305`,模型的 `zero` 是 Xray 专用(Loon 清单里没有)。
|
||
照抄会让这两类节点在 Loon 上加载失败,因此新增 `LOON_VMESS_CIPHER` 映射,
|
||
未列出的取值退回 `auto`。**这是位置参数化必须配套的一步**,不是额外功能。
|
||
* **带引号的位置参数遇到值里的双引号** —— `'"'..v..'"'` 在 `v` 含 `"` 时会产出
|
||
`"pa"ss"`,怎么切没有文档依据。`loon_positional()` 对这种值返回 nil,
|
||
调用方整条丢弃。顺带修掉了**既有**的同类缺陷:anytls 的 Loon 位置参数
|
||
(7.4 之前就带引号)此前没有这层防护。
|
||
|
||
**回归测试**:`tests/anytls_reality_test.lua` 从 110 条扩到 143 条(新增 Loon 位置
|
||
参数与公钥引号断言、Surge 家族整体丢 vless 的断言、QX `obfs=` 分叉与「不带公钥
|
||
维持原样」的对照断言、QX 往返、**Loon 端到端往返**、加密方式映射、双引号边界);
|
||
`tests/protocol_registry_test.lua` 的 `DROPPED` 表补 `vless` 的 surge / surfboard /
|
||
surgemac 与 `ssr` 的 egern;`tests/output_layer_fixes_test.lua` 的 F6 改挂在 Loon 上
|
||
(surge 侧该节点已被整体丢弃,传输层已无从观察)。README / README.en.md 的
|
||
「SSR 只能输出到 Mihomo、Stash、Loon、Egern、Shadowrocket」一句里 Egern 已删
|
||
(它没有 SSR),并补了 VLESS 的同类说明。
|
||
|
||
**反向验证**:把 `output_formats.lua` stash 掉(保留全部新测试)后跑三个受影响的
|
||
文件,得到 **20 条 FAIL**,症状与预测逐条对应:
|
||
|
||
| 文件 | FAIL 数 | 症状 |
|
||
|---|---|---|
|
||
| `tests/anytls_reality_test.lua` | 16 | QX 写的是 `tls-host`/`tls-verification`(无 `obfs=`);Loon 公钥无引号、凭据是具名;surge/surfboard/surgemac 仍输出 vless 节点;双引号未挡 |
|
||
| `tests/output_layer_fixes_test.lua` | 2 | surge 仍保留 vless 节点与成员引用 |
|
||
| `tests/protocol_registry_test.lua` | 2 | `vless`(surge/surfboard/surgemac) 与 `ssr`(egern) 的 `want dropped got true` |
|
||
|
||
`git stash pop` 后 **0 条 FAIL**,全套 57 个文件、0 失败。
|
||
|
||
## 第三轮修复记录(7.2)
|
||
|
||
**7.2-A Egern YAML 生成器** —— 新增 `root/usr/share/substore/output_egern.lua`,
|
||
并删除 `output_formats.to_egern`(连同 `M.generate` 里的 `format == "egern"` 分支)。
|
||
`output.lua` 的分发改为 `output_egern.generate`,下载后缀 `.conf` → `.yaml`
|
||
(`Content-Type` 保持 `text/plain; charset=utf-8`,与同类的 clash / clashmeta /
|
||
stash 三个 YAML 格式一致 —— 单独给 egern 换成 `application/yaml` 只会让同一类内容
|
||
出现两种类型)。
|
||
|
||
**结构**(逐字段对照官方示例 `egernapp.com/docs/configuration/example/` 与协议字段表
|
||
`.../configuration/proxies/`,无推测项):
|
||
|
||
* `proxies:` 是**顶层键**、值是列表,每项是**单键映射**,键名即小写协议名
|
||
(`- shadowsocks:`)—— 不是 Clash 的 `type:` 字段。字段名一律 **snake_case**:
|
||
`user_id` / `peer_public_key` / `preshared_key` / `skip_tls_verify` / `udp_relay` /
|
||
`obfs_password` / `service_name` / `local_ipv4` / `dns_servers` / `udp_relay_mode`。
|
||
* vmess / vless 的传输层是 `transport:` 子映射,键名是传输类型本身:
|
||
`tls` / `ws` / `wss` / `http1` / `http2` / `grpc`。**TLS 也是其中一种**,
|
||
没有顶层 `tls:` 开关 —— 「明文 tcp」就是完全不写 `transport`。这与 Clash 的
|
||
`network: ws` + `ws-opts` 是两套完全不同的写法。
|
||
* Reality 的嵌套位置**按协议分叉**:vmess / vless 在 `transport.<类型>.reality` 里,
|
||
trojan / anytls 是节点**顶层**的 `reality:` 对象。键名是 `public_key` / `short_id`
|
||
(既不是统一模型的 `public-key` / `short-id`,也不是 Clash 的 `reality-opts`)。
|
||
写错键名客户端**不报错**,只是 Reality 静默失效、退回普通 TLS。
|
||
* `policy_groups:` 同为顶层列表,`select` 用 `policies:` 列表;空列表时兜底 `DIRECT`。
|
||
|
||
**协议清单的差异**(`EGERN_KEY`,与 Surge 家族的 `FAMILY_CAPS` 是两回事):
|
||
Egern 有 **VLESS** 与 **WireGuard**,没有 **SSR** 与 **Hysteria v1**。后两者整条丢弃
|
||
(Hysteria v1 的 `obfs` 是普通字符串、没有 `obfs_password`,拿 v2 的键去顶会让客户端
|
||
按错误的协议去连)。`FAMILY_CAPS` 里的 egern 行随之删除。
|
||
|
||
**顺带修正的两处既有行为**(都由 `protocol_registry_test` 的 `DROPPED` 表锁定):
|
||
|
||
| 协议 | 此前 | 现在 | 原因 |
|
||
|---|---|---|---|
|
||
| wireguard | 丢弃(走 Surge 逗号行) | **保留** | Egern 的 WireGuard 有独立协议块,YAML 能完整表达 |
|
||
| hysteria (v1) | 保留(走 Surge 逗号行) | **丢弃** | Egern 的清单里只有 Hysteria2 |
|
||
|
||
**复用而非复制**:YAML 标量转义(引号触发集、控制字符、c-indicator)从
|
||
`output_clash_meta.lua` 导出为 `M.esc_yaml` 共用 —— 另抄一份必然漂移,而
|
||
「未加引号的 `password: %foo` 会让客户端拒绝整份配置」是同一个坑。
|
||
|
||
**未做的一处(不猜)**:vmess 的 `legacy` 只在节点显式带 `legacy` 字段时输出。
|
||
官方文档没有给出「`alterId > 0` ⇒ `legacy: true`」的对应关系,本仓库也没有
|
||
`alterId` 字段,因此不臆测映射,留待有依据时再补。
|
||
|
||
**回归测试**:新增 `tests/output_egern_test.lua`(63 条断言),覆盖顶层结构、
|
||
逐协议的字段名、transport 嵌套(ws/wss/grpc/http2)、Reality 的两种嵌套位置、
|
||
SSR 与 Hysteria v1 的整条丢弃、名字唯一性与重命名、YAML 转义、端口兜底、格式注册。
|
||
`tests/protocol_registry_test.lua` 的 `DROPPED` 表按上表改(删 `wireguard.egern`、
|
||
加 `hysteria.egern`);`tests/anytls_reality_test.lua` 的 egern 断言从「具名密码、
|
||
无 reality」改为 YAML 形态(`password: p` + `reality:` 子对象)。
|
||
|
||
**反向验证**:把 `output.lua` 与 `output_formats.lua` stash 掉(保留全部新测试)后
|
||
跑 `tests/output_egern_test.lua`,得到 **53 条 FAIL**(旧代码走 `to_egern`,输出的是
|
||
逗号行);`git stash pop` 后 **0 条 FAIL**,全套 58 个文件、0 失败。
|
||
|
||
---
|
||
|
||
## 第四轮修复记录(7.7)
|
||
|
||
**7.7-A 后端错误串 msgid 化** —— 后端模块(`root/usr/share/substore/*.lua`)不再
|
||
出现任何中文字面量,全部改为**语言中立的英文 msgid**;翻译只发生在**显示边界**
|
||
(控制器 / 视图)。
|
||
|
||
### 硬约束:后端不能 `require("luci.i18n")`
|
||
|
||
`substore-cron.sh` 会在**独立的 lua 进程**里跑 `core.sync`,那里没有 LuCI 环境
|
||
(没有 `luci.i18n`、没有 `translate()`)。所以后端不能自己翻译,只能返回 msgid。
|
||
这不是风格选择,是运行时的硬约束。
|
||
|
||
### 新增 `root/usr/share/substore/msg.lua`
|
||
|
||
有一类消息是「msgid 前缀 + 动态值」拼出来的,例如 `"Invalid proxy config: " .. reason`。
|
||
整体查表必然落空(值每次都不同),前缀那半句就永远翻译不了。`msg.lua` 用**分隔符**
|
||
把各段拼起来:
|
||
|
||
```lua
|
||
msg.compose("Target actually connects to a private/reserved address (", ip, ")")
|
||
msg.join("Unsupported proxy protocol: ", scheme) -- compose 的两段简写
|
||
msg.compose_list(names, " + ") -- 一串 msgid 用 sep 连接
|
||
msg.translate(s, translate) -- 显示边界按分隔符逐段查表
|
||
```
|
||
|
||
* 分隔符取 `"\1"`(SOH)。它不会出现在任何合法取值里(URL / 主机名 / User-Agent /
|
||
端口 / 协议名在 `http.lua` 里都校验过)。
|
||
* **能安全持久化**:`util.json_encode` 把控制字符转义成 `\u0001`,所以组合消息
|
||
存进 `meta.error` 再读回来仍然完好。
|
||
* **拆分是精确的**:只按**第一个**分隔符切,剩下的递归处理 —— 嵌套(值本身又是
|
||
组合消息)天然成立。全程没有启发式匹配。
|
||
* **幂等**:对已经翻译过的串(不含分隔符)整串查表落空,原样返回 —— 控制器里
|
||
已经 `_()` 过的兜底串再次经过 `tmsg` 也不会被二次处理。
|
||
|
||
### 翻译点(两条例外路径)
|
||
|
||
| 路径 | 载体 | 在哪里翻译 |
|
||
|---|---|---|
|
||
| 失败后跳回列表 / 节点页 | `?err=` | 控制器 `back_to_list` / `back_to_nodes` 里 `tmsg(err)` 后再 `urlencode`(**先把控制字符翻掉再进 URL**) |
|
||
| 更新失败的持久化原因 | `meta.error` | 视图渲染 `it.error` 时 `msg.translate(it.error, luci.i18n.translate)` |
|
||
|
||
`output.generate` 的返回值走 JSON 到视图(`output.htm`),同样在视图侧翻译。
|
||
|
||
### 顺带修掉的一个既有缺陷
|
||
|
||
控制器此前把**已翻译**的串写进持久化的 `meta.error`(`core.save_meta(id, { error = _("…") })`)
|
||
—— 中文管理员触发的失败原因会**泄漏到英文管理员的界面**上。现在存的是 msgid,
|
||
读它的会话按自己的语言翻译。
|
||
|
||
### `util.human_duration` 的写法
|
||
|
||
数字与单位拆成两段(`msg.compose(3, " days")`),单位 msgid **带前导空格** ——
|
||
英文渲染成 `3 days`,中文译文把空格去掉,渲染成 `3天`。这条由**调用**它来断言
|
||
(见下),而不是在源码里认字面量。
|
||
|
||
### 保留的中文字面量(唯一一处)
|
||
|
||
`node.lua:377` 的 `"[^,%s,]+"` —— 这是**正则字符类**,全角逗号是模式的一部分,
|
||
翻译它会直接破坏「香港,日本」这类全角分隔写法的关键词拆分。测试里按**字面量内容**
|
||
(不是行号)列为显式例外,并额外断言「例外必须仍存在于源码中」,防止例外表腐烂成
|
||
「什么都放行」。
|
||
|
||
### 顺带修正的两条用户可见文案
|
||
|
||
扫描时发现两条**已经会到达用户**、但仍是内部诊断口吻的英文串(它们此前漏在
|
||
翻译表之外,中文界面下会显示英文):
|
||
|
||
| 位置 | 此前 | 现在 |
|
||
|---|---|---|
|
||
| `parser.lua:805` | `"bad wireguard conf: no [Peer]"` | `"A WireGuard .conf file has no [Peer] section"` |
|
||
| `parser.lua:904` | `"bad wireguard conf: no usable [Peer] endpoint"` | `"A WireGuard .conf file has no [Peer] with a usable Endpoint"` |
|
||
|
||
(`parse_wireguard_conf` 的返回值由 `M.parse` 里 `if not nodes then return nil, err end`
|
||
原样上抛,与那些被丢弃的逐行诊断不同。其余的 `"bad ss"` / `"no scheme"` 之类只用于
|
||
决定「这一行要不要丢」,原因串被 `parse_lines` 丢掉,**不是**用户可见文案。)
|
||
|
||
同理,`output.lua` / `output_formats.lua` 的 `"unsupported format: " .. tostring(format)`
|
||
改为 `msg.join("Unsupported output format: ", tostring(format))` 并补了译文。
|
||
|
||
### 翻译表
|
||
|
||
`po/zh_Hans/substore.po` 新增 **81 条**(113 处字面量里有重复,去重后加上组合消息的
|
||
各个片段、`FORMAT_LABELS` 的 5 个显示名、以及上面两条改写后的文案)。其中
|
||
`"JSON"` / `"Clash YAML"` / `"WireGuard .conf"` 三条是**恒等译文** —— 它们是格式
|
||
专名,中文界面下也原样显示;写成显式条目是为了让「每个 msgid 都有译文」这条不变式
|
||
保持机械可检,而不是留一个说不清的例外。
|
||
|
||
### 回归测试
|
||
|
||
新增 `tests/backend_i18n_test.lua`(15 条断言),三组:
|
||
|
||
* **A 无 CJK**:去注释后扫全部后端源码的字面量,不得含 CJK(唯一例外见上)。
|
||
* **B 译文齐全**:只认三种**语法位置** —— `return` 语句、`error =` 字段、
|
||
`msg.compose/join/compose_list` 的参数(外加 `FORMAT_LABELS` 的表值)。这三处
|
||
穷举了后端的出口,不靠猜。
|
||
* **B2 / B3**:`util.human_duration` 用**调用**它来断言(比认字面量更接近事实);
|
||
控制器里裸写、会被持久化的 `error = "…"` 单独查(其余控制器文案在源头就
|
||
`_()` 掉了,由 `view_i18n_test` 的 B2 覆盖)。
|
||
|
||
另外两条**反腐烂**断言:`NOT_MESSAGES` 排除表里每一项都必须仍能在源码里找到;
|
||
po 的后端段里不能有**死条目**(每条都得真的被后端用到)。扫描面还有一条
|
||
非空性断言(`checked >= 70`,当前实际约 80),防止正则写错时静默变成假绿。
|
||
|
||
`NOT_MESSAGES` 表**逐条列举**而不是按模式匹配:同处那三种语法位置、但不是文案的
|
||
字面量共 30 条 —— `parser.lua` 的 17 条逐行解析诊断(调用方丢弃原因串)、
|
||
`http.lua` / `probe.lua` 的 5 条 shell 命令行片段、`output_formats.lua` /
|
||
`output_clash_meta.lua` 的 8 条 YAML/INI 模板片段。逐条列的理由是:新增一条就得在
|
||
表里做一次有意识的判断,模式匹配会让新写的文案悄悄漏过去。
|
||
|
||
**同步修改的既有测试(9 个)**:`core_userinfo_test` / `data_integrity_test` /
|
||
`dns_fallback_test` / `list_lock_test` / `network_security_test` / `p1_fixes_test` /
|
||
`p2_batch7_test` / `parser_mixed_format_test` / `wireguard_conf_test` —— 它们此前断言
|
||
的是旧的中文字面量,改为断言 msgid / 组合消息的形态。其中 `p2_batch7_test` 的
|
||
改名规则断言改为对**渲染后**的串断言(`msg.translate(s, function(k) return k end)`),
|
||
因为组合消息在 `"line "` 与数字之间插了分隔符 —— 断言用户真正看到的东西才是对的。
|
||
|
||
**反向验证**:在 `probe.lua` 末尾临时加一条 `return nil, "A brand new untranslated
|
||
message"` 后跑 `tests/backend_i18n_test.lua`,得到 **1 条 FAIL**(`no po entry:
|
||
… A brand new untranslated message`),确认扫描面不是空转;还原后 **15 条全 PASS**。
|
||
|
||
全套 **59 个测试文件、0 失败**。
|
||
|
||
---
|
||
|
||
## 第五轮修复记录(7.9)
|
||
|
||
**7.9 (a)(b)(c)(d) 按官方文档对齐 Loon / Surge 家族的写法**,(e) 保持保守
|
||
(**该保守决策已于第八轮部分修订**,见「第八轮修复记录」)。
|
||
全部改动集中在 `root/usr/share/substore/output_formats.lua` 的 `M.surge_line`
|
||
(生成端)与 `root/usr/share/substore/parser_surge.lua` 的 `parse_surge_line`
|
||
(解析端)—— 两端必须成对,否则导出的配置再导入回来会静默丢字段。
|
||
|
||
### (a) TLS 开关:Loon 用 `over-tls`,Trojan 例外
|
||
|
||
```lua
|
||
local is_loon = (flavor == "loon")
|
||
local tls_flag = is_loon and "over-tls=true" or "tls=true"
|
||
```
|
||
|
||
vmess / vless 的 `if tls then … end` 改用 `tls_flag`。**Trojan 反过来**:Loon 文档
|
||
的明文与 Reality 两条示例都**没有**任何 TLS 键(Trojan 本身建立在 TLS 之上),
|
||
所以 Loon 的 trojan 行整个不写 TLS 参数,Surge 家族的 `tls=true` 保持不动。
|
||
|
||
### (b) UDP 开关:Loon 用 `udp`
|
||
|
||
```lua
|
||
if n.udp then e[#e + 1] = is_loon and "udp=true" or "udp-relay=true" end
|
||
```
|
||
|
||
### (c) Surge 家族补 `encrypt-method`
|
||
|
||
新增 `SURGE_VMESS_CIPHER`(`aes-128-gcm` → `aes-128-gcm`、
|
||
`chacha20-poly1305` → `chacha20-ietf-poly1305`),在**非 Loon** 的 vmess 分支写出。
|
||
映射不到的取值(`zero` / `none` / `auto` / `aes-128-cfb`)**不写该参数**、节点保留:
|
||
Surge 的清单只有那两个取值,写一个客户端读不懂的比不写更糟(写与不写都是
|
||
`aes-128-gcm` 默认值,语义等价)。Loon 侧加密方式本就在位置参数上,不受影响。
|
||
|
||
### (d) Loon 的 ss / ssr / hysteria2 改位置参数
|
||
|
||
```
|
||
Shadowsocks,服务器,端口,加密方式,"密码"
|
||
ShadowsocksR,服务器,端口,加密方式,"密码",protocol=…,obfs=…
|
||
Hysteria2,服务器,端口,"密码"
|
||
```
|
||
|
||
密码走 `loon_positional`(加双引号,值里含双引号则整条丢弃);ssr 的
|
||
`protocol=` / `obfs=` / `obfs-param=` / `protocol-param=` 仍是具名参数。
|
||
|
||
### 解析端同步(**并修掉一个既有缺陷**)
|
||
|
||
`parse_surge_line` 的字段提取补上位置参数:
|
||
|
||
| 协议 | 生成端(Loon) | 解析端 |
|
||
|---|---|---|
|
||
| shadowsocks | `加密方式,"密码"` | `kv.* or pos[1]` / `kv.password or pos[2]` |
|
||
| ssr | 同上 + 具名插件参数 | **新增分支**(此前 ssr 没有任何取字段的分支) |
|
||
| hysteria2 | `"密码"` | `kv.password or pos[1]` |
|
||
|
||
**SSR 此前根本没有取字段的分支** —— 不论哪种写法导入,加密方式、密码、协议插件、
|
||
混淆方式全是 nil,节点带着空凭据进入统一模型,导出时再被静默丢掉。这是本轮顺带
|
||
修掉的既有缺陷,不是 (d) 引入的。`obfs-param` / `protocol-param` 两种拼写都写一份
|
||
(`PROTO_FIELDS` / `DEFAULTS` 用带连字符的规范写法,各输出模块读的是下划线别名,
|
||
与 `parser.lua` 同时写 `method` / `cipher` 同一理由)。
|
||
|
||
### 顺手更正的陈旧注释
|
||
|
||
`surge_line` 里 anytls 分支上方原写着「这是本文件里唯一按 flavor 分叉的协议:
|
||
trojan / vmess / vless 一律用具名参数」—— 7.4-A 之后就不成立了,已改写。
|
||
|
||
### 测试
|
||
|
||
* `tests/ssr_test.lua`:`loon encrypt-method` 一条改为断言位置参数写法,并新增
|
||
**9 条 Loon 输出 → 导入回环**断言。
|
||
* `tests/output_formats_test.lua`:新增 **7.9 专节 24 条**((a) 6 / (b) 3 / (c) 4 /
|
||
(d) 4 / (e) 1 / 回环 6)。**(e) 那 1 条已于第八轮随修订改写**,见「第八轮修复记录」。
|
||
* **反向验证**(逐条把代码改回旧行为,确认测试变红):`tls_flag` 写死 `tls=true`
|
||
→ 3 条 FAIL;UDP 写死 `udp-relay` → 2 条 FAIL;去掉 `encrypt-method` → 2 条 FAIL;
|
||
Loon 的 ss / hysteria2 改回具名 → 2 条 FAIL;解析端 ssr 分支停用 → 5 条 FAIL;
|
||
解析端 hysteria2 / ss 位置参数去掉 → 各 1 条 FAIL。还原后全部 PASS。
|
||
|
||
全套 **59 个测试文件、0 失败**。
|
||
|
||
### 本轮新发现(见上文「第五轮新发现」)
|
||
|
||
(g) Loon 没有 Hysteria v1;(h) `skip-cert-verify=1` 的取值无文档依据;
|
||
(i) 协议名大小写。三条当时只记录、不动代码;(g)(h) 已在第六轮实施,
|
||
(i) 决定保持现状。
|
||
|
||
---
|
||
|
||
## 第六轮修复记录(7.9 的 g / h)
|
||
|
||
第五轮新发现里经决策要实施的两条。(i) 决定保持现状,不动代码。
|
||
|
||
### (g) Loon 丢弃 Hysteria v1
|
||
|
||
```lua
|
||
loon = { vless = true, ssr = true, hysteria = false },
|
||
```
|
||
|
||
判据:Loon 的节点类型清单里只有 **Hysteria2**(`nsloon.app/docs/Node/`)。
|
||
**hysteria 2 是各家通用的**,被丢的只有上一代 v1 —— 两者共用同一个输出分支
|
||
(`elseif proto == "hysteria2" or proto == "hysteria"`),最容易误伤。
|
||
|
||
**顺带把过滤逻辑改成数据驱动**。此前是逐协议写死的:
|
||
|
||
```lua
|
||
elseif p == "ssr" and not caps.ssr then -- 旧
|
||
elseif p == "vless" and not caps.vless then -- 旧
|
||
```
|
||
|
||
```lua
|
||
elseif caps[p] == false then -- 新
|
||
```
|
||
|
||
每加一个维度就要再加一条分支,**漏掉的那条默认是「保留」** —— 这正是本条缺陷的
|
||
成因(`hysteria` 从没进过这张表)。改成查表后,`FAMILY_CAPS` 里标一个 `false`
|
||
即可,不会再有「忘了加分支」这种漏法。`caps[p]` 为 nil(表没表态)一律保留。
|
||
|
||
### (h) `skip-cert-verify` 改写 `true`
|
||
|
||
```lua
|
||
if n["skip-cert-verify"] then e[#e + 1] = "skip-cert-verify=true" end
|
||
```
|
||
|
||
判据:Loon 文档的示例就是 `skip-cert-verify=false`,直接证明 `false` 是合法
|
||
字面量、`true` 同理;而 `1` 在任何一家文档里都找不到(Surge 手册的 TLS 页只写
|
||
「Optional, boolean, default: false」)。解析端两种都认,回环不受影响。
|
||
|
||
**断言面审计**:全仓库只有 `output_formats.lua` 一处写这个串;既有测试里**没有**
|
||
任何一条断言过生成端的 `skip-cert-verify=1` —— `anytls_reality_test.lua:151` 那处
|
||
是把 `=1` 当**输入**喂给解析器,改写后仍然有效。
|
||
|
||
### (i) 协议名大小写:保持现状
|
||
|
||
无证据显示小写会失败(Loon 定位是兼容 Surge 配置,Surge 手册的语法行就是小写),
|
||
改动面却覆盖全部协议。不改。
|
||
|
||
### 测试
|
||
|
||
* `tests/protocol_registry_test.lua`:`hysteria` 的丢弃表加 `loon` 一条。
|
||
* `tests/output_formats_test.lua`:新增 (g) 5 条 / (h) 4 条断言,含回环。
|
||
其中一条如实钉住 **Surge 家族当前仍输出 hysteria v1**(发现 (j),待决策),
|
||
实施 (j) 时要一并翻转。
|
||
* 反向验证:`FAMILY_CAPS.loon.hysteria` 改回不设 / `skip-cert-verify` 改回 `=1`,
|
||
确认测试变红。
|
||
* 全套 **59 个测试文件、0 失败**。
|
||
|
||
### 本轮新发现(见上文「第六轮新发现」)
|
||
|
||
(j) Surge 家族同样没有 Hysteria v1(`manual.nssurge.com/policies/hysteria.html`
|
||
是 404、`hysteria2.html` 存在)。本轮**没有**顺手改 —— (g) 的决策只点名了 Loon。
|
||
**→ 已于第七轮实施。**
|
||
|
||
---
|
||
|
||
## 第七轮修复记录(7.9 的 j)
|
||
|
||
第六轮新发现 (j):Surge 家族的手册协议清单里写的也是 **"Hysteria 2"**,
|
||
`manual.nssurge.com/policies/hysteria.html` 是 404 而同目录的 `hysteria2.html`
|
||
存在 —— 与 Loon 同一情形。本轮按决策把丢弃延伸到 Surge / SurgeMac。
|
||
|
||
### 改动
|
||
|
||
* `root/usr/share/substore/output_formats.lua`
|
||
* `FAMILY_CAPS.surge` / `FAMILY_CAPS.surgemac` 各加 `hysteria = false`。
|
||
**Surfboard 不加** —— 文档 404、证据不足(决策是「Surge + SurgeMac 一起丢」)。
|
||
* `FAMILY_CAPS` 上方的依据注释同步更新:Surge / SurgeMac 的清单补上「也没有
|
||
Hysteria v1」及其探针证据;Surfboard 一段写明「未获证据,故保留」。
|
||
* `surge_config` 的过滤注释补上 `hysteria` 一行,说明它同样按 `FAMILY_CAPS`
|
||
分客户端、Surfboard 因本表沉默而保留。
|
||
* 过滤**逻辑**本身不动:第六轮已改成数据驱动(`elseif caps[p] == false`),
|
||
本轮只是往表里加键 —— 这正是第六轮那次重构要买的东西。
|
||
|
||
### 测试
|
||
|
||
* `tests/protocol_registry_test.lua`:`hysteria` 的丢弃表加 `surge` / `surgemac`
|
||
两条,并注明 Surfboard 不在其中。
|
||
* `tests/output_formats_test.lua`:(j) 新增 5 条断言 —— surge 丢 v1 / 留 v2 /
|
||
组不含 v1、surgemac 丢 v1、**surfboard 留 v1(无证据丢弃)**;第六轮那条
|
||
`surge still keeps hysteria v1 (finding j pending)` 占位断言已删除(被
|
||
`surge drops hysteria v1` 取代)。
|
||
* 反向验证:把 `hysteria = false` 从 surge / surgemac 两行撤掉 → **4 条 FAIL**
|
||
(output_formats 3 条 + registry 1 条),而 `surfboard keeps hysteria v1`
|
||
那条**始终绿**(本轮没碰 Surfboard,它本就不该受影响)→ 说明断言确实指向
|
||
本轮改动,且没有误伤范围外的行为。还原后全绿。
|
||
* 全套 **59 个测试文件、0 失败**。
|
||
|
||
### 本轮新发现
|
||
|
||
无。这是 7.9 决策链的最后一条;实施完 (j) 后 7.9 的 (a)–(e)、(g)–(j) 全部结清
|
||
((f) 复核确认本来就对,(i) 决定保持现状)。
|
||
|
||
**(e) 的例外**:(e) 当时按「保持保守」结清,但第八轮复核发现该保守决策建立在一个
|
||
**错误的注释**上(详见「第八轮修复记录」),于是对**位置参数那一半**做了修订 ——
|
||
具名参数仍保持保守。这是 7.9 唯一一处「结清后又修订」的条目。
|
||
|
||
---
|
||
|
||
## 第八轮修复记录(7.9 (e) 的修订)
|
||
|
||
**7.9 (e) 的保守决策被部分推翻:Loon 引号包裹的位置参数放行含英文逗号的值。**
|
||
|
||
### 起因
|
||
|
||
第五轮把 (e) 判为「保持保守:位置参数或具名值里含英文逗号就整条丢弃」,
|
||
并在 `surge_line` 里写下一段理由,其中一句是错的:
|
||
|
||
> Loon 的那对引号只是标记,值里的逗号照样是分隔符。
|
||
|
||
Loon 官方文档(`nsloon.app/docs/Node/`)原文是「**参数值中含有英文逗号时,请使用
|
||
双引号包裹**」—— 这句话只有在**客户端解析是引号感知**的前提下才成立,也就是说那对
|
||
双引号**确实**能保住值里的逗号。原注释把「引号」当成了纯粹的装饰,据此推出的
|
||
「引号救不了逗号」不成立,整条丢弃的代价(极少见的「密码里带逗号」的节点在 Loon
|
||
上被丢弃)也就失去了依据。
|
||
|
||
### 精确范围(用户确认)
|
||
|
||
只改 **Loon 的引号包裹位置参数**这一种情形,其余一律不动:
|
||
|
||
| 情形 | 第五轮 | 第八轮 |
|
||
|---|---|---|
|
||
| Loon 位置参数,值被 `"…"` 完整包裹 | 丢弃 | **放行** |
|
||
| Loon 位置参数,裸值(无引号,如加密方式) | 丢弃 | 丢弃(无引号语义) |
|
||
| 具名参数(Surge 家族 + Loon 的 `sni=` / `ws-path=` 等) | 丢弃 | 丢弃(Surge 引号语义未获文档证据,且解析端不对具名值 unquote,加引号会断回环) |
|
||
| Surfboard 的裸位置参数(anytls) | 丢弃 | 丢弃(无引号语义) |
|
||
|
||
判据**不是** `is_loon` 而是「这个值是不是引号包裹的」(`v:match('^".*"$')`):
|
||
`loon_positional` 只在 Loon 分支被调用,Surfboard 的 anytls 走裸位置参数,
|
||
两者由这个判据自动分开,不必再分叉一次。
|
||
|
||
### 安全性依据
|
||
|
||
放行不会让生成端产出解析端读不动的行,因为 `loon_positional` 有两条保证:
|
||
|
||
1. 它**无条件**把 Loon 的位置凭据包成 `"…"`,所以引号是 Loon 语法的一部分;
|
||
2. 值里含 `"` 时它直接返回 `nil`(整条丢弃,走不到这一行)—— 于是引号包裹的值
|
||
内部不可能出现落单引号,`parser_surge.split_fields` 的「奇数引号回退」分支
|
||
不会被本行的输出触发。
|
||
|
||
### 改动
|
||
|
||
* `root/usr/share/substore/output_formats.lua`:`surge_line` 的位置参数检查
|
||
`if v:find(",", 1, true) then return nil end` 改为
|
||
`if v:find(",", 1, true) and not v:match('^".*"$') then return nil end`;
|
||
上方注释整段重写(说明引号语义、判据选择与安全性依据)。
|
||
* 具名参数检查(上一段 `for _, kv in ipairs(e)`)**原样不动**。
|
||
|
||
### 测试
|
||
|
||
* `tests/output_formats_test.lua`:第五轮那条 `loon comma password drops node`
|
||
改为 `loon comma password kept (quoted positional)`,并新增回环断言
|
||
`loon rt comma password` / `loon rt comma method`;同时保留
|
||
`surge comma password drops node` 作为反向护栏。
|
||
* `tests/anytls_reality_test.lua`:新增 `surge_line comma in quoted positional
|
||
password kept`、`loon comma password round-trips`、`surge_line comma in loon
|
||
vless uuid kept`;保留 `surge_line comma in named password -> nil` 与
|
||
`surge_line comma in bare positional password -> nil` 两条护栏。
|
||
* 断言一律先 `local line = fmts.surge_line(...)` 再判 `type(line) == "string"`:
|
||
直接在 `nil` 上调 `:find` 会抛错、把同文件后续断言全吞掉,反向验证就只能看到
|
||
「崩了」而看不到是哪条行为变了。
|
||
* **反向验证**:把谓词改回 `if v:find(",", 1, true) then`(撤掉放行)→ **6 条 FAIL**
|
||
全部指向本轮改动(output_formats 3 条 + anytls 3 条),4 条护栏断言
|
||
(`surge comma password drops node`、具名密码、裸位置密码、具名 uuid)**始终绿**
|
||
→ 说明断言确实指向改动,且没有误伤范围外的行为。还原后全绿。
|
||
* 全套 **59 个测试文件、0 失败**。
|
||
|
||
---
|
||
|
||
# 附:P2 修复范围(**历史范围定义,已结清**)
|
||
|
||
> **本节不是待办清单**,是 P2 **开工时**写下的范围定义(`2d29250`,即 P2 批次一
|
||
> 那次提交),原文用的是「**例如** … 等」,本身即带示例性质。P2 已由批次一~批次七
|
||
> 全部实施完毕(见文件开头的进度表)。保留本节是为了说明**当时**划定的边界;
|
||
> 若日后有人照着它去「补修 P2」,那是在修一份已经修完的清单。
|
||
|
||
P2 为审计表中**其余中危 / 低危**项中性质明确、无需另行决策的缺陷,例如
|
||
M6 / M9 / M13 / M14 / M16 / M17 / M18 / M19 / M23 / M24 / M25 / M26 / M27 / M30
|
||
与 L1 / L3~L6 / L8~L26 等。逐项修复并补回归测试,完成后在 CHANGELOG 中记录。
|
||
本文件所列各项**不在** P2 范围内。
|
||
|
||
## 逐条落点(38 项,35 项有据)
|
||
|
||
| 批次 | 版本 | 覆盖的 ID |
|
||
|---|---|---|
|
||
| 批次一 | `[2.6.3-r1]` | M13 / M14 / L13 |
|
||
| 批次二 | `[2.6.4-r1]` | M9 / L16 / L17 / L18 / L20 |
|
||
| 批次三 | `[2.6.5-r1]` | M19 / M23 / M24 / M25 / M27 / L21 / L22 |
|
||
| 批次四 | `[2.6.6-r1]` | M16 / M17 / M18 / M30 |
|
||
| 批次五 | `[2.6.7-r1]` | L3 / L4 |
|
||
| 批次六 | `[2.6.8-r1]` | L10 / L11 / L12 / L14 |
|
||
| 批次七 | `[2.6.9-r1]` | L1 / L5 / L6 / L8 / L9 / L23 / L24 / L25 / L26 |
|
||
| P0 批次四 | `[2.6.0-r5]` | M26(P2 开工前即已修复,此处仅备查) |
|
||
|
||
每一项的内容见对应版本的 CHANGELOG;本表只做 ID → 落点的索引,不重复描述。
|
||
|
||
## 三项查无实据:M6 / L15 / L19
|
||
|
||
这三项落在上面的范围区间内,但仓库里**没有任何记录** —— 既无 CHANGELOG 落点,
|
||
也无缺陷描述。原因见文件开头的「编写纪律」:**原始 70 项审计表(高危 14 /
|
||
中危 30 / 低危 26)从未落盘**。其中 M6 只在上面那一行范围定义里被提及过一次;
|
||
**L15 / L19 连提及都没有**,它们是被 `L8~L26` 这个区间顺带覆盖进去的。
|
||
|
||
**处置:不追补。** 编号本身不指向任何模块或文件,无从复核当时指的是什么。按
|
||
「不猜」原则,本节不为它们补写描述,也不假装它们已被评估过 —— 只如实记下
|
||
「无据可查」这一事实。若日后拿到原始审计表,再按当时的口径单独决策。
|
||
|
||
**两个区间外的编号**,一并备查:**L2** 是 `converter.lua` / `node_converter.lua`
|
||
死代码(见上文 1.4),已在 P4 删除(`[2.6.11-r1]`),本就不在 P2 区间内;
|
||
**L7** 同样查无记录,且不在本区间内。
|