Author [email protected]
Date Aug. 07, 2023
Description VPP自定义插件开发demo
在之前的博客:自定义插件中,我们给出了FD.io VPP的sample插件构建方式,但是并没有去真正开发一个插件。
这篇博客给出一个打印数据包IP头部的完整示例。
1. ping插件分析
插件的例子当然可以去看${VPP_HOME}/src/plugins
下的所有各个插件代码,但是一般都很麻烦,推荐可以从非常简单的ping
的实现开始着手看。
ping一般用于测试网络的连通性。这个插件在VPP中只有三个文件:
$ ls ${VPP_HOME}/src/plugins/ping
CMakeLists.txt ping.c ping.h
- CMakeLists.txt只包含了添加插件
- ping.h:数据结构
- ping.c:主体实现,包括ipv4和ipv6的ping实现。
基本的VPP插件开发步骤:
- init执行函数定义(
VLIB_INIT_FUNCTION(init_func_name)
,这是入口) - 插件注册:包含版本号以及描述。这里的版本号和描述都用于
show plugins
命令。VLIB_PLUGIN_REGISTER () = { .version = VPP_BUILD_VER, .description = "Ping (ping)", };
- CLI注册:就看有没有命令行吧,可以是非必须的。
VLIB_CLI_COMMAND (ping_command, static) = { .path = "ping", // 命令行 .function = ping_ip_address, // 执行函数,用于解析命令行 .short_help = "ping {<ip-addr> | ipv4 <ip4-addr> | ipv6 <ip6-addr>}" " [ipv4 <ip4-addr> | ipv6 <ip6-addr>] [source <interface>]" " [size <pktsize:60>] [interval <sec:1>] [repeat <cnt:5>] [table-id <id:0>]" " [burst <count:1>] [verbose]", // 完整配置 .is_mp_safe = 1, // 多进程安全 };
- 节点注册:IPv4和IPv6要分开
VLIB_REGISTER_NODE (ip6_icmp_echo_request_node,static) = { .function = ip6_icmp_echo_request, // 执行函数 .name = "ip6-icmp-echo-request", // 节点名称 .vector_size = sizeof (u32), .format_trace = format_icmp6_input_trace, // format .n_next_nodes = ICMP6_ECHO_REQUEST_N_NEXT, // 下一个“可能的节点”的数量 .next_nodes = { [ICMP6_ECHO_REQUEST_NEXT_LOOKUP] = "ip6-lookup", // 下一个节点1 [ICMP6_ECHO_REQUEST_NEXT_OUTPUT] = "interface-output", // 下一个节点2 }, };
- node注册位置:可选,ping中没有这个
VNET_FEATURE_INIT(plugin_sample, static) = { .arc_name = "ip4-unicast", .node_name = "plugin_sample", .runs_before = VNET_FEATURES("ip4-lookup"), };
- 配置:在
/etc/vpp/startup.conf
中的配置。可选,ping中没有这个。VLIB_CONFIG_FUNCTION (sample_plugin_configure, // 执行函数 "sample"); // 配置名,例如unix那个,插件名是啥,这里就是啥就行
2. 打印数据包IP头部插件
git clone https://github.com/workerwork/vpp-plugin-sample.git
到${VPP_HOME}/src/plugins
目录下。这个是我们这个插件参考的。之后就可以直接使用了。不过这个是只支持IPv4的,我们修改一下,让它也支持IPv6。
这个里面只有4个有用的文件CMakeLists.txt plugin_sample.c plugin_sample.h plugin_sample_node.c
。其中CMakeLists.txt不需要修改。
2.1. plugin_sample.h
这个里面我们可以修改下版本号,当然实际上改不改也都可以,反正是自定义的。
#ifndef __included_plugin_sample_h__
#define __included_plugin_sample_h__
#include <vnet/vnet.h>
#include <vnet/ip/ip.h>
#include <vppinfra/hash.h>
#include <vppinfra/error.h>
#include <vppinfra/elog.h>
typedef struct {
/* API message ID base */
u16 msg_id_base;
/* convenience */
vnet_main_t * vnet_main;
} plugin_sample_main_t;
extern plugin_sample_main_t plugin_sample_main;
extern vlib_node_registration_t plugin_sample_node;
#define PLUGIN_SAMPLE_PLUGIN_BUILD_VER "1.1"
#endif /* __included_plugin_sample_h__ */
2.2. plugin_sample.c
在此文件中定义feature和cli。在实现IPv6的打印时,我们把这个功能就实现为默认不开启吧。这样在使用打印数据包IP头部时,就需要自己开启了,在使用中需要注意。
#include <vnet/plugin/plugin.h>
#include <plugin_sample/plugin_sample.h>
plugin_sample_main_t plugin_sample_main;
//开关实现
static int
plugin_sample_base_enable_disable(u32 sw_if_index, //index
int enable_disable,
const char *plug_node_name,
const char *plugin_name)
{
vnet_sw_interface_t *sw;
int ret = 0;
/* Utterly wrong? */
//vnet_main结构中的interface_main结构中的sw接口
if (pool_is_free_index (plugin_sample_main.vnet_main->interface_main.sw_interfaces,
sw_if_index)) //接口索引
return VNET_API_ERROR_INVALID_SW_IF_INDEX;
/* Not a physical port? */
sw = vnet_get_sw_interface(plugin_sample_main.vnet_main, //vnet_main结构
sw_if_index);
if (sw->type != VNET_SW_INTERFACE_TYPE_HARDWARE)
return VNET_API_ERROR_INVALID_SW_IF_INDEX;
vnet_feature_enable_disable(plug_node_name,
plugin_name,
sw_if_index,
enable_disable, 0, 0);
return ret;
}
static clib_error_t*
plugin_sample_enable_disable_command_fn(vlib_main_t* vm, //vlib_main结构
unformat_input_t *input,
vlib_cli_command_t *cmd)
{
u32 sw_if_index = ~0; //~0 取反全为1
int enable_disable = 0;
while(unformat_check_input(input) != UNFORMAT_END_OF_INPUT) //非空则继续输入
{
if (unformat(input, "enable"))
enable_disable = 1;
if (unformat(input, "disable"))
enable_disable = 0;
else if (unformat(input, "%U",
unformat_vnet_sw_interface,
plugin_sample_main.vnet_main, &sw_if_index));
else
break;
}
if (sw_if_index == ~0)
return clib_error_return(0, "Please specify an interface...");
//调用plugin_sample_base_enable_disable()
plugin_sample_base_enable_disable(sw_if_index,
enable_disable,
"ip4-unicast", //挂载节点
"plugin_sample");
return 0;
}
static clib_error_t*
plugin_sample6_enable_disable_command_fn(vlib_main_t* vm, //vlib_main结构
unformat_input_t *input,
vlib_cli_command_t *cmd)
{
u32 sw_if_index = ~0; //~0 取反全为1
int enable_disable = 0; // 默认为关闭状态
while(unformat_check_input(input) != UNFORMAT_END_OF_INPUT) //非空则继续输入
{
if (unformat(input, "enable"))
enable_disable = 1;
else if (unformat(input, "disable"))
enable_disable = 0;
else if (unformat(input, "%U",
unformat_vnet_sw_interface,
plugin_sample_main.vnet_main, &sw_if_index));
else
break;
}
if (sw_if_index == ~0)
return clib_error_return(0, "Please specify an interface...");
//调用plugin_sample_base_enable_disable()
plugin_sample_base_enable_disable(sw_if_index,
enable_disable,
"ip6-unicast", //挂载节点
"plugin_sample6");
return 0;
}
//注册开关CLI
//指定interface的开关
VLIB_CLI_COMMAND (plugin_sample_command, static) = {
.path = "plugin sample",
.short_help =
"plugin sample <interface-name> [disable]",
.function = plugin_sample_enable_disable_command_fn,
};
VLIB_CLI_COMMAND (plugin_sample6_command, static) = {
.path = "plugin sample6",
.short_help =
"plugin sample <interface-name> [enable | disable]",
.function = plugin_sample6_enable_disable_command_fn,
};
/*注册插件*****start*****/
VLIB_PLUGIN_REGISTER () = {
.version = PLUGIN_SAMPLE_PLUGIN_BUILD_VER,
.description = "Print IPv4/IPv6 Header",
};
static clib_error_t *plugin_sample_init(vlib_main_t* vm)
{
plugin_sample_main.vnet_main = vnet_get_main();
return 0;
}
VLIB_INIT_FUNCTION(plugin_sample_init);
/*注册插件*****end*****/
//将node注册在ip4-unicast的arc中,指定ip-lookup之前Hook到数据包
VNET_FEATURE_INIT(plugin_sample, static) =
{
.arc_name = "ip4-unicast",
.node_name = "plugin_sample",
.runs_before = VNET_FEATURES("ip4-lookup"),
};
VNET_FEATURE_INIT(plugin_sample6, static) =
{
.arc_name = "ip6-unicast",
.node_name = "plugin_sample6",
.runs_before = VNET_FEATURES("ip6-lookup"),
};
2.3. plugin_sample_node.c
在此文件中定义node和包处理函数。节点注册的时候需要把IPv4和IPv6区分开。
#include <vlib/vlib.h>
#include <vnet/vnet.h>
#include <vnet/pg/pg.h>
#include <vnet/ethernet/ethernet.h>
#include <vppinfra/error.h>
#include <plugin_sample/plugin_sample.h>
typedef enum
{
PLUGIN_SAMPLE_NEXT_IP4,
PLUGIN_SAMPLE_DROP,
PLUGIN_SAMPLE_NEXT_N,
} plugin_sample_next_t;
typedef enum
{
PLUGIN_SAMPLE6_NEXT_IP6,
PLUGIN_SAMPLE6_DROP,
PLUGIN_SAMPLE6_NEXT_N,
} plugin_sample6_next_t;
typedef struct
{
u32 next_index;
u32 sw_if_index;
u8 new_src_mac[6];
u8 new_dst_mac[6];
} plugin_sample_trace_t;
#define foreach_plugin_sample_error \
_(SHOWED, "show packets processed")
typedef enum
{
#define _(sym,str) SAMPLE_ERROR_##sym,
foreach_plugin_sample_error
#undef _
SAMPLE_N_ERROR,
} plugin_sample_error_t;
static char *plugin_sample_error_strings[] = {
#define _(sym, str) str,
foreach_plugin_sample_error
#undef _
};
extern vlib_node_registration_t plugin_sample_node;
static u8 *
format_plugin_sample_trace (u8 * s, va_list * args)
{
s = format(s, "To Do!\n");
return s;
}
//Node处理packet主函数
static uword plugin_sample_node_fn(vlib_main_t *vm, vlib_node_runtime_t *node,
vlib_frame_t * frame)
{
u32 n_left_from, *from, *to_next;
plugin_sample_next_t next_index;
from = vlib_frame_vector_args(frame);
n_left_from = frame->n_vectors;
next_index = node->cached_next_index;
while(n_left_from > 0){
u32 n_left_to_next;
// 从流程中获取包
vlib_get_next_frame(vm, node, next_index, to_next, n_left_to_next);
while(n_left_from > 0 && n_left_to_next > 0){
vlib_buffer_t *b0;
u32 bi0, next0 = 0;
bi0 = to_next[0] = from[0];
from += 1;
to_next += 1;
n_left_to_next -= 1;
n_left_from -= 1;
b0 = vlib_get_buffer(vm, bi0);
//获取到IP header
void *en0 = vlib_buffer_get_current(b0);
int i = 0;
int n = 20;
//打印前n bytes, 即ip header
if ((((u8*)en0)[0] & 0xf0) == 0x40)
{
n = 20;
} else if ((((u8*)en0)[0] & 0xf0) == 0x60)
{
n = 40;
}
for (i = 0; i < n; i++)
{
printf("%02x ", *(u8*)(en0+i));
}
printf("\n");
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;
}
//注册NODE
//node初始化信息,生成一堆该Node的构造/析构函数
VLIB_REGISTER_NODE (plugin_sample_node) = {
.name = "plugin_sample",
.function = plugin_sample_node_fn,
.vector_size = sizeof(u32),
.format_trace = format_plugin_sample_trace,
.type = VLIB_NODE_TYPE_INTERNAL,
.n_errors = ARRAY_LEN(plugin_sample_error_strings),
.error_strings = plugin_sample_error_strings,
.n_next_nodes = PLUGIN_SAMPLE_NEXT_N,
.next_nodes = {
[PLUGIN_SAMPLE_NEXT_IP4] = "ip4-lookup",
[PLUGIN_SAMPLE_DROP] = "error-drop",
},
};
VLIB_REGISTER_NODE (plugin_sample6_node) = {
.name = "plugin_sample6",
.function = plugin_sample_node_fn,
.vector_size = sizeof(u32),
.format_trace = format_plugin_sample_trace,
.type = VLIB_NODE_TYPE_INTERNAL,
.n_errors = ARRAY_LEN(plugin_sample_error_strings),
.error_strings = plugin_sample_error_strings,
.n_next_nodes = PLUGIN_SAMPLE6_NEXT_N,
.next_nodes = {
[PLUGIN_SAMPLE6_NEXT_IP6] = "ip6-lookup",
[PLUGIN_SAMPLE6_DROP] = "error-drop",
},
};
3. 使用
在虚拟机中开启了两台Ubuntu20.04的机器,一台安装好了VPP23.06。在/etc/vpp/startup.conf
中的配置如下。DPDK插件也需要启用,虽然使用一个接口就可以了,但是懒得修改了。网卡使用桥接方式,这样两台虚拟机就能出于同一个网络中了。
unix {
nodaemon
log /var/log/vpp/vpp.log
full-coredump
cli-listen /run/vpp/cli.sock
gid vpp
interactive
## This makes VPP sleep 1ms between each DPDK poll, greatly
## reducing CPU usage, at the expense of latency/throughput.
poll-sleep-usec 1000
startup-config /etc/vpp/init.dat # this also the CLI cmds.
}
api-trace {
on
}
api-segment {
gid vpp
}
socksvr {
default
}
memory {
main-heap-size 512M
main-heap-page-size default-hugepage
}
buffers {
buffers-per-numa 128000
default data-size 2048
page-size default-hugepage
}
statseg {
size 1G
page-size default-hugepage
per-node-counters off
}
cpu {
main-core 0
}
dpdk {
dev default {
num-rx-desc 4096
num-tx-desc 4096
}
dev 0000:00:09.0 {
name eth1
}
dev 0000:00:0a.0 {
name eth2
}
}
/etc/vpp/init.dat
中的命令配置,其实就是给VPP配接口。当然直接运行起来之后直接敲命令也是可以的。
loopback create-interface
set interface state loop0 up
set interface state eth1 up
set interface state eth2 up
set interface ip address eth1 192.168.12.1/24
set interface ip address eth2 192.168.23.1/24
set ip6 address eth1 2001:db8:1::1/64
set ip6 address eth2 2001:db8:2::1/64
编译插件,并运行。
$ cd ${VPP_HOME}
$ make build
$ make run
# 查看插件
DBGvpp# show plugins
Plugin Version Description
...
13. plugin_sample_plugin.so 1.1 Print IPv4/IPv6 Header
...
# 查看接口地址
DBGvpp# show int addr
eth1 (up):
L3 192.168.12.1/24
L3 2001:db8:1::1/64
eth2 (up):
L3 192.168.23.1/24
L3 2001:db8:2::1/64
local0 (dn):
loop0 (up):
假设我们是在H1上运行的VPP,那么给H2配置IP地址。请注意,如果使用虚拟机,请保证H1和H2的eth1都在桥接的同一块网卡。
$ sudo ip addr add 192.168.12.2/24 dev eth1
$ sudo ip addr add 2001:db8:1::2/64 dev eth1
$ ping 192.168.12.1 -c 10 &
$ ping 2001:db8:1::1 -c 10 &
然后在VPP中查看:
# 开启IPv4的打印
DBGvpp# plugin sample eth1 enable
DBGvpp# 45 00 00 54 68 ff 40 00 40 01 38 56 c0 a8 0c 02 c0 a8 0c 01
# 开启IPv6的打印
DBGvpp# plugin sample6 eth1 enable
DBGvpp# 60 00 00 00 00 20 3a ff 20 01 0d b8 00 01 00 00 00 00 00 00 00 00 00 02 20 01 0d b8 00 01 00 00 00 00 00 00 00 00 00 01
4. 参考
- https://github.com/FDio/hicn/blob/master/hicn-plugin/src/hicn.c
- https://workerwork.github.io/posts/vpp-sample-plugin/