Kernel-space telemetry · XDP ring buffer → NetPredict HTTP
The following XDP program parses Ethernet/IP/TCP/UDP headers in kernel space,
maintains a 100 ms rolling window of bytes, packets, drops and inter-arrival jitter,
tracks live flow count in a hash map, and emits a six-feature event on each window
close via a 1 MiB BPF ring buffer. The attached BCC/Python loader converts kernel
units into NetPredict's schema and calls the Treelite C shared object.
netpredict_xdp.c
// netpredict_xdp.c — XDP program exporting 6-feature NetPredict telemetry.
// SPDX-License-Identifier: GPL-2.0
// Build: clang -O2 -g -target bpf -c netpredict_xdp.c -o netpredict_xdp.o
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/udp.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
#define NS_PER_SEC 1000000000ULL
#define WINDOW_NS (NS_PER_SEC / 10) /* 100 ms rolling window */
struct flow_key { __u32 saddr, daddr; __u16 sport, dport; __u8 proto; };
struct window_stat {
__u64 bytes, packets, drops;
__u64 win_start_ns, last_arrival_ns;
__u64 jitter_sum_ns, jitter_count;
__u32 active_flows;
__u16 buffer_util_pct_x100; /* 7250 = 72.50% */
};
struct event {
__u64 ts_ns;
__u32 ingress_bps;
__u32 arrival_pps;
__u32 active_flows;
__u32 buffer_util_pct_x100;
__u32 loss_rate_x1e6;
__u32 jitter_ns;
};
struct { __uint(type, BPF_MAP_TYPE_ARRAY); __type(key, __u32);
__type(value, struct window_stat); __uint(max_entries, 1); } g_stat SEC(".maps");
struct { __uint(type, BPF_MAP_TYPE_HASH); __type(key, struct flow_key);
__type(value, __u64); __uint(max_entries, 65536); } flows SEC(".maps");
struct { __uint(type, BPF_MAP_TYPE_RINGBUF); __uint(max_entries, 1 << 20); } events SEC(".maps");
SEC("xdp") int xdp_netpredict(struct xdp_md *ctx) {
void *data = (void *)(long)ctx->data, *end = (void *)(long)ctx->data_end;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > end) return XDP_PASS;
if (eth->h_proto != bpf_htons(ETH_P_IP)) return XDP_PASS;
struct iphdr *ip = (void *)(eth + 1);
if ((void *)(ip + 1) > end) return XDP_PASS;
struct flow_key k = { .saddr = ip->saddr, .daddr = ip->daddr, .proto = ip->protocol };
if (ip->protocol == IPPROTO_TCP) {
struct tcphdr *t = (void *)ip + ip->ihl * 4;
if ((void *)(t + 1) > end) return XDP_PASS;
k.sport = t->source; k.dport = t->dest;
} else if (ip->protocol == IPPROTO_UDP) {
struct udphdr *u = (void *)ip + ip->ihl * 4;
if ((void *)(u + 1) > end) return XDP_PASS;
k.sport = u->source; k.dport = u->dest;
}
__u64 now = bpf_ktime_get_ns();
__u64 *last_seen = bpf_map_lookup_elem(&flows, &k);
__u64 one = 1;
if (!last_seen) bpf_map_update_elem(&flows, &k, &one, BPF_NOEXIST);
__u32 z = 0;
struct window_stat *w = bpf_map_lookup_elem(&g_stat, &z);
if (!w) return XDP_PASS;
if (!w->win_start_ns) w->win_start_ns = now;
__u64 pkt_len = end - data;
__sync_fetch_and_add(&w->bytes, pkt_len);
__sync_fetch_and_add(&w->packets, 1);
if (w->last_arrival_ns) {
__u64 iat = now - w->last_arrival_ns;
__sync_fetch_and_add(&w->jitter_sum_ns, iat);
__sync_fetch_and_add(&w->jitter_count, 1);
}
w->last_arrival_ns = now;
if (now - w->win_start_ns >= WINDOW_NS) {
struct event *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
if (e) {
__u64 dt = now - w->win_start_ns;
e->ts_ns = now;
e->ingress_bps = (w->bytes * 8ULL * NS_PER_SEC) / dt;
e->arrival_pps = (w->packets * NS_PER_SEC) / dt;
e->active_flows = w->active_flows;
e->buffer_util_pct_x100 = w->buffer_util_pct_x100;
e->loss_rate_x1e6 = w->drops * 1000000ULL / (w->packets + 1);
e->jitter_ns = w->jitter_count ? w->jitter_sum_ns / w->jitter_count : 0;
bpf_ringbuf_submit(e, 0);
}
w->bytes = w->packets = w->drops = 0;
w->jitter_sum_ns = w->jitter_count = 0;
w->win_start_ns = now;
}
return XDP_PASS;
}
char LICENSE[] SEC("license") = "GPL";
loader.py (BCC)
# loader.py — attach XDP program, stream events, feed NetPredict inference
# pip install bcc OR pip install pyroute2 bcc OR cilium/ebpf (Go). Example here: BCC.
from bcc import BPF
import ctypes, time, requests
class Event(ctypes.Structure):
_fields_ = [
("ts_ns", ctypes.c_uint64),
("ingress_bps", ctypes.c_uint32),
("arrival_pps", ctypes.c_uint32),
("active_flows", ctypes.c_uint32),
("buffer_util_pct_x100", ctypes.c_uint32),
("loss_rate_x1e6", ctypes.c_uint32),
("jitter_ns", ctypes.c_uint32),
]
b = BPF(src_file="netpredict_xdp.c", cflags=["-O2"])
fn = b.load_func("xdp_netpredict", BPF.XDP)
b.attach_xdp("eth0", fn, 0)
NETPREDICT_URL = "http://127.0.0.1:8080/predict"
def on_event(cpu, data, size):
e = ctypes.cast(data, ctypes.POINTER(Event)).contents
payload = {
"ingress_mbps": e.ingress_bps / 1e6,
"active_flows": e.active_flows,
"arrival_kpps": e.arrival_pps / 1e3,
"buffer_util_pct": e.buffer_util_pct_x100 / 100.0,
"loss_rate_pct": (e.loss_rate_x1e6 / 1e4),
"jitter_ms": e.jitter_ns / 1e6,
}
r = requests.post(NETPREDICT_URL, json=payload, timeout=0.05).json()
print(f"ŷ={r['y_hat']:.2f}ms [{r['lo']:.2f}, {r['hi']:.2f}] regime={r['regime']}")
b["events"].open_ring_buffer(on_event)
try:
while True:
b.ring_buffer_poll(timeout=100); time.sleep(0.01)
finally:
b.remove_xdp("eth0", 0)