Píšeme ochranu proti DDoS útokům na XDP. Jaderná část

Technologie eXpress Data Path (XDP) umožňuje provádět náhodné zpracování provozu na rozhraních Linuxu předtím, než pakety vstoupí do síťového zásobníku jádra. Aplikace XDP - ochrana proti DDoS útokům (CloudFlare), komplexní filtry, sběr statistik (Netflix). Programy XDP jsou spouštěny virtuálním strojem eBPF, takže mají omezení jak na svůj kód, tak na dostupné funkce jádra v závislosti na typu filtru.

Cílem článku je vyplnit nedostatky mnoha materiálů na XDP. Za prvé poskytují hotový kód, který okamžitě obchází funkce XDP: je připraven k ověření nebo je příliš jednoduchý na to, aby způsoboval problémy. Když se pak pokusíte napsat svůj kód od začátku, nemáte ponětí, co dělat s typickými chybami. Za druhé, způsoby, jak lokálně testovat XDP bez virtuálního počítače a hardwaru, nejsou pokryty, přestože mají svá vlastní úskalí. Text je určen programátorům obeznámeným se sítí a Linuxem, kteří se zajímají o XDP a eBPF.

V tomto díle podrobně pochopíme, jak se XDP filtr sestavuje a jak jej testujeme, poté napíšeme jednoduchou verzi známého mechanismu SYN cookies na úrovni zpracování paketů. Zatím nevytvoříme „bílou listinu“.
ověřené klienty, udržovat počítadla a spravovat filtr - dostatek logů.

Budeme psát v C - není to módní, ale je to praktické. Veškerý kód je dostupný na GitHubu přes odkaz na konci a je rozdělen do commitů podle fází popsaných v článku.

Odmítnutí odpovědnosti. V průběhu tohoto článku vyvinu mini-řešení pro odvrácení DDoS útoků, protože to je realistický úkol pro XDP a mou oblast odborných znalostí. Hlavním cílem je však porozumět technologii, toto není návod na vytvoření hotové ochrany. Kód výukového programu není optimalizován a vynechává některé nuance.

Stručný přehled XDP

Nastíním pouze klíčové body, abych neduplikoval dokumentaci a existující články.

Kód filtru je tedy načten do jádra. Příchozí pakety jsou předávány filtru. V důsledku toho se filtr musí rozhodnout: předat paket do jádra (XDP_PASS), zahodit paket (XDP_DROP) nebo poslat zpět (XDP_TX). Filtr může změnit balení, to platí zejména pro XDP_TX. Program můžete také ukončit (XDP_ABORTED) a resetujte balíček, ale je to analogické assert(0) - pro ladění.

Virtuální stroj eBPF (extended Berkley Packet Filter) je záměrně jednoduchý, aby jádro mohlo zkontrolovat, zda se kód nezacyklí a nepoškodí paměť ostatních. Kumulativní omezení a kontroly:

  • Smyčky (dozadu) jsou zakázány.
  • Existuje zásobník pro data, ale žádné funkce (všechny funkce C musí být vloženy).
  • Přístupy do paměti mimo zásobník a vyrovnávací paměť paketů jsou zakázány.
  • Velikost kódu je omezená, ale v praxi to není příliš podstatné.
  • Povolena jsou pouze volání speciálních funkcí jádra (pomocníci eBPF).

Návrh a instalace filtru vypadá takto:

  1. Zdrojový kód (např kernel.c) je zkompilován do objektu (kernel.o) pro architekturu virtuálního stroje eBPF. Od října 2019 je kompilace do eBPF podporována společností Clang a slíbena v GCC 10.1.
  2. Pokud tento objektový kód obsahuje volání struktur jádra (například tabulek a čítačů), jejich ID jsou nahrazena nulami, což znamená, že takový kód nelze provést. Před načtením do jádra musíte tyto nuly nahradit ID konkrétních objektů vytvořených pomocí volání jádra (propojit kód). Můžete to udělat pomocí externích utilit, nebo můžete napsat program, který propojí a načte konkrétní filtr.
  3. Jádro ověří načtený program. Kontroluje se absence cyklů a nepřekročení hranic paketů a zásobníků. Pokud ověřovatel nemůže prokázat, že kód je správný, program je zamítnut - musíte ho umět potěšit.
  4. Po úspěšném ověření jádro zkompiluje objektový kód architektury eBPF do strojového kódu pro architekturu systému (just-in-time).
  5. Program se připojí k rozhraní a začne zpracovávat pakety.

Protože XDP běží v jádře, ladění se provádí pomocí trasovacích protokolů a ve skutečnosti paketů, které program filtruje nebo generuje. eBPF však zajišťuje bezpečnost staženého kódu pro systém, takže můžete experimentovat s XDP přímo na vašem lokálním Linuxu.

Příprava prostředí

shromáždění

Clang nemůže přímo vytvářet objektový kód pro architekturu eBPF, takže proces se skládá ze dvou kroků:

  1. Zkompilujte kód C do bytecode LLVM (clang -emit-llvm).
  2. Převést bajtkód na objektový kód eBPF (llc -march=bpf -filetype=obj).

Při psaní filtru se bude hodit pár souborů s pomocnými funkcemi a makry z jaderných testů. Je důležité, aby odpovídaly verzi jádra (KVER). Stáhněte si je 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 pro 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 jádra, ARCH - architektura systému. Cesty a nástroje se mohou mezi distribucemi mírně lišit.

Příklad rozdílů pro 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 propojit adresář s pomocnými hlavičkami a několik adresářů s hlavičkami jádra. Symbol __KERNEL__ znamená, že hlavičky UAPI (userspace API) jsou definovány pro kód jádra, protože filtr se spouští v jádře.

Ochranu stohu lze vypnout (-fno-stack-protector), protože ověřovatel kódu eBPF stále kontroluje porušení zásobníku mimo hranice. Vyplatí se hned zapnout optimalizace, protože velikost bajtového kódu eBPF je omezená.

Začněme filtrem, který projde všechny pakety a nedělá nic:

#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 sbírá xdp_filter.o. Kde to teď zkusit?

Zkušební stojan

Stojan musí obsahovat dvě rozhraní: na kterém bude filtr a ze kterého se budou odesílat pakety. Musí se jednat o plnohodnotná linuxová zařízení s vlastními IP, aby bylo možné zkontrolovat, jak běžné aplikace fungují s naším filtrem.

Pro nás jsou vhodná zařízení typu veth (virtuální Ethernet): jedná se o dvojici virtuálních síťových rozhraní „propojených“ přímo k sobě. Můžete je vytvořit takto (v této sekci všechny příkazy ip jsou prováděny z root):

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

Zde xdp-remote и xdp-local — názvy zařízení. Na xdp-local (192.0.2.1/24) bude připojen filtr s xdp-remote (192.0.2.2/24) bude odeslán příchozí provoz. Je tu však problém: rozhraní jsou na stejném počítači a Linux nebude posílat provoz na jedno z nich přes druhé. Můžete to vyřešit složitými pravidly iptables, ale budou muset změnit balíčky, což je pro ladění nepohodlné. Je lepší používat síťové jmenné prostory (dále netns).

Síťový jmenný prostor obsahuje sadu rozhraní, směrovacích tabulek a pravidel NetFilter, která jsou izolována od podobných objektů v jiných sítích. Každý proces běží ve jmenném prostoru a má přístup pouze k objektům těchto sítí. Ve výchozím nastavení má systém jeden síťový jmenný prostor pro všechny objekty, takže můžete pracovat v Linuxu a neznáte netns.

Vytvořme nový jmenný prostor xdp-test a přesuňte to tam xdp-remote.

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

Poté se spustí proces xdp-test, neuvidí xdp-local (standardně zůstane v netns) a při odeslání paketu na 192.0.2.1 jej propustí xdp-remoteprotože je to jediné rozhraní na 192.0.2.0/24 přístupné tomuto procesu. To funguje i v opačném směru.

Při pohybu mezi sítěmi se rozhraní vypne a ztratí svou adresu. Chcete-li nakonfigurovat rozhraní v netns, musíte spustit ip ... v tomto jmenném prostoru pří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

Jak vidíte, neliší se to od nastavení xdp-local ve výchozím jmenném prostoru:

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

Pokud běžíš tcpdump -tnevi xdp-local, můžete vidět, že pakety odeslané z xdp-test, jsou dodávány do tohoto rozhraní:

ip netns exec xdp-test   ping 192.0.2.1

Je vhodné spustit shell dovnitř xdp-test. Úložiště má skript, který automatizuje práci se stojanem, například můžete stojan nakonfigurovat pomocí příkazu sudo ./stand up a smazat jej sudo ./stand down.

Sledování

Filtr je spojen se zařízením takto:

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

Klíč -force potřebné k propojení nového programu, pokud je již propojen jiný. „No news is good news“ není o tomto příkazu, závěr je každopádně objemný. naznačit verbose volitelné, ale s ním se objeví zpráva o práci ověřovače kódu s výpisem sestavy:

Verifier analysis:

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

Odpojení programu od rozhraní:

ip link set dev xdp-local xdp off

Ve skriptu jsou to příkazy sudo ./stand attach и sudo ./stand detach.

O to se můžete přesvědčit připojením filtru ping běží dál, ale funguje program? Přidáme logy. Funkce bpf_trace_printk() podobný printf(), ale podporuje pouze až tři argumenty jiné než vzor a omezený seznam specifikátorů. Makro bpf_printk() zjednoduší volání.

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

Výstup jde do kanálu trasování jádra, který je třeba povolit:

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

Zobrazit vlákno zpráv:

cat /sys/kernel/debug/tracing/trace_pipe

Oba tyto příkazy zavolají sudo ./stand log.

Ping by nyní měl spouštět zprávy takto:

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

Pokud se pozorně podíváte na výstup ověřovače, všimnete si podivných výpočtů:

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

Faktem je, že programy eBPF nemají datovou sekci, takže jediným způsobem, jak zakódovat formátovací řetězec, jsou okamžité argumenty příkazů VM:

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

Z tohoto důvodu výstup ladění značně nadýmá výsledný kód.

Odesílání paketů XDP

Změňme filtr: nechte jej posílat zpět všechny příchozí pakety. To je ze síťového hlediska nesprávné, protože by bylo nutné změnit adresy v hlavičkách, ale nyní je důležitá práce v principu.

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

Běh tcpdump na xdp-remote. Měl by zobrazovat identický odchozí a příchozí požadavek ICMP Echo Request a přestat zobrazovat odpověď ICMP Echo Reply. Ale to se neukazuje. Ukazuje se, že pro práci XDP_TX v programu na xdp-local nutnýdo párového rozhraní xdp-remote byl také přidělen program, i když byl prázdný, a byl vychován.

Jak jsem to věděl?

Sledujte cestu balíčku v jádře Mechanismus událostí perf umožňuje mimochodem používat stejný virtuální stroj, to znamená, že eBPF se používá pro demontáž s eBPF.

Ze zla musíte dělat dobro, protože není z čeho jiného.

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

Co je kód 6?

$ errno 6
ENXIO 6 No such device or address

Funkce veth_xdp_flush_bq() obdrží chybový kód od veth_xdp_xmit(), kde hledat podle ENXIO a najděte komentář.

Obnovme minimální filtr (XDP_PASS) v souboru xdp_dummy.c, přidejte jej do Makefile, svažte jej xdp-remote:

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

Nyní tcpdump ukazuje, co se očekává:

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

Pokud se místo toho zobrazují pouze ARP, musíte filtry odstranit (toto je sudo ./stand detach), pustit ping, poté nastavte filtry a zkuste to znovu. Problém je v tom filtru XDP_TX platné jak na ARP, tak na zásobníku
jmenné prostory xdp-test podařilo „zapomenout“ MAC adresu 192.0.2.1, nebude schopen tuto IP vyřešit.

Formulace problému

Přejděme k uvedenému úkolu: napište mechanismus SYN cookies na XDP.

SYN flood zůstává oblíbeným DDoS útokem, jehož podstata je následující. Když je navázáno spojení (TCP handshake), server přijme SYN, přidělí prostředky pro budoucí spojení, odpoví paketem SYNACK a čeká na ACK. Útočník jednoduše odešle tisíce paketů SYN za sekundu z podvržených adres z každého hostitele v mnohatisícovém botnetu. Server je nucen alokovat zdroje okamžitě po příchodu paketu, ale uvolňuje je po velkém časovém limitu, v důsledku čehož je vyčerpána paměť nebo limity, nejsou přijímána nová připojení a služba je nedostupná.

Pokud nepřidělujete prostředky na základě paketu SYN, ale odpovíte pouze paketem SYNACK, jak potom může server pochopit, že paket ACK, který dorazil později, odkazuje na paket SYN, který nebyl uložen? Falešná ACK totiž může generovat i útočník. Účelem souboru cookie SYN je zakódovat jej seqnum parametry připojení jako hash adres, portů a měnící se sůl. Pokud se ACK podařilo dorazit před změnou salt, můžete hash znovu vypočítat a porovnat s ním acknum. Kovárna acknum útočník nemůže, protože sůl obsahuje tajemství, a nebude mít čas je protřídit kvůli omezenému kanálu.

Soubor cookie SYN je v linuxovém jádře již dlouho implementován a může být dokonce automaticky povolen, pokud SYN přicházejí příliš rychle a hromadně.

Výukový program na TCP handshake

TCP poskytuje přenos dat jako proud bajtů, například požadavky HTTP jsou přenášeny přes TCP. Proud je přenášen po částech v paketech. Všechny pakety TCP mají logické příznaky a 32bitová sekvenční čísla:

  • Kombinace příznaků určuje roli konkrétního balíčku. Příznak SYN označuje, že se jedná o první paket odesílatele na připojení. Příznak ACK znamená, že odesílatel obdržel všechna data připojení až do bajtu acknum. Paket může mít několik příznaků a nazývá se jejich kombinací, například paket SYNACK.

  • Sekvenční číslo (seqnum) udává offset v datovém toku pro první bajt, který je přenášen v tomto paketu. Pokud například v prvním paketu s X bajty dat bylo toto číslo N, v dalším paketu s novými daty to bude N+X. Na začátku spojení si každá strana toto číslo vybere náhodně.

  • Číslo potvrzení (acknum) - stejný posun jako seqnum, ale neurčuje číslo přenášeného bytu, ale číslo prvního bytu od příjemce, které odesílatel neviděl.

Na začátku spojení se musí strany dohodnout seqnum и acknum. Klient s ním odešle paket SYN seqnum = X. Server odpoví paketem SYNACK, kam jej zaznamená seqnum = Y a vystavuje acknum = X + 1. Klient odpoví na SYNACK paketem ACK, kde seqnum = X + 1, acknum = Y + 1. Poté začne vlastní přenos dat.

Pokud partner nepotvrdí přijetí paketu, TCP jej po uplynutí časového limitu znovu odešle.

Proč se soubory cookie SYN nepoužívají vždy?

Za prvé, pokud dojde ke ztrátě SYNACK nebo ACK, budete muset počkat na opětovné odeslání - nastavení připojení se zpomalí. Za druhé v balíčku SYN – a pouze v něm! — je přenášena řada voleb, které ovlivňují další provoz spojení. Bez zapamatování příchozích SYN paketů tak server tyto volby ignoruje a klient je v dalších paketech nepošle. TCP může v tomto případě fungovat, ale alespoň v počáteční fázi se kvalita připojení sníží.

Z pohledu balíčků musí program XDP dělat následující:

  • reagovat na SYN pomocí SYNACK pomocí cookie;
  • reagovat na ACK pomocí RST (odpojit);
  • zahoďte zbývající pakety.

Pseudokód algoritmu spolu s analýzou balíčku:

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

Jeden (*) body, kde potřebujete řídit stav systému, jsou označeny - v první fázi se bez nich obejdete jednoduše implementací TCP handshake s vygenerováním SYN cookie jako seqnum.

Na místě (**), dokud nemáme tabulku, paket přeskočíme.

Implementace TCP handshake

Analýza balíčku a ověření kódu

Budeme potřebovat struktury hlaviček sítě: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) a TCP (uapi/linux/tcp.h). Ten se mi nepodařilo připojit kvůli chybám souvisejícím s atomic64_t, musel jsem zkopírovat potřebné definice do kódu.

Všechny funkce, které jsou zvýrazněny v C kvůli čitelnosti, musí být vložené v bodě volání, protože ověřovač eBPF v jádře zakazuje backtracking, tedy ve skutečnosti smyčky a volání funkcí.

#define INTERNAL static __attribute__((always_inline))

Makro LOG() zakáže tisk v sestavení vydání.

Program je nositelem funkcí. Každý obdrží paket, ve kterém je zvýrazněna odpovídající hlavička úrovně, např. process_ether() očekává, že bude naplněn ether. Na základě výsledků analýzy pole může funkce předat paket na vyšší úroveň. Výsledkem funkce je akce XDP. Prozatím obslužné rutiny SYN a ACK předávají všechny 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);
}

Upozorňuji na kontroly označené A a B. Pokud zakomentujete A, program se sestaví, ale při načítání dojde k chybě ověření:

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!

Řetězec klíčů invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0): Existují cesty provedení, když je třináctý bajt od začátku vyrovnávací paměti mimo paket. Z výpisu je obtížné pochopit, o kterém řádku mluvíme, ale existuje číslo instrukce (12) a disassembler zobrazující řádky zdrojového kódu:

llvm-objdump -S xdp_filter.o | less

V tomto případě ukazuje na čáru

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

což objasňuje, že problém je ether. Bylo by to tak vždycky.

Odpověď na SYN

Cílem v této fázi je vygenerovat správný SYNACK paket s pevnou seqnum, který bude v budoucnu nahrazen souborem cookie SYN. Všechny změny probíhají v process_tcp_syn() a okolní oblasti.

Ověření balíku

Kupodivu zde je nejpozoruhodnější řádek, nebo spíše komentář k němu:

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

Při psaní první verze kódu bylo použito jádro 5.1, u jehož ověřovače byl rozdíl mezi data_end и (const void*)ctx->data_end. V době psaní tohoto článku jádro 5.3.1 tento problém nemělo. Je možné, že kompilátor přistupoval k místní proměnné jinak než k poli. Morálka příběhu: když je vnoření velké, může pomoci zjednodušení kódu.

Další jsou rutinní kontroly délky pro slávu ověřovatele; Ó MAX_CSUM_BYTES níže.

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

Rozbalení balíčku

Vyplnit seqnum и acknum, nastavte ACK (SYN je již nastaven):

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

Prohoďte TCP porty, IP adresu a MAC adresy. Standardní knihovna není z programu XDP přístupná, takže memcpy() — makro, které skrývá Clangovu podstatu.

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

Přepočet kontrolních součtů

Kontrolní součty IPv4 a TCP vyžadují přidání všech 16bitových slov do hlaviček a zapisuje se do nich velikost hlaviček, tedy neznámá v době kompilace. To je problém, protože ověřovatel nepřeskočí normální smyčku na hraniční proměnnou. Ale velikost hlaviček je omezená: každá až 64 bajtů. Můžete vytvořit smyčku s pevným počtem iterací, která může skončit dříve.

Podotýkám, že existuje RFC 1624 o tom, jak částečně přepočítat kontrolní součet, pokud se změní pouze pevná slova balíčků. Metoda však není univerzální a implementace by byla náročnější na údržbu.

Funkce výpočtu kontrolního souč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;
}

Ačkoli size ověřena volacím kódem, je nutná druhá výstupní podmínka, aby ověřovatel mohl prokázat dokončení smyčky.

Pro 32bitová slova je implementována jednodušší verze:

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

Skutečné přepočítání kontrolních součtů a odeslání paketu zpět:

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;

Funkce carry() vytvoří kontrolní součet z 32bitového součtu 16bitových slov podle RFC 791.

Ověření TCP handshake

Filtr správně naváže spojení s netcat, chybí konečné ACK, na které Linux odpověděl paketem RST, protože síťový zásobník nepřijal SYN - byl převeden na SYNACK a odeslán zpět - a z pohledu OS dorazil paket, který nesouvisel s open spojení.

$ 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é kontrolovat s plnohodnotnými aplikacemi a pozorovat tcpdump na xdp-remote protože např. hping3 nereaguje na nesprávné kontrolní součty.

Samotné ověření je z pohledu XDP triviální. Algoritmus výpočtu je primitivní a pravděpodobně zranitelný pro sofistikovaného útočníka. Linuxové jádro například používá kryptografický SipHash, ale jeho implementace pro XDP zjevně přesahuje rámec tohoto článku.

Zavedeno pro nové TODO související s externí komunikací:

  • Program XDP nelze uložit cookie_seed (tajná část soli) v globální proměnné, potřebujete úložiště v jádře, jehož hodnota bude pravidelně aktualizována ze spolehlivého generátoru.

  • Pokud se SYN cookie shoduje v ACK paketu, nemusíte tisknout zprávu, ale zapamatovat si IP ověřeného klienta, abyste z něj mohli dále předávat pakety.

Legitimní ověření klienta:

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

Protokoly ukazují, že kontrola proběhla úspěšně (flags=0x2 - toto 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

I když neexistuje žádný seznam ověřených IP adres, nebude existovat žádná ochrana před samotnou SYN záplavou, ale zde je reakce na záplavu ACK spuštěnou následujícím příkazem:

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

Záznamy protokolu:

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

Závěr

Někdy jsou eBPF obecně a XDP zvláště prezentovány spíše jako pokročilý administrátorský nástroj než jako vývojová platforma. XDP je skutečně nástroj pro zasahování do zpracování paketů jádrem a není alternativou k zásobníku jádra, jako je DPDK a další možnosti přemostění jádra. Na druhou stranu XDP umožňuje implementovat poměrně složitou logiku, která se navíc snadno aktualizuje bez přerušení zpracování provozu. Verifikátor nedělá velké problémy, osobně bych to u částí kódu uživatelského prostoru neodmítl.

V druhé části, pokud je téma zajímavé, doplníme tabulku ověřených klientů a odpojení, implementujeme čítače a napíšeme utilitu uživatelského prostoru pro správu filtru.

Odkazy:

Zdroj: www.habr.com

Přidat komentář