eBPF e XDP: Elaborazione Pacchetti ad Alte Prestazioni e Mitigazione DDoS a Line Rate
Perché XDP È Diverso da Tutto il Resto
Lo stack di rete Linux elabora i pacchetti attraverso una lunga catena: hardware NIC → driver NIC → allocazione sk_buff → netfilter/iptables → buffer di ricezione socket → applicazione. Ogni passo copia dati, alloca memoria e brucia CPU.
XDP si aggancia al driver NIC, prima dell'allocazione sk_buff. Quando arriva un pacchetto, il driver chiama il tuo programma XDP con un puntatore grezzo ai dati del pacchetto. Il programma ritorna una di cinque azioni:
XDP_PASS // Continua elaborazione normale (invia allo stack del kernel)
XDP_DROP // Droppa il pacchetto immediatamente (nessuna allocazione, nessuna copia)
XDP_TX // Rimbalza il pacchetto dalla stessa NIC
XDP_REDIRECT // Invia a un'altra NIC, CPU, o socket AF_XDP
XDP_ABORTED // Errore, droppa e logga
Scrivere il Primo Programma XDP
Un programma XDP minimale che droppa tutto il traffico UDP sulla porta 53:
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/udp.h>
#include <bpf/bpf_helpers.h>
SEC("xdp")
int xdp_dns_filter(struct xdp_md *ctx)
{
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return XDP_PASS;
if (eth->h_proto != __constant_htons(ETH_P_IP))
return XDP_PASS;
struct iphdr *ip = (void *)(eth + 1);
if ((void *)(ip + 1) > data_end)
return XDP_PASS;
if (ip->protocol != IPPROTO_UDP)
return XDP_PASS;
struct udphdr *udp = (void *)ip + (ip->ihl * 4);
if ((void *)(udp + 1) > data_end)
return XDP_PASS;
if (udp->source == __constant_htons(53))
return XDP_DROP;
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
Ogni controllo dei limiti è obbligatorio. Il verificatore BPF rifiuterà il programma se accedi a ip->protocol senza prima dimostrare che (void *)(ip + 1) <= data_end.
Mitigazione DDoS Reale: Rate Limiting con BPF Map
struct rate_limit_entry {
__u64 packet_count;
__u64 last_reset_ns;
};
struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__uint(max_entries, 1000000);
__type(key, __u32);
__type(value, struct rate_limit_entry);
} rate_limits SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, 1);
__type(key, __u32);
__type(value, __u64);
} config SEC(".maps");
SEC("xdp")
int xdp_rate_limiter(struct xdp_md *ctx)
{
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return XDP_PASS;
if (eth->h_proto != __constant_htons(ETH_P_IP))
return XDP_PASS;
struct iphdr *ip = (void *)(eth + 1);
if ((void *)(ip + 1) > data_end)
return XDP_PASS;
__u32 src_ip = ip->saddr;
__u32 config_key = 0;
__u64 *max_pps = bpf_map_lookup_elem(&config, &config_key);
__u64 threshold = max_pps ? *max_pps : 10000;
struct rate_limit_entry *entry = bpf_map_lookup_elem(&rate_limits, &src_ip);
__u64 now = bpf_ktime_get_ns();
if (entry) {
if (now - entry->last_reset_ns > 1000000000ULL) {
entry->packet_count = 1;
entry->last_reset_ns = now;
return XDP_PASS;
}
entry->packet_count++;
if (entry->packet_count > threshold)
return XDP_DROP;
} else {
struct rate_limit_entry new_entry = {
.packet_count = 1,
.last_reset_ns = now,
};
bpf_map_update_elem(&rate_limits, &src_ip, &new_entry, BPF_ANY);
}
return XDP_PASS;
}
BPF_MAP_TYPE_LRU_HASH è la chiave. Durante un DDoS, vedrai milioni di IP sorgente uniche (spoofed). Una hash map normale si riempie e rifiuta nuove entry. La LRU map evicta le entry più vecchie — tipicamente IP d'attacco già rate-limitate.
AF_XDP: Quando Serve Elaborazione Userspace
AF_XDP permette di redirigere pacchetti a un'applicazione userspace attraverso un ring buffer zero-copy:
struct {
__uint(type, BPF_MAP_TYPE_XSKMAP);
__uint(max_entries, 64);
__type(key, __u32);
__type(value, __u32);
} xsks_map SEC(".maps");
SEC("xdp")
int xdp_af_xdp_redirect(struct xdp_md *ctx)
{
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return XDP_PASS;
if (eth->h_proto != __constant_htons(ETH_P_IP))
return XDP_PASS;
struct iphdr *ip = (void *)(eth + 1);
if ((void *)(ip + 1) > data_end)
return XDP_PASS;
if (ip->protocol == IPPROTO_TCP) {
struct tcphdr *tcp = (void *)ip + (ip->ihl * 4);
if ((void *)(tcp + 1) > data_end)
return XDP_PASS;
if (tcp->dest == __constant_htons(8080))
return bpf_redirect_map(&xsks_map, ctx->rx_queue_index, XDP_PASS);
}
return XDP_PASS;
}
AF_XDP offre 8-12 milioni di pacchetti al secondo per core in userspace. Circa 10x più veloce di recvmsg() su un raw socket.
Dentro il Dataplane di Cilium
Cilium è l'uso più sofisticato di BPF per il networking in produzione. Sostituisce completamente kube-proxy e implementa il networking Kubernetes in BPF. Quando un pacchetto lascia il Pod A destinato a un servizio Kubernetes, il programma BPF di Cilium:
- Cerca la destinazione in
SERVICE_MAPper trovare un backend pod - Esegue DNAT (riscrive l'IP destinazione a quella del backend pod)
- Crea un'entry di connection tracking in
CT_MAP - Controlla
POLICY_MAPper verificare che la network policy permetta la connessione - Inoltra il pacchetto direttamente all'interfaccia veth del backend pod
Tutto nel kernel, senza context switch a userspace. I benchmark di Cilium mostrano 2-3x migliore latenza e throughput rispetto a kube-proxy basato su iptables.
Numeri di Produzione
| Metrica | Prima (iptables) | Dopo (XDP) | |---------|-------------------|------------| | Max assorbimento DDoS | ~5 Gbps | 40+ Gbps | | CPU durante flood 10 Gbps | 95% | 8% | | Latenza traffico legittimo durante attacco | 200-500ms | <5ms | | Tempo aggiornamento regole | ~50ms (iptables flush) | <1ms (map update) | | Memoria per IP tracciata | ~512 byte (conntrack) | 16 byte (BPF map) |
La differenza di memoria conta. Durante un DDoS volumetrico con milioni di IP sorgente spoofed, il conntrack Linux consumava gigabyte di RAM e alla fine faceva OOM-kill ai processi. La BPF LRU map usa 16 MB fissi indipendentemente dalla dimensione dell'attacco.
Quando NON Usare XDP
XDP è uno strumento chirurgico, non una soluzione universale:
- Filtraggio a livello applicazione: Usa un reverse proxy. XDP opera su pacchetti grezzi prima del riassemblaggio.
- Ispezione profonda dei pacchetti stateful: Il limite di istruzioni BPF e la mancanza di allocazione dinamica di memoria rendono impraticabile l'analisi stateful complessa.
- Firewalling semplice: Se iptables gestisce il tuo carico, usa iptables.
XDP brilla per: mitigazione DDoS, load balancing, packet steering, telemetria, e qualsiasi cosa dove devi prendere decisioni per-pacchetto a milioni di pacchetti al secondo. La curva di apprendimento è ripida, ma una volta che hai scritto il tuo primo programma XDP che droppa 40 Gbps di traffico d'attacco usando il 3% di un core CPU, non guarderai più iptables allo stesso modo.