Tehnologija eXpress Data Path (XDP) omogućava slobodnu obradu prometa na Linux sučeljima prije nego što paketi uđu u mrežni stog jezgre. Primjena XDP uključuje zaštitu od DDoS napada (CloudFlare), složene filtre i prikupljanje statistike (Netflix). XDP programi se izvršavaju unutar virtualne mašine eBPF, stoga imaju ograničenja u vezi sa vlastitim kodom i dostupnim funkcijama jezgre ovisno o vrsti filtra.
Ovaj članak ima za cilj nadopuniti nedostatke brojnih materijala o XDP-u. Prvo, pružaju gotov kod koji odmah zaobilazi karakteristike XDP-a: pripremljen za verifikaciju ili previše jednostavan da bi izazvao probleme. Prilikom pokušaja pisanja vlastitog koda od nule, nema razumijevanja što učiniti s karakterističnim greškama. Drugo, ne objašnjavaju se načini lokalnog testiranja XDP-a bez VM-a i 'hardvera', unatoč tome što imaju vlastite 'podvodne kamenčiće'. Tekst je namijenjen programerima koji su upoznati s mrežama i Linuxom, a koji su zainteresirani za XDP i eBPF.
U ovom dijelu detaljno ćemo obraditi kako se sastavlja XDP filtar i kako ga testirati, a zatim ćemo napisati jednostavnu verziju poznatog mehanizma SYN cookies na razini obrade paketa. Za sada nećemo formirati 'bijelu listu'
provjerenih klijenata, voditi brojila i upravljati filtrima – dovoljno su logovi.
Pisati ćemo na C – to nije moderno, ali je praktično. Sav kod dostupan je na GitHubu putem linka na kraju i razbijen je na commitove prema fazama opisanima u članku.
Odricanje od odgovornosti. Tijekom članka razvijat će se mini-rješenje za odbijanje DDoS napada, jer je to realističan zadatak za XDP i moja oblast. Glavni cilj je razumjeti tehnologiju, ovo nije vodič za izradu gotove zaštite. Edukativni kod nije optimiziran i izostavlja neke nijanse.
Kratki pregled XDP-a
Izložit ću samo ključne trenutke kako ne bih duplicirao dokumentaciju i postojeće članke.
Dakle, kod filtra se učitava u jezgru. Filtru se prosljeđuju dolazni paketi. Na kraju, filtar treba donijeti odluku: proslijediti paket u jezgru (XDP_PASS), odbaciti paket (XDP_DROP) ili ga poslati natrag (XDP_TX). Filtar može promijeniti paket, što je osobito važno za XDP_TX. Također se može hitno prekinuti program (XDP_ABORTED) i odbaciti paket, ali to je analogno assert(0) — za debugiranje.
Virtualni stroj eBPF (extended Berkley Packet Filter) je posebno napravljen da bude jednostavan kako bi jezgra mogla provjeriti da li kod ne ulazi u beskonačnu petlju i ne oštećuje stranu memoriju. Sveukupna ograničenja i provjere:
- Ciklusi (povratni prijelazi) su zabranjeni.
- Postoji stog za podatke, ali nema funkcija (sve C funkcije moraju biti ugrađene).
- Zabranjeni su pristupi memoriji izvan stoga i paketskog bafera.
- Veličina koda je ograničena, ali u praksi to nije baš presudno.
- Dopušteni su pozivi samo posebnih jezgrenih funkcija (eBPF helpers).
Razvoj i instalacija filtra izgledaju ovako:
- Izvorni kod (na primjer,
kernel.c) se kompajlira u objektni (kernel.o) za arhitekturu virtualnog stroja eBPF. U listopadu 2019. kompajlacija u eBPF podržava Clang i obećana je u GCC 10.1. - Ako u ovom objektu postoje pozivi strukturalnih elemenata jezgra (na primjer, do tablica i brojila), umjesto njihovih ID-eva stoje nuli, to jest, takav kod se ne može izvršiti. Prije učitavanja u jezgru ove nule treba zamijeniti s ID-evima konkretnih objekata stvorenih putem poziva jezgre (spajanje koda). To se može učiniti vanjskim alatima, ili se može napisati program koji će spajati i učitavati konkretni filtar.
- Jezgro verificira učitani program. Provjerava se odsutnost ciklusa i neizlaženje iz granica paketa i stoga. Ako verifier ne može dokazati da je kod ispravan, program se odbacuje - treba znati kako ga zadovoljiti.
- Nakon uspješne verifikacije, jezgro kompajlira objektni kod arhitekture eBPF u strojni kod sistemske arhitekture (just-in-time).
- Program se priključuje sučelju i počinje obrađivati pakete.
Budući da XDP radi unutar jezgre, debagiranje se vodi kroz logove praćenja i, zapravo, kroz pakete koje program filtrira ili generira. Ipak, eBPF osigurava sigurnost učitanog koda za sustav, stoga s XDP-om možete eksperimentirati direktno na lokalnom Linuxu.
Priprema okruženja
Izgradnja
Clang ne može izravno generirati objektni kod za arhitekturu eBPF, stoga se proces sastoji od dva koraka:
- Kompajlirati kod na C u LLVM bajt kod (
clang -emit-llvm). - Pretvoriti bajt kod u objektni kod eBPF (
llc -march=bpf -filetype=obj).
Prilikom pisanja filtra bit će potrebni neki datoteke s pomoćnim funkcijama i makroima . Važno je da odgovaraju verziji jezgra (KVER). Preuzimamo ih u 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 za Arch Linux (jezgra 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 sadrži put do zaglavlja jezgra, ARCH — arhitektura sustava. Putanje i alati mogu se malo razlikovati između distribucija.
Primjer razlika za Debian 10 (jezgro 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 uključuju direktorij s pomoćnim zaglavljima i nekoliko direktorija sa zaglavljima jezgre. Simbol __KERNEL__ označava da se UAPI (API za korisnički prostor) zaglavlja definiraju za kod jezgra, jer se filter izvršava u jezgru.
Zaštitu stoga možemo onemogućiti (-fno-stack-protector), jer verifier koda eBPF ionako provjerava izlazak iz granica stoga. Odmah treba uključiti optimizacije, jer je veličina eBPF bytecode-a ograničena.
Započet ćemo s filtrima koji propuštaju sve pakete i ništa ne radi:
#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";Komanda make prikuplja xdp_filter.o. Gdje ga sada testirati?
Testni sto
Testno okruženje treba uključivati dva sučelja: na kojem će biti filter i s kojeg će se slati paketi. To moraju biti potpuna Linux uređaja sa svojim IP-om, kako bi se provjerilo kako obične aplikacije rade s našim filtrima.
Uređaji tipa veth (virtual Ethernet) su pogodna opcija: to je par virtualnih mrežnih sučelja, „spojenih“ međusobno direktno. Kreirati ih možete ovako (u ovom odjeljku sve naredbe ip izvode se od root):
ip link add xdp-remote type veth peer name xdp-localOvdje xdp-remote i xdp-local — imena uređaja. Na xdp-local (192.0.2.1/24) će se pridružiti filter, s xdp-remote (192.0.2.2/24) će se slati dolazni promet. Međutim, postoji problem: sučelja se nalaze na jednom računalu, i Linux neće slati promet preko jednog od njih na drugi. Može se riješiti pametnim pravilima iptables, ali bi oni morali mijenjati pakete, što nije praktično tijekom debagiranja. Bolje je koristiti mrežne prostore imena (network namespaces, u daljnjem tekstu netns).
Mrežni prostor imena sadrži skup sučelja, tablica usmjeravanja i pravila NetFilter, izolirane od sličnih objekata u drugim netns. Svaki proces radi u nekom prostoru imena i ima pristup samo objektima tog netns. Po zadanim postavkama u sustavu postoji jedinstveni mrežni prostor imena za sve objekte, stoga se može raditi u Linuxu a da se ne zna za netns.
Stvorimo novi prostor imena xdp-test i premjestimo ga tamo xdp-remote.
ip netns add xdp-test
ip link set dev xdp-remote netns xdp-testTada proces koji se izvršava u xdp-test, neće "vidjeti" xdp-local (ostati će u netns po zadanim postavkama) i pri slanju paketa na 192.0.2.1 proslijedit će ga preko xdp-remote, jer je to jedini sučelje u 192.0.2.0/24 dostupno tom procesu. Ovo vrijedi i obrnuto.
Pri premještanju između netns sučelje se spušta i gubi adresu. Da bi se konfiguriralo sučelje u netns, potrebno je pokrenuti ip ... u tom prostoru imena naredbenog 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 upKao što se može vidjeti, to se ne razlikuje od postavljanja xdp-local u prostoru imena po zadanim postavkama:
ip address add 192.0.2.1/24 dev xdp-local
ip link set xdp-local upAko pokrenete tcpdump -tnevi xdp-local, može se vidjeti da paketi poslani iz xdp-test, dostavljaju se na ovo sučelje:
ip netns exec xdp-test ping 192.0.2.1Povoljno je pokrenuti shell u xdp-test. U repozitoriju postoji skripta koja automatizira rad s postavom, na primjer, može se postaviti postava naredbom sudo ./stand up i ukloniti je sudo ./stand down.
Praćenje
Filtriranje se povezuje s uređajem na sljedeći način:
ip -force link set dev xdp-local xdp object xdp_filter.o verboseKljuč -force je potrebno za vezivanje novog programa, ako je drugi već povezan. "Nema vijesti je dobra vijest" nije za ovu naredbu, izlaz je u svakom slučaju opsežan. Nije potrebno verbose ali uz njega dolazi izvještaj o radu verifikatora koda s popisom asemblera:
Analiza verifikatora:
0: (b7) r0 = 2
1: (95) izlazOdvojiti program od sučelja:
ip link set dev xdp-local xdp offU skripti su to komande sudo ./stand attach i sudo ./stand detach.
Povezivanjem filtra, može se provjeriti da ping nastavlja raditi, ali radi li program? Dodajmo logove. Funkcija slična je printf(), ali podržava samo do tri argumenta, osim obrasca, i ograničeni popis specifikatora. Makro bpf_printk() olakšava poziv.
SEC("prog")
int xdp_main(struct xdp_md* ctx) {
+ bpf_printk("dobio paket: %pn", ctx);
return XDP_PASS;
}Izlaz ide u kanal praćenja jezgre, koji treba uključiti:
echo -n 1 | sudo tee /sys/kernel/debug/tracing/options/trace_printkPregled toka poruka:
cat /sys/kernel/debug/tracing/trace_pipeObje ove naredbe pozivaju sudo ./stand log.
Ping bi sada trebao generirati takve poruke:
-110930 [004] ..s1 78803.244967: 0: dobio paket: 00000000ac510377Ako pažljivo pogledate izlaz verifikatora, možete primijetiti čudne izračune:
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) poziv bpf_trace_printk#6Problem je u tome što programi na eBPF nemaju sekciju podataka, tako da je jedini način kodirati formatnu nisku - immediate-argumenti VM naredbi:
$ python -c "import binascii; print(bytes(reversed(binascii.unhexlify('0a7025203a74656b63617020746f67'))))"
b'got packet: %pn'Iz tog razloga, dijagnostički izlaz jako povećava konačni kod.
Slanje XDP paketa
Promijenimo filtar: neka svi dolazni paketi budu poslani natrag. To nije ispravno s mrežnog stajališta, jer bi se trebali mijenjati adrese u zaglavljima, ali trenutno je važna sama funkcionalnost.
bpf_printk("got packet: %pn", ctx);
- return XDP_PASS;
+ return XDP_TX;
}Pokrenite tcpdump na xdp-remote. Trebao bi pokazati identične odlazne i dolazne ICMP Echo Request i prestati prikazivati ICMP Echo Reply. No ne prikazuje. Ispostavlja se da je za rad XDP_TX u programu na xdp-local , da parnom sučelju xdp-remote također je bila dodijeljena barem prazna programa, i bio je aktiviran.
Kako sam to saznao?
omogućava mehanizam perf events, koji, usput rečeno, koristi istu VM, tj. za analizu eBPF koristi se eBPF.
Trebate učiniti dobro iz zla, jer ga više nemate od čega napraviti.
$ 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])Što je kod 6?
$ errno 6
ENXIO 6 Nema takvog uređaja ili adreseFunkcija veth_xdp_flush_bq() dobiva kod greške od veth_xdp_xmit(), gdje pregledom ENXIO i nalazimo komentar.
Vratimo minimalni filtar (XDP_PASS) u datoteci xdp_dummy.c, dodajmo ga u Makefile, povežimo ga s xdp-remote:
ip netns exec remote
ip link set dev int xdp object dummy.oSada tcpdump pokazuje ono što se očekuje:
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 64Ako se umjesto toga prikazuju samo ARP, potrebno je ukloniti filtere (to radi sudo ./stand detach), pustiti ping, zatim postaviti filtere i pokušati ponovno. Problem je u tome da filter XDP_TX djeluje i na ARP, i ako je složena
prostor imena xdp-test zaboravio MAC adresu 192.0.2.1, ne može riješiti ovu IP adresu.
Postavljanje zadatka
Pređimo na zadatak koji je postavljen: napisati XDP mehanizam SYN kolačića.
Do danas je popularni DDoS napad SYN flood, čija suština je sljedeća. Kada se uspostavlja veza (TCP handshake), poslužitelj prima SYN, dodjeljuje resurse za buduću vezu, odgovara SYNACK paketom i čeka ACK. Napadač jednostavno šalje SYN pakete s lažnih adresa u tisućama po sekundi s svakog hosta iz tisuća botnetova. Poslužitelj je prisiljen dodjeljivati resurse odmah po dolasku paketa, a oslobađa ih nakon dugog vremenskog ograničenja, što rezultira iscrpljivanjem memorije ili limita, nove veze se ne prihvaćaju, usluga nije dostupna.
Ako ne dodjeljujemo resurse po SYN paketu, već samo odgovaramo SYNACK paketom, kako će poslužitelj znati da se ACK paket koji dolazi kasnije odnosi na SYN paket koji nije sačuvan? Napadač može generirati i lažne ACK. Suština SYN kolačića je kodiranje u seqnum parametre veze kao hash adresa, portova i promjenjive soli. Ako ACK stigne prije promjene soli, možemo ponovno izračunati hash i usporediti s acknum. Napadač to ne može lažirati, jer sol sadrži tajnu, a ne može to razbiti zbog ograničenog kanala. acknum SYN kolačić je dugo implementiran u Linux jezgru i čak se može automatski uključiti ako SYN dolaze previše brzo i masovno.
Obrazovanje o TCP handshake-u
TCP osigurava prijenos podataka kao tok bajtova, na primjer, HTTP zahtjevi se prenose preko TCP-a. Tok se prenosi u komadima unutar paketa. Svi TCP paketi imaju logičke zastavice i 32-bitne brojeve sekvenci:
Kombinacija zastavica određuje ulogu određenog paketa. Zastavica SYN znači da je to prvi paket šaljatelja u vezi. Zastavica ACK znači da je šaljatelj primio sve podatke veze do bajta
. Paket može imati više zastavica i naziva se prema njihovim kombinacijama, na primjer, SYNACK paket.
acknum. Paket može imati nekoliko oznaka i naziva se prema njihovim kombinacijama, na primjer, SYNACK paket.Broj sekvence (seqnum) određuje pomak u toku podataka za prvi bajt koji se šalje u ovom paketu. Na primjer, ako je u prvom paketu s X bajtovima podataka taj broj bio N, u sljedećem paketu s novim podacima bit će N+X. Na početku veze svaka strana nasumično odabire taj broj.
Broj potvrde (acknum) je sličan pomaku kao seqnum, ali određuje ne broj prenesenog bajta, već broj prvog bajta od primatelja, kojeg pošiljatelj nije vidio.
Na početku veze strane se moraju dogovoriti seqnum i acknum. Klijent šalje SYN paket s njegovim seqnum = X. Server odgovara SYNACK paketom, u koji zapisuje svoj seqnum = Y i postavlja acknum = X + 1. Klijent na SYNACK odgovara ACK paketom, gdje seqnum = X + 1, acknum = Y + 1. Nakon toga započinje sama predaja podataka.
Ako sugovornik ne potvrdi primitak paketa, TCP ga ponovo šalje prema tajmeru.
Zašto se SYN cookies ne koriste uvijek?
Prvo, ako se izgubi SYNACK ili ACK, morat ćete čekati ponovni prijenos — uspostava veze se usporava. Drugo, u SYN paketu — i samo u njemu! — prenosi se niz opcija koje utječu na daljnji rad veze. Ne pamteći dolazne SYN pakete, server na taj način ignorira te opcije, u sljedećim paketima klijent ih već neće poslati. TCP može raditi pri tome, ali barem u početnoj fazi kvaliteta veze će se smanjiti.
Sa stajališta paketa, XDP program treba učiniti sljedeće:
- na SYN odgovoriti SYNACK s cookie;
- na ACK odgovoriti RST (prekid veze);
- ostale pakete odbaciti.
Pseudokod algoritma zajedno s analizom paketa:
Ako to nije Ethernet,
preskoči paket.
Ako to nije IPv4,
preskoči paket.
Ako je adresa u tablici provjerenih, (*)
smanjiti brojač preostalih provjera,
preskoči paket.
Ako to nije TCP,
odbaci paket. (**)
Ako je to SYN,
odgovoriti SYN-ACK s cookie.
Ako je to ACK,
ako u acknum nije cookie,
odbaci paket.
Unesi u tablicu adresu s N preostalih provjera. (*)
Odgovori RST. (**)
U ostalim slučajevima odbaci paket.Jedna (*) označena su mjesta na kojima treba upravljati stanjem sustava — u prvom koraku može se proći bez njih, jednostavno implementirajući TCP handshake s generiranjem SYN cookie kao seqnum.
Na mjestu (**), dok nemamo tablicu, preskočit ćemo paket.
Implementacija TCP handshake-a
Analiza paketa i verifikacija koda
Trebaju nam strukture mrežnih zaglavlja: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) i TCP (uapi/linux/tcp.h). Na kraju nisam uspeo da se povežem zbog grešaka povezanih sa atomic64_t, morao sam da kopiram potrebne definicije u kod.
Sve funkcije koje se definišu u C-u radi lakšeg čitanja, trebaju biti ugrađene na mestu poziva, jer verifikator eBPF u jezgru zabranjuje povratne prelaze, tj. zapravo, petlje i pozive funkcija.
#define INTERNAL static __attribute__((always_inline))Makro LOG() isključuje štampanje u objavljenoj verziji.
Program predstavlja lanac funkcija. Svaka prima paket, u kojem je naglašeno zaglavlje odgovarajuće razine, na primer, process_ether() očekuje da je popunjeno ether. Na osnovu rezultata analize polja funkcija može preneti paket na višu razinu. Rezultat rada funkcije je akcija XDP. Za sada se rukovaoci SYN i ACK propuštaju sve pakete.
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; /* 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);
}Obraćam pažnju na provere označene A i B. Ako komentarišem A, program će se prevesti, ali će prilikom učitavanja biti greška u verifikaciji:
Verifikator analiza:
11: (7b) *(u64 *)(r10 -48) = r1
12: (71) r3 = *(u8 *)(r7 +13)
nevažeći pristup paketu, off=13 veličina=1, R7(id=0,off=0,r=0)
R7 offset je izvan paketa
obrađeno 11 insns (limit 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0
Greška u preuzimanju programa/mape!Ključna linija nevažeći pristup paketu, off=13 veličina=1, R7(id=0,off=0,r=0): postoje putevi izvršenja kada je trinaesti bajt od početka bafera izvan paketa. Prema listingu je malo teže shvatiti o kojoj liniji se radi, ali postoji broj instrukcije (12) i disasemblator koji pokazuje linije izvorne koda:
llvm-objdump -S xdp_filter.o | lessU ovom slučaju se odnosi na liniju
LOG("Ether(proto=0x%x)", bpf_ntohs(ether->h_proto));na kojoj je jasno da je problem u ether. Uvijek tako.
Odgovor na SYN
Cilj u ovoj fazi je stvoriti ispravno SYNACK pakete sa fiksnim seqnum, koji će kasnije biti zamijenjen sa SYN cookie. Sve promjene se događaju u process_tcp_syn() i okolini.
Provjera paketa
Zanimljivo je, ovo je najuočljivija linija, preciznije, komentar na nju:
/* Required to verify checksum calculation */
const void* data_end = (const void*)ctx->data_end;Pri pisanju prve verzije koda korišteno je jezgro 5.1, za čije je verifikator bio razlika između data_end i (const void*)ctx->data_end. Pri pisanju članka jezgro 5.3.1 nije imalo takav problem. Moguće je da se kompajler obraćao lokalnoj varijable drugačije nego polju. Moral - pri velikoj složenosti pojednostavljenje koda može pomoći.
Dalje rutinske provjere dužina u slavu verifikatora; o MAX_CSUM_BYTES ispod.
const u32 ip_len = ip->ihl * 4;
if ((void*)ip + ip_len > data_end) {
return XDP_DROP; /* neispravan paket */
}
if (ip_len > MAX_CSUM_BYTES) {
return XDP_ABORTED; /* ograničenje implementacije */
}
const u32 tcp_len = tcp->doff * 4;
if ((void*)tcp + tcp_len > (void*)ctx->data_end) {
return XDP_DROP; /* neispravan paket */
}
if (tcp_len > MAX_CSUM_BYTES) {
return XDP_ABORTED; /* ograničenje implementacije */
}Obrat paketa
Popunjavamo seqnum i acknum, postavljamo ACK (SYN je već postavljen):
const u32 cookie = 42;
tcp->ack_seq = bpf_htonl(bpf_ntohl(tcp->seq) + 1);
tcp->seq = bpf_htonl(cookie);
tcp->ack = 1;Zamijenimo TCP portove, IP adresu i MAC adrese. Standardna biblioteka nije dostupna iz XDP programa, zato memcpy() — makro koji skriva Clang intrinzične funkcije.
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);Ponovno izračunavanje kontrolnih zbrojeva
Kontrolni zbrojevi IPv4 i TCP zahtijevaju zbrajanje svih 16-bitnih riječi u zaglavljima, a veličina zaglavlja je upisana u njima, to jest, u trenutku kompilacije nije poznata. To je problem, jer verifikator neće proći običnu petlju do varijante granice. Ali veličina zaglavlja je ograničena: do 64 bajta svaki. Može se napraviti petlja s fiksnim brojem iteracija koja može završiti ranije.
Napominjem da postoji o tome kako ponovno izračunati kontrolnu sumu djelomično, ako su izmijenjene samo fiksne riječi paketa. Međutim, način nije univerzalan, a implementaciju bi bilo teže održavati.
Funkcija za izračun kontrolne sume:
#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;
}Unatoč tome što size provjereno od strane pozivnog koda, drugo uvjetno isključenje je potrebno kako bi verifikator mogao dokazati završetak petlje.
Za 32-bitne riječi implementirana je pojednostavljena verzija:
INTERNAL u32
sum16_32(u32 v) {
return (v >> 16) + (v & 0xffff);
}Zapravo, ponovni izračun kontrolnih zbrojeva i slanje paketa natrag:
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;Funkcija carry() pravi kontrolnu sumu iz 32-bitnog zbroja 16-bitnih riječi, u skladu s RFC 791.
Provjera TCP rukovanja
Filter ispravno uspostavlja vezu s netcat, preskočivši završni ACK, na koji je Linux odgovorio RST paketom, jer mrežni stog nije primio SYN - on je bio prepravljen u SYNACK i poslan natrag - i s gledišta OS-a paket je stigao, koji se ne odnosi na otvorene veze.
$ sudo ip netns exec xdp-test nc -nv 192.0.2.1 6666
192.0.2.1 6666: Veza resetirana od strane partneraVažno je provjeriti baš punim aplikacijama i promatrati tcpdump na xdp-remote jer, na primjer, hping3 ne reagira na netočne kontrolne zbrojeve.
SYN cookie
S gledišta XDP, sama provjera je trivijalna. Algoritam izračuna je primitivan i vjerojatno ranjiv na naprednog napadača. Linux kernel, na primjer, koristi kriptografski SipHash, ali njegova implementacija za XDP očito nadilazi okvire ovog članka.
Pojavio se za nove TODO-ove vezane uz vanjsku interakciju:
XDP program ne može pohranjivati
cookie_seed(tajni dio soli) u globalnoj varijabli, potrebno je skladište u kernelu, čija će se vrijednost periodično ažurirati iz pouzdanog generatora.Pri podudaranju SYN cookie u ACK paketu, potrebno je ne ispisivati poruku, već zapamtiti IP provjerenog klijenta kako bi se kasnije propuštali paketi od njega.
Provjera legitimnim klijentom:
$ sudoip netns exec xdp-test nc -nv 192.0.2.1 6666
192.0.2.1 6666: Veza resetirana od strane partneraU logovima je zabilježeno prolazak provjere (flags=0x2 — to je SYN, flags=0x10 — to je 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 20200c0Dokle nema popisa provjerenih IP adresa, zaštite od SYN flooda neće biti, ali evo reakcije na ACK flood, pokrenutu takvom naredbom:
sudo ip netns exec xdp-test hping3 --flood -A -s 1111 -p 2222 192.0.2.1Zabilješke u logu:
Ether(proto=0x800)
IP(src=0x15bd11a dst=0x15bd11e proto=6)
TCP(sport=3236 dport=2222 flags=0x10)
cookie mismatchZaključak
Ponekad se eBPF, a posebno XDP, doživljava više kao alat naprednog administratora, nego kao platforma za razvoj. Naime, XDP je alat za intervenciju u obradi paketa od strane jezgre, a ne alternativa jezgrenom stogu kao što su DPDK i druge varijante za zaobilaženje jezgre. S druge strane, XDP omogućuje implementaciju prilično složene logike koja se, osim toga, lako ažurira bez prekida u obradi prometa. Verifikator ne stvara velike probleme, osobno ne bih odbio nešto takvo za dijelove koda korisničkog prostora.
U drugom dijelu, ako je tema zanimljiva, završit ćemo tablicu provjerenih klijenata i prekida veza, implementirati brojače i napisati korisničku alatku za upravljanje filtrima.
Linkovi:
Izvor: habr.com
