eBPF lets you run sandboxed programs in the Linux kernel without changing kernel source code or loading kernel modules. The most surprising thing about eBPF is that it’s not just for network packet filtering; it’s fundamentally a general-purpose, event-driven computing platform inside the kernel.

Let’s see eBPF in action for network traffic analysis. Imagine you want to see how much traffic is going to a specific service, say a web server running on port 8080.

sudo bpftool prog load bpf_tc_stats.o /sys/fs/bpf/tc_stats
sudo bpftool map create /sys/fs/bpf/tc_stats type hash key 4 value 8 entries 1024
sudo bpftool prog attach pinned /sys/fs/bpf/tc_stats_prog \
    target eth0 egress \
    type tc

This bpftool command sequence loads an eBPF program designed to count bytes and packets per destination IP address, attaches it to the eth0 network interface for egress traffic, and creates a map to store the statistics. The eBPF program itself, written in C and compiled to BPF bytecode, would look something like this:

#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 1024);
    __uint(key_size, sizeof(uint32_t)); // Destination IP
    __uint(value_size, sizeof(uint64_t)); // Byte count
} stats_map SEC(".maps");

SEC("classifier")
int tc_stats_func(struct __sk_buff *skb) {
    void *data_end = (void *)skb->data_end;
    void *data = (void *)skb->data;
    struct ethhdr *eth = data;
    struct iphdr *iph;
    uint32_t daddr;
    uint64_t *count;

    if (data + sizeof(*eth) > data_end)
        return TC_ACT_OK;
    eth = data;

    if (ntohs(eth->h_proto) != ETH_P_IP)
        return TC_ACT_OK;

    iph = data + sizeof(*eth);
    if ((void *)iph + sizeof(*iph) > data_end)
        return TC_ACT_OK;

    daddr = iph->daddr;
    count = bpf_map_lookup_elem(&stats_map, &daddr);
    if (count) {
        *count += skb->len;
    } else {
        uint64_t initial_count = skb->len;
        bpf_map_update_elem(&stats_map, &daddr, &initial_count, BPF_ANY);
    }

    return TC_ACT_OK;
}

char _license[] SEC("license") = "GPL";

This program intercepts network packets. It checks if the packet is an IP packet, extracts the destination IP address, and then uses a BPF map to increment a counter associated with that IP. This happens entirely within the kernel, with minimal overhead. To view the stats, you’d use bpftool again:

sudo bpftool map dump pinned /sys/fs/bpf/tc_stats

This would show output like:

[1]    192.168.1.100: 12345678
[2]    192.168.1.101: 98765432

This demonstrates how eBPF can provide real-time, granular visibility into network traffic without requiring userspace agents or modifying application code.

The core problem eBPF solves is the need for safe, efficient, and dynamic extensibility of the Linux kernel. Traditionally, adding new kernel functionality meant writing a kernel module, which is risky (a bug can crash the entire system), inflexible (requires recompilation and reloading), and often requires significant kernel internals knowledge. eBPF bypasses these issues by allowing users to load small, verifiable programs that execute in response to specific kernel events.

The eBPF runtime in the kernel performs extensive safety checks before allowing a program to run. It verifies that the program will terminate, won’t access invalid memory, and adheres to resource limits. Once verified, the program is attached to a specific "hook" point within the kernel – this could be a network ingress/egress point, a syscall entry, a tracepoint, or a kprobe. When an event occurs at that hook, the eBPF program is executed.

The eBPF program can then interact with BPF maps, which are key-value stores managed by the kernel. These maps are the primary mechanism for eBPF programs to share data with userspace applications or with other eBPF programs. They can store counters, histograms, IP addresses, or even complex data structures. The program can read from and write to these maps, allowing for sophisticated state management and data collection.

The "magic" of eBPF lies in its ability to weave custom logic into the kernel’s execution flow without compromising stability or requiring deep kernel development expertise. You can, for instance, create a program that samples network packets, extracts specific fields, and stores them in a map. A userspace application can then periodically read from this map to reconstruct traffic flows, detect anomalies, or generate metrics. This is far more efficient than having the kernel send every packet to userspace.

One crucial aspect of eBPF maps that many overlook is their ability to be shared between multiple eBPF programs and even between eBPF and userspace. This isn’t just for simple data sharing; it enables complex coordination. For example, one eBPF program could write flow information into a map, and another eBPF program could read that information to enforce rate limits or trigger alerts, all without any userspace intervention once the programs are loaded. The kernel’s BPF verifier ensures that accesses to these shared maps are safe, preventing race conditions and memory corruption.

The next frontier for eBPF users is likely exploring its capabilities in security policy enforcement and fine-grained system auditing.

Want structured learning?

Take the full Ebpf course →