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:
@ -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");
|
||||
|
||||
|
||||
115
services/ja4ebpf/bpf/uprobe_apache.c
Normal file
115
services/ja4ebpf/bpf/uprobe_apache.c
Normal 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";
|
||||
@ -66,6 +66,7 @@ type Config struct {
|
||||
Uprobes struct {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user