Wir schreiben Schutz vor DDoS-Angriffen auf XDP. Nuklearer Teil

Die eXpress Data Path (XDP)-Technologie ermöglicht die zufällige Verarbeitung des Datenverkehrs auf Linux-Schnittstellen, bevor die Pakete in den Kernel-Netzwerkstapel gelangen. Anwendung von XDP – Schutz vor DDoS-Angriffen (CloudFlare), komplexe Filter, Statistikerfassung (Netflix). XDP-Programme werden von der virtuellen eBPF-Maschine ausgeführt und unterliegen daher je nach Filtertyp Einschränkungen sowohl hinsichtlich ihres Codes als auch der verfügbaren Kernelfunktionen.

Der Artikel soll die Mängel zahlreicher Materialien zu XDP beheben. Erstens stellen sie vorgefertigten Code bereit, der die Funktionen von XDP sofort umgeht: Er ist für die Verifizierung vorbereitet oder zu einfach, um Probleme zu verursachen. Wenn Sie dann versuchen, Ihren Code von Grund auf neu zu schreiben, wissen Sie nicht, was Sie mit typischen Fehlern tun sollen. Zweitens werden Möglichkeiten zum lokalen Testen von XDP ohne VM und Hardware nicht behandelt, obwohl sie ihre eigenen Fallstricke haben. Der Text richtet sich an Programmierer, die mit Netzwerken und Linux vertraut sind und sich für XDP und eBPF interessieren.

In diesem Teil werden wir im Detail verstehen, wie der XDP-Filter aufgebaut ist und wie man ihn testet. Anschließend werden wir eine einfache Version des bekannten SYN-Cookie-Mechanismus auf Paketverarbeitungsebene schreiben. Wir werden noch keine „weiße Liste“ erstellen
verifizierte Clients, führen Sie Zähler und verwalten Sie den Filter – genügend Protokolle.

Wir werden in C schreiben – es ist nicht modisch, aber praktisch. Der gesamte Code ist über den Link am Ende auf GitHub verfügbar und ist gemäß den im Artikel beschriebenen Phasen in Commits unterteilt.

Disclaimer. Im Laufe dieses Artikels werde ich eine Mini-Lösung zur Abwehr von DDoS-Angriffen entwickeln, da dies eine realistische Aufgabe für XDP und mein Fachgebiet ist. Das Hauptziel besteht jedoch darin, die Technologie zu verstehen; dies ist kein Leitfaden zur Erstellung eines vorgefertigten Schutzes. Der Tutorial-Code ist nicht optimiert und lässt einige Nuancen aus.

XDP-Kurzübersicht

Ich werde nur die wichtigsten Punkte skizzieren, um Dokumentation und vorhandene Artikel nicht zu duplizieren.

Der Filtercode wird also in den Kernel geladen. Eingehende Pakete werden an den Filter weitergeleitet. Infolgedessen muss der Filter eine Entscheidung treffen: Das Paket an den Kernel weiterleiten (XDP_PASS), Paket abwerfen (XDP_DROP) oder zurückschicken (XDP_TX). Der Filter kann das Paket verändern, dies gilt insbesondere für XDP_TX. Sie können das Programm auch abbrechen (XDP_ABORTED) und das Paket zurücksetzen, aber das ist analog assert(0) - zum Debuggen.

Die virtuelle Maschine eBPF (Extended Berkley Packet Filter) ist bewusst einfach gestaltet, damit der Kernel überprüfen kann, dass der Code keine Schleife bildet und den Speicher anderer Personen nicht beschädigt. Kumulative Einschränkungen und Kontrollen:

  • Schleifen (rückwärts) sind verboten.
  • Es gibt einen Stapel für Daten, aber keine Funktionen (alle C-Funktionen müssen inline sein).
  • Speicherzugriffe außerhalb des Stacks und Paketpuffers sind verboten.
  • Die Codegröße ist begrenzt, in der Praxis spielt dies jedoch keine große Rolle.
  • Es sind nur Aufrufe spezieller Kernelfunktionen (eBPF-Helfer) zulässig.

Das Entwerfen und Installieren eines Filters sieht folgendermaßen aus:

  1. Quellcode (z.B kernel.c) wird in ein Objekt kompiliert (kernel.o) für die eBPF-Architektur der virtuellen Maschine. Ab Oktober 2019 wird die Kompilierung in eBPF von Clang unterstützt und in GCC 10.1 versprochen.
  2. Wenn dieser Objektcode Aufrufe an Kernelstrukturen (z. B. Tabellen und Zähler) enthält, werden deren IDs durch Nullen ersetzt, was bedeutet, dass dieser Code nicht ausgeführt werden kann. Vor dem Laden in den Kernel müssen Sie diese Nullen durch die IDs bestimmter Objekte ersetzen, die durch Kernel-Aufrufe erstellt wurden (verknüpfen Sie den Code). Sie können dies mit externen Dienstprogrammen tun oder ein Programm schreiben, das einen bestimmten Filter verknüpft und lädt.
  3. Der Kernel überprüft das geladene Programm. Das Fehlen von Zyklen und die Nichtüberschreitung von Paket- und Stapelgrenzen werden überprüft. Wenn der Prüfer nicht nachweisen kann, dass der Code korrekt ist, wird das Programm abgelehnt – Sie müssen ihn zufrieden stellen können.
  4. Nach erfolgreicher Verifizierung kompiliert der Kernel den Objektcode der eBPF-Architektur in Maschinencode für die Systemarchitektur (Just-in-Time).
  5. Das Programm stellt eine Verbindung zur Schnittstelle her und beginnt mit der Verarbeitung von Paketen.

Da XDP im Kernel läuft, erfolgt das Debuggen mithilfe von Trace-Protokollen und tatsächlich Paketen, die das Programm filtert oder generiert. eBPF stellt jedoch sicher, dass der heruntergeladene Code für das System sicher ist, sodass Sie direkt auf Ihrem lokalen Linux mit XDP experimentieren können.

Umgebung vorbereiten

Montage

Clang kann keinen Objektcode für die eBPF-Architektur direkt erzeugen, daher besteht der Prozess aus zwei Schritten:

  1. Kompilieren Sie C-Code in LLVM-Bytecode (clang -emit-llvm).
  2. Konvertieren Sie Bytecode in eBPF-Objektcode (llc -march=bpf -filetype=obj).

Beim Schreiben eines Filters sind einige Dateien mit Hilfsfunktionen und Makros hilfreich aus Kerneltests. Es ist wichtig, dass sie mit der Kernel-Version übereinstimmen (KVER). Laden Sie sie herunter 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 (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 enthält den Pfad zu den Kernel-Headern, ARCH - Systemarchitektur. Pfade und Tools können zwischen den Distributionen leicht variieren.

Beispiel für Unterschiede für 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 Verbinden Sie ein Verzeichnis mit Hilfsheadern und mehrere Verzeichnisse mit Kernel-Headern. Symbol __KERNEL__ bedeutet, dass UAPI-Header (Userspace API) für Kernelcode definiert sind, da der Filter im Kernel ausgeführt wird.

Der Stapelschutz kann deaktiviert werden (-fno-stack-protector), da der eBPF-Codeverifizierer weiterhin nach Stack-Out-of-Bounds-Verstößen sucht. Es lohnt sich, Optimierungen sofort einzuschalten, da die Größe des eBPF-Bytecodes begrenzt ist.

Beginnen wir mit einem Filter, der alle Pakete durchlässt und nichts tut:

#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 sammelt xdp_filter.o. Wo kann man es jetzt ausprobieren?

Prüfstand

Der Stand muss zwei Schnittstellen enthalten: auf denen sich ein Filter befindet und von denen aus Pakete gesendet werden. Dabei müssen es sich um vollwertige Linux-Geräte mit eigenen IPs handeln, um zu prüfen, wie reguläre Anwendungen mit unserem Filter funktionieren.

Für uns eignen sich Geräte vom Typ Veth (Virtual Ethernet): Dabei handelt es sich um ein Paar virtueller Netzwerkschnittstellen, die direkt miteinander „verbunden“ sind. Sie können sie wie folgt erstellen (in diesem Abschnitt alle Befehle). ip werden durchgeführt von root):

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

Hier xdp-remote и xdp-local — Gerätenamen. An xdp-local (192.0.2.1/24) wird ein Filter angehängt, mit xdp-remote (192.0.2.2/24) eingehender Datenverkehr wird gesendet. Es gibt jedoch ein Problem: Die Schnittstellen befinden sich auf demselben Computer und Linux sendet keinen Datenverkehr über die andere an eine von ihnen. Das lässt sich mit kniffligen Regeln lösen iptables, aber sie müssen Pakete ändern, was für das Debuggen unpraktisch ist. Es ist besser, Netzwerk-Namespaces (im Folgenden „netns“) zu verwenden.

Ein Netzwerk-Namespace enthält eine Reihe von Schnittstellen, Routing-Tabellen und NetFilter-Regeln, die von ähnlichen Objekten in anderen Netzen isoliert sind. Jeder Prozess läuft in einem Namespace und hat nur Zugriff auf die Objekte dieses Netzwerks. Standardmäßig verfügt das System über einen einzigen Netzwerk-Namespace für alle Objekte, sodass Sie unter Linux arbeiten können, ohne sich mit NetNs auskennen zu müssen.

Lassen Sie uns einen neuen Namespace erstellen xdp-test und verschieben Sie es dorthin xdp-remote.

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

Dann läuft der Prozess ab xdp-test, Wird nicht sehen" xdp-local (es bleibt standardmäßig in netns) und wenn ein Paket an 192.0.2.1 gesendet wird, wird es weitergeleitet xdp-remoteweil es die einzige Schnittstelle auf 192.0.2.0/24 ist, auf die dieser Prozess zugreifen kann. Dies funktioniert auch in umgekehrter Richtung.

Beim Wechsel zwischen Netns fällt die Schnittstelle aus und verliert ihre Adresse. Um die Schnittstelle in netns zu konfigurieren, müssen Sie ausführen ip ... in diesem Befehlsnamensraum 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

Wie Sie sehen, unterscheidet sich dies nicht von der Einstellung xdp-local im Standard-Namespace:

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

Wenn du läufst tcpdump -tnevi xdp-local, können Sie sehen, dass Pakete gesendet werden von xdp-test, werden an diese Schnittstelle geliefert:

ip netns exec xdp-test   ping 192.0.2.1

Es ist praktisch, eine Shell zu starten xdp-test. Das Repository verfügt über ein Skript, das die Arbeit mit dem Stand automatisiert; Sie können den Stand beispielsweise mit dem Befehl konfigurieren sudo ./stand up und löschen Sie es sudo ./stand down.

Nachverfolgung

Der Filter ist dem Gerät wie folgt zugeordnet:

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

Schlüssel -force Wird benötigt, um ein neues Programm zu verknüpfen, wenn bereits ein anderes verknüpft ist. Bei „No news is good news“ geht es nicht um diesen Befehl, das Fazit ist auf jeden Fall umfangreich. angeben verbose optional, aber damit erscheint ein Bericht über die Arbeit des Code-Verifizierers mit einer Assembly-Auflistung:

Verifier analysis:

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

Trennen Sie das Programm von der Schnittstelle:

ip link set dev xdp-local xdp off

Im Skript sind dies Befehle sudo ./stand attach и sudo ./stand detach.

Durch das Anbringen eines Filters können Sie dies sicherstellen ping läuft weiterhin, aber funktioniert das Programm? Fügen wir Protokolle hinzu. Funktion bpf_trace_printk() ähnlich zu printf(), unterstützt aber nur bis zu drei andere Argumente als das Muster und eine begrenzte Liste von Spezifizierern. Makro bpf_printk() vereinfacht den Anruf.

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

Die Ausgabe geht an den Kernel-Trace-Kanal, der aktiviert werden muss:

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

Nachrichtenthread anzeigen:

cat /sys/kernel/debug/tracing/trace_pipe

Beide Befehle führen einen Anruf durch sudo ./stand log.

Ping sollte nun Meldungen wie diese auslösen:

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

Wenn Sie sich die Ausgabe des Prüfers genau ansehen, werden Sie seltsame Berechnungen bemerken:

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

Tatsache ist, dass eBPF-Programme keinen Datenabschnitt haben, sodass die einzige Möglichkeit, eine Formatzeichenfolge zu kodieren, die unmittelbaren Argumente von VM-Befehlen sind:

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

Aus diesem Grund bläht die Debug-Ausgabe den resultierenden Code stark auf.

Senden von XDP-Paketen

Ändern wir den Filter: Lassen Sie ihn alle eingehenden Pakete zurücksenden. Aus Netzwerksicht ist das falsch, da die Adressen in den Headern geändert werden müssten, aber jetzt kommt es auf die grundsätzliche Arbeit an.

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

Rennen tcpdump auf xdp-remote. Es sollte identische ausgehende und eingehende ICMP-Echo-Anfragen anzeigen und keine ICMP-Echo-Antwort mehr anzeigen. Aber es wird nicht angezeigt. Es stellt sich heraus, dass es sich um die Arbeit handelt XDP_TX im Programm auf xdp-local notwendigzur Paarschnittstelle xdp-remote Es wurde auch ein Programm zugewiesen, auch wenn es leer war, und er wurde angehoben.

Woher wusste ich das?

Verfolgen Sie den Pfad eines Pakets im Kernel Der Perf-Events-Mechanismus ermöglicht übrigens die Verwendung derselben virtuellen Maschine, d. h. eBPF wird für Disassemblierungen mit eBPF verwendet.

Du musst aus dem Bösen Gutes machen, denn es gibt nichts anderes, woraus man es machen könnte.

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

Was ist Code 6?

$ errno 6
ENXIO 6 No such device or address

Funktion veth_xdp_flush_bq() erhält einen Fehlercode von veth_xdp_xmit(), wo Suche nach ENXIO und finde den Kommentar.

Stellen wir den Mindestfilter wieder her (XDP_PASS) im Ordner xdp_dummy.c, fügen Sie es dem Makefile hinzu und binden Sie es daran xdp-remote:

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

Jetzt tcpdump zeigt, was erwartet wird:

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

Wenn stattdessen nur ARPs angezeigt werden, müssen Sie die Filter entfernen (dies ist der Fall). sudo ./stand detach), Lass los ping, legen Sie dann Filter fest und versuchen Sie es erneut. Das Problem ist, dass der Filter XDP_TX gültig sowohl auf ARP als auch im Stack
Namensräume xdp-test Wenn es Ihnen gelungen ist, die MAC-Adresse 192.0.2.1 zu „vergessen“, kann diese IP nicht aufgelöst werden.

Formulierung des Problems

Fahren wir mit der genannten Aufgabe fort: Schreiben eines SYN-Cookie-Mechanismus auf XDP.

SYN-Flood ist nach wie vor ein beliebter DDoS-Angriff, dessen Kern wie folgt ist. Wenn eine Verbindung hergestellt wird (TCP-Handshake), empfängt der Server ein SYN, weist Ressourcen für die zukünftige Verbindung zu, antwortet mit einem SYNACK-Paket und wartet auf ein ACK. Der Angreifer sendet einfach Tausende von SYN-Paketen pro Sekunde von gefälschten Adressen von jedem Host in einem Botnetz mit mehreren Tausend Mitgliedern. Der Server ist gezwungen, Ressourcen sofort beim Eintreffen des Pakets zuzuweisen, gibt diese jedoch nach einer langen Zeitüberschreitung frei; in der Folge sind Speicher oder Limits erschöpft, neue Verbindungen werden nicht akzeptiert und der Dienst ist nicht verfügbar.

Wenn Sie keine Ressourcen basierend auf dem SYN-Paket zuweisen, sondern nur mit einem SYNACK-Paket antworten, wie kann der Server dann verstehen, dass sich das später angekommene ACK-Paket auf ein nicht gespeichertes SYN-Paket bezieht? Schließlich kann ein Angreifer auch gefälschte ACKs generieren. Der Zweck des SYN-Cookies besteht darin, ihn zu kodieren seqnum Verbindungsparameter als Hash von Adressen, Ports und wechselndem Salt. Wenn die ACK vor der Salt-Änderung eingetroffen ist, können Sie den Hash erneut berechnen und vergleichen acknum. Schmiede acknum Der Angreifer kann dies nicht, da das Salt das Geheimnis enthält und aufgrund des begrenzten Kanals keine Zeit hat, es zu durchsuchen.

Das SYN-Cookie ist seit langem im Linux-Kernel implementiert und kann sogar automatisch aktiviert werden, wenn SYNs zu schnell und massenhaft eintreffen.

Bildungsprogramm zum TCP-Handshake

TCP ermöglicht die Datenübertragung als Bytestrom. Beispielsweise werden HTTP-Anfragen über TCP übertragen. Der Stream wird stückweise in Paketen übertragen. Alle TCP-Pakete haben logische Flags und 32-Bit-Sequenznummern:

  • Die Kombination der Flags bestimmt die Rolle eines bestimmten Pakets. Das SYN-Flag zeigt an, dass dies das erste Paket des Absenders auf der Verbindung ist. Das ACK-Flag bedeutet, dass der Absender alle Verbindungsdaten bis auf das Byte erhalten hat acknum. Ein Paket kann mehrere Flags haben und wird durch deren Kombination beispielsweise als SYNACK-Paket bezeichnet.

  • Die Sequenznummer (seqnum) gibt den Offset im Datenstrom für das erste Byte an, das in diesem Paket übertragen wird. Wenn diese Zahl beispielsweise im ersten Paket mit X Bytes an Daten N war, ist sie im nächsten Paket mit neuen Daten N+X. Zu Beginn der Verbindung wählt jede Seite diese Nummer zufällig.

  • Bestätigungsnummer (acknum) – der gleiche Offset wie seqnum, bestimmt jedoch nicht die Nummer des übertragenen Bytes, sondern die Nummer des ersten Bytes vom Empfänger, das der Absender nicht gesehen hat.

Zu Beginn der Verbindung müssen sich die Parteien einigen seqnum и acknum. Der Client sendet mit seinem ein SYN-Paket seqnum = X. Der Server antwortet mit einem SYNACK-Paket, in dem er seine Daten aufzeichnet seqnum = Y und entlarvt acknum = X + 1. Der Client antwortet auf SYNACK mit einem ACK-Paket seqnum = X + 1, acknum = Y + 1. Danach beginnt die eigentliche Datenübertragung.

Wenn der Peer den Empfang des Pakets nicht bestätigt, sendet TCP es nach einer Zeitüberschreitung erneut.

Warum werden SYN-Cookies nicht immer verwendet?

Erstens müssen Sie bei Verlust von SYNACK oder ACK auf die erneute Versendung warten – der Verbindungsaufbau verlangsamt sich. Zweitens im SYN-Paket – und nur darin! — Es werden eine Reihe von Optionen übermittelt, die sich auf den weiteren Betrieb der Verbindung auswirken. Ohne sich an eingehende SYN-Pakete zu erinnern, ignoriert der Server diese Optionen; der Client wird sie in den nächsten Paketen nicht senden. TCP kann in diesem Fall funktionieren, aber zumindest im Anfangsstadium nimmt die Qualität der Verbindung ab.

Aus Paketsicht muss ein XDP-Programm Folgendes tun:

  • auf SYN mit SYNACK mit einem Cookie antworten;
  • auf ACK mit RST antworten (Verbindung trennen);
  • Verwerfen Sie die verbleibenden Pakete.

Pseudocode des Algorithmus zusammen mit Paketparsing:

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

Eins (*) Punkte, an denen Sie den Zustand des Systems verwalten müssen, sind markiert – im ersten Schritt können Sie darauf verzichten, indem Sie einfach einen TCP-Handshake mit der Generierung eines SYN-Cookies als Folge implementieren.

Vor Ort (**), obwohl wir keinen Tisch haben, werden wir das Paket überspringen.

Implementierung des TCP-Handshakes

Parsen des Pakets und Überprüfen des Codes

Wir benötigen Netzwerk-Header-Strukturen: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) und TCP (uapi/linux/tcp.h). Letzteres konnte ich aufgrund von Fehlern nicht verbinden atomic64_t, musste ich die notwendigen Definitionen in den Code kopieren.

Alle Funktionen, die in C zur besseren Lesbarkeit hervorgehoben sind, müssen zum Zeitpunkt des Aufrufs inline sein, da der eBPF-Verifizierer im Kernel Backtracking, also Schleifen und Funktionsaufrufe, verbietet.

#define INTERNAL static __attribute__((always_inline))

Makro LOG() Deaktiviert das Drucken im Release-Build.

Das Programm ist ein Funktionsübermittler. Jeder erhält ein Paket, in dem der entsprechende Level-Header hervorgehoben ist, zum Beispiel process_ether() erwartet, dass es gefüllt ist ether. Basierend auf den Ergebnissen der Feldanalyse kann die Funktion das Paket an eine höhere Ebene weiterleiten. Das Ergebnis der Funktion ist die XDP-Aktion. Derzeit leiten die SYN- und ACK-Handler alle Pakete weiter.

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

Ich mache Sie auf die mit A und B gekennzeichneten Häkchen aufmerksam. Wenn Sie A auskommentieren, wird das Programm erstellt, beim Laden tritt jedoch ein Überprüfungsfehler auf:

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!

Schlüsselzeichenfolge invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0): Es gibt Ausführungspfade, wenn das dreizehnte Byte vom Anfang des Puffers außerhalb des Pakets liegt. Aus der Auflistung ist schwer zu erkennen, um welche Zeile es sich handelt, aber es gibt eine Anweisungsnummer (12) und einen Disassembler, der die Zeilen des Quellcodes zeigt:

llvm-objdump -S xdp_filter.o | less

In diesem Fall zeigt es auf die Linie

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

was deutlich macht, dass das Problem besteht ether. Es würde immer so sein.

Auf SYN antworten

Das Ziel in dieser Phase besteht darin, ein korrektes SYNACK-Paket mit einem festen Wert zu generieren seqnum, das in Zukunft durch das SYN-Cookie ersetzt wird. Alle Änderungen erfolgen in process_tcp_syn() und Umgebung.

Paketüberprüfung

Seltsamerweise ist hier die bemerkenswerteste Zeile, oder besser gesagt, der Kommentar dazu:

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

Beim Schreiben der ersten Version des Codes wurde der 5.1-Kernel verwendet, für dessen Verifizierer es einen Unterschied gab data_end и (const void*)ctx->data_end. Zum Zeitpunkt des Verfassens dieses Artikels hatte Kernel 5.3.1 dieses Problem nicht. Es ist möglich, dass der Compiler anders auf eine lokale Variable als auf ein Feld zugegriffen hat. Moral der Geschichte: Die Vereinfachung des Codes kann hilfreich sein, wenn es viele Verschachtelungen gibt.

Als nächstes folgen routinemäßige Längenprüfungen zum Ruhm des Prüfers. Ö MAX_CSUM_BYTES unten.

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

Das Paket auffalten

füllen seqnum и acknum, ACK setzen (SYN ist bereits gesetzt):

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

Tauschen Sie TCP-Ports, IP-Adresse und MAC-Adressen aus. Die Standardbibliothek ist daher über das XDP-Programm nicht zugänglich memcpy() – ein Makro, das die Clang-Funktionen verbirgt.

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

Neuberechnung von Prüfsummen

IPv4- und TCP-Prüfsummen erfordern das Hinzufügen aller 16-Bit-Wörter in den Headern, und die Größe der Header wird in sie geschrieben, ist also zur Kompilierungszeit unbekannt. Dies stellt ein Problem dar, da der Prüfer die normale Schleife zur Grenzvariablen nicht überspringt. Die Größe der Header ist jedoch begrenzt: jeweils bis zu 64 Byte. Sie können eine Schleife mit einer festen Anzahl von Iterationen erstellen, die vorzeitig enden kann.

Ich stelle fest, dass es das gibt RFC 1624 darüber, wie man die Prüfsumme teilweise neu berechnet, wenn nur die festen Wörter der Pakete geändert werden. Allerdings ist die Methode nicht universell und die Implementierung wäre schwieriger aufrechtzuerhalten.

Prüfsummenberechnungsfunktion:

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

Trotz der Tatsache, dass size Wird die Schleife vom aufrufenden Code verifiziert, ist die zweite Exit-Bedingung erforderlich, damit der Verifizierer den Abschluss der Schleife nachweisen kann.

Für 32-Bit-Wörter ist eine einfachere Version implementiert:

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

Tatsächliche Neuberechnung der Prüfsummen und Zurücksenden des Pakets:

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() Erstellt gemäß RFC 32 eine Prüfsumme aus einer 16-Bit-Summe von 791-Bit-Wörtern.

TCP-Handshake-Überprüfung

Der Filter stellt korrekt eine Verbindung her netcat, es fehlte das endgültige ACK, auf das Linux mit einem RST-Paket reagierte, da der Netzwerkstapel SYN nicht empfing – es wurde in SYNACK konvertiert und zurückgesendet – und aus Sicht des Betriebssystems ein Paket ankam, das nichts mit open zu tun hatte Verbindungen.

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

Es ist wichtig, bei vollständigen Bewerbungen zu prüfen und zu beachten tcpdump auf xdp-remote denn zum Beispiel hping3 reagiert nicht auf falsche Prüfsummen.

Aus XDP-Sicht ist die Verifizierung selbst trivial. Der Berechnungsalgorithmus ist primitiv und wahrscheinlich anfällig für einen raffinierten Angreifer. Der Linux-Kernel verwendet beispielsweise das kryptografische SipHash, dessen Implementierung für XDP geht jedoch eindeutig über den Rahmen dieses Artikels hinaus.

Für neue TODOs im Zusammenhang mit der externen Kommunikation eingeführt:

  • Das XDP-Programm kann nicht speichern cookie_seed (der geheime Teil des Salzes) in einer globalen Variablen, benötigen Sie einen Speicher im Kernel, dessen Wert regelmäßig von einem zuverlässigen Generator aktualisiert wird.

  • Wenn das SYN-Cookie im ACK-Paket übereinstimmt, müssen Sie keine Nachricht drucken, sondern sich die IP des verifizierten Clients merken, um weiterhin Pakete von diesem weiterzuleiten.

Überprüfung des legitimen Kunden:

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

Aus den Protokollen geht hervor, dass die Prüfung erfolgreich war (flags=0x2 - das ist SYN, flags=0x10 ist 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

Es gibt zwar keine Liste verifizierter IPs, es gibt jedoch keinen Schutz vor der SYN-Flut selbst, aber hier ist die Reaktion auf eine ACK-Flut, die durch den folgenden Befehl gestartet wird:

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

Protokolleinträge:

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

Abschluss

Manchmal werden eBPF im Allgemeinen und XDP im Besonderen eher als fortgeschrittenes Administratortool denn als Entwicklungsplattform dargestellt. Tatsächlich ist XDP ein Tool zur Beeinträchtigung der Verarbeitung von Paketen durch den Kernel und keine Alternative zum Kernel-Stack, wie DPDK und andere Kernel-Bypass-Optionen. Andererseits ermöglicht Ihnen XDP die Implementierung einer recht komplexen Logik, die darüber hinaus einfach zu aktualisieren ist, ohne dass die Verkehrsverarbeitung unterbrochen wird. Der Verifier bereitet keine großen Probleme; ich persönlich würde ihn für Teile des Userspace-Codes nicht ablehnen.

Wenn das Thema interessant ist, werden wir im zweiten Teil die Tabelle der verifizierten Clients und Verbindungsabbrüche vervollständigen, Zähler implementieren und ein Userspace-Dienstprogramm zur Verwaltung des Filters schreiben.

Links:

Source: habr.com

Kommentar hinzufügen