034 Project 34: eBPF XDP Packet Counter

Updated

September 8, 2026

034 Build an eBPF XDP Packet Counter

Attach an XDP program to a network interface, increment a packet counter in a BPF map, and poll the counter from userspace every second. Linux lab only—use a VM or spare interface.

NIC RX -> XDP program -> map++ -> userspace Lookup every 1s

Problem statement

Goals:

  • Attach XDP to an interface index (or name)
  • Kernel program counts packets (and optionally bytes)
  • Userspace prints packets=N each second
  • Clean detach on exit

Acceptance criteria

  • Document interface selection (ip link)
  • Counter increases under traffic (ping, curl)
  • Userspace reads map without racey panics
  • Defer close/detach on SIGINT
  • Lab safety notes (do not brick remote prod NIC)

Setup

mkdir xdp-count && cd xdp-count
go mod init example.com/xdp-count
go get github.com/cilium/ebpf@latest
go install github.com/cilium/ebpf/cmd/bpf2go@latest
# clang/llvm + headers as in project 33
# go 1.27

Kernel sketch count.bpf.c

//go:build ignore

#include "vmlinux.h"
#include <bpf/bpf_helpers.h>

char LICENSE[] SEC("license") = "Dual BSD/GPL";

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

SEC("xdp")
int xdp_count(struct xdp_md *ctx) {
    __u32 key = 0;
    __u64 *val = bpf_map_lookup_elem(&packet_count, &key);
    if (val) {
        __sync_fetch_and_add(val, 1);
    }
    return XDP_PASS; // do not drop traffic
}

Generate:

# //go:generate bpf2go -cc clang bpf count.bpf.c -- -I...
go generate ./...

Userspace main.go

package main

import (
    "flag"
    "fmt"
    "log"
    "net"
    "os"
    "os/signal"
    "syscall"
    "time"

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

func ifaceIndex(name string) (int, error) {
    iface, err := net.InterfaceByName(name)
    if err != nil {
        return 0, err
    }
    return iface.Index, nil
}

func main() {
    ifname := flag.String("iface", "lo", "interface name")
    flag.Parse()

    if err := rlimit.RemoveMemlock(); err != nil {
        log.Fatal(err)
    }

    idx, err := ifaceIndex(*ifname)
    if err != nil {
        log.Fatal(err)
    }

    objs := bpfObjects{}
    if err := loadBpfObjects(&objs, nil); err != nil {
        log.Fatalf("load: %v", err)
    }
    defer objs.Close()

    l, err := link.AttachXDP(link.XDPOptions{
        Program:   objs.XdpCount, // name from bpf2go
        Interface: idx,
    })
    if err != nil {
        log.Fatalf("attach xdp on %s (%d): %v", *ifname, idx, err)
    }
    defer l.Close()

    fmt.Printf("XDP attached on %s index=%d; Ctrl+C to stop\n", *ifname, idx)

    sig := make(chan os.Signal, 1)
    signal.Notify(sig, os.Interrupt, syscall.SIGTERM)

    ticker := time.NewTicker(time.Second)
    defer ticker.Stop()

    for {
        select {
        case <-sig:
            fmt.Println("detaching")
            return
        case <-ticker.C:
            var key uint32 = 0
            var value uint64
            if err := objs.PacketCount.Lookup(&key, &value); err != nil {
                log.Println("lookup:", err)
                continue
            }
            fmt.Printf("packets=%d\n", value)
        }
    }
}

Field names (XdpCount, PacketCount) must match bpf2go output—rename to match generated code.

Run and verification

ip -br link
go generate ./... && go build -o xdp-count .
sudo ./xdp-count -iface lo
# other terminal:
ping -c 20 127.0.0.1
# counter should rise

Safety

  • Prefer lab VMs and lo or a throwaway veth pair.
  • Always return XDP_PASS until you know what you are dropping.
  • Remote servers: a bad XDP program can cut SSH—use out-of-band console.

Stretch goals

  1. Per-protocol counters (parse Ethernet/IP safely with helpers).
  2. Bytes counter as well as packets.
  3. bpftool prog show / ip link show verification notes.
  4. Pin maps under /sys/fs/bpf for multi-process readers.

Pitfalls

Pitfall Fix
Hard-coded ifindex resolve by name
XDP_DROP by mistake start with PASS
No memlock rlimit rlimit.RemoveMemlock
Generated names mismatch read *_bpfel.go

Learning goals

  • XDP attach path vs tracepoints
  • Array maps for counters
  • Operational safety for datapath programs