Nous écrivons une protection contre les attaques DDoS sur XDP. Partie nucléaire

La technologie EXpress Data Path (XDP) permet un traitement arbitraire du trafic sur les interfaces Linux avant l'arrivée des paquets sur la pile réseau du noyau. Application de XDP - protection contre les attaques DDoS (CloudFlare), filtres sophistiqués, collecte de statistiques (Netflix). Les programmes XDP sont exécutés par la machine virtuelle eBPF, par conséquent, ils ont des restrictions à la fois sur leur code et sur les fonctions du noyau disponibles, selon le type de filtre.


L'article est destiné à combler les lacunes de nombreux matériaux XDP. Premièrement, ils fournissent du code prêt à l'emploi qui contourne immédiatement les fonctionnalités de XDP: préparé pour la vérification ou trop simple pour causer des problèmes. Lorsque vous essayez d'écrire votre code à partir de zéro, vous ne savez pas quoi faire avec les erreurs typiques. Deuxièmement, les méthodes de test local de XDP sans VM ni matériel ne sont pas couvertes, malgré le fait qu'elles aient leurs propres pièges. Le texte est destiné aux programmeurs familiers avec les réseaux et Linux qui sont intéressés par XDP et eBPF.


Dans cette partie, nous examinerons en détail comment le filtre XDP est assemblé et comment le tester, puis nous écrirons une version simple du mécanisme de cookie SYN bien connu au niveau du traitement des paquets. Bien que nous ne formions pas de «liste blanche»
clients vérifiés, garder des compteurs et gérer le filtre - suffisamment de journaux.


Nous écrirons en C - ce n'est pas à la mode, mais 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.


Clause de non-responsabilité. Au cours de l'article, une mini-solution sera développée pour repousser les attaques DDoS, car il s'agit d'une tâche réaliste pour XDP et ma région. Cependant, l'objectif principal est de traiter avec la technologie, ce n'est pas un guide pour créer une protection toute faite. Le code de formation n'est pas optimisé et omet certaines nuances.


XDP en bref


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 envoyés au filtre. Par conséquent, le filtre doit prendre une décision: ignorer le paquet vers le noyau ( XDP_PASS ), jeter le paquet ( XDP_DROP ) ou le renvoyer ( XDP_TX ). Le filtre peut changer le package, c'est particulièrement vrai pour XDP_TX . Vous pouvez également planter le programme ( XDP_ABORTED ) et XDP_ABORTED le package, mais il s'agit d'un analogue d' assert(0) pour le débogage.


La machine virtuelle eBPF (Berkley Packet Filter étendu) est spécialement simplifiée pour que le noyau puisse vérifier que le code ne boucle pas et n'endommage pas la mémoire de quelqu'un d'autre. Restrictions et contrôles agrégés:


  • Cycles interdits (saut en arrière).
  • Il existe une pile de données, mais aucune fonction (toutes les fonctions C doivent être intégrées).
  • L'accès à la mémoire en dehors de la pile et du tampon de paquets est interdit.
  • La taille du code est limitée, mais en pratique ce n'est pas très significatif.
  • Les appels ne sont autorisés que vers des fonctions spéciales du noyau (assistants eBPF).

La conception et l'installation du filtre ressemblent à ceci:


  1. Le code source (par exemple, kernel.c ) est compilé dans l'objet ( kernel.o ) sous l'architecture de la machine virtuelle eBPF. Depuis octobre 2019, la compilation dans eBPF est prise en charge par Clang et promise dans GCC 10.1.
  2. Si dans ce code objet il y a des appels à des structures de noyau (par exemple, des tables et des compteurs), des zéros sont utilisés à la place de leurs ID, c'est-à-dire qu'un tel code ne peut pas être exécuté. Avant de charger dans le noyau, vous devez remplacer ces zéros par l'ID d'objets spécifiques créés via les appels du noyau (code de lien). Vous pouvez le faire avec des utilitaires externes ou vous pouvez écrire un programme qui liera et chargera un filtre spécifique.
  3. Le noyau vérifie le programme chargé. L'absence de boucles et l'absentéisme au-delà des limites du paquet et de la pile sont vérifiés. Si le vérificateur ne peut pas prouver que le code est correct, le programme est rejeté - vous devez pouvoir le satisfaire.
  4. Après une vérification réussie, le noyau compile le code objet de l'architecture eBPF dans le code machine de l'architecture système (juste à temps).
  5. Le programme s'attache à l'interface et commence à traiter les paquets.

Étant donné que XDP fonctionne dans le noyau, le débogage est effectué par les journaux de trace et, en fait, par les paquets que le programme filtre ou génère. Cependant, eBPF assure la sécurité du code chargé pour le système, vous pouvez donc expérimenter avec XDP directement sur Linux local.


Préparation de l'environnement


Assemblage


Clang ne peut pas émettre directement de code objet pour l'architecture eBPF, le processus se compose donc de deux étapes:


  1. Compilez le code C en bytecode LLVM ( clang -emit-llvm ).
  2. Convertissez 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 des tests du noyau sont utiles. Il est important qu'ils correspondent à la version du noyau ( KVER ). Téléchargez-les dans les 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 pour Arch Linux (noyau 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 contient le chemin vers les en-têtes du noyau, ARCH - l'architecture du système. Les chemins et les outils peuvent varier légèrement entre les distributions.


Exemple de différence 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 incluent un répertoire avec des en-têtes auxiliaires et plusieurs répertoires avec des en-têtes de noyau. Le symbole __KERNEL__ signifie que les en-têtes UAPI (API de l'espace utilisateur) sont définis pour le code du noyau, car le filtre s'exécute dans le noyau.


La protection de la pile peut être désactivée ( -fno-stack-protector ), car le vérificateur de code eBPF recherche toujours un moyen de sortir de la pile. L'optimisation doit être incluse immédiatement car la taille du bytecode eBPF est limitée.


Commençons par un filtre qui ignore tous les paquets et ne fait rien:


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

La commande make xdp_filter.o . Où le tester maintenant?


Banc d'essai


Le stand doit comprendre deux interfaces: sur lesquelles il y aura un filtre et à partir desquelles les paquets seront envoyés. Ceux-ci doivent être des appareils Linux à part entière avec leur IP afin de vérifier le fonctionnement des applications régulières avec notre filtre.


Des appareils comme veth (Ethernet virtuel) nous conviennent: ce sont deux interfaces réseau virtuelles qui sont «connectées» directement entre elles. Vous pouvez les créer comme ceci (dans cette section, toutes les commandes ip sont exécutées en tant que root ):


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

Ici xdp-remote et xdp-local sont des noms de périphériques. Un filtre sera attaché à xdp-local (192.0.2.1/24), et le trafic entrant sera envoyé depuis xdp-remote (192.0.2.2/24). Cependant, il y a un problème: les interfaces sont sur la même machine, et Linux n'enverra pas de trafic à l'une via l'autre. Vous pouvez résoudre ce problème avec les règles iptables délicates, mais ils devront modifier les packages, ce qui n'est pas pratique lors du débogage. Il est préférable d'utiliser des espaces de noms réseau (espaces de noms réseau, ci-après netns).


L'espace de noms réseau contient un ensemble d'interfaces, de tables de routage et de règles NetFilter, isolées d'objets similaires dans d'autres réseaux. Chaque processus s'exécute dans un espace de noms et seuls les objets de ce réseau lui sont accessibles. Par défaut, le système possède un seul espace de noms réseau pour tous les objets, vous pouvez donc travailler sur Linux et ne pas connaître les réseaux.


Créez un nouvel espace de noms xdp-test et déplacez xdp-remote .


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

Ensuite, le processus exécuté dans xdp-test ne «verra» pas xdp-local (il restera dans netns par défaut) et l'enverra via xdp-remote lors de l'envoi d'un paquet à 192.0.2.1, car il s'agit de la seule interface de 192.0.2.0/ 24 disponibles pour ce processus. Cela fonctionne également dans la direction opposée.


Lorsque vous vous déplacez entre des réseaux, l'interface tombe et perd l'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 voir, cela ne diffère pas de la définition de 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 vous exécutez 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 d'exécuter le shell dans xdp-test . Il existe un script dans le référentiel qui automatise le travail avec le support, par exemple, vous pouvez configurer le support avec la commande sudo ./stand up et le supprimer avec la commande sudo ./stand down .


Trace


Le filtre est fixé à l'appareil comme suit:


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

Le -force nécessaire pour lier un nouveau programme si un autre est déjà lié. «Aucune nouvelle n'est une bonne nouvelle» ne concerne pas cette commande, la conclusion est en tout cas volumineuse. verbose facultatif, mais avec lui, un rapport apparaît sur le travail du vérificateur de code avec la liste d'assembly:


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

Détachez le programme de l'interface:


 ip link set dev xdp-local xdp off 

Dans le script, ce sont les commandes sudo ./stand attach et sudo ./stand detach .


En attachant un filtre, vous pouvez vérifier que le ping continue de fonctionner, mais le programme fonctionne-t-il? Ajoutez les journaux. La fonction bpf_trace_printk() est similaire à printf() , mais ne prend en charge que jusqu'à trois arguments, à l'exception du modèle, et une liste limitée de qualificatifs. La macro bpf_printk() simplifie l'appel.


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

La sortie va au canal de trace du noyau, que vous devez activer:


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

Afficher le flux de messages:


 cat /sys/kernel/debug/tracing/trace_pipe 

Ces deux commandes appellent le sudo ./stand log .


Ping devrait maintenant déclencher les messages suivants:


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

Si vous regardez attentivement la sortie du vérificateur, vous remarquerez d'étranges calculs:


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

Le fait est que les programmes eBPF n'ont pas de section de données, donc la seule façon de coder une chaîne de format est avec les arguments immédiats des commandes VM:


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

Pour cette raison, la sortie de débogage gonfle considérablement le code résultant.


Envoi de packages XDP


Changeons le filtre: laissez-le renvoyer tous les paquets entrants. Ceci est incorrect du point de vue du réseau, car il serait nécessaire de changer les adresses dans les en-têtes, mais maintenant le travail est important en principe.


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

Exécutez tcpdump sur xdp-remote . Il doit afficher la demande d'écho ICMP sortante et entrante identique et cesser d'afficher la réponse d'écho ICMP. Mais ne montre pas. Il s'avère que pour que XDP_TX fonctionne dans un programme sur xdp-local il est nécessaire que l'interface du xdp-remote soit xdp-remote interface xdp-remote , même si elle est vide, et elle doit être augmentée.


Comment ai-je découvert?

Le mécanisme des événements perf, en passant, utilisant la même machine virtuelle permet de suivre le chemin du package dans le noyau , c'est-à-dire que eBPF est utilisé pour le démontage avec eBPF.


Il faut faire le bien du mal, car il n'y a plus rien à en faire.

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

Qu'est-ce que le code 6?


 $ errno 6 ENXIO 6 No such device or address 

La fonction veth_xdp_flush_bq() reçoit un code d'erreur de veth_xdp_xmit() , où nous recherchons par ENXIO et trouvons un commentaire.


Restaurez le filtre minimum ( XDP_PASS ) dans le fichier xdp_dummy.c , ajoutez-le au Makefile, attachez-le à xdp-remote :


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

Maintenant tcpdump montre ce qui est attendu:


 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 

Si seul ARP est affiché à la place, vous devez supprimer les filtres (cela se fait par sudo ./stand detach ), démarrer le ping , puis définir les filtres et réessayer. Le problème est que le filtre XDP_TX affecte à la fois ARP et si la pile
xdp-test espace de noms xdp-test réussi à «oublier» l'adresse MAC 192.0.2.1, il ne pourra pas résoudre cette IP.


Énoncé du problème


Passons à la tâche indiquée: écrire le mécanisme des cookies SYN sur XDP.


Jusqu'à présent, SYN flood reste une attaque DDoS populaire, dont l'essence est la suivante. Lors de l'établissement d'une connexion (prise de contact TCP), le serveur reçoit un SYN, alloue des ressources pour une future connexion, répond avec un paquet SYNACK et attend un ACK. L'attaquant envoie simplement des paquets SYN à partir de fausses adresses au nombre de milliers par seconde de chaque hôte à partir d'un botnet de plusieurs milliers. Le serveur est obligé d'allouer des ressources immédiatement à l'arrivée du paquet, et libère par un délai d'attente important, en conséquence, la mémoire ou les limites sont épuisées, les nouvelles connexions ne sont pas acceptées, le service n'est pas disponible.


Si vous n'allouez pas de ressources pour le paquet SYN, mais répondez uniquement avec un paquet SYNACK, alors comment le serveur peut-il comprendre que le paquet ACK qui est venu 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. L'essence du cookie SYN est d'encoder les paramètres de seqnum en seqnum sous forme de hachage à partir d'adresses, de ports et de sel de changement. Si l'ACK a réussi à arriver avant le changement de sel, vous pouvez à nouveau calculer le hachage et le comparer avec acknum . L'attaquant ne peut pas acknum , car le sel contient un secret et n'aura pas le temps de trier en raison du canal limité.


Le cookie SYN est implanté depuis longtemps dans le noyau Linux et peut même s'allumer automatiquement si SYN arrive trop rapidement et en masse.


Programme éducatif sur la prise de contact TCP

TCP fournit le transfert de données sous forme de flux d'octets, par exemple, les requêtes HTTP sont envoyées 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 32 bits:


  • La combinaison de drapeaux détermine le rôle d'un package particulier. L'indicateur SYN signifie qu'il s'agit du premier paquet expéditeur dans la connexion. L'indicateur ACK signifie que l'expéditeur a reçu toutes les données de connexion avant l'octet acknum . Un paquet peut avoir plusieurs drapeaux et est appelé par leur combinaison, par exemple, un paquet SYNACK.


  • Le numéro de séquence (seqnum) définit le décalage dans le flux de données pour le premier octet qui est transmis dans ce paquet. Par exemple, si dans le premier paquet avec X octets de données ce nombre était N, dans le paquet suivant avec de nouvelles données ce sera N + X. Au début de la connexion, chaque côté sélectionne ce numéro arbitrairement.


  • Numéro d'accusé de réception (acknum) - le même décalage que le seqnum, mais ne détermine pas le nombre d'octets à transmettre, 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 s'entendre sur le seqnum et l' acknum . Le client envoie un paquet SYN avec son seqnum = X Le serveur répond avec un paquet SYNACK, où il écrit son seqnum = Y et définit 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'interlocuteur ne confirme pas la réception du paquet, TCP le renvoie à nouveau par timeout.


Pourquoi les cookies SYN ne sont-ils pas toujours utilisés?

Premièrement, si SYNACK ou ACK est perdu, vous devrez attendre la réémission - la connexion est ralentie. Deuxièmement, dans le package SYN - et uniquement dans celui-ci! - un certain nombre d'options sont transmises qui affectent le fonctionnement ultérieur de la connexion. Sans se souvenir des paquets SYN entrants, le serveur ignore donc ces options, dans les prochains paquets le client ne les enverra plus. Dans ce cas, TCP peut fonctionner, mais au moins au stade initial, la qualité de la connexion diminuera.


En termes de packages, un programme XDP devrait faire ce qui suit:


  • SYNACK avec cookie pour répondre à SYN;
  • répondre à ACK RST (déconnecter);
  • jetez les autres paquets.

Pseudocode d'algorithme avec analyse du package:


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

Un (*) indique les points dans lesquels contrôler l'état du système - à la première étape, vous pouvez vous en passer en implémentant simplement une prise de contact TCP avec la génération d'un cookie SYN en tant que séquence.


En place (**) , alors que nous n'avons pas de table, nous allons sauter le paquet.


Implémentation du protocole TCP


Analyser le package et vérifier le code


Nous avons 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 ). Le dernier que je n'ai pas pu connecter en raison d'erreurs liées à atomic64_t , j'ai dû copier les définitions nécessaires dans le code.


Toutes les fonctions qui sont allouées en C pour la lisibilité doivent être intégrées à l'endroit de l'appel, car le vérificateur eBPF dans le noyau interdit les transitions de retour, c'est-à-dire, en fait, les boucles et les appels de fonction.


 #define INTERNAL static __attribute__((always_inline)) 

La macro LOG() désactive l'impression dans la version finale.


Le programme est un convoyeur de fonctions. Chacun reçoit un paquet dans lequel l'en-tête du niveau correspondant est mis en évidence, par exemple, process_ether() s'attend ether ce que l' ether soit plein. Sur la base des résultats de l'analyse de champ, la fonction peut transférer le paquet à un niveau supérieur. Le résultat de la fonction est l'action XDP. Jusqu'à présent, les gestionnaires SYN et ACK transmettent tous les paquets.


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

J'attire l'attention sur les vérifications marquées A et B. Si vous commentez A, le programme s'assemblera, mais il y aura une erreur de vérification lors du chargement:


 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! 

La ligne de clé est un 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 à partir du début du tampon est en dehors du paquet. Selon la liste, il est difficile de comprendre 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:


 llvm-objdump -S xdp_filter.o | less 

Dans ce cas, il pointe vers une chaîne


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

par lequel il est clair que le problème est dans l' ether . Il en serait toujours ainsi.


Répondre à SYN


Le but à ce stade est de former un paquet SYNACK correct avec un seqnum fixe, qui sera remplacé par un cookie SYN à l'avenir. Tous les changements se produisent dans process_tcp_syn() et dans les environs.


Vérification du paquet


Curieusement, voici la ligne la plus remarquable, plus précisément, un commentaire à ce sujet:


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

Lors de l'écriture de la première version du code, le noyau 5.1 a été utilisé, pour le vérificateur dont il y avait une différence entre data_end et (const void*)ctx->data_end . Lors de l'écriture de l'article, le noyau 5.3.1 n'avait pas un tel problème. Peut-être que le compilateur a accédé à la variable locale différemment du champ. Moral - un code simplifié peut aider à beaucoup d'imbrication.


Autres vérifications de routine des longueurs en l'honneur du vérificateur; environ MAX_CSUM_BYTES ci-dessous.


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

Répartition des colis


Remplissez seqnum et acknum , définissez ACK (SYN est déjà défini):


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

Échangez les ports TCP, l'adresse IP et l'adresse MAC. La bibliothèque standard n'est pas accessible à partir du programme XDP, donc memcpy() est une macro qui cache l'intrinsèque de Clang.


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

Recalcul du total de contrôle


Les sommes de contrôle IPv4 et TCP nécessitent l'ajout de tous les mots 16 bits dans les en-têtes, et la taille des en-têtes y est écrite, c'est-à-dire qu'au moment de la compilation, elle n'est pas connue. Il s'agit d'un problème car le vérificateur ne sautera pas une boucle régulière vers une limite variable. Mais la taille des en-têtes est limitée: jusqu'à 64 octets chacun. Vous pouvez faire une boucle avec un nombre fixe d'itérations, qui peut se terminer plus tôt que prévu.


Je note qu'il y a RFC 1624 sur la façon de recalculer la somme de contrôle partiellement si seuls les mots de paquets fixes sont modifiés. Cependant, la méthode n'est pas universelle et sa 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; } 

, 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 

Conclusion


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


, , , userspace- .


Références:


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


All Articles