跳转至

协议分层、契约与状态机

协议不是一串能够被解析的字节,而是一组关于方向、时间、状态、错误和演进的共同约定。只记录帧头和字段长度,只能实现“正常样本可以解码”,不能保证异常输入下仍然安全。

通信协议分层地图

先分清六个对象

对象 回答的问题 示例
连接器 导线怎样接入 JST插座、焊盘、USB连接器
电气接口 电压和信号怎样变化 3.3伏UART、CAN差分信号
成帧规则 一帧从哪里开始、在哪里结束 固定长度、长度字段、间隔
消息协议 字节代表什么 MAVLink消息、MSP命令
功能角色 为什么交换数据 遥控输入、参数配置、遥测
安全策略 谁在什么状态下可以做什么 解锁拒绝、签名校验、限速

同一个连接器可以暴露多个接口;同一个接口可以承载多个协议;同一个协议也可以承载不同权限的功能。协议审查必须逐层记录,不能从“插头相同”推导兼容。

最小协议契约

每类消息至少定义:

类别 必须说明
身份 消息ID、发送者、目标、协议和定义版本
结构 帧头、长度、载荷、校验、端序和对齐
数据 类型、单位、比例、坐标系、枚举和保留值
时间 产生时间、发送时间、有效期、超时和时钟基准
顺序 序号、重复、乱序、丢失和重放处理
状态 请求前提、允许状态、转换、完成和回滚
错误 非法长度、未知消息、校验失败和资源耗尽
安全 身份、完整性、机密性、授权和审计
演进 版本发现、扩展规则、弃用和降级

“三个32位浮点数”不是完整契约。接收端仍不知道它们是角度还是角速度、采用弧度还是度、属于哪个坐标系、何时采样、是否有效。

发送成功不等于动作完成

命令常经历:

调用方构造
→ 本地队列接受
→ 帧发送
→ 对端接收并校验
→ 对端完成权限和状态检查
→ 动作进入执行队列
→ 执行完成或失败
→ 返回结果

“写入串口成功”最多证明数据进入本地输出路径。没有对端确认时,不能证明帧已收到;收到确认也不一定证明动作已经完成。协议必须区分:

  • 已接受传输;
  • 已收到帧;
  • 已接受命令;
  • 正在执行;
  • 已完成;
  • 被拒绝;
  • 超时但最终状态未知。

超时不是失败的同义词。若请求已经到达而响应丢失,盲目重试可能重复执行一次性动作。

用状态机描述会话

以参数更新为例:

空闲
→ 读取当前版本
→ 提交候选值
→ 对端验证
→ 对端应用
→ 回读确认
→ 完成

任一步都需要定义:

  • 允许进入的前置状态;
  • 超时;
  • 可重试性;
  • 幂等键或事务标识;
  • 错误响应;
  • 断线后的恢复;
  • 是否需要回滚。

如果写入会改变端口本身,应用后连接可能立刻中断。工具应提前知道这是预期切换还是异常丢线,并通过新端口回读确认。

数据有效性与新鲜度

接收方不能只缓存“最后一个值”。每项数据还需要:

  • 数据有效标志;
  • 采样或产生时间;
  • 接收时间;
  • 最大允许年龄;
  • 来源实例;
  • 序列或更新计数;
  • 失效后的替代策略。

例如,遥控通道数值保持在中位不代表遥控链路正常;位置数值没有变化也可能是静止、冻结或数据源失效。协议必须让应用区分这些状态。

长度与边界

解析器应先验证最外层约束,再读取内部字段:

  1. 输入缓冲区至少包含固定头;
  2. 版本和帧类型受支持;
  3. 声明长度不小于最小值;
  4. 声明长度不大于协议和本地缓冲上限;
  5. 完整帧已经到达;
  6. 校验通过;
  7. 消息自身长度符合定义;
  8. 字段值和组合约束有效。

长度字段的含义必须精确说明。它可能表示:

  • 仅载荷长度;
  • 从类型字段到校验字段的长度;
  • 整个帧长度;
  • 不含固定头但包含尾部;
  • 以字节、字或其他单位计数。

使用错误的长度定义会导致越界、粘帧和错误重同步。

未知和扩展

可演进协议通常要保留:

  • 未知消息的跳过规则;
  • 未知标志位的处理;
  • 扩展字段的追加规则;
  • 保留字段必须为零或必须忽略的要求;
  • 不兼容扩展的明确拒绝。

安全默认并不总是“全部拒绝”。如果规范声明未知兼容标志可忽略,接收端应继续处理基础部分;如果未知不兼容标志会改变帧解释,则必须丢弃。实现应按协议定义处理,不能用一个全局规则替代。

校验、认证和加密

三者解决不同问题:

能力 主要目的 不能自动提供
校验和或CRC 检测传输错误 发送者身份、防恶意篡改、机密性
消息认证码或数字签名 验证持有密钥者和消息完整性 载荷保密
加密 隐藏载荷内容 完整性、身份或防重放,除非模式同时提供

循环冗余校验(CRC,Cyclic Redundancy Check)不是安全认证。攻击者能够重新计算普通CRC时,仍可构造格式正确的恶意帧。

端序、位序和对齐

至少分别说明:

  • 多字节整数采用大端还是小端;
  • 位字段的最低有效位对应什么;
  • 位流是先发送最高位还是最低位;
  • 结构体是否允许填充字节;
  • 浮点格式;
  • 未对齐访问是否允许。

不能直接把网络字节复制到编译器结构体后强制转换。编译器对齐、主机端序和未定义行为都可能破坏可移植性。应使用显式读写函数或由协议生成器产生的序列化代码。

资源和拒绝服务边界

即使每帧都合法,高速输入仍可能耗尽:

  • 接收环形缓冲;
  • 解码队列;
  • 命令队列;
  • 日志带宽;
  • 主循环时间;
  • 动态内存;
  • 响应带宽。

协议实现应设置:

  • 最大帧长;
  • 每连接和每消息速率;
  • 队列深度;
  • 低优先级丢弃策略;
  • 高风险命令并发限制;
  • 解析预算;
  • 错误日志限速。

错误日志本身也可能放大拒绝服务。连续错误帧应累计计数并限速报告。

高风险命令门禁

以下操作不能只凭“命令ID正确”执行:

  • 解锁或直接执行器控制;
  • 修改输出映射和失效保护;
  • 写入任务和地理围栏;
  • 重启、进入引导加载器或刷写;
  • 删除日志、配置或密钥;
  • 修改网络、串口或认证策略。

至少检查发送来源、会话权限、当前飞行状态、物理互锁、参数范围、重复请求和审计记录。协议层的认证不能替代飞控状态机的安全门禁。

可观察实现

实现应暴露而不是隐藏:

  • 接收和发送字节数;
  • 有效帧数;
  • 长度、校验、版本和权限错误;
  • 丢帧、重复和乱序;
  • 队列峰值;
  • 最近有效消息时间;
  • 重连和解析器重同步次数;
  • 高风险命令接受与拒绝原因。

计数器需要明确是否饱和、回绕或重启清零。只显示一个“错误总数”会混淆物理噪声、版本不兼容和权限拒绝。

协议审查问题

  1. 接收端怎样从任意字节位置重新找到合法帧?
  2. 长度字段是否在访问载荷前完成边界检查?
  3. 校验覆盖哪些字段,哪些字段没有保护?
  4. 时间戳由谁产生,重启后是否单调?
  5. 重复命令会执行一次还是多次?
  6. 未知扩展怎样处理?
  7. 连接恢复后沿用旧状态还是重新协商?
  8. 高风险命令的身份、权限和状态门禁在哪里?
  9. 解析器在最大合法流量和恶意流量下是否有预算?
  10. 怎样证明当前工具、消息定义和固件属于同一契约版本?

检查理解

  1. 为什么连接器、接口和协议不能互相替代?
  2. “发送成功”和“动作完成”之间还缺哪些阶段?
  3. CRC为什么不能证明发送者身份?
  4. 未知兼容扩展和未知不兼容扩展应如何区别?
  5. 为什么协议实现需要暴露错误分类和队列峰值?

主要参考