仅仅数年,eBPF 已一跃成为了现代基础设施领域中最热门的技术之一。CNCF 技术监督委员会曾将 eBPF 预测为 2021 年将起飞的技术之一,超过 2500 人报名参加了 2021 年的 eBPF 峰会虚拟会议,与此同时世界上巨头公司共同创建了eBPF 基金会[1]。显而易见,人们对 eBPF 技术抱有很大的兴趣。
在该篇的报告中,作者详细介绍了为何 eBPF 技术让人感到振奋,以及该技术为现在计算机环境带来了怎样的超能力,此外还将介绍了构建 eBPF 工具时所涉及的内容。由于是技术报告,不少内容都是简单介绍,没有涉及到太多的细节,但作者也提供了一些更加深入学习的参考信息。
eBPF 为 Extended Berkeley Packet Filter 的缩写。从字面意思表示网络报文过滤,但如今,eBPF 已被当做技术统称,所代表的已经远超字面代表的含义。
当前,eBPF 是一个框架,允许用户在操作系统的内核中加载和运行自定义程序,可用于扩展甚至修改内核行为。
当 eBPF 程序被加载到内核中时,由验证器(verifier)确保其运行是安全的,否则,验证器就会拒绝加载。验证通过后,一旦 eBPF 程序被加载至内核,并附着到事件之上,当附着的事件发生时,对应的 eBPF 程序就被触发运行。
eBPF 最初是为 Linux 开发的,报告中将重点关注在 Linux 操作系统;但微软正在开发 Windows 的 eBPF 实现[2]。
能够动态地改变内核行为的能力带来了更加强大的能力。eBPF 则允许我们在对应用透明的前提下,在内核中收集应用程序特定的运行信息。我们还可在可观察性基础上创建 eBPF 安全工具,检测和防止内核中恶意活动。基于 eBPF,我们还可以创建强大的、高性能的网络功能,在内核内处理网络数据包,从而避免将数据包转换到用户空间的昂贵成本。
从内核的角度观察应用程序并非是全新的概念,Perf 也可在应用透明的方式,在内核中收集程序行为和性能信息。Perf 等工具固定了搜集数据种类以及格式。但使用 eBPF,我们可拥有更加灵活和定制化的工具。
eBPF 可编程性带来了令人难以置信的强大,但它也很复杂。对我们大多数人来说,主要是使用 eBPF 工具。当前有越来越多的项目和供应商在 eBPF 平台上创建了新一代的可用于观测、安全性、网络等工具,可供我们使用。
eBPF 允许在 Linux 内核中运行用户编写代码,eBPF 技术的出现改变了修改内核功能的游戏规则。
Linux 内核是应用程序和其所运行的硬件之间的软件层。应用程序运行在被称为"用户空间"的非特权层,不能直接访问硬件。应用程序通过系统调用(syscall)请求内核代表其执行。
作为应用程序的开发者,我们通常不会直接使用系统调用接口,这是因为编程语言提供了更加易用的高层次抽象和标准库。如果想了解内核调用的频率,你可以使用 strace 工具来跟踪程序所有的系统调用。
应用程序很大程度上依赖于内核,这意味着如果能够观察到应用程序与内核的交互流程,我们就可了解到更多程序的运行的行为。如想要捕获到打开文件的系统调用,考虑一下如果通过修改内核,添加新的代码并打印相关输出,我们会需要做哪些工作?
Linux 内核很复杂,在撰写本文时,大约有 3000 万代码行[3]。提交新的功能或修改代码都需要对现有代码熟悉,如你不是一个内核开发者,这将会是一个巨大的挑战。
但提交内核功能并不只是纯粹的技术问题。Linux 是一个通用的操作系统,需要在不同的环境和条件下运行。社区必须保证你修改的代码是为了所有人的都可获益。这并不是提交都可通过的事情,大概只有三分之一的提交的内核补丁被接受[4]。
即使经过几个月的讨论和艰苦的开发,提交代码顺利合并到内核中,但是通过发行版到最终使用用户,有可能经过以年记的时间周期。可能会发现,你最喜欢的发行版使用的是几年前的内核版本。例如 RHEL 8.5 版本,日期是 2021 年底发布,附带的内核版本为 4.18 版本,发布于 2018 年 8 月。
正如图 2-1 中的漫画所示,新功能从想法阶段进入生产环境的 Linux 内核,简直需要数年时间。
图 2-1 向内核添加特性功能 (cartoon by Vadim Shchekoldin, Isovalent)
如果你不想等待数年的时间让更改进入内核,那么还可能的方式是通过内核模块,可以按需加载和卸载。在我们为打开文件检测系统调用的示例中,你可以编写一个内核模块来执行此操作。
这里最大的挑战仍然是完整的内核编程。用户接受内核仍然面临内核崩溃和安全运行的诸多挑战,比如程序 Bug 或者恶意代码。
内核的安全性是 Linux 发行版需要如此长时间才会整合新的内核版本的重要原因之一。如果其他人已经在各种情况下运行内核版本数月或数年,那么大多数问题已经都会被解决。发行版维护者可以确信他们提供给用户/客户的内核是经过加固,并且可以安全运行。
eBPF 提供了一种非常不同的安全方法:eBPF 验证器(verifier),其可确保仅在安全运行时才加载 eBPF 程序。
由于 eBPF 允许我们在内核中运行任意代码, eBPF 验证器(verifier)机制用于保障不会导致用户机器崩溃并且损害数据。
验证器分析 eBPF 程序以确保无论任何输入,程序都将始终在有限指令数量内安全地终止。例如,如果一个程序解引用一个指针,验证器会要求程序首先要检查指针以确保其不为空。
验证器还会确保 eBPF 程序只能访问可访问的内存。例如,想象一个在网络栈中触发的 eBPF 程序,并传递了包含真正传输数据的内核 socket _buffer_。这个 eBPF 程序可以调用特定的辅助函数,如 bpf_skb_load_bytes() 来从套接字缓冲区读取数据字节。另一个由系统调用触发的 eBPF 程序,由于没有可用的套接字缓冲区,将不允许使用 bpf_skb_load_bytes() 辅助函数。验证器还需要确保程序只读取该套接字缓冲区中的数据字节——不允许访问任意内存。目的是确保 eBPF 程序从安全角度来看是安全的。
当然,仍然可能会存在恶意的 eBPF 程序,如出于非正当理由获取数据。请注意仅从可验证来源加载受信任的 eBPF 程序,并且仅将管理 eBPF 工具的权限授予信任的具有 root 权限用户。
eBPF 程序可以动态地加载和卸载。一旦 eBPF 程序附着到事件,无论是何种原因触发事件,都会运行附着的 eBPF 程序。例如,如果你将程序附加到系统调用以打开文件,则只要任何进程尝试打开文件,就会触发该程序。加载程序时该进程是否已经在运行并不重要。
这使得基于 eBPF 提供可观察性或安全工具的具有一大优势——eBPF 加载后就可以立即查看机器上发生的一切。
此外,如图 2-2 所示,人们还可以通过 eBPF 非常快速地创建新的内核功能,而无需每个其他 Linux 用户都接受相同的更改。
图 2-2 使用 eBPF 为内核添加函数 (cartoon by Vadim Shchekoldin, Isovalent)
现在你已经看到了 eBPF 是如何允许对内核进行动态的、自定义的改变的,让我们来研究一下如果你想写一个 eBPF 程序会涉及到哪些内容。
eBPF 基金会: https://ebpf.io/foundation/
[2]Windows 的 eBPF 实现: https://github.com/microsoft/ebpf-for-windows
[3]代码行: https://www.phoronix.com/scan.php?page=news_item&px=Linux-5.12-rc1-Code-Size
[4]接受: https://dl.acm.org/doi/pdf/10.5555/2487085.2487111