diff --git a/.gitignore b/.gitignore index 9874a71..7f9e696 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,5 @@ record.log.* # Log files *.log docs/blog + +node_modules/ diff --git a/bpf/container_info.h b/bpf/container_info.h new file mode 100644 index 0000000..09de465 --- /dev/null +++ b/bpf/container_info.h @@ -0,0 +1,99 @@ +/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */ +/* container_info.h — Userspace helpers for container metadata. + * Provides ns_pid and container_id extraction from /proc. + * Shared by process_new.c and sslsniff.c. + */ +#ifndef __CONTAINER_INFO_H +#define __CONTAINER_INFO_H + +#include +#include +#include +#include + +/* Read namespace PID from /proc//status NSpid line. + * Returns the innermost namespace PID, or -1 if not in a namespace. */ +static int get_ns_pid(pid_t host_pid) +{ + char path[64]; + char line[256]; + snprintf(path, sizeof(path), "/proc/%d/status", host_pid); + FILE *f = fopen(path, "r"); + if (!f) + return -1; + int ns_pid = -1; + while (fgets(line, sizeof(line), f)) { + if (strncmp(line, "NSpid:", 6) == 0) { + char *p = line + 6; + int last_pid = -1; + int count = 0; + while (*p) { + while (*p == '\t' || *p == ' ') + p++; + if (*p >= '0' && *p <= '9') { + last_pid = (int)strtol(p, &p, 10); + count++; + } else { + break; + } + } + if (count >= 2) + ns_pid = last_pid; + break; + } + } + fclose(f); + return ns_pid; +} + +/* Read container ID from /proc//cgroup (docker/containerd format). + * Writes short (12-char) container ID to out. Returns 0 on success. */ +static int get_container_id(pid_t host_pid, char *out, size_t out_len) +{ + char path[64]; + char line[512]; + snprintf(path, sizeof(path), "/proc/%d/cgroup", host_pid); + FILE *f = fopen(path, "r"); + if (!f) + return -1; + out[0] = '\0'; + while (fgets(line, sizeof(line), f)) { + char *p = line; + while (*p) { + int hex_len = 0; + char *start = p; + while ((*p >= '0' && *p <= '9') || (*p >= 'a' && *p <= 'f')) { + hex_len++; + p++; + } + if (hex_len == 64) { + int copy_len = (int)out_len - 1 < 12 ? (int)out_len - 1 : 12; + memcpy(out, start, copy_len); + out[copy_len] = '\0'; + fclose(f); + return 0; + } + if (hex_len == 0) + p++; + } + } + fclose(f); + return -1; +} + +/* Print container JSON fields: ,\"ns_pid\":N,\"container_id\":\"xxx\" + * Prints nothing if not in a container. */ +static void print_container_fields(pid_t host_pid) +{ + int ns_pid = get_ns_pid(host_pid); + if (ns_pid > 0) { + printf(",\"ns_pid\":%d", ns_pid); + char container_id[16]; + if (get_container_id(host_pid, container_id, sizeof(container_id)) == 0 && + container_id[0] != '\0') { + printf(",\"container_id\":\"%s\"", container_id); + } + } +} + +#endif /* __CONTAINER_INFO_H */ diff --git a/bpf/container_utils.h b/bpf/container_utils.h new file mode 100644 index 0000000..49aa2bf --- /dev/null +++ b/bpf/container_utils.h @@ -0,0 +1,201 @@ +// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) +// Container-aware library path resolution for uprobe attachment. +// Resolves SSL library paths inside container mount namespaces +// via /proc//maps and /proc//root/. + +#ifndef __CONTAINER_UTILS_H +#define __CONTAINER_UTILS_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef PATH_MAX +#define PATH_MAX 4096 +#endif + +#define MAX_CONTAINER_LIBS 64 + +/* ---------- data structures ---------- */ + +struct pid_lib_entry { + pid_t pid; + char lib_path[PATH_MAX]; /* host-perspective path */ +}; + +struct dynamic_links { + struct bpf_link **links; + int count; + int capacity; +}; + +/* ---------- dynamic link management ---------- */ + +static struct dynamic_links g_extra_links = { + .links = NULL, .count = 0, .capacity = 0 +}; + +static void add_dynamic_link(struct bpf_link *link) +{ + if (!link) + return; + if (g_extra_links.count >= g_extra_links.capacity) { + int new_cap = g_extra_links.capacity ? g_extra_links.capacity * 2 : 16; + struct bpf_link **tmp = realloc(g_extra_links.links, + new_cap * sizeof(struct bpf_link *)); + if (!tmp) { + fprintf(stderr, "realloc dynamic_links failed\n"); + return; + } + g_extra_links.links = tmp; + g_extra_links.capacity = new_cap; + } + g_extra_links.links[g_extra_links.count++] = link; +} + +static void cleanup_dynamic_links(void) +{ + for (int i = 0; i < g_extra_links.count; i++) + bpf_link__destroy(g_extra_links.links[i]); + free(g_extra_links.links); + g_extra_links.links = NULL; + g_extra_links.count = 0; + g_extra_links.capacity = 0; +} + +/* ---------- container library path resolution ---------- */ + +/* + * Find the host-perspective path of a library loaded by a specific PID. + * + * For container processes the in-container path (e.g. /usr/lib/libssl.so.3) + * is translated to /proc//root/usr/lib/libssl.so.3 so that + * bpf_program__attach_uprobe_opts() can locate the correct ELF on the host + * filesystem. + */ +static char *find_library_path_for_pid(pid_t pid, const char *libname, + bool verbose) +{ + static char host_path[PATH_MAX]; + char maps_path[64]; + char line[4096]; + FILE *fp; + + snprintf(maps_path, sizeof(maps_path), "/proc/%d/maps", pid); + fp = fopen(maps_path, "r"); + if (!fp) { + if (verbose) + fprintf(stderr, "Failed to open %s: %s\n", + maps_path, strerror(errno)); + return NULL; + } + + while (fgets(line, sizeof(line), fp)) { + /* only consider executable/read-only mappings with matching lib name */ + if (strstr(line, libname) == NULL) + continue; + if (strstr(line, "r-xp") == NULL && strstr(line, "r--p") == NULL) + continue; + + char *path = strchr(line, '/'); + if (!path) + continue; + + char *nl = strchr(path, '\n'); + if (nl) + *nl = '\0'; + + /* convert to host-perspective path via /proc//root */ + snprintf(host_path, sizeof(host_path), "/proc/%d/root%s", pid, path); + + if (access(host_path, R_OK) == 0) { + fclose(fp); + if (verbose) + fprintf(stderr, + "Found %s for PID %d: %s -> %s\n", + libname, pid, path, host_path); + return host_path; + } + } + + fclose(fp); + return NULL; +} + +/* + * Scan /proc for all processes that have loaded `libname`, returning + * deduplicated (by host path) entries. + */ +static int find_pids_with_library(const char *libname, + struct pid_lib_entry *entries, + int max_entries, + bool verbose) +{ + DIR *proc_dir; + struct dirent *ent; + int count = 0; + + /* Heap-allocated dedup array to avoid 256KB stack allocation */ + char (*seen)[PATH_MAX] = calloc(MAX_CONTAINER_LIBS, PATH_MAX); + int seen_count = 0; + + if (!seen) { + fprintf(stderr, "Failed to allocate dedup buffer\n"); + return 0; + } + + proc_dir = opendir("/proc"); + if (!proc_dir) { + free(seen); + return 0; + } + + while ((ent = readdir(proc_dir)) && count < max_entries) { + pid_t pid = atoi(ent->d_name); + if (pid <= 0) + continue; + + char *path = find_library_path_for_pid(pid, libname, false); + if (!path) + continue; + + /* dedup */ + bool dup = false; + for (int i = 0; i < seen_count; i++) { + if (strcmp(seen[i], path) == 0) { + dup = true; + break; + } + } + if (dup) + continue; + + if (seen_count < MAX_CONTAINER_LIBS) { + strncpy(seen[seen_count], path, PATH_MAX - 1); + seen[seen_count][PATH_MAX - 1] = '\0'; + seen_count++; + } + + entries[count].pid = pid; + strncpy(entries[count].lib_path, path, PATH_MAX - 1); + entries[count].lib_path[PATH_MAX - 1] = '\0'; + count++; + + if (verbose) + fprintf(stderr, "Discovered container SSL lib: %s (via PID %d)\n", + path, pid); + } + + closedir(proc_dir); + free(seen); + return count; +} + +#endif /* __CONTAINER_UTILS_H */ diff --git a/bpf/process b/bpf/process new file mode 100755 index 0000000..44cdd64 Binary files /dev/null and b/bpf/process differ diff --git a/bpf/process.bpf.c b/bpf/process.bpf.c index 6713980..614c019 100644 --- a/bpf/process.bpf.c +++ b/bpf/process.bpf.c @@ -104,26 +104,43 @@ int handle_exec(struct trace_event_raw_sched_process_exec *ctx) unsigned long arg_end = BPF_CORE_READ(mm, arg_end); unsigned long arg_len = arg_end - arg_start; - /* Limit to buffer size */ if (arg_len > MAX_COMMAND_LEN - 1) arg_len = MAX_COMMAND_LEN - 1; - /* Read command line from userspace memory */ if (arg_len > 0) { - long ret = bpf_probe_read_user_str(&e->full_command, arg_len + 1, (void *)arg_start); + /* + * Read the full argv block using bpf_probe_read_user (not _str). + * _str stops at first \0 and only captures argv[0]. + * _user reads raw bytes: "chmod\0+x\0/path\0" -- we get all args. + * + * We always read exactly MAX_COMMAND_LEN-1 bytes (a compile-time + * constant) so that BPF verifiers on all kernel versions can + * prove the access is bounded. This may read past arg_end into + * environment variables, but userspace trims to arg_len. + * + * NO LOOPS in BPF -- all post-processing (\0->space, trimming) + * is done in userspace to stay within the verifier instruction + * limit on kernel 5.15 (1,000,000 insns). + */ + long ret = bpf_probe_read_user(e->full_command, + MAX_COMMAND_LEN - 1, + (void *)arg_start); if (ret < 0) { - /* Fallback to just comm if we can't read cmdline */ - bpf_probe_read_kernel_str(&e->full_command, sizeof(e->full_command), e->comm); + bpf_probe_read_kernel_str(e->full_command, + sizeof(e->full_command), + e->comm); + e->full_command[MAX_COMMAND_LEN - 1] = '\0'; } else { - /* Replace null bytes with spaces for readability */ - for (int i = 0; i < MAX_COMMAND_LEN - 1 && i < ret - 1; i++) { - if (e->full_command[i] == '\0') - e->full_command[i] = ' '; - } + e->full_command[MAX_COMMAND_LEN - 1] = '\0'; } + /* Store actual arg_len in exit_code for userspace trimming. + * exec events don't use exit_code, so this field is free. */ + arg_len &= (MAX_COMMAND_LEN - 1); + e->exit_code = (unsigned)arg_len; } else { - /* No arguments, use comm */ - bpf_probe_read_kernel_str(&e->full_command, sizeof(e->full_command), e->comm); + bpf_probe_read_kernel_str(e->full_command, + sizeof(e->full_command), e->comm); + e->exit_code = 0; } /* successfully submit it to user-space for post-processing */ diff --git a/bpf/process.c b/bpf/process.c index 57c23e4..5665c41 100644 --- a/bpf/process.c +++ b/bpf/process.c @@ -534,7 +534,7 @@ static int handle_event(void *ctx, void *data, size_t data_sz) printf("\"pid\":%d,", e->pid); printf("\"ppid\":%d", e->ppid); printf(",\"filename\":\"%s\"", e->filename); - printf(",\"full_command\":\"%s\"", e->full_command); + printf(",\"full_command\":\"%s\"", postprocess_full_command(e->full_command, MAX_COMMAND_LEN, e->exit_code)); printf("}\n"); fflush(stdout); } else if (tracker->filter_mode == FILTER_MODE_FILTER) { @@ -553,7 +553,7 @@ static int handle_event(void *ctx, void *data, size_t data_sz) printf("\"pid\":%d,", e->pid); printf("\"ppid\":%d", e->ppid); printf(",\"filename\":\"%s\"", e->filename); - printf(",\"full_command\":\"%s\"", e->full_command); + printf(",\"full_command\":\"%s\"", postprocess_full_command(e->full_command, MAX_COMMAND_LEN, e->exit_code)); printf("}\n"); fflush(stdout); } diff --git a/bpf/process_ext/bpf_common.h b/bpf/process_ext/bpf_common.h index 4229e19..b7702bf 100644 --- a/bpf/process_ext/bpf_common.h +++ b/bpf/process_ext/bpf_common.h @@ -100,40 +100,48 @@ static __always_inline void format_fd_detail(char *buf, int buf_len, int fd) } /* Format "N.N.N.N:PORT" for IPv4 addresses without bpf_snprintf */ +/* Fully unrolled for BPF verifier (no loops / back-edges) */ +static __always_inline void write_octet(char *buf, int buf_len, int *pos, u8 val) +{ + if (val >= 100 && *pos < buf_len - 1) buf[(*pos)++] = '0' + val / 100; + if (val >= 10 && *pos < buf_len - 1) buf[(*pos)++] = '0' + (val / 10) % 10; + if (*pos < buf_len - 1) buf[(*pos)++] = '0' + val % 10; +} + static __always_inline void format_ipv4_port(char *buf, int buf_len, u32 ip, u16 port) { int pos = 0; - u8 octets[4]; - octets[0] = ip & 0xFF; - octets[1] = (ip >> 8) & 0xFF; - octets[2] = (ip >> 16) & 0xFF; - octets[3] = (ip >> 24) & 0xFF; - - /* Write each octet */ - for (int o = 0; o < 4 && pos < buf_len - 2; o++) { - if (o > 0 && pos < buf_len - 1) - buf[pos++] = '.'; - u8 val = octets[o]; - if (val >= 100 && pos < buf_len - 1) buf[pos++] = '0' + val / 100; - if (val >= 10 && pos < buf_len - 1) buf[pos++] = '0' + (val / 10) % 10; - if (pos < buf_len - 1) buf[pos++] = '0' + val % 10; - } - - /* :PORT */ + u8 o0 = ip & 0xFF; + u8 o1 = (ip >> 8) & 0xFF; + u8 o2 = (ip >> 16) & 0xFF; + u8 o3 = (ip >> 24) & 0xFF; + + /* Octet 0 */ + write_octet(buf, buf_len, &pos, o0); + /* Octet 1 */ + if (pos < buf_len - 1) buf[pos++] = '.'; + write_octet(buf, buf_len, &pos, o1); + /* Octet 2 */ + if (pos < buf_len - 1) buf[pos++] = '.'; + write_octet(buf, buf_len, &pos, o2); + /* Octet 3 */ + if (pos < buf_len - 1) buf[pos++] = '.'; + write_octet(buf, buf_len, &pos, o3); + + /* :PORT — max 5 digits, write manually */ if (pos < buf_len - 1) buf[pos++] = ':'; - char pdigits[6]; - int plen = 0; unsigned int p = port; - if (p == 0) { - pdigits[plen++] = '0'; - } else { - while (p > 0 && plen < 5) { - pdigits[plen++] = '0' + (p % 10); - p /= 10; - } - } - for (int i = plen - 1; i >= 0 && pos < buf_len - 1; i--) - buf[pos++] = pdigits[i]; + /* Extract each digit (max 65535 = 5 digits) */ + char d4 = '0' + (p / 10000) % 10; + char d3 = '0' + (p / 1000) % 10; + char d2 = '0' + (p / 100) % 10; + char d1 = '0' + (p / 10) % 10; + char d0 = '0' + p % 10; + if (p >= 10000 && pos < buf_len - 1) buf[pos++] = d4; + if (p >= 1000 && pos < buf_len - 1) buf[pos++] = d3; + if (p >= 100 && pos < buf_len - 1) buf[pos++] = d2; + if (p >= 10 && pos < buf_len - 1) buf[pos++] = d1; + if (pos < buf_len - 1) buf[pos++] = d0; buf[pos] = '\0'; } diff --git a/bpf/process_new.bpf.c b/bpf/process_new.bpf.c index 9131132..28eee5d 100644 --- a/bpf/process_new.bpf.c +++ b/bpf/process_new.bpf.c @@ -162,7 +162,7 @@ int handle_exec(struct trace_event_raw_sched_process_exec *ctx) fname_off = ctx->__data_loc_filename & 0xFFFF; bpf_probe_read_str(&e->filename, sizeof(e->filename), (void *)ctx + fname_off); - + /* Capture full command line with arguments from mm->arg_start */ struct mm_struct *mm = BPF_CORE_READ(task, mm); unsigned long arg_start = BPF_CORE_READ(mm, arg_start); unsigned long arg_end = BPF_CORE_READ(mm, arg_end); @@ -172,23 +172,46 @@ int handle_exec(struct trace_event_raw_sched_process_exec *ctx) arg_len = MAX_COMMAND_LEN - 1; if (arg_len > 0) { - long ret = bpf_probe_read_user_str(&e->full_command, arg_len + 1, (void *)arg_start); + /* + * Read the full argv block using bpf_probe_read_user (not _str). + * _str stops at first \0 and only captures argv[0]. + * _user reads raw bytes: "chmod\0+x\0/path\0" -- we get all args. + * + * We always read exactly MAX_COMMAND_LEN-1 bytes (a compile-time + * constant) so that BPF verifiers on all kernel versions can + * prove the access is bounded. This may read past arg_end into + * environment variables, but userspace trims to arg_len. + * + * NO LOOPS in BPF -- all post-processing (\0->space, trimming) + * is done in userspace to stay within the verifier instruction + * limit on kernel 5.15 (1,000,000 insns). + */ + long ret = bpf_probe_read_user(e->full_command, + MAX_COMMAND_LEN - 1, + (void *)arg_start); if (ret < 0) { - bpf_probe_read_kernel_str(&e->full_command, sizeof(e->full_command), e->comm); + bpf_probe_read_kernel_str(e->full_command, + sizeof(e->full_command), + e->comm); + e->full_command[MAX_COMMAND_LEN - 1] = '\0'; } else { - for (int i = 0; i < MAX_COMMAND_LEN - 1 && i < ret - 1; i++) { - if (e->full_command[i] == '\0') - e->full_command[i] = ' '; - } + e->full_command[MAX_COMMAND_LEN - 1] = '\0'; } + /* Store actual arg_len in exit_code for userspace trimming. + * exec events don't use exit_code, so this field is free. */ + arg_len &= (MAX_COMMAND_LEN - 1); + e->exit_code = (unsigned)arg_len; } else { - bpf_probe_read_kernel_str(&e->full_command, sizeof(e->full_command), e->comm); + bpf_probe_read_kernel_str(e->full_command, + sizeof(e->full_command), e->comm); + e->exit_code = 0; } bpf_ringbuf_submit(e, 0); return 0; } + SEC("tp/sched/sched_process_exit") int handle_exit(struct trace_event_raw_sched_process_template *ctx) { diff --git a/bpf/process_new.c b/bpf/process_new.c index 8443917..54a3c43 100644 --- a/bpf/process_new.c +++ b/bpf/process_new.c @@ -22,6 +22,7 @@ #include "process_ext/map_flush.h" #include "process_ext/mem_info.h" #include "process_ext/resource_sampler.h" +#include "container_info.h" #define MAX_COMMAND_LIST 256 #define FILE_DEDUP_WINDOW_NS 60000000000ULL /* 60 seconds */ @@ -98,6 +99,8 @@ static int g_overflow_fd = -1; static int g_exit_mem_fd = -1; /* Page size for memory info */ + + static long page_size_kb; /* Target PID for resource sampling (set from -p or first matched process) */ @@ -497,6 +500,7 @@ static void print_file_open_event(const struct event *e, uint64_t timestamp_ns, printf("\"flags\":%d", e->file_op.flags); if (extra_fields && strlen(extra_fields) > 0) printf(",%s", extra_fields); + print_container_fields(e->pid); printf("}\n"); fflush(stdout); } @@ -682,6 +686,7 @@ static int handle_event(void *ctx, void *data, size_t data_sz) } } + print_container_fields(e->pid); printf("}\n"); fflush(stdout); @@ -717,7 +722,7 @@ static int handle_event(void *ctx, void *data, size_t data_sz) "\"comm\":\"%s\",\"pid\":%d,\"ppid\":%d", (unsigned long long)timestamp_ns, e->comm, e->pid, e->ppid); printf(",\"filename\":\"%s\"", e->filename); - printf(",\"full_command\":\"%s\"", e->full_command); + printf(",\"full_command\":\"%s\"", postprocess_full_command(e->full_command, MAX_COMMAND_LEN, e->exit_code)); /* Memory info at exec */ struct proc_mem_info mem; @@ -727,6 +732,7 @@ static int handle_event(void *ctx, void *data, size_t data_sz) mem.shared_pages * page_size_kb); } + print_container_fields(e->pid); printf("}\n"); fflush(stdout); } else if (tracker->filter_mode == FILTER_MODE_FILTER) { @@ -739,7 +745,8 @@ static int handle_event(void *ctx, void *data, size_t data_sz) "\"comm\":\"%s\",\"pid\":%d,\"ppid\":%d", (unsigned long long)timestamp_ns, e->comm, e->pid, e->ppid); printf(",\"filename\":\"%s\"", e->filename); - printf(",\"full_command\":\"%s\"", e->full_command); + printf(",\"full_command\":\"%s\"", postprocess_full_command(e->full_command, MAX_COMMAND_LEN, e->exit_code)); + print_container_fields(e->pid); printf("}\n"); fflush(stdout); } diff --git a/bpf/process_utils.h b/bpf/process_utils.h index aff9b84..818b7ed 100644 --- a/bpf/process_utils.h +++ b/bpf/process_utils.h @@ -146,4 +146,43 @@ static int count_matching_processes(char **command_list, int command_count, bool -#endif /* __PROCESS_UTILS_H */ \ No newline at end of file + +/* + * postprocess_full_command - Convert raw argv bytes to a readable command string. + * + * BPF reads raw argv memory which contains \0 between arguments and may + * include environment variable data past arg_end. This function: + * 1. Copies data to a local buffer (ringbuf consumer memory is read-only) + * 2. Trims to actual arg_len (from e->exit_code) to remove env var leakage + * 3. Replaces \0 separators with spaces + * + * Returns pointer to a static buffer (NOT thread-safe, single consumer). + */ +static const char *postprocess_full_command(const char *buf, int buf_size, unsigned int arg_len) +{ + static char cmd_buf[MAX_COMMAND_LEN]; + + if (arg_len == 0 || arg_len > (unsigned int)(buf_size - 1)) { + /* No arg_len info: just copy the first null-terminated string */ + int len = 0; + while (len < buf_size - 1 && buf[len] != '\0') + len++; + if (len > 0) + memcpy(cmd_buf, buf, len); + cmd_buf[len] = '\0'; + return cmd_buf; + } + + memcpy(cmd_buf, buf, arg_len); + cmd_buf[arg_len] = '\0'; + + /* Replace \0 separators between argv entries with spaces */ + for (int i = 0; i < (int)arg_len - 1; i++) { + if (cmd_buf[i] == '\0') + cmd_buf[i] = ' '; + } + + return cmd_buf; +} + +#endif /* __PROCESS_UTILS_H */ diff --git a/bpf/sslsniff.c b/bpf/sslsniff.c index 29346e6..fadca82 100644 --- a/bpf/sslsniff.c +++ b/bpf/sslsniff.c @@ -22,6 +22,8 @@ #include "sslsniff.skel.h" #include "sslsniff.h" +#include "container_utils.h" +#include "container_info.h" #define INVALID_UID -1 #define INVALID_PID -1 @@ -393,6 +395,101 @@ int attach_openssl_by_offset(struct sslsniff_bpf *skel, const char *lib, return 0; } + +/* + * Attach OpenSSL uprobe to a container library path. + * Uses dynamic link management instead of skel->links to support + * multiple simultaneous uprobe attachments across containers. + */ +int attach_openssl_container(struct sslsniff_bpf *skel, const char *lib) { + LIBBPF_OPTS(bpf_uprobe_opts, opts); + struct bpf_link *link; + int attached = 0; + + const char *sym_names[] = {"SSL_write", "SSL_read"}; + struct bpf_program *entry_progs[] = { + skel->progs.probe_SSL_rw_enter, + skel->progs.probe_SSL_rw_enter, + }; + struct bpf_program *exit_progs[] = { + skel->progs.probe_SSL_write_exit, + skel->progs.probe_SSL_read_exit, + }; + + for (int i = 0; i < 2; i++) { + /* entry */ + opts.func_name = sym_names[i]; + opts.retprobe = false; + link = bpf_program__attach_uprobe_opts(entry_progs[i], -1, lib, 0, &opts); + if (!libbpf_get_error(link)) { + add_dynamic_link(link); + attached++; + } + else { + link = NULL; + } + /* return */ + opts.retprobe = true; + link = bpf_program__attach_uprobe_opts(exit_progs[i], -1, lib, 0, &opts); + if (!libbpf_get_error(link)) { + add_dynamic_link(link); + attached++; + } + else { + link = NULL; + } + } + + /* SSL_write_ex / SSL_read_ex */ + const char *ex_sym_names[] = {"SSL_write_ex", "SSL_read_ex"}; + struct bpf_program *ex_entry_progs[] = { + skel->progs.probe_SSL_write_ex_enter, + skel->progs.probe_SSL_read_ex_enter, + }; + struct bpf_program *ex_exit_progs[] = { + skel->progs.probe_SSL_write_ex_exit, + skel->progs.probe_SSL_read_ex_exit, + }; + + for (int i = 0; i < 2; i++) { + opts.func_name = ex_sym_names[i]; + opts.retprobe = false; + link = bpf_program__attach_uprobe_opts(ex_entry_progs[i], -1, lib, 0, &opts); + if (!libbpf_get_error(link)) { + add_dynamic_link(link); + attached++; + } + else { + link = NULL; + } + opts.retprobe = true; + link = bpf_program__attach_uprobe_opts(ex_exit_progs[i], -1, lib, 0, &opts); + if (!libbpf_get_error(link)) { + add_dynamic_link(link); + attached++; + } + else { + link = NULL; + } + } + + /* SSL_do_handshake */ + opts.func_name = "SSL_do_handshake"; + opts.retprobe = false; + link = bpf_program__attach_uprobe_opts(skel->progs.probe_SSL_do_handshake_enter, -1, lib, 0, &opts); + if (!libbpf_get_error(link)) { add_dynamic_link(link); attached++; } + else { link = NULL; } + + opts.retprobe = true; + link = bpf_program__attach_uprobe_opts(skel->progs.probe_SSL_do_handshake_exit, -1, lib, 0, &opts); + if (!libbpf_get_error(link)) { add_dynamic_link(link); attached++; } + else { link = NULL; } + + if (verbose) + fprintf(stderr, "Container uprobe: attached %d probes to %s\n", attached, lib); + return attached > 0 ? 0 : -1; +} + /* * Find the path of a library using ldconfig. */ @@ -607,6 +704,9 @@ void print_event(struct probe_SSL_data_t *event, const char *evt) { printf("\"data\":null,\"truncated\":false"); } + // Container info (ns_pid, container_id) if applicable + print_container_fields(event->pid); + // Close JSON object printf("}\n"); fflush(stdout); @@ -663,15 +763,48 @@ int main(int argc, char **argv) { } if (env.openssl) { - char *openssl_path = find_library_path("libssl.so"); - if (verbose) { - fprintf(stderr, "OpenSSL path: %s\n", openssl_path ? openssl_path : "not found"); + char *openssl_path = NULL; + + if (env.pid > 0) { + /* Specified PID: resolve SSL lib from that process's maps + * (works for container processes) */ + openssl_path = find_library_path_for_pid(env.pid, "libssl.so", verbose); + if (verbose) + fprintf(stderr, "OpenSSL path (via PID %d maps): %s\n", + env.pid, openssl_path ? openssl_path : "not found"); + } + + if (!openssl_path) { + /* Fallback: host ldconfig */ + openssl_path = find_library_path("libssl.so"); + if (verbose) + fprintf(stderr, "OpenSSL path (host ldconfig): %s\n", + openssl_path ? openssl_path : "not found"); } + if (openssl_path) { attach_openssl(obj, openssl_path); } else { warn("OpenSSL library not found\n"); } + + /* Scan for additional SSL libraries in container processes */ + if (env.pid <= 0) { + struct pid_lib_entry entries[MAX_CONTAINER_LIBS]; + int count = find_pids_with_library("libssl.so", entries, + MAX_CONTAINER_LIBS, verbose); + for (int i = 0; i < count; i++) { + /* Skip if same as host lib already attached */ + if (openssl_path && + strcmp(entries[i].lib_path, openssl_path) == 0) + continue; + if (verbose) + fprintf(stderr, + "Attaching uprobe to container lib: %s\n", + entries[i].lib_path); + attach_openssl_container(obj, entries[i].lib_path); + } + } } if (env.gnutls) { char *gnutls_path = find_library_path("libgnutls.so"); @@ -750,6 +883,7 @@ int main(int argc, char **argv) { } cleanup: + cleanup_dynamic_links(); if (event_buf) { free(event_buf); event_buf = NULL; diff --git a/collector/src/framework/runners/process.rs b/collector/src/framework/runners/process.rs index 98e1d1b..d012131 100644 --- a/collector/src/framework/runners/process.rs +++ b/collector/src/framework/runners/process.rs @@ -64,37 +64,30 @@ impl Runner for ProcessRunner { let json_stream = self.executor.get_json_stream().await?; // Convert JSON values directly to framework Events - let event_stream = json_stream.map(|json_value| { - // Extract timestamp if available, otherwise use current time + // Filter out metadata events (CLOCK_SYNC etc.) that lack pid/comm + let event_stream = json_stream.filter_map(|json_value| async move { + let pid = json_value.get("pid").and_then(|v| v.as_u64()).map(|p| p as u32)?; let timestamp = json_value.get("timestamp") .and_then(|v| v.as_u64()) .unwrap_or_else(|| { - panic!("Missing timestamp field in process event: {}", json_value); + use std::time::{SystemTime, UNIX_EPOCH}; + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_nanos() as u64) + .unwrap_or(0) }); - - // Extract pid - panic if not found - let pid = json_value.get("pid") - .and_then(|v| v.as_u64()) - .map(|p| p as u32) - .unwrap_or_else(|| { - panic!("Missing pid field in process event: {}", json_value); - }); - - // Extract comm - panic if not found let comm = json_value.get("comm") .and_then(|v| v.as_str()) - .unwrap_or_else(|| { - panic!("Missing comm field in process event: {}", json_value); - }) + .unwrap_or("unknown") .to_string(); - Event::new_with_timestamp( + Some(Event::new_with_timestamp( timestamp, - "process".to_string(), // source is runner name + "process".to_string(), pid, comm, json_value, - ) + )) }); AnalyzerProcessor::process_through_analyzers(Box::pin(event_stream), &mut self.analyzers).await diff --git a/frontend/src/components/process-tree/BlockAdapters.tsx b/frontend/src/components/process-tree/BlockAdapters.tsx index 14d8b2b..8f7b7e6 100644 --- a/frontend/src/components/process-tree/BlockAdapters.tsx +++ b/frontend/src/components/process-tree/BlockAdapters.tsx @@ -316,6 +316,7 @@ export function adaptFileEvent(event: ParsedEvent): UnifiedBlockData { const tags = [operation.toUpperCase()]; if (metadata.fd !== undefined) tags.push(`FD ${metadata.fd}`); if (metadata.size !== undefined) tags.push(formatFileSize(metadata.size)); + if (metadata.container_id) tags.push(`🐳${metadata.container_id}`); // Fold content: file path const foldContent = filepath; @@ -367,9 +368,11 @@ export function adaptProcessEvent(event: ParsedEvent): UnifiedBlockData { const colors = getProcessColors(eventType); const tags = [eventType.toUpperCase()]; if (pid) tags.push(`PID ${pid}`); + if (metadata.container_id) tags.push(`🐳${metadata.container_id}`); - // Fold content: command and PID - const foldContent = comm && pid ? `${comm} (PID: ${pid})` : comm || `PID: ${pid}`; + // Fold content: command and PID, with ns_pid for container processes + const pidDisplay = metadata.ns_pid ? `${pid} (ns:${metadata.ns_pid})` : pid; + const foldContent = comm && pid ? `${comm} (PID: ${pidDisplay})` : comm || `PID: ${pidDisplay}`; // Expanded content: everything const expandedContent = event.content || JSON.stringify(event.metadata, null, 2); @@ -390,22 +393,86 @@ export function adaptProcessEvent(event: ParsedEvent): UnifiedBlockData { export function adaptSSLEvent(event: ParsedEvent): UnifiedBlockData { const metadata = event.metadata || {}; - - const direction = metadata.direction || ''; - const size = metadata.data_size || metadata.size || 0; - const comm = metadata.comm || ''; + const originalSource = metadata.original_source || ''; + const isHttpParser = originalSource === 'http_parser'; + + let direction = ''; + let size = 0; + let foldContent = ''; + let expandedContent = ''; + + if (isHttpParser) { + // http_parser transformed event: fields are method, host, path, body, headers, total_size, message_type, status_code, etc. + const method = metadata.method || ''; + const messageType = metadata.message_type || ''; + const host = metadata.host || metadata.headers?.host || ''; + const path = metadata.path || '/'; + const statusCode = metadata.status_code; + const body = metadata.body || ''; + + direction = messageType === 'response' ? 'RECV' : + messageType === 'request' ? 'SEND' : + (method && method !== 'UNKNOWN' ? 'SEND' : ''); + size = metadata.total_size || metadata.content_length || (typeof body === 'string' ? body.length : 0); + + // Fold content: show HTTP first line summary + const firstLine = metadata.first_line || ''; + if (firstLine) { + foldContent = firstLine; + } else if (statusCode) { + foldContent = `${statusCode} ${host}${path}`; + } else if (method && method !== 'UNKNOWN') { + foldContent = `${method} ${host}${path}`; + } else { + foldContent = `${host}${path}`; + } - // Fold content: size and command - const foldContent = comm ? `${size} bytes - ${comm}` : `${size} bytes`; + // Expanded content: show body if available, else full metadata + if (body && typeof body === 'string' && body.length > 0) { + // Try to pretty-print JSON body + try { + const parsed = JSON.parse(body); + expandedContent = JSON.stringify(parsed, null, 2); + } catch { + expandedContent = body; + } + } else { + expandedContent = event.content || JSON.stringify(metadata, null, 2); + } + } else { + // Raw sslsniff event: fields are function, buf_size, data, comm, ns_pid, container_id + const sslFunction = metadata.function || metadata.direction || ''; + direction = sslFunction.includes('WRITE') || sslFunction.includes('SEND') ? 'SEND' : + sslFunction.includes('READ') || sslFunction.includes('RECV') ? 'RECV' : + sslFunction.includes('HANDSHAKE') ? 'HANDSHAKE' : ''; + size = metadata.buf_size || metadata.data_size || metadata.len || metadata.size || 0; + const comm = metadata.comm || ''; + const sslData = metadata.data || ''; + + if (sslData && typeof sslData === 'string' && sslData.length > 0) { + const previewSource = sslData.slice(0, 240); + const preview = previewSource.replace(/\r\n/g, ' ').replace(/\n/g, ' ').substring(0, 120); + foldContent = preview + (sslData.length > 120 ? '...' : ''); + } else { + foldContent = comm ? `${size} bytes - ${comm}` : `${size} bytes`; + } - // Expanded content: everything - const expandedContent = event.content || JSON.stringify(event.metadata, null, 2); + if (sslData && typeof sslData === 'string' && sslData.length > 0) { + expandedContent = sslData; + } else { + expandedContent = event.content || JSON.stringify(metadata, null, 2); + } + } + + const sizeTag = size > 0 ? formatFileSize(size) : ''; + const containerTag = metadata.container_id ? `🐳${metadata.container_id}` : ''; + const sourceTag = isHttpParser ? 'HTTP' : 'TLS'; return { id: event.id, type: 'ssl', timestamp: event.timestamp, - tags: ['SSL', direction.toUpperCase(), `${size} bytes`].filter(Boolean), + tags: ['SSL', sourceTag, direction, sizeTag, containerTag].filter(Boolean), bgGradient: 'bg-gradient-to-r from-orange-50 via-amber-50 to-yellow-50', borderColor: 'border-orange-400', iconColor: 'text-orange-600',