Commit Graph
2 Commits
Author SHA1 Message Date
chick af738a5bf9 fix(relay): 限额改为从共享策略推导,修掉信封上限过小导致的合法请求被拒
中继此前自带一份限额常量,并在注释里声称"与共享策略一致",而没有任何东西
校验这句话。其中两条已经漂移成真实缺陷:

1. 信封上限 64 KiB 是**信封**的上限,而信封约为其承载 body 的 1.8 倍(body
   先 base64、再 AEAD、再 base64)。于是 body 仍在 agent 的 256 KiB
   maxRequestBytes 之内的请求,在中继处被拒为 relay_payload_too_large——一个
   在局域网上能成功、走中继就失败的操作。响应方向同理:节点返回合法的大
   body,应答在中继被拒,手机白等到超时。实测 120 KiB 的 body 就被拒。
2. mailboxDepth 只靠注释与策略"相等",改任一侧都不会有人发现,而这个深度
   是 agent 重放缓存容量的依据。

现在 relay/src/limits.js 从 contracts/agent-policy.json 推导全部限额,并在
文件不可读、缺 limits、数值不可用时**拒绝启动**:中继无法区分"诚实的大请求"
与"内存耗尽尝试",上限是它唯一的工具,凭空发明一个等于在强制一个没人选过的
数字。推导结果:信封上限 513037 字节、深度 8、请求期限 30s、轮询窗口 24s。

策略摘要在健康检查里公开并逐节点对比:/healthz 返回中继自己那份的摘要,以及
每个在线节点上报的摘要与 policyMatches。它只上报不强制——中继读不到节点上的
文件,声称不一致就拒服务会把"节点跑着旧二进制"变成一次故障;而策略本来就是
agent 在强制。没有上报的节点显示 null 而不是 false,"没说"与"说了不一样的"
是两个不同的发现。

顺带修掉一个只在 sudo 配对时才暴露的缺陷:config.Save 用临时文件加重命名,
在 root 下会把配置文件变成 root 所有,而 systemd 单元以 msa-agent 用户运行,
于是配对看起来成功、节点重启后读不到自己的配置。现在重命名前保留原文件的
uid/gid,且无法保留时报错而非警告。

测试新增 test/limits.test.js(11 项),分三层:推导与策略文件一致、推导出的
上限确实容得下用真实 wire 代码构造的最坏信封(这是旧上限失败的那次测量)、
以及经真实 HTTP 面把合法大请求送过去。另有健康检查的摘要上报与 null 语义。
四处断言都做了变异验证:把上限改回 64 KiB,四项测试失败。

Node 93 项、Go 全包、Swift 99 项、端到端全部通过。
2026-09-16 23:24:28 +08:00
chick 09ccc67799 fix(relay): 校验 agent 握手签名、判定 agent 断线、保住跨重握手的邮箱
三处都是"每个组件各自正确、合起来坏了"的接缝缺陷,其中第一处按部署文档
配置时必然命中。

1. 中继没有校验握手签名的能力。校验器被设计成 agent 条目上的 verify 回调,
   而所有测试都注入了桩,于是唯一真实存在的校验器就是那些桩:按
   docs/deployment.md 写的配置里没有它,第二次 hello 抛
   "agent.verify is not a function",对每台节点回 500。现在配置只接受
   signKey(base64 Ed25519 公钥),中继自己验签,能验不能签;密钥缺失、
   长度不对或非规范 base64 一律拒绝启动——一个启动后拒绝全机群的"健康"
   中继比启动失败更难查。msa-keygen 的输出与部署文档同步补上该字段。

2. agent 断线后中继永远认为它在线。此前只有进程关闭才 detach,而节点被杀、
   断电、拔网线留下的只是一个关闭的 socket:节点会永远在线,手机的每条请求
   白等 30 秒并收到"节点可能已执行"——实际没有任何东西在监听。现在长轮询的
   socket 断开即判定离线(比对 session token,避免陈旧连接顶掉新会话),
   实测从 504/30009ms 变为 503/5ms。

3. 重新握手丢弃邮箱。attach() 整体替换节点记录,连带丢掉已排队的信封,
   于是重连窗口内到达的请求被静默丢弃、手机 5 秒后收到"节点离线",而节点
   其实刚连上。重握手在每次会话过期与每次重连都会发生,是常规路径而非边角。
   现在邮箱与每个信封的 handover 期限跨重握手保留。

同时修掉同一节点第二个长轮询孤儿化第一个的问题:两个 poll 共用槽位,后到的
覆盖先到的 resolve,前一个 promise 永不 settle、HTTP 请求一直挂着。现役
agent 串行轮询碰不到,--once 与守护进程同跑就会命中。现在每个 poll 有自己的
槽位,被顶掉的那个立刻以空结果返回。

测试新增三个文件,并全部改用真实密钥(verify 桩已消失):

- test/config.test.js 只经 start() + 环境变量启动中继并完成握手。第一个缺陷
  能存在,正是因为没有任何测试走这条装载路径——e2e.test.js 自己构造
  createRelay 参数,把配置装载整个绕过了。
- test/liveness.test.js 七项节点生死与重连性质,两个方向的错误都断言:
  漏判(死节点永远在线)与误判(陈旧 socket 顶掉新会话)。
- test/hello.test.js 把握手签名的字节直接与 Go 源码里的字面量比对,并断言
  NUL 分隔符两侧一致——这是 relay 与 agent 之间唯一没有共享代码可依赖的地方。

四处修复都做了变异验证:改回旧行为后对应测试必失败。

Node 82 项、Go 全包、Swift 99 项、端到端(真编译运行 Go agent 经 TLS 接真中继)
全部通过。
2026-09-16 22:55:49 +08:00