← Back to DevBytes

eBPF for Networking: Complete Setup and Configuration Guide

Introduction to eBPF for Networking

eBPF (Extended Berkeley Packet Filter) is a revolutionary technology that allows developers to run sandboxed programs directly inside the Linux kernel without modifying kernel source code or loading custom kernel modules. Originally designed for packet filtering, eBPF has evolved into a powerful general-purpose kernel programmability framework. For networking specifically, eBPF enables high-performance packet processing, traffic observation, load balancing, and security enforcement directly at the kernel level.

At its core, eBPF works by letting you write programs in a restricted C-like language, compile them to BPF bytecode, and have the kernel's JIT compiler translate them into native machine code. The kernel verifier ensures these programs are safe to run, cannot crash the system, and always terminate in bounded time. This combination of safety and performance makes eBPF ideal for networking tasks that require both speed and flexibility.

Why eBPF Matters for Networking

Traditional networking approaches in Linux rely on the kernel's built-in networking stack, iptables rules, or custom kernel modules. Each of these has limitations. The built-in stack is rigid, iptables can become slow with large rule sets, and kernel modules are risky to develop and deploy. eBPF solves these problems by offering a programmable, safe, and fast alternative.

Key Benefits

Prerequisites and Environment Setup

Before writing eBPF networking programs, you need to set up your development environment. This section walks through installing the necessary tools and dependencies on a Linux system.

System Requirements

You need a Linux kernel version 4.18 or newer for good eBPF networking support, though 5.4+ is recommended. You also need root privileges to load eBPF programs. The following setup assumes Ubuntu 22.04 or similar Debian-based distributions.

Installing Dependencies

# Update package lists
sudo apt update

# Install build tools and kernel headers
sudo apt install -y build-essential clang llvm libelf-dev \
    linux-headers-$(uname -r) libbpf-dev bpftool \
    git curl pkg-config

# Verify clang and llvm are installed
clang --version
llc --version

# Verify bpftool is available
bpftool version

Verifying eBPF Support

After installing the dependencies, verify that your kernel supports the eBPF features you need. The following command checks for BPF program type support:

# Check kernel BPF support
cat /proc/sys/kernel/unprivileged_bpf_disabled

# List supported BPF program types
bpftool feature probe | grep program_type

# Check if JIT compilation is enabled
cat /proc/sys/net/core/bpf_jit_enable

If bpf_jit_enable is set to 0, enable it for better performance:

sudo sysctl -w net.core.bpf_jit_enable=1

Writing Your First eBPF Network Program

Now let's write a practical eBPF program that counts network packets on a specific interface. This example uses the XDP (eXpress Data Path) hook, which processes packets at the earliest possible point in the receive path, before they enter the kernel's networking stack.

The eBPF Program (Kernel Side)

Create a file called packet_counter.c with the following content:

// packet_counter.c
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/in.h>

// Define a BPF map to store packet counts
struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __uint(max_entries, 256);
    __type(key, __u32);
    __type(value, __u64);
} packet_count_map SEC(".maps");

SEC("xdp")
int count_packets(struct xdp_md *ctx) {
    // Get pointers to the start and end of the packet data
    void *data_end = (void *)(long)ctx->data_end;
    void *data = (void *)(long)ctx->data;

    // Parse the Ethernet header
    struct ethhdr *eth = data;
    if ((void *)(eth + 1) > data_end) {
        return XDP_PASS;
    }

    // Check if this is an IP packet (Ethernet protocol 0x0800)
    if (eth->h_proto != __builtin_bswap16(ETH_P_IP)) {
        return XDP_PASS;
    }

    // Parse the IP header
    struct iphdr *iph = (void *)(eth + 1);
    if ((void *)(iph + 1) > data_end) {
        return XDP_PASS;
    }

    // Use the IP protocol number as the key (TCP=6, UDP=17, etc.)
    __u32 key = iph->protocol;
    __u64 init_val = 1;

    // Look up the current count for this protocol
    __u64 *count = bpf_map_lookup_elem(&packet_count_map, &key);
    if (count) {
        // Atomically increment the existing count
        __sync_fetch_and_add(count, 1);
    } else {
        // Initialize the count for a new protocol
        bpf_map_update_elem(&packet_count_map, &key, &init_val, BPF_ANY);
    }

    // Pass the packet through to the normal networking stack
    return XDP_PASS;
}

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

This program does several things. First, it defines a BPF map of type BPF_MAP_TYPE_ARRAY to store packet counts keyed by IP protocol number. The SEC("xdp") annotation tells the compiler that this function should be attached to the XDP hook. Inside the function, we parse the Ethernet and IP headers, extract the protocol number, and update the count in the map. The XDP_PASS return value lets the packet continue through the normal kernel networking stack.

Compiling the eBPF Program

Compile the program using clang with the BPF target:

# Compile the eBPF program to BPF bytecode
clang -O2 -g -target bpf -D__TARGET_ARCH_x86 \
    -c packet_counter.c -o packet_counter.o

# Verify the compiled object file
llvm-objdump -S packet_counter.o | head -50

Loading and Attaching eBPF Programs

Now that we have compiled the eBPF program, we need a user-space program to load it into the kernel and attach it to a network interface. We will use the libbpf library for this task.

The User-Space Loader

Create a file called loader.c:

// loader.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <net/if.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>
#include <signal.h>

static volatile int running = 1;

void handle_signal(int sig) {
    running = 0;
}

int main(int argc, char **argv) {
    if (argc != 3) {
        fprintf(stderr, "Usage: %s <bpf_object> <interface>\n", argv[0]);
        return 1;
    }

    const char *obj_path = argv[1];
    const char *ifname = argv[2];

    // Get the interface index
    int ifindex = if_nametoindex(ifname);
    if (ifindex == 0) {
        fprintf(stderr, "Failed to get interface index for %s: %s\n",
                ifname, strerror(errno));
        return 1;
    }

    // Open the BPF object file
    struct bpf_object *obj = bpf_object__open_file(obj_path, NULL);
    if (libbpf_get_error(obj)) {
        fprintf(stderr, "Failed to open BPF object: %s\n",
                strerror(errno));
        return 1;
    }

    // Load the BPF program into the kernel
    if (bpf_object__load(obj)) {
        fprintf(stderr, "Failed to load BPF object: %s\n",
                strerror(errno));
        bpf_object__close(obj);
        return 1;
    }

    // Find the XDP program by its section name
    struct bpf_program *prog = bpf_object__find_program_by_name(obj, "count_packets");
    if (!prog) {
        fprintf(stderr, "Failed to find BPF program\n");
        bpf_object__close(obj);
        return 1;
    }

    int prog_fd = bpf_program__fd(prog);

    // Attach the program to the network interface using XDP
    int err = bpf_xdp_attach(ifindex, prog_fd, XDP_FLAGS_DRV_MODE, NULL);
    if (err) {
        fprintf(stderr, "Failed to attach XDP program (trying SKB mode)\n");
        err = bpf_xdp_attach(ifindex, prog_fd, XDP_FLAGS_SKB_MODE, NULL);
        if (err) {
            fprintf(stderr, "Failed to attach XDP program: %s\n",
                    strerror(errno));
            bpf_object__close(obj);
            return 1;
        }
    }

    printf("eBPF program attached to %s. Press Ctrl+C to detach.\n", ifname);

    // Set up signal handler for clean exit
    signal(SIGINT, handle_signal);
    signal(SIGTERM, handle_signal);

    // Wait for termination signal
    while (running) {
        sleep(1;
    }

    // Detach the program and clean up
    printf("\nDetaching eBPF program...\n");
    bpf_xdp_detach(ifindex, XDP_FLAGS_DRV_MODE, NULL);
    bpf_xdp_detach(ifindex, XDP_FLAGS_SKB_MODE, NULL);
    bpf_object__close(obj);

    printf("Done.\n");
    return 0;
}

Compiling the Loader

# Compile the user-space loader
gcc -O2 -g loader.c -o loader -lbpf -lelf -lz

# Run the loader with the eBPF object and a network interface
sudo ./loader packet_counter.o eth0

Reading Data from User Space

Once the eBPF program is running and counting packets, you need a way to read the data from the BPF map. Let's create a separate program that periodically reads and displays the packet counts.

The Map Reader

Create a file called read_counts.c:

// read_counts.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <bpf/bpf.h>
#include <bpf/libbpf.h>

// Protocol name lookup
const char *proto_name(__u32 proto) {
    switch (proto) {
        case 1:  return "ICMP";
        case 6:  return "TCP";
        case 17: return "UDP";
        case 47: return "GRE";
        case 89: return "OSPF";
        default: return "OTHER";
    }
}

int main(int argc, char **argv) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <bpf_object>\n", argv[0]);
        return 1;
    }

    const char *obj_path = argv[1];

    // Open the BPF object to access its maps
    struct bpf_object *obj = bpf_object__open_file(obj_path, NULL);
    if (libbpf_get_error(obj)) {
        fprintf(stderr, "Failed to open BPF object\n");
        return 1;
    }

    // Find the packet count map by name
    struct bpf_map *map = bpf_object__find_map_by_name(obj, "packet_count_map");
    if (!map) {
        fprintf(stderr, "Failed to find map\n");
        bpf_object__close(obj);
        return 1;
    }

    int map_fd = bpf_map__fd(map);

    printf("Reading packet counts (Ctrl+C to stop):\n");
    printf("%-10s %-15s\n", "Protocol", "Packet Count");
    printf("---------------------------------\n");

    // Read and display counts every 2 seconds
    while (1) {
        printf("\033[2J\033[H"); // Clear screen
        printf("Reading packet counts (Ctrl+C to stop):\n");
        printf("%-10s %-15s\n", "Protocol", "Packet Count");
        printf("---------------------------------\n");

        for (__u32 key = 0; key < 256; key++) {
            __u64 value = 0;
            int err = bpf_map_lookup_elem(map_fd, &key, &value);
            if (err == 0 && value > 0) {
                printf("%-10s %-15llu\n", proto_name(key), value);
            }
        }

        sleep(2);
    }

    bpf_object__close(obj);
    return 0;
}

Compile and run the reader:

# Compile the map reader
gcc -O2 -g read_counts.c -o read_counts -lbpf -lelf -lz

# In a separate terminal, run the reader
sudo ./read_counts packet_counter.o

Advanced Networking Use Cases

Beyond simple packet counting, eBPF can handle sophisticated networking tasks. Here are some common advanced use cases and how to approach them.

Traffic Filtering with XDP Drop

You can modify the XDP program to drop packets matching certain criteria, such as blocking traffic from specific IP addresses:

SEC("xdp")
int drop_packets(struct xdp_md *ctx) {
    void *data_end = (void *)(long)ctx->data_end;
    void *data = (void *)(long)ctx->data;

    struct ethhdr *eth = data;
    if ((void *)(eth + 1) > data_end)
        return XDP_PASS;

    if (eth->h_proto != __builtin_bswap16(ETH_P_IP))
        return XDP_PASS;

    struct iphdr *iph = (void *)(eth + 1);
    if ((void *)(iph + 1) > data_end)
        return XDP_PASS;

    // Block traffic from 192.168.1.100 (0xC0A80164 in hex)
    if (iph->saddr == __builtin_bswap32(0xC0A80164)) {
        return XDP_DROP;
    }

    return XDP_PASS;
}

Socket-Level Filtering with TC

The Traffic Control (TC) hook allows you to attach eBPF programs at the socket buffer level for both ingress and egress traffic. This is useful for more granular control:

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/pkt_cls.h>

SEC("tc")
int tc_packet_marker(struct __sk_buff *skb) {
    // Mark packets for traffic shaping
    if (skb->protocol == __builtin_bswap16(0x0800)) {
        // Set a mark on IP packets for QoS processing
        skb->mark = 0x1234;
    }
    return TC_ACT_OK;
}

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

Attach the TC program using the tc command:

# Compile the TC program
clang -O2 -g -target bpf -D__TARGET_ARCH_x86 \
    -c tc_packet_marker.c -o tc_packet_marker.o

# Attach to ingress on eth0
sudo tc qdisc add dev eth0 clsact
sudo tc filter add dev eth0 ingress bpf da obj tc_packet_marker.o sec tc

# Remove when done
sudo tc filter del dev eth0 ingress
sudo tc qdisc del dev eth0 clsact

Load Balancing with eBPF

eBPF is the foundation of modern load balancers like Facebook's Katran and Cloudflare's L4 load balancer. Here is a simplified example of a round-robin load balancer using XDP:

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/udp.h>
#include <linux/in.h>

#define MAX_BACKENDS 4

struct backend {
    __u32 ip;
    __u16 port;
};

struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __uint(max_entries, MAX_BACKENDS);
    __type(key, __u32);
    __type(value, struct backend);
} backends SEC(".maps");

struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __uint(max_entries, 1);
    __type(key, __u32);
    __type(value, __u32);
} rr_counter SEC(".maps");

SEC("xdp")
int load_balancer(struct xdp_md *ctx) {
    void *data_end = (void *)(long)ctx->data_end;
    void *data = (void *)(long)ctx->data;

    struct ethhdr *eth = data;
    if ((void *)(eth + 1) > data_end)
        return XDP_PASS;

    if (eth->h_proto != __builtin_bswap16(ETH_P_IP))
        return XDP_PASS;

    struct iphdr *iph = (void *)(eth + 1);
    if ((void *)(iph + 1) > data_end)
        return XDP_PASS;

    // Only handle UDP packets for simplicity
    if (iph->protocol != IPPROTO_UDP)
        return XDP_PASS;

    // Get the round-robin counter
    __u32 key = 0;
    __u32 *idx = bpf_map_lookup_elem(&rr_counter, &key);
    if (!idx)
        return XDP_PASS;

    __u32 backend_key = *idx % MAX_BACKENDS;
    __sync_fetch_and_add(idx, 1);

    // Look up the target backend
    struct backend *backend = bpf_map_lookup_elem(&backends, &backend_key);
    if (!backend)
        return XDP_PASS;

    // Rewrite the destination IP
    iph->daddr = backend->ip;

    // Recalculate IP checksum
    iph->check = 0;
    iph->check = __builtin_bswap16(
        ~((__u16)__builtin_bswap32(iph->saddr + iph->daddr +
        iph->tot_len + iph->protocol)) & 0xFFFF);

    return XDP_PASS;
}

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

Best Practices

When developing eBPF networking programs, following best practices ensures your programs are safe, performant, and maintainable.

Always Handle Bounds Checking

The eBPF verifier requires that every pointer access is bounds-checked. Always verify that (struct *)(ptr + 1) <= data_end before accessing packet data. Failing to do so will cause the verifier to reject your program.

Use BPF Maps Efficiently

Choose the right map type for your use case. BPF_MAP_TYPE_ARRAY is fast for indexed lookups, BPF_MAP_TYPE_HASH is better for arbitrary keys, and BPF_MAP_TYPE_PERCPU_ARRAY or BPF_MAP_TYPE_PERCPU_HASH avoid contention on multi-core systems by using per-CPU storage.

Minimize Packet Processing Overhead

Keep your eBPF programs as short as possible. Every cycle spent in an XDP or TC program adds latency to packet processing. Avoid complex logic and offload heavy computation to user space when possible.

Test with the Verifier

Always check verifier output when developing. Use bpftool prog load with the -d flag to see detailed verifier logs, which help identify why a program was rejected:

sudo bpftool prog load packet_counter.o /sys/fs/bpf/packet_counter type xdp -d

Clean Up Resources

Always detach eBPF programs and close file descriptors when your application exits. Pinned programs in /sys/fs/bpf/ persist until manually removed, so clean them up to avoid resource leaks:

# Remove a pinned program
sudo rm /sys/fs/bpf/packet_counter

Use CO-RE for Portability

Compile Once, Run Everywhere (CO-RE) makes eBPF programs portable across different kernel versions. Use bpf_core_read() for reading kernel structures and include vmlinux.h generated by bpftool btf dump instead of kernel headers:

# Generate vmlinux.h
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h

Monitor Performance

Use bpftool prog show to monitor the runtime statistics of your eBPF programs, including how many times they have run and how long they take:

sudo bpftool prog show

Conclusion

eBPF has transformed Linux networking by providing a safe, fast, and programmable way to process packets directly in the kernel. In this tutorial, you learned what eBPF is, why it matters for networking, and how to set up a complete development environment. You wrote a practical XDP packet counter, built a user-space loader using libbpf, read data from BPF maps, and explored advanced use cases including traffic filtering, TC hooks, and load balancing. By following the best practices outlined here, you can build production-quality eBPF networking programs that deliver exceptional performance and flexibility. As the eBPF ecosystem continues to grow with projects like Cilium, Pixie, and Tetragon, mastering these fundamentals will give you a strong foundation for building the next generation of networking tools and infrastructure.

— Ad —

Google AdSense will appear here after approval

← Back to all articles