feat(ja4ebpf): add SSL_write uprobe, HPACK decoder, and AcceptCache for session correlation

Add uprobe_ssl_write_entry/uretprobe_ssl_write_exit to capture server HTTP
responses via SSL_write with direction=1. Implement full HPACK decoder
(RFC 7541 static table, multi-byte integers, literal representations) for
HTTP/2 header extraction. Add AcceptCache mapping {tgid,fd}→SessionKey
from accept4 events as authoritative source for SSL correlation when BPF
ssl_conn_map has src_ip=0. Add ip_total_length to tcp_syn_event BPF struct.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jacquin Antoine
2026-04-15 03:34:43 +02:00
parent a02423fd18
commit 24306ef390
7 changed files with 847 additions and 16 deletions

142
tests/vm/generate-traffic.sh Executable file
View File

@ -0,0 +1,142 @@
#!/usr/bin/env bash
# =============================================================================
# generate-traffic.sh — Generate HTTPS/HTTP traffic from a VM endpoint
#
# Called by run-e2e-test.sh via:
# vagrant ssh $vm -- "source /tmp/e2e-traffic.env && bash /ja4-platform/tests/vm/generate-traffic.sh"
#
# Environment variables (from /tmp/e2e-traffic.env):
# HITS — Number of HTTPS requests (required)
# HITS_HTTP — Number of HTTP requests (default: 0)
# TARGET_IPS — Space-separated list of endpoint IPs (required)
# SNI_HOSTS — Space-separated list of SNI hostnames (required)
# TLS_FLAGS — curl TLS flags e.g. "--tlsv1.2 --tlsv1.3" (required)
# SRC_IP_COUNT — Number of source IPs to rotate (default: 1)
# =============================================================================
set -uo pipefail
HITS="${HITS:-0}"
HITS_HTTP="${HITS_HTTP:-0}"
TARGET_IPS=(${TARGET_IPS:-})
SNI_HOSTS=(${SNI_HOSTS:-platform.test})
TLS_FLAGS="${TLS_FLAGS:---tlsv1.2 --tlsv1.3}"
SRC_IP_COUNT="${SRC_IP_COUNT:-1}"
if [ "$HITS" -eq 0 ] && [ "$HITS_HTTP" -eq 0 ]; then
echo "0/0"
exit 0
fi
# ── Collect source IPs from eth0 ──
SRC_IPS=($(ip -4 addr show eth0 2>/dev/null | awk '/inet / {sub(/\/.*/, "", $2); print $2}'))
if [ ${#SRC_IPS[@]} -eq 0 ]; then
echo "0/${HITS}" > /dev/stderr
exit 1
fi
# ── User-Agent pools ──
UA_BROWSER=(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0.0.0 Safari/537.36"
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 Safari/605.1.15"
"Mozilla/5.0 (X11; Linux x86_64; rv:133.0) Gecko/20100101 Firefox/133.0"
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0"
)
UA_BOT=(
"python-requests/2.32.3"
"curl/8.9.1"
"Go-http-client/2.0"
"python-httpx/0.28.1"
"Googlebot/2.1"
)
PATHS=("/" "/health" "/data" "/api/users" "/api/v1/status" "/api/v1/metrics" \
"/login" "/logout" "/api/search" "/static/main.js" "/static/style.css" \
"/favicon.ico" "/robots.txt" "/sitemap.xml" "/api/v2/data" "/admin")
ok=0
err=0
# ── HTTPS traffic ──
if [ "$HITS" -gt 0 ]; then
for i in $(seq 1 "$HITS"); do
idx=$((i - 1))
target_ip="${TARGET_IPS[$((idx % ${#TARGET_IPS[@]}))]}"
sni_host="${SNI_HOSTS[$((idx % ${#SNI_HOSTS[@]}))]}"
path="${PATHS[$((idx % ${#PATHS[@]}))]}"
# Rotate methods: GET(50%), POST(20%), PUT(10%), DELETE(10%), HEAD(10%)
case $((i % 10)) in
0|1|2|3|4) method="GET" ;;
5|6) method="POST" ;;
7) method="PUT" ;;
8) method="DELETE" ;;
9) method="HEAD" ;;
esac
# 70% browser UA, 30% bot UA
if [ $((i % 10)) -lt 7 ]; then
ua="${UA_BROWSER[$((idx % ${#UA_BROWSER[@]}))]}"
else
ua="${UA_BOT[$((idx % ${#UA_BOT[@]}))]}"
fi
# Build curl flags
resolve_flag="--resolve ${sni_host}:443:${target_ip}"
extra_flags="${resolve_flag} ${TLS_FLAGS}"
# Rotate source IPs if multiple are available
if [ ${#SRC_IPS[@]} -gt 1 ] && [ "$SRC_IP_COUNT" -gt 1 ]; then
src_ip="${SRC_IPS[$((idx % SRC_IP_COUNT))]}"
if [ -n "$src_ip" ]; then
extra_flags="${extra_flags} --interface ${src_ip}"
fi
fi
case $method in
POST)
curl -sf -k ${extra_flags} -X POST "https://${sni_host}${path}" \
-H "User-Agent: ${ua}" -H "Content-Type: application/json" \
-d '{"test":1,"seq":'$i'}' \
--connect-timeout 5 --max-time 10 \
>/dev/null 2>&1 && ok=$((ok + 1)) || err=$((err + 1)) ;;
PUT)
curl -sf -k ${extra_flags} -X PUT "https://${sni_host}${path}" \
-H "User-Agent: ${ua}" \
--connect-timeout 5 --max-time 10 \
>/dev/null 2>&1 && ok=$((ok + 1)) || err=$((err + 1)) ;;
DELETE)
curl -sf -k ${extra_flags} -X DELETE "https://${sni_host}${path}" \
-H "User-Agent: ${ua}" \
--connect-timeout 5 --max-time 10 \
>/dev/null 2>&1 && ok=$((ok + 1)) || err=$((err + 1)) ;;
HEAD)
curl -sf -k ${extra_flags} -I "https://${sni_host}${path}" \
-H "User-Agent: ${ua}" \
--connect-timeout 5 --max-time 10 \
>/dev/null 2>&1 && ok=$((ok + 1)) || err=$((err + 1)) ;;
*)
curl -sf -k ${extra_flags} "https://${sni_host}${path}" \
-H "User-Agent: ${ua}" \
--connect-timeout 5 --max-time 10 \
>/dev/null 2>&1 && ok=$((ok + 1)) || err=$((err + 1)) ;;
esac
done
fi
# ── HTTP traffic (port 80) ──
ok_http=0
if [ "$HITS_HTTP" -gt 0 ]; then
for i in $(seq 1 "$HITS_HTTP"); do
idx=$((i - 1))
# Round-robin across target IPs for HTTP too
target_ip="${TARGET_IPS[$((idx % ${#TARGET_IPS[@]}))]}"
path="${PATHS[$((idx % ${#PATHS[@]}))]}"
# HTTP: use target_ip directly (no --resolve needed for HTTP)
curl -sf "http://${target_ip}${path}" \
--connect-timeout 5 --max-time 10 \
>/dev/null 2>&1 && ok_http=$((ok_http + 1)) || true
done
fi
# Output: HTTPS_ok/HTTPS_total HTTP_ok/HTTP_total
echo "${ok}/${HITS} ${ok_http}/${HITS_HTTP}"