Week: 5 -- Performance Engineering
Points: 25
Time estimate: 90 min lab + 3 hr independent
Deliverable: lab-5-report.md + XDP source files
Objectives
- Write, compile, and load an XDP program that counts packets by protocol using a BPF hash map.
- Extend the program to drop packets from a configurable IP blacklist (loaded at runtime from user space).
- 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:
- Part A: protocol counter output (packet counts by protocol)
- Part B: evidence that the blacklisted IP is dropped (ping failure or iperf3 zero-throughput)
- Part B: evidence that removing from the blacklist restores connectivity
- Part C: completed performance table
- 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 |