In this guide, we'll walk you through building a new eBPF-powered Go application
from scratch. We'll introduce the toolchain, write a minimal eBPF C example and
compile it using bpf2go. Then, we'll put together a Go
application that loads the eBPF program into the kernel and periodically
displays its output.
The application attaches an eBPF program to an XDP hook that counts the number
of packets received by a physical interface. Filtering and modifying packets is
a major use case for eBPF, so you'll see a lot of its features being geared
towards it. However, eBPF's capabilities are ever-growing, and it has been
adopted for tracing, systems and application observability, security and much
more.
//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_countSEC(".maps");// count_packets atomically increases a packet counter on every invocation.SEC("xdp")intcount_packets(){__u32key=0;__u64*count=bpf_map_lookup_elem(&pkt_count,&key);if(count){__sync_fetch_and_add(count,1);}returnXDP_PASS;}char__license[]SEC("license")="Dual MIT/GPL";
Create an empty directory and save this file as counter.c. In the next step,
we'll set up the necessary bits to compile our eBPF C program using bpf2go.
Compile eBPF C and generate scaffolding using bpf2go¶
With the counter.c source file in place, create another file called gen.go
containing a //go:generate statement. This invokes bpf2go when running go
generate in the project directory.
Aside from compiling our eBPF C program, bpf2go will also generate some
scaffolding code we'll use to load our eBPF program into the kernel and interact
with its various components. This greatly reduces the amount of code we need to
write to get up and running.
packagemain//go:generate go tool bpf2go -tags linux counter counter.c
Using a dedicated file for your package's //go:generate statement(s) is
neat for keeping them separated from application logic. At this point in the
guide, we don't have a main.go file yet. Feel free to include it in
existing Go source files if you prefer.
Before using the Go toolchain, Go wants us to declare a Go module. This command
should take care of that:
% gomodinitebpf-test
go: creating new go.mod: module ebpf-testgo: to add module requirements and sums: go mod tidy% gomodtidy
First, add bpf2go as a tool dependency to your Go module. This ensures the
version of bpf2go used by the Go toolchain always matches your version of the
library.
bpf2go built counter.c into counter_bpf*.o behind the scenes using
clang. It generated two object files and two corresponding Go source files
based on the contents of the object files. Do not remove any of these, we'll
need them later.
Neat! Looks like bpf2go automatically generated a scaffolding for interacting
with our count_packets Program from Go. In the next step, we'll explore how to
load our program into the kernel and put it to work by attaching it to an XDP
hook!
Finally, with our eBPF C code compiled and Go scaffolding generated, all that's
left is writing the Go code responsible for loading and attaching the program to
a hook in the Linux kernel.
Click the annotations in the code snippet for some of the
more intricate details. Note that we won't cover anything related to the Go
standard library here.
packagemainimport("log""net""os""os/signal""time""github.com/cilium/ebpf/link""github.com/cilium/ebpf/rlimit")funcmain(){// Remove resource limits for kernels <5.11.iferr:=rlimit.RemoveMemlock();err!=nil{log.Fatal("Removing memlock:",err)}// Load the compiled eBPF ELF and load it into the kernel.varobjscounterObjectsiferr:=loadCounterObjects(&objs,nil);err!=nil{log.Fatal("Loading eBPF objects:",err)}deferobjs.Close()ifname:="eth0"// Change this to an interface on your machine.iface,err:=net.InterfaceByName(ifname)iferr!=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,})iferr!=nil{log.Fatal("Attaching XDP:",err)}deferlink.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(chanos.Signal,5)signal.Notify(stop,os.Interrupt)for{select{case<-tick:varcountuint64err:=objs.PktCount.Lookup(uint32(0),&count)iferr!=nil{log.Fatal("Map lookup:",err)}log.Printf("Received %d packets",count)case<-stop:log.Print("Received signal, exiting..")return}}}
Save this file as main.go in the same directory alongside counter.c and
gen.go.
Now main.go is in place, we can finally compile and run our Go application!
% gobuild&&sudo./ebpf-test
2023/09/20 17:18:43 Counting incoming packets on eth0..2023/09/20 17:18:47 Received 0 packets2023/09/20 17:18:48 Received 4 packets2023/09/20 17:18:49 Received 11 packets2023/09/20 17:18:50 Received 15 packets
Generate some traffic on eth0 and you should see the counter increase.
When iterating on the C code, make sure to keep generated files up-to-date.
Without re-running bpf2go, the eBPF C won't be recompiled, and any changes made
to the C program structure won't be reflected in the Go scaffolding.
Congratulations, you've just built your (presumably) first eBPF-powered Go app!
Hopefully, this guide piqued your interest and gave you a better sense of what
eBPF can do and how it works.
With XDP, we've only barely scratched the surface of eBPF's many use cases and
applications. For more easily-accessible examples, see the main repository's
examples/ folder. It
demonstrates use cases like tracing user space applications, extracting
information from the kernel, attaching eBPF programs to network sockets and
more.
Follow our other guides to continue on your journey of shipping a portable
eBPF-powered application to your users.
Use clang --version to check which version of LLVM you have installed.
Refer to your distribution's package index to finding the right packages to
install, as this tends to vary wildly across distributions. Some
distributions ship clang and llvm-strip in separate packages. ↩
For Debian/Ubuntu, you'll typically need libbpf-dev. On Fedora, it's
libbpf-devel. ↩
On AMD64 Debian/Ubuntu, install linux-headers-amd64. On Fedora, install
kernel-devel.
On Debian, you may also need ln -sf /usr/include/asm-generic/
/usr/include/asm since the example expects to find <asm/types.h>. ↩
Last updated 4 months ago2025-10-13
Authored by Timo Beckers