Ne shkruajmë mbrojtje kundër sulmeve DDoS në XDP. Pjesa bërthamore

Teknologjia eXpress Data Path (XDP) lejon përpunimin arbitrar të trafikut në ndërfaqet Linux përpara se paketat të hyjnë në pirgun e rrjetit të kernelit. Aplikimi i XDP - mbrojtje kundër sulmeve DDoS (CloudFlare), filtra komplekse, grumbullimi i statistikave (Netflix). Programet XDP ekzekutohen nga makina virtuale eBPF, dhe për këtë arsye kanë kufizime si në kodin e tyre ashtu edhe në funksionet e disponueshme të kernelit, në varësi të llojit të filtrit.

Artikulli synon të plotësojë mangësitë e materialeve të shumta në XDP. Së pari, ata ofrojnë kod të gatshëm që anashkalon menjëherë veçoritë e XDP: i përgatitur për verifikim ose shumë i thjeshtë për të shkaktuar probleme. Kur përpiqeni të shkruani kodin tuaj nga e para më vonë, nuk kuptoni se çfarë të bëni me gabimet tipike. Së dyti, nuk mbulon mënyrat për të testuar XDP në nivel lokal pa një VM dhe pajisje, pavarësisht nga fakti se ato kanë grackat e tyre. Teksti është menduar për programuesit e njohur me rrjetet dhe Linux të cilët janë të interesuar në XDP dhe eBPF.

Në këtë pjesë, ne do të kuptojmë në detaje se si është montuar filtri XDP dhe si ta testojmë atë, më pas do të shkruajmë një version të thjeshtë të mekanizmit të njohur të cookies SYN në nivelin e përpunimit të paketave. Derisa të formojmë një "listë të bardhë"
klientë të verifikuar, mbani numërues dhe menaxhoni filtrin - regjistrat e mjaftueshëm.

Ne do të shkruajmë në C - kjo nuk është në modë, por praktike. I gjithë kodi është i disponueshëm në GitHub në lidhjen në fund dhe ndahet në angazhime sipas hapave të përshkruar në artikull.

Disclaimer. Në rrjedhën e artikullit, do të zhvillohet një mini-zgjidhje për zmbrapsjen e sulmeve DDoS, sepse kjo është një detyrë realiste për XDP dhe zonën time. Sidoqoftë, qëllimi kryesor është të kuptojmë teknologjinë, ky nuk është një udhëzues për krijimin e mbrojtjes së gatshme. Kodi i tutorialit nuk është i optimizuar dhe lë disa nuanca.

Një përmbledhje e shkurtër e XDP

Do të deklaroj vetëm pikat kyçe për të mos dublikuar dokumentacionin dhe artikujt ekzistues.

Pra, kodi i filtrit ngarkohet në kernel. Filtri i kalohet paketave hyrëse. Si rezultat, filtri duhet të marrë një vendim: të kalojë paketën në kernel (XDP_PASS), lësho paketën (XDP_DROP) ose kthejeni atë (XDP_TX). Filtri mund të ndryshojë paketimin, kjo është veçanërisht e vërtetë për XDP_TX. Ju gjithashtu mund të prishni programin (XDP_ABORTED) dhe hidhni paketën, por kjo është analoge assert(0) - për korrigjimin e gabimeve.

Makina virtuale eBPF (Exended Berkley Packet Filter) është bërë qëllimisht e thjeshtë në mënyrë që kerneli të kontrollojë që kodi të mos qarkullojë dhe të mos dëmtojë kujtesën e njerëzve të tjerë. Kufizimet dhe kontrollet kumulative:

  • Sythet (kërcimet mbrapa) janë të ndaluara.
  • Ekziston një pirg për të dhëna, por nuk ka funksione (të gjitha funksionet C duhet të jenë të rreshtuara).
  • Qasjet në memorie jashtë stakut dhe buferit të paketave janë të ndaluara.
  • Madhësia e kodit është e kufizuar, por në praktikë kjo nuk është shumë domethënëse.
  • Lejohen vetëm funksione speciale të kernelit (ndihmësit eBPF).

Zhvillimi dhe instalimi i një filtri duket si ky:

  1. kodi burimor (p.sh. kernel.c) përpilon në objekt (kernel.o) për arkitekturën e makinës virtuale eBPF. Që nga tetori 2019, përpilimi në eBPF mbështetet nga Clang dhe premtohet në GCC 10.1.
  2. Nëse në këtë kod objekti ka thirrje për strukturat e kernelit (për shembull, në tabela dhe numërues), në vend të ID-ve të tyre ka zero, domethënë, një kod i tillë nuk mund të ekzekutohet. Përpara se të ngarkohen në kernel, këto zero duhet të zëvendësohen me ID-të e objekteve specifike të krijuara përmes thirrjeve të kernelit (lidhni kodin). Ju mund ta bëni këtë me shërbimet e jashtme, ose mund të shkruani një program që do të lidhë dhe ngarkojë një filtër specifik.
  3. Kerneli verifikon programin që po ngarkohet. Ai kontrollon mungesën e cikleve dhe mosdaljen e kufijve të paketës dhe stivës. Nëse verifikuesi nuk mund të provojë se kodi është i saktë, programi refuzohet - dikush duhet të jetë në gjendje ta kënaqë atë.
  4. Pas verifikimit të suksesshëm, kerneli përpilon kodin e objektit të arkitekturës eBPF në kodin e makinës së arkitekturës së sistemit (vetëm në kohë).
  5. Programi është bashkangjitur në ndërfaqe dhe fillon të përpunojë paketat.

Meqenëse XDP funksionon në kernel, korrigjimi bazohet në regjistrat e gjurmëve dhe, në fakt, në paketat që programi filtron ose gjeneron. Sidoqoftë, eBPF e mban kodin e shkarkuar të sigurt për sistemin, kështu që ju mund të eksperimentoni me XDP direkt në Linux-in tuaj lokal.

Përgatitja e Mjedisit

asamble

Clang nuk mund të lëshojë drejtpërdrejt kodin e objektit për arkitekturën eBPF, kështu që procesi përbëhet nga dy hapa:

  1. Përpiloni kodin C në bajtkodin LLVM (clang -emit-llvm).
  2. Konvertoni bajtkodin në kodin e objektit eBPF (llc -march=bpf -filetype=obj).

Kur shkruani një filtër, do të jenë të dobishëm disa skedarë me funksione ndihmëse dhe makro nga testet e kernelit. Është e rëndësishme që ato të përputhen me versionin e kernelit (KVER). Shkarkoni ato në 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 për Arch Linux (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 përmban shtegun për në kokat e kernelit, ARCH - arkitektura e sistemit. Shtigjet dhe mjetet mund të ndryshojnë pak ndërmjet shpërndarjeve.

Shembull i ndryshimit për Debian 10 (kernel 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 përfshini një direktori me tituj ndihmës dhe disa drejtori me tituj kernel. Simboli __KERNEL__ do të thotë që titujt UAPI (userspace API) janë përcaktuar për kodin e kernelit, pasi filtri ekzekutohet në kernel.

Mbrojtja e pirgut mund të çaktivizohet (-fno-stack-protector) sepse verifikuesi i kodit eBPF kontrollon gjithsesi nëse nuk janë jashtë kufijve të pirgut. Duhet të aktivizoni menjëherë optimizimet, sepse madhësia e bajtkodit eBPF është e kufizuar.

Le të fillojmë me një filtër që kalon të gjitha paketat dhe nuk bën asgjë:

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

Ekip make mbledh xdp_filter.o. Ku mund ta provoni tani?

Stand testimi

Stand duhet të përfshijë dy ndërfaqe: në të cilat do të ketë një filtër dhe nga të cilat do të dërgohen paketat. Këto duhet të jenë pajisje të plota Linux me IP-të e tyre në mënyrë që të kontrolloni se si funksionojnë aplikacionet e rregullta me filtrin tonë.

Pajisjet si veth (Ethernet virtual) janë të përshtatshme për ne: ato janë një palë ndërfaqe rrjetesh virtuale "të lidhura" drejtpërdrejt me njëra-tjetrën. Ju mund t'i krijoni ato si kjo (në këtë seksion, të gjitha komandat ip kryer nga root):

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

Këtu xdp-remote и xdp-local — emrat e pajisjeve. Aktiv xdp-local (192.0.2.1/24) do të bashkëngjitet një filtër, me xdp-remote (192.0.2.2/24) do të dërgohet trafiku në hyrje. Sidoqoftë, ekziston një problem: ndërfaqet janë në të njëjtën makinë dhe Linux nuk do të dërgojë trafik tek njëra prej tyre përmes tjetrës. Ju mund ta zgjidhni atë me rregulla të ndërlikuara iptables, por ata do të duhet të ndryshojnë paketat, gjë që është e papërshtatshme gjatë korrigjimit. Është më mirë të përdorni hapësirat e emrave të rrjetit (hapësirat e emrave të rrjetit, rrjetet e mëtejshme).

Hapësira e emrave të rrjetit përmban një grup ndërfaqesh, tabelash rutimi dhe rregulla NetFilter që janë të izoluara nga objekte të ngjashme në rrjeta të tjera. Çdo proces funksionon në një hapësirë ​​emri dhe vetëm objektet e këtij rrjeti janë të disponueshme për të. Si parazgjedhje, sistemi ka një hapësirë ​​të vetme emri rrjeti për të gjitha objektet, kështu që ju mund të punoni në Linux dhe të mos dini për rrjetet.

Le të krijojmë një hapësirë ​​të re emri xdp-test dhe lëvizni atje xdp-remote.

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

Pastaj procesi fillon xdp-test, nuk do të "shohë" xdp-local (do të mbetet në rrjeta si parazgjedhje) dhe kur dërgoni një paketë në 192.0.2.1 do ta kalojë atë përmes xdp-remote, sepse kjo është e vetmja ndërfaqe në 192.0.2.0/24 e disponueshme për këtë proces. Kjo gjithashtu funksionon në të kundërt.

Kur lëvizni midis rrjetave, ndërfaqja zbret dhe humbet adresën. Për të vendosur një ndërfaqe në rrjeta, duhet të ekzekutoni ip ... në hapësirën e emrave të kësaj komande 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

Siç mund ta shihni, kjo nuk ndryshon nga vendosja xdp-local në hapësirën e emrave të paracaktuar:

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

Nëse vraponi tcpdump -tnevi xdp-local, ju mund të shihni se paketat janë dërguar nga xdp-test, dorëzohen në këtë ndërfaqe:

ip netns exec xdp-test   ping 192.0.2.1

Është i përshtatshëm për të futur një predhë brenda xdp-test. Depoja ka një skript që automatizon punën me stendën, për shembull, mund ta konfiguroni stendën me komandën sudo ./stand up dhe hiqeni atë sudo ./stand down.

gjurmimi

Filtri është ngjitur në pajisje si kjo:

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

Ключ -force nevojiten për të lidhur një program të ri nëse një tjetër është tashmë i lidhur. "Asnjë lajm nuk është lajm i mirë" nuk ka të bëjë me këtë komandë, gjithsesi prodhimi është voluminoz. tregojnë verbose opsionale, por me të shfaqet një raport në lidhje me punën e verifikuesit të kodit me listën e asemblerit:

Verifier analysis:

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

Shkëputni programin nga ndërfaqja:

ip link set dev xdp-local xdp off

Në skenar, këto janë komandat sudo ./stand attach и sudo ./stand detach.

Duke lidhur filtrin, mund të siguroheni që ping vazhdon të funksionojë, por a funksionon programi? Le të shtojmë logot. Funksioni bpf_trace_printk() të ngjashme me printf(), por mbështet vetëm deri në tre argumente përveç modelit dhe një listë të kufizuar specifikuesish. Makro bpf_printk() thjeshton thirrjen.

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

Dalja shkon në kanalin e gjurmës së kernelit, i cili duhet të aktivizohet:

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

Shikoni rrjedhën e mesazhit:

cat /sys/kernel/debug/tracing/trace_pipe

Të dyja këto skuadra bëjnë një thirrje sudo ./stand log.

Ping tani duhet të prodhojë mesazhe si kjo në të:

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

Nëse shikoni nga afër daljen e verifikuesit, mund të vini re llogaritjet e çuditshme:

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

Fakti është se programet eBPF nuk kanë një seksion të dhënash, kështu që mënyra e vetme për të koduar vargun e formatit janë argumentet e menjëhershme të komandave VM:

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

Për këtë arsye, dalja e korrigjimit e fryn shumë kodin që rezulton.

Dërgimi i paketave XDP

Le të ndryshojmë filtrin: le të dërgojë të gjitha paketat në hyrje. Kjo është e pasaktë nga pikëpamja e rrjetit, pasi do të ishte e nevojshme të ndryshoni adresat në tituj, por tani puna në parim është e rëndësishme.

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

Nisja tcpdump mbi xdp-remote. Duhet të shfaqë kërkesën identike të ICMP Echo në dalje dhe në hyrje dhe të ndalojë shfaqjen e Përgjigjes së Echo ICMP. Por nuk shfaqet. Rezulton se punon XDP_TX në programin për xdp-local është e nevojshmepër të çiftuar ndërfaqen xdp-remote u caktua gjithashtu një program, edhe nëse ishte bosh, dhe u ngrit.

Si e dija?

Gjurmimi i shtegut të një pakete në kernel mekanizmi i ngjarjeve perf lejon, meqë ra fjala, përdorimin e të njëjtës makinë virtuale, domethënë eBPF përdoret për çmontim me eBPF.

Ju duhet të bëni të mirën nga e keqja, sepse nuk ka asgjë tjetër për të bërë prej saj.

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

Çfarë është kodi 6?

$ errno 6
ENXIO 6 No such device or address

Funksion veth_xdp_flush_bq() merr kodin e gabimit nga veth_xdp_xmit(), ku kërko nga ENXIO dhe gjeni një koment.

Rivendosni filtrin minimal (XDP_PASS) në dosje xdp_dummy.c, shtoni atë në Makefile, lidheni me xdp-remote:

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

Tani tcpdump tregon se çfarë pritet:

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

Nëse në vend të kësaj shfaqet vetëm ARP, ju duhet të hiqni filtrat (kjo bën sudo ./stand detach), le ping, më pas instaloni filtrat dhe provoni përsëri. Problemi është se filtri XDP_TX ndikon edhe ARP-në, dhe nëse stack
hapësirat e emrave xdp-test arriti të "harrojë" adresën MAC 192.0.2.1, ai nuk do të jetë në gjendje ta zgjidhë këtë IP.

Formulimi i problemit

Le të kalojmë te detyra e deklaruar: të shkruajmë një mekanizëm cookie SYN në XDP.

Deri më tani, përmbytja SYN mbetet një sulm popullor DDoS, thelbi i të cilit është si më poshtë. Kur vendoset një lidhje (shtrëngim duarsh TCP), serveri merr një SYN, shpërndan burime për një lidhje të ardhshme, përgjigjet me një paketë SYNACK dhe pret për një ACK. Sulmuesi thjesht dërgon pako SYN nga adresa të rreme në shumën e mijëra në sekondë nga çdo host në një botnet me shumë mijëra. Serveri detyrohet të ndajë burime menjëherë pas mbërritjes së paketës, por e lëshon atë pas një kohe të gjatë, si rezultat, memoria ose kufijtë janë shteruar, lidhjet e reja nuk pranohen, shërbimi është i padisponueshëm.

Nëse nuk ndani burime në paketën SYN, por përgjigjeni vetëm me një paketë SYNACK, atëherë si mund ta kuptojë serveri që paketa ACK që erdhi më vonë i përket paketës SYN që nuk u ruajt? Në fund të fundit, një sulmues mund të gjenerojë gjithashtu ACK të rreme. Thelbi i cookie SYN është të kodohet në seqnum parametrat e lidhjes si një hash i adresave, porteve dhe ndryshimit të kripës. Nëse ACK arriti të arrijë përpara ndryshimit të kripës, mund të llogarisni përsëri hash-in dhe të krahasoni me acknum. e rreme acknum sulmuesi nuk mundet, pasi kripa përfshin sekretin, dhe nuk do të ketë kohë ta zgjidhë atë për shkak të kanalit të kufizuar.

Cookies SYN janë zbatuar në kernelin Linux për një kohë të gjatë dhe madje mund të aktivizohen automatikisht nëse SYN-të arrijnë shumë shpejt dhe në masë.

Program edukativ mbi shtrëngimin e duarve TCP

TCP siguron transferimin e të dhënave si një rrjedhë bajtësh, për shembull, kërkesat HTTP transmetohen përmes TCP. Rrjedha transmetohet pjesë-pjesë në pako. Të gjitha paketat TCP kanë flamuj logjikë dhe numra sekuence 32-bitësh:

  • Kombinimi i flamujve përcakton rolin e një pakete të veçantë. Flamuri SYN do të thotë se kjo është paketa e parë e dërguesit në lidhje. Flamuri ACK do të thotë që dërguesi ka marrë të gjitha të dhënat e lidhjes deri në një bajt. acknum. Një paketë mund të ketë disa flamuj dhe emërtohet sipas kombinimit të tyre, për shembull, një paketë SYNACK.

  • Numri i sekuencës (seqnum) specifikon kompensimin në rrjedhën e të dhënave për bajtin e parë që dërgohet në këtë paketë. Për shembull, nëse në paketën e parë me X bajt të dhënash ky numër ishte N, në paketën tjetër me të dhëna të reja do të jetë N+X. Në fillim të lidhjes, secila palë zgjedh këtë numër në mënyrë të rastësishme.

  • Numri i konfirmimit (acknum) - i njëjti kompensim si seqnum, por nuk përcakton numrin e bajtit të transmetuar, por numrin e bajtit të parë nga marrësi, të cilin dërguesi nuk e pa.

Në fillim të lidhjes, palët duhet të bien dakord seqnum и acknum. Klienti dërgon një paketë SYN me të seqnum = X. Serveri përgjigjet me një paketë SYNACK, ku shkruan të vetën seqnum = Y dhe ekspozon acknum = X + 1. Klienti i përgjigjet SYNACK me një paketë ACK, ku seqnum = X + 1, acknum = Y + 1. Pas kësaj, fillon transferimi aktual i të dhënave.

Nëse bashkëbiseduesi nuk e pranon marrjen e paketës, TCP e ridërgon atë me kohë.

Pse nuk përdoren gjithmonë skedarët SYN?

Së pari, nëse një SYNACK ose ACK humbet, do të duhet të prisni për një ridërgim - vendosja e lidhjes ngadalësohet. Së dyti, në paketën SYN - dhe vetëm në të! - transmetohen një numër opsionesh që ndikojnë në funksionimin e mëtejshëm të lidhjes. Duke mos mbajtur mend paketat hyrëse SYN, serveri i shpërfill këto opsione, në paketat e mëposhtme klienti nuk do t'i dërgojë më ato. TCP mund të funksionojë në këtë rast, por të paktën në fazën fillestare, cilësia e lidhjes do të ulet.

Për sa i përket paketave, një program XDP duhet të bëjë sa më poshtë:

  • përgjigjuni SYN me SYNACK me cookie;
  • përgjigjuni ACK me RST (prisni lidhjen);
  • hidhni pako të tjera.

Pseudokodi i algoritmit së bashku me analizimin e paketave:

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

Një (*) pikat ku ju duhet të menaxhoni gjendjen e sistemit janë shënuar - në fazën e parë, ju mund të bëni pa to thjesht duke zbatuar një shtrëngim duarsh TCP me gjenerimin e një cookie SYN si një seqnum.

Në vend (**), ndërsa nuk kemi tavolinë, do ta kalojmë paketën.

Implementimi i shtrëngimit të duarve të TCP

Parimi i paketës dhe verifikimi i kodit

Ne kemi nevojë për strukturat e kokës së rrjetit: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) dhe TCP (uapi/linux/tcp.h). I fundit nuk munda të lidhem për shkak të gabimeve që lidhen me të atomic64_t, më duhej të kopjoja përkufizimet e nevojshme në kod.

Të gjitha funksionet që dallohen në C për lexueshmëri duhet të jenë të inlinuara në vendin e thirrjes, pasi verifikuesi eBPF në kernel ndalon kërcimet prapa, domethënë, në fakt, unazat dhe thirrjet e funksioneve.

#define INTERNAL static __attribute__((always_inline))

makro LOG() çaktivizon printimin në një version lëshues.

Programi është një tubacion funksionesh. Secili merr një pako në të cilën theksohet një kokë e nivelit përkatës, për shembull, process_ether() në pritje për t'u mbushur ether. Bazuar në rezultatet e analizës në terren, funksioni mund të transferojë paketën në një nivel më të lartë. Rezultati i funksionit është një veprim XDP. Ndërsa mbajtësit SYN dhe ACK i lejojnë të gjitha paketat të kalojnë.

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

I kushtoj vëmendje kontrolleve të shënuara A dhe B. Nëse komentoni A, programi do të ndërtohet, por do të ketë një gabim verifikimi gjatë ngarkimit:

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!

Varg çelësi invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0): ka shtigje ekzekutimi kur bajt i trembëdhjetë nga fillimi i buferit është jashtë paketës. Është e vështirë të dallosh nga lista për cilën linjë po flasim, por ekziston një numër udhëzimi (12) dhe një çmontues që tregon linjat e kodit burimor:

llvm-objdump -S xdp_filter.o | less

Në këtë rast, ajo tregon vijën

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

gjë që e bën të qartë se problemi është ether. Kështu do të ishte gjithmonë.

Përgjigju SYN

Qëllimi në këtë fazë është të gjenerohet një paketë e saktë SYNACK me një fikse seqnum, e cila do të zëvendësohet nga kuki SYN në të ardhmen. Të gjitha ndryshimet ndodhin në process_tcp_syn() dhe rrethinat.

Kontrollimi i paketës

Mjaft e çuditshme, këtu është rreshti më i shquar, ose më mirë, një koment për të:

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

Gjatë shkrimit të versionit të parë të kodit, u përdor kerneli 5.1, për verifikuesin e të cilit kishte një ndryshim midis data_end и (const void*)ctx->data_end. Në kohën e shkrimit, kerneli 5.3.1 nuk e kishte këtë problem. Ndoshta përpiluesi po aksesonte një ndryshore lokale ndryshe nga një fushë. Morali - në një fole të madhe, thjeshtimi i kodit mund të ndihmojë.

Kontrolle të mëtejshme rutinë të gjatësive për lavdinë e verifikuesit; O MAX_CSUM_BYTES më poshtë.

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

Përhapja e paketës

Ne plotësojmë seqnum и acknum, vendosni ACK (SYN është vendosur tashmë):

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

Ndërroni portet TCP, adresat IP dhe MAC. Biblioteka standarde nuk është e disponueshme nga programi XDP, kështu që memcpy() - një makro që fsheh brendësinë e Clang.

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

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

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

Rillogaritja e shumës së kontrollit

Shumat e kontrollit IPv4 dhe TCP kërkojnë shtimin e të gjitha fjalëve 16-bit në kokë, dhe madhësia e titujve është e shkruar në to, domethënë në kohën e përpilimit është e panjohur. Ky është një problem sepse verifikuesi nuk do të kapërcejë ciklin normal deri në variablin kufitar. Por madhësia e titujve është e kufizuar: deri në 64 bajt secila. Ju mund të bëni një lak me një numër të caktuar përsëritjesh, i cili mund të përfundojë herët.

Unë vërej se ka RFC1624 se si të rillogaritet pjesërisht shuma e kontrollit nëse ndryshohen vetëm fjalët fikse të paketave. Megjithatë, metoda nuk është universale dhe zbatimi do të ishte më i vështirë për t'u ruajtur.

Funksioni i llogaritjes së shumës së kontrollit:

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

Edhe pse size kontrolluar nga kodi thirrës, kushti i dytë i daljes është i nevojshëm në mënyrë që verifikuesi të mund të provojë fundin e ciklit.

Për fjalët 32-bit, zbatohet një version më i thjeshtë:

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

Në fakt, rillogaritja e shumave të kontrollit dhe dërgimi i paketës mbrapsht:

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;

Funksion carry() bën një shumë kontrolli nga një shumë 32-bitëshe fjalësh 16-bitëshe, sipas RFC 791.

Kontrolli i shtrëngimit të duarve TCP

Filtri vendos saktë një lidhje me netcat, duke anashkaluar ACK-në përfundimtare, së cilës Linux iu përgjigj me një paketë RST, meqenëse grupi i rrjetit nuk mori një SYN - ai u konvertua në SYNACK dhe u dërgua përsëri - dhe nga pikëpamja e OS, mbërriti një pako që nuk ishte lidhur me lidhjet e hapura.

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

Është e rëndësishme të kontrolloni me aplikacione të plota dhe të vëzhgoni tcpdump mbi xdp-remote sepse, për shembull, hping3 nuk i përgjigjet kontrolleve të pasakta.

Nga pikëpamja e XDP, kontrolli në vetvete është i parëndësishëm. Algoritmi i llogaritjes është primitiv dhe ndoshta i prekshëm ndaj një sulmuesi të sofistikuar. Kerneli Linux, për shembull, përdor SipHash kriptografike, por zbatimi i tij për XDP është qartë përtej qëllimit të këtij artikulli.

U shfaq për TODO të reja që lidhen me ndërveprimin e jashtëm:

  • Programi XDP nuk mund të ruajë cookie_seed (pjesa sekrete e kripës) në një ndryshore globale, ju nevojitet një depo kernel, vlera e të cilit do të përditësohet periodikisht nga një gjenerator i besueshëm.

  • Nëse kuki SYN në paketën ACK përputhet, nuk keni nevojë të printoni një mesazh, por mbani mend IP-në e klientit të verifikuar në mënyrë që të kapërceni më tej paketat prej tij.

Vleresimi nga nje klient legjitim:

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

Regjistrat regjistronin kalimin e kontrollit (flags=0x2 është SYN flags=0x10 është 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

Për sa kohë që nuk ka listë të IP-ve të verifikuara, nuk do të ketë mbrojtje kundër vetë përmbytjes SYN, por këtu është reagimi ndaj përmbytjes ACK të nisur nga kjo komandë:

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

Regjistrimet e regjistrit:

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

Përfundim

Ndonjëherë eBPF në përgjithësi dhe XDP në veçanti paraqiten më shumë si një mjet i avancuar administratori sesa një platformë zhvillimi. Në të vërtetë, XDP është një mjet për të ndërhyrë në përpunimin e paketave të kernelit, dhe jo një alternativë për pirgun e kernelit, si DPDK dhe opsionet e tjera të anashkalimit të kernelit. Nga ana tjetër, XDP ju lejon të zbatoni logjikë mjaft komplekse, e cila, për më tepër, është e lehtë për t'u përditësuar pa një pauzë në përpunimin e trafikut. Verifikuesi nuk krijon probleme të mëdha, personalisht nuk do ta refuzoja këtë për pjesë të kodit të hapësirës së përdoruesit.

Në pjesën e dytë, nëse tema është interesante, ne do të plotësojmë tabelën e klientëve të verifikuar dhe do të ndërpresim lidhjet, do të implementojmë numërues dhe do të shkruajmë një mjet përdoruesi të hapësirës për të menaxhuar filtrin.

referencat:

Burimi: www.habr.com

Shto një koment