fix(mqtt): 修复 loop_data>512B 溢出发垃圾包致 broker RST 重连风暴 (现场P0)
根因(非平台推测的环形缓冲/event_report队列):
mqtt_publish 的 mqttBuf=512B < loop_data(4通道)604B
→ MQTTSerialize_publish 返回 -2 不写 buf
→ len 为 uint32_t, -2 变 42.9亿
→ WCHNET 把清零缓冲+越界相邻全局(temp_guide=report_config→report_c)
当 520B 垃圾包发出 → broker 见非法类型0x00 RST → 重连风暴
修复(net_srv.c):
- MAX_MQTTBUF_LEN 512→1024 (容纳 604B loop_data)
- mqtt_publish: len uint32→int + 守卫 if(len<=0)return (序列化失败绝不发残缓冲)
- keepalive 9→60 (报告建议, 9s过激)
event_report 协议违规修复(iot_mqtt_srv.c):
- 重连重发保持原 msg_id/ts (V1.04 §5.3-2), 不再作废换号致平台去重失效
验证: tests/test_mqtt_publish_overflow.c 复现旧垃圾包+验证守卫/大缓冲;
tests/test_event_report.c T8 断言重连同 msg_id. 均全过.
遗留: ts=上电秒数非Unix时间戳, 待定方案(设备无RTC/SNTP)
Refs: docs/incidents/2026-07-15-DC045A49718F-protocol-error.md
This commit is contained in:
@@ -6,6 +6,48 @@
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-15 — 🔴 现场事故: MQTT protocol error 重连风暴 (根因/修复)
|
||||
|
||||
### 现象
|
||||
|
||||
设备 DC045A49718F (DLD960GA) 上电后每 ~1s 被 mosquitto 以 `disconnected due to protocol error` 踢下线, 15 分钟 80+ 次重连。平台抓包: 设备发出 520B 垃圾包 = 前 512B 全 0x00 + 尾部 8B ASCII "report_c"。
|
||||
|
||||
### 根因 (静态分析 + 报文长度量化坐实, 与平台"环形缓冲/event_report队列"推测不同)
|
||||
|
||||
**`mqtt_publish` 的 `mqttBuf` 只有 512B, 但 `loop_data`(4通道)≈604B → 溢出。**
|
||||
|
||||
链路:
|
||||
1. `loop_data` 4 通道 JSON = 576B payload / 604B 整包 MQTT > `MAX_MQTTBUF_LEN=512`
|
||||
2. `MQTTSerialize_publish` 检测缓冲不足 → 返回 `MQTTPACKET_BUFFER_TOO_SHORT(-2)`, **且一字节不写 buf**(仍是 clear_mqtt_buf 的全零)
|
||||
3. `mqtt_publish` 里 `len` 是 **uint32_t** → -2 变 4294967294
|
||||
4. `WCHNET_SocketSend(mqttBuf, &len)` 把清零的 512B + **越界相邻全局 `temp_guide`("report_config"→"report_c")** 当一包发出 = 520B 垃圾
|
||||
5. broker 见非法报文类型 0x00 → RST → TCP Timeout(0x40) → 全量重连 → 风暴
|
||||
|
||||
> ⚠️ 与 event_report 无关: 事故日志中设备从未发过 event_report; `mqtt_publish` 是扁平缓冲非环形。此为**先前就存在**的隐患, 4 通道 loop_data 必触发。report_config 响应仅 216B 装得下, 故看着正常。
|
||||
|
||||
### 修复 (net_srv.c)
|
||||
|
||||
1. `MAX_MQTTBUF_LEN` 512 → **1024**: 容纳 604B loop_data + 余量 (TCP 自动分段, MQTT 不关心段边界)
|
||||
2. `mqtt_publish` 的 `len` 改 **int** + **守卫**: `if (len <= 0) return;` 序列化失败绝不发残缓冲 —— 这是根本防线, 即便未来任何 payload 溢出也不再吐垃圾包
|
||||
3. keepalive `MQTT_KEEPALIVE_INTERVAL` 9 → **60** (报告建议; 9s 过激易误断)
|
||||
|
||||
### 协议违规修复 (event_report, 报告实锤)
|
||||
|
||||
- **断线重连后重发用了新 msg_id** → 平台 (sn,msg_id) 去重失效重复入库。修复 `iot_evt_process` 重连沿: 有未决包时保持**原 msg_id/原 ts** 立即重发 (V1.04 §5.3-2), 不再作废换号。
|
||||
|
||||
### 待决 (需老大拍板)
|
||||
|
||||
- **ts 字段是上电秒数 (mstick()/1000) 而非 Unix 时间戳**: MQTT 模式设备无 RTC/SNTP 时间源。选项: (a) 平台以服务端收包时间为准忽略设备 ts (最省, 推荐); (b) 平台下发时间同步命令; (c) 设备加 SNTP。**暂未改**, 待定方案。
|
||||
|
||||
### 验证
|
||||
|
||||
- `tests/test_mqtt_publish_overflow.c`: 复现旧版 512 缓冲发 520B 垃圾包(首字节0x00) + 验证守卫拦截 + 1024 缓冲 641B 正常 + report_config 回归。全过。
|
||||
- `tests/test_event_report.c` T8 改为断言重连保持同 msg_id/ts。8 组全过。
|
||||
|
||||
⚠️ 板上验证: 编译查 .map 确认 RAM 余量(mqttBuf +512B); 烧录后观察不再有 `TCP Timeout` 风暴, loop_data 正常周期上报, 压线圈看 event_report 断线重连去重。
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-15 — event_report 实现: 平台必答 + 设备重发 (协议 V1.04)
|
||||
|
||||
### 实现 (iot_mqtt_srv.c 事件模块 + net_srv.c ACK 路由)
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# 抓包存档:DC045A49718F MQTT protocol error 风暴
|
||||
|
||||
- **文件**: `2026-07-15-DC045A49718F-protocol-error.pcap`(44 包,20s 窗口)
|
||||
- **抓包时间**: 2026-07-15 14:26 左右(设备 14:20 上电后持续复现)
|
||||
- **抓包点**: 腾讯云 159.75.137.141,`tcpdump -i any port 1883 and host 113.91.145.40`
|
||||
- **设备**: DC045A49718F (DLD960GA, hard 1.0 / soft 1.0,新版 event_report 重发固件)
|
||||
|
||||
## 现象
|
||||
|
||||
设备每 ~1s 被 mosquitto 以 `disconnected due to protocol error` 踢下线,
|
||||
15 分钟内 80+ 次重连、87 次 initialize 上报。
|
||||
|
||||
## pcap 关键证据(用 `tcpdump -r <pcap> -nn -ttt -X` 查看)
|
||||
|
||||
正常序列先走完:
|
||||
|
||||
```
|
||||
CONNECT (MQTT 3.1.1, client_id=DC045A49718F, u=admin, keepalive=9)
|
||||
→ CONNACK
|
||||
→ SUBSCRIBE dld960/DC045A49718F/srv → SUBACK
|
||||
→ PUBLISH dld960/DC045A49718F/dev {"cmd":"initialize", msg_id:21, ts:393, ...} ← 报文完好
|
||||
```
|
||||
|
||||
紧接着设备发出致命包:
|
||||
|
||||
```
|
||||
TCP 段 520 字节 = 前 512 字节全 0x00 + 尾部 8 字节 ASCII "report_c"
|
||||
```
|
||||
|
||||
- `0x00` 不是合法 MQTT 控制报文类型(type 0 保留)→ broker 立即 RST 断连
|
||||
- RST 后设备仍继续发第二个 520B 全零段(seq 805:1325)
|
||||
|
||||
## 诊断
|
||||
|
||||
**520 = 512 + 8 → 高度疑似 512B 环形 TX 缓冲区回绕(wrap)处理 bug。**
|
||||
|
||||
推测:往 TX 缓冲写 report_config 响应时跨越回绕边界,"report_c" 落在缓冲区
|
||||
物理尾部,其余部分绕回头部;发送侧却把整块未初始化(全零)缓冲区一次性发出。
|
||||
本次固件恰好新增了事件待发队列 + 5s 重发机制(V1.04),动过 TX 路径,
|
||||
建议重点排查:
|
||||
|
||||
1. 新待发队列的写指针 / 回绕边界处理
|
||||
2. 重发定时器与主循环并发 publish 的互斥(两个写者交叉写 TX 流)
|
||||
3. 发送长度是否误用缓冲区总长而非报文实际长度
|
||||
|
||||
## 顺带实锤的协议违规(固件侧待修)
|
||||
|
||||
1. **断线重连后重发事件用了新 msg_id**:msg_id 9/10 内容完全相同
|
||||
(car_leave ch1 value=48, ts=76),双双入库 → 平台 (dev_serial,msg_id)
|
||||
去重失效。V1.04 §5.3 条 2:重发必须用相同 msg_id(跨重连也要保持)
|
||||
2. **ts 字段是上电秒数**(76/393),不是协议约定的 Unix 时间戳
|
||||
3. keepalive=9s 过于激进(老问题),建议 30~60s
|
||||
|
||||
## 平台侧结论(无需改动)
|
||||
|
||||
event_report 必答闭环工作正常:ack <1s、回显 msg_id、先落库后应答;
|
||||
msg_id 相同的重复包去重有效。重复入库仅发生在固件违规换 msg_id 的场景。
|
||||
Reference in New Issue
Block a user