Kirjutame XDP-le kaitset DDoS-i rünnakute vastu. Tuumaosa

eXpress Data Path (XDP) tehnoloogia võimaldab Linuxi liidestel teostada liikluse juhuslikku töötlemist enne, kui paketid sisenevad kerneli võrgupinu. XDP rakendus - kaitse DDoS rünnakute eest (CloudFlare), keerukad filtrid, statistika kogumine (Netflix). XDP-programme käivitab eBPF-i virtuaalmasin, seega on neil olenevalt filtri tüübist piirangud nii koodile kui ka saadaolevatele kerneli funktsioonidele.

Artikli eesmärk on täita paljude XDP materjalide puudused. Esiteks pakuvad nad valmis koodi, mis läheb kohe XDP funktsioonidest mööda: see on kontrollimiseks ette valmistatud või on probleemide tekitamiseks liiga lihtne. Kui proovite seejärel oma koodi nullist kirjutada, pole teil aimugi, mida tüüpiliste vigadega peale hakata. Teiseks ei käsitleta võimalusi XDP kohapealseks testimiseks ilma VM-i ja riistvarata, hoolimata sellest, et neil on oma lõkse. Tekst on mõeldud programmeerijatele, kes on kursis võrgunduse ja Linuxiga ning on huvitatud XDP-st ja eBPF-ist.

Selles osas saame üksikasjalikult aru, kuidas XDP-filtrit kokku pannakse ja kuidas seda testida, seejärel kirjutame paketitöötluse tasemel lihtsa versiooni tuntud SYN-küpsiste mehhanismist. Me ei loo veel "valget nimekirja".
kontrollitud kliente, pidama loendureid ja hallata filtrit – piisavalt logisid.

Kirjutame C-keeles - see pole moes, kuid on praktiline. Kogu kood on GitHubis saadaval lõpus oleva lingi kaudu ja on jagatud commitsiks vastavalt artiklis kirjeldatud etappidele.

Vastutusest loobumine Selle artikli jooksul töötan välja minilahenduse DDoS-i rünnakute tõrjumiseks, sest see on XDP ja minu valdkonna jaoks realistlik ülesanne. Peamine eesmärk on aga tehnoloogiast aru saada, see ei ole valmiskaitse loomise juhend. Õpetuskood ei ole optimeeritud ja jätab mõned nüansid välja.

XDP lühiülevaade

Toon välja ainult põhipunktid, et mitte dubleerida dokumentatsiooni ja olemasolevaid artikleid.

Niisiis laaditakse filtri kood kernelisse. Sissetulevad paketid edastatakse filtrile. Selle tulemusena peab filter tegema otsuse: edastama paketi kernelisse (XDP_PASS), kukuta pakett (XDP_DROP) või saatke see tagasi (XDP_TX). Filter võib pakendit muuta, eriti kehtib see XDP_TX. Samuti saate programmi katkestada (XDP_ABORTED) ja lähtestage pakett, kuid see on analoogne assert(0) - silumiseks.

eBPF (Extended Berkley Packet Filter) virtuaalmasin on sihilikult lihtsaks tehtud, et kernel saaks kontrollida, kas kood ei loo silmust ega kahjusta teiste inimeste mälu. Kumulatiivsed piirangud ja kontrollid:

  • Silmused (tagurpidi) on keelatud.
  • Andmete jaoks on virn, kuid funktsioone pole (kõik C-funktsioonid peavad olema sisse kirjutatud).
  • Mälu juurdepääs väljaspool pinu ja pakettpuhvrit on keelatud.
  • Koodi suurus on piiratud, kuid praktikas pole see kuigi oluline.
  • Lubatud on kutsuda ainult kerneli erifunktsioone (eBPF-i abistajad).

Filtri projekteerimine ja paigaldamine näeb välja selline:

  1. Lähtekood (nt kernel.c) on kompileeritud objektiks (kernel.o) eBPF virtuaalmasina arhitektuuri jaoks. Alates 2019. aasta oktoobrist toetab eBPF-i koostamist Clang ja seda lubab GCC 10.1.
  2. Kui see objektikood sisaldab väljakutseid kerneli struktuuridele (näiteks tabelitele ja loenduritele), asendatakse nende ID-d nullidega, mis tähendab, et sellist koodi ei saa käivitada. Enne kernelisse laadimist tuleb need nullid asendada konkreetsete tuumakutsetega loodud objektide ID-dega (linkida kood). Saate seda teha väliste utiliitidega või kirjutada programmi, mis lingib ja laadib konkreetse filtri.
  3. Kernel kontrollib laaditud programmi. Kontrollitakse tsüklite puudumist ning paketi- ja virnapiiride ületamist. Kui kontrollija ei suuda koodi õigsust tõestada, lükatakse programm tagasi – peate suutma talle meeldida.
  4. Pärast edukat kontrollimist kompileerib kernel eBPF arhitektuuri objektikoodi süsteemiarhitektuuri masinkoodiks (just-in-time).
  5. Programm kinnitub liidese külge ja hakkab pakette töötlema.

Kuna XDP töötab kernelis, toimub silumine jälgimislogide ja tegelikult pakettide abil, mida programm filtreerib või genereerib. eBPF aga tagab, et allalaaditud kood on süsteemi jaoks turvaline, nii et saate XDP-ga katsetada otse oma kohalikus Linuxis.

Keskkonna ettevalmistamine

Assamblee

Clang ei saa otseselt eBPF-i arhitektuuri objektikoodi toota, seega koosneb protsess kahest etapist:

  1. Kompileeri C kood LLVM baitkoodiks (clang -emit-llvm).
  2. Teisenda baitkood eBPF objektikoodiks (llc -march=bpf -filetype=obj).

Filtri kirjutamisel tuleb kasuks paar faili koos abifunktsioonide ja makrodega kerneli testidest. On oluline, et need vastaksid kerneli versioonile (KVER). Laadige need alla 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 Linuxi jaoks (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 sisaldab teed kerneli päisteni, ARCH — süsteemi arhitektuur. Teed ja tööriistad võivad distributsioonide lõikes veidi erineda.

Näide Debian 10 (kernel 4.19.67) erinevustest

# другая команда
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 ühendage abipäistega kataloog ja mitu kerneli päistega kataloogi. Sümbol __KERNEL__ tähendab, et UAPI (userspace API) päised on määratletud tuuma koodi jaoks, kuna filter käivitatakse tuumas.

Virnakaitse saab keelata (-fno-stack-protector), kuna eBPF-koodi kontrollija kontrollib endiselt virnast väljuvaid rikkumisi. Optimeerimised tasub kohe sisse lülitada, sest eBPF baitkoodi suurus on piiratud.

Alustame filtriga, mis läbib kõik paketid ja ei tee midagi:

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

Meeskond make kogub xdp_filter.o. Kus seda nüüd proovida?

Katselaud

Stendil peab olema kaks liidest: millele tuleb filter ja kust saadetakse pakette. Need peavad olema täisväärtuslikud Linuxi seadmed, millel on oma IP-aadress, et kontrollida, kuidas tavalised rakendused meie filtriga töötavad.

Meile sobivad veth (virtuaalne Ethernet) tüüpi seadmed: need on virtuaalse võrguliidese paar, mis on üksteisega otse "ühendatud". Saate neid luua nii (selles jaotises kõik käsud ip viiakse läbi alates root):

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

see on xdp-remote и xdp-local — seadmete nimed. Peal xdp-local (192.0.2.1/24) kinnitatakse filter, millega xdp-remote (192.0.2.2/24) sissetulev liiklus saadetakse. Siiski on probleem: liidesed on samas masinas ja Linux ei saada liiklust ühele neist teise kaudu. Saate selle lahendada keeruliste reeglitega iptables, kuid nad peavad pakette muutma, mis on silumiseks ebamugav. Parem on kasutada võrgu nimeruume (edaspidi netns).

Võrgu nimeruum sisaldab liideste, marsruutimistabelite ja NetFilteri reeglite komplekti, mis on isoleeritud teistes netnsides sarnastest objektidest. Iga protsess töötab nimeruumis ja sellel on juurdepääs ainult selle netnsi objektidele. Vaikimisi on süsteemil kõigi objektide jaoks üks võrgu nimeruum, nii et saate töötada Linuxis ega tea netns-idest.

Loome uue nimeruumi xdp-test ja liigutage see sinna xdp-remote.

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

Seejärel hakkab protsess käima xdp-test, ei "näe" xdp-local (see jääb vaikimisi netns-i) ja 192.0.2.1-le paketi saatmisel läheb see läbi xdp-remotesest see on ainus liides 192.0.2.0/24, mis sellele protsessile juurde pääseb. See toimib ka vastupidises suunas.

Netnide vahel liikudes läheb liides alla ja kaotab aadressi. Netns-i liidese konfigureerimiseks peate käivitama ip ... selles käsunimeruumis 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

Nagu näete, ei erine see seadistusest xdp-local vaikenimeruumis:

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

Kui jooksed tcpdump -tnevi xdp-local, näete, et paketid saadeti aadressilt xdp-test, tarnitakse sellele liidesele:

ip netns exec xdp-test   ping 192.0.2.1

Mugav on kesta sisse lasta xdp-test. Hoidlas on skript, mis automatiseerib statiiviga tööd, näiteks saab stendi seadistada käsuga sudo ./stand up ja kustutage see sudo ./stand down.

Jälgimine

Filter on seadmega seotud järgmiselt:

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

võti -force vaja uue programmi linkimiseks, kui teine ​​programm on juba lingitud. “No news is good news” ei käi selle käsu kohta, järeldus on igal juhul mahukas. näidata verbose valikuline, kuid koos sellega kuvatakse koodikontrollija töö kohta koosteloendiga aruanne:

Verifier analysis:

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

Programmi ja liidese linkimise tühistamine:

ip link set dev xdp-local xdp off

Skriptis on need käsud sudo ./stand attach и sudo ./stand detach.

Filtri külge kinnitades saate selles veenduda ping töötab edasi, aga kas programm töötab? Lisame logid. Funktsioon bpf_trace_printk() sarnane printf(), kuid toetab peale mustri ainult kuni kolme argumenti ja piiratud loendit spetsifikaatidest. Makro bpf_printk() lihtsustab kõnet.

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

Väljund läheb kerneli jälgimiskanalisse, mis tuleb lubada:

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

Vaata sõnumilõimi:

cat /sys/kernel/debug/tracing/trace_pipe

Mõlemad käsud teevad kõne sudo ./stand log.

Ping peaks nüüd käivitama sellised teated:

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

Kui vaatate kontrollija väljundit tähelepanelikult, märkate kummalisi arvutusi:

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

Fakt on see, et eBPF-i programmidel pole andmeosa, seega on vormingustringi kodeerimiseks ainus viis VM-i käskude vahetud argumendid:

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

Sel põhjusel paisutab silumisväljund saadud koodi oluliselt.

XDP pakettide saatmine

Vahetame filtrit: las see saadab kõik sissetulevad paketid tagasi. Võrgu seisukohalt on see vale, kuna päistes oleks vaja aadresse muuta, aga nüüd on põhimõtteline töö oluline.

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

Käivitamine tcpdump edasi xdp-remote. See peaks näitama identset väljaminevat ja sissetulevat ICMP kajapäringut ja lõpetama ICMP kaja vastuse kuvamise. Aga see ei näita. Selgub, et töö pärast XDP_TX programmis sees xdp-local vajalikpaariliidesele xdp-remote määrati ka programm, isegi kui see oli tühi, ja ta kasvatati üles.

Kuidas ma seda teadsin?

Jälgige tuumas paketi teed Perf sündmuste mehhanism võimaldab muide kasutada sama virtuaalmasinat, st eBPF-i kasutatakse eBPF-iga lahtivõtmiseks.

Kurjast tuleb teha head, sest pole millestki muust teha.

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

Mis on kood 6?

$ errno 6
ENXIO 6 No such device or address

Funktsioon veth_xdp_flush_bq() saab veakoodi veth_xdp_xmit(), kust otsitakse ENXIO ja leidke kommentaar.

Taastame minimaalse filtri (XDP_PASS) failis xdp_dummy.c, lisage see Makefile'i, siduge see xdp-remote:

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

Nüüd jagab ta tcpdump näitab, mida oodatakse:

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

Kui selle asemel kuvatakse ainult ARP-sid, peate filtrid eemaldama (see teeb sudo ./stand detach), lase lahti ping, seejärel määrake filtrid ja proovige uuesti. Probleem on selles, et filter XDP_TX kehtib nii ARP kui ka virna puhul
nimeruumid xdp-test õnnestus MAC-aadress 192.0.2.1 “unustada”, ei suuda see seda IP-d lahendada.

Probleemi avaldus

Liigume edasi nimetatud ülesande juurde: kirjutage XDP-le SYN-küpsiste mehhanism.

SYN-i üleujutus on endiselt populaarne DDoS-rünnak, mille olemus on järgmine. Kui ühendus on loodud (TCP käepigistus), võtab server vastu SYN-i, eraldab ressursid tulevase ühenduse jaoks, vastab SYNACK-paketiga ja ootab ACK-i. Ründaja lihtsalt saadab tuhandeid SYN-pakette sekundis iga hosti võltsitud aadressidelt mitme tuhande tugeva botneti kaudu. Server on sunnitud eraldama ressursse kohe pärast paketi saabumist, kuid vabastab need pärast suurt ajalõpu, mille tulemusena on mälu või limiidid ammendatud, uusi ühendusi ei võeta vastu ja teenus pole saadaval.

Kui te ei eralda ressursse SYN-paketi põhjal, vaid vastate ainult SYNACK-paketiga, siis kuidas saab server aru, et hiljem saabunud ACK-pakett viitab SYN-paketile, mida ei salvestatud? Lõppude lõpuks võib ründaja genereerida ka võltsitud ACK-e. SYN-küpsise mõte on see sisse kodeerida seqnum ühendusparameetrid aadresside, portide ja muutuva soola räsina. Kui ACK jõudis kohale jõuda enne soola vahetamist, saate räsi uuesti arvutada ja sellega võrrelda acknum. Sepis acknum ründaja ei saa, kuna sool sisaldab saladust, ja tal pole piiratud kanali tõttu aega seda sorteerida.

SYN-i küpsis on Linuxi tuumas juba ammu juurutatud ja seda saab isegi automaatselt lubada, kui SYN-id saabuvad liiga kiiresti ja massiliselt.

TCP käepigistuse õppeprogramm

TCP pakub andmeedastust baitide voona, näiteks HTTP päringud edastatakse üle TCP. Voog edastatakse tükkidena pakettidena. Kõikidel TCP-pakettidel on loogilised lipud ja 32-bitised järjenumbrid:

  • Lippude kombinatsioon määrab konkreetse paketi rolli. SYN-i lipp näitab, et see on saatja esimene pakett ühenduses. ACK-lipp tähendab, et saatja on saanud kõik ühendusandmed kuni baidini acknum. Paketil võib olla mitu lippu ja seda kutsutakse nende kombinatsiooniga, näiteks SYNACK-pakett.

  • Jada number (seqnum) määrab nihke andmevoos esimese baidi jaoks, mis selles paketis edastatakse. Näiteks kui esimeses X baiti andmepaketis oli see arv N, siis järgmises uute andmete paketis on see N+X. Ühenduse alguses valib kumbki pool selle numbri juhuslikult.

  • Kinnituse number (acknum) - sama nihe nagu sekvnum, kuid see ei määra edastatava baidi numbrit, vaid adressaadi esimese baidi numbrit, mida saatja ei näinud.

Ühenduse alguses peavad pooled kokku leppima seqnum и acknum. Klient saadab koos omaga SYN-paketi seqnum = X. Server vastab SYNACK-paketiga, kus ta selle salvestab seqnum = Y ja paljastab acknum = X + 1. Klient vastab SYNACK-ile ACK-paketiga, kus seqnum = X + 1, acknum = Y + 1. Pärast seda algab tegelik andmeedastus.

Kui partner ei kinnita paketi kättesaamist, saadab TCP selle pärast ajalõpu uuesti.

Miks SYN-i küpsiseid alati ei kasutata?

Esiteks, kui SYNACK või ACK kaob, peate ootama, kuni see uuesti saadetakse - ühenduse seadistamine aeglustub. Teiseks SYN-i paketis – ja ainult selles! — edastatakse mitmeid valikuid, mis mõjutavad ühenduse edasist toimimist. Sissetulevaid SYN-i pakette meelde jätmata ignoreerib server neid valikuid ning klient ei saada neid järgmistes pakettides. TCP võib sel juhul töötada, kuid vähemalt algstaadiumis ühenduse kvaliteet langeb.

Pakettide vaatenurgast peab XDP programm tegema järgmist:

  • vastake SYN-ile SYNACK-iga küpsisega;
  • vastata ACK-ile RST-ga (katkesta ühendus);
  • ülejäänud pakid ära visata.

Algoritmi pseudokood koos paketi sõelumisega:

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

Üks (*) märgitakse punktid, kus peate haldama süsteemi olekut - esimeses etapis saate ilma nendeta hakkama, rakendades lihtsalt TCP-käepigistust, genereerides järjenumbrina SYN-küpsise.

Kohapeal (**), kui meil pole lauda, ​​jätame paki vahele.

TCP käepigistuse rakendamine

Paketi parsimine ja koodi kontrollimine

Vajame võrgu päise struktuure: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) ja TCP (uapi/linux/tcp.h). Viimast ei saanud ma sellega seotud vigade tõttu ühendada atomic64_t, pidin vajalikud definitsioonid koodi kopeerima.

Kõik funktsioonid, mis on C-s loetavuse huvides esile tõstetud, peavad olema kutsumispunktis sees, kuna kernelis olev eBPF-i kontrollija keelab tagasisuunamise, st tegelikult tsüklite ja funktsioonikutsete.

#define INTERNAL static __attribute__((always_inline))

Makro LOG() keelab väljalaske järgus printimise.

Programm on funktsioonide konveier. Igaüks saab paketi, milles vastava taseme päis on esile tõstetud, näiteks process_ether() loodab, et see täidetakse ether. Väljaanalüüsi tulemuste põhjal saab funktsioon edastada paketi kõrgemale tasemele. Funktsiooni tulemus on XDP toiming. Praegu edastavad SYN-i ja ACK-i töötlejad kõik paketid.

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

Juhin teie tähelepanu kontrollidele, mis on märgitud A ja B. Kui kommenteerite A, siis programm ehitab, kuid laadimisel ilmneb kinnitusviga:

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!

Võtmepael invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0): on täitmisteed, kui kolmeteistkümnes bait puhvri algusest on väljaspool paketti. Loendist on raske aru saada, millisest reast me räägime, kuid seal on juhise number (12) ja demonteerija, mis näitab lähtekoodi ridu:

llvm-objdump -S xdp_filter.o | less

Sel juhul osutab see joonele

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

mis teeb selgeks, et probleem on ether. See oleks alati nii.

Vasta SYN-ile

Eesmärk selles etapis on genereerida õige SYNACK-pakett fikseeritud parameetriga seqnum, mis asendatakse tulevikus SYN-i küpsisega. Kõik muudatused toimuvad sisse process_tcp_syn() ja ümbritsevad alad.

Paki kontrollimine

Kummalisel kombel on siin kõige tähelepanuväärsem rida või õigemini selle kommentaar:

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

Koodi esimese versiooni kirjutamisel kasutati 5.1 kernelit, mille kontrollimisel oli erinevus data_end и (const void*)ctx->data_end. Kirjutamise ajal kernel 5.3.1 seda probleemi ei olnud. Võimalik, et kompilaator pääses kohalikule muutujale juurde teisiti kui väljale. Loo moraal: koodi lihtsustamine võib aidata, kui pesitsemist on palju.

Järgmiseks on rutiinsed pikkuse kontrollid tõendaja hiilguse tagamiseks; O MAX_CSUM_BYTES allpool.

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

Pakendi lahti voltimine

Täidame seqnum и acknum, määrake ACK (SYN on juba määratud):

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

Vahetage TCP-porte, IP-aadressi ja MAC-aadresse. Standardne raamatukogu pole XDP-programmist juurdepääsetav, seega memcpy() - makro, mis peidab Clangi olemusi.

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

Kontrollsummade ümberarvutamine

IPv4 ja TCP kontrollsummad nõuavad päistesse kõigi 16-bitiste sõnade lisamist ning päiste suurus kirjutatakse neisse ehk kompileerimise ajal teadmata. See on probleem, kuna kontrollija ei jäta tavalist tsüklit piirmuutujale vahele. Kuid päiste suurus on piiratud: igaüks kuni 64 baiti. Saate teha kindla arvu iteratsioonidega tsükli, mis võib varakult lõppeda.

Märgin, et on olemas RFC 1624 selle kohta, kuidas kontrollsummat osaliselt ümber arvutada, kui muudetakse ainult pakettide fikseeritud sõnu. Kuid meetod ei ole universaalne ja selle rakendamist oleks raskem säilitada.

Kontrollsumma arvutamise funktsioon:

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

Kuigi size kutsumiskoodiga kontrollitud, on teine ​​väljumistingimus vajalik selleks, et kinnitaja saaks tõestada tsükli lõpetamist.

32-bitiste sõnade jaoks on rakendatud lihtsam versioon:

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

Tegelikult kontrollsummade ümberarvutamine ja paketi tagasisaatmine:

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;

Funktsioon carry() teeb kontrollsumma 32-bitiste sõnade 16-bitisest summast vastavalt RFC 791-le.

TCP käepigistuse kontrollimine

Filter loob õigesti ühenduse netcat, puudub lõplik ACK, millele Linux vastas RST-paketiga, kuna võrgupinn ei saanud SYN-i - see teisendati SYNACK-iks ja saadeti tagasi - ja OS-i seisukohast saabus pakett, mis ei olnud seotud avamisega. ühendused.

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

Oluline on kontrollida täisväärtuslike rakendustega ja jälgida tcpdump edasi xdp-remote sest näiteks hping3 ei reageeri valedele kontrollsummadele.

XDP seisukohast on kontrollimine iseenesest triviaalne. Arvutusalgoritm on primitiivne ja kogenud ründaja suhtes tõenäoliselt haavatav. Näiteks Linuxi tuum kasutab krüptograafilist SipHashi, kuid selle rakendamine XDP jaoks jääb selgelt selle artikli raamidest välja.

Tutvustatakse uute väliskommunikatsiooniga seotud ülesannete jaoks:

  • XDP programm ei saa salvestada cookie_seed (soola salajane osa) globaalses muutujas, vajate tuumas salvestusruumi, mille väärtust uuendatakse perioodiliselt usaldusväärsest generaatorist.

  • Kui SYN-küpsis kattub ACK-paketis, ei pea te sõnumit printima, vaid meeles pidama kontrollitud kliendi IP-aadressi, et jätkata sealt pakettide edastamist.

Õigustatud kliendi kinnitus:

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

Logid näitavad, et kontroll läbis (flags=0x2 - see on SYN, flags=0x10 on 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

Kuigi kinnitatud IP-de loendit pole, pole SYN-i üleujutuse enda eest kaitset, kuid siin on reaktsioon järgmise käsuga käivitatud ACK-uputusele:

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

Logi sissekanded:

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

Järeldus

Mõnikord esitletakse eBPF-i üldiselt ja eriti XDP-d rohkem kui täiustatud administraatori tööriista kui arendusplatvormi. Tõepoolest, XDP on tööriist, mis häirib pakettide töötlemist kerneli poolt, mitte alternatiiv kerneli virnale, nagu DPDK ja muud kerneli möödaviiguvalikud. Teisest küljest võimaldab XDP rakendada üsna keerulist loogikat, mida on pealegi lihtne uuendada ilma liikluse töötlust segamata. Tõendaja ei tekita suuri probleeme; isiklikult ma ei keelduks sellest kasutajaruumi koodi osade puhul.

Teises osas, kui teema pakub huvi, täidame kontrollitud klientide ja katkestuste tabeli, juurutame loendurid ja kirjutame filtri haldamiseks kasutajaruumi utiliidi.

Lingid:

Allikas: www.habr.com

Lisa kommentaar