Vi skriver beskyttelse mot DDoS-angrep på XDP. Kjernefysisk del

eXpress Data Path (XDP)-teknologi gjør at tilfeldig trafikkbehandling kan utføres på Linux-grensesnitt før pakkene går inn i kjernenettverksstakken. Anvendelse av XDP - beskyttelse mot DDoS-angrep (CloudFlare), komplekse filtre, statistikkinnsamling (Netflix). XDP-programmer kjøres av den virtuelle eBPF-maskinen, så de har begrensninger på både koden og de tilgjengelige kjernefunksjonene avhengig av filtertypen.

Artikkelen er ment å fylle manglene til en rekke materialer på XDP. For det første gir de ferdig kode som umiddelbart omgår funksjonene til XDP: den er forberedt for verifisering eller er for enkel til å forårsake problemer. Når du så prøver å skrive koden fra bunnen av, aner du ikke hva du skal gjøre med typiske feil. For det andre dekkes ikke måter å teste XDP lokalt uten VM og maskinvare, til tross for at de har sine egne fallgruver. Teksten er ment for programmerere som er kjent med nettverk og Linux som er interessert i XDP og eBPF.

I denne delen vil vi forstå i detalj hvordan XDP-filteret er satt sammen og hvordan man tester det, så vil vi skrive en enkel versjon av den velkjente SYN-informasjonskapselmekanismen på pakkebehandlingsnivå. Vi vil ikke opprette en "hvit liste" ennå
verifiserte klienter, hold tellere og administrer filteret - nok logger.

Vi vil skrive i C - det er ikke fasjonabelt, men det er praktisk. All kode er tilgjengelig på GitHub via lenken på slutten og er delt inn i commits i henhold til stadiene beskrevet i artikkelen.

Ansvarsfraskrivelse. I løpet av denne artikkelen vil jeg utvikle en miniløsning for å avverge DDoS-angrep, fordi dette er en realistisk oppgave for XDP og mitt ekspertiseområde. Hovedmålet er imidlertid å forstå teknologien; dette er ikke en veiledning for å lage ferdiglaget beskyttelse. Opplæringskoden er ikke optimalisert og utelater noen nyanser.

XDP kort oversikt

Jeg vil skissere bare de viktigste punktene for ikke å duplisere dokumentasjon og eksisterende artikler.

Så filterkoden lastes inn i kjernen. Innkommende pakker sendes til filteret. Som et resultat må filteret ta en beslutning: sende pakken inn i kjernen (XDP_PASS), slipp pakke (XDP_DROP) eller send den tilbake (XDP_TX). Filteret kan endre pakken, dette gjelder spesielt for XDP_TX. Du kan også avbryte programmet (XDP_ABORTED) og tilbakestill pakken, men dette er analogt assert(0) - for feilsøking.

Den virtuelle maskinen eBPF (extended Berkley Packet Filter) er bevisst gjort enkel slik at kjernen kan sjekke at koden ikke går i løkker og ikke skader andres minne. Kumulative restriksjoner og kontroller:

  • Løkker (bakover) er forbudt.
  • Det er en stack for data, men ingen funksjoner (alle C-funksjoner må være innebygd).
  • Minnetilgang utenfor stabelen og pakkebufferen er forbudt.
  • Kodestørrelsen er begrenset, men i praksis er dette lite vesentlig.
  • Bare kall til spesielle kjernefunksjoner (eBPF-hjelpere) er tillatt.

Å designe og installere et filter ser slik ut:

  1. Kildekode (f.eks kernel.c) er kompilert til objekt (kernel.o) for eBPF virtuell maskinarkitektur. Fra oktober 2019 er kompilering til eBPF støttet av Clang og lovet i GCC 10.1.
  2. Hvis denne objektkoden inneholder kall til kjernestrukturer (for eksempel tabeller og tellere), erstattes ID-ene deres med nuller, noe som betyr at slik kode ikke kan kjøres. Før du laster inn i kjernen, må du erstatte disse nullene med ID-ene til spesifikke objekter opprettet gjennom kjernekall (lenke til koden). Du kan gjøre dette med eksterne verktøy, eller du kan skrive et program som kobler og laster et spesifikt filter.
  3. Kjernen bekrefter det innlastede programmet. Fraværet av sykluser og manglende overskridelse av pakke- og stabelgrenser kontrolleres. Hvis verifikatoren ikke kan bevise at koden er riktig, blir programmet avvist - du må kunne glede ham.
  4. Etter vellykket verifisering kompilerer kjernen eBPF-arkitekturobjektkoden til maskinkode for systemarkitekturen (just-in-time).
  5. Programmet kobles til grensesnittet og begynner å behandle pakker.

Siden XDP kjører i kjernen, utføres feilsøking ved hjelp av sporlogger og faktisk pakker som programmet filtrerer eller genererer. Imidlertid sikrer eBPF at den nedlastede koden er sikker for systemet, slik at du kan eksperimentere med XDP direkte på din lokale Linux.

Forberede miljøet

sammenstilling

Clang kan ikke direkte produsere objektkode for eBPF-arkitekturen, så prosessen består av to trinn:

  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 hjelpefunksjoner og makroer være nyttige fra kjernetester. Det er viktig at de samsvarer med kjerneversjonen (KVER). Last dem ned 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 for Arch Linux (kjerne 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 inneholder banen til kjernehodene, ARCH - system arkitektur. Baner og verktøy kan variere litt mellom distribusjoner.

Eksempel på forskjeller for Debian 10 (kjerne 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 koble til en katalog med hjelpehoder og flere kataloger med kjerneoverskrifter. Symbol __KERNEL__ betyr at UAPI (userspace API) overskrifter er definert for kjernekode, siden filteret kjøres i kjernen.

Stabelbeskyttelse kan deaktiveres (-fno-stack-protector), fordi eBPF-kodeverifikatoren fortsatt sjekker for brudd på stabelen utenfor grensene. Det er verdt å slå på optimaliseringer med en gang, fordi størrelsen på eBPF-bytekoden er begrenset.

La oss starte med et filter som sender alle pakker og ikke gjør noe:

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

Lag make samler xdp_filter.o. Hvor skal man prøve det nå?

Prøvestativ

Stativet må inneholde to grensesnitt: som det vil være et filter på og som pakker skal sendes fra. Dette må være fullverdige Linux-enheter med egne IP-er for å sjekke hvordan vanlige applikasjoner fungerer med filteret vårt.

Enheter av typen veth (virtuelt Ethernet) passer for oss: dette er et par virtuelle nettverksgrensesnitt "koblet" direkte til hverandre. Du kan lage dem slik (i denne delen alle kommandoer ip utføres fra root):

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

Her xdp-remote и xdp-local — enhetsnavn. På xdp-local (192.0.2.1/24) vil det festes et filter, med xdp-remote (192.0.2.2/24) vil innkommende trafikk bli sendt. Det er imidlertid et problem: grensesnittene er på samme maskin, og Linux vil ikke sende trafikk til en av dem gjennom den andre. Du kan løse dette med vanskelige regler iptables, men de må endre pakker, noe som er upraktisk for feilsøking. Det er bedre å bruke nettverksnavneområder (heretter netns).

Et nettverksnavneområde inneholder et sett med grensesnitt, rutingtabeller og NetFilter-regler som er isolert fra lignende objekter i andre netn. Hver prosess kjører i et navneområde og har bare tilgang til objektene til nettene. Som standard har systemet et enkelt nettverksnavnområde for alle objekter, slik at du kan jobbe i Linux og ikke vite om netns.

La oss lage et nytt navneområde xdp-test og flytte den dit xdp-remote.

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

Så kjører prosessen inn xdp-test, vil ikke "se" xdp-local (den vil forbli i netns som standard) og når du sender en pakke til 192.0.2.1 vil den sende den gjennom xdp-remotefordi det er det eneste grensesnittet på 192.0.2.0/24 som er tilgjengelig for denne prosessen. Dette fungerer også i motsatt retning.

Ved flytting mellom nett går grensesnittet ned og mister adressen. For å konfigurere grensesnittet i netns må du kjøre ip ... i dette kommandonavnerommet 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 forskjellig fra innstillingen xdp-local i standard navneområde:

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

Hvis du løper tcpdump -tnevi xdp-local, kan du se at pakker sendt fra xdp-test, leveres til dette grensesnittet:

ip netns exec xdp-test   ping 192.0.2.1

Det er praktisk å lansere et skall i xdp-test. Lagret har et skript som automatiserer arbeid med stativet; for eksempel kan du konfigurere stativet med kommandoen sudo ./stand up og slett den sudo ./stand down.

Sporing

Filteret er knyttet til enheten slik:

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

Ключ -force nødvendig for å koble til et nytt program hvis et annet allerede er koblet. "Ingen nyheter er gode nyheter" handler ikke om denne kommandoen, konklusjonen er i alle fall omfangsrik. indikerer verbose valgfritt, men med det vises en rapport om arbeidet til kodeverifikatoren med en monteringsliste:

Verifier analysis:

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

Koble programmet fra grensesnittet:

ip link set dev xdp-local xdp off

I skriptet er dette kommandoer sudo ./stand attach и sudo ./stand detach.

Ved å feste et filter kan du sørge for det ping fortsetter å kjøre, men fungerer programmet? La oss legge til logger. Funksjon bpf_trace_printk() lik printf(), men støtter bare opptil tre andre argumenter enn mønsteret, og en begrenset liste med spesifikasjoner. Makro bpf_printk() forenkler samtalen.

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

Utdataene går til kjernesporingskanalen, som må aktiveres:

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

Se meldingstråd:

cat /sys/kernel/debug/tracing/trace_pipe

Begge disse kommandoene foretar et anrop sudo ./stand log.

Ping skal nå utløse meldinger som dette:

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

Hvis du ser nøye på verifikatorens utgang, vil du legge merke til merkelige 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 dataseksjon, så den eneste måten å kode en formatstreng på er de umiddelbare argumentene til VM-kommandoer:

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

Av denne grunn blåser feilsøkingsutdata den resulterende koden kraftig.

Sender XDP-pakker

La oss endre filteret: la det sende tilbake alle innkommende pakker. Dette er feil fra et nettverkssynspunkt, siden det ville være nødvendig å endre adressene i overskriftene, men nå er det prinsipielle arbeidet viktig.

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

Lansering tcpdump på xdp-remote. Den skal vise identiske utgående og innkommende ICMP Echo Request og slutte å vise ICMP Echo Reply. Men det vises ikke. Det viser seg at for jobb XDP_TX i programmet på xdp-local til pargrensesnittet xdp-remote et program ble også tildelt, selv om det var tomt, og han ble oppdratt.

Hvordan visste jeg dette?

Spor banen til en pakke i kjernen Perf event-mekanismen tillater forresten å bruke den samme virtuelle maskinen, det vil si at eBPF brukes til demontering med eBPF.

Du må gjøre godt ut av det onde, for det er ikke noe annet å gjøre det ut av.

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

Hva er kode 6?

$ errno 6
ENXIO 6 No such device or address

Funksjon veth_xdp_flush_bq() mottar en feilkode fra veth_xdp_xmit(), hvor søk etter ENXIO og finn kommentaren.

La oss gjenopprette minimumsfilteret (XDP_PASS) i filen xdp_dummy.c, legg den til Makefilen, bind den til xdp-remote:

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

tcpdump viser hva som 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 bare ARP-er vises i stedet, må du fjerne filtrene (dette gjør det sudo ./stand detach), gi slipp ping, sett deretter filtre og prøv igjen. Problemet er at filteret XDP_TX gyldig både på ARP og hvis stabelen
navneområder xdp-test klarte å "glemme" MAC-adressen 192.0.2.1, vil den ikke kunne løse denne IP-en.

Formulering av problemet

La oss gå videre til den angitte oppgaven: skriv en SYN-informasjonskapselmekanisme på XDP.

SYN-flom er fortsatt et populært DDoS-angrep, hvis essens er som følger. Når en tilkobling er etablert (TCP-håndtrykk), mottar serveren en SYN, allokerer ressurser for den fremtidige tilkoblingen, svarer med en SYNACK-pakke og venter på en ACK. Angriperen sender ganske enkelt tusenvis av SYN-pakker per sekund fra falske adresser fra hver vert i et multi-tusen-sterkt botnett. Serveren blir tvunget til å tildele ressurser umiddelbart etter ankomst av pakken, men frigjør dem etter en lang tidsavbrudd; som et resultat er minne eller grenser oppbrukt, nye tilkoblinger aksepteres ikke, og tjenesten er utilgjengelig.

Hvis du ikke tildeler ressurser basert på SYN-pakken, men bare svarer med en SYNACK-pakke, hvordan kan da serveren forstå at ACK-pakken som kom senere refererer til en SYN-pakke som ikke ble lagret? Tross alt kan en angriper også generere falske ACK-er. Poenget med SYN-informasjonskapselen er å kode den inn seqnum tilkoblingsparametere som en hash av adresser, porter og skiftende salt. Hvis ACK klarte å komme før saltet ble endret, kan du beregne hashen på nytt og sammenligne den med acknum. Smi acknum angriperen kan ikke, siden saltet inkluderer hemmeligheten, og vil ikke ha tid til å sortere gjennom den på grunn av en begrenset kanal.

SYN-informasjonskapselen har lenge vært implementert i Linux-kjernen og kan til og med aktiveres automatisk hvis SYN-er kommer for raskt og massevis.

Utdanningsprogram om TCP-håndtrykk

TCP gir dataoverføring som en strøm av byte, for eksempel blir HTTP-forespørsler overført over TCP. Strømmen overføres i stykker i pakker. Alle TCP-pakker har logiske flagg og 32-biters sekvensnummer:

  • Kombinasjonen av flagg bestemmer rollen til en bestemt pakke. SYN-flagget indikerer at dette er avsenderens første pakke på forbindelsen. ACK-flagget betyr at avsenderen har mottatt alle tilkoblingsdata opp til byten acknum. En pakke kan ha flere flagg og kalles ved deres kombinasjon, for eksempel en SYNACK-pakke.

  • Sekvensnummer (seqnum) spesifiserer forskyvningen i datastrømmen for den første byten som sendes i denne pakken. For eksempel, hvis i den første pakken med X byte med data var dette tallet N, i den neste pakken med nye data vil det være N+X. I begynnelsen av forbindelsen velger hver side dette tallet tilfeldig.

  • Bekreftelsesnummer (acknum) - samme forskyvning som seqnum, men det bestemmer ikke nummeret på byten som sendes, men nummeret på den første byten fra mottakeren, som avsenderen ikke så.

I begynnelsen av forbindelsen må partene bli enige seqnum и acknum. Klienten sender en SYN-pakke med sin seqnum = X. Serveren svarer med en SYNACK-pakke, hvor den registrerer sin seqnum = Y og avslører acknum = X + 1. Klienten svarer på SYNACK med en ACK-pakke, hvor seqnum = X + 1, acknum = Y + 1. Etter dette starter selve dataoverføringen.

Hvis peeren ikke bekrefter mottak av pakken, sender TCP den på nytt etter et tidsavbrudd.

Hvorfor brukes ikke alltid SYN-informasjonskapsler?

For det første, hvis SYNACK eller ACK går tapt, må du vente på at den sendes igjen - tilkoblingsoppsettet vil tregere. For det andre, i SYN-pakken - og bare i den! — det overføres en rekke alternativer som påvirker den videre driften av forbindelsen. Uten å huske innkommende SYN-pakker, ignorerer serveren disse alternativene, klienten vil ikke sende dem i de neste pakkene. TCP kan fungere i dette tilfellet, men i det minste i det innledende stadiet vil kvaliteten på forbindelsen reduseres.

Når det gjelder pakker, må et XDP-program gjøre følgende:

  • svare på SYN med SYNACK med en informasjonskapsel;
  • svare på ACK med RST (frakoble);
  • kast de resterende pakkene.

Pseudokode for algoritmen sammen med pakkeparsing:

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

En (*) punkter der du trenger å administrere tilstanden til systemet er merket - i det første trinnet kan du klare deg uten dem ved ganske enkelt å implementere et TCP-håndtrykk med generering av en SYN-informasjonskapsel som følgenummer.

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

Implementering av TCP-håndtrykk

Parser pakken og bekrefter koden

Vi trenger nettverkshodestrukturer: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) og TCP (uapi/linux/tcp.h). Jeg klarte ikke å koble til sistnevnte på grunn av feil relatert til atomic64_t, måtte jeg kopiere de nødvendige definisjonene inn i koden.

Alle funksjoner som er uthevet i C for lesbarhet må være innebygd ved anropspunktet, siden eBPF-verifikatoren i kjernen forbyr tilbakesporing, det vil si faktisk løkker og funksjonskall.

#define INTERNAL static __attribute__((always_inline))

Makro LOG() deaktiverer utskrift i utgivelsesbygget.

Programmet er en formidler av funksjoner. Hver mottar en pakke der den tilsvarende nivåoverskriften er uthevet, for eksempel, process_ether() forventer at den blir fylt ether. Basert på resultatene av feltanalyse kan funksjonen sende pakken til et høyere nivå. Resultatet av funksjonen er XDP-handlingen. Foreløpig sender SYN- og ACK-behandlerne 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 gjør oppmerksom på sjekkene merket A og B. Hvis du kommenterer A, vil programmet bygges, men det vil oppstå en bekreftelsesfeil ved lasting:

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økkelstreng invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0): Det er utførelsesbaner når den trettende byten fra begynnelsen av bufferen er utenfor pakken. Det er vanskelig å forstå fra listen hvilken linje vi snakker om, men det er et instruksjonsnummer (12) og en demonteringsenhet som viser linjene med kildekode:

llvm-objdump -S xdp_filter.o | less

I dette tilfellet peker den på linjen

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

som gjør det klart at problemet er ether. Det ville alltid vært slik.

Svar til SYN

Målet på dette stadiet er å generere en korrekt SYNACK-pakke med en fast seqnum, som i fremtiden vil bli erstattet av SYN-informasjonskapselen. Alle endringer skjer i process_tcp_syn() og omkringliggende områder.

Pakkebekreftelse

Merkelig nok, her er den mest bemerkelsesverdige linjen, eller rettere sagt, kommentaren til den:

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

Når du skrev den første versjonen av koden, ble 5.1-kjernen brukt, for verifikatoren som det var en forskjell mellom data_end и (const void*)ctx->data_end. I skrivende stund hadde ikke kjerne 5.3.1 dette problemet. Det er mulig at kompilatoren fikk tilgang til en lokal variabel annerledes enn et felt. Moralen i historien: Å forenkle koden kan hjelpe når det er mye hekking.

Neste er rutinemessige lengdesjekker for verifikatorens ære; 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 */
}

Bretter ut pakken

fylle seqnum и acknum, sett ACK (SYN er allerede satt):

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

Bytt ut TCP-porter, IP-adresser og MAC-adresser. Standardbiblioteket er ikke tilgjengelig fra XDP-programmet, så memcpy() — en makro som skjuler Clang-egenskapene.

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

Omberegning av kontrollsummer

IPv4- og TCP-sjekksummer krever tillegg av alle 16-bits ord i overskriftene, og størrelsen på overskriftene skrives inn i dem, det vil si ukjent på kompileringstidspunktet. Dette er et problem fordi verifikatoren ikke vil hoppe over den normale sløyfen til grensevariabelen. Men størrelsen på overskriftene er begrenset: opptil 64 byte hver. Du kan lage en løkke med et fast antall iterasjoner, som kan avsluttes tidlig.

Jeg merker at det er RFC 1624 om hvordan man delvis regner om sjekksummen hvis bare de faste ordene til pakkene endres. Metoden er imidlertid ikke universell, og implementeringen vil være vanskeligere å opprettholde.

Kontrollsumberegningsfunksjon:

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

Selv om size verifisert av anropskoden, er den andre utgangsbetingelsen nødvendig slik at verifikatoren kan bevise fullføringen av sløyfen.

For 32-bits ord er en enklere versjon implementert:

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

Faktisk beregne sjekksummene på nytt og sende pakken tilbake:

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;

Funksjon carry() lager en kontrollsum fra en 32-bits sum av 16-bits ord, i henhold til RFC 791.

TCP-håndtrykkverifisering

Filteret oppretter korrekt forbindelse med netcat, mangler den endelige ACK, som Linux svarte på med en RST-pakke, siden nettverksstakken ikke mottok SYN - den ble konvertert til SYNACK og sendt tilbake - og fra OS-synspunkt kom en pakke som ikke var relatert til åpen 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 viktig å sjekke med fullverdige søknader og observere tcpdump på xdp-remote fordi f.eks. hping3 svarer ikke på feil kontrollsummer.

Fra et XDP-synspunkt er selve verifiseringen triviell. Beregningsalgoritmen er primitiv og sannsynligvis sårbar for en sofistikert angriper. Linux-kjernen, for eksempel, bruker den kryptografiske SipHash, men implementeringen for XDP er klart utenfor rammen av denne artikkelen.

Introdusert for nye TODOer relatert til ekstern kommunikasjon:

  • XDP-programmet kan ikke lagre cookie_seed (den hemmelige delen av saltet) i en global variabel, trenger du lagring i kjernen, hvis verdi vil periodisk bli oppdatert fra en pålitelig generator.

  • Hvis SYN-informasjonskapselen samsvarer med ACK-pakken, trenger du ikke skrive ut en melding, men husk IP-en til den verifiserte klienten for å fortsette å sende pakker fra den.

Legitime klientbekreftelse:

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

Loggene viser at kontrollen bestått (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

Selv om det ikke er noen liste over bekreftede IP-er, vil det ikke være noen beskyttelse mot selve SYN-flommen, men her er reaksjonen på en ACK-flom som ble lansert av følgende kommando:

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

Loggoppføringer:

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

Konklusjon

Noen ganger presenteres eBPF generelt og XDP spesielt mer som et avansert administratorverktøy enn som en utviklingsplattform. Faktisk er XDP et verktøy for å forstyrre behandlingen av pakker av kjernen, og ikke et alternativ til kjernestabelen, som DPDK og andre kjernebypass-alternativer. På den annen side lar XDP deg implementere ganske kompleks logikk, som dessuten er enkel å oppdatere uten avbrudd i trafikkbehandlingen. Verifikatoren skaper ikke store problemer; personlig ville jeg ikke nekte dette for deler av brukerromskoden.

I den andre delen, hvis emnet er interessant, vil vi fullføre tabellen over bekreftede klienter og frakoblinger, implementere tellere og skrive et brukerområdeverktøy for å administrere filteret.

referanser:

Kilde: www.habr.com

Legg til en kommentar