eXpress Data Path (XDP) tehnoloogia võimaldab Linuxi liidestel teostada juhuslikku liikluse töötlemist enne, kui paketid jõuavad tuuma võrgu stekki. XDP rakendamine - kaitse DDoS-rünnakute eest (CloudFlare), keerukad filtrid, statistika kogumine (Netflix). XDP programmid täidavad virtuaalmasina eBPF, seega on neil piirangud ka oma koodi ja juurdepääsetavate funktsioonide osas sõltuvalt filtri tüübist.
Artikkel on mõeldud mitmete XDP materjalide puuduste kompenseerimiseks. Esiteks esitavad need valmis koodi, mis kohe ületab XDP eripärad: valmistatud kontrollimiseks või liiga lihtne, et probleeme tekitada. Kui proovida hiljem kirjutada oma kood nullist, puudub arusaam, kuidas tegeleda iseloomulike vigadega. Teiseks, ei käsitleta, kuidas testida XDP-d kohalikult ilma virtuaalmasina ja riistvarata, kuigi neil on oma "alused kivid". Tekst on suunatud programmeerijatele, kes tunnevad võrke ja Linuxi ning kellele XDP ja eBPF huvi pakuvad.
Selles osas uurime üksikasjalikult, kuidas XDP filtrit koguda ja kuidas seda testida, seejärel kirjutame lihtsa versiooni tuntud mehhanismist SYN cookies pakettide töötlemise tasemel. Praegu ei hakka me koostama "valget nimekirja".
kontrollitud klientidest, arvestada loendeid ja hallata filtrit - piisab logidest.
Kirjutame C-s - see ei ole moodne, aga praktiline. Kogu kood on saadaval GitHubis lingil allpool ja jagatud commit’ideks etappide järgi, nagu artiklis kirjeldatud.
Märkus. Artikli käigus arendatakse välja mini-lahendus DDoS rünnakute peegelduseks, sest see on realistlik ülesanne XDP jaoks ja minu valdkond. Kuid peamine eesmärk on aru saada tehnoloogiast, see ei ole juhend valmis kaitse loomiseks. Õppimise kood ei ole optimeeritud ja jätab tähelepanuta mõned nüansid.
Lühike ülevaade XDP-st
Esitan ainult peamised punktid, et mitte dubleerida dokumentatsiooni ja olemasolevaid artikleid.
Nii, et tuuma laaditakse filtri kood. Filtri alla kuuluvad sissetulevad paketid. Seetõttu peab filter otsustama: lubada 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 on võimalik programm äkiliselt katkestada (XDP_ABORTED) ja pakett tagasi lükata, kuid see on analooge assert(0) - silumaanimise jaoks.
Virtuaalne masin eBPF (laiendatud Berkley paketi filter) on loodud nii, et tuum saaks kontrollida, kas kood ei jää tsüklisse ega kahjusta teiste mälu. Ühised piirangud ja kontrollid:
- Tsüklid (tagasipöördumised) on keelatud.
- Andmete jaoks on olemas virn, kuid funktsioone ei ole (kõik C funktsioonid peavad olema sissekoonitatud).
- Keelatud on juurdepääs mälule, mis on väljaspool virna ja paketi puhverserverit.
- Koodi suurus on piiratud, kuid praktikas pole see väga oluline.
- Lubatud on kõned ainult spetsiaalsetele tuuma funktsioonidele (eBPF abifunktsioonid).
Filtri arendamine ja installimine toimub järgmiselt:
- Allikakood (näiteks,
kernel.c) kompileeritakse objektiks (kernel.o) eBPF virtuaalse masina arhitektuuri jaoks. Oktoobris 2019 toetab eBPF kompileerimist Clang ja see on lubatud GCC 10.1-s. - Kui selles objektikoodis on viiteid tuuma struktuuridele (näiteks tabelitele ja loenditele), on nende ID-d nullid, seega sellist koodi ei saa täita. Enne tuumasse laadimist tuleb need nullid asendada konkreetsete objektide ID-dega, mis on loodud tuuma kõnede kaudu (siduda kood). Seda saab teha väliste utiliitide abil või kirjutada programm, mis ühendab ja laadib konkreetse filtri.
- Tuum valideerib laaditava programmi. Kontrollitakse tsüklite puudumist ja paketi ning virna piiride ületamist. Kui valideerija ei suuda tõestada, et kood on korrektne, lükatakse programm tagasi - tuleb osata teda rahuldada.
- Pärast eduka valideerimise lõpetamist kompileerib tuum eBPF arhitektuuri objektikoodi masinakoodiks süsteemi arhitektuuri (just-in-time).
- Programm kinnitatakse liidese külge ja hakkab pakette töötlema.
Kuna XDP töötab tuumas, toimub silumine jälgimislogide ja tegelike pakettide kaudu, mida programm filtreerib või genereerib. Sellegipoolest tagab eBPF laaditud koodi turvalisuse süsteemi jaoks, mistõttu saab XDP-d katsetada otse kohaliku Linuxi peal.
Keskkonna ettevalmistamine
Kogumine
Clang ei saa otse genereerida objektikoodi eBPF arhitektuuri jaoks, seega koosneb protsess kahest etapist:
- Kompileerida kood C-s LLVM baitkoodiks (
clang -emit-llvm). - Muutke baitkood eBPF objektikoodiks (
llc -march=bpf -filetype=obj).
Filtri kirjutamisel on kasulik omada paar faili abifunktsioonide ja makrodega . Oluline on, et need vastaksid tuuma versioonile (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 Linuxi jaoks (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 kernel'i pealkirjade juurde, ARCH — süsteemi arhitektuur. Teed ja tööriistad võivad jaotiste vahel veidi erineda.
Debian 10 (kernel 4.19.67) erisuste näide
# другая команда
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 lisavad kausta abipealkirjadega ja mitmeid kernel'i pealkirjade kaustu. Sümbol __KERNEL__ tähendab, et UAPI (user space API) pealkirjad määratakse kernel'i koodile, kuna filter töötab kernel'is.
Stack kaitset on võimalik välja lülitada (-fno-stack-protector), kuna eBPF koodivaatleja kontrollib ikkagi, et ei ületataks stack'i piire. Optimeerimised tuleks kohe sisse lülitada, kuna eBPF baitkoodi suurus on piiratud.
Alustame filtriga, mis laseb kõik paketid läbi 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 tee andmeid aknasüsteemi kohta ("Wayland", "Wayland/drm", "x11") xdp_filter.o. Kuidas seda nüüd testida?
Testseade
Seade peaks sisaldama kahte interfässi: üks, millel on filter, ja üks, kust paketid saadetakse. Need peavad olema täieõiguslikud Linuxi seadmed oma IP-dega, et kontrollida, kuidas tavalised rakendused meie filtriga töötavad.
Veth (virtuaalne Ethernet) tüüpi seadmed sobivad meile: need on paar virtuaalset võrguliidest, mis on omavahel "ühendatud". Saame need luua nii (selles jaotises käitatakse kõik käsklused ip kui root):
ip link add xdp-remote type veth peer name xdp-localSiin xdp-remote ja xdp-local — seadmete nimed. Filtrile on ühendatud xdp-local (192.0.2.1/24), samas kui xdp-remote (192.0.2.2/24) saadab sissetulevat liiklust. Kuid on probleem: interfääsid on ühel masinal, ja Linux ei saada liiklust ühelt teisele. Selle võib lahendada nutikate reeglitega iptables, kuid nad peavad paketid muutma, mis on silumise puhul ebamugav. Parema lahendusena tasub kasutada võrguruume (network namespaces, edaspidi netns).
Võrguruum sisaldab komplekti liideseid, marsruutimistabeleid ja NetFilter reegleid, mis on eraldatud sarnastest objektidest teistes netns-des. Iga protsess töötab mingis nimespinnas ja tal on juurdepääs ainult selle netns-i objektidele. Vaikimisi on süsteemis ainult üks võrguruum kõigi objektide jaoks, seega on võimalik Linuxi kasutada teadmata netns-ist.
Loo uus nimespind xdp-test ja liigutame selle 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 "näe" xdp-local (see jääb vaikimisi netns-i) ja paketi saatmisel aadressile 192.0.2.1 edastab see selle läbi xdp-remote, kuna see on ainus liides 192.0.2.0/24, mis on selle protsessi jaoks saadaval. See toimib ka tagasi suunas.
Nimenspinku üleminekul liides kukkub ja kaotab aadressi. Et seadistada liidest netns-is, tuleb käivitada ip ... selles nimespindas käsureal 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 upKuidas näha, et see ei erine vaikimisi nimespinnas seadistamisest: xdp-local ip address add 192.0.2.1/24 dev xdp-local ip link set xdp-local up
tcpdump -tnevi xdp-localhistory , on näha, et nendest aadressidest saadetud paketid, jõuavad sellele liidesele: xdp-testip netns exec xdp-test ping 192.0.2.1
Mugav on käivitada shell. Repositooriumis on skript, mis automatiseerib stendi tööd, näiteks saab stendi seadistada käsuga xdp-testsudo ./stand up ja hävitada see sudo ./stand down Jälgimine.
Filter kinnitatakse seadmele järgmiselt:
ip -force link set dev xdp-local xdp object xdp_filter.o verbose
-forceVõti on vajalik, et siduda uus programm, kui teine on juba kinnitatud. "No news is good news" ei kehti selle käsu kohta, väljund igal juhul mahukas. Täpsustamine ei ole kohustuslik, kuid koos sellega tuleb koodivaatleja raport koos assembleri loeteluga: verbose Verifier analysis:0: (b7) r0 = 2 1: (95) exit
Eemalda programm liidesest:ip link set dev xdp-local xdp off
Skriptis on need käsudsudo ./stand attach sudo ./stand detach ja Pärast filtri kinnitamist saab veenduda, et.
töötab ikka, kuid kas programm töötab? Lisame logid. Funktsioon ping bpf_trace_printk() on sarnane printf()bpf_printk() lihtsustab kutsumist. SEC("prog") int xdp_main(struct xdp_md* ctx) { + bpf_printk("got packet: %pn", ctx); return XDP_PASS; }
Väljund läheb tuuma jälgimiskanale, mille tuleb sisse lülitada:Вывод идет в канал трассировки ядра, который нужно включить:
echo -n 1 | sudo tee /sys/kernel/debug/tracing/options/trace_printkSõnumivoo vaatamine:
cat /sys/kernel/debug/tracing/trace_pipeMõlemad käsklused teevad kutse sudo ./stand log.
Pinge peaks nüüd genereerima selliseid sõnumeid:
-110930 [004] ..s1 78803.244967: 0: got packet: 00000000ac510377Kui tähelepanelikult 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 selles, et eBPF programmide jaoks ei ole andmesektsioone, seetõttu on ainus viis vormindatud stringi kodeerimiseks VM-i käskude immediate-argumendid:
$ python -c "import binascii; print(bytes(reversed(binascii.unhexlify('0a7025203a74656b63617020746f67'))))"
b'got packet: %pn'Sel põhjusel tiheneb tõrke väljund lõplikku koodi.
XDP paketid
Muudame filtrit: las ta saadab kõik sissetulevad paketid tagasi. See pole võrgu seisukohalt korrektne, kuna oleks pidanud aadresse pealdiste kallal muutma, kuid praegu on oluline, et see töötab.
bpf_printk("got packet: %pn", ctx);
- return XDP_PASS;
+ return XDP_TX;
}Käivitame tcpdump . Tundub, et xdp-remote. See peaks näitama identseid väljaminevaid ja sissetulevaid ICMP Echo Request'e ja lõpetama ICMP Echo Reply'de näitamise. Kuid ta ei näita. Selgub, et selleks, et töötada XDP_TX programmis xdp-local , et ka paarisliidesele xdp-remote oleks määratud programm, isegi kui see on tühi, ja see oleks üles tõstetud.
Kuidas ma seda teada sain?
on võimalik perfi ürituste mehhanismi kaudu, mis muide kasutab sama virtuaalmasinat, see tähendab, et eBPF lahendamiseks rakendatakse eBPF-i.
Sa pead head tegema kurjast, kuna kurja enam pole millekski muuks teha.
$ 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 tähendab kood 6?
$ errno 6
ENXIO 6 Sellist seadet või aadressi ei oleFunktsioon veth_xdp_flush_bq() saab veakoodi veth_xdp_xmit(), kus otsides ENXIO ja leiame kommentaar.
Taastame minimaalfiltri (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, mis oodatud:
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, lipud [DF], protokoll 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, lipud [DF], protokoll 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 (see teeb Pärast filtri kinnitamist saab veenduda, et), lasta ping, seejärel seadistada filtrid ja proovida uuesti. Probleem on see, et filter XDP_TX kehtib nii ARP-le, ja kui
nime ruum xdp-test on suutnud unustada MAC-aadressi 192.0.2.1, ei saa ta seda IP-d lahendada.
Ülesande seadmine
Läheme tagasi väljakuulutatud ülesande juurde: kirjutada XDP mehhanism SYN küpsiste jaoks.
Siiani on populaarseks DDoS-rünnakuks SYN flood, mille olemus on järgmine. Ühenduse loomisel (TCP handshake) saab server SYN, eraldab ressursid tulevase ühenduse jaoks, vastab SYNACK-paketiga ja ootab ACK-d. Ründaja saadab lihtsalt sünteetilisi SYN-pakette vale aadressilt, tuhandetes sekundis iga hostist, mida on tuhandetes botnetides. Server peab eraldama ressursid kohe paketi saabumisel, eraldades need pika aja pärast, mille tulemusena kaob mälu või piirangud, uusi ühendusi ei vastu võeta, teenus on kättesaamatu.
Kui mitte eraldada ressursse igale SYN-paketile, vaid lihtsalt vastata SYNACK-paketiga, kuidas siis server aru saab, et hiljem saabunud ACK-pakett kuulub SYN-paketile, mida ei hoitud? Lõppude lõpuks võib ründaja genereerida ka vale ACK. SYN küpsise olemus on see, et kodeerida seqnum ühenduse parameetrid nagu hash aadressidest, portidest ja muutuva soolaga. Kui ACK on jõudnud enne soola muutumist, saab hääletada hash uuesti ja võrrelda acknum. Vale genereerida acknum ründaja ei saa, kuna sool sisaldab saladust ja ei jõua katsetama piiratud kanali tõttu.
SYN küpsis on pikka aega olnud Linuxi tuumas ja võib isegi automaatselt sisse lülituda, kui SYN-e saab liiga kiiresti ja massiliselt.
TCP handshake'i lühike tutvustus
TCP tagab andmete edastamise kui baitide voog, näiteks edastatakse HTTP-päringud TCP kaudu. Voog edastatakse tükkide kaupa pakettides. Kõigil TCP-pakettidel on loogilised lipud ja 32-bitised järjestuse numbrid:
Lippude kombineerimine määrab konkreetse paketi rolli. Lipp SYN tähendab, et see on saatja esimene pakkumine ühenduses. Lipp ACK tähendab, et saatja on saanud kõik ühenduse andmed kuni baitideni
acknum. Pakett võib omada mitmeid lippe ja seda nimetatakse nende kombinatsiooni järgi, näiteks SYNACK-pakett.Järjekorranumber (seqnum) määrab andmevoos esimesest baitidest, mis edastatakse selles paketis. Näiteks, kui esimeses paketis on X baiti andmeid ja see number oli N, siis järgmisel paketil uutega on see N+X. Ühenduse alguses valida iga pool selle numbri juhuslikult.
Kinnitusnumber (acknum) on sama sihtnumber nagu seqnum, kuid määrab mitte edastatava baidi numbri, vaid esimese baidi numbri saajalt, mida saatja pole näinud.
Ühenduse alguses peavad osalised kokku leppima seqnum ja acknum. Klient saadab SYN-paketi oma seqnum = X. Server vastab SYNACK-paketiga, kuhu kirjutab oma seqnum = Y ja seadis acknum = X + 1. Klient vastab SYNACK-le ACK-paketiga, kus seqnum = X + 1, acknum = Y + 1. Pärast seda algab andmete edastamine.
Kui kõnepartner ei kinnita paketi saamist, saadab TCP selle ajastus ära uuesti.
Miks SYN-küpsiseid ei kasutata alati?
Esiteks, kui SYNACK või ACK kaob, tuleb oodata uuesti saatmist — ühenduse loomine aeglustub. Teiseks, SYN-paketis — ja ainult selles! — edastatakse rida valikuid, mis mõjutavad edasist ühenduse tööd. Mitte salvestades sissetulevaid SYN-pakette, server ignoreerib neid valikuid, järgmistes pakkides klient ei saada neid enam. TCP töötab sel juhul edasi, kuid vähemalt algfaasis ühenduse kvaliteet langeb.
Pakettide vaatenurgast peaks XDP-programm tegema järgmist:
- SYN-le vastama SYNACK-i küpsisega;
- ACK-le vastama RST (katkestama ühenduse);
- muud paketid tagasi lükkama.
Algoritmi pseudokood koos paketi analüüsiga:
Kui see pole Ethernet,
jäta pakk vahele.
Kui see pole IPv4,
jäta pakk vahele.
Kui aadress kontrollitud tabelis,
vähenda jääkide kontrollide arvu,
jäta pakk vahele.
Kui see pole TCP,
lükka pakk tagasi. (**)
Kui see on SYN,
vasta SYN-ACK küpsisega.
Kui see on ACK,
kui acknumis pole küpsist,
lükka pakk tagasi.
Pane aadress N jääkide kontrollidega tabelisse. (*)
Vasta RST. (**)
Muude juhtude korral lükka pakk tagasi.Üks (*) märgitud punktides, kus tuleb süsteemi olekut hallata - esimeses etapis saab ilma nendeta hakkama, lihtsalt tehes TCP handshake ja genereerides SYN küpsise seqnumina.
Koha peal (**), kuni meil pole tabelit, jätame paketi vahele.
TCP handshake'i teostamine
Paketi analüüs ja koodide kontrollimine
Me vajame järgmisi võrgupealkirjade struktuure: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) ja TCP (uapi/linux/tcp.h). Mul ei õnnestunud seda viimast ühendust vigade tõttu edastada, seotud atomic64_t, pidin vajalikud määratlused koodi kopeerima.
Kõik C keeles välja toodud funktsioonid, et paremini lugeda, peavad olema paigaldatud kutsumise kohas, kuna eBPF valideerija tuumas keelab tagasipöördumised, mis tähendab, et tegelikult tsükleid ja funktsioonide kutseid.
#define INTERNAL static __attribute__((always_inline))redshift.compress_table LOG() keelab väljastamise tootmisversioonis.
Programm esindab funktsioonide torustikku. Igaüks neist võtab paketi, milles on vastava taseme pealkiri, näiteks process_ether() ootab, et oleks täidetud ether. Pärast väljade analüüsi tulemust saab funktsioon edastada paketi ülemisele tasemele. Funktsiooni töö tulemus on XDP tegevus. Seni öökapid SYN ja ACK lasevad kõik paketid läbi.
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; /* vigane pakett */
}
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);
}Käivitan tähelepanu kontrollidele, mis on märgitud A ja B. Kui A kommenteerida, kompileeritakse programm, kuid laadimise ajal tekib valideerimise viga:
Verifitseerija analüüs:
11: (7b) *(u64 *)(r10 -48) = r1
12: (71) r3 = *(u8 *)(r7 +13)
väärne juurdepääs paketile, off=13 size=1, R7(id=0,off=0,r=0)
R7 offset on paketist väljaspool
protsessitud 11 insns (limit 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0
Viga programmi/mapi hankimisel!Peamine rida väärne juurdepääs paketile, off=13 size=1, R7(id=0,off=0,r=0): on teid, kus täiendava baidi seitsmeteistkümnes bait on paketi väljas. Koodilõike põhjal on keeruline mõista, millest jutt, kuid tõsi on, et on olemas instruktsiooni number (12) ja disassembleerija, mis näitab originaalkoodi ridu:
llvm-objdump -S xdp_filter.o | lessSelles kontekstis osutab see reale
LOG("Ether(proto=0x%x)", bpf_ntohs(ether->h_proto));mille, et on selge, et probleem on ether. Alati oleks nii.
Vastus SYN-ile
Selle etapi eesmärk on moodustada korrektne SYNACK-pakett, millel on fikseeritud seqnum, mis tulevikus asendatakse SYN küpsisega. Kõik muudatused toimuvad process_tcp_syn() ja ümbruses.
Paketi kontroll
Kuidas imelik, siin on kõige silmapaistvam rida, pigem, kommentaar selle kohta:
/* Required to verify checksum calculation */
const void* data_end = (const void*)ctx->data_end;Esimese koodi versiooni kirjutamisel kasutati põhijude 5.1, mille verifikaatoril oli erinevus data_end ja (const void*)ctx->data_end. Artikli kirjutamise ajal põhijude 5.3.1 ei olnud sellist probleemi. Võib-olla kohtles kompilaator kohalikke muutujaid teisiti kui välja. Moraal - suure siselduse korral võib koodi lihtsustamine olla kasulik.
Edasi rutiinsed pikkuse kontrollid verifikaatori au nimel; kohta MAX_CSUM_BYTES allpool.
const u32 ip_len = ip->ihl * 4;
if ((void*)ip + ip_len > data_end) {
return XDP_DROP; /* vale paket * /
}
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; /* vale paket * /
}
if (tcp_len > MAX_CSUM_BYTES) {
return XDP_ABORTED; /* rakenduse piirang * /
}Paketi pööramine
Täidame seqnum ja acknum, seame ACK (SYN on juba seatud):
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-aadresside ja MAC-aadresside kohad. Standardne teek ei ole XDP-programmist kättesaadav, seega memcpy() — makro, mis peidab Clangi intriinsikuid.
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 nõuavad, et kõik 16-bitised sõnad peavad olema kokku liidetud pead, ning peade suurus on neis kirjas, seega ei ole see kompileerimise hetkel teada. See on probleem, kuna verifikaator ei luba tavalist tsüklit muutuja piirgest. Siiski on peade suurus piiratud: kuni 64 baitid igaühe jaoks. Saame teha fikseeritud iteratsioonide arvu tsüklit, mis võib varakult lõppeda.
Tahan märkida, et on kuidas arvutada kontrollsummat osaliselt, kui on muudetud ainult fikseeritud sõnu pakettide sees. Kuid meetod ei ole universaalne ja rakenduse toetamine oleks keerulisem.
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;
}Vaatamata sellele, et size koodiga, teine väljumise tingimus on vajalik, et valideerija saaks tõestada tsükli lõppu.
32-bitiste sõnade jaoks on rakendatud lihtsustatud versioon:
INTERNAL u32
sum16_32(u32 v) {
return (v >> 16) + (v & 0xffff);
}Ning tegelikult kontrollsummade ülekalkuleerimine ja paketi tagasisaatmine:
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;Funktsioon carry() transformeerib 32-bitise summa 16-bitiste sõnade kontrollsummaks, vastavalt RFC 791.
TCP käepigistuse kontroll
Filtter loob korrektselt ühenduse netcat, vahele jättes lõpliku ACK, millele Linux vastas RST-paketiga, kuna võrgu lekked ei saanud SYN - see muudeti SYNACK-ks ja saadeti tagasi - ja operatsioonisüsteemi vaatenurgast tuli pakett, mis ei kuulunud avatud ühendustele.
$ sudo ip netns exec xdp-test nc -nv 192.0.2.1 6666
192.0.2.1 6666: Ühendus taastatud vastaspoole pooltOluline on testida just korralike rakendustega ja jälgida tcpdump . Tundub, et xdp-remote sest näiteks, hping3 ei reageeri mittetäielikele kontrollsummadele.
SYN cookie
XDP seisukohalt on enda kontroll triviaalne. Kalkulatsiooni algoritm on primitiivne ja tõenäoliselt haavatav kavalate ründajate suhtes. Linuxi kernel, näiteks, kasutab krüptograafilist SipHash'i, kuid selle XDP rakendus ületab selgelt artikli piire.
Ilmus uusi TODO-sid, mis on seotud välise interaktsiooniga:
XDP programm ei saa hoida
cookie_seed(salajane soola osa) globaalsetes muutujaid, on vajalik salvestamine tuumas, mille väärtust värskendatakse perioodiliselt usaldusväärsest generaatorist.SYN cookie kooskõlas ACK-paketiga ei tohi trükkida sõnumit, vaid tuleb meelde jätta kontrollitud kliendi IP, et hiljem tema pakette lubada.
Kontrollimine seaduslikult kliendilt:
$ sudo ip netns exec xdp-test nc -nv 192.0.2.1 6666
192.0.2.1 6666: Ühendus taastatud vastaspoole pooltLogides 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)
cookie matches for client 20200c0Kuni kontrollitud IP-de nimekiri puudub, ei tule kaitset SYN floodi vastu, kuid vastus ACK floodile, mis käivitatakse sellise käsuga:
sudo ip netns exec xdp-test hping3 --flood -A -s 1111 -p 2222 192.0.2.1Logi kirjed:
Ether(proto=0x800)
IP(src=0x15bd11a dst=0x15bd11e proto=6)
TCP(sport=3236 dport=2222 flags=0x10)
cookie mismatchKokkuvõte
Mõnikord esitatakse eBPF ja XDP pigem kui edasijõudnud administraatori tööriista, mitte kui arenduse platvormi. Tõepoolest, XDP on tööriist, mis sekkub pakettide töötlemisse tuumas, mitte alternatiiv tuumastruktuurile nagu DPDK ja muud kernel bypass lahendused. Teiselt poolt võimaldab XDP rakendada üsna keerukat logikat, mida on samas lihtne uuendada ilma liikluse töötlemise pausideta. Verifitseerija ei tekita tõsiseid probleeme ja isiklikult ei ütleks ma sellisest lahendusest ära userspace-koodi osade jaoks.
Teises osas, kui teema on huvitav, viime lõpule kontrollitud klientide tabeli jaühenduse katkestused, rakendame loendurid ning kirjutame userspace utiliidi filtri haldamiseks.
Lingid:
Allikas: habr.com
