Kirjutame XDP-le kaitset DDoS-i rünnakute vastu. Tuumaosa
eXpress Data Path (XDP) tehnoloogia võimaldab Linuxi liidestel teostada liikluse juhuslikku töötlemist enne, kui paketid sisenevad kerneli võrgupinu. XDP rakendus - kaitse DDoS rünnakute eest (CloudFlare), keerukad filtrid, statistika kogumine (Netflix). XDP-programme käivitab eBPF-i virtuaalmasin, seega on neil olenevalt filtri tüübist piirangud nii koodile kui ka saadaolevatele kerneli funktsioonidele.
Artikli eesmärk on täita paljude XDP materjalide puudused. Esiteks pakuvad nad valmis koodi, mis läheb kohe XDP funktsioonidest mööda: see on kontrollimiseks ette valmistatud või on probleemide tekitamiseks liiga lihtne. Kui proovite seejärel oma koodi nullist kirjutada, pole teil aimugi, mida tüüpiliste vigadega peale hakata. Teiseks ei käsitleta võimalusi XDP kohapealseks testimiseks ilma VM-i ja riistvarata, hoolimata sellest, et neil on oma lõkse. Tekst on mõeldud programmeerijatele, kes on kursis võrgunduse ja Linuxiga ning on huvitatud XDP-st ja eBPF-ist.
Selles osas saame üksikasjalikult aru, kuidas XDP-filtrit kokku pannakse ja kuidas seda testida, seejärel kirjutame paketitöötluse tasemel lihtsa versiooni tuntud SYN-küpsiste mehhanismist. Me ei loo veel "valget nimekirja".
kontrollitud kliente, pidama loendureid ja hallata filtrit – piisavalt logisid.
Kirjutame C-keeles - see pole moes, kuid on praktiline. Kogu kood on GitHubis saadaval lõpus oleva lingi kaudu ja on jagatud commitsiks vastavalt artiklis kirjeldatud etappidele.
Vastutusest loobumine Selle artikli jooksul töötan välja minilahenduse DDoS-i rünnakute tõrjumiseks, sest see on XDP ja minu valdkonna jaoks realistlik ülesanne. Peamine eesmärk on aga tehnoloogiast aru saada, see ei ole valmiskaitse loomise juhend. Õpetuskood ei ole optimeeritud ja jätab mõned nüansid välja.
XDP lühiülevaade
Toon välja ainult põhipunktid, et mitte dubleerida dokumentatsiooni ja olemasolevaid artikleid.
Niisiis laaditakse filtri kood kernelisse. Sissetulevad paketid edastatakse filtrile. Selle tulemusena peab filter tegema otsuse: edastama paketi kernelisse (XDP_PASS), kukuta pakett (XDP_DROP) või saatke see tagasi (XDP_TX). Filter võib pakendit muuta, eriti kehtib see XDP_TX. Samuti saate programmi katkestada (XDP_ABORTED) ja lähtestage pakett, kuid see on analoogne assert(0) - silumiseks.
eBPF (Extended Berkley Packet Filter) virtuaalmasin on sihilikult lihtsaks tehtud, et kernel saaks kontrollida, kas kood ei loo silmust ega kahjusta teiste inimeste mälu. Kumulatiivsed piirangud ja kontrollid:
Silmused (tagurpidi) on keelatud.
Andmete jaoks on virn, kuid funktsioone pole (kõik C-funktsioonid peavad olema sisse kirjutatud).
Mälu juurdepääs väljaspool pinu ja pakettpuhvrit on keelatud.
Koodi suurus on piiratud, kuid praktikas pole see kuigi oluline.
Lubatud on kutsuda ainult kerneli erifunktsioone (eBPF-i abistajad).
Filtri projekteerimine ja paigaldamine näeb välja selline:
Lähtekood (nt kernel.c) on kompileeritud objektiks (kernel.o) eBPF virtuaalmasina arhitektuuri jaoks. Alates 2019. aasta oktoobrist toetab eBPF-i koostamist Clang ja seda lubab GCC 10.1.
Kui see objektikood sisaldab väljakutseid kerneli struktuuridele (näiteks tabelitele ja loenduritele), asendatakse nende ID-d nullidega, mis tähendab, et sellist koodi ei saa käivitada. Enne kernelisse laadimist tuleb need nullid asendada konkreetsete tuumakutsetega loodud objektide ID-dega (linkida kood). Saate seda teha väliste utiliitidega või kirjutada programmi, mis lingib ja laadib konkreetse filtri.
Kernel kontrollib laaditud programmi. Kontrollitakse tsüklite puudumist ning paketi- ja virnapiiride ületamist. Kui kontrollija ei suuda koodi õigsust tõestada, lükatakse programm tagasi – peate suutma talle meeldida.
Pärast edukat kontrollimist kompileerib kernel eBPF arhitektuuri objektikoodi süsteemiarhitektuuri masinkoodiks (just-in-time).
Programm kinnitub liidese külge ja hakkab pakette töötlema.
Kuna XDP töötab kernelis, toimub silumine jälgimislogide ja tegelikult pakettide abil, mida programm filtreerib või genereerib. eBPF aga tagab, et allalaaditud kood on süsteemi jaoks turvaline, nii et saate XDP-ga katsetada otse oma kohalikus Linuxis.
Keskkonna ettevalmistamine
Assamblee
Clang ei saa otseselt eBPF-i arhitektuuri objektikoodi toota, seega koosneb protsess kahest etapist:
Kompileeri C kood LLVM baitkoodiks (clang -emit-llvm).
Filtri kirjutamisel tuleb kasuks paar faili koos abifunktsioonide ja makrodega kerneli testidest. On oluline, et need vastaksid kerneli versioonile (KVER). Laadige need alla helpers/:
KDIR sisaldab teed kerneli päisteni, ARCH — süsteemi arhitektuur. Teed ja tööriistad võivad distributsioonide lõikes veidi erineda.
Näide Debian 10 (kernel 4.19.67) erinevustest
# другая команда
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 ühendage abipäistega kataloog ja mitu kerneli päistega kataloogi. Sümbol __KERNEL__ tähendab, et UAPI (userspace API) päised on määratletud tuuma koodi jaoks, kuna filter käivitatakse tuumas.
Virnakaitse saab keelata (-fno-stack-protector), kuna eBPF-koodi kontrollija kontrollib endiselt virnast väljuvaid rikkumisi. Optimeerimised tasub kohe sisse lülitada, sest eBPF baitkoodi suurus on piiratud.
Alustame filtriga, mis läbib kõik paketid ja ei tee midagi:
Meeskond make kogub xdp_filter.o. Kus seda nüüd proovida?
Katselaud
Stendil peab olema kaks liidest: millele tuleb filter ja kust saadetakse pakette. Need peavad olema täisväärtuslikud Linuxi seadmed, millel on oma IP-aadress, et kontrollida, kuidas tavalised rakendused meie filtriga töötavad.
Meile sobivad veth (virtuaalne Ethernet) tüüpi seadmed: need on virtuaalse võrguliidese paar, mis on üksteisega otse "ühendatud". Saate neid luua nii (selles jaotises kõik käsud ip viiakse läbi alates root):
ip link add xdp-remote type veth peer name xdp-local
see on xdp-remote и xdp-local — seadmete nimed. Peal xdp-local (192.0.2.1/24) kinnitatakse filter, millega xdp-remote (192.0.2.2/24) sissetulev liiklus saadetakse. Siiski on probleem: liidesed on samas masinas ja Linux ei saada liiklust ühele neist teise kaudu. Saate selle lahendada keeruliste reeglitega iptables, kuid nad peavad pakette muutma, mis on silumiseks ebamugav. Parem on kasutada võrgu nimeruume (edaspidi netns).
Võrgu nimeruum sisaldab liideste, marsruutimistabelite ja NetFilteri reeglite komplekti, mis on isoleeritud teistes netnsides sarnastest objektidest. Iga protsess töötab nimeruumis ja sellel on juurdepääs ainult selle netnsi objektidele. Vaikimisi on süsteemil kõigi objektide jaoks üks võrgu nimeruum, nii et saate töötada Linuxis ega tea netns-idest.
Loome uue nimeruumi xdp-test ja liigutage see sinna xdp-remote.
ip netns add xdp-test
ip link set dev xdp-remote netns xdp-test
Seejärel hakkab protsess käima xdp-test, ei "näe" xdp-local (see jääb vaikimisi netns-i) ja 192.0.2.1-le paketi saatmisel läheb see läbi xdp-remotesest see on ainus liides 192.0.2.0/24, mis sellele protsessile juurde pääseb. See toimib ka vastupidises suunas.
Netnide vahel liikudes läheb liides alla ja kaotab aadressi. Netns-i liidese konfigureerimiseks peate käivitama ip ... selles käsunimeruumis 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
Nagu näete, ei erine see seadistusest xdp-local vaikenimeruumis:
ip address add 192.0.2.1/24 dev xdp-local
ip link set xdp-local up
Kui jooksed tcpdump -tnevi xdp-local, näete, et paketid saadeti aadressilt xdp-test, tarnitakse sellele liidesele:
ip netns exec xdp-test ping 192.0.2.1
Mugav on kesta sisse lasta xdp-test. Hoidlas on skript, mis automatiseerib statiiviga tööd, näiteks saab stendi seadistada käsuga sudo ./stand up ja kustutage see sudo ./stand down.
Jälgimine
Filter on seadmega seotud järgmiselt:
ip -force link set dev xdp-local xdp object xdp_filter.o verbose
võti -force vaja uue programmi linkimiseks, kui teine programm on juba lingitud. “No news is good news” ei käi selle käsu kohta, järeldus on igal juhul mahukas. näidata verbose valikuline, kuid koos sellega kuvatakse koodikontrollija töö kohta koosteloendiga aruanne:
Verifier analysis:
0: (b7) r0 = 2
1: (95) exit
Programmi ja liidese linkimise tühistamine:
ip link set dev xdp-local xdp off
Skriptis on need käsud sudo ./stand attach и sudo ./stand detach.
Filtri külge kinnitades saate selles veenduda ping töötab edasi, aga kas programm töötab? Lisame logid. Funktsioon bpf_trace_printk() sarnane printf(), kuid toetab peale mustri ainult kuni kolme argumenti ja piiratud loendit spetsifikaatidest. Makro bpf_printk() lihtsustab kõnet.
Sel põhjusel paisutab silumisväljund saadud koodi oluliselt.
XDP pakettide saatmine
Vahetame filtrit: las see saadab kõik sissetulevad paketid tagasi. Võrgu seisukohalt on see vale, kuna päistes oleks vaja aadresse muuta, aga nüüd on põhimõtteline töö oluline.
Käivitamine tcpdump edasi xdp-remote. See peaks näitama identset väljaminevat ja sissetulevat ICMP kajapäringut ja lõpetama ICMP kaja vastuse kuvamise. Aga see ei näita. Selgub, et töö pärast XDP_TX programmis sees xdp-localvajalikpaariliidesele xdp-remote määrati ka programm, isegi kui see oli tühi, ja ta kasvatati üles.
Kuidas ma seda teadsin?
Jälgige tuumas paketi teed Perf sündmuste mehhanism võimaldab muide kasutada sama virtuaalmasinat, st eBPF-i kasutatakse eBPF-iga lahtivõtmiseks.
Kurjast tuleb teha head, sest pole millestki muust teha.
Kui selle asemel kuvatakse ainult ARP-sid, peate filtrid eemaldama (see teeb sudo ./stand detach), lase lahti ping, seejärel määrake filtrid ja proovige uuesti. Probleem on selles, et filter XDP_TX kehtib nii ARP kui ka virna puhul
nimeruumid xdp-test õnnestus MAC-aadress 192.0.2.1 “unustada”, ei suuda see seda IP-d lahendada.
Probleemi avaldus
Liigume edasi nimetatud ülesande juurde: kirjutage XDP-le SYN-küpsiste mehhanism.
SYN-i üleujutus on endiselt populaarne DDoS-rünnak, mille olemus on järgmine. Kui ühendus on loodud (TCP käepigistus), võtab server vastu SYN-i, eraldab ressursid tulevase ühenduse jaoks, vastab SYNACK-paketiga ja ootab ACK-i. Ründaja lihtsalt saadab tuhandeid SYN-pakette sekundis iga hosti võltsitud aadressidelt mitme tuhande tugeva botneti kaudu. Server on sunnitud eraldama ressursse kohe pärast paketi saabumist, kuid vabastab need pärast suurt ajalõpu, mille tulemusena on mälu või limiidid ammendatud, uusi ühendusi ei võeta vastu ja teenus pole saadaval.
Kui te ei eralda ressursse SYN-paketi põhjal, vaid vastate ainult SYNACK-paketiga, siis kuidas saab server aru, et hiljem saabunud ACK-pakett viitab SYN-paketile, mida ei salvestatud? Lõppude lõpuks võib ründaja genereerida ka võltsitud ACK-e. SYN-küpsise mõte on see sisse kodeerida seqnum ühendusparameetrid aadresside, portide ja muutuva soola räsina. Kui ACK jõudis kohale jõuda enne soola vahetamist, saate räsi uuesti arvutada ja sellega võrrelda acknum. Sepis acknum ründaja ei saa, kuna sool sisaldab saladust, ja tal pole piiratud kanali tõttu aega seda sorteerida.
SYN-i küpsis on Linuxi tuumas juba ammu juurutatud ja seda saab isegi automaatselt lubada, kui SYN-id saabuvad liiga kiiresti ja massiliselt.
TCP käepigistuse õppeprogramm
TCP pakub andmeedastust baitide voona, näiteks HTTP päringud edastatakse üle TCP. Voog edastatakse tükkidena pakettidena. Kõikidel TCP-pakettidel on loogilised lipud ja 32-bitised järjenumbrid:
Lippude kombinatsioon määrab konkreetse paketi rolli. SYN-i lipp näitab, et see on saatja esimene pakett ühenduses. ACK-lipp tähendab, et saatja on saanud kõik ühendusandmed kuni baidini acknum. Paketil võib olla mitu lippu ja seda kutsutakse nende kombinatsiooniga, näiteks SYNACK-pakett.
Jada number (seqnum) määrab nihke andmevoos esimese baidi jaoks, mis selles paketis edastatakse. Näiteks kui esimeses X baiti andmepaketis oli see arv N, siis järgmises uute andmete paketis on see N+X. Ühenduse alguses valib kumbki pool selle numbri juhuslikult.
Kinnituse number (acknum) - sama nihe nagu sekvnum, kuid see ei määra edastatava baidi numbrit, vaid adressaadi esimese baidi numbrit, mida saatja ei näinud.
Ühenduse alguses peavad pooled kokku leppima seqnum и acknum. Klient saadab koos omaga SYN-paketi seqnum = X. Server vastab SYNACK-paketiga, kus ta selle salvestab seqnum = Y ja paljastab acknum = X + 1. Klient vastab SYNACK-ile ACK-paketiga, kus seqnum = X + 1, acknum = Y + 1. Pärast seda algab tegelik andmeedastus.
Kui partner ei kinnita paketi kättesaamist, saadab TCP selle pärast ajalõpu uuesti.
Miks SYN-i küpsiseid alati ei kasutata?
Esiteks, kui SYNACK või ACK kaob, peate ootama, kuni see uuesti saadetakse - ühenduse seadistamine aeglustub. Teiseks SYN-i paketis – ja ainult selles! — edastatakse mitmeid valikuid, mis mõjutavad ühenduse edasist toimimist. Sissetulevaid SYN-i pakette meelde jätmata ignoreerib server neid valikuid ning klient ei saada neid järgmistes pakettides. TCP võib sel juhul töötada, kuid vähemalt algstaadiumis ühenduse kvaliteet langeb.
Pakettide vaatenurgast peab XDP programm tegema järgmist:
vastake SYN-ile SYNACK-iga küpsisega;
vastata ACK-ile RST-ga (katkesta ühendus);
ülejäänud pakid ära visata.
Algoritmi pseudokood koos paketi sõelumisega:
Если это не Ethernet,
пропустить пакет.
Если это не IPv4,
пропустить пакет.
Если адрес в таблице проверенных, (*)
уменьшить счетчик оставшихся проверок,
пропустить пакет.
Если это не TCP,
сбросить пакет. (**)
Если это SYN,
ответить SYN-ACK с cookie.
Если это ACK,
если в acknum лежит не cookie,
сбросить пакет.
Занести в таблицу адрес с N оставшихся проверок. (*)
Ответить RST. (**)
В остальных случаях сбросить пакет.
Üks (*) märgitakse punktid, kus peate haldama süsteemi olekut - esimeses etapis saate ilma nendeta hakkama, rakendades lihtsalt TCP-käepigistust, genereerides järjenumbrina SYN-küpsise.
Kohapeal (**), kui meil pole lauda, jätame paki vahele.
TCP käepigistuse rakendamine
Paketi parsimine ja koodi kontrollimine
Vajame võrgu päise struktuure: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) ja TCP (uapi/linux/tcp.h). Viimast ei saanud ma sellega seotud vigade tõttu ühendada atomic64_t, pidin vajalikud definitsioonid koodi kopeerima.
Kõik funktsioonid, mis on C-s loetavuse huvides esile tõstetud, peavad olema kutsumispunktis sees, kuna kernelis olev eBPF-i kontrollija keelab tagasisuunamise, st tegelikult tsüklite ja funktsioonikutsete.
Programm on funktsioonide konveier. Igaüks saab paketi, milles vastava taseme päis on esile tõstetud, näiteks process_ether() loodab, et see täidetakse ether. Väljaanalüüsi tulemuste põhjal saab funktsioon edastada paketi kõrgemale tasemele. Funktsiooni tulemus on XDP toiming. Praegu edastavad SYN-i ja ACK-i töötlejad kõik paketid.
Võtmepael invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0): on täitmisteed, kui kolmeteistkümnes bait puhvri algusest on väljaspool paketti. Loendist on raske aru saada, millisest reast me räägime, kuid seal on juhise number (12) ja demonteerija, mis näitab lähtekoodi ridu:
mis teeb selgeks, et probleem on ether. See oleks alati nii.
Vasta SYN-ile
Eesmärk selles etapis on genereerida õige SYNACK-pakett fikseeritud parameetriga seqnum, mis asendatakse tulevikus SYN-i küpsisega. Kõik muudatused toimuvad sisse process_tcp_syn() ja ümbritsevad alad.
Paki kontrollimine
Kummalisel kombel on siin kõige tähelepanuväärsem rida või õigemini selle kommentaar:
Koodi esimese versiooni kirjutamisel kasutati 5.1 kernelit, mille kontrollimisel oli erinevus data_end и (const void*)ctx->data_end. Kirjutamise ajal kernel 5.3.1 seda probleemi ei olnud. Võimalik, et kompilaator pääses kohalikule muutujale juurde teisiti kui väljale. Loo moraal: koodi lihtsustamine võib aidata, kui pesitsemist on palju.
Järgmiseks on rutiinsed pikkuse kontrollid tõendaja hiilguse tagamiseks; O MAX_CSUM_BYTES allpool.
Vahetage TCP-porte, IP-aadressi ja MAC-aadresse. Standardne raamatukogu pole XDP-programmist juurdepääsetav, seega memcpy() - makro, mis peidab Clangi olemusi.
IPv4 ja TCP kontrollsummad nõuavad päistesse kõigi 16-bitiste sõnade lisamist ning päiste suurus kirjutatakse neisse ehk kompileerimise ajal teadmata. See on probleem, kuna kontrollija ei jäta tavalist tsüklit piirmuutujale vahele. Kuid päiste suurus on piiratud: igaüks kuni 64 baiti. Saate teha kindla arvu iteratsioonidega tsükli, mis võib varakult lõppeda.
Märgin, et on olemas RFC 1624 selle kohta, kuidas kontrollsummat osaliselt ümber arvutada, kui muudetakse ainult pakettide fikseeritud sõnu. Kuid meetod ei ole universaalne ja selle rakendamist oleks raskem säilitada.
Kontrollsumma arvutamise funktsioon:
#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;
}
Kuigi size kutsumiskoodiga kontrollitud, on teine väljumistingimus vajalik selleks, et kinnitaja saaks tõestada tsükli lõpetamist.
32-bitiste sõnade jaoks on rakendatud lihtsam versioon:
INTERNAL u32
sum16_32(u32 v) {
return (v >> 16) + (v & 0xffff);
}
Tegelikult kontrollsummade ümberarvutamine ja paketi tagasisaatmine:
Funktsioon carry() teeb kontrollsumma 32-bitiste sõnade 16-bitisest summast vastavalt RFC 791-le.
TCP käepigistuse kontrollimine
Filter loob õigesti ühenduse netcat, puudub lõplik ACK, millele Linux vastas RST-paketiga, kuna võrgupinn ei saanud SYN-i - see teisendati SYNACK-iks ja saadeti tagasi - ja OS-i seisukohast saabus pakett, mis ei olnud seotud avamisega. ühendused.
$ sudo ip netns exec xdp-test nc -nv 192.0.2.1 6666
192.0.2.1 6666: Connection reset by peer
Oluline on kontrollida täisväärtuslike rakendustega ja jälgida tcpdump edasi xdp-remote sest näiteks hping3 ei reageeri valedele kontrollsummadele.
SYN-i küpsis
XDP seisukohast on kontrollimine iseenesest triviaalne. Arvutusalgoritm on primitiivne ja kogenud ründaja suhtes tõenäoliselt haavatav. Näiteks Linuxi tuum kasutab krüptograafilist SipHashi, kuid selle rakendamine XDP jaoks jääb selgelt selle artikli raamidest välja.
Tutvustatakse uute väliskommunikatsiooniga seotud ülesannete jaoks:
XDP programm ei saa salvestada cookie_seed (soola salajane osa) globaalses muutujas, vajate tuumas salvestusruumi, mille väärtust uuendatakse perioodiliselt usaldusväärsest generaatorist.
Kui SYN-küpsis kattub ACK-paketis, ei pea te sõnumit printima, vaid meeles pidama kontrollitud kliendi IP-aadressi, et jätkata sealt pakettide edastamist.
Mõnikord esitletakse eBPF-i üldiselt ja eriti XDP-d rohkem kui täiustatud administraatori tööriista kui arendusplatvormi. Tõepoolest, XDP on tööriist, mis häirib pakettide töötlemist kerneli poolt, mitte alternatiiv kerneli virnale, nagu DPDK ja muud kerneli möödaviiguvalikud. Teisest küljest võimaldab XDP rakendada üsna keerulist loogikat, mida on pealegi lihtne uuendada ilma liikluse töötlust segamata. Tõendaja ei tekita suuri probleeme; isiklikult ma ei keelduks sellest kasutajaruumi koodi osade puhul.
Teises osas, kui teema pakub huvi, täidame kontrollitud klientide ja katkestuste tabeli, juurutame loendurid ja kirjutame filtri haldamiseks kasutajaruumi utiliidi.