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 de réaliser une pile TCP/IP basique. Pour la promotion 2013, l'implantation doit aller jusqu'à permettre le fonctionnement d'un serveur TCP Echo Protocol (voir le RFC 862). La pile TCP/IP doit être le principal constituant d'une machine virtuelle tournant dans un processus Unix. Cette machine virtuelle doit avoir au moins une interface réseau virtuelle de type Ethernet.

2   Indications pratiques

Vous devez utiliser l'archive http://rex.plil.fr/Enseignement/Reseau/Tutorat10.Reseau.GIS4/NetStackStudents.tgz. Elle contient une arborescence de répertoires avec les Makefile pour les deux bibliothèques et l'exécutable nécessaires au projet.

3   Bibliothèques fournies

Deux bibliothèques vous sont fournies pour mener à bien ce tutorat réseau. Une bibliothèque de gestion d'évenements, très générique et qui pourrait être utilisée dans un autre projet. Une autre bibliothèque concernant les interfaces Ethernet virtuelles, déjà utilisée dans le tutorat précédent de réalisation d'une couche liaison.

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 : Bien sur la valeur pour un indice peut être retouvé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é 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 est une version étendue de la bibliothèque que vous avez utilisé pour le projet de réalisation d'une couche liaison. Dans cette version un événement n'est pas forcement déclenché par une activité sur un descripteur de fichier mais peut l'être par programmation avec effet immédiat ou différé. 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

Commencez par comprendre le fonctionnement de la bibliothèque en ajoutant des fonctions (dont une fonction main) dans le fichier stack.c. Vous devez créer des événements de différents types :

3.5   Prise en main de l'ensemble des bibliothèques

L'idée est maintenant de programmer un embryon de pile TCP/IP capable d'afficher les paquets Ethernet reçus. Pour cela vous trouverez dans les fichiers netether.[hc] les fonctions de décodage et d'émission de paquets Ethernet. Commencez par utiliser la fonction de décodage comme action d'un événement auquel vous allez associer un sélecteur par descripteur de fichier. Vous aurez compris que le descripteur en question doit être obtenu en utilisant la bibliothèque de gestion des interfaces Ethernet virtuelles. Attention, pour que la fonction de décodage fonctionne correctement, l'événement doit être initialisé avec une donnée de type EthernetInterface. Testez votre exécutable stack après l'avoir passé par le script setcap. Pour tester votre embryon de pile, vous devez configurer l'interface TAP avec le script ifconfig. Générez des paquets ARP via la commande ping, votre pile doit les capturer et les afficher.

Dans un second temps, tentez d'émettre un paquet Ethernet. Pour cela créez un événement avec la fonction d'émission Ethernet comme action. Associez à cet événement un sélecteur de type minuteur (réglé à 30s). Prenez garde à bien initialiser le sélecteur avec une donnée de type tableau associatif. Regardez le code pour trouver les éléments que le tableau doit contenir. En particulier, en lisant le code, vous devez vous apercevoir que la donnée associée à l'indice data est utilisée dans un realloc. Il faudra donc insérer la valeur dans le tableau en utilisant le bit AARRAY_DONT_DUPLICATE. Pour vérifier que le paquet est bien envoyé par votre embryon de pile, lancez l'utilitaire ether sur l'interface tap0. Attention ether doit être utilisé, lui aussi, via super.

4   Analyse de l'existant

Il est maintenant temps d'explorer le code de la pile TCP/IP déjà écrit.

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 : 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 la commande ping et vérifiez que la requête ARP de votre machine de TP a bien reçu une réponse.

4.4   Protocole IP

Vérifiez que la pile TCP/IP gère déjà correctement IP. Relancez la manipulation avec la commande ping et vérifiez que les paquets ICMP echo request sont bien reçus par la couche IP. Familiarisez vous avec le code de la fonction ipDecodePacket, puis ajoutez ce qui est nécessaire pour envoyer le paquet ICMP ad hoc quand un paquet IP avec un TTL nul est reçu. Appuyez vous sur le code permettant de signaler à l'expéditeur qu'un protocole IP n'est pas connu de la pile. Trouvez une option à l'utilitaire ping permettant de constater que les options IPv4 sont bien affichées mais non prises en compte.

4.5   Protocole UDP

Commencez par examiner la structure stackProcess du fichier stack.c pour comprendre comment sont déclarés les processus applicatifs (couche 7, applications). Regardez aussi dans le fichier netudp.c comment ces processus sont trouvés et appelés. Enfin regardez le processus udp_echo pour comprendre comment fonctionne un tel processus, en particulier analysez l'utilisation du paramètre type. Testez le processus udp_echo en envoyant un datagramme UDP sur le port UDP 4000 de la pile. Pour ce faire utilisez l'utilitaire nc avec l'option -u.

5   Travail à effectuer

Après les exercices précédents destinés à vous familiariser avec l'environnement de développement de la pile TCP/IP, vous allez enfin contribuer à l'écriture de cette pile.

5.1   Protocole ICMP

Faites vos armes sur un protocole assez simple comme ICMP. Les deux fonctions à coder sont icmpDecodePacket et icmpSendPacket. La fonction icmpSendPacket construit l'entête ICMP devant les données fournies lors de l'appel puis appelle la couche inférieure, c'est à dire IP. Attention, pour optimiser l'espace mémoire, il est recommandé d'utiliser realloc plutôt que malloc. Regardez comment cet appel système est utilisé dans ipFillHeader. La fonction icmpDecodePacket doit implanter au moins deux fonctions; traiter les echo request en envoyant un echo reply et sur réception d'un ICMP port unreachable propager l'information au processus UDP concerné. Testez votre implantation dans les cas suivants : Donnez la taille mémoire utilisée sur le tas dans les situations suivantes : Détaillez l'utilisation mémoire selon les couches de la pile.

5.2   Protocole TCP

Votre travail proprement dit commence ici avec l'implantation du protocole TCP. Il vous est demandé de réaliser les tâches décrites ci-après.

5.3   Tests TCP

Pour tester votre implantation TCP, connectez vous avec l'utilitaire nc sur votre pile TCP/IP. Connectez-vous à la fois sur des ports sans processus liée et sur le port sur lequel écoute votre serveur écho. Testez votre serveur écho avec plusieurs clients simultanés. Testez votre implantation en liant le serveur écho à plusieurs ports TCP, là encore tentez plusieurs connexions simultanées.

6   Améliorations possibles

Voici quelques améliorations possibles à apporter à la pile TCP/IP.

6.1   Configuration de l'interface virtuelle

Ecrire une fonction dans le fichier stack.c permettant de configurer l'interface Ethernet virtuelle en utilisant la primitive ioctl ainsi que les requêtes SIOCSIFADDR et SIOCGIFFLAGS. Ajoutez un champ dans la table des interfaces pour spécifier l'adresse de l'interface virtuelle correspondante et appelez votre fonction dans stackInitializeDevices.

6.2   Gérer les options IPv4

Analyser les options IPv4 dans les fonctions ipDecodePacket et ipSendPacket. Tester la bonne gestion de l'option d'enregistrement de route avec l'utilitaire ping et l'option -R.

6.3   Gérer la défragmentation IPv4

Gérer les fragments IP dans la fonction ipDecodePacket. Créer une structure pour les paquets IP fragmentés comprenant principalement le paquet IP en défragmentation et une carte des données déjà reçues. Lors de la réception d'un fragment aller mettre les données reçues dans le paquet en reconstruction, éventuellement en réallouant l'espace réservé si le nouveau fragment est en dehors du paquet courant. Sur réception du dernier fragment mettre à jour le champs taille du paquet. Enfin quand la carte des données reçues montre que le paquet est complet, le traiter comme un paquet IP classique. Vérifier le bon fonctionnement de la défragmentation en utilisant ping avec une taille de paquet dépassant la taille d'un paquet Ethernet.

6.4   Gérer la fragmentation IPv4

Gérer les fragments IP dans la fonction ipSendPacket. Quand on s'aperçoit que la taille des données IP dépasse la taille permettant d'envoyer un seul paquet Ethernet, découper les données pour envoyer des fragments IP. Vérifier le bon fonctionnement de cette fragmentation en envoyant un datagramme UDP de votre pile vers un serveur UDP de la machine de TP (le serveur étant réalisé par nc avec les options -l et -u).

6.5   Gérer une option TCP

Il existe de nombreuses options TCP qu'il est intéressant d'implanter. Un premier exemple est l'option d'estampille de temps qui permettrait de calculer des temps d'aller-retour pour mettre à jour la valeur des délais pour les minuteurs de ré-émission. Un second exemple est l'option d'accusé de réception sélectif pour accuser réception de données hors séquence. Cette dernière option nécessite de modifier la gestion des paquets TCP en ajoutant un tampon de données reçues pour chaque connexion TCP.

6.6   Eviter l'accumulation de paquets TCP en souffrance

Si un processus TCP génère un flux constant de paquets TCP et que le correspondant n'accuse pas réception, les paquets vont être stockés sous forme de sélecteurs. Il est possible de saturer la mémoire de la pile de cette façon. Implantez un mécanisme permettant de refuser l'envoi d'un paquet TCP si trop de données TCP sont accumulées. Trouvez aussi un protocole pour que les processus TCP puissent gérer ce bloquage. Une piste est l'appel du processus TCP avec un type particulier lorsque la pile s'aperçoit que les données TCP finissent par être accusées par le correspondant.

6.7   Processus TCP serveur web

Ecrire un processus TCP réalisant un serveur HTTP. Prévoir un système de stockage des pages en mémoire et/ou sur système de fichiers.


This document was translated from LATEX by HEVEA.