Files
ja4-platform/tests/vm/test-rpm.sh
Jacquin Antoine d81463a589 fix(tests): rewrite test-rpm.sh for 3 distros × 3 stacks RPM validation
Complete rewrite of the RPM test harness to properly test ja4ebpf across
CentOS 8, Rocky Linux 9/10 with nginx, apache, and hitch+varnish stacks.

Key fixes:
- Replace nested heredoc-over-SSH with upload-then-execute script pattern
- Fix Apache SSLCertificateKey → SSLCertificateKeyFile directive
- Fix hitch config: backend string syntax, user=nobody, combined PEM, --daemon
- Fix timeout+shell-function incompatibility (timeout can't exec functions)
- Fix subshell result counting by parsing output file instead of variables

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 01:03:57 +02:00

373 lines
13 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# test-rpm.sh — Test ja4ebpf via RPM sur les 3 distros × 3 stacks
#
# Usage:
# make rpm-ja4ebpf && ./tests/vm/test-rpm.sh [VM] [STACK]
# ./tests/vm/test-rpm.sh rocky9 nginx
# ./tests/vm/test-rpm.sh all all
#
# VMs: centos8 rocky9 rocky10 (or "all")
# Stacks: nginx apache hitch-varnish (or "all")
set -euo pipefail
PROJECT_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
VM_DIR="$PROJECT_ROOT/tests/vm"
RPM_BASE="$PROJECT_ROOT/services/ja4ebpf/dist/output"
# Timeouts (seconds)
TEST_TIMEOUT=120 # max per VM×stack test
SSH_TIMEOUT=90 # max per SSH command
UPLOAD_TIMEOUT=60 # max for RPM upload
VM="${1:-rocky9}"
STACK="${2:-nginx}"
# Wrapper vagrant — always runs from the Vagrantfile directory
v() { (cd "$VM_DIR" && command vagrant "$@"); }
# SSH with timeout — uses bash -c to avoid timeout+function issue
vssh() {
local VM="$1"; shift
timeout "$SSH_TIMEOUT" bash -c "cd '$VM_DIR' && vagrant ssh '$VM' -- \"\$@\"" -- "$@" 2>/dev/null
}
vm_to_el() {
case "$1" in
centos8) echo "el8" ;;
rocky9) echo "el9" ;;
rocky10) echo "el10" ;;
*) echo "UNKNOWN" ;;
esac
}
ALL_VMS="centos8 rocky9 rocky10"
ALL_STACKS="nginx apache hitch-varnish"
[ "$VM" = "all" ] && VMS="$ALL_VMS" || VMS="$VM"
[ "$STACK" = "all" ] && STACKS="$ALL_STACKS" || STACKS="$STACK"
PASS=0; FAIL=0; SKIP=0; RESULTS=()
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ Test RPM ja4ebpf : VMS=$VMS STACKS=$STACKS"
echo "╚══════════════════════════════════════════════════════════════╝"
# Check RPMs exist
for vm in $VMS; do
el=$(vm_to_el "$vm")
rpm=$(ls -t $RPM_BASE/$el/ja4ebpf-*.rpm 2>/dev/null | head -1)
if [ -z "$rpm" ]; then
echo "ERROR: no RPM for $vm ($el) in $RPM_BASE/$el/"; echo "Run: make rpm-ja4ebpf"; exit 1
fi
echo " $vm$(basename $rpm)"
done
# ── Generate remote setup script ─────────────────────────────────────
generate_setup_script() {
local STACK="$1"
local SETUP_SCRIPT="/tmp/ja4-setup-$$$.sh"
cat > "$SETUP_SCRIPT" << 'SETUP_EOF'
#!/bin/bash
STACK="__STACK__"
# ── Stack functions ──────────────────────────────────────────────────
setup_nginx() {
mkdir -p /run/nginx /var/www/html
echo '{"ok":true}' > /var/www/html/health
openssl req -x509 -nodes -days 365 -subj /CN=test -newkey rsa:2048 \
-keyout /etc/pki/tls/private/test.key -out /etc/pki/tls/certs/test.pem 2>/dev/null || true
cat > /etc/nginx/nginx.conf << 'NGINX_EOF'
worker_processes 1;
events { worker_connections 64; }
http {
access_log off;
server {
listen 80;
listen 443 ssl;
ssl_certificate /etc/pki/tls/certs/test.pem;
ssl_certificate_key /etc/pki/tls/private/test.key;
root /var/www/html;
}
}
NGINX_EOF
nginx && echo " nginx ready"
}
setup_apache() {
mkdir -p /var/www/html
echo '{"ok":true}' > /var/www/html/health
openssl req -x509 -nodes -days 365 -subj /CN=test -newkey rsa:2048 \
-keyout /etc/pki/tls/private/test.key -out /etc/pki/tls/certs/test.pem 2>/dev/null || true
[ -f /etc/httpd/conf.d/ssl.conf ] && mv /etc/httpd/conf.d/ssl.conf /etc/httpd/conf.d/ssl.conf.disabled 2>/dev/null || true
cat > /etc/httpd/conf.d/test.conf << 'APACHE_EOF'
Listen 8080
<VirtualHost *:8080>
DocumentRoot /var/www/html
</VirtualHost>
Listen 8443
<VirtualHost *:8443>
DocumentRoot /var/www/html
SSLEngine on
SSLCertificateFile /etc/pki/tls/certs/test.pem
SSLCertificateKeyFile /etc/pki/tls/private/test.key
</VirtualHost>
APACHE_EOF
httpd && echo " httpd ready"
}
setup_hitch_varnish() {
mkdir -p /var/www/html
echo '{"ok":true}' > /var/www/html/health
openssl req -x509 -nodes -days 365 -subj /CN=test -newkey rsa:2048 \
-keyout /etc/pki/tls/private/test.key -out /etc/pki/tls/certs/test.pem 2>/dev/null || true
# Start simple HTTP backend
nohup python3 -c 'import http.server, os; os.chdir("/var/www/html"); http.server.HTTPServer(("127.0.0.1", 8081), http.server.SimpleHTTPRequestHandler).serve_forever()' </dev/null >/dev/null 2>&1 &
sleep 1
# Varnish
mkdir -p /etc/varnish
cat > /etc/varnish/default.vcl << 'VARNISH_EOF'
vcl 4.1;
backend default { .host = "127.0.0.1"; .port = "8081"; }
VARNISH_EOF
varnishd -a :80 -f /etc/varnish/default.vcl -s malloc,64m 2>/dev/null || true
sleep 1
# Hitch (combined PEM: key + cert)
mkdir -p /etc/hitch
cat /etc/pki/tls/private/test.key /etc/pki/tls/certs/test.pem > /etc/pki/tls/private/hitch.pem
cat > /etc/hitch/hitch.conf << 'HITCH_EOF'
frontend = {
host = "0.0.0.0"
port = "443"
tls = on
pem-file = "/etc/pki/tls/private/hitch.pem"
}
backend = "[127.0.0.1]:80"
user = "nobody"
HITCH_EOF
hitch --daemon --config=/etc/hitch/hitch.conf >/dev/null 2>&1 && echo ' hitch started' || echo ' hitch failed'
sleep 1
echo " hitch+varnish ready"
}
# ── Main setup ──────────────────────────────────────────────────────
# [A] Install RPM
echo " [A] Install RPM..."
pkill ja4ebpf 2>/dev/null || true; sleep 1
yum remove -y ja4ebpf 2>/dev/null || true
yum install -y /tmp/ja4ebpf-test.rpm 2>&1 | tail -3
[ -x /usr/sbin/ja4ebpf ] && echo " binary OK" || { echo " FAIL: binary not found"; exit 1; }
# [B] Configure
echo " [B] Configure..."
mkdir -p /etc/ja4ebpf
cat > /etc/ja4ebpf/config.yml << 'CONF_EOF'
interface: eth0
ssl_lib_path: "/usr/lib64/libssl.so.3"
debug: true
clickhouse:
dsn: "clickhouse://default:@127.0.0.1:9000/ja4_logs"
batch_size: 50
flush_secs: 1
correlation:
timeout_ms: 500
slowloris_ms: 10000
log:
level: "debug"
format: "text"
CONF_EOF
# [C] Start web stack
echo " [C] Stack: $STACK..."
nginx -s stop 2>/dev/null || true
httpd -k stop 2>/dev/null || true
pkill varnishd 2>/dev/null || true
pkill hitch 2>/dev/null || true
pkill -f 'python3 -c' 2>/dev/null || true
sleep 1
case "$STACK" in
nginx) setup_nginx ;;
apache) setup_apache ;;
hitch-varnish) setup_hitch_varnish ;;
esac
# Open firewall
firewall-cmd --add-service=http --add-service=https 2>/dev/null || true
firewall-cmd --add-port=8080/tcp --add-port=8443/tcp 2>/dev/null || true
# [D] Start ja4ebpf
echo " [D] Start ja4ebpf..."
JA4EBPF_CONFIG=/etc/ja4ebpf/config.yml /usr/sbin/ja4ebpf > /tmp/ja4-test.log 2>&1 &
JA4PID=$!
sleep 3
if ! kill -0 $JA4PID 2>/dev/null; then
echo " FAIL: ja4ebpf crashed"
cat /tmp/ja4-test.log
exit 1
fi
echo " PID=$JA4PID"
# Check TC ingress filter
if tc filter show dev eth0 ingress 2>/dev/null | grep -qi "bpf\|direct-action"; then
echo " TC: attached"
else
echo " WARN: TC filter not detected"
fi
SETUP_EOF
# Replace stack placeholder
sed -i "s/__STACK__/$STACK/" "$SETUP_SCRIPT"
echo "$SETUP_SCRIPT"
}
# ── Test one VM×stack combination ───────────────────────────────────
test_vm_stack() {
local VM="$1"
local STACK="$2"
local LABEL="$VM/$STACK"
local START_TIME=$(date +%s)
echo ""
echo "── $LABEL ─────────────────────────────────────────────"
# Check VM running
if ! v status "$VM" 2>/dev/null | grep -q "running"; then
echo " SKIP: VM not running"
echo " SKIP: VM not running (VM off)"
return
fi
# Find RPM
local EL=$(vm_to_el "$VM")
local RPM=$(ls -t $RPM_BASE/$EL/ja4ebpf-*.rpm | head -1)
echo " RPM: $(basename $RPM)"
# Upload RPM
if ! timeout "$UPLOAD_TIMEOUT" sh -c "cd $VM_DIR && vagrant upload $RPM /tmp/ja4ebpf-test.rpm $VM" 2>/dev/null; then
echo " FAIL: RPM upload timeout"
return
fi
# Get VM IP
local VM_IP=$(vssh "$VM" "ip -4 addr show eth0" | awk '/inet /{sub(/\/.*/,"",$2); print $2; exit}')
if [ -z "$VM_IP" ]; then
echo " SKIP: no eth0 IP"
return
fi
echo " IP: $VM_IP"
# Generate and upload setup script
local SETUP_SCRIPT=$(generate_setup_script "$STACK")
if ! timeout "$UPLOAD_TIMEOUT" sh -c "cd $VM_DIR && vagrant upload $SETUP_SCRIPT /tmp/ja4-setup.sh $VM" 2>/dev/null; then
echo " FAIL: setup script upload timeout"
rm -f "$SETUP_SCRIPT"
return
fi
rm -f "$SETUP_SCRIPT"
# Execute setup script on VM via SSH
if ! timeout "$SSH_TIMEOUT" bash -c "cd '$VM_DIR' && vagrant ssh '$VM' -- 'sudo bash /tmp/ja4-setup.sh'" 2>/dev/null; then
echo " FAIL: VM setup timeout/error"
return
fi
# [E] Generate traffic from HOST
echo " [E] Traffic host→VM..."
local HTTP_PORT HTTPS_PORT
case "$STACK" in
nginx) HTTP_PORT=80; HTTPS_PORT=443 ;;
apache) HTTP_PORT=8080; HTTPS_PORT=8443 ;;
hitch-varnish) HTTP_PORT=80; HTTPS_PORT=443 ;;
esac
for i in $(seq 1 3); do
curl -sf --max-time 5 "http://$VM_IP:$HTTP_PORT/health" -o /dev/null 2>&1 && echo " HTTP $i: OK" || echo " HTTP $i: FAIL"
curl -skf --max-time 5 "https://$VM_IP:$HTTPS_PORT/health" -o /dev/null 2>&1 && echo " HTTPS $i: OK" || echo " HTTPS $i: FAIL"
done
# [F] Wait for debug dump
sleep 6
# [G] Collect results
echo " [G] Results..."
vssh "$VM" "sudo cat /tmp/ja4-test.log" > /tmp/vm-ja4-log.txt 2>/dev/null || true
# Cleanup VM
timeout 15 bash -c "cd '$VM_DIR' && vagrant ssh '$VM' -- 'sudo pkill ja4ebpf 2>/dev/null; sudo nginx -s stop 2>/dev/null; sudo httpd -k stop 2>/dev/null; sudo pkill varnishd 2>/dev/null; sudo pkill hitch 2>/dev/null; sudo pkill -f python3 2>/dev/null; echo cleanup_done'" 2>/dev/null > /dev/null || true
# Parse on host side
local LAST_DEBUG=$(grep "\[debug\] BPF:" /tmp/vm-ja4-log.txt 2>/dev/null | tail -1)
local LAST_GO=$(grep "\[debug\] GO:" /tmp/vm-ja4-log.txt 2>/dev/null | tail -1)
local SESSIONS=$(grep -c "session pr" /tmp/vm-ja4-log.txt 2>/dev/null || echo 0)
local SYN_SUB=$(echo "$LAST_DEBUG" | sed -n 's/.*SYN_SUB=\([0-9]*\).*/\1/p')
SYN_SUB=${SYN_SUB:-0}
local ELAPSED=$(( $(date +%s) - START_TIME ))
echo " BPF: $LAST_DEBUG"
echo " GO: $LAST_GO"
echo " sessions=$SESSIONS SYN_SUB=$SYN_SUB (${ELAPSED}s)"
if [ "$SYN_SUB" -gt 0 ] 2>/dev/null; then
echo " PASS: SYN_SUB=$SYN_SUB sessions=$SESSIONS"
else
echo " FAIL: SYN_SUB=0 (no TC capture)"
fi
}
# ── Main loop ────────────────────────────────────────────────────────
for v in $VMS; do
for s in $STACKS; do
LABEL="$v/$s"
TMPFILE=$(mktemp /tmp/ja4-test-XXXXXX.log)
# Run test_vm_stack in background, capture output
(
test_vm_stack "$v" "$s"
) > "$TMPFILE" 2>&1 &
TESTPID=$!
# Wait with timeout
if ! timeout --signal=TERM --kill-after=10 "$TEST_TIMEOUT" bash -c "while kill -0 $TESTPID 2>/dev/null; do sleep 1; done"; then
kill -9 $TESTPID 2>/dev/null || true
wait $TESTPID 2>/dev/null || true
echo ""
echo "── $LABEL ─────────────────────────────────────────────"
echo " TIMEOUT: exceeded ${TEST_TIMEOUT}s"
else
wait $TESTPID 2>/dev/null
fi
cat "$TMPFILE"
# Parse result from output
if grep -q "^ PASS:" "$TMPFILE" 2>/dev/null; then
PASS=$((PASS + 1)); RESULTS+=("PASS $LABEL")
elif grep -q "^ FAIL:" "$TMPFILE" 2>/dev/null; then
FAIL=$((FAIL + 1)); RESULTS+=("FAIL $LABEL")
elif grep -q "^ SKIP:" "$TMPFILE" 2>/dev/null; then
SKIP=$((SKIP + 1)); RESULTS+=("SKIP $LABEL")
else
FAIL=$((FAIL + 1)); RESULTS+=("FAIL $LABEL (no result)")
fi
rm -f "$TMPFILE"
done
done
# ── Summary ──────────────────────────────────────────────────────────
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ SUMMARY: $PASS pass / $FAIL fail / $SKIP skip"
echo "╚══════════════════════════════════════════════════════════════╝"
for r in "${RESULTS[@]}"; do echo " $r"; done
[ "$FAIL" -eq 0 ] && exit 0 || exit 1