Introduction to eBPF
Extended Berkeley Packet Filter (eBPF) is a programmable, in‑kernel virtual machine that runs sandboxed bytecode at specific hook points in the Linux networking stack, tracing subsystem, and other kernel subsystems. Programs are loaded via the bpf() syscall (usually through libbpf, bpftrace, or bpftool) and attached to hooks such as XDP (eXpress Data Path), tc (traffic control), socket filters, kprobes, tracepoints, or perf events. The verifier guarantees that each program terminates, stays within a bounded instruction limit (currently 1 M instructions for most program types), and does not access invalid memory, making eBPF safe for production use.
Benefits of Using eBPF for Packet and Latency Checks
- Zero‑copy packet access – XDP programs see the packet before it is allocated into an skb, allowing inspection or modification with only a few CPU cycles.
- Deterministic low overhead – The verifier enforces a static instruction bound, so the per‑packet cost can be measured and bounded ahead of deployment.
- Rich map infrastructure – eBPF maps (arrays, hash tables, perf rings, stack traces) enable stateful aggregation (e.g., latency histograms, packet counters) without crossing to userspace for every packet.
- Dynamic updates – Programs can be replaced or patched without rebooting or reloading kernel modules, facilitating canary verification gates.
- Broad visibility – The same program can be attached to multiple hooks (XDP, tc, kprobe) to correlate packet‑level events with CPU scheduling or lock latency.
eBPF‑Based Packet Checks
Implementing Packet Checks with eBPF
A packet check typically follows this pattern:
- Hook selection – XDP for the earliest point, or tc ingress/egress if XDP is unavailable.
- Metadata extraction – Parse Ethernet, IPv4/IPv6, TCP/UDP headers using
bpf_ntohs/bpf_ntohlhelpers. - Predicate evaluation – Compare fields against allow/deny lists, check TTL, flags, or payload signatures.
- Action – Return
XDP_PASS,XDP_DROP,XDP_TX, orXDP_ABORTED(tc usesTC_ACT_OK,TC_ACT_SHOT, etc.). - Accounting – Increment per‑CPU counters or update a histogram map for observability.
The verifier rejects programs that contain unbounded loops or variable‑length header walks without proper bounds checks; therefore, header parsing must be done with explicit length checks and constant‑time loops.
Example eBPF Code for Packet Inspection
/* SPDX-License-Identifier: GPL-2.0 */
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
__uint(max_entries, 1);
__type(key, u32);
__type(value, u64);
} pass_cnt SEC(".maps");
SEC("xdp")
int xdp_port_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; /* malformed */
if (eth->h_proto != bpf_htons(ETH_P_IP))
return XDP_PASS; /* non‑IPv4 */
struct iphdr *ip = data + sizeof(struct ethhdr);
if ((void *)(ip + 1) > data_end)
return XDP_PASS;
if (ip->protocol != IPPROTO_TCP)
return XDP_PASS;
struct tcphdr *tcp = data + sizeof(struct ethhdr) + (ip->ihl * 4);
if ((void *)(tcp + 1) > data_end)
return XDP_PASS;
__be16 dport = tcp->dest;
if (dport == bpf_htons(8080)) {
return XDP_DROP;
}
/* Count passed packets */
u32 idx = 0;
u64 *cnt = bpf_map_lookup_elem(&pass_cnt, &idx);
if (cnt)
__sync_fetch_and_add(cnt, 1);
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
CLI Examples for Packet Check Deployment
# Load the program on interface eth0 in XDP native mode
ip link set dev eth0 xdp obj xdp_port_drop.o sec xdp
# Verify attachment
ip -d link show eth0 | grep xdp
# Show per‑CPU pass counters (requires bpftool)
bpftool map show name pass_cnt
bpftool map dump id $(bpftool map show | awk '/pass_cnt/ {print $1}') key 0 value 0
# To detach and restore default behavior
ip link set dev eth0 xdp off
If XDP is unavailable, the same logic can be placed in a tc ingress filter:
tc qdisc add dev eth0 clsact
tc filter add dev eth0 ingress bpf da obj xdp_port_drop.o sec xdp
tc filter show dev eth0 ingress
eBPF‑Based Latency Checks
Understanding Latency Measurement with eBPF
Latency can be measured at different layers:
- XDP‑to‑XDP round‑trip – timestamp at ingress, store in a per‑CPU map, read at egress (or via a loopback device) and compute delta.
- TCP RTT – use tracepoints
tcp_probeor kprobes ontcp_v4_rcv/tcp_v4_sendmsgto captureskb->tstampandtcp_sk(sk)->lsndtime. - Application‑level latency – attach a uprobe to a library function (e.g.,
SSL_write) and kernel tracepoint tosched_switchto measure time spent in kernel vs. user.
Typical pattern:
- Capture
ktime_get_ns()at entry point. - Store the value in a per‑CPU hash keyed by a flow identifier (e.g., 5‑tuple) or a connection struct pointer.
- At exit point, retrieve the start time, compute delta, and update a histogram map (e.g.,
BPF_MAP_TYPE_HISTOGRAMor a custom linear bucket array). - Optionally, push samples to a perf ring buffer for out‑of‑band aggregation.
Because the verifier limits stack depth, storing per‑flow state must use maps; unbounded numbers of flows require LRU eviction or a fixed‑size array with hash‑based indexing.
Example eBPF Code for Latency Measurement
/* SPDX-License-Identifier: GPL-2.0 */
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/pkt_cls.h>
#define MAX_LATENCY_US 2000000 /* 2s max for histogram */
/* Histogram: power‑of‑two buckets, 0..2^20 us */
struct {
__uint(type, BPF_MAP_TYPE_HISTOGRAM);
__uint(max_entries, 1);
__type(key, u32);
__type(value, struct bpf_histogram);
} latency_hist SEC(".maps");
/* Per‑CPU scratch for storing timestamp */
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
__uint(max_entries, 1);
__type(key, u32);
__type(value, u64);
} tmp_ts SEC(".maps");
SEC("xdp")
int xdp_latency(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 != bpf_htons(ETH_P_IP))
return XDP_PASS;
struct iphdr *ip = data + sizeof(struct ethhdr);
if ((void *)(ip + 1) > data_end)
return XDP_PASS;
if (ip->protocol != IPPROTO_TCP)
return XDP_PASS;
struct tcphdr *tcp = data + sizeof(struct ethhdr) + (ip->ihl * 4);
if ((void *)(tcp + 1) > data_end)
return XDP_PASS;
/* Only measure SYN packets (flags == SYN) */
if ((tcp->syn == 1) && (tcp->ack == 0)) {
u64 ts = bpf_ktime_get_ns();
u32 idx = 0;
u64 *ptr = bpf_map_lookup_elem(&tmp_ts, &idx);
if (ptr)
*ptr = ts;
}
/* If we have a stored timestamp, compute delta and histogram */
if (tcp->ack == 1) { /* ACK packet – treat as response */
u32 idx = 0;
u64 *start_ptr = bpf_map_lookup_elem(&tmp_ts, &idx);
if (start_ptr) {
u64 delta = bpf_ktime_get_ns() - *start_ptr;
u32 delta_us = delta / 1000;
if (delta_us <= MAX_LATENCY_US) {
/* Histogram key is always 0; value updated in‑place */
bpf_histogram_increment(&latency_hist, 0, delta_us);
}
/* Clear scratch */
*start_ptr = 0;
}
}
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
Note: The histogram map is a special map type (BPF_MAP_TYPE_HISTOGRAM) that provides atomic bucket increments; if the kernel lacks this type, a regular array with power‑of‑two buckets can be used and updated via bpf_map_update_elem with a fetch‑add loop (still bounded).
CLI Examples for Latency Check Deployment
# Compile with clang targeting BPF
clang -O2 -target bpf -c xdp_latency.c -o xdp_latency.o
# Load on eth0
ip link set dev eth0 xdp obj xdp_latency.o sec xdp
# Verify
ip -d link show eth0 | grep xdp
# Dump histogram (requires bpftool with histogram support)
bpftool map show name latency_hist
bpftool map dump id $(bpftool map show | awk '/latency_hist/ {print $1}') --hex
# To detach
ip link set dev eth0 xdp off
If using tc instead of XDP:
tc qdisc add dev eth0 clsact
tc filter add dev eth0 ingress bpf da obj xdp_latency.o sec xdp
Integration with Remediation Loops
Overview of Remediation Loops
A typical remediation loop for a drained node might look like:
- Detect failure (e.g., health check, node‑drain signal).
- Mark node as unschedulable, evacuate workloads.
- Perform corrective action (e.g., restart service, replace NIC firmware, apply kernel patch).
- Verification gate – run active diagnostics to confirm the node can safely resume traffic.
- If verification passes, return node to service; otherwise, repeat or escalate.
The verification gate must be fast, low‑overhead, and provide decisive evidence (e.g., latency under threshold, zero packet loss for a test flow). eBPF checks satisfy these requirements because they run in‑kernel, can be enabled/disabled on demand, and expose metrics via maps or perf rings without involving userspace for each packet.
eBPF as a Verification Gate in Remediation Loops
The gate operates as follows:
- Pre‑check – Load a lightweight eBPF program that merely counts packets and records latency; ensure the verifier loads and the program attaches without error.
- Measurement window – Run the program for a fixed interval (e.g., 5 s) while sending a synthetic probe flow (e.g., iperf3, ping, or a custom TCP client) across the node.
- Evaluation – After the window, read the eBPF maps: packet counter should match the number of probe packets sent; latency histogram should show 99th‑percentile ≤ target (e.g., 200 µs). Any drops or latency spikes cause the gate to fail.
- Cleanup – Unload the eBPF program (or leave it loaded in a passive mode) before declaring the node healthy.
Because the eBPF program can be atomically replaced, the verification step can be performed as a canary: first load a “monitor‑only” version, then, if needed, swap in an enforcement version (e.g., XDP_DROP for malformed packets) without disturbing traffic.
Example Code for Integrating eBPF with Remediation Loops
#!/usr/bin/env bash
set -euo pipefail
IFACE=eth0
PROG=xdp_latency.o
SEC=xdp
DURATION=5 # seconds of measurement
TARGET_LATENCY_US=200 # 99th‑pct threshold
PROBE_COUNT=1000
# Load the verification program
ip link set dev "$IFACE" xdp obj "$PROG" sec "$SEC"
# Start probe traffic in the background (example using a simple TCP SYN sender)
timeout "$DURATION"s probe_client --count "$PROBE_COUNT" --dst "$(hostname -I | awk '{print $1}')" &
PROBE_PID=$!
# Wait for measurement window
sleep "$DURATION"
# Stop probe traffic
kill "$PROBE_PID" 2>/dev/null || true
# Read results
PACKETS=$(bpftool map show name pass_cnt | awk '/value/ {print $3}')
LATENCY_P99=$(bpftool map dump id $(bpftool map show | awk '/latency_hist/ {print $1}') --hex | \
awk -v target="$TARGET_LATENCY_US" '
BEGIN{sum=0; cnt=0}
{for(i=2;i<=NF;i++) {val=strtonum("0x"$i); cnt+=val; sum+=val*((i-2)*1000)}}
END{printf "%.0f\n", sum/cnt}')
# Evaluate gate
if [[ "$PACKETS" -eq "$PROBE_COUNT" && "$LATENCY_P99" -le "$TARGET_LATENCY_US" ]]; then
echo "Verification PASSED: $PACKETS packets, p99 latency $LATENCY_P99 µs"
else
echo "Verification FAILED: packets=$PACKETS (expected $PROBE_COUNT), p99 latency=$LATENCY_P99 µs (target ≤$TARGET_LATENCY_US)"
fi
# Cleanup – detach the program
ip link set dev "$IFACE" xdp off
This script demonstrates how an eBPF‑based latency checker can serve as the final verification gate before a remediation loop returns a drained node to service, while keeping overhead minimal and providing clear pass/fail criteria.