Vai al contenuto principale
Networking

eBPF e XDP: Elaborazione Pacchetti ad Alte Prestazioni e Mitigazione DDoS a Line Rate

6 min lettura
LD
Lucio Durán
Engineering Manager & AI Solutions Architect
Disponibile anche in: English, Español

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:

  1. Cerca la destinazione in SERVICE_MAP per trovare un backend pod
  2. Esegue DNAT (riscrive l'IP destinazione a quella del backend pod)
  3. Crea un'entry di connection tracking in CT_MAP
  4. Controlla POLICY_MAP per verificare che la network policy permetta la connessione
  5. 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.

ebpfxdpnetworkingddosciliumlinux-kernelelaborazione-pacchetti

Strumenti menzionati in questo articolo

CloudflareProva Cloudflare
DigitalOceanProva DigitalOcean
Divulgazione: Alcuni link in questo articolo sono link di affiliazione. Se ti registri tramite questi, potrei guadagnare una commissione senza costi aggiuntivi per te. Raccomando solo strumenti che uso e di cui mi fido personalmente.
Compartir
Seguime