实现一个基于XDP_eBPF的学习型网桥

eBPF技术风靡当下,eBPF字节码正以星火燎原之势被HOOK在Linux内核中越来越多的位置,在这些HOOK点上,我们可以像编写普通应用程序一样编写内核的HOOK程序,与以往为了实现一个功能动辄patch一整套逻辑框架代码(比如Netfilter)相比,eBPF的工作方式非常灵活。

我们先来看一下目前eBPF的一些重要HOOK点: 将来这个is_XXX序列肯定会不断增加,布满整个内核(有点密集恐惧症症状了…)。

本文将描述如何用eBPF实现一个学习型网桥的快速转发,并将其部署在XDP。

在开始之前,为了让所有人都能看懂本文,我们先来回顾一些前置知识,如果暂时还不懂这些前置知识,没关系,先把程序run起来是一个很好的起点,如果到时候你觉得没意思,再放弃也不迟。

前置知识

  • 什么是BPF和eBPF

简单来讲,BPF是一套完整的 计算机体系结构 。和x86,ARM这些类似,BPF包含自己的指令集和运行时逻辑,同理,就像在x86平台编程,最终要落实到x86汇编指令一样,BPF字节码也可以看成是汇编指令的序列。我们通过tcpdump的-d/-dd参数可见一斑:

BPF的历史非常古老,早在1992年就被构建出来了,其背后的思想是, “与其把数据包复制到用户空间执行用户态程序过滤,不如把过滤程序灌进内核去。”

遗憾的是,BPF后来并没有大行其道,只是被应用于非常有限的并不起眼的比如抓包层面。因此,由于它的语法并不复杂,人们直接手写BPF汇编指令码经简单封装即可生成最终的字节码。

当人们认识到BPF非常强壮的功能并准备将其大用时,指令系统以及操作系统内核均已经持续进化了好多年,这意味着简单的BPF不能再满足需要,它需要 “被复杂化”

于是就出现了eBPF,即extended BPF。总体而言,eBPF相比BPF有了以下改进:1. 更复杂的指令系统。2. 更多可调用的函数。3. … 详情可参见下面的链接:https://lwn.net/Articles/740157/

就像汇编语言进化到C语言一样,直接手写eBPF字节码显得即笨拙又低效,于是人们开始使用C语言直接编写eBPF程序,然后用编译器将其编译成eBPF字节码。遗憾的是,目前eBPF体系结构还不被gcc支持,不过很快就会支持了。我们不得不使用 特定的编译器 来编译eBPF的C代码,比如clang。

  • 什么是XDP

XDP,即eXpress Data Path,它其实是位于网卡驱动程序里的一个快速处理数据包的HOOK点,为什么快?基于以下两点:

  1. 数据包处理位置非常底层,避开了很多内核skb处理开销。

  2. 可以将很多处理逻辑Offload到网卡硬件。

显而易见,在XDP这个HOOK点灌进来一点eBPF字节码,将是一件令人愉快的事情。

  • 学习型网桥

Linux的Bridge模块就是一个学习型网桥,其实就是一个现代交换式以太网交换机,它可以从端口学习到MAC地址,在内部生成MAC/端口映射表,以优化转发效率。

本文我们将用eBPF实现的网桥就是一个学习型网桥,并且它的数据路径和控制路径相分离,用eBPF字节码实现的正是其数据路径,它将被灌入XDP,而控制路径则由一个用户态程序实现。

  • 如何编译eBPF程序

理论的学习自在平时,当打开电脑的时候,最快的速度run起来一些东西令人愉悦。我们不想花大量的时间在环境的搭建上。对于eBPF程序,内核源码树的samples/bpf目录将是一个非常好的起点。

以我自己的环境为例,我使用的是Ubuntu 19.10发行版,5.3.0-19-generic内核,安装源码后,编译之,最后编译samples/bpf即可:

samples/bpf目录下的代码都是比较典型的范例,我们照猫画虎就能实现我们想要的功能。

大体上,每一个范例均由两个部分组成:

  1. XXX_kern.c文件:eBPF字节码本身。

  2. XXX_user.c文件:用户态控制程序,控制eBPF字节码的注入,更新。

即然我们要实现一个网桥,那么文件名我们可以确定为:

  1. xdp bridge kern.c

  2. xdp bridge user.c

同时我们修改Makefile文件,加入这两个文件即可:

网桥XDP快速转发的实现

对上述前置知识有了充分的理解之后,代码就非常简单了,我们剩下的工作就是填充xdp bridge kern.c和xdp bridge user.c两个C文件,然后make它们。

我们先来看xdp bridge kern.c文件:

这里有必要说一下内核对eBPF程序的合法性检查,这个检查一点都不多余,它确保你的eBPF代码是安全的。这样才不会造成内核数据结构被破坏掉,否则,如果任意eBPF程序都能注入内核,那结局显然是细思极恐的。

现在继续我们的用户态C代码:

用户态程序同样很容易理解。

数据面和控制面分离,这是网络设备的标准路数,几十年前就这样了,如今我们也能简单实现一个了,很有趣不是吗?

run起来

执行make之后,我们可以得到可执行文件xdp bridge以及eBPF字节码文件xdp bridge_kern.o,在当前目录下直接执行即可:

在另一个终端查看eBPF字节码里的map,即MAC/端口映射表:

OK,一切顺利。现在让我们正式用它搭建一个网桥吧。

暂时X掉xdp_bridge程序的运行,让我们一步一步来。

首先构建下面的拓扑:

中间的Linux Bridge主机(后面简称主机B)的enp0s9,enp0s10网卡将是我们注入eBPF字节码的位置。

现在让我们在主机B上创建一个标准的Linux网桥:

在主机H1和主机H2的enp0s9上配置同网段的地址:

互相ping确认是通的,并且主机B的enp0s9/enp0s10可以抓到双向包,这说明主机B的Linux标准网桥工作是OK的。

接下来,停掉这一切,把br0也删除掉。重新运行xdp bridge程序,确认OK后创建Linux标准网桥,从H1来ping H2,很畅通,同时我们会发现主机B的xdp bridge程序的输出:

很显然,eBPF的map学习到了新的MAC地址,我们可以用bpftool确认:

此时,主机B的enp0s9和enp0s10就抓不到任何H1和H2之间单播包了。广播包仍然会被上传到慢速路径被标准Linux网桥处理。

我们看trace日志:

虽然主机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

浙江温州皮鞋湿,下雨进水不会胖。

更多精彩,尽在”Linux阅码场”,扫描下方二维码关注

你的随手 转发 或点个 在看 是对我们最大的支持!