项目地址: https://github.com/cilium/ebpf
cilium/ebpf
也叫 cilium/ebpf-go
,这是一个纯 Go 库,用于读取、修改和加载 eBPF 程序,并将它们附加到 Linux 内核中的各种钩子上。
安装相应的包
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
总体来说,通过XDP处理程序对通过的数据包进行计数,并将计数结果存储在eBPF映射中。这种技术可以用于实现轻量级的网络流量统计和监控。
//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
go mod init cilium-ebpf #最好与项目名字一样
bpf2go 允许 在 Go 代码中编译和嵌入用 C 编写的 eBPF 程序,以及编译 C 代码,它会自动生成用于加载和操作的 Go 代码 eBPF 程序和 map 对象。
go get github.com/cilium/ebpf/cmd/bpf2go
这是一个用于代码生成的 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() {}
当前目录下有
root@ubuntu22:~/ebpf/eBPFSecureDev/cilium-ebpf# ls
count.c go.mod go.sum main.go
在当前目录中运行构建命令 go generate
,成功后 bpf2go 自动生成了脚手架,生成了两个 Go 源文件:counter_bpfel.go
和 counter_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 代码生成的结构、函数和常量
用户态代码的目的主要是加载已编译的 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
计数器增加。
当对 eBPF C代码(内核态代码)进行更新后,需要重新运行 go generate 确保使生成的文件保持最新。