Teknologjia eXpress Data Path (XDP) lejon përpunimin e çdo lloji të trafikut në ndërfaqet Linux përpara se paketat të kalojnë në stakun e rrjetit të bërthamës. Përdorimi i XDP — mbrojtje kundër sulmeve DDoS (CloudFlare), filtra të ndërlikuar, mbledhje statistikash (Netflix). Programet XDP ekzekutohen nga makina virtuale eBPF, prandaj kanë kufizime si në kodin e tyre ashtu dhe në funksionalitetet e disponueshme të bërthamës në varësi të llojit të filtrit.
Ky artikull synon të plotësojë mangësitë e shumë materialeve mbi XDP. Së pari, ato ofrojnë kod të gatshëm që menjëherë i kalon veçoritë e XDP: është përgatitur për verifikim ose është shumë i thjeshtë për të shkaktuar probleme. Kur përpiqeni më vonë të shkruani kodin tuaj nga fillimi, nuk ka kuptim se si të veproni me gabimet karakteristike. Së dyti, nuk diskutohen mënyrat për të testuar lokalisht XDP pa VM dhe 'harduer', duke qenë se ato kanë 'gropa' të veta. Teksti është i destinuar për programuesit që janë të njohur me rrjetet dhe Linux-in, të cilëve u intereson XDP dhe eBPF.
Në këtë pjesë do të shqyrtojmë në detaje se si ndërtohet filtri XDP dhe si të testohet, pastaj do të shkruajmë një variant të thjeshtë të mekanizmit të njohur të cookies SYN në nivelin e përpunimit të paketave. Deri atëherë, nuk do të formojmë "listën e bardhë".
klientëve të verifikuar, për të mbajtur numëruesit dhe për të menaxhuar filtrin - mjaft është logjistika.
Do të shkruajmë në C - ndoshta nuk është në modë, por është praktike. Të gjithë kodi është i disponueshëm në GitHub përmes linkut në fund dhe është i ndarë në komitete sipas fazave të përshkruara në artikull.
Përjashtim. Gjatë artikullit do të zhvillohet një zgjidhje të vogël për të reflektuar sulmet DDoS, sepse kjo është një detyrë realiste për XDP dhe fusha ime. Megjithatë, qëllimi kryesor është të kuptohet teknologjia, kjo nuk është një udhëzues për krijimin e një mbrojtjeje të gatshme. Kodi mësimor nuk është optimizuar dhe në disa nuanca është lënë jashtë.
Përmbledhje e shkurtër e XDP
Do të paraqes vetëm pikat kryesore, për të mos e përsëritur dokumentacionin dhe artikujt ekzistues.
Pra, kodi i filtrit ngarkohet në bërthamë. Paketat hyrëse i kalojnë filtrit. Në fund, filtrit i duhet të marrë një vendim: të kalojë paketën në bërthamë (XDP_PASS), ta hedhi paketën (XDP_DROP) ose ta dërgojë atë prapa (XDP_TX). Filtri mund të ndryshojnë paketën, kjo është veçanërisht e rëndësishme për XDP_TX. Gjithashtu, mund të ndërpritet emergjentisht programi (XDP_ABORTED) dhe të rikthehet paketa, por kjo është ekuivalente me assert(0) — për qëllime debguese.
Makinat virtuale eBPF (Extended Berkley Packet Filter) janë bërë të thjeshta, që bërthama të mund të kontrollojë se kodi nuk bllokohet dhe nuk dëmton memorien e të tjerëve. Kufizime dhe verifikime të përbashkëta:
- Ciklet janë të ndaluara (kthime prapa).
- Ka një stack për të dhënat, por nuk ka funksione (të gjitha funksionet C duhet të integrohen).
- Aksesi në memorien jashtë stack-ut dhe tamponit të paketës është i ndaluar.
- Madhësia e kodit është e kufizuar, por në praktikë kjo nuk është shumë e rëndësishme.
- Lejohet thirrja vetëm e funksioneve speciale të bërthamës (ndihmësit e eBPF).
Zhvillimi dhe instalimi i filtrit shihen si:
- Kodi burimor (p.sh.,
kernel.c) kompilohen në objekt (kernel.o) për arkitekturën e makinës virtuale eBPF. Në tetor 2019, kompilimi në eBPF mbështetet nga Clang dhe premtuar në GCC 10.1. - Nëse në këtë kod objekt ka referenca në struktura thelbësore (siç janë tabelat dhe numëruesit), në vend të ID-ve të tyre qëndrojnë zeros, që do të thotë se një kod i tillë nuk mund të realizohet. Para se të ngarkohet në thelb, këto zeros duhet të zëvendësohen me ID-të e objekteve specifike, të krijuara nëpërmjet thirrjeve në thelb (të lidhura me kodin). Kjo mund të bëhet me mjete të jashtme, ose mund të shkruhet një program që do të lidhë dhe ngarkohet filtër konkret.
- Thelbi verifikon programin e ngarkuar. Kontrollohet mungesa e cikleve dhe kalimi përtej kufijve të paketës dhe stack-ut. Nëse verifikuesi nuk mund të provojë që kodi është korrekt, programi refuzohet — është e nevojshme të dish si ta kënaqësh atë.
- Pas verifikimit të suksesshëm, thelbi kompilon kodin objekt të arkitekturës eBPF në kodin e makinës së arkitekturës sistemike (just-in-time).
- Programi lidhet me ndërfaqen dhe fillon të përpunojë paketat.
Për shkak se XDP punon në thelb,_debugimi bëhet përmes logeve të ndjekjes dhe, për më tepër, përmes paketave që programi filtrojnë ose gjeneron. Megjithatë, eBPF siguron sigurinë e kodeve të ngarkuara për sistemin, prandaj mund të eksperimentosh me XDP direkt në Linux-in lokal.
Përgatitja e mjedisit
Ndërtimi
Clang nuk mund të nxjerrë drejtpërdrejt kodin e objektit për arkitekturën eBPF, kështu që procesi përbëhet nga dy hapa:
- Kompiloni kodin në C në kodin byte LLVM (
clang -emit-llvm). - Transformoni kodin byte në kodin e objektit eBPF (
llc -march=bpf -filetype=obj).
Për shkrimin e filtrit do t'ju nevojiten disa skedarë me funksione ndihmëse dhe makrospeciale . Është e rëndësishme që ato të përputhen me versionin e bërthamës (KVER). I 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 (bërthama 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 titujt e bërthamës, ARCH — arkitektura e sistemit. Rrugët dhe mjetet mund të duken pak ndryshe midis distribucioneve.
Shembuj ndryshimesh për Debian 10 (nënshtresa 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 lidhen me drejtorinë e titujve ndihmës dhe disa drejtoritë me titujt e nënshtresës. Simboli __KERNEL__ tregon se titujt UAPI (userspace API) janë të përcaktuar për kodin e nënshtresës, pasi filtrohet në nënshtresë.
Mund të shuhet mbrojtja e stekës (-fno-stack-protector), sepse verifikuesi i kodit eBPF gjithsesi kontrollon daljet jashtë kufijve të stekës. Menjëherë duhet aktivizuar optimizimet, sepse përmasat e kodit eBPF janë të kufizuara.
Le të fillojmë me një filter 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 mbledh xdp_filter.o. Ku do ta provojmë tani?
Njësia e testimit
Njësia duhet të përfshijë dy interfaca: një në të cilën do të jetë filtri dhe një nga e cila do të dërgohen paketat. Këto duhet të jenë pajisje të plota Linux me IP-të e tyre, për të kontrolluar se si aplikacionet e zakonshme punojnë me filtrin tonë.
Pajisjet e tipit veth (Ethernet virtual) na përshtaten: kjo është një çift interfacash virtuale rrjeti, "të lidhura" mes tyre drejtpërdrejt. Mund t'i krijojmë si më poshtë (në këtë seksion të gjitha komandat ip ekzekutohen 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ë bashkëngjitet filtri, me xdp-remote (192.0.2.2/24) do të dërgohet trafiku i hyrës. Megjithatë, ka një problem: ndërfaqet janë në një makinë dhe Linux nuk do të dërgojë trafik në njëra nga ato përmes një tjetër. Kjo mund të zgjidhet me rregulla të mençura. iptables, por do t'u duhet të ndryshojnë paketat, që është e disponueshme gjatë debugging. Më mirë është të përdoren hapësirat e emrave të rrjetit (network namespaces, më vonë netns).
Hapësira e emrave të rrjetit përmban një grup ndërfaqesh, tabela rrugëzim dhe rregulla NetFilter, të izoluara nga objektet e ngjashme në netns të tjera. Çdo proces funksionon në ndonjë hapësirë emri, dhe i janë në dispozicion vetëm objektet e atij netns. Në mënyrë standarde, sistemi ka një hapësirë të vetme emri për të gjitha objektet, kështu që mund të punoni në Linux dhe të mos dini për netns.
Të krijojmë një hapësirë të re emri xdp-test dhe ta transferojmë 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 (do të mbetet në netns në mënyrë standarde) dhe kur dërgon një paketë në 192.0.2.1 do ta kalojë përmes xdp-remote, sepse kjo është e vetmja ndërfaqe në 192.0.2.0/24, e disponueshme për këtë proces. Kjo vlen edhe për anën tjetër.
Kur lëvizni midis netns, interfaci bie dhe humb adresën. Për të konfiguruar interfacin në netns, duhet të nisni ip ... në këtë hapësirë emri 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 emërore të paracaktuar:
ip address add 192.0.2.1/24 dev xdp-local
ip link set xdp-local upNëse ekzekutoni tcpdump -tnevi xdp-local, mund të shihni se paketa, të dërguara nga xdp-test, dorëzohen në këtë interfaci:
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 standin, për shembull, mund të konfiguroni standin me komandën sudo ./stand up dhe ta hiqni atë sudo ./stand down.
Gjetja e rrugës
Filtri lidhët me pajisjen kështu:
ip -force link set dev xdp-local xdp object xdp_filter.o verboseÇelësi -force është e nevojshme për të lidhur një program të ri, nëse një tjetër është lidhur tashmë. «No news is good news» nuk është për këtë komandë, rezultati në çdo rast është voluminoz. Të tregoni verbose nuk është e domosdoshme, por me të vjen një raport mbi punën e verifikuesit të kodit me listimin e asamblesë:
Analiza e Verifikuesit:
0: (b7) r0 = 2
1: (95) exitPër të shkëputur programin nga interfaci:
ip link set dev xdp-local xdp offNë skript janë këto komandat sudo ./stand attach dhe sudo ./stand detach.
Duke lidhur filtrin, mund të siguroheni që ping vazhdon të funksionojë, por a funksionon programi? Le të shtojmë loge. Funksioni është i ngjashëm me printf(), por mbështet deri në tre argumente, përveç formatit, dhe një listë të kufizuar specifikatorësh. Makrosi bpf_printk() thjeshton thirrjen.
SEC("prog")
int xdp_main(struct xdp_md* ctx) {
+ bpf_printk("got packet: %pn", ctx);
return XDP_PASS;
}Dalja shkon në kanal trazirash të bërthamës, i cili duhet të aktivizohet:
echo -n 1 | sudo tee /sys/kernel/debug/tracing/options/trace_printkShikoni fluxin e mesazheve:
cat /sys/kernel/debug/tracing/trace_pipeTë dy këto komanda bëjnë thirrje sudo ./stand log.
Ping tani duhet të sjellë në të mesazhe si këto:
-110930 [004] ..s1 78803.244967: 0: got packet: 00000000ac510377Nëse e shqyrtoni rezultatet 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#6Bëhet fjalë se programet në eBPF nuk kanë seksion të dhënash, prandaj mënyra e vetme për të koduar vargun e formatit është argumentohet e menjëhershme të urdhrit të VM:
$ python -c "import binascii; print(bytes(reversed(binascii.unhexlify('0a7025203a74656b63617020746f67'))))"
b'got packet: %pn'Për këtë arsye, pamja e gabimeve rrit ndjeshëm kodin përfundimtar.
Dërgimi i paketave XDP
Do ta ndryshojmë filtrin: le të dërgojë të gjitha paketat e hyra prapa. Kjo është e papërshtatshme nga një pikëpamje rrjet, sepse do të duhej të ndryshonim adresat në başhe, por tani është e rëndësishme që të punojë në parim.
bpf_printk("mora paketën: %pn", ctx);
- kthehu XDP_PASS;
+ kthehu XDP_TX;
}Nisni tcpdump në xdp-remote. Ai duhet të tregojë ICMP Echo Request të dalshëm dhe të hyra identike dhe të ndalojë tregimin e ICMP Echo Reply. Por nuk tregon. Duke u zbuluar se, për të punuar XDP_TX në programin në xdp-local , që t'i ishte caktuar një program ndërmjetës i barabartë, qoftë edhe bosh, dhe ai të ishte ngjitur. xdp-remote Si e mora vesh këtë?
Gjegjësia e rrugës së paketës në bërthamë
Duhet të bësh mirë nga të këqijat, sepse nuk ka më asgjë tjetër për t'u bërë.
$ sudo perf trace --call-graph dwarf -e 'xdp:*' 0.000 ping/123455 xdp:xdp_bulk_tx:ifindex=19 veprimi=TX dërguar=0 hequr=1 gabim=-6 veth_xdp_flush_bq ([veth]) veth_xdp_flush_bq ([veth]) veth_poll ([veth]) <...>
Çfarë është kodi 6?$ errno 6 ENXIO 6 Nuk ekziston një pajisje ose adresë e tillë
veth_xdp_flush_bq()Funksioni merr kodin e gabimit nga merr kodin e gabimit nga veth_xdp_xmit(), ku kërkesë për ENXIO dhe gjejmë komentarin.
Le të rivendosim filtrin minimal (XDP_PASS) në skedarin xdp_dummy.c, ta shtojmë në Makefile, ta lidhim me xdp-remote:
ip netns exec remote
ip link set dev int xdp object dummy.oTani tcpdump të tregon atë që pritet:
62:57:8e:70:44:64 > 26:0e:25:37:8f:96, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 64, id 13762, offset 0, flags [DF], proto ICMP (1), length 84)
192.0.2.2 > 192.0.2.1: ICMP echo request, id 46966, seq 1, length 64
62:57:8e:70:44:64 > 26:0e:25:37:8f:96, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 64, id 13762, offset 0, flags [DF], proto ICMP (1), length 84)
192.0.2.2 > 192.0.2.1: ICMP echo request, id 46966, seq 1, length 64Nëse vetëm ARP tregohet në vend të kësaj, duhet të hiqen filtrat (kjo e bën sudo ./stand detach), të lëshojmë ping, më pas të vendosim filtrat dhe të provojmë përsëri. Problemi është se filtrin XDP_TX ka efekt edhe në ARP, dhe nëse staku
i hapësirës së emrave xdp-test ka arritur të «harrojë» adresën MAC 192.0.2.1, ai nuk do të jetë në gjendje ta zgjidhë këtë IP.
Vendosja e detyrës
Të kalojmë te detyra e shkruar: të shkruajmë në XDP mekanizmin SYN cookies.
SYN flood vazhdon të jetë një sulm DDoS i njohur, ku ideja është si më poshtë. Gjatë vendosjes së lidhjes (TCP handshake), serveri merr një SYN, alokuar 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 me qindra në sekondë nga çdo host nga një botnet prej mijëra. Serveri është i detyruar të shpërndajë burime menjëherë sapo të mbërrijë paketa, dhe i çliron ato pas një kohëzgjatjeje të madhe, si rezultat, del jashtë memorjes ose kufizimeve, lidhjet e reja nuk pranohen, dhe shërbimi bëhet i paangazhuar.
Nëse nuk alokohen burime për paketën SYN, por vetëm përgjigjet me një paketë SYNACK, si mund ta kuptojë serveri se paketa ACK, që arrin më vonë, i përket paketës SYN që nuk është ruajtur? Sepse sulmuesi mund të gjenerojë edhe ACK të rreme. Ideja pas SYN cookie është që të kodifikojë parametrat e lidhjes si një hash nga adresat, portet dhe kripë që ndryshon. Nëse ACK ka arritur para ndryshimit të kripës, është ende e mundur të llogaritet përsëri hash dhe të krahasohet me seqnum parametrat e lidhjes. acknum. Sulmuesi nuk mund ta falsifikojë, sepse kripa përfshin një sekret, dhe nuk do të arrijë të kalojë për shkak të kanaleve të kufizuara. acknum sulmuesi nuk mundet, pasi kripa përmban sekretin, dhe nuk do të ketë kohë të përfundojë për shkak të kanalit të kufizuar.
SYN cookie është zbatuar prej kohësh në bërthamën Linux dhe madje mund të aktivizohet automatikisht nëse SYN vijnë shumë shpejt dhe masivisht.
Informacion mbi TCP handshake
TCP siguron transmetimin e të dhënave si një rrjedhë bytes, p.sh. HTTP kërkesat transmetohen mbi TCP. Rrjedha transmetohet në copa nëpër paketa. Të gjitha paketat TCP kanë flamuj logjikë dhe numra sekondarë 32-bit:
Kombinimi i flamujve përcakton rolin e paketës përkatëse. Flamuri SYN tregon se kjo është paketa e parë e dërguesit në lidhje. Flamuri ACK tregon se dërguesi ka marrë të gjitha të dhënat e lidhjes deri në byte.
acknum. Paketa mund të ketë disa flamuj dhe quhet sipas kombinimit të tyre, p.sh. paketa SYNACK.Numri i sekuencës (seqnum) përcakton zhvendosjen në rrjedhën e të dhënave për byte-n e parë që dërgohet në këtë paketë. P.sh., nëse në paketën e parë me X byte të dhëna ky numër ishte N, në paketën e ardhshme me të dhëna të reja do të jetë N+X. Në fillim të lidhjes, secila palë e zgjedh këtë numër në mënyrë të rastësishme.
Numri i njohjes (acknum) — është një zhvendosje e ngjashme me seqnum, por përcakton jo numrin e bytes të dërguar, por numrin e bytes të parë nga marrësi, që dërguesi nuk e ka parë.
Në fillim të lidhjes, palët duhet të bien dakord seqnum dhe acknum. Klienti dërgon një paketë SYN me seqnum = X. Serveri përgjigjet me një paketë SYNACK, ku shënon seqnum = Y dhe vendos acknum = X + 1. Klienti në SYNACK përgjigjet me një paketë ACK, ku seqnum = X + 1, acknum = Y + 1. Pas kësaj, fillon transferimi i të dhënave.
Nëse partneri nuk konfirmon marrjen e paketës, TCP e dërgon atë përsëri pas një kohëprerjeje.
Pse SYN cookies nuk përdoren gjithmonë?
Së pari, nëse humbet SYNACK ose ACK, do të duhet të pritet përsëritja — instalimi i lidhjes pengohet. Së dyti, në paketën SYN — dhe vetëm në të — dërgohet një grup opsionesh që nd påvirkojnë funksionimin e mëtejshëm të lidhjes. Duke mos e ruajtur paketën SYN që ka ardhur, serveri për këtë arsye injoron këto opsione, në paketat e ardhshme klienti nuk do t'i dërgojë më ato. TCP mund të funksionojë kështu, por të paktën në fillim cilësia e lidhjes do të përkeqësohet.
Nga pikëpamja e paketimeve, programi XDP duhet të bëjë të следующее:
- për SYN përgjigju me SYNACK duke përdorur cookie;
- për ACK përgjigju me RST (prish lidhjen);
- hedh të gjithë paketat e tjera.
Pseudokodi i algoritmit së bashku me analizimin e paketë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 verifikuar, (*)
ul numrin e kontrollimeve të mbetura,
kaloni paketën.
Nëse kjo nuk është TCP,
hedh paketën. (**)
Nëse kjo është SYN,
përgjigju me SYN-ACK me cookie.
Nëse kjo është ACK,
nëse në acknum nuk është cookie,
hedh paketën.
Regjistro adresën në tabelën me N kontrollime të mbetura. (*)
Përgjigju me RST. (**)
Në rastet e tjera hedh paketën.Një (*) të shënuara pikat, në të cilat duhet të menaxhohet gjendja e sistemit — në fazën e parë mund të kalojmë pa to, thjesht duke realizuar TCP handshake me gjenerimin e SYN cookie si seqnum.
Në vendin (**), derisa të kemi tabelën, do të kalojmë paketën.
Kohëzgjatja e TCP handshake
Analiza e paketës dhe verifikimi i kodit
Na duhen struktura për kryeja të rrjetit: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) dhe TCP (uapi/linux/tcp.h). I fundit nuk arrita ta lidh për shkak të gabimeve, të lidhura me atomic64_t, duhej të kopjoja përcaktimet e nevojshme në kod.
Të gjitha funksionet që dalin në C për lehtësinë e leximit, duhet të inkorporohen në vendin e thirrjes, pasi verifikuesi eBPF në bërthamë ndalon kalimet mbrapa, domethënësisht, ciklet dhe thirrjet e funksioneve.
#define INTERNAL static __attribute__((always_inline))Makro LOG() çaktivizon printimin në ndërtimin e lëshimit.
Programi përbëhet nga një kanavatë funksionesh. Çdo funksion merr një paketë, në të cilën është ndarë një kryehistori përkatëse, për shembull, process_ether() pritet që të mbushet ether. Bazuar në analizën e 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 lejojnë të gjitha paketat.
struktura Packet {
struktura xdp_md* ctx;
struktura ethhdr* ether;
struktura iphdr* ip;
struktura 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) {
struktura 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
struktura iphdr* ip = (struktura iphdr*)(ether + 1);
if ((void*)(ip + 1) > (void*)packet->ctx->data_end) {
return XDP_DROP; /* paketa me keq */
}
packet->ip = ip;
return process_ip(packet);
}
SEC("prog")
int xdp_main(struct xdp_md* ctx) {
struktura Packet packet;
packet.ctx = ctx;
// A
struktura ethhdr* ether = (struktura ethhdr*)(void*)ctx->data;
if ((void*)(ether + 1) > (void*)ctx->data_end) {
return XDP_PASS;
}
packet.ether = ether;
return process_ether(&packet);
}Vërej se janë kontrollimet e shënuara A dhe B. Nëse e komenton A, programi do të ndërtohet, por gjatë ngarkimit do të ketë një gabim verifikimi:
Analiza e Verifikatorit:
11: (7b) *(u64 *)(r10 -48) = r1
12: (71) r3 = *(u8 *)(r7 +13)
qasje e pavlefshme në paketë, off=13 size=1, R7(id=0,off=0,r=0)
Shkalla R7 është jashtë paketës
gjatë procesimit 11 instruksionesh (limit 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0
Gabim në marrjen e programit/map!Vija kyçe qasje e pavlefshme në paketë, off=13 size=1, R7(id=0,off=0,r=0): ka ka rrugë ekzekutimi, kur byte i trembëdhjetë nga fillimi i buffers është jashtë paketës. Nga listimi është pak e vështirë të kuptohet se për cilin rresht bëhet fjalë, por ka një numër instrukcioni (12) dhe një disasëmbles që tregon rreshtat e kodit burimor:
llvm-objdump -S xdp_filter.o | lessNë këtë rast, ai tregon në rreshtin
LOG("Ether(proto=0x%x)", bpf_ntohs(ether->h_proto));ku kuptohet se problemi është ether. Gjithmonë do të ishte kështu.
Përgjigja ndaj SYN
Qëllimi në këtë fazë është të formojë një SYNACK të saktë me seqnum, i cili në të ardhmen do të zëvendësohet me SYN cookie. Të gjitha ndryshimet ndodhin në process_tcp_syn() dhe përreth.
Kontrolli i paketave
Siç duket, kjo është linja më e shkëlqyer, për më saktë, komenti mbi të:
/* Required to verify checksum calculation */
const void* data_end = (const void*)ctx->data_end;Kur u shkrua versioni i parë i kodit, u përdor uji 5.1, për verifikuesin e të cilit kishte një ndryshim midis data_end dhe (const void*)ctx->data_end. Ndërsa po shkruaja artikullin, uji 5.3.1 nuk kishte një problem të tillë. Ndoshta, kompilatori i qaset variablit lokal ndryshe nga një fusha. Mësimi është - në thellësi të mëdha, thjeshtimi i kodit mund të ndihmojë.
Më pas kontrolli rutinor i gjatësive në nder të verifikuesit; për MAX_CSUM_BYTES poshtë.
const u32 ip_len = ip->ihl * 4;
if ((void*)ip + ip_len > data_end) {
return XDP_DROP; /* paketa i keq */
}
if (ip_len > MAX_CSUM_BYTES) {
return XDP_ABORTED; /* kufizim implementimi */
}
const u32 tcp_len = tcp->doff * 4;
if ((void*)tcp + tcp_len > (void*)ctx->data_end) {
return XDP_DROP; /* paketa i keq */
}
if (tcp_len > MAX_CSUM_BYTES) {
return XDP_ABORTED; /* kufizim implementimi */
}Rikthimi i paketës
Mbushim seqnum dhe acknum, vendosim ACK (SYN është vendosur tashmë):
const u32 cookie = 42;
tcp->ack_seq = bpf_htonl(bpf_ntohl(tcp->seq) + 1);
tcp->seq = bpf_htonl(cookie);
tcp->ack = 1;Këmbim i porteve TCP, adresës IP dhe MAC-adresave. 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);Rikthimi i kontrollave të shumave
Kontrollin e sumave IPv4 dhe TCP kërkon mbledhjen e të gjitha fjalëve 16-bit në tituj, dhe madhësia e titujve është e shkruar aty, domethënë, në momentin e kompilimit nuk dihet. Kjo është një problem, sepse verifikuesi nuk do ta kalojë një cikël normal deri në kufirin e variablit. Megjithatë, madhësia e titujve është e kufizuar: deri në 64 byte secili. Mund të bëhet një cikël me një numër fikse iteracionesh, i cili mund të përfundojë më herët.
Vërejtja, se ka për mënyrën se si të rikalkulohet suma e kontrollit pjesërisht, nëse janë ndryshuar vetëm fjalët fikse të paketave. Megjithatë, kjo qasje nuk është universale dhe zbatimi do të ishte më i vështirë për t'u mbajtur.
Funksioni për llogaritjen e sumë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;
}Megjithëse size verifikohet nga kodi që e thërret, kushti i dytë i daljes është i nevojshëm që verifikuesi të mund 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);
}Në fakt, rikalkulimi i sumave të kontrollit dhe dërgimi i paketës përsëri:
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 një kontroll të shumës 32-bitore të fjalëve 16-bitore, sipas RFC 791.
Kontrolli i dorëzimit TCP
Filtri krijon me sukses një lidhje me netcat, duke anashkaluar ACK-në përfundimtare, për të cilën Linux përgjigjej me një paketë RST, pasi steka e rrjetit nuk merrte SYN - ai ishte transformuar në SYNACK dhe dërguar mbrapsht - dhe nga pikëpamja e OS-së, paketa arriti, që s‘ka lidhje me lidhjet e hapura.
$ sudo ip netns exec xdp-test nc -nv 192.0.2.1 6666
192.0.2.1 6666: Lidhja u restaurua nga palëtËshtë e rëndësishme të kontrolloni me aplikacione të plota dhe të vëzhgoni tcpdump në xdp-remote sepse, për shembull, hping3 nuk reagon ndaj kontrollave të papërshtatshme.
SYN cookie
Nga perspektiva e XDP, vetë verifikimi është triviale. Algoritmi i llogaritjes është primitiv dhe ndoshta i ndjeshëm ndaj një sulmuesi të sofistikuar. Një bërthamë Linux, për shembull, përdor SipHash të kriptografisë, por zbatimi i tij për XDP është qartësisht jashtë përmbajtjes së artikullit.
Ka për qëllim gjithashtu TODO të reja që lidhen me ndërveprimin e jashtëm:
Programi XDP nuk mund të ruajë
cookie_seed(pjesën sekrete të kripës) në variablin global, nevojitet një ruajtje në bërthamë, vlera e së cilës do të përditësohet periodikisht nga një gjenerator i besueshëm.Kur ndodhet një SYN cookie në paketën ACK, nuk duhet të printohet një mesazh, por të mbahen mend IP e klientit të verifikuar, për t'i lënë më pas paketat të kalojnë nga ai.
Verifikimi nga një klient legjitim:
$ sudo ip netns exec xdp-test nc -nv 192.0.2.1 6666
192.0.2.1 6666: Connection reset by peerNë log është regjistruar kalimi në verifikim (flags=0x2 — kjo është SYN, flags=0x10 — kjo ë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 matches for client 20200c0Për momentin nuk ka një listë të IP-ve të verifikuara, mbrojtje nga vetë SYN flood nuk do të jetë, por këtu është reagimi ndaj ACK flood, i nisur nga ky komandë:
sudo ip netns exec xdp-test hping3 --flood -A -s 1111 -p 2222 192.0.2.1Shënime në log:
Ether(proto=0x800)
IP(src=0x15bd11a dst=0x15bd11e proto=6)
TCP(sport=3236 dport=2222 flags=0x10)
cookie mismatchPërfundimi
Herë pas here, eBPF dhe veçanërisht XDP paraqiten më shumë si një mjet për administrues të avancuar sesa si një platformë për zhvillim. Në të vërtetë, XDP është një mjet ndërhyrjeje në përpunimin e paketave nga bërthama, dhe jo një alternativë për shtrirjen bërthamore, siç janë DPDK dhe opsionet e tjera për kalimin përtej bërthamës. Nga ana tjetër, XDP lejon implementimin e logjikës së ndërlikuar, e cila gjithashtu përditësohet lehtësisht pa pushim në përpunimin e trafikut. Verifikuesi nuk sjell probleme të mëdha, unë personalisht nuk do ta refuzoja një të tillë për pjesët e kodit në hapësirën e përdoruesit.
Në pjesën e dytë, nëse tema është interesante, do të përfundojmë tabelën e klientëve të verifikuar dhe ndërprerjen e lidhjeve, do të integrojmë numëruesit dhe do të shkruajmë një utilitar për hapësirën e përdoruesit për menaxhimin e filtrit.
Linket:
Burimi: habr.com
