1 points par GN⁺ 2025-03-05 | 1 commentaires | Partager sur WhatsApp
  • Première étape d’une série qui consiste à construire directement une pile TCP/IP en espace utilisateur sous Linux pour apprendre les couches réseau : l’implémentation commence par le traitement des trames Ethernet de couche 2 et les réponses ARP
  • Le trafic réseau bas niveau du noyau est récupéré via un périphérique TAP, et le descripteur de fichier retourné permet de read/write les buffers Ethernet du périphérique virtuel
  • L’en-tête Ethernet est traité avec dmac, smac, ethertype et payload, et si la valeur de ethertype est supérieure ou égale à 1536, elle indique le type de payload ; sinon, elle indique la longueur du payload
  • ARP mappe dynamiquement des adresses de protocole comme une adresse IPv4 à des adresses MAC sur 48 bits, et l’implémentation répond aux requêtes tout en mettant à jour la table de traduction
  • Lors du test avec arping, la réponse ARP de la pile personnalisée est reconnue par le noyau Linux, qui ajoute une entrée 10.0.0.4 dans le cache ARP de l’interface tap0

Point de départ d’une pile TCP/IP en espace utilisateur

  • L’objectif est d’implémenter une pile TCP/IP minimale en espace utilisateur sous Linux afin de mieux comprendre le réseau et la programmation système
  • TCP est devenu complexe avec plus de 30 ans d’accumulation de spécifications, mais ses éléments d’implémentation essentiels peuvent se résumer au parsing de l’en-tête TCP, à la machine à états, au contrôle de congestion et au calcul du délai d’expiration de retransmission
  • Ethernet et IP sont moins complexes que TCP, donc la série commence l’implémentation à partir de la couche 2

Recevoir le trafic Ethernet avec un périphérique TAP

  • Un périphérique TAP Linux est utilisé pour intercepter le trafic réseau bas niveau du noyau Linux
  • Les périphériques TUN/TAP sont souvent utilisés par des applications réseau en espace utilisateur pour manipuler respectivement du trafic L3/L2
    • Le tunneling consiste à encapsuler un paquet dans le payload d’un autre paquet
    • Des programmes comme OpenVPN utilisent aussi des périphériques TUN/TAP
  • Comme la pile réseau est construite à partir de la couche 2, il faut un périphérique TAP et non TUN
  • Le périphérique TAP est créé en ouvrant /dev/net/tap puis avec ioctl(fd, TUNSETIFF, ...)
    • IFF_TAP sélectionne un périphérique TAP
    • IFF_NO_PI évite qu’une information de paquet inutile soit ajoutée devant la trame Ethernet
  • Une fois créé, le descripteur de fichier fd retourné permet de lire et d’écrire dans le buffer Ethernet du périphérique virtuel

Format d’une trame Ethernet

  • Ethernet est la technologie de base qui relie les ordinateurs dans un LAN, et la première norme Ethernet a été publiée en 1980 par Digital Equipment Corporation, Intel et Xerox
  • La première version utilisait un débit d’environ 10 Mb/s et une communication half-duplex, ce qui nécessitait un protocole MAC pour réguler le flux de données
    • Sur les interfaces Ethernet half-duplex, CSMA/CD était nécessaire comme méthode MAC
    • 100BASE-T utilise un câblage twisted-pair pour permettre la communication full-duplex et un débit plus élevé
    • Avec la généralisation des commutateurs Ethernet, la nécessité de CSMA/CD a largement diminué
  • La norme Ethernet est gérée par le groupe de travail IEEE 802.3

En-tête Ethernet utilisé dans l’implémentation

  • L’implémentation inclut if_ether.h de Linux afin d’utiliser la correspondance entre ethertype et valeurs hexadécimales
  • L’en-tête Ethernet est représenté dans une structure C avec les champs suivants
    • dmac : adresse MAC de destination
    • smac : adresse MAC source
    • ethertype : longueur ou type du payload
    • payload : pointeur vers le payload contenant un paquet ARP ou IPv4
  • ethertype est un champ de 2 octets dont la signification dépend de sa valeur
    • Si la valeur est supérieure ou égale à 1536, elle indique le type de payload, comme IPv4 ou ARP
    • Si elle est inférieure, elle indique la longueur du payload
  • Les trames Ethernet peuvent comporter des tags indiquant le VLAN ou la QoS, mais cette implémentation exclut les tags de trame
  • Si la longueur du payload est inférieure à la taille minimale requise de 48 octets sans tag, des octets de padding sont ajoutés à la fin
  • À la fin du format Ethernet Frame se trouve le champ Frame Check Sequence, qui vérifie l’intégrité via un CRC, mais cette implémentation ne le traite pas

Méthode de parsing des trames Ethernet

  • L’attribut packed dans la déclaration de structure empêche le compilateur GNU C d’optimiser le layout mémoire de la structure en y ajoutant des octets de padding pour l’alignement des données
  • Le parsing dans cette implémentation consiste à caster le type du buffer vers la structure de protocole appropriée
    • Exemple : struct eth_hdr *hdr = (struct eth_hdr *) buf;
  • Une approche plus portable consiste à sérialiser manuellement les données du protocole
    • Dans ce cas, le compilateur peut ajouter des octets de padding pour satisfaire les contraintes d’alignement propres au processeur
  • Le traitement des trames Ethernet reçues suit un flux simple
    • Lecture du buffer depuis le périphérique TAP
    • Initialisation de l’en-tête Ethernet avec init_eth_hdr(buf)
    • handle_frame(&netdev, hdr) décide de l’action suivante en fonction de la valeur de ethertype

Structure et rôle des paquets ARP

  • ARP (Address Resolution Protocol) mappe dynamiquement des adresses de protocole comme les adresses IPv4 vers des adresses MAC, qui sont des adresses Ethernet sur 48 bits
  • ARP n’est pas limité à IPv4 et peut être utilisé avec plusieurs protocoles L3
    • Par exemple, CHAOS déclare une adresse de protocole sur 16 bits
  • Dans une communication LAN classique, même si l’on connaît l’adresse IP d’un service, l’adresse MAC reste nécessaire pour l’envoi effectif
  • ARP diffuse une requête en broadcast sur le réseau afin que le propriétaire de l’adresse IP concernée annonce son adresse matérielle

En-tête ARP et payload pour IPv4

  • L’en-tête ARP est composé des champs suivants
    • hwtype : champ de 2 octets indiquant le type de couche liaison ; la valeur pour Ethernet est 0x0001
    • protype : champ de 2 octets indiquant le type de protocole ; la valeur pour IPv4 est 0x0800
    • hwsize : champ de 1 octet indiquant la taille de l’adresse matérielle ; une adresse MAC fait 6 octets
    • prosize : champ de 1 octet indiquant la taille de l’adresse de protocole ; une adresse IPv4 fait 4 octets
    • opcode : champ de 2 octets indiquant le type de message ARP
  • Les valeurs de opcode se répartissent en quatre catégories
    • ARP request : 1
    • ARP reply : 2
    • RARP request : 3
    • RARP reply : 4
  • Les données ARP pour IPv4 sont traitées via une structure arp_ipv4
    • smac : adresse MAC de l’émetteur
    • sip : adresse IP de l’émetteur
    • dmac : adresse MAC du destinataire
    • dip : adresse IP du destinataire

Algorithme de résolution d’adresse et cache

  • L’algorithme de résolution d’adresse de la RFC 826 vérifie le type matériel et le type de protocole, met à jour la table de traduction, puis génère une réponse si l’adresse cible correspond à la machine elle-même
  • La table de traduction stocke les résultats ARP afin que l’hôte puisse retrouver dans le cache les entrées qu’il possède déjà
  • Ce cache réduit les requêtes ARP redondantes et évite de charger inutilement le réseau
  • Le code d’implémentation se trouve dans arp.c

Test de réponse ARP et étape suivante

  • Le test final de l’implémentation ARP consiste à vérifier qu’elle répond correctement aux requêtes
  • En exécutant arping -I tap0 10.0.0.4, on obtient une réponse unicast depuis 10.0.0.4 avec l’adresse MAC 00:0C:29:6D:50:25
  • Ensuite, dans la sortie de arp, une entrée 10.0.0.4 ether 00:0c:29:6d:50:25 tap0 apparaît dans le cache ARP du noyau Linux
  • Avec un traitement minimal des trames Ethernet et une implémentation ARP, on peut déjà constater qu’un périphérique Ethernet personnalisé remplit le cache ARP de l’hôte Linux
  • Le code source du projet est disponible sur GitHub, et l’étape suivante consiste à implémenter ping, c’est-à-dire ICMP echo/reply, ainsi que le parsing des paquets IPv4

1 commentaires

 
GN⁺ 2025-03-05
Avis sur Hacker News
  • Il y a quelques années, j’ai écrit en C une pile réseau en espace utilisateur, en traitant des paquets bruts via une interface TUN, et j’ai réussi à la faire fonctionner dans une certaine mesure.
    Elle intègre aujourd’hui un petit shell permettant de configurer les adresses IP, les routes, etc., et les paquets réseau sont stockés dans une structure hybride qui ressemble à un mélange de mbuf et de sk_buf.
    En revanche, après avoir terminé l’implémentation d’UDP, je n’ai jamais trouvé le temps ni la motivation d’implémenter TCP, et le code est ici : https://github.com/cakturk/unet

    • Il y a très longtemps, j’avais écrit un parseur pcap/tcpdump en bash pur, parce qu’à l’époque c’était le seul outil avec lequel je savais écrire un « programme ».
      Évidemment, c’était probablement l’un des trucs les plus lents et les plus fragiles de l’histoire, mais ça fonctionnait vraiment et c’était assez amusant. J’aimerais bien que ce code traîne encore quelque part.
    • Beaucoup d’appareils embarqués utilisent lwip comme implémentation TCP/IP.
      Le « port POSIX » de lwip récupère lui aussi des octets Ethernet bruts depuis un périphérique TUN/TAP de la même manière.
      https://github.com/lwip-tcpip/lwip/blob/master/contrib/ports...
  • Si l’on compile un noyau Linux minimal sans pile TCP/IP, il fait 400 Ko ; avec la pile TCP/IP, il passe à 800 Ko.
    Dans un projet qui n’avait qu’à envoyer la température, nous mettions la valeur dans un message UDP fabriqué à la main par un petit programme C en espace utilisateur, ce qui a permis de réduire fortement l’espace occupé et la complexité.

    • Vu de quelqu’un qui n’y connaît rien, c’est assez surprenant ; mais j’imagine que ça ne veut pas dire pour autant que la partie TCP/IP représente la moitié de tout le code source du noyau, si ?
    • Je me demande pourquoi la pile IP est aussi grosse. Un binaire de 400 Ko, ça fait déjà pas mal de code ; est-ce parce qu’elle est très optimisée pour les usages de gros serveurs ?
  • Si l’on désactive ARP, on peut configurer la même IP sur plusieurs serveurs du même réseau.
    Si un serveur jouant le rôle de frontal de routage peut transmettre les paquets à l’interface réseau du serveur backend en se basant sur l’adresse MAC, ce backend considérera qu’il est la destination, modifiera les IP source/destination et répondra directement au client. La réponse ne repasse alors pas par le frontal de routage.
    On peut aussi obtenir le même effet sans désactiver ARP, en ajoutant l’adresse IP commune comme alias de l’interface loopback ; le backend se reconnaît alors comme destination tout en évitant les conflits ARP. C’était une astuce utilisée par le répartiteur de charge logiciel IBM WebSphere dans les années 90 et 2000.

    • Cisco IOS SLB peut fonctionner de façon similaire : on ajoute l’IP virtuelle comme alias sur le loopback de chaque serveur de la ferme.
      L’avantage par rapport au load balancing L3 plus courant est qu’il n’est pas nécessaire de réécrire l’en-tête du paquet IP.
    • C’est aussi connu sous le nom de DSR (Direct Server Return) : https://www.haproxy.com/blog/layer-4-load-balancing-direct-s...
    • Si l’on désactive ARP et que l’on configure la même IP sur plusieurs serveurs du même réseau, le switch/bridge ne peut pas apprendre les adresses MAC et continue donc à flooder/broadcaster les paquets vers tous les ports de ce segment.
      Si l’on utilise cette méthode, il vaut donc mieux créer un VLAN dédié.
    • F5 dispose d’un réglage de proxy ARP, ce qui évite d’avoir à faire cela. L’inconvénient est que cela casse souvent DHCP.
    • Pour ce genre de bricolage bas niveau, on peut aussi jouer avec DPDK. ARP y est désactivé par défaut.
  • J’ai fait quelque chose de similaire en Python : https://github.com/georgek/notebooks/blob/master/internet.ip...
    La qualité du code est probablement moins bonne et, pour être honnête, j’ai simplement inventé l’algorithme de résolution d’adresses. J’ai quand même réussi à envoyer un ping à des hôtes Internet via ICMP.
    J’aime le fait que tout tienne entièrement dans un court notebook. L’article original omet dans le texte beaucoup de détails présents dans le code source plus volumineux auquel il renvoie.
    Je l’ai fait sans avoir vu cet article, uniquement à partir de Wikipedia. En revanche, à partir de TCP, la complexité augmente fortement et mon intérêt a un peu baissé. La troisième partie traite apparemment de ce sujet ; je la lirai peut-être un jour pour terminer. Si le réseau vous intéresse, je pense que c’est un exercice faisable et gratifiant pour des programmeurs de tous niveaux.

  • Il y a quelques années, j’ai travaillé sur de l’instrumentation de centrale nucléaire. Le développement côté client se faisait sur des stations de travail Sun et, en fait, j’avais été recruté grâce à mon expérience de TCP/IP, acquise dans le cours « systèmes d’exploitation » de CMU.
    En revanche, les ordinateurs de la centrale étaient des mini-ordinateurs sans pile TCP/IP, si bien que l’équipe a dû écrire sa propre pile.

  • Vers la première minute de l’article, il est dit que « dmac et smac sont des champs assez évidents » ; pour un lecteur qui ne sait pas ce que c’est, cela peut le faire décrocher immédiatement.
    Il se dira : « cet article est destiné aux gens pour qui ces champs sont évidents. Il n’est pas pour moi, donc je vais arrêter de lire. »

    • La phrase complète est : « dmac et smac sont des champs assez évidents. Ils contiennent les adresses MAC des parties qui communiquent (destination et source, respectivement) », donc c’est effectivement expliqué.
      Et puis, pour un article sur la création d’une pile réseau, on peut raisonnablement supposer que le lecteur connaît un minimum les réseaux.
    • Sauf si cela vient d’être mis à jour, la phrase suivante explique justement : « Ils contiennent les adresses MAC des parties qui communiquent (destination et source, respectivement) ».
  • Articles liés :
    Let’s code a TCP/IP stack (2016) - https://news.ycombinator.com/item?id=27654182 - juin 2021, 49 commentaires
    Let’s code a TCP/IP stack, 1: Ethernet & ARP (2016) - https://news.ycombinator.com/item?id=17316487 - juin 2018, 47 commentaires
    Let’s Code a TCP/IP Stack: TCP Retransmission - https://news.ycombinator.com/item?id=14701199 - juillet 2017, 30 commentaires
    Let’s code a TCP/IP stack, 1: Ethernet and ARP - https://news.ycombinator.com/item?id=11234229 - mars 2016, 49 commentaires

  • Je ne sais pas d’où vient l’adresse IP 10.0.0.4 que l’auteur utilise pour le test de résolution ARP
    C’est l’adresse de quoi ? Un périphérique fictif accessible depuis le faux périphérique Ethernet créé ici, ou bien un appareil réellement présent sur le réseau de l’auteur ?

    • Ce n’est pas indiqué dans l’article, mais c’est une valeur codée en dur par l’auteur lors de l’initialisation de l’interface : https://github.com/saminiir/level-ip/blob/e9ceb08f01a5499b85...
      Un périphérique TAP est une sorte de lien Ethernet émulé en logiciel. Quand on lui envoie des paquets, ils sont transmis directement à un programme en espace utilisateur, et c’est ce programme qui décide quelle adresse IP il aura et comment répondre à ARP
      En général, c’est le système d’exploitation qui gère ce genre de choses, et il faut les droits root pour ajouter une adresse IP à une interface. Il en va de même pour l’ouverture d’un périphérique TAP. Le réseau fonctionne globalement de manière coopérative, et un acteur malveillant disposant des droits root sur le réseau peut faire de mauvaises choses
  • Si je me souviens bien, ARP ne fonctionne que sur le segment local. Le routeur renseigne sa propre adresse et transmet le paquet
    Il existe aussi RARP, qui est l’une des manières de demander sa propre adresse IP au « réseau ». Je ne sais pas si RARP fonctionne encore dans des environnements réels aujourd’hui