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 <noreply@anthropic.com>
This commit is contained in:
Jacquin Antoine
2026-04-20 13:38:58 +02:00
parent 382683710a
commit 7dfe640003
4 changed files with 491 additions and 5 deletions

View File

@ -119,6 +119,26 @@ struct nginx_http_event {
__u32 data_len; /* longueur données brutes */ __u32 data_len; /* longueur données brutes */
} __attribute__((packed)); } __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) * 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 */ __u64 count; /* taille maximale demandée */
} __attribute__((packed)); } __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) * Arguments sauvegardés à l'entrée de SSL_read (pour l'uretprobe)
* ---------------------------------------------------------------------------*/ * ---------------------------------------------------------------------------*/
@ -281,3 +310,38 @@ struct {
__type(value, struct nginx_http_event); __type(value, struct nginx_http_event);
} __nginx_buf SEC(".maps"); } __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");

View File

@ -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 <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#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";

View File

@ -66,6 +66,7 @@ type Config struct {
Uprobes struct { Uprobes struct {
Enabled bool `yaml:"enabled"` // activer l'attachement automatique des uprobes Enabled bool `yaml:"enabled"` // activer l'attachement automatique des uprobes
NginxBinPath string `yaml:"nginx_bin_path"` // chemin vers le binaire nginx 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) 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) RetryIntervalSec int `yaml:"retry_interval_sec"` // intervalle entre tentatives (défaut: 2)
} `yaml:"uprobes"` } `yaml:"uprobes"`
@ -89,6 +90,7 @@ func loadConfig(path string) (*Config, error) {
cfg.Log.Format = "json" cfg.Log.Format = "json"
cfg.Uprobes.Enabled = false cfg.Uprobes.Enabled = false
cfg.Uprobes.NginxBinPath = "/usr/sbin/nginx" cfg.Uprobes.NginxBinPath = "/usr/sbin/nginx"
cfg.Uprobes.ApacheEnabled = false
cfg.Uprobes.MaxRetries = 30 cfg.Uprobes.MaxRetries = 30
cfg.Uprobes.RetryIntervalSec = 2 cfg.Uprobes.RetryIntervalSec = 2
@ -141,6 +143,9 @@ func loadConfig(path string) (*Config, error) {
if v := os.Getenv("JA4EBPF_NGINX_BIN_PATH"); v != "" { if v := os.Getenv("JA4EBPF_NGINX_BIN_PATH"); v != "" {
cfg.Uprobes.NginxBinPath = 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 return cfg, nil
@ -295,6 +300,13 @@ func main() {
log.Printf("[ja4ebpf] erreur attachement uprobes nginx: %v", err) 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 --- // --- 5. Gestionnaire de sessions ---
sessionTimeout := time.Duration(cfg.Correlation.TimeoutMS) * time.Millisecond sessionTimeout := time.Duration(cfg.Correlation.TimeoutMS) * time.Millisecond
@ -340,6 +352,9 @@ func main() {
consumed := &eventCounters{} consumed := &eventCounters{}
go consumeNginxHTTPEvents(ctx, ldr.NginxHTTPReader, mgr, &consumed.nginx) 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 --- // --- 9. Goroutines de consommation des ring buffers ---
go consumeSynEvents(ctx, ldr.SynReader, mgr, &consumed.syn) go consumeSynEvents(ctx, ldr.SynReader, mgr, &consumed.syn)
@ -382,6 +397,7 @@ type eventCounters struct {
accept atomic.Uint64 accept atomic.Uint64
httpPlain atomic.Uint64 httpPlain atomic.Uint64
nginx atomic.Uint64 nginx atomic.Uint64
apache atomic.Uint64
} }
// debugStatsDumper affiche les compteurs BPF et les événements consommés toutes les 5 secondes. // 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) 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. // 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 // 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. // 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)) 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))
}
}
}

View File

@ -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" 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" 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" 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). // perCPUBufferSize est la taille du buffer perf per-CPU en octets (256 KB).
const perCPUBufferSize = 256 * 1024 const perCPUBufferSize = 256 * 1024
@ -32,12 +33,14 @@ type Loader struct {
tcObjs *Ja4TcObjects // généré par bpf2go (tc_capture.c) tcObjs *Ja4TcObjects // généré par bpf2go (tc_capture.c)
sslObjs *Ja4SslObjects // généré par bpf2go (uprobe_ssl.c) sslObjs *Ja4SslObjects // généré par bpf2go (uprobe_ssl.c)
nginxObjs *Ja4NginxObjects // généré par bpf2go (uprobe_nginx.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 tcLinks []netlink.Link // interfaces netlink pour cleanup TC
uprobeLinks []link.Link uprobeLinks []link.Link
statsMap *ebpf.Map // map tc_stats pour lecture des compteurs BPF (mode debug) statsMap *ebpf.Map // map tc_stats pour lecture des compteurs BPF (mode debug)
allowedPorts *ebpf.Map // map allowed_ports pour filtrage par port allowedPorts *ebpf.Map // map allowed_ports pour filtrage par port
ignoredSrc *ebpf.Map // map ignored_src (LPM_TRIE) pour filtrage IP/CIDR 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 lit les événements TCP SYN depuis pb_tcp_syn.
SynReader *perf.Reader SynReader *perf.Reader
@ -51,6 +54,8 @@ type Loader struct {
HTTPPlainReader *perf.Reader HTTPPlainReader *perf.Reader
// NginxHTTPReader lit les requêtes HTTP complètes depuis nginx via uprobes. // NginxHTTPReader lit les requêtes HTTP complètes depuis nginx via uprobes.
NginxHTTPReader *perf.Reader 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. // StatNames associe chaque index de compteur BPF à un nom lisible.
@ -150,6 +155,30 @@ func (l *Loader) RemoveNginxPid(pid uint32) error {
return nil 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 // New charge le bytecode eBPF embarqué, supprime la limite mémoire
// RLIMIT_MEMLOCK (requise pour les maps eBPF), // RLIMIT_MEMLOCK (requise pour les maps eBPF),
// et retourne un Loader prêt à être attaché aux hooks. // 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) 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 // Initialiser les readers pour chaque perf event array
synReader, err := perf.NewReader(tcObjs.PbTcpSyn, perCPUBufferSize) synReader, err := perf.NewReader(tcObjs.PbTcpSyn, perCPUBufferSize)
if err != nil { if err != nil {
@ -244,20 +282,39 @@ func New() (*Loader, error) {
return nil, fmt.Errorf("création reader pb_ginx_http: %w", err) 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{ return &Loader{
tcObjs: tcObjs, tcObjs: tcObjs,
sslObjs: sslObjs, sslObjs: sslObjs,
nginxObjs: nginxObjs, nginxObjs: nginxObjs,
apacheObjs: apacheObjs,
statsMap: tcObjs.TcStats, statsMap: tcObjs.TcStats,
allowedPorts: tcObjs.AllowedPorts, allowedPorts: tcObjs.AllowedPorts,
ignoredSrc: tcObjs.IgnoredSrc, ignoredSrc: tcObjs.IgnoredSrc,
nginxPidMap: nginxObjs.NginxPidMap, nginxPidMap: nginxObjs.NginxPidMap,
apachePidMap: apacheObjs.ApachePidMap,
SynReader: synReader, SynReader: synReader,
TLSReader: tlsReader, TLSReader: tlsReader,
SSLReader: sslReader, SSLReader: sslReader,
AcceptReader: acceptReader, AcceptReader: acceptReader,
HTTPPlainReader: httpPlainReader, HTTPPlainReader: httpPlainReader,
NginxHTTPReader: nginxHTTPReader, NginxHTTPReader: nginxHTTPReader,
ApacheHTTPReader: apacheHTTPReader,
}, nil }, nil
} }
@ -477,6 +534,85 @@ func findNginxPIDs() ([]uint32, error) {
return pids, nil 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 // attachSSLWrite attache les uprobes SSL_write pour capturer
// les réponses HTTP du serveur (direction=1). // les réponses HTTP du serveur (direction=1).
func (l *Loader) attachSSLWrite(ex *link.Executable) error { func (l *Loader) attachSSLWrite(ex *link.Executable) error {