XDP rašome apsaugą nuo DDoS atakų. Branduolinė dalis

„eXpress Data Path“ (XDP) technologija leidžia savavališkai apdoroti srautą „Linux“ sąsajose prieš paketams patenkant į branduolio tinklo krūvą. XDP taikymas - apsauga nuo DDoS atakų (CloudFlare), sudėtingi filtrai, statistikos rinkimas (Netflix). XDP programas vykdo virtualioji mašina eBPF, todėl joms taikomi apribojimai ir jų kodui, ir galimoms branduolio funkcijoms, atsižvelgiant į filtro tipą.

Straipsnis skirtas kompensuoti daugelio XDP medžiagų trūkumus. Pirma, jie pateikia paruoštą kodą, kuris iš karto apeina XDP funkcijas: paruoštas patikrinimui arba per paprastas, kad kiltų problemų. Kai vėliau bandote parašyti savo kodą nuo nulio, nesuprantate, ką daryti su tipinėmis klaidomis. Antra, jis neapima būdų, kaip vietoje išbandyti XDP be VM ir aparatinės įrangos, nepaisant to, kad jie turi savų spąstų. Tekstas skirtas programuotojams, susipažinusiems su tinklais ir Linux, kurie domisi XDP ir eBPF.

Šioje dalyje mes išsamiai suprasime, kaip yra surenkamas XDP filtras ir kaip jį išbandyti, tada parašysime paprastą visiems žinomo SYN slapukų mechanizmo versiją paketų apdorojimo lygyje. Kol sudarysime „baltąjį sąrašą“
patikrintus klientus, vesti skaitiklius ir tvarkyti filtrą – pakankamai žurnalų.

Rašysime C – tai ne madinga, bet praktiška. Visas kodas yra prieinamas GitHub pabaigoje esančioje nuorodoje ir yra padalintas į įsipareigojimus pagal straipsnyje aprašytus veiksmus.

Atsakomybės apribojimas. Straipsnio eigoje bus sukurtas mini sprendimas, kaip atremti DDoS atakas, nes tai yra realus XDP ir mano srities uždavinys. Tačiau pagrindinis tikslas yra suprasti technologiją, tai nėra vadovas, kaip sukurti paruoštą apsaugą. Mokymo kodas nėra optimizuotas ir praleidžia kai kuriuos niuansus.

Trumpa XDP apžvalga

Pateiksiu tik pagrindinius dalykus, kad nedubliuotų dokumentacija ir esami straipsniai.

Taigi, filtro kodas įkeliamas į branduolį. Filtras perduoda gaunamus paketus. Dėl to filtras turi priimti sprendimą: perduoti paketą branduoliui (XDP_PASS), mesti paketą (XDP_DROP) arba atsiųskite atgal (XDP_TX). Filtras gali pakeisti pakuotę, tai ypač pasakytina apie XDP_TX. Taip pat galite sulaužyti programą (XDP_ABORTED) ir numeskite pakuotę, bet tai yra analogiška assert(0) - derinimui.

eBPF (Extended Berkley Packet Filter) virtualioji mašina yra sąmoningai supaprastinta, kad branduolys galėtų patikrinti, ar kodas nepersiciklina ir nepažeidžia kitų žmonių atminties. Kaupiami apribojimai ir patikrinimai:

  • Kilpos (šokimai atgal) draudžiamos.
  • Yra duomenų krūva, bet nėra funkcijų (visos C funkcijos turi būti įtrauktos).
  • Prieiga prie atminties už dėklo ir paketų buferio ribų yra draudžiama.
  • Kodo dydis yra ribotas, tačiau praktiškai tai nėra labai reikšminga.
  • Leidžiamos tik specialios branduolio funkcijos (eBPF pagalbininkai).

Filtro kūrimas ir įdiegimas atrodo taip:

  1. šaltinio kodas (pvz. kernel.c) kompiliuoja į objektą (kernel.o) eBPF virtualios mašinos architektūrai. Nuo 2019 m. spalio mėn. kompiliavimą į eBPF palaiko Clang ir žada GCC 10.1.
  2. Jei šiame objekto kode yra iškvietimai į branduolio struktūras (pavyzdžiui, į lenteles ir skaitiklius), vietoj jų ID yra nuliai, tai yra, toks kodas negali būti vykdomas. Prieš įkeliant į branduolį, šie nuliai turi būti pakeisti konkrečių objektų, sukurtų branduolio iškvietimu, ID (susieti kodą). Tai galite padaryti naudodami išorines programas arba galite parašyti programą, kuri susies ir įkels konkretų filtrą.
  3. Branduolys patikrina įkeliamą programą. Ji patikrina, ar nėra ciklų ir ar neišeina iš pakuotės ir krūvos ribų. Jei tikrintojas negali įrodyti, kad kodas teisingas, programa atmetama – reikia mokėti jam įtikti.
  4. Po sėkmingo patikrinimo branduolys sukompiliuoja eBPF architektūros objekto kodą į sistemos architektūros mašinos kodą (laiku).
  5. Programa yra prijungta prie sąsajos ir pradeda apdoroti paketus.

Kadangi XDP veikia branduolyje, derinimas pagrįstas sekimo žurnalais ir, tiesą sakant, paketais, kuriuos programa filtruoja arba generuoja. Tačiau eBPF saugo atsisiųstą kodą sistemoje, todėl galite eksperimentuoti su XDP tiesiog savo vietinėje Linux sistemoje.

Aplinkos paruošimas

Asamblėja

Clang negali tiesiogiai išduoti objekto kodo eBPF architektūrai, todėl procesas susideda iš dviejų žingsnių:

  1. Sukompiliuokite C kodą į LLVM baito kodą (clang -emit-llvm).
  2. Konvertuoti baitinį kodą į eBPF objekto kodą (llc -march=bpf -filetype=obj).

Rašant filtrą, pravers pora failų su pagalbinėmis funkcijomis ir makrokomandomis iš branduolio testų. Svarbu, kad jie atitiktų branduolio versiją (KVER). Atsisiųskite juos į 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, skirtas Arch Linux (branduolis 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 yra kelias į branduolio antraštes, ARCH - architektūros sistema. Skirtinguose platinimuose keliai ir įrankiai gali šiek tiek skirtis.

„Debian 10“ skirtumų pavyzdys (branduolis 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 apima katalogą su pagalbinėmis antraštėmis ir kelis katalogus su branduolio antraštėmis. Simbolis __KERNEL__ reiškia, kad UAPI (vartotojo erdvės API) antraštės yra apibrėžtos branduolio kodui, nes filtras vykdomas branduolyje.

Krūvos apsauga gali būti išjungta (-fno-stack-protector), nes eBPF kodo tikrintuvas vis tiek tikrina, ar nėra krūvos ribų. Turėtumėte nedelsiant įjungti optimizavimą, nes eBPF baito kodo dydis yra ribotas.

Pradėkime nuo filtro, kuris perduoda visus paketus ir nieko nedaro:

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

Komanda make renka xdp_filter.o. Kur dabar galima išbandyti?

Bandymo stendas

Stovas turi turėti dvi sąsajas: ant kurios bus filtras ir iš kurios bus siunčiami paketai. Tai turi būti pilni Linux įrenginiai su savo IP, kad būtų galima patikrinti, kaip įprastos programos veikia su mūsų filtru.

Mums tinka tokie įrenginiai kaip veth (virtualusis Ethernet): tai pora virtualių tinklo sąsajų, „sujungtų“ tiesiogiai viena su kita. Galite juos sukurti taip (šiame skyriuje visos komandos ip atliekama nuo root):

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

Čia xdp-remote и xdp-local — įrenginių pavadinimai. Įjungta xdp-local (192.0.2.1/24) bus pritvirtintas filtras, su xdp-remote (192.0.2.2/24) bus siunčiamas įeinantis srautas. Tačiau yra problema: sąsajos yra tame pačiame kompiuteryje, o „Linux“ nesiųs srauto į vieną iš jų per kitą. Galite tai išspręsti naudodami sudėtingas taisykles iptables, tačiau jiems teks keisti paketus, o tai nepatogu derinant. Geriau naudoti tinklo vardų sritis (tinklo vardų sritis, toliau netns).

Tinklo vardų erdvėje yra sąsajų, maršruto parinkimo lentelių ir „NetFilter“ taisyklių rinkinys, kurie yra atskirti nuo panašių objektų kituose netns. Kiekvienas procesas vyksta tam tikroje vardų erdvėje ir jam pasiekiami tik šio tinklo objektai. Pagal numatytuosius nustatymus sistema turi vieną tinklo vardų erdvę visiems objektams, todėl galite dirbti su Linux ir nežinoti apie netns.

Sukurkime naują vardų erdvę xdp-test ir persikelti ten xdp-remote.

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

Tada vyksta procesas xdp-test, "nepamatys" xdp-local (pagal nutylėjimą jis liks netns) ir siunčiant paketą į 192.0.2.1 jis bus perduotas xdp-remote, nes tai yra vienintelė 192.0.2.0/24 sąsaja, prieinama šiam procesui. Tai taip pat veikia atvirkščiai.

Judant tarp netn sąsaja nusileidžia ir praranda adresą. Norėdami nustatyti sąsają netns, turite paleisti ip ... šioje komandų vardų erdvėje 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

Kaip matote, tai niekuo nesiskiria nuo nustatymo xdp-local numatytojoje vardų srityje:

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

Jei bėgsi tcpdump -tnevi xdp-local, matote, kad paketai siunčiami iš xdp-test, pristatomi į šią sąsają:

ip netns exec xdp-test   ping 192.0.2.1

Patogu įleisti apvalkalą xdp-test. Saugykloje yra scenarijus, kuris automatizuoja darbą su stovu, pvz., stendą galite nustatyti su komanda sudo ./stand up ir pašalinkite jį sudo ./stand down.

sekimas

Filtras prie įrenginio pritvirtinamas taip:

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

Raktas -force reikia susieti naują programą, jei jau susieta kita. „Jokios naujienos yra geros naujienos“ nėra apie šią komandą, bet kokiu atveju išvestis yra didelė. nurodyti verbose neprivaloma, tačiau kartu pasirodo ataskaita apie kodo tikrintojo darbą su surinkėjo sąrašu:

Verifier analysis:

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

Atjunkite programą nuo sąsajos:

ip link set dev xdp-local xdp off

Scenarijuje tai yra komandos sudo ./stand attach и sudo ./stand detach.

Pririšę filtrą galite tuo įsitikinti ping ir toliau veikia, bet ar programa veikia? Pridėkime logotipus. Funkcija bpf_trace_printk() panašus į printf(), bet palaiko tik iki trijų argumentų, išskyrus šabloną, ir ribotą specifikacijų sąrašą. Makro bpf_printk() supaprastina skambutį.

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

Išvestis eina į branduolio sekimo kanalą, kurį reikia įjungti:

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

Žiūrėti pranešimų srautą:

cat /sys/kernel/debug/tracing/trace_pipe

Abi šios komandos skambina sudo ./stand log.

Ping dabar turėtų pateikti tokius pranešimus:

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

Jei atidžiai pažvelgsite į tikrintojo išvestį, galite pastebėti keistus skaičiavimus:

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

Faktas yra tas, kad eBPF programose nėra duomenų skyriaus, todėl vienintelis būdas užkoduoti formato eilutę yra tiesioginiai VM komandų argumentai:

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

Dėl šios priežasties derinimo išvestis labai išpučia gautą kodą.

XDP paketų siuntimas

Pakeiskime filtrą: tegul jis siunčia visus įeinančius paketus atgal. Tinklo požiūriu tai neteisinga, nes reikėtų keisti adresus antraštėse, bet dabar darbas iš esmės svarbus.

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

Paleisti tcpdump apie xdp-remote. Jis turėtų rodyti identišką siunčiamą ir gaunamą ICMP aido užklausą ir nustoti rodyti ICMP aido atsakymą. Bet nerodo. Pasirodo, veikia XDP_TX programoje, skirtoje xdp-local turisusieti sąsają xdp-remote taip pat buvo priskirta programa, net jei ji buvo tuščia, ir ji buvo iškelta.

Kaip aš sužinojau?

Paketo kelio sekimas branduolyje Perf įvykių mechanizmas, beje, leidžia naudoti tą pačią virtualią mašiną, tai yra, eBPF naudojamas išmontuoti su eBPF.

Iš blogio reikia daryti gėrį, nes nieko daugiau iš to padaryti nereikia.

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

Kas yra kodas 6?

$ errno 6
ENXIO 6 No such device or address

Funkcija veth_xdp_flush_bq() gauna klaidos kodą iš veth_xdp_xmit(), kur ieškoti pagal ENXIO ir rasti komentarą.

Atkurkite minimalų filtrą (XDP_PASS) faile xdp_dummy.c, pridėkite jį prie Makefile, susiekite su xdp-remote:

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

dabar tcpdump parodo, ko tikimasi:

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

Jei vietoj to rodomas tik ARP, turite pašalinti filtrus (tai daro sudo ./stand detach), leisti ping, tada įdiekite filtrus ir bandykite dar kartą. Problema ta, kad filtras XDP_TX paveikia ir ARP, o jei kaminas
vardų erdvės xdp-test sugebėjo "pamiršti" MAC adresą 192.0.2.1, jis negalės išspręsti šio IP.

Problemos teiginys

Pereikime prie nurodytos užduoties: parašyti SYN slapukų mechanizmą XDP.

Iki šiol SYN potvynis išlieka populiari DDoS ataka, kurios esmė tokia. Kai užmezgamas ryšys (TCP rankos paspaudimas), serveris gauna SYN, paskirsto resursus būsimam ryšiui, atsako SYNACK paketu ir laukia ACK. Užpuolikas tiesiog siunčia SYN paketus iš netikrų adresų tūkstančiais per sekundę iš kiekvieno pagrindinio kompiuterio kelių tūkstančių robotų tinkle. Serveris yra priverstas paskirstyti resursus iš karto po paketo atėjimo, tačiau po ilgo laiko jį atleidžia, dėl to išsenka atmintis ar limitai, nepriimami nauji ryšiai, paslauga nepasiekiama.

Jei neskiriate išteklių SYN paketui, o atsakote tik SYNACK paketu, kaip serveris gali suprasti, kad vėliau atėjęs ACK paketas priklauso SYN paketui, kuris nebuvo išsaugotas? Juk užpuolikas taip pat gali generuoti netikrus ACK. SYN slapuko esmė yra užkoduoti seqnum ryšio parametrus kaip adresų, prievadų ir besikeičiančios druskos maišą. Jei ACK pavyko gauti iki druskos keitimo, galite dar kartą apskaičiuoti maišą ir palyginti su acknum. netikras acknum užpuolikas negali, nes druska apima paslaptį, ir neturės laiko jos rūšiuoti dėl riboto kanalo.

SYN slapukai „Linux“ branduolyje buvo įdiegti ilgą laiką ir netgi gali būti automatiškai įjungti, jei SYN gaunami per greitai ir masiškai.

Mokomoji programa apie TCP rankos paspaudimą

TCP suteikia duomenų perdavimą kaip baitų srautą, pavyzdžiui, HTTP užklausos perduodamos per TCP. Srautas perduodamas gabalas po gabalo paketais. Visi TCP paketai turi logines vėliavėles ir 32 bitų eilės numerius:

  • Vėliavos derinys apibrėžia konkretaus paketo vaidmenį. SYN vėliavėlė reiškia, kad tai yra pirmasis siuntėjo ryšio paketas. ACK vėliavėlė reiškia, kad siuntėjas gavo visus ryšio duomenis iki baito. acknum. Paketas gali turėti keletą vėliavėlių ir pavadintas pagal jų derinį, pavyzdžiui, SYNACK paketas.

  • Sekos numeris (seqnum) nurodo pirmojo baito, kuris siunčiamas šiame pakete, poslinkį duomenų sraute. Pavyzdžiui, jei pirmame pakete su X baitų duomenų šis skaičius buvo N, kitame pakete su naujais duomenimis jis bus N+X. Ryšio pradžioje kiekviena šalis šį skaičių pasirenka atsitiktinai.

  • Patvirtinimo numeris (acknum) – toks pat poslinkis kaip ir seqnum, tačiau jis nustato ne perduodamo baito numerį, o pirmojo baito iš gavėjo numerį, kurio siuntėjas nematė.

Ryšio pradžioje šalys turi susitarti seqnum и acknum. Klientas kartu su juo siunčia SYN paketą seqnum = X. Serveris atsako SYNACK paketu, kur įrašo savo seqnum = Y ir atskleidžia acknum = X + 1. Klientas į SYNACK atsako ACK paketu, kur seqnum = X + 1, acknum = Y + 1. Po to prasideda tikrasis duomenų perdavimas.

Jei pašnekovas nepatvirtina paketo gavimo, TCP jį iš naujo siunčia pasibaigus laikui.

Kodėl SYN slapukai ne visada naudojami?

Pirma, praradus SYNACK arba ACK, turėsite palaukti pakartotinio siuntimo – ryšio užmezgimas sulėtėja. Antra, SYN pakete – ir tik jame! - perduodama daugybė parinkčių, kurios turi įtakos tolesniam ryšio veikimui. Neatsimindamas gaunamų SYN paketų, serveris nepaiso šių parinkčių, kituose paketuose klientas jų nebesiųs. Šiuo atveju TCP gali veikti, tačiau bent jau pradiniame etape ryšio kokybė sumažės.

Kalbant apie paketus, XDP programa turėtų atlikti šiuos veiksmus:

  • atsakyti į SYN su SYNACK su slapuku;
  • atsakyti ACK su RST (nutraukti ryšį);
  • mesti kitus paketus.

Algoritmo pseudokodas kartu su paketų analize:

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

Vienas (*) pažymėti taškai, kuriuose reikia valdyti sistemos būseną – pirmame etape galite išsiversti be jų tiesiog įgyvendindami TCP rankos paspaudimą sugeneruodami SYN slapuką kaip seką.

Vietoje (**), kol neturime stalo, paketą praleisime.

TCP rankų paspaudimo įgyvendinimas

Paketo analizė ir kodo patikrinimas

Mums reikia tinklo antraštės struktūrų: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) ir TCP (uapi/linux/tcp.h). Paskutinis negalėjau prisijungti dėl klaidų, susijusių su atomic64_t, turėjau nukopijuoti reikiamus apibrėžimus į kodą.

Visos funkcijos, kurios yra išskirtos C, kad būtų galima skaityti, turi būti įtrauktos į iškvietimo vietą, nes eBPF tikrintuvas branduolyje draudžia šuolius atgal, tai yra, kilpas ir funkcijų iškvietimus.

#define INTERNAL static __attribute__((always_inline))

Makro LOG() išjungia spausdinimą leidimo versijoje.

Programa yra funkcijų rinkinys. Kiekvienas gauna paketą, kuriame paryškinta atitinkamo lygio antraštė, pvz. process_ether() laukia, kol bus užpildytas ether. Remiantis lauko analizės rezultatais, funkcija gali perkelti paketą į aukštesnį lygį. Funkcijos rezultatas yra XDP veiksmas. Nors SYN ir ACK tvarkyklės leidžia visus paketus.

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

Atkreipiu dėmesį į čekius, pažymėtus A ir B. Jei komentuosite A, programa bus sukurta, tačiau įkeliant bus patikrinimo klaida:

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!

Raktų eilutė invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0): yra vykdymo keliai, kai tryliktas baitas nuo buferio pradžios yra už paketo ribų. Iš sąrašo sunku pasakyti, apie kurią eilutę kalbame, tačiau yra instrukcijos numeris (12) ir išmontavimo priemonė, rodanti šaltinio kodo eilutes:

llvm-objdump -S xdp_filter.o | less

Šiuo atveju jis nurodo liniją

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

kuris aiškiai parodo, kad problema yra ether. Visada taip būtų.

Atsakykite į SYN

Šio etapo tikslas yra sugeneruoti teisingą SYNACK paketą su fiksuotu seqnum, kurį ateityje pakeis SYN slapukas. Visi pakeitimai vyksta process_tcp_syn() ir aplinka.

Paketo tikrinimas

Kaip bebūtų keista, čia yra pati nuostabiausia eilutė, tiksliau, jos komentaras:

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

Rašant pirmąją kodo versiją buvo naudojamas 5.1 branduolys, kurio tikrinimui buvo skirtumas tarp data_end и (const void*)ctx->data_end. Rašymo metu 5.3.1 branduolys šios problemos neturėjo. Galbūt kompiliatorius vietinį kintamąjį pasiekė kitaip nei lauką. Moralas – dideliame lizde gali padėti kodo supaprastinimas.

Tolesni įprastiniai ilgio patikrinimai – tikrintojo šlovė; O MAX_CSUM_BYTES žemiau.

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

Pakuotės plitimas

Mes užpildome seqnum и acknum, nustatyti ACK (SYN jau nustatyta):

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

Sukeiskite TCP prievadus, IP ir MAC adresus. Standartinė biblioteka nepasiekiama iš XDP programos, todėl memcpy() - makrokomandą, kuri slepia Clang intrinsik.

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

Kontrolinės sumos perskaičiavimas

IPv4 ir TCP kontrolinės sumos reikalauja antraštėse pridėti visus 16 bitų žodžius, o antraščių dydis yra parašytas jose, tai yra, kompiliavimo metu nežinoma. Tai problema, nes tikrintuvas nepraleis įprasto ciklo iki ribinio kintamojo. Tačiau antraščių dydis yra ribotas: kiekviena iki 64 baitų. Galite sukurti kilpą su fiksuotu iteracijų skaičiumi, kuri gali baigtis anksti.

Atkreipiu dėmesį, kad yra RFC 1624 apie tai, kaip iš dalies perskaičiuoti kontrolinę sumą, jei keičiami tik fiksuoti paketų žodžiai. Tačiau metodas nėra universalus, o jį įgyvendinti būtų sunkiau.

Kontrolinės sumos skaičiavimo funkcija:

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

Nors size patikrinta pagal iškvietimo kodą, būtina antroji išėjimo sąlyga, kad tikrintojas galėtų įrodyti ciklo pabaigą.

32 bitų žodžiams įdiegta paprastesnė versija:

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

Iš tikrųjų perskaičiuojame kontrolines sumas ir siunčiame paketą atgal:

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() pagal RFC 32 sudaro kontrolinę sumą iš 16 bitų 791 bitų žodžių sumos.

TCP rankos paspaudimo patikrinimas

Filtras teisingai užmezga ryšį su netcat, praleidžiant galutinį ACK, į kurį Linux atsakė RST paketu, nes tinklo dėklas negavo SYN - jis buvo konvertuotas į SYNACK ir išsiųstas atgal - ir OS požiūriu atėjo paketas, kurio nebuvo susiję su atvirais ryšiais.

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

Svarbu patikrinti su visavertėmis programomis ir stebėti tcpdump apie xdp-remote nes pvz. hping3 nereaguoja į neteisingas kontrolines sumas.

XDP požiūriu pats patikrinimas yra nereikšmingas. Skaičiavimo algoritmas yra primityvus ir tikriausiai pažeidžiamas sudėtingo užpuoliko. Pavyzdžiui, „Linux“ branduolys naudoja kriptografinį „SipHash“, tačiau jo įgyvendinimas XDP aiškiai nepatenka į šio straipsnio taikymo sritį.

Atsirado naujiems TODO, susijusiems su išorine sąveika:

  • XDP programa negali saugoti cookie_seed (slaptoji druskos dalis) globaliame kintamajame, jums reikia branduolio parduotuvės, kurios vertė bus periodiškai atnaujinama iš patikimo generatoriaus.

  • Jei SYN slapukas ACK pakete sutampa, jums nereikia spausdinti pranešimo, bet atsiminti patvirtinto kliento IP, kad toliau praleistumėte paketus iš jo.

Teisėto kliento patvirtinimas:

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

Žurnaluose buvo užfiksuotas patikrinimo praėjimas (flags=0x2 yra SYN, flags=0x10 yra 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

Kol nėra patikrintų IP sąrašo, nebus jokios apsaugos nuo paties SYN potvynio, bet štai reakcija į ACK potvynį, paleistą šia komanda:

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

Žurnalo įrašai:

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

išvada

Kartais eBPF apskritai ir ypač XDP pristatomi kaip pažangus administratoriaus įrankis nei kūrimo platforma. Iš tiesų, XDP yra įrankis, trukdantis branduolio paketų apdorojimui, o ne alternatyva branduolio kaminui, kaip DPDK ir kitos branduolio apėjimo parinktys. Kita vertus, XDP leidžia įgyvendinti gana sudėtingą logiką, kurią, be to, lengva atnaujinti be pauzės srauto apdorojime. Tikriklis didelių problemų nesukelia, asmeniškai aš tokių neatsisakyčiau vartotojo erdvės kodo dalims.

Antroje dalyje, jei tema bus įdomi, užpildysime patikrintų klientų lentelę ir nutrauksime ryšius, įdiegsime skaitiklius ir parašysime vartotojo erdvės įrankį filtrui valdyti.

Nuorodos:

Šaltinis: www.habr.com

Добавить комментарий