简介 eBPF(Extended Berkeley Packet Filter)起源于Linux内核,它可以在中断上下文中(如操作系统内核)运行沙盒程序。它用于安全有效地扩展内核的功能,与传统方法需要修改内核源代码或加载新模块不同,eBPF使得动态定制和优化网络行为成为可能,且不会中断系统操作。
eBPF的强大之处包括:
直接内核交互:eBPF程序在内核中执行,与系统级事件如网络包、系统调用或追踪点交互。
安全执行:eBPF通过验证器在程序运行前检查其逻辑,防止潜在的内核崩溃或安全漏洞。
最低开销:eBPF通过使用即时编译器(JIT),将eBPF字节码转换为针对特定架构的优化机器码,实现近原生执行速度。
eBPF程序是事件驱动的,当内核或应用程序通过某个钩子点时运行。预定义的钩子包括系统调用、函数入口/退出、内核跟踪点、网络事件等。如果预定义的钩子不能满足特定需求,则可以创建内核探针(kprobe)或用户探针(uprobe),以便在内核用户或应用程序的几乎任何位置附加eBPF程序。 确定需要的钩子后,就可以使用bpf系统调用将eBPF程序加载到Linux内核中。当程序被加载到Linux内核时,它在被附加到所请求的钩子上之前需要经过两个步骤:
验证:验证步骤用于保证eBPF程序可以安全运行。它可以验证程序是否满足几个条件:
加载eBPF程序的进程必须有必需的能力(特权)。除非启用非特权eBPF,否则只有特权进程可以加载eBPF程序。
eBPF程序不会崩溃或者对系统造成损害。
eBPF程序一定会运行至结束(即程序不会处于循环状态中,否则会阻碍后续的处理)。
JIT编译:JIT(Just-in-Time)编译步骤将程序的通用字节码高效转换为机器的特定指令集,从而优化程序的执行。这使得eBPF程序可以像本地编译的内核代码或作为内核模块加载的代码一样地运行。
eBPF程序的其中一个重要方面是共享和存储所收集的信息和状态的能力。为此,eBPF程序可以利用eBPF Maps的概念来存储和检索各种数据结构中的数据。如下图所示,eBPF Maps既可以从eBPF程序访问,也可以通过系统调用从用户空间中的应用程序访问。
一个典型的eBPF程序包含两个部分:内核空间代码(*_kern.c)和用户空间代码(*_user.c)。
内核空间代码定义逻辑:
功能
说明
定义事件处理逻辑
编写当某个内核事件触发时要执行的代码(如系统调用被调用、网络包到达、函数进入/退出等)
数据收集与过滤
在内核态直接捕获数据,进行初步过滤和聚合,减少用户态开销
Map操作
将处理结果写入BPF Map(内核与用户空间共享的键值存储)
触发通知
通过bpf_ringbuf_submit()或perf_event向用户空间发送事件
1 2 3 4 5 6 7 8 SEC("tracepoint/syscalls/sys_enter_openat" ) int trace_openat (struct trace_event_raw_sys_enter *ctx) { bpf_printk("PID %d opening file" , bpf_get_current_pid_tgid() >> 32 ); return 0 ; }
用户空间代码负责加载和与内核交互:
功能
说明
加载eBPF程序
将编译好的*_kern.o字节码加载进内核,经过验证器检查
附着到钩子点
把程序挂到具体的事件源(如tracepoint、kprobe、XDP等)
管理BPF Map
创建、读取、更新Map,与内核程序交换数据
处理数据输出
从Map或Ring Buffer读取结果,格式化输出(打印、存文件、发网络等)
生命周期控制
信号处理、优雅退出、资源清理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 int main () { struct bpf_object *obj = bpf_object__open_file("program_kern.o" , NULL ); bpf_object__load(obj); struct bpf_program *prog = bpf_object__find_program_by_name(obj, "trace_openat" ); bpf_program__attach(prog); struct bpf_map *map = bpf_object__find_map_by_name(obj, "events" ); while (running) { } bpf_object__close(obj); }
在实际开发中,eBPF并不是直接使用,而是通过像Cilium、bcc或bpftrace这样的项目间接使用,这些项目提供了eBPF上面的抽象,不需要直接编写定义程序,而是提供了基于意图的来实现的能力,然后用eBPF实现。以下是一些eBPF开发工具:
BCC:一个基于Python的工具链,简化了eBPF程序的编写、编译和加载。它提供了许多预构建的追踪工具,但在依赖和兼容性方面存在一些限制。
eBPF Go库:一个Go库,解耦了获取eBPF字节码的过程与加载和管理eBPF程序的过程。
libbpf-bootstrap:基于libbpf的现代脚手架,提供了高效的工作流用于编写eBPF程序,提供简单的一次性编译过程以生成可重用的字节码。
eunomia-bpf:一个用于编写仅包含内核空间代码的eBPF程序的工具链,它通过动态加载eBPF程序简化了eBPF程序的开发。
Aya:一个纯Rust实现的eBPF开发框架,提供了完全基于Rust的工具链来同时编写内核空间和用户空间代码。
环境搭建 由于eBPF是内核技术,因此需要具备较新版本的Linux内核(推荐Ubuntu24.04),以支持eBPF功能。以ebpf-go框架为例,安装如下依赖与相关包:
1 2 3 4 5 6 7 sudo apt-get install -y make clang llvm libelf-dev libbpf-dev bpfcc-tools libbpfcc-dev linux-tools-$(uname -r) linux-headers-$(uname -r)go env -w GOPROXY=https://goproxy.cn,direct go install github.com/cilium/ebpf/cmd/bpf2go@latest go install github.com/aya-rs/bpf-linker@latest echo 'export PATH=$PATH:$(go env GOPATH)/bin' >> ~/.bashrcsource ~/.bashrc
或者可以搭建一个Docker环境来进行代码的编写和编译。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 FROM --platform=linux/amd64 ubuntu:24.04 ENV DEBIAN_FRONTEND=noninteractiveENV CLANG_VERSION=18 ENV SSH_USER=rootENV SSH_PASSWORD=rootRUN set -eux; \ sed -i 's|http://archive.ubuntu.com/ubuntu|https://mirrors.aliyun.com/ubuntu|g' /etc/apt/sources.list && \ sed -i 's|http://security.ubuntu.com/ubuntu|https://mirrors.aliyun.com/ubuntu|g' /etc/apt/sources.list && \ apt-get update && \ apt-get install -y --no-install-recommends \ ca-certificates build-essential git make cmake pkg-config \ curl wget vim openssh-server sudo \ clang-${CLANG_VERSION} \ lld-${CLANG_VERSION} \ llvm-${CLANG_VERSION} \ llvm-${CLANG_VERSION} -dev \ libclang-${CLANG_VERSION} -dev \ libbpf-dev \ libelf-dev \ zlib1g-dev \ libssl-dev \ linux-headers-generic \ libc6-dev-amd64-cross \ gcc-x86-64-linux-gnu \ linux-tools-generic \ libncurses-dev \ libcap-dev \ libbfd-dev \ trace-cmd tcpdump strace \ iproute2 iputils-ping net-tools ethtool \ python3 python3-pip python3-dev \ python3-setuptools python3-wheel \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* RUN update-alternatives --install /usr/bin/clang clang /usr/bin/clang-${CLANG_VERSION} 100 \ && update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-${CLANG_VERSION} 100 \ && update-alternatives --install /usr/bin/llc llc /usr/bin/llc-${CLANG_VERSION} 100 \ && update-alternatives --install /usr/bin/llvm-strip llvm-strip /usr/bin/llvm-strip-${CLANG_VERSION} 100 \ && update-alternatives --install /usr/bin/llvm-objdump llvm-objdump /usr/bin/llvm-objdump-${CLANG_VERSION} 100 RUN set -eux; \ git clone --recurse-submodules --depth=1 https://github.com/libbpf/bpftool.git /tmp/bpftool && \ cd /tmp/bpftool/src && \ make -j$(nproc ) && \ make install && \ bpftool version && \ rm -rf /tmp/bpftool ENV GOPATH=/root/goENV PATH=$PATH:/usr/local/go/bin:$GOPATH/binRUN set -eux; \ arch ="$(dpkg --print-architecture) " ; \ wget -q "https://go.dev/dl/go1.24.0.linux-${arch} .tar.gz" -O /tmp/go.tar.gz && \ tar -C /usr/local -xzf /tmp/go.tar.gz && \ rm /tmp/go.tar.gz && \ go version RUN set -eux; \ go env -w GOPROXY=https://goproxy.cn,direct && \ go env -w GOSUMDB=sum.golang.google.cn && \ go install github.com/cilium/ebpf/cmd/bpf2go@latest && \ which bpf2go ENV CARGO_HOME=/root/.cargoENV RUSTUP_HOME=/root/.rustupENV PATH=$PATH:$CARGO_HOME/binRUN set -eux; \ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable && \ . $CARGO_HOME /env && \ rustup toolchain install nightly && \ rustup component add rust-src --toolchain nightly && \ rustup component add rustfmt clippy && \ cargo install cargo-generate RUN set -eux; \ cat >> /root/.bashrc <<'EOF' export GOPATH=/root/go export CARGO_HOME=/root/.cargo export RUSTUP_HOME=/root/.rustup export PATH=/usr/local/go/bin:$GOPATH/bin:$CARGO_HOME/bin:$PATH EOF RUN set -eux; \ mkdir -p /var/run/sshd && \ echo "root:${SSH_PASSWORD} " | chpasswd && \ mkdir -p /root/.ssh && \ chmod 700 /root/.ssh && \ sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config && \ sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config && \ sed -i 's/^#\?UsePAM.*/UsePAM no/' /etc/ssh/sshd_config && \ ssh-keygen -A WORKDIR /workspace EXPOSE 22 CMD ["/usr/sbin/sshd" , "-D" , "-e" ]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 version: "3.8" services: ebpf-dev: build: context: . dockerfile: Dockerfile platforms: - linux/amd64 image: ebpf-dev:latest container_name: ebpf-dev hostname: ebpf-dev privileged: false ports: - "2222:22" volumes: - ./workspace:/workspace - ~/.ssh:/root/.ssh:ro deploy: resources: limits: cpus: '4' memory: 8G environment: - GO_ENV=development - GOPROXY=https://goproxy.cn,direct - GOSUMDB=sum.golang.google.cn restart: unless-stopped stdin_open: true tty: true
测试代码 先编写一个eBPF程序,并使用bpf2go进行编译,该程序运行在内核态。它挂到sock/inet_sock_set_state tracepoint,筛选IPv4、TCP、状态变为已建立、且本地端口是22的连接,然后把客户端IP、客户端端口和服务端端口写进ring buffer,上报给用户态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 #define BPF_NO_GLOBAL_DATA #include "vmlinux.h" #include <bpf/bpf_helpers.h> #include <bpf/bpf_tracing.h> #define AF_INET 2 #define IPPROTO_TCP 6 #define TCP_ESTABLISHED 1 #define SSH_PORT 22 struct event { __u8 src_ip[4 ]; __u16 src_port; __u16 dst_port; }; struct { __uint(type, BPF_MAP_TYPE_RINGBUF); __uint(max_entries, 1 << 24 ); } events SEC (".maps" ) ; SEC("tracepoint/sock/inet_sock_set_state" ) int handle_ssh_conn (struct trace_event_raw_inet_sock_set_state *ctx) { if (ctx->family != AF_INET) { return 0 ; } if (ctx->protocol != IPPROTO_TCP) { return 0 ; } if (ctx->newstate != TCP_ESTABLISHED) { return 0 ; } if (ctx->sport != SSH_PORT) { return 0 ; } struct event *e = bpf_ringbuf_reserve(&events, sizeof (*e), 0 ); if (e) { e->src_ip[0 ] = ctx->daddr[0 ]; e->src_ip[1 ] = ctx->daddr[1 ]; e->src_ip[2 ] = ctx->daddr[2 ]; e->src_ip[3 ] = ctx->daddr[3 ]; e->src_port = ctx->dport; e->dst_port = ctx->sport; bpf_ringbuf_submit(e, 0 ); } return 0 ; } char LICENSE[] SEC("license" ) = "Dual BSD/GPL" ;
接着编写用户态程序,负责加载eBPF对象、把程序挂到tracepoint、打开ring buffer读事件,并把读到的源IP打印出来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 package mainimport ( "bytes" "context" "encoding/binary" "errors" "fmt" "log" "net" "os/signal" "syscall" "github.com/cilium/ebpf/link" "github.com/cilium/ebpf/ringbuf" "github.com/cilium/ebpf/rlimit" ) type event struct { SrcIP [4 ]byte SrcPort uint16 DstPort uint16 } func main () { ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() if err := rlimit.RemoveMemlock(); err != nil { log.Fatalf("remove memlock limit failed: %v" , err) } var objs helloObjects if err := loadHelloObjects(&objs, nil ); err != nil { log.Fatalf("load eBPF objects failed: %v" , err) } defer objs.Close() tp, err := link.Tracepoint("sock" , "inet_sock_set_state" , objs.HandleSshConn, nil ) if err != nil { log.Fatalf("attach tracepoint failed: %v" , err) } defer tp.Close() rd, err := ringbuf.NewReader(objs.Events) if err != nil { log.Fatalf("open ring buffer failed: %v" , err) } defer rd.Close() go func () { <-ctx.Done() _ = rd.Close() }() log.Println("eBPF program attached. Printing SSH source IP on new connections. Press Ctrl+C to exit." ) for { rec, err := rd.Read() if err != nil { if errors.Is(err, ringbuf.ErrClosed) { log.Println("stopping..." ) return } log.Printf("read ring buffer failed: %v" , err) continue } var e event if err := binary.Read(bytes.NewReader(rec.RawSample), binary.LittleEndian, &e); err != nil { log.Printf("decode event failed: %v" , err) continue } srcIP := net.IPv4(e.SrcIP[0 ], e.SrcIP[1 ], e.SrcIP[2 ], e.SrcIP[3 ]) fmt.Printf("new SSH connection: src=%s:%d dst_port=%d\n" , srcIP.String(), e.SrcPort, e.DstPort) } }