Ons skryf beskerming teen DDoS-aanvalle op XDP. Kernkrag deel

eXpress Data Path (XDP)-tegnologie laat toe dat lukrake verkeersverwerking op Linux-koppelvlakke uitgevoer word voordat die pakkies die kernnetwerkstapel binnegaan. Toepassing van XDP - beskerming teen DDoS-aanvalle (CloudFlare), komplekse filters, statistiekversameling (Netflix). XDP-programme word uitgevoer deur die eBPF virtuele masjien, so hulle het beperkings op beide hul kode en die beskikbare kernfunksies, afhangende van die filtertipe.

Die artikel is bedoel om die tekortkominge van talle materiaal op XDP te vul. Eerstens verskaf hulle klaargemaakte kode wat onmiddellik die kenmerke van XDP omseil: dit is voorberei vir verifikasie of is te eenvoudig om probleme te veroorsaak. Wanneer jy dan jou kode van nuuts af probeer skryf, het jy geen idee wat om met tipiese foute te doen nie. Tweedens, maniere om XDP plaaslik te toets sonder 'n VM en hardeware word nie gedek nie, ten spyte van die feit dat hulle hul eie slaggate het. Die teks is bedoel vir programmeerders wat vertroud is met netwerk en Linux wat belangstel in XDP en eBPF.

In hierdie deel sal ons in detail verstaan ​​hoe die XDP-filter saamgestel word en hoe om dit te toets, dan sal ons 'n eenvoudige weergawe van die bekende SYN-koekiesmeganisme op die pakketverwerkingsvlak skryf. Ons sal nog nie 'n "wit lys" skep nie
geverifieerde kliënte, hou tellers en bestuur die filter - genoeg logs.

Ons sal in C skryf - dit is nie modieus nie, maar dit is prakties. Alle kode is beskikbaar op GitHub via die skakel aan die einde en is verdeel in commits volgens die stadiums wat in die artikel beskryf word.

Vrywaring. In die loop van hierdie artikel sal ek 'n mini-oplossing ontwikkel om DDoS-aanvalle af te weer, want dit is 'n realistiese taak vir XDP en my gebied van kundigheid. Die hoofdoel is egter om die tegnologie te verstaan; dit is nie 'n gids om gereedgemaakte beskerming te skep nie. Die tutoriaalkode is nie geoptimaliseer nie en laat sommige nuanses weg.

XDP Kort oorsig

Ek sal slegs die sleutelpunte uiteensit om nie dokumentasie en bestaande artikels te dupliseer nie.

Dus, die filterkode word in die kern gelaai. Inkomende pakkies word na die filter deurgegee. As gevolg hiervan moet die filter 'n besluit neem: gee die pakkie in die kern (XDP_PASS), drop pakkie (XDP_DROP) of stuur dit terug (XDP_TX). Die filter kan die pakket verander, dit is veral waar vir XDP_TX. Jy kan ook die program stop (XDP_ABORTED) en stel die pakket terug, maar dit is analoog assert(0) - vir ontfouting.

Die eBPF (extended Berkley Packet Filter) virtuele masjien is doelbewus eenvoudig gemaak sodat die kern kan kyk dat die kode nie lus is nie en nie ander mense se geheue beskadig nie. Kumulatiewe beperkings en tjeks:

  • Lusse (agtertoe) is verbode.
  • Daar is 'n stapel vir data, maar geen funksies nie (alle C-funksies moet inlyn wees).
  • Geheuetoegang buite die stapel en pakkiebuffer word verbied.
  • Die kodegrootte is beperk, maar in die praktyk is dit nie baie betekenisvol nie.
  • Slegs oproepe na spesiale kernfunksies (eBPF-helpers) word toegelaat.

Die ontwerp en installering van 'n filter lyk soos volg:

  1. Bronkode (bv kernel.c) word saamgestel in objek (kernel.o) vir die eBPF virtuele masjien argitektuur. Vanaf Oktober 2019 word samestelling na eBPF deur Clang ondersteun en belowe in GCC 10.1.
  2. As hierdie objekkode oproepe na kernstrukture bevat (byvoorbeeld tabelle en tellers), word hul ID's deur nulle vervang, wat beteken dat sodanige kode nie uitgevoer kan word nie. Voordat u in die kern laai, moet u hierdie nulle vervang met die ID's van spesifieke voorwerpe wat deur kernoproepe geskep is (skakel die kode). Jy kan dit met eksterne nutsprogramme doen, of jy kan 'n program skryf wat 'n spesifieke filter sal koppel en laai.
  3. Die kern verifieer die gelaaide program. Die afwesigheid van siklusse en versuim om pakket- en stapelgrense te oorskry word nagegaan. As die verifieerder nie kan bewys dat die kode korrek is nie, word die program afgekeur – jy moet hom kan behaag.
  4. Na suksesvolle verifikasie, stel die kern die eBPF-argitektuurobjekkode saam in masjienkode vir die stelselargitektuur (net-betyds).
  5. Die program heg aan die koppelvlak en begin pakkies verwerk.

Aangesien XDP in die kern loop, word ontfouting uitgevoer met behulp van spoorlogboeke en, in werklikheid, pakkies wat die program filtreer of genereer. eBPF verseker egter dat die afgelaaide kode veilig is vir die stelsel, sodat jy direk met XDP op jou plaaslike Linux kan eksperimenteer.

Voorbereiding van die omgewing

vergadering

Clang kan nie direk objekkode vir die eBPF-argitektuur produseer nie, so die proses bestaan ​​uit twee stappe:

  1. Stel C-kode saam na LLVM-greepkode (clang -emit-llvm).
  2. Skakel greepkode om na eBPF-objekkode (llc -march=bpf -filetype=obj).

Wanneer 'n filter geskryf word, sal 'n paar lêers met hulpfunksies en makro's nuttig wees van kerntoetse. Dit is belangrik dat hulle ooreenstem met die kern weergawe (KVER). Laai hulle af na 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 vir Arch Linux (kern 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 bevat die pad na die kernopskrifte, ARCH - stelselargitektuur. Paadjies en gereedskap kan effens verskil tussen verspreidings.

Voorbeeld van verskille vir Debian 10 (kern 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 verbind 'n gids met hulpopskrifte en verskeie dopgehou met kernopskrifte. Simbool __KERNEL__ beteken dat UAPI (gebruikersruimte API) opskrifte vir die kernkode gedefinieer word, aangesien die filter in die kern uitgevoer word.

Stapelbeskerming kan gedeaktiveer word (-fno-stack-protector), omdat die eBPF-kodeverifieerder steeds kyk vir stapel buite-grens oortredings. Dit is die moeite werd om dadelik optimalisering aan te skakel, want die grootte van die eBPF-greepkode is beperk.

Kom ons begin met 'n filter wat alle pakkies deurlaat en niks doen nie:

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

Span make versamel xdp_filter.o. Waar om dit nou te probeer?

toetsbank

Die staander moet twee koppelvlakke insluit: waarop daar 'n filter sal wees en waarvandaan pakkies gestuur sal word. Dit moet volwaardige Linux-toestelle met hul eie IP's wees om te kyk hoe gereelde toepassings met ons filter werk.

Toestelle van die tipe veth (virtuele Ethernet) is geskik vir ons: dit is 'n paar virtuele netwerkkoppelvlakke wat direk aan mekaar "gekoppel" is. Jy kan hulle so skep (in hierdie afdeling alle opdragte ip word uitgevoer vanaf root):

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

Hier xdp-remote и xdp-local — toestelname. Aan xdp-local (192.0.2.1/24) sal 'n filter aangeheg word, met xdp-remote (192.0.2.2/24) sal inkomende verkeer gestuur word. Daar is egter 'n probleem: die koppelvlakke is op dieselfde masjien, en Linux sal nie verkeer na een van hulle deur die ander stuur nie. U kan dit met moeilike reëls oplos iptables, maar hulle sal pakkette moet verander, wat ongerieflik is vir ontfouting. Dit is beter om netwerknaamruimtes (hierna netns) te gebruik.

'n Netwerknaamruimte bevat 'n stel koppelvlakke, roeteringstabelle en NetFilter-reëls wat van soortgelyke voorwerpe in ander netns geïsoleer is. Elke proses loop in 'n naamruimte en het slegs toegang tot die voorwerpe van daardie netns. By verstek het die stelsel 'n enkele netwerknaamspasie vir alle voorwerpe, sodat jy in Linux kan werk en nie van netns weet nie.

Kom ons skep 'n nuwe naamruimte xdp-test en skuif dit daarheen xdp-remote.

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

Dan loop die proses in xdp-test, sal nie "sien" xdp-local (dit sal by verstek in netns bly) en wanneer 'n pakkie na 192.0.2.1 gestuur word, sal dit dit deurstuur xdp-remotewant dit is die enigste koppelvlak op 192.0.2.0/24 wat vir hierdie proses toeganklik is. Dit werk ook in die teenoorgestelde rigting.

Wanneer jy tussen netns beweeg, gaan die koppelvlak af en verloor sy adres. Om die koppelvlak in netns op te stel, moet jy hardloop ip ... in hierdie opdrag naamruimte 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

Soos u kan sien, verskil dit nie van die instelling nie xdp-local in die verstek naamruimte:

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

As jy hardloop tcpdump -tnevi xdp-local, kan jy sien dat pakkies vanaf gestuur word xdp-test, word by hierdie koppelvlak afgelewer:

ip netns exec xdp-test   ping 192.0.2.1

Dit is gerieflik om 'n dop in te lanseer xdp-test. Die bewaarplek het 'n skrip wat werk met die staander outomatiseer; u kan byvoorbeeld die staander met die opdrag instel sudo ./stand up en verwyder dit sudo ./stand down.

Opsporing

Die filter word soos volg met die toestel geassosieer:

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

sleutel -force nodig om 'n nuwe program te koppel as 'n ander een reeds gekoppel is. “Geen nuus is goeie nuus” gaan nie oor hierdie opdrag nie, die gevolgtrekking is in elk geval lywig. aandui verbose opsioneel, maar daarby verskyn 'n verslag oor die werk van die kodeverifieerder met 'n samestellinglys:

Verifier analysis:

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

Ontkoppel die program van die koppelvlak:

ip link set dev xdp-local xdp off

In die skrif is dit opdragte sudo ./stand attach и sudo ./stand detach.

Deur 'n filter aan te heg, kan jy seker maak dat ping gaan voort, maar werk die program? Kom ons voeg logs by. Funksie bpf_trace_printk() soortgelyk aan printf(), maar ondersteun slegs tot drie ander argumente as die patroon, en 'n beperkte lys van spesifiseerders. Makro bpf_printk() vereenvoudig die oproep.

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

Die uitvoer gaan na die kernspoorkanaal, wat geaktiveer moet word:

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

Bekyk boodskapdraad:

cat /sys/kernel/debug/tracing/trace_pipe

Albei hierdie opdragte maak 'n oproep sudo ./stand log.

Ping behoort nou boodskappe soos hierdie te aktiveer:

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

As jy noukeurig na die verifieerder se uitset kyk, sal jy vreemde berekeninge opmerk:

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

Die feit is dat eBPF-programme nie 'n data-afdeling het nie, so die enigste manier om 'n formaatstring te enkodeer is die onmiddellike argumente van VM-opdragte:

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

Om hierdie rede blaas ontfoutuitvoer die gevolglike kode grootliks op.

Stuur XDP-pakkies

Kom ons verander die filter: laat dit alle inkomende pakkies terugstuur. Dit is verkeerd uit 'n netwerk oogpunt, aangesien dit nodig sou wees om die adresse in die kopskrifte te verander, maar nou is die werk in beginsel belangrik.

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

Begin tcpdump op xdp-remote. Dit moet identiese uitgaande en inkomende ICMP Echo Request wys en ophou om ICMP Echo Reply te wys. Maar dit wys nie. Dit blyk dat vir werk XDP_TX in die program op xdp-local is nodigna die paar-koppelvlak xdp-remote 'n program is ook aangewys, al was dit leeg, en hy is grootgemaak.

Hoe het ek dit geweet?

Volg die pad van 'n pakket in die kern Die perf events meganisme laat toe, terloops, dieselfde virtuele masjien te gebruik, dit wil sê, eBPF word gebruik vir demontage met eBPF.

Jy moet goed maak uit kwaad, want daar is niks anders om dit uit te maak nie.

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

Wat is kode 6?

$ errno 6
ENXIO 6 No such device or address

Funksie veth_xdp_flush_bq() ontvang 'n foutkode van veth_xdp_xmit(), waar soek deur ENXIO en vind die kommentaar.

Kom ons herstel die minimum filter (XDP_PASS) in lêer xdp_dummy.c, voeg dit by die Makefile, bind dit aan xdp-remote:

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

nou tcpdump wys wat verwag word:

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

As slegs ARP's in plaas daarvan gewys word, moet jy die filters verwyder (dit doen sudo ./stand detach), laat gaan ping, stel dan filters en probeer weer. Die probleem is dat die filter XDP_TX geldig beide op ARP en as die stapel
naamruimtes xdp-test daarin geslaag het om die MAC-adres 192.0.2.1 te "vergeet", sal dit nie hierdie IP kan oplos nie.

Probleemstelling

Kom ons gaan aan na die gestelde taak: skryf 'n SYN-koekiesmeganisme op XDP.

SYN-vloed bly 'n gewilde DDoS-aanval, waarvan die kern soos volg is. Wanneer 'n verbinding tot stand gebring word (TCP-handdruk), ontvang die bediener 'n SYN, ken hulpbronne toe vir die toekomstige verbinding, reageer met 'n SYNACK-pakkie en wag vir 'n ACK. Die aanvaller stuur eenvoudig duisende SYN-pakkies per sekonde vanaf bedrieglike adresse van elke gasheer in 'n multi-duisend-sterk botnet. Die bediener word gedwing om hulpbronne onmiddellik toe te wys met die aankoms van die pakkie, maar stel dit vry na 'n groot tydsduur; gevolglik is geheue of limiete uitgeput, nuwe verbindings word nie aanvaar nie, en die diens is nie beskikbaar nie.

As jy nie hulpbronne toewys op grond van die SYN-pakkie nie, maar slegs reageer met 'n SYNACK-pakkie, hoe kan die bediener dan verstaan ​​dat die ACK-pakkie wat later aangekom het, verwys na 'n SYN-pakkie wat nie gestoor is nie? 'n Aanvaller kan immers ook vals ACK's genereer. Die punt van die SYN-koekie is om dit in te enkodeer seqnum verbindingsparameters as 'n hash van adresse, poorte en veranderende sout. As die ACK daarin geslaag het om te arriveer voordat die sout verander is, kan jy die hash weer bereken en dit vergelyk met acknum. Smee acknum die aanvaller kan nie, aangesien die sout die geheim insluit, en nie tyd sal hê om daardeur te sorteer nie as gevolg van 'n beperkte kanaal.

Die SYN-koekie is lank reeds in die Linux-kern geïmplementeer en kan selfs outomaties geaktiveer word as SYN's te vinnig en massaal opdaag.

Opvoedkundige program oor TCP-handdruk

TCP verskaf data-oordrag as 'n stroom grepe, byvoorbeeld, HTTP-versoeke word oor TCP versend. Die stroom word in stukke in pakkies oorgedra. Alle TCP-pakkies het logiese vlae en 32-bis volgordenommers:

  • Die kombinasie van vlae bepaal die rol van 'n spesifieke pakket. Die SYN-vlag dui aan dat dit die sender se eerste pakkie op die verbinding is. Die ACK-vlag beteken dat die sender al die verbindingsdata tot by die greep ontvang het acknum. 'n Pakkie kan verskeie vlae hê en word deur hul kombinasie genoem, byvoorbeeld 'n SYNACK-pakkie.

  • Volgnommer (volgnummer) spesifiseer die afwyking in die datastroom vir die eerste greep wat in hierdie pakkie versend word. Byvoorbeeld, as in die eerste pakkie met X grepe data hierdie getal N was, in die volgende pakkie met nuwe data sal dit N+X wees. Aan die begin van die verbinding kies elke kant hierdie nommer willekeurig.

  • Erkenningsnommer (acknum) - dieselfde afwyking as seqnum, maar dit bepaal nie die nommer van die greep wat versend word nie, maar die nommer van die eerste greep van die ontvanger, wat die sender nie gesien het nie.

Aan die begin van die verband moet die partye saamstem seqnum и acknum. Die kliënt stuur 'n SYN-pakkie saam met sy seqnum = X. Die bediener reageer met 'n SYNACK-pakkie, waar dit sy opteken seqnum = Y en ontbloot acknum = X + 1. Die kliënt reageer op SYNACK met 'n ACK-pakkie, waar seqnum = X + 1, acknum = Y + 1. Hierna begin die werklike data-oordrag.

As die eweknie nie ontvangs van die pakkie erken nie, stuur TCP dit weer na 'n uitteltyd.

Waarom word SYN-koekies nie altyd gebruik nie?

Eerstens, as SYNACK of ACK verlore gaan, sal jy moet wag dat dit weer gestuur word - die verbindingsopstelling sal stadiger word. Tweedens, in die SYN-pakket - en net daarin! — 'n aantal opsies word oorgedra wat die verdere werking van die verbinding beïnvloed. Sonder om inkomende SYN-pakkies te onthou, ignoreer die bediener dus hierdie opsies; die kliënt sal dit nie in die volgende pakkies stuur nie. TCP kan in hierdie geval werk, maar ten minste in die aanvanklike stadium sal die kwaliteit van die verbinding afneem.

Vanuit 'n pakketperspektief moet 'n XDP-program die volgende doen:

  • reageer op SYN met SINACK met 'n koekie;
  • reageer op ACK met RST (ontkoppel);
  • gooi die oorblywende pakkies weg.

Pseudokode van die algoritme saam met pakketontleding:

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

een (*) punte waar jy die toestand van die stelsel moet bestuur, is gemerk - in die eerste stadium kan jy daarsonder klaarkom deur bloot 'n TCP-handdruk te implementeer met die generering van 'n SYN-koekie as 'n volgnum.

Op die plek (**), terwyl ons nie 'n tafel het nie, sal ons die pakkie oorslaan.

Implementering van TCP-handdruk

Ontleed die pakket en verifieer die kode

Ons sal netwerkkopstrukture benodig: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) en TCP (uapi/linux/tcp.h). Ek kon nie laasgenoemde koppel nie weens foute wat verband hou met atomic64_t, Ek moes die nodige definisies in die kode kopieer.

Alle funksies wat in C uitgelig is vir leesbaarheid, moet by die punt van oproep ingelyn word, aangesien die eBPF-verifieerder in die kern terugspoor verbied, dit wil sê, in werklikheid, lusse en funksie-oproepe.

#define INTERNAL static __attribute__((always_inline))

Makro LOG() deaktiveer drukwerk in die vrystellingbou.

Die program is 'n vervoerder van funksies. Elkeen ontvang 'n pakkie waarin die ooreenstemmende vlakopskrif uitgelig is, byvoorbeeld, process_ether() verwag dat dit gevul sal word ether. Gebaseer op die resultate van veldanalise, kan die funksie die pakkie na 'n hoër vlak oordra. Die resultaat van die funksie is die XDP-aksie. Vir nou slaag die SYN- en ACK-hanteerders alle pakkies.

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

Ek vestig jou aandag op die tjeks wat A en B gemerk is. As jy A kommentaar lewer, sal die program bou, maar daar sal 'n verifikasiefout wees wanneer jy laai:

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!

Sleutelstring invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0): Daar is uitvoeringspaaie wanneer die dertiende greep van die begin van die buffer buite die pakkie is. Dit is moeilik om uit die lys te verstaan ​​van watter reël ons praat, maar daar is 'n instruksienommer (12) en 'n demonteerder wat die reëls van die bronkode wys:

llvm-objdump -S xdp_filter.o | less

In hierdie geval wys dit na die lyn

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

wat dit duidelik maak dat die probleem is ether. Dit sou altyd so wees.

Antwoord op SYN

Die doel op hierdie stadium is om 'n korrekte SYNACK-pakkie met 'n vaste te genereer seqnum, wat in die toekoms deur die SYN-koekie vervang sal word. Alle veranderinge vind plaas in process_tcp_syn() en omliggende gebiede.

Pakketverifikasie

Vreemd genoeg, hier is die merkwaardigste reël, of liewer, die kommentaar daarop:

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

By die skryf van die eerste weergawe van die kode is die 5.1-kern gebruik, vir die verifieerder waarvan daar 'n verskil was tussen data_end и (const void*)ctx->data_end. Ten tyde van die skryf daarvan het kern 5.3.1 nie hierdie probleem gehad nie. Dit is moontlik dat die samesteller 'n plaaslike veranderlike anders as 'n veld verkry het. Moraal van die storie: Vereenvoudiging van die kode kan help wanneer daar baie nes gemaak word.

Volgende is roetine-lengtekontroles vir die glorie van die verifieerder; O MAX_CSUM_BYTES hieronder.

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

Ontvou die pakkie

Ons vul in seqnum и acknum, stel ACK (SYN is reeds gestel):

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

Ruil TCP-poorte, IP-adres en MAC-adresse om. Die standaardbiblioteek is nie toeganklik vanaf die XDP-program nie, dus memcpy() - 'n makro wat die intrinsieke van Clang verberg.

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

Herberekening van kontrolesomme

IPv4- en TCP-kontrolesomme vereis die byvoeging van alle 16-bis-woorde in die kopskrifte, en die grootte van die opskrifte word daarin geskryf, dit wil sê, onbekend tydens samestelling. Dit is 'n probleem omdat die verifieerder nie die normale lus na die grensveranderlike sal oorslaan nie. Maar die grootte van die opskrifte is beperk: tot 64 grepe elk. Jy kan 'n lus maak met 'n vaste aantal iterasies, wat vroeg kan eindig.

Ek merk op dat daar is RFC 1624 oor hoe om die kontrolesom gedeeltelik te herbereken as slegs die vaste woorde van die pakkette verander word. Die metode is egter nie universeel nie, en die implementering sal moeiliker wees om in stand te hou.

Kontrolesom berekening funksie:

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

Alhoewel size geverifieer deur die oproepkode, is die tweede uitgangvoorwaarde nodig sodat die verifieerder die voltooiing van die lus kan bewys.

Vir 32-bis woorde word 'n eenvoudiger weergawe geïmplementeer:

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

Herbereken eintlik die kontrolesomme en stuur die pakkie terug:

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;

Funksie carry() maak 'n kontrolesom van 'n 32-bis-som van 16-bis woorde, volgens RFC 791.

TCP-handdrukverifikasie

Die filter vestig korrek 'n verbinding met netcat, ontbreek die finale ACK, waarop Linux met 'n RST-pakkie gereageer het, aangesien die netwerkstapel nie SYN ontvang het nie - dit is omgeskakel na SYNACK en teruggestuur - en vanuit die OS-oogpunt het 'n pakkie aangekom wat nie verband hou met oop verbindings.

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

Dit is belangrik om na te gaan met volwaardige aansoeke en waar te neem tcpdump op xdp-remote want bv. hping3 reageer nie op verkeerde kontrolesomme nie.

Uit 'n XDP-oogpunt is die verifikasie self triviaal. Die berekeningsalgoritme is primitief en waarskynlik kwesbaar vir 'n gesofistikeerde aanvaller. Die Linux-kern gebruik byvoorbeeld die kriptografiese SipHash, maar die implementering daarvan vir XDP is duidelik buite die bestek van hierdie artikel.

Bekendgestel vir nuwe TODO's wat met eksterne kommunikasie verband hou:

  • XDP-program kan nie stoor nie cookie_seed (die geheime deel van die sout) in 'n globale veranderlike, benodig u berging in die kern, waarvan die waarde periodiek vanaf 'n betroubare kragopwekker opgedateer sal word.

  • As die SYN-koekie by die ACK-pakkie pas, hoef jy nie 'n boodskap te druk nie, maar onthou die IP van die geverifieerde kliënt om voort te gaan om pakkies daaruit te stuur.

Wettige kliëntverifikasie:

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

Die logs wys dat die tjek geslaag het (flags=0x2 - dit is SYN, flags=0x10 is 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

Alhoewel daar geen lys van geverifieerde IP's is nie, sal daar geen beskerming teen die SYN-vloed self wees nie, maar hier is die reaksie op 'n ACK-vloed wat deur die volgende opdrag van stapel gestuur is:

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

Log inskrywings:

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

Gevolgtrekking

Soms word eBPF in die algemeen en XDP in die besonder meer as 'n gevorderde administrateurhulpmiddel as 'n ontwikkelingsplatform aangebied. Inderdaad, XDP is 'n instrument om in te meng met die verwerking van pakkies deur die kern, en nie 'n alternatief vir die kernstapel nie, soos DPDK en ander kernomleidingsopsies. Aan die ander kant laat XDP jou toe om redelik komplekse logika te implementeer, wat boonop maklik is om op te dateer sonder onderbreking in verkeersverwerking. Die verifieerder skep nie groot probleme nie; persoonlik sou ek dit nie weier vir dele van gebruikersruimtekode nie.

In die tweede deel, as die onderwerp interessant is, sal ons die tabel van geverifieerde kliënte en ontkoppelings voltooi, tellers implementeer en 'n gebruikersruimte-hulpmiddel skryf om die filter te bestuur.

verwysings:

Bron: will.com

Voeg 'n opmerking