3.cilium-ebpf

1. 简介

项目地址: https://github.com/cilium/ebpf
cilium/ebpf 也叫 cilium/ebpf-go,这是一个纯 Go 库,用于读取、修改和加载 eBPF 程序,并将它们附加到 Linux 内核中的各种钩子上。

2. 环境

安装相应的包

sudo apt update
sudo apt install clang llvm

安装go

#手动安装
wget https://dl.google.com/go/go1.24.2.linux-amd64.tar.gz
tar -C /usr/local -xzf go1.24.2.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin
go version

#自动安装
snap  install go

3. 案例

3.1. 数据包计数

总体来说,通过XDP处理程序对通过的数据包进行计数,并将计数结果存储在eBPF映射中。这种技术可以用于实现轻量级的网络流量统计和监控。

3.1.1. 编写 eBPF C代码(内核态代码)

//go:build ignore

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>

// 定义了名为 pkt_count 的eBPF映射,这个映射用于存储数据包计数器
// 类型为 BPF_MAP_TYPE_ARRAY 的数组映射,键类型为 __u32,值类型为 __u64,最大条目数为 1
struct {
    __uint(type, BPF_MAP_TYPE_ARRAY); 
    __type(key, __u32);
    __type(value, __u64);
    __uint(max_entries, 1);
} pkt_count SEC(".maps"); 

// count_packets 在每次系统发生xdp事件调用时自动增加一个数据包计数器
SEC("xdp")
int count_packets() {
    __u32 key    = 0; 
    // 查找映射 pkt_count 中键为0的计数器的指针
    __u64 *count = bpf_map_lookup_elem(&pkt_count, &key); 
    if (count) { 
        __sync_fetch_and_add(count, 1); 
    }

    return XDP_PASS; 
}

// 程序的许可证信息
char __license[] SEC("license") = "Dual MIT/GPL";

这个例子中我们需要软链接头文件,以便 eBPF C程序能够找到 asm/types.h 文件。

ln -sf /usr/include/asm-generic/ /usr/include/asm

3.1.2. 初始化 GOMOD

go mod init cilium-ebpf #最好与项目名字一样

3.1.3. 获取 bpf2go

bpf2go 允许 在 Go 代码中编译和嵌入用 C 编写的 eBPF 程序,以及编译 C 代码,它会自动生成用于加载和操作的 Go 代码 eBPF 程序和 map 对象。

go get github.com/cilium/ebpf/cmd/bpf2go

3.1.4. 创建一个代码生成文件

这是一个用于代码生成的 Go 源文件,通过使用 //go:generate 注释,告诉 Go 工具链在构建项目时执行 go generate 命令, go generate 会执行 //go:generate 注释后的命令,这里是运行 bpf2go 工具生成与 counter.c 对应的 Go 代码,生成的代码主要用于与 eBPF 程序进行交互,包括加载 eBPF 程序到内核、定义 eBPF 程序相关的数据结构等。

通过这种方式,可以自动生成用于与 eBPF 程序交互的 Go 代码,避免手动编写与 eBPF 程序交互的冗长代码。这使得在 Go 项目中更容易地集成和使用 eBPF 程序。

gen.go 文件

package main
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go counter counter.c
func test() {}

3.1.5. 编译 eBPF C代码并生成脚手架

当前目录下有

root@ubuntu22:~/ebpf/eBPFSecureDev/cilium-ebpf# ls
count.c  go.mod  go.sum  main.go

在当前目录中运行构建命令 go generate ,成功后 bpf2go 自动生成了脚手架,生成了两个 Go 源文件:counter_bpfel.gocounter_bpfeb.go,以及相应的 .o 目标文件,

root@ubuntu22:~/ebpf/eBPFSecureDev/cilium-ebpf# go generate
root@ubuntu22:~/ebpf/eBPFSecureDev/cilium-ebpf# ls
counter_bpfeb.go  counter_bpfeb.o  counter_bpfel.go  counter_bpfel.o  counter.c  go.mod  go.sum  main.go

这两个文件包含了与 eBPF 程序进行交互所需的 Go 代码,
counter_bpfel.go 是为 Little Endian 架构(例如 x86_64)生成的
counter_bpfeb.go 是为 Big Endian 架构生成的。
生成的.go文件包含了从C 代码生成的结构、函数和常量

3.1.6. 编写 eBPF GO代码(用户态代码)

用户态代码的目的主要是加载已编译的 eBPF ELF 文件并将其加载到内核中,然后将其附加到 Linux 内核的网络接口(通过 XDP hook)。随后,程序周期性地获取 eBPF 映射中的包计数,并在收到中断信号时退出。

前面我们利用 bpf2go 将内核态代码(C语言)转化成GO语言代码,目的就是方便用户态代码(GO语言)能操作内核态代码
mian.go

package main

import (
    "log"
    "net"
    "os"
    "os/signal"
    "time"

    "github.com/cilium/ebpf/link"
    "github.com/cilium/ebpf/rlimit"
)

func main() {
    // 移除了内核版本低于 5.11 的系统上的资源限制,允许程序锁定内存
    if err := rlimit.RemoveMemlock(); err != nil { 
        log.Fatal("Removing memlock:", err)
    }

    // loadCounterObjects 函数用于加载编译后的 eBPF ELF 文件
    // 并将其对象存储在 counterObjects 结构体中(结构体调用于 counter_bpfel.go)
    var objs counterObjects 
    if err := loadCounterObjects(&objs, nil); err != nil {
        log.Fatal("Loading eBPF objects:", err)
    }
    defer objs.Close() 

    ifname := "eth0" // 使用 eth0 作为默认的网络接口
    iface, err := net.InterfaceByName(ifname)   // 获取指定名称的网络接口的信息
    if err != nil {
        log.Fatalf("Getting interface %s: %s", ifname, err)
    }

    // 将 count_packets eBPF 程序附加到指定的网络接口
    link, err := link.AttachXDP(link.XDPOptions{ 
        Program:   objs.CountPackets,
        Interface: iface.Index,
    })
    if err != nil {
        log.Fatal("Attaching XDP:", err)
    }
    defer link.Close() 

    log.Printf("正在对传入数据包进行计数 %s..", ifname)

    // 第一个通道是 tick,它每秒发送一个信号,从而定期获取 eBPF 映射中的包计数
    // 第二个通道是 stop,它监听 os.Interrupt 信号,一旦接收到中断信号,程序就会退出
    tick := time.Tick(time.Second)
    stop := make(chan os.Signal, 5)
    signal.Notify(stop, os.Interrupt)
    for {
        select {
        case <-tick:
            var count uint64
            err := objs.PktCount.Lookup(uint32(0), &count) 
            if err != nil {
                log.Fatal("Map lookup:", err)
            }
            log.Printf("已接受到 %d 数据包", count)
        case <-stop:
            log.Print("接收到信号, 退出..")
            return
        }
    }
}

构建和运行 Go 应用程序 go build

go mod tidy
chmod +x cilium-ebpf && ./cilium-ebpf

计数器增加。
Pasted image 20250420152300

3.1.7. 项目迭代

当对 eBPF C代码(内核态代码)进行更新后,需要重新运行 go generate 确保使生成的文件保持最新。