├── .gitignore ├── Makefile ├── README.md ├── bpf ├── kafka_lag.bpf.c ├── kafka_lag.c ├── kafka_pid.bpf.c ├── kafka_pid.c ├── minimal.c ├── python_lag.bpf.c ├── python_lag.c ├── python_tracer.bpf.c ├── python_tracer.c ├── python_tracer_0.bpf.c └── python_tracer_0.c ├── coordinator.go ├── docs └── logo.png ├── examples └── deploy.example.yaml ├── go.mod ├── go.sum ├── loader └── loader.go ├── test └── test.py └── vm_ebpf.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | deploy.yaml 2 | bpfluga.code-workspace 3 | .vscode 4 | coordinator 5 | loader/loader 6 | bpf/minimal.o 7 | to_consider 8 | PoC 9 | tools 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | compile-bpf: 2 | clang -O2 -g -target bpf -c bpf/minimal.c -o bpf/minimal.o 3 | 4 | compile-loader: 5 | GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '-extldflags "-static"' -o loader/loader loader/loader.go 6 | 7 | compile-coordinator: 8 | go build -o coordinator coordinator.go 9 | 10 | load: 11 | sudo ./coordinator examples/deploy.yaml 12 | 13 | start-vm: 14 | limactl start vm_ebpf 15 | 16 | stop-vm: 17 | limactl stop vm_ebpf 18 | 19 | open-vm: 20 | limactl shell vm_ebpf 21 | 22 | create-vm: 23 | limactl create vm_ebpf.yaml 24 | 25 | delete-vm: 26 | limactl stop vm_ebpf 27 | limactl delete vm_ebpf 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

3 | Logo 4 |
BPFluga 5 |

6 |

7 | An agentless eBPF observability tool to deploy eBPF programs to remote machines and collect metrics from them at scale. 8 |

9 |

10 | 11 | # About 12 | 13 | > [!NOTE] 14 | > This is my toy and side project to learn eBPF. I'm not an expert in this field (yet?). Use it at your own risk. 15 | 16 | BPFluga is an agentless eBPF observability tool designed for modern distributed systems. Built in Go using the cilium/ebpf library. 17 | 18 | Inspired by the graceful beluga whale, bpfluga offers a streamlined and efficient solution to monitor and debug systems. Its agentless architecture allows you to deploy, manage, and detach eBPF programs across your infrastructure via simple SSH commands. 19 | 20 | # Features 21 | 22 | #### Agentless Deployment: 23 | Deploy eBPF programs remotely without installing persistent agents. 24 | 25 | #### Dynamic eBPF Management: 26 | Load, pin, and detach eBPF code programatically based on conditions that you define in a declarative way. 27 | 28 | #### Visualization: 29 | Visualize your collected metrics in Grafana. 30 | 31 | #### RAG: 32 | Use the integrated RAG to answer questions about your collected metrics. 33 | 34 | 35 | **Once the VM is created:** 36 | 37 | Install Go 38 | 39 | `sudo snap install go --classic` 40 | 41 | Create a symlink to the asm-generic directory, to fix this error: 42 | 43 | ``` 44 | In file included from /usr/include/linux/bpf.h:11: 45 | /usr/include/linux/types.h:5:10: fatal error: 'asm/types.h' file not found 46 | 5 | #include 47 | | ^~~~~~~~~~~~~ 48 | ``` 49 | 50 | `ln -sf /usr/include/asm-generic/ /usr/include/asm` 51 | 52 | Also install the following packages: 53 | 54 | `go get github.com/cilium/ebpf` 55 | `go get github.com/cilium/ebpf/link` 56 | -------------------------------------------------------------------------------- /bpf/kafka_lag.bpf.c: -------------------------------------------------------------------------------- 1 | // kafka_lag.bpf.c 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #define TASK_COMM_LEN 16 // Max length of process name 8 | 9 | // Global variable for filtering by Kafka PID 10 | const volatile uint32_t kafka_pid = 0; 11 | 12 | // Mapa: pid -> último timestamp 13 | struct { 14 | __uint(type, BPF_MAP_TYPE_HASH); 15 | __uint(max_entries, 1024); 16 | __type(key, u32); 17 | __type(value, u64); 18 | } recv_ts SEC(".maps"); 19 | 20 | // Perf buffer for user-space events 21 | struct event { 22 | u32 pid; 23 | u64 lag_ns; 24 | char comm[TASK_COMM_LEN]; 25 | }; 26 | 27 | struct { 28 | __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY); 29 | __uint(key_size, sizeof(int)); 30 | __uint(value_size, sizeof(int)); 31 | __uint(max_entries, 1024); 32 | } events SEC(".maps"); 33 | 34 | // SEC("tracepoint/syscalls/sys_enter_recvfrom") 35 | SEC("tracepoint/syscalls/sys_enter_execve") 36 | int handle_recvfrom_enter(struct trace_event_raw_sys_enter *ctx) 37 | { 38 | 39 | u32 pid = bpf_get_current_pid_tgid() >> 32; 40 | char comm[TASK_COMM_LEN]; 41 | bpf_get_current_comm(&comm, sizeof(comm)); 42 | 43 | // Only track Java processes (Kafka clients) 44 | if (bpf_strncmp(comm, "java", 4) == 0) { 45 | bpf_printk("Detected Kafka client process: PID=%d, Name=%s\n", pid, comm); 46 | } 47 | 48 | // Filter: Only process Kafka-related syscalls 49 | // if (kafka_pid && pid != kafka_pid) { 50 | // return 0; 51 | // } 52 | 53 | u64 ts = bpf_ktime_get_ns(); 54 | 55 | bpf_map_update_elem(&recv_ts, &pid, &ts, BPF_ANY); 56 | return 0; 57 | } 58 | 59 | SEC("tracepoint/syscalls/sys_exit_recvfrom") 60 | int handle_recvfrom_exit(struct trace_event_raw_sys_exit *ctx) 61 | { 62 | u32 pid = bpf_get_current_pid_tgid() >> 32; 63 | u64 *prev_ts, now = bpf_ktime_get_ns(); 64 | 65 | // if (kafka_pid && pid != kafka_pid) { 66 | // return 0; 67 | // } 68 | 69 | // Busca el último timestamp 70 | prev_ts = bpf_map_lookup_elem(&recv_ts, &pid); 71 | if (prev_ts) { 72 | // Calcula delta (nanosegundos) 73 | u64 delta = now - *prev_ts; 74 | 75 | struct event evt = {}; 76 | evt.pid = pid; 77 | evt.lag_ns = delta; 78 | bpf_get_current_comm(&evt.comm, sizeof(evt.comm)); 79 | bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &evt, sizeof(evt)); 80 | 81 | // Imprime en el kernel trace 82 | // bpf_printk("Consumer PID=%d, Lag=%llu ns\n", pid, delta); 83 | 84 | // Actualiza el nuevo timestamp 85 | bpf_map_update_elem(&recv_ts, &pid, &now, BPF_ANY); 86 | } 87 | return 0; 88 | } 89 | 90 | char LICENSE[] SEC("license") = "GPL"; 91 | -------------------------------------------------------------------------------- /bpf/kafka_lag.c: -------------------------------------------------------------------------------- 1 | // kafka_lag.c 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "kafka_lag.skel.h" 10 | 11 | 12 | #define TASK_COMM_LEN 16 // Max length of process name 13 | 14 | // Define event struct to match the one in BPF program 15 | struct event { 16 | __u32 pid; 17 | __u64 lag_ns; 18 | char comm[TASK_COMM_LEN]; 19 | }; 20 | 21 | // Handle perf buffer output 22 | //static int handle_event(void *ctx, void *data, size_t data_sz) { 23 | static void handle_event(void *ctx, int cpu, void *data, __u32 data_sz) { 24 | struct event *e = data; 25 | // printf("Kafka Consumer %s[%d], Lag=%llu ns\n", e->comm, e->pid, e->lag_ns); 26 | printf("Processs %s with PID %d had a lag of %llu ns\n", e->comm, e->pid, e->lag_ns); 27 | } 28 | 29 | int find_kafka_pid() { 30 | // FILE *fp = popen("pgrep -f java", "r"); 31 | FILE *fp = popen("(ps aux | grep 'kafka.Kafka' | grep -v grep | awk '{print $2}')", "r"); 32 | if (!fp) return -1; 33 | int pid; 34 | if (fscanf(fp, "%d", &pid) != 1) { 35 | pclose(fp); 36 | return -1; 37 | } 38 | pclose(fp); 39 | return pid; 40 | } 41 | 42 | int main(int argc, char **argv) 43 | { 44 | struct kafka_lag_bpf *skel; 45 | int err, kafka_pid; 46 | 47 | kafka_pid = find_kafka_pid(); 48 | // if (kafka_pid < 0) { 49 | // fprintf(stderr, "Kafka process not found!\n"); 50 | // return 1; 51 | // } 52 | 53 | printf("Monitoring Kafka process PID=%d\n", kafka_pid); 54 | libbpf_set_strict_mode(LIBBPF_STRICT_ALL); 55 | 56 | skel = kafka_lag_bpf__open(); 57 | if (!skel) { 58 | fprintf(stderr, "Failed to open BPF skeleton\n"); 59 | return 1; 60 | } 61 | 62 | skel->rodata->kafka_pid = kafka_pid; // Set Kafka PID in eBPF 63 | 64 | err = kafka_lag_bpf__load(skel); 65 | if (err) { 66 | fprintf(stderr, "Failed to load BPF program\n"); 67 | goto cleanup; 68 | } 69 | 70 | err = kafka_lag_bpf__attach(skel); 71 | if (err) { 72 | fprintf(stderr, "Failed to attach BPF program\n"); 73 | goto cleanup; 74 | } 75 | 76 | printf("Kafka Lag eBPF Running. Press Ctrl+C to stop.\n"); 77 | 78 | // Setup perf buffer reading 79 | struct perf_buffer *pb = perf_buffer__new(bpf_map__fd(skel->maps.events), 8, handle_event, NULL, NULL, NULL); 80 | if (!pb) { 81 | fprintf(stderr, "Failed to create perf buffer\n"); 82 | goto cleanup; 83 | } 84 | 85 | while (1) { 86 | perf_buffer__poll(pb, 100); 87 | } 88 | 89 | cleanup: 90 | kafka_lag_bpf__destroy(skel); 91 | return err; 92 | 93 | // // Configura libbpf para modo estricto (buenas prácticas) 94 | // libbpf_set_strict_mode(LIBBPF_STRICT_ALL); 95 | 96 | // // 1. Cargar skeleton 97 | // skel = kafka_lag_bpf__open(); 98 | // if (!skel) { 99 | // fprintf(stderr, "Error abriendo skeleton\n"); 100 | // return 1; 101 | // } 102 | 103 | // // 2. Cargar programa eBPF en kernel 104 | // err = kafka_lag_bpf__load(skel); 105 | // if (err) { 106 | // fprintf(stderr, "Error al cargar eBPF: %d\n", err); 107 | // goto cleanup; 108 | // } 109 | 110 | // // 3. Adjuntar 111 | // err = kafka_lag_bpf__attach(skel); 112 | // if (err) { 113 | // fprintf(stderr, "Error al adjuntar eBPF: %d\n", err); 114 | // goto cleanup; 115 | // } 116 | 117 | // printf("Kafka Lag eBPF corriendo. Logs en /sys/kernel/debug/tracing/trace_pipe\n"); 118 | // printf("Presiona Ctrl+C para salir...\n"); 119 | 120 | // // Loop infinito 121 | // while (1) { 122 | // sleep(1); 123 | // } 124 | 125 | // cleanup: 126 | // kafka_lag_bpf__destroy(skel); 127 | // return 0; 128 | } 129 | -------------------------------------------------------------------------------- /bpf/kafka_pid.bpf.c: -------------------------------------------------------------------------------- 1 | // kafka_process.bpf.c 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | char LICENSE[] SEC("license") = "GPL"; 8 | 9 | SEC("tracepoint/syscalls/sys_enter_execve") 10 | int handle_execve(struct trace_event_raw_sys_enter *ctx) { 11 | u32 pid = bpf_get_current_pid_tgid() >> 32; 12 | char comm[TASK_COMM_LEN]; 13 | 14 | bpf_get_current_comm(&comm, sizeof(comm)); 15 | 16 | bpf_printk("Detected Kafka-related process: PID=%d, Name=%s\n", pid, comm); 17 | 18 | // Print only Java processes (Kafka clients run under Java) 19 | if (__builtin_memcmp(comm, "java", 4) == 0) { 20 | bpf_printk("Detected Kafka-related process: PID=%d, Name=%s\n", pid, comm); 21 | } 22 | 23 | return 0; 24 | } 25 | -------------------------------------------------------------------------------- /bpf/kafka_pid.c: -------------------------------------------------------------------------------- 1 | // kafka_process.c 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "kafka_pid.skel.h" 7 | 8 | int main() { 9 | struct kafka_pid_bpf *skel; 10 | int err; 11 | 12 | libbpf_set_strict_mode(LIBBPF_STRICT_ALL); 13 | 14 | // Open, load, and attach BPF program 15 | skel = kafka_pid_bpf__open(); 16 | if (!skel) { 17 | fprintf(stderr, "Failed to open BPF skeleton\n"); 18 | return 1; 19 | } 20 | 21 | err = kafka_pid_bpf__load(skel); 22 | if (err) { 23 | fprintf(stderr, "Failed to load BPF program\n"); 24 | goto cleanup; 25 | } 26 | 27 | err = kafka_pid_bpf__attach(skel); 28 | if (err) { 29 | fprintf(stderr, "Failed to attach BPF program\n"); 30 | goto cleanup; 31 | } 32 | 33 | printf("Kafka Process Monitor Running. Press Ctrl+C to stop.\n"); 34 | 35 | // Read and print eBPF logs from trace_pipe 36 | system("sudo cat /sys/kernel/debug/tracing/trace_pipe"); 37 | 38 | cleanup: 39 | kafka_pid_bpf__destroy(skel); 40 | return err; 41 | } 42 | -------------------------------------------------------------------------------- /bpf/minimal.c: -------------------------------------------------------------------------------- 1 | // #include 2 | // #include 3 | 4 | // struct { 5 | // __uint(type, BPF_MAP_TYPE_ARRAY); 6 | // __type(key, __u32); 7 | // __type(value, __u64); 8 | // __uint(max_entries, 1); 9 | // } pkt_count SEC(".maps"); 10 | 11 | // // count_packets atomically increases a packet counter on every invocation. 12 | // SEC("xdp") 13 | // int count_packets() { 14 | // __u32 key = 0; 15 | // __u64 *count = bpf_map_lookup_elem(&pkt_count, &key); 16 | // if (count) { 17 | // __sync_fetch_and_add(count, 1); 18 | // } 19 | 20 | // return XDP_PASS; 21 | // } 22 | 23 | // char __license[] SEC("license") = "Dual MIT/GPL"; 24 | #include 25 | #include 26 | 27 | // Attach to kprobe/sys_clone 28 | SEC("kprobe/sys_clone") 29 | int handle_clone(struct pt_regs *ctx) { 30 | bpf_printk("Hello from handle_clone!\n"); 31 | return 0; 32 | } 33 | 34 | // eBPF programs must have a license 35 | char __license[] SEC("license") = "GPL"; 36 | -------------------------------------------------------------------------------- /bpf/python_lag.bpf.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | char LICENSE[] SEC("license") = "GPL"; 6 | 7 | // Structure for sending events to user-space 8 | struct python_event { 9 | u32 pid; 10 | u64 call_count; 11 | char comm[16]; // Process name 12 | }; 13 | 14 | // BPF Map to track function calls per process 15 | struct { 16 | __uint(type, BPF_MAP_TYPE_HASH); 17 | __uint(max_entries, 1024); 18 | __type(key, u32); 19 | __type(value, u64); 20 | } call_count SEC(".maps"); 21 | 22 | // Ring buffer to send events 23 | struct { 24 | __uint(type, BPF_MAP_TYPE_RINGBUF); 25 | __uint(max_entries, 256 * 1024); 26 | } rb SEC(".maps"); 27 | 28 | // Uprobe: Runs every time a Python function is executed 29 | SEC("uprobe/_PyEval_EvalFrameDefault") 30 | int profile_python_function(struct pt_regs *ctx) { 31 | // u32 pid = bpf_get_current_pid_tgid() >> 32; 32 | // bpf_printk("eBPF function hit! PID=%d", pid); 33 | // u64 *count, new_count = 1; 34 | 35 | // // Lookup current count 36 | // count = bpf_map_lookup_elem(&call_count, &pid); 37 | // if (count) { 38 | // new_count = *count + 1; 39 | // } 40 | // bpf_map_update_elem(&call_count, &pid, &new_count, BPF_ANY); 41 | 42 | // // Send event to user-space 43 | // struct python_event *event = bpf_ringbuf_reserve(&rb, sizeof(struct python_event), 0); 44 | // if (!event) { 45 | // bpf_printk("Ring buffer reservation failed"); 46 | // return 0; 47 | // } 48 | 49 | // event->pid = pid; 50 | // event->call_count = new_count; 51 | // bpf_get_current_comm(&event->comm, sizeof(event->comm)); 52 | // bpf_ringbuf_submit(event, 0); 53 | 54 | // bpf_printk("Event submitted: PID=%d, Calls=%llu", pid, new_count); 55 | // return 0; 56 | bpf_printk("Uprobe triggered"); 57 | u32 pid = bpf_get_current_pid_tgid() >> 32; 58 | 59 | // First debug message 60 | bpf_printk("Uprobe triggered: PID=%d", pid); 61 | 62 | // Try reserving space in the ring buffer 63 | struct python_event *event = bpf_ringbuf_reserve(&rb, sizeof(struct python_event), 0); 64 | if (!event) { 65 | bpf_printk("Ring buffer reservation failed!"); 66 | return 0; 67 | } 68 | 69 | // Filling event 70 | event->pid = pid; 71 | bpf_printk("Event created: PID=%d", pid); 72 | 73 | // Submitting event 74 | bpf_ringbuf_submit(event, 0); 75 | bpf_printk("Event submitted: PID=%d", pid); 76 | 77 | return 0; 78 | } 79 | -------------------------------------------------------------------------------- /bpf/python_lag.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include // Fixes PRIu64 warning 7 | #include "python_lag.skel.h" 8 | 9 | volatile bool exiting = false; 10 | 11 | // Handle Ctrl+C to exit 12 | void sig_handler(int signo) { 13 | exiting = true; 14 | } 15 | 16 | // Structure for receiving events 17 | struct python_event { 18 | __u32 pid; 19 | __u64 call_count; 20 | char comm[16]; // Process name 21 | }; 22 | 23 | // Callback function to process ring buffer events 24 | static int handle_event(void *ctx, void *data, size_t data_sz) { 25 | struct python_event *event = data; 26 | printf("[eBPF] PID: %d, Function Calls: %llu, Process: %s\n", 27 | event->pid, event->call_count, event->comm); 28 | return 0; 29 | } 30 | 31 | // Function to get the correct Python binary 32 | void get_python_binary(char *path, size_t size) { 33 | FILE *cmd = popen("which python3", "r"); 34 | if (cmd) { 35 | fgets(path, size, cmd); 36 | path[strcspn(path, "\n")] = 0; // Remove newline 37 | pclose(cmd); 38 | } 39 | if (strlen(path) == 0) { 40 | strcpy(path, "/usr/bin/python3"); // Fallback 41 | } 42 | } 43 | 44 | int main() { 45 | struct python_lag_bpf *skel; 46 | struct ring_buffer *rb = NULL; 47 | int err; 48 | 49 | // Handle Ctrl+C 50 | signal(SIGINT, sig_handler); 51 | 52 | libbpf_set_strict_mode(LIBBPF_STRICT_ALL); 53 | 54 | // Load the eBPF program 55 | skel = python_lag_bpf__open_and_load(); 56 | if (!skel) { 57 | fprintf(stderr, "Failed to open and load BPF skeleton\n"); 58 | return 1; 59 | } 60 | 61 | // Get Python binary dynamically 62 | char python_path[256] = {0}; 63 | get_python_binary(python_path, sizeof(python_path)); 64 | printf("[+] Using Python binary: %s\n", python_path); 65 | 66 | // Attach uprobe 67 | struct bpf_link *link; 68 | unsigned long offset = 0x4c4510; // Offset for _PyEval_EvalFrameDefault (Python 3.11+) 69 | 70 | link = bpf_program__attach_uprobe( 71 | skel->progs.profile_python_function, 72 | false, // Not a return probe 73 | -1, // Attach to all PIDs 74 | python_path, 75 | offset 76 | ); 77 | 78 | if (!link) { 79 | fprintf(stderr, "[ERROR] Failed to attach uprobe to %s at offset: 0x%lx\n", python_path, offset); 80 | goto cleanup; 81 | } else { 82 | printf("[+] Successfully attached uprobe to %s at offset: 0x%lx\n", python_path, offset); 83 | } 84 | 85 | // Set up ring buffer polling 86 | rb = ring_buffer__new(bpf_map__fd(skel->maps.rb), handle_event, NULL, NULL); 87 | if (!rb) { 88 | fprintf(stderr, "Failed to create ring buffer\n"); 89 | goto cleanup; 90 | } 91 | 92 | // Poll events and print them to console 93 | while (!exiting) { 94 | err = ring_buffer__poll(rb, 100); 95 | if (err == 0) { 96 | // printf("No events received yet...\n"); 97 | // fflush(stdout); 98 | } 99 | } 100 | 101 | cleanup: 102 | ring_buffer__free(rb); 103 | python_lag_bpf__destroy(skel); 104 | return 0; 105 | } 106 | -------------------------------------------------------------------------------- /bpf/python_tracer.bpf.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | /* Define Python frame structure for Python 3.12 */ 6 | struct py_frame { 7 | void *ob_type; 8 | void *f_back; 9 | void *f_code; 10 | void *f_builtins; 11 | void *f_globals; 12 | void *f_locals; 13 | void *f_valuestack; 14 | void *f_stacktop; 15 | void *f_trace; 16 | void *f_exc_type; 17 | void *f_exc_value; 18 | void *f_exc_traceback; 19 | int f_lasti; 20 | int f_lineno; 21 | int f_iblock; 22 | /* More fields follow but we don't need them */ 23 | }; 24 | 25 | /* Define Python code object structure (simplified) */ 26 | struct py_code_object { 27 | void *ob_type; 28 | int co_argcount; 29 | int co_posonlyargcount; 30 | int co_kwonlyargcount; 31 | int co_nlocals; 32 | int co_stacksize; 33 | int co_flags; 34 | void *co_code; 35 | void *co_consts; 36 | void *co_names; 37 | void *co_varnames; 38 | void *co_freevars; 39 | void *co_cellvars; 40 | void *co_filename; 41 | void *co_name; /* Function name */ 42 | int co_firstlineno; 43 | void *co_lnotab; 44 | }; 45 | 46 | /* Define the data structure to pass between kernel and user space */ 47 | struct event { 48 | unsigned int pid; 49 | char function_name[64]; 50 | int args_count; 51 | __u64 timestamp; 52 | }; 53 | 54 | /* Create a BPF ringbuffer map to pass events to userspace */ 55 | struct { 56 | __uint(type, BPF_MAP_TYPE_RINGBUF); 57 | __uint(max_entries, 256 * 1024); 58 | } events SEC(".maps"); 59 | 60 | /* Attach to the Python function execution tracepoint (uprobes) */ 61 | SEC("uprobe/_PyEval_EvalFrameDefault") 62 | int BPF_UPROBE(trace_python_function, struct py_frame *frame) 63 | { 64 | /* Get current process info */ 65 | __u64 id = bpf_get_current_pid_tgid(); 66 | __u32 pid = id >> 32; 67 | 68 | /* Get frame code object */ 69 | struct py_code_object *code_obj = NULL; 70 | bpf_probe_read(&code_obj, sizeof(code_obj), &frame->f_code); 71 | if (!code_obj) 72 | return 0; 73 | 74 | /* Reserve space in the ringbuffer */ 75 | struct event *e = bpf_ringbuf_reserve(&events, sizeof(struct event), 0); 76 | if (!e) 77 | return 0; 78 | 79 | /* Fill the event data */ 80 | e->pid = pid; 81 | e->timestamp = bpf_ktime_get_ns(); 82 | 83 | /* Try to get the function name from code object */ 84 | void *name_obj = NULL; 85 | bpf_probe_read(&name_obj, sizeof(name_obj), &code_obj->co_name); 86 | 87 | /* Set a default name if we can't read it */ 88 | char default_name[64] = ""; 89 | if (!name_obj) { 90 | bpf_probe_read_str(e->function_name, sizeof(e->function_name), default_name); 91 | } else { 92 | /* Read the actual function name string */ 93 | /* Python strings have their character data at an offset from the string object */ 94 | /* This offset may vary depending on Python version - typically 16-24 bytes */ 95 | bpf_probe_read_str(e->function_name, sizeof(e->function_name), name_obj + 24); 96 | } 97 | 98 | /* Get argument count */ 99 | bpf_probe_read(&e->args_count, sizeof(e->args_count), &code_obj->co_argcount); 100 | 101 | /* Submit the event to userspace */ 102 | bpf_ringbuf_submit(e, 0); 103 | 104 | return 0; 105 | } 106 | 107 | char LICENSE[] SEC("license") = "GPL"; -------------------------------------------------------------------------------- /bpf/python_tracer.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "python_tracer.skel.h" 12 | 13 | /* Data structure matching the BPF program's event */ 14 | struct event { 15 | unsigned int pid; 16 | char function_name[64]; 17 | int args_count; 18 | __u64 timestamp; 19 | }; 20 | 21 | static volatile bool exiting = false; 22 | 23 | static void sig_handler(int sig) 24 | { 25 | exiting = true; 26 | } 27 | 28 | /* Callback function for ringbuffer events */ 29 | static int handle_event(void *ctx, void *data, size_t data_sz) 30 | { 31 | const struct event *e = data; 32 | struct tm *tm; 33 | char ts[32]; 34 | time_t t; 35 | 36 | /* Prepare timestamp */ 37 | time(&t); 38 | tm = localtime(&t); 39 | strftime(ts, sizeof(ts), "%H:%M:%S", tm); 40 | 41 | /* Print event details */ 42 | printf("%-8s %-5d %-16s args: %d\n", 43 | ts, e->pid, e->function_name, e->args_count); 44 | 45 | return 0; 46 | } 47 | 48 | int main(int argc, char **argv) 49 | { 50 | struct python_tracer_bpf *skel; 51 | struct ring_buffer *rb = NULL; 52 | int err; 53 | 54 | /* Set up Ctrl-C handler */ 55 | signal(SIGINT, sig_handler); 56 | signal(SIGTERM, sig_handler); 57 | 58 | /* Open and load BPF program */ 59 | skel = python_tracer_bpf__open_and_load(); 60 | if (!skel) { 61 | fprintf(stderr, "Failed to open and load BPF skeleton\n"); 62 | return 1; 63 | } 64 | 65 | /* Attach to Python interpreter */ 66 | printf("Attaching to Python 3.12 interpreter at %s...\n", python_path); 67 | 68 | /* Find the Python shared library if python_path is a script or shebang */ 69 | /* For Python 3.12, we need to locate libpython3.12.so */ 70 | char *python_lib = "/usr/lib/libpython3.12.so.1.0"; 71 | 72 | /* Check if library exists, otherwise try alternate locations */ 73 | if (access(python_lib, F_OK) != 0) { 74 | /* Try to find the actual path with dynamic lookup */ 75 | FILE *cmd = popen("ldd $(which python3) | grep libpython | awk '{print $3}'", "r"); 76 | if (cmd) { 77 | char buf[256]; 78 | if (fgets(buf, sizeof(buf), cmd) != NULL) { 79 | /* Remove newline if present */ 80 | size_t len = strlen(buf); 81 | if (len > 0 && buf[len-1] == '\n') 82 | buf[len-1] = '\0'; 83 | 84 | if (strlen(buf) > 0) { 85 | python_lib = strdup(buf); 86 | printf("Found Python library at: %s\n", python_lib); 87 | } 88 | } 89 | pclose(cmd); 90 | } 91 | } 92 | 93 | /* Attach the uprobe at _PyEval_EvalFrameDefault function for Python 3.12 */ 94 | int fd = skel->progs.trace_python_function.prog_fd; 95 | if (fd < 0) { 96 | fprintf(stderr, "Failed to get program FD\n"); 97 | goto cleanup; 98 | } 99 | 100 | /* Attach uprobe to Python library */ 101 | /* Note: For Python 3.12, we need to attach to _PyEval_EvalFrameDefault */ 102 | int uprobe_fd = bpf_program__attach_uprobe(skel->progs.trace_python_function, 103 | false, /* not a return probe */ 104 | -1, /* any process */ 105 | python_lib, 106 | 0); /* Use 0 for symbols, or find offset with objdump */ 107 | 108 | if (uprobe_fd < 0) { 109 | fprintf(stderr, "Failed to attach uprobe: %d. This could be due to:\n", uprobe_fd); 110 | fprintf(stderr, "1. Symbol not found - try finding it with: objdump -T %s | grep _PyEval_EvalFrameDefault\n", python_lib); 111 | fprintf(stderr, "2. Missing permissions - make sure you're running as root\n"); 112 | fprintf(stderr, "3. Trying different Python library paths\n"); 113 | goto cleanup; 114 | } 115 | 116 | /* Set up ring buffer polling */ 117 | rb = ring_buffer__new(bpf_map__fd(skel->maps.events), handle_event, NULL, NULL); 118 | if (!rb) { 119 | fprintf(stderr, "Failed to create ring buffer\n"); 120 | goto cleanup; 121 | } 122 | 123 | printf("Successfully started! Tracing Python functions...\n"); 124 | printf("%-8s %-5s %-16s %s\n", "TIME", "PID", "FUNCTION", "ARGS"); 125 | 126 | /* Main polling loop */ 127 | while (!exiting) { 128 | err = ring_buffer__poll(rb, 100 /* timeout, ms */); 129 | if (err == -EINTR) { 130 | err = 0; 131 | break; 132 | } 133 | if (err < 0) { 134 | fprintf(stderr, "Error polling ring buffer: %d\n", err); 135 | break; 136 | } 137 | /* Optional: sleep a bit if needed */ 138 | /* usleep(100000); */ 139 | } 140 | 141 | cleanup: 142 | /* Clean up */ 143 | ring_buffer__free(rb); 144 | python_tracer_bpf__destroy(skel); 145 | return err < 0 ? -err : 0; 146 | } -------------------------------------------------------------------------------- /bpf/python_tracer_0.bpf.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | /* Define the data structure to pass between kernel and user space */ 6 | struct event { 7 | unsigned int pid; 8 | char function_name[64]; 9 | int args_count; 10 | __u64 timestamp; 11 | }; 12 | 13 | /* Create a BPF ringbuffer map to pass events to userspace */ 14 | struct { 15 | __uint(type, BPF_MAP_TYPE_RINGBUF); 16 | __uint(max_entries, 256 * 1024); 17 | } events SEC(".maps"); 18 | 19 | /* Attach to the Python function execution tracepoint (uprobes) */ 20 | SEC("uprobe/PyEval_EvalFrameEx") 21 | int BPF_KPROBE(trace_python_function, struct _frame *frame) 22 | { 23 | /* Get current process info */ 24 | __u64 id = bpf_get_current_pid_tgid(); 25 | __u32 pid = id >> 32; 26 | 27 | /* Get frame code object */ 28 | void *code_obj = NULL; 29 | bpf_probe_read(&code_obj, sizeof(code_obj), &frame->f_code); 30 | if (!code_obj) 31 | return 0; 32 | 33 | /* Reserve space in the ringbuffer */ 34 | struct event *e = bpf_ringbuf_reserve(&events, sizeof(struct event), 0); 35 | if (!e) 36 | return 0; 37 | 38 | /* Fill the event data */ 39 | e->pid = pid; 40 | e->timestamp = bpf_ktime_get_ns(); 41 | 42 | /* Try to get the function name from code object */ 43 | void *name_obj = NULL; 44 | bpf_probe_read(&name_obj, sizeof(name_obj), code_obj + 8); // Offset for co_name 45 | 46 | /* Set a default name if we can't read it */ 47 | char default_name[64] = ""; 48 | if (!name_obj) { 49 | bpf_probe_read_str(e->function_name, sizeof(e->function_name), default_name); 50 | } else { 51 | /* Read the actual function name string */ 52 | bpf_probe_read_str(e->function_name, sizeof(e->function_name), name_obj + 16); // String data 53 | } 54 | 55 | /* Get argument count */ 56 | bpf_probe_read(&e->args_count, sizeof(e->args_count), &frame->f_code->co_argcount); 57 | 58 | /* Submit the event to userspace */ 59 | bpf_ringbuf_submit(e, 0); 60 | 61 | return 0; 62 | } 63 | 64 | char LICENSE[] SEC("license") = "GPL"; -------------------------------------------------------------------------------- /bpf/python_tracer_0.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "python_tracer.skel.h" 12 | 13 | /* Data structure matching the BPF program's event */ 14 | struct event { 15 | unsigned int pid; 16 | char function_name[64]; 17 | int args_count; 18 | __u64 timestamp; 19 | }; 20 | 21 | static volatile bool exiting = false; 22 | 23 | static void sig_handler(int sig) 24 | { 25 | exiting = true; 26 | } 27 | 28 | /* Callback function for ringbuffer events */ 29 | static int handle_event(void *ctx, void *data, size_t data_sz) 30 | { 31 | const struct event *e = data; 32 | struct tm *tm; 33 | char ts[32]; 34 | time_t t; 35 | 36 | /* Prepare timestamp */ 37 | time(&t); 38 | tm = localtime(&t); 39 | strftime(ts, sizeof(ts), "%H:%M:%S", tm); 40 | 41 | /* Print event details */ 42 | printf("%-8s %-5d %-16s args: %d\n", 43 | ts, e->pid, e->function_name, e->args_count); 44 | 45 | return 0; 46 | } 47 | 48 | int main(int argc, char **argv) 49 | { 50 | struct python_tracer_bpf *skel; 51 | struct ring_buffer *rb = NULL; 52 | int err; 53 | 54 | /* Set up Ctrl-C handler */ 55 | signal(SIGINT, sig_handler); 56 | signal(SIGTERM, sig_handler); 57 | 58 | /* Open and load BPF program */ 59 | skel = python_tracer_bpf__open_and_load(); 60 | if (!skel) { 61 | fprintf(stderr, "Failed to open and load BPF skeleton\n"); 62 | return 1; 63 | } 64 | 65 | /* Get Python interpreter path 66 | * Note: This path must be adjusted based on the actual location 67 | * of the Python interpreter on the system where this runs. 68 | * Use 'which python3' command to find the path. 69 | */ 70 | char *python_path = "/usr/bin/python3"; 71 | 72 | /* Attach BPF program to Python interpreter */ 73 | printf("Attaching to Python interpreter at %s...\n", python_path); 74 | 75 | /* Attach the uprobe at the PyEval_EvalFrameEx function */ 76 | int fd = skel->progs.trace_python_function.prog_fd; 77 | if (fd < 0) { 78 | fprintf(stderr, "Failed to get program FD\n"); 79 | goto cleanup; 80 | } 81 | 82 | /* Attach uprobe */ 83 | /* Note: The actual offset of PyEval_EvalFrameEx will depend on the specific Python version. 84 | * You may need to find this using 'readelf -s /usr/bin/python3 | grep PyEval_EvalFrameEx' 85 | * or 'nm -D /usr/bin/python3 | grep PyEval_EvalFrameEx' 86 | */ 87 | int uprobe_fd = bpf_program__attach_uprobe(skel->progs.trace_python_function, 88 | false, /* not a return probe */ 89 | -1, /* any process */ 90 | python_path, 91 | 0x4468c8); /* Replace with actual offset */ 92 | 93 | if (uprobe_fd < 0) { 94 | fprintf(stderr, "Failed to attach uprobe: %d\n", uprobe_fd); 95 | goto cleanup; 96 | } 97 | 98 | /* Set up ring buffer polling */ 99 | rb = ring_buffer__new(bpf_map__fd(skel->maps.events), handle_event, NULL, NULL); 100 | if (!rb) { 101 | fprintf(stderr, "Failed to create ring buffer\n"); 102 | goto cleanup; 103 | } 104 | 105 | printf("Successfully started! Tracing Python functions...\n"); 106 | printf("%-8s %-5s %-16s %s\n", "TIME", "PID", "FUNCTION", "ARGS"); 107 | 108 | /* Main polling loop */ 109 | while (!exiting) { 110 | err = ring_buffer__poll(rb, 100 /* timeout, ms */); 111 | if (err == -EINTR) { 112 | err = 0; 113 | break; 114 | } 115 | if (err < 0) { 116 | fprintf(stderr, "Error polling ring buffer: %d\n", err); 117 | break; 118 | } 119 | /* Optional: sleep a bit if needed */ 120 | /* usleep(100000); */ 121 | } 122 | 123 | cleanup: 124 | /* Clean up */ 125 | ring_buffer__free(rb); 126 | python_tracer_bpf__destroy(skel); 127 | return err < 0 ? -err : 0; 128 | } -------------------------------------------------------------------------------- /coordinator.go: -------------------------------------------------------------------------------- 1 | // TODO: Make it portable across different kernels using CO-RE (https://thegraynode.io/posts/portable_bpf_programs/ or bpf2go) 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "gopkg.in/yaml.v3" 7 | "log" 8 | "os" 9 | "os/exec" 10 | ) 11 | 12 | // TODO: Remember to investigate how to separate the YAML tags. 13 | type HostConfig struct { 14 | Name string `yaml:"name"` 15 | Address string `yaml:"address"` 16 | Port int `yaml:"port"` 17 | User string `yaml:"user"` 18 | PrivateKeyPath string `yaml:"privateKeyPath"` 19 | } 20 | 21 | type EBPFConfig struct { 22 | SourceType string `yaml:"sourceType"` 23 | FilePath string `yaml:"filePath"` 24 | FileOutputPath string `yaml:"fileOutputPath"` 25 | CompileFlags string `yaml:"compileFlags"` 26 | LoaderPath string `yaml:"loaderPath"` 27 | LoaderOutputPath string `yaml:"loaderOutputPath"` 28 | } 29 | 30 | // For later use. 31 | type MetricsConfig struct { 32 | Name string `yaml:"name"` 33 | Description string `yaml:"description"` 34 | } 35 | 36 | type DeployConfig struct { 37 | Hosts []HostConfig `yaml:"hosts"` 38 | EBPF EBPFConfig `yaml:"ebpf"` 39 | Metrics []MetricsConfig `yaml:"metrics"` 40 | } 41 | 42 | type CompileResult struct { 43 | name string 44 | outputPath string 45 | err error 46 | } 47 | 48 | // This is for the queue. 49 | type DeployJob struct { 50 | host HostConfig 51 | loaderBin string 52 | ebpfObj string 53 | } 54 | 55 | type Context map[string]interface {} 56 | 57 | type JobHandler interface { 58 | Start() error 59 | JobTransition(currentStepName string, next func(Context) error, ctx Context) error 60 | } 61 | 62 | type Task struct {} 63 | 64 | func (t *Task) JobTransition(stepName string, next func(Context) error, ctx Context) error { 65 | log.Printf("Saved %s", stepName) 66 | return next(ctx) 67 | } 68 | 69 | func (t *Task) JobComplete(ctx Context) error { 70 | log.Printf("Job completed") 71 | return nil 72 | } 73 | 74 | func (t *Task) Start() error { 75 | deployConfigPath := os.Args[1] 76 | deployConfig, err := loadConfig(deployConfigPath) 77 | 78 | if err != nil { 79 | log.Fatalf("Failed to load config: %v", err) 80 | } 81 | 82 | // We add the file config and the first compilation in as a single stage because by only reading the config file 83 | // we'd do not get any action. 84 | loaderResultChan := compileLoader(deployConfig.EBPF.LoaderPath, deployConfig.EBPF.LoaderOutputPath) 85 | loaderResult := <-loaderResultChan 86 | 87 | if loaderResult.err != nil { 88 | log.Fatalf("Loader compilation failed: %v", loaderResult.err) 89 | // break TODO: How to stop the operation and log in DB this stage? 90 | } 91 | 92 | ctx := Context{ 93 | "deployConfig": deployConfig, 94 | } 95 | 96 | return t.JobTransition("start", t.CompileEBPF, ctx) 97 | } 98 | 99 | func (t *Task) CompileEBPF(ctx Context) error { 100 | deployConfig := ctx["deployConfig"].(*DeployConfig) 101 | ebpfResultChan := compileEBPF(deployConfig.EBPF.FilePath, deployConfig.EBPF.FileOutputPath) 102 | ebpfResult := <-ebpfResultChan 103 | 104 | if ebpfResult.err != nil { 105 | log.Fatalf("eBPF compilation failed: %v", ebpfResult.err) 106 | // break TODO: How to stop the operation and log in DB this stage? 107 | } 108 | 109 | return t.JobTransition("compileEBPF", t.DeployToHost, nil) 110 | } 111 | 112 | func (t *Task) DeployToHost(ctx Context) error { 113 | return t.JobTransition("deployToHost", t.JobComplete, nil) 114 | } 115 | 116 | func loadConfig(filename string) (*DeployConfig, error) { 117 | data, err := os.ReadFile(filename) 118 | if err != nil { 119 | return nil, fmt.Errorf("Failed to read config file: %v", err) 120 | } 121 | 122 | var deployConfig DeployConfig 123 | err = yaml.Unmarshal(data, &deployConfig) 124 | if err != nil { 125 | return nil, fmt.Errorf("Failed to unmarshal config: %v", err) 126 | } 127 | 128 | return &deployConfig, nil 129 | } 130 | 131 | func compileLoader(loaderPath string, loaderOutputPath string) <-chan CompileResult { 132 | resultChannel := make(chan CompileResult) 133 | 134 | go func() { 135 | defer close(resultChannel) 136 | // We assume that always the architecture is AMD64 just for the PoC since this is the chip of my local machine. 137 | // for future implementation, I'll add support to compile with different flags. 138 | cmd := exec.Command( 139 | "go", 140 | "build", 141 | "-ldflags", 142 | "-extldflags \"-static\"", 143 | "-o", 144 | loaderOutputPath, 145 | loaderPath, 146 | ) 147 | 148 | cmd.Env = append(os.Environ(), 149 | "GOOS=linux", 150 | "GOARCH=amd64", 151 | "CGO_ENABLED=0", 152 | ) 153 | 154 | output, err := cmd.CombinedOutput() 155 | if err != nil { 156 | resultChannel <- CompileResult{"loader", "", fmt.Errorf("Failed to compile loader: %v: %s", err, output)} 157 | return 158 | } 159 | resultChannel <- CompileResult{"loader", loaderOutputPath, nil} 160 | }() 161 | 162 | return resultChannel 163 | } 164 | 165 | func compileEBPF(filePath string, ebpfObjOutputPath string) <-chan CompileResult { 166 | resultChannel := make(chan CompileResult) 167 | 168 | go func() { 169 | defer close(resultChannel) 170 | // TODO: Verify CO-RE or see the possibility to compile for a matrix of Kernels. 171 | cmd := exec.Command( 172 | "clang", 173 | "-O2", 174 | "-g", 175 | "-target", 176 | "bpf", 177 | "-c", 178 | filePath, 179 | "-o", 180 | ebpfObjOutputPath, 181 | ) 182 | 183 | output, err := cmd.CombinedOutput() 184 | if err != nil { 185 | resultChannel <- CompileResult{"ebpf", "", fmt.Errorf("Failed to compile eBPF: %v\nOutput: %s", err, output)} 186 | return 187 | } 188 | 189 | resultChannel <- CompileResult{"ebpf", filePath + ".o", nil} 190 | }() 191 | 192 | return resultChannel 193 | } 194 | 195 | func deployToHost(deployJob DeployJob) error { 196 | address := deployJob.host.Address 197 | user := deployJob.host.User 198 | keyPath := deployJob.host.PrivateKeyPath 199 | 200 | userAndAddress := fmt.Sprintf("%s@%s", user, address) 201 | 202 | loaderBin := deployJob.loaderBin 203 | ebpfObj := deployJob.ebpfObj 204 | 205 | remoteLoader := "/tmp/loader" 206 | remoteObj := "/tmp/minimal.o" 207 | 208 | // 1. Copy the loader binary to the remote machine 209 | scpCmd1 := exec.Command("scp", "-i", keyPath, loaderBin, fmt.Sprintf("%s:%s", userAndAddress, remoteLoader)) 210 | if out, err := scpCmd1.CombinedOutput(); err != nil { 211 | return fmt.Errorf("Failed to SCP loader: %v\nOutput: %s", err, out) 212 | } 213 | 214 | // 2. Copy the minimal.o eBPF object 215 | scpCmd2 := exec.Command("scp", "-i", keyPath, ebpfObj, fmt.Sprintf("%s:%s", userAndAddress, remoteObj)) 216 | if out, err := scpCmd2.CombinedOutput(); err != nil { 217 | return fmt.Errorf("Failed to SCP eBPF .o file: %v\nOutput: %s", err, out) 218 | } 219 | 220 | // 3. SSH to run the loader 221 | runCmd := fmt.Sprintf("chmod +x %s && (sudo %s -obj %s > /tmp/loader.log 2>&1 & disown) && echo 'Loader started in background' && exit", remoteLoader, remoteLoader, remoteObj) 222 | 223 | remoteCmd := fmt.Sprintf("'%s'", runCmd) 224 | sshCmd := exec.Command("ssh", "-i", keyPath, userAndAddress, "bash", "-c", remoteCmd) 225 | 226 | out, err := sshCmd.CombinedOutput() 227 | if err != nil { 228 | return fmt.Errorf("Loader execution failed: %v\nOutput: %s", err, out) 229 | } 230 | log.Printf("Loader ran successfully! Output:\n%s", out) 231 | 232 | return nil 233 | 234 | // 4. (Optional) Cleanup ephemeral files 235 | // cleanupCmd := fmt.Sprintf("rm %s %s", remoteLoader, remoteObj) 236 | // sshCleanup := exec.Command("ssh", "-i", keyPath, host, cleanupCmd) 237 | // if out, err := sshCleanup.CombinedOutput(); err != nil { 238 | // log.Printf("Cleanup warning: %v\nOutput: %s", err, out) 239 | // } 240 | // log.Println("Cleanup done, ephemeral agentless approach complete.") 241 | } 242 | 243 | func deployWorker(deployJobs <-chan DeployJob, results chan<- error) { 244 | for deployJob := range deployJobs { 245 | log.Printf("Starting deployment to host %s", deployJob.host.Address) 246 | err := deployToHost(deployJob) 247 | if err != nil { 248 | results <- fmt.Errorf("Failed to deploy to host %s: %v", deployJob.host.Address, err) 249 | continue 250 | } 251 | results <- nil 252 | log.Printf("Deployment to host %s completed successfully", deployJob.host.Address) 253 | } 254 | } 255 | 256 | func main() { 257 | var job JobHandler = &Task{} 258 | err := job.Start() 259 | if err != nil { 260 | log.Fatalf("Failed to start job: %v", err) 261 | } 262 | 263 | // loaderBin := loaderResult.outputPath 264 | // ebpfObj := ebpfResult.outputPath 265 | 266 | // numWorkers := 2 // TODO: Investigate more about this. 267 | // deployJobs := make(chan DeployJob, numWorkers) 268 | // deployResults := make(chan error, numWorkers) 269 | 270 | // for i := 0; i < numWorkers; i++ { 271 | // go deployWorker(deployJobs, deployResults) 272 | // } 273 | 274 | // for _, host := range deployConfig.Hosts { 275 | // deployJobs <- DeployJob{host, loaderBin, ebpfObj} 276 | // } 277 | // close(deployJobs) 278 | 279 | // for i := 0; i < numWorkers; i++ { 280 | // if err := <-deployResults; err != nil { 281 | // log.Printf("Failed to deploy to host %s: %v", deployConfig.Hosts[i].Address, err) 282 | // } 283 | // } 284 | } 285 | -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfrojas/bpfluga/92356128836191d88c5eaa710fb9f9591b9ae673/docs/logo.png -------------------------------------------------------------------------------- /examples/deploy.example.yaml: -------------------------------------------------------------------------------- 1 | hosts: 2 | - name: "host1" 3 | address: "host1.example.com" 4 | port: 22 5 | user: "root" 6 | privateKeyPath: "~/.ssh/id_rsa" 7 | - name: "host2" 8 | address: "host2.example.com" 9 | port: 22 10 | user: "root" 11 | privateKeyPath: "~/.ssh/id_rsa" 12 | 13 | ebpf: 14 | sourceType: "local" # Could be 'local' or 'inline' 15 | filePath: "./bpf/minimal.c" # If local 16 | fileOutputPath: "./bpf/minimal.o" 17 | loaderPath: "./loader/loader.go" 18 | loaderOutputPath: "./loader/loader" 19 | compileFlags: "-O2" # TODO: Add support for different flags 20 | 21 | metrics: 22 | - name: "tcp_connections" 23 | description: "Count active TCP connections" 24 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module bpfluga 2 | 3 | go 1.24.0 4 | 5 | require gopkg.in/yaml.v3 v3.0.1 6 | 7 | require ( 8 | github.com/cilium/ebpf v0.17.3 // indirect 9 | golang.org/x/sys v0.30.0 // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cilium/ebpf v0.17.3 h1:FnP4r16PWYSE4ux6zN+//jMcW4nMVRvuTLVTvCjyyjg= 2 | github.com/cilium/ebpf v0.17.3/go.mod h1:G5EDHij8yiLzaqn0WjyfJHvRa+3aDlReIaLVRMvOyJk= 3 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 4 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 5 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 6 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 7 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 8 | -------------------------------------------------------------------------------- /loader/loader.go: -------------------------------------------------------------------------------- 1 | /* 2 | Here is where the cilium/ebpf library loads the eBPF program. The coordinator.go could be written in any language, 3 | at the end, it is just an orchestrator script that sends the eBPF program to the remote machine and runs it. This file 4 | is the reason why I chose Go. 5 | */ 6 | package main 7 | 8 | import ( 9 | "flag" 10 | "fmt" 11 | "log" 12 | "time" 13 | 14 | "github.com/cilium/ebpf" 15 | "github.com/cilium/ebpf/link" 16 | _ "github.com/cilium/ebpf/rlimit" // Auto-raise rlimit if needed 17 | ) 18 | 19 | var ( 20 | objPath = flag.String("obj", "./bpf/minimal.o", "Path to eBPF .o file") 21 | ) 22 | 23 | func main() { 24 | flag.Parse() 25 | 26 | // 1. Load the compiled eBPF .o into a CollectionSpec 27 | spec, err := ebpf.LoadCollectionSpec(*objPath) 28 | if err != nil { 29 | log.Fatalf("Failed to load eBPF collection: %v", err) 30 | } 31 | 32 | // 2. Create a Collection from the spec 33 | coll, err := ebpf.NewCollection(spec) 34 | if err != nil { 35 | log.Fatalf("Failed to create eBPF collection: %v", err) 36 | } 37 | defer coll.Close() 38 | 39 | // 3. Retrieve the program by its SEC name 40 | prog := coll.Programs["handle_clone"] 41 | if prog == nil { 42 | log.Fatalf("Program 'kprobe_sys_clone' not found") 43 | } 44 | defer prog.Close() 45 | 46 | // 4. Attach the program via kprobe to sys_clone 47 | _, err = link.Kprobe("sys_clone", prog, nil) 48 | if err != nil { 49 | log.Fatalf("Failed to attach kprobe: %v", err) 50 | } 51 | //defer kprobe.Close() 52 | 53 | fmt.Println("Atajado kprobe on sys_clone. Check trace output with:") 54 | fmt.Println(" sudo cat /sys/kernel/debug/tracing/trace_pipe") 55 | fmt.Println("Press Ctrl-C or kill this process to detach kprobe (unless pinned).") 56 | 57 | for { 58 | time.Sleep(1 * time.Second) 59 | } 60 | 61 | // fmt.Println("eBPF program loaded and attached. Check `sudo cat /sys/kernel/debug/tracing/trace_pipe`") 62 | // Keep running or exit? For ephemeral usage, you can just exit 63 | // and the eBPF program remains attached only if pinned or until the program object is closed. 64 | 65 | // If you want it to persist after exit, you'd pin it or hold the program open. For demonstration: 66 | // (Uncomment if you want to pin) 67 | // err = prog.Pin("/sys/fs/bpf/minimal_prog") 68 | // if err != nil { 69 | // log.Printf("Warning: failed to pin eBPF program: %v", err) 70 | // } 71 | 72 | // Wait or exit. We'll exit right away. 73 | } 74 | -------------------------------------------------------------------------------- /test/test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Test script with simple functions to trace with our eBPF program. 4 | """ 5 | import time 6 | 7 | def hello_world(name="World"): 8 | """A simple function that prints a greeting.""" 9 | print(f"Hello, {name}!") 10 | return f"Hello, {name}!" 11 | 12 | def calculate_sum(a, b, c=0): 13 | """Calculate the sum of two or three numbers.""" 14 | result = a + b + c 15 | print(f"Sum of {a}, {b}, and {c} is {result}") 16 | return result 17 | 18 | def fibonacci(n): 19 | """Calculate the nth Fibonacci number.""" 20 | if n <= 0: 21 | return 0 22 | elif n == 1: 23 | return 1 24 | else: 25 | return fibonacci(n-1) + fibonacci(n-2) 26 | 27 | if __name__ == "__main__": 28 | print("Starting test script...") 29 | 30 | # Call our functions multiple times 31 | for i in range(3): 32 | hello_world(f"User {i}") 33 | time.sleep(0.5) 34 | 35 | for i in range(2): 36 | calculate_sum(i, i+1, i+2) 37 | time.sleep(0.5) 38 | 39 | print("Calculating Fibonacci numbers...") 40 | for i in range(5): 41 | result = fibonacci(i) 42 | print(f"Fibonacci({i}) = {result}") 43 | time.sleep(0.5) 44 | 45 | print("Test script completed!") -------------------------------------------------------------------------------- /vm_ebpf.yaml: -------------------------------------------------------------------------------- 1 | images: 2 | # - location: "https://cloud-images.ubuntu.com/releases/22.04/release/ubuntu-22.04-server-cloudimg-amd64.img" 3 | # arch: "x86_64" 4 | - location: "https://cloud-images.ubuntu.com/releases/24.04/release-20240423/ubuntu-24.04-server-cloudimg-arm64.img" 5 | arch: "aarch64" 6 | 7 | cpus: 4 8 | memory: "10GiB" 9 | 10 | mounts: 11 | - location: "~" 12 | writable: true 13 | - location: "/tmp/lima" 14 | writable: true 15 | provision: 16 | - mode: system 17 | script: | 18 | #!/bin/bash 19 | set -xe 20 | apt-get update 21 | apt-get install -y apt-transport-https ca-certificates curl clang llvm jq 22 | apt-get install -y libelf-dev libpcap-dev libbfd-dev binutils-dev build-essential make 23 | apt-get install -y linux-tools-common linux-tools-$(uname -r) 24 | apt-get install -y bpfcc-tools 25 | apt-get install -y python3-pip 26 | apt-get install -y linux-headers-$(uname -r) 27 | apt-get install -y libbpf-dev 28 | apt-get install -y openjdk-11-jdk 29 | apt-get install -y zsh 30 | --------------------------------------------------------------------------------