Vi skriver beskyttelse mod DDoS-angreb på XDP. Kernekomponenten

eXpress Data Path (XDP) teknologien muliggør vilkårlig trafikbehandling på Linux-interfacer, før pakker når netværkskernen. Anvendelsen af XDP inkluderer beskyttelse mod DDoS-angreb (CloudFlare), komplekse filtre og indsamling af statistik (Netflix). XDP-programmer kører på den virtuelle maskine eBPF og har derfor begrænsninger både i deres kode og i de tilgængelige kernel-funktioner afhængigt af filtertypen.

Artiklen har til formål at udfylde hullerne i de mange materialer om XDP. For det første præsenteres færdig kode, der straks undgår XDP's særegenheder: den er klar til verifikation eller for simpel til at forårsage problemer. Når man forsøger at skrive sin egen kode fra bunden, er der ikke forståelse for, hvordan man håndterer de karakteristiske fejl. For det andet dækkes metoderne til lokalt at teste XDP uden VM og 'hardware', selvom de har deres egne 'faldgruber'. Teksten er rettet mod programmører, der er bekendte med netværk og Linux, og som er interesserede i XDP og eBPF.

I denne del vil vi grundigt undersøge, hvordan man samler et XDP-filter og hvordan man tester det. Derefter vil vi skrive en simpel version af den kendte SYN cookies mekanisme på pakkehåndteringsniveauet. Vi vil ikke danne en 'hvid liste' af
godkendte kunder, føre tællere og styre filteret - logs vil være tilstrækkeligt.

Vi vil skrive i C - det er ikke trendy, men praktisk. Al koden er tilgængelig på GitHub via linket i bunden og er opdelt i commits i henhold til de faser, der er beskrevet i artiklen.

Ansvarsfraskrivelse. I løbet af artiklen vil der blive udviklet en mini-løsning til at afværge DDoS-angreb, da dette er en realistisk opgave for XDP og mit ekspertiseområde. Den primære mål er dog at forstå teknologien; det er ikke en vejledning til at skabe en klar beskyttelse. Læringskoden er ikke optimeret og udelader visse nuancer.

Kort oversigt over XDP

Jeg vil kun fremhæve de vigtigste punkter for at undgå at duplikere dokumentation og eksisterende artikler.

Så koden til filteret indlæses i kernen. Indkommende pakker sendes til filteret. I sidste ende skal filteret træffe en beslutning: at tillade pakken i kernen (XDP_PASS), at kassere pakken (XDP_DROP) eller at sende den tilbage (XDP_TX). Filteret kan ændre pakken; dette er især relevant for XDP_TX. Det er også muligt at abruptly afslutte programmet (XDP_ABORTED) og kassere pakken, men det svarer til assert(0) - til debugging.

Den virtuelle maskine eBPF (extended Berkley Packet Filter) er designet til at være enkel, så kernen kan verificere, at koden ikke løber ind i uendelige sløjfer og ikke ødelægger andres hukommelse. Samlede begrænsninger og kontroller:

  • Sløjfer (bagudgange) er forbudt.
  • Der er en stak til data, men der er ingen funktioner (alle C-funktioner skal indbygges).
  • Adgang til hukommelse udenfor stakken og pakkebufferet er forbudt.
  • Kode størrelsen er begrænset, men i praksis er det ikke særligt betydningsfuldt.
  • Kun kald af specielle kernfunktioner (eBPF helpers) er tilladt.

Udvikling og installation af filteret ser således ud:

  1. Kildekoden (f.eks. kernel.c) kompileres til objektkode (kernel.o) til arkitekturen for den virtuelle eBPF-maskine. Fra oktober 2019 understøttes kompilering til eBPF af Clang og er lovet i GCC 10.1.
  2. Hvis der i denne objektkode er henvisninger til kernestrukturer (f.eks. til tabeller og tællere), vil deres ID'er være nuller, hvilket vil sige, at sådan kode ikke kan udføres. Før indlæsning i kernen skal disse nuller erstattes med ID'er for specifikke objekter, der er oprettet via kernens funktioner (linke koden). Dette kan gøres med eksterne værktøjer, eller man kan skrive et program, der linker og indlæser det specifikke filter.
  3. Kernen verificerer det indlæste program. Der kontrolleres for fravær af sløjfer og for overholdelse af pakkens og stakkens grænser. Hvis verifikatoren ikke kan bevise, at koden er korrekt, afvises programmet — man skal kunne tilfredsstille den.
  4. Efter en vellykket verifikation kompilerer kernen objektkoden til eBPF-arkitekturen til maskinkode for systemarkitekturen (just-in-time).
  5. Programmet vedhæftes til grænsefladen og begynder at behandle pakker.

Da XDP fungerer i kernen, foregår fejlretning gennem tracing logs og faktisk de pakker, programmet filtrerer eller genererer. Ikke desto mindre sikrer eBPF sikkerheden af den indlæste kode for systemet, så man kan eksperimentere med XDP direkte på lokal Linux.

Forberedelse af miljøet

Kompilering

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

  1. Kompilér C-kode til LLVM bytecode (clang -emit-llvm).
  2. Konverter bytekodet til eBPF objektkode (llc -march=bpf -filetype=obj).

Når man skriver et filter, vil et par filer med hjælpefunktioner og makroer fra kernetests. Det er vigtigt, at de stemmer overens med kernel-versionen (KVER). Henter dem i 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 kernel-overskrifterne, ARCH — systemets arkitektur. Stierne og værktøjerne 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 inkluderer mappen med hjælpeoverskrifter og flere mapper med kernel-overskrifter. Symbolet __KERNEL__ betyder, at UAPI (userspace API) overskrifterne defineres for kernkoden, da filteret kører i kernen.

Stakbeskyttelsen kan slås fra (-fno-stack-protector), da eBPF kodeverifikatoren stadig kontrollerer for stakoverskridelse. Det er en god idé at aktivere optimeringsmuligheder med det samme, da størrelsen på eBPF bytecode er begrænset.

Lad os starte med et filter, der tillader 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";

Kommandoen make bygger xdp_filter.o. Hvor skal det nu testes?

Testbænk

Opsætningen skal have to grænseflader: en hvor filteret vil være, og en hvorfra pakkerne vil blive sendt. Det skal være rigtige Linux-enheder med deres egne IP'er for at teste, hvordan almindelige applikationer fungerer med vores filter.

Veth-enheder (virtual Ethernet) er velegnede: det er et par virtuelle netværksgrænseflader, der er "forbundet" direkte. De kan oprettes sådan (i dette afsnit udføres alle kommandoer ip som køres af root):

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

Her xdp-remote og xdp-local — enhedernes navne. På xdp-local (192.0.2.1/24) vil filteret være tilsluttet, og xdp-remote (192.0.2.2/24) vil den indkommende trafik blive sendt. Men der er et problem: grænsefladerne er på samme maskine, og Linux vil ikke sende trafik fra en af dem over til den anden. Dette kan løses med intelligente regler, iptablesmen det vil ændre pakkerne, hvilket er ubelejligt under debugging. Det er bedre at bruge netværksnavne rum (network namespaces, herefter netns).

Netværksnavnerummet indeholder et sæt interfaces, routing-tabeller og NetFilter-regler, der er isoleret fra lignende objekter i andre netns. Hver proces kører i et bestemt navnerum og har kun adgang til objekterne i dette netns. Som standard er der i systemet kun ét netværksnavnerum for alle objekter, så man kan arbejde i Linux uden at kende til netns.

Lad os oprette et nyt navnerum xdp-test og flytte det dertil xdp-remote.

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

Så vil processen, der kører i xdp-test, ikke "se" xdp-local (den forbliver i netns som standard) og ved afsendelse af et paket til 192.0.2.1 vil det sende det gennem xdp-remote, fordi det er den eneste interface i 192.0.2.0/24, der er tilgængelig for denne proces. Dette gælder også i den modsatte retning.

Når man flytter mellem netns, fjernes interfacen og mister sin adresse. For at konfigurere interfacen i netns, skal man køre ip ... i dette navnerum via kommandolinjen 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 man kan se, adskiller dette sig ikke fra konfigurationen xdp-local i det standard navnerum:

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

Hvis man kører tcpdump -tnevi xdp-local, kan man se, at pakker sendt fra xdp-test, leveres til denne interface:

ip netns exec xdp-test   ping 192.0.2.1

Det er praktisk at starte en shell i xdp-test. I repository'et er der et script, der automatiserer arbejdet med standen, for eksempel kan man opsætte standen med kommandoen sudo ./stand up og fjerne den sudo ./stand down.

Traceroute

Filteret bindes til enhed som følger:

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

Nøgle -force er nødvendigt for at binde et nyt program, hvis et andet allerede er bundet. "No news is good news" gælder ikke for denne kommando, outputtet er uanset omfattende. At angive verbose er valgfrit, men med det får man en rapport om verifieringsværktøjets arbejde med en assembler-listing:

Verifieringsanalyse:

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

At afbinde programmet fra interfacen:

ip link set dev xdp-local xdp off

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

Når filteret er bundet, kan man sikre sig, at ping fortsætter med at fungere, men virker programmet? Lad os tilføje logs. Funktionen bpf_trace_printk() ligner printf(), men understøtter kun op til tre argumenter udover skabelonen, og en begrænset liste af specifikatorer. Makroen bpf_printk() forenkler opkaldet.

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

Output goes to the core tracing channel that needs to be enabled:

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

Viewing the message stream:

cat /sys/kernel/debug/tracing/trace_pipe

Both of these commands invoke sudo ./stand log.

Ping should now generate messages like this:

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

If you take a closer look at the verifier's output, you may notice strange calculations:

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

The thing is that eBPF programs do not have a data section, so the only way to encode a format string is through immediate arguments of the VM commands:

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

For this reason, debug output greatly inflates the final code.

Sending XDP packets

Let's change the filter: let it send all incoming packets back. This is incorrect from a networking perspective, as it would need to change addresses in the headers, but right now, the principle of operation is what matters.

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

Starting tcpdump og få en fungerende kommando. xdp-remote. It should show identical outgoing and incoming ICMP Echo Requests and stop showing ICMP Echo Replies. But it doesn't show. It turns out that for it to work XDP_TX in the program on xdp-local it's necessary, for the paired interface xdp-remote to also have a program assigned to it, even if it's just an empty one, and it must be activated.

How did I find this out?

Tracing the packet path in the kernel is possible via the perf events mechanism, which, by the way, also uses the same virtual machine, that is, for debugging eBPF, eBPF is used.

You must do good out of evil because there's no other source to create it.

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

What is code 6?

$ errno 6
ENXIO 6 No such device or address

Funktion veth_xdp_flush_bq() receives the error code from veth_xdp_xmit(), where searching for ENXIO and find the comment.

Let's restore the minimal filter (XDP_PASS) in the file xdp_dummy.c, add it to the Makefile, link it to xdp-remote:

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

Now tcpdump it shows what is expected:

62:57:8e:70:44:64 > 26:0e:25:37:8f:96, ethertype IPv4 (0x0800), længde 98: (tos 0x0, ttl 64, id 13762, offset 0, flags [DF], proto ICMP (1), længde 84)
    192.0.2.2 > 192.0.2.1: ICMP echo request, id 46966, seq 1, længde 64
62:57:8e:70:44:64 > 26:0e:25:37:8f:96, ethertype IPv4 (0x0800), længde 98: (tos 0x0, ttl 64, id 13762, offset 0, flags [DF], proto ICMP (1), længde 84)
    192.0.2.2 > 192.0.2.1: ICMP echo request, id 46966, seq 1, længde 64

Hvis der i stedet kun vises ARP, skal filtre fjernes (det gør sudo ./stand detach), sende ping, derefter sætte filtre og prøve igen. Problemet er, at filteret XDP_TX virker også på ARP, og hvis stakken
af navneområder xdp-test er blevet "glemt" MAC-adressen 192.0.2.1, kan den ikke opløse denne IP.

Problemformulering

Lad os gå videre til den angivne opgave: at skrive en XDP-mekanisme for SYN-cookies.

Indtil videre er SYN flood en populær DDoS-angrebsmetode, hvis essens er som følger. Ved oprettelse af forbindelse (TCP handshake) modtager serveren SYN, tildeler ressourcer til den kommende forbindelse, svarer med et SYNACK-pakke og venter på ACK. Angriberen sender blot SYN-pakker fra falske adresser i tusindvis hvert sekund fra hver vært i et kæmpe botnet. Serveren skal tildeles ressourcer straks ved ankomsten af pakken, men frigiver dem først efter en lang timeout, hvilket resulterer i, at hukommelsen eller grænserne er udtømt, nye forbindelser accepteres ikke, og tjenesten er utilgængelig.

Hvis der ikke tildeles ressourcer pr. SYN-pakke, men blot svarer med et SYNACK-pakke, hvordan skal serveren så forstå, at ACK-pakken, der kommer senere, hører til SYN-pakken, som ikke blev gemt? For angriberen kan generere falske ACK. Essensen af SYN cookie er at kode i seqnum forbindelsesparametre som en hash af adresser, porte og en skiftende salt. Hvis ACK er kommet før saltet skifter, kan man beregne hashen igen og sammenligne med acknum. At falske det acknum kan angriberen ikke, da saltet inkluderer en hemmelighed, og han ikke når at prøve det på grund af den begrænsede kanal.

SYN cookie er længe blevet implementeret i Linux-kernen og kan endda aktiveres automatisk, hvis SYN ankomme for hurtigt og i store mængder.

Introduktion til TCP handshake.

TCP sikrer overførsel af data som en strøm af byte, for eksempel overføres HTTP-anmodninger over TCP. Strømmen overføres i stykker i pakker. Alle TCP-pakker har logiske flags og 32-bit sekvensnumre:

  • Flagkombinationen bestemmer rollen for en bestemt pakke. Flag SYN angiver, at dette er den første pakke sendt af afsenderen i forbindelsen. Flag ACK betyder, at afsenderen har modtaget alle data i forbindelsen op til byte acknum. En pakke kan have flere flags og kaldes efter deres kombination, for eksempel en SYNACK-pakke.

  • Sekvensnummeret (seqnum) bestemmer forskydningen i datastreamen for den første byte, der sendes i denne pakke. For eksempel, hvis det første pakke med X bytes data havde dette nummer N, vil det næste pakke med nye data have N+X. I starten af forbindelsen vælger hver side dette nummer tilfældigt.

  • Anerkendelsesnummeret (acknum) er en tilsvarende forskydning som seqnum, men bestemmer ikke nummeret på den sendte byte, men nummeret på den første byte fra modtageren, som afsenderen ikke har set.

I starten af forbindelsen skal parterne blive enige seqnum og acknum. Klienten sender en SYN-pakke med sin seqnum = X. Serveren svarer med en SYNACK-pakke, hvor den skriver sin seqnum = Y og viser acknum = X + 1. Klienten reagerer på SYNACK med en ACK-pakke, hvor seqnum = X + 1, acknum = Y + 1. Derefter begynder selve dataoverførslen.

Hvis modparten 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 man vente på genudsendelse - det bremser etableringen af forbindelsen. For det andet, i SYN-pakken - og kun i den! - sendes en række muligheder, der påvirker den videre drift af forbindelsen. Uden at huske indgående SYN-pakker ignorerer serveren således disse muligheder, og i de følgende pakker sender klienten dem ikke længere. TCP kan stadig fungere, men i det mindste i begyndelsesfasen vil kvaliteten af forbindelsen falde.

Set fra pakkers synspunkt skal XDP-programmet gøre følgende:

  • svare på SYN med SYNACK og cookie;
  • svare på ACK med RST (afbryde forbindelsen);
  • forkaste de øvrige pakker.

Pseudo-koden af algoritmen sammen med analysen af pakken:

Hvis det ikke er Ethernet,
    skal pakken overses.
Hvis det ikke er IPv4,
    skal pakken overses.
Hvis adressen er i den verificerede tabel,               (*)
        reducer counter for resterende kontroller,
        skal pakken overses.
Hvis det ikke er TCP,
    nulstil pakken.  (**)
Hvis det er SYN,
    svar med SYN-ACK og cookie.
Hvis det er ACK,
    hvis acknum ikke indeholder cookie,
        nulstil pakken.
    Indtast adressen i tabellen med N resterende kontroller.    (*)
    Svar med RST.   (**)
I alle andre tilfælde nulstil pakken.

En (*) markerede punkter, hvor der er behov for at styre systemets tilstand — i første omgang kan vi undvære dem, bare ved at implementere TCP handshake med generering af SYN cookie som seqnum.

På stedet (**), mens vi ikke har nogen tabel, vil vi oversætte pakken.

Implementering af TCP handshake

Parsing af pakken og verifikation af koden

Vi får brug for data strukturer til netværkshoveder: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) og TCP (uapi/linux/tcp.h). Den sidste kunne jeg ikke tilslutte på grund af fejl relateret til atomic64_t, og jeg var nødt til at kopiere de nødvendige definitioner ind i koden.

Alle funktioner, der i C tildeles for læsbarhed, skal indlægges på kaldestedet, da eBPF-verificatoren i kernen forbinder bagudgående spring, hvilket vil sige, reelt, cyklusser og funktionskald.

#define INTERNAL static __attribute__((always_inline))

Makro LOG() deaktiverer udskrift i udgivelsessamlingen.

Programmet repræsenterer en pipeline af funktioner. Hver tager en pakke, i hvilken den tilsvarende lag hoved er allokeret, for eksempel, process_ether() forventes at være fyldt ether. Baseret på analyse af felterne kan funktionen videresende pakken til et højere niveau. Resultatet af funktionens arbejde — handling XDP. Indtil videre oversætter SYN og ACK 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; /* malformet pakke */
    }

    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 opmærksomheden på de kontroller, der er markeret A og B. Hvis A kommenteres, vil programmet kunne kompilere, men der vil opstå en verifikationsfejl ved indlæsning:

Verifier analysen:


11: (7b) *(u64 *)(r10 -48) = r1
12: (71) r3 = *(u8 *)(r7 +13)
ugyldig adgang til pakken, off=13 størrelse=1, R7(id=0,off=0,r=0)
R7 offset er uden for pakken
behandlet 11 insns (grænse 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0

Fejl ved hentning af program/map!

Nøglelinje ugyldig adgang til pakken, off=13 størrelse=1, R7(id=0,off=0,r=0): der er udførelsesveje, hvor den trettende byte fra starten af bufferen ligger uden for pakken. Det er svært at forstå, hvilken linje der henvises til ud fra oplistningen, men der er et instruktion nummer (12) og en disassembler, der viser kodelinjerne:

llvm-objdump -S xdp_filter.o | less

I dette tilfælde henviser den til linjen

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

ud fra hvilken det er klart, at problemet ligger i ether. Det ville altid være sådan.

Svar på SYN

Målet på dette stadium er at danne et korrekt SYNACK-pakke med en fast seqnum, som senere vil blive erstattet med SYN cookie. Alle ændringer sker i process_tcp_syn() og omegn.

Pakkeverifikation

Mærkeligt nok er dette den mest bemærkelsesværdige linje, nærmere bestemt kommentaren til den:

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

Ved skrivningen af den første version af koden blev kernemodul 5.1 brugt, som for verifikatoren havde forskel mellem data_end og (const void*)ctx->data_end. Ved skrivningen af artiklen havde kernemodul 5.3.1 ikke sådanne problemer. Måske henvendte kompilereren sig til den lokale variabel anderledes end til feltet. Moralen - ved stor indlejring kan forenkling af koden hjælpe.

Dernæst rutinekontroller af længder til ære for verifikatoren; om MAX_CSUM_BYTES nedenfor.

const u32 ip_len = ip->ihl * 4;
if ((void*)ip + ip_len > data_end) {
    return XDP_DROP; /* malformet pakke */
}
if (ip_len > MAX_CSUM_BYTES) {
    return XDP_ABORTED; /* implementeringsbegrænsning */
}

const u32 tcp_len = tcp->doff * 4;
if ((void*)tcp + tcp_len > (void*)ctx->data_end) {
    return XDP_DROP; /* malformet pakke */
}
if (tcp_len > MAX_CSUM_BYTES) {
    return XDP_ABORTED; /* implementeringsbegrænsning */
}

Pakkerotation

Vi fylder seqnum og acknum, vi sætter ACK (SYN er allerede sat):

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

Vi bytter TCP-porte, IP-adresse og MAC-adresser. Det er ikke muligt at bruge standardbiblioteket fra XDP-programmet, så memcpy() er en makro, der skjuler Clang intrinsics.

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, at man lægger alle 16-bit ord i overskrifterne sammen, og størrelsen af overskrifterne er angivet i dem, hvilket vil sige, at den er ukendt på tidspunktet for kompileringen. Dette er et problem, fordi verifikatoren ikke vil godkende en almindelig løkke op til den variable grænse. Men størrelsen af overskrifterne er begrænset: op til 64 byte hver. Man kan lave en løkke med et fast antal iterationer, som kan afsluttes tidligt.

Jeg bemærker, at der er RFC 1624 om hvordan man delvist beregner kontrolsummen, hvis kun faste ord i pakkerne er ændret. Metoden er dog ikke universel, og implementeringen ville være vanskeligere at vedligeholde.

Funktionen til beregning af kontrolsummen:

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

På trods af at størrelse er tjekket af den kaldende kode, er det andet udgangsbetingelse nødvendigt for at verifikatoren kan bevise, at løkken er afsluttet.

For 32-bit ord er der implementeret en enklere version:

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

Selve genberegningen af kontrolsummer og afsendelse af 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 <check = 0;
tcp_csum += sum16(tcp, tcp_len, data_end);
tcp->check = carry(tcp_csum);

return XDP_TX;

Funktion carry() gør 32-bit summen af 16-bit ord til kontrolsummen ifølge RFC 791.

Kontrol af TCP handshake

Filtreret opretter korrekt forbindelse til netcat, og springer den afsluttende ACK over, hvilket Linux svarede med et RST-pakke, da netværksstakken ikke modtog SYN – det blev omdannet til SYNACK og sendt tilbage – og fra OS' perspektiv ankom pakken, som ikke tilhørte åbne forbindelser.

$ sudo ip netns exec xdp-test   nc -nv 192.0.2.1 6666
192.0.2.1 6666: Forbindelse nulstillet af peer

Det er vigtigt at teste med rigtige applikationer og observere tcpdump og få en fungerende kommando. xdp-remote fordi, for eksempel, hping3 ikke reagerer på forkert kontrolsummer.

Set fra XDP's synspunkt er selve kontrollen triviel. Beregningsalgoritmen er primitiv og sandsynligvis sårbar overfor en sofistikeret angriber. Linux-kernen bruger for eksempel kryptografisk SipHash, men dens implementering til XDP ligger klart uden for rammerne af artiklen.

Der er dukket nye TODO'er op relateret til ekstern interaktion:

  • XDP-programmet kan ikke gemme cookie_seed (den hemmelige del af saltet) i en global variabel; der skal være opbevaring i kernen, hvis værdi periodisk opdateres fra en pålidelig generator.

  • Når SYN-cookie i ACK-pakken matcher, skal der ikke printes en besked, men IP'en på den verificerede klient skal huskes, så pakker fra den kan passere senere.

Verifikation som legitim klient:

$ sudo ip netns exec xdp-test nc -nv 192.0.2.1 6666
192.0.2.1 6666: Forbindelsen blev nulstillet af modparten

I loggen er gennemgangen af verifikation registreret (flags=0x2 — det er SYN, flags=0x10 — det 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 matcher for klient 20200c0

Indtil der er en liste over verificerede IP'er, vil der ikke være beskyttelse mod egentlige SYN flood, men her er reaktionen på ACK flood, der aktiveres med følgende kommando:

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

Postinger i loggen:

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

Konklusion

Nogle gange betragtes eBPF generelt og XDP i særdeleshed snarere som et værktøj for den avancerede administrator end som en platform til udvikling. Faktisk er XDP et værktøj til indblanding i pakkehåndteringen fra kernen, ikke en alternativ kerne-stak som DPDK og andre varianter af kernel bypass. På den anden side gør XDP det muligt at implementere temmelig kompleks logik, som desuden er let at opdatere uden pause i trafikbehandlingen. Verifikatoren skaber ikke store problemer, personligt ville jeg ikke have noget imod sådan en for dele af userspace-koden.

I den anden del, hvis emnet er interessant, vil vi færdiggøre tabellen over verificerede klienter og forbindelsens afbrydelser, implementere tællere og skrive en userspace-utility til at administrere filteret.

Links:

Kilde: habr.com

Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere 🔥 Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere | ProHoster