From 7dfe6400030f9170b03ccf18fc6d1a82bb0e3fc2 Mon Sep 17 00:00:00 2001 From: Jacquin Antoine Date: Mon, 20 Apr 2026 13:38:58 +0200 Subject: [PATCH] feat(ebpf): add Apache httpd HTTP capture via read() syscall Add support for capturing HTTP traffic from Apache httpd using tracepoint/kretprobe on read() syscall. Changes: - bpf/uprobe_apache.c: New BPF program for Apache httpd capture - Uses tp/syscalls/sys_enter_read to save arguments - Uses kretprobe/__x64_sys_read to capture data (avoids tracepoint exit issues) - bpf/bpf_types.h: Add Apache-specific structures and maps - struct apache_http_event (same structure as nginx_http_event) - struct read_args (shared between enter/exit) - apache_pid_map for filtering by PID - apache_read_args_map for argument storage - pb_apache_http perf buffer - internal/loader/loader.go: Add Apache support - Add Ja4ApacheObjects, apachePidMap, ApacheHTTPReader - Add go:generate directive for uprobe_apache.c - Add AttachUprobesApache(), AddApachePid(), RemoveApachePid() - Add findApachePIDs() to discover Apache httpd processes - cmd/ja4ebpf/main.go: Add Apache runtime support - Add ApacheEnabled config option - Add attachApacheUprobesWithRetry() with automatic retry - Add consumeApacheHTTPEvents() to process Apache HTTP events - Add apache counter to eventCounters - Update debugStatsDumper to show apache events Configuration: - Enable with: uprobes.apache_enabled=true or JA4EBPF_APACHE_ENABLED=1 - Automatically discovers httpd/apache2 processes via /proc/[pid]/cmdline Co-Authored-By: Claude Opus 4.6 --- services/ja4ebpf/bpf/bpf_types.h | 64 ++++++++ services/ja4ebpf/bpf/uprobe_apache.c | 115 +++++++++++++ services/ja4ebpf/cmd/ja4ebpf/main.go | 179 ++++++++++++++++++++- services/ja4ebpf/internal/loader/loader.go | 138 +++++++++++++++- 4 files changed, 491 insertions(+), 5 deletions(-) create mode 100644 services/ja4ebpf/bpf/uprobe_apache.c diff --git a/services/ja4ebpf/bpf/bpf_types.h b/services/ja4ebpf/bpf/bpf_types.h index 7dc631d..d0a071d 100644 --- a/services/ja4ebpf/bpf/bpf_types.h +++ b/services/ja4ebpf/bpf/bpf_types.h @@ -119,6 +119,26 @@ struct nginx_http_event { __u32 data_len; /* longueur données brutes */ } __attribute__((packed)); +/* --------------------------------------------------------------------------- + * Événement Apache HTTP : requête HTTP complète depuis Apache httpd + * ---------------------------------------------------------------------------*/ +struct apache_http_event { + __u64 pid_tgid; /* PID+TGID du processus Apache */ + __u32 fd; /* descripteur de fichier socket (corrélation TC) */ + __u32 src_ip; /* IP source du client (host byte order) */ + __u16 src_port; /* port source du client */ + __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 */ + __u32 data_len; /* longueur données brutes */ +} __attribute__((packed)); + /* --------------------------------------------------------------------------- * Arguments sauvegardés à l'entrée de read() (pour l'uretprobe nginx) * ---------------------------------------------------------------------------*/ @@ -128,6 +148,15 @@ struct nginx_read_args { __u64 count; /* taille maximale demandée */ } __attribute__((packed)); +/* --------------------------------------------------------------------------- + * Arguments sauvegardés à l'entrée de read() pour Apache httpd + * ---------------------------------------------------------------------------*/ +struct 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) * ---------------------------------------------------------------------------*/ @@ -281,3 +310,38 @@ struct { __type(value, struct nginx_http_event); } __nginx_buf SEC(".maps"); +/* =========================================================================== + * Apache httpd HTTP capture via read() syscall + * =========================================================================== */ + +/* Hash map : PID Apache → flag pour filtrage read (tracepoints) */ +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, 16); + __type(key, __u32); + __type(value, __u8); +} apache_pid_map SEC(".maps"); + +/* Hash map : pid_tgid → read_args (arguments read entry pour Apache) */ +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, 10240); + __type(key, __u64); + __type(value, struct read_args); +} apache_read_args_map SEC(".maps"); + +/* PERCPU_ARRAY temporaire pour apache_http_event (taille ~4KB) */ +struct { + __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); + __uint(max_entries, 1); + __type(key, __u32); + __type(value, struct apache_http_event); +} __apache_buf SEC(".maps"); + +/* PerfEventArray pour événements Apache HTTP */ +struct { + __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY); + __uint(key_size, sizeof(__u32)); + __uint(value_size, sizeof(struct apache_http_event)); +} pb_apache_http SEC(".maps"); + diff --git a/services/ja4ebpf/bpf/uprobe_apache.c b/services/ja4ebpf/bpf/uprobe_apache.c new file mode 100644 index 0000000..7ffaf8d --- /dev/null +++ b/services/ja4ebpf/bpf/uprobe_apache.c @@ -0,0 +1,115 @@ +/* uprobe_apache.c — Tracepoints syscall pour capturer le trafic HTTP depuis Apache httpd + * + * Cette version utilise les tracepoints kernel syscalls/sys_enter_read et + * kretprobe sur __x64_sys_read pour capturer les appels système read() du serveur Apache. + * Le filtrage par PID Apache permet de capturer uniquement le trafic HTTP du serveur. + * + * ============================================================================ + */ + +#include "vmlinux.h" +#include +#include +#include "bpf_types.h" + +/* Taille maximale d'une capture read() */ +#define MAX_READ_SIZE 4096 + +/* ============================================================================ + * tracepoint_sys_enter_read — Entrée du syscall read + * + * Sauvegarde les arguments si le PID correspond à Apache. + * Signature: ssize_t read(int fd, void *buf, size_t count); + * ============================================================================ + */ +SEC("tp/syscalls/sys_enter_read") +int tp_sys_enter_read(struct trace_event_raw_sys_enter *ctx) +{ + __u64 pid_tgid = bpf_get_current_pid_tgid(); + __u32 pid = pid_tgid >> 32; + + /* Vérifier si ce PID est dans la map apache_pid_map */ + __u32 pid_key = pid; + __u8 *enabled = bpf_map_lookup_elem(&apache_pid_map, &pid_key); + if (!enabled || *enabled == 0) { + return 0; /* Pas un PID Apache, ignore */ + } + + /* Sauvegarder les arguments pour l'exit tracepoint */ + struct read_args args = {}; + args.fd = (__s32)ctx->args[0]; /* fd */ + args.buf_ptr = (__u64)ctx->args[1]; /* buf */ + args.count = (__u64)ctx->args[2]; /* count */ + + bpf_map_update_elem(&apache_read_args_map, &pid_tgid, &args, BPF_ANY); + + return 0; +} + +/* ============================================================================ + * kretprobe_sys_exit_read — Sortie du syscall read + * + * Capture les données lues et les envoie vers pb_apache_http. + * Utilise kretprobe pour contourner les limitations de tracepoint exit. + * ============================================================================ + */ +SEC("kretprobe/__x64_sys_read") +int kretprobe_sys_exit_read(struct pt_regs *ctx) +{ + __u64 pid_tgid = bpf_get_current_pid_tgid(); + + /* Récupérer les arguments sauvegardés */ + struct read_args *args = bpf_map_lookup_elem(&apache_read_args_map, &pid_tgid); + if (!args) { + return 0; /* Pas d'arguments correspondants */ + } + + /* Obtenir la valeur de retour (nombre d'octets lus) */ + long retval = PT_REGS_RC(ctx); + if (retval <= 0 || retval > MAX_READ_SIZE) { + /* Erreur, EOF, ou trop de données - nettoyer et sortir */ + bpf_map_delete_elem(&apache_read_args_map, &pid_tgid); + return 0; + } + + /* Taille à copier (minimum entre retval et la taille disponible) */ + __u64 copy_size = retval; + if (copy_size > args->count) { + copy_size = args->count; + } + + /* Préparer l'événement Apache HTTP */ + struct apache_http_event *e = bpf_map_lookup_elem(&__apache_buf, &pid_tgid); + if (!e) { + bpf_map_delete_elem(&apache_read_args_map, &pid_tgid); + return 0; + } + + /* Initialiser l'événement */ + __builtin_memset(e, 0, sizeof(*e)); + e->pid_tgid = pid_tgid; + e->fd = args->fd; + e->timestamp_ns = bpf_ktime_get_ns(); + + /* Récupérer les infos de connexion depuis fd_conn_map */ + struct ssl_conn_info *conn_info = bpf_map_lookup_elem(&fd_conn_map, &args->fd); + if (conn_info) { + e->src_ip = conn_info->src_ip; + e->src_port = conn_info->src_port; + } + + /* Copier les données HTTP depuis l'espace utilisateur */ + __u64 bytes_read = bpf_probe_read_user_str(e->data, sizeof(e->data), (void *)args->buf_ptr); + if (bytes_read > 0) { + e->data_len = bytes_read; + /* Envoyer vers l'espace utilisateur via perf buffer */ + bpf_perf_event_output(ctx, &pb_apache_http, BPF_F_CURRENT_CPU, e, sizeof(*e)); + } + + /* Nettoyer */ + bpf_map_delete_elem(&apache_read_args_map, &pid_tgid); + + return 0; +} + +char LICENSE[] SEC("license") = "GPL"; diff --git a/services/ja4ebpf/cmd/ja4ebpf/main.go b/services/ja4ebpf/cmd/ja4ebpf/main.go index d78e28f..cf7a14d 100644 --- a/services/ja4ebpf/cmd/ja4ebpf/main.go +++ b/services/ja4ebpf/cmd/ja4ebpf/main.go @@ -64,10 +64,11 @@ type Config struct { } `yaml:"log"` Uprobes struct { - Enabled bool `yaml:"enabled"` // activer l'attachement automatique des uprobes - NginxBinPath string `yaml:"nginx_bin_path"` // chemin vers le binaire nginx - MaxRetries int `yaml:"max_retries"` // nombre de tentatives d'attachement (défaut: 30) - RetryIntervalSec int `yaml:"retry_interval_sec"` // intervalle entre tentatives (défaut: 2) + Enabled bool `yaml:"enabled"` // activer l'attachement automatique des uprobes + NginxBinPath string `yaml:"nginx_bin_path"` // chemin vers le binaire nginx + ApacheEnabled bool `yaml:"apache_enabled"` // activer la capture Apache httpd + MaxRetries int `yaml:"max_retries"` // nombre de tentatives d'attachement (défaut: 30) + RetryIntervalSec int `yaml:"retry_interval_sec"` // intervalle entre tentatives (défaut: 2) } `yaml:"uprobes"` } @@ -89,6 +90,7 @@ func loadConfig(path string) (*Config, error) { cfg.Log.Format = "json" cfg.Uprobes.Enabled = false cfg.Uprobes.NginxBinPath = "/usr/sbin/nginx" + cfg.Uprobes.ApacheEnabled = false cfg.Uprobes.MaxRetries = 30 cfg.Uprobes.RetryIntervalSec = 2 @@ -141,6 +143,9 @@ func loadConfig(path string) (*Config, error) { if v := os.Getenv("JA4EBPF_NGINX_BIN_PATH"); v != "" { cfg.Uprobes.NginxBinPath = v } + if v := os.Getenv("JA4EBPF_APACHE_ENABLED"); v != "" { + cfg.Uprobes.ApacheEnabled = strings.EqualFold(v, "true") || v == "1" || v == "yes" + } return cfg, nil @@ -295,6 +300,13 @@ func main() { log.Printf("[ja4ebpf] erreur attachement uprobes nginx: %v", err) } + // --- 4c. Attachement uprobes Apache httpd --- + if cfg.Uprobes.ApacheEnabled { + if err := attachApacheUprobesWithRetry(ctx, ldr, cfg); err != nil { + log.Printf("[ja4ebpf] erreur attachement uprobes Apache: %v", err) + } + } + // --- 5. Gestionnaire de sessions --- sessionTimeout := time.Duration(cfg.Correlation.TimeoutMS) * time.Millisecond @@ -340,6 +352,9 @@ func main() { consumed := &eventCounters{} go consumeNginxHTTPEvents(ctx, ldr.NginxHTTPReader, mgr, &consumed.nginx) + if cfg.Uprobes.ApacheEnabled { + go consumeApacheHTTPEvents(ctx, ldr.ApacheHTTPReader, mgr, &consumed.apache) + } // --- 9. Goroutines de consommation des ring buffers --- go consumeSynEvents(ctx, ldr.SynReader, mgr, &consumed.syn) @@ -382,6 +397,7 @@ type eventCounters struct { accept atomic.Uint64 httpPlain atomic.Uint64 nginx atomic.Uint64 + apache atomic.Uint64 } // debugStatsDumper affiche les compteurs BPF et les événements consommés toutes les 5 secondes. @@ -1255,6 +1271,47 @@ func attachNginxUprobesWithRetry(ctx context.Context, l *loader.Loader, cfg *Con return fmt.Errorf("attachement nginx uprobes échoué après %d tentatives", maxRetries) } +// attachApacheUprobesWithRetry attache les tracepoints/kretprobe Apache avec retry automatique. +// Retente jusqu'à maxRetries fois toutes les retryInterval secondes. +// Utile pour attendre que Apache httpd démarre après ja4ebpf. +func attachApacheUprobesWithRetry(ctx context.Context, l *loader.Loader, cfg *Config) error { + if !cfg.Uprobes.ApacheEnabled { + log.Printf("[uprobes] Apache uprobes désactivés (uprobes.apache_enabled=false)") + return nil + } + + maxRetries := cfg.Uprobes.MaxRetries + retryInterval := time.Duration(cfg.Uprobes.RetryIntervalSec) * time.Second + + log.Printf("[uprobes] tentative d'attachement Apache httpd tracepoints (max_retries=%d, interval=%v)", + maxRetries, retryInterval) + + for attempt := 1; attempt <= maxRetries; attempt++ { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + // Tenter d'attacher les tracepoints/kretprobe Apache + err := l.AttachUprobesApache() + if err == nil { + log.Printf("[uprobes] Apache httpd tracepoints attachés avec succès (tentative %d/%d)", attempt, maxRetries) + return nil + } + + log.Printf("[uprobes] tentative %d/%d: échec attachement Apache: %v, retry dans %v", + attempt, maxRetries, err, retryInterval) + + // Dernière tentative : ne pas sleep + if attempt < maxRetries { + time.Sleep(retryInterval) + } + } + + return fmt.Errorf("attachement Apache httpd tracepoints échoué après %d tentatives", maxRetries) +} + // consumeNginxHTTPEvents lit et traite les événements HTTP depuis nginx via uprobes. // Ces événements contiennent les requêtes HTTP complètes (méthode, URI, headers) capturées // par nginx avant tout traitement, garantissant des headers complets. @@ -1375,3 +1432,117 @@ func consumeNginxHTTPEvents(ctx context.Context, rd *perf.Reader, mgr *correlati pidTgid>>32, fd, httpMethod, uri, len(req.HeaderOrder)) } } + +// consumeApacheHTTPEvents lit et traite les événements HTTP depuis Apache httpd via read(). +func consumeApacheHTTPEvents(ctx context.Context, rd *perf.Reader, mgr *correlation.Manager, counter *atomic.Uint64) { + const minEventSize = 426 + + for { + select { + case <-ctx.Done(): + return + default: + } + + record, err := rd.Read() + if err != nil { + if err == os.ErrClosed { + return + } + continue + } + + data := record.RawSample + if len(data) < minEventSize { + continue + } + + // Parser les metadata + pidTgid := binary.LittleEndian.Uint64(data[0:8]) + fd := binary.LittleEndian.Uint32(data[8:12]) + srcIPRaw := binary.LittleEndian.Uint32(data[12:16]) + srcPort := binary.LittleEndian.Uint16(data[16:18]) + dataLen := binary.LittleEndian.Uint32(data[4082:4086]) + + // Extraire les données HTTP brutes + rawHTTP := string(data[426 : 426+dataLen]) + if len(rawHTTP) < 4 { + continue + } + + // Parser la ligne de requête HTTP + requestLineEnd := strings.Index(rawHTTP, "\r\n") + if requestLineEnd < 0 { + continue + } + requestLine := rawHTTP[:requestLineEnd] + parts := strings.Fields(requestLine) + if len(parts) < 2 { + continue + } + + httpMethod := parts[0] + fullPath := parts[1] + + // Séparer path et query string + queryStart := strings.Index(fullPath, "?") + var uri, query string + if queryStart >= 0 { + uri = fullPath[:queryStart] + query = fullPath[queryStart+1:] + } else { + uri = fullPath + } + + // Parser les headers HTTP + headersData := rawHTTP[requestLineEnd+2:] + var req correlation.HTTPRequest + req.Method = httpMethod + req.Path = uri + req.QueryString = query + req.Timestamp = time.Now() + req.HeaderKV = make(map[string]string) + + lines := strings.Split(headersData, "\r\n") + for _, line := range lines { + if line == "" { + break + } + colon := strings.Index(line, ":") + if colon > 0 { + name := strings.TrimSpace(line[:colon]) + value := "" + if colon+1 < len(line) { + value = strings.TrimSpace(line[colon+1:]) + } + nameLower := strings.ToLower(name) + req.HeaderOrder = append(req.HeaderOrder, nameLower) + req.HeaderKV[nameLower] = value + } + } + req.HeaderOrderSig = strings.Join(req.HeaderOrder, ";") + + // Créer la clé de session + 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 + + // Filtrer les IPs sources ignorées + if isIgnoredIP(key.SrcIP) { + continue + } + + // Mettre à jour la session + mgr.Update(key, func(s *correlation.SessionState) { + s.Requests = append(s.Requests, req) + }) + + counter.Add(1) + log.Printf("[apache] HTTP: pid=%d fd=%d %s %s (headers=%d)", + pidTgid>>32, fd, httpMethod, uri, len(req.HeaderOrder)) + } + } +} diff --git a/services/ja4ebpf/internal/loader/loader.go b/services/ja4ebpf/internal/loader/loader.go index b14d869..ae1352e 100644 --- a/services/ja4ebpf/internal/loader/loader.go +++ b/services/ja4ebpf/internal/loader/loader.go @@ -22,6 +22,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 +//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" Ja4Apache ../../bpf/uprobe_apache.c -- -I../../bpf/headers // perCPUBufferSize est la taille du buffer perf per-CPU en octets (256 KB). const perCPUBufferSize = 256 * 1024 @@ -32,12 +33,14 @@ 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) + apacheObjs *Ja4ApacheObjects // généré par bpf2go (uprobe_apache.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) allowedPorts *ebpf.Map // map allowed_ports pour filtrage par port ignoredSrc *ebpf.Map // map ignored_src (LPM_TRIE) pour filtrage IP/CIDR - nginxPidMap *ebpf.Map // map nginx_pid_map pour filtrage recvfrom par PID + nginxPidMap *ebpf.Map // map nginx_pid_map pour filtrage recvfrom par PID + apachePidMap *ebpf.Map // map apache_pid_map pour filtrage read par PID // SynReader lit les événements TCP SYN depuis pb_tcp_syn. SynReader *perf.Reader @@ -51,6 +54,8 @@ type Loader struct { HTTPPlainReader *perf.Reader // NginxHTTPReader lit les requêtes HTTP complètes depuis nginx via uprobes. NginxHTTPReader *perf.Reader + // ApacheHTTPReader lit les requêtes HTTP complètes depuis Apache httpd via read(). + ApacheHTTPReader *perf.Reader } // StatNames associe chaque index de compteur BPF à un nom lisible. @@ -150,6 +155,30 @@ func (l *Loader) RemoveNginxPid(pid uint32) error { return nil } +// AddApachePid ajoute un PID Apache httpd à la map apache_pid_map pour le filtrage read. +// Un PID Apache activé permettra la capture de ses appels read() via tracepoints. +func (l *Loader) AddApachePid(pid uint32) error { + if l.apachePidMap == nil { + return fmt.Errorf("map apache_pid_map non disponible") + } + var val uint8 = 1 + if err := l.apachePidMap.Put(pid, val); err != nil { + return fmt.Errorf("ajout PID %d dans apache_pid_map: %w", pid, err) + } + return nil +} + +// RemoveApachePid supprime un PID Apache de la map apache_pid_map. +func (l *Loader) RemoveApachePid(pid uint32) error { + if l.apachePidMap == nil { + return fmt.Errorf("map apache_pid_map non disponible") + } + if err := l.apachePidMap.Delete(pid); err != nil { + return fmt.Errorf("suppression PID %d de apache_pid_map: %w", pid, err) + } + return nil +} + // New charge le bytecode eBPF embarqué, supprime la limite mémoire // RLIMIT_MEMLOCK (requise pour les maps eBPF), // et retourne un Loader prêt à être attaché aux hooks. @@ -186,6 +215,15 @@ func New() (*Loader, error) { return nil, fmt.Errorf("chargement objets nginx eBPF: %w", err) } + // Charger les objets Apache httpd (uprobe_apache.c) + apacheObjs := &Ja4ApacheObjects{} + if err := LoadJa4ApacheObjects(apacheObjs, nil); err != nil { + nginxObjs.Close() + sslObjs.Close() + tcObjs.Close() + return nil, fmt.Errorf("chargement objets Apache eBPF: %w", err) + } + // Initialiser les readers pour chaque perf event array synReader, err := perf.NewReader(tcObjs.PbTcpSyn, perCPUBufferSize) if err != nil { @@ -244,20 +282,39 @@ func New() (*Loader, error) { return nil, fmt.Errorf("création reader pb_ginx_http: %w", err) } + apacheHTTPReader, err := perf.NewReader(apacheObjs.PbApacheHttp, perCPUBufferSize) + if err != nil { + apacheHTTPReader.Close() + nginxHTTPReader.Close() + acceptReader.Close() + sslReader.Close() + httpPlainReader.Close() + tlsReader.Close() + synReader.Close() + apacheObjs.Close() + nginxObjs.Close() + sslObjs.Close() + tcObjs.Close() + return nil, fmt.Errorf("création reader pb_apache_http: %w", err) + } + return &Loader{ tcObjs: tcObjs, sslObjs: sslObjs, nginxObjs: nginxObjs, + apacheObjs: apacheObjs, statsMap: tcObjs.TcStats, allowedPorts: tcObjs.AllowedPorts, ignoredSrc: tcObjs.IgnoredSrc, nginxPidMap: nginxObjs.NginxPidMap, + apachePidMap: apacheObjs.ApachePidMap, SynReader: synReader, TLSReader: tlsReader, SSLReader: sslReader, AcceptReader: acceptReader, HTTPPlainReader: httpPlainReader, NginxHTTPReader: nginxHTTPReader, + ApacheHTTPReader: apacheHTTPReader, }, nil } @@ -477,6 +534,85 @@ func findNginxPIDs() ([]uint32, error) { return pids, nil } +// AttachUprobesApache configure les tracepoints/kretprobe read pour capturer +// le trafic HTTP complet depuis Apache httpd. Cette approche utilise les tracepoints +// kernel sys_enter_read et kretprobe __x64_sys_read. +// Le PID Apache est ajouté à la map apache_pid_map pour filtrer les appels read(). +func (l *Loader) AttachUprobesApache() error { + // Attacher le tracepoint sys_enter_read + kpEntry, err := link.Tracepoint("syscalls", "sys_enter_read", + l.apacheObjs.TpSysEnterRead, nil) + if err != nil { + return fmt.Errorf("attachement tracepoint sys_enter_read: %w", err) + } + l.uprobeLinks = append(l.uprobeLinks, kpEntry) + + // Utilisation de Kretprobe pour sys_exit_read (via __x64_sys_read) + // pour contourner les limitations de tracepoint exit sur certains kernels. + kpExit, err := link.Kretprobe("__x64_sys_read", + l.apacheObjs.KretprobeSysExitRead, &link.KprobeOptions{}) + if err != nil { + return fmt.Errorf("attachement kretprobe sys_exit_read: %w", err) + } + l.uprobeLinks = append(l.uprobeLinks, kpExit) + + // Trouver les PIDs Apache httpd en cours d'exécution + pids, err := findApachePIDs() + if err != nil { + return fmt.Errorf("recherche PID Apache: %w", err) + } + if len(pids) == 0 { + return fmt.Errorf("aucun processus Apache httpd trouvé") + } + + // Ajouter tous les PIDS Apache trouvés à la map de filtrage + for _, pid := range pids { + if err := l.AddApachePid(pid); err != nil { + log.Printf("[ja4ebpf] avertissement: ajout PID Apache %d: %v", pid, err) + } else { + log.Printf("[ja4ebpf] tracepoints read activés pour PID Apache %d", pid) + } + } + + return nil +} + +// findApachePIDs trouve tous les PIDs des processus Apache httpd en cours d'exécution. +func findApachePIDs() ([]uint32, error) { + // Lire /proc pour trouver les processus Apache + entries, err := os.ReadDir("/proc") + if err != nil { + return nil, fmt.Errorf("lecture /proc: %w", err) + } + + var pids []uint32 + for _, entry := range entries { + // Vérifier que le nom est un nombre (PID) + if !entry.IsDir() { + continue + } + pid, err := strconv.ParseUint(entry.Name(), 10, 32) + if err != nil { + continue + } + + // Vérifier si c'est un processus Apache en lisant /proc/[pid]/cmdline + cmdlinePath := fmt.Sprintf("/proc/%d/cmdline", pid) + cmdlineData, err := os.ReadFile(cmdlinePath) + if err != nil { + continue + } + + // La cmdline contient le chemin du binaire, ex: "/usr/sbin/httpd" ou "apache2" + cmdline := string(cmdlineData) + if strings.Contains(cmdline, "httpd") || strings.Contains(cmdline, "apache2") { + pids = append(pids, uint32(pid)) + } + } + + return pids, 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 {