Files
proxy-station/docs/SURGE-AUDIT-2026-10-03.md

19 KiB
Raw Blame History

Surge macOS 订阅体检:2026-10-03

本文保留修复前的体检结论;其中行号、缺陷与数量指体检时的源码与订阅快照。已实施的代码/生产修复和后续验证见 修复记录,最新盘点见 当前生产状态。不要把下文历史缺陷与快照当作当前状态。

实际订阅通过 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

配置指纹、规则集指纹和复现结果见 脱敏证据。报告中的 seq 是数据库从 0 起算的顺序;“配置第 N 行”对应本次实际 HTTPS 响应。报告和证据不包含订阅 token、真实节点密码、UUID、节点地址或私有 Host IP。

校验能力以 Surge 官方 CLI 文档 为依据。CLI 返回 OK 证明配置可解析,不能证明每个规则都会被命中,也不能代替节点连接、解锁或泄漏测试。

P1 表示可导致配置不可用、规则丢失或重要控制失效;P2 表示行为偏差或需要修正的兼容性问题。静态推导的网络影响和依赖设备开关的风险明确另行说明。

已确认的业务代码缺陷

C1 · P1:重置规则在存在自定义规则时失败,并先删除模板规则

server/src/routes/rules.ts:60 调用 seedRulesFromTemplate(db, { keepCustom: true })。server/src/db/seed.ts:52 读取自定义规则后只删除模板规则,随后在第 63 行把仍在表中的自定义规则再次 INSERT。自定义规则主键重复,且这些操作没有事务包裹。

隔离数据库复现结果:

首次初始化: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 返回:

Rules must end with FINAL

当前生产恰有一条启用的 FINAL,位于末尾,因此当前配置通过校验。问题在后续编辑入口。官方要求规则以启用的 FINAL 结束;dns-failed 只是 DNS 失败时选用其策略,不会补回缺失的 FINAL。FINAL 规范

修复建议:在写入时保护唯一末尾 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 规则、配置引用格式

修复建议:区分声明名、策略引用、普通规则值、逻辑规则表达式的编码规则;拒绝不能安全表达的名称和换行输入。逻辑规则内部的逗号不能简单按普通字符串一律引用。

C4 · P1:策略组循环可通过 API 写入,导致整个配置被拒绝

server/src/routes/groups.ts:21、第 38 行和第 77 行没有图循环检查。隔离测试建立 A → B → A,两次新建均返回 201,IR 保留这些引用;官方 CLI 拒绝:

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 可包含多个来源组。策略成员导入

当前生产的三个地区组没有显式成员,暂不触发。修复建议:完整保留显式成员和多个动态来源,分别应用 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 规范、TLS 参数

修复建议:在能力层拒绝无法表达的旧协议组合,或按官方参数转换;不能让用户输入被静默忽略。所有输入字段都应有明确的支持/转换/拒绝行为。

体检时生产配置中的行为问题

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 标记,客户端资源可能存在缓存差异,未宣称已观察到设备上的请求失败。官方规则顺序是首条匹配决定策略。规则顺序

修复建议:先确定这两项是需要放行的业务例外,还是需要保持阻断的跟踪服务,再决定是否提前。不能自动为全部跟踪域名放行。

D2 · P2:YouTube UDP 拦截被提前的 YouTube 规则集遮蔽

数据库 seq 37/配置第 135 行的 YouTube 规则集含 DOMAIN-SUFFIX,googlevideo.com,策略为 Streaming;seq 110/配置第 208 行才是:

AND,((PROTOCOL,UDP), (DOMAIN-SUFFIX,googlevideo.com)),REJECT-NO-DROP

对携带 googlevideo.com 主机名的普通 UDP 请求,先前的 DOMAIN-SUFFIX 已匹配,后面的 AND 不会生效。STUN 请求另有更前的拦截,所以不能由此认定 STUN 保护失效。逻辑表达式本身通过官方语法校验。逻辑规则、规则顺序

修复建议:若确实要让这类视频流量回落 TCP,把特定 UDP 规则放到相关服务域名规则之前;不要无依据地扩大为全局 UDP 拦截。

D3 · P2:AI DNS 映射忽略更早的连接例外,且 googleapis 覆盖整个服务族

server/src/services/generators/surge-ai-dns.ts:13 收集所有指向 AI 的规则,并把结果整体放在数据库 Host 原文之前,不计算其他策略的更早例外。

本次可确认的例子:

连接策略: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 映射

修复建议:明确 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 规则

修复建议:先确定 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 实际测试依据不一致。自动测速组

修复建议:把可用的设置模型改为官方支持的节点/全局测试参数;不要仅在组编辑界面保留一个看似可用但不会生效的字段。

D6 · P1(取决于设备可达性):控制接口使用全接口监听和模板默认口令

生产 General 同时配置 Remote Controller 和 HTTP API,均绑定 0.0.0.0,认证值与公开模板一致,并开启 Web Dashboard。模板本身也在仓库中,因此这些认证值不能视为私密、独立的设备凭据。订阅生成器原样透传 General。

这表示网络可达的控制接口存在风险;没有检查设备防火墙或公网端口,不认定当前已暴露互联网。官方说明这两个入口具有控制 Surge 的能力,HTTPS 不能弥补已知认证值的问题。控制接口规范

修复建议:无远程控制需求时移除;本机需求绑定回环;有局域网控制需求时使用独立凭据与访问限制,避免所有订阅设备共享模板口令。

D7 · P2(取决于设备开关):MITM 关闭服务端证书验证

实际订阅有 skip-server-cert-verify=true 和 5 个 MITM hostname,但没有 CA 内容或 Keystore 引用。官方说明此项会在 MITM 时关闭真实目标服务器的证书验证;HTTPS 重写需要 MITM 开启和受信任证书。MITM 规范

未检查设备是否关联其他包含 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 的首项是未选过或保存的选择已失效时的默认;不能据此断言用户当前选中的出口。手动选择组

PROCESS-NAME,/Applications/FFSB.app/ 的首尾斜线属于官方 App Bundle 前缀匹配模式,不应误报为非法目录规则。进程规则

URL-REGEX 的作用对象是经过 HTTP 引擎处理的 URL,HTTPS 还取决于 MITM;正则中出现 SMTP/HELO 不会使它变成邮件协议匹配规则。HTTP 规则范围

已核实正常与验证边界

  • HTTPS 订阅的 #!MANAGED-CONFIG self URL 与实际请求完全一致;没有回落到内网 URL。当前 interval=86400, strict=true 受官方支持,但更新间隔不是推送保证,strict 也意味着更新失败时的可用性取舍。托管配置
  • 生产三个地区正则能收纳当前 22 个可用节点,组引用无环,没有悬空规则策略,FINAL 位于末尾。
  • 8 个 SS over WS 排除符合 Surge 的能力范围;不要为了使节点数相同强行输出。Shadowsocks
  • Hysteria2 的 salamander-password 受支持,但官方注明需要 Mac 6.4.3+;实际检查使用 6.9.1。Host 规则集映射需要 Mac 5.10.0+。Hysteria2
  • 60 个外部资源本次均可下载。global_plus.conf 只有注释,没有有效条目;这是零覆盖,不是失效 HTTP 响应。资源可动态更新,设备缓存内容可能不同。
  • 官方允许规则集中的非法行被跳过并记录警告。因此本次快照检查和 CLI 返回 OK,不能证明全部 166,028 个条目均已被客户端接受;逐条加载警告仍需设备侧检查。规则集规范
  • 现有 STUN 拦截的类型、关键字、顺序可用。生产没有全局 PROTOCOL,UDP,REJECT;udp-policy-not-supported-behaviour=REJECT 仅是匹配到不支持 UDP 的策略后的回落,不是全局禁用 UDP。协议规则
  • Host 的 server: 映射只管 Surge 自己的 DNS 客户端,不会因 use-local-host-item-for-proxy=true 而改变远端代理的解析器;该开关只作用于 IP 映射。Host 代理行为
  • 当前四个站点的节点域名都有显式 Host IP 映射。没有因此假设永远不会出现 DNS 引导回退;仍需核对所选节点与设备日志。加密 DNS 引导与回退
  • 没有切换本机配置、修改保存的策略选择、发起节点测速/带宽测试、导入 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 路径更新生产规则。