Pišemo zaščito pred napadi DDoS na XDP. Jedrski del

Tehnologija eXpress Data Path (XDP) omogoča obdelavo naključnega prometa na vmesnikih Linuxa, preden paketi vstopijo v omrežni sklad jedra. Uporaba XDP - zaščita pred DDoS napadi (CloudFlare), kompleksni filtri, zbiranje statistik (Netflix). Programe XDP izvaja navidezni stroj eBPF, zato imajo omejitve glede kode in razpoložljivih funkcij jedra, odvisno od vrste filtra.

Članek je namenjen zapolnitvi pomanjkljivosti številnih gradiv o XDP. Prvič, zagotavljajo že pripravljeno kodo, ki takoj zaobide funkcije XDP: pripravljena je za preverjanje ali pa je preveč preprosta, da bi povzročala težave. Ko nato poskušate napisati svojo kodo iz nič, nimate pojma, kaj storiti s tipičnimi napakami. Drugič, načini za lokalno testiranje XDP brez VM in strojne opreme niso zajeti, kljub dejstvu, da imajo svoje pasti. Besedilo je namenjeno programerjem, ki poznajo omrežja in Linux in jih zanimata XDP in eBPF.

V tem delu bomo podrobno razumeli, kako je filter XDP sestavljen in kako ga preizkusiti, nato pa bomo napisali preprosto različico dobro znanega mehanizma piškotkov SYN na ravni obdelave paketov. Zaenkrat ne bomo ustvarili "belega seznama"
preverjene stranke, vodi števce in upravlja filter - dovolj dnevnikov.

Pisali bomo v C - ni modno, je pa praktično. Vsa koda je na voljo na GitHubu prek povezave na koncu in je razdeljena na objave glede na faze, opisane v članku.

Odpoved odgovornosti. V tem članku bo razvita mini rešitev za zaščito pred napadi DDoS, ker je to realna naloga za XDP in moje področje. Vendar je glavni cilj razumeti tehnologijo; to ni vodnik za ustvarjanje že pripravljene zaščite. Koda vadnice ni optimizirana in izpušča nekatere nianse.

Kratek pregled XDP

Navedel bom le ključne točke, da ne podvajam dokumentacije in obstoječih člankov.

Torej se koda filtra naloži v jedro. Dohodni paketi se posredujejo filtru. Posledično mora filter sprejeti odločitev: posredovati paket v jedro (XDP_PASS), spusti paket (XDP_DROP) ali pošljite nazaj (XDP_TX). Filter lahko spremeni paket, to še posebej velja za XDP_TX. Program lahko tudi prekinete (XDP_ABORTED) in ponastavite paket, vendar je to analogno assert(0) - za odpravljanje napak.

Navidezni stroj eBPF (razširjeni Berkley Packet Filter) je namenoma poenostavljen, tako da lahko jedro preveri, ali se koda ne vrti v zankah in ne poškoduje pomnilnika drugih ljudi. Kumulativne omejitve in pregledi:

  • Zanke (nazaj) so prepovedane.
  • Obstaja sklad za podatke, vendar ni funkcij (vse funkcije C morajo biti vstavljene).
  • Dostopi do pomnilnika zunaj sklada in medpomnilnika paketov so prepovedani.
  • Velikost kode je omejena, vendar v praksi ni zelo pomembna.
  • Dovoljeni so le klici posebnih funkcij jedra (eBPF helpers).

Oblikovanje in namestitev filtra izgleda takole:

  1. Izvorna koda (npr kernel.c) se prevede v objekt (kernel.o) pod arhitekturo virtualnega stroja eBPF. Od oktobra 2019 prevajanje v eBPF podpira Clang in je obljubljeno v GCC 10.1.
  2. Če ta objektna koda vsebuje klice struktur jedra (na primer tabel in števcev), se njihovi ID-ji nadomestijo z ničlami, kar pomeni, da takšne kode ni mogoče izvesti. Pred nalaganjem v jedro morate te ničle zamenjati z ID-ji določenih objektov, ustvarjenih s klici jedra (povežite kodo). To lahko storite z zunanjimi pripomočki ali pa napišete program, ki bo povezal in naložil določen filter.
  3. Jedro preveri naloženi program. Preveri se odsotnost ciklov in neprehodnost meja paketov in skladov. Če preveritelj ne more dokazati, da je koda pravilna, je program zavrnjen - morate mu biti sposobni ugoditi.
  4. Po uspešnem preverjanju jedro prevede objektno kodo arhitekture eBPF v strojno kodo sistemske arhitekture (pravočasno).
  5. Program se priključi na vmesnik in začne obdelovati pakete.

Ker se XDP izvaja v jedru, se odpravljanje napak izvaja z uporabo dnevnikov sledenja in pravzaprav paketov, ki jih program filtrira ali ustvari. Vendar eBPF zagotavlja, da je naložena koda varna za sistem, tako da lahko eksperimentirate z XDP neposredno v lokalnem Linuxu.

Priprava okolja

Skupščina

Clang ne more neposredno izdelati objektne kode za arhitekturo eBPF, zato je postopek sestavljen iz dveh korakov:

  1. Prevedi kodo C v bajtno kodo LLVM (clang -emit-llvm).
  2. Pretvori bajtno kodo v objektno kodo eBPF (llc -march=bpf -filetype=obj).

Pri pisanju filtra bo koristnih nekaj datotek s pomožnimi funkcijami in makri iz testov jedra. Pomembno je, da se ujemajo z različico jedra (KVER). Prenesite jih v helpers/:

export KVER=v5.3.7
export BASE=https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/plain/tools/testing/selftests/bpf
wget -P helpers --content-disposition "${BASE}/bpf_helpers.h?h=${KVER}" "${BASE}/bpf_endian.h?h=${KVER}"
unset KVER BASE

Makefile za Arch Linux (jedro 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 vsebuje pot do glav jedra, ARCH — sistemska arhitektura. Poti in orodja se lahko med distribucijami nekoliko razlikujejo.

Primer razlik za Debian 10 (jedro 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 povežite imenik s pomožnimi glavami in več imenikov z glavami jedra. Simbol __KERNEL__ pomeni, da so glave UAPI (userspace API) definirane za kodo jedra, saj se filter izvaja v jedru.

Zaščito sklada lahko onemogočite (-fno-stack-protector), ker preverjanje kode eBPF še vedno preverja kršitve sklada zunaj meja. Optimizacije se splača vklopiti takoj, saj je velikost bajtne kode eBPF omejena.

Začnimo s filtrom, ki prepušča vse pakete in ne naredi ničesar:

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

Ekipa make zbira xdp_filter.o. Kje poskusiti zdaj?

Testno stojalo

Stojalo mora vsebovati dva vmesnika: na katerem bo filter in s katerega se bodo pošiljali paketi. To morajo biti polnopravne naprave Linux z lastnim IP-jem, da lahko preverimo, kako običajne aplikacije delujejo z našim filtrom.

Za nas so primerne naprave tipa veth (virtualni ethernet): to je par navideznih omrežnih vmesnikov, »povezanih« neposredno med seboj. Ustvarite jih lahko tako (v tem razdelku so vsi ukazi ip se izvajajo od root):

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

Tukaj xdp-remote и xdp-local — imena naprav. Vklopljeno xdp-local (192.0.2.1/24) bo priložen filter, z xdp-remote (192.0.2.2/24) dohodni promet bo poslan. Vendar obstaja težava: vmesnika sta na istem računalniku in Linux ne bo poslal prometa enemu od njih prek drugega. To lahko rešite z zapletenimi pravili iptables, vendar bodo morali spremeniti pakete, kar je neprijetno za odpravljanje napak. Bolje je uporabiti omrežne imenske prostore (v nadaljevanju netns).

Imenski prostor omrežja vsebuje nabor vmesnikov, usmerjevalnih tabel in pravil NetFilter, izoliranih od podobnih objektov v drugih omrežjih. Vsak proces se izvaja v imenskem prostoru in ima dostop samo do objektov tega omrežja. Privzeto ima sistem en sam omrežni imenski prostor za vse objekte, tako da lahko delate v Linuxu in ne poznate netns.

Ustvarimo nov imenski prostor xdp-test in ga premaknite tja xdp-remote.

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

Nato se začne postopek xdp-test, ne bo "videl" xdp-local (privzeto bo ostal v netns) in pri pošiljanju paketa na 192.0.2.1 ga bo posredoval prek xdp-remote, ker je to edini vmesnik na 192.0.2.0/24, ki je dostopen temu procesu. To deluje tudi v nasprotni smeri.

Pri premikanju med omrežji se vmesnik izklopi in izgubi naslov. Če želite konfigurirati vmesnik v netns, morate zagnati ip ... v tem ukaznem imenskem prostoru 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

Kot lahko vidite, se to ne razlikuje od nastavitve xdp-local v privzetem imenskem prostoru:

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

Če tečeš tcpdump -tnevi xdp-local, lahko vidite, da so paketi poslani iz xdp-test, so dostavljeni v ta vmesnik:

ip netns exec xdp-test   ping 192.0.2.1

Priročno je zagnati lupino xdp-test. Repozitorij ima skript, ki avtomatizira delo s stojalom, stojalo lahko na primer konfigurirate z ukazom sudo ./stand up in ga izbrišite sudo ./stand down.

Sledenje

Filter je povezan z napravo, kot je ta:

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

Ključ -force potrebno za povezovanje novega programa, če je drug že povezan. »Nobena novica je dobra novica« ne govori o tem ukazu, rezultat je v vsakem primeru obsežen. kažejo verbose neobvezno, vendar se z njim prikaže poročilo o delu preverjalnika kode s seznamom sklopov:

Verifier analysis:

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

Prekinite povezavo programa z vmesnikom:

ip link set dev xdp-local xdp off

V skriptu so to ukazi sudo ./stand attach и sudo ./stand detach.

S pritrditvijo filtra se lahko prepričate o tem ping še naprej deluje, a program deluje? Dodajmo dnevnike. funkcija bpf_trace_printk() podoben printf(), vendar podpira le do tri argumente, razen vzorca, in omejen seznam specifikatorjev. Makro bpf_printk() poenostavi klic.

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

Izhod gre v kanal sledenja jedra, ki mora biti omogočen:

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

Ogled niti sporočila:

cat /sys/kernel/debug/tracing/trace_pipe

Oba ukaza izvedeta klic sudo ./stand log.

Ping bi zdaj moral sprožiti sporočila, kot je ta:

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

Če pozorno pogledate izpis preveritelja, boste opazili čudne izračune:

0: (bf) r3 = r1
1: (18) r1 = 0xa7025203a7465
3: (7b) *(u64 *)(r10 -8) = r1
4: (18) r1 = 0x6b63617020746f67
6: (7b) *(u64 *)(r10 -16) = r1
7: (bf) r1 = r10
8: (07) r1 += -16
9: (b7) r2 = 16
10: (85) call bpf_trace_printk#6
<...>

Dejstvo je, da programi eBPF nimajo podatkovnega odseka, zato so edini način za kodiranje formatnega niza neposredni argumenti ukazov VM:

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

Iz tega razloga rezultat odpravljanja napak močno napihne končno kodo.

Pošiljanje paketov XDP

Spremenimo filter: naj pošlje nazaj vse dohodne pakete. Z omrežnega vidika je to nepravilno, saj bi bilo treba spremeniti naslove v glavah, zdaj pa je pomembno načelno delo.

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

Kosilo tcpdumpxdp-remote. Moral bi prikazati enake odhodne in dohodne ICMP Echo Request in prenehati prikazovati ICMP Echo Reply. Vendar se ne pokaže. Izkazalo se je, da za delo XDP_TX v programu naprej xdp-local morajona vmesnik za par xdp-remote dodeljen je bil tudi program, tudi če je bil prazen, in je bil dvignjen.

Kako sem to vedel?

Sledite poti paketa v jedru Mehanizem perf dogodkov mimogrede omogoča uporabo istega virtualnega stroja, to pomeni, da se eBPF uporablja za obravnavo eBPF.

Iz zla morate narediti dobro, ker ga ni mogoče narediti iz ničesar drugega.

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

Kaj je koda 6?

$ errno 6
ENXIO 6 No such device or address

Funkcija veth_xdp_flush_bq() prejme kodo napake od veth_xdp_xmit(), kjer iščete po ENXIO in poiščite komentar.

Obnovimo minimalni filter (XDP_PASS) v datoteki xdp_dummy.c, ga dodajte v Makefile, ga povežite z xdp-remote:

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

Zdaj tcpdump prikazuje, kaj se pričakuje:

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

Če so namesto tega prikazani samo ARP-ji, morate odstraniti filtre (to stori sudo ./stand detach), izpusti ping, nato nastavite filtre in poskusite znova. Težava je v tem, da filter XDP_TX velja tako za ARP kot za sklad
imenski prostori xdp-test uspelo »pozabiti« naslov MAC 192.0.2.1, ne bo mogel razrešiti tega IP-ja.

Izjava o težavah

Preidimo k navedeni nalogi: napisati mehanizem piškotkov SYN na XDP.

SYN flood ostaja priljubljen napad DDoS, katerega bistvo je naslednje. Ko je povezava vzpostavljena (TCP rokovanje), strežnik prejme SYN, dodeli sredstva za prihodnjo povezavo, odgovori s paketom SYNACK in čaka na ACK. Napadalec preprosto pošilja pakete SYN z lažnih naslovov v tisočih na sekundo iz vsakega gostitelja v večtisoč močnem botnetu. Strežnik je prisiljen dodeliti vire takoj po prejemu paketa, vendar jih sprosti po dolgi časovni omejitvi; posledično so pomnilnik ali omejitve izčrpani, nove povezave niso sprejete in storitev ni na voljo.

Če virov ne dodelite na podlagi paketa SYN, ampak samo odgovorite s paketom SYNACK, kako lahko potem strežnik razume, da se paket ACK, ki je prispel pozneje, nanaša na paket SYN, ki ni bil shranjen? Navsezadnje lahko napadalec ustvari tudi lažne ACK-e. Bistvo piškotka SYN je, da se kodira seqnum parametri povezave kot zgoščevanje naslovov, vrat in spreminjajoče se soli. Če je ACK uspel prispeti, preden je bila sol spremenjena, lahko znova izračunate zgoščeno vrednost in jo primerjate z acknum. Kovati acknum napadalec ne more, ker sol vključuje skrivnost in je ne bo mogel razvrstiti zaradi omejenega kanala.

Piškotek SYN je že dolgo implementiran v jedro Linuxa in se lahko celo samodejno omogoči, če SYN prispejo prehitro in množično.

Izobraževalni program o rokovanju TCP

TCP zagotavlja prenos podatkov kot tok bajtov, na primer zahteve HTTP se prenašajo prek TCP. Tok se prenaša po delih v paketih. Vsi paketi TCP imajo logične zastavice in 32-bitne zaporedne številke:

  • Kombinacija zastavic določa vlogo posameznega paketa. Oznaka SYN pomeni, da je to pošiljateljev prvi paket v povezavi. Zastavica ACK pomeni, da je pošiljatelj prejel vse podatke o povezavi do bajta acknum. Paket ima lahko več zastavic in je poimenovan po njihovi kombinaciji, na primer paket SYNACK.

  • Zaporedna številka (seqnum) določa odmik v podatkovnem toku za prvi bajt, ki se prenaša v tem paketu. Na primer, če je bilo v prvem paketu z X bajti podatkov to število N, bo v naslednjem paketu z novimi podatki N+X. Na začetku povezave vsak udeleženec naključno izbere to številko.

  • Številka potrditve (acknum) je enak odmik kot seqnum, vendar ne določa številke bajta, ki se prenaša, temveč številko prvega bajta od prejemnika, ki ga pošiljatelj ni videl.

Na začetku priklopa se morata stranki dogovoriti seqnum и acknum. Odjemalec pošlje paket SYN s svojim seqnum = X. Strežnik se odzove s paketom SYNACK, kjer zapiše svoje seqnum = Y in eksponati acknum = X + 1. Odjemalec odgovori na SYNACK s paketom ACK, kjer seqnum = X + 1, acknum = Y + 1. Po tem se začne dejanski prenos podatkov.

Če vrstnik ne potrdi prejema paketa, ga TCP po preteku časovne omejitve ponovno pošlje.

Zakaj se piškotki SYN ne uporabljajo vedno?

Prvič, če se SYNACK ali ACK izgubi, boste morali počakati na ponovno pošiljanje, kar upočasni vzpostavitev povezave. Drugič, v paketu SYN - in samo v njem! — prenese se več možnosti, ki vplivajo na nadaljnje delovanje povezave. Ker si strežnik ne zapomni dohodnih paketov SYN, te možnosti ignorira, odjemalec jih ne bo več poslal v naslednjih paketih. TCP lahko v tem primeru deluje, vendar se bo vsaj na začetni stopnji kakovost povezave zmanjšala.

Z vidika paketov mora program XDP narediti naslednje:

  • odgovori na SYN s SYNACK s piškotkom;
  • odgovori na ACK z RST (prekini povezavo);
  • zavrzite preostale pakete.

Psevdokoda algoritma skupaj z razčlenjevanjem paketa:

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

ena (*) Označene so točke, kjer morate upravljati stanje sistema - na prvi stopnji lahko storite brez njih, tako da preprosto implementirate TCP rokovanje z generiranjem piškotka SYN kot seqnum.

Na kraju samem (**), dokler nimamo mize, bomo paket preskočili.

Izvajanje rokovanja TCP

Razčlenjevanje paketa in preverjanje kode

Potrebovali bomo strukture omrežnih glav: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) in TCP (uapi/linux/tcp.h). Slednjega nikoli nisem mogel povezati zaradi napak, povezanih z atomic64_t, sem moral kopirati potrebne definicije v kodo.

Vse funkcije, ki so v C-ju dodeljene za berljivost, morajo biti vstavljene na klicno točko, saj preverjalnik eBPF v jedru prepoveduje skoke nazaj, to je pravzaprav zanke in klice funkcij.

#define INTERNAL static __attribute__((always_inline))

Makro LOG() onemogoči tiskanje v gradnji izdaje.

Program je transporter funkcij. Vsak prejme paket, v katerem je označena glava ustrezne ravni, npr. process_ether() pričakuje, da bo zapolnjen ether. Na podlagi rezultatov terenske analize lahko funkcija prenese paket na višji nivo. Rezultat funkcije je dejanje XDP. Zaenkrat upravljalnika SYN in ACK posredujeta vse pakete.

struct Packet {
    struct xdp_md* ctx;

    struct ethhdr* ether;
    struct iphdr* ip;
    struct tcphdr* tcp;
};

INTERNAL int process_tcp_syn(struct Packet* packet) { return XDP_PASS; }
INTERNAL int process_tcp_ack(struct Packet* packet) { return XDP_PASS; }
INTERNAL int process_tcp(struct Packet* packet) { ... }
INTERNAL int process_ip(struct Packet* packet) { ... }

INTERNAL int
process_ether(struct Packet* packet) {
    struct ethhdr* ether = packet->ether;

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

    if (ether->h_proto != bpf_ntohs(ETH_P_IP)) {
        return XDP_PASS;
    }

    // B
    struct iphdr* ip = (struct iphdr*)(ether + 1);
    if ((void*)(ip + 1) > (void*)packet->ctx->data_end) {
        return XDP_DROP; /* malformed packet */
    }

    packet->ip = ip;
    return process_ip(packet);
}

SEC("prog")
int xdp_main(struct xdp_md* ctx) {
    struct Packet packet;
    packet.ctx = ctx;

    // A
    struct ethhdr* ether = (struct ethhdr*)(void*)ctx->data;
    if ((void*)(ether + 1) > (void*)ctx->data_end) {
        return XDP_PASS;
    }

    packet.ether = ether;
    return process_ether(&packet);
}

Opozarjam vas na kljukici, označeni z A in B. Če zakomentirate A, bo program gradil, vendar bo pri nalaganju prišlo do napake pri preverjanju:

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!

Niz ključev invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0): Obstajajo izvedbene poti, ko je trinajsti bajt od začetka vmesnega pomnilnika zunaj paketa. Iz seznama je težko razumeti, o kateri vrstici govorimo, vendar obstaja številka navodil (12) in disassembler, ki prikazuje vrstice izvorne kode:

llvm-objdump -S xdp_filter.o | less

V tem primeru kaže na črto

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

kar jasno pove, da je težava ether. Vedno bi bilo tako.

Odgovorite na SYN

Cilj na tej stopnji je ustvariti pravilen paket SYNACK s fiksnim seqnum, ki ga bo v prihodnosti nadomestil piškotek SYN. Vse spremembe se zgodijo v process_tcp_syn() in okoliških območjih.

Preverjanje paketa

Nenavadno je, da je tukaj najbolj izjemna vrstica ali bolje rečeno komentar k njej:

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

Pri pisanju prve različice kode je bilo uporabljeno jedro 5.1, za katerega preverjalnik je obstajala razlika med data_end и (const void*)ctx->data_end. V času pisanja članka jedro 5.3.1 te težave ni imelo. Morda je prevajalnik dostopal do lokalne spremenljivke drugače kot do polja. Narava zgodbe: Pri velikih situacijah gnezdenja lahko pomaga poenostavitev kode.

Sledijo rutinski pregledi dolžine za slavo overitelja; O MAX_CSUM_BYTES spodaj.

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

Odpiranje paketa

Izpolnimo seqnum и acknum, nastavite ACK (SYN je že nastavljen):

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

Zamenjajte vrata TCP, naslov IP in naslove MAC. Standardna knjižnica ni dostopna iz programa XDP, zato memcpy() — makro, ki skriva notranjost Clanga.

const u16 temp_port = tcp->source;
tcp->source = tcp->dest;
tcp->dest = temp_port;

const u32 temp_ip = ip->saddr;
ip->saddr = ip->daddr;
ip->daddr = temp_ip;

struct ethhdr temp_ether = *ether;
memcpy(ether->h_dest, temp_ether.h_source, ETH_ALEN);
memcpy(ether->h_source, temp_ether.h_dest, ETH_ALEN);

Ponovno izračunavanje kontrolnih vsot

Kontrolne vsote IPv4 in TCP zahtevajo dodajanje vseh 16-bitnih besed v glavah, velikost glav pa je zapisana v njih, kar je v času prevajanja neznana. To je težava, ker preverjalnik ne bo šel skozi običajno zanko do meje spremenljivke. Toda velikost glav je omejena: vsaka do 64 bajtov. Ustvarite lahko zanko s fiksnim številom ponovitev, ki se lahko predčasno konča.

Opažam, da obstaja RFC 1624 o tem, kako delno ponovno izračunati kontrolno vsoto, če se spremenijo samo fiksne besede paketov. Metoda pa ni univerzalna, izvajanje pa bi bilo težje vzdrževati.

Funkcija izračuna kontrolne vsote:

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

čeprav size preverjeno s klicno kodo, je drugi izhodni pogoj potreben, da lahko preveritelj dokaže zaključek zanke.

Za 32-bitne besede je implementirana enostavnejša različica:

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

Dejansko preračunavanje kontrolnih vsot in pošiljanje paketa nazaj:

ip->check = 0;
ip->check = carry(sum16(ip, ip_len, data_end));

u32 tcp_csum = 0;
tcp_csum += sum16_32(ip->saddr);
tcp_csum += sum16_32(ip->daddr);
tcp_csum += 0x0600;
tcp_csum += tcp_len << 8;
tcp->check = 0;
tcp_csum += sum16(tcp, tcp_len, data_end);
tcp->check = carry(tcp_csum);

return XDP_TX;

Funkcija carry() naredi kontrolno vsoto iz 32-bitne vsote 16-bitnih besed v skladu z RFC 791.

Preverjanje rokovanja TCP

Filter pravilno vzpostavi povezavo z netcat, preskoči končni ACK, na katerega je Linux odgovoril s paketom RST, ker omrežni sklad ni prejel SYN - pretvorjen je bil v SYNACK in poslan nazaj - in z vidika OS je prispel paket, ki ni bil povezan z odprte povezave.

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

Pomembno je preveriti s polnopravnimi aplikacijami in spremljati tcpdumpxdp-remote ker npr. hping3 se ne odziva na napačne kontrolne vsote.

Z vidika XDP je samo preverjanje nepomembno. Algoritem izračuna je primitiven in verjetno ranljiv za prefinjene napadalce. Jedro Linuxa na primer uporablja kriptografski SipHash, vendar njegova implementacija za XDP očitno presega obseg tega članka.

Pojavilo se je za nove TODO-je, povezane z zunanjo interakcijo:

  • Program XDP ne more shraniti cookie_seed (skrivni del soli) v globalni spremenljivki potrebujete shrambo v jedru, vrednost v kateri bo občasno posodobljena iz zanesljivega generatorja.

  • Če se piškotek SYN ujema v paketu ACK, vam ni treba natisniti sporočila, vendar si zapomnite IP preverjenega odjemalca, da lahko nadaljujete s posredovanjem paketov od njega.

Preverjanje zakonite stranke:

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

Dnevniki beležijo zaključek preverjanja (flags=0x2 - to 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

Čeprav ni seznama preverjenih IP-jev, ne bo zaščite pred samo poplavo SYN, vendar je tukaj reakcija na poplavo ACK, ki jo sproži naslednji ukaz:

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

Vnosi v dnevnik:

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

Zaključek

Včasih sta eBPF na splošno in zlasti XDP predstavljena bolj kot napredno skrbniško orodje kot kot razvojna platforma. XDP je dejansko orodje za poseganje v obdelavo paketov s strani jedra in ne alternativa skladu jedra, kot je DPDK in druge možnosti obhoda jedra. Po drugi strani vam XDP omogoča implementacijo precej zapletene logike, ki jo je poleg tega enostavno posodobiti brez prekinitve obdelave prometa. Verifikator ne dela velikih težav, osebno ga ne bi zavrnil za dele kode uporabniškega prostora.

V drugem delu bomo, če bo tema zanimiva, dopolnili tabelo verificiranih odjemalcev in odklopov, implementirali števce in napisali uporabniški pripomoček za upravljanje filtra.

Reference:

Vir: www.habr.com

Dodaj komentar