news 2026/10/26 2:08:18

VPP插件开发实战:源码、节点注册与feature arc挂载排错

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
VPP插件开发实战:源码、节点注册与feature arc挂载排错

简介:面向网络数据平面开发者与 VPP 初学者,这份软件/插件资料围绕可编译、可对照的 VPP 源码和示例插件展开,帮助理解图节点机制、插件注册流程、CLI/API 扩展以及报文从入口到转发的处理路径,适合具备一定 C 语言与 Linux 基础的读者研读。压缩包为 rar 格式,共约 2000 个文件、22.26MB,其中 C 源码 1329 个、头文件 798 个,另有 rst 文档、Python 测试脚本、API 定义、C++ 源码、构建配置与补丁文件,能够支撑从源码检索、模块梳理到编译验证的完整学习过程。已有 561 人学习,资源价值在于把抽象架构落到具体代码:读者可参考示例插件完成节点注册、命令注册、API 调用和基础转发实验,并借文档与脚本理解构建、测试和调试方法。内容还涉及转发、NAT、ACL、IPsec、VPPCOM 与 CLI 等模块,可作为插件开发、代码调试及二次开发的参考素材,便于按目录结构定位相关实现并逐步搭建自己的实验环境。

1. VPP 源码与插件实例:从一台转发节点丢包说起

把 VPP 源码拉下来并不难,难的是第一次写插件时不知道.so该放哪、节点为什么不进 graph、CLI 为什么注册成功却查不到。这个软件/插件资源包给的是一套可编译的 VPP 源码树和一个最小插件实例:注册新节点、处理 buffer、补一条 CLI,再挂到ip4-unicast的 feature arc 上。它适合已经能跑 VPP、想从“调命令”走到“改转发路径”的开发者;也适合要排查插件加载失败、节点不生效的维护者。下面按源码结构、插件注册、节点实现、feature 挂载和排错顺序拆开,代码都能直接落到源码树里。

2. 源码目录到 .so 加载:插件在 VPP 里到底走了哪条链路

2.1 先认目录:vlib、vnet、plugins 各自管什么

VPP 的源码树看起来大,真正写插件时高频打交道的目录并不多。先把边界认清楚,后面找宏、找节点、找 feature arc 才不会在src/里乱翻。下面这张表是我带人读源码时最先画的草图,实际目录可能随版本有增减,但职责划分基本稳定。

目录主要职责写插件时什么时候会进去
src/vppinfravec、hash、format、内存池、位图等基础容器要看format()、vec_*、clib_error_t的定义
src/vlib主循环、graph node、buffer、CLI、插件加载注册节点、写节点函数、注册 CLI 命令
src/vnet以太网、IP、接口、feature arc、邻接表挂ip4-unicast、找ip4-lookup节点名
src/plugins官方插件和示例插件抄最小插件的目录结构、CMake 写法
src/vpp主程序入口、启动参数解析看startup.conf怎么进入插件加载
build-root构建产物、安装目录、插件.so找vpp、vppctl、vpp_plugins

写插件最常踩的误区是把src/plugins当成“可选目录”。实际上插件和核心代码共用同一套vlib基础设施:节点注册宏、buffer 操作、next frame、CLI 命令树都在vlib里。插件被编译成动态库,但运行时拿到的是同一个vlib_main_t,所以节点函数签名、buffer 引用规则和核心节点完全一致。先接受这一点,后面看示例插件就不会觉得宏是黑匣子。

另一个需要提前建立的认知是:VPP 的转发路径不是一条固定函数调用链,而是一张在启动时装配出来的 graph。每个节点注册时声明自己有哪些 next node,feature arc 再把候选节点插到某个 arc 的前后关系里。插件节点只要注册成功,就会成为 graph 里的一个节点;但“注册成功”和“流量经过”是两件事,后者还取决于 feature 是否在接口上启用、arc 是否匹配、next node 是否接对。第 4 章会专门拆这个链路,这里先记住目录和职责。

2.2 插件注册宏与初始化顺序:构造函数段不是玄学

VPP 插件的最小骨架通常由几类注册宏组成:插件描述、初始化函数、节点、feature、CLI 命令。它们不是普通函数调用,而是把静态结构放进特定段,等加载器或初始化阶段统一扫描。第一次看会觉得“写个结构体就注册了”,实际顺序有讲究:插件描述决定.so是否被加载,初始化函数在插件加载后执行,节点和 CLI 在更早的注册阶段进入全局表,feature 则在 graph 初始化时参与 arc 装配。

下面是一个最小插件的注册骨架,文件名可以叫myplugin.c。这段代码不处理报文,只把插件身份、初始化入口和一条 CLI 命令挂上去,用来验证加载链路是否通。

/* myplugin.c:最小插件骨架,先验证 .so 能被加载 */ #include <vlib/vlib.h> #include <vlib/unix/unix.h> #include <vnet/vnet.h> /* 插件描述:加载器靠它识别插件,default_disabled 决定默认是否启用 */ VLIB_PLUGIN_REGISTER () = { .version = "1.0.0", .description = "myplugin sample plugin", .default_disabled = 0, }; /* 插件初始化:此时 vlib 已经起来,适合初始化插件自己的全局状态 */ static clib_error_t * myplugin_init (vlib_main_t * vm) { vlib_cli_output (vm, "myplugin: init"); return 0; } VLIB_INIT_FUNCTION (myplugin_init); /* 一条最小 CLI,用来确认命令树里能看到插件 */ static clib_error_t * myplugin_hello_fn (vlib_main_t * vm, unformat_input_t * input, vlib_cli_command_t * cmd) { vlib_cli_output (vm, "myplugin: hello"); return 0; } VLIB_CLI_COMMAND (myplugin_hello_command, static) = { .path = "myplugin hello", .short_help = "myplugin hello", .function = myplugin_hello_fn, };

这段代码里最容易被忽略的是VLIB_PLUGIN_REGISTER()的default_disabled。设为0时,插件默认参与加载;设为1时,需要在startup.conf的plugins段显式启用。调试阶段我一般先设0,确认.so能被扫描到,再改成1验证配置开关。VLIB_INIT_FUNCTION注册的函数会在插件加载后执行,适合做全局状态清零;不要在这里做重活,更不要假设所有节点已经完成 graph 装配。

节点注册和 feature 注册是另一组宏。节点注册告诉 graph “有一个叫myplugin-node的节点”,feature 注册告诉 arc “这个节点可以挂在ip4-unicast上”。两者可以放在同一个文件,也可以拆到node.c。拆文件的好处是节点函数和注册结构放在一起,改 next node 时不容易漏。CLI 命令则建议单独放cli.c,因为命令路径冲突时更容易定位。下面这个骨架展示了节点和 feature 的注册位置,具体节点函数第 3 章再展开。

/* node.c:节点与 feature 的注册骨架 */ #include "myplugin.h" /* 节点注册:名字、函数、next node 表都在这里定 */ VLIB_REGISTER_NODE (myplugin_node, static) = { .function = myplugin_node_fn, .name = "myplugin-node", .vector_size = sizeof (u32), .format_trace = format_myplugin_trace, .type = VLIB_NODE_TYPE_INTERNAL, .n_next_nodes = MYPLUGIN_N_NEXT, .next_nodes = { [MYPLUGIN_NEXT_IP4_LOOKUP] = "ip4-lookup", [MYPLUGIN_NEXT_DROP] = "error-drop", }, }; /* feature 注册:只是进入候选池,不等于在接口上启用 */ VNET_FEATURE_INIT (myplugin_feature, static) = { .arc_name = "ip4-unicast", .node_name = "myplugin-node", .runs_before = VNET_FEATURES ("ip4-lookup"), };

节点注册里的.n_next_nodes和.next_nodes必须对得上。MYPLUGIN_N_NEXT是枚举总数,next_nodes用指定下标填节点名,顺序错一位就会把报文送到错误分支。format_trace不是必填,但调试时非常有用;没有它,show trace只能看到 buffer 地址,排错效率会低很多。feature 注册里的runs_before表示这个 feature 节点排在ip4-lookup之前,适合做采样、计数、ACL 前置判断这类动作。不要随便写runs_after,先看清 arc 里已有节点的顺序。

2.3 把源码编出来:build-root 与插件 .so 的落点

源码树里加完插件目录后,第一步不是改业务逻辑,而是把构建跑通。VPP 的构建系统会先生成 CMake 工程,再编译vlib、vnet、插件和主程序。下面这组命令是常见做法,具体依赖包名称随发行版不同会有差异,但顺序不变:装依赖、装外部依赖、构建。

# 在 VPP 源码根目录执行 make install-dep make install-ext-deps make build # 构建完成后找 vpp、vppctl 和插件 .so find build-root -name 'vpp' -type f | head find build-root -name 'vppctl' -type f | head find build-root -name 'myplugin_plugin.so' -type f

make install-dep装的是编译工具和基础库,make install-ext-deps处理 VPP 依赖的外部组件,make build才真正生成构建目录。build-root下通常会有install-vpp-native/vpp这样的安装前缀,bin/vpp、bin/vppctl、lib/vpp_plugins都在里面。插件.so的命名规则一般是插件名_plugin.so,所以myplugin编译出来是myplugin_plugin.so。如果find找不到,先看 CMake 有没有把子目录加进去,再看编译日志里有没有myplugin的 target。

启动时用源码树里的vpp和vppctl,不要和系统里已安装的版本混用。插件 ABI 和主程序版本强相关,混用最常见的结果是dlopen失败或加载后崩溃。启动配置里可以显式写插件:

# startup.conf 片段:显式启用插件,路径按实际安装目录调整 plugins { plugin myplugin_plugin.so { enable } }

plugin后面写.so文件名,不写路径时加载器会去默认插件目录找;如果放在自定义目录,用path指定。配置改完后启动 VPP,先执行show plugins,确认列表里有myplugin_plugin.so。再看启动日志里有没有myplugin: init,这两步都过了,才说明插件加载链路是通的。很多“节点不生效”的问题,其实卡在这一步:插件根本没加载,后面查 graph 都是白费。

3. 最小插件实例:注册节点、处理 buffer、补一条 CLI

3.1 插件目录与 CMakeLists:别把插件名和 .so 名搞混

在src/plugins下新建myplugin目录,至少放myplugin.h、myplugin.c、node.c、cli.c和CMakeLists.txt。头文件里放插件全局状态、next node 枚举和函数声明;myplugin.c放插件注册和初始化;node.c放节点函数与节点注册;cli.c放命令。目录名、插件名、.so名是三件事:目录叫myplugin,CMake target 叫myplugin,产物叫myplugin_plugin.so,启动配置里写的是产物名。

# src/plugins/myplugin/CMakeLists.txt add_vpp_plugin(myplugin SOURCES myplugin.c node.c cli.c INSTALL_HEADERS myplugin.h )

add_vpp_plugin是 VPP 构建系统里的插件宏,SOURCES后面列源文件,INSTALL_HEADERS后面列需要安装的头文件。如果插件带 API 文件,再加API_FILES myplugin.api;如果有多架构源文件,用MULTIARCH_SOURCES。写完子目录的 CMakeLists 后,还要在src/plugins/CMakeLists.txt里加一行add_subdirectory(myplugin)。有些版本的构建系统会自动扫描,但显式加一行最稳,也方便确认插件有没有进入构建。

头文件里先把全局状态和 next node 枚举定下来。下面这个结构只保留计数和开关,够验证链路,不要一上来就塞复杂表项。

/* myplugin.h:插件全局状态与 next node 枚举 */ #ifndef __included_myplugin_h__ #define __included_myplugin_h__ #include <vlib/vlib.h> #include <vnet/vnet.h> #include <vnet/ip/ip.h> typedef struct { u32 enabled; /* CLI 开关,验证阶段先用全局开关 */ u64 packets; /* 经过节点的报文数 */ u64 bytes; /* 经过节点的字节数 */ } myplugin_main_t; extern myplugin_main_t myplugin_main; typedef enum { MYPLUGIN_NEXT_IP4_LOOKUP = 0, MYPLUGIN_NEXT_DROP, MYPLUGIN_N_NEXT, } myplugin_next_t; #endif

enabled用u32而不是int,是为了和 VPP CLI 里常见的u32参数解析保持一致。packets、bytes用u64,避免长时间打流溢出。枚举里MYPLUGIN_NEXT_IP4_LOOKUP放在 0,节点注册的next_nodes数组也按这个下标填ip4-lookup。如果后面要加“送到 error-drop”的分支,MYPLUGIN_NEXT_DROP就是 1,MYPLUGIN_N_NEXT自动变成 2。枚举顺序一旦被外部代码依赖,就不要随便插到中间。

3.2 节点函数怎么写:从 frame 取 buffer 到 next0 分流

节点函数是插件真正处理报文的地方。VPP 一次把一批 buffer 索引放进 frame,节点函数从vlib_frame_vector_args(frame)拿到u32数组,再用vlib_get_buffer()把索引转成vlib_buffer_t *。处理完以后,通过vlib_get_next_frame()拿到下一跳的输出槽位,把 buffer 索引写进去,最后用vlib_validate_buffer_enqueue_x1()处理“下一跳和当前缓存不一致”的情况。

/* node.c:节点函数,先做计数,再送 ip4-lookup */ #include "myplugin.h" myplugin_main_t myplugin_main; static u8 * format_myplugin_trace (u8 * s, va_list * args) { vlib_main_t *vm = va_arg (*args, vlib_main_t *); vlib_node_t *node = va_arg (*args, vlib_node_t *); u32 bi = va_arg (*args, u32); vlib_buffer_t *b = vlib_get_buffer (vm, bi); (void) node; s = format (s, "myplugin: len %u", b->current_length); return s; } static uword myplugin_node_fn (vlib_main_t * vm, vlib_node_runtime_t * node, vlib_frame_t * frame) { myplugin_main_t *mpm = &myplugin_main; u32 n_left_from = frame->n_vectors; u32 *from = vlib_frame_vector_args (frame); u32 next_index = node->cached_next_index; while (n_left_from > 0) { u32 n_left_to_next = 0; u32 *to_next = 0; vlib_get_next_frame (vm, node, next_index, to_next, n_left_to_next); while (n_left_from > 0 && n_left_to_next > 0) { u32 bi0 = from[0]; vlib_buffer_t *b0 = vlib_get_buffer (vm, bi0); u32 next0 = MYPLUGIN_NEXT_IP4_LOOKUP; if (mpm->enabled) { mpm->packets++; mpm->bytes += b0->current_length; } /* 先把当前 buffer 放进下一跳槽位,再处理 next0 不一致的情况 */ to_next[0] = bi0; to_next += 1; n_left_to_next -= 1; from += 1; n_left_from -= 1; vlib_validate_buffer_enqueue_x1 (vm, node, next_index, to_next, n_left_to_next, bi0, next0); } vlib_put_next_frame (vm, node, next_index, n_left_to_next); } return frame->n_vectors; } VLIB_REGISTER_NODE (myplugin_node, static) = { .function = myplugin_node_fn, .name = "myplugin-node", .vector_size = sizeof (u32), .format_trace = format_myplugin_trace, .type = VLIB_NODE_TYPE_INTERNAL, .n_next_nodes = MYPLUGIN_N_NEXT, .next_nodes = { [MYPLUGIN_NEXT_IP4_LOOKUP] = "ip4-lookup", [MYPLUGIN_NEXT_DROP] = "error-drop", }, };

这段代码的关键在三个位置。第一,vlib_get_next_frame()拿到的to_next和n_left_to_next是“当前下一跳还能放多少 buffer”,内层循环必须同时检查n_left_from和n_left_to_next,否则会写穿输出槽位。第二,to_next[0] = bi0之后要马上递减n_left_to_next,再调用vlib_validate_buffer_enqueue_x1();这个函数的参数是“剩余空间”,不是“已写入数量”。第三,next0当前固定为MYPLUGIN_NEXT_IP4_LOOKUP,等 feature arc 启用后,VPP 会把节点的 next 改成 arc 里的下一节点,所以节点注册里的ip4-lookup只是默认落点,不是永久绑定。

format_myplugin_trace()里用b->current_length而不是自己算current_data偏移,是为了避免多级 buffer 或头部偏移时打印错误。trace 格式函数只在show trace时调用,但一旦越界就会把 VPP 打崩,所以读 buffer 字段前先确认bi是有效的。节点函数里不要直接修改b0->current_data,要移动头部用vlib_buffer_advance(),要改长度用vlib_buffer_length_in_chain()配合相应宏,这些规则和核心节点完全一样。

3.3 CLI 与开关:把 enable/disable 落到节点状态

验证节点是否执行,最快的方式是加一个全局开关和一条show命令。开关放在myplugin_main里,节点函数每次判断;show命令打印计数。这样不用反复改代码、重编插件,就能确认“流量有没有经过节点”和“计数有没有增长”。

/* cli.c:插件开关与计数查看 */ #include "myplugin.h" static clib_error_t * myplugin_set_enabled_fn (vlib_main_t * vm, unformat_input_t * input, vlib_cli_command_t * cmd) { myplugin_main_t *mpm = &myplugin_main; u32 enabled = 0; if (!unformat (input, "%u", &enabled)) return clib_error_return (0, "expect 0 or 1"); mpm->enabled = enabled ? 1 : 0; vlib_cli_output (vm, "myplugin enabled: %u", mpm->enabled); return 0; } static clib_error_t * myplugin_show_fn (vlib_main_t * vm, unformat_input_t * input, vlib_cli_command_t * cmd) { myplugin_main_t *mpm = &myplugin_main; vlib_cli_output (vm, "enabled %u packets %llu bytes %llu", mpm->enabled, mpm->packets, mpm->bytes); return 0; } VLIB_CLI_COMMAND (myplugin_set_enabled_command, static) = { .path = "myplugin set enabled", .short_help = "myplugin set enabled <0|1>", .function = myplugin_set_enabled_fn, }; VLIB_CLI_COMMAND (myplugin_show_command, static) = { .path = "show myplugin", .short_help = "show myplugin", .function = myplugin_show_fn, };

unformat(input, "%u", &enabled)解析无符号整数,失败时返回错误信息。命令路径用空格分层,myplugin set enabled 1会落到myplugin_set_enabled_fn()。show myplugin放在show命名空间下,方便和其他插件的查看命令放在一起。CLI 命令注册宏的变量名不能重复,路径也不能和已有命令冲突;如果启动日志里报命令冲突,先改路径前缀,不要急着怀疑节点代码。

初始化函数里要把enabled和计数清零。下面这段可以放在myplugin.c,和插件注册放在一起。

/* myplugin.c:初始化插件全局状态 */ static clib_error_t * myplugin_init (vlib_main_t * vm) { myplugin_main_t *mpm = &myplugin_main; mpm->enabled = 0; mpm->packets = 0; mpm->bytes = 0; return 0; } VLIB_INIT_FUNCTION (myplugin_init);

VLIB_INIT_FUNCTION的执行时机在插件加载后、graph 装配前后,具体顺序取决于注册的 init 等级。这里只做状态清零,不依赖其他节点,所以不用关心顺序。如果插件初始化要读配置或建表,才需要考虑 init 等级和依赖关系。验证阶段保持初始化函数简单,能减少变量。

4. 挂到 IP 转发路径:feature arc 注册与 graph 验证

4.1 feature arc 的注册条件:arc_name、runs_before 怎么填

节点注册成功只说明 graph 里有这个节点,不代表 IP 流量会经过它。要让节点进入ip4-unicast的转发路径,需要注册 feature,并在接口上启用。feature 注册结构里最关键的是arc_name、node_name和runs_before。arc_name必须和 VPP 已有的 arc 名完全一致,node_name必须是已经注册的节点名,runs_before或runs_after决定它相对哪个节点插入。

/* node.c:把 myplugin-node 注册到 ip4-unicast arc */ VNET_FEATURE_INIT (myplugin_feature, static) = { .arc_name = "ip4-unicast", .node_name = "myplugin-node", .runs_before = VNET_FEATURES ("ip4-lookup"), };

`arc_name

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/26 2:07:24

DL/T 376.1协议Java实现:生产级电表通信底座

简介&#xff1a;本资源是面向电力行业Java开发者的376.1通信协议&#xff08;ANSI C12.18-2004标准&#xff09;完整实现方案&#xff0c;专为智能电表与电网管理系统间的数据解析、报文构建及安全通信提供开箱即用的工程化支持&#xff0c;适用于AMR/AMI系统集成、电表数据采…

作者头像 李华
网站建设 2026/10/26 2:07:23

DQN车间排产实战:马尔可夫建模、奖励塑形与训练排查

简介&#xff1a;这套代码面向车间排产调度优化&#xff0c;聚焦DQN算法在动态生产环境中的策略学习与调度决策&#xff0c;适合强化学习学习者、运筹优化研究人员及工业工程师参考复现。资源包共3个Python文件&#xff0c;整体约5KB&#xff0c;体量轻巧却结构完整&#xff0c…

作者头像 李华
网站建设 2026/10/26 2:06:52

支付回调验签连续失败三天,最后发现是网关的时间戳单位问题

三天、五个错误方向&#xff0c;最后发现是「毫秒」和「秒」的默认值不一致。图 1&#xff5c;失败点不在支付服务&#xff0c;在网关的时间戳窗口——这是三天里最后才看到的一格。 读完你能拿到三样东西&#xff1a;① 验签失败的 5 步拆解排查法&#xff08;不用再猜&#x…

作者头像 李华
网站建设 2026/10/26 2:06:28

Android WebDAV客户端:Kotlin+OkHttp目录浏览与断点续传

简介&#xff1a;这是一份面向 Android 开发者的 WebDAV 客户端项目源码包&#xff0c;围绕移动端访问支持 WebDAV 协议的远程文件服务器展开&#xff0c;适合需要实现远程目录浏览、文件上传下载、重命名、移动与删除等功能的开发者参考学习。压缩包共 149 个文件&#xff0c;…

作者头像 李华
网站建设 2026/10/26 2:05:03

pandas手搓海龟法则:量化回测从信号到仓位与绩效

简介&#xff1a;面向量化交易初学者与已具备pandas基础的个人交易者&#xff0c;这套海龟交易法则Python实现资料把经典趋势跟踪规则落到可运行的数据分析流程中&#xff0c;重点处理历史行情读取、指标计算、突破判断与交易管理等问题。压缩包共3个文件&#xff0c;包含2个CS…

作者头像 李华
网站建设 2026/10/26 2:04:41

YOLOv5+ArcFace人脸检测与识别一体化实战

简介&#xff1a;本资源是一个基于YOLOv5与ArcFace双模型协同的人脸识别完整项目实现&#xff0c;面向深度学习初学者、计算机视觉开发者及AI安防应用实践者&#xff0c;解决端到端人脸检测高精度身份识别的技术落地问题&#xff0c;适用于门禁系统、考勤管理、智能监控等实际场…

作者头像 李华