ARTICLE DETAIL

资讯详情

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

Suricata 应用层解析器(App Layer Parser)开发指南:回调协议与 AppLayerResult 返回值语义

Suricata 应用层解析器(App Layer Parser)开发指南:回调协议与 AppLayerResult 返回值语义 网络安全【免费下载链接】suricataSuricata is a network Intrusion Detection System, Intrusion Prevention System and Network Security Monitoring engine developed by the OISF and the Suricata community.项目地址https://gitcode.com/gh_mirrors/su/suricata点击查看免费下载导读本文是 Suricata 开发者指南doc/userguide/devguide/extending/app-layer/中“Parsers”一章的完整展开面向希望为 Suricata 扩展自定义应用层协议解析器的开发者。文章以解析器回调函数签名为骨架系统讲解AppLayerResult三种返回值APP_LAYER_OK/APP_LAYER_ERROR/APP_LAYER_INCOMPLETE的精确语义、consumed与needed的字节级计算规则并结合仓库源码C 与 Rust 两套实现给出可复制、可运行的示例。读完本文你将能正确实现一个遵守 Suricata 应用层 API 约束的协议解析回调并理解不完整数据INCOMPLETE机制在 TCP 流重组中的底层原理。1. 解析器回调API 在程序启动时注册的调用入口Suricata 的应用层App Layer框架通过回调机制驱动协议解析。这些回调在程序启动阶段setup 阶段完成注册运行时由流引擎与数据包处理路径统一调度解析器自身并不关心数据来自 TCP 重装流还是 UDP 数据报。1.1 回调函数原型解析回调的标准函数原型定义如下typedef AppLayerResult (*AppLayerParserFPtr)(Flow *f, void *protocol_state, AppLayerParserState *pstate, const uint8_t *buf, uint32_t buf_len, void *local_storage, const uint8_t flags);各参数含义Flow *f当前流对象携带流级状态方向、协议映射、文件标志等void *protocol_state协议私有状态如 HTTP 的htp_state由协议的StateAlloc回调创建并随流生命周期存在AppLayerParserState *pstate由应用层框架维护的解析器状态包含解码事件、事务进度等信息buf/buf_len本次交付给解析器的数据及其长度重组后的 TCP 流数据或 UDP 载荷local_storage解析器注册的本地存储flags数据方向等标志如STREAM_TOSERVER/STREAM_TOCLIENT。当前仓库的签名演进在本仓库的 src/app-layer-parser.h 中实际签名已把buf/buf_len/flags封装为StreamSlice结构体typedef struct StreamSlice { const uint8_t *input; uint32_t input_len; /// STREAM_* flags uint8_t flags; uint64_t offset; } StreamSlice; typedef AppLayerResult (*AppLayerParserFPtr)(Flow *f, void *protocol_state, AppLayerParserState *pstate, StreamSlice stream_slice, void *local_storage);StreamSlice在 src/app-layer-parser.h 中定义并通过StreamSliceGetData()/StreamSliceGetDataLen()等内联函数见 src/app-layer-parser.h访问数据。它额外携带offset流内偏移供解析器在跨分片处理时定位。编写新解析器时应以本仓库当前签名为准。1.2 C 语言示例HTTP 解析器是 C 侧最典型的实现。它的请求数据处理函数签名如下见 src/app-layer-htp.cstatic AppLayerResult HTPHandleRequestData(Flow *f, void *htp_state, AppLayerParserState *pstate, const uint8_t *input, uint32_t input_len, void *local_data, const uint8_t flags);该回调通过AppLayerParserRegisterParser()在注册阶段绑定到IPPROTO_TCP, ALPROTO_HTTP1, STREAM_TOSERVER方向见 src/app-layer-htp.c。1.3 Rust 语言示例Rust 侧解析器以#[no_mangle]导出 C ABI 函数与 C 原型一一对应。文档给出的 DNS TCP 响应解析示例#[no_mangle] pub extern C fn rs_dns_parse_response_tcp(_flow: *const core::Flow, state: *mut std::os::raw::c_void, _pstate: *mut AppLayerParserState, input: *const u8, input_len: u32, _data: *mut std::os::raw::c_void, _flags: u8) - AppLayerResult在本仓库的 rust/src/applayer.rs 中Rust 侧统一封装为ParseFn类型同样采用StreamSlice传递数据pub type ParseFn unsafe extern C fn( flow: *mut Flow, state: *mut c_void, pstate: *mut AppLayerParserState, stream_slice: StreamSlice, data: *mut c_void, ) - AppLayerResult;更贴近日常开发的是 rust/src/applayertemplate/template.rs 中parse_request的写法——解析器内部直接操作[u8]切片并在需要更多数据时返回AppLayerResult::incomplete(consumed, needed)fn parse_request(mut self, input: [u8]) - AppLayerResult { // Were not interested in empty requests. if input.is_empty() { return AppLayerResult::ok(); } // ... let mut start input; while !start.is_empty() { match parser::parse_message(start) { Ok((rem, request)) { start rem; let mut tx self.new_tx(); tx.request Some(request); // ... self.transactions.push_back(tx); } Err(nom::Err::Incomplete(_)) { // 数据不足先消费已解析的部分再要求更多字节 let consumed input.len() - start.len(); let needed start.len() 1; return AppLayerResult::incomplete(consumed as u32, needed as u32); } Err(_) { return AppLayerResult::err(); } } } // Input was fully consumed. return AppLayerResult::ok(); }该模板还展示了事务transaction的创建、事件上报set_event与TEMPLATE_MAX_TX上限约束是完整的 Rust 解析器骨架可直接作为新协议开发的起点。2. AppLayerResult解析器的唯一返回值所有解析回调统一返回AppLayerResult类型。在 C 侧其结构体定义位于 src/app-layer-parser.htypedef struct AppLayerResult { int32_t status; // 状态码0 OK-1 ERROR1 INCOMPLETE uint32_t consumed; // INCOMPLETE 时已消费的字节数 uint32_t needed; // INCOMPLETE 时还需要多少字节 } AppLayerResult;三种结果的可能取值与对应宏定义见 src/app-layer-parser.hC 返回值Rust 返回值语义APP_LAYER_OKAppLayerResult::ok()解析器成功消费了本次数据APP_LAYER_ERRORAppLayerResult::err()解析器遇到不可恢复错误本流停止处理APP_LAYER_INCOMPLETE(c,n)AppLayerResult::incomplete(c,n)解析器消费了c字节还需要n字节才会再次被调用Rust 侧这三种构造方法由AppLayerResultRusttrait 提供见 rust/ffi/src/applayer.rs其底层实现直接映射 C 结构体impl AppLayerResultRust for AppLayerResult { /// parser has successfully processed in the input, and has consumed all of it fn ok() - Self { Default::default() // status 0 } /// parser has hit an unrecoverable error... fn err() - Self { AppLayerResult { status: -1, ..Default::default() } } /// parser needs more data... fn incomplete(consumed: u32, needed: u32) - Self { Self { status: 1, consumed, needed } } }C 侧宏定义与之等价见 src/app-layer-parser.h#define APP_LAYER_OK (AppLayerResult) { 0, 0, 0 } #define APP_LAYER_ERROR (AppLayerResult) { -1, 0, 0 } #define APP_LAYER_INCOMPLETE(c,n) (AppLayerResult) { 1, (c), (n) }对i32和bool返回值Rust 解析器还可以直接使用.into()转换为AppLayerResult简化简单分支的代码。3. 三种返回值的精确定义与调用约定3.1 APP_LAYER_OK / AppLayerResult::ok()返回 “OK” 表示解析器已成功消费本次交付的全部数据。API 将在有更多数据可用时再次调用解析器。这是最常见的返回值大部分协议解析循环在成功处理完一批数据后返回它。例如 rust/src/bittorrent_dht/bittorrent_dht.rs 中解析器按方向循环解析消息任何一条消息解析失败即转为AppLayerResult::err()否则保持ok()。3.2 APP_LAYER_ERROR / AppLayerResult::err()返回 “ERROR” 表示解析器遇到了不可恢复的错误API 将停止处理该协议且整个流的后续数据都不会再交给该解析器对应当前流应用层跟踪的终止。关键约束文档明确强调不要用它来表示可恢复的错误。可恢复错误应通过设置解码事件events来处理而不是返回 ERROR。在 src/app-layer-parser.c 中AppLayerParserParse()对返回值进行分发res.status 0时累加解析器错误计数器并跳转到错误处理路径该流即被标记为不再进行应用层解析。C 侧的真实使用可以参考 FTP 解析器src/app-layer-ftp.c其函数注释明确写着* \retval APP_LAYER_OK when input was process successfully * \retval APP_LAYER_ERROR when a unrecoverable error was encountered例如命令解析彻底失败时直接SCReturnStruct(APP_LAYER_ERROR)见 src/app-layer-ftp.c。3.3 APP_LAYER_INCOMPLETE / AppLayerResult::incomplete()这是最有价值也最容易被误用的返回值。许多协议以记录record为单位传输记录长度常常写在记录头部的最前几个字节。当解析器只拿到一条记录的部分数据时它可以读出记录头中的长度字段返回APP_LAYER_INCOMPLETE(consumed, needed)告诉 API “本次已消费consumed字节还需要needed字节才足够处理下一条记录”API 会推迟下一次调用直到凑够所需数据或流结束。consumed 与 needed 的精确语义consumed本次交付数据中已经被处理掉的字节数从本次输入起点开始计算needed在已消费字节之上还需要多少字节才能继续解析。文档给出的示例图解[ 32 record 1 ][ 32 record 2 ][ 32 r.. ] 0 31 32 63 64 72 ^ ^ consumed: 64 ---------------/ | needed: 32 -------------------/即前两条各 32 字节的记录已完整消费consumed 64第三条记录只有 8 字节64..72还差 24 字节才能凑满 32 字节的记录因此needed 32注意这里的needed指“在已消费 64 字节之上再需要的字节数”。引擎侧的校验与处理在 src/app-layer-parser.c 中AppLayerParserParse()对 INCOMPLETE 返回值做了严格的合法性校验DEBUG_VALIDATE_BUG_ON(res.consumed input_len); DEBUG_VALIDATE_BUG_ON(res.needed input_len - res.consumed); DEBUG_VALIDATE_BUG_ON(res.needed 0); /* incomplete is only supported for TCP */ DEBUG_VALIDATE_BUG_ON(f-proto ! IPPROTO_TCP); /* 对不合法使用返回码的情况将协议置于错误状态 */ if (res.consumed input_len || res.needed res.consumed input_len) { AppLayerIncInternalErrorCounter(tv, f); goto error; }校验规则可归纳为consumed不得超过本次输入长度consumed needed必须不小于本次输入长度即需要的字节数必须覆盖剩余未处理部分needed不能为 0为 0 意味着没有等待的必要应直接返回 OKINCOMPLETE 仅对 TCP 有效UDP 是数据报语义无流式重组直接返回即可。校验通过后引擎会把needed写入 TCP 会话对应方向的data_required字段见 src/app-layer-parser.cif (direction 0) { /* 告诉流引擎还需要多少数据才再次调用解析器 */ ssn-client.data_required res.needed; } else { ssn-server.data_required res.needed; }这样流引擎stream engine就知道在收到足够多的重组数据之前不必再调用解析器——这正是 INCOMPLETE 机制减少无效调用、降低 CPU 开销的原理。再次被调用的两个时机文档明确解析器会在以下两种情况下被再次调用needed指定的数据已凑齐——正常情况解析器可继续解析完整记录流结束EOF时——此时交付的数据必然是不完整的解析器必须自行决定如何处理尾部残片例如丢弃、上报事件或按不完整记录处理。引擎侧AppLayerParserParse()的调用条件也印证了这一点if (input_len 0 || (flags STREAM_EOF))见 src/app-layer-parser.c即 EOF 时即使无数据也会调用一次解析器让其处理流尾状态。4. 支持不完整数据记录头 流式数据体的实战模式4.1 为什么需要流式处理对某些协议完全缓冲queue整条记录再处理可能是巨大的资源浪费。文档指出SMB 和 NFS 在文件传输中可能使用非常大的记录——如果把整个文件体都等齐再交给解析器内存开销不可接受。此时应采用“记录头用 INCOMPLETE 逻辑等待记录体数据流式进入解析器”的模式仅当记录头含长度字段不完整时返回APP_LAYER_INCOMPLETE等待头部凑齐记录头一旦完整立即返回APP_LAYER_OK消费头数据并让后续记录体数据随流持续流入解析器边收边处理。4.2 源码中的真实案例DCERPCRust 实现是这种模式最典型的体现。在 rust/src/dcerpc/dcerpc.rs 中当流中存在不完整数据时解析器先消费能安全消费的部分再要求至少 2 字节以判断是否是新记录的开始_ { consumed cur_i.len() as u32; // 至少需要 2 字节才能判断是否是新记录的开始 if consumed 2 { consumed 0; } else { consumed - 1; } SCLogDebug!(DCERPC record NOT found); return AppLayerResult::incomplete(consumed, 2); }而在 rust/src/dcerpc/dcerpc.rs 中解析器先解析 DCERPC 固定头部再根据头部中的fraglen分片长度字段动态计算需要的数据量return AppLayerResult::incomplete(consumed, DCERPC_HDR_LEN as u32); // ... return AppLayerResult::incomplete(consumed, fraglen.into());这正是“长度字段在头部、按需等待”的标准实现头部长度不足时等头部DCERPC_HDR_LEN头部齐全后按记录实际长度fraglen继续等数据。HTTPC 实现中的协议升级upgrade场景也用到了 INCOMPLETE。在 src/app-layer-htp.c 中当 HTTP/1.1 响应头出现h2cHTTP/2 cleartext upgrade时解析器消费掉 HTTP/1 部分后把剩余字节数作为needed返回让引擎在下一轮把余下数据交给 HTTP/2 解析器// During HTTP2 upgrade, we may consume the HTTP1 part of the data // and we need to parser the remaining part with HTTP2 if (consumed 0 consumed input_len) { SCReturnStruct(APP_LAYER_INCOMPLETE(consumed, input_len - consumed)); } SCReturnStruct(APP_LAYER_OK);WebSocket 升级路径的逻辑完全相同见 src/app-layer-htp.c。这展示了 INCOMPLETE 除了“等待更多数据”之外的另一用途精确控制本次消费边界实现协议间的数据交接。4.3 编写建议综合文档与源码实现 INCOMPLETE 逻辑时应遵循只在 TCP 协议上使用UDP 解析器直接返回 OK 或 ERRORconsumed只统计确实已处理的字节宁可保守如 DCERPC 保留 1 字节用于判断边界needed的取值要使consumed needed覆盖全部剩余输入否则触发引擎的内部错误路径流结束时EOF 调用不要依赖 INCOMPLETE 语义自行处理残片对可恢复的格式异常用AppLayerParserSetDecoderEvent之类的事件机制上报而不是返回 ERROR——ERROR 会永久终止该流后续所有解析。5. 深入回调在引擎中的调用链了解返回值之后掌握回调被调用的上下文有助于写出正确的解析器。AppLayerParserParse()见 src/app-layer-parser.c是引擎侧统一入口其调用链为TCP 流重组完成后由应用层分发逻辑调用src/app-layer.c 等处调用前框架会确保AppLayerParserState与协议状态alstate已分配src/app-layer-parser.c流存在 GAP丢包空洞时若解析器未注册APP_LAYER_PARSER_OPT_ACCEPT_GAPS选项直接进入错误路径并触发原始流重检src/app-layer-parser.c解析结果按status分发 0进入错误状态、 0按 INCOMPLETE 处理、 0视为正常消费src/app-layer-parser.c。C 侧对应选项常量定义在 src/app-layer-parser.hRust 侧在 rust/ffi/src/applayer.rs。这套框架对 C 与 Rust 解析器完全一致Rust 通过 rust/src/applayer.rs 的AppLayerRegisterParser将RustParser结构映射为 C 侧的AppLayerParser注册项。6. 相关文档与进一步阅读应用层整体架构与协议注册流程overview.rst应用层事务Transaction模型transactions.rst应用层帧Frames机制app-layer-frames.rst完整的 Rust 解析器模板rust/src/applayertemplate/template.rs解析器状态与状态机定义src/app-layer-parser.h、src/app-layer-parser.c本文涉及的三个关键源码锚点可对照阅读AppLayerResult 结构体与宏、引擎侧返回值分发与 data_required 写入、Rust 侧 AppLayerResultRust trait。赞分享网络安全【免费下载链接】suricataSuricata is a network Intrusion Detection System, Intrusion Prevention System and Network Security Monitoring engine developed by the OISF and the Suricata community.项目地址https://gitcode.com/gh_mirrors/su/suricata点击查看免费下载相关推荐ArkAnalyzer返回语句函数返回值分析ArkAnalyzer返回语句函数返回值分析 引言 在ArkTS语言开发中函数返回值分析是静态程序分析的关键环节。ArkAnalyzer作为面向ArkTS语静态分析开发工具OpenHarmonySalt 内置 local 返回器Returner完全指南用法、原理与自定义返回器开发Salt 内置 local 返回器Returner完全指南用法、原理与自定义返回器开发 Salt 的返回器Returner机制允许把 minion 执运维配置管理后端Arduino LoRa与LoRaWAN的区别你应该选择哪个Arduino LoRa与LoRaWAN的区别你应该选择哪个 Arduino LoRa是一个用于通过LoRa无线电发送和接收数据的Arduino库它直接暴物联网通信嵌入式上一篇Mi-Create免费开源的小米手表表盘制作工具零基础也能做出自己的表盘下一篇把NCM拖成MP3ncmdump本地免费转换网易云音乐一篇讲透创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表