# VPP 开发简析 ![node_graph.png](../../_static/node_graph.png) ![node.png](../../_static/node.png) ## 软件框架 VPP 的软件框架主要分为四个层面: ![code_architecture.png](../../_static/code_architecture.png) - Plugins:包含越来越丰富的数据平面插件集,可以认为每一个插件是一个小型的 APP。 - VNET:与 VPP 的网络接口(L2-4)协同工作,执行会话和流量管理,并与设备和数据控制平面配合使用。 - VLIB:矢量处理库。VLIB 层还处理各种应用程序管理功能,例如:缓冲区,内存和 graph 管理,维护和导出计数器,线程管理,数据包跟踪。VLIB 实现调试 CLI 等。 - VPP Infra:VPP 基础设施层,包含核心库源代码。提供一些基本的通用的功能函数库,包括内存管理,向量操作,hash, timer 等 ## 核心概念 ### Node VPP 通过 Node 的级联组成 Graph,实现报文的流转处理;Node 就是处理报文的最小逻辑单元,一个 Node 就代表着一个 处理报文的逻辑。 Node 级联的需要关注: * 上一级 pre_node。 * 报文处理。 * 处理完成后的下一级 Node。 node 注册关键宏: ```c /* 注册 node 宏 */ VLIB_REGISTER_NODE (gtpu4_input_node) = { .name = "gtpu4-input", // name 必须唯一 /* Takes a vector of packets. */ .vector_size = sizeof (u32), .n_errors = GTPU_N_ERROR, .error_strings = gtpu_error_strings, .n_next_nodes = GTPU_INPUT_N_NEXT, // 下挂了多少可调度的 node .next_nodes = { #define _(s,n) [GTPU_INPUT_NEXT_##s] = n, // 下挂的可调度的 node foreach_gtpu_input_next #undef _ }, ... /* 注册 node 处理函数宏 ,需要注意 gtpu4_input_node 必须通过 注册node 宏注册 node 一致*/ VLIB_NODE_FN (gtpu4_input_node) (vlib_main_t * vm, vlib_node_runtime_t * node, vlib_frame_t * from_frame) { return gtpu_input(vm, node, from_frame, /* is_ip4 */ 1); } ``` 如何级联进 Graph,主要就是上一级 pre_node。 * 静态方式:通过修改需要级联的上一级 pre_node 的 next_nodes, 并且需要对应修改 其处理函数,使报文可正常流转到本 Node。 * 固有钩子函数方式: 在不使用 Feature 机制的话,结点间的关系相对来说更加静态,只能在编译的时候确定结点间的关系,不能在运行的时候进行改变,可以插入节点的地方只能由系统提供的几个接口。 向这些入口登记函数后,后续的数据流将传到你定义的结点。 可能还有其他的一些插入结点的函数,这里只列出常用到的几个函数: * L1 `vnet_hw_interface_rx_redirect_to_node (vnet_main_t *vnm, u32 hw_if_index, u32 node_index)` 将某个 hw interface 的 rx 数据重定向到某个结点,node_index 为结点的index索引。 * L2、L3 `ethernet_register_input_type (vlib_main_t *vm, ethernet_type_t type, u32 node_index)` 将在 `ethernet-input` 结点后插入特定 type 的结点,这里 type 包括 `ethernet_type(0x806, ARP)`、`ethernet_type (0x8100, VLAN)`、`ethernet_type (0x800, IP4)` 等二、三层协议。 具体支持的相关协议见 `src/vnet/ethernet/types.def` 文件。 * L4 `ip4_register_protocol (u32 protocol, u32 node_index)` 将在 `ip4-local` 结点后插入特定 protocol 的结点,这里 protocol 包括 `ip_protocol (6, TCP)`、`ip_protocol (17, UDP)` 等四层协议。 具体支持的相关协议见 `src/vnet/ip/protocols.def` 文件。 * L5 `udp_register_dst_port (vlib_main_t * vm, udp_dst_port_t dst_port, u32 node_index, u8 is_ip4)` 将在 `ip4-udp-lookup` 结点后插入特定 `dst_port` 的结点,这里 `dst_port` 包括 `ip_port (WWW, 80)` 等五层应用端口。 具体支持的相关端口见 `src/vnet/ip/ports.def` 文件。 ### Feature VPP 本身的所谓的静态 Node 框架比较固定,各个 Node 之间逻辑连接已经固化,为此新版本增加了 Feature 机制,这里 Feature 机制本质上来说还是结点,只不过该结点可以在运行的时候通过命令进行配置是否打开或关闭,从而影响数据流的走向。 对新加入的结点进行管理,新的 Feature,即:我们新建的结点。必须属于某个 Arc 类,并作用于某个 Interface 实体。通过 `set interface feature arc [disable]` 命令来开启或关闭该 Feature 功能。通常 Arc 类的名字对应为其起点结点的名字,使用命令开启关闭 Feature 功能能动态的改变数据的流向。 如果选择按照 Feature 机制来加入结点的话需要注意以下几点: - VPP 提供的 Arc 类比较多,我们需要自己选择合适的 Arc 来插入我们的结点: 1. `nsh-output` 2. `mpls-output` 3. `mpls-input` 4. `ip6-drop` 5. `ip6-punt` 6. `ip6-local` 7. `ip6-output` 8. `ip6-multicast` 9. `ip6-unicast` 10. `ip4-drop` 11. `ip4-punt` 12. `ip4-local` 13. `ip4-output` 14. `ip4-multicast` 15. `ip4-unicast` 16. `ethernet-output` 17. `interface-output` 18. `device-input` - Feature 相关宏: ```c /* 注册 feature 宏*/ VNET_FEATURE_INIT (test0214, static) = { .arc_name = "device-input", .node_name = "test0214", // feature 相关 node,必须和 node name 相同 .runs_before = VNET_FEATURES ("ethernet-input"), }; /* feature 打开关闭 cli */ VLIB_CLI_COMMAND (test0214_enable_disable_command, static) = { .path = "test0214 enable-disable", .short_help = "test0214 enable-disable [disable]", .function = test0214_enable_disable_command_fn, }; /* feature 相关 node 注册 */ VLIB_REGISTER_NODE (test0214_node) = { .name = "test0214", .vector_size = sizeof (u32), .format_trace = format_test0214_trace, .type = VLIB_NODE_TYPE_INTERNAL, .n_errors = ARRAY_LEN(test0214_error_strings), .error_strings = test0214_error_strings, .n_next_nodes = TEST0214_N_NEXT, /* edit / add dispositions here */ .next_nodes = { [TEST0214_NEXT_INTERFACE_OUTPUT] = "interface-output", }, }; /* 打开 feature 相关函数*/ static clib_error_t * test0214_enable_disable_command_fn (vlib_main_t * vm, unformat_input_t * input, vlib_cli_command_t * cmd) { test0214_main_t * tmp = &test0214_main; u32 sw_if_index = ~0; int enable_disable = 1; int rv; while (unformat_check_input (input) != UNFORMAT_END_OF_INPUT) { if (unformat (input, "disable")) enable_disable = 0; else if (unformat (input, "%U", unformat_vnet_sw_interface, tmp->vnet_main, &sw_if_index)); else break; } if (sw_if_index == ~0) return clib_error_return (0, "Please specify an interface..."); rv = test0214_enable_disable (tmp, sw_if_index, enable_disable); ... return 0; } int test0214_enable_disable (test0214_main_t * tmp, u32 sw_if_index, int enable_disable) { vnet_sw_interface_t * sw; int rv = 0; /* Utterly wrong? */ if (pool_is_free_index (tmp->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 (tmp->vnet_main, sw_if_index); if (sw->type != VNET_SW_INTERFACE_TYPE_HARDWARE) return VNET_API_ERROR_INVALID_SW_IF_INDEX; test0214_create_periodic_process (tmp); vnet_feature_enable_disable ("device-input", "test0214", sw_if_index, enable_disable, 0, 0); ... } ``` ### Plugin 更全面的功能,相当于一个 小型 App 的处理功能。 注册 Plugin 关键宏: ```c /* plugin 初始化函数,基本所有初始化都会在该函数中完成,包括 node 在 graph 中的挂载 */ VLIB_INIT_FUNCTION (test0214_init); /* 相关feature */ VNET_FEATURE_INIT (test0214, static) = { .arc_name = "device-input", .node_name = "test0214", .runs_before = VNET_FEATURES ("ethernet-input"), }; /* 注册 plugin */ VLIB_PLUGIN_REGISTER () = { .version = VPP_BUILD_VER, .description = "test0214 plugin description goes here", }; ``` 注 :VLIB_INIT_FUNCTION 宏讲述透彻参考 https://blog.csdn.net/qq_39965097/article/details/103726055 ### 如何创建自己的 Plugin 目前 VPP 提供了一个创建插件的脚本,直接使用这个脚本就可以创建我们需要的插件基本框架。 如果自己系统没有安装 emacs,需要安装一下: ```bash sudo apt update sudo apt install -y emacs ``` 需要提供两个设置: 1. 插件的名字 2. 调度类型,有双单环路对还是四单环路对下面是具体命令: ```bash $ cd ./src/plugins $ ../../extras/emacs/make-plugin.sh Loading /scratch/vpp-docs/extras/emacs/tunnel-c-skel.el (source)... Loading /scratch/vpp-docs/extras/emacs/tunnel-decap-skel.el (source)... Loading /scratch/vpp-docs/extras/emacs/tunnel-encap-skel.el (source)... Loading /scratch/vpp-docs/extras/emacs/tunnel-h-skel.el (source)... Loading /scratch/vpp-docs/extras/emacs/elog-4-int-skel.el (source)... Loading /scratch/vpp-docs/extras/emacs/elog-4-int-track-skel.el (source)... Loading /scratch/vpp-docs/extras/emacs/elog-enum-skel.el (source)... Loading /scratch/vpp-docs/extras/emacs/elog-one-datum-skel.el (source)... Plugin name: test0214 Dispatch type [dual or qs]: dual (Shell command succeeded with no output) OK... ``` 调度类型暂时我还不太清楚有多大差异,暂时选择dual模式,后面自己根据自己业务,对插件做相关的修改就行。 生成出来的文件: ```bash $ cd .plugins/test0214 $ ls CMakeLists.txt node.c setup.pg test0214.api test0214.c test0214.h test0214_periodic.c test0214_test.c ``` #### 重新编译插件 ```bash $ cd $ make rebuild [or rebuild-release] ``` #### 验证插件是否正常 ```bash vpp# show plugins test0214 Plugin path is: /usr/lib/x86_64-linux-gnu/vpp_plugins:/usr/lib/vpp_plugins Plugin Version Description ...... 14. test0214_plugin.so 1.0-release test0214 plugin description goes here ...... ``` 如果上面有显示自己插件的信息,表示你提供的插件功能基本完备,能正常加载使用了。 #### 测试插件 默认创建的插件已经实现了以下功能: 1. 注册了 process 节点,监听插件是否工作的事件(MYPLUGIN_EVENT_PERIODIC_ENABLE_DISABLE),通过命令行来触发 (VLIB_CLI_COMMAND (myplugin_enable_disable_command, static)) 这个事件。使用这里 enable 了,该插件才会 work。 2. 注册了内部节点,让其在 ethernet-input 节点运行之前运行。 ```bash VLIB_REGISTER_NODE (test0214_node) = { .name = "test0214", .vector_size = sizeof (u32), .format_trace = format_test0214_trace, .type = VLIB_NODE_TYPE_INTERNAL, .n_errors = ARRAY_LEN(test0214_error_strings), .error_strings = test0214_error_strings, .n_next_nodes = TEST0214_N_NEXT, /* edit / add dispositions here */ .next_nodes = { [TEST0214_NEXT_INTERFACE_OUTPUT] = "interface-output", }, }; VNET_FEATURE_INIT (test0214, static) = { .arc_name = "device-input", .node_name = "test0214", .runs_before = VNET_FEATURES ("ethernet-input"), }; ``` 3. 在内部节点的实现函数里面(VLIB_NODE_FN (myplugin_node)),主要实现功能是对 input 节点收进来的报文,做一个 src dst mac 交换,然后源端口发送出去。可自行阅读 ```bash VLIB_NODE_FN (test0214_node) (vlib_main_t * vm, vlib_node_runtime_t * node, vlib_frame_t * frame) ``` ## VPP 中如何处理报文流转 VPP 中的报文流转实际体现在两个方面: * 如何获取到报文,然后按自己逻辑正确处理 * 如何将处理完成的报文 送入到 期望的next node 关键函数: 如何获取报文: * `vlib_frame_vector_args (frame)` 本 Node 收到 Vector 起始地址 * `vlib_get_next_frame (vm, node, next_index, to_next, n_left_to_next)` 获取下一 Node 的收包缓存空闲首地址 * `vlib_buffer_get_current (b0)` 获取 vlib_buffer_t 如何将处理完成的报文送入期望的 next node: * `vlib_validate_buffer_enqueue_x1 (vm, node, next_index, to_next, n_left_to_next, bi0, next0)` 根据真实下一 node(next0) ,调整next_index: 默认的下一结点的 index;next0: 实际的下一个结点的 index next0 == next_index则不需要做特别的处理,报文会自动进入下一个节点 next0 != next_index则需要对该数据包做调整,从之前next_index对应的frame中删除,添加到next0对应的frame中 * `STATIC_ASSERT (sizeof (upf_buffer_opaque_t) <=STRUCT_SIZE_OF (vnet_buffer_opaque2_t, unused),"upf_buffer_opaque_t too large for vnet_buffer_opaque2_t")` `#define upf_buffer_opaque(b) ((upf_buffer_opaque_t *)((u8 *)((b)->opaque2) + STRUCT_OFFSET_OF (vnet_buffer_opaque2_t, unused)))` 自定义报文元数据; * `vlib_put_next_frame (vm, node, next_index, n_left_to_next)` 所有流程都正确处理完毕后,下一结点的 frame 上已经有本结点处理过后的数据索引执行该函数,将相关信息登记到 vlib_pending_frame_t 中,准备开始调度处理 ```c always_inline uword gtpu_input (vlib_main_t *vm, vlib_node_runtime_t *node, vlib_frame_t *from_frame, u8 is_ip4) { u32 n_left_from, next_index, *from, *to_next; // flowtable_main_t* fm = &flowtable_main; clib_bihash_kv_16_8_t last_key4; clib_bihash_kv_24_8_t last_key6; u32 pkts_decapsulated = 0; if (is_ip4) memset (&last_key4, 0xff, sizeof (last_key4)); else memset (&last_key6, 0xff, sizeof (last_key6)); // 获取报文对应vector 包起始地址 from = vlib_frame_vector_args (from_frame); n_left_from = from_frame->n_vectors; // 获取上一次NODE 处理后对应的 NEXT-NODE 索引 next_index = node->cached_next_index; upf_trace ("######gtpu_input#####\n"); // 一次单个处理报文 while (n_left_from > 0) { u32 n_left_to_next; // to_next: next_index所指下一个节点的收包缓存的空闲位置首地址 // 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) { u32 bi0; vlib_buffer_t *b0; // u32 is_reverse0; // next0 下一节点索引 u32 next0 = GTPU_INPUT_NEXT_DROP; ip4_header_t *ip4_0; ip6_header_t *ip6_0; gtpu_header_t *gtpu0; u32 gtpu_hdr_len0 = 0; u32 session_index0; u32 error0; u16 hdr_len0; pdu_sess_info_t *pdu_sess_info_p = NULL; upf_session_t *sx; bi0 = from[0]; to_next[0] = bi0; from += 1; to_next += 1; n_left_from -= 1; n_left_to_next -= 1; // 从当前node 中 获取vlib_buffer_t b0 = vlib_get_buffer (vm, bi0); /* udp leaves current_data pointing at the gtpu header */ // 从vlib_buffer_t 中获取buffer 头,类似于rte_pktmbuf_mtod gtpu0 = vlib_buffer_get_current (b0); hdr_len0 = is_ip4 ? sizeof (*ip4_0) : sizeof (*ip6_0); hdr_len0 += sizeof (udp_header_t); if (is_ip4) { // 移动b0至原始报文外层ip 头 vlib_buffer_advance ( b0, -(word) (sizeof (udp_header_t) + sizeof (ip4_header_t))); ip4_0 = vlib_buffer_get_current (b0); } else { vlib_buffer_advance ( b0, -(word) (sizeof (udp_header_t) + sizeof (ip6_header_t))); ip6_0 = vlib_buffer_get_current (b0); } session_index0 = ~0; error0 = 0; // PREDICT_FALSE 相当于unlikely if (PREDICT_FALSE ((gtpu0->ver_flags & GTPU_VER_MASK) != GTPU_V1_VER)) { error0 = GTPU_ERROR_BAD_VER; next0 = GTPU_INPUT_NEXT_DROP; goto trace00; } ... ... session_index0 = upf_buffer_opaque (b0)->upf.session_index; if (session_index0 == ~0) { next0 = GTPU_INPUT_NEXT_DROP; goto trace00; } sx = pool_elt_at_index (upf_main.sessions, session_index0); /* Manipulate gtpu header */ if ((gtpu0->ver_flags & GTPU_E_S_PN_BIT) != 0) { /* Manipulate Sequence Number and N-PDU Number */ /* TBD */ /* Manipulate Next Extension Header */ if (gtpu0->ver_flags & 0x04) { u8 gtp_ex_hdr_type; u8 gtp_ex_hdr_len; u16 total_gtp_ex_hdr_len = 0; u8 *gtp_ex_hdr_p; gtp_ex_hdr_type = gtpu0->next_ext_type; gtp_ex_hdr_p = (u8 *)gtpu0 + sizeof (gtpu_header_t); while (gtp_ex_hdr_type) { // printf("%s:%d gtp_ex_hdr_type:%x\n", // __func__,__LINE__,gtp_ex_hdr_type); gtp_ex_hdr_len = *gtp_ex_hdr_p; total_gtp_ex_hdr_len = +gtp_ex_hdr_len * 4; #if 0 if (total_gtp_ex_hdr_len > gtpu_hdr_len0 - sizeof (gtpu_header_t)) { error0 = GTPU_ERROR_BAD_FLAGS; next0 = GTPU_INPUT_NEXT_DROP; goto trace00; } #endif gtp_ex_hdr_p++; switch (gtp_ex_hdr_type) { case GTP_EX_TYPE_PDU_SESS: pdu_sess_info_p = (pdu_sess_info_t *)gtp_ex_hdr_p; break; default: break; } u32 offset; offset = gtp_ex_hdr_len * 4 - 2; gtp_ex_hdr_p = gtp_ex_hdr_p + offset; gtp_ex_hdr_type = *gtp_ex_hdr_p; gtp_ex_hdr_p++; } gtpu_hdr_len0 = sizeof (gtpu_header_t) + total_gtp_ex_hdr_len; } } else { gtpu_hdr_len0 = sizeof (gtpu_header_t) - 4; } hdr_len0 += gtpu_hdr_len0; upf_buffer_opaque (b0)->upf.data_offset = hdr_len0; upf_buffer_opaque (b0)->upf.teid = clib_net_to_host_u32 (gtpu0->teid); upf_buffer_opaque (b0)->upf.flags = (is_ip4) ? BUFFER_GTP_UDP_IP4 : BUFFER_GTP_UDP_IP6; if (NULL != pdu_sess_info_p) { upf_buffer_opaque (b0)->upf.extension_hdr_flag = GTP_EXT_HDR_FLAG; upf_buffer_opaque (b0)->upf.pdu_type = pdu_sess_info_p->pdu_type; upf_buffer_opaque (b0)->upf.qfi = pdu_sess_info_p->qfi; upf_buffer_opaque (b0)->upf.rqi = pdu_sess_info_p->rqi; upf_trace ("upf_buffer_opaque (b0)->upf.qfi:%d, dir:%d", upf_buffer_opaque (b0)->upf.qfi, pdu_sess_info_p->pdu_type); } if (sx->pdn_type == PDN_TYPE_ETHERNET) { next0 = GTPU_INPUT_NEXT_ETH_INPUT; } /* inner IP header */ else if (is_v4_packet ( (u8 *)(vlib_buffer_get_current (b0) + hdr_len0))) { ip4_0 = vlib_buffer_get_current (b0) + hdr_len0; if (PREDICT_FALSE (ip4_is_fragment (ip4_0))) { vnet_buffer (b0)->ip.reass.next_index = upf_main.ip4_reass_next; vlib_buffer_advance ( b0, upf_buffer_opaque (b0)->upf.data_offset); next0 = GTPU_INPUT_NEXT_IP4_REASSEMBLY; } else { // 指定next node next0 = GTPU_INPUT_NEXT_PDR_DETECT; } } else if (is_v6_packet ( (u8 *)(vlib_buffer_get_current (b0) + hdr_len0))) { ip6_0 = vlib_buffer_get_current (b0) + hdr_len0; if (PREDICT_FALSE (ip6_0->protocol == IP_PROTOCOL_IPV6_FRAGMENTATION)) { vnet_buffer (b0)->ip.reass.next_index = upf_main.ip6_reass_next; vlib_buffer_advance ( b0, upf_buffer_opaque (b0)->upf.data_offset); next0 = GTPU_INPUT_NEXT_IP6_REASSEMBLY; } else { next0 = GTPU_INPUT_NEXT_PDR_DETECT; } } else { next0 = GTPU_INPUT_NEXT_PDR_DETECT; } pkts_decapsulated++; trace00: b0->error = error0 ? node->errors[error0] : 0; if (PREDICT_FALSE (b0->flags & VLIB_BUFFER_IS_TRACED)) { gtpu_rx_trace_t *tr = vlib_add_trace (vm, node, b0, sizeof (*tr)); tr->next_index = next0; tr->error = error0; tr->session_index = session_index0; tr->teid = clib_net_to_host_u32 (gtpu0->teid); } 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); } /* Do we still need this now that tunnel tx stats is kept? */ vlib_node_increment_counter ( vm, is_ip4 ? gtpu4_input_node.index : gtpu6_input_node.index, GTPU_ERROR_DECAPSULATED, pkts_decapsulated); return from_frame->n_vectors; } ``` ## vpp 初始化流程 ![vpp_development_image1.png](../../_static/vpp_development_image1.png)