- 后端
- 物联网
- 消息队列
- 通信
【免费下载链接】emqx
The most scalable and reliable MQTT broker for AI, IoT, IIoT and connected vehicles
导读
本文深入解析 EMQX 在 MQTT 5.0 协议处理上的一个关键合规性修复(对应 changelog 条目changes/ee/fix-16782.en.md):当客户端在 PUBLISH 报文中携带Subscription-Identifier属性时,EMQX 现在会将其判定为协议错误(Protocol Error),并主动断开该客户端连接。读完本文,你将理解 MQTT 5.0 中Subscription-Identifier属性的合法使用场景、EMQX 从报文解析到连接断开的完整校验链路,以及该修复对服务端安全性与协议互操作性的实际意义。
背景:Subscription-Identifier 属性在 MQTT 5.0 中的定位
Subscription-Identifier(订阅标识符)是 MQTT 5.0 引入的可变报头属性,其属性标识符(Property Identifier)为0x0B,取值为一个可变字节整数,范围是 1 到 268,435,455(0xFFFFFFF)。
按照 MQTT 5.0 规范,该属性仅允许出现在两类报文中:
- SUBSCRIBE 报文:客户端在订阅时附带一个或多个订阅标识符,用于把某条订阅与一个业务标识绑定;
- 服务端下发的 PUBLISH 报文:服务端在向客户端投递消息时,把与该消息匹配的订阅所对应的订阅标识符放入 PUBLISH 属性中,客户端据此可以快速判断"这条消息来自哪条订阅",从而在单连接上实现类似多租户的路由分流,无需解析主题。
关键约束:客户端发送给服务端的 PUBLISH 报文不得包含Subscription-Identifier属性。规范明确,若出现该情况,服务端必须将其视为协议错误(Protocol Error),并按协议错误流程处理(发送带原因码0x82的 DISCONNECT 报文并关闭连接)。
这个约束背后的逻辑很清晰:订阅标识符的"生产者"只能是服务端(它在投递时生成),客户端没有资格在发布消息时伪造订阅标识。如果允许客户端任意携带该属性,既违背规范语义,也可能被用于混淆服务端对消息来源的判断。
修复内容概述
本次修复(changelog 原文):
Fixed MQTT v5 protocol handling for invalid PUBLISH properties. If a client sends a PUBLISH packet containing
Subscription-Identifier, EMQX now treats it as a protocol error and disconnects the client.
即:此前 EMQX 对客户端 PUBLISH 中携带的非法Subscription-Identifier属性缺乏严格的协议校验;修复后,一旦检测到该属性出现在客户端 PUBLISH 报文中,EMQX 将其按协议错误处理并断开连接。
源码级解析:从属性解析到连接断开的完整链路
要理解这次修复,可以沿 EMQX 处理一条入站 PUBLISH 报文的完整路径,逐层查看相关实现。
第一步:帧层属性解析(emqx_frame.erl)
EMQX 的 MQTT 帧解析器位于 apps/emqx/src/emqx_frame.erl。在解析报文属性时,属性标识符0x0B被识别为Subscription-Identifier,其值按可变字节整数解析并存入属性映射:
parse_property(<<16#0B, Bin/binary>>, Props, StrictMode, MaxUserProps, NUserProps) -> {Val, Rest} = parse_variable_byte_integer(Bin), parse_property( Rest, put_prop('Subscription-Identifier', Val, Props, StrictMode), StrictMode, MaxUserProps, NUserProps );这段代码位于 emqx_frame.erl。可以看到,帧解析层对属性本身是"中立"的——它只负责把字节流正确地解码成属性键值对,至于该属性出现在哪种报文中是否合法,交由上层报文校验逻辑判定。帧层还负责属性序列化(serialize_property),即服务端在生成下行 PUBLISH / CONNACK 时按相同规则编码。
第二步:报文合法性校验(emqx_packet.erl)
报文级别的语义校验集中在 apps/emqx/src/emqx_packet.erl 的emqx_packet:check/1,2中。本次修复的核心逻辑就在 PUBLISH 属性的专项校验函数check_pub_props/1:
check_pub_props(#{'Topic-Alias' := 0}) -> {error, ?RC_TOPIC_ALIAS_INVALID}; check_pub_props(#{'Subscription-Identifier' := _}) -> {error, ?RC_PROTOCOL_ERROR}; check_pub_props(#{'Response-Topic' := ResponseTopic}) -> try emqx_topic:validate(name, ResponseTopic) of true -> ok catch error:_Error -> {error, ?RC_PROTOCOL_ERROR} end; check_pub_props(_Props) -> ok.代码见 emqx_packet.erl。要点如下:
- 只要 PUBLISH 属性映射中出现
Subscription-Identifier键,立即返回{error, ?RC_PROTOCOL_ERROR}(协议错误原因码); - 其余 PUBLISH 属性按各自规则继续校验,例如
Topic-Alias不允许为 0、Response-Topic必须是合法主题名; - 该校验属于 PUBLISH 报文整体
check/1流程的一部分,与 SUBSCRIBE 报文的校验(check_subscribe/3)相互独立。
作为对照,emqx_packet.erl 中 SUBSCRIBE 报文的Subscription-Identifier校验只检查取值范围(1..0xFFFFFFF),超范围才返回?RC_SUBSCRIPTION_IDENTIFIERS_NOT_SUPPORTED——这正体现了"同一属性在不同报文中的合法性与语义完全不同"这一设计原则。
第三步:通道层执行断开(emqx_channel.erl)
报文经帧解析后,由通道进程emqx_channel处理。在 apps/emqx/src/emqx_channel.erl,PUBLISH 报文进入handle_in后先做合法性检查:
handle_in(?PUBLISH_PACKET(_QoS, _Topic, _PacketId) = Packet, Channel) -> case emqx_packet:check(Packet) of ok -> ?EXT_TRACE_CLIENT_PUBLISH(...), ... {error, ReasonCode} -> ?TRACE("MQTT", "invalid_publish_packet", #{reason => emqx_reason_codes:name(ReasonCode)}), handle_out(disconnect, ReasonCode, Channel) end;当emqx_packet:check(Packet)返回{error, ?RC_PROTOCOL_ERROR}(即上述check_pub_props的判定结果)时,通道层调用handle_out(disconnect, ?RC_PROTOCOL_ERROR, Channel):
- 服务端向客户端发送DISCONNECT 报文,原因码为协议错误(
0x82); - 随后关闭该客户端的网络连接。
这与 MQTT 5.0 规范对协议错误的处置要求一致:服务端应发送 DISCONNECT(原因码 0x82)并终止连接。handle_out(disconnect, ...)在本文件中多处被用于各类协议错误场景(如 emqx_channel.erl),是 EMQX 统一的协议错误出口。
服务端正常使用侧:Subscription-Identifier 的合法路径
为了对照理解,再看服务端合法使用该属性的两条路径:
路径一:SUBSCRIBE → 存储。客户端在 SUBSCRIBE 中携带订阅标识符时,emqx_channel.erl 的enrich_subopts_subid会把它写入对应订阅选项的subid字段,随后用于会话/订阅管理:
enrich_subopts_subid(TopicFilters, #{sub_props := #{'Subscription-Identifier' := SubId}}) -> [{Topic, SubOpts#{subid => SubId}} || {Topic, SubOpts} <- TopicFilters]; enrich_subopts_subid(TopicFilters, _State) -> TopicFilters.路径二:CONNACK 能力声明。EMQX 在 CONNACK 中向客户端声明自己支持订阅标识符(emqx_channel.erl):
NAckProps = AckProps#{ ... 'Subscription-Identifier-Available' => 1, ... },只有服务端在 CONNACK 中声明Subscription-Identifier-Available: 1,客户端才被允许在 SUBSCRIBE 中使用订阅标识符;而无论服务端是否支持,客户端 PUBLISH 都绝不允许携带该属性。本次修复正是堵住了后一个方向的合规漏洞。
修复的工程价值
从工程角度看,本次修复带来三个层面的收益:
- 协议合规性:EMQX 作为 MQTT Broker,其入站报文校验必须与 MQTT 5.0 规范保持一致。对非法 PUBLISH 属性不再"宽容放行",避免出现规范不允许的报文被静默接受的兼容性偏差。
- 安全与健壮性:协议错误的客户端(可能是实现有缺陷的 SDK,也可能是恶意构造报文的攻击者)会在连接建立后立即被识别并断开,防止其利用该连接持续发送畸形报文,减轻服务端无谓的资源占用。
- 互操作性保障:其他严格遵守规范的客户端和服务端可以确信,EMQX 对 PUBLISH 属性的处理符合 MQTT 5.0 语义,降低跨厂商联调时因属性误用导致的隐性故障。
排查与验证建议
如果你在升级 EMQX 后观察到某些 MQTT 5.0 客户端频繁被断开,可按下述思路排查:
- 抓取客户端发出的 PUBLISH 报文,检查其属性列表中是否包含
Subscription-Identifier(属性标识0x0B)。合规的客户端 SDK 不应在发布消息时添加该属性; - 在 EMQX 日志中检索
invalid_publish_packet跟踪日志,其reason字段会给出protocol_error之类的具体原因(对应 emqx_channel.erl 的?TRACE输出); - 若是自研客户端,请在发布路径上移除
Subscription-Identifier属性;若使用第三方 SDK,可联系 SDK 维护方确认其 MQTT 5.0 属性处理是否符合规范。
小结
fix-16782是一个典型的"小而关键"的协议合规修复:它在 emqx_packet.erl 中为客户端 PUBLISH 报文增加Subscription-Identifier属性的非法性判定,并由 emqx_channel.erl 在报文入口统一执行"协议错误即断开"的处理策略。整条链路——帧解析(emqx_frame.erl)→ 语义校验(emqx_packet.erl)→ 通道处置(emqx_channel.erl)——清晰展现了 EMQX 对 MQTT 5.0 报文"先校验、后处理"的设计思想,也提醒客户端开发者:MQTT 5.0 的属性使用有严格的报文上下文约束,任何"想当然"的属性携带都可能触发服务端的协议错误处理。
- 后端
- 物联网
- 消息队列
- 通信
【免费下载链接】emqx
The most scalable and reliable MQTT broker for AI, IoT, IIoT and connected vehicles
相关推荐
EMQX 严格模式(strict_mode)拒绝重复 MQTT v5 属性:行为变更与配置指南
EMQX 严格模式(strict_mode)拒绝重复 MQTT v5 属性:行为变更与配置指南 导读 :本文围绕 EMQX 5.x 中关于 MQTT v5 报文
后端物联网消息队列通信MQTT 5.0全支持!EMQX协议兼容性与性能测试报告
MQTT 5.0全支持!EMQX协议兼容性与性能测试报告 引言:MQTT 5.0时代的消息传递挑战 你是否正在为物联网项目选择合适的MQTT代理?面对海量设备连
后端物联网消息队列通信EMQX 消除 MQTT v5 CONNACK 拒绝连接后的 `unclean_terminate` 误报警告日志(PR 15872 修复解析)
EMQX 消除 MQTT v5 CONNACK 拒绝连接后的 unclean_terminate 误报警告日志(PR 15872 修复解析) 导读 本文围绕 E
后端物联网消息队列通信
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考