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:
@ -64,10 +64,11 @@ type Config struct {
|
||||
} `yaml:"log"`
|
||||
|
||||
Uprobes struct {
|
||||
Enabled bool `yaml:"enabled"` // activer l'attachement automatique des uprobes
|
||||
NginxBinPath string `yaml:"nginx_bin_path"` // chemin vers le binaire nginx
|
||||
MaxRetries int `yaml:"max_retries"` // nombre de tentatives d'attachement (défaut: 30)
|
||||
RetryIntervalSec int `yaml:"retry_interval_sec"` // intervalle entre tentatives (défaut: 2)
|
||||
Enabled bool `yaml:"enabled"` // activer l'attachement automatique des uprobes
|
||||
NginxBinPath string `yaml:"nginx_bin_path"` // chemin vers le binaire nginx
|
||||
ApacheEnabled bool `yaml:"apache_enabled"` // activer la capture Apache httpd
|
||||
MaxRetries int `yaml:"max_retries"` // nombre de tentatives d'attachement (défaut: 30)
|
||||
RetryIntervalSec int `yaml:"retry_interval_sec"` // intervalle entre tentatives (défaut: 2)
|
||||
} `yaml:"uprobes"`
|
||||
}
|
||||
|
||||
@ -89,6 +90,7 @@ func loadConfig(path string) (*Config, error) {
|
||||
cfg.Log.Format = "json"
|
||||
cfg.Uprobes.Enabled = false
|
||||
cfg.Uprobes.NginxBinPath = "/usr/sbin/nginx"
|
||||
cfg.Uprobes.ApacheEnabled = false
|
||||
cfg.Uprobes.MaxRetries = 30
|
||||
cfg.Uprobes.RetryIntervalSec = 2
|
||||
|
||||
@ -141,6 +143,9 @@ func loadConfig(path string) (*Config, error) {
|
||||
if v := os.Getenv("JA4EBPF_NGINX_BIN_PATH"); v != "" {
|
||||
cfg.Uprobes.NginxBinPath = v
|
||||
}
|
||||
if v := os.Getenv("JA4EBPF_APACHE_ENABLED"); v != "" {
|
||||
cfg.Uprobes.ApacheEnabled = strings.EqualFold(v, "true") || v == "1" || v == "yes"
|
||||
}
|
||||
|
||||
|
||||
return cfg, nil
|
||||
@ -295,6 +300,13 @@ func main() {
|
||||
log.Printf("[ja4ebpf] erreur attachement uprobes nginx: %v", err)
|
||||
}
|
||||
|
||||
// --- 4c. Attachement uprobes Apache httpd ---
|
||||
if cfg.Uprobes.ApacheEnabled {
|
||||
if err := attachApacheUprobesWithRetry(ctx, ldr, cfg); err != nil {
|
||||
log.Printf("[ja4ebpf] erreur attachement uprobes Apache: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- 5. Gestionnaire de sessions ---
|
||||
sessionTimeout := time.Duration(cfg.Correlation.TimeoutMS) * time.Millisecond
|
||||
@ -340,6 +352,9 @@ func main() {
|
||||
consumed := &eventCounters{}
|
||||
|
||||
go consumeNginxHTTPEvents(ctx, ldr.NginxHTTPReader, mgr, &consumed.nginx)
|
||||
if cfg.Uprobes.ApacheEnabled {
|
||||
go consumeApacheHTTPEvents(ctx, ldr.ApacheHTTPReader, mgr, &consumed.apache)
|
||||
}
|
||||
|
||||
// --- 9. Goroutines de consommation des ring buffers ---
|
||||
go consumeSynEvents(ctx, ldr.SynReader, mgr, &consumed.syn)
|
||||
@ -382,6 +397,7 @@ type eventCounters struct {
|
||||
accept atomic.Uint64
|
||||
httpPlain atomic.Uint64
|
||||
nginx atomic.Uint64
|
||||
apache atomic.Uint64
|
||||
}
|
||||
|
||||
// debugStatsDumper affiche les compteurs BPF et les événements consommés toutes les 5 secondes.
|
||||
@ -1255,6 +1271,47 @@ func attachNginxUprobesWithRetry(ctx context.Context, l *loader.Loader, cfg *Con
|
||||
return fmt.Errorf("attachement nginx uprobes échoué après %d tentatives", maxRetries)
|
||||
}
|
||||
|
||||
// attachApacheUprobesWithRetry attache les tracepoints/kretprobe Apache avec retry automatique.
|
||||
// Retente jusqu'à maxRetries fois toutes les retryInterval secondes.
|
||||
// Utile pour attendre que Apache httpd démarre après ja4ebpf.
|
||||
func attachApacheUprobesWithRetry(ctx context.Context, l *loader.Loader, cfg *Config) error {
|
||||
if !cfg.Uprobes.ApacheEnabled {
|
||||
log.Printf("[uprobes] Apache uprobes désactivés (uprobes.apache_enabled=false)")
|
||||
return nil
|
||||
}
|
||||
|
||||
maxRetries := cfg.Uprobes.MaxRetries
|
||||
retryInterval := time.Duration(cfg.Uprobes.RetryIntervalSec) * time.Second
|
||||
|
||||
log.Printf("[uprobes] tentative d'attachement Apache httpd tracepoints (max_retries=%d, interval=%v)",
|
||||
maxRetries, retryInterval)
|
||||
|
||||
for attempt := 1; attempt <= maxRetries; attempt++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
// Tenter d'attacher les tracepoints/kretprobe Apache
|
||||
err := l.AttachUprobesApache()
|
||||
if err == nil {
|
||||
log.Printf("[uprobes] Apache httpd tracepoints attachés avec succès (tentative %d/%d)", attempt, maxRetries)
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Printf("[uprobes] tentative %d/%d: échec attachement Apache: %v, retry dans %v",
|
||||
attempt, maxRetries, err, retryInterval)
|
||||
|
||||
// Dernière tentative : ne pas sleep
|
||||
if attempt < maxRetries {
|
||||
time.Sleep(retryInterval)
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("attachement Apache httpd tracepoints échoué après %d tentatives", maxRetries)
|
||||
}
|
||||
|
||||
// consumeNginxHTTPEvents lit et traite les événements HTTP depuis nginx via uprobes.
|
||||
// Ces événements contiennent les requêtes HTTP complètes (méthode, URI, headers) capturées
|
||||
// par nginx avant tout traitement, garantissant des headers complets.
|
||||
@ -1375,3 +1432,117 @@ func consumeNginxHTTPEvents(ctx context.Context, rd *perf.Reader, mgr *correlati
|
||||
pidTgid>>32, fd, httpMethod, uri, len(req.HeaderOrder))
|
||||
}
|
||||
}
|
||||
|
||||
// consumeApacheHTTPEvents lit et traite les événements HTTP depuis Apache httpd via read().
|
||||
func consumeApacheHTTPEvents(ctx context.Context, rd *perf.Reader, mgr *correlation.Manager, counter *atomic.Uint64) {
|
||||
const minEventSize = 426
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
record, err := rd.Read()
|
||||
if err != nil {
|
||||
if err == os.ErrClosed {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
data := record.RawSample
|
||||
if len(data) < minEventSize {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parser les metadata
|
||||
pidTgid := binary.LittleEndian.Uint64(data[0:8])
|
||||
fd := binary.LittleEndian.Uint32(data[8:12])
|
||||
srcIPRaw := binary.LittleEndian.Uint32(data[12:16])
|
||||
srcPort := binary.LittleEndian.Uint16(data[16:18])
|
||||
dataLen := binary.LittleEndian.Uint32(data[4082:4086])
|
||||
|
||||
// Extraire les données HTTP brutes
|
||||
rawHTTP := string(data[426 : 426+dataLen])
|
||||
if len(rawHTTP) < 4 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parser la ligne de requête HTTP
|
||||
requestLineEnd := strings.Index(rawHTTP, "\r\n")
|
||||
if requestLineEnd < 0 {
|
||||
continue
|
||||
}
|
||||
requestLine := rawHTTP[:requestLineEnd]
|
||||
parts := strings.Fields(requestLine)
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
httpMethod := parts[0]
|
||||
fullPath := parts[1]
|
||||
|
||||
// Séparer path et query string
|
||||
queryStart := strings.Index(fullPath, "?")
|
||||
var uri, query string
|
||||
if queryStart >= 0 {
|
||||
uri = fullPath[:queryStart]
|
||||
query = fullPath[queryStart+1:]
|
||||
} else {
|
||||
uri = fullPath
|
||||
}
|
||||
|
||||
// Parser les headers HTTP
|
||||
headersData := rawHTTP[requestLineEnd+2:]
|
||||
var req correlation.HTTPRequest
|
||||
req.Method = httpMethod
|
||||
req.Path = uri
|
||||
req.QueryString = query
|
||||
req.Timestamp = time.Now()
|
||||
req.HeaderKV = make(map[string]string)
|
||||
|
||||
lines := strings.Split(headersData, "\r\n")
|
||||
for _, line := range lines {
|
||||
if line == "" {
|
||||
break
|
||||
}
|
||||
colon := strings.Index(line, ":")
|
||||
if colon > 0 {
|
||||
name := strings.TrimSpace(line[:colon])
|
||||
value := ""
|
||||
if colon+1 < len(line) {
|
||||
value = strings.TrimSpace(line[colon+1:])
|
||||
}
|
||||
nameLower := strings.ToLower(name)
|
||||
req.HeaderOrder = append(req.HeaderOrder, nameLower)
|
||||
req.HeaderKV[nameLower] = value
|
||||
}
|
||||
}
|
||||
req.HeaderOrderSig = strings.Join(req.HeaderOrder, ";")
|
||||
|
||||
// Créer la clé de session
|
||||
var key correlation.SessionKey
|
||||
key.SrcIP[0] = byte(srcIPRaw >> 24)
|
||||
key.SrcIP[1] = byte(srcIPRaw >> 16)
|
||||
key.SrcIP[2] = byte(srcIPRaw >> 8)
|
||||
key.SrcIP[3] = byte(srcIPRaw)
|
||||
key.SrcPort = srcPort
|
||||
|
||||
// Filtrer les IPs sources ignorées
|
||||
if isIgnoredIP(key.SrcIP) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Mettre à jour la session
|
||||
mgr.Update(key, func(s *correlation.SessionState) {
|
||||
s.Requests = append(s.Requests, req)
|
||||
})
|
||||
|
||||
counter.Add(1)
|
||||
log.Printf("[apache] HTTP: pid=%d fd=%d %s %s (headers=%d)",
|
||||
pidTgid>>32, fd, httpMethod, uri, len(req.HeaderOrder))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user