eXpress Data Path (XDP) tehnoloogia võimaldab teostada suvalist liikluse töötlemist Linuxi liidestel enne, kui paketid jõuavad tuumapõhisesse võrgu kokku. XDP rakendamine — DDoS-rünnakute kaitse (CloudFlare), keerukad filtrid, statistika kogumine (Netflix). XDP programmid toimivad eBPF virtuaalses masinas, seega on neil piirangud nii oma koodile kui ka tuuma funktsioonidele sõltuvalt filtritüübist.
Artikkel on mõeldud täiendama mitmeid XDP materjale. Esiteks, neis esitatakse valmis kood, mis kohe ringiga ületab XDP eripärad: on ette valmistatud valideerimiseks või liiga lihtne, et probleeme tekitada. Kui proovida hiljem kirjutada enda koodi nullist, ei mõista, mida teha iseloomulike vigadega. Teiseks, ei käsitleta viise XDP kohapealseks testimiseks ilma VM ja "rahardware"-ta, kuigi neil on oma "komistuskivid". Tekst on suunatud programmeerijatele, kes on tuttavad võrkude ja Linuxiga ning kellele XDP ja eBPF huvi pakub.
Selles osas uurime üksikasjalikult, kuidas XDP-filter kokku pannakse ja kuidas seda testida, seejärel kirjutame lihtsa versiooni tuntud mehhanismist SYN cookies pakettide töötlemise tasemel. Pidagem enne 'valge nimekirja' koostamist.
kontrollitud klientide, nõudluse jälgimist ja filtri haldamist – piisab logidest.
Kirjutame C keeles – see ei ole moes, aga on praktiline. Kood on saadaval GitHubis lingil lõpus ja jaotatud commitideks etappide kaupa, nagu artiklis kirjeldatud.
Disclaimer. Artikli käigus arendatakse välja mini-lahendus DDoS-rünnakute tõrjumiseks, kuna see on realistlik ülesanne XDP jaoks ja minu valdkond. Siiski on peamine eesmärk mõista tehnoloogiat, see pole valmis kaitse loomise juhend. Õppimiskood ei ole optimeeritud ja hüppab mööda mõnest nüansist.
XDP lühike ülevaade
Toon välja ainult võtmehetked, et mitte dubleerida dokumentatsiooni ja olemasolevaid artikleid.
Nii laaditakse tuuma filterkood. Filter saab sissetulevaid pakette. Lõppkokkuvõttes peab filter otsustama: edastada pakett tuuma (XDP_PASS), tagasi lükata pakett (XDP_DROP) või saata see tagasi (XDP_TX). Filter võib paketti muuta, mis on eriti oluline. XDP_TX. Samuti saab programmi äkki peatada (XDP_ABORTED) ja paketti lähtestada, kuid see sarnaneb assert(0) — silumise jaoks.
eBPF (laiendatud Berkley paketifilter) virtuaalne masin on spetsiaalselt loodud lihtsana, et südamik saaks kontrollida, et kood ei jää kinni ja ei kahjusta teise mäluruumi. Kogumipiirangud ja kontrollid:
- Tsükli kasutamine (tagasipöördumised) on keelatud.
- Andmete jaoks on virn, kuid funktsioone ei ole (kõik C funktsioonid peavad olema sisse kodeeritud).
- Mälu viitamine virnast ja paketi puhvrist väljapoole on keelatud.
- Koodi suurus on piiratud, kuid praktikas pole see väga oluline.
- Lubatud on ainult südamiku spetsiaalsete funktsioonide (eBPF abistajate) kutsumised.
Filtri arendamine ja seadistamine näeb välja selline:
- Allika kood (näiteks,
kernel.c) kompileeritakse eBPF virtuaalmasina arhitektuuri jaoks objektiks (kernel.o). 2019. aasta oktoobris toetab eBPF kompileerimist Clang ja GCC 10.1 peaks lubama. - Kui antud objekti kodeerimisel on tuumastruktuuride (nt tabelite ja loendurite) viidatud, siis nende ID-d on nullid, mis tähendab, et sellist koodi ei saa täita. Enne laadimist tuumasse tuleb need nullid asendada konkreetsete objektide ID-dega, mis on loodud tuuma kutsete kaudu (linkida kood). Seda saab teha väliste utiliitide abil või kirjutada programmi, mis linkib ja laadib konkreetse filtri.
- Tuum kontrollib laadimist. Kontrollitakse silmuste puudumist ja paketi ja stack'i piiride ületamist. Kui valideerija ei suuda tõestada, et kood on korrektne, lükatakse programm tagasi, — tuleb osata teda rahuldada.
- Pärast eduka valideerimise korral tuum kompileerib eBPF arhitektuuri objekti koodi süsteemi arhitektuuri masina koodiks (just-in-time).
- Programm kinnitatakse liidese külge ja hakkab pakette töötlema.
Kuna XDP töötab tuumas, toimub tõrkeotsing jälgimislogide ja tegelikult kanaliseeritavate või genereeritavate pakettide kaudu. Siiski tagab eBPF laaditud koodi turvalisuse süsteemile, seega saab XDP-d katsetada otse lokaalses Linuxis.
Keskkonna ettevalmistamine
Koostamine
Clang ei saa otse eBPF arhitektuuri jaoks objekti kodeerimist välja anda, seega protsess koosneb kahest sammust:
- Kood C-s tuleb kompileerida LLVM-baitkoodi (
clang -emit-llvm). - Muutke baitkood eBPF objektikodeeringuks (
llc -march=bpf -filetype=obj).
Filtri kirjutamisel on kasulik paar faili abifunktsioonide ja makrodega . Oluline on, et need vastaksid kerneliversioonile (KVER). Laadime need alla 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 Arch Linuxile (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 ./*.oKDIR sisaldab teed kernelipeadete juurde, ARCH – süsteemi arhitektuur. Teed ja tööriistad võivad jaotustes veidi erineda.
Näide erinevustest 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 ühendab katalooge koos abipealkirjadega ja mitu katalooge tuumapealkirjadega. Sümbol __KERNEL__ tähendab, et UAPI (kasutajaliidese API) pealkirjad määratakse tuumakoodi jaoks, kuna filtrit rakendatakse tuumas.
Virustõrje saab välja lülitada (-fno-stack-protector), kuna eBPF koodi valideerija kontrollib siiski kuhjumise ületamist. Kohe on mõistlik aktiveerida optimeerimised, kuna eBPF baitkoodi suurus on piiratud.
Alustame filtriga, mis laseb läbi kõik paketid ja ei tee midagi:
#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";Meeskond make kogub xdp_filter.o. Kuidas seda nüüd testida?
Teststand
Seade peaks sisaldama kahte liidest: ühte, kus on filter, ja kust saadetakse pakette. Need peavad olema täisfunktsionaalsed Linuxi seadmed oma IP-dega, et kontrollida, kuidas tavad rakendused töötavad meie filtriga.
Veth (virtuaalne Ethernet) seadmed sobivad meile: need on paar virtuaalset võrgu liidest, mis on otseselt "ühendatud". Saame neid luua nii (selles osas kõik käsud ip teostatakse root):
ip link add xdp-remote type veth peer name xdp-localSiit xdp-remote ja xdp-local — seadmete nimed. Peale xdp-local (192.0.2.1/24) liidetakse filter, kus xdp-remote (192.0.2.2/24) suunatakse sissetulev liiklus. Siiski on probleem: liidesed asuvad ühel masinal ja Linux ei saada liiklust ühelt neist teisele. Seda saab lahendada nutikate reeglitega. iptables, kuid nad peavad pakette muutma, mis muudab tõrkeotsingu keeruliseks. Paremini kasutada võrgu nimeruume (network namespaces, edaspidi netns).
Võrgunime ruum sisaldab liideseid, marsruutimistabeleid ja NetFilter reegleid, mis on teistest netns-idest eraldatud. Iga protsess töötab mingis nimeruumis ja tal on juurdepääs ainult sellele netns-ile. Vaikimisi on süsteemis üksainus võrgunime ruum kõigi objektide jaoks, seega saab Linuxis töötada ilma netns-i teadmiseta.
Loome uue nime ruumi xdp-test ja liigutame sinna xdp-remote.
ip netns add xdp-test
ip link set dev xdp-remote netns xdp-testSiis protsess, mis töötab xdp-test, ei näe« xdp-local (see jääb vaikimisi netns-i) ja paketi saatmisel aadressile 192.0.2.1 edastab see selle kaudu xdp-remote, kuna see on ainus liides aadressil 192.0.2.0/24, mis on sellele protsessile kättesaadav. See kehtib ka vastupidises suunas.
Netns'i vahetamisel lakkab liides tööle ja kaotab aadressi. Liidese seadistamiseks netns'is tuleb käivitada ip ... selles käsurea nimede ruumis 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 upNagu näha, ei erine see seadistamisest xdp-local vaikimisi nimede ruumis:
ip address add 192.0.2.1/24 dev xdp-local
ip link set xdp-local upKui käivitada tcpdump -tnevi xdp-local, näeme, et paketid, mis on saadetud xdp-test, jõuavad sellele liidesele:
ip netns exec xdp-test ping 192.0.2.1On mugav käivitada shell xdp-test. Repositooriumis on skript, mis automatiseerib stendi tööd, näiteks saab seista seadistada käsuga sudo ./stand up ja eemaldada selle sudo ./stand down.
Jälgimine
Filter seondub seadmele nii:
ip -force link set dev xdp-local xdp object xdp_filter.o verboseAla -force on vajalik, et siduda uus programm, kui teine on juba seotud. «No news is good news» ei kehti selle käsu kohta, väljund on igal juhul mahukas. Viitamine verbose ei ole kohustuslik, kuid koos sellega ilmub koodivaatleja tööaruande loend assembleriga:
Verifikaatori analüüs:
0: (b7) r0 = 2
1: (95) exitProgrammi eemaldamine liideselt:
ip link set dev xdp-local xdp offSkriptis on need käsud sudo ./stand attach ja sudo ./stand detach.
Filtri rakendamisel saab kinnitada, et ping see töötab, aga kas programm töötab? Lisame logid. Funktsioon on sarnane printf(), kuid toetab vaid kolme argumenti lisaks mustrile ja piiratud nimekirja specifieritest. Makros bpf_printk() lihtsustab kutsumist.
SEC("prog")
int xdp_main(struct xdp_md* ctx) {
+ bpf_printk("saadi pakk: %pn", ctx);
return XDP_PASS;
}Väljund suunatakse tuuma jälgimiskanale, mis tuleb aktiveerida:
echo -n 1 | sudo tee /sys/kernel/debug/tracing/options/trace_printkSõnumivoogude vaatamine:
cat /sys/kernel/debug/tracing/trace_pipeMõlemad käsud teevad kutsumise sudo ./stand log.
Ping peaks nüüd genereerima selliseid sõnumeid:
-110930 [004] ..s1 78803.244967: 0: saadi pakk: 00000000ac510377Kui vaadata valideerija väljundit, võib märgata kummalisi arvutusi:
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#6Asi on see, et eBPF programmide andmesection puudub, seega ainus viis formaadi stringi kodeerida on VM-i käsu immediate-argumendid:
$ python -c "import binascii; print(bytes(reversed(binascii.unhexlify('0a7025203a74656b63617020746f67'))))"
b'got packet: %pn'Seetõttu paisutab silumise väljund oluliselt lõppkoodi.
XDP paketite saatmine
Muudame filtri: laske tal kõik sissetulevad paketid tagasi saata. See ei ole võrgus õigesti, kuna aadresse tuleks pealkirjades muuta, kuid praegu on oluline, et see töötab üldiselt.
bpf_printk("saime paketi: %pn", ctx);
- return XDP_PASS;
+ return XDP_TX;
}Käivitage tcpdump järgnevaga xdp-remote. See peaks näitama identseid väljaminevaid ja sissetulevaid ICMP Echo Request ja lõpetama ICMP Echo Reply kuvamise. Kuid ei kuva. Selgub, et selle tööks XDP_TX programmis xdp-local , et paarisliidesele xdp-remote tuleb samuti määrata programm, olgu või tühi, ja see peab olema üles tõstetud.
Kust ma seda teadsin?
on võimalik perf events mehhanismi abil, kes muide kasutab sama virtuaalmasinat, st eBPF-i probleemide lahendamisel kasutatakse eBPF-d.
Sa pead tegema head kurjast, sest head pole enam kuhugi võtta.
$ 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])Mis on kood 6?
$ errno 6
ENXIO 6 Sellist seadet või aadressi ei oleFunction veth_xdp_flush_bq() näitab veakoodi veth_xdp_xmit(), kus otsime ENXIO ja leiame kommentaari.
Taastame minimaalne filter (XDP_PASS) failis xdp_dummy.c, lisame selle Makefile'i, seome selle xdp-remote:
ip netns exec remote
ip link set dev int xdp object dummy.oNüüd tcpdump näitab seda, mida oodati:
62:57:8e:70:44:64 > 26:0e:25:37:8f:96, ethertype IPv4 (0x0800), pikkus 98: (tos 0x0, ttl 64, id 13762, offset 0, flags [DF], proto ICMP (1), pikkus 84)
192.0.2.2 > 192.0.2.1: ICMP echo request, id 46966, seq 1, pikkus 64
62:57:8e:70:44:64 > 26:0e:25:37:8f:96, ethertype IPv4 (0x0800), pikkus 98: (tos 0x0, ttl 64, id 13762, offset 0, flags [DF], proto ICMP (1), pikkus 84)
192.0.2.2 > 192.0.2.1: ICMP echo request, id 46966, seq 1, pikkus 64Kui selle asemel kuvatakse ainult ARP, tuleb filtrid eemaldada (seda teeb sudo ./stand detach), lasta ping, seejärel seada filtrid ning proovida uuesti. Probleem on selles, et filter XDP_TX kehtib ka ARP-le ja kui
nime ruum xdp-test on suutnud «unustada» MAC-aadressi 192.0.2.1, ei suuda see IP-d lahendada.
Ülesande seadmine
Liigume edasi nimetatud ülesande juurde: kirjutada XDP-s SYN küpsiseid.
Siiani on endiselt populaarne DDoS-rünnak SYN flood, mille olemus on järgmine. Ühenduse loomisel (TCP kätevahetus) saab server SYN-i, eraldab ressursse tulevase ühenduse jaoks, vastab SYNACK-paketiga ja ootab ACK-d. Ründaja saadab lihtsalt sün-paketid valeaadressidelt, ulatudes tuhandeid sekundis igast hostist tuhandest zombist. Server on sunnitud eraldama ressursse kohe paketi saabudes, kuid vabastab need pika ootetähtaega, mille tulemusena lõpevad mälu või piirid, uued ühendused ei ole vastu võetud, teenus on kättesaamatu.
Kui mitte eraldada ressursse sün-pakettide jaoks, vaid lihtsalt vastata SYNACK-paketiga, kuidas server siis mõista, et hiljem saabunud ACK-pakett kuulub sün-paketile, mis ei olnud salvestatud? Lõppude lõpuks võib ründaja genereerida ka vale ACK-e. SYN cookie olemus on kodeerida seqnum ühenduse parameetrid nagu hash aadressidest, portidest ja muutuvast soolast. Kui ACK jõudis enne soola vahetust, saab uuesti hash'i arvutada ja võrrelda acknum. Ründaja ei saa seda valeandmete genereerimise tõttu teha, kuna sool sisaldab salajast teavet ja ta ei suuda seda läbi põrgatusega saavutada piiratust tõttu. acknum ründaja ei saa, kuna sool sisaldab saladust ja ei pruugi piiratud kanali tõttu läbi kümber mängida.
SYN cookie on juba ammu rakendatud Linuxi tuumas ja võib automaatselt aktiveeruda, kui SYN-sid saadetakse liiga kiiresti ja massiliselt.
TCP käepide ülevaade
TCP tagab andmete ülekande byte'ide voona, näiteks HTTP-päringud saadetakse TCP üle. Voog edastatakse pakettidena osade kaupa. Kõigil TCP-pakettidel on loogilised lipud ja 32-bitised järjestuse numbrid:
Lipude kombinatsioon määrab konkreetse paketi rolli. SYN-lipp tähendab, et see on saatja esimene pakett ühenduses. ACK-lipp tähendab, et saatja on saanud kõik ühenduse andmed kuni byte'ini.
acknum. Paketil võivad olla mitu lippu ja seda nimetatakse nende kombinatsiooni järgi, näiteks SYNACK-pakett.Järjestuse number (seqnum) määrab andmevoos esimese byte'i nihke, mis edastatakse selles paketis. Näiteks, kui esimeses paketis on X byte andmeid ja selle numbri väärtus oli N, siis järgmises paketis uute andmete puhul on see N+X. Ühenduse alguses valib iga osapool selle numbri juhuslikult.
Tunnustuse number (acknum) – sama nihke nagu seqnum, kuid määrab mitte edastatud byte'i numbri, vaid vastuvõtja esimest byte'i, mida saatja ei ole näinud.
Ühenduse alguses peavad pooled kokku leppima seqnum ja acknum. Klient saadab SYN-paketi enda seqnum = X. Server vastab SYNACK-paketi, kuhu kirjutab oma seqnum = Y ja määrab acknum = X + 1. Klient vastab SYNACK'ile ACK-paketiga, kus seqnum = X + 1, acknum = Y + 1. Pärast seda algab andmete edastamine.
Kui vestluspartner ei kinnita paketi vastuvõtmist, saadab TCP selle ajalõpu järgi uuesti.
Miks SYN küpsiseid ei kasutata alati?
Esiteks, kui SYNACK või ACK kaob, tuleb oodata uuesti saatmist – see aeglustab ühenduse loomist. Teiseks, SYN-paketis – ja ainult selles! – edastatakse rida valikuid, mis mõjutavad ühenduse edasist tööd. Kui server ei mäleta sisenevaid SYN-pakette, ignoreerib see seega neid valikuid ning järgmistes pakettides ei saada klient neid enam. TCP võib sel juhul töötada, kuid vähemalt algfaasis väheneb ühenduse kvaliteet.
Pakettide seisukohalt peab XDP-programm tegema järgmist:
- SYN-le vastata SYNACK koos küpsisega;
- ACK-le vastata RST (ühenduse katkestamine);
- teised paketid kõrvaldada.
Pseudo-koodi algoritm koos paketi analüüsiga:
Kui see ei ole Ethernet,
jäta pakkumine vahele.
Kui see ei ole IPv4,
jäta pakkumine vahele.
Kui aadress on kontrollitud tabelis, (*)
vähenda järelejäänud kontrollide arvu,
jäta pakkumine vahele.
Kui see ei ole TCP,
eemalda pakkumine. (**)
Kui see on SYN,
vasta SYN-ACK koos küpsisega.
Kui see on ACK,
kui acknum sisaldab mitte küpsist,
eemalda pakkumine.
Lisa tabelisse aadress koos N järelejäänud kontrollidega. (*)
Vasta RST. (**)
Muudel juhtudel eemalda pakkumine.Üks (*) on märgitud punktid, kus tuleb hallata süsteemi olekut - esimeses etapis võib neist loobuda, lihtsalt rakendades TCP handshake koos SYN küpsise genereerimisega seqnum'i asemel.
Koha peal (**), seni kuni meil ei ole tabelit, jätame pakkumise vahele.
TCP handshake'i teostamine
Paketi analüüs ja koodi valideerimine
Me vajame võrguhädaste struktuure: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) ja TCP (uapi/linux/tcp.h). Viimasega mul ei õnnestunud liituda vigade tõttu, mis on seotud atomic64_t, pidin vajalikud määratlused kopeerima koodi.
Kõik funktsioonid, mis C-s on esile tõstetud lugemise mugavuse jaoks, peavad olema paigutatud väljakutsumisekohta, kuna eBPF valideerija tuumas keelab tagasipöörded, see tähendab, et tegelikult, tsüklid ja funktsioonikutsed.
#define INTERNAL static __attribute__((always_inline))Makro LOG() keelab trüki väljundit vabastamise versioonis.
Programm esindab funktsioonide toru. Igaüks neist võtab vastu paketti, kus on eraldatud vastava taseme pealkiri, näiteks, process_ether() ootab, et ether. Analüüsi tulemusena võib funktsioon edastada paketi kõrgemale tasemele. Funktsiooni töö tulemus on XDP tegevus. Seni töötlevad SYN ja ACK kõik paketid edasi.
struct Packet {
struct xdp_md* ctx;
struct ethhdr* ether;
struct iphdr* ip;
struct tcphdr* tcp;
};
INTERNAAL int process_tcp_syn(struct Packet* packet) { return XDP_PASS; }
INTERNAAL int process_tcp_ack(struct Packet* packet) { return XDP_PASS; }
INTERNAAL int process_tcp(struct Packet* packet) { ... }
INTERNAAL int process_ip(struct Packet* packet) { ... }
INTERNAAL 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);
}Pööran tähelepanu A ja B tähistatud kontrollidele. Kui jätta A kommenteerimata, kompileerub programm, kuid laadimise ajal ilmneb verifitseerimise viga:
Verifitseerimise analüüs:
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!Peamine rida invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0): on olemas teid, kus kolmeteistkümnes bait puhvri algusest asub paketi väljas. Loendi põhjal on raske mõista, millise real on jutt, kuid seal on käskluse number (12) ja deassembly, mis näitab algkoodi:
llvm-objdump -S xdp_filter.o | lessAntud juhul viitab see reale
LOG("Ether(proto=0x%x)", bpf_ntohs(ether->h_proto));millest on arusaadav, et probleem on ether. Alati võiks nii olla.
Vastus SYN
Eesmärk sel etapil on koostada korrektne SYNACK-pakett koos fikseeritud seqnum, mis tulevikus asendatakse SYN cookie'ga. Kõik muudatused toimuvad process_tcp_syn() ja selle ümbruses.
Paketi kontrollimine
Kuidas kummaline, siin on kõige silmapaistvam rida, täpsemalt, sellele rida kommentaar:
/* Required to verify checksum calculation */
const void* data_end = (const void*)ctx->data_end;Esimese koodi versiooni kirjutamisel kasutati kernelit 5.1, mille verifitseerija jaoks oli vahe data_end ja (const void*)ctx->data_end. Artikli kirjutamise ajal ei olnud kernel 5.3.1 sellist probleemi. Võib-olla käitus kompilaator lokaalse muutuja osas teisiti kui väljanäitajaga. Moraal — suure sügava kihi puhul võib koodi lihtsustamine aidata.
Järgmisena rutiinsed kontrollid pikkuste osas verifitseerija heaks; MAX_CSUM_BYTES alla.
const u32 ip_len = ip->ihl * 4;
if ((void*)ip + ip_len > data_end) {
return XDP_DROP; /* vigane pakett */
}
if (ip_len > MAX_CSUM_BYTES) {
return XDP_ABORTED; /* rakenduse piirang */
}
const u32 tcp_len = tcp->doff * 4;
if ((void*)tcp + tcp_len > (void*)ctx->data_end) {
return XDP_DROP; /* vigane pakett */
}
if (tcp_len > MAX_CSUM_BYTES) {
return XDP_ABORTED; /* rakenduse piirang */
}Paketi pööramine
Täidame seqnum ja acknum, seadistame ACK (SYN on juba seadistatud):
const u32 cookie = 42;
tcp->ack_seq = bpf_htonl(bpf_ntohl(tcp->seq) + 1);
tcp->seq = bpf_htonl(cookie);
tcp->ack = 1;Vahetame TCP portide, IP aadressi ja MAC-aadresside kohad. Tavaline teek on XDP-programmist kättesaamatu, seetõttu memcpy() — makro, mis peidab Clang'i sisemist funktsionaalsust.
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);Kontrollsummade ümberarvutamine
IPv4 ja TCP kontrollsummad vajavad kõigi 16-bitiste sõnade liitmist pealkirjades, samas kui pealkirjade suurus on neis kirjas, st kompileerimise hetkel teadmata. See on probleem, kuna valideerija ei luba tavalist tsüklit kuni muutuva piirini. Samas on pealkirjade suurus piiratud: igaühe maksimaalne maht on 64 baiti. Tsükli saab teha fikseeritud iteratsioonide arvuga, mis võib lõppeda enneaegselt.
Tahan märkida, et on kuidas kontrollsummat osaliselt ümber arvutada, kui on muudetud ainult fikseeritud sõnu pakettides. Kuid see meetod ei ole universaalne, ja selle rakendamine oleks keerulisem hooldada.
Kontrollsummade 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;
}Hoolimata sellest, et size on seda kontrollitud kutsuva koodi poolt, on teine väljunditingimus vajalik, et valideerija saaks tõestada tsükli lõpetamist.
32-bitiste sõnade jaoks on lihtsustatud versioon:
INTERNAL u32
sum16_32(u32 v) {
return (v >> 16) + (v & 0xffff);
}Tegelik kontrollsummade ümberarvutamine ja paketi saatmine tagasi:
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;Function carry() muudab 32-bit summast 16-bit sõnade kontrollsummaks, vastavalt RFC 791-le.
TCP käepigistuse kontroll
Filter loob õigesti ühenduse netcat, jättes vahele lõpp ACK, millele Linux vastas RST-paketiga, kuna võrgu jätk ei saanud SYN-i — see muudeti SYNACK-iks ja saadeti tagasi — ja operatsioonisüsteemi vaatepunktist saabus pakett, mis ei kuulunud avatud ühenduste hulka.
$ sudo ip netns exec xdp-test nc -nv 192.0.2.1 6666
192.0.2.1 6666: Ühendus taastati poolusestOn oluline kontrollida just täielike rakendustega ja jälgida tcpdump järgnevaga xdp-remote sest näiteks hping3 ei reageeri vigaste kontrollsummadele.
SYN cookie
XDP-st vaatenurgast on kontroll triviaalne. Arvutamisalgorütm on primitiivne ja tõenäoliselt haavatav peene ründaja suhtes. Linuxi kärn, näiteks, kasutab krüptograafilist SipHash'i, kuid selle rakendamine XDP-le on ilmselgelt artikli piiridest väljas.
Ilmusid uued TODO-d, mis on seotud välise suhtlemisega:
XDP programm ei saa säilitada
cookie_seed(salajane soola) globaalsetes muutuja, peavad olema tuumale salvestamisruum, mille väärtust värskendatakse regulaarselt usaldusväärsest generaatorist.Kui SYN küpsis vastab ACK paketi sees, ei tohiks sõnumit välja trükkida, vaid peab meelde jätma kontrollitud kliendi IP, et hiljem lubada temalt pakettide läbimine.
Legitiimse kliendi kontroll:
$ sudo ip netns exec xdp-test nc -nv 192.0.2.1 6666
192.0.2.1 6666: Yhendus on partneri poolt tulemustanudLogides on fikseeritud kontrolli läbimine (flags=0x2 — see on SYN, flags=0x10 — see on 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)
küpsis vastab kliendile 20200c0Praegu pole kontrollitud IP-de loendit, traditsiooniline kaitse SYN floodi vastu ei toimi, kuid siin on reaktsioon ACK floodile, mis käivitatakse sellise käsuga:
sudo ip netns exec xdp-test hping3 --flood -A -s 1111 -p 2222 192.0.2.1Logis olevad kirjed:
Ether(proto=0x800)
IP(src=0x15bd11a dst=0x15bd11e proto=6)
TCP(sport=3236 dport=2222 flags=0x10)
küpsis ei vastaKokkuvõte
Mõnikord esindab eBPF üldiselt ja XDP eelkõige rohkem edasijõudnud administraatori tööriista kui arendamise platvormi. XDP on tõepoolest tööriist, mis sekkub tuumapakettide käitlusse, mitte alternatiiv tuumastakile nagu DPDK ja muud kernel bypass variandid. Teisest küljest võimaldab XDP rakendada üsna keerukat loogikat, mida saab kergesti uuendada ilma liikluse töötlemises pausita. Verifier ei tekita suuri probleeme, isiklikult ei loobuks ma sellisest lahendusest userspace-koodi osade jaoks.
Teises osas, kui teema on huvitav, viime lõpuni kontrollitud klientide tabeli ja ühenduste katkestuse, rakendame loendurid ning kirjutame userspace utiliidi filtri haldamiseks.
Lingid:
Allikas: habr.com
