小试ebpf-go

ebpf-go是cilium开发的一个使用go来编写ebfp程序的框架

环境准备

  • Linux 内核版本 5.7 或更高版本,用于 bpf_link 支持

  • LLVM 11 或更高版本 (clang 和 llvm-strip)

  • libbpf headers

    • Fedora是libbpf-devel
    • debian/ubuntu是libbpf-dev
  • Linux 内核头文件

    • amd64架构的ubuntu/debian是linux-headers-amd64
    • 在Fedora上是kernel-devel这个包
    • 在debian上可能需要执行ln -sf /usr/include/asm-generic/ /usr/include/asm,下面的例子需要<asm/types.h>
  • ebpf-go 的 Go 模块支持的 Go 编译器版本

  • 安装依赖

sudo apt install clang
sudo apt install llvm
apt install libbpf-dev

sudo apt install pkg-config
sudo apt install m4
sudo apt install libelf-dev
sudo apt install libpcap-dev
sudo apt install gcc-multilib

例子

  • 大概流程如下:
graph LR
    A[eBPF的C代码] --> B[Go代码绑定]
    B --> C[main.go集成]
    C --> D[生成最终可执行文件]
  • 创建一个空目录

  • 这个ebpf的程序,将其保存为counter.c放在创建的目录中,下面的操作都在这个目录中

//go:build ignore

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

struct {
    __uint(type, BPF_MAP_TYPE_ARRAY); 
    __type(key, __u32);
    __type(value, __u64);
    __uint(max_entries, 1);
} pkt_count SEC(".maps"); 

// count_packets atomically increases a packet counter on every invocation.
SEC("xdp") 
int count_packets() {
    __u32 key    = 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";
  • 保存为gen.go,用于执行go generate
package main

//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -tags linux counter counter.c
  • 初始化go mod
go mod init ebpf-test
go mod tidy
  • 安装bpf2go
go get github.com/cilium/ebpf/cmd/bpf2go
  • 使用go generate生成go代码,如果环境依赖有问题这步会报错
go generate
  • 保存为main.go调用生成的go代码
package main

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

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

func main() {
    // Remove resource limits for kernels <5.11.
    if err := rlimit.RemoveMemlock(); err != nil {
        log.Fatal("Removing memlock:", err)
    }

    // Load the compiled eBPF ELF and load it into the kernel.
    var objs counterObjects
    if err := loadCounterObjects(&objs, nil); err != nil {
        log.Fatal("Loading eBPF objects:", err)
    }
    defer objs.Close()

    ifname := "eth0" // Change this to an interface on your machine.
    iface, err := net.InterfaceByName(ifname)
    if err != nil {
        log.Fatalf("Getting interface %s: %s", ifname, err)
    }

    // Attach count_packets to the network interface.
    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("Counting incoming packets on %s..", ifname)

    // Periodically fetch the packet counter from PktCount,
    // exit the program when interrupted.
    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("Received %d packets", count)
        case <-stop:
            log.Print("Received signal, exiting..")
            return
        }
    }
}
  • 最终目录下应该有下面的文件
tree ebpf-test
# ebpf-test
# ├── counter.c
# ├── counter_bpfeb.go
# ├── counter_bpfeb.o
# ├── counter_bpfel.go
# ├── counter_bpfel.o
# ├── gen.go
# ├── go.mod
# ├── go.sum
# └── main.go
  • 编译运行,可以看到日志打印
go build && sudo ./ebpf-test
# 2025/06/12 16:37:08 Counting incoming packets on eth0..
# 2025/06/12 16:37:09 Received 16 packets
# 2025/06/12 16:37:10 Received 26 packets
# 2025/06/12 16:37:11 Received 32 packets
  • 和其他的代码生成器一样,如果c代码有变动需要重新运行go generate重新生成go代码

参考资料

https://ebpf-go.dev/guides/getting-started/