feat: 合并边缘节点运维并完善 Surge 配置安全
迁入通用节点部署工具、离线交付页生成器和共用技能,隔离真实配置与交付产物。统一 Codex 与 Claude Code 开发约定,补充节点迁移与运维文档。 完善 FINAL、策略组引用、输入校验、Surge 字段编码与 AI DNS 例外保护,增加隔离测试和订阅验证工具。 验证:全量类型检查、44 项单测、前端构建、脚本语法、技能入口与凭据隔离检查均通过。
This commit is contained in:
224
docs/SURGE-AUDIT-2026-10-03.md
Normal file
224
docs/SURGE-AUDIT-2026-10-03.md
Normal file
@@ -0,0 +1,224 @@
|
||||
# Surge macOS 订阅体检:2026-10-03
|
||||
|
||||
本文保留修复前的体检结论;其中行号指体检时的源码与订阅快照。已实施的代码/生产修复和后续验证见 [修复记录](SURGE-FIX-2026-10-03.md)。
|
||||
|
||||
实际订阅通过 Surge 6.9.1 的原生语法校验,生产数据与当前生成器一致。但存在规则被遮蔽、DNS 与连接策略不一致,以及管理 API 能接受最终导致无效配置或丢失规则的数据操作。
|
||||
|
||||
本次只查询生产管理 API、盘点数据库行数、读取 HTTPS 订阅和公开规则集;没有修改生产节点、规则、设置,没有部署、重启或切换本机活动配置。订阅 GET 按现有业务逻辑正常更新访问计数和访问时间(`server/src/routes/sub.ts:30`)。所有 API 写入复现都使用 `/private/tmp/` 下的隔离数据库与占位凭据。
|
||||
|
||||
## 范围与证据
|
||||
|
||||
| 项目 | 实际结果 |
|
||||
|---|---|
|
||||
| 生产节点 | 30 个,均启用;Surge 输出 22 个,排除 8 个 SS over WS |
|
||||
| 生产策略组 | 14 个,均启用;无循环引用;地区组有效候选为美国 10、新加坡 6、日本 6 |
|
||||
| 生产规则 | 116 条,其中 115 条启用;旧 `PROTOCOL,DOH,AI` 已停用 |
|
||||
| 最终订阅规则 | 118 条:115 条数据库规则 + 3 条生成的 DNS 出口规则 |
|
||||
| 生产规则集 | 数据库登记 62 个;当前配置引用 60 个不同远程 URL |
|
||||
| 生产订阅 | 1 个启用订阅,节点范围为空数组,即全部启用节点 |
|
||||
| 原生语法校验 | Surge Mac 6.9.1(12290),`surge-cli --check` 返回 `OK`、退出码 0 |
|
||||
| 代码与生产一致性 | 用生产 API 快照重建 IR,由本地实际生成器输出;与 HTTPS 订阅逐字节相同 |
|
||||
| 远程依赖 | 60/60 URL 本次返回 HTTP 200,合计 166,028 个非空、非注释条目,无 FINAL 条目 |
|
||||
| 使用本次规则集快照的校验 | 将远程 URL 替换为临时本地文件,再用官方 CLI 校验:`OK` |
|
||||
|
||||
配置指纹、规则集指纹和复现结果见 [脱敏证据](SURGE-AUDIT-2026-10-03.evidence.json)。报告中的 `seq` 是数据库从 0 起算的顺序;“配置第 N 行”对应本次实际 HTTPS 响应。报告和证据不包含订阅 token、真实节点密码、UUID、节点地址或私有 Host IP。
|
||||
|
||||
校验能力以 [Surge 官方 CLI 文档](https://manual.nssurge.com/tools/cli.html) 为依据。CLI 返回 OK 证明配置可解析,不能证明每个规则都会被命中,也不能代替节点连接、解锁或泄漏测试。
|
||||
|
||||
P1 表示可导致配置不可用、规则丢失或重要控制失效;P2 表示行为偏差或需要修正的兼容性问题。静态推导的网络影响和依赖设备开关的风险明确另行说明。
|
||||
|
||||
## 已确认的业务代码缺陷
|
||||
|
||||
### C1 · P1:重置规则在存在自定义规则时失败,并先删除模板规则
|
||||
|
||||
`server/src/routes/rules.ts:60` 调用 `seedRulesFromTemplate(db, { keepCustom: true })`。`server/src/db/seed.ts:52` 读取自定义规则后只删除模板规则,随后在第 63 行把仍在表中的自定义规则再次 INSERT。自定义规则主键重复,且这些操作没有事务包裹。
|
||||
|
||||
隔离数据库复现结果:
|
||||
|
||||
```text
|
||||
首次初始化:89 条模板规则
|
||||
新增自定义规则:HTTP 201,合计 90 条
|
||||
POST /api/rules/reset:HTTP 500
|
||||
错误:UNIQUE constraint failed: rules.id
|
||||
操作后:模板规则 0 条,自定义规则 1 条
|
||||
```
|
||||
|
||||
生产快照中存在自定义规则,具备触发条件。**本次没有在生产调用 reset**,没有认定生产发生过此故障。
|
||||
|
||||
修复建议:把整个重置过程放入事务;保留自定义规则时更新其顺序,或在事务内明确删除后重插,避免重复主键;同时明确“重置”究竟是否保留自定义规则。现有 update-db 技能描述为清掉自定义规则,与当前代码的 `keepCustom: true` 意图不一致,需要一起纠正。
|
||||
|
||||
### C2 · P1:FINAL 并未得到完整保护,普通操作可使全部订阅无效
|
||||
|
||||
`server/src/services/ir.ts:102` 先过滤 `enabled=true`;后面的 FINAL 特判只处理策略引用失效,不能保住已被停用、删除或改成其他类型的 FINAL。`server/src/routes/rules.ts:83` 和第 92 行允许停用及删除唯一 FINAL,排序接口也没有校验末尾位置或完整 ids。
|
||||
|
||||
隔离 API 复现:创建 FINAL 返回 201,停用返回 200,生成器输出中 FINAL 数量变为 0;官方 CLI 返回:
|
||||
|
||||
```text
|
||||
Rules must end with FINAL
|
||||
```
|
||||
|
||||
当前生产恰有一条启用的 FINAL,位于末尾,因此当前配置通过校验。问题在后续编辑入口。官方要求规则以启用的 FINAL 结束;`dns-failed` 只是 DNS 失败时选用其策略,不会补回缺失的 FINAL。[FINAL 规范](https://manual.nssurge.com/rules/final.html)
|
||||
|
||||
修复建议:在写入时保护唯一末尾 FINAL,完整校验排序,拒绝产生无效状态的修改;生成侧再验证,不要仅靠注释声称“永远保留”。
|
||||
|
||||
### C3 · P1:输入与序列化允许合法 API 请求生成无效 Surge 配置
|
||||
|
||||
`shared/src/schemas.ts:6` 对节点名只校验长度;`server/src/services/generators/surge.ts:51` 直接拼接声明名。规则值在第 117 行直接拼接;`quoteName()` 只为策略引用加引号,没有完整处理引号/反斜线。
|
||||
|
||||
实际路由的隔离复现:
|
||||
|
||||
| 请求 | API 结果 | 官方 CLI 结果 |
|
||||
|---|---|---|
|
||||
| 新建名为 `Bad = Name` 的 VMess 节点 | 201 | `Unknown proxy type: Name = vmess` |
|
||||
| 新建 URL-REGEX,值为 `^https://example.com/(a\|b),?c` | 201 | 把 `?c` 误当策略名,拒绝配置 |
|
||||
|
||||
表中正则的 `\|` 仅为 Markdown 表格转义;实际测试值使用普通 `|`。
|
||||
|
||||
Surge 对含逗号的正则值要求引用;新版本还有明确的引号、反斜线转义规则。这是已用官方解析器复现的错误,不是按自建解析器猜测。[HTTP 规则](https://manual.nssurge.com/rules/http.html)、[配置引用格式](https://manual.nssurge.com/profile/format.html)
|
||||
|
||||
修复建议:区分声明名、策略引用、普通规则值、逻辑规则表达式的编码规则;拒绝不能安全表达的名称和换行输入。逻辑规则内部的逗号不能简单按普通字符串一律引用。
|
||||
|
||||
### C4 · P1:策略组循环可通过 API 写入,导致整个配置被拒绝
|
||||
|
||||
`server/src/routes/groups.ts:21`、第 38 行和第 77 行没有图循环检查。隔离测试建立 `A → B → A`,两次新建均返回 201,IR 保留这些引用;官方 CLI 拒绝:
|
||||
|
||||
```text
|
||||
Loop reference is found in the policy group: A.
|
||||
```
|
||||
|
||||
当前生产 14 个组没有循环。修复建议:新建、修改成员与改名时,在事务内校验引用存在、类型正确及图无环;不能把存在性检查当成循环检查。
|
||||
|
||||
### C5 · P2:filterRegex 分支丢弃显式成员,只保留第一个来源组
|
||||
|
||||
`server/src/services/generators/surge.ts:85` 在存在正则时不输出 `memberNames`,非 includeAllNodes 情形也只取第一个 group 成员。
|
||||
|
||||
占位 IR 复现:输入组同时有显式 `DIRECT`、`includeAllNodes=true` 和正则,输出只有动态来源与正则,DIRECT 被丢弃。官方规定正则只作用于动态导入的成员,显式成员不受过滤;`include-other-group` 可包含多个来源组。[策略成员导入](https://manual.nssurge.com/policy-groups/policy-including.html)
|
||||
|
||||
当前生产的三个地区组没有显式成员,暂不触发。修复建议:完整保留显式成员和多个动态来源,分别应用 Surge 规定的语义。
|
||||
|
||||
### C6 · P2:VMess 参数映射不完整
|
||||
|
||||
`server/src/services/generators/surge.ts:13` 没有输出 VMess 的 `skipCertVerify`,忽略 `security`,并无条件输出 `vmess-aead=true`。占位测试确认 `skipCertVerify=true` 不出现在结果中;`alterId=64` 也仍输出 AEAD。
|
||||
|
||||
官方要求 AEAD 设置与服务端匹配,并支持 TLS 证书校验参数及指定的加密方式。当前生产 VMess 均为 `alterId=0`、`security=auto`、`skipCertVerify=false`,没有证据表明现有节点因此连接失败。[VMess 规范](https://manual.nssurge.com/policies/vmess.html)、[TLS 参数](https://manual.nssurge.com/policies/tls.html)
|
||||
|
||||
修复建议:在能力层拒绝无法表达的旧协议组合,或按官方参数转换;不能让用户输入被静默忽略。所有输入字段都应有明确的支持/转换/拒绝行为。
|
||||
|
||||
## 当前生产配置中的行为问题
|
||||
|
||||
### D1 · P1:两条例外规则被更早的拦截集覆盖
|
||||
|
||||
本次下载的 `https://ruleset.skk.moe/List/non_ip/reject.conf` 含有对 `amplitude.com` 的 DOMAIN-SUFFIX,以及对 `appsflyer` 的 DOMAIN-KEYWORD。
|
||||
|
||||
| 域名 | 更早匹配 | 后面的例外规则 |
|
||||
|---|---|---|
|
||||
| `api.amplitude.com` | seq 11,REJECT | seq 94,Automatic |
|
||||
| `inapps.appsflyer.com` | seq 11,REJECT | seq 66,Bybit |
|
||||
|
||||
现有例外不会覆盖早先的匹配。结论针对 Rule-Based 模式及本次规则集快照中的域名匹配;生产规则没有 pre-matching 标记,客户端资源可能存在缓存差异,未宣称已观察到设备上的请求失败。官方规则顺序是首条匹配决定策略。[规则顺序](https://manual.nssurge.com/rules/overview.html)
|
||||
|
||||
修复建议:先确定这两项是需要放行的业务例外,还是需要保持阻断的跟踪服务,再决定是否提前。不能自动为全部跟踪域名放行。
|
||||
|
||||
### D2 · P2:YouTube UDP 拦截被提前的 YouTube 规则集遮蔽
|
||||
|
||||
数据库 seq 37/配置第 135 行的 YouTube 规则集含 `DOMAIN-SUFFIX,googlevideo.com`,策略为 Streaming;seq 110/配置第 208 行才是:
|
||||
|
||||
```ini
|
||||
AND,((PROTOCOL,UDP), (DOMAIN-SUFFIX,googlevideo.com)),REJECT-NO-DROP
|
||||
```
|
||||
|
||||
对携带 googlevideo.com 主机名的普通 UDP 请求,先前的 DOMAIN-SUFFIX 已匹配,后面的 AND 不会生效。STUN 请求另有更前的拦截,所以不能由此认定 STUN 保护失效。逻辑表达式本身通过官方语法校验。[逻辑规则](https://manual.nssurge.com/rules/logical.html)、[规则顺序](https://manual.nssurge.com/rules/overview.html)
|
||||
|
||||
修复建议:若确实要让这类视频流量回落 TCP,把特定 UDP 规则放到相关服务域名规则之前;不要无依据地扩大为全局 UDP 拦截。
|
||||
|
||||
### D3 · P2:AI DNS 映射忽略更早的连接例外,且 googleapis 覆盖整个服务族
|
||||
|
||||
`server/src/services/generators/surge-ai-dns.ts:13` 收集所有指向 AI 的规则,并把结果整体放在数据库 Host 原文之前,不计算其他策略的更早例外。
|
||||
|
||||
本次可确认的例子:
|
||||
|
||||
```text
|
||||
连接策略:seq 8 的 Unbreak 集匹配 update.googleapis.com → DIRECT
|
||||
AI 域名规则:seq 30,DOMAIN-SUFFIX,googleapis.com → AI
|
||||
Host 第 224 行:*.googleapis.com → Cloudflare DoH
|
||||
Host 第 253 行:update.googleapis.com → 原来的国内 DNS
|
||||
```
|
||||
|
||||
Host 首条匹配生效,所以后面的精确 DNS 设置被前面的通配覆盖;连接仍明确直连,而 Surge 自己解析该域名时按 AI Host 项处理。`youtube.googleapis.com` 也被 seq 30 送到 AI,而不会使用后面的 YouTube Streaming 策略。后一项是否符合业务意图需要确认。
|
||||
|
||||
官方允许 Host 绑定规则集,当前语法没有问题;问题是两个独立的有序匹配系统没有同样的例外优先级。[Host 映射](https://manual.nssurge.com/dns/local-dns-mapping.html)
|
||||
|
||||
修复建议:明确 Google AI 与其他 Google API 的范围,并在生成 Host 时保留需要优先的精确例外。对于连接规则中的 IP、进程或复杂表达式,不能承诺 Host 能完全复刻。
|
||||
|
||||
### D4 · P2:靠前的 IP 拦截在业务域名规则前触发 DNS
|
||||
|
||||
seq 12/配置第 110 行加载 `https://ruleset.skk.moe/List/ip/reject.conf`,位于 AI、YouTube、Bybit 等规则之前。本次文件含 778 条 IP/ASN 条目,其中 736 条没有 `no-resolve`。
|
||||
|
||||
按官方执行语义,没有在更前规则被决定、且目标还是域名的请求会在这些 IP 条目处触发解析;DNS 失败时转到 FINAL 的策略,不继续匹配后面的业务域名规则。这是规范推导的控制流影响,未测量延迟或宣称已抓到泄漏。[IP 与 DNS 规则](https://manual.nssurge.com/rules/ip.html)
|
||||
|
||||
修复建议:先确定 IP 黑名单是否应当主动解析所有域名;可考虑在该 RULE-SET 上加 `no-resolve`,或调整域名与 IP 规则顺序。前者会改变对未解析域名的 IP 拦截覆盖,不能当成无代价优化。
|
||||
|
||||
### D5 · P2:三个地区测速地址在 Surge 中实际未生效
|
||||
|
||||
数据库三个 url-test 组的 `testUrl` 为 Cloudflare 的 HTTP 204 地址,配置第 83–85 行输出为组参数 `url=`。全局 `proxy-test-url` 却为 Apple 的地址,节点没有 `test-url`。
|
||||
|
||||
当前官方规范明确组上的旧 `url=` 无效,测速以节点 `test-url` 或全局地址为准。因此 UI 中组级测速地址与 Surge 实际测试依据不一致。[自动测速组](https://manual.nssurge.com/policy-groups/url-test.html)
|
||||
|
||||
修复建议:把可用的设置模型改为官方支持的节点/全局测试参数;不要仅在组编辑界面保留一个看似可用但不会生效的字段。
|
||||
|
||||
### D6 · P1(取决于设备可达性):控制接口使用全接口监听和模板默认口令
|
||||
|
||||
生产 General 同时配置 Remote Controller 和 HTTP API,均绑定 `0.0.0.0`,认证值与公开模板一致,并开启 Web Dashboard。模板本身也在仓库中,因此这些认证值不能视为私密、独立的设备凭据。订阅生成器原样透传 General。
|
||||
|
||||
这表示网络可达的控制接口存在风险;没有检查设备防火墙或公网端口,**不认定当前已暴露互联网**。官方说明这两个入口具有控制 Surge 的能力,HTTPS 不能弥补已知认证值的问题。[控制接口规范](https://manual.nssurge.com/profile/general.html)
|
||||
|
||||
修复建议:无远程控制需求时移除;本机需求绑定回环;有局域网控制需求时使用独立凭据与访问限制,避免所有订阅设备共享模板口令。
|
||||
|
||||
### D7 · P2(取决于设备开关):MITM 关闭服务端证书验证
|
||||
|
||||
实际订阅有 `skip-server-cert-verify=true` 和 5 个 MITM hostname,但没有 CA 内容或 Keystore 引用。官方说明此项会在 MITM 时关闭真实目标服务器的证书验证;HTTPS 重写需要 MITM 开启和受信任证书。[MITM 规范](https://manual.nssurge.com/http/mitm.html)
|
||||
|
||||
未检查设备是否关联其他包含 CA 的本地配置、模块或证书设置,因此不能宣称现在的 HTTPS 重写有效,也不能宣称已经发生 MITM 安全事件。修复建议:若需要 MITM,保留上游验证并在设备侧核实证书与开关;若仅需要代理分流,评估是否应移除不需要的 MITM 与重写配置。
|
||||
|
||||
## 需要明确业务意图的配置
|
||||
|
||||
这些项目有官方支持,不直接判为错误:
|
||||
|
||||
| 项目 | 当前实际行为 | 需要明确的意图 |
|
||||
|---|---|---|
|
||||
| `⚡️ Automatic` | 实际类型是 select,默认第一项 Singapore,不跨地区自动选最快 | 名称是否应改,或者确实需要跨地区自动选择 |
|
||||
| `🎯 NoAuto` | 首项 Mainland → DIRECT;FINAL 指向它并带 dns-failed | 新设备/无保存选择时,未知流量与 DNS 失败是否应默认直连 |
|
||||
| `🪙 Bybit` | 首项 DIRECT | 是否希望交易业务默认直连,还是应固定所选出口 |
|
||||
| `📋 All Nodes` 与地区组 | Direct 和 Warp 节点混在一起,地区 url-test 可切换出口 | AI/交易业务是否需要限制出口类型及切换范围 |
|
||||
| FFSB 的进程直连 | App Bundle 写法有效,但规则在所有服务/通用集合之后 | 是只对未被前面规则匹配的请求直连,还是要整个 App 强制直连 |
|
||||
| `DOMAIN-KEYWORD,byb` | 对任何包含该短字符串的域名匹配 | 是否可以缩小为已知域名,降低误收范围 |
|
||||
| SMTP 相关直连 | `DOMAIN-SUFFIX,smtp` 匹配域名后缀;`URL-REGEX,(Subject\|HELO\|SMTP)` 检查 HTTP URL | 是否原本想匹配邮件协议;这两条不会识别邮件正文或 SMTP 命令 |
|
||||
|
||||
select 的首项是未选过或保存的选择已失效时的默认;不能据此断言用户当前选中的出口。[手动选择组](https://manual.nssurge.com/policy-groups/select.html)
|
||||
|
||||
`PROCESS-NAME,/Applications/FFSB.app/` 的首尾斜线属于官方 App Bundle 前缀匹配模式,不应误报为非法目录规则。[进程规则](https://manual.nssurge.com/rules/process.html)
|
||||
|
||||
URL-REGEX 的作用对象是经过 HTTP 引擎处理的 URL,HTTPS 还取决于 MITM;正则中出现 SMTP/HELO 不会使它变成邮件协议匹配规则。[HTTP 规则范围](https://manual.nssurge.com/rules/http.html)
|
||||
|
||||
## 已核实正常与验证边界
|
||||
|
||||
- HTTPS 订阅的 `#!MANAGED-CONFIG` self URL 与实际请求完全一致;没有回落到内网 URL。当前 `interval=86400, strict=true` 受官方支持,但更新间隔不是推送保证,strict 也意味着更新失败时的可用性取舍。[托管配置](https://manual.nssurge.com/profile/managed-profile.html)
|
||||
- 生产三个地区正则能收纳当前 22 个可用节点,组引用无环,没有悬空规则策略,FINAL 位于末尾。
|
||||
- 8 个 SS over WS 排除符合 Surge 的能力范围;不要为了使节点数相同强行输出。[Shadowsocks](https://manual.nssurge.com/policies/shadowsocks.html)
|
||||
- Hysteria2 的 `salamander-password` 受支持,但官方注明需要 Mac 6.4.3+;实际检查使用 6.9.1。Host 规则集映射需要 Mac 5.10.0+。[Hysteria2](https://manual.nssurge.com/policies/hysteria2.html)
|
||||
- 60 个外部资源本次均可下载。`global_plus.conf` 只有注释,没有有效条目;这是零覆盖,不是失效 HTTP 响应。资源可动态更新,设备缓存内容可能不同。
|
||||
- 官方允许规则集中的非法行被跳过并记录警告。因此本次快照检查和 CLI 返回 OK,不能证明全部 166,028 个条目均已被客户端接受;逐条加载警告仍需设备侧检查。[规则集规范](https://manual.nssurge.com/rules/ruleset.html)
|
||||
- 现有 STUN 拦截的类型、关键字、顺序可用。生产没有全局 PROTOCOL,UDP,REJECT;`udp-policy-not-supported-behaviour=REJECT` 仅是匹配到不支持 UDP 的策略后的回落,不是全局禁用 UDP。[协议规则](https://manual.nssurge.com/rules/protocol-and-network.html)
|
||||
- Host 的 `server:` 映射只管 Surge 自己的 DNS 客户端,不会因 `use-local-host-item-for-proxy=true` 而改变远端代理的解析器;该开关只作用于 IP 映射。[Host 代理行为](https://manual.nssurge.com/dns/local-dns-mapping.html)
|
||||
- 当前四个站点的节点域名都有显式 Host IP 映射。没有因此假设永远不会出现 DNS 引导回退;仍需核对所选节点与设备日志。[加密 DNS 引导与回退](https://manual.nssurge.com/dns/encrypted-dns.html)
|
||||
- 没有切换本机配置、修改保存的策略选择、发起节点测速/带宽测试、导入 MITM CA 或抓取设备业务流量。
|
||||
|
||||
## 建议处理顺序
|
||||
|
||||
1. 修复 C1–C4:事务、FINAL、字段编码与循环校验,防止普通管理操作毁掉订阅。
|
||||
2. 明确业务例外后修正规则顺序:D1、D2;同时处理 D3 的 DNS 例外、D4 的主动 DNS 与 FINAL 失败策略。
|
||||
3. 收紧实际需要的控制入口与 MITM 参数:D6、D7。
|
||||
4. 对齐测速与节点映射:D5、C5、C6,再明确跨地区/出口选择策略。
|
||||
5. 对修复后的配置重复官方校验,然后在保留用户选择的前提下进行设备侧请求匹配、DNS、节点连接和业务回归。
|
||||
|
||||
本报告完成体检与复现,未实施以上修复。后续修改应分别列出业务代码变更和生产 API 数据变更;当前不要通过 reset 路径更新生产规则。
|
||||
Reference in New Issue
Block a user