Coder une pile TCP/IP 1 : Ethernet et ARP (2016)
(saminiir.com)- 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/writeles buffers Ethernet du périphérique virtuel - L’en-tête Ethernet est traité avec
dmac,smac,ethertypeetpayload, et si la valeur deethertypeest 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ée10.0.0.4dans le cache ARP de l’interfacetap0
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/tappuis avecioctl(fd, TUNSETIFF, ...)IFF_TAPsélectionne un périphérique TAPIFF_NO_PIévite qu’une information de paquet inutile soit ajoutée devant la trame Ethernet
- Une fois créé, le descripteur de fichier
fdretourné 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.hde 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 destinationsmac: adresse MAC sourceethertype: longueur ou type du payloadpayload: pointeur vers le payload contenant un paquet ARP ou IPv4
ethertypeest 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
packeddans 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;
- Exemple :
- 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 deethertype
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 est0x0001protype: champ de 2 octets indiquant le type de protocole ; la valeur pour IPv4 est0x0800hwsize: champ de 1 octet indiquant la taille de l’adresse matérielle ; une adresse MAC fait 6 octetsprosize: champ de 1 octet indiquant la taille de l’adresse de protocole ; une adresse IPv4 fait 4 octetsopcode: champ de 2 octets indiquant le type de message ARP
- Les valeurs de
opcodese répartissent en quatre catégories- ARP request :
1 - ARP reply :
2 - RARP request :
3 - RARP reply :
4
- ARP request :
- Les données ARP pour IPv4 sont traitées via une structure
arp_ipv4smac: adresse MAC de l’émetteursip: adresse IP de l’émetteurdmac: adresse MAC du destinatairedip: 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 depuis10.0.0.4avec l’adresse MAC00:0C:29:6D:50:25 - Ensuite, dans la sortie de
arp, une entrée10.0.0.4 ether 00:0c:29:6d:50:25 tap0apparaî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
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
É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.
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é.
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.
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.
Si l’on utilise cette méthode, il vaut donc mieux créer un VLAN dédié.
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. »
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.
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 ?
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