Teknologjia eXpress Data Path (XDP) lejon ekzekutimin e përpunimeve të arbitrave të trafikut mbi ndërfaqet Linux para se paketat të hyjnë në strukturën e rrjetit të kernelit. Përdorimi i XDP — mbrojtje kundër sulmeve DDoS (CloudFlare), filtrore të komplikuara, mbledhja e statistikave (Netflix). Programet XDP ekzekutohen nga makina virtuale eBPF, prandaj kanë kufizime si mbi kodin e tyre, ashtu edhe mbi funksionet e disponueshme të kernelit, në varësi të tipit të filtrit.
Ky artikull ka për qëllim të mbushë boshllëqet e materialeve të shumta mbi XDP. Së pari, në ato materiale jepet një kod i gatshëm, i cili përshkruan menjëherë karakteristikat e XDP-së: i përgatitur për verifikim ose shumë i thjeshtë për të shkaktuar probleme. Kur provohet që të shkruhet kodi nga e para, nuk ka kuptim se çfarë duhet bërë me gabimet karakteristike. Së dyti, nuk përmenden metodat për të testuar lokalmente XDP pa VM dhe „harduer“, ndërkohë që ata kanë „pengesa“ të tyret. Teksti është i orientuar për programuesit e njohur me rrjetet dhe Linux, të cilëve u intereson XDP dhe eBPF.
Në këtë pjesë do të shqyrtojmë në detaje si ndërtohet filtri XDP dhe si mund ta testojmë, pastaj do të shkruajmë një version të thjeshtë të mekanizmit të njohur të cookies SYN në nivelin e përpunimit të paketave. Nuk do të krijojmë ende „lista të bardha“
të klientëve të verifikuar, për të mbajtur numëruesit dhe për të menaxhuar filtrin — mjaft janë logjet.
Do të shkruajmë në C — kjo nuk është moderne, por është praktike. I gjithë kodi është i disponueshëm në GitHub në lidhjen në fund dhe është i ndarë në komitete sipas etapave të përshkruara në artikull.
Diskleimer. Gjatë artikullit do të zhvillohet një zgjidhje mini për të reflektuar nga sulmet DDoS, sepse kjo është një detyrë realiste për XDP dhe fusha ime. Megjithatë, objekti kryesor është të kuptohet teknologjia, kjo nuk është një udhëzues për krijimin e mbrojtjes së gatshme. Kodi edukativ nuk është optimizuar dhe lë jashtë disa nuanca.
Përmbledhje e shkurtër e XDP
Do të përshkruaj vetëm momentet kryesore, për të mos kopjuar dokumentacionin dhe artikujt ekzistues.
Pra, kodi i filtrit ngarkohet në bërthamë. Filtri merr paketat hyrëse. Si rezultat, filtri duhet të marrë një vendim: të lejojë paketën në bërthamë (XDP_PASS), të heqë paketën (XDP_DROP) ose ta dërgojë atë përsëri (XDP_TX). Filtri mund të modifikojë paketën, kjo është veçanërisht e rëndësishme për XDP_TX. Gjithashtu mund të ndërpresë programin për herë emergjente (XDP_ABORTED) dhe të heqë paketën, por kjo është ekuivalente me assert(0) — për debugging.
Makinë virtuale eBPF (Extended Berkley Packet Filter) është dizajnuar në mënyrë që bërthama të mund të verifikojë se kodi nuk është në cikël dhe nuk dëmton memorjen e të tjerëve. Kufizimet dhe verifikimet e përbashkëta:
- Ciklet (kthimet prapa) janë të ndaluara.
- Ka një stack për të dhëna, por nuk ka funksione (të gjitha funksionet C duhet të integrohen).
- Janë të ndaluara qasjet në memorje jashtë stack-ut dhe buffers-it të paketave.
- Madhësia e kodit është e kufizuar, por në praktikë kjo nuk është shumë e rëndësishme.
- Lejohen thirrjet vetëm për funksione të veçanta të bërthamës (eBPF helpers).
Zhvillimi dhe instalimi i filtrit duken kështu:
- Kodi burimor (p.sh.,
kernel.c) kompilohet në objekt (kernel.o) për arkitekturën e makinës virtuale eBPF. Që në tetor 2019, kompilimi në eBPF mbështetet nga Clang dhe pritet në GCC 10.1. - Nëse ky kod objektiv ka qasje në strukturat e bërthamës (p.sh., në tabelat dhe numëruesit), vendet e tyre ID janë zero, pra duke i ekzekutuar një kod të tillë nuk është e mundur. Para se të ngarkohet në bërthamë, duhet që këto zero të zëvendësohen me ID të objekteve konkrete, të krijuara përmes thirrjeve të bërthamës (të lidhen kodet). Kjo mund të bëhet me utilitete të jashtme, ose mund të shkruhet një program që do të lidhe dhe ngarkojë filtrin konkret.
- Bërthama verifikon programin e ngarkuar. Kontrollohet mungesa e cikleve dhe mosdalja jashtë kufijve të paketave dhe stack-ut. Nëse verifikuesi nuk mund të provojë se kodi është korrekt, programi refuzohet - duhet të jeni në gjendje ta kënaqi atë.
- Pas një verifikimi të suksesshëm, bërthama kompilon kodin objektiv të arkitekturës eBPF në kod makine për arkitekturën sistemore (just-in-time).
- Programi lidhet me ndërfaqen dhe fillon të përpunojë paketat.
Duke qenë se XDP punon në bërthamë, debagimi bëhet përmes log-eve të gjurmimit dhe, në fakt, përmes paketave që programi filtrin ose gjeneron. Megjithatë, eBPF siguron sigurinë e kodit të ngarkuar për sistemin, prandaj mund të eksperimentoni me XDP direkt në Linux-in tuaj lokal.
Përgatitja e mjedisit
Ndërtimi
Clang nuk mund të lëshojë drejtpërdrejt kod objektiv për arkitekturën eBPF, kështu që procesi përbëhet nga dy hapa:
- Kompiloni kodin në C në byte-code LLVM (
clang -emit-llvm). - Transformoni byte-code në kod objektiv eBPF (
llc -march=bpf -filetype=obj).
Gjatë shkruajtes së filtrit, do të nevojiten disa skedarë me funksione ndihmëse dhe makros . Është e rëndësishme që ata të përputhen me versionin e bërthamës (KVER). Shkarkojmë ato 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 për Arch Linux (nëndër për 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 përmban rrugën për kryeformulimet e kernelit, ARCH — arkitekturën e sistemit. Rrugët dhe mjetet mund të ndryshojnë pak midis shpërndarjeve.
Shembulli i ndryshimeve për Debian 10 (nëndër për 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 përfshijnë drejtorinë me kryeformulimet ndihmëse dhe disa drejtorira me kryeformulime kernelit. Simboli __KERNEL__ tregon se kryeformulimet UAPI (userspace API) janë të përcaktuara për kodin e kernelit, pasi filtri ekzekutohet në kernel.
Mbrojtja e stack-ut mund të çaktivizohet (-fno-stack-protector), sepse verifikuesi i kodit eBPF gjithsesi kontrollon për tejkalim të kufijve të stack-ut. Duhet menjëherë të aktivizohen optimizimet, sepse madhësia e kodit eBPF është e kufizuar.
Le ta fillojmë me një filtr që kalon të gjitha paketat dhe nuk bën asgjë:
#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";Ekipa make shkarkon xdp_filter.o. Ku mund ta provojmë tani?
Standi i testimit
Stendi duhet të përmbajë dy interfeces: mbi të cilin do të jetë filtri dhe nga i cili do të dërgohen paketat. Këto duhet të jenë pajisje të plota Linux me IP-të e tyre, për të kontrolluar si funksionojnë aplikacionet e zakonshme me filtrin tonë.
Pajisjet e tipit veth (Ethernet virtual) na përshtaten: ky është një çift interfecesh virtuale të rrjetit, "të lidhura" drejtpërdrejt me njëra-tjetrën. Mund të krijohen kështu (në këtë seksion të gjitha komandat ip në kryhen nga root):
ip link add xdp-remote type veth peer name xdp-localKëtu xdp-remote dhe xdp-local — emrat e pajisjeve. Në xdp-local (192.0.2.1/24) do të lidhet filtri, nga xdp-remote (192.0.2.2/24) do të dërgohet trafiku hyrës. Megjithatë, ka një problem: interfecet janë në të njëjtën makinë, dhe Linux nuk do të dërgojë trafik nga njëra te tjetra. Kjo mund të zgjidhet me rregulla të mençura iptables, por ato do të kenë për të ndryshuar paketat, që është e paprecedent gjatë debugimit. Është më mirë të përdorim hapësirat e emrave të rrjetit (network namespaces, më pas netns).
Hapësira e emrave të rrjetit përmban një grup ndërfaqesh, tabelash rrugëzimi dhe rregullash NetFilter, të izoluara nga objekte të ngjashme në nete të tjera. Çdo proces punon në një hapësirë emri, dhe i janë të aksesueshme vetëm objektet e atij neti. Në mënyrë të parazgjedhur, sistemi ka një hapësirë të vetme emri rrjeti për të gjitha objektet, prandaj mund të punoni në Linux dhe të mos dini për netns.
Të krijojmë një hapësirë të re emri xdp-test dhe ta zhvendosim atje xdp-remote.
ip netns add xdp-test
ip link set dev xdp-remote netns xdp-testAtëherë procesi që ekzekutohet në xdp-test, nuk do të "shohë" xdp-local (ai do të mbetet në netnsin e parazgjedhur) dhe kur dërgohet një paketë në 192.0.2.1, do ta kalojë atë përmes xdp-remote, sepse kjo është e vetmja ndërfaqe brenda 192.0.2.0/24, e disponueshme për këtë proces. Kjo vlen edhe për anën tjetër.
Kur zhvendosesh midis netns, ndërfaqja hiqet dhe humb adresën. Për të konfiguruar ndërfaqen në netns, duhet të nisni ip ... në këtë hapësirë emri të komandës 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 upSiç mund të shihet, kjo nuk ndryshon nga konfigurimi xdp-local në hapësirën e emrit të parazgjedhur:
ip address add 192.0.2.1/24 dev xdp-local
ip link set xdp-local upNëse ekzekutohet tcpdump -tnevi xdp-local, mund të shihet se paketat e dërguara nga xdp-test, dërgohen në këtë ndërfaqe:
ip netns exec xdp-test ping 192.0.2.1Është e dobishme të nisni një shell në xdp-test. Në depo ka një skript që automatizon punën me kioskun, për shembull, mund të konfiguroni kioskun me komandën sudo .\/stand up dhe ta hiqni atë sudo .\/stand down.
Gjurmimi
Filtri lidhet me pajisjen kështu:
ip -force link set dev xdp-local xdp object xdp_filter.o verboseÇelësi -force nevojitet për të lidhur një program të ri, nëse një tjetër është lidhur tashmë. "Përgjigje e mirë është pa lajme të këqija" nuk është për këtë komandë, output është në çdo rast i bollshëm. Të specifikohet verbose nuk është e obligueshme, por me të vjen një raport mbi punën e verifikuesit të kodit me listingun e assemblerit:
Analiza e verifikuesit:
0: (b7) r0 = 2
1: (95) exitPër të shkëputur programin nga ndërfaqja:
ip link set dev xdp-local xdp offNë skript kjo janë komandat sudo .\/stand attach dhe sudo .\/stand detach.
Duke lidhur filtrin, mund të sigurohemi që ping vazhdojnë të punojnë, por a funksionon programi? Le të shtojmë regjistrime. Funksioni është e ngjashme me printf(), por mbështet vetëm deri në tre argumente, përveç modelit, dhe një listë të kufizuar specifikatorësh. Makroja bpf_printk() e thjeshton thirrjen.
SEC("prog")
int xdp_main(struct xdp_md* ctx) {
+ bpf_printk("mora paketë: %pn", ctx);
return XDP_PASS;
}Dalja po daloni e kanalit të gjurmimit të bërthamës, i cili duhet të aktivizohet:
echo -n 1 | sudo tee /sys/kernel/debug/tracing/options/trace_printkShikoni rrjedhën e mesazheve:
cat /sys/kernel/debug/tracing/trace_pipeTë dy këto komanda bëjnë thirrje sudo ./stand log.
Ping tani duhet të shkaktojë mesazhe të tilla në të:
-110930 [004] ..s1 78803.244967: 0: mori paketë: 00000000ac510377Nëse e shqyrtoni daljen e verifikuesit, mund të vini re llogaritje të çuditshme:
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#6Problemi është se programet në eBPF nuk kanë një seksion të dhënash, prandaj mënyra e vetme për të kode formatin e vargjeve është argumentet e menjëhershme të komandave të VM:
$ python -c "import binascii; print(bytes(reversed(binascii.unhexlify('0a7025203a74656b63617020746f67'))))"
b'got packet: %pn'Për këtë arsye, dalja e dëshmuesit tejkap rezultatin përfundimtar.
Dërgimi i paketimeve XDP
Të ndryshojmë filtrin: le të dërgojë të gjitha paketat e ardhshme përsëri. Kjo është e pasaktë nga pikëpamja e rrjetit, sepse do të ishte e nevojshme të ndryshoheshin adresat në header, por tani rëndësia është të punuarit në thelb.
bpf_printk("got packet: %pn", ctx);
- return XDP_PASS;
+ return XDP_TX;
}Po e nisim tcpdump në xdp-remote. Ai duhet të tregojë ICMP Echo Request dalëse dhe ardhëse identike dhe të ndalojë tregimin e ICMP Echo Reply. Por nuk po e bën. Duke u zbuluar, për të punuar XDP_TX në programin në xdp-local , që të ketë një program të caktuar për ndërfaqen përkatëse, edhe nëse është e zbrazët, dhe të jetë aktivizuar. xdp-remote Si e di këtë?
Ndjekja e rrugës së paketave në bërthamë
Duhet të bësh të mirën nga e keqja, sepse nuk ka asgjë tjetër për të bërë.
$ 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])
Çfarë është kodi 6?$ errno 6 ENXIO 6 Nuk ka një pajisje ose adresë të tillë
veth_xdp_flush_bq()Funksioni merr kodin e gabimit nga veth_xdp_xmit() , ku kërkojmë përENXIO dhe gjejmë komentarin. Le t'i rikthejmë filtrin minimal (
) në skedarinXDP_PASSxdp_dummy.c , ta shtojmë në Makefile, ta lidhim meip netns exec remote ip link set dev int xdp object dummy.o xdp-remote:
Tanitregon atë që pritej: tcpdump показывает то, что ожидается:
62:57:8e:70:44:64 > 26:0e:25:37:8f:96, etertype IPv4 (0x0800), gjatësi 98: (tos 0x0, ttl 64, id 13762, offset 0, flags [DF], proto ICMP (1), gjatësi 84)
192.0.2.2 > 192.0.2.1: kërkesë echo ICMP, id 46966, seq 1, gjatësi 64
62:57:8e:70:44:64 > 26:0e:25:37:8f:96, etertype IPv4 (0x0800), gjatësi 98: (tos 0x0, ttl 64, id 13762, offset 0, flags [DF], proto ICMP (1), gjatësi 84)
192.0.2.2 > 192.0.2.1: kërkesë echo ICMP, id 46966, seq 1, gjatësi 64Nëse në vend të kësaj shfaqen vetëm ARP, duhet të hiqni filtrat (kjo e bën sudo .\/stand detach), të lëshoni ping, pastaj të vendosni filtrat dhe të provoni përsëri. Problemi është se filtri XDP_TX vepron edhe mbi ARP, dhe nëse staku
i hapësirave të emrave xdp-test ka harruar 'MAC-adresin' 192.0.2.1, ai nuk do të jetë në gjendje të zgjidhë këtë IP.
Formulimi i detyrës
Le të kalojmë në detyrën e specifikuar: të shkruajmë një mekanizëm SYN cookies në XDP.
Akoma një sulm DDoS i njohur mbetet SYN flood, i cili ka këtë përmbajtje. Kur krijohet një lidhje (TCP handshake), serveri merr SYN, shpërndan burime për lidhjen e ardhshme, përgjigjet me një paketë SYNACK dhe pret ACK. Sulmuesi thjesht dërgon paketa SYN nga adresa të rreme në numra prej mijëra në sekondë nga çdo host në një botnet me mijëra. Serveri është i detyruar të ndajë burime menjëherë me mbërritjen e paketës, dhe i lirohet gjatë një kohe të madhe, si rezultat, memoria ose kufijtë përfundojnë, lidhjet e reja nuk pranohen, shërbimi nuk është i aksesueshëm.
Nëse nuk ndahen burime për çdo paketë SYN, por thjesht përgjigjet me paketën SYNACK, si mund ta kuptojë serveri se paketa ACK, e cila ka ardhur më vonë, i përket paketës SYN, e cila nuk u ruajt? Sepse sulmuesi mund të gjenerojë edhe ACK false. Qëllimi i SYN cookie është të kodejë në seqnum parametrat e lidhjes si një hash nga adresat, portet dhe kripë e ndryshueshme. Nëse ACK arrin para ndryshimit të kripës, mund të llogaritet përsëri hash-i dhe të krahasohet me acknum.Sulmuesi nuk mund ta falsifikojë, pasi kripa përfshin një sekret, dhe nuk do të ketë kohë të provoni për shkak të kanalit të kufizuar. acknum. SYN cookie është zbatuar prej kohësh në bërthamën Linux dhe mund të aktivizohet automatikisht nëse SYN vijnë shumë shpejt dhe masivisht.
Njohuri mbi TCP handshake
TCP siguron transferimin e të dhënave si një rrjedhë bajtësh, për shembull, kërkesat HTTP dërgohen mbi TCP. Rrjedha transmetohet në cope të paketave. Të gjitha paketat TCP kanë flamuj logjikë dhe numra sekuencash 32-bit:
TCP обеспечивает передачу данных как потока байтов, например, поверх TCP передаются HTTP-запросы. Поток передается по кусочкам в пакетах. У всех пакетов TCP есть логические флаги и 32-битные номера последовательностей:
Kombinimi i flamujve përcakton rolin e një paci të veçantë. Flamuri SYN do të thotë se ky është paci i parë i dërguar në një lidhje. Flamuri ACK do të thotë se dërguesi ka marrë të dhënat e gjitha lidhjes deri në byt.
acknum.. Paci mund të ketë disa flamuj dhe quhet sipas kombinimit të tyre, për shembull, paci SYNACK.Numri i sekuencës (seqnum) përcakton offset-in në rrjedhën e të dhënave për bytin e parë që dërgohet në këtë pacë. Për shembull, nëse në pacin e parë janë X byte të dhëna dhe ky numër ishte N, në pacin e ardhshëm me të dhëna të reja ai do të jetë N+X. Në fillim të lidhjes, çdo palë e zgjedh këtë numër në mënyrë të rastit.
Numri i njohjes (acknum) është një offset i ngjashëm me seqnum, por përcakton jo numrin e byteve të dërguara, por numrin e bytit të parë nga marrësi, të cilin dërguesi nuk e ka parë.
Në fillim të lidhjes, palët duhet të bien dakord seqnum dhe acknum.. Klienti dërgon një pacë SYN me seqnum = X. Serveri përgjigjet me pacë SYNACK, ku regjistron seqnum = Y dhe vendos acknum = X + 1. Klienti në SYNACK përgjigjet me pacën ACK, ku seqnum = X + 1, acknum = Y + 1. Pas kësaj fillon në mënyrë faktike transferimi i të dhënave.
Nëse bashkëbiseduesi nuk konfirmon marrjen e pacës, TCP e dërgon atë përsëri pas një kohe.
Pse nuk përdoren gjithmonë SYN cookies?
Së pari, nëse humbet SYNACK ose ACK, do të duhet të pritet për dërgim të përsëritur - vendoset ngadalësimi i krijimit të lidhjes. Së dyti, në pacën SYN - dhe vetëm në të! - dërgohen disa opsione që ndikojnë në funksionimin e mëtejshëm të lidhjes. Duke mos mbajtur mend pacat SYN në hyrje, serveri kështu e injoron këto opsione, në pacat e ardhshme klienti nuk do t'i dërgojë ato. TCP mund të funksionojë në këtë mënyrë, por të paktën në fazën fillestare cilësia e lidhjes do të ulet.
Nga pikëpamja e pacave, programi XDP duhet të bëjë si më poshtë:
- të përgjigjet SYNACK me cookie për SYN;
- të përgjigjet me RST për ACK (të ndërpresë lidhjen);
- të heqë të tjerët.
Pseudokodi i algoritmit së bashku me analizën e pacës:
Nëse kjo nuk është Ethernet,
kaloni paketën.
Nëse kjo nuk është IPv4,
kaloni paketën.
Nëse adresa është në tabelën e kontrolluar, (*)
ulni numrin e provave të mbetura,
kaloni paketën.
Nëse kjo nuk është TCP,
hiqni paketën. (**)
Nëse kjo është SYN,
përgjigjuni me SYN-ACK duke dërguar cookie.
Nëse kjo është ACK,
nëse në acknum nuk është cookie,
hiqni paketën.
Shtoni në tabelën e adresave me N provat e mbetura. (*)
Përgjigjuni me RST. (**)
Në rastet e tjera, hiqni paketën.Një (*) janë shënuar pikat në të cilat duhet të menaxhohet gjendja e sistemit — në fazën e parë mund të shpëtojmë pa to, duke realizuar thjesht TCP handshake me gjenerimin e SYN cookie si seqnum.
Në vendin (**), derisa të kemi tabelë, do të kalojmë paketën.
Realizimi i TCP handshake
Parapregatitja dhe verifikimi i kodit të paketës
Na nevojiten strukturat e titujve të rrjetit: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) dhe TCP (uapi/linux/tcp.h). Të fundit nuk arrita ta lidhja për shkak të gabimeve që lidhen me atomic64_t, isha e detyruar të kopjoj përkufizimet e nevojshme në kod.
Të gjitha funksionet, të cilat në C përkushtohen për lehtësinë e leximit, duhet të jenë të plota në vendin e thirrjes, pasi verifikuesi eBPF në bërthamë ndalon kalimet mbrapsht, pra, në thelb, ciklet dhe thirrjet e funksioneve.
#define INTERNAL static __attribute__((always_inline))Makro LOG() ndalon shtypjen në ndërtimin e lëshimit.
Programi paraqet një tubacion të funksioneve. Çdo funksion merr një paketë, në të cilën është e ndarë një titull përkatës, për shembull, process_ether() pret që të jetë e mbushur me ether. Në rezultate të analizave të fushave, funksioni mund të dërgojë paketën në nivelin më të lartë. Rezultati i punës së funksionit është veprimi XDP. Deri tani, trajtuesit SYN dhe ACK kalojnë të gjitha paketat.
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; /* paketa e keqe */
}
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ërejtje për kontrollimet e shënuara me A dhe B. Nëse komenton A, programi do të mblidhet, por gjatë ngarkimit do të ketë një gabim verifikimi:
Analiza e verifikuesit:
11: (7b) *(u64 *)(r10 -48) = r1
12: (71) r3 = *(u8 *)(r7 +13)
qasje të pavlefshme në paketë, off=13 madhësia=1, R7(id=0,off=0,r=0)
Offseti R7 është jashtë paketës
u përpunuan 11 instruksione (limit 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0
Gabim në marrjen e programit/mapës!Vija kyçe qasje të pavlefshme në paketë, off=13 madhësia=1, R7(id=0,off=0,r=0): ka rrugë ekzekutimi kur byte i trembëdhjetë nga fillimi i tamponit ndodhet jashtë paketës. Nga listingu është e vështirë të kuptohet për cilën vijë bëhet fjalë, por ka numrin e instruksionit (12) dhe disassemblerin që tregon radhët e kodit burimor:
llvm-objdump -S xdp_filter.o | lessNë këtë rast, ai tregon për vijën
LOG("Ether(proto=0x%x)", bpf_ntohs(ether->h_proto));nga e cila është e qartë se problemi është ether. Gjithmonë do të ishte kështu.
Përgjigja ndaj SYN
Qëllimi në këtë hap është të formoni paketën e saktë SYNACK me seqnum, e cila në të ardhmen do të zëvendësohet me një SYN cookie. Të gjitha ndryshimet ndodhin në process_tcp_syn() dhe përreth.
Kontrolli i paketës
Çuditërisht, kjo është vija më e veçantë, më saktësisht, komenti lidhur me të:
/* Required to verify checksum calculation */
const void* data_end = (const void*)ctx->data_end;Gjatë shkruarjes së versionit të parë të kodit u përdor një bërthamë 5.1, për verifikuesin e cilësisë kishte një ndryshim midis data_end dhe (const void*)ctx->data_end. Gjatë shkruarjes së artikullit, bërthama 5.3.1 nuk kishte një problem të tillë. Ndoshta, kompileri u referua në variablin lokal ndryshe nga sa në fushë. Moraliteti - në nivele të thella, thjeshtimi i kodit mund të ndihmojë.
Pastaj kontrollimet rutine të gjatësive për lavdinë e verifikuesit; për MAX_CSUM_BYTES më poshtë.
const u32 ip_len = ip->ihl * 4;
if ((void*)ip + ip_len > data_end) {
return XDP_DROP; /* paketë e deformuar */
}
if (ip_len > MAX_CSUM_BYTES) {
return XDP_ABORTED; /* kufizim në implementim */
}
const u32 tcp_len = tcp->doff * 4;
if ((void*)tcp + tcp_len > (void*)ctx->data_end) {
return XDP_DROP; /* paketë e deformuar */
}
if (tcp_len > MAX_CSUM_BYTES) {
return XDP_ABORTED; /* kufizim në implementim */
}Rivlerësimi i paketës
Plotësojmë seqnum dhe acknum., vendosim ACK (SYN tashmë është vendosur):
const u32 cookie = 42;
tcp->ack_seq = bpf_htonl(bpf_ntohl(tcp->seq) + 1);
tcp->seq = bpf_htonl(cookie);
tcp->ack = 1;Ndryshojmë pozitat e porteve TCP, adresat IP dhe MAC. Biblioteka standarde nuk është e disponueshme nga programi XDP, prandaj memcpy() — makros që fsheh intrinzikun 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);Rivlerësimi i Kontrollit të Shumave
Sasit e kontrollit IPv4 dhe TCP kërkon shumimin e të gjitha fjalëve 16-bit në headers, dhe madhësia e headers është shkruar në to, dmth në momentin e përpilimit nuk është e njohur. Kjo është një problem, sepse verifikatori nuk do të lejojë një cikël të zakonshëm deri në kufirin e ndryshorit. Megjithatë, madhësia e headers është e kufizuar: deri në 64 byte secila. Mund të bëhet një cikël me një numër të fijuar iteraimesh, që mund të përfundojë para kohe.
Vërej se ka për atë se si të ripërcaktojmë kontrollet e shumës pjesërisht, nëse janë ndryshuar vetëm fjalët fikse të paketeve. Megjithatë, mënyra nuk është universale dhe realizimi do të ishte më i vështirë për t'u mbështetur.
Funksioni i llogaritjes së shumës së kontrollit:
#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;
}Megjithatë, size i kontrolluar nga kodi thirrës, kushti i dytë i daljes është i nevojshëm që verifikatori të jetë në gjendje të provojë përfundimin e ciklit.
Për fjalët 32-bit është realizuar një version më i thjeshtë:
INTERNAL u32
sum16_32(u32 v) {
return (v >> 16) + (v & 0xffff);
}Fakti i ripërllogaritjes së kontrollave dhe dërgimi i paketës prapa:
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;Funksioni carry() bën nga shumimi 32-bit i fjalëve 16-bit një shumë kontrolli, sipas RFC 791.
Verifikimi i dorëzimit TCP
Filtri e vendos saktë lidhjen me netcat, duke e kaluar ACK-në përfundimtare, për të cilën Linux u përgjigj me një paketë RST, pasi staku i rrjetit nuk mori SYN - ai u rikonvertua në SYNACK dhe u dërgua përsëri - dhe nga pikëpamja e OS-së, mbërriti një paketë që nuk i përket lidhjeve të hapura.
$ sudo ip netns exec xdp-test nc -nv 192.0.2.1 6666
192.0.2.1 6666: Lidhja u rivendos nga pala tjetërËshtë e rëndësishme të kontrollohet pikërisht me aplikacione të plota dhe të vëzhgohen tcpdump në xdp-remote sepse, për shembull, hping3 nuk reagon ndaj shumave kontrolli të papërshtatshme.
SYN cookie
Nga pikëpamja e XDP, vetë kontrolli është trivial. Algoritmi i llogaritjes është primitiv dhe, ndoshta, i ndjeshëm për një sulmues të sofistikuar. Për shembull, bërthama Linux përdor SipHash kriptografike, por realizimi i tij për XDP është qartë jashtë përmbajtjes së këtij artikulli.
Ndodhi për TODO të reja, të lidhura me ndërveprimin e jashtëm:
Programi XDP nuk mund të ruajë
cookie_seed(pjesa sekrete e krip të kripës) në një ndryshore globale, duhet një ruajtje në bërthamë, vlera e së cilës do të përditësohet periodikisht nga një gjenerator të besueshëm.Kur në kohëzgjatjen e cookie SYN në paketën ACK, duhet të kujdesemi për të mos printuar një mesazh, por të mbajmë mend IP e klientit të verifikuar, në mënyrë që më pas të lejojmë paketat prej tij.
Verifikimi nga një klient legjitim:
$ sudo ip netns exec xdp-test nc -nv 192.0.2.1 6666
192.0.2.1 6666: Koneksioni u rivendos nga partneriNë logat është regjistruar kalimi i verifikimit (flags=0x2 — ky është SYN, flags=0x10 — ky është 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 përputhet për klientin 20200c0Për momentin nuk ka listë të IP-ve të verifikuara, kështu që nuk do të ketë mbrojtje nga sulmet SYN flood, por kjo është reagimi ndaj ACK flood, e cila fillohet nga komanda e tillë:
sudo ip netns exec xdp-test hping3 --flood -A -s 1111 -p 2222 192.0.2.1Shënimet në log:
Ether(proto=0x800)
IP(src=0x15bd11a dst=0x15bd11e proto=6)
TCP(sport=3236 dport=2222 flags=0x10)
cookie nuk përputhetPërfundim
Nganjëherë eBPF, dhe XDP më konkretisht, paraqitet më shumë si një mjet për administratorët e avancuar, sesa si një platformë për zhvillim. Në të vërtetë, XDP është një mjet ndërhyrje në përpunimin e paketave nga bërthama, dhe jo një alternativë për stakun e bërthamës si DPDK dhe opsione të tjera për shpërndarje të bërthamës. Nga ana tjetër, XDP lejon implementimin e logjikës mjaft të komplikuar, e cila gjithashtu mund të përditësohet lehtësisht pa ndërprerje në përpunimin e trafikut. Verifikuesi nuk krijon probleme të mëdha, personalisht nuk do të heqja dorë nga diçka e tillë për pjesët e kodit të userspace.
Në pjesën e dytë, nëse temë interesante, do ta përfundojmë tabelën e klientëve të verifikuar dhe ndarjet e lidhjeve, do të vendosim numërues dhe do të shkruajmë një utilitar userspace për menaxhimin e filtrit.
Lidhjet:
Burimi: habr.com
