ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

EMQX 会话过期时间上限收敛:DISCONNECT 报文中的 Session Expiry Interval 钳制机制解析

EMQX 会话过期时间上限收敛:DISCONNECT 报文中的 Session Expiry Interval 钳制机制解析 后端物联网消息队列通信【免费下载链接】emqxThe most scalable and reliable MQTT broker for AI, IoT, IIoT and connected vehicles项目地址https://gitcode.com/gh_mirrors/em/emqx点击查看免费下载导读本篇文章聚焦 EMQX 对 MQTT 5.0Session-Expiry-Interval会话过期时间间隔上限max_session_expiry_interval的强制收敛能力重点讲解一项重要修复该上限此前只作用于 CONNECT 阶段客户端声明的值客户端可以在 DISCONNECT 报文里再次抬高会话过期时间从而绕过限制修复后EMQX 会在 DISCONNECT 阶段对客户端携带的Session-Expiry-Interval同样执行钳制clamp。读完本文你将掌握该配置项的作用范围、修复前后的行为差异、底层源码调用链以及对应的测试验证用例能够据此在真实集群中正确配置并核查会话过期上限是否真正生效。一、问题背景会话过期上限的绕过漏洞在 MQTT 5.0 协议中Session-Expiry-Interval是一个核心属性它决定会话在连接断开后继续保留的时间CONNECT 报文MQTT 5.0 规范 3.1.2.11.2中的该属性声明连接建立后会话的过期时间DISCONNECT 报文MQTT 5.0 规范 3.14.2.2.2中的该属性则可以在断开连接时重新指定会话过期时间。EMQX 提供了mqtt.max_session_expiry_interval配置项用于限制客户端可请求的最大会话过期时间。然而在修复之前这一上限只作用于 CONNECT 阶段的值存在一个明显的绕过路径客户端在 CONNECT 时声明一个不超过上限的会话过期时间随后在 DISCONNECT 时携带一个远超上限的Session-Expiry-Interval值。由于 DISCONNECT 阶段的处理没有做钳制会话实际过期时间会被延长到超过配置上限。这正是 changes/ee/fix-18477.en.md 所记录的问题客户端可以在断开连接时把会话过期时间扩展到配置限制之外。修复的核心内容即一句话将mqtt.max_session_expiry_interval应用于客户端在 DISCONNECT 报文中携带的会话过期时间此前该上限只作用于 CONNECT 阶段提供的值因此客户端可以在断开时将会话过期时间扩展到配置上限之外。二、配置项全景max_session_expiry_interval 的定位与默认值2.1 Schema 定义该配置项在 apps/emqx/src/emqx_schema.erl 中定义{max_session_expiry_interval, sc( hoconsc:union([duration(), infinity]), #{ default infinity, desc ?DESC(mqtt_max_session_expiry_interval), importance ?IMPORTANCE_LOW } )},类型hoconsc:union([duration(), infinity])即既可以是时长duration字符串如1h、30m、3600s也可以是infinity默认值infinity表示不限制客户端请求的任意会话过期时间都会被原样采纳它属于mqtt配置段下的session子项与相邻的session_expiry_interval、message_expiry_interval等配置共同构成会话生命周期管理配置族。2.2 官方描述在 rel/i18n/emqx_schema.hocon 中有对该配置的完整描述Caps the maximum session expiry interval that an MQTT 5.0 client may request via itsSession-Expiry-IntervalCONNECT property. If the client requests a longer value, the server silently clamps it down to this limit (and reflects the clamped value back to the client via theSession-Expiry-Intervalproperty in CONNACK, per MQTT 5.0 spec section 3.2.2.3.2). Has no effect on MQTT 3.1.1 / 3.1 clients, whose session expiry is fully server-controlled viasession_expiry_interval. Default:infinity(no clamp).关键信息可以归纳为行为维度说明生效协议仅对 MQTT 5.0 客户端生效不生效协议MQTT 3.1.1 / 3.1 客户端其会话过期完全由服务端session_expiry_interval控制钳制动作客户端请求值超过上限时服务端静默钳制到上限并通过 CONNACK 的Session-Expiry-Interval属性将钳制后的值回显给客户端默认值infinity不钳制2.3 配置示例在etc/emqx.conf或集群级配置中典型配置形如mqtt { session { max_session_expiry_interval 1h } }设置后任何 MQTT 5.0 客户端在 CONNECT 或 DISCONNECT 中声明的会话过期时间若超过 1 小时都会被收敛到 1 小时。三、修复前的行为缺陷DISCONNECT 阶段的钳制缺失3.1 CONNECT 阶段早已有钳制修复之前CONNECT 阶段的钳制逻辑已经存在。核心实现在 apps/emqx/src/emqx_channel.erl%% If the Session Expiry Interval is absent the value 0 is used. expiry_interval(Zone, #mqtt_packet_connect{ proto_ver ?MQTT_PROTO_V5, properties ConnProps }) - RequestedSec emqx_mqtt_props:get(Session-Expiry-Interval, ConnProps, 0), MaxMs get_mqtt_conf(Zone, max_session_expiry_interval), timer:seconds(clamp_session_expiry(RequestedSec, MaxMs)); expiry_interval(Zone, #mqtt_packet_connect{clean_start false}) - get_mqtt_conf(Zone, session_expiry_interval); expiry_interval(_, #mqtt_packet_connect{clean_start true}) - 0. clamp_session_expiry(RequestedSec, infinity) - RequestedSec; clamp_session_expiry(RequestedSec, MaxMs) when is_integer(MaxMs) - min(RequestedSec, MaxMs div 1000).这里可以提炼出三条明确的语义MQTT 5.0从 CONNECT 属性中取出Session-Expiry-Interval缺省按 0 处理与max_session_expiry_interval比较后取mininfinity配置直接原样放行MQTT 3.1.1 且clean_sessionfalse不使用客户端请求值直接采用 zone 配置session_expiry_interval与max_session_expiry_interval无关MQTT 3.1.1 且clean_sessiontrue会话过期时间恒为 0即连接断开立即结束会话。3.2 缺陷所在DISCONNECT 路径未走钳制在修复前DISCONNECT 的处理路径是apps/emqx/src/emqx_channel.erlprocess_disconnect( ?DISCONNECT_PACKET(ReasonCode, Properties), Channel #channel{conninfo ConnInfo} ) - NConnInfo ConnInfo#{disconn_props Properties}, NChannel maybe_clean_will_msg(ReasonCode, Channel#channel{conninfo NConnInfo}), post_process_disconnect(ReasonCode, Properties, NChannel). %% MQTT-v5.0: 3.14.2.2.2 Session Expiry Interval post_process_disconnect( _ReasonCode, #{Session-Expiry-Interval : Interval}, Channel #channel{conninfo #{expiry_interval : 0}} ) when Interval 0 - handle_out(disconnect, ?RC_PROTOCOL_ERROR, Channel); post_process_disconnect(ReasonCode, Properties, Channel) - NChannel maybe_update_expiry_interval(Properties, Channel), {ok, {close, disconnect_reason(ReasonCode)}, NChannel}.post_process_disconnect/3首先遵循 MQTT 5.0 规范 3.14.2.2.2如果 CONNECT 时会话过期时间为 0即会话不持久而 DISCONNECT 中却携带了非 0 值则按协议错误处理发送0x82PROTOCOL_ERROR并断开连接。在协议错误检查通过后进入maybe_update_expiry_interval/2更新会话过期时间——但这一步骤在修复前没有经过clamp_session_expiry/2的钳制客户端传入多少就存多少从而绕过了max_session_expiry_interval的限制。四、修复后的实现DISCONNECT 阶段同样钳制修复后的关键改动在 apps/emqx/src/emqx_channel.erl 的maybe_update_expiry_interval/2maybe_update_expiry_interval( #{Session-Expiry-Interval : Interval}, Channel #channel{conninfo ConnInfo, clientinfo #{zone : Zone}} ) - MaxMs get_mqtt_conf(Zone, max_session_expiry_interval), ClampedSec clamp_session_expiry(Interval, MaxMs), case ClampedSec Interval of true - ?TRACE(MQTT, session_expiry_interval_clamped, #{ requested_seconds Interval, clamped_seconds ClampedSec }); false - ok end, EI timer:seconds(ClampedSec), OldEI maps:get(expiry_interval, ConnInfo, 0), case OldEI : EI of true - Channel; false - NChannel Channel#channel{conninfo ConnInfo#{expiry_interval EI}}, %% Check if the client turns off persistence (turning it on is disallowed) case EI : 0 andalso OldEI 0 of true - ok emqx_session:destroy(NChannel#channel.session), NChannel#channel{session undefined}; false - NChannel end end; maybe_update_expiry_interval(_Properties, Channel) - Channel.新逻辑的完整行为链条如下取出上限通过get_mqtt_conf(Zone, max_session_expiry_interval)按 zone 获取当前配置的上限钳制计算调用与 CONNECT 阶段完全相同的clamp_session_expiry/2即min(RequestedSec, MaxMs div 1000)infinity配置原样放行降级可观测当钳制确实发生ClampedSec Interval时输出名为session_expiry_interval_clamped的 MQTT 跟踪日志同时携带requested_seconds客户端请求值与clamped_seconds钳制后值便于排障更新会话状态将钳制后的值写入conninfo中的expiry_interval关闭持久化特判如果钳制后的值为 0 且旧值大于 0即客户端想关闭持久化则立即销毁会话emqx_session:destroy/1并清空 channel 中的 session 引用——这里保留了 MQTT 5.0 只允许“关闭持久化”、不允许“重新开启持久化”的语义缺省分支若 DISCONNECT 报文未携带Session-Expiry-Interval属性则保持原值不变。4.1 与 CONNECT 阶段共享的钳制函数值得注意的是修复后 CONNECT 与 DISCONNECT 两条路径都收敛到同一个clamp_session_expiry/2apps/emqx/src/emqx_channel.erl保证了语义的一致性clamp_session_expiry(RequestedSec, infinity) - RequestedSec; clamp_session_expiry(RequestedSec, MaxMs) when is_integer(MaxMs) - min(RequestedSec, MaxMs div 1000).配置为infinity时任何请求值都被原样保留修复前默认行为也是向后兼容的关键配置为整数毫秒时先除以 1000 换算成秒再与请求秒数取最小值。4.2 修复带来的行为变化对比场景修复前修复后CONNECT 请求 3600s上限 30s钳制为 30s钳制为 30s不变CONNECT 请求 10sDISCONNECT 请求 3600s上限 30s会话过期时间变为 3600s绕过上限钳制为 30sCONNECT 请求 10sDISCONNECT 请求 60s上限 1h60s60s未超上限原样生效上限为infinityDISCONNECT 请求 86400s86400s86400s不钳制DISCONNECT 请求 0关闭持久化上限 30s会话立即销毁会话立即销毁钳制不会把 0 抬成非 0CONNECT 时过期时间为 0DISCONNECT 携带非 0 值协议错误协议错误协议检查优先于钳制五、测试验证专测套件如何锁定新行为修复附带了一个专门的测试套件 apps/emqx/test/emqx_session_expiry_clamp_SUITE.erl覆盖 CONNECT 与 DISCONNECT 两条路径上的钳制行为其中与本次修复直接相关的用例包括5.1 DISCONNECT 超上限被钳制t_disconnect_above_cap_is_clamped/1第 197-205 行上限设为 30 秒CONNECT 时请求 10 秒随后在 DISCONNECT 中请求 3600 秒断言最终stored_expiry_ms(ClientId)等于 30 秒上限t_disconnect_above_cap_is_clamped(_Config) - MaxMs timer:seconds(30), set_max(MaxMs), ClientId v5-disc-above-cap, {Client, _Connack} connect_v5( ClientId, true, #{Session-Expiry-Interval 10} ), send_disconnect(Client, #{Session-Expiry-Interval 3600}), ?retry(100, 50, ?assertEqual(MaxMs, stored_expiry_ms(ClientId))).5.2 DISCONNECT 未超上限保持不变t_disconnect_below_cap_unchanged/1第 211-218 行上限设为 1 小时DISCONNECT 请求 60 秒断言最终存储值恰为 60 秒。5.3 默认 infinity 配置下原样生效t_disconnect_infinity_unchanged/1第 224-231 行使用默认infinity上限DISCONNECT 请求 86400 秒被完整保留——这保证了默认配置下行为与修复前完全一致不破坏现有部署。5.4 钳制不得遮蔽协议错误检查t_disconnect_nonzero_after_zero_protocol_error/1第 255-265 行CONNECT 时过期时间为 0、DISCONNECT 携带非 0 值即使配置了 1 秒上限仍必须按 MQTT 5.0 规范返回PROTOCOL_ERROR证明钳制逻辑位于协议检查之后、不会遮蔽规范要求的错误处理。5.5 零值语义不被破坏t_disconnect_zero_destroys_session/1第 237-248 行配置上限时DISCONNECT 请求 0 依然销毁会话emqx_cm:get_chan_info(ClientId)返回undefined说明min(0, MaxMs div 1000) 0钳制不会把 0 变成非 0。套件通过init_per_suite/1以mqtt { session_expiry_interval 1h }启动 EMQX并在每个用例前用emqx_config:put_zone_conf(default, [mqtt, max_session_expiry_interval], ...)动态调整上限init_per_testcase默认重置为infinity展示了该配置项可在运行时热更新、逐用例验证的特性。六、配置建议与排查指引6.1 何时需要配置该上限资源可控希望约束单个客户端占用会话存储的时间避免恶意或异常客户端无限期延长会话导致内存/持久化资源被长期占用合规与运维策略业务上有明确的会话保留期限要求例如最长 24 小时、7 天等默认不干预若无需限制保持默认infinity即可客户端声明多少便保留多少注意仍需配合session_expiry_interval管理 MQTT 3.x 客户端的会话保留。6.2 建议的完整配置mqtt { session { # MQTT 5.0 客户端可请求的最大会话过期时间超过将被静默收敛。 # 支持时长写法30s / 15m / 2h / 7d或 infinity不限制 max_session_expiry_interval 24h # MQTT 3.1.1 / 3.1 客户端clean_sessionfalse的会话过期时间 session_expiry_interval 2h } }6.3 验证与排障配置后生效范围该上限只影响 MQTT 5.0 客户端MQTT 3.1.1/3.1 客户端的行为由session_expiry_interval全权决定不受该配置影响观察钳制是否发生开启 MQTT 跟踪日志后当 DISCONNECT 请求值被收敛时会输出session_expiry_interval_clamped跟踪记录其中包含requested_seconds与clamped_seconds两个字段可据此确认收敛行为运行时验证可在测试环境通过emqx_config:put_zone_conf/3之类的配置通道动态调整该值配合 apps/emqx/test/emqx_session_expiry_clamp_SUITE.erl 中的用例思路编写端到端验证使用 MQTT 5.0 客户端分别在 CONNECT 与 DISCONNECT 中声明超过上限的会话过期时间观察实际存储值是否被收敛到上限。结语本次修复changes/ee/fix-18477.en.md填补了 EMQX 会话过期上限在 DISCONNECT 路径上的漏洞此前客户端只需在断开连接时重新声明一个更大的Session-Expiry-Interval即可绕过mqtt.max_session_expiry_interval现在该上限在 CONNECT 与 DISCONNECT 两条路径上统一生效且以共享的clamp_session_expiry/2保证语义一致。修复同时兼顾了三点细节infinity默认值下行为完全向后兼容、0 值关闭持久化的语义不被破坏、MQTT 5.0 的协议错误检查优先于钳制逻辑。对于需要严格控制会话资源占用、实施会话保留策略的 EMQX 运维与开发者而言正确配置max_session_expiry_interval并借助上述跟踪日志与测试用例验证收敛行为是确保策略真正落地的关键。赞分享后端物联网消息队列通信【免费下载链接】emqxThe most scalable and reliable MQTT broker for AI, IoT, IIoT and connected vehicles项目地址https://gitcode.com/gh_mirrors/em/emqx点击查看免费下载相关推荐EMQX mqtt.max_session_expiry_interval 配置为 MQTT 5.0 会话过期间隔设置服务端上限EMQX mqtt.max_session_expiry_interval 配置为 MQTT 5.0 会话过期间隔设置服务端上限 导读 本文围绕 EMQX 新后端物联网消息队列通信EMQX 会话上限超限后的重连恢复机制解析——基于 v5.8.5 行为修复 14654EMQX 会话上限超限后的重连恢复机制解析——基于 v5.8.5 行为修复 14654 导读 本文围绕 EMQX 仓库变更记录 fix 14654.en.md后端物联网消息队列通信EMQX 会话历史保留session_history_retain清理修复深度解析从 10,000 会话上限 Bug 到分块 GC 机制EMQX 会话历史保留session_history_retain清理修复深度解析从 10,000 会话上限 Bug 到分块 GC 机制 导读 本文围绕后端物联网消息队列通信上一篇PoeCharm终极指南如何用中文角色构建器快速打造流放之路强力BD下一篇Windows系统优化革命Win11Debloat如何重塑你的数字工作空间创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表