Skriver skydd mot DDoS-attacker på XDP. Kärndel

Ett XDP (eXpress Data Path) gör det möjligt att behandla trafik på Linux-gränssnitt innan paketen når kärnans nätverksstack. XDP används för DDoS-skydd (CloudFlare), komplexa filter och insamling av statistik (Netflix). XDP-program körs på den virtuella maskinen eBPF, vilket innebär begränsningar både för koden och tillgängliga kärnfunktioner beroende på filtret.

Den här artikeln syftar till att fylla de brister som finns i många av de material som handlar om XDP. För det första levererar dessa färdig kod som omedelbart kringgår XDP:s särdrag: den är anpassad för verifiering eller så simpel att den inte orsakar problem. När man senare försöker skriva egen kod från grunden saknas förståelsen för hur man hanterar typiska misstag. För det andra behandlas inte metoder för att lokalt testa XDP utan virtuell maskin och 'hårdvara', trots att de har sina egna 'fallgropar'. Texten riktar sig till programmerare som är bekanta med nätverk och Linux och som är intresserade av XDP och eBPF.

I den här delen ska vi noggrant gå igenom hur ett XDP-filter byggs och hur det testas, och sedan skriva en enkel variant av den kända mekanismen SYN cookies på paketbehandlingsnivå. Vi kommer för tillfället inte att skapa en 'vit lista'.
av verifierade kunder, hålla räkningar och hantera filtret - loggar räcker.

Vi kommer att skriva på C - det är inte trendigt, men praktiskt. All kod är tillgänglig på GitHub via länken i slutet och är uppdelad i commits enligt de steg som beskrivs i artikeln.

Ansvarsfriskrivning. Under artikelns gång kommer en minilösning för att avvisa DDoS-attacker att utvecklas, eftersom det är en realistisk uppgift för XDP och mitt område. Huvudmålet är dock att förstå teknologin; detta är inte en guide för att skapa färdigt skydd. Lärandekoden är inte optimerad och utelämnar vissa nyanser.

Kort översikt av XDP.

Jag kommer endast att ta upp de viktigaste punkterna för att inte duplicera dokumentationen och befintliga artiklar.

Så, koden för filtret laddas in i kärnan. Ingående paket överlämnas till filtret. Resultatet bör filtret besluta: antingen släppa paketet till kärnan (XDP_PASS), droppa paketet (XDP_DROP) eller skicka tillbaka det (XDP_TX). Filtret kan ändra paketet, vilket är särskilt relevant för XDP_TX. Det går också att akut avbryta programmet (XDP_ABORTED) och droppa paketet, men detta är motsvarande assert(0) — för felsökning.

Den virtuella maskinen eBPF (extended Berkley Packet Filter) är speciellt utformad för att vara enkel, så att kärnan kan kontrollera att koden inte går i oändlig loop och inte skadar annan minnesanvändning. Sammanlagda begränsningar och kontroller:

  • Cirklar (bakåtriktade övergångar) är förbjudna.
  • Det finns en stack för data, men inga funktioner (alla C-funktioner måste vara inbäddade).
  • Åtkomst till minne utanför stacken och paketbufferten är förbjuden.
  • Kodens storlek är begränsad, men i praktiken är detta inte särskilt betydelsefullt.
  • Endast anrop av speciella kärnfunktioner (eBPF helpers) är tillåtna.

Utveckling och installation av filter ser ut så här:

  1. Källkoden (till exempel, kernel.c) kompileras till objektkod (kernel.o) för arkitekturen av den virtuella eBPF-maskinen. Från och med oktober 2019 stöds kompilering till eBPF av Clang och lovas i GCC 10.1.
  2. Om det finns åtkomst i denna objektkod till kärnstrukturer (till exempel, till tabeller och räknare), så stå istället för deras ID nollor, vilket innebär att sådan kod inte kan köras. Innan den laddas in i kärnan måste dessa nollor bytas ut mot ID för specifika objekt, skapade genom kärnan (länka koden). Detta kan göras med externa verktyg eller så kan man skriva ett program som kommer att länka och ladda det specifika filtret.
  3. Kärnan verifierar det inladdade programmet. Det kontrolleras att det inte finns några cykler och att man inte går utanför paketets och stackens gränser. Om verifieraren inte kan bevisa att koden är korrekt, avvisas programmet – man måste kunna vinna dess gillande.
  4. Efter en lyckad verifiering kompilerar kärnan objektkoden för eBPF-arkitekturen till maskinkod för systemarkitekturen (just-in-time).
  5. Programmet kopplas till gränssnittet och börjar bearbeta paket.

Eftersom XDP fungerar i kärnan, görs felsökning genom loggar från spårning samt genom de paket som programmet filtrerar eller genererar. Ändå garanterar eBPF säkerheten för den inladdade koden för systemet, så man kan experimentera med XDP direkt på en lokal Linux.

Förberedelse av miljön

Samling

Clang kan inte direkt producera objektkod för eBPF-arkitekturen, så processen består av två steg:

  1. Kompilera C-koden till LLVM byte-kod (clang -emit-llvm).
  2. Omvandla byte-koden till eBPF-objektkod (llc -march=bpf -filetype=obj).

När man skriver ett filter är det bra att ha ett par filer med hjälpfunktioner och makron från kärnans tester. Det är viktigt att de motsvarar kärnans version (KVER). Ladda ner dem till 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 för Arch Linux (kärna 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 innehåller sökvägen till kärnans rubriker, ARCH är systemarkitekturen. Sökvägar och verktyg kan variera något mellan distributioner.

Exempel på skillnader för Debian 10 (kärna 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 inkluderar katalogen med hjälpbibliotekens rubriker och flera kataloger med kärnans rubriker. Symbolen __KERNEL__ indikerar att UAPI-rubriker (userspace API) definieras för kärnkoden, eftersom filtret körs i kärnan.

Stackskydd kan inaktiveras (-fno-stack-protector), eftersom eBPF-kodverifieraren fortfarande kontrollerar gränserna för stacken. Optimiseringar bör aktivt slås på, eftersom storleken på eBPF-bytkod är begränsad.

Låt oss börja med en filter som tillåter alla paket och inte gör något:

#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 bygger xdp_filter.o. Var kan vi nu testa det?

Testbänk

Stellan bör inkludera två gränssnitt: ett där filtret kommer att appliceras och ett från vilket paket kommer att skickas. Dessa bör vara hela Linux-enheter med sina egna IP-adresser för att kontrollera hur vanliga applikationer fungerar med vårt filter.

Enheter av typen veth (virtuell Ethernet) fungerar för oss: det är ett par virtuella nätverksgränssnitt som "kopplas" samman direkt. De kan skapas så här (i detta avsnitt utförs alla kommandon ip av root):

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

Här xdp-remote och xdp-local är namnen på enheterna. På xdp-local (192.0.2.1/24) kommer filtret att kopplas, medan xdp-remote (192.0.2.2/24) kommer att användas för inkommande trafik. Men det finns ett problem: gränssnitten är på samma maskin, och Linux kommer inte att skicka trafik till ett av dem genom det andra. Detta kan lösas med kluriga regler, iptablesmen det skulle kräva att man ändrar paketen, vilket är opraktiskt vid felsökning. Det är bättre att använda nätverksnamnrum (network namespaces, nedan netns).

Nätverksnamnrymden innehåller en uppsättning gränssnitt, routingtavlor och NetFilter-regler, isolerade från liknande objekt i andra netns. Varje process körs i ett visst namnrymd, och den har endast tillgång till objekt i denna netns. Som standard finns det ett enda nätverksnamnrymd för alla objekt i systemet, så man kan arbeta i Linux utan att känna till netns.

Låt oss skapa ett nytt namnrymd xdp-test och flytta dit xdp-remote.

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

Då kommer processen som körs i xdp-test, inte att «se» xdp-local (den kommer att förbli i standard netns) och vid sändning av ett paket till 192.0.2.1 kommer det att överföras genom xdp-remote, eftersom det är det enda gränssnittet i 192.0.2.0/24 som är tillgängligt för denna process. Detta gäller även för den motsatta riktningen.

Vid flyttning mellan netns avregistreras gränssnittet och förlorar sin adress. För att konfigurera gränssnittet i netns måste man köra ip ... i detta namnrymd via kommandot 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 skiljer det sig inte från konfigurationen xdp-local i standard namnrymd:

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

Om du kör tcpdump -tnevi xdp-local, man kan se att paketen som skickas från xdp-test, levereras till detta gränssnitt:

ip netns exec xdp-test   ping 192.0.2.1

Det är bekvämt att köra en shell i xdp-test. I arkivet finns ett skript som automatiserar arbetet med stand, till exempel kan man konfigurera standen med kommandot sudo ./stand up och ta bort den sudo ./stand down.

Traceroute

Filterbinder sig till enheten så här:

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

Nyckeln -force krävs för att binda ett nytt program om ett annat redan är bundet. «No news is good news» gäller inte för detta kommando, utdata är omfattande oavsett. Att ange verbose är valfritt, men med det får man en rapport om verifierarens arbete med en lista över assemblerkoden:

Verifieraranalys:

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

Att avbinda programmet från gränssnittet:

ip link set dev xdp-local xdp off

I skriptet är det kommandon sudo ./stand attach och sudo ./stand detach.

När filtret har bundits kan man säkerställa att ping fortsätter att fungera, men fungerar programmet? Låt oss lägga till loggar. Funktionen bpf_trace_printk() liknar på printf(), men stödjer högst tre argument utöver mallen, och en begränsad lista över specifikatorer. Makrot bpf_printk() förenklar anropet.

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

Utdata går till kärnans spårningskanal, som måste aktiveras:

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

Visa meddelandeströmmen:

cat /sys/kernel/debug/tracing/trace_pipe

Båda dessa kommandon gör ett anrop sudo ./stand log.

Ping bör nu generera sådana meddelanden:

-110930 [004] ..s1 78803.244967: 0: fick paket: 00000000ac510377

Om man ser noga på verifierarens utdata, kan man notera konstiga beräkningar:

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

Problemet är att program på eBPF saknar en datasektion, så det enda sättet att koda formatsträngen är via omedelbara argument i VM-kommandon:

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

Av denna anledning blåser debug-utdata upp den slutliga koden kraftigt.

Skicka XDP-paket

Låt oss ändra filtret: låt det skicka tillbaka alla inkommande paket. Detta är oetiskt ur nätverkssynpunkt, eftersom man skulle behöva ändra adresser i rubrikerna, men nu är själva funktionen viktig.

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

Starta Fungerar libdns korrekt? => Kör till xdp-remote. Det bör visa identiska utgående och inkommande ICMP Echo Request och sluta visa ICMP Echo Reply. Men det gör det inte. Det visar sig att för att fungera XDP_TX i programmet på xdp-local krävs, så att det parande gränssnittet xdp-remote också tilldelades ett program, åtminstone ett tomt, och att det varit upprättat.

Hur visste jag detta?

Spåra paketets väg i kärnan möjliggör mekanismen för perf-events, för övrigt samma virtuell maskin som används, det vill säga för analysering av eBPF används eBPF.

Du måste göra gott av det onda, för det finns inget annat att göra det 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])

Vad är kod 6?

$ errno 6
ENXIO 6 Ingen sådan enhet eller adress

Funktionen veth_xdp_flush_bq() får felkod från veth_xdp_xmit(), där vi söker efter ENXIO och hittar kommentaren.

Återställ det minimi filtret (XDP_PASS) i filen xdp_dummy.c, lägg till det i Makefile, länka det till xdp-remote:

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

Nu Fungerar libdns korrekt? => Kör visar det som förväntas:

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

Om endast ARP visas istället, måste filtren tas bort (detta görs sudo ./stand detach), släppas ping, och sedan installera filtren och försöka igen. Problemet är att filtret XDP_TX gäller både ARP och om stacken
namnrymd xdp-test har «glömt» MAC-adressen 192.0.2.1, kommer den inte att kunna lösa denna IP.

Uppgiftsställande

Låt oss gå vidare till den angivna uppgiften: skriva en XDP-mekanism för SYN-cookies.

Fortfarande är SYN flood en populär DDoS-attack, vars essens är följande. När en anslutning upprättas (TCP-handshake) får servern en SYN, tilldelar resurser för den framtida anslutningen, svarar med ett SYNACK-paket och väntar på ACK. Angriparen skickar bara SYN-paket från falska adresser i tusentals per sekund från varje värd i ett tiotusental botnät. Servern tvingas tilldela resurser omedelbart vid paketets ankomst, och frigör dem först efter en lång timeout, vilket resulterar i uttömning av minnet eller begränsningar, nya anslutningar accepteras inte, tjänsten är otillgänglig.

Om man inte tilldelar resurser per SYN-paket, utan endast svarar med ett SYNACK-paket, hur ska servern då förstå att ACK-paketet som kommer senare hänför sig till SYN-paketet som inte sparades? Eftersom angriparen kan generera och falska ACK. Essensen av SYN-cookie är att koda i seqnum anslutningsparametrar som en hash av adresser, portar och en varierande salt. Om ACK har kommit innan saltet har ändrats, kan man räkna hash igen och jämföra med acknum. Att förfalska acknum kan angriparen inte, eftersom saltet inkluderar en hemlighet, och han hinner inte prova alla möjliga alternativ på grund av den begränsade bandbredden.

SYN-cookies har länge införts i Linux-kärnan och kan till och med automatiskt aktiveras om SYN kommer för snabbt och massivt.

Grundläggande information om TCP-handshake

TCP säkerställer överföring av data som en ström av byte, till exempel överförs HTTP-förfrågningar över TCP. Strömmen överförs i bitar i paket. Alla TCP-paket har logiska flaggor och 32-bitars sekvensnummer:

  • Kombinationen av flaggor definierar rollen för det specifika paketet. FLAGGEN SYN betyder att detta är det första paketet från avsändaren i anslutningen. FLAGGEN ACK betyder att avsändaren har mottagit all data om anslutningen fram till byte acknum. Paketet kan ha flera flaggor och kallas efter deras kombination, till exempel SYNACK-paket.

  • Sekvensnummer (seqnum) definierar förskjutningen i datastreamen för den första byten som överförs i detta paket. Till exempel, om sekvensnumret i det första paketet med X byte data var N, kommer det i nästa paket med nya data att vara N+X. I början av anslutningen väljer varje part detta nummer slumpmässigt.

  • Bekräftelsenummer (acknum) är en lika stor förskjutning som seqnum, men det definierar inte numret på den överförda byten utan numret på den första byten från mottagaren som avsändaren inte har sett.

I början av anslutningen måste parterna komma överens seqnum och acknum. Klienten skickar ett SYN-paket med sin seqnum = X. Servern svarar med ett SYNACK-paket där den skriver sin seqnum = Y och ställer in acknum = X + 1. Klienten svarar på SYNACK med ett ACK-paket där seqnum = X + 1, acknum = Y + 1. Efter detta börjar själva dataöverföringen.

Om motparten inte bekräftar mottagandet av paketet, kommer TCP att skicka det igen efter en timeout.

Varför används inte SYN-cookies alltid?

För det första, om SYNACK eller ACK går förlorad, måste man vänta på att det ska skickas igen - vilket fördröjer etableringen av anslutningen. För det andra, i SYN-paketet - och endast i detta! - överförs en mängd alternativ som påverkar den fortsatta användningen av anslutningen. Genom att inte komma ihåg inkommande SYN-paket ignorerar servern således dessa alternativ och i följande paket kommer klienten inte att skicka dem. TCP kan fortfarande fungera, men åtminstone i det initiala skedet kommer kvaliteten på anslutningen att försämras.

Ur paketens synvinkel måste XDP-programmet göra följande:

  • svara på SYN med SYNACK med cookie;
  • svara på ACK med RST (avsluta anslutningen);
  • övriga paket ska slängas.

Pseudokod för algoritmen tillsammans med paketanalys:

Om det inte är Ethernet,
    hoppa över paketet.
Om det inte är IPv4,
    hoppa över paketet.
Om adressen finns i den verifierade tabellen,               (*)
        minska antalet kvarvarande kontroller,
        hoppa över paketet.
Om det inte är TCP,
    släng paketet.     (**)
Om det är SYN,
    svara med SYN-ACK med cookie.
Om det är ACK,
    om acknum inte innehåller cookie,
        släng paketet.
    Skriv in adressen i tabellen med N kvarvarande kontroller.    (*)
    Svara med RST.   (**)
I övriga fall släng paketet.

En (*) markerade punkter där det behövs hantera systemets tillstånd - i det första skedet kan vi klara oss utan dem genom att helt enkelt implementera TCP handshake med generering av SYN cookie som seqnum.

På plats (**), så länge vi inte har någon tabell, kommer vi att hoppa över paketet.

Genomförande av TCP-handshake

Analys av paket och verifiering av kod

Vi behöver strukturer för nätverkshuvuden: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) och TCP (uapi/linux/tcp.h). Jag lyckades aldrig koppla in den sista på grund av fel relaterade till atomic64_t, och var tvungen att kopiera de nödvändiga definitionerna till koden.

Alla funktioner som i C framhävs för att underlätta läsningen, måste inbäddas på plats vid anropet, eftersom verifieraren eBPF i kärnan förbjuder bakåtövergångar, det vill säga, faktiska loopar och funktionsanrop.

#define INTERNAL static __attribute__((always_inline))

Makro LOG() inaktiverar utskrift i releaseversionen.

Programmet består av en kedja av funktioner. Varje funktion tar ett paket där motsvarande nivåhuvud är uttalat, till exempel, process_ether() förväntar sig att etherär ifyllt. Baserat på analysresultaten kan funktionen överföra paketet till en högre nivå. Resultatet av funktionen är XDP-åtgärden. Just nu passerar hanterarna för SYN och ACK alla paket.

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; /* felaktigt paket */
    }

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

Jag uppmärksammar kontrollerna märkta A och B. Om du kommenterar A, kommer programmet att kompileras, men vid laddning kommer det att finnas ett verifieringsfel:

Verifieraranalys:


11: (7b) *(u64 *)(r10 -48) = r1
12: (71) r3 = *(u8 *)(r7 +13)
Ogiltig åtkomst till paket, off=13 storlek=1, R7(id=0,off=0,r=0)
R7-offset ligger utanför paketet
Bearbetade 11 insns (gräns 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0

Fel vid hämtning av program/karta!

Nyckelrad Ogiltig åtkomst till paket, off=13 storlek=1, R7(id=0,off=0,r=0): det finns exekveringsvägar där den trettonde byten från början av bufferten ligger utanför paketet. Det är lite svårt att förstå från listningen vilken rad som avses, men det finns ett instruktionnummer (12) och en deassemblerare som visar raderna i koden:

llvm-objdump -S xdp_filter.o | less

I det här fallet pekar den på raden

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

vilket gör det tydligt att problemet ligger i ether. Det skulle alltid vara så.

Svar på SYN

Målet i detta skede är att skapa ett korrekt SYNACK-paket med ett fast seqnum, som i framtiden kommer att ersättas med SYN cookie. Alla ändringar sker i process_tcp_syn() och omgivningarna.

Paketkontroll

Konstigt nog är detta den mest anmärkningsvärda raden, snarare, kommentaren till den:

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

Vid skrivandet av den första versionen av koden användes kärna 5.1, för vilken verifierarens skillnad låg mellan data_end och (const void*)ctx->data_end. Vid skrivandet av artikeln hade kärna 5.3.1 inte detta problem. Det är möjligt att kompilatorn behandlade den lokala variabeln annorlunda än fältet. Moralen – vid stor inbäddning kan kodförenkling hjälpa.

Därefter rutinmässiga längdkontroller för verifieraren; om MAX_CSUM_BYTES under.

const u32 ip_len = ip->ihl * 4;
if ((void*)ip + ip_len > data_end) {
    return XDP_DROP; 
    /* felaktigt paket */
}
if (ip_len > MAX_CSUM_BYTES) {
    return XDP_ABORTED; 
    /* implementationsbegränsning */
}

const u32 tcp_len = tcp->doff * 4;
if ((void*)tcp + tcp_len > (void*)ctx->data_end) {
    return XDP_DROP; 
    /* felaktigt paket */
}
if (tcp_len > MAX_CSUM_BYTES) {
    return XDP_ABORTED; 
    /* implementationsbegränsning */
}

Paketvändning

Fyller i seqnum och acknum, ställer vi in ACK (SYN har redan ställts in):

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

Byter platser på TCP-portar, IP-adress och MAC-adresser. Standardbiblioteket är inte tillgängligt från XDP-programmet, så memcpy() – makro som döljer Clangs 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);

Omkalkulering av kontrollsummor

Kontrollsummor för IPv4 och TCP kräver addition av alla 16-bitsord i rubrikerna, och storleken på rubrikerna är skriven i dem, vilket innebär att vid kompileringstillfället är okänt. Det här är ett problem eftersom verifieraren inte kommer att passera en vanlig slinga till variabelgränsen. Men storleken på rubrikerna är begränsad: upp till 64 byte varje. Man kan göra en slinga med ett fast antal iterationer som kan avslutas i förtid.

Jag påpekar att det finns RFC 1624 om hur man delvis beräknar kontrollsumman om endast fasta ord i paketen har ändrats. Men metoden är inte universell, och implementeringen skulle vara svårare att underhålla.

Funktionen för att beräkna kontrollsumman:

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

Trots att size Kontrolleras av anropande kod, det andra utgångsvillkoret krävs för att verifieraren ska kunna bevisa att cykeln har slutförts.

En enklare version är implementerad för 32-bitars ord:

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

Själva beräkningen av kontrollsummor och återförsändning av paketet:

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;

Funktionen carry() gör kontrollsumman av en 32-bitars summa av 16-bitars ord, enligt RFC 791.

Kontroll av TCP-handshake

Filtret sätter korrekt upp förbindelsen med netcat, genom att hoppa över det avslutande ACK, vilket Linux svarade med ett RST-paket, eftersom nätverksstacken inte fick SYN - det hade omformats till SYNACK och skickats tillbaka - och ur OS:s perspektiv anlände paketet som inte hörde till öppna förbindelser.

$ sudo ip netns exec xdp-test   nc -nv 192.0.2.1 6666
192.0.2.1 6666: Anslutningen återställd av peer

Det är viktigt att kontrollera med riktiga applikationer och observera Fungerar libdns korrekt? => Kör till xdp-remote eftersom, till exempel, hping3 inte reagerar på felaktiga kontrollsummor.

Ur XDP:s perspektiv är själva kontrollen trivial. Beräkningsalgoritmen är primitiv och, troligtvis, sårbar för sofistikerade angripare. Linux-kärnan använder till exempel den kryptografiska SipHash, men dess implementering för XDP faller tydligt utanför artikelns ämne.

Har dykt upp nya TODO, relaterade till externa interaktioner:

  • XDP-programmet kan inte lagra cookie_seed (den hemliga delen av saltet) i en global variabel, det behövs ett lagringsutrymme i kärnan, vars värde periodiskt uppdateras från en pålitlig generator.

  • Vid matchande SYN cookie i ACK-paketet bör inget meddelande skrivas ut, utan den verifierade klientens IP bör sparas för att sedan tillåta paket från den.

Kontroll av legitim klient:

$ sudo ip netns exec xdp-test   nc -nv 192.0.2.1 6666
192.0.2.1 6666: Anslutningen återställd av peer

I loggarna är genomgången av kontrollen registrerad (flags=0x2 — det är SYN, flags=0x10 — det är 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 matchar för klient 20200c0

Det finns fortfarande ingen lista över verifierade IP, skydd mot själva SYN flood kommer inte att finnas, men här är reaktionen på ACK flood, som initieras med kommandot:

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

Poster i loggen:

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

Slutsats

Ibland uppfattas eBPF och särskilt XDP mer som ett verktyg för den avancerade administratören, snarare än en plattform för utveckling. Faktum är att XDP är ett verktyg för att ingripa i kärnans paketbehandling, och inte en alternativ kärnstack, som DPDK och andra variant av kernel bypass. Å sin sida möjliggör XDP implementering av ganska komplex logik, som dessutom är lätt att uppdatera utan att avbryta trafikbehandlingen. Verifieraren skapar inte stora problem, personligen skulle jag inte tveka att använda en sådan för delar av userspace-koden.

I den andra delen, om ämnet är intressant, ska vi färdigställa tabellen med testade kunder och kopplingsavbrott, införa räknare och skriva en userspace-nytta för att hantera filtret.

Länkar:

Källa: habr.com

Köp pålitlig webbhotell med DDoS-skydd, VPS VDS-servrar 🔥 Köp pålitlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster