Classroom Glossary Public page

Lab 5: eBPF/XDP Program Authoring -- Line-Rate Packet Drop and Measurement

283 words

Week: 5 -- Performance Engineering
Points: 25
Time estimate: 90 min lab + 3 hr independent
Deliverable: lab-5-report.md + XDP source files


Objectives

  1. Write, compile, and load an XDP program that counts packets by protocol using a BPF hash map.
  2. Extend the program to drop packets from a configurable IP blacklist (loaded at runtime from user space).
  3. Measure per-packet latency and throughput vs. an equivalent iptables drop rule.

Requires: Linux kernel 5.15+; clang/llvm; libbpf-dev; iproute2 with XDP support.


Part A: Packet Counter with BPF Map (30 min)

Write lab5/xdp_counter.c:

// xdp_counter.c -- count packets by IP protocol
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>

// Map: protocol number (u8) -> packet count (u64)
struct {
    __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
    __uint(max_entries, 256);
    __type(key, __u32);
    __type(value, __u64);
} proto_count SEC(".maps");

SEC("xdp")
int xdp_count(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 (bpf_ntohs(eth->h_proto) != ETH_P_IP) return XDP_PASS;

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

    __u32 proto = ip->protocol;
    __u64 *count = bpf_map_lookup_elem(&proto_count, &proto);
    if (count) {
        (*count)++;
    }
    return XDP_PASS;
}

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

Write lab5/read_counter.py to read and display map contents:

#!/usr/bin/env python3
"""Read the proto_count XDP map and display protocol distribution."""
import ctypes
import subprocess
import json

def read_xdp_map():
    result = subprocess.run(
        ["bpftool", "map", "dump", "name", "proto_count", "-j"],
        capture_output=True, text=True
    )
    entries = json.loads(result.stdout)
    
    proto_names = {1: "ICMP", 6: "TCP", 17: "UDP", 47: "GRE", 50: "ESP"}
    
    for entry in entries:
        key = entry["key"]
        value = sum(entry["value"])  # sum per-cpu values
        if value > 0:
            proto = key[0] if isinstance(key, list) else key
            name = proto_names.get(proto, f"proto-{proto}")
            print(f"Protocol {name:8} ({proto:3}): {value:>10} packets")

if __name__ == "__main__":
    read_xdp_map()

Compile, load, generate traffic, and read:

# Compile
clang -O2 -target bpf -c lab5/xdp_counter.c -o lab5/xdp_counter.o

# Load on loopback (safe for testing)
sudo ip link set dev lo xdp obj lab5/xdp_counter.o sec xdp

# Generate test traffic (ping + curl + nc UDP)
ping -c 100 localhost &
curl http://localhost/ 2>/dev/null &
echo "test" | nc -u localhost 9999 &
wait

# Read counter
python3 lab5/read_counter.py

# Detach
sudo ip link set dev lo xdp off

Record the packet counts by protocol.


Part B: Runtime-Configurable IP Blacklist (30 min)

Extend xdp_counter.c to a new program lab5/xdp_blacklist.c that drops packets from IPs in a BPF hash map:

// xdp_blacklist.c
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>

// Blacklist map: src_ip (u32 big-endian) -> 1 (u8)
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 65536);
    __type(key, __u32);
    __type(value, __u8);
} blacklist SEC(".maps");

// Drop counter
struct {
    __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
    __uint(max_entries, 1);
    __type(key, __u32);
    __type(value, __u64);
} drop_count SEC(".maps");

SEC("xdp")
int xdp_blacklist_drop(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 (bpf_ntohs(eth->h_proto) != ETH_P_IP) return XDP_PASS;

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

    __u32 src = ip->saddr;
    __u8 *blocked = bpf_map_lookup_elem(&blacklist, &src);
    if (blocked && *blocked == 1) {
        // Increment drop counter
        __u32 key = 0;
        __u64 *cnt = bpf_map_lookup_elem(&drop_count, &key);
        if (cnt) (*cnt)++;
        return XDP_DROP;
    }
    return XDP_PASS;
}

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

Write lab5/manage_blacklist.py to add/remove IPs at runtime:

#!/usr/bin/env python3
"""Manage XDP blacklist map entries."""
import socket
import struct
import subprocess
import sys

def ip_to_hex(ip_str: str) -> str:
    """Convert dotted-decimal IP to hex for bpftool."""
    packed = socket.inet_aton(ip_str)
    # BPF map stores in network byte order
    return " ".join(f"0x{b:02x}" for b in packed)

def add_to_blacklist(ip: str):
    key_hex = ip_to_hex(ip)
    subprocess.run(
        ["bpftool", "map", "update", "name", "blacklist",
         "key", *key_hex.split(), "value", "0x01"],
        check=True
    )
    print(f"Added {ip} to blacklist")

def remove_from_blacklist(ip: str):
    key_hex = ip_to_hex(ip)
    subprocess.run(
        ["bpftool", "map", "delete", "name", "blacklist",
         "key", *key_hex.split()],
        check=True
    )
    print(f"Removed {ip} from blacklist")

if __name__ == "__main__":
    action, ip = sys.argv[1], sys.argv[2]
    if action == "add":
        add_to_blacklist(ip)
    elif action == "del":
        remove_from_blacklist(ip)

Test the runtime blacklist:

# Load XDP program
clang -O2 -target bpf -c lab5/xdp_blacklist.c -o lab5/xdp_blacklist.o
sudo ip link set dev eth0 xdp obj lab5/xdp_blacklist.o sec xdp

# Add test IP to blacklist
sudo python3 lab5/manage_blacklist.py add 10.0.0.100

# Verify traffic from 10.0.0.100 is dropped
# (use nping or iperf3 from a container at 10.0.0.100)

# Remove from blacklist -- traffic should resume
sudo python3 lab5/manage_blacklist.py del 10.0.0.100

Part C: XDP vs iptables Performance Comparison (30 min)

Measure the packet-drop rate for XDP_DROP vs iptables DROP:

# Baseline: no filtering
iperf3 -c <target> -u -b 10G -t 10 --get-server-output > baseline.txt

# Test 1: XDP_DROP (use Part B's program)
# Load blacklist program; add source IP; run iperf3
iperf3 -c <target> -u -b 10G -t 10 --get-server-output > xdp_drop.txt

# Test 2: iptables DROP (equivalent rule)
sudo ip link set dev eth0 xdp off   # detach XDP
sudo iptables -I INPUT -s <source_ip> -j DROP
iperf3 -c <target> -u -b 10G -t 10 --get-server-output > iptables_drop.txt
sudo iptables -D INPUT -s <source_ip> -j DROP

# Compare
echo "Baseline Mpps:"
grep -o '[0-9.]* Mbits' baseline.txt
echo "XDP_DROP Mpps:"
grep -o '[0-9.]* Mbits' xdp_drop.txt
echo "iptables DROP Mpps:"
grep -o '[0-9.]* Mbits' iptables_drop.txt

Complete the performance table:

| Method | Throughput before drop | CPU usage | Packets dropped/sec |
|---|---|---|---|
| Baseline (no filter) | | | N/A |
| XDP_DROP | | | |
| iptables DROP | | | |

Lab Report

Create lab-5-report.md with:

  1. Part A: protocol counter output (packet counts by protocol)
  2. Part B: evidence that the blacklisted IP is dropped (ping failure or iperf3 zero-throughput)
  3. Part B: evidence that removing from the blacklist restores connectivity
  4. Part C: completed performance table
  5. Analysis: explain in 2-3 sentences why XDP_DROP has lower CPU cost than iptables DROP, referencing where in the kernel's receive path each operates

Grading

Component Points
Part A: XDP counter loads + protocol counts correct 6
Part B: runtime blacklist add/remove works without program reload 8
Part C: performance table completed with actual measurements 7
Analysis: correct kernel-path explanation 4
Total 25