[B-06][medium] PublishDown 不校验目标是否仍是当前且已订阅的连接:顶号窗口内旧连接的帧发给新连接、握手前的帧推进空主题 #13

Closed
opened 2026-09-30 13:56:52 +08:00 by nixevol · 1 comment
Owner

编号:B-06 严重级:medium 工作线:broker(internal/broker) 来源:审查 P-16、P-22、P-25
依赖:B-03 (#10)(校验放在入队之前) 被依赖:无

结论与统一方案

  1. 旧连接的帧落到新连接:实现 mochi 的 OnSessionEstablish 钩子(在 mochi 替换同编号客户端之前调用),把该编号当前的旧连接标记为 superseded;PublishDown 对被标记或不再是当前连接的目标返回 ErrNoConnection。消息线按现有"发布失败 1 秒后重推"处理,不会丢。(审查 P-16)
  2. 握手前的帧推进空主题(纵深防御):PublishDown 在目标连接尚未订阅 nix/c/{id}/down 时返回新的 ErrNotSubscribed(包一层 ErrNoConnection),避免 mochi 对无订阅者主题静默返回成功。主修复在 C-01(消息线只对已握手连接推送),本条只做兜底。(审查 P-02 的 broker 部分,原文附在 C-01)
  3. 数据竞争:maxRecvBytes 的读写统一加锁或改为原子变量。(审查 P-22 第二点)
  4. resp 大小上限不一致:导出 broker 的有效载荷上限计算(MaxPacketSize - 128 与 max_receive_bytes 取小),cmd/nixmsg/uplink.go 的 publishResp 复用它,避免 resp 落在两者之间被静默丢弃。(审查 P-25)

审查 P-22 的其余两点:握手计时器取消归 B-09,认证超时归 B-12。

改动文件

internal/broker/hooks.go、broker.go(若 B-03 已落地则在其下发模块);cmd/nixmsg/uplink.go(仅 publishResp 一处)。

与其他问题的交互 / 冲突说明

  • 依赖 B-03:下发改到每连接队列后,校验放在入队之前。
  • 与 C-01 互补:C-01 让消息线不在握手前推送,本条让 broker 在误推时返回错误。
  • uplink.go 只改 publishResp;生命周期函数归 B-09,HandleUplink 归 C-05。

验收与测试

  • 两个客户端用同一编号顶号,在旧连接 OnDisconnect 之前 PublishDown(旧 connID) 返回错误,新连接收不到这一帧。
  • 已建立会话但未订阅时 PublishDown 返回 ErrNotSubscribed。
  • -race 下并发 hello 与推送无竞争报告(本机无 CGO 时在 golang 官方镜像里跑)。
  • 声明 MaxPacketSize=1000 的连接请求 950 字节的响应,收到 response_too_large。

问题明细(各区审查原文,证据含文件与行号)

以下是本次复审各区审查报告的原文段落。A、M、I、P、S 开头的是原始发现编号(A 管理后台与网页、M 消息核心、I 身份认证群在线、P 传输平台部署、S SDK)。解决方案以本 issue 上方的"结论与统一方案"为准;原文里的方案与之不一致时,按上方执行。

[P-16] 带 connID 的 PublishDown 只检查连接存在,不检查是否仍是当前连接:顶号窗口内,旧连接的帧会发给新连接

  • 严重级:medium
  • 分类:并发 / 协议一致性
  • 现象与影响:
    • port 的接口约定写的是"connID 非空时仅在该连接仍是当前连接时发布",但 lookupConn 只要在表里找到就算成功。
    • mochi 按 ClientID 路由,新连接接入后已经替换了 mochi 客户端表里的旧连接;而旧连接的状态要等它自己的 OnDisconnect 才删除。
    • 如果旧连接的读循环正卡在 q.push(队列满),OnDisconnect 会一直拖到 worker 腾出位置。这段时间里:
      • 旧连接排队请求的 resp,以及 message 按旧 connID 的推送(message/push.go:90-99),都会发到新连接上。
      • Go SDK 的 rid 每个 Client 从 1 开始递增(sdk/go/client.go:133-135),两台设备之间很容易撞号,结果被配到错误的请求上。
      • 新连接还没握手就收到 msg。
  • 证据:broker.go:202-244、294-306;app/port/port.go:72-74;mochi server.go:455-474、1014-1021。
  • 文档依据:port 接口约定;DEVELOPMENT 5(旧连接的断开事件不能影响新连接)、6.1。
  • 为何不是故意设计:和自己的接口约定相反。
  • 解决方案:实现 mochi 的 OnSessionEstablish 钩子,它在 mochi 替换客户端之前调用。在里面把该编号当前的旧连接状态标记为 superseded(原子布尔);PublishDown 遇到被标记的连接返回 ErrNoConnection。Disconnect 仍按对象断开,不受影响。
  • 改动文件:internal/broker/hooks.go、broker.go
  • 与其他模块的交互/冲突风险:顶号那几毫秒内的发布会失败,按现有逻辑 1 秒后重推,不会丢。
  • 需补测试:两个 net.Pipe 客户端用同一编号顶号;在旧连接 OnDisconnect 之前,PublishDown(旧 connID) 返回错误,新连接收不到这一帧。
  • 置信度:代码阅读确定;实际窗口长度未测

[P-22] broker 连接状态的三处小毛病

  • 严重级:low
  • 分类:并发 / 质量
  • 现象与影响:
    • 握手计时器取消无效:OnDisconnect 先从表里删掉连接,再按 connID 去找它的握手计时器,自然找不到,所以计时器从来没被取消(hooks.go:196-211、broker.go:469-480、session.go:128-130)。它只能等 30 秒后自己触发,空跑一次全表扫描。
    • 数据竞争:maxRecvBytes 写入时加锁,PublishDown 读取时不加锁(broker.go:211、360-368)。
    • 认证没有超时:OnConnect 用 context.Background() 做认证(hooks.go:70)。SDK 30 秒超时断开之后,这次 argon2 校验仍会排到并算完;登录风暴时这些废弃请求堆在队列前面,造成拥塞(auth/pool.go:58-74 的 acquire 是支持 ctx 的)。
  • 文档依据:DEVELOPMENT 5、12。
  • 为何不是故意设计:三处都没有记录,属于实现疏漏。
  • 解决方案:
    • OnDisconnect 里直接对这个连接状态加锁,停掉它的计时器;
    • 读 maxRecvBytes 时加锁(或改成原子变量);
    • 认证用 30 秒超时的 ctx,超时按内部错误处理:不回 CONNACK,客户端按网络故障重连,符合第 5 节。
  • 改动文件:internal/broker/hooks.go、broker.go
  • 与其他模块的交互/冲突风险:无。
  • 需补测试:断线后计时器已停止;在 -race 下并发执行 hello 和推送。
  • 置信度:代码阅读确定

[P-25] resp 大小检查没扣 128 字节包头预算,落在这个窗口里的 resp 整条丢失

  • 严重级:low
  • 分类:协议一致性
  • 现象与影响:
    • uplink 按 min(max_receive_bytes, MaxPacketSize) 判断 resp 是否超限,broker 按 MaxPacketSize - 128 判断。
    • resp 长度落在这两者之间时,uplink 放行、broker 拒绝,只记一条错误日志,客户端既收不到结果、也收不到 response_too_large,只能等超时。
    • DEVIATIONS L-UPLINK 第 3 条把影响描述为"略严或略松",实际后果是整条响应丢失。
  • 证据:uplink.go:282-300、303-315;broker.go:316-329。
  • 文档依据:DEVELOPMENT 7.5「resp 超限改发 response_too_large 错误」。
  • 为何不是故意设计:偏差记录只承认"略严或略松",没有预料到会整条丢响应。
  • 解决方案:uplink 复用 broker 的计算(导出成公共函数),或者同样减去 128。
  • 改动文件:cmd/nixmsg/uplink.go、internal/broker/broker.go
  • 与其他模块的交互/冲突风险:无。
  • 需补测试:声明 MaxPacketSize=1000 的连接请求一个 950 字节的响应,应收到 response_too_large。
  • 置信度:代码阅读确定

复审基线:main 4059a15(2026-09-30)。编号说明、各工作线的合并顺序、共享文件归属见总览 #7。

**编号**:B-06 **严重级**:medium **工作线**:broker(internal/broker) **来源**:审查 P-16、P-22、P-25 **依赖**:B-03 (#10)(校验放在入队之前) **被依赖**:无 ### 结论与统一方案 1. **旧连接的帧落到新连接**:实现 mochi 的 `OnSessionEstablish` 钩子(在 mochi 替换同编号客户端之前调用),把该编号当前的旧连接标记为 `superseded`;`PublishDown` 对被标记或不再是当前连接的目标返回 `ErrNoConnection`。消息线按现有"发布失败 1 秒后重推"处理,不会丢。(审查 P-16) 2. **握手前的帧推进空主题(纵深防御)**:`PublishDown` 在目标连接尚未订阅 `nix/c/{id}/down` 时返回新的 `ErrNotSubscribed`(包一层 `ErrNoConnection`),避免 mochi 对无订阅者主题静默返回成功。主修复在 C-01(消息线只对已握手连接推送),本条只做兜底。(审查 P-02 的 broker 部分,原文附在 C-01) 3. **数据竞争**:`maxRecvBytes` 的读写统一加锁或改为原子变量。(审查 P-22 第二点) 4. **resp 大小上限不一致**:导出 broker 的有效载荷上限计算(`MaxPacketSize - 128` 与 `max_receive_bytes` 取小),`cmd/nixmsg/uplink.go` 的 `publishResp` 复用它,避免 resp 落在两者之间被静默丢弃。(审查 P-25) 审查 P-22 的其余两点:握手计时器取消归 B-09,认证超时归 B-12。 ### 改动文件 `internal/broker/hooks.go`、`broker.go`(若 B-03 已落地则在其下发模块);`cmd/nixmsg/uplink.go`(仅 `publishResp` 一处)。 ### 与其他问题的交互 / 冲突说明 - 依赖 B-03:下发改到每连接队列后,校验放在入队之前。 - 与 C-01 互补:C-01 让消息线不在握手前推送,本条让 broker 在误推时返回错误。 - uplink.go 只改 `publishResp`;生命周期函数归 B-09,`HandleUplink` 归 C-05。 ### 验收与测试 - 两个客户端用同一编号顶号,在旧连接 `OnDisconnect` 之前 `PublishDown(旧 connID)` 返回错误,新连接收不到这一帧。 - 已建立会话但未订阅时 `PublishDown` 返回 `ErrNotSubscribed`。 - `-race` 下并发 hello 与推送无竞争报告(本机无 CGO 时在 golang 官方镜像里跑)。 - 声明 MaxPacketSize=1000 的连接请求 950 字节的响应,收到 `response_too_large`。 --- ### 问题明细(各区审查原文,证据含文件与行号) > 以下是本次复审各区审查报告的原文段落。A、M、I、P、S 开头的是原始发现编号(A 管理后台与网页、M 消息核心、I 身份认证群在线、P 传输平台部署、S SDK)。**解决方案以本 issue 上方的"结论与统一方案"为准**;原文里的方案与之不一致时,按上方执行。 #### [P-16] 带 connID 的 PublishDown 只检查连接存在,不检查是否仍是当前连接:顶号窗口内,旧连接的帧会发给新连接 - **严重级**:medium - **分类**:并发 / 协议一致性 - **现象与影响**: - port 的接口约定写的是"connID 非空时仅在该连接仍是当前连接时发布",但 `lookupConn` 只要在表里找到就算成功。 - mochi 按 ClientID 路由,新连接接入后已经替换了 mochi 客户端表里的旧连接;而旧连接的状态要等它自己的 OnDisconnect 才删除。 - 如果旧连接的读循环正卡在 `q.push`(队列满),OnDisconnect 会一直拖到 worker 腾出位置。这段时间里: - 旧连接排队请求的 resp,以及 message 按旧 connID 的推送(`message/push.go:90-99`),都会发到新连接上。 - Go SDK 的 rid 每个 Client 从 1 开始递增(`sdk/go/client.go:133-135`),两台设备之间很容易撞号,结果被配到错误的请求上。 - 新连接还没握手就收到 msg。 - **证据**:`broker.go:202-244`、`294-306`;`app/port/port.go:72-74`;mochi `server.go:455-474`、`1014-1021`。 - **文档依据**:port 接口约定;DEVELOPMENT 5(旧连接的断开事件不能影响新连接)、6.1。 - **为何不是故意设计**:和自己的接口约定相反。 - **解决方案**:实现 mochi 的 `OnSessionEstablish` 钩子,它在 mochi 替换客户端之前调用。在里面把该编号当前的旧连接状态标记为 `superseded`(原子布尔);`PublishDown` 遇到被标记的连接返回 `ErrNoConnection`。`Disconnect` 仍按对象断开,不受影响。 - **改动文件**:`internal/broker/hooks.go`、`broker.go` - **与其他模块的交互/冲突风险**:顶号那几毫秒内的发布会失败,按现有逻辑 1 秒后重推,不会丢。 - **需补测试**:两个 net.Pipe 客户端用同一编号顶号;在旧连接 OnDisconnect 之前,`PublishDown(旧 connID)` 返回错误,新连接收不到这一帧。 - **置信度**:代码阅读确定;实际窗口长度未测 #### [P-22] broker 连接状态的三处小毛病 - **严重级**:low - **分类**:并发 / 质量 - **现象与影响**: - **握手计时器取消无效**:`OnDisconnect` 先从表里删掉连接,再按 connID 去找它的握手计时器,自然找不到,所以计时器从来没被取消(`hooks.go:196-211`、`broker.go:469-480`、`session.go:128-130`)。它只能等 30 秒后自己触发,空跑一次全表扫描。 - **数据竞争**:`maxRecvBytes` 写入时加锁,`PublishDown` 读取时不加锁(`broker.go:211`、`360-368`)。 - **认证没有超时**:`OnConnect` 用 `context.Background()` 做认证(`hooks.go:70`)。SDK 30 秒超时断开之后,这次 argon2 校验仍会排到并算完;登录风暴时这些废弃请求堆在队列前面,造成拥塞(`auth/pool.go:58-74` 的 acquire 是支持 ctx 的)。 - **文档依据**:DEVELOPMENT 5、12。 - **为何不是故意设计**:三处都没有记录,属于实现疏漏。 - **解决方案**: - OnDisconnect 里直接对这个连接状态加锁,停掉它的计时器; - 读 `maxRecvBytes` 时加锁(或改成原子变量); - 认证用 30 秒超时的 ctx,超时按内部错误处理:不回 CONNACK,客户端按网络故障重连,符合第 5 节。 - **改动文件**:`internal/broker/hooks.go`、`broker.go` - **与其他模块的交互/冲突风险**:无。 - **需补测试**:断线后计时器已停止;在 `-race` 下并发执行 hello 和推送。 - **置信度**:代码阅读确定 #### [P-25] resp 大小检查没扣 128 字节包头预算,落在这个窗口里的 resp 整条丢失 - **严重级**:low - **分类**:协议一致性 - **现象与影响**: - uplink 按 `min(max_receive_bytes, MaxPacketSize)` 判断 resp 是否超限,broker 按 `MaxPacketSize - 128` 判断。 - resp 长度落在这两者之间时,uplink 放行、broker 拒绝,只记一条错误日志,客户端既收不到结果、也收不到 `response_too_large`,只能等超时。 - DEVIATIONS L-UPLINK 第 3 条把影响描述为"略严或略松",实际后果是整条响应丢失。 - **证据**:`uplink.go:282-300`、`303-315`;`broker.go:316-329`。 - **文档依据**:DEVELOPMENT 7.5「resp 超限改发 response_too_large 错误」。 - **为何不是故意设计**:偏差记录只承认"略严或略松",没有预料到会整条丢响应。 - **解决方案**:uplink 复用 broker 的计算(导出成公共函数),或者同样减去 128。 - **改动文件**:`cmd/nixmsg/uplink.go`、`internal/broker/broker.go` - **与其他模块的交互/冲突风险**:无。 - **需补测试**:声明 MaxPacketSize=1000 的连接请求一个 950 字节的响应,应收到 `response_too_large`。 - **置信度**:代码阅读确定 --- <sub>复审基线:main `4059a15`(2026-09-30)。编号说明、各工作线的合并顺序、共享文件归属见总览 #7。</sub>
nixevol added the P2-mediumlane/brokerreview-2026-09-30 labels 2026-09-30 13:56:52 +08:00
Author
Owner

已合入 origin/main 0c9b459。落地提交 91e887b fix: 完成 broker 复审 B-03 至 B-12 (#13)。

已合入 origin/main `0c9b459`。落地提交 `91e887b` fix: 完成 broker 复审 B-03 至 B-12 (#13)。
Sign in to join this conversation.