十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

ndpi-lua:用Lua绑定nDPI实现深度包检测与协议识别

ndpi-lua:用Lua绑定nDPI实现深度包检测与协议识别 简介面向 Lua 与 C 语言开发者这是一份演示 nDPI 深度包检测库集成方式的玩具级示例工程适合网络流量分析、协议识别以及 Lua 扩展编程的入门实践。程序读取 pcap 文件借助 nDPI 逐包识别协议并通过 Lua 回调函数触发对应处理逻辑完整展示了从 C 库封装到 Lua 脚本调用的链路。压缩包共 19 个文件包含 10 个头文件、C 源码、Lua 脚本、Makefile、共享库、示例 pcap 包及 README其中头文件声明 nDPI 接口C 源码实现 Lua 绑定Lua 脚本定义回调行为Makefile 可一键编译共享库便于直接链接整体仅 951KB结构紧凑、层次清晰适合快速阅读与二次开发。已有 320 人学习下载可作为网络流量分析课程配套示例或个人实验的参考样例。通过该工程读者能掌握 nDPI 基础 API 的使用方式理解 libndpilua 的封装思路还能学习如何定义自定义回调函数、解析数据包、注册 Lua 处理逻辑并直接借助构建脚本和测试包开展实验验证。1. 为什么会有 ndpi-lua 这个玩具程序先从我遇到的问题说起。我在做流量分析相关的内部工具需要判断网络流里跑的是什么协议——HTTP、DNS、TLS、QUIC 这些常见协议还好一旦遇到 P2P 或者加密流量靠端口号猜根本不靠谱。开源的深度包检测方案里nDPI 是我一直比较喜欢的选择。它是 ntop 团队维护的深度包检测库最早从 OpenDPI 演化而来后来在 ntopng、nProbe 这些产品里被反复打磨能识别的协议数量相当可观业界用得也多。nDPI 的用法其实很单纯把数据包喂进去它返回识别出的协议类型。底层实现是特征匹配、状态机、启发式判断等多种手段结合不是简单查表。但问题在于我那个工具本身是 Lua 写的而 nDPI 的官方接口是纯 C。每次验证识别逻辑都要先写一段 C 代码、重新编译、再跑一遍这个循环太痛苦了。我的第一反应是找现成的 Lua 绑定结果搜了一圈发现ntopng 里确实有一套内置的 Lua API但那是和 ntopng 框架深度绑定的拆不出来单独用GitHub 上也有几个零星绑定项目基本都停更在旧版 nDPI 的接口上拿到新版直接编译不过。既然没有可用的现成绑定那就自己动手写一个最小的胶水层这就是 ndpi-lua 的起点。它是个典型的玩具程序不追求生产级健壮性目标非常明确——验证Lua 能不能顺畅地驱动 nDPI 完成协议识别同时把调用链路里的细节、坑点完整记录下来。用玩具来定位它是因为我清楚它的边界只支持读取 pcap 文件做离线测试不处理分片重组不做多流并发管理。这些限制对一个测试平台来说反而刚刚好能让代码聚焦在绑定层本身。如果你也在做协议识别相关的脚本化工作或者纠结怎么给某个 C 库写 Lua 绑定这篇文章应该能帮你省下不少时间。下面我按整个项目的推进顺序把环境搭建、绑定设计、调用流程、踩坑记录逐段讲清楚。文章里的代码都是简化示例重点在调用逻辑和设计思路上nDPI 的 API 在不同版本里变动很频繁具体函数签名务必以你实际编译的版本头文件为准。2. 搭环境时最容易忽略的几个点2.1 nDPI 编译不是简单的 configure makeREADME 上写的流程是./autogen.sh ./configure make实际执行时有两个隐藏坑。第一个是 pcap 依赖。nDPI 做协议识别本身不依赖 pcap但你要把数据包喂给它就绕不开抓包文件的解析。我选择把 pcap 解析放在 C 侧这样 Lua 侧代码可以保持干净所以编译前必须确认 libpcap 的开发头文件都在。Ubuntu 上装libpcap-devmacOS 上系统通常自带但头文件路径偶尔会飘configure 阶段报找不到 pcap 的话先检查环境变量里有没有把LIBPCAP_CFLAGS和LIBPCAP_LIBS指对。第二个坑是版本差异。nDPI 4.x 和更早版本的 API 差别非常大ndpi_process_packet的参数顺序、ndpi_detection_module_struct的获取方式都不一样。网上随便搜到的文章大部分是 2.x 时代的写法直接抄过来在新版本上根本编不过。我的建议是直接编译当前稳定版并且以安装目录下的头文件声明为准不要相信任何网上代码里的函数签名。2.2 Lua 版本怎么选Lua 版本的选择会影响后面所有代码。ntopng 生态内部用的是 Lua 5.1 兼容层为了配合 LuaJIT如果你的目标是把代码往 ntopng 方向扩展选 5.1 更稳妥如果只是做独立测试工具Lua 5.4 用起来更舒服luaL_newlib、luaL_checkudata这些辅助宏更规范userdata 和 metatable 的用法也更清晰。ndpi-lua 选的是 5.4理由就一条独立测试工具不用考虑历史兼容。这里有个很容易被忽略的点include 路径。如果系统里同时存在/usr/include/lua5.3和自编译的/usr/local/include/lua5.4编胶水层时会出现代码里调用的函数链接不上或者版本错乱的怪问题。我的做法是把 Lua 的安装路径直接写进 C 模块的 Makefile用绝对路径引用避开系统默认搜索路径造成的歧义。2.3 绑定方案自写胶水层是唯一靠谱的路径动手之前我专门列过一个方案对比结论很直接方案优点缺点用 ntopng 内置 Lua API功能全有 ntop 官方维护与 ntopng 框架深度耦合无法独立使用用 GitHub 第三方绑定不用自己写胶水层版本老旧接口和现版 nDPI 对不上编译困难自写最小 C 胶水层完全可控按需暴露接口需要懂一点 Lua C API调试有成本对玩具项目来说自写是唯一靠谱的路径。C 库的 Lua 绑定本质就三件事把 C 对象包装成 userdata、把 C 函数注册成 Lua 函数、把结果结构体转成 Lua 值。真正花时间的不是绑定代码本身而是理解 nDPI 的调用流程。后面你会发现绑定层总共就几十行难点全在 nDPI 自己的状态模型上。3. 从 pcap 文件到协议识别核心调用流程拆解3.1 Lua 侧 API 怎么设计设计 Lua API 时我坚持一个原则让 Lua 脚本读起来像在描述测试意图而不是在操作底层细节。所以最终暴露给 Lua 的只有四个操作创建检测模块、创建流量状态、处理数据包、获取识别结果。Lua 侧长这样local ndpi require(ndpi) -- 创建检测模块 local ctx ndpi.init() -- 创建一个 flow代表一条网络流 local flow ctx:new_flow() -- 逐包处理回调 local function on_packet(pkt, len) ctx:process(flow, pkt, len) -- 尝试获取识别结果 local proto flow:get_protocol() if proto ~ Unknown then print(string.format(第 %d 个包识别为: %s, pkt.index, proto)) end end -- C 侧解析 pcap 文件逐包回调 Lua 函数 local ok, err ctx:run_pcap(test.pcap, on_packet) if not ok then io.stderr:write(err, \n) end flow:free() ctx:free()run_pcap在 C 侧完成 pcap 文件解析每读到一个包就回调 Lua 的on_packet。这样 Lua 侧不用处理二进制格式pcap 解析的脏活留在 C 层职责划分很干净。你可能会问为什么不直接在 Lua 里读 pcap 文件因为 Lua 标准库没有二进制包解析能力要么引入第三方库要么在 C 侧解决。对一个测试程序来说C 侧解决是成本最低的。3.2 C 侧胶水层实现C 侧的核心是三个函数的注册和 userdata 的 metatable 管理。先看初始化#include lua.h #include lauxlib.h #include lualib.h #include ndpi_api.h #include pcap.h typedef struct { ndpi_detection_module_struct *mod; } ndpi_ctx; typedef struct { struct ndpi_flow_struct *flow; struct ndpi_id_struct *src; struct ndpi_id_struct *dst; } ndpi_flow; static int l_new_ctx(lua_State *L) { ndpi_ctx *ctx (ndpi_ctx *)lua_newuserdata(L, sizeof(ndpi_ctx)); memset(ctx, 0, sizeof(ndpi_ctx)); ctx-mod ndpi_init_detection_module(); if (ctx-mod NULL) { luaL_error(L, ndpi_init_detection_module failed); } luaL_getmetatable(L, ndpi.ctx); lua_setmetatable(L, -2); return 1; }注意lua_newuserdata之后必须立刻把 metatable 挂上去否则 Lua 端拿到的是一个没有方法的裸 userdata。处理数据包的函数更关键static int l_process_packet(lua_State *L) { ndpi_ctx *ctx (ndpi_ctx *)luaL_checkudata(L, 1, ndpi.ctx); ndpi_flow *f (ndpi_flow *)luaL_checkudata(L, 2, ndpi.flow); size_t len 0; const unsigned char *data (const unsigned char *)luaL_checklstring(L, 3, len); uint64_t tick (uint64_t)luaL_optinteger(L, 4, 0); ndpi_process_packet(ctx-mod, f-flow, data, len, tick, f-src, f-dst); return 0; }这里有一个调试了半天的细节luaL_checklstring拿到的数据里如果包含\0字节二进制数据包几乎一定有长度参数必须显式传入否则 Lua 会按 C 字符串截断。我在最初版本里用了lua_tostring而不是luaL_checklstring结果所有超过首个\0的数据都被截掉nDPI 自然什么也识别不出来。3.3 为什么要维护 flow 状态nDPI 的协议识别不是一个包就能出结果而是给每条网络流维护一个独立状态机。你要先为每个五元组源 IP、目的 IP、源端口、目的端口、传输层协议创建ndpi_flow_struct然后把属于这条流的数据包逐个喂进去nDPI 内部会累积状态等到收集到足够证据再返回识别结果。这也正是深度包检测的含义它看的不是单个数据包而是整个流的交互过程。比如 TLS 握手需要先看 ClientHelloHTTP 需要读请求头这些都要在 flow 上下文里被记录和分析。如果为每个包都新建 flow那些需要多包证据的协议永远识别不出来。所以 Lua 侧ctx:new_flow()对应的 C 代码是static int l_new_flow(lua_State *L) { ndpi_ctx *ctx (ndpi_ctx *)luaL_checkudata(L, 1, ndpi.ctx); ndpi_flow *f (ndpi_flow *)lua_newuserdata(L, sizeof(ndpi_flow)); f-flow ndpi_flow_malloc(SIZEOF_FLOW_STRUCT); f-src ndpi_malloc(SIZEOF_ID_STRUCT); f-dst ndpi_malloc(SIZEOF_ID_STRUCT); memset(f-source, 0, sizeof(f-source)); memset(f-dst, 0, sizeof(f-dst)); luaL_getmetatable(L, ndpi.flow); lua_setmetatable(L, -2); return 1; }ndpi_flow_malloc和SIZEOF_FLOW_STRUCT是 nDPI 提供的宏因为 flow 结构体内部大小不公开不让你直接malloc(sizeof(...))。这部分在新版本里有变化有的版本改成ndpi_flow_struct可以直接分配具体还是那句话看你的头文件。3.4 识别结果的类型与转化nDPI 返回的协议信息是一个结构体包含主协议和应用协议两个维度。比如 HTTPS 流量主协议是 TLS应用协议是 HTTP。用ndpi_get_proto_name()把协议 ID 转成可读字符串再用ndpi_protocol2str()或者自己拼字符串。我的 C 侧把结果压成 Lua 字符串返回static int l_get_protocol(lua_State *L) { ndpi_flow *f (ndpi_flow *)luaL_checkudata(L, 1, ndpi.flow); ndpi_protocol p ndpi_detection_get_protocol(f-flow-ndpi_flow); const char *name ndpi_get_proto_name(f-flow-ndpi_flow, p.app_protocol); lua_pushstring(L, name); return 1; }这段逻辑在新版本里的函数名是ndpi_detection_get_protocol在老版本里直接读flow-detected_protocol字段。版本不同写法不同这是整个项目里最需要你灵活处理的点。我最终在代码里加了一层编译期条件判断用#if NDPI_VERSION_NUM区分新旧 API避免每次升级都要改业务代码。4. 测试中踩过的坑和完整排查过程4.1 编译失败API 版本差异带来的第一个下马威第一次编译胶水层直接报错error: too many arguments to function ndpi_process_packet我参照的是网上一个旧教程的函数签名传了 8 个参数但新版 nDPI 只有 6 个。解决方式不是去翻教程而是直接打开ndpi_api.h查声明。这个教训我反复提nDPI 版本迭代时 API 变化很大尤其 4.0 之后把很多检测细节收进了内部结构体外部接口越来越精简。以后升级版本时第一件事永远是对着头文件核对函数签名而不是相信任何博客或旧代码。4.2 运行时报错userdata 和 metatable 的注册顺序跑通编译后Lua 侧一运行就报attempt to index a userdata value (field new_flow)这个错误的原因很典型我在luaopen_ndpi里先写了函数注册表后注册 metatable 的__index字段导致 userdata 上挂载的方法没生效。正确顺序是先用luaL_newmetatable创建 metatable把方法表塞进去再设置__index指向方法表最后才用luaL_setfuncs注册模块函数。顺序反了Lua 解释器不会报编译错误只会在运行时给你一个晦涩的索引错误。排查这个问题的过程也值得一提。我一开始以为是代码逻辑问题加了一堆 print 调试后来想到用luaL_traceback把调用栈打出来才定位到是 metatable 没挂上。当时在lua_pushcclosure那行反复断点最后突然意识到是注册顺序的问题。这个坑对新手特别不友好因为 Lua 的 userdata 错误信息提示很弱。4.3 所有流都返回 Unknown链路层的偏移问题跑通了基本流程但测试 pcap 文件时所有流都返回 Unknown。这是整个项目里最让人抓狂的问题因为代码逻辑看着完全正确nDPI 初始化也成功了就是识别不出来。排查过程是这样的我先用 tcpdump 抓了一个包含 HTTP 请求的 pcap 文件然后用 C 侧打印喂给 nDPI 的前几个字节发现开头是00 0c 29 ...这是 VMware 虚拟机的 MAC 地址也就是说我喂进去的是完整的 Ethernet 帧。但 nDPI 的ndpi_process_packet期望的数据起点是 IP 头。Ethernet 帧头 14 字节没有去掉导致 nDPI 把 MAC 地址当成 IP 头解析后面全部错位。修复方式很简单读取 pcap 时检查链路类型Ethernet 类型就跳过 14 字节如果是 VLAN 标签再额外跳过 4 字节Linux cooked captureSLL则是 16 字节。我写了个小函数统一处理偏移量这个问题就再没出现过。这也是一个非常重要的通用经验喂给深度包检测库的数据起点必须是 L3 层链路层头是你自己负责剥离的库不会替你处理。4.4 识别率不稳定的元凶单向流量和 current_tick偏移问题解决后我的测试 pcap 里 HTTP 能识别了但 TLS 流量经常识别不出来。一开始以为是 nDPI 对 TLS 支持不好后来发现是测试 pcap 只抓了一个方向的数据包。请记住一个重要事实TLS 的识别依赖 ClientHello 里的 SNI 字段和服务器端证书信息。如果你只抓了客户端到服务器的单向流量或者抓包时间太短没覆盖到握手过程nDPI 无法形成完整的流状态结果自然不确定。所以做测试时一定要用双向完整的 pcap 文件最好包含完整的连接建立过程。另外还有个参数容易忽略current_tick。这个时间戳参数影响 nDPI 内部的超时和业务逻辑判断。我在 Lua 脚本里最开始固定传 0结果部分协议的状态机因为时间不推进而走不到检测完成的阶段。建议每读到一个包就传入真实的抓包时间戳pcap 文件里就有取出来传给 C 侧就行。4.5 常见问题速查表现象根因解决办法编译报 too many/few argumentsnDPI API 版本差异打开 ndpi_api.h 核对函数签名Lua 报 attempt to index a userdatametatable 注册顺序错误先 newmetatable 再设置 __index所有流返回 UnknownEthernet 头没剥离从 IP 头开始喂数据部分协议识别不出只喂了单向流量使用双向完整的 pcap识别结果不稳定current_tick 传 0使用真实抓包时间戳5. 玩具程序的价值边界和后续扩展思路5.1 这个玩具能做什么、不能做什么做完这个项目我对玩具程序的定位有了更清晰的认识。ndpi-lua 能做的是快速验证某个 pcap 文件里有哪些协议、nDPI 的检测结果是否符合预期、不同版本 nDPI 的行为差异在哪里。这些场景下Lua 脚本改起来比 C 快得多测试成本几乎是零。但它不是生产工具。它没有处理分片重组没有管理多条并发流没有处理 IPv6 扩展头也不支持在线抓包。这些能力在真实业务里是必须的但在一个测试平台里只会分散注意力。正因为砍掉了这些代码才足够小新手也能一眼看懂整体逻辑而不是被各种边界条件淹没。5.2 扩展方向从玩具到实弹如果你想让这个玩具变得更实弹有几个明确的方向。第一个是把run_pcap换成run_live接上 libpcap 的实时抓包能力这样就能对网卡流量做实时协议识别。改动其实不大把 pcap 文件的读取换成pcap_next_ex循环就可以了Lua 侧 API 几乎不用变。第二个是丰富输出格式。现在只是打印协议名可以把结果整理成 JSON 或 CSV方便后续用脚本分析。我在项目里加了一个选项把每个包的识别结果追加到一个 CSV 文件里这样就能统计某个 pcap 文件里各协议的比例排查流量构成特别方便。第三个是考虑 LuaJIT。如果后续要处理大量数据包纯 Lua 回调的开销会变得明显换 LuaJIT 可以显著提升回调路径的性能。但前提是胶水层代码要兼容 5.1 的 API这需要在设计之初就留好余地。5.3 最后分享两个小技巧根据我这次实际操作的经验有两个技巧特别值得分享。第一个是准备一个协议样本库。我在本机专门建了一个pcaps/目录里面存了 HTTP、DNS、TLS、QUIC、SSH 等常见协议的小文件都是从日常抓包里截取的双向流量每个都验证过识别结果正确。以后改任何代码跑一遍样本库就能快速确认没有回归问题。这件事的投入产出比极高强烈建议你也建一个。第二个是善用 nDPI 自带的测试工具。nDPI 源码里有一个example/ndpiReader编译后可以直接拿它跑 pcap 文件输出每个流的识别结果。我把它作为标准答案用来对照验证我的 Lua 绑定结果是否一致。当你怀疑是自己代码问题还是 nDPI 本身行为时用官方工具跑一遍能省掉大量无谓的猜测。玩具程序不一定只是玩具。这个项目让我把 nDPI 的调用模型摸了个透后来那些可以上生产环境的工具核心逻辑几乎都是从这段玩具代码迭代来的。有时候恰恰是没负担的小项目能把技术细节看得最清楚。本文还有配套的精品资源点击获取
返回列表