Vi skriver skydd mot DDoS-attacker på XDP. Nukleär del

eXpress Data Path (XDP)-teknik gör att slumpmässig trafikbearbetning kan utföras på Linux-gränssnitt innan paketen går in i kärnnätverksstacken. Tillämpning av XDP - skydd mot DDoS-attacker (CloudFlare), komplexa filter, statistikinsamling (Netflix). XDP-program exekveras av den virtuella eBPF-maskinen, så de har begränsningar för både sin kod och de tillgängliga kärnfunktionerna beroende på filtertypen.

Artikeln är avsedd att fylla bristerna i många material på XDP. För det första tillhandahåller de färdig kod som omedelbart kringgår funktionerna i XDP: den är förberedd för verifiering eller är för enkel för att orsaka problem. När du sedan försöker skriva din kod från början har du ingen aning om vad du ska göra med typiska fel. För det andra omfattas inte sätt att lokalt testa XDP utan virtuell dator och hårdvara, trots att de har sina egna fallgropar. Texten är avsedd för programmerare som är bekanta med nätverk och Linux och som är intresserade av XDP och eBPF.

I den här delen kommer vi att förstå i detalj hur XDP-filtret är monterat och hur man testar det, sedan kommer vi att skriva en enkel version av den välkända SYN-cookies-mekanismen på paketbearbetningsnivån. Vi kommer inte att skapa en "vit lista" ännu
verifierade klienter, hålla räknare och hantera filtret - tillräckligt med loggar.

Vi kommer att skriva i C - det är inte på modet, men det är 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.

Varning. Under loppet av den här artikeln kommer jag att utveckla en minilösning för att avvärja DDoS-attacker, eftersom detta är en realistisk uppgift för XDP och mitt expertområde. Huvudmålet är dock att förstå tekniken, detta är inte en guide för att skapa färdiga skydd. Handledningskoden är inte optimerad och utelämnar några nyanser.

XDP Kort översikt

Jag kommer endast att beskriva de viktigaste punkterna för att inte kopiera dokumentation och befintliga artiklar.

Så, filterkoden laddas in i kärnan. Inkommande paket skickas till filtret. Som ett resultat måste filtret fatta ett beslut: skicka paketet till kärnan (XDP_PASS), släpp paket (XDP_DROP) eller skicka tillbaka den (XDP_TX). Filtret kan ändra paketet, detta gäller särskilt för XDP_TX. Du kan också avbryta programmet (XDP_ABORTED) och återställ paketet, men detta är analogt assert(0) - för felsökning.

Den virtuella maskinen eBPF (extended Berkley Packet Filter) är medvetet enkel så att kärnan kan kontrollera att koden inte loopar och inte skadar andras minne. Kumulativa begränsningar och kontroller:

  • Slingor (bakåt) är förbjudna.
  • Det finns en stack för data, men inga funktioner (alla C-funktioner måste vara infogade).
  • Minnesåtkomst utanför stacken och paketbufferten är förbjuden.
  • Kodstorleken är begränsad, men i praktiken är detta inte särskilt betydande.
  • Endast anrop till speciella kärnfunktioner (eBPF-hjälpare) är tillåtna.

Att designa och installera ett filter ser ut så här:

  1. Källkod (t.ex kernel.c) kompileras till objekt (kernel.o) för eBPFs virtuella maskinarkitektur. Från och med oktober 2019 stöds kompilering till eBPF av Clang och utlovas i GCC 10.1.
  2. Om den här objektkoden innehåller anrop till kärnstrukturer (till exempel tabeller och räknare), ersätts deras ID med nollor, vilket innebär att sådan kod inte kan köras. Innan du laddar in i kärnan måste du ersätta dessa nollor med ID:n för specifika objekt som skapats genom kärnanrop (länk koden). Du kan göra detta med externa verktyg, eller så kan du skriva ett program som länkar och laddar ett specifikt filter.
  3. Kärnan verifierar det laddade programmet. Frånvaron av cykler och misslyckande med att överskrida paket- och stackgränser kontrolleras. Om verifieraren inte kan bevisa att koden är korrekt, avvisas programmet - du måste kunna behaga honom.
  4. Efter framgångsrik verifiering kompilerar kärnan eBPF-arkitekturobjektkoden till maskinkod för systemarkitekturen (just-in-time).
  5. Programmet ansluter till gränssnittet och börjar bearbeta paket.

Eftersom XDP körs i kärnan, utförs felsökning med hjälp av spårloggar och faktiskt paket som programmet filtrerar eller genererar. Emellertid ser eBPF till att den nedladdade koden är säker för systemet, så att du kan experimentera med XDP direkt på din lokala Linux.

Att förbereda miljön

aggregatet

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

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

När du skriver ett filter kommer ett par filer med hjälpfunktioner och makron att vara användbara från kärntester. Det är viktigt att de matchar kärnversionen (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ärnrubriken, ARCH - system arkitektur. 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 koppla ihop en katalog med extra rubriker och flera kataloger med kärnhuvuden. Symbol __KERNEL__ betyder att UAPI (userspace API)-rubriker är definierade för kärnkoden, eftersom filtret exekveras i kärnan.

Stackskydd kan inaktiveras (-fno-stack-protector), eftersom eBPF-kodverifieraren fortfarande kontrollerar stack out-of-bounds-överträdelser. Det är värt att aktivera optimeringar direkt, eftersom storleken på eBPF-bytekoden är begränsad.

Låt oss börja med ett filter som skickar 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 samlar in xdp_filter.o. Var ska man prova det nu?

Testbänk

Stativet måste innehålla två gränssnitt: där det kommer att finnas ett filter och från vilket paket kommer att skickas. Dessa måste vara fullfjädrade Linux-enheter med egna IP-adresser för att kunna kontrollera hur vanliga applikationer fungerar med vårt filter.

Enheter av typen veth (virtuellt Ethernet) är lämpliga för oss: dessa är ett par virtuella nätverksgränssnitt "anslutna" direkt till varandra. Du kan skapa dem så här (i det här avsnittet alla kommandon ip utförs från root):

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

Här xdp-remote и xdp-local — enhetsnamn. På xdp-local (192.0.2.1/24) kommer ett filter att fästas, med xdp-remote (192.0.2.2/24) kommer inkommande trafik att skickas. Det finns dock ett problem: gränssnitten är på samma maskin, och Linux kommer inte att skicka trafik till en av dem genom den andra. Du kan lösa detta med knepiga regler iptables, men de kommer att behöva ändra paket, vilket är obekvämt för felsökning. Det är bättre att använda nätverksnamnområden (nedan kallade netns).

Ett nätverksnamnområde innehåller en uppsättning gränssnitt, routingtabeller och NetFilter-regler som är isolerade från liknande objekt i andra netn. Varje process körs i ett namnområde och har bara tillgång till objekten i det netns. Som standard har systemet ett enda nätverksnamnområde för alla objekt, så du kan arbeta i Linux och inte känna till netns.

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

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

Sedan kör processen in xdp-test, kommer inte att "se" xdp-local (det kommer att finnas kvar i netns som standard) och när du skickar ett paket till 192.0.2.1 kommer det att passera igenom det xdp-remoteeftersom det är det enda gränssnittet på 192.0.2.0/24 som är tillgängligt för denna process. Detta fungerar också i motsatt riktning.

Vid förflyttning mellan netns går gränssnittet ner och förlorar sin adress. För att konfigurera gränssnittet i netns måste du köra ip ... i detta kommandonamnutrymme 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 skiljer sig detta inte från inställningen xdp-local i standardnamnutrymmet:

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

Om du springer tcpdump -tnevi xdp-local, kan du se att paket 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 lansera ett skal i xdp-test. Förvaret har ett skript som automatiserar arbetet med stativet; till exempel kan du konfigurera stativet med kommandot sudo ./stand up och radera den sudo ./stand down.

Spårning

Filtret är associerat med enheten så här:

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

nyckel -force behövs för att länka ett nytt program om ett annat redan är länkat. "Inga nyheter är goda nyheter" handlar inte om detta kommando, slutsatsen är i alla fall omfattande. ange verbose valfritt, men med det visas en rapport om kodverifierarens arbete med en sammanställningslista:

Verifier analysis:

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

Ta bort länken till programmet från gränssnittet:

ip link set dev xdp-local xdp off

I skriptet är dessa kommandon sudo ./stand attach и sudo ./stand detach.

Genom att fästa ett filter kan du säkerställa det ping fortsätter att köras, men fungerar programmet? Låt oss lägga till loggar. Fungera bpf_trace_printk() Liknande printf(), men stöder bara upp till tre andra argument än mönstret och en begränsad lista med specifikationer. Makro bpf_printk() förenklar samtalet.

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

Utdata går till kärnspårningskanalen, som måste aktiveras:

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

Visa meddelandetråd:

cat /sys/kernel/debug/tracing/trace_pipe

Båda dessa kommandon ringer ett samtal sudo ./stand log.

Ping bör nu utlösa meddelanden som detta:

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

Om du tittar noga på verifierarens utdata kommer du att märka 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
<...>

Faktum är att eBPF-program inte har en datasektion, så det enda sättet att koda en formatsträng är de omedelbara argumenten för VM-kommandon:

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

Av denna anledning sväller felsökningsutdata den resulterande koden kraftigt.

Skickar XDP-paket

Låt oss byta filter: låt det skicka tillbaka alla inkommande paket. Detta är felaktigt ur nätverkssynpunkt, eftersom det skulle vara nödvändigt att ändra adresserna i rubrikerna, men nu är principarbetet viktigt.

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

Vi lanserar tcpdump på xdp-remote. Den ska visa identiska utgående och inkommande ICMP Echo Request och sluta visa ICMP Echo Reply. Men det syns inte. Det visar sig att för arbete XDP_TX i programmet på xdp-local nödvändigttill pargränssnittet xdp-remote ett program tilldelades också, även om det var tomt, och han uppfostrades.

Hur visste jag detta?

Spåra sökvägen till ett paket i kärnan Perf event-mekanismen tillåter förresten att använda samma virtuella maskin, det vill säga eBPF används för demontering med eBPF.

Du måste göra gott av det onda, för det finns inget annat att göra det ur.

$ 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 No such device or address

Funktion veth_xdp_flush_bq() får en felkod från veth_xdp_xmit(), där sök efter ENXIO och hitta kommentaren.

Låt oss återställa minimifiltret (XDP_PASS) i filen xdp_dummy.c, lägg till den i Makefilen, bind den till xdp-remote:

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

Nu tcpdump visar vad som förväntas:

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

Om endast ARP visas istället måste du ta bort filtren (det gör det sudo ./stand detach), släpp taget ping, ställ in filter och försök igen. Problemet är att filtret XDP_TX giltig både på ARP och om stacken
namnrymder xdp-test lyckades "glömma" MAC-adressen 192.0.2.1, kommer den inte att kunna lösa denna IP.

Problem uttalande

Låt oss gå vidare till den angivna uppgiften: skriv en SYN-cookiesmekanism på XDP.

SYN flood är fortfarande en populär DDoS-attack, vars kärna är som följer. När en anslutning upprättas (TCP-handskakning) tar servern emot ett SYN, allokerar resurser för den framtida anslutningen, svarar med ett SYNACK-paket och väntar på ett ACK. Angriparen skickar helt enkelt tusentals SYN-paket per sekund från falska adresser från varje värd i ett multi-tusenstarkt botnät. Servern tvingas tilldela resurser omedelbart vid ankomsten av paketet, men släpper dem efter en lång timeout; som ett resultat är minne eller gränser uttömda, nya anslutningar accepteras inte och tjänsten är inte tillgänglig.

Om du inte allokerar resurser baserat på SYN-paketet utan bara svarar med ett SYNACK-paket, hur kan då servern förstå att ACK-paketet som kom senare refererar till ett SYN-paket som inte sparades? En angripare kan trots allt också generera falska ACK. Poängen med SYN-cookien är att koda in den seqnum anslutningsparametrar som en hash av adresser, portar och bytesalt. Om ACK lyckades komma fram innan saltet ändrades kan du räkna ut hashen igen och jämföra med acknum. Förfalska acknum angriparen kan inte, eftersom saltet innehåller hemligheten, och kommer inte att ha tid att sortera igenom det på grund av en begränsad kanal.

SYN-kakan har länge implementerats i Linux-kärnan och kan till och med aktiveras automatiskt om SYN:er kommer för snabbt och i massor.

Utbildningsprogram om TCP-handskakning

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

  • Kombinationen av flaggor bestämmer rollen för ett visst paket. SYN-flaggan indikerar att detta är avsändarens första paket på anslutningen. ACK-flaggan betyder att avsändaren har tagit emot all anslutningsdata upp till byten acknum. Ett paket kan ha flera flaggor och anropas genom deras kombination, till exempel ett SYNACK-paket.

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

  • Kvittensnummer (acknum) - samma offset som seqnum, men det bestämmer inte numret på den byte som sänds, utan numret på den första byten från mottagaren, som avsändaren inte såg.

I början av anslutningen måste parterna komma överens seqnum и acknum. Klienten skickar ett SYN-paket med sin seqnum = X. Servern svarar med ett SYNACK-paket, där den registrerar sitt seqnum = Y och avslöjar 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 peer inte bekräftar mottagandet av paketet, skickar TCP det igen efter en timeout.

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

För det första, om SYNACK eller ACK försvinner, måste du vänta på att det skickas igen - anslutningsinställningen kommer att sakta ner. För det andra i SYN-paketet - och bara i det! — ett antal alternativ överförs som påverkar den fortsatta driften av förbindelsen. Utan att komma ihåg inkommande SYN-paket så ignorerar servern dessa alternativ, klienten kommer inte att skicka dem i nästa paket. TCP kan fungera i det här fallet, men åtminstone i det inledande skedet kommer kvaliteten på anslutningen att minska.

När det gäller paket måste ett XDP-program göra följande:

  • svara på SYN med SYNACK med en cookie;
  • svara på ACK med RST (koppla från);
  • kassera de återstående paketen.

Pseudokod för algoritmen tillsammans med paketanalys:

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

Ett (*) punkter där du behöver hantera systemets tillstånd är markerade - i det första steget kan du klara dig utan dem genom att helt enkelt implementera en TCP-handskakning med generering av en SYN-cookie som ett sekvensnummer.

På pricken (**), medan vi inte har ett bord, kommer vi att hoppa över paketet.

Implementering av TCP-handskakning

Parsar paketet och verifierar koden

Vi kommer att behöva nätverkshuvudstrukturer: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) och TCP (uapi/linux/tcp.h). Jag kunde inte ansluta den senare på grund av fel relaterade till atomic64_t, jag var tvungen att kopiera de nödvändiga definitionerna till koden.

Alla funktioner som är markerade i C för läsbarhet måste infogas vid anropspunkten, eftersom eBPF-verifieraren i kärnan förbjuder backtracking, det vill säga i själva verket loopar och funktionsanrop.

#define INTERNAL static __attribute__((always_inline))

Makro LOG() inaktiverar utskrift i versionsversionen.

Programmet är en förmedlare av funktioner. Var och en tar emot ett paket där motsvarande nivåhuvud är markerat, t.ex. process_ether() förväntar sig att den ska fyllas ether. Baserat på resultaten av fältanalys kan funktionen skicka paketet till en högre nivå. Resultatet av funktionen är XDP-åtgärden. För närvarande skickar SYN- och ACK-hanterarna 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; /* 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);
}

Jag uppmärksammar er på kontrollerna märkta A och B. Om du kommenterar A kommer programmet att byggas, men det kommer att uppstå ett verifieringsfel när du laddar:

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!

Nyckelsträng invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0): Det finns exekveringsvägar när den trettonde byten från början av bufferten är utanför paketet. Det är svårt att förstå från listan vilken rad vi talar om, men det finns ett instruktionsnummer (12) och en disassembler som visar raderna med källkod:

llvm-objdump -S xdp_filter.o | less

I det här fallet pekar den på linjen

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

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

Svar till SYN

Målet i detta skede är att generera ett korrekt SYNACK-paket med en fast seqnum, som i framtiden kommer att ersättas av SYN-cookien. Alla förändringar sker i process_tcp_syn() och omgivande områden.

Paketverifiering

Märkligt nog, här är den mest anmärkningsvärda raden, eller snarare kommentaren till den:

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

När den första versionen av koden skrevs användes 5.1-kärnan, för verifieraren var det skillnad mellan data_end и (const void*)ctx->data_end. I skrivande stund hade inte kärnan 5.3.1 detta problem. Det är möjligt att kompilatorn fick åtkomst till en lokal variabel på ett annat sätt än ett fält. Berättelsens moral: Att förenkla koden kan hjälpa när det är mycket häckande.

Nästa är rutinmässiga längdkontroller för verifierarens ära; O MAX_CSUM_BYTES nedan.

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

Viker ut paketet

fylla seqnum и acknum, ställ in ACK (SYN är redan inställt):

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

Byt ut TCP-portar, IP-adresser och MAC-adresser. Standardbiblioteket är inte tillgängligt från XDP-programmet, så memcpy() — ett makro som döljer Clangs inneboende.

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

Omräkning av kontrollsummor

IPv4- och TCP-kontrollsummor kräver tillägg av alla 16-bitars ord i rubrikerna, och storleken på rubrikerna skrivs in i dem, det vill säga okänd vid kompilering. Detta är ett problem eftersom verifieraren inte kommer att hoppa över den normala slingan till gränsvariabeln. Men storleken på rubrikerna är begränsad: upp till 64 byte vardera. Du kan göra en loop med ett fast antal iterationer, som kan avslutas tidigt.

Jag noterar att det finns RFC 1624 om hur man delvis räknar om kontrollsumman om bara de fasta orden i paketen ändras. Metoden är dock inte universell, och implementeringen skulle vara svårare att underhålla.

Funktion för beräkning av kontrollsumma:

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

Fastän size verifieras av anropskoden, är det andra utgångsvillkoret nödvändigt så att verifieraren kan bevisa slutförandet av slingan.

För 32-bitars ord är en enklare version implementerad:

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

Att faktiskt räkna om kontrollsummorna och skicka tillbaka 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 << 8;
tcp->check = 0;
tcp_csum += sum16(tcp, tcp_len, data_end);
tcp->check = carry(tcp_csum);

return XDP_TX;

Funktion carry() gör en kontrollsumma från en 32-bitars summa av 16-bitars ord, enligt RFC 791.

TCP-handskakningsverifiering

Filtret upprättar korrekt koppling med netcat, saknade den sista ACK, som Linux svarade på med ett RST-paket, eftersom nätverksstacken inte tog emot SYN - den konverterades till SYNACK och skickades tillbaka - och från OS-synpunkt kom ett paket som inte var relaterat till open anslutningar.

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

Det är viktigt att kontrollera med fullfjädrade ansökningar och observera tcpdump på xdp-remote eftersom t.ex. hping3 svarar inte på felaktiga kontrollsummor.

Ur XDP-synpunkt är själva verifieringen trivial. Beräkningsalgoritmen är primitiv och sannolikt sårbar för en sofistikerad angripare. Linuxkärnan, till exempel, använder den kryptografiska SipHash, men dess implementering för XDP ligger helt klart utanför den här artikelns räckvidd.

Introducerad för nya TODOs relaterade till extern kommunikation:

  • XDP-programmet kan inte lagras cookie_seed (den hemliga delen av saltet) i en global variabel behöver du lagring i kärnan, vars värde kommer att uppdateras med jämna mellanrum från en pålitlig generator.

  • Om SYN-cookien matchar i ACK-paketet behöver du inte skriva ut ett meddelande, utan kom ihåg IP-adressen för den verifierade klienten för att kunna fortsätta skicka paket från den.

Legitim klientverifiering:

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

Loggarna visar att kontrollen godkändes (flags=0x2 - det här är SYN, flags=0x10 ä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 matches for client 20200c0

Även om det inte finns någon lista över verifierade IP-adresser, kommer det inte att finnas något skydd från själva SYN-floden, men här är reaktionen på en ACK-flod som startas av följande kommando:

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

Loggposter:

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

Slutsats

Ibland presenteras eBPF i allmänhet och XDP i synnerhet mer som ett avancerat administratörsverktyg än som en utvecklingsplattform. XDP är faktiskt ett verktyg för att störa behandlingen av paket av kärnan, och inte ett alternativ till kärnstacken, som DPDK och andra alternativ för förbikoppling av kärnan. Å andra sidan låter XDP dig implementera ganska komplex logik, som dessutom är lätt att uppdatera utan avbrott i trafikbearbetningen. Verifieraren skapar inga stora problem, personligen skulle jag inte vägra detta för delar av användarutrymmeskoden.

I den andra delen, om ämnet är intressant, kommer vi att komplettera tabellen över verifierade klienter och frånkopplingar, implementera räknare och skriva ett användarutrymmesverktyg för att hantera filtret.

Länkar:

Källa: will.com

Lägg en kommentar