PROTOCOLS-FUN . eBPF

eBPF in the Network Stack

read & write the wirein-kernel, verified
rev 2026.08
hooks XDP · tc · socket
safety verifier + maps
use case JA4T/JA4L fingerprinting

eBPF runs tiny verifier-checked programs on the kernel's packet path -- to read details the socket API never exposes and to rewrite packets on egress. Anchored here to one use case: recovering (and forging) the TCP-layer JA4+ fingerprints a normal server can't see. See also Capture & Filter (classic BPF, the ancestor) and Attack & Mitigation.

problemthe SYN is gone by accept()

JA4 and JA4H come from data your process can see - the TLS ClientHello, the HTTP request. The transport fingerprints (JA4T, JA4L) cannot be seen that way. By the time accept() hands you a socket, the kernel has already completed the 3-way handshake and thrown away the client's SYN - and that SYN's window size, MSS, and above all its TCP option ordering are the JA4T fingerprint. No syscall gets them back. The only place that information still exists is the kernel's packet path, as the packet flies by. That is exactly where eBPF runs.

 wire ─▶ [NIC] ─▶ XDP ─▶ tc clsact ─▶ netfilter ─▶ TCP handshake ─▶ [socket recv-q] ─▶ accept()
          driver   ingress   in + egress      nft          (kernel)          buffered           your app
          └────────────── the SYN is visible HERE ──────────────┘                               │
                                                        kernel completes + discards it ──────────┘
                                                        so by accept() the JA4T signal is GONE
eBPF is the clever way, not the standard way. A passive sensor on a tap (Zeek, Suricata) already sees the raw SYN; a TLS-terminating edge (a CDN) already holds the ClientHello. Both compute JA4 without eBPF. eBPF is what lets a normal endpoint recover the transport fingerprint the kernel discarded - see the fingerprinting tab of Attack & Mitigation.
modelverified programs on hook points, talking to userland via maps

An eBPF program is a small function attached to a kernel hook. Before it loads, the verifier proves it terminates and only touches memory it's allowed to - so it can't panic the kernel. It shares state with userland through maps (hash, array, LRU, ring buffer). Different hooks see the packet at different stages:

hookwherecan it
XDPNIC driver, before an skb existsobserve / drop / redirect at line rate (DDoS scrubbing); ingress only
tc (clsact)after skb, ingress and egressread + rewrite packets; needed for anything on the way out
socket / sockopsat the socket layertune connections, steer SO_REUSEPORT, sample data
kprobe / tracepointarbitrary kernel functionsobservability + tracing (not the packet path)
the verifierstatic-analyzes the program (bounded loops, no wild pointers) before load - the reason eBPF is safe to run in production kernels
mapsthe userland bridge; an LRU_HASH keyed by flow is the classic "kernel writes, userland reads" pattern
bpf2gocodegen that keeps the C struct and the Go struct byte-identical, so map layouts never drift
$ bpftool prog loadall ja4t.bpf.o /sys/fs/bpf/ja4t type xdp # load + verify $ bpftool net attach xdpgeneric pinned /sys/fs/bpf/ja4t/xdp_ja4t dev eth0 $ bpftool map dump name ja4t_map # captured SYN fingerprints
patternparse the SYN -> a map keyed by flow

The sensor attaches at tc/XDP and inspects every SYN and SYN-ACK before the kernel digests them: it parses the TCP options in wire order into JA4T, timestamps the packet with bpf_ktime_get_ns and reads the TTL for JA4L, then writes the result into a map keyed by the client (ip, port). Userland looks that map up by conn.RemoteAddr() when the request arrives - and there's no race, because the SYN always precedes the handshake.

a JA4T fingerprint
64240_2-4-8-1-3_1460_8
64240TCP window size from the SYN
2-4-8-1-3option kinds in wire order (2 MSS, 4 SACK-OK, 8 timestamps, 1 NOP, 3 window-scale) - the most OS-discriminating part
1460MSS value
8window scale shift
observe, never drop. The sensor returns XDP_PASS / passes the skb - it only reads, so it's safe to attach in front of a live listener. The same program timestamps the SYN-ACK's egress to derive one-way latency (JA4L), which is why it lives at tc (both directions), not XDP (ingress only).
patternlet the kernel build the SYN, rewrite it on egress

JA4T is the fingerprint everyone assumes is unforgeable, because the kernel emits SYN options from system-wide sysctls - there's no per-connection knob for "send options in this order." eBPF forges it anyway, by rewriting the packet on the tc egress path while leaving the TCP stack intact.

Phase A (values)rewrite window / MSS / window-scale in place, recompute the TCP checksum. A SYN has no payload, so the sum covers only the ≤60-byte header - small and verifier-friendly
Phase B (order)reordering options changes the length + data-offset. Since the SYN's options are its tail, bpf_skb_change_tail resizes it; then re-validate every pointer, rewrite doff + IP length, drop in the target OS template, recompute both checksums
so the kernel TCP fingerprint isn't unspoofable ground truth - it just costs an eBPF egress program. Real-NIC catch: locally-generated segments hit the egress hook as CHECKSUM_PARTIAL (the NIC finishes the sum via TX offload), so on a physical interface you disable offload first: ethtool -K eth0 tx off, or the hardware clobbers the checksum you computed.
use casefingerprint the transport layer, livetoolja4-loko

A JA4+ toolkit that fingerprints who connects from raw wire data, using eBPF for the transport members an endpoint can't otherwise reach. Organizing idea: the spoofability gradient -- harder to forge the lower you go, as control moves app → library → kernel.

memberlayerforge difficulty
JA4HHTTPtrivial - it's request text
JA4 (TLS)librarymoderate - needs uTLS to shape the ClientHello
JA4T (TCP)kernelhard - needs the eBPF egress rewriter above
JA4L (latency)physicscan't - eBPF can rewrite TTL, not the speed of light
ja4-loko - our JA4+ research toolkit does exactly this: an eBPF tc sensor for JA4T/JA4L off the wire, a raw-ClientHello peek for JA4, and an eBPF egress rewriter that forges JA4T (option order and all, tcpdump-verified). It's the reference implementation of everything on this page. github.com/hed0rah/ja4-loko.
the real defense is coherence. Any one layer is forgeable, so the bar is faking every layer at once, live - and the one signal no field can lie about is timing (JA4L). Same "obscurity is a layer, not the control" lesson as address scanning and fingerprint spoofing everywhere else in this family.
the bigger pictureeBPF is the kernel's programmable layer

Fingerprinting is one use; eBPF long outgrew networking. Same model - verified programs on hook points, maps to userland - applied across the kernel:

observabilitybpftrace one-liners over kprobes/tracepoints; the "print any kernel event" superpower
networkingCilium (k8s CNI), XDP load-balancing and line-rate DDoS drop before the stack ever sees the flood
securityFalco, LSM hooks, seccomp-style syscall filtering - policy enforced in-kernel
TLS visibilityuprobes on SSL_read/SSL_write read plaintext at the library boundary - observability without a MITM proxy
same lineage. Classic BPF - the capture-filter language on the Capture & Filter page - is the ancestor: a tiny in-kernel program that matches packets. eBPF is its grown-up descendant: from "match packets" to "run verified programs almost anywhere in the kernel."