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 */
|
__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");
|
||||||
|
|
||||||
|
|||||||
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";
|
||||||
@ -64,10 +64,11 @@ type Config struct {
|
|||||||
} `yaml:"log"`
|
} `yaml:"log"`
|
||||||
|
|
||||||
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
|
||||||
MaxRetries int `yaml:"max_retries"` // nombre de tentatives d'attachement (défaut: 30)
|
ApacheEnabled bool `yaml:"apache_enabled"` // activer la capture Apache httpd
|
||||||
RetryIntervalSec int `yaml:"retry_interval_sec"` // intervalle entre tentatives (défaut: 2)
|
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"`
|
} `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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user