XDP入门--eBPF程序实现网桥/二层交换机转发功能

news/2024/11/20 0:35:11/

本文目录

  • 1、试验环境
  • 2、eBPF字节码源代码实现
  • 3、用户态应用层管理与控制程序的源代码实现
  • 4、编译与运行
  • 5、测试结果

我们在此文的进阶部分 或者 此文中已经描述了如何设置Linux网桥,并将多个以太接口加入网桥后实现一个最基本的二层交换机的二层交换转发功能。Linux网桥可以学习到连接到网桥的每个端口上的各个设备MAC地址,并生成MAC/端口映射表,进而依据这个MAC/端口映射表对收到的数据包进行二层转发。

主要分以下二个过程:

  • MAC地址的学习与老化,生成并更新MAC/端口映射表(控制面过程)
  • 根据MAC/端口映射表,将收到的报文从正确的端口进行转发(数据面过程)

我们知道,配置完Linux网桥后,就自动具备了这个功用,但我们知道,如果使用Linux网桥来实现这个功能的话,每个数据包,需要经过XDP, Qdisc, Bridge_check,netfilter的link-layer的各个tables/chains的处理后(详见此文),才能转到正确的出口,转发的链路很长,效率和性能比较低。

这里我们准备使用BPF框架实现的Linux网桥的基本功能,以绕过Qdisc, bridge_check, netfilter等内核协议栈处理程序,实现性能更好的Linux网桥。
本例子中,会实现数据面和控制面分离(前提,读者已经理解了如何用用户态程序自动去加载与卸载eBPF字节码):

  • 由BPF程序在XDP框架里查询MAC地址与端口对应表实现报文转发
  • 由运行在用户态的应用程序管理MAC地址与端口对应表的更新与老化

我们可以借此进一步理解BPF, BPF map的框架与BPF程序的工作原理。

1、试验环境

试验环境与上一篇文章XDP入门–通过用户态程序自动加载与卸载eBPF程序字节码到网卡相同。

硬件:基于树莓派Zero w + 带二个以太网卡的扩展底板----图中的RPi
网络:如下图所示

                                                     +- RPi -------+          +- old pc1----+|         Eth0+----------+ Eth0        |    +- Router ----+                     |  DHCP server|          | 10.0.0.10   || Firewall    |                     |   10.0.0.1  |          |             |
(Internet)---WAN-+ DHCP server +-WLAN AP-+-)))   (((-+ WLAN        |          +-------------+| 192.168.3.1 |                     |             |          +-------------+                     |             |          +- old pc2----+|         Eth1+----------+ Eth0        |   |             |          | 10.0.0.4    |                                                       +-------------+          |             |+-------------+

在这里插入图片描述

2、eBPF字节码源代码实现

实现的原理很简单,检查每个收到的报文的目标mac地址,并在MAC地址-端口对应表里查询:

  1. 如果查不到,则上送给内核tcp/ip协议栈处理
  2. 如果查到了,则按目标mac地址对应的端口直接转发报文
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include "/usr/include/bpf/bpf_helpers.h"#ifndef __section
# define __section(NAME)                  \__attribute__((section(NAME), used))
#endif// mac_port_map保存该目标MAC地址/端口的映射关系表,以目标MAC地址为key, 以端口为value
struct bpf_map_def __section("maps") mac_port_map = {.type = BPF_MAP_TYPE_HASH,.key_size = sizeof(long long),.value_size = sizeof(int),.max_entries = 100,
};__section("prog")
int xdp_bridge_prog(struct xdp_md *ctx)
{void *data_end = (void *)(long)ctx->data_end;void *data = (void *)(long)ctx->data;long dst_mac = 0;int in_index = ctx->ingress_ifindex, *out_index;// data即数据包开始位置struct ethhdr *eth = (struct ethhdr *)data;char info_fmt[] = "Dst Addr:0x%llx From:[%d]---Redirect to:[%d]\r\n";char info_fmt1[] = "xdp_pass";char info_fmt2[] = "xdp_drop";// 错误包检查,必选 if (data + sizeof(struct ethhdr) > data_end) {return XDP_DROP;}// 获取目标MAC地址__builtin_memcpy(&dst_mac, eth->h_dest, 6);// 目标MAC地址/端口的映射表里查找目标端口out_index = bpf_map_lookup_elem(&mac_port_map, &dst_mac);if (out_index == 0) {// 如若找不到,则上传到内核TCP/IP协议栈处理bpf_trace_printk(info_fmt1, sizeof(info_fmt1));return XDP_PASS;}// 错误报文,进出同一个端口的,丢弃if (in_index == *out_index) {bpf_trace_printk(info_fmt2, sizeof(info_fmt2));return XDP_DROP;}// 打印转发信息到/sys/kernel/tracing/trace_pipebpf_trace_printk(info_fmt, sizeof(info_fmt), dst_mac, in_index, *out_index);// 转发到目标端口return  bpf_redirect(*out_index, 0);
}char _license[] SEC("license") = "GPL";

3、用户态应用层管理与控制程序的源代码实现

功能也不复杂:

  1. 加载指定的eBPF字节码进所有二个网卡
  2. 监听mac地址notify更新、删除信息,并将目标MAC地址/端口的映射关系更新到mac_port_map 表,供eBPF字节码查询使用
  3. 退出时自动卸载eBPF字节码
#include <stdio.h>
#include <signal.h>
#include <sys/socket.h>
#include <net/if.h>
#include <bpf/bpf.h>
#include <linux/bpf.h>
#include <linux/rtnetlink.h>
#include "/usr/src/linux-6.1/tools/testing/selftests/bpf/bpf_util.h"int flags = XDP_FLAGS_UPDATE_IF_NOEXIST;
static int mac_port_map_fd;
static int *ifindex_list;// 退出时卸载XDP
static void int_exit(int sig)
{int i = 0;for (i = 0; i < 2; i++) {bpf_set_link_xdp_fd(ifindex_list[i], -1, 0);}exit(0);
}int main(int argc, char *argv[])
{int sock, i;char buf[1024];char filename[64];static struct sockaddr_nl g_addr;struct bpf_object *obj;struct bpf_prog_load_attr prog_load_attr = {.prog_type      = BPF_PROG_TYPE_XDP,};int prog_fd;printf("we are starting...\r\n");snprintf(filename, sizeof(filename), "bridge.o");prog_load_attr.file = filename;// 载入eBPF代码if (bpf_prog_load_xattr(&prog_load_attr, &obj, &prog_fd)) {return 1;}mac_port_map_fd = bpf_object__find_map_fd_by_name(obj, "mac_port_map");ifindex_list = (int *)calloc(2, sizeof(int *));//通过ifname查询所有二个网卡的ifindexifindex_list[0] = if_nametoindex(argv[1]);ifindex_list[1] = if_nametoindex(argv[2]);for (i = 0; i < 2; i++) {// 将eBPF字节码注入到所有网卡if (bpf_set_link_xdp_fd(ifindex_list[i], prog_fd, flags) < 0) {printf("link set xdp fd failed\n");return 1;}}// 设置CTRL+C退出程序时要执行的卸载函数signal(SIGINT, int_exit);bzero(&g_addr, sizeof(g_addr));g_addr.nl_family = AF_NETLINK;g_addr.nl_groups = RTM_NEWNEIGH;printf("we are starting socket...\r\n");if ((sock = socket(AF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE)) < 0) {int_exit(0);return -1;}if (bind(sock, (struct sockaddr *) &g_addr, sizeof(g_addr)) < 0) {int_exit(0);return 1;}// 持续监听socket,捕获更新信息,更新删除MAC/端口对应表while (1) {int len;struct nlmsghdr *nh;struct ndmsg *ifimsg ;int ifindex = 0;unsigned char *cmac;unsigned long long lkey = 0;len = recv(sock, buf, sizeof(buf), 0);printf("recv...\r\n");if (len <= 0) continue;printf("get mac notification\r\n");for (nh = (struct nlmsghdr *)buf; NLMSG_OK(nh, len); nh = NLMSG_NEXT(nh, len)) {ifimsg = NLMSG_DATA(nh) ;if (ifimsg->ndm_family != AF_BRIDGE) {continue;printf("not AF_BRIDGE\r\n");}printf("AF_BRIDGE\r\n");// 获取notify信息中的端口ifindex = ifimsg->ndm_ifindex;for (i = 0; i < 2; i++) {printf("find ifindex = %d\r\n", ifindex);if (ifindex == ifindex_list[i]) break;}if (i == 2) continue;printf("i=%d,  ifindex=%d\r\n",i,ifindex);// 获取notify信息中的MAC地址cmac = (unsigned char *)ifimsg + sizeof(struct ndmsg) + 4;memcpy(&lkey, cmac, 6);printf("sizeof lkey %d\r\n", sizeof(lkey));printf("2nd i=%d, ifindex=%d\r\n",i,ifindex);if (nh->nlmsg_type == RTM_DELNEIGH) {bpf_map_delete_elem(mac_port_map_fd, (const void *)&lkey);printf("Delete XDP item from [HW Address:Port] table-------------[0x%llx]:[%d]\r\n", lkey, ifindex);} else if (nh->nlmsg_type == RTM_NEWNEIGH) {bpf_map_update_elem(mac_port_map_fd, (const void *)&lkey, (const void *)&ifindex, 0);printf("Update XDP item from [HW Address:Port] table-------------[0x%llx]:[%d]\r\n", lkey, ifindex);}}printf("out of for()\r\n");}
}

4、编译与运行

  • 编译
sudo clang -O2 -Wall -target bpf -c bridge.c -o bridge.o
gcc main.c -lbpf

在当前目录下生成a.out和brideg.o二个文件

  • 运行
 sudo ./a.out eth0 eth1

5、测试结果

我们通过,关闭和开启网桥的功能,来对这个代码进行测试。

步骤如下:

  1. 初始状态下,网桥br0是开启,未运行本程序时,我们从10.0.0.4 ping 10.0.0.10,然后在10.0.0.4这个网口上用tcpdump命令抓包。
    可以看到:
  • ping是通的
    在这里插入图片描述

  • tcpdump能看到icmp req/reply报文
    在这里插入图片描述

  1. 运行程序,并先关闭然后开启网桥
  • 运行程序
sudo ./a.out eth0 eth1
  • 在另外一个终端运行关闭和开启网桥,来触发MAC地址端口的更新通知
sudo ifconfig br0 down
sudo ifconfig br0 up

用户面程序会打印如下,先是删除二个表项,然后增加二个表项

get mac notification
AF_BRIDGE
find ifindex = 3
i=0,  ifindex=3
sizeof lkey 8
2nd i=0, ifindex=3
Delete XDP item from [HW Address:Port] table-------------[0x26513558577c]:[3]
out of for()
recv...
get mac notification
AF_BRIDGE
find ifindex = 4
find ifindex = 4
i=1,  ifindex=4
sizeof lkey 8
2nd i=1, ifindex=4
Delete XDP item from [HW Address:Port] table-------------[0x40f046730318]:[4]
out of for()
recv...
get mac notification
out of for()
recv...
get mac notification
out of for()
recv...
get mac notification
out of for()
recv...
get mac notification
AF_BRIDGE
find ifindex = 3
i=0,  ifindex=3
sizeof lkey 8
2nd i=0, ifindex=3
Update XDP item from [HW Address:Port] table-------------[0x26513558577c]:[3]
out of for()
recv...
get mac notification
AF_BRIDGE
find ifindex = 4
find ifindex = 4
i=1,  ifindex=4
sizeof lkey 8
2nd i=1, ifindex=4
Update XDP item from [HW Address:Port] table-------------[0x40f046730318]:[4]
out of for()
recv...
get mac notification
out of for()
recv...
get mac notification
out of for()
recv...
get mac notification
out of for()
recv...
get mac notification
out of for()
  • 查看ping和tcpdump的情况
    此时,ping是通的,但tcpdump里抓不到的icmp 的req/reply报文了
    在这里插入图片描述
    在这里插入图片描述
  • 查看内核打印log记录

我们可以看到10.0.0.4和10.0.0.10之间的ping报文已经直接在XDP里转发走了。

sudo cat /sys/kernel/tracing/trace_pipe

在这里插入图片描述


http://www.ppmy.cn/news/98521.html

相关文章

Linux下的yum和vim

目录 一、Linux软件包管理器yum1.1 何为软件包&#xff1f;1.2 rzsz工具1.3 如何安装和卸载软件&#xff1f;1.4 Linux的软件生态 二、vim文本编辑器 一、Linux软件包管理器yum 1.1 何为软件包&#xff1f; 软件包可以理解成是windows下别人提前编译好的安装包程序&#xff0…

Linux——使用命令行参数管理环境变量

目录 使用命令行参数获取用户在DOS命令行输入的指令&#xff1a; 方法&#xff1a;代码如下&#xff1a; 使用命令行参数获取并打印部分或者整体环境变量的方法&#xff1a; 方法1&#xff1a; 运行结果&#xff1a; 方法2&#xff1a;使用外部链接environ: 使用命令行参数…

应该选择网络安全还是程序员?

很长的时间我都在思考这个问题.&#xff0c;根据自己的经验和朋友们的讨论后得出了一些结论&#xff0c;网络安全 这个概念太广&#xff0c;我就以安服/渗透岗作为比较的对象&#xff0c;题主可以参考一下&#xff1a; 程序员&#xff1a; 优点&#xff1a; 1.薪资非常高&a…

【js】对象属性的拦截和Proxy代理与Reflect映射的用法与区别

✍️ 作者简介: 前端新手学习中。 &#x1f482; 作者主页: 作者主页查看更多前端教学 &#x1f393; 专栏分享&#xff1a;css重难点教学 Node.js教学 从头开始学习 ajax学习 文章目录 对象属性的拦截介绍SetGet 对象的拦截介绍使用对象属性拦截和对象拦截区别练习题 映射…

Etcdctl 命令v3

一、v3必须导出环境变量 export ETCDCTL_API3 二、查看版本 etcdctl version 三、写入键 1.基本 etcdctl put foo bar 2.绑定租约 etcdctl put foo bar --leasexxxx 四、获取键 1.基本 etcdctl get foo 2.按十六进制获取 etcdctl get foo --hex 3.只读取键值 et…

Spring高手之路——深入理解与实现IOC依赖查找与依赖注入

本文从xml开始讲解&#xff0c;注解后面给出 文章目录 1. 一个最基本的 IOC 依赖查找实例2. IOC 的两种实现方式2.1 依赖查找&#xff08;Dependency Lookup&#xff09;2.2 依赖注入&#xff08;Dependency Injection&#xff09; 3. 在三层架构中的 service 层与 dao 层体会依…

数字信号处理8:利用Python进行数字信号处理基础

我前两天买了本MATLAB信号处理&#xff0c;但是很无语&#xff0c;感觉自己对MATLAB的语法很陌生&#xff0c;看了半天也觉得自己写不出来&#xff0c;所以就对着MATLAB自己去写用Python进行的数字信号处理基础&#xff0c;我写了两天左右&#xff0c;基本上把matlab书上的代码…

​【编写UI自动化测试集】Appium+Python+Unittest+HTMLRunner​

简介 获取AppPackage和AppActivity 定位UI控件的工具 脚本结构 PageObject分层管理 HTMLTestRunner生成测试报告 启动appium server服务 以python文件模式执行脚本生成测试报告 下载与安装 下载需要自动化测试的App并安装到手机 获取AppPackage和AppActivity 方法一 有源码的…