Scriviamo una protezione contro gli attacchi DDoS su XDP. Parte fondamentale

La tecnologia eXpress Data Path (XDP) consente di eseguire un'elaborazione arbitraria del traffico sulle interfacce Linux prima che i pacchetti raggiungano lo stack di rete del kernel. L'uso di XDP include la protezione contro attacchi DDoS (CloudFlare), filtri complessi e raccolta di statistiche (Netflix). I programmi XDP vengono eseguiti in una macchina virtuale eBPF, pertanto hanno limitazioni sia sul proprio codice che sulle funzioni del kernel disponibili, a seconda del tipo di filtro.

Questo articolo si propone di colmare le lacune presenti in numerosi materiali su XDP. In primo luogo, forniscono codice pronto all'uso che immediatamente bypassa le peculiarità di XDP: è preparato per la verifica o è troppo semplice da causare problemi. Quando si prova a scrivere il proprio codice da zero, non si ha chiarezza su come affrontare gli errori caratteristici. In secondo luogo, non vengono trattati i modi per testare localmente XDP senza VM e 'hardware', nonostante presentino le loro 'insidie'. Il testo è destinato a programmatori familiari con reti e Linux, interessati a XDP ed eBPF.

In questa sezione esploreremo in dettaglio come viene assemblato il filtro XDP e come testarlo, quindi scriveremo una versione semplice del noto meccanismo SYN cookies a livello di elaborazione dei pacchetti. Fino ad allora non creeremo una "lista bianca".
Clienti verificati, contatori e gestione del filtro - basteranno i log.

Scriveremo in C - non è di moda, ma è pratico. Tutto il codice è disponibile su GitHub tramite il link alla fine ed è suddiviso in commit secondo le fasi descritte nell'articolo.

Disclaimer. Durante l'articolo sarà sviluppata una mini-soluzione per riflettere gli attacchi DDoS, poiché è una sfida realistica per XDP e il mio campo. Tuttavia, l'obiettivo principale è comprendere la tecnologia, non è un manuale per la creazione di una protezione pronta all'uso. Il codice didattico non è ottimizzato e tralascia alcuni dettagli.

Panoramica di XDP

Presento solo i punti chiave, per non duplicare la documentazione e gli articoli esistenti.

Quindi, il codice del filtro viene caricato nel kernel. I pacchetti in arrivo vengono trasferiti al filtro. Alla fine, il filtro deve decidere: lasciare passare il pacchetto nel kernel (XDP_PASS), scartare il pacchetto (XDP_DROP) oppure rimandarlo indietro (XDP_TX). Il filtro può modificare il pacchetto, il che è particolarmente rilevante per XDP_TX. È anche possibile interrompere forzatamente il programma (XDP_ABORTED) e annullare il pacchetto, ma ciò è analogo a assert(0) — per il debug.

La macchina virtuale eBPF (Extended Berkeley Packet Filter) è progettata per essere semplice, affinché il kernel possa verificare che il codice non entri in loop e non danneggi la memoria altrui. Le restrizioni e i controlli complessivi sono:

  • I cicli (ritorni) sono vietati.
  • C'è uno stack per i dati, ma non ci sono funzioni (tutte le funzioni C devono essere integrate).
  • Gli accessi alla memoria al di fuori dello stack e del buffer del pacchetto sono vietati.
  • La dimensione del codice è limitata, ma in pratica ciò non è molto rilevante.
  • Sono consentiti solo chiamate a funzioni speciali del kernel (eBPF helpers).

Lo sviluppo e l'installazione del filtro si presentano come segue:

  1. Il codice sorgente (ad esempio, kernel.c) viene compilato in un oggetto (kernel.o) per l'architettura della macchina virtuale eBPF. A ottobre 2019, la compilazione in eBPF è supportata da Clang e promessa in GCC 10.1.
  2. Se nel codice oggetto ci sono riferimenti alle strutture del kernel (ad esempio, a tabelle e contatori), invece dei loro ID ci sono degli zeri, il che significa che tale codice non può essere eseguito. Prima di caricarlo nel kernel, è necessario sostituire questi zeri con gli ID dei singoli oggetti creati tramite chiamate al kernel (linkare il codice). Questo può essere fatto con utilità esterne, oppure si può scrivere un programma che linka e carica un filtro specifico.
  3. Il kernel verifica il programma caricato. Viene controllata l'assenza di cicli e l'uscita fuori dai limiti del package e dello stack. Se il verificatore non riesce a dimostrare che il codice è corretto, il programma viene rifiutato: è necessario sapere come compiacerlo.
  4. Dopo una verifica riuscita, il kernel compila il codice oggetto dell'architettura eBPF in codice macchina dell'architettura di sistema (just-in-time).
  5. Il programma viene collegato all'interfaccia e inizia a elaborare i pacchetti.

Poiché XDP opera nel kernel, il debug avviene tramite i log di tracciamento e, effettivamente, dai pacchetti che il programma filtra o genera. Tuttavia, eBPF garantisce la sicurezza del codice caricato per il sistema, quindi è possibile sperimentare con XDP direttamente su Linux locale.

Preparazione dell'ambiente

Build

Clang non può generare direttamente codice oggetto per l'architettura eBPF, quindi il processo si compone di due fasi:

  1. Compilare il codice C in bytecode LLVM (clang -emit-llvm).
  2. Convertire il bytecode in codice oggetto eBPF (llc -march=bpf -filetype=obj).

Quando si scrive un filtro, sarà utile avere un paio di file con funzioni ausiliarie e macro dei test del kernel. È importante che corrispondano alla versione del kernel (KVER). Li scarichiamo in helpers/:

export KVER=v5.3.7
export BASE=https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/plain/tools/testing/selftests/bpf
wget -P helpers --content-disposition "${BASE}/bpf_helpers.h?h=${KVER}" "${BASE}/bpf_endian.h?h=${KVER}"
unset KVER BASE

Makefile per Arch Linux (kernel 5.3.7):

CLANG ?= clang
LLC ?= llc

KDIR ?= /lib/modules/$(shell uname -r)/build
ARCH ?= $(subst x86_64,x86,$(shell uname -m))

CFLAGS = 
    -Ihelpers 
    
    -I$(KDIR)/include 
    -I$(KDIR)/include/uapi 
    -I$(KDIR)/include/generated/uapi 
    -I$(KDIR)/arch/$(ARCH)/include 
    -I$(KDIR)/arch/$(ARCH)/include/generated 
    -I$(KDIR)/arch/$(ARCH)/include/uapi 
    -I$(KDIR)/arch/$(ARCH)/include/generated/uapi 
    -D__KERNEL__ 
    
    -fno-stack-protector -O2 -g

xdp_%.o: xdp_%.c Makefile
    $(CLANG) -c -emit-llvm $(CFLAGS) $< -o - | 
    $(LLC) -march=bpf -filetype=obj -o $@

.PHONY: all clean

all: xdp_filter.o

clean:
    rm -f ./*.o

KDIR contiene il percorso agli header del kernel, ARCH — architettura del sistema. I percorsi e gli strumenti possono variare leggermente tra le distribuzioni.

Esempio di differenze per Debian 10 (kernel 4.19.67)

# другая команда
CLANG ?= clang
LLC ?= llc-7

# другой каталог
KDIR ?= /usr/src/linux-headers-$(shell uname -r)
ARCH ?= $(subst x86_64,x86,$(shell uname -m))

# два дополнительных каталога -I
CFLAGS = 
    -Ihelpers 
    
    -I/usr/src/linux-headers-4.19.0-6-common/include 
    -I/usr/src/linux-headers-4.19.0-6-common/arch/$(ARCH)/include 
    # далее без изменений

CFLAGS collegano la directory con gli header ausiliari e diverse directory con gli header del kernel. Il simbolo __KERNEL__ indica che gli header UAPI (userspace API) sono definiti per il codice del kernel, poiché il filtro viene eseguito nel kernel.

La protezione dello stack può essere disattivata (-fno-stack-protector), perché il verificatore di codice eBPF verifica comunque l'overflow dello stack. Dovrebbero essere attivate fin da subito le ottimizzazioni, poiché la dimensione del bytecode eBPF è limitata.

Iniziamo con un filtro che consente tutti i pacchetti e non fa nulla:

#include <uapi/linux/bpf.h>

#include <bpf_helpers.h>

SEC("prog")
int xdp_main(struct xdp_md* ctx) {
    return XDP_PASS;
}

char _license[] SEC("license") = "GPL";

Team make raccoglie xdp_filter.o. Dove ora possiamo testarlo?

Banchi di prova

Il setup dovrebbe includere due interfacce: una su cui sarà presente il filtro e l'altra da cui saranno inviati i pacchetti. Queste devono essere apparecchiature Linux complete con i propri IP, per verificare come le applicazioni standard interagiscono con il nostro filtro.

Le interfacce di tipo veth (Ethernet virtuale) sono adatte: si tratta di una coppia di interfacce di rete virtuali "collegate" direttamente l'una all'altra. Possono essere create in questo modo (in questa sezione, tutti i comandi ip vengono eseguiti da root):

ip link add xdp-remote type veth peer name xdp-local

Qui xdp-remotexdp-local — nomi dei dispositivi. Su xdp-local (192.0.2.1/24) sarà collegato un filtro, con xdp-remote (192.0.2.2/24) riceverà il traffico in entrata. Tuttavia, c'è un problema: le interfacce si trovano sulla stessa macchina e Linux non invierà il traffico a una di esse attraverso l'altra. Questo può essere risolto con delle regole astute. iptables, ma dovranno modificare i pacchetti, il che è scomodo durante il debug. È meglio usare gli spazi dei nomi di rete (network namespaces, di seguito netns).

Uno spazio dei nomi di rete contiene un insieme di interfacce, tabelle di routing e regole NetFilter, isolati da oggetti simili in altri netns. Ogni processo opera in uno spazio dei nomi e ha accesso solo agli oggetti di quel netns. Per impostazione predefinita, nel sistema esiste un unico spazio dei nomi di rete per tutti gli oggetti, quindi è possibile lavorare in Linux senza conoscere netns.

Creiamo un nuovo spazio dei nomi xdp-test e spostiamolo lì xdp-remote.

ip netns add xdp-test
ip link set dev xdp-remote netns xdp-test

Allora, il processo che si esegue in xdp-test, non 'vedrà' xdp-local (rimarrà nel netns predefinito) e quando invierà un pacchetto a 192.0.2.1 lo trasmetterà attraverso xdp-remote, poiché questa è l'unica interfaccia in 192.0.2.0/24 accessibile a questo processo. Questo vale anche per l'altra direzione.

Quando si passa tra netns, l'interfaccia viene abbassata e perde indirizzo. Per configurare l'interfaccia in netns, è necessario eseguire ip ... in questo spazio dei nomi della riga di comando ip netns exec:

ip netns exec xdp-test 
    ip address add 192.0.2.2/24 dev xdp-remote
ip netns exec xdp-test 
    ip link set xdp-remote up

Come si può notare, questo non è diverso dalla configurazione xdp-local nello spazio dei nomi predefinito:

    ip address add 192.0.2.1/24 dev xdp-local
    ip link set xdp-local up

Se eseguite tcpdump -tnevi xdp-local, si può vedere che i pacchetti inviati da xdp-test, vengono recapitati su questa interfaccia:

ip netns exec xdp-test   ping 192.0.2.1

È comodo avviare una shell in xdp-test. Nel repository c'è uno script che automatizza il lavoro con il banco di prova, ad esempio, si può configurare il banco con il comando sudo ./stand up e rimuoverlo sudo ./stand down.

Tracciamento

Il filtro viene collegato al dispositivo in questo modo:

ip -force link set dev xdp-local xdp object xdp_filter.o verbose

Chiave -force è necessario per collegare un nuovo programma, se un altro è già collegato. "No news is good news" non si applica a questo comando, l'output è comunque esteso. Specificare verbose non è obbligatorio, ma con esso appare un rapporto sull'attività del verificatore del codice con un elenco dell'assembly:

Verifier analysis:

0: (b7) r0 = 2
1: (95) exit

Distaccare il programma dall'interfaccia:

ip link set dev xdp-local xdp off

In questo script il comando è sudo ./stand attachsudo ./stand detach.

Collegando il filtro, è possibile verificare che ping continua a funzionare, ma il programma funziona? Aggiungiamo i log. La funzione bpf_trace_printk() sembra simile a printf(), supporta solo fino a tre argomenti, escluso il modello, e un elenco limitato di specificatori. Il macro bpf_printk() semplifica la chiamata.

   SEC("prog")
   int xdp_main(struct xdp_md* ctx) {
+      bpf_printk("ricevuto pacchetto: %pn", ctx);
       return XDP_PASS;
   }

L'output va nel canale di tracciamento del kernel, che deve essere abilitato:

echo -n 1 | sudo tee /sys/kernel/debug/tracing/options/trace_printk

Visualizzazione del flusso di messaggi:

cat /sys/kernel/debug/tracing/trace_pipe

Entrambe queste comandi effettuano la chiamata sudo ./stand log.

Il ping ora dovrebbe generare messaggi come questi:

-110930 [004] ..s1 78803.244967: 0: ricevuto pacchetto: 00000000ac510377

Se si presta attenzione all'output del verificatore, si possono notare calcoli strani:

0: (bf) r3 = r1
1: (18) r1 = 0xa7025203a7465
3: (7b) *(u64 *)(r10 -8) = r1
4: (18) r1 = 0x6b63617020746f67
6: (7b) *(u64 *)(r10 -16) = r1
7: (bf) r1 = r10
8: (07) r1 += -16
9: (b7) r2 = 16
10: (85) call bpf_trace_printk#6

Il punto è che i programmi eBPF non hanno una sezione dati, quindi l'unico modo per codificare la stringa di formato sono gli argomenti immediati dei comandi della VM:

$ python -c "import binascii; print(bytes(reversed(binascii.unhexlify('0a7025203a74656b63617020746f67'))))"
b'risultato pacchetto: %pn'

Per questo motivo, l'output di debug ingrandisce notevolmente il codice finale.

Invio di pacchetti XDP

Modificheremo il filtro: lasciamo che tutti i pacchetti in entrata vengano restituiti. Questo non è corretto dal punto di vista di rete, poiché sarebbe necessario modificare gli indirizzi negli header, ma per ora è importante che funzioni.

       bpf_printk("pacchetto ricevuto: %pn", ctx);
-      return XDP_PASS;
+      return XDP_TX;
   }

Avviamo tcpdump con xdp-remote. Deve mostrare richieste ICMP Echo identiche in uscita e in entrata e smettere di mostrare le risposte ICMP Echo. Ma non mostra. È stato scoperto che per funzionare XDP_TX nel programma su xdp-local è necessario, affinché l'interfaccia corrispondente xdp-remote sia anch'essa assegnata a un programma, anche se vuoto, e sia attivata.

Come l'ho scoperto?

Tracciare il percorso del pacchetto nel kernel è possibile grazie al meccanismo perf events, per inciso, che utilizza la stessa macchina virtuale, quindi per l'analisi con eBPF si applica eBPF.

Devi fare del bene dal male, perché non c'è più nulla da cui farlo.

$ sudo perf trace --call-graph dwarf -e 'xdp:*'
   0.000 ping/123455 xdp:xdp_bulk_tx:ifindex=19 action=TX sent=0 drops=1 err=-6
                                     veth_xdp_flush_bq ([veth])
                                     veth_xdp_flush_bq ([veth])
                                     veth_poll ([veth])

Che cos'è il codice 6?

$ errno 6
ENXIO 6 Nessun dispositivo o indirizzo di questo tipo

Funzione veth_xdp_flush_bq() riceve il codice di errore da veth_xdp_xmit(), dove si cerca per ENXIO e troviamo il commento.

Ripristineremo il filtro minimo (XDP_PASS) nel file xdp_dummy.c, lo aggiungeremo al Makefile, lo collegheremo a xdp-remote:

ip netns exec remote 
    ip link set dev int xdp object dummy.o

Ora tcpdump mostra ciò che ci si aspetta:

62:57:8e:70:44:64 > 26:0e:25:37:8f:96, ethertype IPv4 (0x0800), lunghezza 98: (tos 0x0, ttl 64, id 13762, offset 0, flags [DF], proto ICMP (1), lunghezza 84)
    192.0.2.2 > 192.0.2.1: richiesta di eco ICMP, id 46966, seq 1, lunghezza 64
62:57:8e:70:44:64 > 26:0e:25:37:8f:96, ethertype IPv4 (0x0800), lunghezza 98: (tos 0x0, ttl 64, id 13762, offset 0, flags [DF], proto ICMP (1), lunghezza 84)
    192.0.2.2 > 192.0.2.1: richiesta di eco ICMP, id 46966, seq 1, lunghezza 64

Se invece vengono mostrati solo ARP, è necessario rimuovere i filtri (questo lo fa sudo ./stand detach), avviare ping, poi impostare i filtri e riprovare. Il problema è che il filtro XDP_TX funziona anche per ARP, e se lo stack
dello spazio dei nomi xdp-test ha “dimenticato” l'indirizzo MAC 192.0.2.1, non sarà in grado di risolvere questo IP.

Definizione del compito

Passiamo al compito dichiarato: scrivere un meccanismo SYN cookies su XDP.

Un tipo di attacco DDoS ancora molto diffuso è il SYN flood, il cui funzionamento è il seguente. Durante l'instaurazione di una connessione (TCP handshake), il server riceve un SYN, riserva risorse per la futura connessione, risponde con un pacchetto SYNACK e attende un ACK. L'attaccante invia semplicemente pacchetti SYN con indirizzi falsi in quantità di migliaia al secondo da ciascun host di un botnet di diverse migliaia di unità. Il server è costretto a riservare risorse immediatamente all'arrivo del pacchetto e le rilascia dopo un lungo timeout, risultando quindi in un esaurimento della memoria o dei limiti, con nuove connessioni non accettate e il servizio reso non disponibile.

Se non si riservano risorse per ciascun pacchetto SYN, ma si risponde solamente con un pacchetto SYNACK, come può il server capire che il pacchetto ACK, arrivato in un secondo momento, appartiene al pacchetto SYN che non è stato memorizzato? Dopotutto, l'attaccante potrebbe generare anche ACK falsi. Il principio dei SYN cookie è quello di codificare le seqnum informazioni di connessione come un hash degli indirizzi, delle porte e di un sale variabile. Se l'ACK è arrivato prima del cambio del sale, è possibile ricalcolare l'hash e confrontarlo con acknum. L'attaccante non può falsificarlo, poiché il sale include un segreto e non riuscirà a tentare con forza bruta a causa della limitazione della banda. acknum атакующий не может, так как соль включает секрет, а перебрать не успеет из-за ограниченного канала.

Il cookie SYN è stato implementato nel kernel Linux da tempo e può addirittura attivarsi automaticamente se i pacchetti SYN vengono inviati troppo rapidamente e in massa.

Guida introduttiva al handshake TCP

TCP fornisce il trasferimento dei dati come un flusso di byte; ad esempio, le richieste HTTP vengono trasmesse sopra TCP. Il flusso viene trasmesso a pezzi in pacchetti. Tutti i pacchetti TCP hanno flag logici e numeri di sequenza a 32 bit:

  • La combinazione di flag determina il ruolo specifico di un pacchetto. Il flag SYN indica che si tratta del primo pacchetto del mittente nella connessione. Il flag ACK significa che il mittente ha ricevuto tutti i dati della connessione fino al byte. acknumUn pacchetto può avere più flag e viene chiamato in base alla loro combinazione, ad esempio, pacchetto SYNACK.

  • Il numero di sequenza (seqnum) definisce l'offset nel flusso di dati per il primo byte trasmesso in questo pacchetto. Ad esempio, se nel primo pacchetto ci sono X byte di dati e questo numero era N, nel successivo pacchetto con nuovi dati sarà N+X. All'inizio della connessione, ciascuna parte sceglie questo numero in modo arbitrario.

  • Numero di riconoscimento (acknum) — ha lo stesso offset di seqnum, ma definisce non il numero di byte trasmessi, ma il numero del primo byte dall'host ricevente che il mittente non ha ancora visto.

All'inizio della connessione, le parti devono concordare seqnumacknum. Il client invia un pacchetto SYN con il proprio seqnum = X. Il server risponde con un pacchetto SYNACK, nel quale registra il proprio seqnum = Y e imposta acknum = X + 1. Il client risponde al SYNACK con un pacchetto ACK, dove seqnum = X + 1, acknum = Y + 1. Dopo di ciò inizia il vero e proprio trasferimento dei dati.

Se il partner non conferma la ricezione del pacchetto, TCP lo reinvia dopo un timeout.

Perché i SYN cookies non vengono sempre utilizzati?

In primo luogo, se il SYNACK o l'ACK vanno persi, si dovrà attendere un nuovo invio, il che rallenta l'instaurazione della connessione. In secondo luogo, nel pacchetto SYN — e solo in esso! — viene trasmesso un certo numero di opzioni che influiscono sul funzionamento successivo della connessione. Non memorizzando i pacchetti SYN in arrivo, il server ignora in questo modo queste opzioni e nei pacchetti successivi il client non le invierà più. TCP può comunque continuare a funzionare, ma almeno nella fase iniziale la qualità della connessione si deteriorerà.

Dal punto di vista dei pacchetti, il programma XDP deve fare quanto segue:

  • rispondere SYNACK con cookie;
  • rispondere RST (interrompere la connessione) su ACK;
  • scartare gli altri pacchetti.

Pseudocodice dell'algoritmo insieme all'analisi del pacchetto:

Se non è Ethernet,
    saltare il pacchetto.
Se non è IPv4,
    saltare il pacchetto.
Se l'indirizzo è nella tabella dei controllati,
        ridurre il contatore dei controlli rimanenti,
        saltare il pacchetto.
Se non è TCP,
    scartare il pacchetto.     (**)
Se è SYN,
    rispondere con SYN-ACK e cookie.
Se è ACK,
    se acknum non contiene cookie,
        scartare il pacchetto.
    Inserire nella tabella l'indirizzo con N controlli rimanenti.    (*)
    Rispondere RST.   (**)
In altri casi, scartare il pacchetto.

Uno (*) i punti contrassegnati in cui è necessario gestire lo stato del sistema — nella prima fase possiamo fare a meno di essi, implementando semplicemente il TCP handshake con la generazione di un SYN cookie come seqnum.

In questo posto (**), finché non abbiamo una tabella, salteremo il pacchetto.

Implementazione del TCP handshake

Analisi del pacchetto e verifica del codice

Avremo bisogno delle strutture delle intestazioni di rete: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) e TCP (uapi/linux/tcp.h). Non sono riuscito a collegare l'ultimo a causa di errori legati a atomic64_t, ho dovuto copiare le definizioni necessarie nel codice.

Tutte le funzioni evidenziate in C per facilitare la lettura devono essere incorporate nel punto di chiamata, poiché il verificatore eBPF nel kernel vieta i salti all'indietro, ovvero, di fatto, cicli e chiamate di funzioni.

#define INTERNAL static __attribute__((always_inline))

Macro LOG() disabilita la stampa nella build di rilascio.

Il programma è una pipeline di funzioni. Ognuna di esse accetta un pacchetto, nel quale è evidenziato l'intestazione del livello corrispondente, ad esempio, process_ether() si aspetta che sia riempito ether. In base all'analisi dei campi, la funzione può inviare il pacchetto al livello superiore. Il risultato dell'operazione della funzione è un'azione XDP. Attualmente, i gestori SYN e ACK passano tutti i pacchetti.

struct Packet {
    struct xdp_md* ctx;

    struct ethhdr* ether;
    struct iphdr* ip;
    struct tcphdr* tcp;
};

INTERNAL int process_tcp_syn(struct Packet* packet) { return XDP_PASS; }
INTERNAL int process_tcp_ack(struct Packet* packet) { return XDP_PASS; }
INTERNAL int process_tcp(struct Packet* packet) { ... }
INTERNAL int process_ip(struct Packet* packet) { ... }

INTERNAL int
process_ether(struct Packet* packet) {
    struct ethhdr* ether = packet->ether;

    LOG("Ether(proto=0x%x)", bpf_ntohs(ether->h_proto));

    if (ether->h_proto != bpf_ntohs(ETH_P_IP)) {
        return XDP_PASS;
    }

    // B
    struct iphdr* ip = (struct iphdr*)(ether + 1);
    if ((void*)(ip + 1) > (void*)packet->ctx->data_end) {
        return XDP_DROP; /* пакетом с ошибкой */
    }

    packet->ip = ip;
    return process_ip(packet);
}

SEC("prog")
int xdp_main(struct xdp_md* ctx) {
    struct Packet packet;
    packet.ctx = ctx;

    // A
    struct ethhdr* ether = (struct ethhdr*)(void*)ctx->data;
    if ((void*)(ether + 1) > (void*)ctx->data_end) {
        return XDP_PASS;
    }

    packet.ether = ether;
    return process_ether(&packet);
}

Fai attenzione alle verifiche contrassegnate A e B. Se commenti A, il programma verrà compilato, ma durante il caricamento si verificherà un errore di verifica:

Analisi del Verificatore:


11: (7b) *(u64 *)(r10 -48) = r1
12: (71) r3 = *(u8 *)(r7 +13)
accesso non valido al pacchetto, off=13 size=1, R7(id=0,off=0,r=0)
L'offset R7 è al di fuori del pacchetto
11 istruzioni elaborate (limite 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0

Errore durante il recupero del programma/mappa!

Stringa chiave accesso non valido al pacchetto, off=13 size=1, R7(id=0,off=0,r=0): ci sono casi in cui il tredicesimo byte dall'inizio del buffer si trova al di fuori del pacchetto. Dalla listing è difficile capire a quale riga si riferisca, ma c'è il numero dell'istruzione (12) e il disassemblatore che mostra le righe del codice sorgente:

llvm-objdump -S xdp_filter.o | less

In questo caso indica la riga

LOG("Ether(proto=0x%x)", bpf_ntohs(ether->h_proto));

da cui è chiaro che il problema è ether. Sempre così.

Risposta su SYN

L'obiettivo in questa fase è formare un pacchetto SYNACK corretto con il seqnum, che in futuro sarà sostituito da un cookie SYN. Tutte le modifiche avvengono in process_tcp_syn() e nelle sue vicinanze.

Controllo del pacchetto

Stranamente, ecco la riga più interessante, anzi, il commento ad essa:

/* Required to verify checksum calculation */
const void* data_end = (const void*)ctx->data_end;

Durante la scrittura della prima versione del codice è stato utilizzato il kernel 5.1, per il quale c'era una differenza tra data_end(const void*)ctx->data_end. Durante la stesura dell'articolo il kernel 5.3.1 non presentava questo problema. È possibile che il compilatore trattasse la variabile locale diversamente rispetto al campo. La morale è che a grandi livelli di annidamento, semplificare il codice può aiutare.

In seguito, controlli di routine sulle lunghezze in onore del verificatore; su MAX_CSUM_BYTES qui sotto.

const u32 ip_len = ip->ihl * 4;
if ((void*)ip + ip_len > data_end) {
    return XDP_DROP; /* pacchetto malformato */
}
if (ip_len > MAX_CSUM_BYTES) {
    return XDP_ABORTED; /* limitazione dell'implementazione */
}

const u32 tcp_len = tcp->doff * 4;
if ((void*)tcp + tcp_len > (void*)ctx->data_end) {
    return XDP_DROP; /* pacchetto malformato */
}
if (tcp_len > MAX_CSUM_BYTES) {
    return XDP_ABORTED; /* limitazione dell'implementazione */
}

Inversione del pacchetto

Riempire seqnum e acknum, impostiamo ACK (SYN è già impostato):

const u32 cookie = 42;
tcp->ack_seq = bpf_htonl(bpf_ntohl(tcp->seq) + 1);
tcp->seq = bpf_htonl(cookie);
tcp->ack = 1;

Scambiamo le porte TCP, l'indirizzo IP e gli indirizzi MAC. La libreria standard non è accessibile da un programma XDP, quindi memcpy() — macro che nasconde l'intrinseco Clang.

const u16 temp_port = tcp->source;
tcp->source = tcp->dest;
tcp->dest = temp_port;

const u32 temp_ip = ip->saddr;
ip->saddr = ip->daddr;
ip->daddr = temp_ip;

struct ethhdr temp_ether = *ether;
memcpy(ether->h_dest, temp_ether.h_source, ETH_ALEN);
memcpy(ether->h_source, temp_ether.h_dest, ETH_ALEN);

Ricalcolo dei checksum

Le somme di controllo IPv4 e TCP richiedono la somma di tutte le parole a 16 bit negli header, e la dimensione degli header è annotata in essi, quindi al momento della compilazione non è nota. Questo rappresenta un problema, poiché il verificatore non consentirà un ciclo normale fino al confine della variabile. Tuttavia, la dimensione degli header è limitata: massimo 64 byte ciascuno. È possibile creare un ciclo con un numero fisso di iterazioni, che può terminare anticipatamente.

Voglio notare che esiste RFC 1624 su come ricalcolare la somma di controllo parzialmente, se sono state modificate solo determinate parole fisse dei pacchetti. Tuttavia, questo metodo non è universale e sarebbe più difficile mantenere l'implementazione.

Funzione per il calcolo della somma di controllo:

#define MAX_CSUM_WORDS 32
#define MAX_CSUM_BYTES (MAX_CSUM_WORDS * 2)

INTERNAL u32
sum16(const void* data, u32 size, const void* data_end) {
    u32 s = 0;
#pragma unroll
    for (u32 i = 0; i < MAX_CSUM_WORDS; i++) {
        if (2*i >= size) {
            return s; /* normal exit */
        }
        if (data + 2*i + 1 + 1 > data_end) {
            return 0; /* should be unreachable */
        }
        s += ((const u16*)data)[i];
    }
    return s;
}

Nonostante dimensione sia verificato dal codice chiamante, la seconda condizione di uscita è necessaria affinché il verificatore possa dimostrare il completamento del ciclo.

Per le parole a 32 bit è stata realizzata una versione più semplice:

INTERNAL u32
sum16_32(u32 v) {
    return (v >> 16) + (v & 0xffff);
}

In sostanza, il ricalcolo delle somme di controllo e l'invio del pacchetto indietro:

ip->check = 0;
ip->check = carry(sum16(ip, ip_len, data_end));

u32 tcp_csum = 0;
tcp_csum += sum16_32(ip->saddr);
tcp_csum += sum16_32(ip->daddr);
tcp_csum += 0x0600;
tcp_csum += tcp_len <check = 0;
tcp_csum += sum16(tcp, tcp_len, data_end);
tcp->check = carry(tcp_csum);

return XDP_TX;

Funzione carry() crea una somma di controllo da 16 bit a partire da una somma di 32 bit, secondo la RFC 791.

Verifica l'handshake TCP

Il filtro stabilisce correttamente una connessione con netcat, ignorando l'ACK finale, a cui Linux rispondeva con un pacchetto RST, poiché lo stack di rete non riceveva il SYN — era stato trasformato in SYNACK e rimandato indietro — e, dal punto di vista del sistema operativo, il pacchetto è arrivato, non appartenente a connessioni aperte.

$ sudo ip netns exec xdp-test   nc -nv 192.0.2.1 6666
192.0.2.1 6666: Connessione ripristinata dal peer

È importante controllare usando applicazioni complete e osservare tcpdump con xdp-remote perché, ad esempio, hping3 non risponde a somme di controllo non corrette.

Dal punto di vista di XDP, la verifica stessa è banale. L'algoritmo di calcolo è primitivo e probabilmente vulnerabile a un attaccante sofisticato. Il kernel Linux, ad esempio, utilizza SipHash crittografico, ma la sua implementazione per XDP esula chiaramente dall'argomento di questo articolo.

Sono emersi nuovi TODO legati all'interazione esterna:

  • Il programma XDP non può memorizzare cookie_seed (parte segreta del sale) in una variabile globale, è necessario uno storage nel nucleo, il cui valore sarà aggiornato periodicamente da un generatore affidabile.

  • In caso di corrispondenza del SYN cookie nel pacchetto ACK, non bisogna stampare un messaggio, ma memorizzare l'IP del cliente verificato, così da far passare i pacchetti da lui.

Verifica da parte di un cliente legittimo:

$ sudo ip netns exec xdp-test   nc -nv 192.0.2.1 6666
192.0.2.1 6666: Connessione reimpostata dal peer

Nei log è stata registrata la verifica (flags=0x2 — questo è un SYN, flags=0x10 — questo è un ACK):

Ether(proto=0x800)
  IP(src=0x20e6e11a dst=0x20e6e11e proto=6)
    TCP(sport=50836 dport=6666 flags=0x2)
Ether(proto=0x800)
  IP(src=0xfe2cb11a dst=0xfe2cb11e proto=6)
    TCP(sport=50836 dport=6666 flags=0x10)
      cookie corrisponde al cliente 20200c0

Attualmente non ci sono liste di IP verificati, quindi non ci sarà protezione contro il SYN flood stesso, ma ecco la reazione a un ACK flood, lanciato con questo comando:

sudo ip netns exec xdp-test   hping3 --flood -A -s 1111 -p 2222 192.0.2.1

Registrazioni nel log:

Ether(proto=0x800)
  IP(src=0x15bd11a dst=0x15bd11e proto=6)
    TCP(sport=3236 dport=2222 flags=0x10)
      cookie non corrisponde

Conclusione

A volte, eBPF in generale e XDP in particolare vengono percepiti più come uno strumento per amministratori esperti piuttosto che come una piattaforma di sviluppo. Infatti, XDP è uno strumento di intervento nel trattamento dei pacchetti da parte del kernel, e non un'alternativa allo stack del kernel come DPDK e altre opzioni di bypass del kernel. D'altra parte, XDP permette di implementare logiche piuttosto complesse che, tra l'altro, possono essere facilmente aggiornate senza interruzioni nel trattamento del traffico. Il verificatore non crea grandi problemi; personalmente, non rinuncerei a qualcosa del genere per le parti del codice in userspace.

Nella seconda parte, se l'argomento è interessante, completeremo la tabella dei clienti verificati e delle interruzioni di connessione, implementeremo contatori e scriveremo un'utilità in userspace per gestire il filtro.

Link:

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster