diff --git a/Makefile b/Makefile index dd0e5f6..2df50ae 100644 --- a/Makefile +++ b/Makefile @@ -175,7 +175,8 @@ vm-rebuild-ja4ebpf: ## Recompiler ja4ebpf dans la VM (après modifications) 'export PATH=/usr/local/go/bin:$$PATH && \ cd /ja4-platform/services/ja4ebpf && \ GOWORK=off go generate ./internal/loader/ && \ - GOWORK=off CGO_ENABLED=0 go build -o /usr/local/bin/ja4ebpf ./cmd/ja4ebpf/ && \ + GOWORK=off CGO_ENABLED=0 go build -o /tmp/ja4ebpf ./cmd/ja4ebpf/ && \ + sudo mv /tmp/ja4ebpf /usr/local/bin/ja4ebpf && \ echo "ja4ebpf rebuilt OK"' test-vm-nginx: ## Test nginx dans la VM (L3/L4/TLS/L7 HTTP complet) diff --git a/services/ja4ebpf/bpf/bpf_types.h b/services/ja4ebpf/bpf/bpf_types.h index f9d3c0c..10b3110 100644 --- a/services/ja4ebpf/bpf/bpf_types.h +++ b/services/ja4ebpf/bpf/bpf_types.h @@ -28,8 +28,8 @@ * Événement TCP SYN : émis pour chaque nouvelle connexion TCP observée * ---------------------------------------------------------------------------*/ struct tcp_syn_event { - __u32 src_ip; /* adresse source (network byte order) */ - __u32 dst_ip; /* adresse destination (network byte order) */ + __u32 src_ip; /* adresse source (host byte order, via bpf_ntohl) */ + __u32 dst_ip; /* adresse destination (host byte order, via bpf_ntohl) */ __u16 src_port; /* port source (host byte order) */ __u16 dst_port; /* port destination (host byte order) */ __u8 ttl; /* TTL IP */ @@ -47,9 +47,9 @@ struct tcp_syn_event { * Événement TLS ClientHello : émis quand un ClientHello TLS est détecté * ---------------------------------------------------------------------------*/ struct tls_hello_event { - __u32 src_ip; /* adresse source (network byte order) */ + __u32 src_ip; /* adresse source (host byte order, via bpf_ntohl) */ __u16 src_port; /* port source (host byte order) */ - __u8 payload[512]; /* payload ClientHello brut */ + __u8 payload[2048]; /* payload ClientHello brut (capturé jusqu'à 2048 octets) */ __u16 payload_len; /* longueur effective du payload */ __u64 timestamp_ns; /* horodatage kernel */ } __attribute__((packed)); diff --git a/services/ja4ebpf/bpf/tc_capture.c b/services/ja4ebpf/bpf/tc_capture.c index 8b88acf..f93d753 100644 --- a/services/ja4ebpf/bpf/tc_capture.c +++ b/services/ja4ebpf/bpf/tc_capture.c @@ -1,15 +1,16 @@ /* ============================================================================ - * tc_capture.c — Programme TC ingress : capture des TCP SYN et TLS ClientHello + * tc_capture.c — Programme XDP ingress : capture des TCP SYN et TLS ClientHello * - * Attaché sur l'interface réseau en ingress via TC (Traffic Control). - * Émet des événements vers les ring buffers rb_tcp_syn et rb_tls_hello. + * Remplace l'ancienne version TC (SCHED_CLS + TCX) par un hook XDP compatible + * depuis le kernel 4.8. Utilisé en mode XDP_GENERIC sur Rocky Linux 9 (5.14). * * Conventions vérificateur eBPF : - * - Tous les offsets variables (ihl, doff) sont stockés en __u32 et bornés - * explicitement avant tout usage en arithmétique de pointeur. - * - Les lectures de longueur variable (options TCP, payload TLS/HTTP) sont - * effectuées via bpf_skb_load_bytes() dans des tampons de pile locaux, - * évitant ainsi toute arithmétique de pointeur sur des données paquet. + * - Tous les accès mémoire paquet utilisent de l'arithmétique de pointeur + * directe avec bornes explicites (data / data_end). + * - Les copies de longueur variable utilisent des boucles bornées (sans + * #pragma unroll) : le vérificateur kernel ≥ 5.3 les accepte nativement. + * - Les options TCP sont copiées brutes ; MSS et Window Scale sont extraits + * côté Go (userspace) depuis le tableau tcp_options_raw. * ============================================================================ */ #include "vmlinux.h" @@ -24,7 +25,7 @@ /* Constantes IP */ #define IPPROTO_TCP 6 -#define IP_DF 0x4000 /* bit Don't Fragment */ +#define IP_DF 0x4000 /* Constantes TCP */ #define TH_SYN 0x02 @@ -32,66 +33,57 @@ #define TH_FIN 0x01 #define TH_RST 0x04 -/* Port HTTPS standard */ +/* Ports */ #define HTTPS_PORT 443 - -/* Ports HTTP en clair */ #define HTTP_PORT 80 #define HTTP_ALT_PORT 8080 -/* Type de contenu TLS : Handshake */ +/* TLS */ #define TLS_CONTENT_HANDSHAKE 0x16 -/* Type de message TLS : ClientHello */ #define TLS_MSG_CLIENT_HELLO 0x01 -/* Taille maximale du payload TLS à copier (puissance de 2) */ -#define MAX_TLS_PAYLOAD 512 +/* Tailles maximales des payloads copiés */ +#define MAX_TLS_PAYLOAD 2048 +#define MAX_HTTP_PAYLOAD 1024 +#define MAX_TCP_OPTIONS 40 -/* Longueur maximale des options TCP en octets */ -#define MAX_TCP_OPTIONS 40 - -/* --------------------------------------------------------------------------- - * Structure interne pour le parsing de l'en-tête Ethernet - * ---------------------------------------------------------------------------*/ +/* Structure Ethernet locale (évite d'inclure linux/if_ether.h) */ struct ethhdr_local { - __u8 h_dest[6]; - __u8 h_source[6]; + __u8 h_dest[6]; + __u8 h_source[6]; __be16 h_proto; } __attribute__((packed)); /* --------------------------------------------------------------------------- - * capture_tc_ingress — Point d'entrée TC ingress + * capture_xdp — Point d'entrée XDP ingress * - * Inspecte chaque paquet entrant, détecte les TCP SYN et les ClientHello TLS, - * et soumet les événements correspondants aux ring buffers. + * Observe chaque paquet ingress en lecture seule (retourne toujours XDP_PASS). + * Émet des événements vers les ring buffers pour TCP SYN, TLS ClientHello + * et les payloads HTTP en clair. * ---------------------------------------------------------------------------*/ -SEC("tc/ingress") -int capture_tc_ingress(struct __sk_buff *skb) +SEC("xdp") +int capture_xdp(struct xdp_md *ctx) { - void *data = (void *)(long)skb->data; - void *data_end = (void *)(long)skb->data_end; + void *data = (void *)(long)ctx->data; + void *data_end = (void *)(long)ctx->data_end; - /* --- Parsing Ethernet --- */ + /* --- Ethernet --- */ struct ethhdr_local *eth = data; if ((void *)(eth + 1) > data_end) - return TC_ACT_OK; - + return XDP_PASS; if (bpf_ntohs(eth->h_proto) != ETH_P_IP) - return TC_ACT_OK; + return XDP_PASS; - /* --- Parsing IPv4 --- */ + /* --- IPv4 --- */ struct iphdr *ip = data + ETH_HLEN; if ((void *)(ip + 1) > data_end) - return TC_ACT_OK; - + return XDP_PASS; if (ip->protocol != IPPROTO_TCP) - return TC_ACT_OK; + return XDP_PASS; - /* ihl stocké en u32 et borné explicitement : le vérificateur peut ainsi - * prouver que ip_hlen ∈ [20, 60] sans risque d'overflow signé. */ __u32 ihl = ip->ihl & 0x0F; if (ihl < 5) - return TC_ACT_OK; + return XDP_PASS; __u32 ip_hlen = ihl << 2; /* ∈ [20, 60] */ __u32 src_ip = ip->saddr; @@ -101,194 +93,168 @@ int capture_tc_ingress(struct __sk_buff *skb) __u16 frag_off = bpf_ntohs(ip->frag_off); __u8 df_bit = (frag_off & IP_DF) ? 1 : 0; - /* --- Parsing TCP --- */ - struct tcphdr *tcp = data + ETH_HLEN + ip_hlen; - if ((void *)(tcp + 1) > data_end) - return TC_ACT_OK; + /* --- TCP à offset variable --- */ + struct tcphdr *tcp = (void *)ip + ip_hlen; + if ((void *)(tcp + 1) > data_end) /* valide tcp[0..19] */ + return XDP_PASS; - __u16 src_port = bpf_ntohs(tcp->source); - __u16 dst_port = bpf_ntohs(tcp->dest); - __u16 window = bpf_ntohs(tcp->window); - /* Lecture des flags via offset constant (octet 13 de l'en-tête TCP) */ - __u8 tcp_flags = ((__u8 *)tcp)[13]; + __u16 src_port = bpf_ntohs(tcp->source); + __u16 dst_port = bpf_ntohs(tcp->dest); + __u16 window = bpf_ntohs(tcp->window); + + /* Flags via les champs de bits du struct (sûr pour le vérificateur) */ + __u8 tcp_flags = 0; + if (tcp->syn) tcp_flags |= TH_SYN; + if (tcp->ack) tcp_flags |= TH_ACK; + if (tcp->fin) tcp_flags |= TH_FIN; + if (tcp->rst) tcp_flags |= TH_RST; - /* doff stocké en u32 et borné : tcp_hlen ∈ [20, 60] */ __u32 doff = tcp->doff; if (doff < 5) - return TC_ACT_OK; + return XDP_PASS; __u32 tcp_hlen = doff << 2; /* ∈ [20, 60] */ - /* Offset absolu du début du payload applicatif dans le paquet */ - __u32 payload_off = ETH_HLEN + ip_hlen + tcp_hlen; + /* Offset du payload applicatif */ + void *payload = (void *)tcp + tcp_hlen; - /* --- Détection TCP SYN (SYN set, ACK clear) --- */ + /* =================================================================== + * TCP SYN : extraction des paramètres L3/L4 + * ===================================================================*/ if ((tcp_flags & TH_SYN) && !(tcp_flags & TH_ACK)) { - struct tcp_syn_event *evt = bpf_ringbuf_reserve(&rb_tcp_syn, sizeof(*evt), 0); + struct tcp_syn_event *evt = + bpf_ringbuf_reserve(&rb_tcp_syn, sizeof(*evt), 0); if (!evt) - return TC_ACT_OK; + return XDP_PASS; - evt->src_ip = bpf_ntohl(src_ip); - evt->dst_ip = bpf_ntohl(dst_ip); - evt->src_port = src_port; - evt->dst_port = dst_port; - evt->ttl = ttl; - evt->df_bit = df_bit; - evt->ip_id = ip_id; - evt->window_size = window; - evt->window_scale = 0xFF; /* absent par défaut */ - evt->mss = 0; /* absent par défaut */ - evt->timestamp_ns = bpf_ktime_get_ns(); + evt->src_ip = bpf_ntohl(src_ip); + evt->dst_ip = bpf_ntohl(dst_ip); + evt->src_port = src_port; + evt->dst_port = dst_port; + evt->ttl = ttl; + evt->df_bit = df_bit; + evt->ip_id = ip_id; + evt->window_size = window; + evt->window_scale = 0xFF; /* défaut = absent */ + evt->mss = 0; + evt->timestamp_ns = bpf_ktime_get_ns(); evt->tcp_options_len = 0; - /* Lecture des options TCP dans un tampon de pile local (copie brute). - * Le scan MSS/WS utilise bpf_skb_load_bytes avec offset variable plutôt - * que opts_buf[j] : l'accès pile à index variable génère une erreur - * vérificateur ("invalid variable-offset read from stack") car le tnum de - * j accumule des bits carries au fil des incréments j += len (u8). */ - __u32 opts_off = ETH_HLEN + ip_hlen + 20; - __u32 opts_bytes = tcp_hlen - 20; /* tcp_hlen >= 20, donc >= 0 */ - if (opts_bytes > MAX_TCP_OPTIONS) - opts_bytes = MAX_TCP_OPTIONS; + /* Copie brute des options TCP (MSS/WS extraits en userspace Go). + * Boucle bornée à MAX_TCP_OPTIONS = 40 itérations : triviale pour + * le vérificateur kernel ≥ 5.3, sans #pragma unroll. */ + __u8 *opts_start = (__u8 *)(tcp + 1); /* après les 20 octets fixes */ + __u32 opts_len = tcp_hlen - 20; /* ∈ [0, 40] */ + if (opts_len > MAX_TCP_OPTIONS) + opts_len = MAX_TCP_OPTIONS; - if (opts_bytes > 0) { - __u8 opts_buf[MAX_TCP_OPTIONS] = {0}; - /* Lecture à taille constante : le vérificateur connaît la borne. */ - if (bpf_skb_load_bytes(skb, opts_off, opts_buf, MAX_TCP_OPTIONS) == 0) { - /* Copie brute dans l'événement */ - __builtin_memcpy(evt->tcp_options_raw, opts_buf, MAX_TCP_OPTIONS); - evt->tcp_options_len = (__u8)opts_bytes; - - /* Scan MSS et Window Scale via bpf_skb_load_bytes (offset variable - * dans le paquet = autorisé ; index variable dans la pile = refusé). */ - __u32 j = 0; - __u8 hdr2[2] = {0}; - __u8 one[1] = {0}; - #pragma unroll - for (int iter = 0; iter < MAX_TCP_OPTIONS; iter++) { - if (j + 1 >= opts_bytes) - break; - /* Lire kind et len d'un coup depuis le paquet */ - if (bpf_skb_load_bytes(skb, opts_off + j, hdr2, 2) < 0) - break; - __u8 kind = hdr2[0]; - if (kind == 0) - break; /* EOL */ - if (kind == 1) { - j++; - continue; /* NOP : 1 octet */ - } - __u8 len = hdr2[1]; - if (len < 2 || j + len > opts_bytes) - break; - - /* MSS (option 2) : 4 octets */ - if (kind == 2 && len == 4) { - __u8 mss_buf[2] = {0}; - if (bpf_skb_load_bytes(skb, opts_off + j + 2, mss_buf, 2) == 0) { - __u16 mss_val; - __builtin_memcpy(&mss_val, mss_buf, 2); - evt->mss = bpf_ntohs(mss_val); - } - } - /* Window Scale (option 3) : 3 octets */ - if (kind == 3 && len == 3) { - if (bpf_skb_load_bytes(skb, opts_off + j + 2, one, 1) == 0) - evt->window_scale = one[0]; - } - j += len; - } + if (opts_len > 0) { + #pragma clang loop unroll(disable) + for (__u32 i = 0; i < MAX_TCP_OPTIONS; i++) { + if (i >= opts_len) + break; + if (opts_start + i + 1 > (__u8 *)data_end) + break; + evt->tcp_options_raw[i] = opts_start[i]; } + evt->tcp_options_len = (__u8)opts_len; } bpf_ringbuf_submit(evt, 0); } - /* --- Détection TLS ClientHello (port 443) --- */ + /* =================================================================== + * TLS ClientHello (port 443) + * ===================================================================*/ if (dst_port == HTTPS_PORT) { - /* Vérifier qu'il y a au moins 6 octets pour l'en-tête TLS record */ - if (payload_off + 6 > skb->len) - return TC_ACT_OK; + /* Au moins 6 octets pour l'en-tête TLS record + type message */ + if (payload + 6 > data_end) + return XDP_PASS; - __u8 tls_hdr[6]; - if (bpf_skb_load_bytes(skb, payload_off, tls_hdr, sizeof(tls_hdr)) < 0) - return TC_ACT_OK; + __u8 tls_type = ((__u8 *)payload)[0]; + __u8 tls_msg_type = ((__u8 *)payload)[5]; + if (tls_type != TLS_CONTENT_HANDSHAKE || tls_msg_type != TLS_MSG_CLIENT_HELLO) + return XDP_PASS; - /* Handshake (0x16) + ClientHello (0x01 au byte 5) */ - if (tls_hdr[0] != TLS_CONTENT_HANDSHAKE || tls_hdr[5] != TLS_MSG_CLIENT_HELLO) - return TC_ACT_OK; + __u32 avail = (__u8 *)data_end - (__u8 *)payload; + /* avail ≥ 6 (vérifié ci-dessus), on plafonne à MAX_TLS_PAYLOAD */ + if (avail > MAX_TLS_PAYLOAD) + avail = MAX_TLS_PAYLOAD; + /* Barrière compilateur : coupe le lien CSE entre avail et (data_end - payload). + * Sans cette barrière, clang génère un test "PTR_TO_PACKET <<= 32" (compare + * data_end == payload pour l'entrée de boucle) que le vérificateur eBPF rejette. + * La barrière force une comparaison scalaire (avail == 0) à la place. */ + asm volatile("" : "+r"(avail)); struct tls_hello_event *tls_evt = bpf_ringbuf_reserve(&rb_tls_hello, sizeof(*tls_evt), 0); if (!tls_evt) - return TC_ACT_OK; + return XDP_PASS; tls_evt->src_ip = bpf_ntohl(src_ip); tls_evt->src_port = src_port; tls_evt->timestamp_ns = bpf_ktime_get_ns(); + tls_evt->payload_len = (__u16)avail; - /* Calcul de la longueur disponible. - * IMPORTANT : appliquer le masque SANS cap préalable. Si un cap - * `if (avail > N) avail = N` précède le masque, le compilateur - * supprime l'AND (semantically redundant). Sans cap, le compilateur - * conserve l'AND et le vérificateur en déduit avail ∈ [0, 511]. - * Cas edge : avail exactement multiple de 512 → avail & 511 = 0. */ - __u32 avail = skb->len - payload_off; - avail &= (MAX_TLS_PAYLOAD - 1); /* verifier : avail ∈ [0, 511] */ - if (avail == 0) { - bpf_ringbuf_discard(tls_evt, 0); - return TC_ACT_OK; - } - - tls_evt->payload_len = (__u16)avail; - - if (bpf_skb_load_bytes(skb, payload_off, tls_evt->payload, avail) < 0) { - bpf_ringbuf_discard(tls_evt, 0); - return TC_ACT_OK; + /* Copie bornée du payload TLS. + * Pour tout i < avail : payload + i < payload + avail ≤ data_end. + * Le vérificateur kernel ≥ 5.3 peut vérifier cette boucle sans unroll. */ + __u8 *src = (__u8 *)payload; + #pragma clang loop unroll(disable) + for (__u32 i = 0; i < MAX_TLS_PAYLOAD; i++) { + if (i >= avail) + break; + if (src + i + 1 > (__u8 *)data_end) + break; + tls_evt->payload[i] = src[i]; } bpf_ringbuf_submit(tls_evt, 0); - return TC_ACT_OK; + return XDP_PASS; } - /* --- Détection payload HTTP en clair (port 80 / 8080) --- */ + /* =================================================================== + * HTTP en clair (port 80 / 8080) + * ===================================================================*/ if (dst_port == HTTP_PORT || dst_port == HTTP_ALT_PORT) { - /* Ignorer SYN, FIN, RST : seuls les segments de données nous intéressent */ + /* Ignorer SYN, FIN, RST : seuls les segments de données */ if (tcp_flags & (TH_SYN | TH_FIN | TH_RST)) - return TC_ACT_OK; + return XDP_PASS; + if (payload >= data_end) + return XDP_PASS; - if (payload_off >= skb->len) - return TC_ACT_OK; - - __u32 avail = skb->len - payload_off; - /* Même stratégie que pour TLS : masque SANS cap préalable. - * Le compilateur conserve l'AND, le vérificateur déduit [0, 4095]. */ - avail &= 0xFFF; /* verifier : avail ∈ [0, 4095], smin ≥ 0 */ - if (avail == 0) - return TC_ACT_OK; + __u32 avail = (__u8 *)data_end - (__u8 *)payload; + if (avail > MAX_HTTP_PAYLOAD) + avail = MAX_HTTP_PAYLOAD; + /* Même barrière que pour la section TLS : force comparaison scalaire. */ + asm volatile("" : "+r"(avail)); struct http_plain_event *h_evt = bpf_ringbuf_reserve(&rb_http_plain, sizeof(*h_evt), 0); if (!h_evt) - return TC_ACT_OK; + return XDP_PASS; h_evt->src_ip = bpf_ntohl(src_ip); h_evt->dst_ip = bpf_ntohl(dst_ip); h_evt->src_port = src_port; h_evt->dst_port = dst_port; - h_evt->payload_len = (__u16)avail; h_evt->timestamp_ns = bpf_ktime_get_ns(); + h_evt->payload_len = (__u16)avail; - if (bpf_skb_load_bytes(skb, payload_off, h_evt->payload, avail) < 0) { - bpf_ringbuf_discard(h_evt, 0); - return TC_ACT_OK; + __u8 *src = (__u8 *)payload; + #pragma clang loop unroll(disable) + for (__u32 i = 0; i < MAX_HTTP_PAYLOAD; i++) { + if (i >= avail) + break; + if (src + i + 1 > (__u8 *)data_end) + break; + h_evt->payload[i] = src[i]; } bpf_ringbuf_submit(h_evt, 0); } - return TC_ACT_OK; + return XDP_PASS; } char LICENSE[] SEC("license") = "GPL"; - diff --git a/services/ja4ebpf/cmd/ja4ebpf/main.go b/services/ja4ebpf/cmd/ja4ebpf/main.go index 434d291..3ee1f3c 100644 --- a/services/ja4ebpf/cmd/ja4ebpf/main.go +++ b/services/ja4ebpf/cmd/ja4ebpf/main.go @@ -163,6 +163,7 @@ func main() { go consumeTLSEvents(ctx, ldr.TLSReader, mgr) go consumeSSLEvents(ctx, ldr.SSLReader, mgr) go consumeAcceptEvents(ctx, ldr.AcceptReader, mgr) + go consumeHTTPPlainEvents(ctx, ldr.HTTPPlainReader, mgr) log.Printf("[ja4ebpf] démon actif — en attente des événements") @@ -177,6 +178,46 @@ func main() { log.Printf("[ja4ebpf] arrêt terminé") } +// parseTCPOptions extrait le MSS et le Window Scale depuis les options TCP brutes. +// Les options TCP suivent le format TLV (Type-Length-Value), sauf les options 0 et 1. +// Retourne (mss=0, windowScale=0xFF) si les options sont absentes ou mal formées. +func parseTCPOptions(opts []byte) (mss uint16, windowScale uint8) { + windowScale = 0xFF // 0xFF = absent (convention C) + i := 0 + for i < len(opts) { + kind := opts[i] + i++ + switch kind { + case 0: // End of Options + return + case 1: // NOP — padding, pas de longueur + continue + default: + if i >= len(opts) { + return + } + length := int(opts[i]) + i++ + if length < 2 || i+length-2 > len(opts) { + return // option malformée + } + val := opts[i : i+length-2] + switch kind { + case 2: // MSS + if len(val) >= 2 { + mss = binary.BigEndian.Uint16(val[0:2]) + } + case 3: // Window Scale + if len(val) >= 1 { + windowScale = val[0] + } + } + i += length - 2 + } + } + return +} + // consumeSynEvents lit les événements TCP SYN depuis le ring buffer // et met à jour l'état L3/L4 des sessions. func consumeSynEvents(ctx context.Context, rd *ringbuf.Reader, mgr *correlation.Manager) { @@ -195,14 +236,17 @@ func consumeSynEvents(ctx context.Context, rd *ringbuf.Reader, mgr *correlation. continue } - // Taille minimale attendue (voir struct tcp_syn_event) - if len(record.RawSample) < 20 { + // struct tcp_syn_event (packed): + // src_ip(4)+dst_ip(4)+src_port(2)+dst_port(2)+ttl(1)+df_bit(1)+ip_id(2)+ + // window_size(2)+window_scale(1)+mss(2)+tcp_options_raw[40]+tcp_options_len(1)+timestamp_ns(8) + // offsets: 0 4 8 10 12 13 14 16 18 19 21 61 62 + if len(record.RawSample) < 62 { continue } data := record.RawSample - // Décoder les champs de tcp_syn_event - srcIPRaw := binary.BigEndian.Uint32(data[0:4]) + // src_ip et src_port stockés en host byte order (bpf_ntohl/bpf_ntohs dans BPF C). + srcIPRaw := binary.LittleEndian.Uint32(data[0:4]) srcPort := binary.LittleEndian.Uint16(data[8:10]) var key correlation.SessionKey @@ -212,19 +256,21 @@ func consumeSynEvents(ctx context.Context, rd *ringbuf.Reader, mgr *correlation. key.SrcIP[3] = byte(srcIPRaw) key.SrcPort = srcPort - ttl := data[4] - dfBit := data[5] != 0 - ipID := binary.LittleEndian.Uint16(data[6:8]) - windowSize := binary.LittleEndian.Uint16(data[10:12]) - windowScale := data[12] - mss := binary.LittleEndian.Uint16(data[13:15]) + // Champs IP/TCP aux offsets corrects (dst_ip occupe les octets 4-7) + ttl := data[12] + dfBit := data[13] != 0 + ipID := binary.LittleEndian.Uint16(data[14:16]) + windowSize := binary.LittleEndian.Uint16(data[16:18]) - optLen := int(data[55]) + optLen := int(data[61]) if optLen > 40 { optLen = 40 } tcpOpts := make([]byte, optLen) - copy(tcpOpts, data[15:15+optLen]) + copy(tcpOpts, data[21:21+optLen]) + + // Analyser les options TCP brutes pour extraire MSS et Window Scale + mss, windowScale := parseTCPOptions(tcpOpts) mgr.Update(key, func(s *correlation.SessionState) { s.L3L4 = &correlation.L3L4{ @@ -237,6 +283,10 @@ func consumeSynEvents(ctx context.Context, rd *ringbuf.Reader, mgr *correlation. TCPOptionsRaw: tcpOpts, SYNTimestamp: time.Now(), } + // Si TLS est déjà présent (arrivé avant SYN), marquer la session corrélée. + if s.TLS != nil { + s.Correlated = true + } }) } } @@ -259,18 +309,20 @@ func consumeTLSEvents(ctx context.Context, rd *ringbuf.Reader, mgr *correlation. continue } - // Taille minimale : src_ip(4) + src_port(2) + payload[512] + payload_len(2) - if len(record.RawSample) < 8 { + // struct tls_hello_event (packed): + // src_ip(4) + src_port(2) + payload[2048] + payload_len(2) + timestamp_ns(8) + // offsets: 0 4 6 2054 2056 + if len(record.RawSample) < 2056 { continue } data := record.RawSample - srcIPRaw := binary.BigEndian.Uint32(data[0:4]) - srcPort := binary.LittleEndian.Uint16(data[4:6]) - payloadLen := binary.LittleEndian.Uint16(data[518:520]) + srcIPRaw := binary.LittleEndian.Uint32(data[0:4]) + srcPort := binary.LittleEndian.Uint16(data[4:6]) + payloadLen := binary.LittleEndian.Uint16(data[2054:2056]) - if int(payloadLen) > 512 { - payloadLen = 512 + if int(payloadLen) > 2048 { + payloadLen = 2048 } payload := make([]byte, payloadLen) copy(payload, data[6:6+payloadLen]) @@ -285,6 +337,7 @@ func consumeTLSEvents(ctx context.Context, rd *ringbuf.Reader, mgr *correlation. // Parser le ClientHello et calculer JA4 ch, err := parser.ParseClientHello(payload) if err != nil { + log.Printf("[warn] TLS parse error: %v", err) continue } @@ -500,7 +553,92 @@ func consumeAcceptEvents(ctx context.Context, rd *ringbuf.Reader, mgr *correlati key.SrcIP[3] = byte(srcIPRaw) key.SrcPort = srcPort + // Ignorer les événements accept4 sans IP valide (bpf_probe_read_user a échoué) + if srcIPRaw == 0 && srcPort == 0 { + continue + } + // S'assurer que la session existe mgr.GetOrCreate(key) } } + +// consumeHTTPPlainEvents lit les payloads HTTP en clair depuis le ring buffer XDP. +// Parse la requête HTTP/1.x ou détecte la préface HTTP/2 pour les connexions +// non-chiffrées sur les ports 80/8080. +func consumeHTTPPlainEvents(ctx context.Context, rd *ringbuf.Reader, mgr *correlation.Manager) { + for { + select { + case <-ctx.Done(): + return + default: + } + + record, err := rd.Read() + if err != nil { + if err == ringbuf.ErrClosed { + return + } + continue + } + + data := record.RawSample + // struct http_plain_event: src_ip(4)+dst_ip(4)+src_port(2)+dst_port(2)+payload(4096)+payload_len(2)+timestamp_ns(8) + if len(data) < 14 { + continue + } + + // src_ip et src_port en host byte order (bpf_ntohl appliqué dans tc_capture.c) + srcIPRaw := binary.LittleEndian.Uint32(data[0:4]) + srcPort := binary.LittleEndian.Uint16(data[8:10]) + + if srcIPRaw == 0 && srcPort == 0 { + continue + } + + var key correlation.SessionKey + key.SrcIP[0] = byte(srcIPRaw >> 24) + key.SrcIP[1] = byte(srcIPRaw >> 16) + key.SrcIP[2] = byte(srcIPRaw >> 8) + key.SrcIP[3] = byte(srcIPRaw) + key.SrcPort = srcPort + + // Extraire le payload HTTP + if len(data) < 4110 { + continue + } + payloadLen := int(binary.LittleEndian.Uint16(data[4108:4110])) + if payloadLen > 4096 { + payloadLen = 4096 + } + if payloadLen == 0 { + continue + } + if 12+payloadLen > len(data) { + payloadLen = len(data) - 12 + } + httpData := data[12 : 12+payloadLen] + + // Routeur Magic Bytes : HTTP/1.x uniquement sur port 80 + if parser.IsHTTP1Request(httpData) { + req := parser.ParseHTTP1Request(httpData) + if req == nil { + continue + } + mgr.Update(key, func(s *correlation.SessionState) { + s.Requests = append(s.Requests, correlation.HTTPRequest{ + Timestamp: time.Now(), + Method: req.Method, + Path: req.Path, + QueryString: req.Query, + HeaderOrder: req.Headers, + HeaderOrderSig: req.HeaderSig, + }) + // Corréler si L3/L4 est déjà présent (TCP SYN capturé) + if s.L3L4 != nil { + s.Correlated = true + } + }) + } + } +} diff --git a/services/ja4ebpf/internal/loader/ja4ssl_x86_bpfel.go b/services/ja4ebpf/internal/loader/ja4ssl_x86_bpfel.go new file mode 100644 index 0000000..fb16d7b --- /dev/null +++ b/services/ja4ebpf/internal/loader/ja4ssl_x86_bpfel.go @@ -0,0 +1,183 @@ +// Code generated by bpf2go; DO NOT EDIT. +//go:build 386 || amd64 + +package loader + +import ( + "bytes" + _ "embed" + "fmt" + "io" + + "github.com/cilium/ebpf" +) + +type Ja4SslAcceptEvent struct { + PidTgid uint64 + Fd uint32 + SrcIp uint32 + SrcPort uint16 + TimestampNs uint64 +} + +type Ja4SslAcceptKey struct { + PidTgid uint64 + Fd uint32 +} + +type Ja4SslSslConnInfo struct { + Fd uint32 + SrcIp uint32 + SrcPort uint16 +} + +type Ja4SslSslReadArgs struct { + SslPtr uint64 + BufPtr uint64 + Num uint32 +} + +// LoadJa4Ssl returns the embedded CollectionSpec for Ja4Ssl. +func LoadJa4Ssl() (*ebpf.CollectionSpec, error) { + reader := bytes.NewReader(_Ja4SslBytes) + spec, err := ebpf.LoadCollectionSpecFromReader(reader) + if err != nil { + return nil, fmt.Errorf("can't load Ja4Ssl: %w", err) + } + + return spec, err +} + +// LoadJa4SslObjects loads Ja4Ssl and converts it into a struct. +// +// The following types are suitable as obj argument: +// +// *Ja4SslObjects +// *Ja4SslPrograms +// *Ja4SslMaps +// +// See ebpf.CollectionSpec.LoadAndAssign documentation for details. +func LoadJa4SslObjects(obj interface{}, opts *ebpf.CollectionOptions) error { + spec, err := LoadJa4Ssl() + if err != nil { + return err + } + + return spec.LoadAndAssign(obj, opts) +} + +// Ja4SslSpecs contains maps and programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type Ja4SslSpecs struct { + Ja4SslProgramSpecs + Ja4SslMapSpecs +} + +// Ja4SslSpecs contains programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type Ja4SslProgramSpecs struct { + KprobeAccept4Entry *ebpf.ProgramSpec `ebpf:"kprobe_accept4_entry"` + KretprobeAccept4Exit *ebpf.ProgramSpec `ebpf:"kretprobe_accept4_exit"` + UprobeSslReadEntry *ebpf.ProgramSpec `ebpf:"uprobe_ssl_read_entry"` + UprobeSslSetFd *ebpf.ProgramSpec `ebpf:"uprobe_ssl_set_fd"` + UretprobeSslReadExit *ebpf.ProgramSpec `ebpf:"uretprobe_ssl_read_exit"` +} + +// Ja4SslMapSpecs contains maps before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type Ja4SslMapSpecs struct { + AcceptArgsMap *ebpf.MapSpec `ebpf:"accept_args_map"` + AcceptMap *ebpf.MapSpec `ebpf:"accept_map"` + FdConnMap *ebpf.MapSpec `ebpf:"fd_conn_map"` + RbAccept *ebpf.MapSpec `ebpf:"rb_accept"` + RbHttpPlain *ebpf.MapSpec `ebpf:"rb_http_plain"` + RbSslData *ebpf.MapSpec `ebpf:"rb_ssl_data"` + RbTcpSyn *ebpf.MapSpec `ebpf:"rb_tcp_syn"` + RbTlsHello *ebpf.MapSpec `ebpf:"rb_tls_hello"` + SslArgsMap *ebpf.MapSpec `ebpf:"ssl_args_map"` + SslConnMap *ebpf.MapSpec `ebpf:"ssl_conn_map"` +} + +// Ja4SslObjects contains all objects after they have been loaded into the kernel. +// +// It can be passed to LoadJa4SslObjects or ebpf.CollectionSpec.LoadAndAssign. +type Ja4SslObjects struct { + Ja4SslPrograms + Ja4SslMaps +} + +func (o *Ja4SslObjects) Close() error { + return _Ja4SslClose( + &o.Ja4SslPrograms, + &o.Ja4SslMaps, + ) +} + +// Ja4SslMaps contains all maps after they have been loaded into the kernel. +// +// It can be passed to LoadJa4SslObjects or ebpf.CollectionSpec.LoadAndAssign. +type Ja4SslMaps struct { + AcceptArgsMap *ebpf.Map `ebpf:"accept_args_map"` + AcceptMap *ebpf.Map `ebpf:"accept_map"` + FdConnMap *ebpf.Map `ebpf:"fd_conn_map"` + RbAccept *ebpf.Map `ebpf:"rb_accept"` + RbHttpPlain *ebpf.Map `ebpf:"rb_http_plain"` + RbSslData *ebpf.Map `ebpf:"rb_ssl_data"` + RbTcpSyn *ebpf.Map `ebpf:"rb_tcp_syn"` + RbTlsHello *ebpf.Map `ebpf:"rb_tls_hello"` + SslArgsMap *ebpf.Map `ebpf:"ssl_args_map"` + SslConnMap *ebpf.Map `ebpf:"ssl_conn_map"` +} + +func (m *Ja4SslMaps) Close() error { + return _Ja4SslClose( + m.AcceptArgsMap, + m.AcceptMap, + m.FdConnMap, + m.RbAccept, + m.RbHttpPlain, + m.RbSslData, + m.RbTcpSyn, + m.RbTlsHello, + m.SslArgsMap, + m.SslConnMap, + ) +} + +// Ja4SslPrograms contains all programs after they have been loaded into the kernel. +// +// It can be passed to LoadJa4SslObjects or ebpf.CollectionSpec.LoadAndAssign. +type Ja4SslPrograms struct { + KprobeAccept4Entry *ebpf.Program `ebpf:"kprobe_accept4_entry"` + KretprobeAccept4Exit *ebpf.Program `ebpf:"kretprobe_accept4_exit"` + UprobeSslReadEntry *ebpf.Program `ebpf:"uprobe_ssl_read_entry"` + UprobeSslSetFd *ebpf.Program `ebpf:"uprobe_ssl_set_fd"` + UretprobeSslReadExit *ebpf.Program `ebpf:"uretprobe_ssl_read_exit"` +} + +func (p *Ja4SslPrograms) Close() error { + return _Ja4SslClose( + p.KprobeAccept4Entry, + p.KretprobeAccept4Exit, + p.UprobeSslReadEntry, + p.UprobeSslSetFd, + p.UretprobeSslReadExit, + ) +} + +func _Ja4SslClose(closers ...io.Closer) error { + for _, closer := range closers { + if err := closer.Close(); err != nil { + return err + } + } + return nil +} + +// Do not access this directly. +// +//go:embed ja4ssl_x86_bpfel.o +var _Ja4SslBytes []byte diff --git a/services/ja4ebpf/internal/loader/ja4ssl_x86_bpfel.o b/services/ja4ebpf/internal/loader/ja4ssl_x86_bpfel.o new file mode 100644 index 0000000..676b7f4 Binary files /dev/null and b/services/ja4ebpf/internal/loader/ja4ssl_x86_bpfel.o differ diff --git a/services/ja4ebpf/internal/loader/ja4tc_x86_bpfel.go b/services/ja4ebpf/internal/loader/ja4tc_x86_bpfel.go new file mode 100644 index 0000000..549e3eb --- /dev/null +++ b/services/ja4ebpf/internal/loader/ja4tc_x86_bpfel.go @@ -0,0 +1,168 @@ +// Code generated by bpf2go; DO NOT EDIT. +//go:build 386 || amd64 + +package loader + +import ( + "bytes" + _ "embed" + "fmt" + "io" + + "github.com/cilium/ebpf" +) + +type Ja4TcAcceptEvent struct { + PidTgid uint64 + Fd uint32 + SrcIp uint32 + SrcPort uint16 + TimestampNs uint64 +} + +type Ja4TcAcceptKey struct { + PidTgid uint64 + Fd uint32 +} + +type Ja4TcSslConnInfo struct { + Fd uint32 + SrcIp uint32 + SrcPort uint16 +} + +type Ja4TcSslReadArgs struct { + SslPtr uint64 + BufPtr uint64 + Num uint32 +} + +// LoadJa4Tc returns the embedded CollectionSpec for Ja4Tc. +func LoadJa4Tc() (*ebpf.CollectionSpec, error) { + reader := bytes.NewReader(_Ja4TcBytes) + spec, err := ebpf.LoadCollectionSpecFromReader(reader) + if err != nil { + return nil, fmt.Errorf("can't load Ja4Tc: %w", err) + } + + return spec, err +} + +// LoadJa4TcObjects loads Ja4Tc and converts it into a struct. +// +// The following types are suitable as obj argument: +// +// *Ja4TcObjects +// *Ja4TcPrograms +// *Ja4TcMaps +// +// See ebpf.CollectionSpec.LoadAndAssign documentation for details. +func LoadJa4TcObjects(obj interface{}, opts *ebpf.CollectionOptions) error { + spec, err := LoadJa4Tc() + if err != nil { + return err + } + + return spec.LoadAndAssign(obj, opts) +} + +// Ja4TcSpecs contains maps and programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type Ja4TcSpecs struct { + Ja4TcProgramSpecs + Ja4TcMapSpecs +} + +// Ja4TcSpecs contains programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type Ja4TcProgramSpecs struct { + CaptureXdp *ebpf.ProgramSpec `ebpf:"capture_xdp"` +} + +// Ja4TcMapSpecs contains maps before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type Ja4TcMapSpecs struct { + AcceptMap *ebpf.MapSpec `ebpf:"accept_map"` + FdConnMap *ebpf.MapSpec `ebpf:"fd_conn_map"` + RbAccept *ebpf.MapSpec `ebpf:"rb_accept"` + RbHttpPlain *ebpf.MapSpec `ebpf:"rb_http_plain"` + RbSslData *ebpf.MapSpec `ebpf:"rb_ssl_data"` + RbTcpSyn *ebpf.MapSpec `ebpf:"rb_tcp_syn"` + RbTlsHello *ebpf.MapSpec `ebpf:"rb_tls_hello"` + SslArgsMap *ebpf.MapSpec `ebpf:"ssl_args_map"` + SslConnMap *ebpf.MapSpec `ebpf:"ssl_conn_map"` +} + +// Ja4TcObjects contains all objects after they have been loaded into the kernel. +// +// It can be passed to LoadJa4TcObjects or ebpf.CollectionSpec.LoadAndAssign. +type Ja4TcObjects struct { + Ja4TcPrograms + Ja4TcMaps +} + +func (o *Ja4TcObjects) Close() error { + return _Ja4TcClose( + &o.Ja4TcPrograms, + &o.Ja4TcMaps, + ) +} + +// Ja4TcMaps contains all maps after they have been loaded into the kernel. +// +// It can be passed to LoadJa4TcObjects or ebpf.CollectionSpec.LoadAndAssign. +type Ja4TcMaps struct { + AcceptMap *ebpf.Map `ebpf:"accept_map"` + FdConnMap *ebpf.Map `ebpf:"fd_conn_map"` + RbAccept *ebpf.Map `ebpf:"rb_accept"` + RbHttpPlain *ebpf.Map `ebpf:"rb_http_plain"` + RbSslData *ebpf.Map `ebpf:"rb_ssl_data"` + RbTcpSyn *ebpf.Map `ebpf:"rb_tcp_syn"` + RbTlsHello *ebpf.Map `ebpf:"rb_tls_hello"` + SslArgsMap *ebpf.Map `ebpf:"ssl_args_map"` + SslConnMap *ebpf.Map `ebpf:"ssl_conn_map"` +} + +func (m *Ja4TcMaps) Close() error { + return _Ja4TcClose( + m.AcceptMap, + m.FdConnMap, + m.RbAccept, + m.RbHttpPlain, + m.RbSslData, + m.RbTcpSyn, + m.RbTlsHello, + m.SslArgsMap, + m.SslConnMap, + ) +} + +// Ja4TcPrograms contains all programs after they have been loaded into the kernel. +// +// It can be passed to LoadJa4TcObjects or ebpf.CollectionSpec.LoadAndAssign. +type Ja4TcPrograms struct { + CaptureXdp *ebpf.Program `ebpf:"capture_xdp"` +} + +func (p *Ja4TcPrograms) Close() error { + return _Ja4TcClose( + p.CaptureXdp, + ) +} + +func _Ja4TcClose(closers ...io.Closer) error { + for _, closer := range closers { + if err := closer.Close(); err != nil { + return err + } + } + return nil +} + +// Do not access this directly. +// +//go:embed ja4tc_x86_bpfel.o +var _Ja4TcBytes []byte diff --git a/services/ja4ebpf/internal/loader/ja4tc_x86_bpfel.o b/services/ja4ebpf/internal/loader/ja4tc_x86_bpfel.o new file mode 100644 index 0000000..9dda9a6 Binary files /dev/null and b/services/ja4ebpf/internal/loader/ja4tc_x86_bpfel.o differ diff --git a/services/ja4ebpf/internal/loader/loader.go b/services/ja4ebpf/internal/loader/loader.go index caf07a9..7267a7f 100644 --- a/services/ja4ebpf/internal/loader/loader.go +++ b/services/ja4ebpf/internal/loader/loader.go @@ -10,7 +10,6 @@ import ( "net" "os" - "github.com/cilium/ebpf" "github.com/cilium/ebpf/link" "github.com/cilium/ebpf/ringbuf" "github.com/cilium/ebpf/rlimit" @@ -122,23 +121,31 @@ func New() (*Loader, error) { }, nil } -// AttachTC attache le programme TC ingress sur l'interface réseau spécifiée. -// Utilise TCX (TC eXpress) disponible depuis le noyau 6.6+. +// AttachTC attache le programme XDP sur l'interface réseau spécifiée. +// Essaie le mode natif XDP (driver support) puis se replie sur le mode générique +// (SKB_MODE, compatible kernel ≥ 4.8, fonctionne dans les VMs). func (l *Loader) AttachTC(iface string) error { - // Résoudre l'interface réseau par son nom netIface, err := net.InterfaceByName(iface) if err != nil { return fmt.Errorf("interface réseau %q introuvable: %w", iface, err) } - // Attacher le programme TC en ingress via TCX - lnk, err := link.AttachTCX(link.TCXOptions{ + // Mode natif (meilleure performance sur serveurs avec NIC compatible XDP) + lnk, err := link.AttachXDP(link.XDPOptions{ Interface: netIface.Index, - Program: l.tcObjs.CaptureTcIngress, - Attach: ebpf.AttachTCXIngress, + Program: l.tcObjs.CaptureXdp, + Flags: link.XDPDriverMode, }) if err != nil { - return fmt.Errorf("attachement TC ingress sur %q: %w", iface, err) + // Repli sur le mode générique (VMs, NICs sans driver XDP natif) + lnk, err = link.AttachXDP(link.XDPOptions{ + Interface: netIface.Index, + Program: l.tcObjs.CaptureXdp, + Flags: link.XDPGenericMode, + }) + if err != nil { + return fmt.Errorf("attachement XDP sur %q (natif et générique): %w", iface, err) + } } l.tcLink = lnk diff --git a/services/ja4ebpf/internal/parser/tls.go b/services/ja4ebpf/internal/parser/tls.go index 96560fa..5028acc 100644 --- a/services/ja4ebpf/internal/parser/tls.go +++ b/services/ja4ebpf/internal/parser/tls.go @@ -47,8 +47,12 @@ func ParseClientHello(payload []byte) (*ClientHello, error) { recordVersion := binary.BigEndian.Uint16(payload[1:3]) recordLength := int(binary.BigEndian.Uint16(payload[3:5])) - if len(payload) < 5+recordLength { - return nil, fmt.Errorf("record TLS tronqué: attendu %d octets, reçu %d", 5+recordLength, len(payload)) + // Le XDP capture au maximum MAX_TLS_PAYLOAD (512) octets. + // Si la taille du record TLS dépasse les données disponibles, on travaille + // avec ce qu'on a (le ClientHello est toujours en début de record). + available := len(payload) - 5 + if recordLength > available { + recordLength = available } // Parsing du message Handshake @@ -64,8 +68,9 @@ func ParseClientHello(payload []byte) (*ClientHello, error) { // Longueur du ClientHello (3 octets big-endian) chLen := int(uint32(hs[1])<<16 | uint32(hs[2])<<8 | uint32(hs[3])) - if len(hs) < 4+chLen { - return nil, fmt.Errorf("ClientHello tronqué: attendu %d octets", 4+chLen) + // Tolérance à la troncature XDP : on travaille avec ce qu'on a + if chLen > len(hs)-4 { + chLen = len(hs) - 4 } ch := &ClientHello{RecordVersion: recordVersion} @@ -98,9 +103,9 @@ func ParseClientHello(payload []byte) (*ClientHello, error) { csLen := int(binary.BigEndian.Uint16(data[offset : offset+2])) offset += 2 if len(data) < offset+csLen { - return nil, fmt.Errorf("ClientHello: cipher suites tronquées") + csLen = len(data) - offset // troncature tolérée } - for i := 0; i < csLen; i += 2 { + for i := 0; i+2 <= csLen; i += 2 { cs := binary.BigEndian.Uint16(data[offset+i : offset+i+2]) ch.CipherSuites = append(ch.CipherSuites, cs) } @@ -108,12 +113,12 @@ func ParseClientHello(payload []byte) (*ClientHello, error) { // Compression Methods (longueur 1 octet + données) if len(data) < offset+1 { - return nil, fmt.Errorf("ClientHello: longueur compression manquante") + return ch, nil // troncature : retourner ce qu'on a } compLen := int(data[offset]) offset++ if len(data) < offset+compLen { - return nil, fmt.Errorf("ClientHello: méthodes de compression tronquées") + compLen = len(data) - offset } ch.CompressionMethods = data[offset : offset+compLen] offset += compLen @@ -125,7 +130,7 @@ func ParseClientHello(payload []byte) (*ClientHello, error) { extTotalLen := int(binary.BigEndian.Uint16(data[offset : offset+2])) offset += 2 if len(data) < offset+extTotalLen { - return nil, fmt.Errorf("ClientHello: extensions tronquées") + extTotalLen = len(data) - offset // troncature tolérée } // Parsing des extensions diff --git a/tests/vm/Vagrantfile b/tests/vm/Vagrantfile index a6243da..a38061f 100644 --- a/tests/vm/Vagrantfile +++ b/tests/vm/Vagrantfile @@ -6,16 +6,15 @@ # Fournit un environnement kernel complet pour les tests eBPF : # - tracefs / debugfs montés # - perf_kprobe PMU disponible -# - uprobes fonctionnels avec accept4 kprobe/tracepoint +# - uprobes fonctionnels avec accept4 tracepoints # # Prérequis (host Ubuntu) : -# sudo apt-get install -y vagrant libvirt-daemon-system libvirt-clients \ -# qemu-kvm ruby-libvirt +# sudo apt-get install -y libvirt-daemon-system libvirt-clients qemu-kvm libvirt-dev ruby-dev # vagrant plugin install vagrant-libvirt # sudo usermod -aG libvirt,kvm $USER # puis se reconnecter # # Utilisation : -# vagrant up # créer + provisionner la VM (première fois ~5 min) +# vagrant up # créer + provisionner (~5 min) # vagrant ssh # connexion SSH # make test-vm-nginx # lancer les tests depuis le host # vagrant destroy -f # détruire la VM @@ -23,44 +22,39 @@ Vagrant.configure("2") do |config| - # ── Box Rocky Linux 9 ────────────────────────────────────────────────────── + # ── Box Rocky Linux 9 avec provider libvirt (image qcow2) ───────────────── config.vm.box = "generic/rocky9" - # ── Réseau : IP privée pour accès depuis le host ─────────────────────────── - config.vm.network "private_network", ip: "192.168.56.10" + # ── Désactiver synced_folder par défaut (utiliser rsync explicitement) ───── + config.vm.synced_folder ".", "/vagrant", disabled: true - # ── Ressources VM ───────────────────────────────────────────────────────── + # ── Provider libvirt ─────────────────────────────────────────────────────── config.vm.provider :libvirt do |v| - v.cpus = 4 - v.memory = 4096 - v.nested = false # pas besoin de virtualisation imbriquée - # Pour VirtualBox (fallback) + v.cpus = 4 + v.memory = 4096 + v.nested = false + v.cpu_mode = "host-passthrough" # expose les capacités CPU hôte → KVM perf + v.driver = "kvm" + v.disk_bus = "virtio" + v.nic_model_type = "virtio" end - config.vm.provider :virtualbox do |v| - v.cpus = 4 - v.memory = 4096 - v.customize ["modifyvm", :id, "--nicpromisc2", "allow-all"] - end - - # ── Montage du projet ───────────────────────────────────────────────────── - # Le répertoire racine du projet est monté dans /ja4-platform + # ── Synchronisation du projet via rsync ──────────────────────────────────── config.vm.synced_folder "../..", "/ja4-platform", type: "rsync", - rsync__exclude: [".git/", "old/", "*.rpm", "services/*/target/"] + rsync__exclude: [".git/", "old/", "*.rpm", "dist/"] - # ── Provisioning ───────────────────────────────────────────────────────── + # ── Provisioning ─────────────────────────────────────────────────────────── config.vm.provision "shell", path: "provision.sh" - # ── Message post-démarrage ──────────────────────────────────────────────── + # ── Message post-démarrage ───────────────────────────────────────────────── config.vm.post_up_message = <<~MSG VM ja4ebpf prête ! - - Depuis le répertoire tests/vm/ : - vagrant ssh # connexion interactive - make -C ../.. test-vm-nginx # lancer le test nginx - make -C ../.. test-vm-matrix # lancer tous les tests - - IP de la VM : 192.168.56.10 + + Depuis la racine du projet : + make vm-ssh # connexion interactive + make test-vm-nginx # test nginx complet (L3/L4 + TLS + L7) + make test-vm-all # tous les tests + make vm-rebuild-ja4ebpf # resynchroniser + recompiler après modif MSG end diff --git a/tests/vm/run-tests-vm.sh b/tests/vm/run-tests-vm.sh index 4a1062b..486ed0f 100755 --- a/tests/vm/run-tests-vm.sh +++ b/tests/vm/run-tests-vm.sh @@ -15,6 +15,9 @@ # ============================================================================= set -euo pipefail +# S'assurer que /usr/local/bin et go sont dans PATH (nécessaire pour sudo bash) +export PATH="/usr/local/bin:/usr/local/go/bin:$PATH" + STACK="${1:-nginx}" KEEP_RUNNING="${KEEP_RUNNING:-false}" PROJECT="/ja4-platform" @@ -50,7 +53,7 @@ check_prerequisites() { cd "$PROJECT/services/ja4ebpf" export PATH="/usr/local/go/bin:$PATH" GOWORK=off go generate ./internal/loader/ 2>&1 | tail -3 - GOWORK=off CGO_ENABLED=0 go build -o /usr/local/bin/ja4ebpf ./cmd/ja4ebpf/ + GOWORK=off CGO_ENABLED=0 go build -o /tmp/ja4ebpf_new ./cmd/ja4ebpf/ && mv /tmp/ja4ebpf_new /usr/local/bin/ja4ebpf } command -v docker >/dev/null 2>&1 || { fail "Docker non installé"; exit 1; } @@ -103,6 +106,8 @@ setup_nginx() { # Créer les fichiers de test mkdir -p /var/www/html + # /run/nginx est un tmpfs recréé à chaque boot, nginx en a besoin pour son PID + mkdir -p /run/nginx echo '{"status":"ok","stack":"nginx-vm"}' > /var/www/html/health for p in data api/users api/data/test; do mkdir -p "/var/www/html/$(dirname $p)" @@ -144,7 +149,7 @@ EOF # Lancer avec les capabilities nécessaires # Dans la VM (root), on peut lancer directement - ja4ebpf -config /tmp/ja4ebpf.yml > /tmp/ja4ebpf.log 2>&1 & + JA4EBPF_CONFIG=/tmp/ja4ebpf.yml ja4ebpf > /tmp/ja4ebpf.log 2>&1 & JA4EBPF_PID=$! sleep 3