Píšeme ochranu proti DDoS útokom na XDP. Jadrová časť
Technológia eXpress Data Path (XDP) umožňuje ľubovoľné spracovanie prevádzky na rozhraniach Linuxu predtým, ako pakety vstúpia do zásobníka siete jadra. Aplikácia XDP - ochrana pred DDoS útokmi (CloudFlare), komplexné filtre, zber štatistík (Netflix). Programy XDP sú vykonávané virtuálnym strojom eBPF, a preto majú obmedzenia týkajúce sa kódu aj dostupných funkcií jadra v závislosti od typu filtra.
Cieľom tohto článku je nahradiť nedostatky mnohých materiálov na XDP. Po prvé, poskytujú hotový kód, ktorý okamžite obchádza funkcie XDP: pripravený na overenie alebo príliš jednoduchý na to, aby spôsoboval problémy. Keď sa neskôr pokúsite napísať svoj vlastný kód od začiatku, nerozumiete tomu, čo robiť s typickými chybami. Po druhé, nezahŕňa spôsoby lokálneho testovania XDP bez VM a hardvéru, napriek tomu, že majú svoje vlastné úskalia. Text je určený programátorom znalým sietí a Linuxu, ktorí sa zaujímajú o XDP a eBPF.
V tejto časti podrobne pochopíme, ako sa XDP filter zostavuje a ako ho testujeme, potom napíšeme jednoduchú verziu známeho mechanizmu SYN cookies na úrovni spracovania paketov. Kým nevytvoríme „bielu listinu“
overených klientov, viesť počítadlá a spravovať filter - dostatok logov.
Budeme písať v C - to nie je módne, ale praktické. Všetok kód je dostupný na GitHub na odkaze na konci a je rozdelený do commitov podľa krokov opísaných v článku.
Disclaimer. V priebehu článku bude vyvinuté mini-riešenie na odpudzovanie DDoS útokov, pretože toto je reálna úloha pre XDP a moju oblasť. Hlavným cieľom je však pochopiť technológiu, toto nie je návod na vytvorenie hotovej ochrany. Kód tutoriálu nie je optimalizovaný a vynecháva niektoré nuansy.
Stručný prehľad XDP
Uvediem len kľúčové body, aby som neduplikoval dokumentáciu a existujúce články.
Kód filtra sa teda načíta do jadra. Filtrom prechádzajú prichádzajúce pakety. V dôsledku toho sa filter musí rozhodnúť: odovzdať paket do jadra (XDP_PASS), pustiť paket (XDP_DROP) alebo ho pošlite späť (XDP_TX). Filter môže zmeniť balenie, to platí najmä pre XDP_TX. Môžete tiež zlyhať program (XDP_ABORTED) a zhoďte balík, ale toto je analogické assert(0) - na ladenie.
Virtuálny stroj eBPF (extended Berkley Packet Filter) je zámerne jednoduchý, aby jadro mohlo skontrolovať, či sa kód nezacyklí a nepoškodí pamäť iných ľudí. Kumulatívne obmedzenia a kontroly:
Slučky (skoky späť) sú zakázané.
Existuje zásobník pre dáta, ale žiadne funkcie (všetky funkcie C musia byť vložené).
Prístupy do pamäte mimo zásobníka a vyrovnávacej pamäte paketov sú zakázané.
Veľkosť kódu je obmedzená, ale v praxi to nie je veľmi podstatné.
Povolené sú iba špeciálne funkcie jadra (pomocníci eBPF).
Vývoj a inštalácia filtra vyzerá takto:
zdrojový kód (napr. kernel.c) skompiluje do objektu (kernel.o) pre architektúru virtuálneho stroja eBPF. Od októbra 2019 je kompilácia do eBPF podporovaná spoločnosťou Clang a sľúbená v GCC 10.1.
Ak sú v tomto objektovom kóde volania štruktúr jadra (napríklad tabuliek a počítadiel), namiesto ich ID sú nuly, to znamená, že takýto kód nie je možné vykonať. Pred načítaním do jadra musia byť tieto nuly nahradené ID špecifických objektov vytvorených prostredníctvom volaní jadra (prepojiť kód). Môžete to urobiť pomocou externých nástrojov alebo môžete napísať program, ktorý prepojí a načíta konkrétny filter.
Jadro overuje načítavanie programu. Kontroluje absenciu cyklov a neopustenie hraníc balíka a zásobníka. Ak overovateľ nedokáže, že kód je správny, program je zamietnutý – človek ho musí vedieť potešiť.
Po úspešnom overení jadro skompiluje objektový kód architektúry eBPF do strojového kódu systémovej architektúry (just-in-time).
Program je pripojený k rozhraniu a začne spracovávať pakety.
Keďže XDP beží v jadre, ladenie sa vykonáva pomocou protokolov sledovania a v skutočnosti pomocou paketov, ktoré program filtruje alebo generuje. eBPF však uchováva stiahnutý kód bezpečný pre systém, takže môžete experimentovať s XDP priamo na vašom lokálnom Linuxe.
Príprava prostredia
zhromaždenia
Clang nemôže priamo vydať objektový kód pre architektúru eBPF, takže proces pozostáva z dvoch krokov:
Kompilujte kód C do bajtového kódu LLVM (clang -emit-llvm).
Previesť bajtkód na objektový kód eBPF (llc -march=bpf -filetype=obj).
Pri písaní filtra sa bude hodiť pár súborov s pomocnými funkciami a makrami z testov jadra. Je dôležité, aby sa zhodovali s verziou jadra (KVER). Stiahnite si ich do helpers/:
KDIR obsahuje cestu k hlavičkám jadra, ARCH - architektúra systému. Cesty a nástroje sa môžu medzi jednotlivými distribúciami mierne líšiť.
Príklad rozdielu pre 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 obsahuje adresár s pomocnými hlavičkami a niekoľko adresárov s hlavičkami jadra. Symbol __KERNEL__ znamená, že hlavičky UAPI (userspace API) sú definované pre kód jadra, keďže filter sa vykonáva v jadre.
Ochranu stohu je možné vypnúť (-fno-stack-protector), pretože overovač kódu eBPF aj tak kontroluje, či nie sú mimo hraníc zásobníka. Mali by ste okamžite povoliť optimalizácie, pretože veľkosť bajtkódu eBPF je obmedzená.
Začnime s filtrom, ktorý prejde všetky pakety a nerobí nič:
Tím make zbiera xdp_filter.o. Kde si to teraz môžete vyskúšať?
skúšobná stolica
Stojan by mal obsahovať dve rozhrania: na ktorom bude filter a z ktorého sa budú odosielať pakety. Musia to byť plnohodnotné linuxové zariadenia s vlastnými IP, aby bolo možné skontrolovať, ako fungujú bežné aplikácie s naším filtrom.
Zariadenia ako veth (virtuálny Ethernet) sú pre nás vhodné: ide o dvojicu virtuálnych sieťových rozhraní „prepojených“ priamo medzi sebou. Môžete ich vytvoriť takto (v tejto časti sú všetky príkazy ip vykonávané od root):
ip link add xdp-remote type veth peer name xdp-local
Tu xdp-remote и xdp-local — názvy zariadení. Zapnuté xdp-local (192.0.2.1/24) bude pripojený filter s xdp-remote (192.0.2.2/24) bude odoslaná prichádzajúca komunikácia. Je tu však problém: rozhrania sú na rovnakom počítači a Linux nebude posielať prevádzku na jedno z nich cez druhé. Môžete to vyriešiť zložitými pravidlami iptables, ale budú musieť zmeniť balíčky, čo je pri ladení nepohodlné. Je lepšie používať sieťové menné priestory (sieťové menné priestory, ďalej netns).
Priestor názvov siete obsahuje množinu rozhraní, smerovacích tabuliek a pravidiel NetFilter, ktoré sú izolované od podobných objektov v iných sieťach. Každý proces beží v nejakom mennom priestore a sú mu dostupné iba objekty týchto sietí. V predvolenom nastavení má systém jeden sieťový menný priestor pre všetky objekty, takže môžete pracovať na Linuxe a neviete o netns.
Vytvorme nový menný priestor xdp-test a presťahovať sa tam xdp-remote.
ip netns add xdp-test
ip link set dev xdp-remote netns xdp-test
Potom sa spustí proces xdp-test, neuvidí xdp-local (štandardne zostane v netns) a pri odoslaní paketu na 192.0.2.1 ho prejde xdp-remote, pretože je to jediné rozhranie na 192.0.2.0/24 dostupné pre tento proces. Toto funguje aj opačne.
Pri pohybe medzi sieťami sa rozhranie vypne a stratí adresu. Ak chcete nastaviť rozhranie v netns, musíte spustiť ip ... v tomto mennom priestore príkazu 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
Ako vidíte, toto sa nelíši od nastavenia xdp-local v predvolenom priestore názvov:
ip address add 192.0.2.1/24 dev xdp-local
ip link set xdp-local up
Ak sa spustí tcpdump -tnevi xdp-local, môžete vidieť, že pakety odoslané z xdp-test, sa doručujú do tohto rozhrania:
ip netns exec xdp-test ping 192.0.2.1
Je vhodné spustiť shell xdp-test. Úložisko má skript, ktorý automatizuje prácu so stojanom, stojan si napríklad nastavíte príkazom sudo ./stand up a odstráňte ho sudo ./stand down.
sledovanie
Filter je pripevnený k zariadeniu takto:
ip -force link set dev xdp-local xdp object xdp_filter.o verbose
kľúč -force potrebné na prepojenie nového programu, ak je už pripojený iný. "No news is good news" nie je o tomto príkaze, výstup je aj tak objemný. naznačiť verbose voliteľné, ale spolu s ním sa zobrazí správa o práci overovača kódu s výpisom assembleru:
Verifier analysis:
0: (b7) r0 = 2
1: (95) exit
Odpojte program od rozhrania:
ip link set dev xdp-local xdp off
V skripte sú to príkazy sudo ./stand attach и sudo ./stand detach.
Väzbou filtra sa o tom môžete uistiť ping naďalej funguje, ale funguje program? Pridajme logá. Funkcia bpf_trace_printk() podobný printf(), ale podporuje iba tri argumenty iné ako vzor a obmedzený zoznam špecifikátorov. Makro bpf_printk() zjednoduší hovor.
Z tohto dôvodu výstup ladenia značne nafukuje výsledný kód.
Odosielanie paketov XDP
Zmeňme filter: nech posiela všetky prichádzajúce pakety späť. Z hľadiska siete je to nesprávne, pretože by bolo potrebné zmeniť adresy v hlavičkách, ale teraz je dôležitá práca v princípe.
Spustiť tcpdump na xdp-remote. Mala by zobrazovať rovnakú odchádzajúce a prichádzajúce ICMP Echo Request a prestať zobrazovať ICMP Echo Reply. Ale neukazuje sa. Ukázalo sa, že funguje XDP_TX v programe pre xdp-localmuštspárovať rozhranie xdp-remote bol tiež priradený program, aj keď bol prázdny, a bol spustený.
ako som to vedel?
Sledovanie cesty balíka v jadre mechanizmus udalostí perf umožňuje, mimochodom, používať rovnaký virtuálny stroj, to znamená, že eBPF sa používa na demontáž s eBPF.
Zo zla musíte robiť dobro, pretože z toho nie je nič iné.
Ak sa namiesto toho zobrazuje iba ARP, musíte odstrániť filtre (to znamená sudo ./stand detach), nech ping, potom nainštalujte filtre a skúste to znova. Problém je v tom, že filter XDP_TX ovplyvňuje aj ARP a ak zásobník
menné priestory xdp-test podarilo "zabudnúť" MAC adresu 192.0.2.1, nebude vedieť vyriešiť túto IP.
Vyhlásenie o probléme
Prejdime k uvedenej úlohe: napísať mechanizmus SYN cookie na XDP.
Až doteraz zostáva SYN flood populárnym DDoS útokom, ktorého podstata je nasledovná. Keď je nadviazané spojenie (TCP handshake), server prijme SYN, pridelí zdroje pre budúce spojenie, odpovie paketom SYNACK a čaká na ACK. Útočník jednoducho posiela SYN pakety z falošných adries v množstve tisícok za sekundu z každého hostiteľa v multi-tisícovom botnete. Server je nútený alokovať zdroje ihneď po príchode paketu, ale uvoľní ho po dlhom časovom limite, následkom čoho je vyčerpaná pamäť alebo limity, nové spojenia nie sú akceptované, služba je nedostupná.
Ak nevyčleníte prostriedky na paket SYN, ale odpoviete iba paketom SYNACK, ako môže server pochopiť, že paket ACK, ktorý prišiel neskôr, patrí do paketu SYN, ktorý nebol uložený? Falošné ACK totiž dokáže generovať aj útočník. Podstatou súboru cookie SYN je zakódovať seqnum parametre pripojenia ako hash adries, portov a meniacich sa solí. Ak sa ACK podarilo doraziť pred zmenou soli, môžete znova vypočítať hash a porovnať s acknum. falošný acknum Útočník nemôže, pretože soľ obsahuje tajomstvo, a nebude mať čas ho pretriediť kvôli obmedzenému kanálu.
SYN cookies sú v linuxovom jadre implementované už dlhú dobu a môžu byť dokonca automaticky povolené, ak SYN prichádzajú príliš rýchlo a hromadne.
Vzdelávací program na TCP handshake
TCP poskytuje prenos údajov ako prúd bajtov, napríklad požiadavky HTTP sa prenášajú cez TCP. Prúd sa prenáša kus po kuse v paketoch. Všetky pakety TCP majú logické príznaky a 32-bitové poradové čísla:
Kombinácia príznakov definuje úlohu konkrétneho balíka. Príznak SYN znamená, že ide o prvý paket odosielateľa pri pripojení. Príznak ACK znamená, že odosielateľ prijal všetky údaje o spojení až do bajtu. acknum. Paket môže mať niekoľko príznakov a je pomenovaný podľa ich kombinácie, napríklad paket SYNACK.
Poradové číslo (seqnum) určuje posun v dátovom toku pre prvý bajt odoslaný v tomto pakete. Napríklad, ak v prvom pakete s X bajtmi dát bolo toto číslo N, v ďalšom pakete s novými dátami to bude N+X. Na začiatku spojenia si každá strana náhodne vyberie toto číslo.
Potvrdzovacie číslo (acknum) - rovnaký posun ako seqnum, ale neurčuje číslo prenášaného bajtu, ale číslo prvého bajtu od príjemcu, ktorý odosielateľ nevidel.
Na začiatku spojenia sa musia strany dohodnúť seqnum и acknum. Klient pošle SYN paket s jeho seqnum = X. Server odpovie paketom SYNACK, kam zapíše svoj vlastný seqnum = Y a vystavuje acknum = X + 1. Klient odpovie na SYNACK paketom ACK, kde seqnum = X + 1, acknum = Y + 1. Potom sa začne skutočný prenos údajov.
Ak účastník nepotvrdí prijatie paketu, TCP ho znova odošle po uplynutí časového limitu.
Prečo sa cookies SYN nepoužívajú vždy?
Po prvé, ak sa stratí SYNACK alebo ACK, budete musieť počkať na opätovné odoslanie - nadväzovanie spojenia sa spomalí. Po druhé, v pakete SYN - a iba v ňom! - prenáša sa množstvo možností, ktoré ovplyvňujú ďalšiu prevádzku spojenia. Server si nepamätá prichádzajúce SYN pakety, takže tieto voľby ignoruje, v nasledujúcich paketoch ich už klient nebude posielať. TCP môže v tomto prípade fungovať, ale aspoň v počiatočnej fáze sa kvalita pripojenia zníži.
Pokiaľ ide o balíky, program XDP by mal robiť nasledovné:
odpovedať na SYN pomocou SYNACK s cookie;
odpovedzte ACK s RST (prerušte spojenie);
zahodiť ďalšie pakety.
Pseudokód algoritmu spolu s analýzou paketov:
Если это не Ethernet,
пропустить пакет.
Если это не IPv4,
пропустить пакет.
Если адрес в таблице проверенных, (*)
уменьшить счетчик оставшихся проверок,
пропустить пакет.
Если это не TCP,
сбросить пакет. (**)
Если это SYN,
ответить SYN-ACK с cookie.
Если это ACK,
если в acknum лежит не cookie,
сбросить пакет.
Занести в таблицу адрес с N оставшихся проверок. (*)
Ответить RST. (**)
В остальных случаях сбросить пакет.
Jeden (*) body, v ktorých potrebujete spravovať stav systému, sú označené - v prvej fáze sa bez nich zaobídete jednoduchou implementáciou TCP handshake s vygenerovaním SYN cookie ako seqnum.
Na mieste (**), kým nemáme tabuľku, balíček preskočíme.
Implementácia TCP handshake
Analýza balíka a overenie kódu
Potrebujeme štruktúry hlavičiek siete: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) a TCP (uapi/linux/tcp.h). Posledný, ktorý som nemohol pripojiť kvôli chybám, ktoré súvisia atomic64_t, musel som skopírovať potrebné definície do kódu.
Všetky funkcie, ktoré sú v C rozlíšené kvôli čitateľnosti, musia byť vložené na stránke volania, pretože verifikátor eBPF v jadre zakazuje skoky späť, teda v skutočnosti slučky a volania funkcií.
Program je reťazec funkcií. Každý dostane paket, v ktorom je zvýraznená hlavička zodpovedajúcej úrovne, napr. process_ether() čaká na naplnenie ether. Na základe výsledkov analýzy poľa môže funkcia preniesť paket na vyššiu úroveň. Výsledkom funkcie je akcia XDP. Zatiaľ čo manipulátory SYN a ACK nechajú prejsť všetky pakety.
Kľúčový reťazec invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0): existujú cesty vykonávania, keď je trinásty bajt od začiatku vyrovnávacej pamäte mimo paketu. Z výpisu je ťažké povedať, o ktorom riadku hovoríme, ale existuje číslo inštrukcie (12) a disassembler, ktorý zobrazuje riadky zdrojového kódu:
čím je jasné, že problém je ether. Vždy by to tak bolo.
Odpovedzte SYN
Cieľom v tejto fáze je vygenerovať správny SYNACK paket s fixným seqnum, ktorý bude v budúcnosti nahradený súborom cookie SYN. Všetky zmeny prebiehajú v process_tcp_syn() a okolie.
Kontrola balíka
Napodiv, tu je najpozoruhodnejší riadok, alebo skôr komentár k nemu:
Pri písaní prvej verzie kódu bolo použité jadro 5.1, pre ktorého overovač bol rozdiel medzi data_end и (const void*)ctx->data_end. V čase písania tohto článku jadro 5.3.1 tento problém nemalo. Možno kompilátor pristupoval k lokálnej premennej inak ako k poľu. Morálka - pri veľkom hniezdení môže pomôcť zjednodušenie kódu.
Ďalšie rutinné kontroly dĺžok pre slávu overovateľa; O MAX_CSUM_BYTES ниже.
Vymeňte TCP porty, IP a MAC adresy. Štandardná knižnica nie je dostupná z programu XDP, takže memcpy() — makro, ktoré skrýva Clangovu vnútornú stránku.
Kontrolné súčty IPv4 a TCP vyžadujú pridanie všetkých 16-bitových slov v hlavičkách a veľkosť hlavičiek je v nich zapísaná, to znamená, že v čase kompilácie nie je známa. Toto je problém, pretože overovateľ nepreskočí normálnu slučku až do hraničnej premennej. Veľkosť hlavičiek je však obmedzená: každá do 64 bajtov. Môžete vytvoriť slučku s pevným počtom iterácií, ktorá môže skončiť skôr.
Podotýkam, že existuje RFC 1624 o tom, ako čiastočne prepočítať kontrolný súčet, ak sa zmenia iba pevné slová paketov. Metóda však nie je univerzálna a jej implementácia by bola náročnejšia na údržbu.
Funkcia výpočtu kontrolného súčtu:
#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;
}
Hoci size skontrolovaný volacím kódom, je potrebná druhá výstupná podmienka, aby overovateľ mohol dokázať koniec cyklu.
Pre 32-bitové slová je implementovaná jednoduchšia verzia:
INTERNAL u32
sum16_32(u32 v) {
return (v >> 16) + (v & 0xffff);
}
Vlastné prepočítanie kontrolných súčtov a odoslanie paketu späť:
Funkcia carry() vytvára kontrolný súčet z 32-bitového súčtu 16-bitových slov podľa RFC 791.
Kontrola nadviazania spojenia TCP
Filter správne vytvorí spojenie s netcat, preskočenie záverečného ACK, na ktoré Linux odpovedal paketom RST, keďže sieťový zásobník nedostal SYN - bol konvertovaný na SYNACK a odoslaný späť - a z pohľadu OS prišiel paket, ktorý nebol súvisiace s otvorenými spojeniami.
$ sudo ip netns exec xdp-test nc -nv 192.0.2.1 6666
192.0.2.1 6666: Connection reset by peer
Je dôležité kontrolovať s plnohodnotnými aplikáciami a pozorovať tcpdump na xdp-remote lebo napr. hping3 nereaguje na nesprávne kontrolné súčty.
SYN cookie
Samotná kontrola je z pohľadu XDP triviálna. Algoritmus výpočtu je primitívny a pravdepodobne zraniteľný voči sofistikovanému útočníkovi. Linuxové jadro napríklad používa kryptografický SipHash, ale jeho implementácia pre XDP zjavne presahuje rámec tohto článku.
Objavilo sa pre nové TODO súvisiace s externou interakciou:
Program XDP nemôže ukladať cookie_seed (tajná časť soli) v globálnej premennej potrebujete úložisko jadra, ktorého hodnota sa bude pravidelne aktualizovať zo spoľahlivého generátora.
Ak sa SYN cookie v ACK pakete zhoduje, nemusíte tlačiť správu, ale zapamätajte si IP overeného klienta, aby ste z neho mohli ďalej preskakovať pakety.
Pokiaľ neexistuje zoznam overených IP, nebude existovať žiadna ochrana pred samotnou SYN flood, ale tu je reakcia na ACK flood spustenú týmto príkazom:
sudo ip netns exec xdp-test hping3 --flood -A -s 1111 -p 2222 192.0.2.1
Niekedy sú eBPF vo všeobecnosti a XDP konkrétne prezentované skôr ako pokročilý administrátorský nástroj než vývojová platforma. XDP je skutočne nástroj na zasahovanie do spracovania paketov jadra a nie alternatíva k zásobníku jadra, ako napríklad DPDK a iné možnosti obchádzania jadra. Na druhej strane vám XDP umožňuje implementovať pomerne zložitú logiku, ktorá sa navyše ľahko aktualizuje bez prestávky v spracovaní prevádzky. Verifikátor nerobí veľké problémy, osobne by som takéto časti kódu užívateľského priestoru neodmietol.
V druhej časti, ak je téma zaujímavá, doplníme tabuľku overených klientov a prerušíme spojenia, implementujeme počítadlá a napíšeme utilitu používateľského priestoru na správu filtra.