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:
@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user