Az XDP-re írunk védelmet a DDoS támadások ellen. Nukleáris rész

Az eXpress Data Path (XDP) technológia lehetővé teszi a forgalom tetszőleges feldolgozását a Linux felületeken, mielőtt a csomagok belépnének a kernel hálózati veremébe. XDP alkalmazása - DDoS támadások elleni védelem (CloudFlare), összetett szűrők, statisztikai adatok gyűjtése (Netflix). Az XDP-programokat az eBPF virtuális gép hajtja végre, ezért a szűrő típusától függően korlátozások vonatkoznak a kódjukra és az elérhető kernelfunkciókra.

A cikk célja, hogy pótolja az XDP-vel kapcsolatos számos anyag hiányosságait. Először is olyan kész kódot adnak, amely azonnal megkerüli az XDP funkcióit: felkészült az ellenőrzésre, vagy túl egyszerű ahhoz, hogy problémákat okozzon. Amikor később a semmiből próbálja megírni saját kódját, nem érti, hogy mit kezdjen a tipikus hibákkal. Másodszor, nem terjed ki az XDP helyi tesztelésének módjaira virtuális gép és hardver nélkül, annak ellenére, hogy megvannak a maguk buktatói. A szöveg azoknak a programozóknak szól, akik ismerik a hálózatokat és a Linuxot, akik érdeklődnek az XDP és az eBPF iránt.

Ebben a részben részletesen megismerjük az XDP szűrő összeállítását és tesztelését, majd megírjuk a jól ismert SYN cookie-k mechanizmusának egyszerű változatát csomagfeldolgozási szinten. Amíg nem alkotunk egy "fehér listát"
ellenőrzött ügyfelek, számlálók vezetése és a szűrő kezelése - elegendő napló.

C-ben fogunk írni - ez nem divatos, de praktikus. Az összes kód elérhető a GitHubon a végén található linken, és a cikkben leírt lépések szerint véglegesítésekre van felosztva.

Fontos. A cikk során egy mini-megoldást fogok kifejleszteni a DDoS támadások kivédésére, mert ez reális feladat az XDP és az én szakterületem számára. A fő cél azonban a technológia megértése, ez nem útmutató a kész védelem létrehozásához. Az oktatóprogram kódja nincs optimalizálva, és kihagy néhány árnyalatot.

Az XDP rövid áttekintése

Csak a kulcsfontosságú pontokat közlöm, hogy ne duplikáljam a dokumentációt és a meglévő cikkeket.

Tehát a szűrőkód betöltődik a kernelbe. A szűrő átadja a bejövő csomagokat. Ennek eredményeként a szűrőnek döntenie kell: átadja a csomagot a kernelnek (XDP_PASS), dobja le a csomagot (XDP_DROP) vagy küldje vissza (XDP_TX). A szűrő megváltoztathatja a csomagot, ez különösen igaz XDP_TX. A program összeomolhat (XDP_ABORTED) és dobja le a csomagot, de ez analóg assert(0) - hibakereséshez.

Az eBPF (extended Berkley Packet Filter) virtuális gépet szándékosan egyszerűsítették, hogy a kernel ellenőrizni tudja, hogy a kód nem hurkol, és nem károsítja-e mások memóriáját. Halmozott korlátozások és ellenőrzések:

  • A hurkok (visszaugrások) tilosak.
  • Van egy verem az adatokhoz, de nincsenek függvények (minden C függvénynek be kell illesztenie).
  • A veremen és a csomagpufferen kívüli memória-hozzáférés tilos.
  • A kód mérete korlátozott, de a gyakorlatban ez nem túl jelentős.
  • Csak speciális kernelfüggvények (eBPF helperek) hívása engedélyezett.

A szűrő fejlesztése és telepítése így néz ki:

  1. forráskód (pl. kernel.c) objektummá fordítja (kernel.o) az eBPF virtuális gép architektúrához. 2019 októberétől az eBPF-re történő fordítást a Clang támogatja, és a GCC 10.1 ígérete szerint.
  2. Ha ebben az objektumkódban kernelstruktúrák (például táblák és számlálók) hívásai vannak, akkor az azonosítóik helyett nullák vannak, vagyis az ilyen kód nem hajtható végre. A kernelbe való betöltés előtt ezeket a nullákat le kell cserélni a kernelhívásokkal létrehozott objektumok azonosítóira (linkelni kell a kódot). Ezt megteheti külső segédprogramokkal, vagy írhat egy programot, amely egy adott szűrőt kapcsol be és tölt be.
  3. A kernel ellenőrzi a betöltendő programot. Ellenőrzi, hogy nincsenek-e ciklusok, és nem léptek-e ki a csomag és a verem határai. Ha az ellenőrző nem tudja bizonyítani a kód helyességét, akkor a program elutasításra kerül – tetszeni kell neki.
  4. A sikeres ellenőrzés után a kernel lefordítja az eBPF architektúra objektumkódját rendszerarchitektúra gépi kódjává (just-in-time).
  5. A program csatlakozik az interfészhez, és megkezdi a csomagok feldolgozását.

Mivel az XDP a kernelben fut, a hibakeresést nyomkövetési naplók, sőt, a program által szűrt vagy generált csomagok végzik. Az eBPF azonban biztonságban tartja a letöltött kódot a rendszer számára, így kísérletezhet az XDP-vel közvetlenül a helyi Linuxon.

A környezet előkészítése

gyülekezés

A Clang nem tud közvetlenül objektumkódot kiadni az eBPF architektúrához, ezért a folyamat két lépésből áll:

  1. Fordítsa le a C kódot LLVM bájtkódra (clang -emit-llvm).
  2. Bájtkód konvertálása eBPF objektumkódmá (llc -march=bpf -filetype=obj).

Szűrő írásakor jól jön pár fájl segédfunkciókkal és makróval kernel tesztekből. Fontos, hogy megegyezzenek a kernel verziójával (KVER). Töltse le őket ide 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 Arch Linuxhoz (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 tartalmazza a kernelfejlécek elérési útját, ARCH - Rendszer Felépítés. Az elérési utak és eszközök kissé eltérhetnek a disztribúciók között.

Példa a különbségekre a Debian 10-hez (4.19.67-es kernel)

# другая команда
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 csatlakoztasson egy könyvtárat segédfejlécekkel és több könyvtárat kernelfejlécekkel. Szimbólum __KERNEL__ azt jelenti, hogy az UAPI (userspace API) fejlécek a kernelkódhoz vannak definiálva, mivel a szűrő a kernelben fut le.

A veremvédelem kikapcsolható (-fno-stack-protector). Azonnal engedélyeznie kell az optimalizálást, mert az eBPF bájtkód mérete korlátozott.

Kezdjük egy szűrővel, amely átenged minden csomagot, és nem csinál semmit:

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

Csapat make gyűjt xdp_filter.o. Hol lehet most tesztelni?

Próbapad

Az állványnak két interfészt kell tartalmaznia: amelyen lesz egy szűrő, és amelyről csomagokat küldenek. Ezeknek teljes Linux-eszközöknek kell lenniük saját IP-címmel, hogy ellenőrizhessük, hogyan működnek a normál alkalmazások a szűrőnkkel.

Az olyan eszközök, mint a veth (virtuális Ethernet) megfelelőek számunkra: egy pár virtuális hálózati interfész, amelyek közvetlenül „csatlakoznak” egymáshoz. Ezeket így hozhatja létre (ebben a szakaszban az összes parancs ip től előadták root):

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

Itt xdp-remote и xdp-local — az eszközök nevei. Tovább xdp-local (192.0.2.1/24) szűrő kerül rögzítésre, azzal xdp-remote (192.0.2.2/24) bejövő forgalom elküldésre kerül. Van azonban egy probléma: az interfészek ugyanazon a gépen vannak, és a Linux nem küld forgalmat egyikre a másikon keresztül. Trükkös szabályokkal meg tudod oldani iptables, de csomagokat kell cserélniük, ami hibakereséskor kényelmetlen. Érdemesebb hálózati névtereket (hálózati névtereket, további netn-eket) használni.

A hálózati névtér interfészeket, útválasztási táblákat és NetFilter-szabályokat tartalmaz, amelyek el vannak különítve más netns hasonló objektumaitól. Mindegyik folyamat valamilyen névtérben fut, és csak ennek a netns-nek az objektumai érhetők el számára. Alapértelmezés szerint a rendszer egyetlen hálózati névtérrel rendelkezik az összes objektum számára, így Linuxon dolgozhat, és nem tud a netns-ről.

Hozzunk létre egy új névteret xdp-test és költözz oda xdp-remote.

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

Ezután beindul a folyamat xdp-test, nem "látja" xdp-local (alapértelmezés szerint a netns-ben marad) és a 192.0.2.1-re küldve átadja azt xdp-remote, mert ez az egyetlen interfész a 192.0.2.0/24-ben a folyamat számára. Ez fordítva is működik.

Netn-ek közötti mozgáskor az interfész lemegy és elveszti a címet. A netns felületének beállításához futnia kell ip ... ebben a parancs névtérben 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

Amint látja, ez nem különbözik a beállítástól xdp-local az alapértelmezett névtérben:

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

Ha futni tcpdump -tnevi xdp-local, láthatja, hogy a címről küldött csomagok xdp-test, erre a felületre kerülnek:

ip netns exec xdp-test   ping 192.0.2.1

Kényelmes egy shell bejáratása xdp-test. A repository rendelkezik egy szkripttel, amely automatizálja az állvánnyal való munkát, például az állványt a paranccsal állíthatja be sudo ./stand up és távolítsa el sudo ./stand down.

nyomon követése

A szűrő a következőképpen van társítva az eszközhöz:

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

kulcs -force egy új program összekapcsolásához szükséges, ha egy másik már kapcsolódik. A "nincs hír jó hír" nem erről a parancsról szól, a kimenet amúgy is terjedelmes. jelezze verbose opcionális, de ezzel együtt megjelenik egy jelentés a kódellenőrző munkájáról az assembler listával:

Verifier analysis:

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

Válassza le a programot a felületről:

ip link set dev xdp-local xdp off

A szkriptben ezek a parancsok sudo ./stand attach и sudo ./stand detach.

A szűrő bekötésével megbizonyosodhat arról ping továbbra is működik, de működik a program? Adjunk hozzá logókat. Funkció bpf_trace_printk() hasonló printf(), de a mintán kívül legfeljebb három argumentumot és a specifikációk korlátozott listáját támogat. Makró bpf_printk() leegyszerűsíti a hívást.

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

A kimenet a kernel nyomkövetési csatornájára megy, amelyet engedélyezni kell:

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

Üzenetfolyam megtekintése:

cat /sys/kernel/debug/tracing/trace_pipe

Mindkét csapat hívást kezdeményez sudo ./stand log.

A Ping-nek most ehhez hasonló üzeneteket kell létrehoznia benne:

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

Ha alaposan megnézi a hitelesítő kimenetét, furcsa számításokat vehet észre:

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

Az a tény, hogy az eBPF programoknak nincs adatrésze, így a formátum karakterlánc kódolásának egyetlen módja a VM parancsok azonnali argumentuma:

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

Emiatt a hibakeresési kimenet nagymértékben felduzzasztja az eredményül kapott kódot.

XDP-csomagok küldése

Változtassuk meg a szűrőt: küldje vissza az összes bejövő csomagot. Ez hálózati szempontból helytelen, hiszen a fejlécekben módosítani kellene a címeket, de most az elvi munka a fontos.

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

Dob tcpdump on xdp-remote. Ugyanazt a kimenő és bejövő ICMP visszhang kérést kell mutatnia, és le kell állítania az ICMP Echo Reply megjelenítését. De nem látszik. Kiderült, hogy működik XDP_TX számára készült programban xdp-local szükségesinterfész párosításához xdp-remote egy program is hozzá lett rendelve, még ha üres is, és fel lett emelve.

honnan tudtam?

Egy csomag útvonalának nyomon követése a kernelben A perf események mechanizmusa egyébként lehetővé teszi ugyanazt a virtuális gépet, vagyis az eBPF-et az eBPF-fel való szétszerelésre használják.

A rosszból jót kell csinálnod, mert mást nem lehet belőle csinálni.

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

Mi az a 6-os kód?

$ errno 6
ENXIO 6 No such device or address

Funkció veth_xdp_flush_bq() -től kapja meg a hibakódot veth_xdp_xmit(), ahol a keresés ENXIO és keressen egy megjegyzést.

Állítsa vissza a minimális szűrőt (XDP_PASS) fájlban xdp_dummy.c, add hozzá a Makefile-hoz, kötődj hozzá xdp-remote:

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

Most tcpdump megmutatja, hogy mi várható:

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

Ha csak az ARP jelenik meg helyette, akkor el kell távolítania a szűrőket (ezért sudo ./stand detach), hagyjuk ping, majd telepítse a szűrőket, és próbálja újra. A probléma a szűrővel van XDP_TX hatással van az ARP-re is, és ha a verem
névterek xdp-test sikerült "elfelejteni" a 192.0.2.1 MAC címet, nem fogja tudni feloldani ezt az IP-t.

Probléma nyilatkozat

Térjünk át a megadott feladatra: írjunk egy SYN cookie-mechanizmust az XDP-n.

A SYN flood továbbra is népszerű DDoS támadás, amelynek lényege a következő. Amikor létrejön a kapcsolat (TCP kézfogás), a szerver SYN-t kap, erőforrásokat foglal le a jövőbeli kapcsolathoz, SYNACK csomaggal válaszol, és vár egy ACK-re. A támadó egyszerűen másodpercenként több ezer SYN-csomagot küld hamisított címekről egy többezer erős botnetben. A szerver a csomag megérkezésekor azonnal kénytelen erőforrásokat lefoglalni, de nagy időkorlát után felszabadítja azokat, aminek következtében a memória vagy a korlátok kimerülnek, az új kapcsolatokat nem fogadják el, és a szolgáltatás nem elérhető.

Ha nem foglal le erőforrásokat a SYN csomagon, hanem csak egy SYNACK csomaggal válaszol, akkor hogyan értheti meg a szerver, hogy a később érkező ACK csomag a nem mentett SYN csomaghoz tartozik? Végül is a támadó hamis ACK-ket is generálhat. A SYN cookie lényege a kódolás seqnum kapcsolati paraméterek címek, portok és változó só hash-jeként. Ha az ACK-nek sikerült megérkeznie a sócsere előtt, újra kiszámolhatja a hash-t, és összehasonlíthatja vele acknum. Kohó acknum a támadó nem tudja, mivel a só tartalmazza a titkot, és a korlátozott csatorna miatt nem lesz ideje átválogatni.

A SYN cookie-kat már régóta implementálták a Linux kernelben, és akár automatikusan is engedélyezhetők, ha a SYN-ek túl gyorsan és tömegesen érkeznek.

Oktatási program a TCP kézfogásról

A TCP bájtfolyamként biztosítja az adatok átvitelét, például a HTTP-kéréseket TCP-n keresztül továbbítják. Az adatfolyamot darabonként csomagokban továbbítják. Minden TCP-csomagnak van logikai zászlója és 32 bites sorszáma:

  • A zászlók kombinációja határozza meg egy adott csomag szerepét. A SYN jelző azt jelenti, hogy ez a küldő első csomagja a kapcsolaton. Az ACK jelző azt jelenti, hogy a küldő megkapta az összes kapcsolati adatot egy bájt erejéig. acknum. Egy csomagnak több jelzője is lehet, és ezek kombinációjáról nevezik el, például egy SYNACK csomag.

  • A sorozatszám (seqnum) meghatározza az adatfolyam eltolását a csomagban elküldött első bájthoz. Például, ha az első X bájt adatot tartalmazó csomagban ez a szám N volt, a következő új adatot tartalmazó csomagban N+X lesz. A kapcsolat elején minden fél véletlenszerűen választja ki ezt a számot.

  • Nyugtázási szám (acknum) - ugyanaz az eltolás, mint a seqnum, de nem az átvitt bájt számát határozza meg, hanem a címzetttől származó első bájt számát, amelyet a küldő nem látott.

A kapcsolat kezdetén a feleknek meg kell állapodniuk seqnum и acknum. A kliens SYN-csomagot küld vele seqnum = X. A szerver egy SYNACK csomaggal válaszol, ahová a sajátját írja seqnum = Y és leleplezi acknum = X + 1. A kliens a SYNACK-re egy ACK csomaggal válaszol, ahol seqnum = X + 1, acknum = Y + 1. Ezt követően kezdődik a tényleges adatátvitel.

Ha a beszélgetőpartner nem nyugtázza a csomag átvételét, a TCP időtúllépéssel újra elküldi.

Miért nem mindig használják a SYN cookie-kat?

Először is, ha egy SYNACK vagy ACK elveszik, meg kell várnia az újraküldést - a kapcsolat létrehozása lelassul. Másodszor, a SYN csomagban - és csakis abban! - számos olyan opció kerül továbbításra, amelyek befolyásolják a kapcsolat további működését. A bejövő SYN csomagokra nem emlékszik, így a szerver figyelmen kívül hagyja ezeket az opciókat, a következő csomagokban a kliens már nem küldi el azokat. A TCP ebben az esetben működhet, de legalább a kezdeti szakaszban a kapcsolat minősége romlik.

A csomagok tekintetében az XDP programnak a következőket kell tennie:

  • válaszoljon a SYN-re a SYNACK-kal cookie-val;
  • válasz ACK-t az RST-vel (szakítsa meg a kapcsolatot);
  • dobja le a többi csomagot.

Az algoritmus pszeudokódja a csomagelemzéssel együtt:

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

Egy (*) meg vannak jelölve azok a pontok, ahol kezelni kell a rendszer állapotát – az első szakaszban megteheti ezeket anélkül, hogy egyszerűen végrehajt egy TCP kézfogást egy SYN cookie szekvenciaként történő generálásával.

Helyben (**), amíg nincs asztalunk, kihagyjuk a csomagot.

TCP kézfogás megvalósítás

Csomagelemzés és kódellenőrzés

Hálózati fejlécstruktúrákra van szükségünk: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) és TCP (uapi/linux/tcp.h). Az utolsóhoz kapcsolódó hibák miatt nem tudtam csatlakozni atomic64_t, a szükséges definíciókat be kellett másolnom a kódba.

A C-ben az olvashatóság szempontjából megkülönböztetett összes függvényt be kell illeszteni a hívás helyére, mivel a kernelben lévő eBPF-ellenőrző tiltja a visszaugrást, vagyis a ciklusokat és a függvényhívásokat.

#define INTERNAL static __attribute__((always_inline))

Makró LOG() letiltja a nyomtatást egy kiadási buildben.

A program függvények csővezetéke. Mindegyik kap egy csomagot, amelyben a megfelelő szintű fejléc van kiemelve, pl. process_ether() kitöltésre vár ether. A terepi elemzés eredményei alapján a függvény a csomagot magasabb szintre tudja vinni. A függvény eredménye egy XDP művelet. Míg a SYN és ACK kezelők minden csomagot átengednek.

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

Figyelem az A és B jelzésű csekket. Ha A-t kommentelsz, akkor a program felépül, de betöltéskor ellenőrzési hiba lesz:

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!

Kulcssor invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0): vannak olyan végrehajtási utak, amikor a puffer kezdetétől számított tizenharmadik bájt a csomagon kívül van. A felsorolásból nehéz megállapítani, hogy melyik sorról beszélünk, de van egy utasításszám (12) és egy disassembler, amely megmutatja a forráskód sorait:

llvm-objdump -S xdp_filter.o | less

Ebben az esetben a vonalra mutat

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

ami egyértelművé teszi, hogy a probléma az ether. Mindig így lenne.

Válasz a SYN-nek

A cél ebben a szakaszban az, hogy egy helyes SYNACK csomagot állítsunk elő fixen seqnum, amelyet a jövőben a SYN cookie vált fel. Minden változás benne történik process_tcp_syn() és környéke.

A csomag ellenőrzése

Furcsa módon itt van a legfigyelemreméltóbb sor, vagy inkább egy megjegyzés hozzá:

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

A kód első verziójának megírásakor az 5.1-es kernelt használták, melynek ellenőrzőjénél különbség volt a data_end и (const void*)ctx->data_end. A cikk írásakor az 5.3.1-es kernelnek nem volt ilyen problémája. Lehet, hogy a fordító egy helyi változóhoz máshogy fér hozzá, mint egy mezőhöz. Erkölcsi – egy nagy fészekben a kód egyszerűsítése segíthet.

A hosszúság további rutinellenőrzése a hitelesítő dicsőségére; O MAX_CSUM_BYTES alább.

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

A csomag kibontása

tölt seqnum и acknum, állítsa be az ACK-t (a SYN már be van állítva):

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

Cserélje fel a TCP-portokat, IP-címeket és MAC-címeket. A szabványos könyvtár nem érhető el az XDP programból, így memcpy() - egy makró, amely elrejti a Clang intrinsik-et.

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

Ellenőrző összeg újraszámítása

Az IPv4 és a TCP ellenőrző összegek megkövetelik az összes 16 bites szó hozzáadását a fejlécekben, és a fejlécek mérete bele van írva, vagyis a fordítás időpontjában nem ismert. Ez azért probléma, mert az ellenőrző nem hagyja ki a normál ciklust a határváltozóig. A fejlécek mérete azonban korlátozott: egyenként legfeljebb 64 bájt. Rögzített számú iterációt tartalmazó ciklust készíthet, amely korán véget érhet.

Megjegyzem, van RFC 1624 arról, hogyan lehet részben újraszámolni az ellenőrző összeget, ha csak a csomagok rögzített szavait változtatjuk meg. A módszer azonban nem univerzális, a megvalósítást nehezebb lenne fenntartani.

Ellenőrző összeg számítási funkció:

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

Habár size Ha a hívókód ellenőrzi, a második kilépési feltétel szükséges ahhoz, hogy a hitelesítő bizonyítsa a hurok végét.

A 32 bites szavakhoz egy egyszerűbb verzió kerül megvalósításra:

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

Az ellenőrző összegek újraszámítása és a csomag visszaküldése:

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;

Funkció carry() ellenőrző összeget készít 32 bites szavak 16 bites összegéből az RFC 791 szerint.

TCP kézfogás ellenőrzése

A szűrő megfelelően hoz létre kapcsolatot netcat, kihagyva az utolsó ACK-t, amire a Linux RST csomaggal válaszolt, mivel a hálózati verem nem kapott SYN-t - SYNACK-re konvertálták és visszaküldték - és az operációs rendszer szempontjából egy olyan csomag érkezett, ami nem nyitott kapcsolatokkal kapcsolatos.

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

Fontos, hogy teljes értékű alkalmazásokkal ellenőrizze és figyelje meg tcpdump on xdp-remote mert pl. hping3 nem reagál a helytelen ellenőrző összegekre.

Az XDP szempontjából maga az ellenőrzés triviális. A számítási algoritmus primitív, és valószínűleg sebezhető a kifinomult támadókkal szemben. A Linux kernel például a kriptográfiai SipHash-t használja, de az XDP-hez való megvalósítása egyértelműen túlmutat e cikk keretein.

Megjelent a külső interakcióhoz kapcsolódó új TODO-khoz:

  • Az XDP program nem tárolható cookie_seed (a só titkos része) egy globális változóban, szükséged van egy kerneltárolóra, amelynek értéke rendszeresen frissül egy megbízható generátorból.

  • Ha az ACK csomagban lévő SYN cookie egyezik, akkor nem kell üzenetet nyomtatnia, de emlékeznie kell az ellenőrzött kliens IP-jére, hogy további csomagokat kihagyhasson belőle.

Érvényesítés jogos ügyfél által:

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

A naplók rögzítették az ellenőrzés áthaladását (flags=0x2 a SYN, flags=0x10 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

Amíg nincs az ellenőrzött IP-k listája, nem lesz védelem maga a SYN elárasztás ellen, de itt van a reakció az ACK elárasztásra, amelyet ez a parancs indított:

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

Naplóbejegyzések:

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

Következtetés

Néha az eBPF általában és különösen az XDP inkább fejlett rendszergazdai eszköz, mint fejlesztői platform. Valójában az XDP egy eszköz a kernelcsomag-feldolgozás megzavarására, és nem a kernelverem alternatívája, mint például a DPDK és más kernel-bypass opciók. Másrészt az XDP meglehetősen bonyolult logika megvalósítását teszi lehetővé, amely ráadásul könnyen frissíthető a forgalomfeldolgozás szüneteltetése nélkül. A hitelesítő nem okoz nagy problémákat, én személy szerint nem utasítanám el a felhasználói terület kód részeinél.

A második részben, ha érdekes a téma, kiegészítjük az ellenőrzött kliensek táblázatát és megszakítjuk a kapcsolatokat, implementáljuk a számlálókat és írunk egy userspace segédprogramot a szűrő kezelésére.

referenciák:

Forrás: will.com

Hozzászólás