ProHoster > Blog > administration > Nous écrivons une protection contre les attaques DDoS sur XDP. Partie nucléaire
Nous écrivons une protection contre les attaques DDoS sur XDP. Partie nucléaire
La technologie eXpress Data Path (XDP) permet d'effectuer un traitement aléatoire du trafic sur les interfaces Linux avant que les paquets n'entrent dans la pile réseau du noyau. Application de XDP - protection contre les attaques DDoS (CloudFlare), filtres complexes, collecte de statistiques (Netflix). Les programmes XDP sont exécutés par la machine virtuelle eBPF, ils ont donc des restrictions sur leur code et sur les fonctions disponibles du noyau en fonction du type de filtre.
L'article est destiné à combler les lacunes de nombreux documents sur XDP. Premièrement, ils fournissent du code prêt à l'emploi qui contourne immédiatement les fonctionnalités de XDP : il est préparé pour la vérification ou est trop simple pour causer des problèmes. Lorsque vous essayez ensuite d’écrire votre code à partir de zéro, vous ne savez pas quoi faire des erreurs typiques. Deuxièmement, les moyens de tester XDP localement sans machine virtuelle ni matériel ne sont pas abordés, malgré le fait qu'ils comportent leurs propres pièges. Le texte est destiné aux programmeurs familiarisés avec les réseaux et Linux et intéressés par XDP et eBPF.
Dans cette partie, nous comprendrons en détail comment le filtre XDP est assemblé et comment le tester, puis nous écrirons une version simple du mécanisme bien connu des cookies SYN au niveau du traitement des paquets. Pour l’instant, nous ne créerons pas de « liste blanche »
clients vérifiés, conservez les compteurs et gérez le filtre - suffisamment de journaux.
On écrira en C - ce n'est pas à la mode, mais c'est pratique. Tout le code est disponible sur GitHub via le lien à la fin et est divisé en commits selon les étapes décrites dans l'article.
Avertissement. Au cours de cet article, je développerai une mini-solution pour parer aux attaques DDoS, car il s'agit d'une tâche réaliste pour XDP et mon domaine. Cependant, l’objectif principal est de comprendre la technologie ; ceci n’est pas un guide pour créer une protection toute faite. Le code du tutoriel n'est pas optimisé et omet quelques nuances.
Brève présentation de XDP
Je ne soulignerai que les points clés afin de ne pas dupliquer la documentation et les articles existants.
Ainsi, le code du filtre est chargé dans le noyau. Les paquets entrants sont transmis au filtre. En conséquence, le filtre doit prendre une décision : transmettre le paquet dans le noyau (XDP_PASS), déposer le paquet (XDP_DROP) ou renvoyez-le (XDP_TX). Le filtre peut changer de package, cela est particulièrement vrai pour XDP_TX. Vous pouvez également abandonner le programme (XDP_ABORTED) et réinitialisez le package, mais c'est analogue assert(0) - pour le débogage.
La machine virtuelle eBPF (extended Berkley Packet Filter) est délibérément simplifiée afin que le noyau puisse vérifier que le code n'entre pas en boucle et n'endommage pas la mémoire des autres. Restrictions et contrôles cumulatifs :
Les boucles (à l’envers) sont interdites.
Il existe une pile pour les données, mais pas de fonctions (toutes les fonctions C doivent être intégrées).
Les accès à la mémoire en dehors de la pile et du tampon de paquets sont interdits.
La taille du code est limitée, mais en pratique cela n’est pas très significatif.
Seuls les appels à des fonctions spéciales du noyau (assistants eBPF) sont autorisés.
La conception et l'installation d'un filtre ressemblent à ceci :
Code source (par exemple kernel.c) est compilé en objet (kernel.o) sous l'architecture de machine virtuelle eBPF. Depuis octobre 2019, la compilation vers eBPF est prise en charge par Clang et promise dans GCC 10.1.
Si ce code objet contient des appels aux structures du noyau (par exemple, des tables et des compteurs), leurs identifiants sont remplacés par des zéros, ce qui signifie qu'un tel code ne peut pas être exécuté. Avant de charger dans le noyau, vous devez remplacer ces zéros par les identifiants d'objets spécifiques créés via les appels du noyau (lier le code). Vous pouvez le faire avec des utilitaires externes ou écrire un programme qui reliera et chargera un filtre spécifique.
Le noyau vérifie le programme chargé. L'absence de cycles et le non-traversement des limites des paquets et des piles sont vérifiés. Si le vérificateur ne peut pas prouver que le code est correct, le programme est rejeté - vous devez pouvoir lui plaire.
Après une vérification réussie, le noyau compile le code objet de l'architecture eBPF en code machine de l'architecture système (juste à temps).
Le programme s'attache à l'interface et commence à traiter les paquets.
Étant donné que XDP s'exécute dans le noyau, le débogage est effectué à l'aide de journaux de trace et, en fait, de paquets que le programme filtre ou génère. Cependant, eBPF garantit que le code chargé est sécurisé pour le système, vous pouvez donc expérimenter XDP directement sur votre Linux local.
Préparation de l'environnement
assemblage
Clang ne peut pas produire directement de code objet pour l'architecture eBPF, le processus se compose donc de deux étapes :
Compiler le code C en bytecode LLVM (clang -emit-llvm).
Convertir le bytecode en code objet eBPF (llc -march=bpf -filetype=obj).
Lors de l'écriture d'un filtre, quelques fichiers avec des fonctions auxiliaires et des macros seront utiles à partir des tests du noyau. Il est important qu'ils correspondent à la version du noyau (KVER). Téléchargez-les sur helpers/:
KDIR contient le chemin d'accès aux en-têtes du noyau, ARCH - Architecture du système. Les chemins et les outils peuvent varier légèrement entre les distributions.
Exemple de différences pour Debian 10 (noyau 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 connectez un répertoire avec des en-têtes auxiliaires et plusieurs répertoires avec des en-têtes de noyau. Symbole __KERNEL__ signifie que les en-têtes UAPI (userspace API) sont définis pour le code du noyau, puisque le filtre est exécuté dans le noyau.
La protection de la pile peut être désactivée (-fno-stack-protector), car le vérificateur de code eBPF vérifie toujours les violations hors limites de la pile. Cela vaut la peine d'activer les optimisations immédiatement, car la taille du bytecode eBPF est limitée.
Commençons par un filtre qui laisse passer tous les paquets et ne fait rien :
Équipe make recueille xdp_filter.o. Où l'essayer maintenant ?
banc d'essai
Le stand doit comprendre deux interfaces : sur laquelle il y aura un filtre et à partir de laquelle les paquets seront envoyés. Il doit s'agir d'appareils Linux à part entière dotés de leur propre adresse IP afin de vérifier le fonctionnement des applications classiques avec notre filtre.
Les appareils de type veth (virtual Ethernet) nous conviennent : ce sont une paire d'interfaces réseau virtuelles « connectées » directement entre elles. Vous pouvez les créer comme ceci (dans cette section toutes les commandes ip sont effectués à partir de root):
ip link add xdp-remote type veth peer name xdp-local
il est xdp-remote и xdp-local — les noms des appareils. Sur xdp-local (192.0.2.1/24), un filtre sera attaché, avec xdp-remote (192.0.2.2/24) le trafic entrant sera envoyé. Cependant, il y a un problème : les interfaces sont sur la même machine, et Linux n'enverra pas de trafic vers l'une d'elles via l'autre. Vous pouvez résoudre ce problème avec des règles délicates iptables, mais ils devront changer de package, ce qui n'est pas pratique pour le débogage. Il est préférable d'utiliser des espaces de noms réseau (ci-après netns).
Un espace de noms réseau contient un ensemble d'interfaces, de tables de routage et de règles NetFilter, isolés des objets similaires dans d'autres réseaux. Chaque processus s'exécute dans un espace de noms et n'a accès qu'aux objets de ce réseau. Par défaut, le système dispose d'un seul espace de noms réseau pour tous les objets, vous pouvez donc travailler sous Linux sans connaître netns.
Créons un nouvel espace de noms xdp-test et déplace-le là xdp-remote.
ip netns add xdp-test
ip link set dev xdp-remote netns xdp-test
Ensuite, le processus s'exécutant xdp-test, je ne "verrai" pas xdp-local (il restera dans netns par défaut) et lors de l'envoi d'un paquet vers 192.0.2.1, il le transmettra via xdp-remote, car c'est la seule interface sur 192.0.2.0/24 accessible à ce processus. Cela fonctionne également dans le sens inverse.
Lors du déplacement entre les réseaux, l'interface tombe en panne et perd son adresse. Pour configurer l'interface dans netns, vous devez exécuter ip ... dans cet espace de noms de commande 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
Comme vous pouvez le constater, ce n'est pas différent du paramètre xdp-local dans l'espace de noms par défaut :
ip address add 192.0.2.1/24 dev xdp-local
ip link set xdp-local up
Si tu cours tcpdump -tnevi xdp-local, vous pouvez voir que les paquets envoyés depuis xdp-test, sont livrés à cette interface :
ip netns exec xdp-test ping 192.0.2.1
Il est pratique de lancer un shell dans xdp-test. Le référentiel dispose d'un script qui automatise le travail avec le stand ; par exemple, vous pouvez configurer le stand avec la commande sudo ./stand up et supprime-le sudo ./stand down.
Tracé
Le filtre est associé à un appareil comme celui-ci :
ip -force link set dev xdp-local xdp object xdp_filter.o verbose
clé -force nécessaire pour lier un nouveau programme si un autre est déjà lié. « Pas de nouvelles, bonnes nouvelles » ne concerne pas cette commande, le résultat est de toute façon volumineux. indiquer verbose facultatif, mais avec lui, un rapport apparaît sur le travail du vérificateur de code avec la liste des assemblys :
Verifier analysis:
0: (b7) r0 = 2
1: (95) exit
Dissociez le programme de l'interface :
ip link set dev xdp-local xdp off
Dans un script, ce sont des commandes sudo ./stand attach и sudo ./stand detach.
En attachant un filtre, vous pouvez vous assurer que ping continue de fonctionner, mais le programme fonctionne-t-il ? Ajoutons des journaux. Fonction bpf_trace_printk() semblable à printf(), mais ne prend en charge que trois arguments autres que le modèle et une liste limitée de spécificateurs. Macro bpf_printk() simplifie l'appel.
Le fait est que les programmes eBPF n'ont pas de section de données, donc le seul moyen d'encoder une chaîne de format est les arguments immédiats des commandes VM :
Pour cette raison, la sortie du débogage gonfle considérablement le code final.
Envoi de paquets XDP
Changeons le filtre : laissez-le renvoyer tous les paquets entrants. C'est incorrect d'un point de vue réseau, puisqu'il faudrait changer les adresses dans les en-têtes, mais maintenant le travail en principe est important.
Run tcpdump sur xdp-remote. Il doit afficher une demande d'écho ICMP sortante et entrante identique et cesser d'afficher la réponse d'écho ICMP. Mais ça ne se voit pas. Il s'avère que pour le travail XDP_TX au programme sur xdp-localnécessaireà l'interface de paire xdp-remote un programme lui a également été attribué, même s'il était vide, et il a été élevé.
Comment ai-je su cela ?
Tracer le chemin d'un paquet dans le noyau Le mécanisme des événements de performance permet d'ailleurs d'utiliser la même machine virtuelle, c'est-à-dire que eBPF est utilisé pour gérer eBPF.
Vous devez faire du mal du bien, car il n’y a rien d’autre pour le faire.
Si seuls les ARP sont affichés à la place, vous devez supprimer les filtres (cela ne sudo ./stand detach), lâcher ping, puis définissez les filtres et réessayez. Le problème est que le filtre XDP_TX valable pour ARP et stack
espaces de noms xdp-test réussi à « oublier » l’adresse MAC 192.0.2.1, il ne pourra pas résoudre cette IP.
Formulation du problème
Passons à la tâche indiquée : écrire un mécanisme de cookies SYN sur XDP.
SYN Flood reste une attaque DDoS populaire, dont l’essence est la suivante. Lorsqu'une connexion est établie (TCP handshake), le serveur reçoit un SYN, alloue des ressources pour la future connexion, répond avec un paquet SYNACK et attend un ACK. L'attaquant envoie simplement des paquets SYN à partir de fausses adresses par milliers par seconde depuis chaque hôte d'un botnet de plusieurs milliers de personnes. Le serveur est obligé d'allouer des ressources immédiatement à l'arrivée du paquet, mais les libère après un long délai d'attente ; en conséquence, la mémoire ou les limites sont épuisées, les nouvelles connexions ne sont pas acceptées et le service n'est pas disponible.
Si vous n'allouez pas de ressources en fonction du paquet SYN, mais répondez uniquement avec un paquet SYNACK, comment le serveur peut-il alors comprendre que le paquet ACK arrivé plus tard fait référence à un paquet SYN qui n'a pas été enregistré ? Après tout, un attaquant peut également générer de faux ACK. Le but du cookie SYN est de l'encoder dans seqnum paramètres de connexion sous forme de hachage d'adresses, de ports et de sel changeant. Si l'ACK a réussi à arriver avant que le sel ne soit modifié, vous pouvez recalculer le hachage et le comparer avec acknum. Forger acknum l'attaquant ne le peut pas, puisque le sel contient un secret, et ne pourra pas le trier en raison d'un canal limité.
Le cookie SYN est implémenté depuis longtemps dans le noyau Linux et peut même être automatiquement activé si les SYN arrivent trop rapidement et en masse.
Programme éducatif sur la poignée de main TCP
TCP permet la transmission de données sous forme de flux d'octets. Par exemple, les requêtes HTTP sont transmises via TCP. Le flux est transmis par morceaux en paquets. Tous les paquets TCP ont des indicateurs logiques et des numéros de séquence de 32 bits :
La combinaison d'indicateurs détermine le rôle d'un package particulier. L'indicateur SYN signifie qu'il s'agit du premier paquet de l'expéditeur dans la connexion. Le drapeau ACK signifie que l'expéditeur a reçu toutes les données de connexion jusqu'à l'octet acknum. Un paquet peut avoir plusieurs indicateurs et est nommé par leur combinaison, par exemple un paquet SYNACK.
Le numéro de séquence (seqnum) spécifie le décalage dans le flux de données pour le premier octet transmis dans ce paquet. Par exemple, si dans le premier paquet contenant X octets de données, ce nombre était N, dans le prochain paquet contenant de nouvelles données, ce sera N+X. Au début de la connexion, chaque interlocuteur choisit ce numéro au hasard.
Le numéro d'accusé de réception (acknum) est le même décalage que seqnum, mais il ne détermine pas le numéro de l'octet transmis, mais le numéro du premier octet du destinataire, que l'expéditeur n'a pas vu.
Au début de la connexion, les parties doivent convenir seqnum и acknum. Le client envoie un paquet SYN avec son seqnum = X. Le serveur répond avec un paquet SYNACK, où il enregistre son seqnum = Y et expositions acknum = X + 1. Le client répond à SYNACK avec un paquet ACK, où seqnum = X + 1, acknum = Y + 1. Après cela, le transfert de données proprement dit commence.
Si l'homologue n'accuse pas réception du paquet, TCP le renvoie après un délai d'attente.
Pourquoi les cookies SYN ne sont-ils pas toujours utilisés ?
Premièrement, si le SYNACK ou l'ACK est perdu, vous devrez attendre un nouvel envoi, ce qui ralentira l'établissement de la connexion. Deuxièmement, dans le package SYN - et uniquement dedans ! — un certain nombre d'options sont transmises et affectent le fonctionnement ultérieur de la connexion. En ne mémorisant pas les paquets SYN entrants, le serveur ignore donc ces options ; le client ne les enverra plus dans les paquets suivants. TCP peut fonctionner dans ce cas, mais au moins au stade initial, la qualité de la connexion diminuera.
Du point de vue des packages, un programme XDP doit effectuer les opérations suivantes :
répondre à SYN avec SYNACK avec un cookie ;
répondre à ACK avec RST (déconnexion) ;
jetez les paquets restants.
Pseudocode de l'algorithme avec analyse du package :
Если это не Ethernet,
пропустить пакет.
Если это не IPv4,
пропустить пакет.
Если адрес в таблице проверенных, (*)
уменьшить счетчик оставшихся проверок,
пропустить пакет.
Если это не TCP,
сбросить пакет. (**)
Если это SYN,
ответить SYN-ACK с cookie.
Если это ACK,
если в acknum лежит не cookie,
сбросить пакет.
Занести в таблицу адрес с N оставшихся проверок. (*)
Ответить RST. (**)
В остальных случаях сбросить пакет.
Un (*) Les points où vous devez gérer l'état du système sont marqués - dans un premier temps, vous pouvez vous en passer en implémentant simplement une poignée de main TCP avec la génération d'un cookie SYN comme numéro de séquence.
Sur place (**), tant que nous n'avons pas de table, nous sauterons le paquet.
Implémentation de la négociation TCP
Analyser le package et vérifier le code
Nous aurons besoin de structures d'en-tête de réseau : Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) et TCP (uapi/linux/tcp.h). Je n'ai jamais réussi à connecter ce dernier à cause d'erreurs liées à atomic64_t, j'ai dû copier les définitions nécessaires dans le code.
Toutes les fonctions allouées pour la lisibilité en C doivent être intégrées au point d'appel, car le vérificateur eBPF du noyau interdit les sauts en arrière, c'est-à-dire en fait les boucles et les appels de fonction.
Macro LOG() désactive l'impression dans la version release.
Le programme est un convoyeur de fonctions. Chacun reçoit un paquet dans lequel un en-tête du niveau approprié est mis en évidence, par exemple : process_ether() s'attend à ce qu'il soit rempli ether. Sur la base des résultats de l'analyse de terrain, la fonction peut transmettre le paquet à un niveau supérieur. Le résultat de la fonction est une action XDP. Pour l'instant, les gestionnaires SYN et ACK transmettent tous les paquets.
J'attire votre attention sur les vérifications marquées A et B. Si vous commentez A, le programme sera construit, mais il y aura une erreur de vérification lors du chargement :
Chaîne de clé invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0): Il existe des chemins d'exécution lorsque le treizième octet depuis le début du tampon est en dehors du paquet. Il est difficile de comprendre à partir du listing de quelle ligne nous parlons, mais il y a un numéro d'instruction (12) et un désassembleur montrant les lignes du code source :
ce qui montre clairement que le problème est ether. Ce serait toujours comme ça.
Répondre à SYN
Le but à ce stade est de générer un paquet SYNACK correct avec un seqnum, qui sera remplacé à l'avenir par le cookie SYN. Tous les changements se produisent dans process_tcp_syn() et les environs.
Vérification du colis
Curieusement, voici la ligne la plus remarquable, ou plutôt son commentaire :
Lors de l'écriture de la première version du code, le noyau 5.1 a été utilisé, pour le vérificateur duquel il y avait une différence entre data_end и (const void*)ctx->data_end. Au moment de la rédaction de cet article, le noyau 5.3.1 n'avait pas ce problème. Peut-être que le compilateur a accédé à une variable locale différemment d'un champ. Moralité de l'histoire : dans les situations d'imbrication importantes, la simplification du code peut aider.
Viennent ensuite les contrôles de longueur de routine pour la gloire du vérificateur ; Ô MAX_CSUM_BYTES ci-dessous.
Échangez les ports TCP, l'adresse IP et les adresses MAC. La bibliothèque standard n'est pas accessible depuis le programme XDP, donc memcpy() - une macro qui cache les intrinsèques de Clang.
Les sommes de contrôle IPv4 et TCP nécessitent l'ajout de tous les mots de 16 bits dans les en-têtes, et la taille des en-têtes y est écrite, c'est-à-dire inconnue au moment de la compilation. C'est un problème car le vérificateur ne parcourra pas la boucle normale jusqu'à la limite variable. Mais la taille des en-têtes est limitée : jusqu'à 64 octets chacun. Vous pouvez créer une boucle avec un nombre fixe d'itérations, qui peut se terminer plus tôt.
Je remarque qu'il y a RFC 1624 sur la façon de recalculer partiellement la somme de contrôle si seuls les mots fixes des packages sont modifiés. Cependant, la méthode n’est pas universelle et la mise en œuvre serait plus difficile à maintenir.
Fonction de calcul de la somme de contrôle :
#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;
}
Bien que size vérifiée par le code appelant, la deuxième condition de sortie est nécessaire pour que le vérificateur puisse prouver l'achèvement de la boucle.
Pour les mots de 32 bits, une version plus simple est implémentée :
INTERNAL u32
sum16_32(u32 v) {
return (v >> 16) + (v & 0xffff);
}
En fait, recalculer les sommes de contrôle et renvoyer le paquet :
Fonction carry() effectue une somme de contrôle à partir d'une somme de 32 bits de mots de 16 bits, conformément à la RFC 791.
Vérification de la poignée de main TCP
Le filtre établit correctement une connexion avec netcat, en ignorant l'ACK final, auquel Linux a répondu avec un paquet RST, car la pile réseau n'a pas reçu le SYN - il a été converti en SYNACK et renvoyé - et du point de vue du système d'exploitation, un paquet est arrivé qui n'était pas lié à connexions ouvertes.
$ sudo ip netns exec xdp-test nc -nv 192.0.2.1 6666
192.0.2.1 6666: Connection reset by peer
Il est important de vérifier avec des applications à part entière et de surveiller tcpdump sur xdp-remote parce que, par exemple, hping3 ne répond pas aux sommes de contrôle incorrectes.
Cookie SYN
Du point de vue XDP, la vérification elle-même est triviale. L'algorithme de calcul est primitif et probablement vulnérable à un attaquant sophistiqué. Le noyau Linux, par exemple, utilise le cryptographique SipHash, mais son implémentation pour XDP dépasse clairement le cadre de cet article.
Apparu pour de nouveaux TODO liés à l'interaction externe :
Le programme XDP ne peut pas stocker cookie_seed (la partie secrète du sel) dans une variable globale, vous avez besoin d'un stockage dans le noyau, dont la valeur sera périodiquement mise à jour à partir d'un générateur fiable.
Si le cookie SYN correspond au paquet ACK, vous n'avez pas besoin d'imprimer de message, mais mémorisez l'adresse IP du client vérifié afin de continuer à lui transmettre des paquets.
Bien qu'il n'y ait pas de liste d'adresses IP vérifiées, il n'y aura aucune protection contre le flot SYN lui-même, mais voici la réaction à un flot ACK lancé par la commande suivante :
sudo ip netns exec xdp-test hping3 --flood -A -s 1111 -p 2222 192.0.2.1
Parfois, eBPF en général et XDP en particulier sont présentés davantage comme un outil d'administration avancé que comme une plateforme de développement. En effet, XDP est un outil permettant d'interférer avec le traitement des paquets par le noyau, et non une alternative à la pile du noyau, comme DPDK et d'autres options de contournement du noyau. D'autre part, XDP permet de mettre en œuvre une logique assez complexe, qui, de plus, est facile à mettre à jour sans interruption du traitement du trafic. Le vérificateur ne crée pas de gros problèmes ; personnellement, je ne refuserais pas cela pour des parties du code de l'espace utilisateur.
Dans la deuxième partie, si le sujet est intéressant, nous compléterons le tableau des clients vérifiés et des déconnexions, implémenterons des compteurs et rédigerons un utilitaire en espace utilisateur pour gérer le filtre.