Tutorat de programmation réseau
(Polytech'Lille, département GIS, quatrième année).
Conception et réalisation d'une pile TCP/IP.
Xavier Redon
1 Cahier des charges
Dans ce projet, votre but est d'améliorer une pile TCP/IPv4 basique et de débuter
l'écriture d'une pile TCP/IPv6 basique. Pour la promotion 2014, l'implantation
de la pile IPv6 doit aller jusqu'à ICMPv6. Il doit être possible de vérifier
le fonctionnement de la pile avec l'utilitaire ping6.
2 Indications pratiques
Vous devez utiliser l'archive
http://rex.plil.fr/Enseignement/Reseau/Tutorat12.Reseau.GIS4/NetStackV2.4.tgz.
Cette archive contient la version de la pile TCP/IPv4 mise au point par les promotions
précédentes. L'architecture du projet est constituée de deux bibliothèques et d'un
programme principal.
3 Bibliothèques fournies
Trois bibliothèques sont utilisées pour construire la pile. Une bibliothèque concernant
les interfaces Ethernet virtuelles et une bibliothèque de gestion d'évenements ont déjà
été présentées dans le tutorat précédent de réalisation d'une couche liaison. Une troisième
bibliothèque permet de gérer des tableaux associatifs.
3.1 Bibliothèque de tableaux associatifs
Le but de ces tableaux est de remplacer les structures statiques de C. De nombreuses
structures seraient nécessaires pour communiquer entre les différentes couches de
la pile TCP/IP. Les tableaux associatifs remplacent ces structures.
Un tableau associatif s'initialise à NULL :
AssocArray *a=NULL;
Il est possible d'ajouter un élément au tableau (ici on ajoute
l'entier value avec l'indice index) :
arraysSetValue(&a,"index",(void *)&value,sizeof(value),drapeau);
La valeur doit toujours être passée par référence. Le dernier paramètre
drapeau peut modifier le comportement de la fonction :
-
le bit AARRAY_FORCE_NUMERIC indique l'indice est de
type entier, ce qui permet des optimisations mémoire ;
- le bit AARRAY_DONT_COMPACT indique qu'il ne faut pas
tenter d'optimiser l'espace mémoire pour des valeurs de petite taille ;
- le bit AARRAY_DONT_DUPLICATE indique qu'il ne faut pas
dupliquer la valeur (en utilisant un malloc) mais utiliser directement
le pointeur passé en paramètre, du coup cette valeur ne sera pas libérée
par un arraysFreeArray.
Bien sûr la valeur pour un indice peut être retrouvée :
int size;
int *value=(int *)arrayGetValue(a,"index",&size,drapeau);
Le paramètre drapeau doit être AARRAY_FORCE_NUMERIC si la
valeur a été ajoutée de façon identique. La taille de la valeur désignée
par le pointeur retourné est stockée dans le paramètre size (sauf
si ce paramètre est NULL).
Enfin un tableau peut être affiché par la fonction arraysDisplayArray
et détruit par la fonction arraysFreeArray.
3.2 Bibliothèque de gestion d'événements
Cette bibliothèque permet de gérer des événements déclenchés avec effet immédiat, avec
effet différé ou par une activité sur un descripteur de fichier. La création et la
suppression d'événements se font avec les fonctions
int eventsCreate(int priority,void *data);
void eventsRemove(int identity);
A ce niveau on associe une priorité à l'événement; si plusieurs événements sont actifs
en même temps, les actions liées au plus prioritaire seront exécutées en premier. On
associe aussi une donnée à l'événement sous la forme d'un pointeur générique; les actions
de l'événement peuvent l'exploiter lorsqu'elles sont appellées. Pour associer une ou
plusieurs actions à un événement utilisez la fonction
int eventsAddAction(int identity,
unsigned char (*handler)(EventsEvent *,EventsSelector *),
int level);
Comme les événements, les actions peuvent être exécutées dans un ordre précis en jouant
sur le paramètre level. Les deux paramètres passés à la fonction d'action permettent
d'accéder aux champs de l'événement et aux champs du sélecteur ayant déclenché l'événement.
Dans les champs utiles de l'événement on peut citer le champ data_init contenant la
donnée liée à l'événement. Dans les champs utiles du sélecteur on peut citer les champs
data_this contenant la donnée liée au selecteur et le champ selector contenant,
par exemple, le numéro du descripteur, dans le cas d'une activation sur descripteur de fichier.
Enfin, pour décrire comment un événement est déclenché, trois fonctions permettent d'associer
des sélecteurs à des événements :
int eventsTrigger(int identity,void *data);
int eventsSchedule(int identity,long timeout,void *data);
int eventsAssociateDescriptor(int identity,int descriptor,void *data);
La première fonction déclenche l'événement immédiatement; ses actions sont appelées
dès que la fonction principale de surveillance des événements s'active. La seconde
fonction programme un déclenchement différé au bout de timeout micro-secondes.
Enfin la dernière fonction associe un événement à un descripteur; les actions de
l'événement sont exécutées dès que des données en lecture sont disponible sur le
descripteur.
Enfin le gestionnaire d'événement est démarré par la fonction
void eventsScan(void);
L'appel de cette fonction est bloquant. La fonction ne peut se terminer que si aucun
événement ne possède plus de sélecteur. Il faut noter que c'est une situation qui
peut se produire; les sélecteurs insérés par les fonctions eventsTrigger et
eventsSchedule sont supprimés après l'exécutions des actions correspondantes.
De même si une des actions liées à un événement déclenché sur un sélecteur de type
descripteur retourne une valeur négative le sélecteur est supprimé.
3.3 Bibliothèque de gestion d'interfaces Ethernet virtuelles
Comme dans le projet précédent cette bibliothèque contient une unique fonction
permettant de créer une interface Ethernet virtuelle. Ne pas oublier les drapeaux
IFF_TAP et IFF_NO_PI lors de l'appel. Il est rappelé que la lecture et
l'écriture sur une interface TAP est triviale; les paquets sont envoyés directement
sans mention de leur taille. La création d'une interface TAP nécessite des droits
spécifiques. Pour donner ces droits à votre programme vous utiliserez le script
setcap déclaré dans le fichier super.tab des machines de TP. De la
même façon, un utilisateur normal ne peut pas configurer une interface réseau;
utilisez à nouveau la commande super mais en appelant cette fois le script
ifconfig.
3.4 Prise en main de la bibliothèque de gestion d'événements
Vous allez utiliser la bibliothèque des événements pour écrire un petit programme
de test.
Commencez par créer un événement dont l'action est l'affichage des données
du sélecteur, c'est à dire du déclencheur de l'action. Pour vérifier que cette
première partie fonctionne, ajoutez deux sélecteurs à l'événement pour déclenchements
différés de 30s et 60s. Comme données de ces sélecteurs utilisez des chaînes de
caractères assez longues et allouées sur le tas. Ces données doivent être libérées
après affichage.
Dans un second temps faites en sorte que les deux sélecteurs se déclenchent sous
l'une ou l'autre des deux conditions suivantes : l'expiration d'un minuteur
(même durées) ou la détection d'une activité sur l'entrée standard. Il est demandé
de ne pas dupliquer les chaînes de caractères. L'action doit maintenant afficher
la donnée du sélecteur et le texte tapé sur l'entrée standard ou un message expliquant
que l'utilisateur n'a pas tapé le texte assez vite.
Il est recommandé de procéder en créant un événement supplémentaire et de faire en
sorte que les sélecteurs se déclenchent sur l'activité d'un descripteur. Le descripteur
étant le descripteur en lecture d'un pipe. Utilisez valgrind pour vérifier que
votre programme ne présente pas de fuite mémoire.
4 Amélioration de la pile TCP/IPv4
Il est maintenant temps d'améliorer la pile TCP/IPv4 de vos ainés.
4.1 Structure en couches
Regardez comment les différentes couches de la pile TCP/IP sont déclarées dans la structure
de données stackLayers du fichier stack.c. Explorez aussi les fichiers net*.c
pour comprendre comment les différentes couches appellent les couches supérieures (dans les
fonctions xxxDecodePacket) et les couches inférieures (dans les fonctions
xxxSendPacket). En particulier, notez comment les tableaux associatifs sont
utilisés pour communiquer entre les couches.
4.2 Gestion mémoire
En supposant que la primitive realloc soit capable de changer la taille des blocs
mémoire sans jamais devoir déplacer ces blocs et que la fonction memmove soit
capable de déplacer les données sans jamais utiliser de mémoire tampon, donnez la
taille de l'espace mémoire utilisée sur le tas dans les situations suivantes :
-
récupération d'une requête ARP et réponse ARP ;
- récupération d'un paquet UDP et réponse UDP à ce paquet UDP.
Détaillez l'utilisation mémoire selon les couches de la pile.
4.3 Protocole ARP
Vérifiez que la pile TCP/IP gère déjà correctement ARP, en particulier les requêtes ARP.
Lancez la pile, affectez une adresse IP à l'interface Ethernet virtuelle dans le même
réseau IP que la pile, et lancez un ping sur l'adresse IP de la pile. Arrêtez
l'utilitaire ping et vérifiez que la requête ARP de votre machine de TP a bien
reçu une réponse.
4.4 Prise en compte des réponse ARP
Relancez l'utilitaire ping sur l'adresse IPv4 de votre pile TCP/IPv4.
Comparez le premier temps d'aller-retour des sondes ICMPv4 avec les temps suivants.
Que constatez-vous ? Examinez le code pour comprendre le pourquoi de vos constatations.
Vos observations doivent vous conduire à une constante particulière. Quel est son nom ?
4.5 Amélioration pour ARP
En utilisant l'astuce de programmation employée dans l'exercice 3.4 supprimez
le délai de prise en compte des réponses ARP dans la pile TCP/IPv4. Vous pouvez suivre
les suggestions suivantes.
-
Modifiez la structure de la table ARP pour permettre d'y stocker deux types d'entrées;
les entrées classiques et les entrées incomplètes. Les entrées classiques sont constituées
d'une adresse IP, d'une adresse MAC et d'une estampille de temps. Les entrées incomplètes
comprennent un identifiant, l'adresse IP et un descripteur.
- Modifiez le mode de réémission d'un paquet IP; passez d'une réémission sur expiration
de minuteur à une réémission sur activité d'un tube. Le descripteur d'écriture du tube est
passé à la couche ARP à la faveur de la demande d'émission de requête ARP.
- Créez un événement local à la couche ARP. L'action liée à cet événement doit utiliser
l'identifiant d'entrée incomplète passée en donnée du sélecteur pour trouver le descripteur
permettant de signaler l'arrivée d'une réponse ARP.
- Faites en sorte qu'une entrée incomplète soit insérée dans le cache ARP dès l'envoi
d'une requête ARP. En profiter pour prévoir le déclenchement d'un événement ARP après
l'expiration d'un minuteur.
- Lors de l'arrivée d'une réponse ARP, trouver l'ensemble des entrées incomplètes
concernées. Signaler les paquets en attente à l'origine de ces entrées incomplètes.
- Modifier le mécanisme de purge du cache ARP pour supprimer les entrées incomplètes
ayant été utilisées.
5 Ajout des protocoles IPv6/ICMPv6
Le coeur de votre tutorat est de commencer l'écriture de la pile IPv6. Vous commencerez
par modifier la couche Ethernet pour permettre la réception des paquets de diffusion.
Puis vous implanterez une version basique des protocoles IPv6 et ICMPv6. Votre protocole
ICMPv6 doit gérer la découverte de voisin et les paquets d'écho. Ainsi, il sera possible
de tester votre pile à l'aide de l'utilitaire ping6.
5.1 Modification du protocole Ethernet
Faites en sorte que la couche Ethernet puisse gérer un tableau des adresses de diffusion
à honorer. En effet le protocole IPv6 ne peut pas se contenter de la diffusion totale
ff:ff:ff:ff:ff:ff. Il est suggéré d'ajouter un tableau des adresses de diffusion
à la structure de l'interface Ethernet. Les fonctions de gestion des adresses de diffusion
devraient être au niveau des fichiers stack.[ch].
La mise à jour du tableau des adresses de diffusion devrait se faire lors de la configuration
des interfaces réseau. Vous pouvez ajouter une fonction d'initialisation dans le tableau des
protocoles (type StackLayers). Les fonctions d'initialisations devraient être définies
dans les fichiers netxxx.[ch] correspondants. Il est suggéré de passer en paramètres de
la fonction un tableau associatif comportant au moins un pointeur vers l'interface réseau
courante.
5.2 Protocole IP version 6
Passons à la couche réseau IPv6. Vous allez créer deux fichiers dans le projet. Le
fichier des definitions netipv6.h et le fichier des fonctions netipv6.c.
Définissez la structure des adresses IPv6 et des entêtes IPv6 dans le fichier netipv6.h.
Ecrivez les fonctions de manipulation des adresses IPv6 dans le fichier netipv6.c.
5.2.1 Interfaces réseau
Vous allez devoir modifier la structure des interfaces réseau de stack.h en ajoutant
l'adresse IPv6 de l'interface et un tableau des adresses IPv6 de diffusion que l'interface
réseau doit prendre en compte. Au niveau du fichier stack.c, ajoutez une adresse
IPv6 dans la variable de l'interface réseau eth0.
5.2.2 Initialisation
La fonction d'initialisation du protocole IPv6 doit enregistrer les adresses Ethernet
de diffusion utilisées par IPv6 dans le tableau ad hoc de l'interface réseau courante.
Elle doit aussi calculer la liste des adresses IPv6 de diffusion nécessaire et la copier
dans la structure de l'interface réseau courante.
5.2.3 Fonction de décodage
La fonction de décodage ipv6DecodePacket doit vérifier les points suivants :
-
l'adresse IPv6 de destination est une adresse de l'interface ou une adresse de
diffusion gérée par l'interface,
- le nombre de sauts doit être strictement positif,
- le numéro d'entête suivante est un numéro de protocole,
- la taille dans les entêtes IPv6 est cohérente avec la taille du paquet.
N'oubliez pas le code de déverminage permettant, au minimum, d'afficher les paquets
IPv6 entrants et verifiés.
5.2.4 Fonction d'encodage
En ce qui concerne l'envoi de paquet (fonction ipv6SendPacket) les points suivants
sont à considérer :
-
Le paquet IPv6 est à constituer en fonction des paramètres d'appel obligatoires
de la fonction (protocole IPv6, adresse IPv6 de destination et données IPv6), en fonction
des paramètres optionnels (voir la structure IPv4_send_options et les constantes
IPV4_SEND_OPTION_xxxx dans l'archive) et enfin des paramètres disponibles
dans la structure d'interface réseau (adresse IPv6 source).
- Le paquet IPv6 doit être envoyé via la fonction d'envoi de la couche liaison; il
faut donc vérifier la taille du paquet (en toute théorie il faudrait fragmenter si le
paquet IPv6 ne tient pas dans la trame de la couche liaison) et trouver l'adresse MAC de
destination.
- Si le cache des voisins ne contient pas la bonne association, une découverte
de voisin doit être menée et le paquet IPv6 doit être relancé lors de la réception de
l'annonce de voisin ou à l'expiration d'un minuteur. Prévoir un compteur pour les éliminer
à terme.
- Un protocole IPv6 spécial doit être prévu pour les paquets à retransmettre, ces
paquets sont effet complets avec leurs entêtes IPv6, ipv6SendPacket doit donc les
traiter de façon particulière.
5.3 Protocole ICMP version 6
Le protocole ICMPv4 est simple à implanter. Dans la fonction de réception icmpv6DecodePacket
il faut vérifier la somme de contrôle ICMPvv64 (avec la fonction genericChecksum).
Votre couche ICMPv6 doit traiter les découvertes de voisins et répondre à une requête d'écho.
Vous n'aurez, bien sur, pas oublié d'insérer du code de déverminage permettant, au minimum,
d'afficher les paquets ICMPv6 entrants et sortants. Une fois ICMPv6 implanté vous vous
précipiterez pour tester le bon fonctionnement de votre pile et lançant de la machine
réelle un ping6 sur l'adresse IPv6 de votre pile (via l'interface tap0).
This document was translated from LATEX by
HEVEA.