Wir schreiben Schutz gegen DDoS-Angriffe auf XDP. Kernteil

Die XDP-Technologie (EXpress Data Path) ermöglicht die willkürliche Verarbeitung des Datenverkehrs auf Linux-Schnittstellen, bevor Pakete auf dem Kernel-Netzwerkstapel eintreffen. Anwendung von XDP - Schutz vor DDoS-Angriffen (CloudFlare), ausgefeilte Filter, Statistiksammlung (Netflix). XDP-Programme werden von der virtuellen eBPF-Maschine ausgeführt. Daher unterliegen sie je nach Filtertyp Einschränkungen hinsichtlich des Codes und der verfügbaren Kernelfunktionen.


Der Artikel soll die Mängel zahlreicher XDP-Materialien ausgleichen. Erstens stellen sie vorgefertigten Code bereit, der die Funktionen von XDP sofort umgeht: Zur Überprüfung vorbereitet oder zu einfach, um Probleme zu verursachen. Wenn Sie versuchen, Ihren Code von Grund auf neu zu schreiben, wissen Sie nicht, wie Sie mit typischen Fehlern umgehen sollen. Zweitens werden Methoden zum lokalen Testen von XDP ohne VMs 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 detailliert untersuchen, wie der XDP-Filter zusammengesetzt ist und wie er getestet wird. Anschließend werden wir eine einfache Version des bekannten SYN-Cookie-Mechanismus auf Paketverarbeitungsebene schreiben. Wir werden zwar keine "weiße Liste" bilden
Kunden verifiziert, Zähler geführt und Filter verwaltet - genügend Protokolle.


Wir werden in C schreiben - das ist nicht in Mode, aber praktisch. Der gesamte Code ist auf GitHub über den Link am Ende verfügbar und wird gemäß den im Artikel beschriebenen Schritten in Commits unterteilt.


Haftungsausschluss. Während des Artikels wird eine Minilösung entwickelt, um DDoS-Angriffe abzuwehren, da dies eine realistische Aufgabe für XDP und meinen Bereich ist. Das Hauptziel ist jedoch der Umgang mit Technologie. Dies ist kein Leitfaden für die Schaffung eines vorgefertigten Schutzes. Der Trainingscode ist nicht optimiert und lässt einige Nuancen aus.


XDP auf einen Blick


Ich werde nur wichtige Punkte skizzieren, um die Dokumentation und die vorhandenen Artikel nicht zu duplizieren.


Der Filtercode wird also in den Kernel geladen. Eingehende Pakete werden an den Filter gesendet. Infolgedessen muss der Filter eine Entscheidung treffen: Überspringen Sie das Paket zum Kernel ( XDP_PASS ), verwerfen Sie das Paket ( XDP_DROP ) oder senden Sie es zurück ( XDP_TX ). Der Filter kann das Paket ändern, dies gilt insbesondere für XDP_TX . Sie können das Programm auch zum Absturz bringen ( XDP_ABORTED ) und das Paket verwerfen. Dies ist jedoch ein Analogon von assert(0) zum Debuggen.


Die virtuelle eBPF-Maschine (erweiterter Berkley Packet Filter) wurde speziell vereinfacht, damit der Kernel überprüfen kann, ob der Code keine Schleife durchläuft und den Speicher eines anderen nicht beschädigt. Aggregierte Einschränkungen und Überprüfungen:


  • Verbotene Zyklen (zurückspringen).
  • Es gibt einen Stapel für Daten, aber keine Funktionen (alle C-Funktionen müssen inline sein).
  • Der Zugriff auf Speicher außerhalb des Stapels und des Paketpuffers ist verboten.
  • Die Codegröße ist begrenzt, aber in der Praxis ist dies nicht sehr wichtig.
  • Aufrufe sind nur für spezielle Kernelfunktionen (eBPF-Helfer) zulässig.

Das Design und die Installation des Filters sehen folgendermaßen aus:


  1. Der Quellcode (z. B. kernel.c ) wird unter der Architektur der virtuellen eBPF-Maschine in das Objekt ( kernel.o ) kompiliert. Ab Oktober 2019 wird die Kompilierung in eBPF von Clang unterstützt und in GCC 10.1 versprochen.
  2. Wenn in diesem Objektcode Kernelstrukturen (z. B. Tabellen und Zähler) aufgerufen werden, werden anstelle ihrer IDs Nullen verwendet, dh, ein solcher Code kann nicht ausgeführt werden. Vor dem Laden in den Kernel müssen Sie diese Nullen durch die ID bestimmter Objekte ersetzen, die durch Kernelaufrufe erstellt wurden (Linkcode). 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 Schleifen und Fehlzeiten über die Grenzen von Paket und Stapel hinaus werden überprüft. Wenn der Prüfer nicht nachweisen kann, dass der Code korrekt ist, wird das Programm abgelehnt - Sie müssen in der Lage sein, ihm zu gefallen.
  4. Nach erfolgreicher Überprüfung kompiliert der Kernel den eBPF-Architekturobjektcode in den Maschinencode der Systemarchitektur (Just-in-Time).
  5. Das Programm wird an die Schnittstelle angeschlossen und beginnt mit der Verarbeitung von Paketen.

Da XDP im Kernel funktioniert, erfolgt das Debuggen über Ablaufverfolgungsprotokolle und tatsächlich über Pakete, die das Programm filtert oder generiert. EBPF bietet jedoch Sicherheit für den geladenen Code des Systems, sodass Sie direkt unter lokalem Linux mit XDP experimentieren können.


Umweltvorbereitung


Montage


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


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

Beim Schreiben eines Filters sind einige Dateien mit Zusatzfunktionen und Makros aus Kerneltests hilfreich. Es ist wichtig, dass sie mit der Kernel-Version ( KVER ) KVER . Laden Sie sie in helpers/ herunter:


 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 - die Architektur des Systems. Pfade und Werkzeuge können zwischen den Verteilungen geringfügig variieren.


Differenzbeispiel 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 enthalten ein Verzeichnis mit zusätzlichen Headern und mehrere Verzeichnisse mit Kernel-Headern. Das Symbol __KERNEL__ bedeutet, dass UAPI-Header (Userspace-APIs) für den Kernelcode definiert sind, da der Filter im Kernel ausgeführt wird.


Der -fno-stack-protector kann deaktiviert werden ( -fno-stack-protector ), da der eBPF-Code-Verifizierer weiterhin nach einem Ausweg aus dem Stapel sucht. Die Optimierung sollte sofort einbezogen werden, da die Größe des eBPF-Bytecodes begrenzt ist.


Beginnen wir mit einem Filter, der alle Pakete überspringt 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"; 

Der Befehl make xdp_filter.o . Wo kann man es jetzt testen?


Prüfstand


Der Stand sollte zwei Schnittstellen enthalten: auf denen sich ein Filter befindet und von denen Pakete gesendet werden. Dies müssen vollwertige Linux-Geräte mit ihrer IP sein, um zu überprüfen, wie reguläre Anwendungen mit unserem Filter funktionieren.


Geräte wie veth (virtuelles Ethernet) sind für uns geeignet: Sie sind ein Paar virtueller Netzwerkschnittstellen, die direkt miteinander „verbunden“ sind. Sie können sie folgendermaßen erstellen (in diesem Abschnitt werden alle ip Befehle als root ):


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

Hier sind xdp-remote und xdp-local Gerätenamen. Ein Filter wird an xdp-local (192.0.2.1/24) angehängt, und eingehender Datenverkehr wird von xdp-remote (192.0.2.2/24) gesendet. Es gibt jedoch ein Problem: Die Schnittstellen befinden sich auf demselben Computer, und Linux sendet keinen Datenverkehr über den anderen an einen von ihnen. Sie können dies mit den kniffligen iptables Regeln lösen, aber sie müssen die Pakete ändern, was beim Debuggen unpraktisch ist. Es ist besser, Netzwerk-Namespaces (Netzwerk-Namespaces, im Folgenden netns) zu verwenden.


Der Netzwerk-Namespace enthält eine Reihe von Schnittstellen, Routing-Tabellen und NetFilter-Regeln, die von ähnlichen Objekten in anderen Netzwerken isoliert sind. Jeder Prozess wird in einem Namespace ausgeführt, auf den nur Objekte dieser Netze zugreifen können. Standardmäßig verfügt das System über einen einzigen Netzwerk-Namespace für alle Objekte, sodass Sie unter Linux arbeiten können und keine Kenntnisse über Netze haben.


Erstellen Sie einen neuen xdp-test Namespace und verschieben Sie xdp-remote .


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

Dann wird der in xdp-test Prozess xdp-local nicht "sehen" (er bleibt standardmäßig in netns) und wird beim Senden eines Pakets an 192.0.2.1 über xdp-remote , da dies die einzige Schnittstelle in 192.0.2.0/ ist. 24 für diesen Prozess verfügbar. Dies funktioniert auch in die entgegengesetzte Richtung.


Beim Wechsel zwischen den Netzwerken wird die Schnittstelle gelöscht und verliert die Adresse. Um die Schnittstelle in netns zu konfigurieren, müssen Sie ip ... in diesem ip netns exec Befehls 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 von xdp-local im Standard-Namespace:


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

Wenn Sie tcpdump -tnevi xdp-local , können Sie sehen, dass von xdp-test gesendete Pakete an diese Schnittstelle gesendet werden:


 ip netns exec xdp-test ping 192.0.2.1 

Es ist praktisch, die Shell im xdp-test . Im Repository befindet sich ein Skript, das die Arbeit mit dem Stand automatisiert. Sie können den Stand beispielsweise mit dem Befehl sudo ./stand up konfigurieren und mit dem Befehl sudo ./stand down löschen.


Trace


Der Filter wird wie folgt an das Gerät angeschlossen:


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

Der -force benötigt, um ein neues Programm zu binden, wenn bereits ein anderes gebunden ist. "Keine Nachricht ist eine gute Nachricht" handelt nicht von diesem Befehl, die Schlussfolgerung ist auf jeden Fall umfangreich. verbose optional, aber damit wird ein Bericht über die Arbeit des Codeverifizierers mit der Assembly-Liste angezeigt:


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

Lösen Sie das Programm von der Oberfläche:


 ip link set dev xdp-local xdp off 

Im Skript sind dies die sudo ./stand detach sudo ./stand attach und sudo ./stand detach .


Durch Anhängen eines Filters können Sie überprüfen, ob ping weiterhin funktioniert. Funktioniert das Programm jedoch? Fügen Sie die Protokolle hinzu. Die Funktion bpf_trace_printk() ähnelt printf() , unterstützt jedoch nur bis zu drei Argumente, mit Ausnahme der Vorlage und einer begrenzten Liste von Qualifizierern. Das Makro bpf_printk() vereinfacht den Aufruf.


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

Die Ausgabe geht an den Kernel-Trace-Kanal, den Sie aktivieren müssen:


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

Nachrichtenfluss anzeigen:


 cat /sys/kernel/debug/tracing/trace_pipe 

Beide Befehle rufen sudo ./stand log .


Ping sollte nun die folgenden Meldungen auslösen:


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

Wenn Sie sich die Ausgabe des Überprü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. Die einzige Möglichkeit, eine Formatzeichenfolge zu codieren, besteht in den unmittelbaren Argumenten der VM-Befehle:


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

Aus diesem Grund erhöht die Debug-Ausgabe den resultierenden Code erheblich.


Senden von XDP-Paketen


Lassen Sie uns den Filter ändern: Lassen Sie ihn alle eingehenden Pakete zurücksenden. Dies ist aus Netzwerksicht falsch, da die Adressen in den Headern geändert werden müssten, aber jetzt ist die Arbeit im Prinzip wichtig.


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

Führen Sie tcpdump auf xdp-remote . Es sollte identische ausgehende und eingehende ICMP-Echoanforderung anzeigen und keine ICMP-Echoantwort mehr anzeigen. Zeigt aber nicht. Es stellt sich heraus, dass für XDP_TX , damit es in einem Programm auf xdp-local , die xdp-remote Schnittstelle xdp-remote werden muss, auch wenn sie leer ist, und sie muss xdp-remote werden.


Wie habe ich es herausgefunden?

Der Perf-Ereignismechanismus, der dieselbe virtuelle Maschine verwendet, ermöglicht es übrigens, den Paketpfad im Kernel zu verfolgen , dh eBPF wird zum Zerlegen mit eBPF verwendet.


Aus dem Bösen muss man Gutes machen, denn es gibt nichts mehr zu machen.

 $ 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 

Die Funktion veth_xdp_flush_bq() erhält einen Fehlercode von veth_xdp_xmit() , wo wir nach ENXIO suchen und einen Kommentar finden.


XDP_PASS den Mindestfilter ( XDP_PASS ) in der Datei xdp_dummy.c , fügen Sie ihn dem Makefile hinzu und hängen Sie ihn an xdp-remote :


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

Jetzt zeigt tcpdump , 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 ARP angezeigt wird, müssen Sie die Filter entfernen (dies erfolgt durch sudo ./stand detach ), den ping starten, dann die Filter einstellen und es erneut versuchen. Das Problem ist, dass der XDP_TX Filter sowohl ARP als auch den Stack beeinflusst
xdp-test Namespace hat es geschafft, die MAC-Adresse 192.0.2.1 zu "vergessen". Diese IP kann nicht aufgelöst werden.


Erklärung des Problems


Fahren wir mit der angegebenen Aufgabe fort: Schreiben Sie den SYN-Cookie-Mechanismus auf XDP.


Bis jetzt ist die SYN-Flut ein beliebter DDoS-Angriff, dessen Kern wie folgt ist. Beim Herstellen einer Verbindung (TCP-Handshake) empfängt der Server eine SYN, weist Ressourcen für eine zukünftige Verbindung zu, antwortet mit einem SYNACK-Paket und wartet auf eine ACK. Der Angreifer sendet einfach SYN-Pakete von gefälschten Adressen in Höhe von Tausenden pro Sekunde von jedem Host aus einem Multi-Tausend-Botnetz. Der Server ist gezwungen, Ressourcen sofort nach dem Eintreffen des Pakets zuzuweisen, und wird durch ein großes Zeitlimit frei, wodurch Speicher oder Limits erschöpft werden, neue Verbindungen nicht akzeptiert werden und der Dienst nicht verfügbar ist.


Wenn Sie dem SYN-Paket keine Ressourcen zuweisen, sondern nur mit einem SYNACK-Paket antworten, wie kann der Server dann verstehen, dass das später eingegangene ACK-Paket auf ein nicht gespeichertes SYN-Paket verweist? Schließlich kann ein Angreifer auch gefälschte ACKs generieren. Die Essenz des SYN-Cookies besteht darin, die seqnum in seqnum als Hash aus Adressen, Ports und wechselndem Salt zu codieren. Wenn die ACK vor dem acknum angekommen ist, können Sie den Hash erneut berechnen und mit acknum vergleichen. Der Angreifer kann kein acknum vortäuschen, da das Salz ein Geheimnis enthält und aufgrund des begrenzten Kanals keine Zeit zum acknum hat.


SYN-Cookie ist seit langem im Linux-Kernel implementiert und wird möglicherweise sogar automatisch aktiviert, wenn SYN zu schnell und in großen Mengen eintrifft.


Bildungsprogramm zum TCP-Handshake

TCP stellt die Datenübertragung als Bytestrom bereit. Beispielsweise werden HTTP-Anforderungen über TCP gesendet. Der Stream wird in Paketen in Stücken übertragen. Alle TCP-Pakete haben logische Flags und 32-Bit-Sequenznummern:


  • Die Kombination von Flags bestimmt die Rolle eines bestimmten Pakets. Das SYN-Flag bedeutet, dass dies das erste Senderpaket in der Verbindung ist. Das ACK-Flag bedeutet, dass der Absender alle Verbindungsdaten vor dem acknum . Ein Paket kann mehrere Flags haben und wird durch ihre Kombination aufgerufen, beispielsweise ein SYNACK-Paket.


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


  • Bestätigungsnummer (acknum) - der gleiche Versatz wie seqnum, bestimmt jedoch nicht die Anzahl der zu übertragenden Bytes, sondern die Nummer des ersten Bytes des Empfängers, das der Absender nicht gesehen hat.



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


Wenn der Gesprächspartner den Empfang des Pakets nicht bestätigt, sendet TCP es erneut per Timeout.


Warum werden SYN-Cookies nicht immer verwendet?

Wenn SYNACK oder ACK verloren gehen, müssen Sie zunächst auf das erneute Senden warten - die Verbindung wird verlangsamt. Zweitens im SYN-Paket - und nur darin! - Es werden eine Reihe von Optionen übertragen, die sich auf den weiteren Betrieb der Verbindung auswirken. Ohne sich an die eingehenden SYN-Pakete zu erinnern, ignoriert der Server diese Optionen. In den nächsten Paketen sendet der Client sie nicht mehr. In diesem Fall kann TCP funktionieren, aber zumindest in der Anfangsphase nimmt die Qualität der Verbindung ab.


In Bezug auf Pakete sollte ein XDP-Programm Folgendes tun:


  • SYNACK mit Cookie, um auf SYN zu antworten;
  • auf ACK RST reagieren (trennen);
  • andere Pakete verwerfen.

Algorithmus Pseudocode zusammen mit dem Parsen des Pakets:


    Ethernet,  .    IPv4,  .     , (*)    ,  .    TCP,  . (**)   SYN,  SYN-ACK  cookie.   ACK,   acknum   cookie,  .      N  . (*)  RST. (**)     . 

Ein (*) gibt die Punkte an, an denen der Status des Systems gesteuert werden soll. In der ersten Phase können Sie auf diese Punkte verzichten, indem Sie einfach einen TCP-Handshake mit der Generierung eines SYN-Cookies als Folge implementieren.


An Ort und Stelle (**) überspringen wir das Paket, obwohl wir keine Tabelle haben.


TCP-Handshake-Implementierung


Analysieren Sie das Paket und überprüfen Sie den Code


Wir benötigen Netzwerk-Header-Strukturen: Ethernet ( uapi/linux/if_ether.h ), IPv4 ( uapi/linux/ip.h ) und TCP ( uapi/linux/tcp.h ). Als letztes konnte ich aufgrund von Fehlern im Zusammenhang mit atomic64_t keine Verbindung atomic64_t . Ich musste die erforderlichen Definitionen in den Code kopieren.


Alle Funktionen, die in C zur besseren Lesbarkeit zugewiesen sind, sollten am Ort des Aufrufs eingebaut werden, da der eBPF-Verifizierer im Kernel Rückübergänge verhindert, dh Schleifen und Funktionsaufrufe.


 #define INTERNAL static __attribute__((always_inline)) 

Das Makro LOG() deaktiviert das Drucken im Release-Build.


Das Programm ist ein Förderer von Funktionen. Jeder empfängt ein Paket, in dem der Header der entsprechenden Ebene hervorgehoben ist. Beispielsweise erwartet process_ether() dass der ether voll ist. Basierend auf den Ergebnissen der Feldanalyse kann die Funktion das Paket auf eine höhere Ebene übertragen. Das Ergebnis der Funktion ist die XDP-Aktion. Bisher übergeben SYN- und ACK-Handler alle Pakete.


 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 auf die mit A und B gekennzeichneten Schecks aufmerksam. Wenn Sie A auskommentieren, wird das Programm zusammengestellt, aber beim Laden tritt 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! 

Die Schlüsselzeile ist 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. Gemäß der Auflistung ist es schwierig zu verstehen, um welche Zeile es sich handelt, aber es gibt eine Anweisungsnummer (12) und einen Disassembler, der die Zeilen des Quellcodes anzeigt:


 llvm-objdump -S xdp_filter.o | less 

In diesem Fall zeigt es auf eine Zeichenfolge


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

womit klar ist, dass das Problem im ether . Es wäre immer so.


Antworte auf SYN


Das Ziel in dieser Phase ist es, ein korrektes SYNACK-Paket mit einer festen seqnum , das in Zukunft durch ein SYN-Cookie ersetzt wird. Alle Änderungen erfolgen in process_tcp_syn() und in der Umgebung.


Paketprüfung


Seltsamerweise ist hier die bemerkenswerteste Zeile, genauer gesagt, ein 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 data_end ein Unterschied zwischen data_end und (const void*)ctx->data_end . Beim Schreiben des Artikels hatte der 5.3.1-Kernel kein solches Problem. Möglicherweise hat der Compiler anders als im Feld auf die lokale Variable zugegriffen. Moralisch vereinfachter Code kann bei vielen Verschachtelungen helfen.


Weitere routinemäßige Längenprüfungen zu Ehren des Prüfers; über 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 */ } 

Paket verteilt


Füllen Sie seqnum und acknum , setzen Sie ACK (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-Adresse aus. Auf die Standardbibliothek kann vom XDP-Programm nicht zugegriffen werden, daher ist memcpy() ein Makro, das die Clang-Eigenschaft 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 der Prüfsumme


Die 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 diese geschrieben, dh zum Zeitpunkt der Kompilierung ist dies nicht bekannt. Dies ist ein Problem, da der Prüfer eine reguläre Schleife nicht zu einer variablen Grenze ü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 RFC 1624 gibt, wie die Prüfsumme teilweise neu berechnet werden kann, wenn nur feste Paketwörter geändert werden. Die Methode ist jedoch 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; } 

, size , , .


32- :


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

:


 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; 

carry() 32- 16- , RFC 791.


TCP


netcat , ACK, Linux RST-, SYN — SYNACK - , .


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

tcpdump xdp-remote , , hping3 .



XDP . , , . Linux, , SipHash, XDP .


TODO, :


  • XDP- cookie_seed ( ) , , .


  • SYN cookie ACK- , IP , .



:


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

( flags=0x2 — SYN, flags=0x10 — 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 

IP, SYN flood , ACK flood, :


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

:


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

Fazit


eBPF XDP , . , XDP — , , DPDK kernel bypass. , XDP , , , . , userspace-.


, , , userspace- .


Referenzen:


Source: https://habr.com/ru/post/de473286/


All Articles