Scriviamo protezione contro gli attacchi DDoS su XDP. Parte nucleare

La tecnologia eXpress Data Path (XDP) consente l'elaborazione arbitraria del traffico sulle interfacce Linux prima che i pacchetti entrino nello stack di rete del kernel. Applicazione di XDP: protezione dagli attacchi DDoS (CloudFlare), filtri complessi, raccolta di statistiche (Netflix). I programmi XDP vengono eseguiti dalla macchina virtuale eBPF e pertanto hanno restrizioni sia sul loro codice che sulle funzioni del kernel disponibili, a seconda del tipo di filtro.

L'articolo ha lo scopo di compensare le carenze di numerosi materiali su XDP. Innanzitutto, forniscono codice già pronto che aggira immediatamente le funzionalità di XDP: preparato per la verifica o troppo semplice per causare problemi. Quando provi a scrivere il tuo codice da zero in un secondo momento, non capisci cosa fare con gli errori tipici. In secondo luogo, non copre i modi per testare localmente XDP senza una VM e hardware, nonostante abbiano le loro insidie. Il testo è destinato a programmatori che hanno familiarità con le reti e Linux e sono interessati a XDP e eBPF.

In questa parte, capiremo in dettaglio come viene assemblato il filtro XDP e come testarlo, quindi scriveremo una versione semplice del noto meccanismo dei cookie SYN a livello di elaborazione dei pacchetti. Finché non formiamo una "lista bianca"
clienti verificati, mantenere i contatori e gestire il filtro - registri sufficienti.

Scriveremo in C: questo non è di moda, ma pratico. Tutto il codice è disponibile su GitHub al link in fondo ed è suddiviso in commit secondo i passaggi descritti nell'articolo.

Esclusione di responsabilità. Nel corso dell'articolo verrà sviluppata una mini-soluzione per respingere gli attacchi DDoS, perché questo è un compito realistico per XDP e la mia area. Tuttavia, l'obiettivo principale è comprendere la tecnologia, questa non è una guida per creare una protezione già pronta. Il codice del tutorial non è ottimizzato e omette alcune sfumature.

Una breve panoramica di XDP

Indicherò solo i punti chiave per non duplicare la documentazione e gli articoli esistenti.

Quindi, il codice del filtro viene caricato nel kernel. Il filtro passa i pacchetti in entrata. Di conseguenza, il filtro deve prendere una decisione: passare il pacchetto al kernel (XDP_PASS), lascia cadere il pacchetto (XDP_DROP) o inviarlo indietro (XDP_TX). Il filtro può cambiare il pacchetto, questo è particolarmente vero per XDP_TX. Puoi anche mandare in crash il programma (XDP_ABORTED) e rilasciare il pacchetto, ma questo è analogo assert(0) - per il debug.

La macchina virtuale eBPF (extended Berkley Packet Filter) è volutamente semplificata in modo che il kernel possa verificare che il codice non vada in loop e non danneggi la memoria di altre persone. Restrizioni e controlli cumulativi:

  • I loop (salti indietro) sono vietati.
  • C'è uno stack per i dati, ma nessuna funzione (tutte le funzioni C devono essere incorporate).
  • Gli accessi alla memoria al di fuori dello stack e del buffer dei pacchetti sono proibiti.
  • La dimensione del codice è limitata, ma in pratica non è molto significativa.
  • Sono consentite solo funzioni speciali del kernel (helper eBPF).

Lo sviluppo e l'installazione di un filtro si presenta così:

  1. codice sorgente (es. kernel.c) compila per obiettare (kernel.o) per l'architettura della macchina virtuale eBPF. A partire da ottobre 2019, la compilazione in eBPF è supportata da Clang e promessa in GCC 10.1.
  2. Se in questo codice oggetto ci sono chiamate alle strutture del kernel (ad esempio, a tabelle e contatori), al posto dei loro ID ci sono zeri, cioè tale codice non può essere eseguito. Prima di caricare nel kernel, questi zeri devono essere sostituiti con gli ID di oggetti specifici creati tramite chiamate al kernel (link al codice). Puoi farlo con utilità esterne o puoi scrivere un programma che collegherà e caricherà un filtro specifico.
  3. Il kernel verifica il programma caricato. Controlla l'assenza di cicli e la mancata uscita del pacchetto e dei limiti dello stack. Se il verificatore non può dimostrare che il codice è corretto, il programma viene rifiutato: bisogna essere in grado di accontentarlo.
  4. Dopo una verifica riuscita, il kernel compila il codice oggetto dell'architettura eBPF nel codice macchina dell'architettura di sistema (just-in-time).
  5. Il programma è collegato all'interfaccia e avvia l'elaborazione dei pacchetti.

Poiché XDP viene eseguito nel kernel, il debugging si basa sui log di traccia e, di fatto, sui pacchetti che il programma filtra o genera. Tuttavia, eBPF mantiene il codice scaricato al sicuro per il sistema, quindi puoi sperimentare XDP direttamente sul tuo Linux locale.

Preparare l'ambiente

montaggio

Clang non può emettere direttamente il codice oggetto per l'architettura eBPF, quindi il processo consiste in due fasi:

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

Quando si scrive un filtro, torneranno utili un paio di file con funzioni ausiliarie e macro dai test del kernel. È importante che corrispondano alla versione del kernel (KVER). Scaricali 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 delle intestazioni del kernel, ARCH - Architettura di sistema. I percorsi e gli strumenti possono variare leggermente tra le distribuzioni.

Esempio di differenza 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 includere una directory con intestazioni ausiliarie e diverse directory con intestazioni del kernel. Simbolo __KERNEL__ significa che le intestazioni UAPI (userspace API) sono definite per il codice del kernel, poiché il filtro viene eseguito nel kernel.

La protezione dello stack può essere disabilitata (-fno-stack-protector) perché il verificatore del codice eBPF controlla comunque i limiti dello stack non fuori. Dovresti abilitare immediatamente le ottimizzazioni, perché la dimensione del bytecode eBPF è limitata.

Iniziamo con un filtro che passa 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";

Squadra make raccoglie xdp_filter.o. Dove puoi provarlo ora?

banco di prova

Lo stand dovrebbe includere due interfacce: su cui ci sarà un filtro e da cui verranno inviati i pacchetti. Questi devono essere dispositivi Linux completi con i propri IP per verificare come funzionano le normali applicazioni con il nostro filtro.

Dispositivi come veth (virtual Ethernet) sono adatti a noi: sono una coppia di interfacce di rete virtuali “connesse” direttamente tra loro. Puoi crearli in questo modo (in questa sezione, tutti i comandi ip eseguito da root):

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

Qui xdp-remote и xdp-local — nomi dei dispositivi. SU xdp-local (192.0.2.1/24) sarà collegato un filtro, con xdp-remote (192.0.2.2/24) verrà inviato 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. Puoi risolverlo con regole complicate iptables, ma dovranno cambiare i pacchetti, il che è scomodo durante il debug. È preferibile utilizzare gli spazi dei nomi di rete (spazi dei nomi di rete, ulteriori netns).

Lo spazio dei nomi di rete contiene un insieme di interfacce, tabelle di instradamento e regole NetFilter che sono isolate da oggetti simili in altre netn. Ogni processo viene eseguito in uno spazio dei nomi e sono disponibili solo gli oggetti di questa rete. Per impostazione predefinita, il sistema ha un unico spazio dei nomi di rete per tutti gli oggetti, quindi puoi lavorare su Linux e non conoscere netns.

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

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

Quindi il processo in esecuzione xdp-test, non "vedrà" xdp-local (rimarrà in netns per impostazione predefinita) e quando si invia un pacchetto a 192.0.2.1 lo passerà attraverso xdp-remote, perché questa è l'unica interfaccia in 192.0.2.0/24 disponibile per questo processo. Questo funziona anche al contrario.

Quando ci si sposta tra netns, l'interfaccia si interrompe e perde l'indirizzo. Per configurare un'interfaccia in netns, devi eseguire ip ... in questo spazio dei nomi dei comandi 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 puoi vedere, questo non è diverso dall'impostazione 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 corri tcpdump -tnevi xdp-local, puoi vedere che i pacchetti inviati da xdp-test, vengono inviati a questa interfaccia:

ip netns exec xdp-test   ping 192.0.2.1

È conveniente eseguire una shell xdp-test. Il repository ha uno script che automatizza il lavoro con lo stand, ad esempio, puoi impostare lo stand con il comando sudo ./stand up e rimuoverlo sudo ./stand down.

tracciare

Il filtro è 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 ne è già collegato un altro. "Nessuna notizia è una buona notizia" non riguarda questo comando, l'output è comunque voluminoso. indicare verbose facoltativo, ma con esso viene visualizzato un rapporto sul lavoro del verificatore di codice con l'elenco dell'assemblatore:

Verifier analysis:

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

Scollegare il programma dall'interfaccia:

ip link set dev xdp-local xdp off

Nello script, questi sono i comandi sudo ./stand attach и sudo ./stand detach.

Associando il filtro, puoi assicurartene ping continua a funzionare, ma il programma funziona? Aggiungiamo i loghi. Funzione bpf_trace_printk() simile a printf(), ma supporta solo fino a tre argomenti diversi dal modello e un elenco limitato di specificatori. Macro bpf_printk() semplifica la chiamata.

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

L'output va al canale di traccia del kernel, che deve essere abilitato:

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

Visualizza il flusso di messaggi:

cat /sys/kernel/debug/tracing/trace_pipe

Entrambe queste squadre fanno una chiamata sudo ./stand log.

Ping ora dovrebbe produrre messaggi come questo:

<...>-110930 [004] ..s1 78803.244967: 0: got packet: 00000000ac510377

Se osservi attentamente l'output del verificatore, puoi notare strani calcoli:

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 fatto è 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 VM:

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

Per questo motivo, l'output di debug gonfia notevolmente il codice risultante.

Invio di pacchetti XDP

Cambiamo il filtro: lascia che rispedisca tutti i pacchetti in arrivo. Questo non è corretto dal punto di vista della rete, poiché sarebbe necessario modificare gli indirizzi nelle intestazioni, ma ora il lavoro in linea di principio è importante.

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

Lanciamo tcpdump su xdp-remote. Dovrebbe mostrare la stessa ICMP Echo Request in uscita e in entrata e smettere di mostrare ICMP Echo Reply. Ma non si vede. Risulta funzionare XDP_TX nel programma per xdp-local necessarioper accoppiare l'interfaccia xdp-remote è stato anche assegnato un programma, anche se vuoto, ed è stato sollevato.

Come lo sapevo?

Tracciare il percorso di un pacchetto nel kernel il meccanismo degli eventi perf consente, tra l'altro, di utilizzare la stessa macchina virtuale, ovvero eBPF viene utilizzato per il disassemblaggio con eBPF.

Devi ricavare il bene dal male, perché non c'è nient'altro da ricavarne.

$ 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])
                                     <...>

Cos'è il codice 6?

$ errno 6
ENXIO 6 No such device or address

Funzione veth_xdp_flush_bq() ottiene il codice di errore da veth_xdp_xmit(), dove cercare per ENXIO e trova un commento.

Ripristina il filtro minimo (XDP_PASS) nel fascicolo xdp_dummy.c, aggiungilo al Makefile, bind to 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), length 98: (tos 0x0, ttl 64, id 13762, offset 0, flags [DF], proto ICMP (1), length 84)
    192.0.2.2 > 192.0.2.1: ICMP echo request, id 46966, seq 1, length 64
62:57:8e:70:44:64 > 26:0e:25:37:8f:96, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 64, id 13762, offset 0, flags [DF], proto ICMP (1), length 84)
    192.0.2.2 > 192.0.2.1: ICMP echo request, id 46966, seq 1, length 64

Se invece viene visualizzato solo ARP, è necessario rimuovere i filtri (questo rende sudo ./stand detach), permettere ping, quindi installa i filtri e riprova. Il problema è che il filtro XDP_TX influisce anche su ARP e se lo stack
spazi dei nomi xdp-test riuscito a "dimenticare" l'indirizzo MAC 192.0.2.1, non sarà in grado di risolvere questo IP.

Formulazione del problema

Passiamo all'attività dichiarata: scrivere un meccanismo di cookie SYN su XDP.

Fino ad ora, il SYN flood rimane un popolare attacco DDoS, la cui essenza è la seguente. Quando viene stabilita una connessione (TCP handshake), il server riceve un SYN, alloca le risorse per una connessione futura, risponde con un pacchetto SYNACK e attende un ACK. L'attaccante invia semplicemente pacchetti SYN da indirizzi falsi per un importo di migliaia al secondo da ciascun host in una multi-migliaia di botnet. Il server è costretto ad allocare le risorse immediatamente all'arrivo del pacchetto, ma lo rilascia dopo un lungo timeout, di conseguenza la memoria oi limiti sono esauriti, non vengono accettate nuove connessioni, il servizio non è disponibile.

Se non allocate risorse sul pacchetto SYN, ma rispondete solo con un pacchetto SYNACK, allora come può il server capire che il pacchetto ACK arrivato successivamente appartiene al pacchetto SYN che non è stato salvato? Dopotutto, un utente malintenzionato può anche generare falsi ACK. L'essenza del cookie SYN è codificare seqnum parametri di connessione come hash di indirizzi, porte e sale modificabili. Se l'ACK è riuscito ad arrivare prima del cambio di sale, puoi calcolare nuovamente l'hash e confrontarlo con acknum. impostore acknum l'attaccante non può, poiché il sale include il segreto e non avrà il tempo di esaminarlo a causa del canale limitato.

I cookie SYN sono stati implementati nel kernel Linux per molto tempo e possono anche essere abilitati automaticamente se i SYN arrivano troppo velocemente e in massa.

Programma didattico sull'handshake TCP

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

  • La combinazione di flag definisce il ruolo di un particolare pacchetto. Il flag SYN indica che questo è il primo pacchetto del mittente sulla connessione. Il flag ACK indica che il mittente ha ricevuto tutti i dati di connessione fino a un byte. acknum. Un pacchetto può avere diversi flag e prende il nome dalla loro combinazione, ad esempio un pacchetto SYNACK.

  • Il numero di sequenza (seqnum) specifica l'offset nel flusso di dati per il primo byte inviato in questo pacchetto. Ad esempio, se nel primo pacchetto con X byte di dati questo numero era N, nel pacchetto successivo con nuovi dati sarà N+X. All'inizio della connessione, ciascuna parte sceglie questo numero a caso.

  • Numero di riconoscimento (acknum): lo stesso offset di seqnum, ma non determina il numero del byte trasmesso, ma il numero del primo byte dal destinatario, che il mittente non ha visto.

All'inizio della connessione, le parti devono concordare seqnum и acknum. Il client invia un pacchetto SYN con il suo seqnum = X. Il server risponde con un pacchetto SYNACK, dove scrive il proprio seqnum = Y ed espone acknum = X + 1. Il client risponde a SYNACK con un pacchetto ACK, dove seqnum = X + 1, acknum = Y + 1. Successivamente, inizia l'effettivo trasferimento dei dati.

Se l'interlocutore non conferma la ricezione del pacchetto, TCP lo rispedisce per timeout.

Perché i cookie SYN non vengono sempre utilizzati?

Innanzitutto, se un SYNACK o un ACK viene perso, dovrai attendere un nuovo invio: la creazione della connessione rallenta. In secondo luogo, nel pacchetto SYN - e solo in esso! - vengono trasmesse una serie di opzioni che influiscono sull'ulteriore funzionamento della connessione. Non ricordando i pacchetti SYN in arrivo, il server quindi ignora queste opzioni, nei pacchetti successivi il client non li invierà più. TCP può funzionare in questo caso, ma almeno nella fase iniziale la qualità della connessione diminuirà.

In termini di pacchetti, un programma XDP dovrebbe fare quanto segue:

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

Pseudocodice dell'algoritmo insieme all'analisi dei pacchetti:

Если это не Ethernet,
    пропустить пакет.
Если это не IPv4,
    пропустить пакет.
Если адрес в таблице проверенных,               (*)
        уменьшить счетчик оставшихся проверок,
        пропустить пакет.
Если это не TCP,
    сбросить пакет.     (**)
Если это SYN,
    ответить SYN-ACK с cookie.
Если это ACK,
    если в acknum лежит не cookie,
        сбросить пакет.
    Занести в таблицу адрес с N оставшихся проверок.    (*)
    Ответить RST.   (**)
В остальных случаях сбросить пакет.

Uno (*) sono contrassegnati i punti in cui è necessario gestire lo stato del sistema: nella prima fase è possibile farne a meno semplicemente implementando un handshake TCP con la generazione di un cookie SYN come seqnum.

Sul posto (**), mentre non abbiamo un tavolo, salteremo il pacchetto.

Implementazione dell'handshake TCP

Analisi dei pacchetti e verifica del codice

Abbiamo bisogno di strutture di intestazione di rete: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) e TCP (uapi/linux/tcp.h). L'ultimo non sono riuscito a connettermi a causa di errori relativi a atomic64_t, ho dovuto copiare le definizioni necessarie nel codice.

Tutte le funzioni che si distinguono in C per la leggibilità devono essere inline nel sito di chiamata, poiché il verificatore eBPF nel kernel vieta i salti all'indietro, ovvero, di fatto, i loop e le chiamate di funzione.

#define INTERNAL static __attribute__((always_inline))

Макрос LOG() disabilita la stampa in una build di rilascio.

Il programma è una pipeline di funzioni. Ciascuno riceve un pacchetto in cui è evidenziata un'intestazione del livello corrispondente, ad esempio, process_ether() in attesa di essere riempito ether. Sulla base dei risultati dell'analisi sul campo, la funzione può trasferire il pacchetto a un livello superiore. Il risultato della funzione è un'azione XDP. Mentre i gestori SYN e ACK lasciano passare 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; /* malformed packet */
    }

    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);
}

Presto attenzione ai controlli contrassegnati con A e B. Se commenti A, il programma verrà compilato, ma ci sarà un errore di verifica durante il caricamento:

Verifier analysis:

<...>
11: (7b) *(u64 *)(r10 -48) = r1
12: (71) r3 = *(u8 *)(r7 +13)
invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0)
R7 offset is outside of the packet
processed 11 insns (limit 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0

Error fetching program/map!

Stringa chiave invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0): ci sono percorsi di esecuzione quando il tredicesimo byte dall'inizio del buffer è fuori dal pacchetto. È difficile dire dall'elenco di quale riga stiamo parlando, ma c'è un numero di istruzione (12) e un disassemblatore che mostra le righe del codice sorgente:

llvm-objdump -S xdp_filter.o | less

In questo caso, punta alla linea

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

il che chiarisce che il problema è ether. Sarebbe sempre così.

Rispondi a SYN

L'obiettivo in questa fase è generare un pacchetto SYNACK corretto con un'estensione fissa seqnum, che in futuro sarà sostituito dal cookie SYN. Tutti i cambiamenti avvengono in process_tcp_syn() e dintorni.

Controllo del pacco

Stranamente, ecco la riga più notevole, o meglio, un 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 verificatore di cui c'era una differenza tra data_end и (const void*)ctx->data_end. Al momento della scrittura, il kernel 5.3.1 non presentava questo problema. Forse il compilatore stava accedendo a una variabile locale in modo diverso rispetto a un campo. Morale: su un grande annidamento, semplificare il codice può aiutare.

Ulteriori controlli di routine delle lunghezze per la gloria del verificatore; O MAX_CSUM_BYTES qui di seguito.

const u32 ip_len = ip->ihl * 4;
if ((void*)ip + ip_len > data_end) {
    return XDP_DROP; /* malformed packet */
}
if (ip_len > MAX_CSUM_BYTES) {
    return XDP_ABORTED; /* implementation limitation */
}

const u32 tcp_len = tcp->doff * 4;
if ((void*)tcp + tcp_len > (void*)ctx->data_end) {
    return XDP_DROP; /* malformed packet */
}
if (tcp_len > MAX_CSUM_BYTES) {
    return XDP_ABORTED; /* implementation limitation */
}

Pacchetto diffuso

riempire seqnum и acknum, impostare 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;

Scambia porte TCP, indirizzi IP e MAC. La libreria standard non è disponibile dal programma XDP, quindi memcpy() — una macro che nasconde l'intrinsik di 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 checksum

I checksum IPv4 e TCP richiedono l'aggiunta di tutte le parole a 16 bit nelle intestazioni e la dimensione delle intestazioni è scritta in esse, ovvero al momento della compilazione è sconosciuta. Questo è un problema perché il verificatore non salterà il ciclo normale fino alla variabile limite. Ma la dimensione delle intestazioni è limitata: fino a 64 byte ciascuna. Puoi creare un ciclo con un numero fisso di iterazioni, che può terminare in anticipo.

prendo atto che c'è RFC 1624 su come ricalcolare parzialmente il checksum se vengono modificate solo le parole fisse dei pacchetti. Tuttavia, il metodo non è universale e l'implementazione sarebbe più difficile da mantenere.

Funzione di calcolo del checksum:

#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;
}

Sebbene size controllata dal codice chiamante, la seconda condizione di uscita è necessaria affinché il verificatore possa provare la fine del ciclo.

Per le parole a 32 bit, viene implementata una versione più semplice:

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

In realtà ricalcolando i checksum e inviando indietro il pacchetto:

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 << 8;
tcp->check = 0;
tcp_csum += sum16(tcp, tcp_len, data_end);
tcp->check = carry(tcp_csum);

return XDP_TX;

Funzione carry() crea un checksum da una somma a 32 bit di parole a 16 bit, secondo RFC 791.

Controllo dell'handshake TCP

Il filtro stabilisce correttamente una connessione con netcat, saltando l'ACK finale, a cui Linux ha risposto con un pacchetto RST, poiché lo stack di rete non ha ricevuto un SYN - è stato convertito in SYNACK e rimandato indietro - e dal punto di vista del sistema operativo è arrivato un pacchetto che non era relative alle connessioni aperte.

$ sudo ip netns exec xdp-test   nc -nv 192.0.2.1 6666
192.0.2.1 6666: Connection reset by peer

È importante verificare con applicazioni a tutti gli effetti e osservare tcpdump su xdp-remote perché, ad esempio, hping3 non risponde a checksum errati.

Dal punto di vista di XDP, il controllo stesso è banale. L'algoritmo di calcolo è primitivo e probabilmente vulnerabile a un aggressore sofisticato. Il kernel Linux, ad esempio, utilizza il SipHash crittografico, ma la sua implementazione per XDP va chiaramente oltre lo scopo di questo articolo.

Apparso per i nuovi TODO relativi all'interazione esterna:

  • Il programma XDP non può memorizzare cookie_seed (la parte segreta del sale) in una variabile globale, è necessario un archivio del kernel il cui valore verrà periodicamente aggiornato da un generatore affidabile.

  • Se il cookie SYN nel pacchetto ACK corrisponde, non è necessario stampare un messaggio, ma ricordare l'IP del client verificato per saltare ulteriormente i pacchetti da esso.

Convalida da parte di un cliente legittimo:

$ sudoip netns exec xdp-test   nc -nv 192.0.2.1 6666
192.0.2.1 6666: Connection reset by peer

I registri registravano il passaggio dell'assegno (flags=0x2 è SYN, flags=0x10 è 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 matches for client 20200c0

Finché non esiste un elenco di IP verificati, non ci sarà alcuna protezione contro il SYN flood stesso, ma ecco la reazione all'ACK flood lanciato da questo comando:

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

Voci di registro:

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

conclusione

A volte eBPF in generale e XDP in particolare sono presentati più come uno strumento di amministrazione avanzato che come una piattaforma di sviluppo. In effetti, XDP è uno strumento per interferire con l'elaborazione dei pacchetti del kernel e non un'alternativa allo stack del kernel, come DPDK e altre opzioni di bypass del kernel. D'altra parte, XDP consente di implementare una logica piuttosto complessa, che peraltro è facile da aggiornare senza interruzioni nell'elaborazione del traffico. Il verificatore non crea grossi problemi, personalmente non lo rifiuterei per parti di codice in userspace.

Nella seconda parte, se l'argomento è interessante, completeremo la tabella dei client verificati e interromperemo le connessioni, implementeremo i contatori e scriveremo un'utilità in spazio utente per gestire il filtro.

Links:

Fonte: habr.com

Aggiungi un commento