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 :

  1. 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.
  2. 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.
  3. 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.
  4. 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).
  5. 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 :

  1. Compiler le code C en bytecode LLVM (clang -emit-llvm).
  2. 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/:

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

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

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

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

La sortie est envoyée au canal de trace du noyau, qui doit être activé :

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

Afficher le fil de discussion :

cat /sys/kernel/debug/tracing/trace_pipe

Ces deux commandes effectuent un appel sudo ./stand log.

Ping devrait maintenant déclencher des messages comme celui-ci :

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

Si vous regardez attentivement les résultats du vérificateur, vous remarquerez des calculs étranges :

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 le seul moyen d'encoder une chaîne de format est les arguments immédiats des commandes VM :

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

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.

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

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-local né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.

$ 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

Fonction veth_xdp_flush_bq() reçoit un code d'erreur de veth_xdp_xmit(), où rechercher par ENXIO et trouvez le commentaire.

Restaurons le filtre minimum (XDP_PASS) dans le fichier xdp_dummy.c, ajoutez-le au Makefile, liez-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 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.

#define INTERNAL static __attribute__((always_inline))

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.

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

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!

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 :

llvm-objdump -S xdp_filter.o | less

Dans ce cas, il pointe vers la ligne

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

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 :

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

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

Déplier le colis

Remplir seqnum и 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 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.

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 des sommes de contrôle

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 :

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;

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.

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.

Vérification du client légitime :

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

Les journaux enregistrent la fin du contrôle (flags=0x2 - c'est SYN, flags=0x10 est 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

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

Entrées de journal :

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

Conclusion

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.

Liens:

Source: habr.com

Ajouter un commentaire