Skip to content

第 8 章:校验、提交与回滚——配置怎样安全落地

生成正确的 Mapping 只是中点。候选配置还要写文件、让 Mihomo 验证、选择热加载或重启,并在任一步失败时恢复。这里用 Verge patch、Clash patch 和端口修改三条链路,比较不同风险等级的提交协议。

一、验证的对象是候选 Runtime

CoreConfigValidator 不直接验证 IVerge 或某段 Merge,而是验证增强后的完整 runtime。原因很直接:

  • 单独合法的两个片段合并后可能冲突;
  • Sequence 删除节点后 group 可能悬空;
  • TUN/DNS 派生可能形成核心不支持的组合;
  • Script 可以返回任意结构。

验证流程通常是:

text
Config::generate()

Config::generate_file(Check 或 Run)

启动 Mihomo config test / validator

ValidationOutcome

ValidationOutcome 不只是 bool,还区分 valid、busy、skipped 和错误原因,让自动更新与手动操作采取不同反馈策略。

二、Clash patch:按字段决定 reload 或 restart

feat::patch_clash(patch) 先写入 clash Draft,再分类:

字段动作
secret / external-controller重新生成并 restart core
allow-lan生成、校验并更新配置
mode更新 runtime、核心配置和 tray
其他可热更新字段patch runtime 后 checked update

成功后 apply Clash Draft 并写文件;失败则 discard。

这里没有一个“所有设置都 restart”的粗暴策略。热更新减少连接中断,但 controller/secret 改变了通信边界,必须完整重启。

三、Verge patch:字段到副作用集合

determine_update_flags() 将 patch 转成 bitflags:

  • RESTART_CORE
  • CLASH_CONFIG
  • VERGE_CONFIG
  • LAUNCH
  • SYS_PROXY
  • HOTKEY
  • tray menu/icon/tooltip/click behavior
  • LIGHT_WEIGHT
  • LANGUAGE
  • LOG_LEVEL / LOG_FILE

随后 process_terminated_flags() 按顺序执行副作用。

为什么先计算 flags?

  1. 同一 patch 的多个字段可以合并副作用,例如只重启一次;
  2. 字段与副作用的映射集中可审计;
  3. 平台条件在映射阶段处理,不散落到 UI;
  4. 未来增加字段时,测试可以直接检查 flags。

四、Verge patch 的事务边界

apply_verge_patch()

  1. claim Verge Draft;
  2. stage patch;
  3. 计算 flags;
  4. 执行重启、代理、托盘等副作用;
  5. 全部成功后 commit;
  6. 刷新自动备份设置;
  7. 按需写 verge.yaml

早期常见 bug 是副作用中的 ? 提前返回,discard 分支实际上不可达,失败 candidate 长留在 Draft,后续 latest_arc() 都看到一个从未成功的值。RAII transaction 把回滚从“记得写 catch”变成结构保证。

五、为什么 TUN patch 后还要 reconcile

打开 TUN 可能触发 core reload,但 RunState 本身不一定变化。若当前既没有可用 Service,也没有管理员权限,界面设置会显示已开,实际流量却无法进入 TUN。

patch_verge() 在 apply 后显式调用 reconcile_tun_availability()

  • RunState 若证明当前不具备 TUN 能力;
  • 且没有特权操作/用户决策正在进行;
  • 则关闭或 session-suppress TUN,并重新生成配置。

这说明“设置是否允许”有时依赖能力状态,而能力状态未必会因设置变化自动发事件。需要在调用图上增加明确 reconciliation 点。

六、Mixed Port fallback:真正的跨层事务

启动时发现选定 mixed port 被外部占用,会寻找下一个不与以下端口冲突的候选:

  • 其他代理 listener;
  • external controller;
  • Verge 中保存的 socks/http/redir/tproxy;
  • 当前用户 Service 已拥有的核心端口要被特殊识别。

候选找到后:

text
claim Clash + Verge + Runtime

stage 两个源层的新端口

重新 enhance 到 Runtime draft

Mihomo 校验候选

捕获三个配置文件快照

依次持久化 Clash、Verge、Runtime

commit 三层 Draft

发刷新事件和延迟通知

任何文件写入失败:

  1. 先 rollback 内存;
  2. 再恢复所有文件;
  3. 若恢复也失败,把原错误与 rollback error 合并返回。

错误不能被后一个恢复错误覆盖,否则诊断时只看到“恢复失败”,不知道最初是哪次写入触发。

七、运行中保存多个 listener 更复杂

设置页一次可修改 mixed/socks/http/redir/tproxy 的 enabled 与 port。保存前必须:

  • 校验地址和所有端口;
  • 探测 TCP/UDP 冲突;
  • 暂时 core_starting() 关闭 PAC;
  • 捕获旧文件;
  • stage Clash/Verge/Runtime;
  • 生成并校验;
  • 停止/替换核心;
  • 写文件并 commit;
  • 恢复系统代理。

若失败且原来有核心:

  • discard 所有 drafts;
  • restore 文件;
  • 若核心已停则 start 旧核心,否则 restart;
  • 合并文件恢复与生命周期恢复错误。

这已经不是传统 ACID,而是一个带补偿动作的状态迁移。

八、CoreManager 防止并发配置更新

config_update_in_progress: AtomicBool 提供快速互斥:

  • try_start_config_update() 以 swap claim;
  • scopeguard 在所有路径 finish_config_update()
  • 第二个更新得到 Busy/Skipped,而不是与第一个交错。

外层还有 lifecycle_lock: tokio::Mutex<()> 串行化 start/stop/restart 和 handoff。固定锁序是:

text
config_update_in_progress → lifecycle_lock

锁序写进结构注释,是避免未来新增路径造成死锁的重要做法。

九、校验通过也不代表副作用成功

需要区分三类失败:

  1. 内容失败:YAML/Script/核心校验错误;
  2. 持久化失败:文件权限、磁盘、部分写入;
  3. 运行失败:Service IPC、Sidecar 拉起、端口绑定、系统代理恢复。

每一类的恢复证据不同:

  • 内容失败不应停止旧核心;
  • 文件失败要恢复快照;
  • 运行失败要看核心是否其实已经 ready,不能只看 Result;
  • 系统代理失败可能发生在核心成功启动后,此时应该保留核心并报告恢复问题。

十、默认配置兜底

冷启动配置无效时,generate_and_validate() 会让 CoreManager 使用最小默认配置,并延迟向前端发 notice。这样:

  • 应用仍有机会启动核心和 UI;
  • 用户能打开编辑器修复原配置;
  • 原文件不会被静默替换成默认值;
  • 错误原因保留在通知与日志中。

“以默认配置运行”和“把用户配置覆盖为默认”是两件完全不同的事,前者是可恢复降级,后者是数据破坏。

十一、可迁移的提交协议

对任何会影响外部进程的配置,可以复用以下模板:

text
Claim → Stage → Materialize → Validate → Snapshot
→ Apply side effects → Persist → Commit → Publish

失败:Rollback memory → Restore files → Compensate side effects

其中 Publish 最后做,因为事件是“事实已经改变”的通知,不应被当作事务的一部分。

本章源码索引

  • src-tauri/src/feat/config.rs::patch_clash:Clash patch 分类
  • src-tauri/src/feat/config.rs::determine_update_flags:Verge 字段到副作用
  • src-tauri/src/feat/config.rs::apply_verge_patch:RAII patch 事务
  • src-tauri/src/core/validate.rs:核心配置校验与结果类型
  • src-tauri/src/core/manager/config.rs:checked/forced update 和默认配置
  • src-tauri/src/config/port.rs:启动 Mixed Port fallback
  • src-tauri/src/feat/listener.rs:运行中 listener 保存与补偿
  • src-tauri/src/config/snapshot.rs:文件快照
  • src-tauri/src/core/manager/mod.rs:更新标志与 lifecycle lock

原创文档 CC BY-SA 4.0 · 站点代码 MIT