Píšeme ochranu proti DDoS útokom na XDP. Jadrová časť

Technológia eXpress Data Path (XDP) umožňuje ľubovoľné spracovanie prevádzky na rozhraniach Linuxu predtým, ako pakety vstúpia do zásobníka siete jadra. Aplikácia XDP - ochrana pred DDoS útokmi (CloudFlare), komplexné filtre, zber štatistík (Netflix). Programy XDP sú vykonávané virtuálnym strojom eBPF, a preto majú obmedzenia týkajúce sa kódu aj dostupných funkcií jadra v závislosti od typu filtra.

Cieľom tohto článku je nahradiť nedostatky mnohých materiálov na XDP. Po prvé, poskytujú hotový kód, ktorý okamžite obchádza funkcie XDP: pripravený na overenie alebo príliš jednoduchý na to, aby spôsoboval problémy. Keď sa neskôr pokúsite napísať svoj vlastný kód od začiatku, nerozumiete tomu, čo robiť s typickými chybami. Po druhé, nezahŕňa spôsoby lokálneho testovania XDP bez VM a hardvéru, napriek tomu, že majú svoje vlastné úskalia. Text je určený programátorom znalým sietí a Linuxu, ktorí sa zaujímajú o XDP a eBPF.

V tejto časti podrobne pochopíme, ako sa XDP filter zostavuje a ako ho testujeme, potom napíšeme jednoduchú verziu známeho mechanizmu SYN cookies na úrovni spracovania paketov. Kým nevytvoríme „bielu listinu“
overených klientov, viesť počítadlá a spravovať filter - dostatok logov.

Budeme písať v C - to nie je módne, ale praktické. Všetok kód je dostupný na GitHub na odkaze na konci a je rozdelený do commitov podľa krokov opísaných v článku.

Disclaimer. V priebehu článku bude vyvinuté mini-riešenie na odpudzovanie DDoS útokov, pretože toto je reálna úloha pre XDP a moju oblasť. Hlavným cieľom je však pochopiť technológiu, toto nie je návod na vytvorenie hotovej ochrany. Kód tutoriálu nie je optimalizovaný a vynecháva niektoré nuansy.

Stručný prehľad XDP

Uvediem len kľúčové body, aby som neduplikoval dokumentáciu a existujúce články.

Kód filtra sa teda načíta do jadra. Filtrom prechádzajú prichádzajúce pakety. V dôsledku toho sa filter musí rozhodnúť: odovzdať paket do jadra (XDP_PASS), pustiť paket (XDP_DROP) alebo ho pošlite späť (XDP_TX). Filter môže zmeniť balenie, to platí najmä pre XDP_TX. Môžete tiež zlyhať program (XDP_ABORTED) a zhoďte balík, ale toto je analogické assert(0) - na ladenie.

Virtuálny stroj eBPF (extended Berkley Packet Filter) je zámerne jednoduchý, aby jadro mohlo skontrolovať, či sa kód nezacyklí a nepoškodí pamäť iných ľudí. Kumulatívne obmedzenia a kontroly:

  • Slučky (skoky späť) sú zakázané.
  • Existuje zásobník pre dáta, ale žiadne funkcie (všetky funkcie C musia byť vložené).
  • Prístupy do pamäte mimo zásobníka a vyrovnávacej pamäte paketov sú zakázané.
  • Veľkosť kódu je obmedzená, ale v praxi to nie je veľmi podstatné.
  • Povolené sú iba špeciálne funkcie jadra (pomocníci eBPF).

Vývoj a inštalácia filtra vyzerá takto:

  1. zdrojový kód (napr. kernel.c) skompiluje do objektu (kernel.o) pre architektúru virtuálneho stroja eBPF. Od októbra 2019 je kompilácia do eBPF podporovaná spoločnosťou Clang a sľúbená v GCC 10.1.
  2. Ak sú v tomto objektovom kóde volania štruktúr jadra (napríklad tabuliek a počítadiel), namiesto ich ID sú nuly, to znamená, že takýto kód nie je možné vykonať. Pred načítaním do jadra musia byť tieto nuly nahradené ID špecifických objektov vytvorených prostredníctvom volaní jadra (prepojiť kód). Môžete to urobiť pomocou externých nástrojov alebo môžete napísať program, ktorý prepojí a načíta konkrétny filter.
  3. Jadro overuje načítavanie programu. Kontroluje absenciu cyklov a neopustenie hraníc balíka a zásobníka. Ak overovateľ nedokáže, že kód je správny, program je zamietnutý – človek ho musí vedieť potešiť.
  4. Po úspešnom overení jadro skompiluje objektový kód architektúry eBPF do strojového kódu systémovej architektúry (just-in-time).
  5. Program je pripojený k rozhraniu a začne spracovávať pakety.

Keďže XDP beží v jadre, ladenie sa vykonáva pomocou protokolov sledovania a v skutočnosti pomocou paketov, ktoré program filtruje alebo generuje. eBPF však uchováva stiahnutý kód bezpečný pre systém, takže môžete experimentovať s XDP priamo na vašom lokálnom Linuxe.

Príprava prostredia

zhromaždenia

Clang nemôže priamo vydať objektový kód pre architektúru eBPF, takže proces pozostáva z dvoch krokov:

  1. Kompilujte kód C do bajtového kódu LLVM (clang -emit-llvm).
  2. Previesť bajtkód na objektový kód eBPF (llc -march=bpf -filetype=obj).

Pri písaní filtra sa bude hodiť pár súborov s pomocnými funkciami a makrami z testov jadra. Je dôležité, aby sa zhodovali s verziou jadra (KVER). Stiahnite si ich do 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 pre 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 obsahuje cestu k hlavičkám jadra, ARCH - architektúra systému. Cesty a nástroje sa môžu medzi jednotlivými distribúciami mierne líšiť.

Príklad rozdielu pre 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 obsahuje adresár s pomocnými hlavičkami a niekoľko adresárov s hlavičkami jadra. Symbol __KERNEL__ znamená, že hlavičky UAPI (userspace API) sú definované pre kód jadra, keďže filter sa vykonáva v jadre.

Ochranu stohu je možné vypnúť (-fno-stack-protector), pretože overovač kódu eBPF aj tak kontroluje, či nie sú mimo hraníc zásobníka. Mali by ste okamžite povoliť optimalizácie, pretože veľkosť bajtkódu eBPF je obmedzená.

Začnime s filtrom, ktorý prejde všetky pakety a nerobí nič:

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

Tím make zbiera xdp_filter.o. Kde si to teraz môžete vyskúšať?

skúšobná stolica

Stojan by mal obsahovať dve rozhrania: na ktorom bude filter a z ktorého sa budú odosielať pakety. Musia to byť plnohodnotné linuxové zariadenia s vlastnými IP, aby bolo možné skontrolovať, ako fungujú bežné aplikácie s naším filtrom.

Zariadenia ako veth (virtuálny Ethernet) sú pre nás vhodné: ide o dvojicu virtuálnych sieťových rozhraní „prepojených“ priamo medzi sebou. Môžete ich vytvoriť takto (v tejto časti sú všetky príkazy ip vykonávané od root):

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

Tu xdp-remote и xdp-local — názvy zariadení. Zapnuté xdp-local (192.0.2.1/24) bude pripojený filter s xdp-remote (192.0.2.2/24) bude odoslaná prichádzajúca komunikácia. Je tu však problém: rozhrania sú na rovnakom počítači a Linux nebude posielať prevádzku na jedno z nich cez druhé. Môžete to vyriešiť zložitými pravidlami iptables, ale budú musieť zmeniť balíčky, čo je pri ladení nepohodlné. Je lepšie používať sieťové menné priestory (sieťové menné priestory, ďalej netns).

Priestor názvov siete obsahuje množinu rozhraní, smerovacích tabuliek a pravidiel NetFilter, ktoré sú izolované od podobných objektov v iných sieťach. Každý proces beží v nejakom mennom priestore a sú mu dostupné iba objekty týchto sietí. V predvolenom nastavení má systém jeden sieťový menný priestor pre všetky objekty, takže môžete pracovať na Linuxe a neviete o netns.

Vytvorme nový menný priestor xdp-test a presťahovať sa tam xdp-remote.

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

Potom sa spustí proces xdp-test, neuvidí xdp-local (štandardne zostane v netns) a pri odoslaní paketu na 192.0.2.1 ho prejde xdp-remote, pretože je to jediné rozhranie na 192.0.2.0/24 dostupné pre tento proces. Toto funguje aj opačne.

Pri pohybe medzi sieťami sa rozhranie vypne a stratí adresu. Ak chcete nastaviť rozhranie v netns, musíte spustiť ip ... v tomto mennom priestore príkazu 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

Ako vidíte, toto sa nelíši od nastavenia xdp-local v predvolenom priestore názvov:

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

Ak sa spustí tcpdump -tnevi xdp-local, môžete vidieť, že pakety odoslané z xdp-test, sa doručujú do tohto rozhrania:

ip netns exec xdp-test   ping 192.0.2.1

Je vhodné spustiť shell xdp-test. Úložisko má skript, ktorý automatizuje prácu so stojanom, stojan si napríklad nastavíte príkazom sudo ./stand up a odstráňte ho sudo ./stand down.

sledovanie

Filter je pripevnený k zariadeniu takto:

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

kľúč -force potrebné na prepojenie nového programu, ak je už pripojený iný. "No news is good news" nie je o tomto príkaze, výstup je aj tak objemný. naznačiť verbose voliteľné, ale spolu s ním sa zobrazí správa o práci overovača kódu s výpisom assembleru:

Verifier analysis:

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

Odpojte program od rozhrania:

ip link set dev xdp-local xdp off

V skripte sú to príkazy sudo ./stand attach и sudo ./stand detach.

Väzbou filtra sa o tom môžete uistiť ping naďalej funguje, ale funguje program? Pridajme logá. Funkcia bpf_trace_printk() podobný printf(), ale podporuje iba tri argumenty iné ako vzor a obmedzený zoznam špecifikátorov. Makro bpf_printk() zjednoduší hovor.

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

Výstup ide do kanála sledovania jadra, ktorý je potrebné povoliť:

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

Zobraziť tok správ:

cat /sys/kernel/debug/tracing/trace_pipe

Oba tieto tímy si zavolajú sudo ./stand log.

Ping by mal teraz produkovať správy ako je tento:

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

Ak sa pozriete pozorne na výstup overovača, môžete si všimnúť zvláštne výpočty:

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
<...>

Faktom je, že programy eBPF nemajú dátovú sekciu, takže jediným spôsobom, ako zakódovať formátovací reťazec, sú okamžité argumenty príkazov VM:

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

Z tohto dôvodu výstup ladenia značne nafukuje výsledný kód.

Odosielanie paketov XDP

Zmeňme filter: nech posiela všetky prichádzajúce pakety späť. Z hľadiska siete je to nesprávne, pretože by bolo potrebné zmeniť adresy v hlavičkách, ale teraz je dôležitá práca v princípe.

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

Spustiť tcpdump na xdp-remote. Mala by zobrazovať rovnakú odchádzajúce a prichádzajúce ICMP Echo Request a prestať zobrazovať ICMP Echo Reply. Ale neukazuje sa. Ukázalo sa, že funguje XDP_TX v programe pre xdp-local muštspárovať rozhranie xdp-remote bol tiež priradený program, aj keď bol prázdny, a bol spustený.

ako som to vedel?

Sledovanie cesty balíka v jadre mechanizmus udalostí perf umožňuje, mimochodom, používať rovnaký virtuálny stroj, to znamená, že eBPF sa používa na demontáž s eBPF.

Zo zla musíte robiť dobro, pretože z toho nie je nič iné.

$ 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])
                                     <...>

Čo je kód 6?

$ errno 6
ENXIO 6 No such device or address

Funkcia veth_xdp_flush_bq() dostane kód chyby z veth_xdp_xmit(), kde hľadať podľa ENXIO a nájsť komentár.

Obnovte minimálny filter (XDP_PASS) v súbore xdp_dummy.c, pridajte ho do súboru Makefile, naviažte na xdp-remote:

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

teraz tcpdump ukazuje, čo sa očakáva:

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

Ak sa namiesto toho zobrazuje iba ARP, musíte odstrániť filtre (to znamená sudo ./stand detach), nech ping, potom nainštalujte filtre a skúste to znova. Problém je v tom, že filter XDP_TX ovplyvňuje aj ARP a ak zásobník
menné priestory xdp-test podarilo "zabudnúť" MAC adresu 192.0.2.1, nebude vedieť vyriešiť túto IP.

Vyhlásenie o probléme

Prejdime k uvedenej úlohe: napísať mechanizmus SYN cookie na XDP.

Až doteraz zostáva SYN flood populárnym DDoS útokom, ktorého podstata je nasledovná. Keď je nadviazané spojenie (TCP handshake), server prijme SYN, pridelí zdroje pre budúce spojenie, odpovie paketom SYNACK a čaká na ACK. Útočník jednoducho posiela SYN pakety z falošných adries v množstve tisícok za sekundu z každého hostiteľa v multi-tisícovom botnete. Server je nútený alokovať zdroje ihneď po príchode paketu, ale uvoľní ho po dlhom časovom limite, následkom čoho je vyčerpaná pamäť alebo limity, nové spojenia nie sú akceptované, služba je nedostupná.

Ak nevyčleníte prostriedky na paket SYN, ale odpoviete iba paketom SYNACK, ako môže server pochopiť, že paket ACK, ktorý prišiel neskôr, patrí do paketu SYN, ktorý nebol uložený? Falošné ACK totiž dokáže generovať aj útočník. Podstatou súboru cookie SYN je zakódovať seqnum parametre pripojenia ako hash adries, portov a meniacich sa solí. Ak sa ACK podarilo doraziť pred zmenou soli, môžete znova vypočítať hash a porovnať s acknum. falošný acknum Útočník nemôže, pretože soľ obsahuje tajomstvo, a nebude mať čas ho pretriediť kvôli obmedzenému kanálu.

SYN cookies sú v linuxovom jadre implementované už dlhú dobu a môžu byť dokonca automaticky povolené, ak SYN prichádzajú príliš rýchlo a hromadne.

Vzdelávací program na TCP handshake

TCP poskytuje prenos údajov ako prúd bajtov, napríklad požiadavky HTTP sa prenášajú cez TCP. Prúd sa prenáša kus po kuse v paketoch. Všetky pakety TCP majú logické príznaky a 32-bitové poradové čísla:

  • Kombinácia príznakov definuje úlohu konkrétneho balíka. Príznak SYN znamená, že ide o prvý paket odosielateľa pri pripojení. Príznak ACK znamená, že odosielateľ prijal všetky údaje o spojení až do bajtu. acknum. Paket môže mať niekoľko príznakov a je pomenovaný podľa ich kombinácie, napríklad paket SYNACK.

  • Poradové číslo (seqnum) určuje posun v dátovom toku pre prvý bajt odoslaný v tomto pakete. Napríklad, ak v prvom pakete s X bajtmi dát bolo toto číslo N, v ďalšom pakete s novými dátami to bude N+X. Na začiatku spojenia si každá strana náhodne vyberie toto číslo.

  • Potvrdzovacie číslo (acknum) - rovnaký posun ako seqnum, ale neurčuje číslo prenášaného bajtu, ale číslo prvého bajtu od príjemcu, ktorý odosielateľ nevidel.

Na začiatku spojenia sa musia strany dohodnúť seqnum и acknum. Klient pošle SYN paket s jeho seqnum = X. Server odpovie paketom SYNACK, kam zapíše svoj vlastný seqnum = Y a vystavuje acknum = X + 1. Klient odpovie na SYNACK paketom ACK, kde seqnum = X + 1, acknum = Y + 1. Potom sa začne skutočný prenos údajov.

Ak účastník nepotvrdí prijatie paketu, TCP ho znova odošle po uplynutí časového limitu.

Prečo sa cookies SYN nepoužívajú vždy?

Po prvé, ak sa stratí SYNACK alebo ACK, budete musieť počkať na opätovné odoslanie - nadväzovanie spojenia sa spomalí. Po druhé, v pakete SYN - a iba v ňom! - prenáša sa množstvo možností, ktoré ovplyvňujú ďalšiu prevádzku spojenia. Server si nepamätá prichádzajúce SYN pakety, takže tieto voľby ignoruje, v nasledujúcich paketoch ich už klient nebude posielať. TCP môže v tomto prípade fungovať, ale aspoň v počiatočnej fáze sa kvalita pripojenia zníži.

Pokiaľ ide o balíky, program XDP by mal robiť nasledovné:

  • odpovedať na SYN pomocou SYNACK s cookie;
  • odpovedzte ACK s RST (prerušte spojenie);
  • zahodiť ďalšie pakety.

Pseudokód algoritmu spolu s analýzou paketov:

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

Jeden (*) body, v ktorých potrebujete spravovať stav systému, sú označené - v prvej fáze sa bez nich zaobídete jednoduchou implementáciou TCP handshake s vygenerovaním SYN cookie ako seqnum.

Na mieste (**), kým nemáme tabuľku, balíček preskočíme.

Implementácia TCP handshake

Analýza balíka a overenie kódu

Potrebujeme štruktúry hlavičiek siete: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) a TCP (uapi/linux/tcp.h). Posledný, ktorý som nemohol pripojiť kvôli chybám, ktoré súvisia atomic64_t, musel som skopírovať potrebné definície do kódu.

Všetky funkcie, ktoré sú v C rozlíšené kvôli čitateľnosti, musia byť vložené na stránke volania, pretože verifikátor eBPF v jadre zakazuje skoky späť, teda v skutočnosti slučky a volania funkcií.

#define INTERNAL static __attribute__((always_inline))

makro LOG() zakáže tlač v zostave vydania.

Program je reťazec funkcií. Každý dostane paket, v ktorom je zvýraznená hlavička zodpovedajúcej úrovne, napr. process_ether() čaká na naplnenie ether. Na základe výsledkov analýzy poľa môže funkcia preniesť paket na vyššiu úroveň. Výsledkom funkcie je akcia XDP. Zatiaľ čo manipulátory SYN a ACK nechajú prejsť všetky pakety.

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);
}

Venujem pozornosť kontrolám označeným A a B. Ak zakomentujete A, program sa zostaví, ale pri načítaní sa vyskytne chyba overenia:

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!

Kľúčový reťazec invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0): existujú cesty vykonávania, keď je trinásty bajt od začiatku vyrovnávacej pamäte mimo paketu. Z výpisu je ťažké povedať, o ktorom riadku hovoríme, ale existuje číslo inštrukcie (12) a disassembler, ktorý zobrazuje riadky zdrojového kódu:

llvm-objdump -S xdp_filter.o | less

V tomto prípade ukazuje na čiaru

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

čím je jasné, že problém je ether. Vždy by to tak bolo.

Odpovedzte SYN

Cieľom v tejto fáze je vygenerovať správny SYNACK paket s fixným seqnum, ktorý bude v budúcnosti nahradený súborom cookie SYN. Všetky zmeny prebiehajú v process_tcp_syn() a okolie.

Kontrola balíka

Napodiv, tu je najpozoruhodnejší riadok, alebo skôr komentár k nemu:

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

Pri písaní prvej verzie kódu bolo použité jadro 5.1, pre ktorého overovač bol rozdiel medzi data_end и (const void*)ctx->data_end. V čase písania tohto článku jadro 5.3.1 tento problém nemalo. Možno kompilátor pristupoval k lokálnej premennej inak ako k poľu. Morálka - pri veľkom hniezdení môže pomôcť zjednodušenie kódu.

Ďalšie rutinné kontroly dĺžok pre slávu overovateľa; O MAX_CSUM_BYTES ниже.

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

Rozšírenie balíka

vyplniť seqnum и acknum, nastavte ACK (SYN už je nastavený):

const u32 cookie = 42;
tcp->ack_seq = bpf_htonl(bpf_ntohl(tcp->seq) + 1);
tcp->seq = bpf_htonl(cookie);
tcp->ack = 1;

Vymeňte TCP porty, IP a MAC adresy. Štandardná knižnica nie je dostupná z programu XDP, takže memcpy() — makro, ktoré skrýva Clangovu vnútornú stránku.

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);

Prepočet kontrolného súčtu

Kontrolné súčty IPv4 a TCP vyžadujú pridanie všetkých 16-bitových slov v hlavičkách a veľkosť hlavičiek je v nich zapísaná, to znamená, že v čase kompilácie nie je známa. Toto je problém, pretože overovateľ nepreskočí normálnu slučku až do hraničnej premennej. Veľkosť hlavičiek je však obmedzená: každá do 64 bajtov. Môžete vytvoriť slučku s pevným počtom iterácií, ktorá môže skončiť skôr.

Podotýkam, že existuje RFC 1624 o tom, ako čiastočne prepočítať kontrolný súčet, ak sa zmenia iba pevné slová paketov. Metóda však nie je univerzálna a jej implementácia by bola náročnejšia na údržbu.

Funkcia výpočtu kontrolného súčtu:

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

Hoci size skontrolovaný volacím kódom, je potrebná druhá výstupná podmienka, aby overovateľ mohol dokázať koniec cyklu.

Pre 32-bitové slová je implementovaná jednoduchšia verzia:

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

Vlastné prepočítanie kontrolných súčtov a odoslanie paketu späť:

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;

Funkcia carry() vytvára kontrolný súčet z 32-bitového súčtu 16-bitových slov podľa RFC 791.

Kontrola nadviazania spojenia TCP

Filter správne vytvorí spojenie s netcat, preskočenie záverečného ACK, na ktoré Linux odpovedal paketom RST, keďže sieťový zásobník nedostal SYN - bol konvertovaný na SYNACK a odoslaný späť - a z pohľadu OS prišiel paket, ktorý nebol súvisiace s otvorenými spojeniami.

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

Je dôležité kontrolovať s plnohodnotnými aplikáciami a pozorovať tcpdump na xdp-remote lebo napr. hping3 nereaguje na nesprávne kontrolné súčty.

Samotná kontrola je z pohľadu XDP triviálna. Algoritmus výpočtu je primitívny a pravdepodobne zraniteľný voči sofistikovanému útočníkovi. Linuxové jadro napríklad používa kryptografický SipHash, ale jeho implementácia pre XDP zjavne presahuje rámec tohto článku.

Objavilo sa pre nové TODO súvisiace s externou interakciou:

  • Program XDP nemôže ukladať cookie_seed (tajná časť soli) v globálnej premennej potrebujete úložisko jadra, ktorého hodnota sa bude pravidelne aktualizovať zo spoľahlivého generátora.

  • Ak sa SYN cookie v ACK pakete zhoduje, nemusíte tlačiť správu, ale zapamätajte si IP overeného klienta, aby ste z neho mohli ďalej preskakovať pakety.

Overenie legitímnym klientom:

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

Denníky zaznamenali prechod kontroly (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

Pokiaľ neexistuje zoznam overených IP, nebude existovať žiadna ochrana pred samotnou SYN flood, ale tu je reakcia na ACK flood spustenú týmto príkazom:

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

Záznamy denníka:

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

Záver

Niekedy sú eBPF vo všeobecnosti a XDP konkrétne prezentované skôr ako pokročilý administrátorský nástroj než vývojová platforma. XDP je skutočne nástroj na zasahovanie do spracovania paketov jadra a nie alternatíva k zásobníku jadra, ako napríklad DPDK a iné možnosti obchádzania jadra. Na druhej strane vám XDP umožňuje implementovať pomerne zložitú logiku, ktorá sa navyše ľahko aktualizuje bez prestávky v spracovaní prevádzky. Verifikátor nerobí veľké problémy, osobne by som takéto časti kódu užívateľského priestoru neodmietol.

V druhej časti, ak je téma zaujímavá, doplníme tabuľku overených klientov a prerušíme spojenia, implementujeme počítadlá a napíšeme utilitu používateľského priestoru na správu filtra.

odkazy:

Zdroj: hab.com

Pridať komentár