eBPF programs can attach to many different points in the kernel, each with unique characteristics and use cases.
Let’s see eBPF in action with XDP, a high-performance packet processing hook. Imagine you have a network interface eth0 and you want to drop all UDP packets destined for port 53.
# Compile a simple XDP program
clang -O2 -target bpf -c xdp_drop_udp53.c -o xdp_drop_udp53.o
# Load the XDP program onto eth0
ip link set dev eth0 xdp obj xdp_drop_udp53.o sec xdp
# Verify it's loaded
ip link show dev eth0
In xdp_drop_udp53.c (not shown here for brevity, but it would contain logic to inspect the packet and return XDP_DROP for UDP port 53), the xdp section is crucial. This tells the kernel loader where the eBPF program’s entry point is within the compiled object file. The ip link set dev eth0 xdp obj ... sec xdp command is the magic that attaches this compiled eBPF bytecode to the xdp hook on eth0. Now, before the kernel even considers routing the packet, your eBPF program gets a shot at it.
The core problem eBPF solves, especially with program types like XDP, is enabling user-defined logic to run at extremely low levels of the networking stack or kernel execution without modifying kernel source code or loading kernel modules. This drastically reduces latency and increases throughput by allowing decisions to be made much earlier in the processing pipeline.
Internally, when a packet arrives at eth0, the kernel invokes the eBPF verifier to ensure your program is safe (won’t crash the kernel, infinite loops, etc.). Once verified, the eBPF JIT compiler translates the bytecode into native machine code for your CPU. This native code is then executed for every packet that reaches the XDP hook. The return value from your eBPF program dictates what happens next: XDP_PASS lets the packet continue normal processing, XDP_DROP discards it, XDP_TX sends it back out the same interface, and XDP_REDIRECT can send it to another interface or even another CPU.
Beyond XDP, other program types offer different capabilities. TC (Traffic Control) hooks into the kernel’s queuing discipline, allowing for more complex packet manipulation after initial routing decisions have been made, but still before the packet leaves the kernel. Tracepoint and Kprobe attach to specific kernel functions, enabling deep introspection and debugging of kernel behavior. Socket programs can attach to specific sockets to filter or modify data before it’s sent or after it’s received.
The power of eBPF lies in its versatility and safety. You can write a program to drop packets, redirect them, modify headers, collect statistics, or even trace specific kernel events, all without recompiling the kernel or risking system instability. The verifier is your guardian, ensuring that whatever you attach will behave predictably.
When you attach an eBPF program to a tracepoint, like sys_enter_open, the eBPF program receives a pt_regs struct. This struct is a snapshot of the CPU’s registers at the moment the tracepoint was hit, including arguments passed to the open system call. However, not all arguments are directly accessible or immediately useful. For instance, string arguments like the filename passed to open are often pointers within the user-space memory of the calling process. To read these strings safely, you must use helper functions provided by the eBPF runtime, such as bpf_probe_read_user_str(). This function handles the complexity of safely accessing user memory, checking for valid pointers and buffer sizes, and preventing your eBPF program from crashing the kernel if the user-space pointer is invalid or the memory region is not accessible.
The next frontier is often exploring how to make these eBPF programs dynamic, updating their logic or configuration on the fly without needing to reload the entire program.