Skip to content
LinkState
Go back

Use eBPF as the Final Gate Before Re-Adding a Node

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


eBPF‑Based Packet Checks

Implementing Packet Checks with eBPF

A packet check typically follows this pattern:

  1. Hook selection – XDP for the earliest point, or tc ingress/egress if XDP is unavailable.
  2. Metadata extraction – Parse Ethernet, IPv4/IPv6, TCP/UDP headers using bpf_ntohs/bpf_ntohl helpers.
  3. Predicate evaluation – Compare fields against allow/deny lists, check TTL, flags, or payload signatures.
  4. Action – Return XDP_PASS, XDP_DROP, XDP_TX, or XDP_ABORTED (tc uses TC_ACT_OK, TC_ACT_SHOT, etc.).
  5. 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:

Typical pattern:

  1. Capture ktime_get_ns() at entry point.
  2. Store the value in a per‑CPU hash keyed by a flow identifier (e.g., 5‑tuple) or a connection struct pointer.
  3. At exit point, retrieve the start time, compute delta, and update a histogram map (e.g., BPF_MAP_TYPE_HISTOGRAM or a custom linear bucket array).
  4. 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:

  1. Detect failure (e.g., health check, node‑drain signal).
  2. Mark node as unschedulable, evacuate workloads.
  3. Perform corrective action (e.g., restart service, replace NIC firmware, apply kernel patch).
  4. Verification gate – run active diagnostics to confirm the node can safely resume traffic.
  5. 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:

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.


Share this post on:

Previous Post
CLI, graph diff, or LLM for namespace triage
Next Post
Can AI predict BGP bestpath changes better than a checklist