Mēs rakstām aizsardzību pret DDoS uzbrukumiem XDP. Kodolenerģijas daļa

eXpress Data Path (XDP) tehnoloÄ£ija ļauj veikt nejauÅ”u trafika apstrādi Linux saskarnēs, pirms paketes nonāk kodola tÄ«kla kaudzē. XDP pielietojums - aizsardzÄ«ba pret DDoS uzbrukumiem (CloudFlare), sarežģīti filtri, statistikas apkopoÅ”ana (Netflix). XDP programmas izpilda eBPF virtuālā maŔīna, tāpēc tām ir ierobežojumi gan kodam, gan pieejamajām kodola funkcijām atkarÄ«bā no filtra veida.

Raksts ir paredzēts, lai aizpildÄ«tu daudzu XDP materiālu trÅ«kumus. Pirmkārt, tie nodroÅ”ina gatavu kodu, kas nekavējoties apiet XDP funkcijas: tas ir sagatavots pārbaudei vai ir pārāk vienkārÅ”s, lai radÄ«tu problēmas. Mēģinot rakstÄ«t kodu no jauna, jums nav ne jausmas, ko darÄ«t ar tipiskām kļūdām. Otrkārt, nav ietverti veidi, kā lokāli pārbaudÄ«t XDP bez virtuālās maŔīnas un aparatÅ«ras, neskatoties uz to, ka tiem ir savas nepilnÄ«bas. Teksts ir paredzēts programmētājiem, kuri pārzina tÄ«klu un Linux, kurus interesē XDP un eBPF.

Å ajā daļā mēs detalizēti sapratÄ«sim, kā tiek salikts XDP filtrs un kā to pārbaudÄ«t, pēc tam uzrakstÄ«sim vienkārÅ”u versiju par labi zināmo SYN sÄ«kfailu mehānismu pakeÅ”u apstrādes lÄ«menÄ«. Mēs vēl neveidosim ā€œbalto sarakstuā€.
verificēti klienti, uzturēt skaitītājus un pārvaldīt filtru - pietiekami daudz žurnālu.

Mēs rakstÄ«sim C ā€” tas nav modē, bet tas ir praktiski. Viss kods ir pieejams GitHub, izmantojot saiti beigās, un ir sadalÄ«ts saistÄ«bās saskaņā ar rakstā aprakstÄ«tajiem posmiem.

Atruna Å Ä« raksta laikā es izstrādāŔu mini risinājumu, lai novērstu DDoS uzbrukumus, jo tas ir reāls XDP un manas kompetences jomas uzdevums. Tomēr galvenais mērÄ·is ir izprast tehnoloÄ£iju; tas nav ceļvedis gatavas aizsardzÄ«bas izveidoÅ”anai. ApmācÄ«bas kods nav optimizēts un izlaiž dažas nianses.

XDP īss pārskats

Es izklāstÄ«Å”u tikai galvenos punktus, lai nedublētos dokumentācija un esoÅ”ie raksti.

Tātad filtra kods tiek ielādēts kodolā. IenākoŔās paketes tiek nodotas filtram. Rezultātā filtram ir jāpieņem lēmums: nodot paketi kodolā (XDP_PASS), nomest paciņu (XDP_DROP) vai nosÅ«tÄ«t atpakaļ (XDP_TX). Filtrs var mainÄ«t iepakojumu, jo Ä«paÅ”i tas attiecas uz XDP_TX. Varat arÄ« pārtraukt programmu (XDP_ABORTED) un atiestatiet pakotni, taču tas ir lÄ«dzÄ«gi assert(0) - atkļūdoÅ”anai.

eBPF (paplaÅ”inātais Berkley pakeÅ”u filtrs) virtuālā maŔīna ir apzināti vienkārÅ”a, lai kodols varētu pārbaudÄ«t, vai kods neveidojas cilpa un nebojā citu cilvēku atmiņu. KumulatÄ«vie ierobežojumi un pārbaudes:

  • Cilpas (atpakaļ) ir aizliegtas.
  • Ir datu steks, bet nav funkciju (visām C funkcijām jābÅ«t iekļautām).
  • Piekļuve atmiņai ārpus steka un pakeÅ”u bufera ir aizliegta.
  • Koda lielums ir ierobežots, taču praksē tas nav Ä«paÅ”i nozÄ«mÄ«gi.
  • Ir atļauti tikai izsaukumi uz Ä«paŔām kodola funkcijām (eBPF palÄ«giem).

Filtra projektÄ“Å”ana un uzstādÄ«Å”ana izskatās Ŕādi:

  1. Avota kods (piem kernel.c) ir apkopots objektā (kernel.o) eBPF virtuālās maŔīnas arhitektÅ«rai. No 2019. gada oktobra kompilāciju eBPF atbalsta Clang un sola GCC 10.1.
  2. Ja Å”is objekta kods satur izsaukumus uz kodola struktÅ«rām (piemēram, tabulām un skaitÄ«tājiem), to ID tiek aizstāti ar nullēm, kas nozÄ«mē, ka Ŕādu kodu nevar izpildÄ«t. Pirms ielādÄ“Å”anas kodolā Ŕīs nulles jāaizstāj ar konkrētu objektu ID, kas izveidoti, izmantojot kodola izsaukumus (saistiet kodu). To var izdarÄ«t, izmantojot ārējās utilÄ«tas, vai arÄ« varat uzrakstÄ«t programmu, kas saistÄ«s un ielādēs noteiktu filtru.
  3. Kodols pārbauda ielādēto programmu. Tiek pārbaudÄ«ts ciklu neesamÄ«ba un nespēja pārsniegt pakeÅ”u un steku robežas. Ja pārbaudÄ«tājs nevar pierādÄ«t, ka kods ir pareizs, programma tiek noraidÄ«ta - jums ir jāspēj viņu iepriecināt.
  4. Pēc veiksmÄ«gas pārbaudes kodols apkopo eBPF arhitektÅ«ras objekta kodu sistēmas arhitektÅ«ras maŔīnkodā (tieÅ”i laikā).
  5. Programma pievienojas saskarnei un sāk apstrādāt pakeŔus.

Tā kā XDP darbojas kodolā, atkļūdoÅ”ana tiek veikta, izmantojot izsekoÅ”anas žurnālus un faktiski paketes, kuras programma filtrē vai Ä£enerē. Tomēr eBPF nodroÅ”ina, ka lejupielādētais kods ir droÅ”s sistēmai, lai jÅ«s varētu eksperimentēt ar XDP tieÅ”i savā vietējā Linux.

Vides sagatavoŔana

Montāža

Clang nevar tieÅ”i radÄ«t objekta kodu eBPF arhitektÅ«rai, tāpēc process sastāv no diviem posmiem:

  1. Kompilējiet C kodu LLVM baitkodā (clang -emit-llvm).
  2. Konvertēt baitkodu par eBPF objekta kodu (llc -march=bpf -filetype=obj).

Rakstot filtru, noderēs pāris faili ar palÄ«gfunkcijām un makro no kodola testiem. Ir svarÄ«gi, lai tie atbilstu kodola versijai (KVER). Lejupielādējiet tos uz 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 operētājsistēmai Arch Linux (kodolu 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 satur ceļu uz kodola galvenēm, ARCH ā€” sistēmas arhitektÅ«ra. Dažādos izplatÄ«jumos ceļi un rÄ«ki var nedaudz atŔķirties.

Debian 10 atŔķirÄ«bu piemērs (kodolu 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 savienojiet direktoriju ar papildu galvenēm un vairākus direktorijus ar kodola galvenēm. Simbols __KERNEL__ nozÄ«mē, ka UAPI (lietotāja telpas API) galvenes ir definētas kodola kodam, jo ā€‹ā€‹filtrs tiek izpildÄ«ts kodolā.

Stacka aizsardzÄ«bu var atspējot (-fno-stack-protector), jo eBPF koda verificētājs joprojām pārbauda, ā€‹ā€‹vai nav pārkāptas steka ārpus robežām. Optimizāciju ir vērts ieslēgt uzreiz, jo eBPF baitkoda lielums ir ierobežots.

Sāksim ar filtru, kas iztur visas paketes un neko nedara:

#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 savāc xdp_filter.o. Kur to tagad izmēģināt?

Testa stends

Stendā jāiekļauj divas saskarnes: uz kuras būs filtrs un no kuras tiks sūtītas paketes. Tām ir jābūt pilnvērtīgām Linux ierīcēm ar saviem IP, lai pārbaudītu, kā parastās lietojumprogrammas darbojas ar mūsu filtru.

Mums ir piemērotas veth (virtuālā Ethernet) tipa ierÄ«ces: tie ir virtuālo tÄ«kla interfeisu pāris, kas ā€œsavienotiā€ tieÅ”i viens ar otru. JÅ«s varat tos izveidot Ŕādi (Å”ajā sadaļā visas komandas ip tiek veiktas no root):

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

Å eit xdp-remote Šø xdp-local ā€” ierīču nosaukumi. Ieslēgts xdp-local (192.0.2.1/24) tiks pievienots filtrs, ar xdp-remote (192.0.2.2/24) tiks nosÅ«tÄ«ta ienākoŔā trafika. Tomēr pastāv problēma: saskarnes atrodas tajā paŔā datorā, un Linux nenosÅ«tÄ«s trafiku uz vienu no tiem caur otru. To var atrisināt ar sarežģītiem noteikumiem iptables, taču viņiem bÅ«s jāmaina pakotnes, kas ir neērti atkļūdoÅ”anai. Labāk ir izmantot tÄ«kla nosaukumu telpas (turpmāk netns).

TÄ«kla nosaukumvieta satur saskarņu, marÅ”rutÄ“Å”anas tabulu un NetFilter kārtulu kopu, kas ir izolētas no lÄ«dzÄ«giem objektiem citos netns. Katrs process darbojas nosaukumvietā, un tam ir piekļuve tikai Ŕī netns objektiem. Pēc noklusējuma sistēmai ir viena tÄ«kla nosaukumvieta visiem objektiem, lai jÅ«s varētu strādāt operētājsistēmā Linux un nezināt par netns.

Izveidosim jaunu nosaukumvietu xdp-test un pārvietojiet to uz turieni xdp-remote.

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

Pēc tam process sākas xdp-test, "neredzēs" xdp-local (pēc noklusējuma tas paliks netns) un, nosÅ«tot paketi uz 192.0.2.1, tā to nodos cauri xdp-remotejo tā ir vienÄ«gā saskarne 192.0.2.0/24, kas pieejama Å”im procesam. Tas darbojas arÄ« pretējā virzienā.

Pārvietojoties starp netns, saskarne pazÅ«d un zaudē adresi. Lai konfigurētu saskarni netns, jums ir jāpalaiž ip ... Å”ajā komandas nosaukumvietā 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

Kā redzat, tas neatŔķiras no iestatÄ«juma xdp-local noklusējuma nosaukumvietā:

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

Ja tu skrien tcpdump -tnevi xdp-local, jÅ«s varat redzēt, ka paketes nosÅ«tÄ«tas no xdp-test, tiek piegādāti uz Å”o interfeisu:

ip netns exec xdp-test   ping 192.0.2.1

Ir ērti palaist čaulu xdp-test. Repozitorijā ir skripts, kas automatizē darbu ar stendu, piemēram, stendu var konfigurēt ar komandu sudo ./stand up un izdzēsiet to sudo ./stand down.

IzsekoŔana

Filtrs ir saistīts ar ierīci Ŕādi:

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

TaustiņŔ -force nepiecieÅ”ams, lai saistÄ«tu jaunu programmu, ja cita jau ir saistÄ«ta. ā€œNav ziņu nav labas ziņasā€ nav par Å”o komandu, secinājums jebkurā gadÄ«jumā ir apjomÄ«gs. norādÄ«t verbose neobligāti, bet kopā ar to tiek parādÄ«ts ziņojums par koda verificētāja darbu ar montāžas sarakstu:

Verifier analysis:

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

Atsaistīt programmu no saskarnes:

ip link set dev xdp-local xdp off

Skriptā tās ir komandas sudo ./stand attach Šø sudo ./stand detach.

Par to varat pārliecināties, pievienojot filtru ping turpina darboties, bet vai programma darbojas? Pievienosim žurnālus. Funkcija bpf_trace_printk() lÄ«dzÄ«gs printf(), bet atbalsta tikai lÄ«dz trim argumentiem, izņemot modeli, un ierobežotu specifikāciju sarakstu. Makro bpf_printk() vienkārÅ”o zvanu.

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

Izvade tiek nosÅ«tÄ«ta uz kodola izsekoÅ”anas kanālu, kas ir jāiespējo:

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

Skatīt ziņojuma pavedienu:

cat /sys/kernel/debug/tracing/trace_pipe

Abas Ŕīs komandas veic zvanu sudo ./stand log.

Ping tagad vajadzētu aktivizēt Ŕādus ziņojumus:

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

Ja uzmanīgi aplūkosit verificētāja izvadi, jūs pamanīsit dīvainus aprēķinus:

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

Fakts ir tāds, ka eBPF programmām nav datu sadaļas, tāpēc vienÄ«gais veids, kā kodēt formāta virkni, ir tieÅ”ie VM komandu argumenti:

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

Å Ä« iemesla dēļ atkļūdoÅ”anas izvade ievērojami palielina iegÅ«to kodu.

XDP pakeŔu sūtīŔana

MainÄ«sim filtru: ļaujiet tam sÅ«tÄ«t atpakaļ visas ienākoŔās paketes. Tas ir nepareizi no tÄ«kla viedokļa, jo bÅ«tu jāmaina adreses galvenēs, bet tagad darbs principā ir svarÄ«gs.

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

Palaist tcpdump par xdp-remote. Tam ir jāparāda identisks izejoÅ”ais un ienākoÅ”ais ICMP atbalss pieprasÄ«jums un jāpārtrauc rādÄ«t ICMP atbalss atbildi. Bet tas neparādās. Izrādās, ka darbam XDP_TX programmā ieslēgts xdp-local ir nepiecieÅ”amauz pāra saskarni xdp-remote tika pieŔķirta arÄ« programma, pat ja tā bija tukÅ”a, un viņŔ tika audzināts.

Kā es to uzzināju?

Izsekojiet pakotnes ceļu kodolā Perf notikumu mehānisms, starp citu, ļauj izmantot to paŔu virtuālo maŔīnu, tas ir, eBPF tiek izmantots demontāžai ar eBPF.

No ļauna ir jāizdara labs, jo nav nekā cita, no kā to izdarīt.

$ 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 ir kods 6?

$ errno 6
ENXIO 6 No such device or address

Funkcija veth_xdp_flush_bq() saņem kļūdas kodu no veth_xdp_xmit(), kur meklēt pēc ENXIO un atrodiet komentāru.

Atjaunosim minimālo filtru (XDP_PASS) failā xdp_dummy.c, pievienojiet to Makefile, piesaistiet to xdp-remote:

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

Tagad tcpdump parāda, kas tiek gaidīts:

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

Ja tā vietā tiek rādīti tikai ARP, jums ir jānoņem filtri (tas tiek darīts sudo ./stand detach), atlaidiet ping, pēc tam iestatiet filtrus un mēģiniet vēlreiz. Problēma ir tāda, ka filtrs XDP_TX derīga gan ARP, gan ja steka
nosaukumvietas xdp-test izdevās ā€œaizmirstā€ MAC adresi 192.0.2.1, tas nevarēs atrisināt Å”o IP.

Problēmas paziņojums

Pārejam pie norādītā uzdevuma: uzrakstiet SYN sīkfailu mehānismu uz XDP.

SYN plÅ«di joprojām ir populārs DDoS uzbrukums, kura bÅ«tÄ«ba ir Ŕāda. Kad savienojums ir izveidots (TCP rokasspiediens), serveris saņem SYN, pieŔķir resursus turpmākajam savienojumam, atbild ar SYNACK paketi un gaida ACK. Uzbrucējs vienkārÅ”i nosÅ«ta tÅ«kstoÅ”iem SYN pakeÅ”u sekundē no viltotām adresēm no katra saimniekdatora vairāku tÅ«kstoÅ”u robotu tÄ«klā. Serveris ir spiests pieŔķirt resursus uzreiz pēc paketes pienākÅ”anas, bet atbrÄ«vo tos pēc liela taimauta, kā rezultātā tiek iztērēta atmiņa vai ierobežojumi, jauni savienojumi netiek pieņemti un pakalpojums nav pieejams.

Ja jÅ«s nepieŔķirat resursus, pamatojoties uz SYN paketi, bet atbildat tikai ar SYNACK paketi, kā tad serveris var saprast, ka ACK pakete, kas tika saņemta vēlāk, attiecas uz SYN paketi, kas netika saglabāta? Galu galā uzbrucējs var Ä£enerēt arÄ« viltotus ACK. SYN sÄ«kfaila mērÄ·is ir to iekodēt seqnum savienojuma parametrus kā adreÅ”u, portu un mainÄ«go sāls jauktu. Ja ACK izdevās saņemt pirms sāls maiņas, varat vēlreiz aprēķināt hash un salÄ«dzināt to ar acknum. Kalts acknum uzbrucējs nevar, jo sāls ietver noslēpumu, un ierobežotā kanāla dēļ viņam nebÅ«s laika to Ŕķirot.

SYN sīkfails jau sen ir ieviests Linux kodolā, un to var pat automātiski iespējot, ja SYN pienāk pārāk ātri un masveidā.

Izglītības programma par TCP rokasspiedienu

TCP nodroÅ”ina datu pārraidi kā baitu straumi, piemēram, HTTP pieprasÄ«jumi tiek pārsÅ«tÄ«ti pa TCP. Straume tiek pārraidÄ«ta gabalos pa paketēm. Visām TCP paketēm ir loÄ£iski karodziņi un 32 bitu kārtas numuri:

  • Karogu kombinācija nosaka konkrētas pakotnes lomu. SYN karodziņŔ norāda, ka Ŕī ir sÅ«tÄ«tāja pirmā pakete savienojumā. ACK karodziņŔ nozÄ«mē, ka sÅ«tÄ«tājs ir saņēmis visus savienojuma datus lÄ«dz baitam acknum. Paketei var bÅ«t vairāki karodziņi, un to izsauc pēc to kombinācijas, piemēram, SYNACK pakete.

  • SecÄ«bas numurs (seqnum) norāda nobÄ«di datu straumē pirmajam baitam, kas tiek pārsÅ«tÄ«ts Å”ajā paketē. Piemēram, ja pirmajā paketē ar X baitiem datu Å”is skaitlis bija N, tad nākamajā paketē ar jauniem datiem tas bÅ«s N+X. Savienojuma sākumā katra puse izvēlas Å”o numuru nejauÅ”i.

  • Apstiprinājuma numurs (acnum) - tāda pati nobÄ«de kā seqnum, taču tas nenosaka pārsÅ«tāmā baita numuru, bet gan pirmā baita numuru no adresāta, kuru sÅ«tÄ«tājs neredzēja.

Savienojuma sākumā pusēm ir jāvienojas seqnum Šø acknum. Klients kopā ar to nosÅ«ta SYN paketi seqnum = X. Serveris atbild ar SYNACK paketi, kur tas ieraksta to seqnum = Y un atmasko acknum = X + 1. Klients atbild uz SYNACK ar ACK paketi, kur seqnum = X + 1, acknum = Y + 1. Pēc tam sākas faktiskā datu pārsÅ«tÄ«Å”ana.

Ja partneris neapstiprina paketes saņemÅ”anu, TCP pēc taimauta to nosÅ«ta atkārtoti.

Kāpēc ne vienmēr tiek izmantotas SYN sīkdatnes?

Pirmkārt, ja SYNACK vai ACK tiek zaudēts, jums bÅ«s jāgaida, lÄ«dz tas tiks nosÅ«tÄ«ts vēlreiz - savienojuma iestatÄ«Å”ana palēnināsies. Otrkārt, SYN pakotnē - un tikai tajā! ā€” tiek pārraidÄ«tas vairākas opcijas, kas ietekmē savienojuma turpmāko darbÄ«bu. Neatceroties ienākoŔās SYN paketes, serveris ignorē Ŕīs opcijas; klients tās nesÅ«tÄ«s nākamajās paketēs. TCP Å”ajā gadÄ«jumā var darboties, taču vismaz sākotnējā posmā savienojuma kvalitāte samazināsies.

Runājot par pakotnēm, XDP programmai ir jāveic Ŕādas darbÄ«bas:

  • atbildēt uz SYN ar SYNACK ar sÄ«kfailu;
  • atbildēt uz ACK ar RST (atvienot);
  • izmetiet atlikuŔās paketes.

Algoritma pseidokods kopā ar pakotnes parsÄ“Å”anu:

Š•ŃŠ»Šø этŠ¾ Š½Šµ Ethernet,
    ŠæрŠ¾ŠæустŠøть ŠæŠ°ŠŗŠµŃ‚.
Š•ŃŠ»Šø этŠ¾ Š½Šµ IPv4,
    ŠæрŠ¾ŠæустŠøть ŠæŠ°ŠŗŠµŃ‚.
Š•ŃŠ»Šø Š°Š“рŠµŃ Š² тŠ°Š±Š»ŠøцŠµ ŠæрŠ¾Š²ŠµŃ€ŠµŠ½Š½Ń‹Ń…,               (*)
        уŠ¼ŠµŠ½ŃŒŃˆŠøть счŠµŃ‚чŠøŠŗ Š¾ŃŃ‚Š°Š²ŃˆŠøхся ŠæрŠ¾Š²ŠµŃ€Š¾Šŗ,
        ŠæрŠ¾ŠæустŠøть ŠæŠ°ŠŗŠµŃ‚.
Š•ŃŠ»Šø этŠ¾ Š½Šµ TCP,
    сŠ±Ń€Š¾ŃŠøть ŠæŠ°ŠŗŠµŃ‚.     (**)
Š•ŃŠ»Šø этŠ¾ SYN,
    Š¾Ń‚Š²ŠµŃ‚Šøть SYN-ACK с cookie.
Š•ŃŠ»Šø этŠ¾ ACK,
    ŠµŃŠ»Šø Š² acknum Š»ŠµŠ¶Šøт Š½Šµ cookie,
        сŠ±Ń€Š¾ŃŠøть ŠæŠ°ŠŗŠµŃ‚.
    Š—Š°Š½ŠµŃŃ‚Šø Š² тŠ°Š±Š»Šøцу Š°Š“рŠµŃ с N Š¾ŃŃ‚Š°Š²ŃˆŠøхся ŠæрŠ¾Š²ŠµŃ€Š¾Šŗ.    (*)
    ŠžŃ‚Š²ŠµŃ‚Šøть RST.   (**)
Š’ Š¾ŃŃ‚Š°Š»ŃŒŠ½Ń‹Ń… сŠ»ŃƒŃ‡Š°ŃŃ… сŠ±Ń€Š¾ŃŠøть ŠæŠ°ŠŗŠµŃ‚.

Viens (*) ir atzÄ«mēti punkti, kur jums jāpārvalda sistēmas stāvoklis - pirmajā posmā jÅ«s varat iztikt bez tiem, vienkārÅ”i ievieÅ”ot TCP rokasspiedienu ar SYN sÄ«kfaila Ä£enerÄ“Å”anu kā secÄ«bu.

Uz vietas (**), kamēr mums nav galda, mēs izlaidīsim paciņu.

TCP rokasspiediena ievieŔana

Paka parsÄ“Å”ana un koda pārbaude

Mums bÅ«s nepiecieÅ”amas tÄ«kla galvenes struktÅ«ras: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) un TCP (uapi/linux/tcp.h). Es nevarēju savienot pēdējo kļūdu dēļ, kas saistÄ«tas ar atomic64_t, man vajadzēja iekopēt kodā nepiecieÅ”amās definÄ«cijas.

Visas funkcijas, kas ir izceltas C, lai nodroÅ”inātu lasāmÄ«bu, ir jāiekļauj izsaukuma punktā, jo eBPF verificētājs kodolā aizliedz atpakaļsekoÅ”anu, tas ir, cilpas un funkciju izsaukumus.

#define INTERNAL static __attribute__((always_inline))

Makro LOG() atspējo drukāŔanu izlaiduma versijā.

Programma ir funkciju konveijers. Katrs saņem paketi, kurā ir izcelta atbilstoŔā lÄ«meņa galvene, piemēram, process_ether() sagaida, ka tas tiks aizpildÄ«ts ether. Pamatojoties uz lauka analÄ«zes rezultātiem, funkcija var nodot paketi augstākam lÄ«menim. Funkcijas rezultāts ir XDP darbÄ«ba. Pagaidām SYN un ACK apstrādātāji nodod visas paketes.

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

Es vērÅ”u jÅ«su uzmanÄ«bu uz čekiem, kas atzÄ«mēti ar A un B. Ja komentēsit A, programma tiks veidota, taču ielādes laikā tiks parādÄ«ta pārbaudes kļūda:

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!

Atslēgu virkne invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0): ir izpildes ceļi, kad trīspadsmitais baits no bufera sākuma atrodas ārpus paketes. No saraksta ir grūti saprast, par kuru rindiņu mēs runājam, taču ir instrukcijas numurs (12) un demontētājs, kas parāda avota koda rindas:

llvm-objdump -S xdp_filter.o | less

Šajā gadījumā tas norāda uz līniju

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

kas skaidri parāda, ka problēma ir ether. Tas vienmēr bÅ«tu Ŕādi.

Atbildēt uz SYN

Å ajā posmā mērÄ·is ir Ä£enerēt pareizu SYNACK paketi ar fiksētu seqnum, kas nākotnē tiks aizstāts ar SYN sÄ«kfailu. Visas izmaiņas notiek iekŔā process_tcp_syn() un apkārtējās teritorijas.

Pakas pārbaude

Savādi, bet Å”eit ir visievērojamākā lÄ«nija vai drÄ«zāk tās komentārs:

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

Rakstot pirmo koda versiju, tika izmantots 5.1 kodols, kura pārbaudÄ«tājam bija atŔķirÄ«ba starp data_end Šø (const void*)ctx->data_end. RakstÄ«Å”anas laikā kodolam 5.3.1 nebija Ŕīs problēmas. Iespējams, ka kompilators piekļuva lokālajam mainÄ«gajam citādi nekā laukam. Stāsta morāle: koda vienkārÅ”oÅ”ana var palÄ«dzēt, ja ir daudz ligzdoÅ”anas.

Nākamās ir kārtējās garuma pārbaudes, lai novērtētu verificētāja godÄ«bu; O MAX_CSUM_BYTES zemāk.

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

Paciņas atlocÄ«Å”ana

Mēs aizpildām seqnum Šø acknum, iestatiet ACK (SYN jau ir iestatÄ«ts):

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

Apmainiet TCP portus, IP adreses un MAC adreses. Standarta bibliotēka nav pieejama no XDP programmas, tāpēc memcpy() ā€” makro, kas slēpj Clang bÅ«tÄ«bas.

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

Kontrolsummu pārrēķins

IPv4 un TCP kontrolsummās galvenēs ir jāpievieno visi 16 bitu vārdi, un tajos tiek ierakstÄ«ts galveņu lielums, tas ir, kompilÄ“Å”anas laikā tas nav zināms. Tā ir problēma, jo pārbaudÄ«tājs neizlaidÄ«s parasto cilpu uz robežas mainÄ«go. Bet galveņu lielums ir ierobežots: katrs lÄ«dz 64 baitiem. Varat izveidot cilpu ar noteiktu atkārtojumu skaitu, kas var beigties agri.

Es atzīmēju, ka ir RFC 1624 par to, kā daļēji pārrēķināt kontrolsummu, ja tiek mainīti tikai pakotņu fiksētie vārdi. Tomēr metode nav universāla, un to būtu grūtāk uzturēt.

Kontrolsummas aprēķināŔanas 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;
}

Lai gan size verificēts ar izsaucēja kodu, ir nepiecieÅ”ams otrais izejas nosacÄ«jums, lai pārbaudÄ«tājs varētu pierādÄ«t cilpas pabeigÅ”anu.

32 bitu vārdiem ir ieviesta vienkārŔāka versija:

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

Faktiski pārrēķinot kontrolsummas un nosūtot paketi atpakaļ:

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() saskaņā ar RFC 32 veido kontrolsummu no 16 bitu vārdu 791 bitu summas.

TCP rokasspiediena pārbaude

Filtrs pareizi izveido savienojumu ar netcat, trÅ«kst galÄ«gā ACK, uz kuru Linux atbildēja ar RST paketi, jo tÄ«kla steks nesaņēma SYN - tas tika pārveidots par SYNACK un nosÅ«tÄ«ts atpakaļ - un no OS viedokļa atnāca pakete, kas nebija saistÄ«ta ar atvērÅ”anu. savienojumiem.

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

Ir svarÄ«gi pārbaudÄ«t ar pilnvērtÄ«gām lietojumprogrammām un novērot tcpdump par xdp-remote jo, piemēram, hping3 nereaģē uz nepareizām kontrolsummām.

No XDP viedokļa pati pārbaude ir triviāla. Aprēķinu algoritms ir primitÄ«vs un, iespējams, neaizsargāts pret sarežģītu uzbrucēju. Piemēram, Linux kodols izmanto kriptogrāfisko SipHash, taču tā ievieÅ”ana XDP nepārprotami neietilpst Ŕī raksta darbÄ«bas jomā.

Ieviests jauniem TODO, kas saistīti ar ārējo komunikāciju:

  • XDP programma nevar saglabāt cookie_seed (sāls slepenā daļa) globālajā mainÄ«gajā, jums ir nepiecieÅ”ama krātuve kodolā, kuras vērtÄ«ba periodiski tiks atjaunināta no uzticama Ä£eneratora.

  • Ja SYN sÄ«kfails sakrÄ«t ACK paketē, jums nav jādrukā ziņojums, bet jāatceras verificētā klienta IP, lai turpinātu nosÅ«tÄ«t pakeÅ”u no tā.

Likumīga klienta verifikācija:

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

Žurnāli parāda, ka pārbaude ir izturēta (flags=0x2 - tas ir SYN, flags=0x10 ir 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

Lai gan nav pārbaudīto IP saraksta, nebūs aizsardzības pret paŔu SYN plūdiem, taču Ŕeit ir reakcija uz ACK plūdiem, ko palaida Ŕāda komanda:

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

Žurnāla ieraksti:

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

Secinājums

Dažreiz eBPF kopumā un jo Ä«paÅ”i XDP tiek pasniegts vairāk kā uzlabots administratora rÄ«ks, nevis kā izstrādes platforma. PatieŔām, XDP ir rÄ«ks, kas traucē kodola pakeÅ”u apstrādi, nevis alternatÄ«va kodola stekam, piemēram, DPDK un citas kodola apieÅ”anas opcijas. Savukārt XDP ļauj ieviest visai sarežģītu loÄ£iku, kuru turklāt ir viegli atjaunināt, netraucējot trafika apstrādē. Verifikators lielas problēmas nerada, personÄ«gi es neatteiktos no lietotāja telpas koda daļām.

Otrajā daļā, ja tēma ir interesanta, aizpildīsim verificēto klientu un atvienojumu tabulu, ieviesīsim skaitītājus un uzrakstīsim userspace utilītu filtra pārvaldībai.

Saites:

Avots: www.habr.com

Pievieno komentāru