Skip to content
Menu

Networking8 min read

eBPF Fundamentals

Sandboxed programs in the kernel — the programmable data plane

What Is eBPF?

eBPF (extended Berkeley Packet Filter) allows you to run sandboxed, event-driven programs inside the Linux kernel without modifying kernel source code or loading kernel modules. Think of it as a safe, programmable virtual machine embedded in the kernel that lets you hook into virtually any kernel event — networking, tracing, security — and run custom logic at native speed.

Why This Matters for Infrastructure

  • No kernel recompilation — deploy new kernel-level logic without rebooting or rebuilding the kernel
  • No kernel modules — no risk of crashing the kernel with buggy .ko modules
  • Safety guaranteed — the kernel verifier statically analyzes every eBPF program before loading
  • Near-native performance — JIT-compiled to native machine code, not interpreted
  • Dynamic — load/unload programs at runtime, no downtime

Note

In practice: eBPF is the technology behind Cilium (K8s CNI), Falco (runtime security), bpftrace (dynamic tracing), Katran (Facebook's L4 load balancer), and Cloudflare's DDoS mitigation. If you're running a modern Linux infrastructure, you're almost certainly running eBPF programs already.

History: BPF to eBPF

  1. BPF (1992)

    Berkeley Packet Filter — tcpdump's engine for filtering packets in the kernel

  2. cBPF in Linux

    "Classic" BPF — limited instruction set, packet filtering only

  3. eBPF (2014+)

    Extended BPF — general-purpose kernel programmability, 64-bit registers, maps, helpers

  4. Modern eBPF

    CO-RE, BTF, bounded loops, BPF LSM, signed programs — production-grade

Feature Classic BPF (cBPF) Extended BPF (eBPF)
Registers 2 (32-bit) 11 (64-bit, R0-R10)
Instruction set ~30 instructions ~100+ instructions
Use case Packet filtering only Networking, tracing, security, profiling
Data sharing None eBPF maps (kernel <-> userspace)
JIT compilation Optional, limited Default on modern kernels
Helper functions None Extensive kernel-provided API

The Verifier: The Safety Guarantee

The eBPF verifier is a static analysis engine inside the kernel that inspects every eBPF program before it is loaded. If the verifier rejects a program, it never runs. This is the fundamental reason eBPF is safe to run in production kernels — it is mathematically impossible for a verified program to crash the kernel.

  1. Write eBPF program

    C or Rust, compiled to eBPF bytecode

  2. bpf() syscall

    Userspace requests program load

  3. Verifier

    Static analysis — reject or accept

  4. JIT Compile

    Bytecode -> native machine code

  5. Attach to hook

    XDP, tc, kprobe, tracepoint...

What the Verifier Checks

Safety Invariants

  • No infinite loops — only bounded loops allowed (verifier must prove termination). Before kernel 5.3, loops were not allowed at all.
  • No null pointer dereferences — every pointer must be checked before use
  • Stack size limited to 512 bytes — prevents stack overflow in kernel context
  • Memory access bounds checking — no out-of-bounds reads/writes
  • All code paths must terminate — DAG analysis, no unreachable code
  • Only approved helper functions — cannot call arbitrary kernel functions

How It Works

  • Walks all possible execution paths (DAG traversal)
  • Tracks register types and value ranges at each instruction
  • Ensures map accesses use correct key/value sizes
  • Verifies pointer arithmetic stays within bounds
  • Checks that bpf_probe_read() is used for kernel memory access
  • Complexity limit: max ~1M verified instructions (prevents verifier DoS)

Warning

Verifier complexity: The verifier is the biggest pain point in eBPF development. Programs that are logically correct may be rejected because the verifier can't prove they're safe. This is why toolchains like libbpf + CO-RE and higher-level frameworks like Cilium/bpftrace exist — they generate verifier-friendly code.

JIT Compilation

After verification, eBPF bytecode is JIT-compiled to native machine instructions (x86_64, ARM64, etc.). This means eBPF programs run at near-native speed — there is no interpreter overhead in the hot path.

Without JIT (Interpreter)
  • Each eBPF instruction decoded at runtime
  • Switch-case dispatch per opcode
  • Significant overhead per packet/event
  • Still safe, just slower
With JIT (Default)
  • eBPF bytecode translated to native instructions once
  • Direct CPU execution, no dispatch overhead
  • Performance within 5-10% of hand-written kernel C
  • Enabled by default: net.core.bpf_jit_enable=1
console
console

    $ sysctl net.core.bpf_jit_enable
    net.core.bpf_jit_enable = 1
    

    $ # Verify JIT is active for loaded programs:
    $ bpftool prog list
    6: xdp  name xdp_drop  tag abc123  gpl  jited 96B  loaded_at 2024-01-15
  

Hook Points: Where eBPF Programs Attach

eBPF programs are event-driven — they execute when a specific kernel event fires. The "hook point" determines when and where your code runs. This is what makes eBPF so versatile: the same technology powers networking, observability, and security.

Packet Path Through the Kernel (with eBPF Hook Points)

  1. NIC (Hardware)

    Packet arrives

  2. XDP

    Earliest hook. Before sk_buff allocation. Can DROP/PASS/TX/REDIRECT

  3. tc ingress

    Traffic control layer. Has full sk_buff metadata. Can classify/redirect

  4. Netfilter / iptables

    Traditional firewall chains (PREROUTING, INPUT, FORWARD...)

  5. Socket Layer

    cgroup/socket eBPF hooks

  6. Application

    Userspace receives data

Hook Point When It Fires Use Cases Performance
XDP Packet arrives at NIC driver (pre-sk_buff) DDoS mitigation, load balancing, packet filtering Fastest — millions of pps
tc (traffic control) Ingress/egress after initial processing Packet mangling, policy enforcement, redirects Very fast, full sk_buff access
cgroup Socket operations for a cgroup Per-container networking policy, connect/bind interception Per-socket granularity
socket_filter Per-socket packet filtering Application-level packet inspection Socket-scoped
kprobes Any kernel function entry/return (dynamic) Debugging, tracing, profiling kernel internals Overhead per invocation
tracepoints Static kernel instrumentation points Stable tracing interface (survives kernel upgrades) Lower overhead than kprobes
fentry/fexit Function entry/exit (modern, BTF-based) Safer, faster alternative to kprobes (5.5+) Near-zero overhead
LSM Linux Security Module hooks Security policy enforcement (file access, process creation) Security-critical path

Tip

Key insight: The earlier in the packet path you attach, the faster you can act — but the less metadata you have. XDP sees raw packets (no sk_buff), so it's blazing fast but has limited context. tc sees full sk_buff metadata. Choose the right hook for your use case.

eBPF Maps: Shared Data Structures

eBPF maps are kernel-resident data structures that serve as the primary communication channel between eBPF programs (kernel space) and userspace applications. Maps persist across program invocations — they're how you maintain state, share configuration, and export telemetry.

Map Architecture

Userspace App
bpftool / libbpf / Go / Rust
eBPF Map
Kernel memory
eBPF Program
Runs on kernel events
Map Type Structure Lookup Use Cases
BPF_MAP_TYPE_HASH Key-value hash table O(1) Connection tracking, rate limiting, blocklists
BPF_MAP_TYPE_ARRAY Fixed-size indexed array O(1) Configuration, counters, per-CPU stats
BPF_MAP_TYPE_RINGBUF Lock-free ring buffer FIFO Stream events to userspace efficiently (preferred over perf_event)
BPF_MAP_TYPE_LRU_HASH Hash table with LRU eviction O(1) Bounded-size caches, conntrack tables with auto-cleanup
BPF_MAP_TYPE_PERCPU_HASH Per-CPU hash (no locks) O(1) High-throughput counters, stats without lock contention
BPF_MAP_TYPE_PERCPU_ARRAY Per-CPU array (no locks) O(1) Per-CPU scratch space, histogram buckets
BPF_MAP_TYPE_LPM_TRIE Longest prefix match trie O(log n) IP prefix/CIDR matching, routing tables
BPF_MAP_TYPE_PROG_ARRAY Array of eBPF program fds O(1) Tail calls — chain eBPF programs together

Note

Per-CPU maps are critical for performance: In a networking hot path processing millions of packets per second, lock contention on shared maps would destroy throughput. Per-CPU variants give each core its own copy — userspace aggregates when reading.

Map Lifecycle

  1. Createbpf(BPF_MAP_CREATE, ...) at program load time (usually via libbpf)
  2. Pin — optionally pin to /sys/fs/bpf/ so maps persist beyond process lifetime
  3. Use — eBPF programs call bpf_map_lookup_elem(), bpf_map_update_elem(), bpf_map_delete_elem()
  4. Read from userspace — control plane reads/writes maps via bpf() syscall or bpftool map dump
  5. Cleanup — maps are reference-counted; destroyed when no programs or file descriptors reference them

Helper Functions

eBPF programs cannot call arbitrary kernel functions. Instead, the kernel exposes a curated set of helper functions that eBPF programs can invoke. The verifier ensures only valid helpers are called for the given program type.

Map Operations

c
// Lookup a value by key
void *bpf_map_lookup_elem(&map, &key);

// Insert or update
int bpf_map_update_elem(&map, &key, &val, flags);

// Delete an entry
int bpf_map_delete_elem(&map, &key);

Packet Operations

c
// Redirect packet to another interface
int bpf_redirect(ifindex, flags);

// Modify packet bytes
int bpf_skb_store_bytes(skb, off, &data, len, flags);

// Adjust packet headroom
int bpf_xdp_adjust_head(xdp_md, delta);

Tracing & Context

c
// Get current PID and TID
u64 bpf_get_current_pid_tgid();

// Safely read kernel memory
int bpf_probe_read(&dst, size, &src);

// Get current timestamp (ns)
u64 bpf_ktime_get_ns();

Output & Debug

c
// Send event to ring buffer
int bpf_ringbuf_output(&ringbuf, &data, size, flags);

// Print to trace pipe (debug only)
int bpf_trace_printk(fmt, fmt_size, ...);

// Get current cgroup id
u64 bpf_get_current_cgroup_id();

eBPF Program Types

Each eBPF program has a type that determines which hook points it can attach to, which helpers it can call, and what context (input data) it receives. The verifier enforces these constraints.

Program Type Context Struct Attach Point Primary Use
BPF_PROG_TYPE_XDP xdp_md NIC driver (pre-sk_buff) Packet filtering, DDoS, load balancing
BPF_PROG_TYPE_SCHED_CLS __sk_buff tc ingress/egress Traffic shaping, policy, packet mangling
BPF_PROG_TYPE_CGROUP_SKB __sk_buff cgroup ingress/egress Per-container network policy
BPF_PROG_TYPE_SOCKET_FILTER __sk_buff Socket Per-socket packet filtering
BPF_PROG_TYPE_KPROBE pt_regs Any kernel function Dynamic tracing, debugging
BPF_PROG_TYPE_TRACEPOINT Varies per tracepoint Static kernel tracepoints Stable tracing interface
BPF_PROG_TYPE_SOCK_OPS bpf_sock_ops Socket operations TCP tuning, socket-level decisions
BPF_PROG_TYPE_LSM Varies per hook LSM security hooks Security policy enforcement

eBPF Architecture Summary

The Complete Picture

Userspace (Control Plane)
libbpf / cilium-agent / bpftool
Loads programs, reads/writes maps
Configures policy and routes
bpf() syscall
Kernel (Data Plane)
Verifier -> JIT -> Hook
eBPF programs execute on events
Maps store shared state

Tip

The split architecture pattern: In production eBPF systems (Cilium, Falco, Katran), you always have a userspace control plane (Go, Rust, C++) that manages eBPF programs and maps, plus kernel data plane eBPF programs that do the fast-path work. The control plane configures policy via maps; the data plane enforces it at line rate.

eBPF Development Ecosystem

Tool / Library Language Description
libbpf C Reference library for loading eBPF programs. CO-RE (Compile Once, Run Everywhere)
cilium/ebpf Go Pure-Go library for eBPF. Used by Cilium, Hubble, Tetragon
aya Rust Write eBPF programs and userspace in Rust. No C dependency
bpftrace DSL High-level tracing language (like awk for eBPF). One-liners for debugging
bpftool CLI Inspect loaded eBPF programs, maps, links. Essential debugging tool
bcc Python/C BPF Compiler Collection. Older toolchain, being superseded by libbpf + CO-RE
console
console

    $ # List all loaded eBPF programs
    $ bpftool prog list
    6: xdp  name xdp_firewall  tag a1b2c3d4  gpl  jited 208B
    12: sched_cls  name tc_policy  tag e5f6a7b8  gpl  jited 384B
    

    $ # Dump a map's contents
    $ bpftool map dump id 4
    key: 0a 00 01 01  value: 00 00 00 42
    key: 0a 00 01 02  value: 00 00 01 a3
    

    $ # One-liner: count syscalls by process
    $ bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'
    Attaching 1 probe...
    @[kubelet]: 4521
    @[containerd]: 3102
    @[cilium-agent]: 1847
  
Solidnines — solidnines.com