Die eXpress Data Path (XDP)-Technologie ermöglicht eine beliebige Verarbeitung von Datenverkehr an Linux-Schnittstellen, bevor die Pakete in den Netzwerk-Stack des Kernels gelangen. Der Einsatz von XDP umfasst den Schutz vor DDoS-Angriffen (CloudFlare), komplexe Filter und das Sammeln von Statistiken (Netflix). XDP-Programme werden von der eBPF-virtuellen Maschine ausgeführt, sodass sie Einschränkungen bezüglich ihres Codes und der verfügbaren Kernel-Funktionen abhängig vom Filtertyp unterliegen.
Der Artikel soll die Mängel zahlreicher Materialien zu XDP ausgleichen. Erstens wird in diesen fertiger Code bereitgestellt, der sofort die Besonderheiten von XDP umgeht: er ist für die Verifikation vorbereitet oder zu einfach, um Probleme zu verursachen. Bei dem Versuch, eigenen Code von Grund auf zu schreiben, fehlt das Verständnis dafür, was mit typischen Fehlern zu tun ist. Zweitens werden Möglichkeiten zur lokalen Testung von XDP ohne VM und „Hardware“ nicht behandelt, obgleich auch diese ihre eigenen „Fallstricke“ haben. Der Text richtet sich an Programmierer, die mit Netzwerken und Linux vertraut sind und Interesse an XDP und eBPF haben.
In diesem Abschnitt werden wir detailliert untersuchen, wie ein XDP-Filter aufgebaut wird und wie man ihn testet, anschließend werden wir eine einfache Variante des bekannten Mechanismus der SYN-Cookies auf Paketeebene schreiben. Bis dahin werden wir keine „Whitelist“
von überprüften Clients erstellen, keine Zähler führen und den Filter verwalten – Logs sind ausreichend.
Wir werden in C schreiben – das ist nicht trendy, aber praktisch. Der gesamte Code ist auf GitHub unter dem Link am Ende verfügbar und in Commits entsprechend den in dem Artikel beschriebenen Phasen aufgeteilt.
Haftungsausschluss. Im Verlauf des Artikels wird eine Mini-Lösung zur Abwehr von DDoS-Angriffen entwickelt, da dies eine realistische Aufgabe für XDP und mein Gebiet ist. Das Hauptziel ist jedoch, die Technologie zu verstehen, es ist kein Handbuch zur Erstellung einer vollständigen Schutzlösung. Der Beispielcode ist nicht optimiert und lässt einige Nuancen außer Acht.
Kurze Übersicht über XDP
Ich werde nur die Schlüsselthemen ansprechen, um die Dokumentation und bereits bestehende Artikel nicht zu duplizieren.
Also wird der Filtercode in den Kernel geladen. Eingehende Pakete werden an den Filter übergeben. Am Ende muss der Filter eine Entscheidung treffen: Das Paket in den Kernel weiterleiten (XDP_PASS), das Paket verwerfen (XDP_DROP) oder es zurücksenden (XDP_TX). Der Filter kann das Paket ändern, was insbesondere für XDP_TXrelevant ist. Außerdem kann die Ausführung des Programms abrupt unterbrochen werden (XDP_ABORTED) und das Paket verworfen werden, aber dies ist vergleichbar mit assert(0) — für Debugging-Zwecke.
Die eBPF (extended Berkley Packet Filter) virtuelle Maschine wurde speziell so konzipiert, dass das Kernel überprüfen kann, ob der Code nicht in einer Schleife steckt und keinen fremden Speicher beschädigt. Gesamte Einschränkungen und Prüfungen:
- Schleifen (Rückwärtsübergänge) sind verboten.
- Es gibt einen Stack für Daten, aber keine Funktionen (alle C-Funktionen müssen eingebaut werden).
- Zugriffe auf den Speicher außerhalb des Stacks und des Paketpuffers sind verboten.
- Die Größe des Codes ist begrenzt, hat aber in der Praxis nicht allzu große Auswirkungen.
- Nur spezielle Kernel-Funktionen (eBPF-Helpers) sind zulässig.
Die Entwicklung und Installation eines Filters sieht folgendermaßen aus:
- Der Quellcode (zum Beispiel,
kernel.c) wird in ein Objekt (kernel.o) für die Architektur der eBPF virtuellen Maschine kompiliert. Ab Oktober 2019 wird die Kompilierung in eBPF von Clang unterstützt und in GCC 10.1 versprochen. - Wenn dieser Objektcode auf Kernel-Strukturen (zum Beispiel, auf Tabellen und Zähler) zugreift, stehen anstelle ihrer IDs Nullen, das heißt, dieser Code kann nicht ausgeführt werden. Vor dem Laden ins Kernel müssen diese Nullen durch die IDs konkreter Objekte ersetzt werden, die durch Kernelaufrufe erstellt wurden (den Code verlinken). Dies kann mit externen Hilfsprogrammen oder durch das Schreiben eines Programms geschehen, das den spezifischen Filter verlinkt und lädt.
- Das Kernel verifiziert das geladene Programm. Es wird auf das Vorhandensein von Schleifen und Überschreitungen der Paket- und Stackgrenzen geprüft. Wenn der Verifier nicht nachweisen kann, dass der Code korrekt ist, wird das Programm abgelehnt – man muss lernen, ihn zu besänftigen.
- Nach erfolgreicher Verifizierung kompiliert das Kernel den Objektcode der eBPF-Architektur in den Maschinencode der Systemarchitektur (just-in-time).
- Das Programm wird an die Schnittstelle angehängt und beginnt, Pakete zu verarbeiten.
Da XDP im Kernel arbeitet, erfolgt das Debugging über Trace-Logs und tatsächlich über die Pakete, die das Programm filtert oder erzeugt. Dennoch sorgt eBPF für die Sicherheit des geladenen Codes für das System, sodass man mit XDP direkt auf einem lokalen Linux experimentieren kann.
Vorbereitung der Umgebung
Bau
Clang kann keinen Objektcode direkt für eBPF-Architekturen erzeugen, daher besteht der Prozess aus zwei Schritten:
- Den C-Code in LLVM-Bitcode kompilieren (
clang -emit-llvm). - Den Bitcode in eBPF-Objektcode umwandeln (
llc -march=bpf -filetype=obj).
Beim Schreiben des Filters kann eine Reihe von Dateien mit Hilfsfunktionen und Makros von KVER). Wir laden sie herunterladen inhelpers/ Helfer/:
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 BASEMakefile für Arch Linux (Kern 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 ./*.oKDIR enthält den Pfad zu den Kernel-Headern, ARCH — die Architektur des Systems. Die Pfade und Werkzeuge können zwischen den Distributionen leicht variieren.
Beispiel für Unterschiede bei 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 binden das Verzeichnis der Hilfsheader und mehrere Verzeichnisse der Kernel-Header ein. Das Zeichen __KERNEL__ zeigt an, dass die UAPI-Header (Userspace API) für den Kernel-Code definiert sind, da der Filter im Kernel ausgeführt wird.
Der Stackschutz kann deaktiviert werden (-fno-stack-protector), weil der eBPF-Codeverifier die Überprüfung auf Stacküberlauf ohnehin durchführt. Es ist ratsam, gleich Optimierungen zu aktivieren, da die Größe des eBPF-Bitcodes begrenzt ist.
Beginnen wir mit einem Filter, der alle Pakete durchlässt und nichts macht:
#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 Daten über das Fenstersystem („Wayland“, „Wayland/drm“, „x11“) xdp_filter.o. Wo soll er jetzt getestet werden?
Teststand
Der Stand muss zwei Schnittstellen umfassen: eine, auf der der Filter läuft und eine, von der die Pakete gesendet werden. Es sollten vollständige Linux-Geräte mit ihren IPs sein, um zu überprüfen, wie normale Anwendungen mit unserem Filter arbeiten.
Geräte wie veth (virtuelles Ethernet) sind geeignet: Es handelt sich um ein Paar virtueller Netzwerkinterfaces, die direkt miteinander "verbunden" sind. Sie können so erstellt werden (in diesem Abschnitt werden alle Befehle ip von root):
ip link add xdp-remote type veth peer name xdp-localHier xdp-remote und xdp-local — die Namen der Geräte. An xdp-local (192.0.2.1/24) wird der Filter angeschlossen, von xdp-remote (192.0.2.2/24) wird der eingehende Verkehr gesendet. Es gibt jedoch ein Problem: Die Schnittstellen befinden sich auf demselben Gerät, und Linux wird den Verkehr nicht über die andere Schnittstelle leiten. Dies kann mit cleveren Regeln gelöst werden, iptablesaber sie müssen die Pakete ändern, was beim Debuggen unpraktisch ist. Es ist besser, Netzwerknamensräume (Network Namespaces, im Folgenden netns) zu verwenden.
Der Netzwerk-Namensraum enthält eine Sammlung von Schnittstellen, Routing-Tabellen und NetFilter-Regeln, die von ähnlichen Objekten in anderen netns isoliert sind. Jeder Prozess läuft in einem bestimmten Namensraum und hat Zugriff nur auf die Objekte dieses netns. Standardmäßig gibt es im System einen einzigen Netzwerk-Namensraum für alle Objekte, sodass man in Linux arbeiten kann, ohne von netns zu wissen.
Lassen Sie uns einen neuen Namensraum erstellen xdp-test und dorthin verschieben xdp-remote.
ip netns add xdp-test
ip link set dev xdp-remote netns xdp-testDann wird der Prozess, der in xdp-testläuft, "nicht sehen" xdp-local (er bleibt im standardmäßigen netns) und bei der Übertragung eines Pakets an 192.0.2.1 wird es über xdp-remoteweitergeleitet, weil dies das einzige Interface in 192.0.2.0/24 ist, das diesem Prozess zur Verfügung steht. Dies wirkt sich auch umgekehrt aus.
Beim Wechsel zwischen netns wird das Interface deaktiviert und verliert die Adresse. Um das Interface im netns zu konfigurieren, müssen Sie ip ... in diesem Namensraum ausführen 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 upWie man sehen kann, unterscheidet sich das nicht von der Konfiguration xdp-local im standardmäßigen Namensraum:
ip address add 192.0.2.1/24 dev xdp-local
ip link set xdp-local upWenn Sie tcpdump -tnevi xdp-localausführen, werden Sie sehen, dass Pakete, die von xdp-testgesendet werden, an dieses Interface zugestellt werden:
ip netns exec xdp-test ping 192.0.2.1Es ist praktisch, eine Shell in xdp-testzu starten. Im Repository gibt es ein Skript, das die Arbeit mit der Testumgebung automatisiert, beispielsweise kann die Testumgebung mit dem Befehl sudo .\/stand up eröffnet und entfernt werden sudo .\/stand down.
Tracing
Der Filter wird so an das Gerät gebunden:
ip -force link set dev xdp-local xdp object xdp_filter.o verboseSchlüssel -force ist erforderlich, um ein neues Programm zu binden, wenn bereits eines gebunden ist. "Keine Nachrichten sind gute Nachrichten" gilt nicht für diesen Befehl, die Ausgabe ist in jedem Fall umfangreich. Es ist nicht erforderlich, verbose anzugeben, aber damit erhalten Sie einen Bericht über die Arbeit des Code-Verifiers mit einer Liste des Assembler-Codes:
Verifier-Analyse:
0: (b7) r0 = 2
1: (95) exitUm das Programm vom Interface zu trennen:
ip link set dev xdp-local xdp offIn dem Skript sind dies die Befehle sudo .\/stand attach und sudo .\/stand detach.
Nachdem der Filter gebunden ist, können wir sicherstellen, dass ping weiter funktioniert, aber funktioniert das Programm? Lassen Sie uns Logs hinzufügen. Die Funktion ähnelt der printf(), unterstützt jedoch nur bis zu drei Argumente neben dem Muster und eine eingeschränkte Liste von Spezifizierern. Das Makro bpf_printk() erleichtert den Aufruf.
SEC("prog")
int xdp_main(struct xdp_md* ctx) {
+ bpf_printk("Paket erhalten: %pn", ctx);
return XDP_PASS;
}Die Ausgabe erfolgt in den Kernel-Debug-Kanal, der aktiviert werden muss:
echo -n 1 | sudo tee /sys/kernel/debug/tracing/options/trace_printkAnzeigen des Nachrichtenstroms:
cat /sys/kernel/debug/tracing/trace_pipeBeide dieser Befehle führen einen Aufruf durch sudo ./stand log.
Ping sollte jetzt solche Nachrichten auslösen:
-110930 [004] ..s1 78803.244967: 0: got packet: 00000000ac510377Wenn man sich die Ausgabe des Verifiers ansieht, kann man seltsame Berechnungen feststellen:
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#6Das Problem ist, dass Programme auf eBPF keinen Datenbereich haben, sodass die einzige Möglichkeit, einen Formatstring zu kodieren, die Immediate-Argumente der VM-Befehle sind:
$ python -c "import binascii; print(bytes(reversed(binascii.unhexlify('0a7025203a74656b63617020746f67'))))"
b'got packet: %pn'Aus diesem Grund bläht die Debug-Ausgabe den endgültigen Code erheblich auf.
Versenden von XDP-Paketen
Ändern wir den Filter: Lassen wir ihn alle eingehenden Pakete zurücksenden. Dies ist aus netzwerktechnischer Sicht nicht korrekt, da man die Adressen in den Headern ändern müsste, aber momentan ist die grundlegende Funktionalität wichtig.
bpf_printk("got packet: %pn", ctx);
- return XDP_PASS;
+ return XDP_TX;
}Starten tcpdump auf xdp-remote. Er sollte identische ausgehende und eingehende ICMP Echo Request anzeigen und aufhören, ICMP Echo Reply anzuzeigen. Tut er jedoch nicht. Es stellt sich heraus, dass XDP_TX im Programm auf xdp-local , damit auch der gegenüberliegende Interface xdp-remote ein Programm zugewiesen wurde, wenn auch nur ein leeres, und es aktiviert wurde.
Wie habe ich das herausgefunden?
ermöglicht der Mechanismus der perf Events, der übrigens dieselbe virtuelle Maschine verwendet, dh zur Analyse von eBPF wird eBPF angewendet.
Du musst Gutes aus Bösem machen, denn es gibt nichts anderes.
$ 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 der Code 6?
$ errno 6
ENXIO 6 Kein solches Gerät oder keine solche AdresseFunktion veth_xdp_flush_bq() erhält den Fehlercode von veth_xdp_xmit(), wo wir nach ENXIO suchen und den Kommentar finden.
Lass uns den minimalen Filter wiederherstellen (XDP_PASS) in der Datei xdp_dummy.c, fügen wir ihn in die Makefile ein, binden ihn an xdp-remote:
ip netns exec remote
ip link set dev int xdp object dummy.oJetzt tcpdump zeigt das Erwartete:
62:57:8e:70:44:64 > 26:0e:25:37:8f:96, Ethertyp IPv4 (0x0800), Länge 98: (tos 0x0, ttl 64, id 13762, offset 0, flags [DF], proto ICMP (1), Länge 84)
192.0.2.2 > 192.0.2.1: ICMP Echo-Anforderung, id 46966, seq 1, Länge 64
62:57:8e:70:44:64 > 26:0e:25:37:8f:96, Ethertyp IPv4 (0x0800), Länge 98: (tos 0x0, ttl 64, id 13762, offset 0, flags [DF], proto ICMP (1), Länge 84)
192.0.2.2 > 192.0.2.1: ICMP Echo-Anforderung, id 46966, seq 1, Länge 64Wenn stattdessen nur ARP angezeigt wird, müssen die Filter entfernt werden (das erledigt sudo .\/stand detach), lass sie durch ping, dann Filter einrichten und es erneut versuchen. Das Problem ist, dass der Filter XDP_TX auch auf ARP wirkt, und wenn der Stack
des Namensraums xdp-test vergessen hat, die MAC-Adresse 192.0.2.1 zu speichern, kann er diese IP nicht auflösen.
Aufgabenstellung
Kommen wir zur angegebenen Aufgabe: einen SYN-Cookie-Mechanismus auf XDP zu schreiben.
Bis jetzt ist die beliebteste DDoS-Attacke das SYN-Flooding, dessen Kern folgendes ist. Bei der Verbindungsherstellung (TCP-Handschlag) erhält 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 SYN-Pakete von gefälschten Adressen in einer Menge von tausenden pro Sekunde von jedem Host aus einem Botnetz von mehreren tausend. Der Server ist gezwungen, sofort Ressourcen bei Paketankunft bereitzustellen und gibt sie nach einem langen Timeout frei. Infolgedessen werden der Speicher oder die Limits erschöpft, neue Verbindungen werden nicht angenommen, der Dienst ist nicht verfügbar.
Wenn keine Ressourcen pro SYN-Paket zugewiesen werden, sondern nur mit einem SYNACK-Paket geantwortet wird, wie soll der Server dann verstehen, dass das später eintreffende ACK-Paket zum SYN-Paket gehört, das nicht gespeichert wurde? Schließlich kann der Angreifer auch gefälschte ACKs generieren. Der Sinn des SYN-Cookies besteht darin, die seqnum Verbindungsparameter als Hash aus Adressen, Ports und wechselndem Salt zu kodieren. Wenn das ACK angekommen ist, bevor das Salt wechselt, kann der Hash erneut berechnet und mit dem acknumverglichen werden. Fälschen kann der Angreifer nicht, da das Salt ein Geheimnis enthält, und er wird es aufgrund der beschränkten Bandbreite nicht schaffen, es zu durchprobieren. acknum SYN-Cookies sind seit langem im Linux-Kernel implementiert und können sogar automatisch aktiviert werden, wenn SYNs zu schnell und massenhaft ankommen.
TCP-Handschlag Grundlagen
TCP gewährleistet die Datenübertragung als Byte-Stream, zum Beispiel werden HTTP-Anfragen über TCP übertragen. Der Stream wird in Paketen stückweise übertragen. Alle TCP-Pakete haben logische Flags und 32-Bit-Sequenznummern:
TCP gewährleistet die Übertragung von Daten als Byte-Stream; beispielsweise werden HTTP-Anfragen über TCP übertragen. Der Stream wird in Pakete aufgeteilt. Alle TCP-Pakete haben logische Flags und 32-Bit-Sequenznummern:
Die Kombination von Flags bestimmt die Rolle des spezifischen Pakets. Das FLAG SYN bedeutet, dass dies das erste Paket des Senders in der Verbindung ist. Das FLAG ACK bedeutet, dass der Sender alle Daten der Verbindung bis zum Byte empfangen hat.
acknum. Ein Paket kann mehrere Flags haben und wird nach ihrer Kombination benannt, zum Beispiel SYNACK-Paket.Die Sequenznummer (seqnum) bestimmt die Verschiebung im Datenstrom für das erste Byte, das in diesem Paket übertragen wird. Zum Beispiel, wenn in dem ersten Paket mit X Bytes Daten diese Nummer N war, wird sie im nächsten Paket mit neuen Daten N+X sein. Zu Beginn der Verbindung wählt jede Seite diese Nummer willkürlich.
Die Bestätigungsnummer (acknum) ist eine ähnliche Verschiebung wie seqnum, bestimmt aber nicht die Nummer des übertragenen Bytes, sondern die Nummer des ersten Bytes vom Empfänger, das der Sender nicht gesehen hat.
Zu Beginn der Verbindung müssen die Seiten sich einigen seqnum und acknum. Der Client sendet ein SYN-Paket mit seiner seqnum = X. Der Server antwortet mit einem SYNACK-Paket, in dem er seine seqnum = Y einträgt und acknum = X + 1. Der Client antwortet auf SYNACK mit einem ACK-Paket, in dem 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 nach einem Timeout erneut.
Warum werden SYN-Cookies nicht immer verwendet?
Erstens, wenn SYNACK oder ACK verloren geht, muss auf die Wiederübertragung gewartet werden – dies verlangsamt die Herstellung der Verbindung. Zweitens werden im SYN-Paket – und nur in diesem! – eine Reihe von Optionen übertragen, die die zukünftige Funktionsweise der Verbindung beeinflussen. Wenn der Server die eingehenden SYN-Pakete nicht speichert, ignoriert er somit diese Optionen, und der Client sendet sie in den nächsten Paketen nicht mehr. TCP kann dabei arbeiten, aber zumindest in der Anfangsphase wird die Qualität der Verbindung sinken.
Aus der Sicht der Pakete sollte das XDP-Programm Folgendes tun:
- auf SYN mit SYNACK und Cookie antworten;
- auf ACK mit RST antworten (Verbindung trennen);
- alle anderen Pakete zurückweisen.
Pseudocode des Algorithmus zusammen mit der Analyse des Pakets:
Wenn es nicht Ethernet ist,
Paket überspringen.
Wenn es nicht IPv4 ist,
Paket überspringen.
Wenn die Adresse in der Überprüfungstabelle steht, (*)
Zähler der verbleibenden Prüfungen reduzieren,
Paket überspringen.
Wenn es nicht TCP ist,
Paket zurücksetzen. (**)
Wenn es SYN ist,
mit SYN-ACK und Cookie antworten.
Wenn es ACK ist,
wenn in acknum nicht das Cookie steht,
Paket zurücksetzen.
Adresse mit N verbleibenden Prüfungen in die Tabelle eintragen. (*)
RST antworten. (**)
In allen anderen Fällen Paket zurücksetzen.Eine (*) markierte Punkte, bei denen der Zustand des Systems verwaltet werden muss – in der ersten Phase kann man darauf verzichten und einfach das TCP-Handshake mit der Generierung von SYN-Cookies als seqnum umsetzen.
An Ort (**), solange wir keine Tabelle haben, werden wir das Paket überspringen.
Implementierung des TCP-Handshakes
Paket analysieren und Code verifizieren
Wir benötigen die Strukturen der Netzwerkheader: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) und TCP (uapi/linux/tcp.h). Letzteren konnte ich wegen Fehlern, die mit atomic64_tverbunden sind, nicht einbinden, musste die notwendigen Definitionen ins Code kopieren.
Alle Funktionen, die in C zur besseren Lesbarkeit definiert sind, sollten an der Stelle aufgerufen werden, wo sie gebraucht werden, da der eBPF-Validator im Kernel Rücksprünge verbietet, das heißt, faktisch Schleifen und Funktionsaufrufe.
#define INTERNAL static __attribute__((always_inline))Makro LOG() deaktiviert die Ausgabe im Release-Build.
Das Programm ist eine Pipeline von Funktionen. Jede akzeptiert ein Paket, in dem der Header der entsprechenden Ebene ausgewiesen ist, beispielsweise process_ether() erwartet, dass ausgefüllt ist ether. Wegen der Analyseergebnisse kann die Funktion das Paket an die höhere Ebene weiterleiten. Das Ergebnis der Funktion ist eine XDP-Aktion. Bis jetzt überspringen die Behandler SYN und ACK 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; /* fehlerhaftes Paket */
}
packet->ip = ip;
return process_ip(packet);
}
SEC("prog")
int xdp_main(struct xdp_md* ctx) {
struct Packet packet;
packet.ctx = ctx;
// A
struct ethhdr* ether = (struct ethhdr*)(void*)ctx->data;
if ((void*)(ether + 1) > (void*)ctx->data_end) {
return XDP_PASS;
}
packet.ether = ether;
return process_ether(&packet);
}Ich weise auf die Überprüfungen hin, die mit A und B gekennzeichnet sind. Wenn A auskommentiert wird, kompiliert das Programm, aber beim Laden tritt ein Verifizierungsfehler auf:
Verifizierer-Analyse:
11: (7b) *(u64 *)(r10 -48) = r1
12: (71) r3 = *(u8 *)(r7 +13)
Ungültiger Zugriff auf das Paket, off=13 Größe=1, R7(id=0,off=0,r=0)
R7-Offset liegt außerhalb des Pakets
Bearbeitet 11 Insns (Limit 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0
Fehler beim Abrufen des Programms/Karte!Schlüsselzeile Ungültiger Zugriff auf das Paket, off=13 Größe=1, R7(id=0,off=0,r=0): Es gibt Ausführungswege, bei denen das dreizehnte Byte vom Anfang des Puffers außerhalb des Pakets liegt. Es ist aufgrund des Listings schwierig zu verstehen, um welche Zeile es sich handelt, aber es gibt eine Instruktionsnummer (12) und einen Disassembler, der die Quellcodezeilen anzeigt:
llvm-objdump -S xdp_filter.o | lessIn diesem Fall verweist es auf die Zeile
LOG("Ether(proto=0x%x)", bpf_ntohs(ether->h_proto));aus der klar wird, dass das Problem bei ether. Wenn das doch immer so wäre.
Antwort auf SYN
Das Ziel in diesem Stadium ist es, ein korrektes SYNACK-Paket mit festgelegtem seqnum, das in Zukunft durch ein SYN-Cookie ersetzt wird. Alle Änderungen erfolgen in process_tcp_syn() und Umgebung.
Paketüberprüfung
Seltsamerweise ist dies die bemerkenswerteste Zeile, genauer gesagt, der Kommentar dazu:
/* Required to verify checksum calculation */
const void* data_end = (const void*)ctx->data_end;Bei der Erstellung der ersten Version des Codes wurde Kernel 5.1 verwendet, für dessen Verifizierer es einen Unterschied zwischen data_end und (const void*)ctx->data_end. Zum Zeitpunkt des Schreibens des Artikels hatte Kernel 5.3.1 dieses Problem nicht. Möglicherweise hat der Compiler eine lokale Variable anders behandelt als ein Feld. Die Moral ist, dass bei großer Verschachtelung der Code vereinfacht werden kann.
Dann folgen routinemäßige Längenkontrollen im Dienste des Verifizierers; über MAX_CSUM_BYTES darunter.
const u32 ip_len = ip->ihl * 4;
if ((void*)ip + ip_len > data_end) {
return XDP_DROP; /* fehlerhaftes Paket */
}
if (ip_len > MAX_CSUM_BYTES) {
return XDP_ABORTED; /* Implementierungsbeschränkung */
}
const u32 tcp_len = tcp->doff * 4;
if ((void*)tcp + tcp_len > (void*)ctx->data_end) {
return XDP_DROP; /* fehlerhaftes Paket */
}
if (tcp_len > MAX_CSUM_BYTES) {
return XDP_ABORTED; /* Implementierungsbeschränkung */
}Paketumkehrung
Wir füllen seqnum und acknum, setzen wir ACK (SYN wurde bereits gesetzt):
const u32 cookie = 42;
tcp->ack_seq = bpf_htonl(bpf_ntohl(tcp->seq) + 1);
tcp->seq = bpf_htonl(cookie);
tcp->ack = 1;Wir tauschen die TCP-Ports, IP-Adressen und MAC-Adressen. Die Standardbibliothek ist aus dem XDP-Programm nicht verfügbar, daher memcpy() ist ein Makro, das die Clang-Intrinsik 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üfziffern
Die Prüfzellen von IPv4 und TCP erfordern die Addition aller 16-Bit-Wörter in den Headern, wobei die Größe der Header darin vermerkt ist, also zum Zeitpunkt der Kompilierung unbekannt ist. Das ist ein Problem, weil der Prüfer einen normalen Zyklus bis zur variablen Grenze nicht zulässt. Der Header ist jedoch auf maximal 64 Bytes begrenzt. Es ist möglich, eine Schleife mit einer festen Anzahl von Iterationen zu machen, die vorzeitig beendet werden kann.
Ich möchte darauf hinweisen, dass es gibt, wie man die Prüfzelle teilweise neu berechnet, wenn nur feste Worte in den Paketen geändert wurden. Diese Methode ist jedoch nicht universell, und die Implementierung wäre schwieriger zu warten.
Funktion zur Berechnung der Prüfzelle:
#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 Größe es vom aufrufenden Code überprüft wurde, ist eine zweite Ausstiegsbedingung notwendig, damit der Prüfer den Abschluss der Schleife beweisen kann.
Für 32-Bit-Wörter wurde eine einfachere Version implementiert:
INTERNAL u32
sum16_32(u32 v) {
return (v >> 16) + (v & 0xffff);
}Wirklich die Neuberechnung der Prüfzellen und das 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 <check = 0;
tcp_csum += sum16(tcp, tcp_len, data_end);
tcp->check = carry(tcp_csum);
return XDP_TX;Funktion carry() wandelt eine 32-Bit-Summe in eine 16-Bit-Prüfzelle um, gemäß RFC 791.
Überprüfung des TCP-Handschlags
Der Filter stellt korrekt eine Verbindung zu netcat, indem das finale ACK übersprungen wird, auf das Linux mit einem RST-Paket reagierte, da der Netzwerkstack kein SYN empfangen hatte – es wurde in ein SYNACK umgewandelt und zurückgesendet – und aus Sicht des Betriebssystems ein Paket ankam, das nicht zu offenen Verbindungen gehörte.
$ sudo ip netns exec xdp-test nc -nv 192.0.2.1 6666
192.0.2.1 6666: Verbindung durch Peer zurückgesetztEs ist wichtig, dies mit voll funktionsfähigen Anwendungen zu überprüfen und zu beobachten, tcpdump auf xdp-remote da beispielsweise hping3 nicht auf inkorrekte Prüfzellen reagiert.
SYN-Cookie
Aus der Perspektive von XDP ist die eigentliche Überprüfung trivial. Der Berechnungsalgorithmus ist primitiv und wahrscheinlich anfällig für ausgeklügelte Angreifer. Der Linux-Kernel verwendet beispielsweise den kryptografischen SipHash, dessen Implementierung für XDP jedoch eindeutig den Rahmen dieses Artikels sprengt.
Es sind neue TODOs aufgetaucht, die mit externen Interaktionen zu tun haben:
Das XDP-Programm kann nicht
cookie_seed(den geheimen Teil des Salzes) in einer globalen Variablen speichern, es benötigt ein Speicher in den Kern, dessen Wert regelmäßig aus einem vertrauenswürdigen Generator aktualisiert wird.Bei Übereinstimmung des SYN-Cookies im ACK-Paket sollte keine Nachricht ausgegeben, sondern die IP des geprüften Clients gespeichert werden, um später Pakete von ihm passieren zu lassen.
Überprüfung durch legitimen Client:
$ sudo ip netns exec xdp-test nc -nv 192.0.2.1 6666
192.0.2.1 6666: Verbindung durch Peer zurückgesetztIn den Logs wurde die Überprüfung erfolgreich verzeichnet (flags=0x2 — das ist SYN, flags=0x10 — das 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 passt zu Client 20200c0Solange keine Liste validierter IPs vorhanden ist, wird es keinen Schutz gegen die SYN Flood geben, aber hier ist die Reaktion auf die ACK Flood, die mit folgendem Befehl gestartet wird:
sudo ip netns exec xdp-test hping3 --flood -A -s 1111 -p 2222 192.0.2.1Einträge im Log:
Ether(proto=0x800)
IP(src=0x15bd11a dst=0x15bd11e proto=6)
TCP(sport=3236 dport=2222 flags=0x10)
Cookie stimmt nicht übereinFazit
Manchmal wird eBPF im Allgemeinen und XDP im Besonderen eher als Werkzeug für den fortgeschrittenen Administrator betrachtet als als Plattform für die Entwicklung. Tatsächlich ist XDP ein Werkzeug zur Einmischung in die Paketverarbeitung des Kernels und keine Alternative zum Kernelsatz, wie DPDK und andere Varianten des Kernel Bypass. Andererseits ermöglicht XDP die Implementierung recht komplexer Logik, die zudem leicht ohne Unterbrechung des Datenverkehrs aktualisiert werden kann. Der Verifier verursacht keine großen Probleme, ich würde so etwas nicht für Teile des Userspace-Codes ablehnen.
Im zweiten Teil, falls das Thema interessant ist, werden wir die Tabelle der überprüften Clients und der Verbindungsabbrüche vervollständigen, Zähler implementieren und ein Userspace-Tool zur Verwaltung des Filters schreiben.
Links:
Quelle: habr.com
