eBPF programs aren’t just small snippets of code; they’re actual, compiled programs that run directly within the Linux kernel without ever needing to modify its source code.
Let’s see eBPF in action, tracing network packets. Imagine we want to see every TCP SYN packet hitting our server.
# Load the eBPF program
bpftool prog load <program_file.o> /sys/fs/bpf/my_tcp_syn_tracer type tracepoint
# Create a map to store counts
bpftool map create /sys/fs/bpf/my_syn_counts type hash key_size 4 value_size 4 entries 1024
# Attach the program to the appropriate tracepoint
bpftool prog attach id <program_id> tracepoint/tcp_v4_connect
# Now, let's see the data
bpftool map dump id <map_id>
This is where the magic happens. When a TCP connection attempt (specifically, the tcp_v4_connect tracepoint) is hit, our eBPF program runs. It checks if the packet is a SYN flag. If it is, it increments a counter associated with the source IP address in a special data structure called an eBPF map. The bpftool map dump command then lets us peek at those counts in real-time.
At its core, eBPF solves the problem of extending kernel functionality with safety and efficiency. Traditionally, adding new kernel features meant recompiling the entire kernel or loading kernel modules, both of which are risky. eBPF allows you to load small, verified programs that attach to specific points within the kernel (like system calls, tracepoints, or network events) and execute custom logic.
The key components are:
- eBPF Programs: These are the actual logic you want to run. They’re written in a restricted C and compiled into eBPF bytecode. The restrictions are crucial for safety, preventing arbitrary memory access or infinite loops.
- eBPF Maps: These are shared data structures that programs can read from and write to. They can be hash tables, arrays, LRU deques, etc. They act as the communication channel between eBPF programs and userspace, or even between different eBPF programs.
- Helper Functions: eBPF programs can’t do everything themselves. They call predefined helper functions provided by the kernel (e.g.,
bpf_ktime_get_ns()for current time,bpf_perf_event_output()to send data to userspace). - Attachment Points: Programs are attached to specific hooks in the kernel. These can be tracepoints (predefined kernel event markers), kprobes (dynamic kernel function entry/exit), network interfaces, or security hooks.
The kernel verifier is the unsung hero. Before any eBPF program can be loaded, it undergoes rigorous static analysis. The verifier checks for:
- Bounded execution: Ensures the program will always terminate (no infinite loops).
- Valid memory access: Guarantees programs only access memory they’re allowed to, preventing kernel panics.
- Correct helper function usage: Verifies that helper functions are called with the right arguments and that any data passed to them is valid.
- Map access: Ensures programs interact with maps correctly, without out-of-bounds access.
If the verifier finds any potential issue, it rejects the program. This safety net is what allows eBPF to be dynamically loaded and unloaded without compromising kernel stability.
The real power of eBPF lies in its ability to dynamically attach to kernel events and inspect/modify kernel behavior without recompilation. For instance, you can write an eBPF program that intercepts every sendmsg system call, inspects the data being sent, and based on its content, decides whether to allow or block the operation—all before the kernel’s standard network stack even processes it. This allows for incredibly fine-grained observability and security policies.
When you think about tracing, it’s not just about seeing what happened, but why. eBPF programs can collect contextual data from multiple kernel subsystems simultaneously, stitch it together, and present it in a correlated way. For example, you could trace a network request, correlate it with the specific process making the request, and even log the relevant system calls made by that process during the transaction.
The most surprising thing about eBPF programs is that they often don’t need a complex userspace daemon to function. Many eBPF applications can be entirely self-contained. A program attached to a network interface can, using an eBPF map, directly update counters that are then exposed via a sysfs or procfs entry, or even trigger actions through other kernel mechanisms, eliminating the need for constant polling from userspace.
The next logical step after understanding how to load and attach eBPF programs is exploring how to use eBPF for more advanced networking tasks, like implementing custom load balancing or network policy enforcement directly in the kernel.