BastionGuard/thirdparty/bastionguard-secure-connection/daemon/ebpf/bsc_ebpf.c
specialworld83 f0f913a209 Release 2.0
2026-07-15 10:52:22 +02:00

447 lines
16 KiB
C

// SPDX-License-Identifier: GPL-2.0-only
/*
* BastionGuard™ Secure Connection — eBPF probe kernel-side (esteso)
* Copyright (C) 2025–2026 Calogero Scarnà
*
* COPERTURA EVENTI
* ================
* Il programma aggancia tre punti nel kernel per catturare praticamente
* tutte le connessioni uscenti di ogni protocollo supportato:
*
* 1) tracepoint/sock/inet_sock_set_state → transizioni TCP a ESTABLISHED.
* Copre sia connessioni attive (dopo connect()) sia passive (dopo
* accept()). Avevamo gia' questo: resta per retro-compatibilita' e
* perche' e' il punto piu' "pulito" per attribuire il processo
* di un socket appena handshake-ato.
*
* 2) kprobe/tcp_v4_connect + kprobe/tcp_v6_connect
* Cattura ogni tentativo di connect() TCP — anche quelli che falliscono
* per network unreachable, timeout SYN, ecc. Era il gap principale:
* prima vedevamo solo le connessioni che si stabilivano correttamente.
*
* 3) kprobe/udp_sendmsg + kprobe/udpv6_sendmsg
* Cattura ogni datagramma UDP in uscita con destinazione esplicita.
* Il tracepoint inet_sock_set_state NON copre UDP senza connect(),
* e la stragrande maggioranza del traffico UDP (DNS, QUIC, mDNS,
* NTP, DHCP...) usa sendto()/sendmsg() diretti.
*
* DEDUPLICAZIONE UDP
* ==================
* Senza dedup, un client QUIC spara migliaia di sendmsg() al secondo e
* la ring buffer si riempirebbe di eventi ripetuti. Usiamo una LRU hash
* map (BPF_MAP_TYPE_LRU_HASH, 8192 entry) con chiave <pid, dst_ip, dst_port>
* e valore timestamp ns: emettiamo un evento solo se non ne abbiamo emesso
* uno identico negli ultimi UDP_DEDUP_WINDOW_NS nanosecondi (default 5s).
* La LRU evita la fuga di memoria: entry vecchie vengono sfrattate in
* automatico.
*
* Lo stesso meccanismo (con finestra piu' corta) si applica a TCP connect
* per gestire il retry: Linux riprova SYN 3-5 volte, non ci interessa
* vedere 5 righe "Firefox connect a 1.2.3.4:443".
*
* REQUISITI
* =========
* kernel >= 5.8 (bpf_ringbuf_*, LRU_HASH, fentry a volonta')
* BTF abilitato (CONFIG_DEBUG_INFO_BTF=y)
* CAP_BPF + CAP_PERFMON (o CAP_SYS_ADMIN in fallback)
*/
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_endian.h>
/* ── Costanti kernel ──────────────────────────────────────────────── */
#define AF_INET 2
#define AF_INET6 10
#define TCP_ESTABLISHED 1
#define IPPROTO_TCP 6
#define IPPROTO_UDP 17
/* Dimensione ring buffer: 8 MB — piu' grande dell'originale perche'
* adesso copriamo anche UDP/connect e il volume totale cresce. */
#define RINGBUF_SIZE (8 * 1024 * 1024)
/* Finestra dedup in nanosecondi. */
#define UDP_DEDUP_WINDOW_NS (5ULL * 1000ULL * 1000ULL * 1000ULL) /* 5 s */
#define TCP_DEDUP_WINDOW_NS (2ULL * 1000ULL * 1000ULL * 1000ULL) /* 2 s */
/* ── Struttura evento condivisa con userspace ─────────────────────── */
/*
* ATTENZIONE: deve combaciare byte-per-byte con bsc::ebpf::ConnEvent
* in daemon/include/bsc/ebpf/EBPFMon.hpp. Campo `event_kind` aggiunto
* per distinguere la sorgente (tracepoint/kprobe); i vecchi loader che
* non lo conoscono leggeranno 0 nel padding, quindi retro-compat ok.
*/
struct bsc_conn_event {
/* processo */
__u32 pid;
__u32 tid;
__u32 uid;
__u32 gid;
char comm[16]; /* task_comm_len */
/* rete */
__u8 family; /* AF_INET o AF_INET6 */
__u8 proto; /* IPPROTO_TCP=6 o IPPROTO_UDP=17 */
__u8 event_kind; /* 0=tp_set_state, 1=tcp_connect, 2=udp_sendmsg */
__u8 _pad[1];
__u16 src_port; /* host byte order */
__u16 dst_port; /* host byte order */
/* indirizzi: primi 4 byte validi per IPv4, tutti 16 per IPv6 */
__u8 src_addr[16];
__u8 dst_addr[16];
/* timestamp monotono (ns) */
__u64 ts_ns;
};
/* Chiave di dedup: pid + dst_ip + dst_port sufficiente.
* Non includiamo src_port perche' per UDP e' effimero e farebbe saltare
* il dedup. */
struct dedup_key {
__u32 pid;
__u16 dst_port;
__u8 family;
__u8 proto;
__u8 dst_addr[16];
};
/* ── Maps ─────────────────────────────────────────────────────────── */
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, RINGBUF_SIZE);
} bsc_events SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__uint(max_entries, 8192);
__type(key, struct dedup_key);
__type(value, __u64); /* ts_ns ultimo emit */
} bsc_dedup SEC(".maps");
/* ── Helper: riempie i campi "processo" dell'evento ───────────────── */
static __always_inline void fill_proc(struct bsc_conn_event* ev)
{
const __u64 pid_tgid = bpf_get_current_pid_tgid();
ev->pid = (__u32)(pid_tgid >> 32);
ev->tid = (__u32)(pid_tgid & 0xffffffffull);
const __u64 uid_gid = bpf_get_current_uid_gid();
ev->uid = (__u32)(uid_gid & 0xffffffffull);
ev->gid = (__u32)(uid_gid >> 32);
bpf_get_current_comm(ev->comm, sizeof(ev->comm));
}
/* ── Helper: dedup check. Ritorna true se dobbiamo emettere. ──────── */
static __always_inline bool should_emit(const struct dedup_key* k,
__u64 now_ns,
__u64 window_ns)
{
__u64* last = bpf_map_lookup_elem(&bsc_dedup, k);
if (last) {
if (now_ns - *last < window_ns) return false; /* scarto: duplicato */
*last = now_ns; /* update in-place */
return true;
}
bpf_map_update_elem(&bsc_dedup, k, &now_ns, BPF_ANY);
return true;
}
/* =================================================================== *
* 1) tracepoint TCP state change (ESTABLISHED)
* =================================================================== */
SEC("tracepoint/sock/inet_sock_set_state")
int bsc_inet_sock_set_state(struct trace_event_raw_inet_sock_set_state *ctx)
{
if (ctx->newstate != TCP_ESTABLISHED)
return 0;
const __u16 family = ctx->family;
if (family != AF_INET && family != AF_INET6)
return 0;
const __u8 proto = (__u8)ctx->protocol;
if (proto != IPPROTO_TCP && proto != IPPROTO_UDP)
return 0;
struct bsc_conn_event *ev = bpf_ringbuf_reserve(&bsc_events,
sizeof(*ev), 0);
if (!ev) return 0;
fill_proc(ev);
ev->family = (__u8)family;
ev->proto = proto;
ev->event_kind = 0;
ev->_pad[0] = 0;
ev->src_port = bpf_ntohs(ctx->sport);
ev->dst_port = bpf_ntohs(ctx->dport);
if (family == AF_INET) {
__builtin_memset(ev->src_addr, 0, sizeof(ev->src_addr));
__builtin_memset(ev->dst_addr, 0, sizeof(ev->dst_addr));
bpf_probe_read_kernel(ev->src_addr, 4, &ctx->saddr);
bpf_probe_read_kernel(ev->dst_addr, 4, &ctx->daddr);
} else {
BPF_CORE_READ_INTO(ev->src_addr, ctx, saddr_v6);
BPF_CORE_READ_INTO(ev->dst_addr, ctx, daddr_v6);
}
ev->ts_ns = bpf_ktime_get_ns();
bpf_ringbuf_submit(ev, 0);
return 0;
}
/* =================================================================== *
* 2) TCP connect (IPv4 + IPv6)
* =================================================================== *
*
* Firma: int tcp_v4_connect(struct sock* sk, struct sockaddr* uaddr, int len)
* int tcp_v6_connect(struct sock* sk, struct sockaddr* uaddr, int len)
*
* Emettiamo l'evento subito all'ingresso — abbiamo gia' tutti i dati
* (sk contiene src/dst). Il connect puo' ancora fallire ma per il
* firewall conta il tentativo.
*/
SEC("kprobe/tcp_v4_connect")
int BPF_KPROBE(bsc_tcp_v4_connect, struct sock* sk)
{
if (!sk) return 0;
const __u64 now = bpf_ktime_get_ns();
/* Leggi destinazione dalla struct sock (inet_sock → skc_daddr/dport).
* NOTA: in tcp_v4_connect la destinazione viene scritta prima
* della chiamata effettiva al connect — quindi e' gia' nel sk. */
__u32 daddr = BPF_CORE_READ(sk, __sk_common.skc_daddr);
__u32 saddr = BPF_CORE_READ(sk, __sk_common.skc_rcv_saddr);
__u16 dport = BPF_CORE_READ(sk, __sk_common.skc_dport); /* net byte order */
__u16 sport = BPF_CORE_READ(sk, __sk_common.skc_num); /* host byte order */
dport = bpf_ntohs(dport);
/* saddr puo' essere 0 (kernel non ha ancora scelto src): non e' errore */
struct dedup_key k = {};
const __u64 pid_tgid = bpf_get_current_pid_tgid();
k.pid = (__u32)(pid_tgid >> 32);
k.dst_port = dport;
k.family = AF_INET;
k.proto = IPPROTO_TCP;
__builtin_memcpy(k.dst_addr, &daddr, 4);
if (!should_emit(&k, now, TCP_DEDUP_WINDOW_NS)) return 0;
struct bsc_conn_event *ev = bpf_ringbuf_reserve(&bsc_events,
sizeof(*ev), 0);
if (!ev) return 0;
fill_proc(ev);
ev->family = AF_INET;
ev->proto = IPPROTO_TCP;
ev->event_kind = 1;
ev->_pad[0] = 0;
ev->src_port = sport;
ev->dst_port = dport;
__builtin_memset(ev->src_addr, 0, sizeof(ev->src_addr));
__builtin_memset(ev->dst_addr, 0, sizeof(ev->dst_addr));
__builtin_memcpy(ev->src_addr, &saddr, 4);
__builtin_memcpy(ev->dst_addr, &daddr, 4);
ev->ts_ns = now;
bpf_ringbuf_submit(ev, 0);
return 0;
}
SEC("kprobe/tcp_v6_connect")
int BPF_KPROBE(bsc_tcp_v6_connect, struct sock* sk)
{
if (!sk) return 0;
const __u64 now = bpf_ktime_get_ns();
__u16 dport = BPF_CORE_READ(sk, __sk_common.skc_dport);
__u16 sport = BPF_CORE_READ(sk, __sk_common.skc_num);
dport = bpf_ntohs(dport);
struct dedup_key k = {};
const __u64 pid_tgid = bpf_get_current_pid_tgid();
k.pid = (__u32)(pid_tgid >> 32);
k.dst_port = dport;
k.family = AF_INET6;
k.proto = IPPROTO_TCP;
BPF_CORE_READ_INTO(k.dst_addr, sk, __sk_common.skc_v6_daddr);
if (!should_emit(&k, now, TCP_DEDUP_WINDOW_NS)) return 0;
struct bsc_conn_event *ev = bpf_ringbuf_reserve(&bsc_events,
sizeof(*ev), 0);
if (!ev) return 0;
fill_proc(ev);
ev->family = AF_INET6;
ev->proto = IPPROTO_TCP;
ev->event_kind = 1;
ev->_pad[0] = 0;
ev->src_port = sport;
ev->dst_port = dport;
BPF_CORE_READ_INTO(ev->src_addr, sk, __sk_common.skc_v6_rcv_saddr);
BPF_CORE_READ_INTO(ev->dst_addr, sk, __sk_common.skc_v6_daddr);
ev->ts_ns = now;
bpf_ringbuf_submit(ev, 0);
return 0;
}
/* =================================================================== *
* 3) UDP sendmsg (IPv4 + IPv6)
* =================================================================== *
*
* Firma: int udp_sendmsg(struct sock* sk, struct msghdr* msg, size_t len)
* int udpv6_sendmsg(struct sock* sk, struct msghdr* msg, size_t len)
*
* Se il socket e' stato "connesso" con connect(), skc_daddr/skc_dport
* contengono la destinazione. Altrimenti la destinazione e' in
* msg->msg_name (sockaddr_in/6). Gestiamo entrambi i casi.
*/
/* Estrae dst_ip e dst_port v4 dalla msghdr se presente.
* Ritorna true se ha letto con successo. */
static __always_inline bool extract_msg_v4(struct msghdr* msg,
__u32* out_daddr,
__u16* out_dport)
{
if (!msg) return false;
void* name = BPF_CORE_READ(msg, msg_name);
int nlen = BPF_CORE_READ(msg, msg_namelen);
if (!name || nlen < (int)sizeof(struct sockaddr_in)) return false;
struct sockaddr_in sa = {};
if (bpf_probe_read_kernel(&sa, sizeof(sa), name) != 0) return false;
if (sa.sin_family != AF_INET) return false;
*out_daddr = sa.sin_addr.s_addr; /* network byte order */
*out_dport = bpf_ntohs(sa.sin_port);
return true;
}
static __always_inline bool extract_msg_v6(struct msghdr* msg,
__u8 out_daddr[16],
__u16* out_dport)
{
if (!msg) return false;
void* name = BPF_CORE_READ(msg, msg_name);
int nlen = BPF_CORE_READ(msg, msg_namelen);
if (!name || nlen < (int)sizeof(struct sockaddr_in6)) return false;
struct sockaddr_in6 sa = {};
if (bpf_probe_read_kernel(&sa, sizeof(sa), name) != 0) return false;
if (sa.sin6_family != AF_INET6) return false;
__builtin_memcpy(out_daddr, &sa.sin6_addr, 16);
*out_dport = bpf_ntohs(sa.sin6_port);
return true;
}
SEC("kprobe/udp_sendmsg")
int BPF_KPROBE(bsc_udp_sendmsg, struct sock* sk, struct msghdr* msg)
{
if (!sk) return 0;
__u32 daddr = 0;
__u16 dport = 0;
/* 1) prova dalla msghdr (UDP unconnected) */
if (!extract_msg_v4(msg, &daddr, &dport)) {
/* 2) fallback: dalla struct sock (UDP connected) */
daddr = BPF_CORE_READ(sk, __sk_common.skc_daddr);
__u16 d = BPF_CORE_READ(sk, __sk_common.skc_dport);
dport = bpf_ntohs(d);
}
if (dport == 0) return 0; /* socket senza destinazione risolta */
const __u64 now = bpf_ktime_get_ns();
__u32 saddr = BPF_CORE_READ(sk, __sk_common.skc_rcv_saddr);
__u16 sport = BPF_CORE_READ(sk, __sk_common.skc_num);
struct dedup_key k = {};
const __u64 pid_tgid = bpf_get_current_pid_tgid();
k.pid = (__u32)(pid_tgid >> 32);
k.dst_port = dport;
k.family = AF_INET;
k.proto = IPPROTO_UDP;
__builtin_memcpy(k.dst_addr, &daddr, 4);
if (!should_emit(&k, now, UDP_DEDUP_WINDOW_NS)) return 0;
struct bsc_conn_event *ev = bpf_ringbuf_reserve(&bsc_events,
sizeof(*ev), 0);
if (!ev) return 0;
fill_proc(ev);
ev->family = AF_INET;
ev->proto = IPPROTO_UDP;
ev->event_kind = 2;
ev->_pad[0] = 0;
ev->src_port = sport;
ev->dst_port = dport;
__builtin_memset(ev->src_addr, 0, sizeof(ev->src_addr));
__builtin_memset(ev->dst_addr, 0, sizeof(ev->dst_addr));
__builtin_memcpy(ev->src_addr, &saddr, 4);
__builtin_memcpy(ev->dst_addr, &daddr, 4);
ev->ts_ns = now;
bpf_ringbuf_submit(ev, 0);
return 0;
}
SEC("kprobe/udpv6_sendmsg")
int BPF_KPROBE(bsc_udpv6_sendmsg, struct sock* sk, struct msghdr* msg)
{
if (!sk) return 0;
__u8 daddr[16] = {};
__u16 dport = 0;
if (!extract_msg_v6(msg, daddr, &dport)) {
/* fallback: socket connected */
BPF_CORE_READ_INTO(daddr, sk, __sk_common.skc_v6_daddr);
__u16 d = BPF_CORE_READ(sk, __sk_common.skc_dport);
dport = bpf_ntohs(d);
}
if (dport == 0) return 0;
const __u64 now = bpf_ktime_get_ns();
__u16 sport = BPF_CORE_READ(sk, __sk_common.skc_num);
struct dedup_key k = {};
const __u64 pid_tgid = bpf_get_current_pid_tgid();
k.pid = (__u32)(pid_tgid >> 32);
k.dst_port = dport;
k.family = AF_INET6;
k.proto = IPPROTO_UDP;
__builtin_memcpy(k.dst_addr, daddr, 16);
if (!should_emit(&k, now, UDP_DEDUP_WINDOW_NS)) return 0;
struct bsc_conn_event *ev = bpf_ringbuf_reserve(&bsc_events,
sizeof(*ev), 0);
if (!ev) return 0;
fill_proc(ev);
ev->family = AF_INET6;
ev->proto = IPPROTO_UDP;
ev->event_kind = 2;
ev->_pad[0] = 0;
ev->src_port = sport;
ev->dst_port = dport;
BPF_CORE_READ_INTO(ev->src_addr, sk, __sk_common.skc_v6_rcv_saddr);
__builtin_memcpy(ev->dst_addr, daddr, 16);
ev->ts_ns = now;
bpf_ringbuf_submit(ev, 0);
return 0;
}
char LICENSE[] SEC("license") = "GPL";