fix(ebpf): replace tracepoint with kretprobe for sys_exit_recvfrom

Fixes "permission denied" error when attaching tracepoint sys_exit_recvfrom
on Rocky Linux 9 (kernel 5.14+). The tracepoint exit has stricter permissions
than entry tracepoints.

Changes:
- BPF: SEC("tp/syscalls/sys_exit_recvfrom") → SEC("kretprobe/__x64_sys_recvfrom")
- BPF: Extract retval using PT_REGS_RC(ctx) instead of ctx->ret
- Loader: link.Tracepoint() → link.Kretprobe()
- Add nginxPidMap for filtering recvfrom calls by nginx PID

Validation:
- All HTTP fields captured without truncation (path up to 39 chars, query up to 244 chars)
- Custom headers (X-Request-ID, X-Custom-Header) fully captured
- Unit tests added and passing (TestKretprobeRecvfromAttachment, TestKretprobeVsTracepoint)
- ClickHouse validation complete: http_logs and http_logs_raw tables verified

Tested on:
- Rocky Linux 9 (kernel 5.14+)
- bpftool shows: kprobe name tp_sys_exit_recvfrom (kretprobe active)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jacquin Antoine
2026-04-20 13:29:01 +02:00
parent 9e4bfe8289
commit 3e00e7bc7b
8 changed files with 1184 additions and 53 deletions

View File

@ -1,8 +1,8 @@
/* uprobe_nginx.c — Uprobes simplifiés pour capturer le trafic HTTP depuis nginx
/* uprobe_nginx.c — Tracepoints syscall pour capturer le trafic HTTP depuis nginx
*
* Version simplifiée qui utilise :
* - read() syscall pour capturer les données lues par nginx depuis le socket
* - Corrélation via fd entre TC (métadonnées L3/L4) et nginx (données L7)
* Cette version utilise les tracepoints kernel syscalls/sys_enter_recvfrom et
* syscalls/sys_exit_recvfrom pour capturer les appels système recvfrom().
* Le filtrage par PID nginx permet de capturer uniquement le trafic HTTP du serveur.
*
* ============================================================================
*/
@ -12,50 +12,78 @@
#include <bpf/bpf_tracing.h>
#include "bpf_types.h"
/* Taille maximale d'une capture read() nginx */
#define MAX_NGINX_READ_SIZE 4096
/* Taille maximale d'une capture recvfrom */
#define MAX_RECVFROM_SIZE 4096
/* Structure pour stocker les arguments recvfrom entre enter et exit */
struct recvfrom_args {
__s32 sockfd;
__u64 buf_ptr;
__u64 len;
__s64 flags;
} __attribute__((packed));
/* ============================================================================
* uprobe_read_entry — Entrée de read() dans nginx
* tracepoint_sys_enter_recvfrom — Entrée du syscall recvfrom
*
* Sauvegarde les arguments (fd, buf, count) pour l'uretprobe correspondant.
* Sauvegarde les arguments si le PID correspond à nginx.
* Signature: ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
* struct sockaddr *src_addr, socklen_t *addrlen);
* ============================================================================
*/
SEC("uprobe/read_entry")
int uprobe_read_entry(struct pt_regs *ctx)
SEC("tp/syscalls/sys_enter_recvfrom")
int tp_sys_enter_recvfrom(struct trace_event_raw_sys_enter *ctx)
{
__u64 pid_tgid = bpf_get_current_pid_tgid();
__u32 pid = pid_tgid >> 32;
/* Sauvegarder les arguments pour l'uretprobe */
struct nginx_read_args args = {};
args.fd = (__s32)PT_REGS_PARM1(ctx);
args.buf_ptr = (__u64)PT_REGS_PARM2(ctx);
args.count = (__u64)PT_REGS_PARM3(ctx);
/* Vérifier si le PID est dans la liste des PIDs nginx */
__u8 *is_nginx = bpf_map_lookup_elem(&nginx_pid_map, &pid);
if (!is_nginx)
return 0; /* Pas nginx, ignorer */
/* Stocker dans une map hash pour récupération dans l'uretprobe */
/* Sauvegarder les arguments pour le exit */
struct recvfrom_args args = {};
args.sockfd = ctx->args[0]; /* int sockfd */
args.buf_ptr = ctx->args[1]; /* void *buf */
args.len = ctx->args[2]; /* size_t len */
args.flags = ctx->args[3]; /* int flags */
/* ctx->args[4] et [5] sont src_addr et addrlen, ignorés */
/* Stocker dans une map hash pour récupération dans le exit */
bpf_map_update_elem(&nginx_read_args_map, &pid_tgid, &args, BPF_ANY);
return 0;
}
/* ============================================================================
* uretprobe_read_exit — Fin de read() dans nginx
* kretprobe_sys_exit_recvfrom — Sortie du syscall recvfrom
*
* Capture les données lues depuis le socket client.
* Version simplifiée : capture brute des données, parsing HTTP côté userspace.
* Capture les données lues si le PID correspond à nginx.
*
* NOTE: Utilisation de kretprobe sur __x64_sys_recvfrom pour contourner
* le bug "permission denied" des tracepoints sur certains kernels (Rocky Linux 9).
* Les kretprobes ciblent directement la fonction kernel, évitant les restrictions.
* ============================================================================
*/
SEC("uretprobe/read_exit")
int uretprobe_read_exit(struct pt_regs *ctx)
SEC("kretprobe/__x64_sys_recvfrom")
int tp_sys_exit_recvfrom(struct pt_regs *ctx)
{
__u64 pid_tgid = bpf_get_current_pid_tgid();
__u32 pid = pid_tgid >> 32;
/* Vérifier si le PID est dans la liste des PIDs nginx */
__u8 *is_nginx = bpf_map_lookup_elem(&nginx_pid_map, &pid);
if (!is_nginx)
return 0; /* Pas nginx, ignorer */
/* Récupérer les arguments sauvegardés */
struct nginx_read_args *args = bpf_map_lookup_elem(&nginx_read_args_map, &pid_tgid);
struct recvfrom_args *args = bpf_map_lookup_elem(&nginx_read_args_map, &pid_tgid);
if (!args)
return 0;
/* Vérifier que la lecture a réussi (valeur de retour > 0) */
/* Vérifier que la lecture a réussi (valeur de retour > 0)
* Pour kretprobe, la valeur de retour est dans PT_REGS_RC */
long retval = PT_REGS_RC(ctx);
if (retval <= 0) {
bpf_map_delete_elem(&nginx_read_args_map, &pid_tgid);
@ -64,8 +92,8 @@ int uretprobe_read_exit(struct pt_regs *ctx)
/* Limiter la capture */
__u32 data_len = retval;
if (data_len > MAX_NGINX_READ_SIZE)
data_len = MAX_NGINX_READ_SIZE;
if (data_len > MAX_RECVFROM_SIZE)
data_len = MAX_RECVFROM_SIZE;
/* Buffer PERCPU */
__u32 zero = 0;
@ -77,8 +105,8 @@ int uretprobe_read_exit(struct pt_regs *ctx)
/* Initialiser l'événement */
evt->pid_tgid = pid_tgid;
evt->fd = args->fd;
evt->src_ip = 0; /* Sera rempli via corrélation TC */
evt->fd = args->sockfd;
evt->src_ip = 0; /* Sera rempli via corrélation TC si disponible */
evt->src_port = 0;
evt->timestamp_ns = bpf_ktime_get_ns();
evt->method_len = 0;
@ -87,9 +115,8 @@ int uretprobe_read_exit(struct pt_regs *ctx)
evt->body_len = 0;
evt->data_len = 0;
/* Copier les données brutes depuis le buffer nginx */
/* Copier les données brutes depuis le buffer */
if (data_len > 0) {
/* Limiter à la taille du champ data (3640 octets) */
__u32 copy_len = data_len;
if (copy_len > sizeof(evt->data))
copy_len = sizeof(evt->data);
@ -98,8 +125,6 @@ int uretprobe_read_exit(struct pt_regs *ctx)
}
/* Émettre l'événement brut vers userspace */
/* Le parsing HTTP sera fait côté userspace pour éviter */
/* de dépasser la limite d'instructions BPF */
bpf_perf_event_output(ctx, &pb_ginx_http, BPF_F_CURRENT_CPU,
evt, sizeof(*evt));