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

Technologie eXpress Data Path (XDP) umožňuje provádět libovolné zpracování provozu na rozhraních Linuxu dříve, než pakety vniknou do síťového zásobníku jádra. Použití XDP — ochrana před DDoS útoky (CloudFlare), složité filtry, shromažďování statistik (Netflix). Programy XDP se vykonávají na virtuálním stroji eBPF, takže mají omezení jak na svůj kód, tak na dostupné funkce jádra v závislosti na typu filtru.

Článek má za cíl doplnit nedostatky četných materiálů týkajících se XDP. Za prvé, dávají hotový kód, který ihned obchází zvláštnosti XDP: je připraven pro verifikaci nebo je příliš jednoduchý na to, aby vyvolal problémy. Při pokusu napsat vlastní kód od nuly není pochopení, co dělat s charakteristickými chybami. Za druhé, nezabývá se způsoby místního testování XDP bez VM a "hardwaru", přičemž mají svá "podvodná" místa. Text je určen pro programátory obeznámené se sítěmi a Linuxem, kteří se zajímají o XDP a eBPF.

V této části se detailně podíváme, jak se sestavuje XDP filtr a jak ho testovat, poté napíšeme jednoduchou variantu známého mechanismu SYN cookies na úrovni zpracování paketů. Zatím nebudeme vytvářet "bílý seznam"
ověřených klientů, vést počítadla a spravovat filtr — na to stačí logy.

Budeme psát v C — není to módní, ale praktické. Celý kód je dostupný na GitHubu prostřednictvím odkazu na konci a je rozdělen na commity podle etap popsaných v článku.

Zřeknutí se odpovědnosti. V průběhu článku bude vyvíjeno mini-řešení pro ochranu před DDoS útoky, protože to je realistický úkol pro XDP a má obor. Hlavním cílem je však pochopit technologii, nejde o návod na vytvoření hotové ochrany. Výukový kód není optimalizován a vynechává některé nuance.

Krátký přehled XDP

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

Takže do jádra je načten kód filtru. Filtru jsou předávány příchozí pakety. Výsledkem by měl být filtr, který se rozhodne: propustit paket do jádra (XDP_PASS), zrušit paket (XDP_DROP) nebo jej poslat zpět (XDP_TX). Filtr může paket upravit, což je zvláště důležité pro XDP_TX. Také je možné nouzově přerušit program (XDP_ABORTED) a zrušit paket, ale to je ekvivalent assert(0) — pro ladění.

Virtuální stroj eBPF (extended Berkley Packet Filter) je navržen tak, aby bylo jádro schopno zkontrolovat, že kód se nezacyklí a nepoškodí cizí paměť. Soubory omezení a kontroly:

  • Cykly (návratové skoky) jsou zakázány.
  • Existuje zásobník pro data, ale žádné funkce (všechny funkce C musí být in-line).
  • Přístupy k paměti mimo zásobník a buffer paketu jsou zakázány.
  • Velikost kódu je omezená, ale v praxi to není příliš významné.
  • Povolené jsou pouze volání speciálních funkcí jádra (eBPF helpers).

Vývoj a zavedení filtru vypadá takto:

  1. Zdrojový kód (například, kernel.c) se kompiluje do objektového (kernel.o) kódu pro architekturu virtuálního stroje eBPF. K říjnu 2019 je podpora kompilace do eBPF zajištěna Clangem a slíbena v GCC 10.1.
  2. Pokud tento objektový kód obsahuje přístupy k strukturám jádra (například k tabulkám a počítadlům), místo jejich ID jsou tam nuly, což znamená, že takový kód nelze provést. Před načtením do jádra je třeba tyto nuly nahradit ID konkrétních objektů, vytvořených prostřednictvím volání jádra (přilinkovat kód). Může to udělat externí nástroje, nebo napsat program, který bude konkrétní filtr přilinkovávat a načítat.
  3. Jádro verifikuje načítaný program. Kontroluje, zda nejsou cykly a zda nepřekračují hranici paketu a zásobníku. Pokud verifikátor nemůže prokázat, že kód je správný, program je zamítnut – musíte umět ho potěšit.
  4. Po úspěšné verifikaci jádro kompiluje objektový kód architektury eBPF do strojového kódu systémové architektury (just-in-time).
  5. Program je připojen k rozhraní a začíná zpracovávat pakety.

Jelikož XDP pracuje v jádře, ladění se provádí pomocí sledovacích logů a také prostřednictvím paketů, které program filtruje nebo generuje. Nicméně, eBPF zajišťuje bezpečnost načteného kódu pro systém, takže experimentování s XDP je možné přímo na lokálním Linuxu.

Příprava prostředí

Sestavení

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

  1. Zkompilovat kód C do bytecode LLVM (clang -emit-llvm).
  2. Převedení bytecode do objektového kódu eBPF (llc -march=bpf -filetype=obj).

Při psaní filtru bude užitečné mít pár souborů s pomocnými funkcemi a makry z testů jádra. Je důležité, aby odpovídaly verzi jádra (KVER). Stáhnout je můžeme z 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 (jadro 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 — architekturu systému. Cesty a nástroje se mohou mírně lišit mezi distribucemi.

Příklad rozdílů pro Debian 10 (jádro 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 zahrnují adresář s pomocnými hlavičkami a několik adresářů s hlavičkami jádra. Symbol __KERNEL__ označuje, že hlavičky UAPI (uživatelské rozhraní API) jsou definovány pro kód jádra, protože filtr se provádí v jádře.

Ochranu zásobníku lze vypnout (-fno-stack-protector), protože verifier kódu eBPF i tak kontroluje přetečení zásobníku. Okamžitě je dobré povolit optimalizace, protože velikost bytecode eBPF je omezena.

Začneme s filtrem, který propouští všechny pakety a nic nedělá:

#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 shromažďuje xdp_filter.o. Kde to nyní vyzkoušet?

Testovací stanice

Testovací stanice by měla mít dvě rozhraní: jedno s filtrem a druhé, ze kterého budou dotovány pakety. Musí to být plnohodnotná zařízení Linux s vlastními IP, aby se ověřilo, jak normální aplikace pracují s naším filtrem.

Zařízení typu veth (virtuální Ethernet) jsou nám vhodná: jde o pár virtuálních síťových rozhraní, "spojených" mezi sebou přímo. Vytvořit je lze takto (v této části jsou všechny příkazy ip prováděny od root):

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

Zde xdp-remote a xdp-local — názvy zařízení. Na xdp-local (192.0.2.1/24) bude připojen filtr, z xdp-remote (192.0.2.2/24) bude odesílán příchozí provoz. Existuje však problém: rozhraní jsou na jednom stroji a Linux neodešle provoz na jedno z nich přes druhé. Toto lze vyřešit chytrými pravidly iptables, ale těch se pak musí měnit pakety, což je nevhodné při ladění. Lépe je použít síťová jmenné prostory (network namespaces, dále netns).

Síťový prostor jmen obsahuje sadu rozhraní, tabulek směrování a pravidel NetFilter, izolovaných od podobných objektů v jiných netns. Každý proces běží v nějakém prostoru jmen a má přístup pouze k objektům tohoto netns. Ve výchozím nastavení je v systému jediné síťové jméno pro všechny objekty, což znamená, že můžete pracovat v Linuxu a nemusíte vědět o netns.

Vytvoříme nový prostor jmen xdp-test a přesuneme tam xdp-remote.

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

Pak proces, který běží v xdp-test, nebude "vidět" xdp-local (zůstane ve výchozím netns) a při odesílání paketu na 192.0.2.1 ho předá přes xdp-remote, protože to je jediné rozhraní v 192.0.2.0/24 dostupné tomuto procesu. Platí to i pro opačný směr.

Při přesouvaní mezi netns rozhraní se opouští a ztrácí adresu. Aby bylo možné nakonfigurovat rozhraní v netns, je třeba spustit ip ... v tomto prostoru jmen pomocí 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 je vidět, to se neliší od konfigurace xdp-local ve výchozím prostoru jmen:

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

Pokud spustíte tcpdump -tnevi xdp-local, je možné vidět, že pakety odeslané z xdp-test, jsou doručovány na toto rozhraní:

ip netns exec xdp-test   ping 192.0.2.1

Je pohodlné spustit shell v xdp-test. V repozitáři je skript, který automatizuje práci s testovacím prostředím, například můžete nastavit prostředí pomocí příkazu sudo ./stand up a odstranit ho sudo ./stand down.

Sledování trasy

Filtr se připojuje k zařízení takto:

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

Klíč -force je potřeba pro připojení nového programu, pokud je již jiný připojen. "No news is good news" neplatí pro tento příkaz, výstup je v každém případě objemný. Uvádět verbose není povinné, ale s ním se objeví zpráva o činnosti verifikátoru kódu s výpisem assembleru:

Verifier analysis:

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

Odpojit program od rozhraní:

ip link set dev xdp-local xdp off

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

Po připojení filtru si můžete být jisti, že ping pokračuje v práci, ale funguje program? Přidáme logy. Funkce bpf_trace_printk() je podobný printf(), ale podporuje maximálně tři argumenty, kromě formátu, a omezený seznam specifikátorů. Makro bpf_printk() usnadňuje 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 sledování jádra, který je potřeba aktivovat:

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

Zobrazení proudu zpráv:

cat /sys/kernel/debug/tracing/trace_pipe

Obě tyto příkazy volají sudo ./stand log.

Ping nyní by měl vyvolávat takové zprávy:

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

Pokud se podíváte na výstup verifikátoru, můžete si všimnout 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

Problém spočívá v tom, že programy na eBPF nemají sekci dat, a proto je jediný způsob, jak zakódovat formátovací řetězec, pomocí bezprostředních argumentů VM:

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

Z tohoto důvodu ladicí výstup značně zvětšuje finální kód.

Odesílání paketů XDP

Změníme filtr: ať pošle všechny příchozí pakety zpět. Z hlediska sítě to není správné, protože by se měly měnit adresy v hlavičkách, ale nyní jde o funkčnost.

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

Spouštíme tcpdump na xdp-remote. Měl by zobrazit identické odchozí a příchozí ICMP Echo Request a přestat zobrazovat ICMP Echo Reply. Ale nezobrazuje to. Ukazuje se, že pro funkčnost XDP_TX v programu na xdp-local je nutné, aby párovému rozhraní xdp-remote byla také přiřazena alespoň prázdná aplikace, a aby bylo aktivováno.

Jak jsem to zjistil?

Sledování cesty paketu jádrem umožňuje mechanismus perf events, který navíc používá stejnou virtuální stroj, tedy pro analýzu eBPF se používá eBPF.

Musíš udělat dobro ze zla, protože už není nic jiného, z čeho by se dalo udělat.

$ 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 Neexistující zařízení nebo adresa

Funkce veth_xdp_flush_bq() dostává kód chyby od veth_xdp_xmit(), kde hledám ENXIO a nacházím komentář.

Obnovíme minimální filtr (XDP_PASS) v souboru xdp_dummy.c, přidáme ho do Makefile a připojíme k xdp-remote:

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

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

62:57:8e:70:44:64 > 26:0e:25:37:8f:96, ethertype IPv4 (0x0800), délka 98: (tos 0x0, ttl 64, id 13762, offset 0, flags [DF], proto ICMP (1), délka 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), délka 98: (tos 0x0, ttl 64, id 13762, offset 0, flags [DF], proto ICMP (1), délka 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, je třeba odebrat filtry (to udělá sudo ./stand detach), uvolnit ping, poté nastavit filtry a zkusit to znovu. Problém je v tom, že filtr XDP_TX působí jak na ARP, a pokud zásobník
prostoru názvů xdp-test zapomněl MAC adresu 192.0.2.1, nebude schopen vyřešit tuto IP.

Formulace úkolu

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

Dosud populární DDoS útok zůstává SYN flood, jehož jádro spočívá v následujícím. Při navazování spojení (TCP handshake) server obdrží SYN, vyčlení prostředky pro budoucí spojení, odpoví SYNACK paketem a čeká na ACK. Útočník jednoduše posílá SYN pakety z falešných adres v počtu tisíců za sekundu z každého hostitele z tisícového botnetu. Server musí okamžitě vyčlenit prostředky při příjezdu paketu, a uvolňuje je po dlouhém časovém limitu, což vede k vyčerpání paměti nebo limitů, nové spojení nejsou přijímány, služba je nedostupná.

Pokud nevyčleňujete prostředky na SYN paket, a pouze odpovídáte SYNACK paketem, jak má server poznat, že ACK paket, který přišel později, se vztahuje na SYN paket, který nebyl uchován? Vždyť útočník může generovat i falešné ACK. Jádro SYN cookie spočívá v kódování v seqnum parametrech spojení jako hash z adres, portů a měnícího se soli. Pokud ACK dorazil před změnou soli, lze znovu spočítat hash a porovnat s acknum. Podvrhnout acknum útočník nemůže, protože sůl zahrnuje tajemství, a nedokáže jej prolomit kvůli omezenému kanálu.

SYN cookie byl dávno realizován v jádře Linux a může se dokonce automaticky zapnout, pokud přicházejí SYN příliš rychle a hromadně.

Informace o TCP handshake

TCP zajišťuje přenos dat jako proud bytů, například nad TCP jsou přenášeny HTTP žádosti. Proud je přenášen po kouskách v paketech. Všechny TCP pakety mají logické příznaky a 32bitová čísla sekvence:

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

  • Sekvenční číslo (seqnum) určuje posun v toku dat pro první bajt, který je přenášen v tomto paketu. Například, pokud v prvním paketu s X bajty dat bylo toto číslo N, v následujícím paketu s novými daty bude N+X. Na začátku spojení si každá strana tento číslo vybírá zcela náhodně.

  • Potvrzovací číslo (acknum) je stejné posunutí jako seqnum, ale určuje nikoli číslo přenášeného bajtu, ale číslo prvního bajtu od příjemce, který odesílatel neviděl.

Na začátku spojení se strany musí dohodnout seqnum a acknum. Klient odešle SYN paket se svým seqnum = X. Server odpovídá SYNACK paketem, do kterého zapíše své seqnum = Y a nastaví acknum = X + 1. Klient na SYNACK odpovídá ACK paketem, kde seqnum = X + 1, acknum = Y + 1. Poté začíná vlastní přenos dat.

Pokud protistrana nepotvrdí přijetí paketu, TCP ho znovu odešle po timeoutu.

Proč se SYN cookies nepoužívají vždy?

Za prvé, pokud se ztratí SYNACK nebo ACK, bude třeba čekat na opětovné odeslání – což zpomaluje navazování spojení. Za druhé, v SYN paketu – a pouze v něm! – se přenáší řada voleb, které ovlivňují další fungování spojení. Tím, že server nezapamatuje příchozí SYN pakety, tímto způsobem ignoruje tyto volby, v dalších paketech už klient tyto nevložil. TCP však může fungovat, ale minimálně v počáteční fázi se kvalita spojení sníží.

Z pohledu paketů musí XDP program provést následující:

  • na SYN odpovědět SYNACK s cookie;
  • na ACK odpovědět RST (zrušit spojení);
  • ostatní pakety zahodit.

Pseudokód algoritmu spolu s analýzou paketu:

Pokud to není Ethernet,
    přeskočte paket.
Pokud to není IPv4,
    přeskočte paket.
Pokud adresa je v tabulce ověřených,               (*)
        snižte čítač zbývajících ověření,
        přeskočte paket.
Pokud to není TCP,
    zahodit paket.     (**)
Pokud to je SYN,
    odpovědět SYN-ACK s cookie.
Pokud to je ACK,
    pokud v acknum leží ne cookie,
        zahodit paket.
    Záznam do tabulky adresy s N zbývajícími ověřeními.    (*)
    Odpovědět RST.   (**)
V ostatních případech zahodit paket.

Jedna (*) jsou označeny body, kde je třeba řídit stav systému – v první fázi se bez nich obejdeme, jednoduše implementujeme TCP handshake s generováním SYN cookie jako seqnum.

Na místě (**), zatímco nemáme tabulku, budeme paket přeskočit.

Realizace TCP handshake

Analýza paketu a ověření kódu

Budeme potřebovat struktury síťových záhlaví: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) a TCP (uapi/linux/tcp.h). Nakonec se mi nepodařilo připojit kvůli chybám spojeným s atomic64_t, musel jsem zkopírovat potřebné definice do kódu.

Všechny funkce, které jsou v C vyčleněny pro snadnost čtení, by měly být vloženy na místo jejich volání, protože ověřovač eBPF v jádře zakazuje zpětné skoky, tedy fakticky cykly a volání funkcí.

#define INTERNAL static __attribute__((always_inline))

Makro LOG() vypíná tisk v release verzi.

Program představuje potrubí funkcí. Každá funkce přijímá paket, ve kterém je vyčleněna hlavička příslušné úrovně, například process_ether() očekává, že je vyplněno ether. Na základě analýzy polí může funkce předat paket na vyšší úroveň. Výsledek práce funkce je akce XDP. Zatím obsluhovače SYN a ACK propouští 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);
}

Prosím věnujte pozornost kontrolám označeným A a B. Pokud zakomentujete A, program se zkompiluje, ale při načítání bude chyba ověření:

Ověření analýzy:


11: (7b) *(u64 *)(r10 -48) = r1
12: (71) r3 = *(u8 *)(r7 +13)
neplatný přístup k paketu, off=13 velikost=1, R7(id=0,off=0,r=0)
R7 offset je mimo paket
zpracováno 11 insns (limit 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0

Chyba při získávání programu/mapy!

Klíčový řetězec neplatný přístup k paketu, off=13 velikost=1, R7(id=0,off=0,r=0): existují cestovní vykonání, kdy třináctý bajt od začátku bufferu leží mimo paket. Podle výpisu je těžké pochopit, o jaký řádek se jedná, ale máme číslo instrukce (12) a disassembler, který ukazuje řádky zdrojového kódu:

llvm-objdump -S xdp_filter.o | less

V tomto případě odkazuje na řádek

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

což jasně ukazuje, že problém je v ether. Vždy by to tak bylo.

Odpověď na SYN

Cílem v této fázi je vytvořit správný SYNACK paket se stanoveným seqnum, který bude v budoucnu nahrazen SYN cookie. Všechny změny se provádějí v process_tcp_syn() a okolí.

Kontrola paketu

Jak podivné, tohle je ta nejvýznamnější řádka, přesněji řečeno, komentář k ní:

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

Při psaní první verze kódu byl použit kernel 5.1, pro jehož verifikátor byla rozdíl mezi data_end a (const void*)ctx->data_end. Při psaní článku kernel 5.3.1 takový problém neměl. Možná se kompilátor na lokální proměnnou díval jinak než na pole. Morálka - při velkém zanoření může zjednodušení kódu pomoci.

Dále rutinní kontroly délek na slávu verifikátoru; o 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 */
}

Obrátka paketu

Vyplňujeme seqnum a acknum, nastavujeme ACK (SYN již byl nastaven):

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

Vyměníme porty TCP, adresu IP a MAC adresy. Standardní knihovna není dostupná z XDP programu, proto memcpy() je makro, které skrývá intrinsic Clang.

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í sečtení všech 16bitových slov v hlavičkách, a protože velikost hlaviček je v nich zapsána, tedy k okamžiku kompilace není známa. To je problém, protože verifikátor nepustí běžný cyklus na proměnné hranici. Nicméně velikost hlaviček je omezena: na 64 bajtů každý. Lze vytvořit cyklus s pevným počtem iterací, který může skončit předčasně.

Povšimněte si, že existuje RFC 1624 o tom, jak částečně přepočítat kontrolní součet, pokud byla změněna pouze fixní slova paketů. Nicméně tento způsob není univerzální a jeho implementaci by bylo obtížné udržovat.

Funkce pro výpočet 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;
}

I přes to, že size ověřeno volajícím kódem, druhá podmínka odchodu je nezbytná, aby mohl potvrzovač dokázat dokončení smyčky.

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

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

Samotný přepočet 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 <check = 0;
tcp_csum += sum16(tcp, tcp_len, data_end);
tcp->check = carry(tcp_csum);

return XDP_TX;

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

Ověření TCP handshake

Filtr správně navazuje spojení s netcat, přičemž se vynechává konečné ACK, na které Linux reagoval RST paketem, protože síťový zásobník nedostal SYN - byl převeden na SYNACK a vrácen zpět - a z pohledu OS dorazil paket, který nesouvisel s otevřenými spojeními.

$ sudo ip netns exec xdp-test   nc -nv 192.0.2.1 6666
192.0.2.1 6666: Spojení resetováno partnerem

Je důležité testovat skutečnými aplikacemi a sledovat tcpdump na xdp-remote protože například hping3 ne reaguje na nesprávné kontrolní součty.

Z pohledu XDP je samotná kontrola triviální. Algoritmus výpočtu je primitivní a pravděpodobně zranitelný vůči sofistikovanému útočníkovi. Jádro Linuxu například používá kryptografický SipHash, ale jeho implementace pro XDP jasně překračuje rámec článku.

Objevil se pro nové TODO, související s externí interakcí:

  • XDP program nemůže uchovávat cookie_seed (tajnou část soli) ve globální proměnné, potřebuje úložiště v jádře, jehož hodnota bude pravidelně aktualizována z důvěryhodného generátoru.

  • Při shodě SYN cookie v ACK paketu je třeba nevytisknout zprávu, ale pamatovat si IP ověřeného klienta, abychom později mohli propustit pakety od něj.

Ověření legitimním klientem:

$ sudo ip netns exec xdp-test   nc -nv 192.0.2.1 6666
192.0.2.1 6666: Spojení resetováno partnerem

V logu je zaznamenáno úspěšné ověření (flags=0x2 — to je SYN, flags=0x10 — to je ACK):

Ether(proto=0x800)
  IP(src=0x20e6e11a dst=0x20e6e11e proto=6)
    TCP(sport=50836 dport=6666 flags=0x2)
Ether(proto=0x800)
  IP(src=0xfe2cb11a dst=0xfe2cb11e proto=6)
    TCP(sport=50836 dport=6666 flags=0x10)
      cookie matches for client 20200c0

Zatímco není seznam ověřených IP, ochrany proti SYN flood nebude, ale zde je reakce na ACK flood, spuštěný takovýmto příkazem:

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

Záznamy v logu:

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

Závěr

Někdy se eBPF a zejména XDP vnímají spíše jako nástroj pokročilého administrátora, než jako platforma pro vývoj. Opravdu, XDP je nástroj pro zásah do zpracování paketů jádrem, a nikoli alternativou k jádrovému stacku jako DPDK a další varianty pro obcházení jádra. Na druhé straně, XDP umožňuje realizaci poměrně složité logiky, kterou je navíc snadné aktualizovat bez přerušení zpracování provozu. Verifikátor nevytváří velké problémy, osobně bych se nezříkal takového nástroje pro části kódu běžícího v uživatelském prostoru.

Ve druhé části, pokud je téma zajímavé, dokončíme tabulku ověřených klientů a výpadků spojení, implementujeme počitadla a napíšeme utilitu pro uživatelský prostor pro správu filtru.

Odkazy:

Zdroj: habr.com

Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster