libbpfgo

repository·main·Indexed 21 days ago

https://github.com/aquasecurity/libbpfgo

A thin Go wrapper around the standard C libbpf library that allows Go developers to interact with eBPF programs. It abstracts C technicalities into idiomatic Go patterns, providing structs and channels for managing BPF modules, programs, maps, and event consumption via RingBuffer and PerfBuffer. It is used by projects such as Tracee for runtime security and tracing.

Tokens
22K
Snippets
64
Records
88
Agent score
74%

What's inside libbpfgo

  1. How libbpfgo works: Core Concepts

    main

    libbpfgo abstracts C technicalities into idiomatic Go patterns: it translates low-level return codes into Go error types, organizes functionality around Go structs, and uses Go channels for event consumption.

    A typical workflow for using the library is:

    1. Compile: Compile your BPF program into an object file.
    2. Initialize Module: Create a Module struct representing the unit of BPF functionality around your compiled object file.
    3. Load Programs: Use the BPFProg struct to load BPF programs from the object file.
    4. Attach: Attach BPFProg to system facilities (e.g., "raw tracepoints" or "kprobes") using its associated functions.
    5. Manage Maps: Use the BPFMap struct and its methods to instantiate and manipulate BPF Maps.
    6. Handle Events: Use the RingBuffer struct and its associated objects to communicate events from your BPF program to userspace via channels.
    // initializing
    import bpf "github.com/aquasecurity/libbpfgo"
    ...
    bpfModule := bpf.NewModuleFromFile(bpfObjectPath)
    bpfModule.BPFLoadObject()
    
    // maps
    mymap, _ := bpfModule.GetMap("mymap")
    mymap.Update(key, value)
    
    // ring buffer
    rb, _ := bpfModule.InitRingBuffer("events", eventsChannel, buffSize)
    rb.Poll(300)
    e := <-eventsChannel
  2. Use spinlocks in BTF annotated BPF maps

    main

    In BPF maps that are BTF annotated, you can use bpf_spin_lock to perform atomic reads and updates of map values. This is useful for ensuring data consistency when multiple BPF programs or CPU cores access the same map entry simultaneously.

    Important Compatibility Note: Spinlocks are NOT supported in programs of the following types:

    • BPF_PROG_TYPE_KPROBE
    • BPF_PROG_TYPE_TRACEPOINT
    • BPF_PROG_TYPE_PERF_EVENT
    • BPF_PROG_TYPE_RAW_TRACEPOINT
  3. Share a ringbuffer across multiple BPF objects via map pinning

    main

    You can have multiple BPF objects that share a single ringbuffer by using map pinning. This allows different BPF programs to write to and read from the same underlying ringbuffer instance.

    To implement this, define the ringbuffer in your C code using the LIBBPF_PIN_BY_NAME attribute. When libbpf loads subsequent BPF objects that contain a map with the same name and the same pinning attribute, it will automatically reuse the existing file descriptor from the BPF file system instead of creating a new map. This ensures all objects are wired to the same underlying map.

    struct {
        __uint(pinning, LIBBPF_PIN_BY_NAME);
        __uint(type, BPF_MAP_TYPE_RINGBUF);
        __uint(max_entries, 1 << 24);
    } events SEC(".maps");
  4. Understand libbpfgo versioning and libbpf requirements

    main

    libbpfgo follows semantic versioning for its releases:

    • Major releases: Breaking changes or major milestones (e.g., reaching parity with libbpf's API).
    • Minor releases: New support for libbpf APIs.
    • Patch releases: Bug fixes.

    libbpf support numbering: The version string includes the minimum required libbpf version. Example: v0.2.1-libbpf-0.4.0 means libbpfgo version 0.2.1 requires libbpf version 0.4.0 or newer.

    Recommendation: If your distribution uses custom or backported libbpf packages, use static compilation to ensure compatibility.

  5. Set up a development environment with Vagrant

    main

    To run libbpfgo in a controlled environment, you can use the provided Vagrant configuration. The project uses the bento/ubuntu-24.04 box, which supports multiple providers including virtualbox (for amd64) and parallels (for arm64 and amd64).

    It is recommended to use the project's Makefile rules to manage the Vagrant lifecycle. You can override the ARCH environment variable if your specific architecture and provider require it.

    # Example: Starting Vagrant with a specific architecture
    make vagrant-up ARCH=amd64
  6. Install Vagrant requirements on Darwin (macOS)

    main

    To use Vagrant on macOS, you need to install the Vagrant binary via Homebrew. If you intend to use Parallels as your provider, you must also install the Parallels application and the corresponding Vagrant plugin.

    # Install Vagrant
    brew install vagrant
    
    # Install Parallels and the Vagrant plugin
    brew install --cask parallels
    vagrant plugin install vagrant-parallels
  7. Build libbpfgo with Makefile

    main

    The project provides several GNU Makefile rules for different linking strategies.

    Dynamic Linking (Uses OS libbpf)

    Requires a recent enough libbpf package and headers installed on your OS.

    • make libbpfgo-dynamic: Builds dynamic libbpfgo.
    • make libbpfgo-dynamic-test: Runs go test with dynamic libbpfgo.
    • make selftest-dynamic: Builds tests with dynamic libbpfgo.
    • make selftest-dynamic-run: Runs tests using dynamic libbpfgo.

    Static Linking (Uses libbpf submodule)

    Note: You must run git submodule init to sync the libbpf submodule before building.

    • make libbpfgo-static: Builds static libbpfgo.
    • make libbpfgo-static-test: Runs go test with static libbpfgo.
    • make selftest-static: Builds tests with static libbpfgo.
    • make selftest-static-run: Runs all static selftests.

    General Rules

    • make all: Builds libbpfgo (dynamic).
    • make clean: Cleans the entire tree.
    # Example: Build statically linked libbpfgo
    $ make libbpfgo-static
    
    # Example: Build and run all static selftests
    $ make selftest-static-run
  8. Install libbpfgo

    main

    libbpfgo uses CGO to interop with the C library libbpf. Simply importing the package is not sufficient; you must ensure libbpf is available at link or runtime using one of these two methods:

    1. System Shared Object: Install libbpf as a shared object on your operating system. Your distribution may already provide it, or you can build it from source.
    2. Vendored Dependency (Static Linking): Embed libbpf directly into your Go project. This statically links the code into your resulting binary, eliminating runtime dependencies on the host OS. This is the approach used by the Tracee project.
  9. Use RingBuffer to consume eBPF events

    main

    The RingBuffer type is used to poll and consume data from an eBPF ring buffer. It manages a background goroutine that calls the underlying C ring_buffer__poll function.

    To use it:

    1. Initialize the RingBuffer (typically via a constructor not shown in this file, but linked to a BPFMap).
    2. Call Poll(timeout) to start the background polling process. The timeout parameter specifies how long to wait in milliseconds.
    3. Call Stop() to signal the polling goroutine to exit and to drain event channels to prevent deadlocks.
    4. Call Close() to stop the buffer, free the underlying C memory (ring_buffer__free), and remove associated event channels.
    // Example lifecycle of a RingBuffer
    rb := &libbpfgo.RingBuffer{...}
    
    // Start polling with a 300ms timeout
    rb.Poll(300)
    
    // ... perform work ...
    
    // Stop the polling goroutine
    rb.Stop()
    
    // Clean up resources and C memory
    rb.Close()
  10. Consume eBPF events with PerfBuffer

    main

    The PerfBuffer type is used to consume events from an eBPF perf buffer. It provides a mechanism to poll for data and receive events via a Go channel.

    To use a PerfBuffer:

    1. Initialize the buffer (typically via a constructor not shown in this file, but linked to a BPFMap).
    2. Call Poll(timeout) to start an asynchronous goroutine that gathers data from the buffer.
    3. The timeout parameter specifies how long to wait in milliseconds for data during each poll cycle.
    4. Listen to the eventsChan for incoming []byte data.
    5. Call Stop() to signal the polling goroutine to exit and drain remaining channels to prevent deadlocks.
    6. Call Close() to stop the buffer and free the underlying C resources.
    // Example usage pattern
    // Note: Initialization of PerfBuffer is assumed to happen via other libbpfgo APIs
    
    // Start polling with a 300ms timeout
    pb.Poll(300)
    
    // Consume events in a loop
    go func() {
        for event := range pb.eventsChan {
            fmt.Printf("Received event: %x\n", event)
        }
    }()
    
    // ... later ...
    
    pb.Stop()
    pb.Close()
  11. Use BPFMap for eBPF map management

    main

    The BPFMap type is a high-level wrapper around a libbpf bpf_map. It provides an interface for interacting with eBPF maps, including retrieving values, updating entries, deleting keys, and managing map properties like capacity and pinning.

    Important Note on Slices and Arrays: When using unsafe.Pointer with slices or arrays (for keys or values), you must point to the first element of the slice/array rather than the slice/array object itself to avoid undefined behavior.

    Example for keys:

    key := []byte{'a', 'b', 'c'}
    keyPtr := unsafe.Pointer(&key[0])
    // Use keyPtr in API calls