Scrivemu prutezzione contra attacchi DDoS in XDP. Parte nucleare

A tecnulugia eXpress Data Path (XDP) permette un prucessu arbitrariu di u trafficu nantu à l'interfacce Linux prima chì i pacchetti entranu in a pila di a rete di u kernel. Applicazione di XDP - prutezzione contra attacchi DDoS (CloudFlare), filtri cumplessi, cullizzioni di statistiche (Netflix). I prugrammi XDP sò eseguiti da a macchina virtuale eBPF, è per quessa anu restrizioni à u so codice è à e funzioni di kernel dispunibili, secondu u tipu di filtru.

L'articulu hè destinatu à cumpensà i difetti di numerosi materiali nantu à XDP. Prima, furnisce un codice prontu chì sguassate immediatamente e funziunalità di XDP: preparatu per a verificazione o troppu simplice per causà prublemi. Quandu pruvate à scrive u vostru propiu codice da zero dopu, ùn ci hè micca capitu di ciò chì fà cù l'errori tipici. Siconda, ùn copre micca e manere di pruvà XDP in u locu senza una VM è hardware, malgradu u fattu chì anu i so propri trappule. U testu hè destinatu à i programatori familiarizati cù e rete è Linux chì anu interessatu in XDP è eBPF.

In questa parte, avemu da capisce in dettu cumu u filtru XDP hè assemblatu è cumu per pruvà, allora scriveremu una versione simplice di u famosu mecanismu di cookies SYN à u nivellu di trasfurmazioni di pacchetti. Finu à fà una "lista bianca"
clienti verificati, mantene i contatori è gestisce u filtru - abbastanza logs.

Scriveremu in C - questu ùn hè micca di moda, ma praticu. Tuttu u codice hè dispunibule nantu à GitHub à u ligame à a fine è hè divisu in commits secondu i passi descritti in l'articulu.

Disclaimer. In u cursu di l'articulu, una mini-soluzione per repulsà l'attacchi DDoS serà sviluppata, perchè questu hè un compitu realisticu per XDP è a mo zona. In ogni casu, u scopu principale hè di capisce a tecnulugia, questu ùn hè micca una guida per creà una prutezzione pronta. U codice tutoriale ùn hè micca ottimizatu è omette alcune sfumature.

Una breve panoramica di XDP

Diteraghju solu i punti chjave per ùn duplicà a documentazione è l'articuli esistenti.

Dunque, u codice di filtru hè caricatu in u kernel. U filtru hè passatu i pacchetti entranti. In u risultatu, u filtru deve piglià una decisione: passà u pacchettu à u kernel (XDP_PASS), drop packet (XDP_DROP) o rinviallu (XDP_TX). U filtru pò cambià u pacchettu, questu hè soprattuttu veru per XDP_TX. Pudete ancu crash u prugramma (XDP_ABORTED) è abbandunà u pacchettu, ma questu hè analogu assert(0) - per debugging.

A macchina virtuale eBPF (extended Berkley Packet Filter) hè deliberatamente simplificata in modu chì u kernel pò verificà chì u codice ùn hè micca loop è ùn dannu micca a memoria di l'altri. Restrizioni cumulative è cuntrolli:

  • I loops (salti in daretu) sò pruibiti.
  • Ci hè una pila per i dati, ma nisuna funzione (tutte e funzioni C devenu esse inlineate).
  • L'accessi à a memoria fora di a pila è u buffer di pacchetti sò pruibiti.
  • A dimensione di u codice hè limitata, ma in pratica ùn hè micca assai significativu.
  • Solu e funzioni di kernel speciale (eBPF helpers) sò permesse.

Sviluppà è installà un filtru s'assumiglia cusì:

  1. codice fonte (es. kernel.c) compile à l'oggettu (kernel.o) per l'architettura di a macchina virtuale eBPF. Da ottobre 2019, a compilazione à eBPF hè supportata da Clang è prumessa in GCC 10.1.
  2. Se in questu codice d'ughjettu ci sò chjamati à strutture di kernel (per esempiu, à tavule è cuntatori), invece di i so ID ci sò zeri, vale à dì, tali codice ùn pò esse eseguitu. Prima di carricà in u kernel, sti zeri devenu esse rimpiazzati cù l'ID di l'uggetti specifichi creati per e chjama di u kernel (ligà u codice). Pudete fà questu cù utilità esterne, o pudete scrive un prugramma chì ligame è carricà un filtru specificu.
  3. U kernel verifica u prugramma chì hè caricatu. Verifica l'absenza di ciculi è a non uscita di u pacchettu è i limiti di stack. Se u verificatore ùn pò micca pruvucà chì u codice hè currettu, u prugramma hè rifiutatu - unu deve esse capace di piacè.
  4. Dopu a verificazione successu, u kernel compile u codice di l'ughjettu di l'architettura eBPF in u codice di a macchina di l'architettura di u sistema (just-in-time).
  5. U prugramma hè attaccatu à l'interfaccia è principia à trattà i pacchetti.

Siccomu XDP corre in u kernel, u debugging hè basatu annantu à i logs di traccia è, in fattu, nantu à i pacchetti chì u prugramma filtra o genera. Tuttavia, eBPF mantene u codice scaricatu sicuru per u sistema, cusì pudete sperimentà cù XDP ghjustu in u vostru Linux locale.

A preparazione di l'ambiente

Assemblea

Clang ùn pò micca emette direttamente codice d'ughjettu per l'architettura eBPF, cusì u prucessu hè custituitu di dui passi:

  1. Cumpilà u codice C à u bytecode LLVM (clang -emit-llvm).
  2. Cunvertisce bytecode in codice d'ughjettu eBPF (llc -march=bpf -filetype=obj).

Quandu scrivite un filtru, un coppiu di schedari cù funzioni ausiliarii è macros seranu utili da i testi di kernel. Hè impurtante chì currispondenu à a versione di u kernel (KVER). Scaricali à 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 cuntene u percorsu à l'intestazione di u kernel, ARCH - l'architettura di u sistema. I camini è l'arnesi pò varià ligeramente trà e distribuzioni.

Esempiu 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 includenu un repertoriu cù intestazioni ausiliarii è parechji cartulari cù intestazioni di kernel. Simbulu __KERNEL__ significa chì i headers UAPI (userspace API) sò definiti per u codice di u kernel, postu chì u filtru hè eseguitu in u kernel.

A prutezzione di stack pò esse disattivata (-fno-stack-protector) perchè u verificatore di codice eBPF verifica in ogni modu chì i limiti di stack ùn sò micca fora. Avete da attivà immediatamente ottimisazioni, perchè a dimensione di u bytecode eBPF hè limitata.

Cuminciamu cù un filtru chì passa tutti i pacchetti è ùn faci nunda:

#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 racoglie xdp_filter.o. Induve pudete pruvà avà?

banc d'essai

U stand duveria include dui interfacce: nantu à quale ci sarà un filtru è da quale i pacchetti seranu mandati. Quessi deve esse dispositi Linux cumpleti cù u so propiu IP per verificà cumu l'applicazioni regulari funzionanu cù u nostru filtru.

Dispositivi cum'è veth (Ethernet virtuale) sò adattati per noi: sò un paru di interfacce di rete virtuale "cunnessi" direttamente à l'altri. Pudete creà cusì (in questa sezione, tutti i cumandamenti ip realizatu da root):

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

xdp-remote и xdp-local - nomi di dispusitivi. On xdp-local (192.0.2.1/24) un filtru sarà attaccatu, cù xdp-remote (192.0.2.2/24) u trafficu entrante serà mandatu. Tuttavia, ci hè un prublema: l'interfacce sò nantu à a listessa macchina, è Linux ùn mandarà micca u trafficu à unu di elli per l'altru. Puderete risolve cù e regule difficili iptables, ma anu da cambià i pacchetti, chì hè inconveniente quandu u debugging. Hè megliu aduprà spazii di nomi di rete (spazi di nomi di rete, più netns).

U spaziu di nomi di a rete cuntene un inseme di interfacce, tabelle di routing è regule NetFilter chì sò isolati da oggetti simili in altri netns. Ogni prucessu scorri in qualchi namespace, è solu l'uggetti di stu netns sò dispunibuli. Per automaticamente, u sistema hà un unicu spaziu di nomi di rete per tutti l'uggetti, perchè pudete travaglià in Linux è ùn sapete micca di netns.

Creemu un novu spaziu di nomi xdp-test è si move quì xdp-remote.

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

Allora u prucessu in esecuzione xdp-test, ùn "vedrà" xdp-local (rimanerà in netns per difettu) è quandu invià un pacchettu à 192.0.2.1 u passerà. xdp-remote, perchè questu hè l'unica interfaccia à 192.0.2.0/24 dispunibule per stu prucessu. Questu travaglia ancu in reverse.

Quandu si move trà netns, l'interfaccia scende è perde l'indirizzu. Per stallà una interfaccia in netns, avete bisognu di eseguisce ip ... in questu spaziu di nomi di cumanda 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

Comu pudete vede, questu ùn hè micca sfarente di u paràmetru xdp-local in u spaziu di nomi predeterminatu:

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

Se corre tcpdump -tnevi xdp-local, pudete vede chì i pacchetti mandati da xdp-test, sò mandati à sta interfaccia:

ip netns exec xdp-test   ping 192.0.2.1

Hè cunvenutu per curriri una shell in xdp-test. U repositoriu hà un script chì automatizeghja u travagliu cù u stand, per esempiu, pudete stallà u stand cù u cumandimu sudo ./stand up è sguassate sudo ./stand down.

traccia

U filtru hè attaccatu à u dispusitivu cusì:

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

Chjave -force bisognu di ligà un novu prugramma se un altru hè digià ligatu. "Nisuna nutizia hè una bona nutizia" ùn hè micca nantu à questu cumandamentu, a pruduzzioni hè voluminosa in ogni modu. indicà verbose optativu, ma cun ellu un rapportu nantu à u travagliu di u verificatore di codice cù a lista di l'assembler appare:

Verifier analysis:

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

Staccate u prugramma da l'interfaccia:

ip link set dev xdp-local xdp off

In u script, questi sò i cumandamenti sudo ./stand attach и sudo ./stand detach.

Per ubligatoriu di u filtru, pudete assicurà chì ping cuntinueghja à travaglià, ma u prugramma funziona? Aghjunghjemu i loghi. Funzione bpf_trace_printk() simile à printf(), ma solu sustene finu à trè argumenti altru ch'è u mudellu, è una lista limitata di specificatori. Macro bpf_printk() simplificà a chjama.

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

L'output va à u canale di traccia di u kernel, chì deve esse attivatu:

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

Vede u flussu di missaghju:

cat /sys/kernel/debug/tracing/trace_pipe

E duie squadre facenu una chjama sudo ./stand log.

Ping deve avà pruduce missaghji cum'è questu in questu:

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

Se guardate attentamente l'output di u verificatore, pudete vede calculi 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
<...>

U fattu hè chì i prugrammi eBPF ùn anu micca una sezione di dati, cusì l'unicu modu per codificà a stringa di formatu hè l'argumenti immediati di i cumandamenti VM:

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

Per questu mutivu, l'output di debug gonfia assai u codice resultanti.

Invià i pacchetti XDP

Cambiemu u filtru: lasciate rinvià tutti i pacchetti entranti. Questu hè incorrectu da u puntu di vista di a rete, postu chì saria necessariu di cambià l'indirizzi in l'intestazione, ma avà u travagliu in principiu hè impurtante.

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

Lanciari tcpdump nantu xdp-remote. Duverebbe mustrà a richiesta d'Echo ICMP in uscita è in entrata identica è cessà di mostrà Risposta Echo ICMP. Ma ùn mostra micca. Risulta à travaglià XDP_TX in u prugramma per xdp-local hè necessariuper accoppià l'interfaccia xdp-remote un prugramma hè statu ancu assignatu, ancu s'ellu era viotu, è hè statu risuscitatu.

Cumu sapia ?

Trace u percorsu di un pacchettu in u kernel u mecanismu di l'eventi perf permette, per via, di utilizà a stessa macchina virtuale, vale à dì, eBPF hè utilizatu per u disassemblamentu cù eBPF.

Duvete fà bè da u male, perchè ùn ci hè nunda di più per fà.

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

Cosa hè u codice 6?

$ errno 6
ENXIO 6 No such device or address

funziunava veth_xdp_flush_bq() riceve codice d'errore da veth_xdp_xmit(), induve cercate ENXIO è truvà un cumentu.

Ripristina u filtru minimu (XDP_PASS) in u schedariu xdp_dummy.c, aghjunghje à u Makefile, ligate à xdp-remote:

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

Avà tcpdump mostra ciò chì 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 solu ARP hè mostratu invece, avete bisognu di sguassà i filtri (questu face sudo ./stand detach), lascia ping, dopu installate i filtri è pruvate di novu. U prublema hè chì u filtru XDP_TX afecta ancu ARP, è se a pila
spazii di nomi xdp-test hà sappiutu di "scurdà" di l'indirizzu MAC 192.0.2.1, ùn puderà micca risolve sta IP.

Formulazione di u prublema

Passemu à u compitu dichjaratu: scrive un mecanismu di cookie SYN in XDP.

Finu à avà, l'inundazione SYN resta un attaccu DDoS populari, l'essenza di quale hè a siguenti. Quandu una cunnessione hè stabilita (TCP handshake), u servitore riceve un SYN, attribuisce risorse per una futura cunnessione, risponde cù un pacchettu SYNACK, è aspetta un ACK. L'attaccante simpricimenti manda pacchetti SYN da indirizzi falsi in quantità di millaie per seconda da ogni host in una botnet multi-mila. U servitore hè furzatu à attribuisce risorse immediatamenti à l'arrivu di u pacchettu, ma u libera dopu un longu tempu, per quessa, a memoria o i limiti sò esauriti, novi cunnessione ùn sò micca accettati, u serviziu ùn hè micca dispunibule.

Se ùn avete micca attribuite risorse nantu à u pacchettu SYN, ma solu risponde cù un pacchettu SYNACK, allora cumu u servitore capisce chì u pacchettu ACK chì hè vinutu dopu appartene à u pacchettu SYN chì ùn hè micca salvatu? Dopu tuttu, un attaccante pò ancu generà ACK falsi. L'essenza di a cookie SYN hè di codificà seqnum paràmetri di cunnessione cum'è un hash di indirizzi, porti è cambiante di sali. Se l'ACK hà sappiutu ghjunghje prima di u cambiamentu di u salinu, pudete calculà l'hash di novu è paragunate cù acknum. falsu acknum l'attaccante ùn pò micca, postu chì u salinu include u sicretu, è ùn avarà micca tempu di sorte per via di u canali limitatu.

I cookies SYN sò stati implementati in u kernel Linux per un bellu pezzu è ponu ancu esse attivati ​​automaticamente se i SYN arrivanu troppu rapidamente è in massa.

Prugramma educativu nantu à a stretta di mano TCP

TCP furnisce u trasferimentu di dati cum'è un flussu di bytes, per esempiu, e dumande HTTP sò trasmesse nantu à TCP. U flussu hè trasmessu pezzu per pezzu in pacchetti. Tutti i pacchetti TCP anu bandieri lògichi è numeri di sequenza di 32 bit:

  • A cumminazzioni di bandiere definisce u rolu di un pacchettu particulari. A bandiera SYN significa chì questu hè u primu pacchettu di u mittente nantu à a cunnessione. A bandiera ACK significa chì u mittente hà ricevutu tutte e dati di cunnessione finu à un byte. acknum. Un pacchettu pò avè parechje bandiere è hè chjamatu dopu a so cumminazione, per esempiu, un pacchettu SYNACK.

  • U numeru di sequenza (seqnum) specifica l'offset in u flussu di dati per u primu byte chì hè mandatu in stu pacchettu. Per esempiu, se in u primu pacchettu cù X bytes di dati stu numeru era N, in u prossimu pacchettu cù novi dati serà N + X. À u principiu di a cunnessione, ogni partitu sceglie stu numeru in modu aleatoriu.

  • U numeru di ricunniscenza (acknum) - u listessu offset cum'è seqnum, ma ùn determina micca u numeru di u byte trasmessu, ma u numeru di u primu byte da u destinatariu, chì u mittente ùn hà micca vistu.

À u principiu di a cunnessione, i partiti devenu accunsentì seqnum и acknum. U cliente manda un pacchettu SYN cù u so seqnum = X. U servitore risponde cù un pacchettu SYNACK, induve scrive u so propiu seqnum = Y è espone acknum = X + 1. U cliente risponde à SYNACK cù un pacchettu ACK, induve seqnum = X + 1, acknum = Y + 1. Dopu à quessa, u trasferimentu di dati vera principia.

Se l'interlocutore ùn ricunnosce micca a ricezione di u pacchettu, u TCP u rinvia per timeout.

Perchè i cookies SYN ùn sò micca sempre usati?

Prima, se un SYNACK o ACK hè persu, duverete aspittà per un resend - u stabilimentu di cunnessione rallenta. Siconda, in u pacchettu SYN - è solu in questu! - una quantità di opzioni sò trasmesse chì affettanu u funziunamentu ulteriore di a cunnessione. Ùn ricurdendu micca i pacchetti SYN in entrata, u servitore ignora cusì queste opzioni, in i pacchetti seguenti u cliente ùn li manda più. TCP pò travaglià in questu casu, ma almenu in u stadiu iniziale, a qualità di a cunnessione diminuite.

In quantu à i pacchetti, un prugramma XDP deve fà e seguenti:

  • risponde à SYN cù SYNACK cù cookie;
  • risponde à ACK cù RST (rompe a cunnessione);
  • caccià altri pacchetti.

Pseudocode di l'algoritmu cù l'analisi di pacchetti:

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

Unu (*) i punti induve avete bisognu di gestisce u statu di u sistema sò marcati - in a prima tappa, pudete fà senza elli, simpliciamente implementendu un TCP handshake cù a generazione di una cookie SYN cum'è seqnum.

In situ (**), Mentre ùn avemu micca un tavulinu, saltaremu u pacchettu.

Implementazione di handshake TCP

Analisi di u pacchettu è verificazione di codice

Avemu bisognu di strutture di header di rete: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) è TCP (uapi/linux/tcp.h). L'ultimu ùn aghju micca pussutu cunnette per errori ligati atomic64_t, aghju avutu à copià e definizioni necessarie in u codice.

Tutte e funzioni chì sò distinti in C per a leggibilità deve esse inlineate in u situ di a chjama, postu chì u verificatore eBPF in u kernel pruibisce i salti di ritornu, vale à dì, in fattu, i loops è e chjama di funzione.

#define INTERNAL static __attribute__((always_inline))

Macro LOG() disattiva a stampa in una versione di versione.

U prugramma hè un pipeline di funzioni. Ognunu riceve un pacchettu in u quale hè evidenziatu un intestazione di u livellu currispundente, per esempiu, process_ether() aspittendu à esse pienu ether. Basatu nantu à i risultati di l'analisi di u campu, a funzione pò trasfiriri u pacchettu à un livellu più altu. U risultatu di a funzione hè una azione XDP. Mentre i gestori SYN è ACK lascianu passà 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);
}

Aghju attentu à i cuntrolli marcati A è B. Se cummentate A, u prugramma hà da custruisce, ma ci sarà un errore di verificazione quandu si carica:

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 chjave invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0): ci sò camini d'esekzione quandu u tredicesimu byte da u principiu di u buffer hè fora di u pacchettu. Hè difficiuli di dì da a lista di quale linea parlemu, ma ci hè un numeru d'istruzzioni (12) è un disassembler chì mostra e linee di u codice fonte:

llvm-objdump -S xdp_filter.o | less

In questu casu, punta à a linea

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

chì rende chjaru chì u prublema hè ether. Saria sempre cusì.

Rispondi à SYN

L'ughjettu in questa tappa hè di generà un pacchettu SYNACK currettu cù un fissu seqnum, chì serà rimpiazzatu da a cookie SYN in u futuru. Tutti i cambiamenti sò fatti in process_tcp_syn() è u circondu.

Cuntrollà u pacchettu

Curiosamente, eccu a linea più rimarchevule, o piuttostu, un cumentu à questu:

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

Quandu scrivite a prima versione di u codice, u kernel 5.1 hè stata utilizata, per u verificatore di quale ci era una diffarenza trà data_end и (const void*)ctx->data_end. À u mumentu di a scrittura, u kernel 5.3.1 ùn hà micca avutu stu prublema. Forse u compilatore accede à una variabile lucale diversamente da un campu. Morale - nantu à un grande nesting, simplificà u codice pò aiutà.

Ulteriori cuntrolli di rutina di lunghezze per a gloria di u verificatore; O MAX_CSUM_BYTES quì sottu.

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 */
}

U pacchettu sparghje

Cumpigliemu seqnum и acknum, imposta ACK (SYN digià stabilitu):

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

Scambià i porti TCP, l'indirizzi IP è MAC. A biblioteca standard ùn hè micca dispunibule da u prugramma XDP, cusì memcpy() - una macro chì piatta u Clang intrinsik.

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

Recalculazione di checksum

IPv4 è TCP checksums necessitanu l'aghjunzione di tutte e parolle 16-bit in l'intestazione, è a dimensione di l'intestazione hè scritta in elli, vale à dì, à u mumentu di a compilazione hè scunnisciutu. Questu hè un prublema perchè u verificatore ùn saltarà micca u ciclu normale finu à a variabile di u cunfini. Ma a dimensione di l'intestazione hè limitata: finu à 64 bytes ognunu. Pudete fà un ciclu cù un numeru fissu di iterazioni, chì pò finisce prima.

Aghju nutatu chì ci hè RFC 1624 circa quantu à ricalculate u checksum parzialmente si solu i paroli fissi di i pacchetti sò cambiatu. Tuttavia, u metudu ùn hè micca universale, è l'implementazione seria più difficiuli di mantene.

Funzione di calculu di 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;
}

Eppuru size verificatu da u codice di chjama, a seconda cundizione di uscita hè necessaria per chì u verificatore pò pruvà a fine di u ciclu.

Per e parolle 32-bit, una versione più simplice hè implementata:

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

Attualmente ricalculendu i checksums è rinvià u pacchettu:

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;

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

Cuntrolla di handshake TCP

U filtru stabilisce currettamente una cunnessione cù netcat, saltendu l'ACK finali, à quale Linux hà rispostu cù un pacchettu RST, postu chì a pila di rete ùn hà micca ricevutu un SYN - hè stata cunvertita in SYNACK è rinviata - è da u puntu di vista di u SO, un pacchettu hè ghjuntu chì ùn era micca. in relazione cù e cunnessione aperte.

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

Hè impurtante di verificà cù l'applicazioni cumpletu è osservà tcpdump nantu xdp-remote perchè, per esempiu, hping3 ùn risponde micca à checksums sbagliati.

Da u puntu di vista di XDP, u cuntrollu stessu hè triviale. L'algoritmu di calculu hè primitivu è probabilmente vulnerabile à un attaccu sofisticatu. U kernel Linux, per esempiu, usa u SipHash criptograficu, ma a so implementazione per XDP hè chjaramente fora di u scopu di questu articulu.

Apparsu per novi TODO ligati à l'interazzione esterna:

  • U prugramma XDP ùn pò micca almacenà cookie_seed (a parte secreta di u salinu) in una variabile globale, avete bisognu di una tenda di kernel chì u valore serà aghjurnatu periodicamente da un generatore affidabile.

  • Se a cookie SYN in u pacchettu ACK currisponde, ùn avete micca bisognu di stampà un missaghju, ma ricordate l'IP di u cliente verificatu per saltà più pacchetti da ellu.

Validazione da un cliente legittimu:

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

I logs anu registratu u passaghju di u cuntrollu (flags=0x2 hè SYN, flags=0x10 hè 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

Mentre ùn ci hè micca una lista di IP verificate, ùn ci sarà micca prutezzione contru l'inundazione SYN stessu, ma quì hè a reazione à l'inundazione ACK lanciata da questu cumandamentu:

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

Entrate di log:

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

cunchiusioni

Calchì volta eBPF in generale è XDP in particulare sò presentati cum'è più un strumentu di amministratore avanzatu chè una piattaforma di sviluppu. In verità, XDP hè un strumentu per interferiscenu cù u processu di pacchetti di kernel, è micca una alternativa à a pila di kernel, cum'è DPDK è altre opzioni di bypass di kernel. Per d 'altra banda, XDP permette di implementà una logica piuttostu cumplessa, chì, in più, hè faciule d'aghjurnà senza una pausa in u processu di trafficu. U verificatore ùn crea micca grandi prublemi, personalmente ùn aghju micca ricusatu tali per parti di u codice di l'usu.

In a seconda parte, se u tema hè interessante, compie a tavola di i clienti verificati è rompe e cunnessione, implementà i cuntatori è scrive una utilità di u spaziu d'utilizatori per gestisce u filtru.

Referenze:

Source: www.habr.com

Add a comment