├── .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 |
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 |
--------------------------------------------------------------------------------