ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

EMQX 畸形首包处理改进:无效 CONNECT 分类与协议提示日志深度解析

EMQX 畸形首包处理改进:无效 CONNECT 分类与协议提示日志深度解析 EMQX 畸形首包处理改进无效 CONNECT 分类与协议提示日志深度解析【免费下载链接】emqxThe most scalable and reliable MQTT broker for AI, IoT, IIoT and connected vehicles项目地址: https://gitcode.com/gh_mirrors/em/emqx导读MQTT 服务端口上经常会出现非 MQTT 流量——浏览器误连、健康检查探测、运维工具扫描甚至是把 HTTP、SSH、Redis 等协议的客户端直接指到了 1883 端口。EMQX 在 fix-16779 变更中系统性地改进了对这些畸形首包malformed first packets的处理把 CONNECT 之前到达的任何非 MQTT 数据包统一分类为无效 CONNECT 数据包invalid CONNECT packets并在关闭连接时输出带有协议猜测提示protocol hints的日志帮助运维人员一眼判断这到底是谁在连接、发来了什么。本文基于 changes/ee/fix-16779.en.md 展开结合 emqx_frame.erl、emqx_channel.erl、emqx_connection.erl 的源码实现与测试用例完整还原这一改进的前因后果与底层机制。一、问题背景为什么首包校验如此关键MQTT 连接建立的第一个数据包必须是 CONNECT 报文。EMQX 的连接解析流程中第一个到达的字节流会进入 emqx_frame.erl 的parse/2进行帧解析而validate_connect_first/2承担着首包必须是 CONNECT的守门职责%% Reject any packet received before CONNECT, while only the fixed header is %% read, so that no body byte is buffered for an unauthenticated connection. validate_connect_first(?CONNECT, _Options) - ok; validate_connect_first(_Type, #options{expect_connect false}) - ok; validate_connect_first(Type, _Options) - ?PARSE_ERR(#{ cause unexpected_packet_before_connect, header_type emqx_packet:type_name(Type) }).源码位置apps/emqx/src/emqx_frame.erl#L263-L273从这段代码可以看到三个关键设计只要固定头fixed header就能判定解析器只读取 1 字节固定头即可判断首包类型不需要缓冲任何 body 字节因此未经认证的连接不会占用内存缓冲区错误信息结构化错误以 map 形式携带cause unexpected_packet_before_connect和header_type如SUBSCRIBE、PUBLISH为上层分类统计提供了依据严格模式开关expect_connect选项控制 CONNECT 围栏fence是否生效emqx_frame:update_opts/2在协商完成后会清除该围栏但保留已协商的版本。二、核心改进一畸形首包统一分类为无效 CONNECT在 fix-16779 之前不同类型的首包错误走的是各自零散的处理路径。本次变更在 emqx_channel.erl 的handle_frame_error/2中为conn_state idle即 CONNECT 尚未解析阶段增加了专门的分支%% Frame error before CONNECT is parsed (conn_state still idle). %% This happens when the first packet is not a valid MQTT CONNECT, %% e.g. an HTTP request or other non-MQTT protocol sent to the MQTT port. %% No CONNACK is sent because no MQTT version has been negotiated yet. handle_frame_error( Reason, Channel #channel{conn_state idle} ) - shutdown( shutdown_count(frame_error_kind(Reason, invalid_connect_packet), Reason, Channel), Channel );源码位置apps/emqx/src/emqx_channel.erl#L1397-L1408这段实现体现了几个核心语义分类统一只要是在 CONNECT 被成功解析之前发生的帧错误无论原因是unexpected_packet_before_connect、bad_frame_header还是其他解析错误都通过frame_error_kind/2归入invalid_connect_packet这一计数类别而不是散落在frame_error大类中不发送 CONNACK由于此时尚未协商出任何 MQTT 版本协议双方没有共同语言因此直接关闭连接绝不回应 CONNACK连接进程退出原因保持为原子shutdown/2的关闭原因最终是原子形式的关闭计数这让连接监督者supervisor能维护稳定的 shutdown 计数器而不是报告为无法识别的 error 级关闭——畸形客户端输入属于客户端问题不应污染 broker 的错误日志。对照来看同样是帧错误emqx_channel.erl中针对不同连接阶段有不同的处置策略apps/emqx/src/emqx_channel.erl#L1384-L1448连接阶段处置方式已建立连接 frame_too_largeMQTT v5 发送DISCONNECTRC_PACKET_TOO_LARGE旧版本直接关闭conn_state idle本次变更分类为invalid_connect_packet直接关闭不发 CONNACKconn_state connecting解析 CONNECT 中出错MQTT v5 回发携带 reason code 的CONNACK旧版本静默关闭已建立连接上的其他帧错误发送DISCONNECT后关闭conn_state disconnected仅记录malformed_mqtt_messageinfo 日志保持连接状态不变计数器命名的设计意图frame_error_kind/2决定关闭计数器使用哪个名字apps/emqx/src/emqx_channel.erl#L1450-L1469frame_error_kind(Reason, _Default) when is_atom(Reason) - Reason; frame_error_kind(#{cause : frame_too_large}, _Default) - frame_too_large; frame_error_kind(#{cause : connect_packet_too_large}, _Default) - connect_packet_too_large; frame_error_kind(#{cause : too_many_user_properties}, _Default) - too_many_user_properties; frame_error_kind(_Reason, Default) - Default.设计要点计数器名必须来自有界集合bounded set否则指标会失控原子形式的错误直接用原子命名计数器map 形式的错误默认共享Default具体原因保留在关闭原因和 trace 中唯一例外是frame_too_large、connect_packet_too_large、too_many_user_properties——这三者表示客户端超过了运维配置的某个限额是独立的运维信号必须与客户端在发垃圾数据走invalid_connect_packet等默认分类区分开。运维人员看计数器时能明确分辨客户端撞到了你设的限流与客户端在发乱码。三、核心改进二日志中的协议提示protocol hints3.1 首包协议猜测器本次变更最直观的成果体现在日志上。新的guess_first_packet_protocol/1函数apps/emqx/src/emqx_frame.erl#L275-L285会对收到的畸形首包做两层信息提取guess_first_packet_protocol(Type:4, _Flags:4, _/binary Data) - {Preview, PreviewEncoding} format_data_prefix(Data, 32), #{ packet_type emqx_packet:type_name(Type), resemble_protocol guess_plaintext_protocol(Data), received_prefix Preview, received_prefix_encoding PreviewEncoding }; guess_first_packet_protocol(_Data) - #{}.它产出的字段包括packet_type首字节高 4 位解析出的 MQTT 报文类型名如CONNECT、PUBLISH用于确认这确实不是合法 CONNECTresemble_protocol最关键的协议猜测结果见下文received_prefix收到的数据前缀最多 32 字节供人工核对received_prefix_encoding前缀的编码方式printable表示可打印 ASCIIhex表示十六进制转储避免二进制垃圾污染日志可读性。3.2 协议指纹识别表guess_plaintext_protocol/1apps/emqx/src/emqx_frame.erl#L287-L330取首包前 16 字节做前缀匹配目前能识别以下协议指纹前缀特征识别结果典型场景GET/POST/PUT/HEAD/DELETE/OPTIONS/CONNECT/TRACE/PATCHhttp浏览器或 HTTP 客户端误连 MQTT 端口PRI * HTTP/2.0http2_prefaceHTTP/2 客户端含 curl --http2误连PROXYproxy_protocol_v1HAProxy 等未启用 PROXY 协议就转发\r\n\r\n\0\r\nQUIT\n特征前缀proxy_protocol_v2PROXY 协议 v2 二进制签名SSH-sshSSH 客户端指错端口EHLO/HELOsmtpSMTP 客户端误连*1\r\n/*2\r\n/*3\r\nredis_respRedis RESP 协议客户端误连可打印 ASCII32~126plain_text裸文本探测如MAIL FROM:其余情况unknown二进制数据配合 hex 前缀输出测试 apps/emqx/test/emqx_frame_SUITE.erl#L320-L406 的t_guess_first_packet_protocol/1覆盖了上表中的全部识别分支包括 8 种 HTTP 方法逐一验证以及二进制数据0,1,2,3,4应输出resemble_protocol : unknown且前缀以hex编码展示的用例。3.3 传输层错误转换与日志连接进程侧emqx_connection.erl 的handle_sock_error/2在收到 socket 错误时会先调用maybe_log_first_packet_non_mqtt/2apps/emqx/src/emqx_connection.erl#L1349-L1358maybe_log_first_packet_non_mqtt(emsgsize, #state{channel Channel}) - case emqx_channel:info(conn_state, Channel) of idle - ?SLOG(info, #{ msg first_packet_probably_not_mqtt, reason emsgsize }); _ - ok end; maybe_log_first_packet_non_mqtt(_Reason, _State) - ok.源码位置apps/emqx/src/emqx_connection.erl#L1397-L1408当连接仍处于idleCONNECT 未解析状态时emsgsize这类传输层错误会被记录为first_packet_probably_not_mqtt的 info 级日志——这一日志文案本身就是本次变更的一部分它明确告知运维人员首个数据包很可能不是 MQTT而不是含糊的 socket 错误。配套的connect_too_large_error/2apps/emqx/src/emqx_connection.erl#L1373-L1388处理另一种常见情况TCP 传输层用packet_size等于max_connect_packet_size在解析器看到任何字节之前就拒绝了超大的 CONNECTgen_tcp上报emsgsizessl上报{invalid_packet, Data}。该函数把这两种传输错误统一转换为与流式解析器一致的connect_packet_too_large帧错误保证 shutdown 计数器不随传输层不同而分裂同时避免把客户端原始字节带进进程退出原因。四、配套防线CONNECT 大小与剩余长度校验畸形首包不仅包括非 MQTT 协议还包括伪装成 CONNECT 的超大包。validate_frame_len/3apps/emqx/src/emqx_frame.erl#L375-L390在读取任何 body 字节之前就从固定头校验帧长度CONNECT 报文同时受max_connect_size与max_size双重限制——这是刻意设计未认证客户端不能让 broker 缓冲一整个max_size的 body因此 CONNECT 单独收紧到max_connect_packet_size超限时报connect_packet_too_large携带limit与received字段与frame_too_large区分对应上文的独立计数器剩余长度remaining length解析同样有保护可变字节整数超过 4 字节上限时报malformed_variable_byte_integerapps/emqx/src/emqx_frame.erl#L358-L361。五、测试验证行为被逐条固化本次变更的行为在测试中被系统性地固化是理解预期语义的最佳教材。5.1 连接层测试apps/emqx/test/emqx_connection_SUITE.erl#L248-L318 的t_parse_incoming/1覆盖了五种典型畸形首包垃圾字节严格模式下for_testing报bad_frame_header宽松解析器则会当作部分字节缓冲SUBSCRIBE 作为首包16#82, 16#00在 idle 状态报unexpected_packet_before_connect并携带header_type : SUBSCRIBE与resemble_protocol提示——这正是文档所述日志中增加协议提示的直接验证CONNECT 剩余长度为 016#10, 16#00报zero_remaining_lenv3.1.1 CONNECT 密码标志位异常按 MQTT 规范 [MQTT-3.1.2-22] 严格拒绝报invalid_password_flag同时携带proto_ver : 4、proto_name : MQTT等字段便于运维定位具体违规点已连接状态下的错误不做提示增强bad_subqos在 connected 状态下保持原子形式不做协议猜测等富化处理避免在正常连接上浪费开销。测试还通过meck强制emqx_frame:parse抛错验证异常路径同样被转换为frame_error而不是崩溃。5.2 协议猜测测试apps/emqx/test/emqx_frame_SUITE.erl#L320-L406 的t_guess_first_packet_protocol/1逐条验证了 HTTP/HTTP2/PROXY v1/v2/SSH/SMTP/Redis RESP/plain_text/unknown 共 9 类协议的识别指纹。其中对?CONNECT:4, 0:4, 0的断言packet_type : CONNECT说明即使首包确实是 CONNECT 类型也能被正确识别为上层区分合法 CONNECT 但解析失败与完全非 MQTT提供依据。5.3 传输层与 WebSocket 测试apps/emqx/test/emqx_socket_connection_SUITE.erl#L151-L154TCP 传输场景验证首包非 CONNECT 时报unexpected_packet_before_connect并携带resemble_protocolapps/emqx/test/emqx_ws_connection_SUITE.erl#L685WebSocket 场景下 PUBLISH 作为首包同样被拒。六、运维视角如何利用这些改进排查问题fix-16779 的最终价值体现在排障效率上。升级后遇到1883 端口有异常连接时可以从以下路径定位看日志搜索first_packet_probably_not_mqtt配合resemble_protocol、received_prefix、received_prefix_encoding字段即可判断是 HTTP 健康检查、Redis 客户端、PROXY 协议配置错误还是纯粹的二进制垃圾扫描看连接关闭计数器invalid_connect_packet计数器统计所有 CONNECT 前的帧错误connect_packet_too_large与frame_too_large单独统计限额违规通过三者的相对数量可以区分被误连攻击与客户端配置了过大的报文看 traceframe_error的完整 map含 cause 与具体字段会通过emqx_trace输出需要完整报文细节时可在此获取而连接进程的退出原因保持为原子形式不会因日志字段缺失而丢失监控语义。结语fix-16779 虽然是一个单点修复却完整覆盖了识别—分类—记录—统计四个环节emqx_frame在解析层识别畸形首包并猜测协议指纹emqx_channel按连接阶段将错误分类到有界的计数器集合emqx_connection在传输层补充first_packet_probably_not_mqtt日志并统一超大 CONNECT 的报错路径三套测试套件把每种预期行为固化成了可回归的断言。对于运维一个暴露在公网、面向海量异构客户端的 MQTT broker 而言这种把模糊的 socket 错误变成可读的协议诊断信息的改进正是可观测性的基础积累。【免费下载链接】emqxThe most scalable and reliable MQTT broker for AI, IoT, IIoT and connected vehicles项目地址: https://gitcode.com/gh_mirrors/em/emqx创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表