diff --git a/services/ja4ebpf/bpf/bpf_types.h b/services/ja4ebpf/bpf/bpf_types.h index 6fe4310..ed55b6d 100644 --- a/services/ja4ebpf/bpf/bpf_types.h +++ b/services/ja4ebpf/bpf/bpf_types.h @@ -99,6 +99,35 @@ struct http_plain_event { __u64 timestamp_ns; /* horodatage kernel */ } __attribute__((packed)); +/* --------------------------------------------------------------------------- + * Événement Nginx HTTP : requête HTTP complète depuis nginx (via uprobes) + * ---------------------------------------------------------------------------*/ +struct nginx_http_event { + __u64 pid_tgid; /* PID+TGID du processus nginx */ + __u32 fd; /* descripteur de fichier socket (corrélation TC) */ + __u32 src_ip; /* IP source du client (host byte order, corrélation TC) */ + __u16 src_port; /* port source du client (corrélation TC) */ + __u64 timestamp_ns; /* horodatage kernel */ + __u8 http_method[16]; /* méthode HTTP (GET, POST, etc.) */ + __u8 uri[256]; /* URI demandée (sans query string) */ + __u8 query[128]; /* query string */ + __u8 data[3640]; /* données HTTP brutes (headers + body) */ + __u32 method_len; /* longueur méthode */ + __u32 uri_len; /* longueur URI */ + __u32 query_len; /* longueur query string */ + __u32 body_len; /* longueur body (0 pour l'instant) */ + __u32 data_len; /* longueur données brutes */ +} __attribute__((packed)); + +/* --------------------------------------------------------------------------- + * Arguments sauvegardés à l'entrée de read() (pour l'uretprobe nginx) + * ---------------------------------------------------------------------------*/ +struct nginx_read_args { + __s32 fd; /* descripteur de fichier */ + __u64 buf_ptr; /* pointeur vers buffer de réception */ + __u64 count; /* taille maximale demandée */ +} __attribute__((packed)); + /* --------------------------------------------------------------------------- * Arguments sauvegardés à l'entrée de SSL_read (pour l'uretprobe) * ---------------------------------------------------------------------------*/ @@ -164,6 +193,13 @@ struct { __uint(value_size, sizeof(__u32)); } pb_http_plain SEC(".maps"); +/* Perf event array : requêtes HTTP complètes depuis nginx (kernel 4.4+) */ +struct { + __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY); + __uint(key_size, sizeof(__u32)); + __uint(value_size, sizeof(__u32)); +} pb_ginx_http SEC(".maps"); + /* ── PERCPU_ARRAY temporaires pour les structs > 512o (stack eBPF) ──── */ /* TLS hello event : 2064 octets, ne tient pas sur la stack */ struct { @@ -221,3 +257,19 @@ struct { __type(value, struct ssl_conn_info); } fd_conn_map SEC(".maps"); +/* Hash map : pid_tgid → nginx_read_args (arguments read entry pour nginx) */ +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, 10240); + __type(key, __u64); + __type(value, struct nginx_read_args); +} nginx_read_args_map SEC(".maps"); + +/* PERCPU_ARRAY temporaire pour nginx_http_event (taille ~4KB) */ +struct { + __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); + __uint(max_entries, 1); + __type(key, __u32); + __type(value, struct nginx_http_event); +} __nginx_buf SEC(".maps"); + diff --git a/services/ja4ebpf/bpf/uprobe_nginx.c b/services/ja4ebpf/bpf/uprobe_nginx.c new file mode 100644 index 0000000..f83d087 --- /dev/null +++ b/services/ja4ebpf/bpf/uprobe_nginx.c @@ -0,0 +1,110 @@ +/* uprobe_nginx.c — Uprobes simplifiés pour capturer le trafic HTTP depuis nginx + * + * Version simplifiée qui utilise : + * - read() syscall pour capturer les données lues par nginx depuis le socket + * - Corrélation via fd entre TC (métadonnées L3/L4) et nginx (données L7) + * + * ============================================================================ + */ + +#include "vmlinux.h" +#include +#include +#include "bpf_types.h" + +/* Taille maximale d'une capture read() nginx */ +#define MAX_NGINX_READ_SIZE 4096 + +/* ============================================================================ + * uprobe_read_entry — Entrée de read() dans nginx + * + * Sauvegarde les arguments (fd, buf, count) pour l'uretprobe correspondant. + * ============================================================================ + */ +SEC("uprobe/read_entry") +int uprobe_read_entry(struct pt_regs *ctx) +{ + __u64 pid_tgid = bpf_get_current_pid_tgid(); + + /* Sauvegarder les arguments pour l'uretprobe */ + struct nginx_read_args args = {}; + args.fd = (__s32)PT_REGS_PARM1(ctx); + args.buf_ptr = (__u64)PT_REGS_PARM2(ctx); + args.count = (__u64)PT_REGS_PARM3(ctx); + + /* Stocker dans une map hash pour récupération dans l'uretprobe */ + bpf_map_update_elem(&nginx_read_args_map, &pid_tgid, &args, BPF_ANY); + + return 0; +} + +/* ============================================================================ + * uretprobe_read_exit — Fin de read() dans nginx + * + * Capture les données lues depuis le socket client. + * Version simplifiée : capture brute des données, parsing HTTP côté userspace. + * ============================================================================ + */ +SEC("uretprobe/read_exit") +int uretprobe_read_exit(struct pt_regs *ctx) +{ + __u64 pid_tgid = bpf_get_current_pid_tgid(); + + /* Récupérer les arguments sauvegardés */ + struct nginx_read_args *args = bpf_map_lookup_elem(&nginx_read_args_map, &pid_tgid); + if (!args) + return 0; + + /* Vérifier que la lecture a réussi (valeur de retour > 0) */ + long retval = PT_REGS_RC(ctx); + if (retval <= 0) { + bpf_map_delete_elem(&nginx_read_args_map, &pid_tgid); + return 0; + } + + /* Limiter la capture */ + __u32 data_len = retval; + if (data_len > MAX_NGINX_READ_SIZE) + data_len = MAX_NGINX_READ_SIZE; + + /* Buffer PERCPU */ + __u32 zero = 0; + struct nginx_http_event *evt = bpf_map_lookup_elem(&__nginx_buf, &zero); + if (!evt) { + bpf_map_delete_elem(&nginx_read_args_map, &pid_tgid); + return 0; + } + + /* Initialiser l'événement */ + evt->pid_tgid = pid_tgid; + evt->fd = args->fd; + evt->src_ip = 0; /* Sera rempli via corrélation TC */ + evt->src_port = 0; + evt->timestamp_ns = bpf_ktime_get_ns(); + evt->method_len = 0; + evt->uri_len = 0; + evt->query_len = 0; + evt->body_len = 0; + evt->data_len = 0; + + /* Copier les données brutes depuis le buffer nginx */ + if (data_len > 0) { + /* Limiter à la taille du champ data (3640 octets) */ + __u32 copy_len = data_len; + if (copy_len > sizeof(evt->data)) + copy_len = sizeof(evt->data); + bpf_probe_read_user(evt->data, copy_len, (void *)args->buf_ptr); + evt->data_len = copy_len; + } + + /* Émettre l'événement brut vers userspace */ + /* Le parsing HTTP sera fait côté userspace pour éviter */ + /* de dépasser la limite d'instructions BPF */ + bpf_perf_event_output(ctx, &pb_ginx_http, BPF_F_CURRENT_CPU, + evt, sizeof(*evt)); + + bpf_map_delete_elem(&nginx_read_args_map, &pid_tgid); + return 0; +} + +char LICENSE[] SEC("license") = "GPL"; diff --git a/services/ja4ebpf/internal/loader/loader.go b/services/ja4ebpf/internal/loader/loader.go index d6d92c8..51a6e15 100644 --- a/services/ja4ebpf/internal/loader/loader.go +++ b/services/ja4ebpf/internal/loader/loader.go @@ -19,6 +19,7 @@ import ( //go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc clang -target amd64 -cflags "-O2 -g -Wall -D__TARGET_ARCH_x86 -Wno-pass-failed" Ja4Tc ../../bpf/tc_capture.c -- -I../../bpf/headers //go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc clang -target amd64 -cflags "-O2 -g -Wall -D__TARGET_ARCH_x86 -Wno-pass-failed" Ja4Ssl ../../bpf/uprobe_ssl.c -- -I../../bpf/headers +//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc clang -target amd64 -cflags "-O2 -g -Wall -D__TARGET_ARCH_x86 -Wno-pass-failed" Ja4Nginx ../../bpf/uprobe_nginx.c -- -I../../bpf/headers // perCPUBufferSize est la taille du buffer perf per-CPU en octets (256 KB). const perCPUBufferSize = 256 * 1024 @@ -28,6 +29,7 @@ const perCPUBufferSize = 256 * 1024 type Loader struct { tcObjs *Ja4TcObjects // généré par bpf2go (tc_capture.c) sslObjs *Ja4SslObjects // généré par bpf2go (uprobe_ssl.c) + nginxObjs *Ja4NginxObjects // généré par bpf2go (uprobe_nginx.c) tcLinks []netlink.Link // interfaces netlink pour cleanup TC uprobeLinks []link.Link statsMap *ebpf.Map // map tc_stats pour lecture des compteurs BPF (mode debug) @@ -44,6 +46,8 @@ type Loader struct { AcceptReader *perf.Reader // HTTPPlainReader lit les payloads HTTP en clair depuis pb_http_plain. HTTPPlainReader *perf.Reader + // NginxHTTPReader lit les requêtes HTTP complètes depuis nginx via uprobes. + NginxHTTPReader *perf.Reader } // StatNames associe chaque index de compteur BPF à un nom lisible. @@ -147,6 +151,14 @@ func New() (*Loader, error) { return nil, fmt.Errorf("chargement objets SSL eBPF: %w", err) } + // Charger les objets nginx/uprobe (uprobe_nginx.c) + nginxObjs := &Ja4NginxObjects{} + if err := LoadJa4NginxObjects(nginxObjs, nil); err != nil { + sslObjs.Close() + tcObjs.Close() + return nil, fmt.Errorf("chargement objets nginx eBPF: %w", err) + } + // Initialiser les readers pour chaque perf event array synReader, err := perf.NewReader(tcObjs.PbTcpSyn, perCPUBufferSize) if err != nil { @@ -193,9 +205,22 @@ func New() (*Loader, error) { return nil, fmt.Errorf("création reader pb_accept: %w", err) } + nginxHTTPReader, err := perf.NewReader(nginxObjs.PbGinxHttp, perCPUBufferSize) + if err != nil { + nginxHTTPReader.Close() + acceptReader.Close() + sslReader.Close() + httpPlainReader.Close() + tlsReader.Close() + synReader.Close() + sslObjs.Close() + return nil, fmt.Errorf("création reader pb_ginx_http: %w", err) + } + return &Loader{ tcObjs: tcObjs, sslObjs: sslObjs, + nginxObjs: nginxObjs, statsMap: tcObjs.TcStats, allowedPorts: tcObjs.AllowedPorts, ignoredSrc: tcObjs.IgnoredSrc, @@ -204,6 +229,7 @@ func New() (*Loader, error) { SSLReader: sslReader, AcceptReader: acceptReader, HTTPPlainReader: httpPlainReader, + NginxHTTPReader: nginxHTTPReader, }, nil } @@ -343,6 +369,36 @@ func (l *Loader) AttachAcceptProbe() error { return nil } +// AttachUprobesNginx attache les uprobes read() dans nginx pour capturer +// le trafic HTTP complet. Cette approche utilise read() syscall qui est +// appelé par nginx pour lire les requêtes depuis les clients. +func (l *Loader) AttachUprobesNginx(nginxBinPath string) error { + if _, err := os.Stat(nginxBinPath); err != nil { + return fmt.Errorf("binaire nginx %q: %w", nginxBinPath, err) + } + + ex, err := link.OpenExecutable(nginxBinPath) + if err != nil { + return fmt.Errorf("ouverture exécutable %q pour uprobe: %w", nginxBinPath, err) + } + + // Attacher uprobe sur read() (entrée) + readEntryLink, err := ex.Uprobe("read", l.nginxObjs.UprobeReadEntry, nil) + if err != nil { + return fmt.Errorf("attachement uprobe read (entry): %w", err) + } + l.uprobeLinks = append(l.uprobeLinks, readEntryLink) + + // Attacher uretprobe sur read() (sortie) pour capturer les données lues + readExitLink, err := ex.Uretprobe("read", l.nginxObjs.UretprobeReadExit, nil) + if err != nil { + return fmt.Errorf("attachement uretprobe read (exit): %w", err) + } + l.uprobeLinks = append(l.uprobeLinks, readExitLink) + + return nil +} + // attachSSLWrite attache les uprobes SSL_write pour capturer // les réponses HTTP du serveur (direction=1). func (l *Loader) attachSSLWrite(ex *link.Executable) error { @@ -378,6 +434,9 @@ func (l *Loader) Close() error { if l.SynReader != nil { l.SynReader.Close() } + if l.NginxHTTPReader != nil { + l.NginxHTTPReader.Close() + } // Détacher les filtres TC ingress sur toutes les interfaces for _, nlLink := range l.tcLinks {