We schrijven bescherming tegen DDoS-aanvallen op XDP. Nucleair onderdeel

eXpress Data Path (XDP)-technologie maakt willekeurige verwerking van verkeer op Linux-interfaces mogelijk voordat pakketten de kernel-netwerkstack binnenkomen. Toepassing van XDP - bescherming tegen DDoS-aanvallen (CloudFlare), complexe filters, statistiekenverzameling (Netflix). XDP-programma's worden uitgevoerd door de eBPF virtuele machine en hebben daarom beperkingen op zowel hun code als de beschikbare kernelfuncties, afhankelijk van het type filter.

Het artikel is bedoeld om de tekortkomingen van tal van materialen op XDP goed te maken. Ten eerste leveren ze kant-en-klare code die direct de features van XDP omzeilt: voorbereid voor verificatie of te simpel om problemen te veroorzaken. Wanneer u later uw eigen code vanaf nul probeert te schrijven, weet u niet wat u met typische fouten moet doen. Ten tweede behandelt het geen manieren om XDP lokaal te testen zonder een VM en hardware, ondanks het feit dat ze hun eigen valkuilen hebben. De tekst is bedoeld voor programmeurs die bekend zijn met netwerken en Linux en geïnteresseerd zijn in XDP en eBPF.

In dit deel zullen we in detail begrijpen hoe het XDP-filter is samengesteld en hoe het te testen, en vervolgens zullen we een eenvoudige versie van het bekende SYN-cookiemechanisme op pakketverwerkingsniveau schrijven. Totdat we een "witte lijst" vormen
geverifieerde klanten, houd tellers bij en beheer het filter - voldoende logboeken.

We zullen in C schrijven - dit is niet modieus, maar praktisch. Alle code is beschikbaar op GitHub via de link aan het einde en is verdeeld in commits volgens de stappen beschreven in het artikel.

Disclaimer. In de loop van het artikel zal een mini-oplossing voor het afweren van DDoS-aanvallen worden ontwikkeld, omdat dit een realistische taak is voor XDP en mijn omgeving. Het belangrijkste doel is echter om de technologie te begrijpen, dit is geen gids voor het creëren van kant-en-klare bescherming. De zelfstudiecode is niet geoptimaliseerd en laat enkele nuances weg.

Een kort overzicht van XDP

Ik zal alleen de belangrijkste punten vermelden om de documentatie en bestaande artikelen niet te dupliceren.

De filtercode wordt dus in de kernel geladen. Het filter wordt doorgegeven aan inkomende pakketten. Als resultaat moet het filter een beslissing nemen: om het pakket door te geven aan de kernel (XDP_PASS), laat pakket vallen (XDP_DROP) of stuur het terug (XDP_TX). Het filter kan het pakket veranderen, dit geldt met name voor XDP_TX. U kunt het programma ook laten crashen (XDP_ABORTED) en laat het pakket vallen, maar dit is analoog assert(0) - voor debuggen.

De virtuele machine eBPF (extended Berkley Packet Filter) is met opzet eenvoudig gemaakt, zodat de kernel kan controleren of de code niet in een lus loopt en het geheugen van anderen niet beschadigt. Cumulatieve beperkingen en controles:

  • Loops (terugspringen) zijn verboden.
  • Er is een stapel voor gegevens, maar geen functies (alle C-functies moeten inlined zijn).
  • Toegang tot geheugen buiten de stapel- en pakketbuffer is verboden.
  • De grootte van de code is beperkt, maar in de praktijk is dit niet erg significant.
  • Alleen speciale kernelfuncties (eBPF-helpers) zijn toegestaan.

Het ontwikkelen en installeren van een filter ziet er als volgt uit:

  1. broncode (bijv. kernel.c) compileert naar object (kernel.o) voor de virtuele machine-architectuur van eBPF. Vanaf oktober 2019 wordt compileren naar eBPF ondersteund door Clang en beloofd in GCC 10.1.
  2. Als er in deze objectcode aanroepen zijn naar kernelstructuren (bijvoorbeeld naar tabellen en tellers), zijn er in plaats van hun ID's nullen, dat wil zeggen dat dergelijke code niet kan worden uitgevoerd. Voordat ze in de kernel worden geladen, moeten deze nullen worden vervangen door de ID's van specifieke objecten die zijn gemaakt via kernelaanroepen (link de code). U kunt dit doen met externe hulpprogramma's, of u kunt een programma schrijven dat een specifiek filter koppelt en laadt.
  3. De kernel verifieert het programma dat wordt geladen. Het controleert op de afwezigheid van cycli en het niet verlaten van de pakket- en stapelgrenzen. Als de verificateur niet kan bewijzen dat de code correct is, wordt het programma afgewezen - men moet hem tevreden kunnen stellen.
  4. Na succesvolle verificatie compileert de kernel de objectcode van de eBPF-architectuur in de machinecode van de systeemarchitectuur (just-in-time).
  5. Het programma is gekoppeld aan de interface en begint met het verwerken van pakketten.

Omdat XDP in de kernel draait, is foutopsporing gebaseerd op traceerlogboeken en in feite op pakketten die het programma filtert of genereert. eBPF houdt de gedownloade code echter veilig voor het systeem, zodat u met XDP rechtstreeks op uw lokale Linux kunt experimenteren.

De omgeving voorbereiden

montage

Clang kan niet direct objectcode uitgeven voor de eBPF-architectuur, dus het proces bestaat uit twee stappen:

  1. C-code compileren naar LLVM bytecode (clang -emit-llvm).
  2. Converteer bytecode naar eBPF-objectcode (llc -march=bpf -filetype=obj).

Bij het schrijven van een filter komen een aantal bestanden met hulpfuncties en macro's goed van pas van kerneltesten. Het is belangrijk dat ze overeenkomen met de kernelversie (KVER). Download ze naar 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 voor Arch Linux (kernel 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 bevat het pad naar de kernelheaders, ARCH - Systeem Architectuur. Paden en tools kunnen enigszins verschillen tussen distributies.

Verschilvoorbeeld voor Debian 10 (kernel 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 omvatten een directory met hulpheaders en verschillende directories met kernelheaders. Symbool __KERNEL__ betekent dat UAPI-headers (userspace API) zijn gedefinieerd voor de kernelcode, aangezien het filter wordt uitgevoerd in de kernel.

Stapelbeveiliging kan worden uitgeschakeld (-fno-stack-protector) omdat de eBPF-codeverificator toch controleert op niet-uit-stapelgrenzen. U moet onmiddellijk optimalisaties inschakelen, omdat de grootte van de eBPF-bytecode beperkt is.

Laten we beginnen met een filter dat alle pakketten doorlaat en niets doet:

#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 verzamelt xdp_filter.o. Waar kun je het nu testen?

Testbank

De standaard moet twee interfaces bevatten: waarop een filter komt en van waaruit pakketten worden verzonden. Dit moeten volledige Linux-apparaten zijn met hun eigen IP's om te controleren hoe gewone applicaties werken met ons filter.

Apparaten zoals veth (virtueel ethernet) zijn geschikt voor ons: het zijn een paar virtuele netwerkinterfaces die rechtstreeks met elkaar zijn "verbonden". U kunt ze op deze manier maken (in deze sectie worden alle commando's ip uitgevoerd vanaf root):

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

Hier xdp-remote и xdp-local - apparaatnamen. Op xdp-local (192.0.2.1/24) wordt een filter bevestigd, met xdp-remote (192.0.2.2/24) binnenkomend verkeer wordt verzonden. Er is echter een probleem: de interfaces bevinden zich op dezelfde machine en Linux stuurt geen verkeer naar de ene via de andere. Je kunt het oplossen met lastige regels iptables, maar ze zullen pakketten moeten wijzigen, wat onhandig is bij het debuggen. Het is beter om netwerknaamruimten te gebruiken (netwerknaamruimten, verder netns).

De netwerknaamruimte bevat een set interfaces, routeringstabellen en NetFilter-regels die geïsoleerd zijn van vergelijkbare objecten in andere netns. Elk proces wordt in een bepaalde naamruimte uitgevoerd en alleen de objecten van deze netns zijn beschikbaar. Het systeem heeft standaard een enkele netwerknaamruimte voor alle objecten, zodat u op Linux kunt werken en geen weet hebt van netns.

Laten we een nieuwe naamruimte maken xdp-test en daarheen verhuizen xdp-remote.

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

Dan loopt het proces in xdp-test, zal niet "zien" xdp-local (het blijft standaard in netns) en wanneer een pakket naar 192.0.2.1 wordt verzonden, wordt het doorgegeven xdp-remote, omdat dat de enige interface op 192.0.2.0/24 is die beschikbaar is voor dit proces. Dit werkt ook omgekeerd.

Bij het wisselen tussen netwerken gaat de interface uit en verliest het adres. Om een ​​interface in netns op te zetten, moet u uitvoeren ip ... in deze opdrachtnaamruimte 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

Zoals je kunt zien, is dit niet anders dan instellen xdp-local in de standaard naamruimte:

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

Als rennen tcpdump -tnevi xdp-local, kunt u zien dat pakketten worden verzonden vanaf xdp-test, worden geleverd aan deze interface:

ip netns exec xdp-test   ping 192.0.2.1

Het is handig om een ​​shell in te voeren xdp-test. De repository heeft een script dat het werken met de standaard automatiseert, u kunt bijvoorbeeld de standaard instellen met de opdracht sudo ./stand up en verwijder het sudo ./stand down.

traceren

Het filter wordt als volgt aan het apparaat bevestigd:

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

sleutel -force nodig om een ​​nieuw programma te koppelen als er al een ander programma is gekoppeld. "Geen nieuws is goed nieuws" gaat niet over dit commando, de output is hoe dan ook volumineus. aanwijzen verbose optioneel, maar er verschijnt een rapport over het werk van de codeverifier met de assembler-lijst:

Verifier analysis:

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

Maak het programma los van de interface:

ip link set dev xdp-local xdp off

In het script zijn dit de commando's sudo ./stand attach и sudo ./stand detach.

Door het filter te binden, kunt u ervoor zorgen dat ping blijft werken, maar werkt het programma ook? Laten we logo's toevoegen. Functie bpf_trace_printk() gelijkwaardig aan printf(), maar ondersteunt slechts maximaal drie andere argumenten dan het patroon en een beperkte lijst met specificaties. Makro bpf_printk() vereenvoudigt het bellen.

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

De uitvoer gaat naar het kernel-traceerkanaal, dat moet worden ingeschakeld:

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

Berichtstroom bekijken:

cat /sys/kernel/debug/tracing/trace_pipe

Beide teams bellen sudo ./stand log.

Ping zou er nu berichten als deze in moeten produceren:

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

Als je goed kijkt naar de uitvoer van de verifier, zie je vreemde berekeningen:

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

Het feit is dat eBPF-programma's geen gegevenssectie hebben, dus de enige manier om de formaatreeks te coderen, zijn de onmiddellijke argumenten van de VM-opdrachten:

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

Om deze reden wordt de resulterende code enorm vergroot door foutopsporingsuitvoer.

XDP-pakketten verzenden

Laten we het filter veranderen: laat het alle inkomende pakketten terugsturen. Dit is vanuit netwerkoogpunt onjuist, aangezien het nodig zou zijn om de adressen in de headers te wijzigen, maar nu is het werk in principe belangrijk.

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

Lancering tcpdump op xdp-remote. Het zou identieke uitgaande en inkomende ICMP Echo Request moeten tonen en ICMP Echo Reply niet meer weergeven. Maar het laat zich niet zien. Blijkt te werken XDP_TX in het programma voor xdp-local noodzakelijkinterface koppelen xdp-remote er werd ook een programma toegewezen, ook al was het leeg, en het werd verhoogd.

Hoe wist ik dat?

Het pad van een pakket in de kernel traceren het perf events-mechanisme maakt het trouwens mogelijk om dezelfde virtuele machine te gebruiken, dat wil zeggen dat eBPF wordt gebruikt voor demontage met eBPF.

Je moet van het kwade het goede maken, want er valt niets anders van te maken.

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

Wat is code 6?

$ errno 6
ENXIO 6 No such device or address

Functie veth_xdp_flush_bq() krijgt foutcode van veth_xdp_xmit(), waar zoeken op ENXIO en vind een opmerking.

Herstel het minimumfilter (XDP_PASS) in bestand xdp_dummy.c, voeg het toe aan de Makefile, bind aan xdp-remote:

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

Nu tcpdump laat zien wat er verwacht wordt:

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

Als in plaats daarvan alleen ARP wordt weergegeven, moet u de filters verwijderen (dit maakt sudo ./stand detach), laten ping, installeer vervolgens filters en probeer het opnieuw. Het probleem is dat het filter XDP_TX beïnvloedt ook ARP, en als de stack
naamruimten xdp-test erin geslaagd om het MAC-adres 192.0.2.1 te "vergeten", zal hij dit IP-adres niet kunnen oplossen.

Formulering van het probleem

Laten we verder gaan met de vermelde taak: een SYN-cookiemechanisme op XDP schrijven.

Tot nu toe blijft de SYN-flood een populaire DDoS-aanval, waarvan de essentie als volgt is. Wanneer een verbinding tot stand is gebracht (TCP-handshake), ontvangt de server een SYN, wijst bronnen toe voor een toekomstige verbinding, reageert met een SYNACK-pakket en wacht op een ACK. De aanvaller verstuurt simpelweg duizenden SYN-pakketten per seconde vanaf elke host in een duizenden botnet. De server wordt gedwongen om onmiddellijk na aankomst van het pakket bronnen toe te wijzen, maar geeft het vrij na een lange time-out, met als resultaat dat het geheugen of de limieten zijn uitgeput, nieuwe verbindingen worden niet geaccepteerd, de service is niet beschikbaar.

Als u geen bronnen toewijst aan het SYN-pakket, maar alleen reageert met een SYNACK-pakket, hoe kan de server dan begrijpen dat het ACK-pakket dat later binnenkwam, behoort tot het SYN-pakket dat niet is opgeslagen? Een aanvaller kan immers ook valse ACK's genereren. De essentie van de SYN-cookie is om in te coderen seqnum verbindingsparameters als een hash van adressen, poorten en veranderend zout. Als de ACK vóór de zoutwisseling is aangekomen, kunt u de hasj opnieuw berekenen en vergelijken met acknum. nep acknum de aanvaller kan dat niet, aangezien het zout het geheim bevat, en zal geen tijd hebben om het te doorzoeken vanwege het beperkte kanaal.

SYN-cookies zijn al lang in de Linux-kernel geïmplementeerd en kunnen zelfs automatisch worden ingeschakeld als SYN's te snel en te massaal aankomen.

Educatief programma over TCP-handshake

TCP biedt de overdracht van gegevens in de vorm van een stroom bytes. HTTP-verzoeken worden bijvoorbeeld via TCP verzonden. De stream wordt stuk voor stuk in pakketjes verzonden. Alle TCP-pakketten hebben logische vlaggen en 32-bits volgnummers:

  • De combinatie van vlaggen definieert de rol van een bepaald pakket. De SYN-vlag betekent dat dit het eerste pakket van de afzender op de verbinding is. De ACK-vlag betekent dat de zender alle verbindingsgegevens tot een byte heeft ontvangen. acknum. Een pakket kan verschillende vlaggen hebben en wordt genoemd naar hun combinatie, bijvoorbeeld een SYNACK-pakket.

  • Volgnummer (seqnum) specificeert de offset in de gegevensstroom voor de eerste byte die in dit pakket wordt verzonden. Als in het eerste pakket met X bytes aan gegevens dit aantal bijvoorbeeld N was, is dit in het volgende pakket met nieuwe gegevens N+X. Aan het begin van de verbinding kiest elke partij willekeurig dit nummer.

  • Bevestigingsnummer (acknum) - dezelfde offset als seqnum, maar het bepaalt niet het nummer van de verzonden byte, maar het nummer van de eerste byte van de ontvanger, die de afzender niet heeft gezien.

Aan het begin van de aansluiting moeten de partijen het eens zijn seqnum и acknum. De client stuurt een SYN-pakket met zijn seqnum = X. De server antwoordt met een SYNACK-pakket, waar het zijn eigen pakket schrijft seqnum = Y en blootlegt acknum = X + 1. De client reageert op SYNACK met een ACK-pakket, waar seqnum = X + 1, acknum = Y + 1. Daarna begint de daadwerkelijke gegevensoverdracht.

Als de gesprekspartner de ontvangst van het pakket niet bevestigt, verzendt TCP het door een time-out opnieuw.

Waarom worden SYN-cookies niet altijd gebruikt?

Ten eerste, als een SYNACK of ACK verloren gaat, moet u wachten op een nieuwe verzending - de verbindingsopbouw vertraagt. Ten tweede, in het SYN-pakket - en alleen erin! - er worden een aantal opties doorgegeven die van invloed zijn op de verdere werking van de verbinding. Inkomende SYN-pakketten niet onthouden, de server negeert deze opties dus, in de volgende pakketten zal de client ze niet meer verzenden. TCP kan in dit geval werken, maar in ieder geval in de beginfase zal de kwaliteit van de verbinding afnemen.

In termen van pakketten zou een XDP-programma het volgende moeten doen:

  • reageer op SYN met SYNACK met cookie;
  • antwoord ACK met RST (verbreek de verbinding);
  • laat andere pakketten vallen.

Pseudocode van het algoritme samen met pakketparsing:

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

Een (*) de punten waarop u de status van het systeem moet beheren, zijn gemarkeerd - in de eerste fase kunt u ze missen door simpelweg een TCP-handshake te implementeren met het genereren van een SYN-cookie als een seqnum.

Ter plekke (**), terwijl we geen tafel hebben, slaan we het pakket over.

Implementatie van TCP-handshake

Pakketparsing en codeverificatie

We hebben netwerkheaderstructuren nodig: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) en TCP-(uapi/linux/tcp.h). Met de laatste kon ik geen verbinding maken vanwege fouten in verband met atomic64_t, moest ik de nodige definities in de code kopiëren.

Alle functies die voor leesbaarheid in C worden onderscheiden, moeten op de aanroepsite worden inlined, aangezien de eBPF-verifier in de kernel terugsprongen verbiedt, dat wil zeggen in feite loops en functieaanroepen.

#define INTERNAL static __attribute__((always_inline))

Makro LOG() schakelt afdrukken uit in een release-build.

Het programma is een pijplijn van functies. Elk ontvangt een pakket waarin een koptekst van het overeenkomstige niveau is gemarkeerd, bijvoorbeeld process_ether() wachten om gevuld te worden ether. Op basis van de resultaten van veldanalyse kan de functie het pakket naar een hoger niveau verplaatsen. Het resultaat van de functie is een XDP-actie. Terwijl de SYN- en ACK-handlers alle pakketten doorlaten.

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

Ik let op de controles gemarkeerd met A en B. Als u A uitcommentarieert, zal het programma bouwen, maar er zal een verificatiefout optreden bij het laden:

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!

Sleutelreeks invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0): er zijn uitvoeringspaden wanneer de dertiende byte vanaf het begin van de buffer buiten het pakket ligt. Het is moeilijk te zien aan de hand van de lijst over welke regel we het hebben, maar er is een instructienummer (12) en een disassembler die de regels van de broncode laat zien:

llvm-objdump -S xdp_filter.o | less

In dit geval wijst het naar de lijn

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

waaruit duidelijk blijkt dat het probleem is ether. Zo zou het altijd zijn.

Reageer op SYN

Het doel in dit stadium is om een ​​correct SYNACK-pakket te genereren met een vast seqnum, die in de toekomst zal worden vervangen door de SYN-cookie. Alle wijzigingen vinden plaats in process_tcp_syn() en omgeving.

Controle van het pakket

Vreemd genoeg is hier de meest opmerkelijke regel, of beter gezegd, een commentaar erop:

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

Bij het schrijven van de eerste versie van de code is gebruik gemaakt van de 5.1 kernel, voor de verifier was er een verschil tussen data_end и (const void*)ctx->data_end. Op het moment van schrijven had de 5.3.1-kernel dit probleem niet. Misschien had de compiler op een andere manier toegang tot een lokale variabele dan tot een veld. Moraal - bij een grote nesting kan het vereenvoudigen van de code helpen.

Verder routinecontroles van lengtes voor de glorie van de verificateur; O MAX_CSUM_BYTES hieronder.

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

Pakket verspreid

Vullen seqnum и acknum, stel ACK in (SYN is al ingesteld):

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

Verwissel TCP-poorten, IP- en MAC-adressen. De standaardbibliotheek is dus niet beschikbaar vanuit het XDP-programma memcpy() — een macro die de Clang intrinsik verbergt.

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

Checksum herberekening

IPv4- en TCP-checksums vereisen de toevoeging van alle 16-bits woorden in de headers en de grootte van de headers is erin geschreven, dat wil zeggen dat op het moment van compilatie onbekend is. Dit is een probleem omdat de verifier de normale lus niet zal overslaan tot de grensvariabele. Maar de grootte van de headers is beperkt: tot 64 bytes elk. Je kunt een lus maken met een vast aantal iteraties, die voortijdig kan eindigen.

Ik constateer dat er is RFC 1624 over hoe de controlesom gedeeltelijk opnieuw kan worden berekend als alleen de vaste woorden van de pakketten worden gewijzigd. De methode is echter niet universeel en de implementatie zou moeilijker te handhaven zijn.

Checksum berekeningsfunctie:

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

Hoewel size gecontroleerd door de aanroepende code, is de tweede exit-voorwaarde nodig zodat de verificateur het einde van de lus kan bewijzen.

Voor 32-bits woorden is een eenvoudigere versie geïmplementeerd:

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

De controlesommen opnieuw berekenen en het pakket terugsturen:

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;

Functie carry() maakt een checksum uit een 32-bits som van 16-bits woorden, volgens RFC 791.

TCP-handshakecontrole

Het filter brengt correct een verbinding tot stand met netcat, waarbij de laatste ACK werd overgeslagen, waarop Linux reageerde met een RST-pakket, aangezien de netwerkstack geen SYN ontving - het werd geconverteerd naar SYNACK en teruggestuurd - en vanuit het oogpunt van het besturingssysteem arriveerde er een pakket dat niet was gerelateerd aan open verbindingen.

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

Het is belangrijk om te controleren met volwaardige applicaties en te observeren tcpdump op xdp-remote omdat, bijvoorbeeld hping3 reageert niet op onjuiste checksums.

Vanuit het oogpunt van XDP is de controle zelf triviaal. Het berekeningsalgoritme is primitief en waarschijnlijk kwetsbaar voor een geavanceerde aanvaller. De Linux-kernel gebruikt bijvoorbeeld de cryptografische SipHash, maar de implementatie ervan voor XDP valt duidelijk buiten het bestek van dit artikel.

Verscheen voor nieuwe TODO's met betrekking tot externe interactie:

  • XDP-programma kan niet opslaan cookie_seed (het geheime deel van de salt) in een globale variabele, heb je een kernelopslag nodig waarvan de waarde periodiek wordt bijgewerkt door een betrouwbare generator.

  • Als de SYN-cookie in het ACK-pakket overeenkomt, hoeft u geen bericht af te drukken, maar onthoudt u het IP-adres van de geverifieerde client om er verder pakketten van over te slaan.

Validatie door een legitieme klant:

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

De logboeken registreerden de passage van de cheque (flags=0x2 is SYN, flags=0x10 is 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

Zolang er geen lijst met geverifieerde IP's is, is er geen bescherming tegen de SYN-flood zelf, maar hier is de reactie op de ACK-flood die door dit commando wordt gelanceerd:

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

Log-items:

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

Conclusie

Soms worden eBPF in het algemeen en XDP in het bijzonder gepresenteerd als meer een geavanceerde beheerderstool dan als een ontwikkelingsplatform. XDP is inderdaad een hulpmiddel om de verwerking van kernelpakketten te verstoren, en geen alternatief voor de kernelstack, zoals DPDK en andere opties voor het omzeilen van de kernel. Aan de andere kant stelt XDP u in staat om vrij complexe logica te implementeren, die bovendien eenvoudig te updaten is zonder een pauze in de verkeersverwerking. De verifier veroorzaakt geen grote problemen, persoonlijk zou ik dit niet weigeren voor delen van de userspace-code.

In het tweede deel, als het onderwerp interessant is, zullen we de tabel met geverifieerde clients invullen en verbindingen verbreken, tellers implementeren en een gebruikersruimtehulpprogramma schrijven om het filter te beheren.

referenties:

Bron: www.habr.com

Voeg een reactie