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.
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
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:
| hook | where | can it |
|---|---|---|
| XDP | NIC driver, before an skb exists | observe / drop / redirect at line rate (DDoS scrubbing); ingress only |
| tc (clsact) | after skb, ingress and egress | read + rewrite packets; needed for anything on the way out |
| socket / sockops | at the socket layer | tune connections, steer SO_REUSEPORT, sample data |
| kprobe / tracepoint | arbitrary kernel functions | observability + tracing (not the packet path) |
LRU_HASH keyed by flow is the classic "kernel writes, userland reads" patternThe 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.
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).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.
bpf_skb_change_tail resizes it; then re-validate every pointer, rewrite doff + IP length, drop in the target OS template, recompute both checksumsCHECKSUM_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.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.
| member | layer | forge difficulty |
|---|---|---|
| JA4H | HTTP | trivial - it's request text |
| JA4 (TLS) | library | moderate - needs uTLS to shape the ClientHello |
| JA4T (TCP) | kernel | hard - needs the eBPF egress rewriter above |
| JA4L (latency) | physics | can't - eBPF can rewrite TTL, not the speed of light |
Fingerprinting is one use; eBPF long outgrew networking. Same model - verified programs on hook points, maps to userland - applied across the kernel:
bpftrace one-liners over kprobes/tracepoints; the "print any kernel event" superpowerSSL_read/SSL_write read plaintext at the library boundary - observability without a MITM proxy