Vi skriver beskyttelse mod DDoS-angreb på XDP. Nuklear del

eXpress Data Path (XDP) teknologi gør det muligt at udføre tilfældig trafikbehandling på Linux-grænseflader, før pakkerne kommer ind i kernenetværksstakken. Anvendelse af XDP - beskyttelse mod DDoS-angreb (CloudFlare), komplekse filtre, statistikindsamling (Netflix). XDP-programmer udføres af den virtuelle eBPF-maskine, så de har begrænsninger på både deres kode og de tilgængelige kernefunktioner afhængigt af filtertypen.

Artiklen er beregnet til at udfylde manglerne ved adskillige materialer på XDP. For det første leverer de færdiglavet kode, der straks omgår funktionerne i XDP: den er forberedt til verifikation eller er for enkel til at forårsage problemer. Når du så forsøger at skrive din kode fra bunden, aner du ikke hvad du skal gøre med typiske fejl. For det andet er måder at teste XDP på ​​lokalt uden en VM og hardware ikke dækket, på trods af at de har deres egne faldgruber. Teksten er beregnet til programmører, der er fortrolige med netværk og Linux, og som er interesserede i XDP og eBPF.

I denne del vil vi i detaljer forstå, hvordan XDP-filteret er samlet, og hvordan man tester det, så vil vi skrive en simpel version af den velkendte SYN-cookies-mekanisme på pakkebehandlingsniveauet. Vi vil ikke oprette en "hvid liste" endnu
verificerede klienter, hold tællere og administrer filteret - nok logfiler.

Vi vil skrive i C - det er ikke moderigtigt, men det er praktisk. Al kode er tilgængelig på GitHub via linket i slutningen og er opdelt i commits i henhold til trinene beskrevet i artiklen.

Ansvarsfraskrivelse. I løbet af denne artikel vil jeg udvikle en miniløsning til at afværge DDoS-angreb, fordi dette er en realistisk opgave for XDP og mit ekspertiseområde. Men hovedmålet er at forstå teknologien; dette er ikke en guide til at skabe færdiglavet beskyttelse. Selvstudiekoden er ikke optimeret og udelader nogle nuancer.

XDP kort oversigt

Jeg vil kun skitsere de vigtigste punkter for ikke at duplikere dokumentation og eksisterende artikler.

Så filterkoden indlæses i kernen. Indgående pakker sendes til filteret. Som et resultat skal filteret træffe en beslutning: send pakken ind i kernen (XDP_PASS), drop pakke (XDP_DROP) eller send den tilbage (XDP_TX). Filteret kan ændre pakken, dette gælder især for XDP_TX. Du kan også afbryde programmet (XDP_ABORTED) og nulstil pakken, men dette er analogt assert(0) - til fejlretning.

eBPF (extended Berkley Packet Filter) virtuelle maskine er bevidst gjort simpel, så kernen kan kontrollere, at koden ikke går i løkker og ikke beskadiger andres hukommelse. Kumulative restriktioner og kontroller:

  • Loops (baglæns) er forbudt.
  • Der er en stak til data, men ingen funktioner (alle C-funktioner skal være inlinet).
  • Hukommelsesadgang uden for stakken og pakkebufferen er forbudt.
  • Kodestørrelsen er begrænset, men i praksis er dette ikke særlig væsentligt.
  • Kun kald til specielle kernefunktioner (eBPF-hjælpere) er tilladt.

Design og installation af et filter ser sådan ud:

  1. Kildekode (f kernel.c) er kompileret til objekt (kernel.o) for den virtuelle eBPF-arkitektur. Fra oktober 2019 er kompilering til eBPF understøttet af Clang og lovet i GCC 10.1.
  2. Hvis denne objektkode indeholder kald til kernestrukturer (for eksempel tabeller og tællere), erstattes deres ID'er med nuller, hvilket betyder, at en sådan kode ikke kan udføres. Før du indlæser i kernen, skal du erstatte disse nuller med ID'erne for specifikke objekter, der er oprettet gennem kernekald (link koden). Du kan gøre dette med eksterne hjælpeprogrammer, eller du kan skrive et program, der vil linke og indlæse et bestemt filter.
  3. Kernen verificerer det indlæste program. Fraværet af cyklusser og manglende overskridelse af pakke- og stakgrænser kontrolleres. Hvis verifikatoren ikke kan bevise, at koden er korrekt, afvises programmet - du skal være i stand til at behage ham.
  4. Efter vellykket verifikation kompilerer kernen eBPF-arkitekturobjektkoden til maskinkode til systemarkitekturen (just-in-time).
  5. Programmet knytter sig til grænsefladen og begynder at behandle pakker.

Da XDP kører i kernen, udføres debugging ved hjælp af sporlogs og faktisk pakker, som programmet filtrerer eller genererer. eBPF sikrer dog, at den downloadede kode er sikker for systemet, så du kan eksperimentere med XDP direkte på din lokale Linux.

Forberedelse af miljøet

samling

Clang kan ikke direkte producere objektkode til eBPF-arkitekturen, så processen består af to trin:

  1. Kompiler C-kode til LLVM-bytekode (clang -emit-llvm).
  2. Konverter bytekode til eBPF objektkode (llc -march=bpf -filetype=obj).

Når du skriver et filter, vil et par filer med hjælpefunktioner og makroer være nyttige fra kernetest. Det er vigtigt, at de matcher kerneversionen (KVER). Download dem til 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 til Arch Linux (kerne 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 indeholder stien til kernehovederne, ARCH — systemarkitektur. Stier og værktøjer kan variere lidt mellem distributioner.

Eksempel på forskelle for Debian 10 (kerne 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 forbinde en mappe med hjælpeoverskrifter og flere mapper med kerneoverskrifter. Symbol __KERNEL__ betyder, at UAPI (userspace API) overskrifter er defineret for kernekode, da filteret udføres i kernen.

Stakbeskyttelse kan deaktiveres (-fno-stack-protector), fordi eBPF-kodeverifikatoren stadig tjekker for stak-out-of-bounds-overtrædelser. Det er værd at slå optimeringer til med det samme, fordi størrelsen af ​​eBPF-bytekoden er begrænset.

Lad os starte med et filter, der sender alle pakker og ikke gør noget:

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

Team make samler ind xdp_filter.o. Hvor skal man prøve det nu?

Prøvestativ

Standen skal indeholde to grænseflader: hvorpå der vil være et filter og hvorfra pakker sendes. Disse skal være fuldgyldige Linux-enheder med deres egne IP'er for at kunne tjekke, hvordan almindelige applikationer fungerer med vores filter.

Enheder af typen veth (virtuel Ethernet) er velegnede til os: disse er et par virtuelle netværksgrænseflader "forbundet" direkte til hinanden. Du kan oprette dem på denne måde (i dette afsnit alle kommandoer ip udføres fra root):

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

Her xdp-remote и xdp-local — enhedsnavne. På xdp-local (192.0.2.1/24) vil der blive monteret et filter, med xdp-remote (192.0.2.2/24) sendes indgående trafik. Der er dog et problem: grænsefladerne er på den samme maskine, og Linux vil ikke sende trafik til den ene af dem gennem den anden. Du kan løse dette med vanskelige regler iptables, men de bliver nødt til at ændre pakker, hvilket er ubelejligt til fejlretning. Det er bedre at bruge netværksnavneområder (herefter netns).

Et netværksnavneområde indeholder et sæt grænseflader, routingtabeller og NetFilter-regler, der er isoleret fra lignende objekter i andre netns. Hver proces kører i et navneområde og har kun adgang til objekterne i det netns. Som standard har systemet et enkelt netværksnavneområde for alle objekter, så du kan arbejde i Linux og ikke kender til netns.

Lad os oprette et nyt navneområde xdp-test og flytte den dertil xdp-remote.

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

Så kører processen ind xdp-test, vil ikke "se" xdp-local (det forbliver som standard i netns), og når du sender en pakke til 192.0.2.1, vil det sende det igennem xdp-remotefordi det er den eneste grænseflade på 192.0.2.0/24, der er tilgængelig for denne proces. Dette virker også i den modsatte retning.

Når du flytter mellem netns, går grænsefladen ned og mister sin adresse. For at konfigurere grænsefladen i netns skal du køre ip ... i dette kommandonavneområde 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

Som du kan se, er dette ikke anderledes end indstillingen xdp-local i standardnavnerummet:

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

Hvis du løber tcpdump -tnevi xdp-local, kan du se, at pakker sendt fra xdp-test, leveres til denne grænseflade:

ip netns exec xdp-test   ping 192.0.2.1

Det er praktisk at starte en shell i xdp-test. Lagret har et script, der automatiserer arbejdet med standen; for eksempel kan du konfigurere standen med kommandoen sudo ./stand up og slette den sudo ./stand down.

Sporing

Filteret er knyttet til enheden på denne måde:

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

nøgle -force nødvendig for at linke et nyt program, hvis et andet allerede er linket. "Ingen nyheder er gode nyheder" handler ikke om denne kommando, konklusionen er i hvert fald omfangsrig. angive verbose valgfrit, men med det vises en rapport om kodeverifikatorens arbejde med en samleliste:

Verifier analysis:

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

Fjern linket til programmet fra grænsefladen:

ip link set dev xdp-local xdp off

I scriptet er disse kommandoer sudo ./stand attach и sudo ./stand detach.

Ved at påsætte et filter kan du sikre dig det ping fortsætter med at køre, men virker programmet? Lad os tilføje logfiler. Fungere bpf_trace_printk() svarende til printf(), men understøtter kun op til tre andre argumenter end mønsteret og en begrænset liste over specifikationer. Makro bpf_printk() forenkler opkaldet.

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

Outputtet går til kernesporingskanalen, som skal aktiveres:

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

Se meddelelsestråd:

cat /sys/kernel/debug/tracing/trace_pipe

Begge disse kommandoer foretager et opkald sudo ./stand log.

Ping skulle nu udløse meddelelser som dette:

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

Hvis du ser nøje på verifikatorens output, vil du bemærke mærkelige beregninger:

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

Faktum er, at eBPF-programmer ikke har en datasektion, så den eneste måde at kode en formatstreng på er de umiddelbare argumenter for VM-kommandoer:

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

Af denne grund blæser fejlfindingsoutput den resulterende kode i høj grad.

Sender XDP-pakker

Lad os ændre filteret: lad det sende alle indgående pakker tilbage. Dette er forkert set fra et netværkssynspunkt, da det ville være nødvendigt at ændre adresserne i headerne, men nu er det principielle arbejde vigtigt.

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

Lad os starte tcpdump på xdp-remote. Den skal vise identiske udgående og indgående ICMP Echo Request og stoppe med at vise ICMP Echo Reply. Men det viser sig ikke. Det viser sig, at for arbejde XDP_TX i programmet på xdp-local skaltil pargrænsefladen xdp-remote et program blev også tildelt, selvom det var tomt, og han blev opdraget.

Hvordan vidste jeg det?

Spor stien til en pakke i kernen Perf event-mekanismen tillader i øvrigt at bruge den samme virtuelle maskine, det vil sige, at eBPF bruges til adskillelser med eBPF.

Du skal gøre godt ud af det onde, for der er ikke andet at gøre det ud af.

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

Hvad er kode 6?

$ errno 6
ENXIO 6 No such device or address

Funktion veth_xdp_flush_bq() modtager en fejlkode fra veth_xdp_xmit(), hvor søg efter ENXIO og find kommentaren.

Lad os gendanne minimumsfilteret (XDP_PASS) i filen xdp_dummy.c, føj det til Makefilen, bind det til xdp-remote:

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

Nu tcpdump viser hvad der forventes:

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

Hvis kun ARP'er vises i stedet, skal du fjerne filtrene (dette gør sudo ./stand detach), Giv slip ping, indstil derefter filtre og prøv igen. Problemet er, at filteret XDP_TX gyldig både på ARP og hvis stakken
navnerum xdp-test lykkedes at "glemme" MAC-adressen 192.0.2.1, vil den ikke være i stand til at løse denne IP.

Formulering af problemet

Lad os gå videre til den angivne opgave: skriv en SYN-cookies-mekanisme på XDP.

SYN flood forbliver et populært DDoS-angreb, hvis essens er som følger. Når en forbindelse er etableret (TCP-handshake), modtager serveren en SYN, allokerer ressourcer til den fremtidige forbindelse, svarer med en SYNACK-pakke og venter på en ACK. Angriberen sender simpelthen tusindvis af SYN-pakker i sekundet fra spoofede adresser fra hver vært i et multi-tusind-stærkt botnet. Serveren er tvunget til at allokere ressourcer umiddelbart efter ankomsten af ​​pakken, men frigiver dem efter en lang timeout; som følge heraf er hukommelse eller grænser opbrugt, nye forbindelser accepteres ikke, og tjenesten er utilgængelig.

Hvis du ikke allokerer ressourcer baseret på SYN-pakken, men kun svarer med en SYNACK-pakke, hvordan kan serveren så forstå, at den ACK-pakke, der ankom senere, refererer til en SYN-pakke, der ikke blev gemt? En angriber kan trods alt også generere falske ACK'er. Pointen med SYN-cookien er at kode den ind seqnum forbindelsesparametre som en hash af adresser, porte og skiftende salt. Hvis ACK nåede at nå frem før saltet blev ændret, kan du beregne hashen igen og sammenligne den med acknum. Forge acknum angriberen kan ikke, da saltet indeholder hemmeligheden, og vil ikke have tid til at sortere igennem det på grund af en begrænset kanal.

SYN-cookien har længe været implementeret i Linux-kernen og kan endda aktiveres automatisk, hvis SYN'er ankommer for hurtigt og massevis.

Uddannelsesprogram om TCP-håndtryk

TCP leverer datatransmission som en strøm af bytes, for eksempel transmitteres HTTP-anmodninger over TCP. Strømmen transmitteres i stykker i pakker. Alle TCP-pakker har logiske flag og 32-bit sekvensnumre:

  • Kombinationen af ​​flag bestemmer rollen for en bestemt pakke. SYN-flaget angiver, at dette er afsenderens første pakke på forbindelsen. ACK-flaget betyder, at afsenderen har modtaget alle forbindelsesdata op til byten acknum. En pakke kan have flere flag og kaldes af deres kombination, for eksempel en SYNACK-pakke.

  • Sekvensnummer (seqnum) angiver offset i datastrømmen for den første byte, der transmitteres i denne pakke. For eksempel, hvis dette tal var N i den første pakke med X bytes data, vil det i den næste pakke med nye data være N+X. I begyndelsen af ​​forbindelsen vælger hver side dette tal tilfældigt.

  • Kvitteringsnummer (acknum) - samme offset som seqnum, men det bestemmer ikke nummeret på den byte, der sendes, men nummeret på den første byte fra modtageren, som afsenderen ikke så.

Ved starten af ​​forbindelsen skal parterne blive enige seqnum и acknum. Klienten sender en SYN-pakke med sin seqnum = X. Serveren svarer med en SYNACK-pakke, hvor den optager sin seqnum = Y og afslører acknum = X + 1. Klienten svarer på SYNACK med en ACK-pakke, hvor seqnum = X + 1, acknum = Y + 1. Herefter begynder selve dataoverførslen.

Hvis peeren ikke bekræfter modtagelsen af ​​pakken, sender TCP den igen efter en timeout.

Hvorfor bruges SYN-cookies ikke altid?

For det første, hvis SYNACK eller ACK går tabt, skal du vente på, at den bliver sendt igen - forbindelsesopsætningen vil blive langsommere. For det andet i SYN-pakken - og kun i den! — der sendes en række muligheder, som påvirker den videre drift af forbindelsen. Uden at huske indgående SYN-pakker ignorerer serveren således disse muligheder; klienten vil ikke sende dem i de næste pakker. TCP kan fungere i dette tilfælde, men i det mindste i den indledende fase vil kvaliteten af ​​forbindelsen falde.

Fra et pakkeperspektiv skal et XDP-program gøre følgende:

  • svare på SYN med SYNACK med en cookie;
  • reagere på ACK med RST (afbryde forbindelsen);
  • kassere de resterende pakker.

Pseudokode for algoritmen sammen med pakkeparsing:

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

En (*) punkter, hvor du skal administrere systemets tilstand, er markeret - i første fase kan du undvære dem ved blot at implementere et TCP-håndtryk med generering af en SYN-cookie som følgenummer.

På stedet (**), mens vi ikke har et bord, springer vi pakken over.

Implementering af TCP-håndtryk

Parser pakken og bekræfter koden

Vi skal bruge netværkshovedstrukturer: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) og TCP (uapi/linux/tcp.h). Jeg var ikke i stand til at forbinde sidstnævnte på grund af fejl relateret til atomic64_t, jeg var nødt til at kopiere de nødvendige definitioner ind i koden.

Alle funktioner, der er fremhævet i C for at kunne læses, skal være inlinet ved opkaldspunktet, da eBPF-verifikatoren i kernen forbyder backtracking, det vil sige i virkeligheden, loops og funktionskald.

#define INTERNAL static __attribute__((always_inline))

Makro LOG() deaktiverer udskrivning i release build.

Programmet er en formidler af funktioner. Hver modtager en pakke, hvor den tilsvarende niveauoverskrift er fremhævet, f.eks. process_ether() forventer, at den bliver fyldt ether. Baseret på resultaterne af feltanalyse kan funktionen videregive pakken til et højere niveau. Resultatet af funktionen er XDP-handlingen. Indtil videre sender SYN- og ACK-handlerne alle pakker.

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

Jeg henleder din opmærksomhed på checkene markeret A og B. Hvis du kommenterer A ud, vil programmet bygge, men der vil være en verifikationsfejl ved indlæsning:

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!

Nøglestreng invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0): Der er eksekveringsstier, når den trettende byte fra begyndelsen af ​​bufferen er uden for pakken. Det er svært at forstå ud fra listen, hvilken linje vi taler om, men der er et instruktionsnummer (12) og en disassembler, der viser linjerne med kildekode:

llvm-objdump -S xdp_filter.o | less

I dette tilfælde peger den på linjen

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

hvilket gør det klart, at problemet er ether. Sådan ville det altid være.

Svar til SYN

Målet på dette trin er at generere en korrekt SYNACK-pakke med en fast seqnum, som i fremtiden vil blive erstattet af SYN-cookien. Alle ændringer sker i process_tcp_syn() og omkringliggende områder.

Pakkebekræftelse

Mærkeligt nok er her den mest bemærkelsesværdige linje, eller rettere, kommentaren til den:

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

Ved skrivning af den første version af koden blev 5.1 kernen brugt, til verifikatoren der var forskel mellem data_end и (const void*)ctx->data_end. I skrivende stund havde kerne 5.3.1 ikke dette problem. Det er muligt, at compileren fik adgang til en lokal variabel anderledes end et felt. Historiens moral: At forenkle koden kan hjælpe, når der er meget nesting.

Dernæst er rutinemæssige længdetjek for verifikatorens herlighed; O MAX_CSUM_BYTES nedenfor.

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

Folder pakken ud

udfylde seqnum и acknum, indstil ACK (SYN er allerede indstillet):

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

Skift TCP-porte, IP-adresse og MAC-adresser. Standardbiblioteket er ikke tilgængeligt fra XDP-programmet, så memcpy() — en makro, der skjuler Clang-egenskaberne.

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

Genberegning af kontrolsummer

IPv4- og TCP-kontrolsummer kræver tilføjelse af alle 16-bit ord i overskrifterne, og størrelsen af ​​overskrifterne er skrevet ind i dem, det vil sige ukendt på kompileringstidspunktet. Dette er et problem, fordi verifikatoren ikke vil springe den normale løkke over til grænsevariablen. Men størrelsen af ​​overskrifterne er begrænset: op til 64 bytes hver. Du kan lave en loop med et fast antal iterationer, som kan slutte tidligt.

Jeg bemærker, at der er RFC 1624 om, hvordan man delvist genberegner kontrolsummen, hvis kun pakkernes faste ord ændres. Metoden er dog ikke universel, og implementeringen ville være sværere at vedligeholde.

Kontrolsumsberegningsfunktion:

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

Selvom size verificeret af den kaldende kode, er den anden udgangsbetingelse nødvendig, så verifikatoren kan bevise fuldførelsen af ​​sløjfen.

For 32-bit ord er en enklere version implementeret:

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

Faktisk genberegne kontrolsummerne og sende pakken tilbage:

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;

Funktion carry() laver en kontrolsum fra en 32-bit sum af 16-bit ord, ifølge RFC 791.

TCP-håndtryk verifikation

Filteret etablerer korrekt forbindelse med netcat, springer den endelige ACK over, som Linux reagerede på med en RST-pakke, da netværksstakken ikke modtog SYN - den blev konverteret til SYNACK og sendt tilbage - og fra OS synspunkt ankom en pakke, der ikke var relateret til åben forbindelser.

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

Det er vigtigt at tjekke med fuldgyldige ansøgninger og observere tcpdump på xdp-remote fordi der f.eks. hping3 reagerer ikke på forkerte kontrolsummer.

Fra et XDP-synspunkt er selve verifikationen triviel. Beregningsalgoritmen er primitiv og sandsynligvis sårbar over for en sofistikeret angriber. Linux-kernen bruger for eksempel den kryptografiske SipHash, men dens implementering til XDP er klart uden for denne artikels omfang.

Introduceret til nye TODO'er relateret til ekstern kommunikation:

  • XDP-programmet kan ikke gemme cookie_seed (den hemmelige del af saltet) i en global variabel har du brug for lagring i kernen, hvis værdi regelmæssigt opdateres fra en pålidelig generator.

  • Hvis SYN-cookien matcher i ACK-pakken, behøver du ikke at udskrive en besked, men huske IP-adressen på den verificerede klient for at fortsætte med at sende pakker fra den.

Legitime klientbekræftelse:

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

Logfilerne viser, at kontrollen bestod (flags=0x2 - dette er SYN, flags=0x10 er 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

Selvom der ikke er nogen liste over verificerede IP'er, vil der ikke være nogen beskyttelse mod selve SYN-floden, men her er reaktionen på en ACK-oversvømmelse lanceret af følgende kommando:

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

Logposter:

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

Konklusion

Nogle gange præsenteres eBPF generelt og XDP i særdeleshed mere som et avanceret administratorværktøj end som en udviklingsplatform. Faktisk er XDP et værktøj til at forstyrre behandlingen af ​​pakker af kernen, og ikke et alternativ til kernestakken, som DPDK og andre kernebypass-muligheder. På den anden side giver XDP dig mulighed for at implementere ret kompleks logik, som desuden er let at opdatere uden afbrydelser i trafikbehandlingen. Verifikatoren skaber ikke store problemer; personligt ville jeg ikke afvise dette for dele af brugerrumskoden.

I den anden del, hvis emnet er interessant, vil vi færdiggøre tabellen over bekræftede klienter og afbrydelser, implementere tællere og skrive et brugerrumsværktøj til at styre filteret.

referencer:

Kilde: www.habr.com

Tilføj en kommentar