Na XDP pišemo zaštitu od DDoS napada. Nuklearni dio

Tehnologija eXpress Data Path (XDP) omogućuje proizvoljnu obradu prometa na Linux sučeljima prije nego što paketi uđu u mrežni stog kernela. Primjena XDP - zaštita od DDoS napada (CloudFlare), složeni filteri, prikupljanje statistike (Netflix). XDP programe izvršava eBPF virtualni stroj i stoga imaju ograničenja i na njihov kod i na dostupne funkcije kernela, ovisno o vrsti filtra.

Namjera članka je nadoknaditi nedostatke brojnih materijala o XDP-u. Prvo, oni pružaju gotov kod koji odmah zaobilazi značajke XDP-a: pripremljen za provjeru ili previše jednostavan da uzrokuje probleme. Kada kasnije pokušate napisati vlastiti kod od nule, nema razumijevanja što učiniti s tipičnim pogreškama. Drugo, ne pokriva načine lokalnog testiranja XDP-a bez VM-a i hardvera, unatoč činjenici da oni imaju svoje zamke. Tekst je namijenjen programerima poznavateljima mreža i Linuxa koje zanimaju XDP i eBPF.

U ovom ćemo dijelu detaljno razumjeti kako se sastavlja XDP filter i kako ga testirati, zatim ćemo napisati jednostavnu verziju dobro poznatog mehanizma SYN kolačića na razini obrade paketa. Dok ne formiramo "bijelu listu"
provjereni klijenti, voditi brojače i upravljati filtrom - dovoljno zapisa.

Napisat ćemo u C - ovo nije moderno, ali praktično. Sav kod je dostupan na GitHubu na poveznici na kraju i podijeljen je u commitove prema koracima opisanim u članku.

Odricanje. U tijeku članka razvit će se mini rješenje za odbijanje DDoS napada jer je to realan zadatak za XDP i moje područje. Međutim, glavni cilj je razumjeti tehnologiju, ovo nije vodič za stvaranje gotove zaštite. Kôd vodiča nije optimiziran i izostavlja neke nijanse.

Kratak pregled XDP-a

Navest ću samo ključne točke kako ne bih duplirao dokumentaciju i postojeće članke.

Dakle, kod filtera se učitava u kernel. Filtru se prosljeđuju dolazni paketi. Kao rezultat toga, filter mora donijeti odluku: proslijediti paket kernelu (XDP_PASS), ispusti paket (XDP_DROP) ili poslati natrag (XDP_TX). Filter može mijenjati paket, ovo posebno vrijedi za XDP_TX. Također možete srušiti program (XDP_ABORTED) i ispustite paket, ali ovo je analogno assert(0) - za otklanjanje pogrešaka.

eBPF (prošireni Berkley Packet Filter) virtualni stroj namjerno je napravljen jednostavnim kako bi kernel mogao provjeriti da se kod ne ponavlja i ne oštećuje tuđu memoriju. Kumulativna ograničenja i provjere:

  • Loopovi (skokovi unazad) su zabranjeni.
  • Postoji stog za podatke, ali nema funkcija (sve C funkcije moraju biti ugrađene).
  • Zabranjeni su pristupi memoriji izvan stoga i međuspremnika paketa.
  • Veličina koda je ograničena, ali u praksi to nije previše značajno.
  • Dopuštene su samo posebne funkcije jezgre (eBPF pomoćnici).

Razvoj i instaliranje filtra izgleda ovako:

  1. izvorni kod (npr. kernel.c) kompajlira u objekt (kernel.o) za arhitekturu eBPF virtualnog stroja. Od listopada 2019. kompajliranje u eBPF podržava Clang i obećano je u GCC 10.1.
  2. Ako u ovom objektnom kodu postoje pozivi strukturama kernela (na primjer, tablicama i brojačima), umjesto njihovih ID-ova postoje nule, odnosno takav se kod ne može izvršiti. Prije učitavanja u kernel, ove nule moraju biti zamijenjene ID-ovima specifičnih objekata kreiranih kroz kernel pozive (povežite kod). To možete učiniti s vanjskim uslužnim programima ili možete napisati program koji će povezati i učitati određeni filtar.
  3. Kernel provjerava program koji se učitava. Provjerava odsutnost ciklusa i neizlazak granica paketa i hrpe. Ako verifikator ne može dokazati da je kod ispravan, program se odbija - mora se moći zadovoljiti.
  4. Nakon uspješne provjere, kernel kompajlira objektni kod eBPF arhitekture u strojni kod arhitekture sustava (just-in-time).
  5. Program je priključen na sučelje i počinje s obradom paketa.

Budući da XDP radi u jezgri, otklanjanje pogrešaka provodi se zapisnicima praćenja i, zapravo, paketima koje program filtrira ili generira. Međutim, eBPF čuva preuzeti kod na sigurnom za sustav, tako da možete eksperimentirati s XDP-om izravno na svom lokalnom Linuxu.

Priprema okoliša

zbor

Clang ne može izravno izdati objektni kod za eBPF arhitekturu, tako da se proces sastoji od dva koraka:

  1. Kompajlirajte C kod u LLVM bajt kod (clang -emit-llvm).
  2. Pretvori bajt kod u eBPF objektni kod (llc -march=bpf -filetype=obj).

Prilikom pisanja filtra dobro će doći nekoliko datoteka s pomoćnim funkcijama i makroima iz kernel testova. Važno je da odgovaraju verziji kernela (KVER). Preuzmite ih na 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 BASE

Makefile za Arch Linux (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 ./*.o

KDIR sadrži put do zaglavlja kernela, ARCH - Arhitektura sustava. Putovi i alati mogu se malo razlikovati između distribucija.

Primjer razlike za Debian 10 (kernel 4.19.67)

# другая команда
CLANG ?= clang
LLC ?= llc-7

# другой каталог
KDIR ?= /usr/src/linux-headers-$(shell uname -r)
ARCH ?= $(subst x86_64,x86,$(shell uname -m))

# два дополнительных каталога -I
CFLAGS = 
    -Ihelpers 
    
    -I/usr/src/linux-headers-4.19.0-6-common/include 
    -I/usr/src/linux-headers-4.19.0-6-common/arch/$(ARCH)/include 
    # далее без изменений

CFLAGS uključuju direktorij s pomoćnim zaglavljima i nekoliko direktorija sa zaglavljima jezgre. Simbol __KERNEL__ znači da su zaglavlja UAPI (API korisničkog prostora) definirana za kod kernela, budući da se filtar izvršava u kernelu.

Zaštita skupa može se onemogućiti (-fno-stack-protector) jer verifikator eBPF koda ionako provjerava da li nije izvan granica stoga. Trebali biste odmah omogućiti optimizacije jer je veličina bajt koda eBPF ograničena.

Počnimo s filtrom koji propušta sve pakete i ne radi ništa:

#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";

Momčad make prikuplja xdp_filter.o. Gdje ga sada možete testirati?

Ispitno postolje

Stalak bi trebao sadržavati dva sučelja: na kojem će biti filter i s kojeg će se slati paketi. To moraju biti puni Linux uređaji s vlastitim IP-ovima kako bismo provjerili kako obične aplikacije rade s našim filtrom.

Uređaji poput veth (virtualni Ethernet) prikladni su za nas: oni su par virtualnih mrežnih sučelja "povezanih" izravno jedno s drugim. Možete ih izraditi ovako (u ovom odjeljku sve naredbe ip izvedeno iz root):

ip link add xdp-remote type veth peer name xdp-local

Ovdje xdp-remote и xdp-local — imena uređaja. Na xdp-local (192.0.2.1/24) bit će priložen filtar, sa xdp-remote (192.0.2.2/24) poslat će se dolazni promet. Međutim, postoji problem: sučelja su na istom stroju i Linux neće slati promet jednom od njih preko drugog. Možete to riješiti lukavim pravilima iptables, ali će morati promijeniti pakete, što je nezgodno prilikom otklanjanja pogrešaka. Bolje je koristiti mrežne prostore imena (mrežni prostori imena, dalje netns).

Prostor imena mreže sadrži skup sučelja, tablica usmjeravanja i NetFilter pravila koja su izolirana od sličnih objekata u drugim mrežama. Svaki proces se izvodi u nekom prostoru imena i dostupni su mu samo objekti ove mreže. Prema zadanim postavkama, sustav ima jedinstven mrežni prostor imena za sve objekte, tako da možete raditi na Linuxu i ne znati za netns.

Kreirajmo novi imenski prostor xdp-test i preseli se tamo xdp-remote.

ip netns add xdp-test
ip link set dev xdp-remote netns xdp-test

Zatim se pokreće proces xdp-test, neće "vidjeti" xdp-local (ostat će u netns prema zadanim postavkama) i kada šalje paket na 192.0.2.1 proći će ga kroz xdp-remote, jer je to jedino sučelje na 192.0.2.0/24 dostupno ovom procesu. Ovo također radi i obrnuto.

Prilikom prelaska između mreža, sučelje pada i gubi adresu. Za postavljanje sučelja u netns, morate pokrenuti ip ... u ovom prostoru imena naredbi 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 up

Kao što vidite, ovo se ne razlikuje od postavljanja xdp-local u zadanom prostoru imena:

    ip address add 192.0.2.1/24 dev xdp-local
    ip link set xdp-local up

Ako trči tcpdump -tnevi xdp-local, možete vidjeti da su paketi poslani s xdp-test, isporučuju se na ovo sučelje:

ip netns exec xdp-test   ping 192.0.2.1

Prikladno je pokrenuti školjku xdp-test. Repozitorij ima skriptu koja automatizira rad sa postoljem, na primjer, možete postaviti postolje naredbom sudo ./stand up i uklonite ga sudo ./stand down.

traganje

Filtar je pričvršćen na uređaj ovako:

ip -force link set dev xdp-local xdp object xdp_filter.o verbose

ključ -force potreban za povezivanje novog programa ako je drugi već povezan. "Nema vijesti je dobra vijest" ne odnosi se na ovu naredbu, izlaz je ionako obiman. naznačiti verbose izborno, ali uz njega se pojavljuje izvješće o radu verifikatora koda s popisom asemblera:

Verifier analysis:

0: (b7) r0 = 2
1: (95) exit

Odvojite program od sučelja:

ip link set dev xdp-local xdp off

U skripti su to naredbe sudo ./stand attach и sudo ./stand detach.

Vezivanjem filtra možete to osigurati ping nastavlja raditi, ali radi li program? Dodajmo logotipe. Funkcija bpf_trace_printk() slično printf(), ali podržava samo do tri argumenta osim uzorka i ograničen popis specifikacija. Makro bpf_printk() pojednostavljuje poziv.

   SEC("prog")
   int xdp_main(struct xdp_md* ctx) {
+      bpf_printk("got packet: %pn", ctx);
       return XDP_PASS;
   }

Izlaz ide na kanal praćenja jezgre, koji mora biti omogućen:

echo -n 1 | sudo tee /sys/kernel/debug/tracing/options/trace_printk

Prikaz tijeka poruke:

cat /sys/kernel/debug/tracing/trace_pipe

Oba ova tima upućuju poziv sudo ./stand log.

Ping bi sada u sebi trebao proizvoditi poruke poput ove:

<...>-110930 [004] ..s1 78803.244967: 0: got packet: 00000000ac510377

Ako 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) call bpf_trace_printk#6
<...>

Činjenica je da eBPF programi nemaju podatkovni odjeljak, tako da su jedini način za kodiranje niza formata neposredni argumenti VM naredbi:

$ python -c "import binascii; print(bytes(reversed(binascii.unhexlify('0a7025203a74656b63617020746f67'))))"
b'got packet: %pn'

Iz tog razloga izlaz za otklanjanje pogrešaka uvelike povećava rezultirajući kod.

Slanje XDP paketa

Promijenimo filtar: neka šalje sve dolazne pakete natrag. To je netočno s mrežnog gledišta, jer bi bilo potrebno promijeniti adrese u zaglavljima, ali sada je važan rad u načelu.

       bpf_printk("got packet: %pn", ctx);
-      return XDP_PASS;
+      return XDP_TX;
   }

Pokreni tcpdump na xdp-remote. Trebao bi prikazati identičan odlazni i dolazni ICMP Echo Request i prestati prikazivati ​​ICMP Echo Reply. Ali ne vidi se. Ispostavilo se da djeluje XDP_TX u programu za xdp-local nužanza uparivanje sučelja xdp-remote također je dodijeljen program, čak i ako je bio prazan, i podignut je.

Kako sam znao?

Praćenje putanje paketa u kernelu mehanizam perf događaja omogućuje, usput, korištenje istog virtualnog stroja, odnosno eBPF se koristi za rastavljanje s eBPF-om.

Od zla morate napraviti dobro, jer se od njega ne može napraviti ništa drugo.

$ 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 šifra 6?

$ errno 6
ENXIO 6 No such device or address

Funkcija veth_xdp_flush_bq() dobiva kod greške od veth_xdp_xmit(), gdje traži po ENXIO i pronađite komentar.

Vratite minimalni filtar (XDP_PASS) u datoteci xdp_dummy.c, dodajte ga u Makefile, povežite s xdp-remote:

ip netns exec remote 
    ip link set dev int xdp object dummy.o

Sada tcpdump pokazuje š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 64

Ako se umjesto toga prikazuje samo ARP, trebate ukloniti filtre (ovo čini sudo ./stand detach), neka ping, zatim instalirajte filtre i pokušajte ponovo. Problem je u tome što filter XDP_TX također utječe na ARP, a ako stek
imenski prostori xdp-test uspio "zaboraviti" MAC adresu 192.0.2.1, neće moći riješiti ovu IP adresu.

Formuliranje problema

Prijeđimo na navedeni zadatak: napisati mehanizam SYN kolačića na XDP.

Do sada, SYN poplava ostaje popularan DDoS napad, čija je suština sljedeća. Kada se veza uspostavi (TCP rukovanje), 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 količini od tisuća u sekundi sa svakog hosta u više tisuća botnet-a. Poslužitelj je prisiljen alocirati resurse odmah po dolasku paketa, ali ga oslobađa nakon dugog vremenskog ograničenja, kao rezultat toga, memorija ili ograničenja su iscrpljeni, nove veze se ne prihvaćaju, usluga je nedostupna.

Ako ne dodijelite resurse na SYN paketu, nego samo odgovorite sa SYNACK paketom, kako onda poslužitelj može razumjeti da ACK paket koji je došao kasnije pripada SYN paketu koji nije spremljen? Uostalom, napadač također može generirati lažne ACK-ove. Bit SYN kolačića je kodiranje u seqnum parametri veze kao hash adresa, portova i promjene soli. Ako je ACK uspio stići prije promjene soli, možete ponovno izračunati hash i usporediti ga acknum. lažan acknum napadač ne može, budući da sol uključuje tajnu, i neće imati vremena pregledati je zbog ograničenog kanala.

SYN kolačići implementirani su u Linux kernel već dugo vremena i mogu se čak automatski omogućiti ako SYN-ovi stignu prebrzo i u velikom broju.

Obrazovni program o TCP rukovanju

TCP osigurava prijenos podataka kao tok bajtova, na primjer, HTTP zahtjevi se prenose preko TCP-a. Tok se prenosi dio po dio u paketima. Svi TCP paketi imaju logičke oznake i 32-bitne redne brojeve:

  • Kombinacija zastavica definira ulogu određenog paketa. Oznaka SYN znači da je ovo prvi paket pošiljatelja na vezi. ACK oznaka znači da je pošiljatelj primio sve podatke o vezi do bajta. acknum. Paket može imati nekoliko zastavica i nazvan je prema njihovoj kombinaciji, 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 bajtova podataka taj broj bio N, u sljedećem paketu s novim podacima to će biti N+X. Na početku razgovora svaka strana odabire ovaj broj nasumično.

  • Broj potvrde (acknum) - isti pomak kao seqnum, ali ne određuje broj poslanog bajta, već broj prvog bajta od primatelja, koji pošiljatelj nije vidio.

Na početku spajanja strane se moraju dogovoriti seqnum и acknum. Klijent šalje SYN paket sa svojim seqnum = X. Poslužitelj odgovara SYNACK paketom, gdje zapisuje svoje seqnum = Y i razotkriva acknum = X + 1. Klijent odgovara na SYNACK s ACK paketom, gdje seqnum = X + 1, acknum = Y + 1. Nakon toga počinje stvarni prijenos podataka.

Ako sugovornik ne potvrdi primitak paketa, TCP ga ponovno šalje nakon isteka vremena.

Zašto se SYN kolačići ne koriste uvijek?

Prvo, ako se izgubi SYNACK ili ACK, morat ćete pričekati ponovno slanje - 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, poslužitelj stoga ignorira ove opcije, u sljedećim paketima klijent ih više neće slati. TCP u ovom slučaju može raditi, ali barem u početnoj fazi kvaliteta veze će se smanjiti.

Što se tiče paketa, XDP program bi trebao raditi sljedeće:

  • odgovoriti na SYN sa SYNACK s kolačićem;
  • odgovoriti na ACK s RST (prekinuti vezu);
  • ispustite druge pakete.

Pseudokod algoritma zajedno s raščlanjivanjem paketa:

Если это не Ethernet,
    пропустить пакет.
Если это не IPv4,
    пропустить пакет.
Если адрес в таблице проверенных,               (*)
        уменьшить счетчик оставшихся проверок,
        пропустить пакет.
Если это не TCP,
    сбросить пакет.     (**)
Если это SYN,
    ответить SYN-ACK с cookie.
Если это ACK,
    если в acknum лежит не cookie,
        сбросить пакет.
    Занести в таблицу адрес с N оставшихся проверок.    (*)
    Ответить RST.   (**)
В остальных случаях сбросить пакет.

Jedan (*) označene su točke u kojima trebate upravljati stanjem sustava - u prvoj fazi možete i bez njih jednostavnim implementiranjem TCP rukovanja uz generiranje SYN kolačića kao seqnuma.

Na stranici (**), dok nemamo stol, paket ćemo preskočiti.

Implementacija TCP rukovanja

Raščlanjivanje paketa i provjera koda

Potrebne su nam strukture zaglavlja mreže: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) i TCP (uapi/linux/tcp.h). Posljednji nisam mogao spojiti zbog grešaka povezanih s njim atomic64_t, morao sam kopirati potrebne definicije u kod.

Sve funkcije koje se razlikuju u C-u radi čitljivosti moraju biti ugrađene na mjesto poziva, budući da eBPF verifikator u kernelu zabranjuje povratne skokove, odnosno, zapravo, petlje i pozive funkcija.

#define INTERNAL static __attribute__((always_inline))

Makro LOG() onemogućuje ispis u verziji izdanja.

Program je cjevovod funkcija. Svaki prima paket u kojem je označeno zaglavlje odgovarajuće razine, na primjer, process_ether() čekaju da budu ispunjeni ether. Na temelju rezultata analize polja, funkcija može prenijeti paket na višu razinu. Rezultat funkcije je XDP akcija. Dok rukovatelji 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 pozornost na provjere označene A i B. Ako komentirate A, program će se izgraditi, ali će se pojaviti pogreška provjere prilikom učitavanja:

Verifier analysis:

<...>
11: (7b) *(u64 *)(r10 -48) = r1
12: (71) r3 = *(u8 *)(r7 +13)
invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0)
R7 offset is outside of the packet
processed 11 insns (limit 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0

Error fetching program/map!

Ključni niz invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0): postoje staze izvršenja kada je trinaesti bajt od početka međuspremnika izvan paketa. Teško je iz popisa reći o kojoj liniji govorimo, ali postoji broj instrukcije (12) i disassembler koji prikazuje retke izvornog koda:

llvm-objdump -S xdp_filter.o | less

U ovom slučaju pokazuje na liniju

LOG("Ether(proto=0x%x)", bpf_ntohs(ether->h_proto));

što jasno daje do znanja da je problem ether. Uvijek bi bilo tako.

Odgovorite na SYN

Cilj u ovoj fazi je generirati ispravan SYNACK paket s fiksnim seqnum, koji će u budućnosti biti zamijenjen kolačićem SYN. Sve promjene se događaju u process_tcp_syn() i okolice.

Provjera paketa

Začudo, evo najznamenitijeg retka, odnosno komentara na njega:

/* Required to verify checksum calculation */
const void* data_end = (const void*)ctx->data_end;

Prilikom pisanja prve verzije koda korišten je kernel 5.1 za čiji je verifikator postojala razlika između data_end и (const void*)ctx->data_end. U vrijeme pisanja, kernel 5.3.1 nije imao ovaj problem. Možda je kompajler pristupao lokalnoj varijabli drugačije nego polju. Moral - kod velikog ugniježđivanja može pomoći pojednostavljenje koda.

Daljnje rutinske provjere duljina za slavu verifikatora; O MAX_CSUM_BYTES u nastavku.

const u32 ip_len = ip->ihl * 4;
if ((void*)ip + ip_len > data_end) {
    return XDP_DROP; /* malformed packet */
}
if (ip_len > MAX_CSUM_BYTES) {
    return XDP_ABORTED; /* implementation limitation */
}

const u32 tcp_len = tcp->doff * 4;
if ((void*)tcp + tcp_len > (void*)ctx->data_end) {
    return XDP_DROP; /* malformed packet */
}
if (tcp_len > MAX_CSUM_BYTES) {
    return XDP_ABORTED; /* implementation limitation */
}

Širenje paketa

Ispunjavamo seqnum и acknum, postavite 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;

Zamijenite TCP portove, IP i MAC adrese. Standardna knjižnica nije dostupna iz programa XDP, tako da memcpy() — makronaredba koja skriva Clang intrinsik.

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 kontrolne sume

IPv4 i TCP kontrolni zbrojevi zahtijevaju dodavanje svih 16-bitnih riječi u zaglavlja, a veličina zaglavlja je zapisana u njima, odnosno u trenutku kompilacije je nepoznata. To je problem jer verifikator neće preskočiti normalnu petlju sve do granične varijable. Ali veličina zaglavlja je ograničena: do 64 bajta svako. Možete napraviti petlju s fiksnim brojem ponavljanja, koja može završiti ranije.

Napominjem da postoji RFC 1624 o tome kako ponovno djelomično izračunati kontrolni zbroj ako se promijene samo fiksne riječi paketa. Međutim, metoda nije univerzalna, a provedbu bi bilo teže održati.

Funkcija izračuna 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;
}

Iako size provjerava pozivni kod, drugi izlazni uvjet je neophodan kako bi verifikator mogao dokazati kraj petlje.

Za 32-bitne riječi implementirana je jednostavnija verzija:

INTERNAL u32
sum16_32(u32 v) {
    return (v >> 16) + (v & 0xffff);
}

Zapravo ponovno izračunavanje 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 << 8;
tcp->check = 0;
tcp_csum += sum16(tcp, tcp_len, data_end);
tcp->check = carry(tcp_csum);

return XDP_TX;

Funkcija carry() čini kontrolni zbroj od 32-bitnog zbroja 16-bitnih riječi, prema RFC 791.

Provjera TCP rukovanja

Filtar ispravno uspostavlja vezu s netcat, preskačući konačni ACK, na što je Linux odgovorio RST paketom, budući da mrežni stog nije primio SYN - pretvoren je u SYNACK i poslan natrag - a sa stajališta OS-a, stigao je paket koji nije vezane uz otvorene veze.

$ sudo ip netns exec xdp-test   nc -nv 192.0.2.1 6666
192.0.2.1 6666: Connection reset by peer

Važno je provjeriti s punopravnim aplikacijama i promatrati tcpdump na xdp-remote jer npr. hping3 ne reagira na netočne kontrolne zbrojeve.

Sa stajališta XDP-a sama provjera je trivijalna. Algoritam izračuna je primitivan i vjerojatno ranjiv na sofisticiranog napadača. Linux kernel, na primjer, koristi kriptografski SipHash, ali njegova implementacija za XDP očito je izvan opsega ovog članka.

Pojavilo se za nove TODO-ove koji se odnose na vanjsku interakciju:

  • XDP program ne može pohraniti cookie_seed (tajni dio soli) u globalnoj varijabli potrebna vam je kernel pohrana čija će se vrijednost povremeno ažurirati iz pouzdanog generatora.

  • Ako se SYN kolačić u ACK paketu podudara, ne morate ispisivati ​​poruku, ali zapamtite IP verificiranog klijenta kako biste dalje preskakali pakete s njega.

Validacija od strane legitimnog klijenta:

$ sudoip netns exec xdp-test   nc -nv 192.0.2.1 6666
192.0.2.1 6666: Connection reset by peer

Dnevnici su zabilježili prolazak provjere (flags=0x2 je SYN, flags=0x10 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 20200c0

Sve dok ne postoji popis provjerenih IP-ova, neće biti zaštite od samog SYN flooda, ali evo reakcije na ACK flood pokrenut ovom naredbom:

sudo ip netns exec xdp-test   hping3 --flood -A -s 1111 -p 2222 192.0.2.1

Unosi u dnevnik:

Ether(proto=0x800)
  IP(src=0x15bd11a dst=0x15bd11e proto=6)
    TCP(sport=3236 dport=2222 flags=0x10)
      cookie mismatch

Zaključak

Ponekad se eBPF općenito, a posebno XDP predstavljaju više kao napredni administratorski alat nego razvojna platforma. Doista, XDP je alat za ometanje obrade kernel paketa, a ne alternativa kernel stacku, poput DPDK-a i drugih opcija zaobilaženja kernela. S druge strane, XDP vam omogućuje implementaciju prilično složene logike, koja se, osim toga, lako ažurira bez pauze u obradi prometa. Verifikator ne stvara velike probleme, osobno ga ne bih odbio za dijelove koda korisničkog prostora.

U drugom dijelu, ako je tema zanimljiva, dopunit ćemo tablicu verificiranih klijenata i prekinuti veze, implementirati brojače i napisati userspace utility za upravljanje filterom.

reference:

Izvor: www.habr.com

Dodajte komentar