eBPF源于BPF[1],本质上是处于内核中的一个高效与灵活的虚类虚拟机组件,以一种安全的方式在许多内核hook点执行字节码。BPF最初的目的是用于高效网络报文过滤,经过重新设计,eBPF不再局限于网络协议栈,已经成为内核顶级的子系统,演进为一个通用执行引擎。
开发者可基于eBPF开发性能分析工具、软件定义网络、安全等诸多场景。本文将介绍eBPF的前世今生,并构建一个eBPF环境进行开发实践,文中所有的代码可以在我的Github[2]中找到。
BPF,是类Unix系统上数据链路层的一种原始接口,提供原始链路层封包的收发。1992年,StevenMcCanne和VanJacobson写了一篇名为TheBSDPacketFilter:ANewArchitectureforUser-levelPacketCapture[3]的论文。在文中,作者描述了他们如何在Unix内核实现网络数据包过滤,这种新的技术比当时最先进的数据包过滤技术快20倍。
BPF在数据包过滤上引入了两大革新:
由于这些巨大的改进,所有的Unix系统都选择采用BPF作为网络数据包过滤技术,直到今天,许多Unix内核的派生系统中(包括Linux内核)仍使用该实现。tcpdump的底层采用BPF作为底层包过滤技术,我们可以在命令后面增加-d来查看tcpdump过滤条件的底层汇编指令。
eBPF新的设计针对现代硬件进行了优化,所以eBPF生成的指令集比旧的BPF解释器生成的机器码执行得更快。扩展版本也增加了虚拟机中的寄存器数量,将原有的2个32位寄存器增加到10个64位寄存器。由于寄存器数量和宽度的增加,开发人员可以使用函数参数自由交换更多的信息,编写更复杂的程序。总之,这些改进使eBPF版本的速度比原来的BPF提高了4倍。
2014年6月,eBPF扩展到用户空间,这也成为了BPF技术的转折点。正如Alexei在提交补丁的注释中写到:「这个补丁展示了eBPF的潜力」。当前,eBPF不再局限于网络栈,已经成为内核顶级的子系统。
对比Web的发展,eBPF与内核的关系有点类似于JavaScript与浏览器内核的关系,eBPF相比于直接修改内核和编写内核模块提供了一种新的内核可编程的选项。eBPF程序架构强调安全性和稳定性,看上去更像内核模块,但与内核模块不同,eBPF程序不需要重新编译内核,并且可以确保eBPF程序运行完成,而不会造成系统的崩溃。
eBPF分为用户空间程序和内核程序两部分:
eBPF整体结构图如下:
用户空间程序与内核中的BPF字节码交互的流程主要如下:
eBPF技术虽然强大,但是为了保证内核的处理安全和及时响应,内核中的eBPF技术也给予了诸多限制,当然随着技术的发展和演进,限制也在逐步放宽或者提供了对应的解决方案。
在深入介绍eBPF特性之前,让我们GetHandsDirty,切切实实的感受eBPF程序到底是什么,我们该如何开发eBPF程序。随着eBPF生态的演进,现在已经有越来越多的工具链用于开发eBPF程序,在后文也会详细介绍:
系统环境如下,采用腾讯云CVM,Ubuntu20.04,内核版本5.4.0
前面说到eBPF通常由内核空间程序和用户空间程序两部分组成,现在samples/bpf目录下有很多这种程序,内核空间程序以_kern.c结尾,用户空间程序以_user.c结尾。先不看这些复杂的程序,我们手动写一个eBPF程序的HelloWorld。
内核中的程序hello_kern.c:
它提供了bpf编程需要的很多symbol。例如
等等
来自libbpf,需要自行安装。我们引用这个头文件是因为调用了bpf_printk()。这是一个kernelhelperfunction。
这里我们简单解读下内核态的ebpf程序,非常简单:
用户态程序hello_user.c
修改samples/bpf目录下的Makefile文件,在对应的位置添加以下三行:
如果针对某个特定需求的Hook点不存在,可以通过kprobe或者uprobe来在内核或者用户程序的几乎所有地方挂载eBPF程序。
Withgreatpowertheremustalsocomegreatresponsibility.
每一个eBPF程序加载到内核都要经过Verification,用来保证eBPF程序的安全性,主要包括:
64位的x86_64、arm64、ppc64、s390x、mips64、sparc64和32位的arm、x86_32架构都内置了in-kerneleBPFJIT编译器,它们的功能都是一样的,可以用如下方式打开:
要判断哪些平台支持eBPFJIT,可以在内核源文件中grepHAVE_EBPF_JIT:
BPFMap的交互场景有以下几种:
共享map的BPF程序不要求是相同的程序类型,例如tracing程序可以和网络程序共享map,单个BPF程序目前最多可直接访问64个不同map。
当前可用的通用map有:
当前内核中的非通用map有:
eBPF程序不能够随意调用内核函数,如果这么做的话会导致eBPF程序与特定的内核版本绑定,相反它内核定义的一系列Helperfunctions。Helperfunctions使得BPF能够通过一组内核定义的稳定的函数调用来从内核中查询数据,或者将数据推送到内核。所有的BPF辅助函数都是核心内核的一部分,无法通过内核模块来扩展或添加。当前可用的BPF辅助函数已经有几十个,并且数量还在不断增加,你可以在LinuxManualPage:bpf-helpers[11]看到当前Linux支持的Helperfunctions。
不同类型的BPF程序能够使用的辅助函数可能是不同的,例如:
所有的辅助函数都共享同一个通用的、和系统调用类似的函数方法,其定义如下:
虽然cBPF允许其加载指令(loadinstructions)进行超出范围的访问(overload),以便从一个看似不可能的包偏移量(packetoffset)获取数据以唤醒多功能辅助函数,但每个cBPFJIT仍然需要为这个cBPF扩展实现对应的支持。而在eBPF中,JIT编译器会以一种透明和高效的方式编译新加入的辅助函数,这意味着JIT编译器只需要发射(emit)一条调用指令(callinstruction),因为寄存器映射的方式使得BPF排列参数的方式(assignments)已经和底层架构的调用约定相匹配了。这使得基于辅助函数扩展核心内核(corekernel)非常方便。所有的BPF辅助函数都是核心内核的一部分,无法通过内核模块(kernelmodule)来扩展或添加。
前面提到的函数签名还允许校验器执行类型检测(typecheck)。上面的structbpf_func_proto用于存放校验器必需知道的所有关于该辅助函数的信息,这样校验器可以确保辅助函数期望的类型和BPF程序寄存器中的当前内容是匹配的。
参数类型范围很广,从任意类型的值,到限制只能为特定类型,例如BPF栈缓冲区(stackbuffer)的pointer/size参数对,辅助函数可以从这个位置读取数据或向其写入数据。对于这种情况,校验器还可以执行额外的检查,例如,缓冲区是否已经初始化过了。
尾调用的机制是指:一个BPF程序可以调用另一个BPF程序,并且调用完成后不用返回到原来的程序。
BPF辅助函数的调用约定也适用于BPF函数间调用:
当前,BPF函数间调用和BPF尾调用是不兼容的,因为后者需要复用当前的栈设置(stacksetup),而前者会增加一个额外的栈帧,因此不符合尾调用期望的布局。
BPFJIT编译器为每个函数体发射独立的镜像(emitseparateimagesforeachfunctionbody),稍后在最后一通JIT处理(finalJITpass)中再修改镜像中函数调用的地址。已经证明,这种方式需要对各种JIT做最少的修改,因为在实现中它们可以将BPF函数间调用当做常规的BPF辅助函数调用。
BPFmap和程序作为内核资源只能通过文件描述符访问,其背后是内核中的匿名inode。这带来了很多优点:
但同时,文件描述符受限于进程的生命周期,使得map共享之类的操作非常笨重,这给某些特定的场景带来了很多复杂性。
例如iproute2,其中的tc或XDP在准备环境、加载程序到内核之后最终会退出。在这种情况下,从用户空间也无法访问这些map了,而本来这些map其实是很有用的。例如,在datapath的ingress和egress位置共享的map(可以统计包数、字节数、PPS等信息)。另外,第三方应用可能希望在BPF程序运行时监控或更新map。
相应的,BPF系统调用扩展了两个新命令,如下图所示:
为了避免代码被损坏,BPF会在程序的生命周期内,在内核中将BPF解释器解释后的整个镜像(structbpf_prog)和JIT编译之后的镜像(structbpf_binary_header)锁定为只读的。在这些位置发生的任何数据损坏(例如由于某些内核bug导致的)会触发通用的保护机制,因此会造成内核崩溃而不是允许损坏静默地发生。
查看哪些平台支持将镜像内存(imagememory)设置为只读的,可以通过下面的搜索:
为了防御Spectrev2攻击,Linux内核提供了CONFIG_BPF_JIT_ALWAYS_ON选项,打开这个开关后BPF解释器将会从内核中完全移除,永远启用JIT编译器:
将/proc/sys/net/core/bpf_jit_harden设置为1会为非特权用户的JIT编译做一些额外的加固工作。这些额外加固会稍微降低程序的性能,但在有非受信用户在系统上进行操作的情况下,能够有效地减小潜在的受攻击面。但与完全切换到解释器相比,这些性能损失还是比较小的。对于x86_64JIT编译器,如果设置了CONFIG_RETPOLINE,尾调用的间接跳转(indirectjump)就会用retpoline实现。写作本文时,在大部分现代Linux发行版上这个配置都是打开的。
当前,启用加固会在JIT编译时盲化(blind)BPF程序中用户提供的所有32位和64位常量,以防御JITspraying攻击,这些攻击会将原生操作码作为立即数注入到内核。这种攻击有效是因为:立即数驻留在可执行内核内存(executablekernelmemory)中,因此某些内核bug可能会触发一个跳转动作,如果跳转到立即数的开始位置,就会把它们当做原生指令开始执行。
盲化JIT常量通过对真实指令进行随机化(randomizingtheactualinstruction)实现。在这种方式中,通过对指令进行重写,将原来基于立即数的操作转换成基于寄存器的操作。指令重写将加载值的过程分解为两部分:
这样原始的imm立即数就驻留在寄存器中,可以用于真实的操作了。这里介绍的只是加载操作的盲化过程,实际上所有的通用操作都被盲化了。下面是加固关闭的情况下,某个程序的JIT编译结果:
BPF网络程序,尤其是tc和XDPBPF程序在内核中都有一个offload到硬件的接口,这样就可以直接在网卡上执行BPF程序。
当前,Netronome公司的nfp驱动支持通过JIT编译器offloadBPF,它会将BPF指令翻译成网卡实现的指令集。另外,它还支持将BPFmapsoffload到网卡,因此offloadedBPF程序可以执行map查找、更新和删除操作。
eBPF提供了`bpf()`[14]系统调用来对BPFMap或程序进行操作,其函数原型如下:
cmd可以为一下几种类型,基本上可以分为操作eBPFMap和操作eBPF程序两种类型:
bpf_attrunion的结构如下所示,根据不同的cmd可以填充不同的信息。
对于用户态程序,则其函数原型如下,其中通过fd来访问eBPFmap。
实际上,程序类型本质上定义了一个API。甚至还创建了新的程序类型,以区分允许调用的不同的函数列表(比如BPF_PROG_TYPE_CGROUP_SKB对比BPF_PROG_TYPE_SOCKET_FILTER)。
bpf程序会被hook到内核不同的hook点上。不同的hook点的入口参数,能力有所不同。因而定义了不同的progtype。不同的progtype的bpf程序能够调用的kernelfunction集合也不一样。当bpf程序加载到内核时,内核的verifier程序会根据bpfprogtype,检查程序的入口参数,调用了哪些helperfunction。
目前内核支持的eBPF程序类型列表如下所示:
随着新程序类型的添加,内核开发人员同时发现也需要添加新的数据结构。
举个例子BPF_PROG_TYPE_SCHED_CLSbpfprog,能够访问哪些bpfhelperfunction呢?让我们来看看源代码是如何实现的。
每一种progtype会定义一个structbpf_verifier_ops结构体。当progload到内核时,内核会根据它的type,调用相应结构体的get_func_proto函数。
每一种progtype的调用时机都不同。
BPF_PROG_TYPE_SCHED_CLS的调用过程如下。
egress方向上,tcp/ip协议栈运行之后,有一个hook点。这个hook点可以attachBPF_PROG_TYPE_SCHED_CLStype的egress方向的bpfprog。在这段bpf代码执行之后,才会运行qos,tcpdump,xmit到网卡driver的代码。在这段bpf代码中你可以修改报文里面的内容,地址等。修改之后,通过tcpdump可以看到,因为tcpdump代码在此之后才执行。
BPF_PROG_RUN传给bpfprog的入口参数是skb,其类型是structsk_buff,定义在文件include/linux/skbuff.h中。
但是在bpf代码中,为了安全,不能直接访问sk_buff。bpf中是通过访问struct__sk_buff来访问structsk_buff的。__sk_buff是sk_buff的一个子集,是sk_buff面向bpf程序的接口。bpf代码中对__sk_buff的访问会在verifier程序中翻译成对sk_buff相应fileds的访问。
在加载bpfprog的时候,verifier会调用上面tc_cls_act_verifier_ops结构体里面的tc_cls_act_convert_ctx_access的钩子。它最终会调用下面的函数修改ebpf的指令,使得对__sk_buff的访问变成对structsk_buff的访问。
一种type的bpfprog可以挂到内核中不同的hook点,这些不同的hook点就是不同的attachtype。
其对应关系在下面函数[22]中定义了。
有趣的是,BPF_PROG_TYPE_SCHED_CLS类型的bpfprog不能通过bpf系统调用来attach,因为它没有定义对应的attachtype。故它的attach需要通过netlinkinterface额外的实现,还是非常复杂的。
内核中的progtype目前有30种。每一种type能做的事情有所差异,这里只讲讲我平时工作用过的几种。
理解一种progtype的最好的方法是
在tcp协议event发生时调用的bpf钩子,定义了15种event。这些event的attachtype都是BPF_CGROUP_SOCK_OPS。不同的调用点会传入不同的enum,比如:
主要作用:tcp调优,event统计等。
它对应很多attachtype,一般在bind,connect时调用,传入sock的地址。
主要作用:例如cilium中clusterip的实现,在主动connect时,修改了目的ip地址,就是利用这个。
BPF_PROG_TYPE_CGROUP_SOCK_ADDR,这种类型的程序使您可以在由特定cgroup控制的用户空间程序中操纵IP地址和端口号。在某些情况下,当您要确保一组特定的用户空间程序使用相同的IP地址和端口时,系统将使用多个IP地址.当您将这些用户空间程序放在同一cgroup中时,这些BPF程序使您可以灵活地操作这些绑定。这样可以确保这些应用程序的所有传入和传出连接均使用BPF程序提供的IP和端口。
BPF_PROG_TYPE_SK_MSG,Thesetypesofprogramsletyoucontrolwhetheramessagesenttoasocketshouldbedelivered.当内核创建了一个socket,它会被存储在前面提到的map中。当你attach一个程序到这个socketmap的时候,所有的被发送到那些socket的message都会被filter。在filtermessage之前,内核拷贝了这些data,因此你可以读取这些message,而且可以给出你的决定:例如,SK_PASS和SK_DROP。
调用点:tcpsendmsg时会调用。
主要作用:做sockredir用的。
调用点:getsockopt,setsockopt
类似ftrace的kprobe,在函数出入口的hook点,debug用的。
类似ftrace的tracepoint。
如上面的例子
网卡驱动收到packet时,尚未生成sk_buff数据结构之前的一个hook点。
XDP定义了很多的处理方式,例如
BPF_PROG_TYPE_CGROUP_SKB允许你过滤整个cgroup的网络流量。在这种程序类型中,你可以在网络流量到达这个cgoup中的程序前做一些控制。内核试图传递给同一cgroup中任何进程的任何数据包都将通过这些过滤器之一。同时,您可以决定cgroup中的进程通过该接口发送网络数据包时该怎么做。其实,你可以发现它和BPF_PROG_TYPE_SOCKET_FILTER的类型很类似。最大的不同是cgroup_skb是attach到这个cgroup中的所有进程,而不是特殊的进程。在container的环境中,bpf是非常有用的。
在sockcreate,release,post_bind时调用的。主要用来做一些权限检查的。
BPF_PROG_TYPE_CGROUP_SOCK,这种类型的bpf程序允许你,在一个cgroup中的任何进程打开一个socket的时候,去执行你的Bpf程序。这个行为和CGROUP_SKB的行为类似,但是它是提供给你cgoup中的进程打开一个新的socket的时候的情况,而不是给你网络数据包通过的权限控制。这对于为可以打开套接字的程序组提供安全性和访问控制很有用,而不必分别限制每个进程的功能。
BCC是BPF的编译工具集合,前端提供Python/LuaAPI,本身通过C/C++语言实现,集成LLVM/Clang对BPF程序进行重写、编译和加载等功能,提供一些更人性化的函数给用户使用。
虽然BCC竭尽全力地简化BPF程序开发人员的工作,但其“黑魔法”(使用Clang前端修改了用户编写的BPF程序)使得出现问题时,很难找到问题的所在以及解决方法。必须记住命名约定和自动生成的跟踪点结构。且由于libbcc库内部集成了庞大的LLVM/Clang库,使其在使用过程中会遇到一些问题:
性能优化大师BrendanGregg在用libbpf+BPFCO-RE转换一个BCC工具后给出了性能对比数据:
AsmycolleagueJasonpointedout,thememoryfootprintofopensnoopasCO-REismuchlowerthanopensnoop.py.9MbytesforCO-REvs80MbytesforPython.
我们可以看到在运行时相比BCC版本,libbpf+BPFCO-RE版本节约了近9倍的内存开销,这对于物理内存资源已经紧张的服务器来说会更友好。
bpftraceisahigh-leveltracinglanguageforLinuxeBPFandavailableinrecentLinuxkernels(4.x).bpftraceusesLLVMasabackendtocompilescriptstoeBPFbytecodeandmakesuseofBCCforinteractingwiththeLinuxeBPFsubsystemaswellasexistingLinuxtracingcapabilities:kerneldynamictracing(kprobes),user-leveldynamictracing(uprobes),andtracepoints.Thebpftracelanguageisinspiredbyawk,CandpredecessortracerssuchasDTraceandSystemTap.
长治等保,等保,网络安全,网络等保,等级保护,网络安全等保,网络安全等级保护,长治网络安全等级保护,等保公司,等保测评,等级保护2.0,定级,定级备案,等保备案,长治等保备案,山西等保备案,系统测评,系统备案,网安备案,等保备案服务,等保咨询,公安局备案,二级等保,三级等保,三级测评,系统整改,做等保的公司,网站建设,企业网站建设,企业网站开发,企业网站运维,Linux系统运维,Windows系统运维,服务器运维,环境部署,环境搭建,私有云存储