Tutorat système & réseau commutateur virtuel
Nathalie Devesa & Xavier Redon
1 Description générale du projet
1.1 Objectif
L'objectif du projet est de réaliser des réseaux privés en lancant des commutateurs virtuels
sur toutes les machines à relier, en connectant les machines à leurs commutateurs en utilisant
des interfaces Ethernet virtuelles (TAP) et en interconnectant les commutateurs entre eux par des
connexions TCP (sockets classiques). Il suffit alors de réaliser la configuration IP des
interfaces Ethernet virtuelles; des VPN (Virtual Private Network) sont alors constitués.
1.2 Architecture générale de l'application
L'application est constituée de 2 exécutables : le commutateur virtuel et le client
d'administration. Pour permettre de traiter simultanément plusieurs ports ainsi que les requêtes
de l'administrateur, le commutateur doit être multi-threadé. Un processus léger (thread) est
lancé pour chaque port connecté à une interface Ethernet virtuelle et pour chaque port connecté
à un autre commutateur virtuel. Ces processus légers sont chargés de récupérer les paquets Ethernet
arrivant sur les ports et de les propager aux bons ports cibles.
De même, un processus léger est chargé de traiter d'éventuelles requêtes de l'administrateur. Pour
chacune de ses requêtes, l'administrateur lance un client d'administration. Celui-ci communique
avec le commutateur par IPC (files de messages).
Enfin un processus léger est chargé de purger les mémoires associées aux ports. Ces mémoires
stockent les adresses Ethernet des stations connectées aux ports. Les adresses sont trouvées
dans le champ adresse source des paquets arrivant sur les ports. Chaque adresse est mémorisée
avec la date d'arrivée du paquet. Les adresses expirent au bout d'un certain temps et c'est au
processus léger de purge de retirer les adresses expirées.
1.3 Exemple d'utilisation
Prenons comme exemple d'utilisation du commutateur virtuel, une création de réseaux
privés entre machines personnelles connectées à Internet par des boites ADSL quelconques
(marquées box sur le schéma). Il est courant que les ordinateurs derrière ces boites ne
puissent que se connecter vers Internet et non être accédées en provenance d'Internet.
On suppose tout de même que la boite d'adresse IP IPbox0 est configurée pour
rediriger son port 4000 vers la machine d'adresse IP IPpc0. C'est à dire, vers
le port du commutateur virtuel tournant sur cette machine. Prenons aussi l'hypothèse
que l'on réalise les configurations de commutateurs virtuels décrites dans le schéma
ci-dessous. Dans ce schéma, la notation PxVy signifie une connexion au port x
du commutateur virtuel, port qui se trouve dans le VLAN y.
Du coup, alors que les ordinateurs 0, 1 et 2 ne peuvent pas se contacter par les
adresses IP de leurs interfaces Ethernet (ou de leurs boites ADSL) ils peuvent le
faire en utilisant les adresses des interfaces virtuelles dans le réseau IP
192.168.100.0/24. C'est aussi vrai pour les ordinateurs 3 et 4 dans le
même réseau IP. C'est enfin vrai pour les ordinateurs 0, 5 et 6 dans le
réseau IP 192.168.200.0/24. Accessoirement, on se rend compte que l'on peut
utiliser les mêmes réseaux IP pour des groupes différents de machines du moment que
les groupes se trouvent dans des VLAN différents.
Vous allez pouvoir tester votre commutateur dans une situation similaire en
constituant des VPNs entre des machines du réseau de TP de l'école et des machines
des salles de projets informatiques. En effet, si les machines de TP peuvent contacter
les machines de projets, l'inverse n'est pas vrai à cause des règles de filtrage
implantées sur le routeur de l'école. Les machines de projets ne peuvent contacter,
comme machine de TP, que le seul serveur weppes (et encore que sur les ports
22 et 4200 à 4242). Pour vos tests, il vous est donc suggéré de lancer un commutateur
virtuel sur weppes et d'autres commutateurs sur des machines de TP et de
projets. Reliez les commutateurs au commutateur central sur weppes et vérifiez
que dans vos VPNs les machines de projets peuvent accéder aux machines de TP.
1.4 Le commutateur virtuel
Le fonctionnement de base du commutateur est de lire des paquets Ethernet
sur ses ports et de les transmettre sur les bons ports. Le principe est
de gérer une liste d'adresses Ethernet associée à chaque port. Si l'on trouve
l'adresse de destination du paquet dans la liste d'un port, le paquet est
transmis sur cet unique port. Dans le cas contraire ou si l'adresse Ethernet
est une adresse de diffusion (bit de poid faible de l'octet de poids fort à 1),
le paquet est transmis sur tous les ports sauf le port d'origine. Pour remplir
les listes, il suffit d'ajouter (ou de rafraîchir) l'adresse Ethernet source
dans la liste à chaque fois qu'un paquet se présente sur un port.
Il est prévu d'implanter aussi la notion de VLAN (Virtual LAN); à chaque port est
associé un numéro de VLAN. Seuls les ports dans le même VLAN peuvent communiquer;
cela permet de réaliser des VPN différents en utilisant une même instance de
commutateur virtuel.
La structure de données principale est celle d'un port de commutateur qui contient
principalement la liste des adresses Ethernet apprises, le numéro de VLAN et
le descripteur de connexion (un descripteur de fichier pour les interfaces Ethernet
virtuelles et un descripteur de socket pour les connexions inter-commutateurs).
Un tableau de taille fixe de ces structures représente un commutateur virtuel.
Pour chaque port connecté, un processus léger scrute le descripteur de connexion
associé. A chaque arrivée de paquet, le processus léger détermine les ports de
destination et écrit les paquets sur ces ports. Pour éviter que les paquets à
destination d'un port soient corrompus, il faut absolument considérer l'écriture
sur un port comme une section critique.
1.5 Connexion entre deux commutateurs
Lorsqu'un commutateur A reçoit l'ordre du client d'administration de se connecter sur un
autre commutateur virtuel B les actions suivantes sont exécutées ;
-
le commutateur A se connecte sur le serveur TCP de B ;
- le commutateur A indique son port local de connexion par
la commande lport <n° de port> ;
- le commutateur B accuse réception de la commande lport
par la réponse OK ou SYNTAX en cas d'erreur de syntaxe
ou BOUNDS en cas de numéro de port fantaisiste ;
- le commutateur A indique le port de connexion sur B par
la commande dport <n° de port> ;
- si le port demandé est libre, B répond par OK sinon par
SYNTAX ou BOUNDS ou BUSY ;
- si le port demandé est libre, le commutateur A stocke le descripteur
de socket correspondant à la connexion dans la structure du port local et
se met à gérer le port ;
- si le port demandé est libre, le commutateur B stocke le descripteur
de socket de dialogue correspondant à la connexion dans la structure du
port distant et se met à gérer le port.
Lorsque l'un des deux commutateurs souhaite terminer la connexion, il arrête simplement
la connexion TCP sans envoyer de message particulier. Le correspondant doit savoir
repérer l'arrêt de la connexion. Il faut prendre soin de repasser les ports concernés
dans l'état déconnecté et d'arrêter le processus léger correspondant.
1.6 Les requêtes administrateur
L'administrateur lance le client d'administration à chaque fois qu'il veut effectuer
une requête (la requête est fournie en argument). L'administrateur peut envoyer plusieurs
requêtes simultanément, c'est à dire exécuter plusieurs instances du client d'administration
simultanément. Les requêtes à disposition de l'administrateur sont les suivantes :
-
aide :
- obtenir la liste des requêtes possibles ;
- lister :
- lister tous les ports d'un commutateur en indiquant
si le port est connecté ou non ;
- afficher <n° de port> :
- afficher le type d'un port connecté
(interface Ethernet virtuelle ou connexion TCP vers un autre commutateur),
la caractéristique de la connexion (nom de l'interface virtuelle ou adresse
IP du commutateur virtuel et numéro du port distant) et enfin le numéro de
VLAN du port ;
- adresses <n° de port> :
- lister les adresses Ethernet liées à un port
avec leurs âges ;
- connecter_tap <n° de port> :
- connecter un port à une interface Ethernet
virtuelle, l'interface virtuelle doit être créée par le commutateur et son nom
doit être retourné ;
- connecter_tcp <n° de port local>:[<port TCP>@]<nom machine>:<n° port distant> :
-
connecter un port local à un port d'un commutateur distant, indiquer la raison
d'un éventuel échec ;
- vlan <n° de port>:<n° de VLAN> :
- affecter un numéro de VLAN à un
port local ;
- deconnecter <n° de port> :
- déconnecter un port local de son interface
Ethernet virtuelle ou de son commutateur distant et donc arrêter le processus
léger correspondant ;
- stats <n° de port> :
- afficher les statistiques d'un port, c'est à dire
le nombre d'octets reçus sur le port et le nombre d'octets envoyés du port ;
- scruter :
- scruter les événements de configuration du commutateur,
principalement les connexions et déconnexions de ports ;
- sniffer <n° de port> :
- afficher les paquets arrivant ou partant d'un
port, plusieurs gestionnaires doivent pouvoir être lancés pour surveiller plusieurs
ports ;
- stopper :
- arrêter le commutateur virtuel.
Tout dialogue entre le commutateur et le client d'administration est réalisé par
des communications IPC (envoi et réception de messages). Pour permettre l'exécution
simultanée de plusieurs clients d'administration il faut créer une file de messages
de réponse par client et envoyer le descripteur de cette file dans les requêtes. Pour
implanter les commandes scruter et sniffer, il est recommandé de définir,
au niveau du commutateur virtuel, un tableau des clients d'administration en attente
d'informations. Ainsi à chaque nouvelle information à leur transmettre, il suffit de
parcourir ce tableau et de les contacter sur leur file de messages de réponse. Prenez
soin de protéger les accès au tableau des clients d'administration.
2 Organisation du travail
2.1 Généralités
Il est fortement conseillé de suivre les étapes proposées ci-après pour réaliser le travail.
2.2 Organisation modulaire
Le projet est constitué de 3 bibliothèques et des programmes commutateur virtuel et
client d'administration. Chacune de ces entités est développée dans un répertoire propre.
On gère donc les cinq répertoires suivants :
-
Network -
- pour les sources de la bibliothèque contenant les fonctions
de gestion réseau libnet ;
- Threads -
- pour les sources de la bibliothèque contenant les fonctions
de gestion des threads libthrd ;
- IPC -
- pour les sources de la bibliothèque contenant les fonctions de
gestion des communications inter-processus libipc ;
- Switch -
- pour le programme principal, l'algorithme du commutateur,
la gestion des connexion et le traitement des commandes de l'administrateur ;
- Admin -
- pour le client d'administration.
Cette arborescence et quelques squelettes de fichiers sont disponibles sous forme
d'un fichier au format tar et compressé à l'URL
http://www.plil.net/~rex/Enseignement/Systeme/Tutorat.GIS4.commutateur/commutateur.tgz.
Transférez ce fichier dans votre compte Polytech'Lille et décompressez-le avec
la commande tar xvzf commutateur.tgz. Vous pouvez constater que très peu de code vous
est fourni :
-
quelques fonctions d'affichage dans la bibliothèque Network pour impulser
l'affichage de déverminage ;
- le code de la fonction de création d'interface Ethernet virtuelle.
Cependant, le répertoire créé contient déjà un fichier Makefile global et des Makefile
annexes dans chaque sous-répertoire. La cible clean du Makefile global est opérationnelle,
à vous de compléter le fichier pour rendre la cible all, elle aussi, opérationnelle. Vous avez
un exemple de Makefile permettant de générer un exécutable et un exemple de Makefile
permettant de construire une bibliothèque. Appuyez vous sur ces deux exemples pour compléter les
autres Makefile. Le projet doit pouvoir être généré par la simple commande make exécutée
dans le répertoire principal du projet. Il doit être aussi possible de regénérer complètement le
projet par la commande make clean all lancée du même répertoire.
On prévoira de pouvoir compiler les différents sources avec un drapeau DEBUG (option
-DDEBUG de gcc), permettant un affichage conditionnel d'informations de déverminage
des programmes.
2.3 Sockets et serveur TCP
Il s'agit dans cette étape de réaliser un serveur TCP basique à l'aide de
l'interface de programmation des sockets.
Ecrivez dans le module libnet.c (répertoire Network), les deux
fonctions suivantes :
-
int initialisationServeur(short int *); Cette fonction prend en
paramètre le port sur lequel il faut écouter et retourne la socket de
lecture. Si le port fourni est nul, le port affecté par le système est
retourné. Il vous est demandé d'activer l'option de réutilisation
d'adresse sur la socket d'écoute ainsi que de désactiver les tampons
TCP.
- int boucleServeur(int, void (*)(int)); Cette fonction effectue
l'écoute sur la socket passée en premier argument et lors d'une
connexion, exécute la fonction passée en second argument. Cette
fonction passée en argument doit être une fonction qui prend une socket
en unique paramètre. Lors d'une connexion de client, la fonction
boucleServeur lance donc cette fonction avec la socket de dialogue
en paramètre.
Testez cette bibliothèque en écrivant un programme commutateur.c (répertoire
Switch) qui l'utilise et dont la fonction de traitement des connexions
(celle appelée par boucleServeur) effectue juste une écriture de message
dans la socket et clôt la connexion. Pour écrire, et plus tard lire, sur la
connexion utilisez les fonctions classiques comme fgets ou fprintf.
Pour y arriver vous devez transformer le descripteur de socket en une structure
de fichier par la primitive fdopen.
Ce programme peut prendre des arguments : -p <port> ou --port <port>
pour spécifier un numéro de port différent de celui par défaut (port
4000). Pour traiter les arguments, utilisez la fonction
getopt_long (voir la page de manuel correspondante). Si les arguments
sont incorrects, on doit afficher un message qui précise la syntaxe.
Pour plus de clarté, l'analyse des arguments et l'affichage de la syntaxe
seront écrits dans des fonctions séparées.
Modifiez la fonction de traitement des connexions afin que le serveur affiche
le nom de la machine distante (sur la sortie standard) et qu'elle ne ferme la
connexion qu'après le traitement des commandes lport et dport.
Vous devrez écrire dans libnet une fonction SocketVersNom qui prend
une socket en argument et renvoie le nom dans une chaîne de caractères. Vous
utiliserez les fonctions getpeername et gethostbyaddr (voir les pages
de manuel et le support du cours de réseau
http://www.plil.net/~rex/Enseignement/Reseau/Reseau.GIS4)
Testez votre serveur avec plusieurs commandes nc simultanées. Conclusions ?
Maintenant que vous avez écrit un embryon de serveur TCP, passez au client TCP.
Ajoutez les fonctions nomVersAdresse et connexionServeur dans
libnet (les prototypes sont fournies dans le fichier de définition de
la bibliothèque). Réalisez un exécutable temporaire utilisant ces deux fonctions
pour se connecter sur le serveur commutateur en envoyant les deux commandes
lport et dport.
2.4 Un commutateur virtuel à base de processus légers
Pour que votre commutateur puisse traiter plusieurs ports simultanément, vous
allez lancer un processus léger (thread) par port. Pour cela, implémentez la
fonction publique de libthrd (répertoire Threads) :
int lanceThread(void (*)(int),int);
Cette fonction doit avoir comme action de lancer un thread dans le mode
détaché. Ce thread doit exécuter la fonction passée en paramètre. La dite
fonction prenant elle même comme paramètre le second paramètre de
lanceThread. Il vous est conseillé d'utiliser une fonction intermédiaire
récupérant un pointeur vers une structure comprenant les deux paramètres de
lanceThread.
Testez votre fonction lanceThread en créant une nouvelle fonction dans
commutateur.c qui appelle votre fonction de traitement des connexions
via lanceThread. Utilisez la nouvelle fonction comme paramètre de
boucleServeur. Pour plus de clarté, ce serait une bonne idée de déplacer
les fonctions de traitement de connexions du fichier commutateur.c vers
le fichier gestionConnexions.c.
Vérifiez que votre commutateur est maintenant capable de gérer plusieurs
connexions simultanément (avec votre exécutable client TCP ou avec l'utilitaire
nc).
La fonction lanceThread est utilisée dans 5 situations différentes. Tout
d'abord comme expliqué plus haut pour gérer des connexions TCP ; dans ce cas
l'entier passé à la fonction paramètre est un descripteur de socket de dialogue.
Mais lanceThread est aussi utilisée pour démarrer le processus léger de gestion
de la file de message d'administration et le processus léger de purge des adresses
Ethernet. Dans ces deux cas, l'entier passé à la fonction paramètre n'est pas utilisé
(passez la valeur 0 par exemple). La fonction lanceThread est enfin utilisée
pour lancer des processus légers de gestion de port suite à des requêtes de connexion
du client d'administration à une interface Ethernet virtuelle ou à un commutateur
distant. Dans ces deux derniers cas, les entiers passés à la fonction paramètre sont
des descripteurs ; un descripteur de fichier pour une connexion à une interface Ethernet
virtuelle et un descripteur de socket pour une connexion à un commutateur distant.
2.5 Structure de données du commutateur
La structure de données du commutateur a déjà été évoquée plus haut, en particulier
dans la section 1.4.
Définissez cette structure de données dans le fichier entête commutateur.h et
écrivez sa fonction d'initialisation.
La structure des clients d'administration est plus à sa place dans le fichier entête
gestionAdmin.h.
2.6 Analyse des opérations sur la structure de données
Analysez les opérations nécessaires sur les structures de données afin de
déterminer celles qui nécessitent l'utilisation de sémaphores.
Implantez dans votre bibliothèque libthrd.a les deux fonctions publiques :
void P(int) ;
void V(int) ;
Ces fonctions cachent totalement le fait que vous utilisez des verrous
d'exclusion mutelle pour threads POSIX. En particulier, les verrous sont
représentés par une constante.
En implantant dans le module algorithmeCommutateur.c les fonctions nécessaires
au fonctionnement du commutateur et dans le module commandesAdmin.c les fonctions
nécessaires à la gestion du client d'administration vous prendrez soin
d'y ajouter les poses et levées de verrous nécessaires.
2.7 Le client d'administration
Il vous faut implanter le client d'administration et le processus léger chargé
de traiter les requêtes de l'administrateur dans le commutateur.
Les commandes sont passées en argument au client d'administration (donc pas
d'interface textuelle, mais une analyse des arguments au moyen de
getopt_long). Ce processus communique avec le commutateur au moyen
de deux files de messages : une pour les commandes et une pour les réponses.
La file des commandes est créée par le commutateur à l'initialisation. Un thread,
lancé par le commutateur est chargé de scruter en permanence cette file. La file
des réponses est créée par le client d'administration.
La bibliothèque libipc contiendra toutes les fonctions permettant de
cacher le fait que la communication est implantée par IPC (selon le même
principe que celui utilisé pour les sémaphores).
Le processus léger de gestion des requêtes est lancé par la fonction lanceThread
de la bibliothèque libthrd. Cette fois le second paramètre n'est pas utile,
vous pouvez passer une valeur quelconque.
Lorsque que le commutateur se termine, tous les clients d'administration
en mode de scrutation ou de reniflage doivent se terminer aussi.
2.8 Le fonctionnement du commutateur
Vous pouvez maintenant vous attaquer à l'écriture du code concernant le
commutateur virtuel. Il vous est conseillé d'utiliser les fichiers suivants :
-
commutateur.h : fichier des structures de données ;
- commutateur.c : programme principal et analyse des arguments ;
- gestionAdmin.c : fonction de lecture de la file de message et
d'analyse des requêtes du client d'administration ;
- commandesAdmin.c : une fonction de traitement pour chaque type
de requête du client d'administration ;
- gestionConnexions.c : la fonction de gestion des demandes de
connexion des commutateurs distants ;
- algorithmeCommutateur.c : implantation de l'algorithme de
commutation entre les ports.
Vous ferez en sorte que les écritures de paquets sur les connexions TCP se
fassent dans un format ASCII. Il est demandé que la taille du paquet soit d'abord
écrite en décimal sur une ligne puis que les octets du paquets soient écrit à la
suite en hexadécimal sur deux chiffres. Les octets sont séparés par des espaces.
Vous pouvez effectuer des sauts de lignes pour faciliter la lecture des paquets.
De cette façon le fonctionnement du commutateur virtuel peut être testé en lançant
plusieurs nc, en effectuant manuellement les commandes lport et dport
puis en envoyant des paquets toujours manuellement.
Pour la création des interfaces Ethernet virtuelles vous utiliserez la fonction
creationInterfaceVirtuelle disponible dans la bibliothèque libnet.
Un exécutable d'un utilisateur de base n'ayant pas les droits pour créer une
interface réseau vous utiliserez le script super setcap pour ajouter ces
droits. De la même façon vous ne pouvez pas directement configurer les interfaces
virtuelles. Vous passerez donc par le script super ifconfig.
Pour réaliser la déconnexion d'un port, une solution consiste à ajouter un tube
(pipe) dans la structure des ports. Le processus léger de gestion de port se met
alors en écoute sur le descripteur du port et sur le tube (primitive select).
Si l'activité est détectée sur le tube, le processus léger se termine sinon le paquet
Ethernet est lu sur le descripteur du port. L'arrêt d'un processus léger de gestion de
port peut ainsi s'obtenir par simple écriture dans son tube.
This document was translated from LATEX by
HEVEA.