Tehnologia eXpress Data Path (XDP) permite procesarea personalizată a traficului pe interfețele Linux înainte ca pachetele să ajungă în stiva de rețea a nucleului. Utilizările XDP includ protecția împotriva atacurilor DDoS (CloudFlare), filtre complexe și colectarea de statistici (Netflix). Programele XDP sunt executate de o mașină virtuală eBPF, prin urmare au limitări atât în codul lor, cât și în funcțiile disponibile ale nucleului, în funcție de tipul de filtru.
Articolul își propune să completeze lacunele din numeroasele materiale despre XDP. În primul rând, acestea oferă cod gata preparat, care ocolește imediat particularitățile XDP: acesta este pregătit pentru verificare sau este prea simplu pentru a cauza probleme. Când încercați ulterior să scrieți propriul cod de la zero, nu există o înțelegere a modului de abordare a erorilor caracteristice. În al doilea rând, nu sunt discutate metodele de testare locală a XDP fără VM și hardware, având în vedere că acestea au propriile capcane. Textul este destinat programatorilor familiarizați cu rețelele și Linux, care sunt interesați de XDP și eBPF.
În această parte, vom analiza în detaliu cum se construiește un filtru XDP și cum se testează, apoi vom scrie o variantă simplă a cunoscutului mecanism SYN cookies la nivelul procesării pachetelor. Până atunci, nu vom forma „lista albă”
a clienților verificați, vom ține contoare și vom gestiona filtrul — sunt suficiente jurnalele.
Vom scrie în C — nu este o modă, dar este practic. Tot codul este disponibil pe GitHub la linkul de la final și este structurat pe commit-uri conform etapelor descrise în articol.
Declinare de responsabilitate. În cadrul articolului va fi dezvoltată o mini-soluție pentru respingerea atacurilor DDoS, deoarece aceasta este o sarcină realistă pentru XDP și domeniul meu. Totuși, principalul obiectiv este de a înțelege tehnologia, acesta nu este un ghid pentru crearea unei protecții gata făcute. Codul didactic nu este optimizat și omite unele nuanțe.
Prezentare generală a XDP
Voi prezenta doar punctele cheie, pentru a nu duplicat documentația și articolele existente.
Așadar, codul filtrului este încărcat în nucleu. Pachetele de intrare sunt transmise filtrului. În final, filtrul trebuie să decidă: să permită pachetul în nucleu (XDP_PASS), să reseteze pachetul (XDP_DROP) sau să-l trimită înapoi (XDP_TX). Filtrul poate modifica pachetul, acesta fiind un aspect deosebit de relevant pentru XDP_TX. De asemenea, este posibil să opriți programul în caz de urgență (XDP_ABORTED) și să resetați pachetul, dar aceasta este echivalentul assert(0) — pentru depanare.
Mașina virtuală eBPF (Extended Berkley Packet Filter) este proiectată să fie simplă, astfel încât nucleul să poată verifica că codul nu intră într-o buclă și nu afectează memoria altcuiva. Limitările și verificările cumulative sunt:
- Buclele (trecerile înapoi) sunt interzise.
- Există un stivă pentru date, dar nu există funcții (toate funcțiile C trebuie să fie integrate).
- Accesul la memoria din afara stivei și a buffer-ului de pachete este interzis.
- Dimensiunea codului este limitată, dar în practică acest lucru nu contează foarte mult.
- Sunt permise doar apelurile la funcțiile speciale ale nucleului (eBPF helpers).
Dezvoltarea și instalarea filtrului arată astfel:
- Codul sursă (de exemplu,
kernel.c) este compilat în cod obiect (kernel.o) pentru arhitectura mașinii virtuale eBPF. În octombrie 2019, compilarea în eBPF este suportată de Clang și promisiune pentru GCC 10.1. - Dacă în acest cod obiect există apeluri către structuri ale nucleului (de exemplu, către tabele și contoare), în loc de ID-urile lor sunt zeros, ceea ce înseamnă că nu se poate executa un astfel de cod. Înainte de a fi încărcate în nucleu, aceste zero-uri trebuie înlocuite cu ID-urile obiectelor concrete create prin apeluri către nucleu (se leagă codul). Acest lucru poate fi realizat cu utilitare externe sau poate fi scris un program care să leagă și să încarce filtrul specific.
- Nucleul verifică programul care este încărcat. Se verifică absența buclelor și să nu iasă din limitele pachetului și stivei. Dacă verificatorul nu poate dovedi că codul este corect, programul este respins — trebuie să știi să-l îmbunezi.
- După verificarea de succes, nucleul compilează codul obiect al arhitecturii eBPF în cod mașină pentru arhitectura sistemului (just-in-time).
- Programul se atașează la interfață și începe să proceseze pachete.
Deoarece XDP funcționează în nucleu, depanarea se face pe baza jurnalelor de urmărire și pe baza pachetelor pe care programul le filtrează sau le generează. Totuși, eBPF asigură securitatea codului încărcat pentru sistem, astfel încât se poate experimenta cu XDP chiar pe Linux local.
Pregătirea mediului
Compilare
Clang nu poate genera direct cod obiect pentru arhitectura eBPF, așa că procesul constă în două etape:
- Compilarea codului C în bytecode LLVM (
clang -emit-llvm). - Transformarea bytecode-ului în cod obiect eBPF (
llc -march=bpf -filetype=obj).
Când scrii filtrul, va fi util un set de fișiere cu funcții auxiliare și macrocomenzi . Este important ca acestea să corespundă versiunii nucleului (KVER). Descărcăm în 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 BASEMakefile pentru Arch Linux (nucleu 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 ./*.oKDIR conține calea către capetele nucleului, ARCH — arhitectura sistemului. Cărțile și uneltele pot varia puțin între distribuții.
Exemplu de diferențe pentru Debian 10 (nucleu 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 includ directorul cu capete auxiliare și câteva directoare cu capete de nucleu. Simbolul __KERNEL__ înseamnă că capetele UAPI (userspace API) sunt definite pentru codul nucleului, deoarece filtrul rulează în nucleu.
Protecția stivei poate fi dezactivată (-fno-stack-protector), deoarece verificatorul de cod eBPF tot verifică depășirile stivei. Este bine să activăm optimizările, deoarece dimensiunea codului byte eBPF este limitată.
Să începem cu un filtru care permite toate pachetele și nu face nimic:
#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";Comanda make colectează xdp_filter.o. Unde să-l testăm acum?
Banc de testare
Standul ar trebui să includă două interfețe: pe care va fi filtrul și de unde vor fi trimise pachetele. Acestea ar trebui să fie dispozitive Linux complete cu IP-urile lor, pentru a verifica cum funcționează aplicațiile obișnuite cu filtrul nostru.
Dispozitivele de tip veth (Ethernet virtual) ne sunt potrivite: acestea sunt o pereche de interfețe de rețea virtuale, „conectate” direct între ele. Le putem crea astfel (în această secțiune toate comenzile ip sunt executate de root):
ip link add xdp-remote type veth peer name xdp-localAici xdp-remote și xdp-local — numele dispozitivelor. Pe xdp-local (192.0.2.1/24) va fi conectat filtrul, iar xdp-remote (192.0.2.2/24) va trimite traficul de intrare. Totuși, există o problemă: interfețele sunt pe aceeași mașină, iar Linux nu va trimite traficul pe unul dintre ele prin celălalt. Putem rezolva asta cu reguli ingenioase, iptablesdar va trebui să modifice pachetele, ceea ce este incomod pentru depanare. Mai bine să folosim spațiile de nume de rețea (network namespaces, în continuare netns).
Spațiul de nume de rețea conține un set de interfețe, tabele de rutare și reguli NetFilter, izolate de obiectele similare din alte netns. Fiecare proces funcționează într-un anumit spațiu de nume, având acces doar la obiectele din acel netns. În mod implicit, sistemul are un singur spațiu de nume de rețea pentru toate obiectele, astfel că se poate lucra în Linux fără a cunoaște netns.
Să creăm un nou spațiu de nume xdp-test și să mutăm acolo xdp-remote.
ip netns add xdp-test
ip link set dev xdp-remote netns xdp-testAtunci procesul care rulează în xdp-test, nu va „vedea” xdp-local (va rămâne în netns-ul implicit) și, în momentul în care trimite un pachet către 192.0.2.1, îl va transmite prin xdp-remote, deoarece acesta este singurul interfață din 192.0.2.0/24, disponibil acestui proces. Aceasta funcționează și în direcția opusă.
Când se mută între netns, interfața este dezactivată și îi pierde adresa. Pentru a configura interfața în netns, trebuie să rulați ip ... în acest spațiu de nume executând 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 upDupă cum se poate observa, aceasta nu se deosebește de configurarea xdp-local în spațiul de nume implicit:
ip address add 192.0.2.1/24 dev xdp-local
ip link set xdp-local upDacă se rulează tcpdump -tnevi xdp-local, se poate observa că pachetele trimise din xdp-test, sunt livrate pe această interfață:
ip netns exec xdp-test ping 192.0.2.1Este util să rulați un shell în xdp-test. În repository există un script care automatizează lucrul cu standul, de exemplu, se poate configura standul cu comanda sudo ./stand up și să-l ștergeți sudo ./stand down.
Traseu
Filtrul se leagă de dispozitiv astfel:
ip -force link set dev xdp-local xdp object xdp_filter.o verboseCheia -force este necesară pentru a lega un nou program, dacă altul este deja legat. „No news is good news” nu se aplică pentru această comandă, ieșirea este voluminoasă în orice caz. Specificarea verbose nu este obligatorie, dar cu aceasta apare un raport despre activitatea verificatorului de cod cu listarea assembler-ului:
Analiza verificatorului:
0: (b7) r0 = 2
1: (95) exitDezlegarea programului de interfață:
ip link set dev xdp-local xdp offÎn script, aceste comenzi sunt sudo ./stand attach și sudo ./stand detach.
După legarea filtrului, putem verifica dacă ping continuă să funcționeze, dar funcționează programul? Să adăugăm loguri. Funcția se aseamănă cu printf(), dar acceptă doar până la trei argumente, în afară de șablon, și o listă limitată de specificatori. Macro-ul bpf_printk() simplifică apelul.
SEC("prog")
int xdp_main(struct xdp_md* ctx) {
+ bpf_printk("am primit pachet: %pn", ctx);
return XDP_PASS;
}Ieșirea se face prin canalul de trasare a nucleului, care trebuie activat:
echo -n 1 | sudo tee /sys/kernel/debug/tracing/options/trace_printkVizualizarea fluxului de mesaje:
cat /sys/kernel/debug/tracing/trace_pipeAmbele comenzi fac apel sudo ./stand log.
Ping ar trebui acum să genereze mesaje de acest tip:
-110930 [004] ..s1 78803.244967: 0: got packet: 00000000ac510377Dacă ne uităm atent la ieșirea verficatorului, putem observa calcule ciudate:
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#6Problema este că programele pe eBPF nu au secțiune de date, astfel încât singura modalitate de a codifica un șir formatat sunt argumentele imediate ale comenzilor VM:
$ python -c "import binascii; print(bytes(reversed(binascii.unhexlify('0a7025203a74656b63617020746f67'))))"
b'got packet: %pn'Din acest motiv, ieșirea de depanare umflă semnificativ codul final.
Trimiterea pachetelor XDP
Să modificăm filtrul: să permită trimiterea înapoi a tuturor pachetelor de intrare. Acest lucru este incorect din punct de vedere rețelei, deoarece ar trebui să se schimbe adresele din anteturi, dar acum este importantă funcționarea în principiu.
bpf_printk("got packet: %pn", ctx);
- return XDP_PASS;
+ return XDP_TX;
}Lansăm tcpdump pe xdp-remote. Ar trebui să afișeze ICMP Echo Request identice pentru ieșiri și intrări și să înceteze să afișeze ICMP Echo Reply. Dar nu o face. Se dovedește că, pentru funcționare XDP_TX în programul pe xdp-local , pentru ca interfața pereche xdp-remote să aibă, de asemenea, un program alocat, chiar dacă este gol, și să fie activată.
Cum am aflat asta?
permite mecanismul perf events, care folosește aceeași mașină virtuală, adică pentru dezvăluiri cu eBPF se utilizează eBPF.
Trebuie să faci bine din rău, pentru că nu există altceva din ce să îl faci.
$ 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])Ce este codul 6?
$ errno 6
ENXIO 6 Nu există un astfel de dispozitiv sau adresăFuncția veth_xdp_flush_bq() primește codul de eroare de la veth_xdp_xmit(), unde căutând ENXIO și găsim comentariul.
Să restaurăm filtrul minim (XDP_PASS) în fișierul xdp_dummy.c, să-l adăugăm în Makefile, să-l legăm la xdp-remote:
ip netns exec remote
ip link set dev int xdp object dummy.oAcum tcpdump arată ceea ce se așteaptă:
62:57:8e:70:44:64 > 26:0e:25:37:8f:96, tipul etichetei IPv4 (0x0800), lungime 98: (tos 0x0, ttl 64, id 13762, offset 0, flags [DF], proto ICMP (1), lungime 84)
192.0.2.2 > 192.0.2.1: cerere de echo ICMP, id 46966, seq 1, lungime 64
62:57:8e:70:44:64 > 26:0e:25:37:8f:96, tipul etichetei IPv4 (0x0800), lungime 98: (tos 0x0, ttl 64, id 13762, offset 0, flags [DF], proto ICMP (1), lungime 84)
192.0.2.2 > 192.0.2.1: cerere de echo ICMP, id 46966, seq 1, lungime 64Dacă în schimb se afișează doar ARP, trebuie să eliminăm filtrele (aceasta face sudo ./stand detach), să lăsăm ping, apoi să stabilim filtrele și să încercăm din nou. Problema este că filtrul XDP_TX acționează și asupra ARP-ului, iar dacă stiva
spațiilor de nume xdp-test a reușit să „uită” adresa MAC 192.0.2.1, nu va putea rezolva acest IP.
Formularea problemei
Să trecem la sarcina declarată: a scrie un mecanism SYN cookies pe XDP.
Până acum, atacul DDoS popular rămâne SYN flood, iar esența acestuia este următoarea. La stabilirea conexiunii (handshake TCP), serverul primește SYN, alocă resurse pentru viitoarea conexiune, răspunde cu un pachet SYNACK și așteaptă ACK. Atacatorul pur și simplu trimite pachete SYN din adrese false în număr de mii pe secundă de la fiecare host dintr-un botnet de mii. Serverul este nevoit să aloce resurse imediat la sosirea pachetului, iar eliberează după un timp mare, rezultând în epuizarea memoriei sau a limitelor, iar noile conexiuni nu sunt acceptate, serviciul devine inaccesibil.
Dacă nu se alocă resurse pe fiecare pachet SYN, ci doar se răspunde cu un pachet SYNACK, cum poate serverul să înțeleagă că pachetul ACK, sosit mai târziu, se referă la pachetul SYN care nu a fost salvat? Deoarece atacatorul poate genera și ACK-uri false. Esența SYN cookie este de a codifica în seqnum parametrii conexiunii ca un hash din adrese, porturi și o sare schimbătoare. Dacă ACK a reușit să vină înainte de schimbarea sării, se poate recalcula hash-ul și compara cu acknum. Un atacator nu poate falsifica, deoarece sarea include un secret, iar a o încerca nu va reuși din cauza lățimii de bandă limitate. acknum SYN cookie a fost implementat de mult în nucleul Linux și poate fi activat automat dacă SYN-urile vin prea repede și în masă.
Lecție despre TCP handshake
TCP asigură transmiterea datelor ca un flux de octeți, de exemplu, cererile HTTP sunt transmise pe TCP. Fluxul este transmis în părți în pachete. Toate pachetele TCP au steaguri logice și numere de secvență de 32 de biți:
TCP asigură transmiterea datelor ca un flux de biți, de exemplu, cererile HTTP sunt transmise prin TCP. Fluxul este transmis în bucăți, în pachete. Toate pachetele TCP au steaguri logice și numere de secvență pe 32 de biți:
Combinarea flagurilor definește rolul unui pachet specific. Flagul SYN înseamnă că acesta este primul pachet trimis de expeditor într-o conexiune. Flagul ACK înseamnă că expeditorul a primit toate datele conexiunii până la byte-ul
acknum. Pachetul poate avea mai multe flaguri și este denumit în funcție de combinația acestora, de exemplu, pachet SYNACK.Numărul de secvență (seqnum) definește offsetul în fluxul de date pentru primul byte care este transmis în acest pachet. De exemplu, dacă în primul pachet au fost X byte de date cu acest număr N, în următorul pachet cu date noi va fi N+X. La începutul conexiunii, fiecare parte își alege acest număr aleatoriu.
Numărul de confirmare (acknum) - este un offset similar cu seqnum, dar definește nu numărul byte-ului transmis, ci numărul primului byte de la destinatar, pe care expeditorul nu l-a văzut.
La începutul conexiunii, părțile trebuie să convină seqnum și acknum. Clientul trimite un pachet SYN cu seqnum = X. Serverul răspunde cu un pachet SYNACK, în care scrie seqnum = Y și setează acknum = X + 1. Clientul răspunde la SYNACK cu un pachet ACK, în care seqnum = X + 1, acknum = Y + 1. După aceasta începe efectiv transmiterea datelor.
Dacă interlocutorul nu confirmă primirea pachetului, TCP îl trimite din nou după un timeout.
De ce nu se folosesc întotdeauna cookie-urile SYN?
În primul rând, dacă SYNACK sau ACK se pierd, va trebui să așteptăm retransmiterea - se încetinește stabilirea conexiunii. În al doilea rând, în pachetul SYN - și numai în acesta! - se transmit o serie de opțiuni care influențează funcționarea ulterioară a conexiunii. Neținând minte pachetele SYN primite, serverul astfel ignoră aceste opțiuni, iar în pachetele următoare clientul nu le va mai trimite. TCP poate funcționa în continuare, dar calitatea conexiunii va scădea, cel puțin la etapa inițială.
Din punctul de vedere al pachetelor, programul XDP trebuie să facă următoarele:
- să răspundă cu SYNACK la SYN cu un cookie;
- să răspundă cu RST la ACK (a rupe conexiunea);
- să elimine celelalte pachete.
Pseudo-codul algoritmului împreună cu explicația pachetului:
Dacă nu este Ethernet,
treceți peste pachet.
Dacă nu este IPv4,
treceți peste pachet.
Dacă adresa se află în tabelul de verificare, (*)
reduceți contorul de verificări rămase,
treceți peste pachet.
Dacă nu este TCP,
resetați pachetul. (**)
Dacă este SYN,
răspundeți cu SYN-ACK și cookie.
Dacă este ACK,
dacă acknum nu conține cookie,
resetați pachetul.
Introduceți în tabel adresa cu N verificări rămase. (*)
Răspundeți cu RST. (**)
În celelalte cazuri, resetați pachetul.O una (*) marcate punctele unde trebuie să gestionăm starea sistemului – în prima etapă putem să ne descurcăm fără ele, implementând doar handshake-ul TCP cu generarea cookie-ului SYN ca seqnum.
Pe loc (**), până nu avem tabelul, vom trece peste pachet.
Implementarea handshake-ului TCP
Analiza pachetului și verificarea codului
Ne vor trebui structuri pentru antetele de rețea: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) și TCP (uapi/linux/tcp.h). Ultima nu am reușit să o conectez din cauza erorilor legate de atomic64_t, a trebuit să copiez definițiile necesare în cod.
Toate funcțiile, care în C sunt separate pentru a facilita citirea, trebuie să fie integrate acolo unde sunt apelate, deoarece verificatorul eBPF din nucleu interzice salturile înapoi, adică, de fapt, ciclurile și apelurile de funcții.
#define INTERNAL static __attribute__((always_inline))Macro LOG() dezactivează tipărirea în compilarea de producție.
Programul reprezintă un lanț de funcții. Fiecare primește un pachet, în care este alocat antetul corespunzător nivelului, de exemplu, process_ether() se așteaptă ca să fie completat ether. În urma analizei câmpurilor, funcția poate transmite pachetul la nivelul superior. Rezultatul funcției este o acțiune XDP. Până acum, manipulatoarele SYN și ACK trec toate pachetele.
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; /* pachet malițios */
}
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);
}Vă atrag atenția asupra verificărilor marcate A și B. Dacă comentăm A, programul se va compila, dar la încărcare va apărea o eroare de verificare:
Analiza verifier-ului:
11: (7b) *(u64 *)(r10 -48) = r1
12: (71) r3 = *(u8 *)(r7 +13)
acces invalid la pachet, off=13 size=1, R7(id=0,off=0,r=0)
Offset R7 este în afara pachetului
procesate 11 insns (limită 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0
Eroare la preluarea programului/mapa!Linie cheie acces invalid la pachet, off=13 size=1, R7(id=0,off=0,r=0): există căi de execuție în care cel de-al treisprezecelea byte de la începutul buffer-ului se află în afara pachetului. Din listing este destul de greu de înțeles despre ce linie este vorba, însă există numărul instrucțiunii (12) și disasembler-ul care arată liniile codului sursă:
llvm-objdump -S xdp_filter.o | lessÎn acest caz, el indică linia
LOG("Ether(proto=0x%x)", bpf_ntohs(ether->h_proto));din care se înțelege că problema constă în ether. Așa ar fi tot timpul.
Răspuns la SYN
Obiectivul în această etapă este de a forma un pachet SYNACK corect cu seqnum, care în viitor va fi înlocuit cu un SYN cookie. Toate modificările au loc în process_tcp_syn() și împrejurimi.
Verificarea pachetului
Ciudat, iată cea mai notabilă linie, mai precis, comentariul la aceasta:
/* Required to verify checksum calculation */
const void* data_end = (const void*)ctx->data_end;Când am scris prima versiune a codului, s-a folosit kernelul 5.1, pentru care verificatorul avea diferențe între data_end și (const void*)ctx->data_end. În momentul redactării articolului, kernelul 5.3.1 nu avea astfel de probleme. Poate, compilatorul accesa variabila locală altfel decât un câmp. Morala – la o adâncime mare, simplificarea codului poate ajuta.
Mai departe, verificări rutinare ale lungimilor în favoarea verificatorului; despre MAX_CSUM_BYTES mai jos.
const u32 ip_len = ip->ihl * 4;
if ((void*)ip + ip_len > data_end) {
return XDP_DROP; /* pachet malformat */
}
if (ip_len > MAX_CSUM_BYTES) {
return XDP_ABORTED; /* limitare a implementării */
}
const u32 tcp_len = tcp->doff * 4;
if ((void*)tcp + tcp_len > (void*)ctx->data_end) {
return XDP_DROP; /* pachet malformat */
}
if (tcp_len > MAX_CSUM_BYTES) {
return XDP_ABORTED; /* limitare a implementării */
}Inversarea pachetului
Umplem seqnum și acknum, setăm ACK (SYN deja setat):
const u32 cookie = 42;
tcp->ack_seq = bpf_htonl(bpf_ntohl(tcp->seq) + 1);
tcp->seq = bpf_htonl(cookie);
tcp->ack = 1;Schimbăm porturile TCP, adresa IP și adresele MAC. Biblioteca standard nu este disponibilă din programul XDP, așa că memcpy() – macro, care ascunde intrinsec 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);Recalcularea sumelor de control
Sumele de control IPv4 și TCP necesită adunarea tuturor cuvintelor de 16 biți din antete, iar dimensiunea anteturilor este scrisă în ele, adică în momentul compilării nu este cunoscută. Aceasta este o problemă, deoarece verificatorul nu va permite un ciclu obișnuit până la limita variabilă. Totuși, dimensiunea anteturilor este limitată: până la 64 de octeți fiecare. Se poate face un ciclu cu un număr fix de iterații, care poate termina anticipat.
Voi menționa că există despre cum să se recalculeze suma de control parțial, dacă au fost modificate doar cuvintele fixe ale pachetelor. Cu toate acestea, metoda nu este universală, iar implementarea ar fi mai greu de întreținut.
Funcția de calcul al sumei de control:
#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;
}Deși dimensiune a fost verificată de codul invocator, a doua condiție de ieșire este necesară pentru ca verificatorul să poată demonstra finalizarea ciclului.
Pentru cuvintele de 32 de biți a fost implementată o versiune mai simplă:
INTERNAL u32
sum16_32(u32 v) {
return (v >> 16) + (v & 0xffff);
}Recalcularea efectivă a sumelor de control și trimiterea pachetului înapoi:
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;Funcția carry() transformă suma de 32 de biți în suma de control de 16 biți, conform RFC 791.
Verificarea handshake-ului TCP
Filtrul stabilește corect conexiunea cu netcat, trecând peste ACK-ul final, la care Linux a răspuns cu un pachet RST, deoarece stiva de rețea nu a primit SYN – a fost transformat în SYNACK și trimis înapoi – și din punctul de vedere al SO-ului pachetul a sosit, neavând legătură cu conexiunile deschise.
$ sudo ip netns exec xdp-test nc -nv 192.0.2.1 6666
192.0.2.1 6666: Conexiune resetată de peerEste important să se verifice exact cu aplicații complete și să se observe tcpdump pe xdp-remote deoarece, de exemplu, hping3 nu reacționează la sumele de control incorecte.
SYN cookie
Din perspectiva XDP, verificarea în sine este trivială. Algoritmul de calcul este primitiv și, probabil, vulnerabil la un atacator avansat. Kernel-ul Linux, de exemplu, folosește SipHash criptografic, dar implementarea sa pentru XDP depășește clar sfera acestui articol.
A apărut pentru noi TODO-uri legate de interacțiunea externă:
Programul XDP nu poate stoca
cookie_seed(partea secretă a sării) într-o variabilă globală, este nevoie de un depozit în kernel, al cărui valoare va fi actualizată periodic dintr-un generator de încredere.Când se potrivește SYN cookie în pachetul ACK, nu trebuie să imprimăm un mesaj, ci să salvăm IP-ul clientului verificat, pentru a permite ulterior pachetele de la acesta.
Verificare de către clientul legitim:
$ sudo ip netns exec xdp-test nc -nv 192.0.2.1 6666
192.0.2.1 6666: Conexiune resetată de peerÎn jurnale s-a înregistrat trecerea verificării (flags=0x2 — aceasta este SYN, flags=0x10 — aceasta este 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 se potrivește pentru client 20200c0Până nu există o listă de IP-uri verificate, protecția împotriva inundației SYN nu va exista, dar iată reacția la inundația ACK, declanșată de această comandă:
sudo ip netns exec xdp-test hping3 --flood -A -s 1111 -p 2222 192.0.2.1Înregistrări în jurnal:
Ether(proto=0x800)
IP(src=0x15bd11a dst=0x15bd11e proto=6)
TCP(sport=3236 dport=2222 flags=0x10)
cookie nepotrivitConcluzie
Uneori, eBPF și, în special, XDP sunt văzute mai degrabă ca un instrument pentru administratori avansați decât ca o platformă pentru dezvoltare. Într-adevăr, XDP este un instrument de intervenție în procesarea pachetelor de către nucleu, nu o alternativă la stiva kernel, precum DPDK și alte variante de kernel bypass. Pe de altă parte, XDP permite implementarea unei logici destul de complexe, care, de asemenea, poate fi actualizată ușor fără a întrerupe procesarea traficului. Verificatorul nu creează mari probleme, personal n-aș refuza un astfel de instrument pentru părți de cod în userspace.
În partea a doua, dacă subiectul este interesant, vom completa tabelul clienților verificați și al deconectărilor, vom implementa contoare și vom scrie o unealtă în userspace pentru gestionarea filtrului.
Linkuri:
Sursa: habr.com
