实现一个基于XDP/eBPF的学习型网桥
From 《实现一个基于XDP_eBPF的学习型网桥》
eBPF技术风靡当下,eBPF字节码正以星火燎原之势被HOOK在Linux内核中越来越多的位置,在这些HOOK点上,我们可以像编写普通应用程序一样编写内核的HOOK程序,与以往为了实现一个功能动辄patch一整套逻辑框架代码(比如基于Netfilter的iptales,freeswan,ipvs…)相比,eBPF的工作方式非常简便且灵活。
我们先来看一下目前eBPF的一些重要HOOK点:
将来这个is_XXX序列肯定会不断增加,布满整个内核(有点密集恐惧症症状了…)。
eBPF可以实现大部分内核数据的采集功能,但这些没意思。有点意思的是eBPF还可以完成内核路径的HOOK,换句话说,eBPF可以向内核注入一段代码,至于这段代码可以干什么,随他去吧!
本文将描述如何用eBPF实现一个学习型网桥的快速转发,并将其部署在XDP。
在开始之前,为了让包括我自己在内的所有人都能看懂本文(我承认自己照猫画虎写了这个代码),我们先来回顾一些前置知识,如果暂时还不懂这些前置知识,没关系,先把程序run起来是一个很好的起点,如果程序exit或者被X的时候你觉得这毫无意义,再放弃也不迟。
前置知识
- 什么是BPF和eBPF
简单来讲,BPF是一套完整的 计算机体系结构 规范。和x86,ARM这些类似,BPF规范了自己的指令集和运行时逻辑,同理,就像在x86平台编程,最终要落实到x86汇编指令一样,BPF字节码也可以看成是汇编指令的序列。我们通过tcpdump的-d/-dd参数可见一斑:
[root@localhost ~]# tcpdump -i any tcp and host 1.1.1.1 -d
(000) ldh [14]
(001) jeq #0x86dd jt 10 jf 2
(002) jeq #0x800 jt 3 jf 10
(003) ldb [25]
(004) jeq #0x6 jt 5 jf 10
(005) ld [28]
(006) jeq #0x1010101 jt 9 jf 7
(007) ld [32]
(008) jeq #0x1010101 jt 9 jf 10
(009) ret #262144
(010) ret #0
[root@localhost ~]#
BPF的历史非常古老,早在1992年就被构建出来了,其背后的思想是, “与其把数据包复制到用户空间执行用户态程序过滤,不如把过滤程序灌进内核去。”
遗憾的是,BPF后来并没有大行其道,其应用仅仅被局限在非常有限的且并不起眼的比如抓包领域。因此,由于它的语法并不复杂,人们直接手写BPF汇编指令码经简单封装即可生成最终的字节码。
手写BPF指令码,然后加载,这是一件简单的事情,它无法形成一股洪流把所有人卷入其中。
当人们认识到BPF非常强壮的功能并欲将其大用时,指令系统以及操作系统内核均已经持续进化了好多年,这意味着简单的BPF不能再满足需要,它需要 “被复杂化” 。
于是就出现了eBPF,即extended BPF。总体而言,eBPF相比BPF有了以下改进:
1. 更复杂的指令系统。
2. 更多可调用的函数。
3. …
详情可参见下面的链接:
https://lwn.net/Articles/740157/
就像汇编语言进化到C语言一样,直接手写eBPF字节码除了炫技之外显得即笨拙又低效,手写eBPF指令显得一无是处,被经理嘲笑,于是人们重走了老路,人们开始使用C语言直接编写eBPF程序,然后用编译技术将其编译成eBPF字节码。
遗憾的是,目前eBPF体系结构还不被gcc支持,不过很快就会支持了:
https://lwn.net/Articles/796317/
当下,我们不得不使用 特定的编译器 来编译eBPF的C代码,比如clang。
- 什么是XDP
XDP,即eXpress Data Path,它其实是位于网卡驱动程序里的一个快速处理数据包的HOOK点,为什么快?基于以下两点:
- 数据包处理位置非常底层,避开了很多内核skb处理开销。
- 可以将很多处理逻辑Offload到网卡硬件。
显而易见,在XDP这个HOOK点灌进来一点eBPF字节码,将是一件令人愉快的事情。
- 学习型网桥
Linux的Bridge模块就是一个学习型网桥,其实就是一个现代交换式以太网交换机,它可以从端口学习到MAC地址,在内部生成MAC/端口映射表,以优化转发效率。
本文我们将用eBPF实现的网桥就是一个学习型网桥,并且它的数据路径和控制路径相分离,用eBPF字节码实现的正是其数据路径,它将被灌入XDP,而控制路径则由一个用户态程序实现。这是一个学习XDP/eBPF很好的起点。
- 如何编译eBPF程序
理论的学习自在平时(比如厕上,床上,车上),但当一个程序员打开电脑的时候,最快的速度run起来一些东西才是令人愉悦的,否则干嘛不用手机。
我们谁都不想花大量的时间在环境的搭建上。对于eBPF程序,内核源码树的samples/bpf目录将是一个非常好的起点。
以我自己的实验环境为例,我使用的是Ubuntu 19.10发行版,5.3.0-19-generic内核,安装源码后,编译之,最后编译samples/bpf即可:
root@zhaoya-VirtualBox:/usr/src/linux-source-5.3.0/linux-source-5.3.0/samples/bpf# make
make -C ../../ /usr/src/linux-source-5.3.0/linux-source-5.3.0/samples/bpf/ BPF_SAMPLES_PATH=/usr/src/linux-source-5.3.0/linux-source-5.3.0/samples/bpf
make[1]: Entering directory '/usr/src/linux-source-5.3.0/linux-source-5.3.0'CALL scripts/checksyscalls.shCALL scripts/atomic/check-atomics.shDESCEND objtool...
samples/bpf目录下的代码都是比较典型的范例,我们照猫画虎就能实现我们想要的功能。
大体上,每一个范例均由两个部分组成:
- XXX_kern.c文件:eBPF字节码本身。
- XXX_user.c文件:用户态控制程序,控制eBPF字节码的注入,更新。
即然我们要实现一个网桥,那么文件名我们可以确定为:
- xdp_bridge_kern.c
- xdp_bridge_user.c
同时我们修改Makefile文件,加入这两个文件的相关编译描述即可:
root@zhaoya-VirtualBox: samples/bpf# cat Makefile
...
hostprogs-y += xdp_bridge # new
...
xdp_bridge-objs := xdp_bridge_user.o # new
...
always += xdp_bridge_kern.o # new
环境具备,只差代码。
网桥XDP快速转发的实现
对上述前置知识有了充分的理解之后,代码就非常简单了,我们剩下的工作就是填充xdp_bridge_kern.c和xdp_bridge_user.c两个C文件,然后make它们。
我们先来看xdp_bridge_kern.c文件:
// xdp_bridge_kern.c
#include <uapi/linux/bpf.h>
#include <linux/if_ether.h>
#include "bpf_helpers.h"// mac_port_map保存该交换机的MAC/端口映射
struct bpf_map_def SEC("maps") mac_port_map = {.type = BPF_MAP_TYPE_HASH,.key_size = sizeof(long),.value_size = sizeof(int),.max_entries = 100,
};// 以下函数是网桥转发路径的eBPF主函数实现
SEC("xdp_br")
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[] = "Destination Address: %lx Redirect to:[%d] From:[%d]\n";// 畸形包必须丢弃,否则无法通过内核的eBPF字节码合法性检查if (data + sizeof(struct ethhdr) > data_end) {return XDP_DROP;}// 获取目标MAC地址__builtin_memcpy(&dst_mac, eth->h_dest, 6);// 在MAC/端口映射表里查找对应该MAC的端口out_index = bpf_map_lookup_elem(&mac_port_map, &dst_mac);if (out_index == NULL) { // 如若找不到,则上传到慢速路径,必要时由控制路径更新MAC/端口表项。return XDP_PASS;}// 非Hairpin下生效if (in_index == *out_index) { // Hairpin ?return XDP_DROP;}// 简单打印些调试信息bpf_trace_printk(info_fmt, sizeof(info_fmt), dst_mac, *out_index, in_index);// 转发到出端口return bpf_redirect(*out_index, 0);
}char _license[] SEC("license") = "GPL";
这里有必要说一下内核对eBPF程序的合法性检查,这个检查一点都不多余,它确保你的eBPF代码是安全的。这样才不会造成内核数据结构被破坏掉,否则,如果任意eBPF程序都能注入内核,那结局显然是细思极恐的一场悲剧,我们不能犯形而上学的错误。
现在继续我们的用户态C代码:
// xdp_bridge_user.c#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 "bpf_util.h"int flags = XDP_FLAGS_UPDATE_IF_NOEXIST;
static int mac_port_map_fd;
static int *ifindex_list;// 退出时卸载掉XDP的eBPF字节码
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指明eBPF字节码注入的位置,我们网桥的例子中当然是XDP.prog_type = BPF_PROG_TYPE_XDP,};int prog_fd;snprintf(filename, sizeof(filename), "xdp_bridge_kern.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 *));// 我们的例子中仅仅支持两个端口的网桥,事实上可以多个。ifindex_list[0] = if_nametoindex(argv[1]);ifindex_list[1] = if_nametoindex(argv[2]);for (i = 0; i < 2/*total */; i++) {// 将eBPF字节码注入到感兴趣网卡的XDPif (bpf_set_link_xdp_fd(ifindex_list[i], prog_fd, flags) < 0) {printf("link set xdp fd failed\n");return 1;}}signal(SIGINT, int_exit);bzero(&g_addr, sizeof(g_addr));g_addr.nl_family = AF_NETLINK;g_addr.nl_groups = RTM_NEWNEIGH;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,捕获Linux网桥上传的notify信息,从而更新,删除eBPF的map里特定的MAC/端口表项while (1) {int len;struct nlmsghdr *nh;struct ndmsg *ifimsg ;int ifindex = 0;unsigned char *cmac;unsigned long lkey = 0;len = recv(sock, buf, sizeof(buf), 0);if (len <= 0) continue;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;}// 获取notify信息中的端口ifindex = ifimsg->ndm_ifindex;for (i = 0; i < 2; i++) {if (ifindex == ifindex_list[i]) break;}if (i == 2) continue;// 获取notify信息中的MAC地址cmac = (unsigned char *)ifimsg + sizeof(struct ndmsg) + 4;memcpy(&lkey, cmac, 6);if (nh->nlmsg_type == RTM_DELNEIGH) {bpf_map_delete_elem(mac_port_map_fd, (const void *)&lkey);printf("Delete XDP bpf map-[HW Address:Port] item Key:[%lx] Value:[%d]\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 bpf map-[HW Address:Port] item Key:[%lx] Value:[%d]\n", lkey, ifindex);}}}
}
用户态程序同样很容易理解。
数据面和控制面分离,这是网络设备的标准路数,几十年前就这样了,熟悉Cisco CEF的当然知道这意味着什么,现如今我们也能简单实现一个了,很有趣不是吗?
然而code is cheap,so make it running。
run起来
执行make之后,我们可以得到可执行文件xdp_bridge以及eBPF字节码文件xdp_bridge_kern.o,在当前目录下直接执行即可:
root@zhaoya-VirtualBox:samples/bpf# ./xdp_bridge enp0s9 enp0s10
在另一个终端查看eBPF字节码里的map,即MAC/端口映射表:
root@zhaoya-VirtualBox:/home/zhaoya# bpftool p |tail -n 4
166: xdp name xdp_bridge_prog tag 956a68e9ac54a0b3 gplloaded_at 2019-11-08T01:14:46+0800 uid 0xlated 576B jited 340B memlock 4096B map_ids 105btf_id 114
root@zhaoya-VirtualBox:/home/zhaoya# bpftool map dump id 105
Found 0 elements
root@zhaoya-VirtualBox:/home/zhaoya#
OK,一切顺利。现在让我们正式用它搭建一个网桥吧。
暂时X掉xdp_bridge程序的运行,让我们一步一步来。
首先构建下面的拓扑:
中间的Linux Bridge主机(后面简称主机B)的enp0s9,enp0s10网卡将是我们注入eBPF字节码的位置。
现在让我们在主机B上创建一个标准的Linux网桥:
brctl addbr br0;
brctl addif br0 enp0s9;
brctl addif br0 enp0s10;
ifconfig br0 up;
在主机H1和主机H2的enp0s9上配置同网段的地址:
H1-enp0s9:40.40.40.201/24
H2-enp0s9:40.40.40.100/24
互相ping确认是通的,并且主机B的enp0s9/enp0s10可以抓到双向包,这说明主机B的Linux标准网桥工作是OK的。
接下来,停掉这一切,把br0也删除掉。重新运行xdp_bridge程序,确认OK后创建Linux标准网桥,从H1来ping H2,很畅通,同时我们会发现主机B的xdp_bridge程序的输出:
root@zhaoya-VirtualBox:/usr/src/linux-source-5.3.0/linux-source-5.3.0/samples/bpf# ./xdp_bridge enp0s9 enp0s10
Update XDP bpf map-[HW Address:Port] item Key:[683dbb270008] Value:[4]
Update XDP bpf map-[HW Address:Port] item Key:[683dbb270008] Value:[4]
Update XDP bpf map-[HW Address:Port] item Key:[e7f09f270008] Value:[5]
Update XDP bpf map-[HW Address:Port] item Key:[e7f09f270008] Value:[5]Update XDP bpf map-[HW Address:Port] item Key:[e6f09f270008] Value:[4]
很显然,eBPF的map学习到了新的MAC地址,我们可以用bpftool确认:
root@zhaoya-VirtualBox:~# bpftool p |tail -n 4
170: xdp name xdp_bridge_prog tag 956a68e9ac54a0b3 gplloaded_at 2019-11-08T01:26:19+0800 uid 0xlated 576B jited 340B memlock 4096B map_ids 107btf_id 117
root@zhaoya-VirtualBox:~# bpftool map dump id 107
key: 08 00 27 9f f0 e7 00 00 value: 05 00 00 00
key: 08 00 27 9f f0 e6 00 00 value: 04 00 00 00
key: 08 00 27 bb 3d 68 00 00 value: 04 00 00 00
Found 3 elements
此时,主机B的enp0s9和enp0s10就抓不到任何H1和H2之间单播包了。广播包仍然会被上传到慢速路径被标准Linux网桥处理。
我们看trace日志:
root@zhaoya-VirtualBox:~# cat /sys/kernel/debug/tracing/trace_pipe<idle>-0 [003] ..s. 44274.198178: 0: Destination Address: e6f09f270008 Redirect to:[4] From:[5]...
虽然主机B的网卡上没有抓到包,但如何确保数据包真的就是从XDP的eBPF字节码转发走的而不是直接飞过去的呢?
很好的问题,这作为下一个练习不是更好吗?嗯,你应该试试加一个统计功能,而这个并不复杂。
资源与引用
本文只是抛砖引玉,如果觉得不过瘾,是时候穿着皮鞋就着啤酒戴着墨镜读一下下面的资源了:
https://arthurchiao.github.io/blog/cilium-bpf-xdp-reference-guide-zh/
https://docs.cilium.io/en/v1.6/bpf/
https://github.com/tklauser/filter2xdp
https://klyr.github.io/posts/ebpf/
https://linux.cn/article-9507-1.html
https://jvns.ca/blog/2017/06/28/notes-on-bpf—ebpf/
https://www.iovisor.org/technology/xdp
…
对了,如果你在使用VirtualBox搭建桥接环境遇到问题的时候,请参考这篇:
https://blog.csdn.net/dog250/article/details/102972031
经理必须穿一次白风衣,白西装,白皮鞋,并且搭配黑袜子,一定要戴墨镜,不然绝不是真正的经理。
浙江温州皮鞋湿,下雨进水不会胖。
实现一个基于XDP/eBPF的学习型网桥相关推荐
- 实现基于XDP/eBPF的快速路由转发功能
周末用eBPF实现了学习型网桥的XDP快速转发路径之后,再来用eBPF实现一个快速路由转发.同样很有意思. 关于eBPF和XDP的前置基础知识,我在前面实现网桥转发路径前已经概览过了,所以本文不再赘述 ...
- keras冻结_【连载】深度学习第22讲:搭建一个基于keras的迁移学习花朵识别系统(附数据)...
在上一讲中,和大家探讨了迁移学习的基本原理,并利用 keras 基于 VGG16 预训练模型简单了在 mnist 数据集上做了演示.鉴于大家对于迁移学习的兴趣,本节将继续基于迁移学习利用一些花朵数据搭 ...
- 如何做一个基于微信在线教育学习小程序系统毕业设计毕设作品
分析架构 我们开发系统,常规有两个架构,一个BS架构(浏览器/服务器模式),一个CS(客户端/服务器端模式):我们微信小程序项目属于CS架构,C客户端是我们要开发的小程序,S端是我们要开发的后台管理系 ...
- 一个基于 Transformer 的深度学习架构,在基因调控中组蛋白代码的定量破译方面性能超群...
编辑 | 萝卜皮 通过组蛋白修饰对转录控制的定量表征受到许多计算研究的挑战,但其中大多数只关注启动子周围的狭窄和线性基因组区域,留下了改进的空间. 韩国首尔大学的研究人员提出了 Chromoforme ...
- 基于python的系统构建_搭建一个基于python的深度学习环境
最近就要学习深度学习了,怎么能没有一个合适的开发环境呢? 那么,开始吧 系统方面我选择了Ubuntu GNOME 16.04版本,因为在Ubuntu中进行配置相对于红帽系要方便一些.毕竟谁也不想正在愉 ...
- 教你如何打造一个适合学生党的学习型iPad
iPad是目前最好用的平板电脑,功能非常强大.但是民间也流传着这样一句传言"买前生产力,买后爱奇艺".很多人买iPad之前是想着使用iPad来进行学习的,但是买后却把它当作了追剧神 ...
- PPT作品分享——创建学习型组织,打造企业核心竞争力
创建学习型组织,打造企业核心竞争力 1. 什么是学习型组织 2. 为什么要创建学习型组织 3. 如何创建学习型组织 3.1 实现自我超越:组织生命的源泉 3.2 完善心智模式:提高组织的"智 ...
- 基于python的深度学习框架有_《用Python实现深度学习框架》上市
朋友们,<用Python实现深度学习框架>已经由人民邮电出版社出版上市了.在这本书中,我们带领读者仅用Python+Numpy实现一个基于计算图的深度学习框架MatrixSlow.本书讲解 ...
- 推荐 | 一个统计硕士的深度学习算法工程师的成长之路
公众号推荐 推荐人/文文 俗话说,一个人走得快,但一群人可以走的远.在数据科学和机器学习的道路上,相信每个人都不是闭门造车的人.技术学习除了在个人努力外,交流和分享也是很重要的一部分. 今天给大家推荐 ...
- 【毕业设计之python系列】基于Flask的在线学习笔记的设计与实现
基于Flask的在线学习笔记的设计与实现 摘要 在线学习笔记系统是一种为学生和教师提供在线学习和教学的平台.本文基于Flask框架,设计并实现了一个在线学习笔记系统.该系统支持用户注册.登录.创建课程 ...
最新文章
- 如何检查字符串是否包含特定单词?
- js模板引擎——art Template
- QS首发大学百强排名,华东五校表现惊艳,老牌985望尘莫及
- rabbitmq+topic+java_译:5.RabbitMQ Java Client 之 Topics (主题)
- phpcms父级调用二级子栏目名称和二级栏目文章 - 代码篇
- python删除列表元素 // 列表的切片
- 部编版是什么版本_教材部编版和人教版的区别
- php7慢,php-finfo在7.3和7.2上明显慢
- 初步创建vue/cli工程教程
- 【设计模式】适配器模式(Adapter Pattern)
- python 建站 上传文件_python往网站上传数据
- jquery.validate动态更改校验规则
- mysql5.7 timestemp default value error
- java List的初始化
- python实现验证码图像数据去噪处理的心路历程
- 机器学习中优化算法论文合集
- 【教程】适用于AIDE 2.1.5版的API文档设置
- 如何录制电脑内部声音
- 数据分析——AB测试
- NTP时钟源(GPS时间源)介绍与分析
热门文章
- lcd1602c语言程序分析,LCD1602 C程序
- 织梦采集侠教程设置免费版本
- flash动画转html5 效果,一键把SWF转HTML5 canvas动画的工具-Fanvas
- 优化算法(一)—— 模拟退火算法(SA算法) 实战
- 中国移动亮相2012亚洲移动通信博览会
- org.aspectj aspectjweaver 报错
- 渗透测试工具Nmap从初级到高级
- linux显卡驱动编译安装,联想Y470下CentOS 6.4 AMD显卡驱动编译安装与配置
- (二)NIST CSF-框架基础
- 2008年IT日历 02