使用nDPI库的程序返回错误结果,持续输出无效分类值求助
你的程序持续输出Invalid category: -842150451,本质是ndpi_protocol结构体的category字段为未初始化的垃圾值,核心问题出在nDPI初始化、流结构体管理和数据包处理的逻辑错误上。以下是具体问题和修复方案:
关键错误点分析
重复执行nDPI初始化收尾
ndpi_finalize_initialization是初始化阶段的收尾操作,应该只调用一次。你在read_packet函数中每个数据包都调用,会破坏nDPI的内部检测状态,导致协议检测逻辑失效。未正确初始化
ndpi_flow_struct
直接用new创建ndpi_flow_struct,没有调用nDPI提供的ndpi_flow_malloc初始化函数,结构体内部成员都是随机垃圾值,后续检测时会读取无效数据。每个数据包新建流结构体
网络流是由多个数据包组成的连续会话,你为每个数据包创建独立的ndpi_flow_struct,导致nDPI无法累积流的上下文信息,根本无法完成协议检测,同时会造成严重内存泄漏。不安全的类型转换
强制将Tins::PDU转换为Tins::Packet是不安全的,PDU是抽象基类,并非所有PDU实例都是Packet,会导致未定义行为。未处理协议查找失败的异常
rfind_pdu<Tins::IP>()和rfind_pdu<Tins::TCP>()如果找不到对应层,会抛出std::runtime_error,导致程序崩溃。
修复后的完整代码
#include <iostream> #include <tins/tins.h> #include "ndpi_api.h" #include <unordered_map> #include <tuple> // 流的五元组key,用于跟踪已存在的流 using FlowKey = std::tuple<uint32_t, uint32_t, uint16_t, uint16_t, uint8_t>; // 自定义哈希函数,用于FlowKey的unordered_map struct FlowKeyHash { std::size_t operator()(const FlowKey& k) const { return std::hash<uint32_t>()(std::get<0>(k)) ^ std::hash<uint32_t>()(std::get<1>(k)) ^ std::hash<uint16_t>()(std::get<2>(k)) ^ std::hash<uint16_t>()(std::get<3>(k)) ^ std::hash<uint8_t>()(std::get<4>(k)); } }; // 存储流信息的结构体 struct ndpi_flow_info { ndpi_protocol detected_protocol; struct ndpi_flow_struct *ndpi_flow; }; // 全局nDPI检测模块 struct ndpi_detection_module_struct* ndpi_struct = nullptr; // 流表,用于管理所有活跃流 std::unordered_map<FlowKey, ndpi_flow_info, FlowKeyHash> flow_table; // 获取流的五元组key(自动按统一顺序排序,避免双向流重复) FlowKey get_flow_key(const Tins::IP& ip, const Tins::Transport& transport) { uint8_t proto = ip.protocol(); uint32_t src_ip = ip.src_addr(); uint32_t dst_ip = ip.dst_addr(); uint16_t src_port = transport.sport(); uint16_t dst_port = transport.dport(); // 统一五元组顺序,确保双向流对应同一个key if ((src_ip > dst_ip) || (src_ip == dst_ip && src_port > dst_port)) { std::swap(src_ip, dst_ip); std::swap(src_port, dst_port); } return std::make_tuple(src_ip, dst_ip, src_port, dst_port, proto); } // 处理单个数据包 int process_packet(const Tins::Packet& packet) { try { const Tins::IP& ip = packet.pdu()->rfind_pdu<Tins::IP>(); const Tins::Transport& transport = packet.pdu()->rfind_pdu<Tins::Transport>(); // 获取流key FlowKey key = get_flow_key(ip, transport); auto it = flow_table.find(key); ndpi_flow_info* flow_info; // 如果流不存在,创建并初始化新流 if (it == flow_table.end()) { ndpi_flow_info new_flow; new_flow.ndpi_flow = ndpi_flow_malloc(ndpi_struct); if (!new_flow.ndpi_flow) { std::cout << "Memory allocation failed for flow" << std::endl; return -1; } ndpi_init_flow(ndpi_struct, new_flow.ndpi_flow); new_flow.detected_protocol = ndpi_protocol::ndpi_protocol_unknown; it = flow_table.insert({key, new_flow}).first; } flow_info = &it->second; // 序列化整个数据包(从链路层开始) std::vector<uint8_t> packet_data = packet.pdu()->serialize(); uint64_t time_ms = packet.timestamp().seconds() * 1000 + packet.timestamp().microseconds() / 1000; // 准备流输入信息 ndpi_flow_input_info input; memset(&input, 0, sizeof(input)); input.flow = flow_info->ndpi_flow; input.flow_id = (void*)&key; input.seen_flow_beginning = (it->second.ndpi_flow->num_processed_pkts == 0) ? 1 : 0; // 检测协议 flow_info->detected_protocol = ndpi_detection_process_packet(ndpi_struct, flow_info->ndpi_flow, packet_data.data(), packet_data.size(), time_ms, &input); // 打印检测结果(仅当协议确定时) if (flow_info->detected_protocol.protocol_id != NDPI_PROTOCOL_UNKNOWN && flow_info->detected_protocol.category >= 0 && flow_info->detected_protocol.category < NDPI_PROTOCOL_NUM_CATEGORIES) { const char* category_name = ndpi_category_get_name(ndpi_struct, flow_info->detected_protocol.category); std::cout << "Flow between " << ip.src_addr() << ":" << transport.sport() << " and " << ip.dst_addr() << ":" << transport.dport() << " -> Category: " << category_name << ", Protocol: " << ndpi_get_proto_name(ndpi_struct, &flow_info->detected_protocol) << std::endl; } return 0; } catch (const std::runtime_error& e) { // 忽略不包含IP/传输层的数据包 return 0; } } int main() { // 初始化nDPI ndpi_struct = ndpi_init_detection_module(ndpi_no_prefs); if (!ndpi_struct) { std::cout << "nDPI initialization failed" << std::endl; return -1; } ndpi_finalize_initialization(ndpi_struct); // 配置嗅探器 Tins::SnifferConfiguration config; config.set_promisc_mode(true); config.set_filter("ip"); // 捕获所有IP数据包 Tins::NetworkInterface iface = Tins::NetworkInterface::default_interface(); std::cout << "Sniffing on interface: " << iface.name() << " (" << iface.friendly_name() << ")" << std::endl; // 开始嗅探 try { Tins::Sniffer sniffer(iface.name(), config); sniffer.sniff_loop([&](const Tins::Packet& packet) -> bool { process_packet(packet); return true; }); } catch (const std::exception& e) { std::cout << "Sniffer error: " << e.what() << std::endl; } // 清理资源 for (auto& entry : flow_table) { ndpi_flow_free(ndpi_struct, entry.second.ndpi_flow); } ndpi_exit_detection_module(ndpi_struct); return 0; }
关键修复说明
正确的nDPI初始化流程
将ndpi_finalize_initialization移至main函数中,仅在初始化阶段调用一次,确保nDPI内部状态稳定。流的生命周期管理
使用std::unordered_map维护流表,通过五元组跟踪活跃流,每个流只创建一个ndpi_flow_struct,并使用ndpi_flow_malloc和ndpi_init_flow正确初始化,程序退出时统一释放资源。安全的数据包处理
使用try-catch捕获协议层查找失败的异常,避免程序崩溃;直接使用Tins::Packet获取时间戳,避免不安全的类型转换。正确的流输入参数
设置seen_flow_beginning标识流的第一个数据包,帮助nDPI正确初始化流检测上下文。
排查思路总结
遇到此类问题时,按以下顺序排查:
- 检查nDPI初始化流程是否正确,确保初始化函数只调用一次
- 验证
ndpi_flow_struct是否通过nDPI提供的函数正确初始化,避免直接使用原始内存分配 - 确认流的生命周期管理是否正确,同一个流必须复用同一个
ndpi_flow_struct - 检查参数传递是否符合nDPI API要求,尤其是时间戳、数据包长度等关键参数
- 排查是否存在未初始化的内存或无效的类型转换
内容的提问来源于stack exchange,提问作者Ostrale

