Pišemo zaštitu od DDoS napada na XDP. Nuklearni dio

Tehnologija eXpress Data Path (XDP) omogućava da se izvrši nasumična obrada prometa na Linux sučeljima prije nego što paketi uđu u mrežni stog kernela. Primena XDP-a - zaštita od DDoS napada (CloudFlare), složeni filteri, prikupljanje statistike (Netflix). XDP programe izvršava eBPF virtuelna mašina, tako da imaju ograničenja i na njihov kod i na dostupne funkcije kernela u zavisnosti od tipa filtera.

Članak ima za cilj da nadoknadi nedostatke brojnih materijala o XDP-u. Prvo, oni daju gotov kod koji odmah zaobilazi karakteristike XDP-a: pripremljen za verifikaciju ili previše jednostavan da izazove probleme. Kada kasnije pokušate da napišete sopstveni kod od nule, nema razumevanja šta da radite sa tipičnim greškama. Drugo, ne pokriva načine lokalnog testiranja XDP-a bez VM-a i hardvera, uprkos činjenici da oni imaju svoje zamke. Tekst je namijenjen programerima koji poznaju mreže i Linux koji su zainteresirani za XDP i eBPF.

U ovom dijelu ćemo detaljno razumjeti kako se XDP filter sastavlja i kako ga testirati, zatim ćemo napisati jednostavnu verziju dobro poznatog mehanizma SYN kolačića na nivou obrade paketa. Dok ne formiramo "bijelu listu"
verifikovani klijenti, vodite brojače i upravljajte filterom - dovoljno dnevnika.

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

Odricanje od odgovornosti. U toku ovog članka biće razvijeno mini rešenje za odbijanje DDoS napada, jer je to realan zadatak za XDP i moju oblast. Međutim, glavni cilj je razumjeti tehnologiju; ovo nije vodič za kreiranje gotove zaštite. Kod tutorijala nije optimizovan i izostavlja neke nijanse.

Kratak pregled XDP-a

Navešću samo ključne tačke kako ne bih duplirao dokumentaciju i postojeće članke.

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

Virtuelna mašina eBPF (prošireni Berkley Packet Filter) namerno je pojednostavljena tako da kernel može da proveri da se kod ne vrti u petlji i da ne ošteti memoriju drugih ljudi. Kumulativna ograničenja i provjere:

  • Loops (skokovi nazad) su zabranjeni.
  • Postoji stog za podatke, ali nema funkcija (sve C funkcije moraju biti umetnute).
  • Pristup memoriji izvan steka i bafera paketa je zabranjen.
  • Veličina koda je ograničena, ali u praksi to nije mnogo značajno.
  • Dozvoljene su samo posebne funkcije kernela (eBPF pomoćnici).

Dizajniranje i ugradnja filtera izgleda ovako:

  1. Izvorni kod (npr kernel.c) kompajlira u objekt (kernel.o) za arhitekturu virtuelne mašine eBPF. Od oktobra 2019. Clang podržava kompajliranje u eBPF i obećava ga 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 kod se ne može izvršiti. Prije učitavanja u kernel, ove nule moraju biti zamijenjene ID-ovima određenih objekata kreiranih kroz pozive kernela (povezati kod). To možete učiniti s vanjskim uslužnim programima ili možete napisati program koji će povezati i učitati određeni filter.
  3. Kernel provjerava program koji se učitava. Provjerava odsustvo ciklusa i neizlazak granica paketa i steka. Ako verifikator ne može dokazati da je kod ispravan, program se odbacuje - mora se moći ugoditi njemu.
  4. Nakon uspješne verifikacije, kernel kompajlira objektni kod eBPF arhitekture u mašinski kod arhitekture sistema (baš na vrijeme).
  5. Program je povezan sa interfejsom i počinje da obrađuje pakete.

Pošto XDP radi u kernelu, otklanjanje grešaka se zasniva na evidenciji praćenja i, u stvari, na paketima koje program filtrira ili generiše. Međutim, eBPF čuva preuzeti kod bezbednim za sistem, tako da možete eksperimentisati sa XDP-om direktno na vašem lokalnom Linux-u.

Priprema okoline

Montaža

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

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

Prilikom pisanja filtera dobro će doći nekoliko datoteka s pomoćnim funkcijama i makroima iz testova kernela. 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 putanju do zaglavlja kernela, ARCH — arhitektura sistema. Putevi i alati mogu se neznatno 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čiti direktorij s pomoćnim zaglavljima i nekoliko direktorija sa zaglavljima kernela. Simbol __KERNEL__ znači da su UAPI (userspace API) zaglavlja definirana za kod kernela, budući da se filter izvršava u kernelu.

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

Počnimo s filterom 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";

tim make prikuplja xdp_filter.o. Gdje ga sada možete testirati?

Test stalak

Stalak treba da sadrži dva interfejsa: na kojem će biti filter i sa kojih će se slati paketi. To moraju biti potpuni Linux uređaji sa vlastitim IP adresama kako bi provjerili kako obične aplikacije rade s našim filterom.

Uređaji kao što je veth (virtualni Ethernet) su prikladni za nas: oni su par virtuelnih mrežnih interfejsa koji su „povezani” direktno jedan na drugi. Možete ih kreirati ovako (u ovom odjeljku sve naredbe ip izvedeno od root):

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

to je xdp-remote и xdp-local — nazivi uređaja. On xdp-local (192.0.2.1/24) filter će biti pričvršćen, sa xdp-remote (192.0.2.2/24) dolazni saobraćaj će biti poslat. Međutim, postoji problem: sučelja su na istom stroju, a Linux neće slati promet jednom od njih preko drugog. Ovo možete riješiti škakljivim pravilima iptables, ali će morati promijeniti pakete, što je nezgodno prilikom otklanjanja grešaka. Bolje je koristiti mrežne prostore imena (mrežni prostori imena, dalje mreže).

Mrežni imenski prostor sadrži skup sučelja, tablica rutiranja i NetFilter pravila koja su izolirana od sličnih objekata u drugim mrežama. Svaki proces se pokreće u nekom imenskom prostoru i samo objekti ove mreže su mu dostupni. Podrazumevano, sistem ima jedan mrežni prostor imena za sve objekte, tako da možete raditi na Linuxu i ne znati za netns.

Kreirajmo novi prostor imena xdp-test i preseliti se tamo xdp-remote.

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

Zatim se proces pokreće xdp-test, neće "vidjeti" xdp-local (podrazumevano će ostati u netns) i prilikom slanja paketa na 192.0.2.1 će ga proći kroz xdp-remote, jer je to jedini interfejs na 192.0.2.0/24 koji je dostupan ovom procesu. Ovo radi i obrnuto.

Kada se krećete između mreža, interfejs se spušta i gubi adresu. Da biste podesili sučelje u netns, morate pokrenuti ip ... u ovom imenskom prostoru naredbe 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 podešavanja xdp-local u zadanom imenskom prostoru:

    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 iz xdp-test, isporučuju se na ovaj interfejs:

ip netns exec xdp-test   ping 192.0.2.1

Zgodno je pokrenuti školjku xdp-test. Repozitorijum ima skriptu koja automatizuje rad sa postoljem, na primer, možete podesiti štand komandom sudo ./stand up i uklonite ga sudo ./stand down.

praćenje

Filter je povezan sa uređajem poput ovog:

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

Ključ -force potrebno za povezivanje novog programa ako je drugi već povezan. "Nijedna vijest nije dobra vijest" se ne odnosi na ovu naredbu, rezultat je ionako obiman. ukazati verbose opciono, ali uz njega se pojavljuje izvještaj o radu verifikatora koda sa listingom asemblera:

Verifier analysis:

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

Prekinite vezu programa sa interfejsom:

ip link set dev xdp-local xdp off

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

Vezivanjem filtera možete se uvjeriti u to ping nastavlja da radi, ali da li program funkcioniše? Hajde da dodamo logotipe. Funkcija bpf_trace_printk() slicno printf(), ali podržava samo do tri argumenta osim uzorka i ograničenu listu 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 kernela, koji treba biti omogućen:

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

Pogledaj nit poruka:

cat /sys/kernel/debug/tracing/trace_pipe

Oba ova tima pozivaju sudo ./stand log.

Ping bi sada trebao proizvoditi poruke poput ove:

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

Ako pažljivo pogledate izlaz verifikatora, možete primijetiti čudne prorač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 odjeljak podataka, tako da je 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 grešaka uvelike naduvava rezultujući kod.

Slanje XDP paketa

Promijenimo filter: neka šalje sve dolazne pakete nazad. Ovo je netačno sa mrežne tačke gledišta, jer bi bilo potrebno promijeniti adrese u zaglavljima, ali sada je važan princip rada.

       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 eho zahtjev i prestati prikazivati ​​ICMP eho odgovor. Ali to se ne pokazuje. Ispostavilo se da za posao XDP_TX u programu za xdp-local je neophodnoza uparivanje interfejsa xdp-remote program je također dodijeljen, čak i ako je bio prazan, i on je podignut.

Kako sam znao?

Praćenje putanje paketa u kernelu mehanizam perf događaja omogućava, inače, korištenje iste virtuelne mašine, odnosno eBPF se koristi za rastavljanje sa eBPF-om.

Od zla morate napraviti dobro, jer se od njega ne može ništa drugo 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])
                                     <...>

Šta je kod 6?

$ errno 6
ENXIO 6 No such device or address

funkcija veth_xdp_flush_bq() prima šifru greške od veth_xdp_xmit(), gdje traži po ENXIO i pronađite komentar.

Vratite minimalni filter (XDP_PASS) u fajlu xdp_dummy.c, dodajte ga u Makefile, povežite se na xdp-remote:

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

Sada tcpdump pokazuje šta 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 filtere (ovo čini sudo ./stand detach), neka ping, zatim instalirajte filtere i pokušajte ponovo. Problem je što filter XDP_TX utiče i na ARP, a ako stek
imenski prostori xdp-test uspeo da "zaboravi" MAC adresu 192.0.2.1, neće moći da reši ovu IP adresu.

Izjava o problemu

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

Do sada, SYN flood ostaje popularan DDoS napad, čija je suština sljedeća. Kada se veza uspostavi (TCP rukovanje), server prima SYN, dodeljuje resurse za buduću vezu, odgovara SYNACK paketom i čeka ACK. Napadač jednostavno šalje SYN pakete sa lažnih adresa u količini od hiljada u sekundi sa svakog hosta više hiljada botneta. Server je primoran da dodijeli resurse odmah po dolasku paketa, ali ga otpušta nakon dužeg vremena čekanja, kao rezultat toga, memorija ili ograničenja su iscrpljena, nove veze se ne prihvaćaju, usluga je nedostupna.

Ako ne dodijelite resurse na osnovu SYN paketa, već samo odgovorite sa SYNACK paketom, kako onda server može razumjeti da se ACK paket koji je stigao kasnije odnosi na SYN paket koji nije sačuvan? Na kraju krajeva, napadač također može generirati lažne ACK-ove. Smisao SYN kolačića je da ga kodira seqnum parametri veze kao hash adresa, portova i promjenjive soli. Ako je ACK uspio stići prije promjene soli, možete ponovo izračunati hash i uporediti sa acknum. lažno acknum napadač ne može, jer sol uključuje tajnu, i neće imati vremena da je razvrstane zbog ograničenog kanala.

SYN kolačići su implementirani u Linux kernel već duže vrijeme i čak se mogu automatski omogućiti ako SYN stignu prebrzo i masovno.

Edukativni program o TCP rukovanju

TCP omogućava 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 zastavice i 32-bitne sekvence:

  • Kombinacija zastavica definira ulogu određenog paketa. Oznaka SYN znači da je ovo prvi paket pošiljaoca na vezi. Oznaka ACK znači da je pošiljalac primio sve podatke o vezi do jednog bajta. acknum. Paket može imati nekoliko zastavica i nazvan je po njihovoj kombinaciji, na primjer, SYNACK paket.

  • Broj sekvence (seqnum) specificira pomak u toku podataka za prvi bajt koji se šalje u ovom paketu. Na primjer, ako je u prvom paketu sa X bajtova podataka ovaj broj bio N, u sljedećem paketu sa novim podacima bit će N+X. Na početku veze, svaka strana bira ovaj broj nasumično.

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

Na početku veze, strane se moraju dogovoriti seqnum и acknum. Klijent šalje SYN paket sa svojim seqnum = X. Server odgovara SYNACK paketom, gdje upisuje svoj seqnum = Y i izlaže acknum = X + 1. Klijent odgovara na SYNACK sa ACK paketom, gdje seqnum = X + 1, acknum = Y + 1. Nakon toga počinje stvarni prijenos podataka.

Ako sagovornik ne potvrdi prijem paketa, TCP ga ponovo šalje po isteku vremena.

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

Prvo, ako se izgubi SYNACK ili ACK, morat ćete pričekati ponovno slanje - uspostavljanje veze se usporava. Drugo, u SYN paketu - i samo u njemu! - prenosi se niz opcija koje utiču na dalji rad veze. Ne pamteći dolazne SYN pakete, server tako ignoriše ove opcije, u sledećim paketima klijent ih više neće slati. TCP može raditi u ovom slučaju, ali barem u početnoj fazi, kvaliteta veze će se smanjiti.

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

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

Pseudokod algoritma zajedno sa raščlanjivanjem paketa:

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

Jedan (*) Označene su tačke na kojima treba da upravljate stanjem sistema - u prvoj fazi možete bez njih jednostavnim implementiranjem TCP rukovanja sa generisanjem SYN kolačića kao sekvence.

Na mjestu (**), dok nemamo tabelu, preskočićemo paket.

Implementacija TCP rukovanja

Parsiranje paketa i provjera koda

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

Sve funkcije koje se razlikuju u C-u zbog čitljivosti moraju biti umetnute na mjesto poziva, pošto eBPF verifikator u kernelu zabranjuje skokove unatrag, odnosno, u stvari, petlje i pozive funkcija.

#define INTERNAL static __attribute__((always_inline))

Makro LOG() onemogućuje štampanje u verziji izdanja.

Program je niz funkcija. Svaki prima paket u kojem je istaknuto zaglavlje odgovarajućeg nivoa, na primjer, process_ether() čeka da se popuni ether. Na osnovu rezultata analize polja, funkcija može proslijediti paket na viši nivo. Rezultat funkcije je XDP akcija. Za sada, SYN i ACK rukovaoci 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 provjere označene A i B. Ako komentarišete A, program će se izgraditi, ali će doći do greške pri učitavanju:

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!

Key string invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0): postoje putevi izvršenja kada je trinaesti bajt od početka bafera izvan paketa. Iz listinga je teško zaključiti o kojoj liniji je riječ, ali postoji broj instrukcije (12) i disassembler koji prikazuje redove 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. Uvek bi bilo tako.

Odgovorite SYN

Cilj u ovoj fazi je generirati ispravan SYNACK paket sa fiksnim seqnum, koji će u budućnosti biti zamijenjen SYN kolačićem. Sve promjene se dešavaju u process_tcp_syn() i okolnim područjima.

Provjera paketa

Začudo, evo najupečatljivijeg reda, odnosno komentara na njega:

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

Prilikom pisanja prve verzije koda korišteno je jezgro 5.1, za čiji verifikator je postojala razlika između data_end и (const void*)ctx->data_end. U vrijeme pisanja ovog teksta, kernel 5.3.1 nije imao ovaj problem. Možda je kompajler pristupao lokalnoj varijabli drugačije od polja. Moral - na velikom gniježđenju, pojednostavljivanje koda može pomoći.

Dalje rutinske provjere dužina za slavu verifikatora; O MAX_CSUM_BYTES ispod.

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 */
}

Package spread

Mi popunjavamo 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 biblioteka nije dostupna iz XDP programa, tako da memcpy() — makro koji 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 sumi zahtijevaju dodavanje svih 16-bitnih riječi u zaglavlja, a veličina zaglavlja je upisana u njima, odnosno nepoznata u vrijeme kompajliranja. Ovo je problem jer verifikator neće proći kroz normalnu petlju do granice varijable. Ali veličina zaglavlja je ograničena: do 64 bajta svako. Možete napraviti petlju sa fiksnim brojem iteracija, koje se mogu završiti ranije.

Napominjem da postoji RFC 1624 o tome kako djelimično ponovo izračunati kontrolnu sumu ako se promijene samo fiksne riječi paketa. Međutim, metoda nije univerzalna, a implementaciju bi bilo teže održavati.

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 provjereno pozivnim kodom, 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 preračunavanje kontrolnih suma i slanje paketa nazad:

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() pravi kontrolnu sumu od 32-bitne sume 16-bitnih riječi, prema RFC 791.

TCP provjera rukovanja

Filter ispravno uspostavlja vezu sa netcat, preskačući konačni ACK, na koji je Linux odgovorio sa RST paketom, budući da mrežni stog nije primio SYN - bio je konvertovan u SYNACK i poslat nazad - i sa stanovišta OS-a, stigao je paket koji nije bio povezan sa 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 monitorom tcpdump na xdp-remote jer npr. hping3 ne odgovara na netačne kontrolne sume.

Sa stanovišta XDP-a, sama provjera je trivijalna. Algoritam proračuna je primitivan i vjerovatno ranjiv na sofisticiranog napadača. Linux kernel, na primjer, koristi kriptografski SipHash, ali njegova implementacija za XDP je očigledno izvan okvira ovog članka.

Pojavio se za nove TODO-e vezane za vanjsku interakciju:

  • XDP program ne može pohraniti cookie_seed (tajni dio soli) u globalnoj varijabli, potrebno vam je skladište kernela čija će vrijednost biti periodično ažurirana iz pouzdanog generatora.

  • Ako se SYN kolačić u ACK paketu podudara, ne morate ispisivati ​​poruku, već zapamtite IP provjerenog klijenta kako biste dalje preskočili 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 lista verifikovanih IP adresa, neće biti zaštite od same SYN poplave, ali evo reakcije na ACK poplavu koju pokreće ova naredba:

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 i XDP posebno predstavljaju više kao napredni administratorski alat nego kao razvojna platforma. Zaista, XDP je alat za ometanje obrade paketa od strane kernela, a ne alternativa stogu kernela, kao što je DPDK i druge opcije zaobilaženja kernela. S druge strane, XDP vam omogućava implementaciju prilično složene logike, koju je, osim toga, lako ažurirati bez prekida u obradi prometa. Verifikator ne stvara velike probleme; lično, ne bih to odbio za dijelove koda korisničkog prostora.

U drugom dijelu, ako je tema interesantna, upotpunit ćemo tabelu provjerenih klijenata i prekinuti veze, implementirati brojače i napisati uslužni program korisničkog prostora za upravljanje filterom.

Reference:

izvor: www.habr.com

Dodajte komentar