首页 > 其他分享 >VPP 插件分析与开发

VPP 插件分析与开发

时间:2023-08-07 19:03:16浏览次数:36  
标签:node 插件 VPP plugin 00 next sample 开发 main

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插件开发步骤:

  1. init执行函数定义(VLIB_INIT_FUNCTION(init_func_name),这是入口)
  2. 插件注册:包含版本号以及描述。这里的版本号和描述都用于show plugins命令。
    VLIB_PLUGIN_REGISTER () = {
        .version = VPP_BUILD_VER,
        .description = "Ping (ping)",
    };
    
  3. 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, // 多进程安全
     };
    
  4. 节点注册: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
       },
     };
    
  5. node注册位置:可选,ping中没有这个
     VNET_FEATURE_INIT(plugin_sample, static) =
     {
         .arc_name = "ip4-unicast",
         .node_name = "plugin_sample",
         .runs_before = VNET_FEATURES("ip4-lookup"),
     };
    
  6. 配置:在/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. 参考

  1. https://github.com/FDio/hicn/blob/master/hicn-plugin/src/hicn.c
  2. https://workerwork.github.io/posts/vpp-sample-plugin/

标签:node,插件,VPP,plugin,00,next,sample,开发,main
From: https://blog.51cto.com/basilguo/6996318

相关文章

  • Vue学习笔记:路由开发 Part 03
    在Part1中提到了router-link。本文档使用一个标签栏来演示其功能在之前的例子中引入一个新的组件TabBartabbar.vue<template><divclass="tabbar"><ul><li><router-linkto="/center"active-class="tabbar-active">Cent......
  • 关于Java的IO流开发
    IO概述回想之前写过的程序,数据都是在内存中,一旦程序运行结束,这些数据都没有了,等下次再想使用这些数据,可是已经没有了。那怎么办呢?能不能把运算完的数据都保存下来,下次程序启动的时候,再把这些数据读出来继续使用呢?其实要把数据持久化存储,就需要把内存中的数据存储到内存以外的其他......
  • 用低代码开发平台构建高效敏捷工作流
    随着行业业务发展及业务流程诉求的增长,企业信息资源越来越表现出一种异构分布、松散耦合的特点。实现大规模、异构、分布式执行环境,使得相互关联的任务能够高效运转成为了业务管理的强诉求。以事件驱动和数据驱动来进行应用系统构建也就变得更加实效。因此,工作流平台成为建设信息......
  • 微信开发之自动同意好友的技术实现
    简要描述:同意添加好友请求URL:http://域名地址/acceptUser请求方式:POST请求头Headers:Content-Type:application/jsonAuthorization:login接口返回参数:参数名必选类型说明wId是string登录实例标识v1是stringv1(从消息回调中取)v2是stringv2(从消......
  • 《VTK图形图像开发进阶》第2章——VTK智能指针
    2.1引用计数如果很多对象有相同的值,在程序里没有必要将这个值存储多次。更好的办法是让所有的对象共享这个值。这么做不但节省内存,而且可以使程序运行得更快,因为不需要构造和析构这个值的副本。引用计数就是这样一个技巧,它允许多个有相同值的对象共享这个值。引用计数是个简单......
  • Django博客开发教程:创建项目
    我们对需求和数据库都进行分析了之后,我们就开始来创建我们的项目。教程是在windows10操作系统下,用的Python3.6和django2.1.1,开发工具为pycharm。打开我们的Pycharm,新建一个项目。说明:1为项目保存路径,myblog为项目名。2为选择使用的虚拟环境软件,这里选virtualenv。3为虚拟环境......
  • springboot智能3D导诊系统源码,基于规则模板的开发原理
    互联网智慧3D导诊系统源码通过智能导诊,进行自助问询及挂号服务,减轻导诊台护士压力,挂号更加方便快捷。技术架构:springboot+redis+mybatisplus+mysql+RocketMQ  智慧导诊系统开发原理导诊系统从原理上大致可分为基于规则模板和基于数据模型两类。1、基于规则推理的方法通过人工建......
  • vue图片压缩插件
    图片压缩插件1.安装插件npmijs-image-compressor2.引入importImageCompressorfrom'js-image-compressor'3.使用compressionImage(file){returnnewPromise((resolve,reject)=>{//eslint-disable-next-lineno-newnewImageCom......
  • 1、Django博客开发教程:开发前的准备
    开发前的准备:1、安装好Python环境。Python3安装详细步骤2、安装好virtualenv虚拟环境。virtualenv虚拟环境安装方法3、安装好Pycharm开发工具。 ......
  • Django-4.2博客开发教程:数据库操作-页面动态展示数据库中的数据(十)
    1、数据准备工作首先增加2篇文章用于展示数据。 我用的mysql数据库,使用pycharm的DBBrowser进行数据查询。双击blog库下面对应的文章表,则显示当前数据。 2、查询数据并动态展示models.py里的类就是一个模板,在views.py引入并实例化。即将值查询出来并赋值到一个对象,在页......