Az XDP-re írunk védelmet a DDoS támadások ellen. Nukleáris rész
Az eXpress Data Path (XDP) technológia lehetővé teszi a forgalom tetszőleges feldolgozását a Linux felületeken, mielőtt a csomagok belépnének a kernel hálózati veremébe. XDP alkalmazása - DDoS támadások elleni védelem (CloudFlare), összetett szűrők, statisztikai adatok gyűjtése (Netflix). Az XDP-programokat az eBPF virtuális gép hajtja végre, ezért a szűrő típusától függően korlátozások vonatkoznak a kódjukra és az elérhető kernelfunkciókra.
A cikk célja, hogy pótolja az XDP-vel kapcsolatos számos anyag hiányosságait. Először is olyan kész kódot adnak, amely azonnal megkerüli az XDP funkcióit: felkészült az ellenőrzésre, vagy túl egyszerű ahhoz, hogy problémákat okozzon. Amikor később a semmiből próbálja megírni saját kódját, nem érti, hogy mit kezdjen a tipikus hibákkal. Másodszor, nem terjed ki az XDP helyi tesztelésének módjaira virtuális gép és hardver nélkül, annak ellenére, hogy megvannak a maguk buktatói. A szöveg azoknak a programozóknak szól, akik ismerik a hálózatokat és a Linuxot, akik érdeklődnek az XDP és az eBPF iránt.
Ebben a részben részletesen megismerjük az XDP szűrő összeállítását és tesztelését, majd megírjuk a jól ismert SYN cookie-k mechanizmusának egyszerű változatát csomagfeldolgozási szinten. Amíg nem alkotunk egy "fehér listát"
ellenőrzött ügyfelek, számlálók vezetése és a szűrő kezelése - elegendő napló.
C-ben fogunk írni - ez nem divatos, de praktikus. Az összes kód elérhető a GitHubon a végén található linken, és a cikkben leírt lépések szerint véglegesítésekre van felosztva.
Fontos. A cikk során egy mini-megoldást fogok kifejleszteni a DDoS támadások kivédésére, mert ez reális feladat az XDP és az én szakterületem számára. A fő cél azonban a technológia megértése, ez nem útmutató a kész védelem létrehozásához. Az oktatóprogram kódja nincs optimalizálva, és kihagy néhány árnyalatot.
Az XDP rövid áttekintése
Csak a kulcsfontosságú pontokat közlöm, hogy ne duplikáljam a dokumentációt és a meglévő cikkeket.
Tehát a szűrőkód betöltődik a kernelbe. A szűrő átadja a bejövő csomagokat. Ennek eredményeként a szűrőnek döntenie kell: átadja a csomagot a kernelnek (XDP_PASS), dobja le a csomagot (XDP_DROP) vagy küldje vissza (XDP_TX). A szűrő megváltoztathatja a csomagot, ez különösen igaz XDP_TX. A program összeomolhat (XDP_ABORTED) és dobja le a csomagot, de ez analóg assert(0) - hibakereséshez.
Az eBPF (extended Berkley Packet Filter) virtuális gépet szándékosan egyszerűsítették, hogy a kernel ellenőrizni tudja, hogy a kód nem hurkol, és nem károsítja-e mások memóriáját. Halmozott korlátozások és ellenőrzések:
A hurkok (visszaugrások) tilosak.
Van egy verem az adatokhoz, de nincsenek függvények (minden C függvénynek be kell illesztenie).
A veremen és a csomagpufferen kívüli memória-hozzáférés tilos.
A kód mérete korlátozott, de a gyakorlatban ez nem túl jelentős.
Csak speciális kernelfüggvények (eBPF helperek) hívása engedélyezett.
A szűrő fejlesztése és telepítése így néz ki:
forráskód (pl. kernel.c) objektummá fordítja (kernel.o) az eBPF virtuális gép architektúrához. 2019 októberétől az eBPF-re történő fordítást a Clang támogatja, és a GCC 10.1 ígérete szerint.
Ha ebben az objektumkódban kernelstruktúrák (például táblák és számlálók) hívásai vannak, akkor az azonosítóik helyett nullák vannak, vagyis az ilyen kód nem hajtható végre. A kernelbe való betöltés előtt ezeket a nullákat le kell cserélni a kernelhívásokkal létrehozott objektumok azonosítóira (linkelni kell a kódot). Ezt megteheti külső segédprogramokkal, vagy írhat egy programot, amely egy adott szűrőt kapcsol be és tölt be.
A kernel ellenőrzi a betöltendő programot. Ellenőrzi, hogy nincsenek-e ciklusok, és nem léptek-e ki a csomag és a verem határai. Ha az ellenőrző nem tudja bizonyítani a kód helyességét, akkor a program elutasításra kerül – tetszeni kell neki.
A sikeres ellenőrzés után a kernel lefordítja az eBPF architektúra objektumkódját rendszerarchitektúra gépi kódjává (just-in-time).
A program csatlakozik az interfészhez, és megkezdi a csomagok feldolgozását.
Mivel az XDP a kernelben fut, a hibakeresést nyomkövetési naplók, sőt, a program által szűrt vagy generált csomagok végzik. Az eBPF azonban biztonságban tartja a letöltött kódot a rendszer számára, így kísérletezhet az XDP-vel közvetlenül a helyi Linuxon.
A környezet előkészítése
gyülekezés
A Clang nem tud közvetlenül objektumkódot kiadni az eBPF architektúrához, ezért a folyamat két lépésből áll:
Fordítsa le a C kódot LLVM bájtkódra (clang -emit-llvm).
Szűrő írásakor jól jön pár fájl segédfunkciókkal és makróval kernel tesztekből. Fontos, hogy megegyezzenek a kernel verziójával (KVER). Töltse le őket ide helpers/:
KDIR tartalmazza a kernelfejlécek elérési útját, ARCH - Rendszer Felépítés. Az elérési utak és eszközök kissé eltérhetnek a disztribúciók között.
Példa a különbségekre a Debian 10-hez (4.19.67-es kernel)
# другая команда
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 csatlakoztasson egy könyvtárat segédfejlécekkel és több könyvtárat kernelfejlécekkel. Szimbólum __KERNEL__ azt jelenti, hogy az UAPI (userspace API) fejlécek a kernelkódhoz vannak definiálva, mivel a szűrő a kernelben fut le.
A veremvédelem kikapcsolható (-fno-stack-protector). Azonnal engedélyeznie kell az optimalizálást, mert az eBPF bájtkód mérete korlátozott.
Kezdjük egy szűrővel, amely átenged minden csomagot, és nem csinál semmit:
Csapat make gyűjt xdp_filter.o. Hol lehet most tesztelni?
Próbapad
Az állványnak két interfészt kell tartalmaznia: amelyen lesz egy szűrő, és amelyről csomagokat küldenek. Ezeknek teljes Linux-eszközöknek kell lenniük saját IP-címmel, hogy ellenőrizhessük, hogyan működnek a normál alkalmazások a szűrőnkkel.
Az olyan eszközök, mint a veth (virtuális Ethernet) megfelelőek számunkra: egy pár virtuális hálózati interfész, amelyek közvetlenül „csatlakoznak” egymáshoz. Ezeket így hozhatja létre (ebben a szakaszban az összes parancs ip től előadták root):
ip link add xdp-remote type veth peer name xdp-local
Itt xdp-remote и xdp-local — az eszközök nevei. Tovább xdp-local (192.0.2.1/24) szűrő kerül rögzítésre, azzal xdp-remote (192.0.2.2/24) bejövő forgalom elküldésre kerül. Van azonban egy probléma: az interfészek ugyanazon a gépen vannak, és a Linux nem küld forgalmat egyikre a másikon keresztül. Trükkös szabályokkal meg tudod oldani iptables, de csomagokat kell cserélniük, ami hibakereséskor kényelmetlen. Érdemesebb hálózati névtereket (hálózati névtereket, további netn-eket) használni.
A hálózati névtér interfészeket, útválasztási táblákat és NetFilter-szabályokat tartalmaz, amelyek el vannak különítve más netns hasonló objektumaitól. Mindegyik folyamat valamilyen névtérben fut, és csak ennek a netns-nek az objektumai érhetők el számára. Alapértelmezés szerint a rendszer egyetlen hálózati névtérrel rendelkezik az összes objektum számára, így Linuxon dolgozhat, és nem tud a netns-ről.
Hozzunk létre egy új névteret xdp-test és költözz oda xdp-remote.
ip netns add xdp-test
ip link set dev xdp-remote netns xdp-test
Ezután beindul a folyamat xdp-test, nem "látja" xdp-local (alapértelmezés szerint a netns-ben marad) és a 192.0.2.1-re küldve átadja azt xdp-remote, mert ez az egyetlen interfész a 192.0.2.0/24-ben a folyamat számára. Ez fordítva is működik.
Netn-ek közötti mozgáskor az interfész lemegy és elveszti a címet. A netns felületének beállításához futnia kell ip ... ebben a parancs névtérben 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
Amint látja, ez nem különbözik a beállítástól xdp-local az alapértelmezett névtérben:
ip address add 192.0.2.1/24 dev xdp-local
ip link set xdp-local up
Ha futni tcpdump -tnevi xdp-local, láthatja, hogy a címről küldött csomagok xdp-test, erre a felületre kerülnek:
ip netns exec xdp-test ping 192.0.2.1
Kényelmes egy shell bejáratása xdp-test. A repository rendelkezik egy szkripttel, amely automatizálja az állvánnyal való munkát, például az állványt a paranccsal állíthatja be sudo ./stand up és távolítsa el sudo ./stand down.
nyomon követése
A szűrő a következőképpen van társítva az eszközhöz:
ip -force link set dev xdp-local xdp object xdp_filter.o verbose
kulcs -force egy új program összekapcsolásához szükséges, ha egy másik már kapcsolódik. A "nincs hír jó hír" nem erről a parancsról szól, a kimenet amúgy is terjedelmes. jelezze verbose opcionális, de ezzel együtt megjelenik egy jelentés a kódellenőrző munkájáról az assembler listával:
Verifier analysis:
0: (b7) r0 = 2
1: (95) exit
Válassza le a programot a felületről:
ip link set dev xdp-local xdp off
A szkriptben ezek a parancsok sudo ./stand attach и sudo ./stand detach.
A szűrő bekötésével megbizonyosodhat arról ping továbbra is működik, de működik a program? Adjunk hozzá logókat. Funkció bpf_trace_printk() hasonló printf(), de a mintán kívül legfeljebb három argumentumot és a specifikációk korlátozott listáját támogat. Makró bpf_printk() leegyszerűsíti a hívást.
Emiatt a hibakeresési kimenet nagymértékben felduzzasztja az eredményül kapott kódot.
XDP-csomagok küldése
Változtassuk meg a szűrőt: küldje vissza az összes bejövő csomagot. Ez hálózati szempontból helytelen, hiszen a fejlécekben módosítani kellene a címeket, de most az elvi munka a fontos.
Dob tcpdump on xdp-remote. Ugyanazt a kimenő és bejövő ICMP visszhang kérést kell mutatnia, és le kell állítania az ICMP Echo Reply megjelenítését. De nem látszik. Kiderült, hogy működik XDP_TX számára készült programban xdp-localszükségesinterfész párosításához xdp-remote egy program is hozzá lett rendelve, még ha üres is, és fel lett emelve.
Ha csak az ARP jelenik meg helyette, akkor el kell távolítania a szűrőket (ezért sudo ./stand detach), hagyjuk ping, majd telepítse a szűrőket, és próbálja újra. A probléma a szűrővel van XDP_TX hatással van az ARP-re is, és ha a verem
névterek xdp-test sikerült "elfelejteni" a 192.0.2.1 MAC címet, nem fogja tudni feloldani ezt az IP-t.
Probléma nyilatkozat
Térjünk át a megadott feladatra: írjunk egy SYN cookie-mechanizmust az XDP-n.
A SYN flood továbbra is népszerű DDoS támadás, amelynek lényege a következő. Amikor létrejön a kapcsolat (TCP kézfogás), a szerver SYN-t kap, erőforrásokat foglal le a jövőbeli kapcsolathoz, SYNACK csomaggal válaszol, és vár egy ACK-re. A támadó egyszerűen másodpercenként több ezer SYN-csomagot küld hamisított címekről egy többezer erős botnetben. A szerver a csomag megérkezésekor azonnal kénytelen erőforrásokat lefoglalni, de nagy időkorlát után felszabadítja azokat, aminek következtében a memória vagy a korlátok kimerülnek, az új kapcsolatokat nem fogadják el, és a szolgáltatás nem elérhető.
Ha nem foglal le erőforrásokat a SYN csomagon, hanem csak egy SYNACK csomaggal válaszol, akkor hogyan értheti meg a szerver, hogy a később érkező ACK csomag a nem mentett SYN csomaghoz tartozik? Végül is a támadó hamis ACK-ket is generálhat. A SYN cookie lényege a kódolás seqnum kapcsolati paraméterek címek, portok és változó só hash-jeként. Ha az ACK-nek sikerült megérkeznie a sócsere előtt, újra kiszámolhatja a hash-t, és összehasonlíthatja vele acknum. Kohó acknum a támadó nem tudja, mivel a só tartalmazza a titkot, és a korlátozott csatorna miatt nem lesz ideje átválogatni.
A SYN cookie-kat már régóta implementálták a Linux kernelben, és akár automatikusan is engedélyezhetők, ha a SYN-ek túl gyorsan és tömegesen érkeznek.
Oktatási program a TCP kézfogásról
A TCP bájtfolyamként biztosítja az adatok átvitelét, például a HTTP-kéréseket TCP-n keresztül továbbítják. Az adatfolyamot darabonként csomagokban továbbítják. Minden TCP-csomagnak van logikai zászlója és 32 bites sorszáma:
A zászlók kombinációja határozza meg egy adott csomag szerepét. A SYN jelző azt jelenti, hogy ez a küldő első csomagja a kapcsolaton. Az ACK jelző azt jelenti, hogy a küldő megkapta az összes kapcsolati adatot egy bájt erejéig. acknum. Egy csomagnak több jelzője is lehet, és ezek kombinációjáról nevezik el, például egy SYNACK csomag.
A sorozatszám (seqnum) meghatározza az adatfolyam eltolását a csomagban elküldött első bájthoz. Például, ha az első X bájt adatot tartalmazó csomagban ez a szám N volt, a következő új adatot tartalmazó csomagban N+X lesz. A kapcsolat elején minden fél véletlenszerűen választja ki ezt a számot.
Nyugtázási szám (acknum) - ugyanaz az eltolás, mint a seqnum, de nem az átvitt bájt számát határozza meg, hanem a címzetttől származó első bájt számát, amelyet a küldő nem látott.
A kapcsolat kezdetén a feleknek meg kell állapodniuk seqnum и acknum. A kliens SYN-csomagot küld vele seqnum = X. A szerver egy SYNACK csomaggal válaszol, ahová a sajátját írja seqnum = Y és leleplezi acknum = X + 1. A kliens a SYNACK-re egy ACK csomaggal válaszol, ahol seqnum = X + 1, acknum = Y + 1. Ezt követően kezdődik a tényleges adatátvitel.
Ha a beszélgetőpartner nem nyugtázza a csomag átvételét, a TCP időtúllépéssel újra elküldi.
Miért nem mindig használják a SYN cookie-kat?
Először is, ha egy SYNACK vagy ACK elveszik, meg kell várnia az újraküldést - a kapcsolat létrehozása lelassul. Másodszor, a SYN csomagban - és csakis abban! - számos olyan opció kerül továbbításra, amelyek befolyásolják a kapcsolat további működését. A bejövő SYN csomagokra nem emlékszik, így a szerver figyelmen kívül hagyja ezeket az opciókat, a következő csomagokban a kliens már nem küldi el azokat. A TCP ebben az esetben működhet, de legalább a kezdeti szakaszban a kapcsolat minősége romlik.
A csomagok tekintetében az XDP programnak a következőket kell tennie:
válaszoljon a SYN-re a SYNACK-kal cookie-val;
válasz ACK-t az RST-vel (szakítsa meg a kapcsolatot);
dobja le a többi csomagot.
Az algoritmus pszeudokódja a csomagelemzéssel együtt:
Если это не Ethernet,
пропустить пакет.
Если это не IPv4,
пропустить пакет.
Если адрес в таблице проверенных, (*)
уменьшить счетчик оставшихся проверок,
пропустить пакет.
Если это не TCP,
сбросить пакет. (**)
Если это SYN,
ответить SYN-ACK с cookie.
Если это ACK,
если в acknum лежит не cookie,
сбросить пакет.
Занести в таблицу адрес с N оставшихся проверок. (*)
Ответить RST. (**)
В остальных случаях сбросить пакет.
Egy (*) meg vannak jelölve azok a pontok, ahol kezelni kell a rendszer állapotát – az első szakaszban megteheti ezeket anélkül, hogy egyszerűen végrehajt egy TCP kézfogást egy SYN cookie szekvenciaként történő generálásával.
Helyben (**), amíg nincs asztalunk, kihagyjuk a csomagot.
TCP kézfogás megvalósítás
Csomagelemzés és kódellenőrzés
Hálózati fejlécstruktúrákra van szükségünk: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) és TCP (uapi/linux/tcp.h). Az utolsóhoz kapcsolódó hibák miatt nem tudtam csatlakozni atomic64_t, a szükséges definíciókat be kellett másolnom a kódba.
A C-ben az olvashatóság szempontjából megkülönböztetett összes függvényt be kell illeszteni a hívás helyére, mivel a kernelben lévő eBPF-ellenőrző tiltja a visszaugrást, vagyis a ciklusokat és a függvényhívásokat.
Makró LOG() letiltja a nyomtatást egy kiadási buildben.
A program függvények csővezetéke. Mindegyik kap egy csomagot, amelyben a megfelelő szintű fejléc van kiemelve, pl. process_ether() kitöltésre vár ether. A terepi elemzés eredményei alapján a függvény a csomagot magasabb szintre tudja vinni. A függvény eredménye egy XDP művelet. Míg a SYN és ACK kezelők minden csomagot átengednek.
Kulcssor invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0): vannak olyan végrehajtási utak, amikor a puffer kezdetétől számított tizenharmadik bájt a csomagon kívül van. A felsorolásból nehéz megállapítani, hogy melyik sorról beszélünk, de van egy utasításszám (12) és egy disassembler, amely megmutatja a forráskód sorait:
ami egyértelművé teszi, hogy a probléma az ether. Mindig így lenne.
Válasz a SYN-nek
A cél ebben a szakaszban az, hogy egy helyes SYNACK csomagot állítsunk elő fixen seqnum, amelyet a jövőben a SYN cookie vált fel. Minden változás benne történik process_tcp_syn() és környéke.
A csomag ellenőrzése
Furcsa módon itt van a legfigyelemreméltóbb sor, vagy inkább egy megjegyzés hozzá:
A kód első verziójának megírásakor az 5.1-es kernelt használták, melynek ellenőrzőjénél különbség volt a data_end и (const void*)ctx->data_end. A cikk írásakor az 5.3.1-es kernelnek nem volt ilyen problémája. Lehet, hogy a fordító egy helyi változóhoz máshogy fér hozzá, mint egy mezőhöz. Erkölcsi – egy nagy fészekben a kód egyszerűsítése segíthet.
A hosszúság további rutinellenőrzése a hitelesítő dicsőségére; O MAX_CSUM_BYTES alább.
Cserélje fel a TCP-portokat, IP-címeket és MAC-címeket. A szabványos könyvtár nem érhető el az XDP programból, így memcpy() - egy makró, amely elrejti a Clang intrinsik-et.
Az IPv4 és a TCP ellenőrző összegek megkövetelik az összes 16 bites szó hozzáadását a fejlécekben, és a fejlécek mérete bele van írva, vagyis a fordítás időpontjában nem ismert. Ez azért probléma, mert az ellenőrző nem hagyja ki a normál ciklust a határváltozóig. A fejlécek mérete azonban korlátozott: egyenként legfeljebb 64 bájt. Rögzített számú iterációt tartalmazó ciklust készíthet, amely korán véget érhet.
Megjegyzem, van RFC 1624 arról, hogyan lehet részben újraszámolni az ellenőrző összeget, ha csak a csomagok rögzített szavait változtatjuk meg. A módszer azonban nem univerzális, a megvalósítást nehezebb lenne fenntartani.
Ellenőrző összeg számítási funkció:
#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;
}
Habár size Ha a hívókód ellenőrzi, a második kilépési feltétel szükséges ahhoz, hogy a hitelesítő bizonyítsa a hurok végét.
A 32 bites szavakhoz egy egyszerűbb verzió kerül megvalósításra:
INTERNAL u32
sum16_32(u32 v) {
return (v >> 16) + (v & 0xffff);
}
Az ellenőrző összegek újraszámítása és a csomag visszaküldése:
Funkció carry() ellenőrző összeget készít 32 bites szavak 16 bites összegéből az RFC 791 szerint.
TCP kézfogás ellenőrzése
A szűrő megfelelően hoz létre kapcsolatot netcat, kihagyva az utolsó ACK-t, amire a Linux RST csomaggal válaszolt, mivel a hálózati verem nem kapott SYN-t - SYNACK-re konvertálták és visszaküldték - és az operációs rendszer szempontjából egy olyan csomag érkezett, ami nem nyitott kapcsolatokkal kapcsolatos.
$ sudo ip netns exec xdp-test nc -nv 192.0.2.1 6666
192.0.2.1 6666: Connection reset by peer
Fontos, hogy teljes értékű alkalmazásokkal ellenőrizze és figyelje meg tcpdump on xdp-remote mert pl. hping3 nem reagál a helytelen ellenőrző összegekre.
SYN süti
Az XDP szempontjából maga az ellenőrzés triviális. A számítási algoritmus primitív, és valószínűleg sebezhető a kifinomult támadókkal szemben. A Linux kernel például a kriptográfiai SipHash-t használja, de az XDP-hez való megvalósítása egyértelműen túlmutat e cikk keretein.
Megjelent a külső interakcióhoz kapcsolódó új TODO-khoz:
Az XDP program nem tárolható cookie_seed (a só titkos része) egy globális változóban, szükséged van egy kerneltárolóra, amelynek értéke rendszeresen frissül egy megbízható generátorból.
Ha az ACK csomagban lévő SYN cookie egyezik, akkor nem kell üzenetet nyomtatnia, de emlékeznie kell az ellenőrzött kliens IP-jére, hogy további csomagokat kihagyhasson belőle.
Amíg nincs az ellenőrzött IP-k listája, nem lesz védelem maga a SYN elárasztás ellen, de itt van a reakció az ACK elárasztásra, amelyet ez a parancs indított:
sudo ip netns exec xdp-test hping3 --flood -A -s 1111 -p 2222 192.0.2.1
Néha az eBPF általában és különösen az XDP inkább fejlett rendszergazdai eszköz, mint fejlesztői platform. Valójában az XDP egy eszköz a kernelcsomag-feldolgozás megzavarására, és nem a kernelverem alternatívája, mint például a DPDK és más kernel-bypass opciók. Másrészt az XDP meglehetősen bonyolult logika megvalósítását teszi lehetővé, amely ráadásul könnyen frissíthető a forgalomfeldolgozás szüneteltetése nélkül. A hitelesítő nem okoz nagy problémákat, én személy szerint nem utasítanám el a felhasználói terület kód részeinél.
A második részben, ha érdekes a téma, kiegészítjük az ellenőrzött kliensek táblázatát és megszakítjuk a kapcsolatokat, implementáljuk a számlálókat és írunk egy userspace segédprogramot a szűrő kezelésére.