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 :
-
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 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 :
-
à déclenchement immédiat et affichant la donnée de l'événement et celle du
sélecteur (données de type chaîne de caractères), créez un événement de ce
type avec 10 sélecteurs, tous possédant des données différentes ;
- à déclenchement différé et affichant la donnée du sélecteur, créez un
événement de ce type avec deux sélecteurs, un sélecteur avec un minuteur
de 10 secondes et non récurrent, un sélecteur avec un minuteur de 5 secondes
et se réinitialisant 10 fois ;
- à déclenchement lié à un descripteur et affichant les données lues sur le
descripteur préfixées d'une chaîne quelconque, créez un événement de ce type
et un sélecteur lié à l'entrée standard.
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 :
-
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
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 :
-
connectez vous avec nc sur un port TCP quelconque; vous devez recevoir
un message ICMP indiquant que le protocole TCP n'est pas géré par la pile ;
- connectez vous sur un port UDP non lié à un processus, nc doit se
terminer instantanément sur réception d'un ICMP port unreachable ;
- lancez un ping sur votre pile TCP/IP, l'utilitaire doit indiquer que
les ICMP echo reply sont bien reçus, trouvez comment diminuer le temps
d'aller-retour de la première sonde ;
- enfin écrivez un processus UDP client et faites lui traiter le cas de l'absence
de serveur UDP; implantez le serveur UDP sur la machine de TP avec nc
et les options -u -l, si le serveur UDP existe faites en sorte que
le client UDP de votre pile affiche la réponse du serveur, dans le cas
contraire affichez un message mentionnant l'absence de serveur.
Donnez la taille mémoire utilisée sur le tas dans les situations suivantes :
-
réception d'un echo request et réponse avec un echo reply
quand l'adresse IP de la destination du echo reply n'est pas
dans le cache ARP de la pile ;
- même question dans le cas où l'adresse IP est dans le cache ARP.
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.
-
Définissez la structure des paquets TCP sur le modèle de la structure des
paquets IP.
- Définissez la structure des connexions TCP stockant en particulier les
numéros de séquence et l'état des connexions TCP
(voir http://en.wikipedia.org/wiki/File:Tcp_state_diagram_fixed.svg).
Il faut, bien évidement, écrire les fonctions de gestion du tableau des connexions
TCP; ajout d'une connexion en utilisant éventuellement realloc, suppression
d'une connexion et recherche d'une connexion.
- Ecrivez la fonction tcpDecodePacket dont la principale fonction est
de vérifier la somme de contrôle TCP, de s'assurer que le paquet est bien le suivant
dans la séquence TCP et dans l'affirmative de traiter ce paquet. Le traitement consiste
soit à répondre à un paquet SYN ou FIN, soit à fournir au processus correspondant les
données TCP (voire envoyer un paquet TCP RST si aucun processus n'est lié au port
précisé). Le traitement inclut aussi l'envoi d'un accusé de réception. L'accusé
peut être soit envoyé directement, soit être programmé pour inclusion dans un prochain
paquet de données à destination du correspondant. Dans ce dernier cas il faut prévoir
un minuteur pour envoi de l'accusé dans le cas où aucun paquet de donnée n'est envoyé
au correspondant durant un certain laps de temps.
- Ecrivez la fonction tcpSendPacket pour envoyer un paquet TCP. Cette
fonction va remplir les champs des entêtes TCP et calculer la somme de contrôle. Le
cas échéant (voir discussion ci-dessus), un accusé de réception peut être ajouté au
paquet. Le paquet est alors envoyé via la couche inférieure, c'est à dire IP. Une
ré-émission du même paquet doit être programmé au bout d'un temps raisonnable. Pour
réaliser cette ré-émission il est conseillé de déclencher un appel identique à la
fonction tcpSendPacket à l'aide de la fonction eventsSchedule. Il faudra
donc vérifier en début de la fonction tcpSendPacket que le numéro de séquence
du paquet à envoyer ne soit pas inférieur à la séquence courante. Le cas écheant le
paquet doit être supprimé sans déclencher de ré-émission.
- Ecrivez une fonction dans le fichier stack.c pour envoyer un paquet
TCP sur le modèle de la fonction qui génère un paquet UDP (stackUDPSendDatagram).
L'appel de cette fonction avec un tableau de données vide doit avoir une sémantique
particulière. Si la connexion TCP avec le correspondant n'est pas déjà établie, alors
une structure connexion est allouée et le premier paquet TCP SYN est envoyé au
correspondant. Si la connexion TCP avec le correspondant est déjà établie, alors
un paquet FIN est envoyé au correspondant, la structure de connexion sera libérée
sur réception du paquet FIN du correspondant.
- Ecrivez dans processes.c la fonction tcp_echo qui va réaliser le
serveur d'écho TCP. Le serveur doit afficher un message sur sa sortie standard quand un
client se connecte (avec les coordonnées du client) et quand un client se déconnecte
(toujours avec les coordonnées du client).
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.