Skip to content

Latest commit

 

History

History
289 lines (190 loc) · 19.7 KB

eBPF详解.md

File metadata and controls

289 lines (190 loc) · 19.7 KB

前言

Linux系统分为内核空间和用户空间,内核空间是操作系统的核心,对所有硬件具备不受限制的完整访问能力,而用户空间运行的这是非内核进程,即用户的程序,这些进程仅能通过内核开放的系统调用,实现对硬件的有限访问,即用户空间的程序一定会经过内核空间的过滤。

在之前大多程序,例如网络监控器,都是作为用户级进程运行的。为了分析只在内核空间运行的数据,它们必须将这些数据从内核空间复制到用户空间的内存中去,并进行上下文切换,这与直接在内核空间分析数据相比,性能开销巨大。

随着近些年网路速度和流量的疯涨,一些应用程序必须处理大量的数据(如音频、流媒体数据)。要在用户空间监控分析那么多流量数据已经不可行了。

BPF

BPF全称是Berkeley Packet Filter,是类Unix上数据链路层的一种原始接口,提供原始链路层封包的收发。

  • 一个新的VM设计,可以有效的工作在基于寄存器结构的CPU之上
  • 应用程序使用缓存只复制与过滤数据包相关的数据,不会复制数据包的所有信息,最大程度地减少了BPF处理的数据,提高处理效率

tcpdump的底层采用了BPF作为底层包过滤技术

BPF架构

发展到现在升级为eBPF(extented Berkeley Packet Filter),演进成一套通用执行引擎,提供可基于系统或程序事件高效安全执行特定代码的通用能力。

内核技术的挑战

从操作系统内核的角度来看,面临很多的挑战。

  1. 复杂性不断增加,性能和可拓展性新需求: 内核(general kernel),必须在子系统复杂度不断增长的前提下,满足这些性能和可拓展性的需求。
  2. 永远保持向后兼容: Linus torvalds:“never break user spcae”,这对用户来说是好事,但是对于内核开发者来说,必须保证引入新的代码时,仍然需要兼容数年前的老代码,这会使得原本复杂的内核更加复杂,对于网络来说,这意味着快速收发包路径(fast path)将会收到影响。
  3. Feature creeping normality: 内核不允许一次性引入非常大的改动,只能拆分为数量众多的小patch,每次合并的patch保证系统的向后兼容,并且对系统的影响非常小。

首先,eBPF带来的好处从长期来说会减少未来的Feature creeping normality,因为用户希望内核实现的功能不再需要通过内核去提供,只需要一段eBPF代码,动态加载到内核即可。

其次,因为eBPF,内核也不再去引入一些影响fast path的代码,避免性能的下降。

同时,eBPF使得内核完全可编程,安全的可编程。

eBPF基础

「What」eBPF是一个用于访问Linux内核服务和硬件的方法。

Linux内核一直是实现监视/可观察性,网络和安全性的理想场所。不幸的是,这通常是不切实际的,因为它需要更改内核源代码或加载内核模块,并导致彼此堆叠的抽象层。eBPF是一项革命性的技术,可以在Linux内核中运行沙盒程序,而无需更改内核源代码或加载内核模块。通过使Linux内核可编程,基础架构软件可以利用现有的层,使它们更加智能和功能丰富,而无需继续为系统增加额外的复杂性层。

  1. 将eBPF指令映射到平台原生指令时开销尽可能小(对x86和arm64平台进行了许多优化)
  2. 内核加载eBPF代码时要验证代码的安全性(限制为一个最小指令集的原因,这样来确保时可验证的,进而确定是安全的)

但内核模块中引入 bug 是一件极度危险的事情 —— 它会导致内核 crash。 此时 BPF 的优势就体现出来了:**校验器(verifier)**会检查是否有越界内存访问 、无限循环等问题,一旦发现就会拒绝加载,而非将这些问题留到运行时(导致 内核 crash 等破坏系统稳定性的行为)。

eBPF架构

版本

version 功能
4.1 kprobe support
4.4 Perf events
4.7 Tracepoints support
4.8 XDP core
4.10 cgroups support

具体支持情况:https://github.com/iovisor/bcc/blob/master/docs/kernel-versions.md

工作流程

eBPF提供的基本模块(building blocks)和程序附着点(attachment points),可以通过编写eBPF来attach到这些hook来完成某些高级功能,这是eBPF的主要工作方式。

  1. 将eBPF程序编译成字节码
  2. 在载入到Hook之前,需要进入到虚拟机中进行程序校验eBPF verifier(通过沙盒机制来检查每个程序是否能够安全运行在内核中)
  • 一定是一个有向无环图
  • 没有不可达代码
  • 大小限制
  • 不能不检查间接引用
  • eBPF 堆栈大小被限制在 MAX_BPF_STACK,截止到内核 Linux 5.8 版本,被设置为 512。目前没有计划增加这个限制,解决方法是改用 BPF Map,它的大小是无限的。
  • eBPF 字节码大小最初被限制为 4096 条指令,截止到内核 Linux 5.8 版本, 当前已将放宽至 100 万指令( BPF_COMPLEXITY_LIMIT_INSNS),对于无权限的BPF程序,仍然保留4096条限制 ( BPF_MAXINSNS )
  1. JIT编译(保证程序本地运行的高性能)
  2. 通过JIT编译后,将编译后的程序附加到内核中各种系统调用的hook上
  3. 在程序被触发时,调用辅助函数处理数据
  4. 在用户空间和内核空间之间使用键值对共享数据

eBPF vs BPF

eBPF相比于BPF优势:

  1. eBPF能被更多的事件触发,而BPF只能由包到达触发
  2. 能在内核中运行任意eBPF代码,而BPF只能实现包过滤
  3. eBPF能够trace很多数据,比如监控系统调用来进行安全监管
  4. eBPF可用于很多方面:网络性能、防火墙、安全、tracing和设备驱动

eBPF核心概念

通常情况下,eBPF程序由两部分组成:

  1. 内核空间部分:内核事件触发执行,如网卡收到一个包、系统调用创建了一个shell进程。
  2. 用户空间部分:通过某种共享数据的方式(BPF maps),来读取内核中产生的数据。

img

BPF Hooks

即哪些地方可以加载BPF程序,在目前的Linux内核中已有的hook类型如下:

  • kernel functions (kprobes)
  • userspace functions (uprobes)
  • system calls
  • fentry/fexit
  • Tracepoints
  • network devices (tc/xdp)
  • network routes
  • TCP congestion algorithms
  • sockets (data level)

从文件打开、创建TCP连接、Socket连接到发送系统消息几乎所有的系统调用,加上用户空间的各种动态信息,都能加载BPF程序。在内核中形成不同的BPF程序类型,在加载时会有类型判断。

如下图内核代码片段是用来判断BPF程序类型:

static int load_and_attach(const char *event, struct bpf_insn *prog, int size) {
    bool is_socket = strncmp(event, "socket",6)=0;
    bool is_kprobe = strncmp(event, "kprobe/", 7-o;
    bool is_kretprobe = strncmp(event, "kretprobe/"10)==0;
    bool is tracepoint = strncmp(event, "tracepoint/", 11)==0
    bool is raw tracepoint = strncmp(event, "rawtracepoint/",15)=0; bool is_xdp = strncmp(event,"xdp"3)==0;
    bool is perf event - strncmp(event, "perf event"10) ==0;
    bool is_cgroup skb = strncmp(event, "cgroup/skb"10)==0;
    bool is_cgroup_sk = strncmp(event, "cgroup/sock"11)==0;
    bool is_sockops = strncmp(event, "sockops"7)==0;
    bool is_sk_skb =strncmp(event,"sk_skb"6)==0;
    boolis sk msg = strncmp(event, "skmsg"6)==0;
    size_t insns_cnt = size/ sizeof(struct bpf_insn);
    enum bpf_prog_type prog_type;
    char buf[256];
    intfa,efd, err, id;
    struct perf_event_attr attr ={};
    attr.type = PERF_TYPE_TRACEPOINT:
    attr.sample_type = PERF_SAMPLE_RAW;
    attr.sample_period =1;
    attr.wakeup _events =1;
    // 判断语句
    if(is_socket) {
    }...
}

BPF Map

BPF程序本身只有指令,不会包含实际数据以及状态,可以在BPF程序上创建BPF Map来存储数据状态、统计信息和指标信息等。

BPF Helper Funcation

操作BPF程序和BPF Map的工具类函数;直接调用内核模块接口会与内核版本强耦合,因此提供BPF辅助函数作为稳定API进行跨内核版本移植。

  • 内核helper:定义在bpf/bpf_helpers.h,直接通过变量指针访问
  • 用户态helper:定义在tools/lib/bpf/bpf.h,需要通过fd向内核发送消息来访问

BPF Tracing

Tracing视为profiling和debuggin收集数据的方法,目标是在运行时提供有用的信息,BPF相比其他tracing技术只在系统中增加少量的性能和时延开销。

传统Linux使用probes的方法是写程序并编译到内核模块,BPF piggybakc在tracing probes来收集信息。

常用Probes

  • Kernel probes:没有稳定的ABI,会随着内核版本变更也改变;kprobe —— 允许在任意内核指令之前插入BPF程序,kretprobe —— 允许在内核指令执行完成返回时插入BPF程序
  • Tracepoints:内核代码中的静态marker,能用于在运行的内核中附加代码,具有稳定的API
  • User-Space Probes:允许在用户空间运行的程序中设置动态flag;定义一个uprobe后,内核会在附加的指令附加创建一个trap(INT3中断进入内核态), 应用到达指令后内核会触发一个事件,该事件将probe函数作为callback
  • User Statically Defifined Tracepoints(USDTs):在用户态为应用提供静态tracepoints

XDP

XDP(eXpress Data Path)提供了一个内核态、高性能、可编程 BPF 包处理框架(a framework for BPF that enables high-performance programmable packet processing in the Linux kernel)。这个框架在软件中最早可以处理包的位置(即网卡驱动收到包的 时刻)运行 BPF 程序。

XDP Hook位于网络驱动的fast path(用来描述一个程序中与正常路径相比指令路径长度较短的路径)上,XDP程序直接从接受缓冲区(receive ring)中将包获取,无需执行任何耗时操作,如分配skb然后将包推送到网络协议栈等,因此只要又CPU资源,XDP BPF程序就能够在最早的位置执行处理。

基本概念

概述:

  • 为接收的包做决定,可以编辑接收的包的内容或返回一个结果码(包括:丢弃、从原接口发出、放行到后续协议栈等)
  • XDP可以push或pull包头(比如:如果内核不支持封装格式或协议,XDP能够进行解封装或翻译,并把结果发送给内核处理)
  • XDP通过bpf syscall来控制,类型为BPF_PROG_TYPE_XDP

「What」定义:

  • 在网络data path上一个安全、可编程、高性能、内核综合的包处理器
  • 能够在当NIC驱动接收到一个数据包时就执行BPF程序
  • 运行XDP程序在很早的点就对接收的包进行丢弃、修改或放行处理

特点:

  • XDP和Linux内核以及基础设施协同工作,不向一些运行在用户空间的网络框架(DPDK)是绕过内核的,可以带来几方面的好处:
    • XDP可以复用所有上游开发的内核网络驱动、用户空间工具,以及其他一些可用的内核 基础设施,例如 BPF 辅助函数在调用自身时可以使用系统路由表、socket 等等。
    • 因为驻留在内核空间,因此 XDP 在访问硬件时与内核其他部分有相同的安全模型。
    • 将包从 XDP 送到内核中非常简单,可以复用内核中这个健壮、高效、使用广泛的 TCP/IP 协议栈,而不是像一些用户态框架一样需要自己维护一个独立的 TCP/IP 协 议栈。
    • 无需跨内核/用户空间边界,因为正在被处理的包已经在内核中,因此可以灵活地将 其转发到内核内的其他实体,例如容器的命名空间或内核网络栈自身。
  • 用XDP做包处理时没有内存分配
  • XDP程序只能操作线性的非分片包,具有包的开始和结束指针
  • 无法访问整个包的metadata,所以输入的context为xdp_buff
  • 由于是eBPF程序,XDP程序有受限的执行时间,所以在网络pipeline中有固定的使用开销

XDP工作模式

XDP有三种工作模式,默认是native模式:

  • Native XDP

    默认模式,在这种模式中,XDP BPF 程序直接运行在网络驱动的早期接收路径上( early receive path)。大部分广泛使用的 10G 及更高速的网卡都已经支持这种模式 。

  • Offloaded XDP

    在这种模式中,XDP BPF 程序直接 offload 到网卡,而不是在主机的 CPU 上执行。 因此,本来就已经很低的 per-packet 开销完全从主机下放到网卡,能够比运行在 native XDP 模式取得更高的性能。这种 offload 通常由智能网卡实现,这些网卡有多 线程、多核流处理器(flow processors),一个位于内核中的 JIT 编译器( in-kernel JIT compiler)将 BPF 翻译成网卡的原生指令。

    支持 offloaded XDP 模式的驱动通常也支持 native XDP 模式,因为 BPF 辅助函数可 能目前还只支持后者。

  • Generic XDP

    对于还没有实现 native 或 offloaded XDP 的驱动,内核提供了一个 generic XDP 选 项,这种模式不需要任何驱动改动,因为相应的 XDP 代码运行在网络栈很后面的一个 位置(a much later point)。

    这种设置主要面向的是用内核的 XDP API 来编写和测试程序的开发者,并且无法达到 前面两种模式能达到的性能。对于在生产环境使用 XDP,推荐要么选择 native 要么选择 offloaded 模式。

带来的变化

原本包的路径如下:

NIC设备 → Driver → Traffic Control → Netfilter → TCP Stack → Socket

offloaded模式工作在NIC上,而native模式工作在Driver上。

使用案例

  • DDoS 防御、防火墙

    XDP BPF 的一个基本特性就是用 XDP_DROP 命令驱动将包丢弃,由于这个丢弃的位置 非常早,因此这种方式可以实现高效的网络策略,平均到每个包的开销非常小( per-packet cost)。这对于那些需要处理任何形式的 DDoS 攻击的场景来说是非常理 想的,而且由于其通用性,使得它能够在 BPF 内实现任何形式的防火墙策略,开销几乎为零, 例如,作为 standalone 设备(例如通过 XDP_TX 清洗流量);或者广泛部署在节点 上,保护节点的安全(通过 XDP_PASS 或 cpumap XDP_REDIRECT 允许“好流量”经 过)。

    Offloaded XDP 更进一步,将本来就已经很小的 per-packet cost 全部下放到网卡以 线速(line-rate)进行处理。

  • 转发和负载均衡

    XDP 的另一个主要使用场景是包转发和负载均衡,这是通过 XDP_TXXDP_REDIRECT 动作实现的。

    XDP 层运行的 BPF 程序能够任意修改(mangle)数据包,即使是 BPF 辅助函数都能增 加或减少包的 headroom,这样就可以在将包再次发送出去之前,对包进行任何的封装/解封装。

    利用 XDP_TX 能够实现 hairpinned(发卡)模式的负载均衡器,这种均衡器能够 在接收到包的网卡再次将包发送出去,而 XDP_REDIRECT 动作能够将包转发到另一个 网卡然后发送出去。

    XDP_REDIRECT 返回码还可以和 BPF cpumap 一起使用,对那些目标是本机协议栈、 将由 non-XDP 的远端(remote)CPU 处理的包进行负载均衡。

  • 栈前(Pre-stack)过滤/处理

    除了策略执行,XDP 还可以用于加固内核的网络栈,这是通过 XDP_DROP 实现的。 这意味着,XDP 能够在可能的最早位置丢弃那些与本节点不相关的包,这个过程发生在 内核网络栈看到这些包之前。例如假如我们已经知道某台节点只接受 TCP 流量,那任 何 UDP、SCTP 或其他四层流量都可以在发现后立即丢弃。

    这种方式的好处是包不需要再经过各种实体(例如 GRO 引擎、内核的 flow dissector 以及其他的模块),就可以判断出是否应该丢弃,因此减少了内核的 受攻击面。正是由于 XDP 的早期处理阶段,这有效地对内核网络栈“假装”这些包根本 就没被网络设备看到。

    另外,如果内核接收路径上某个潜在 bug 导致 ping of death 之类的场景,那我们能 够利用 XDP 立即丢弃这些包,而不用重启内核或任何服务。而且由于能够原子地替换 程序,这种方式甚至都不会导致宿主机的任何流量中断。

    栈前处理的另一个场景是:在内核分配 skb 之前,XDP BPF 程序可以对包进行任意 修改,而且对内核“假装”这个包从网络设备收上来之后就是这样的。对于某些自定义包 修改(mangling)和封装协议的场景来说比较有用,在这些场景下,包在进入 GRO 聚 合之前会被修改和解封装,否则 GRO 将无法识别自定义的协议,进而无法执行任何形 式的聚合。

    XDP 还能够在包的前面 push 元数据(非包内容的数据)。这些元数据对常规的内核栈 是不可见的(invisible),但能被 GRO 聚合(匹配元数据),稍后可以和 tc ingress BPF 程 序一起处理,tc BPF 中携带了 skb 的某些上下文,例如,设置了某些 skb 字段。

  • 流抽样(Flow sampling)和监控

    XDP 还可以用于包监控、抽样或其他的一些网络分析,例如作为流量路径中间节点 的一部分;或运行在终端节点上,和前面提到的场景相结合。对于复杂的包分析,XDP 提供了设施来高效地将网络包(截断的或者是完整的 payload)或自定义元数据 push 到 perf 提供的一个快速、无锁、per-CPU 内存映射缓冲区,或者是一 个用户空间应用。

    这还可以用于流分析和监控,对每个流的初始数据进行分析,一旦确定是正常流量,这个流随 后的流量就会跳过这个监控。感谢 BPF 带来的灵活性,这使得我们可以实现任何形式 的自定义监控或采用。

eBPF工具

参考