Skip to content

Instantly share code, notes, and snippets.

@maxux
Created April 12, 2016 14:43
Show Gist options
  • Save maxux/b8e8baeac1ac3478e3abcd68b2cfca7b to your computer and use it in GitHub Desktop.
Save maxux/b8e8baeac1ac3478e3abcd68b2cfca7b to your computer and use it in GitHub Desktop.
\documentclass[twoside,openright]{article}
\usepackage[top=2cm, bottom=2cm, left=2cm, right=2cm]{geometry}
\usepackage[francais]{babel}
\usepackage{titling}
\usepackage{graphicx}
\usepackage[usenames,dvipsnames]{xcolor}
\usepackage{scrextend}
\changefontsizes[20pt]{12pt}
\usepackage{floatrow}
\usepackage{minted}
\usepackage{courier}
\usepackage{caption}
\usepackage{fontspec}
\setmonofont{DejaVu Sans Mono}
\definecolor{mintedbg}{rgb}{0.9, 0.9, 0.9}
\newminted{c}{
bgcolor=mintedbg,
tabsize=8,
fontsize=\footnotesize
}
\newminted{text}{
bgcolor=mintedbg,
tabsize=8,
fontsize=\footnotesize
}
\newminted{bash}{
bgcolor=mintedbg,
tabsize=8,
fontsize=\footnotesize
}
\floatsetup[listing]{style=Plaintop}
\floatsetup[figure]{style=Plaintop}
\setlength{\intextsep}{20pt plus 1.0pt minus 2.0pt}
\let\stdsection\section
\renewcommand*\section{\cleardoublepage\stdsection}
\title{Isolation de processus par virtualisation de la couche réseau dans un système open source compatible Windows.}
\author{Daniel Maxime}
\date{Mai 2014}
\graphicspath{{content/}}
\newcommand\call[1]{{\mbox{\color{MidnightBlue}#1}}}
\newcommand\hexa[1]{{\color{OliveGreen}#1}}
\newcommand\file[1]{{\color{BrickRed}\thinspace#1\thinspace}}
\renewcommand\listoflistingscaption{Table des extraits de code}
\begin{document}
\maketitle
\newpage
\tableofcontents
\clearpage
\listoffigures
\newpage
\listoflistings
\newpage
\section{Introduction}
\subsection{Présentation de l'entreprise}
La société Level IT a été fondée en décembre 2000 par deux ingénieurs civils (Hault Olivier et Laconte Luc)
de l'Université de Liège. Son activitée principale est axée sur le développement et la commercialisation
de solutions informatiques permettant de répondre aux challenges et attentes de ses clients en termes
d'excellence dans des domaines multiples comme la gestion de production, les
Systèmes de Management Intégrés (SMI), la Total Productive Maintenance (TPM),
ainsi que la gestion au quotidien des plans d'actions, d'exigences légales et de certifications
(ISO, OSHAS, REACH, ...).
\newline
Avec ses produits phares (tel que Practeos, SAMS, Phone2Blog, ...) et grâce à ses principaux clients
(ArcelorMittal, Tecteo, GlaxoSmithKline, Université de Liège, Nomacorc, ...),
Level IT a su s'imposer comme référence en termes de développements de logiciels de qualité en Wallonie.
\newline
Son implantation stratégique au coeur du « Liège Science Park » vise à consolider ses activités de
Recherches et Développement (R\&D), tout en maintenant des relations étroites avec des acteurs
de référence dans les domaines académiques, institutionnels, industriels et de services.
\newline
Leur principale technologie de développement repose sur Microsoft .NET mais ils ne se limitent
pas à ça. En effet, en Recherche et Développement, une multitude de choix de langage de programmation
différentes peuvent être utilisé pour répondre au mieux aux attentes des clients.
\newline
\begin{center}
\includegraphics[scale=1.8]{logo-levelit.eps}
\end{center}
\newpage
\subsection{Cahier des charges}
Certains systèmes d'exploitation, à l'heure actuelle, n'utilisent plus exclusivement
la « virtualisation » à proprement parler, comme moyen d'isolement d'applications sur une même machine physique.
Certains systèmes (comme Linux par exemple) permettent de lancer des applications dans
différents namespaces, qui sont en fait une instanciation de différentes parties du système,
permettant l'isolement de l'application. Pour ce qui est de la partie réseau, cela permet
à différentes applications d'avoir différentes stack TCP/IP indépendantes l'une de l'autre.
Pour l'instant, cela n'est pas possible nativement sur les systèmes Microsoft Windows.
\newline
Ce stage/TFE consiste à implémenter ce système de namespace réseau dans le kernel opensource de
ReactOS qui fonctionne de la même façon que le kernel Microsoft Windows NT.
Par exemple, on doit pouvoir lancer une application dans un namespace spécifique
qui serait totalement indépendant des autres, pour avoir un isolement réseau pour l'application.
Il faut que cet isolement puisse accéder au monde extérieur sans interférer avec les autres parties
isolées. Il faut également pouvoir connecter plusieurs parties isolées ensemble.
\newline
Cet isolement permet d'avoir une table de routage, des interfaces réseaux, des règles de firewalling, etc.
totalement indépendants vis-à-vis des autres namespaces sur le même système hôte.
\newline
Le cadre de ce projet sera une implémentation opensource. L'ajout de cette option doit ne pas
interférer avec l'utilisation actuelle du système (il doit être optionnel et transparent pour l'application).
Les applications déjà existantes doivent toujours pouvoir fonctionner de la même façon.
\newline
Actuellement, une solution propriétaire existe sous Microsoft Windows pour ça: Parallels Virtuozzo Containers
(ou Parallels Containers for Windows). Cette solution requiert l'utilisation de
la suite Parallels pour fonctionner. Pour les autres systèmes, des solutions
tels que : OpenVZ, Linux-VServer, Solaris Containers, FreeBDS Jail, sysjail, HP-UX Containers,...
sont des implémentations de virtualisation au niveau OS, mais présentent toutes différents
avantages et inconvénients. Exemple : à l'heure actuelle sous Linux, à part OpenVZ, aucune solution
ne propose une isolation totale des privilèges root.
\newline
Sous Windows, excepté « Parallels Containers », aucun système de virtualisation au
niveau OS (iCore Virtual Accounts ou Sandboxie) ne supporte l'isolement de la stack réseau.
\newpage
\section{ReactOS}
ReactOS est un système d'exploitation libre visant à être binairement compatible
avec les systèmes d'exploitations propriétaires Microsoft Windows NT.
\subsection{Historique}
En 1996, des personnes ont formé un groupe appelé « FreeWin95 », visant à implémenter
un clône de Windows 95. Cependant, suite à de trop longues discussions à propos de
l'implémentation du système, le projet n'a absolument pas abouti.
\newline
Fin 1997, Jason Filby, un développeur d'Oracle (Durban, Afrique du Sud)
alors chef coordinateur du projet FreeWin95, fait une demande générale sur la mailing list
pour demander de relancer le projet. Il a été décidé de cibler Windows NT et d'obtenir des résultats
plutôt que de parler indéfiniment sur comment faire le système.
Leur but était réellement d'écrire du code pour avoir quelque chose de fonctionnel.
\newline
Le projet a été renommé en « ReactOS » suite à l'insatisfaction du monopole de Microsoft sur le marché
des systèmes d'exploitation.
\newline
En février 1998, ReactOS est né. La première version disponible dans le svn est la 0.0.1, elle comporte
12000 lignes de code C (et 30000 lignes de headers). On y retrouve un noyau très léger mais avec déjà
une arborescence similaire à l'actuelle. Cette version ne contient qu'un noyau et un boot loader.
\newline
Par la suite, les débuts de ReactOS ont été difficiles et lents. Seuls quelques personnes savaient
comment écrire un noyau (en effet, ce n'est pas à la portée de tout le monde d'écrire un scheduler, un
boot loader, etc.).
Une fois le noyau un peu plus complet et stable, des drivers de base tels
que les drivers IDE ou simplement un driver clavier ont pu être écrits et de plus en plus de personnes ont
pu contribuer et commencer à coder à grande échelle pour le système.
\subsection{A propos de ReactOS et idéologie}
ReactOS est un système d'exploitation libre et open source basé sur l'architecture de Windows NT, supportant
les logiciels et pilotes existant pour Windows.
\newline
ReactOS n'est pas basé sur Linux (comme l'est le projet WINE), cependant WINE est également implémenté
dans ReactOS et constitue même la majorité du mode utilisateur du système. A l'heure actuelle, ReactOS est
destiné aux développeurs plutôt qu'à des utilisateurs finaux, mais à terme, le système devrait être utilisable
par n'importe qui, de la même façon qu'un Windows actuel. Leur but est de pouvoir faire un système binairement
compatible avec Windows, sans être forcé de suivre la démarche commercial de Microsoft, ainsi que leurs idées
(par exemple, ne pas forcer l'utilisation de Metro).
\subsubsection{Compatibilité}
« Change your OS, not your software! » est un des slogans de ReactOS. L'idéologie du groupe est qu'on ne
devrait pas changer de logiciels sous prétexte qu'on change de système d'exploitation. C'est pourquoi ils
veulent faire fonctionner les logiciels et pilotes Windows sur leur système libre.
\subsubsection{Sécurité}
Contrairement à ce qu'on pourrait penser, le noyau NT a une très bonne sécurité de par sa conception.
Il utilise des listes de contrôle d'accès évolués et est flexible. La mauvaise réputation qui a été apportée
avec Windows XP principalement est due au fait que pour garder une rétro-compatibilité et pour aider les gens
à migrer de Windows 9x vers Windows XP, il a fallu désactiver par défaut une grosse partie de la sécurité.
\newline
ReactOS a été prévu pour prendre directement l'aspect sécurisé de Windows NT.
\subsubsection{Légèreté}
ReactOS est conçu pour être léger et puissant. L'interface graphique se veut simple et légère comme celle
de Windows 95 tout en gardant l'efficacité et les nouvelles fonctionnalités d'un Windows récent.
\subsubsection{Libre}
Le code source de ReactOS est disponible sous licence GNU GPL et sous BSD. La possibilité de voir le code
source du noyau est facilement accessible via l'SVN et sa modification l'est aussi une fois inscrit sur
le site principal de ReactOS.
\subsubsection{Confiance}
Depuis 1996, ReactOS a été écrit « from scratch », c'est à dire depuis rien.
C'est une réimplémentation solide du noyau NT, prévu aussi bien pour l'embarqué que pour les
ordinateurs personnels et serveurs.
De plus, ReactOS prend en charge plusieurs méthodes d'implémentations basées sur d'autres familles d'OS tel
que UNIX, VMS et OS/2 (exemple, il y'a une implémentation de la couche IP qui prend en charge
les sockets BSD, ça veut dire qu'une application codée pour utiliser les sockets BSD peut potentiellement
tourner sous ReactOS).
\subsubsection{Orienté objet}
ReactOS n'est pas un système orienté objet au sens propre du terme, mais il utilise des objets pour la
représentation interne du système (comme Windows NT, ce concept sera expliqué plus loin lors de la
communication entre le driver et le kernel). ReactOS n'est pas « pour » l'idéologie UNIX qui dit que
« tout est fichier ». Bien que cela fasse la force de UNIX, c'est également le goulot d'étranglement
d'après les développeurs et l'idéologie du groupe ReactOS (je n'ai pas trouvé de source à ce propos
mais le code source ne montre en effet pas beaucoup d'allusion au fonctionnement de UNIX).
\newline
Pour ma part, je ne partage pas cet avis à propos du goulot sous UNIX.
\begin{figure}[!h]
\caption{Capture d'écran de ReactOS 0.3.16 LiveCD dans VirtualBox}
\centering
\includegraphics[scale=0.6]{reactos-livecd.eps}
\end{figure}
\subsection{Technique}
ReactOS respecte assez bien, d'un point de vue logique, le fonctionnement d'un noyau NT, mais l'implémentation
qui en est faite n'est pas toujours identique ou optimale. L'un des message important que les développeurs font
passer lors d'un développement dans le système est: faire les choses bien est un bon début, mais les résultats
sont plus importants que les performances. Le côté optimisation pourra être fait après par quelqu'un d'autre,
le but premier est d'avoir quelque chose de fonctionnel.
\newline
Une fois qu'on commence à regarder le code source, on comprend que c'est clairement ça. Le code est loin d'être
optimisé, mais il est fonctionnel. Une grosse partie n'est pas commentée et la documentation n'est pas forcément
facile à trouver non plus. Un « doxygen » (générateur de documentation C++) est disponible pour le code, mais
il n'est pas assez bien utilisé pour pouvoir s'en servir de référence pour le code.
\newline
\newpage
\section{Le réseau dans ReactOS}
La partie réseau de ReactOS n'est pas la partie du système la plus mise à jour. La couche TCP/IP
\footnote{Par abus de langage, on emploie le terme « stack TCP/IP » pour l'entièreté de la couche réseau
dans un système d'exploitation, mais en réalité dans le cadre de ce travail, seul la « stack IP » est mise
en cause, en effet nous n'irons pas plus haut que la couche 3 du modèle OSI. Nous verrons cela plus tard.}
a initialement été écrite en 2000 (en se référant aux dates de modifications en commentaires dans le code)
et n'a pas vraiment été mise à jour depuis. On se retrouve donc avec du vieux code mais fonctionnel. A noter
aussi que seul le strict minimum a été implémenté comme cela sera montré plus loin.
\newline
ReactOS utilise le même fonctionnement global que Windows XP. Le principal repose sur NDIS.
Ensuite, on retrouve une bibliothèque IP (couche 3) et une implémentation de lwIP (utilisée partiellement).
\subsection{NDIS (Network Driver Interface Specification)}
NDIS est une API pour les cartes réseaux écrite conjointement par Microsoft et 3Com et principalement
utilisée dans les systèmes Microsoft Windows. Le projet est propriétaire et fermé, nous n'avons donc pas
accès aux sources de l'implémentation, mais des versions opensource ont été faites par Reverse Engineering
et il est donc possible d'utiliser des drivers NDIS sous Linux, FreeBSD, etc.
\newline
NDIS dispose de trois grandes parties:
\begin{itemize}
\itemsep0em
\item Miniport Drivers: ces drivers implémentent le fonctionnement hardware (ou logique en cas
de cartes virtuelles) des cartes réseaux. C'est là que les interruptions matérielles, etc. sont gérées.
\item ProtocolDrivers: ces drivers peuvent se binder à un ou des Miniports et communiquer avec eux
de façon standardisée avec l'API.
\item Intermediate Drivers: ces drivers se situent entre les Miniports et les Protocols Drivers. Ils
permettent de filtrer le contenu qui y transite.
\end{itemize}
Typiquement, les drivers réseaux sont donc des Miniports, le driver TCP/IP est un ProtocolDriver et
les Intermediates Drivers font office de Firewall, de Load Balancing Failover ou encore de translation
entre des vieux transports et un driver Miniport qui ne le supporterait pas.
\newline
ReactOS utilise bien ce système de fonctionnement et implémente NDIS 5 (les versions 6 et supérieures ne sont
pas supportées). Le système dispose de 3 drivers par
défaut: ne2000, pcnet et rtl8193. Un grand nombre de drivers binaire faits pour Windows sont
fonctionnels dans ReactOS, une liste des drivers testés est disponible sur le Wiki
\footnote{https://www.reactos.org/wiki/Supported\_Hardware/Network\_cards}.
\begin{figure}[!h]
\caption{Couches NDIS}
\centering
\includegraphics[scale=0.15]{ndis-layers.eps}
\end{figure}
\newpage
\subsection{Implémentation de la couche 3 (tcpip.sys)}
Le driver \file{tcpip.sys} (dont les sources se trouvent dans \file{/drivers/network/tcpip/}) contient tout ce qui
est nécessaire à la réception d'un paquet NDIS et la gestion interne des différentes tables
(routing (RIB), forwarding (FIB), ARP, ...). Le driver TCP/IP à proprement parler est un ProtocolDriver NDIS.
\file{tcpip.sys} est linké (statiquement\footnote{Cette précision est importante pour la suite})
à une bibliothèque logicielle qui s'appelle « ip » (source \file{/lib/network/ip}).
Cette bibliothèque s'occupe de garder en mémoire toutes les informations de couche 3 pour le système
(listes des adresses ip, les interfaces, leur binding avec NDIS, etc.).
Des précisions l'implémentation de la \call{libip} sont à la section \ref{sub:libip} page \pageref{sub:libip}.
Pour faire communiquer le ProtocolDriver avec le user-mode plus haut, le système utilise « TDI » pour
faire le lien entre les deux. TDI est expliqué section \ref{sub:tdi} page \pageref{sub:tdi}.
\begin{figure}[!h]
\caption{Couches TDI - NDIS }
\centering
\includegraphics[scale=0.15]{ndis-tdi.eps}
\end{figure}
\newpage
\section{Les namespaces}
Dans le sens large du terme, un « namespace » (espace de nom) est l'isolation d'un élément ou d'un
groupe d'éléments, par un nom. On retrouve cette notion aussi bien dans la programmation qu'ailleurs,
comme ici, dans une isolation système.
\subsection{Virtualisation}
Si la virtualisation est très à la mode ces temps ci, c'est parce qu'elle apporte un réel gain de rentabilité
du matériel dont on dispose. En effet, la virtualisation permet de partager
les ressources d'une machine pour plusieurs utilisateurs, programmes, systèmes d'exploitation, ...
\newline
Cependant le terme « Virtualisation » est très vague. Il existe plusieurs techniques de
virtualisation:
\subsubsection{Virtualisation noyau user-space}
Ce procédé permet de lancer un noyau par dessus un noyau existant, sans
l'isoler. Chaque noyau a son propre espace utilisateur, comme une application
classique, la seule isolation est celle qui se trouve à l'intérieur de cet espace
utilisateur. Cette solution n'est vraiment utile que lors du développement
d'un noyau pour pouvoir le tester plus facilement (jusqu'à une certaine
limite, en effet vu comme ça, le noyau ne dispose pas d'un accès matériel comme
un vrai noyau l'aurait).
\begin{figure}[!h]
\caption{Diagramme d'une virtualisation en noyau user-mode}
\centering
\includegraphics{Diagramme_ArchiKernelUserSpace.eps}
\end{figure}
\subsubsection{Hyperviseur de type 2}
Un hyperviseur de type 2 est un logiciel qui tourne sur le système d'exploitation
hôte et permet de lancer un ou plusieurs systèmes d'exploitation invités.
Le logiciel virtualise (ou émule) le matériel pour les systèmes invités. Ces
systèmes pensent alors dialoguer directement avec le matériel alors qu'en
réalité il y a une couche entre les deux.
Les solutions les plus connues sont VMware Player/Workstation,
Oracle VirtualBox ou encore Parallels Desktop.
\newline
La différence entre l'émulation et la virtualisation est l'accessibilité
du système hôte. Un émulateur s'occupe de transférer les demandes du système
invité vers le système hôte. L'avantage est côté performances vu qu'il ne s'agit
que d'une translation de données, le problème est que le système invité doit
être compatible avec le matériel du système hôte.
Pour faire tourner des systèmes totalement différents et même non compatibles
avec le matériel hôte (exemple faire tourner un système embarqué ARM ou MIPS
sur du x86), il faut passer par un émulateur. Son job consiste, lui, à traduire
tout ce qui passe entre le système invité et hôte. L'impacte sur les performances
est très important au vu du nombre d'opérations qu'il faut faire. Un émulateur
libre très connu est \call{qemu}. Cet émulateur (qui fait aussi de la virtualisation)
a été utilisé dans ce projet car il propose des outils de débogage très avancés.
\begin{figure}[!h]
\caption{Diagramme de virtualisation avec un hyperviseur de type 2}
\centering
\includegraphics{Diagramme_ArchiEmulateur.eps}
\end{figure}
\subsubsection{Hyperviseur de type 1}
Un hyperviseur de type 1 est comme un noyau très léger et dédié pour faire
tourner des machines virtuelles dessus. L'avantage d'un système dédié est
qu'il est prévu et optimisé pour, la gestion des ressources est grandement
mieux gérée par ce système et permet des performances très élevées. Ce type
d'hyperviseur n'est du coup pas prévu pour tourner sur une machine end-user
mais bien dédié sur un serveur qui ne fait que ça. C'est actuellement la
solution la plus aboutie pour virtualiser entièrement un système d'exploitation
complet en dégradant le moins les performances.
\newline
Cependant, l'utilisation d'un noyau très léger a pour contrainte de n'être
compatible qu'avec un nombre restreint de matériel et ces solutions
sont généralement onéreuses. Heureusement, des solutions libres existent
comme Citrix Xen, KVM, ... Côté propriétaire, les plus connus sont
Microsoft Hyper-V, VMware ESX.
\begin{figure}[!h]
\caption{Diagramme de virtualisation avec un hyperviseur de type 1}
\centering
\includegraphics{Diagramme_ArchiHyperviseur.eps}
\end{figure}
\subsubsection{Virtualisation par Isolateur}
Un isolateur est un logiciel qui isole l'exécution d'un programme dans un
contexte bien précis. L'isolateur permet de ne montrer que ce qu'il veut
à l'application qui tourne derrière, c'est lui qui s'occupe de filtrer
le monde réel à l'application qu'il englobe. De ce fait on peut faire tourner
plusieurs instances d'une application sans que l'une n'interfère avec l'autre.
\newline
Niveau performance, cette technique est très rentable car l'overhead (le
temps perdu à se gérer soi-même) est très faible. Il n'y a qu'un seul noyau
qui tourne, qu'une seule gestion de la machine globale et des petites parties
qui s'occupent d'isoler.
\newline
Sous Linux, c'est effectué via les Namespaces Linux. Il dispose de plusieurs
formes et permet d'isoler différentes parties du système indépendamment. On
peut par exemple n'isoler que le réseau, que les processus, que les systèmes
de fichiers, etc.
\newline
Sous BSD, ce procédé s'appelle BSD Jail, sous Solaris, ce sont des « zones ».
A l'heure actuelle, sous Windows il n'y a qu'une solution propriétaire
développée par Parallels qui s'applique comme surcouche sur le système
d'exploitation.
\begin{figure}[!h]
\caption{Diagramme d'une virtualisation par Isolateur}
\centering
\includegraphics{Diagramme_ArchiIsolateur.eps}
\end{figure}
\subsection{ReactOS}
C'est cette dernière technique qui a été retenue pour mon implémentation dans ReactOS.
De la même façon que sous Linux, nous allons isoler la partie réseau du système pour permettre
de lancer plusieurs applications (identiques ou différentes) dans des contextes distincts.
\newline
\newline
\newline
\subsection{Les network namespaces}
Un « namespace réseau » est un contexte réseau complet isolé. Un contexte réseau complet prend réellement
toutes les notions de réseaux d'une stack TCP/IP générale: table de routage, table ARP, interfaces IP,
cartes réseaux, couche ethernet, etc.
\begin{figure}[!h]
\caption{Schéma de deux namespaces réseau dans un même système}
\centering
\includegraphics[scale=0.25]{network-namespaces.eps}
\end{figure}
En d'autres termes, avoir plusieurs namespaces réseau dans un même système consiste à instancier plusieurs
fois la stack TCP/IP sans aucun liens (au départ en tout cas, rien n'empêche la connexion des namespaces
entre eux, comme on verra plus loin) entre ces différentes instances.
\subsection{Alternatives à l'isolation réseau sous d'autres systèmes}
Sous Windows, la seule solution qui permette cette approche est la solution propriétaire
« Parallels Virtuozzo Containers », hélas il n'y a que très peu d'informations sur le fonctionnement de son
implémentation.
\newline
Sous Cisco, ce principe est appelé « VRF » (Virtual Routing and Forwarding), il consiste à avoir
plusieurs tables de routage différentes qui co-existent sur un même routeur.
\newline
Sous Solaris (depuis Solaris 10, 2005) on retrouve une isolation « Solaris Containers » qui inclut
ce qu'ils appellent des « Zones ». Chaque zone a son propre nom (node name), sa propre stack réseau
et son propre stockage.
\newline
Sous FreeBSD, la technique d'isolation se nomme « FreeBSD Jails », cependant cette isolation
ne crée pas une nouvelle stack réseau. Il n'y a qu'une protection (isolation) aux niveaux processus, fichier
et des sessions utilisateur/administrateur.
\newline
%% FIXME: network namespace Linux
Pour expliquer comment j'ai implémenté l'isolation de la stack réseau dans ReactOS, je vais expliquer
le fonctionnement interne de la stack et comment j'ai isolé les différentes parties qui en dépendent (processus, etc.).
Je vais commencer par les processus, puis les différentes couches OSI intéressantes (layer 3 puis layer 2)
pour enfin finir sur l'implémentation du lien entre les couches supérieures du système (le user-mode) et la stack
réseau.
\newpage
\section{Implémentation des namespaces réseau dans ReactOS}
L'un des points les plus importants est qu'il ne faut absolument pas casser la compatibilité des applications
déjà existantes avec le système, en y incluant les namespaces. Tout doit être transparent. Pour ce qui est du code,
pour rendre sa maintenance la plus facile possible, il est préférable de ne modifier qu'une petite partie du système
et d'y centraliser toutes les modifications plutôt que de faire des changements plic-ploc un peu partout dans le kernel.
\subsubsection{Gestion des Processus}
Sous Linux, la création d'un namespace se fait via un flag dans le syscall \call{clone()}. L'apparition
des différents namespaces sous Linux ont ajouté les flags suivant:
\begin{itemize}
\item \textbf{CLONE\_NEWNS}\newline
Isolation des points de montages
\item \textbf{CLONE\_NEWUTS}\newline
Isolation du hostname et domain name
\item \textbf{CLONE\_NEWIPC}\newline
Isolation des communications inter-processus
\item \textbf{CLONE\_NEWPID}\newline
Isolation des PID, cela a pour effet de pouvoir avoir plusieurs
processus avec le même PID et avoir différents \call{init} comme parent (PID 1)
\item \textbf{CLONE\_NEWNET}\newline
Isolation de la couche réseau (routes, ARP, etc.)
\item \textbf{CLONE\_NEWUSER}\newline
Isolation des user-id et group-id ce qui a pour effet de pouvoir séparer
les permissions et les privilèges entre le namespace et l'extérieur (par exemple être root
dans le namespace mais ne pas l'être dans l'environnement extérieur)
\newline
\end{itemize}
L'idée que j'ai retenue pour implémenter une isolation réseau sous ReactOS est de faire un procédé similaire.
Sous Windows il n'existe pas de syscall POSIX \call{clone()} (ce syscall est utilisé par \call{fork()}
et \call{pthread()}, par exemple). Pour lancer un nouveau processus, Windows dispose du syscall
\call{CreateProcess()}. J'ai donc ajouté un flag que l'on peut passer en paramètre à \call{CreateProcess()}.
Les flags existant sont (par exemple):
\begin{itemize}
\item \textbf{CREATE\_NEW\_CONSOLE}\newline
Le nouveau processus est ouvert dans une nouvelle console au lieu d'hériter de la
console parente (par défaut)
\item \textbf{CREATE\_SUSPENDED}\newline
Le thread principal est lancé en mode suspendu et ne sera actif que lors d'un
appel à \call{ResumeThread()}
\item \textbf{DEBUG\_PROCESS}\newline
Active le mode de debug d'un processus (et de ses fils), ce qui permet l'utilisation
de \call{WaitForDebugEvent()} entre autres
\item ...
\newline
\end{itemize}
Pour garder une cohérence entre Linux et Windows, j'ai fait un flag
\linebreak\call{PROCESS\_CREATE\_FLAGS\_NEWNET}.
Ce flag permet de lancer une application (console par exemple, mais n'importe quelle application
peut être exécutée par là. L'avantage de lancer une console est qu'on dispose
alors d'un « launcher » pour lancer d'autres application dans le même namespace). Les namespaces sont
automatiquement hérités de leurs parents, donc tout ce qui sera lancé depuis le namespace 1 (par exemple), à
moins de n'avoir le flag qui crée un nouveau namespace spécifié, sera également attaché au namespace 1.
\newline
Tant qu'il existe au moins un processus dans un namespace, celui-ci est considéré comme actif. Une fois
qu'il ne reste plus de processus attaché à un namespace, ce dernier est détruit et son ID sera le prochain
utilisé.
\newline
%
% process.c - kill.c
%
\subsubsection{Implémentation du gestionnaire de namespaces réseau}
Pour ce qui est de l'implémentation, la création d'un namespace se fait dans
\linebreak\file{/ntoskrnl/ps/process.c}
dans la fonction \call{PspCreateProcess()}. J'ai également créé un nouveau fichier dans
\file{/ntoskrnl/netns/} qui se nomme \file{networknamespace.c} pour gérer les appels
de drivers et la communication avec le kernel.
Dans \file{/ntoskrnl/ps/kill.c} se trouve le code qui gère la fin d'un processus. Quand un processus est fermé (ou tué),
le gestionnaire de namespace est appelé pour vérifier si il existe encore un processus dans le namespace dans
lequel se trouvait le processus, si ce n'est pas le cas, le namespace est supprimé et ce qui y était lié est déchargé.
\newline
Pour stocker (faire le lien) entre un namespace et un processus, une bonne solution serait d'implémenter
un système permettant d'isoler les processus entre eux, mais cette solution consiste plus à implémenter
l'équivalent d'un \call{CLONE\_NEWPID}, ce qui n'est pas le but recherché. La méthode la plus simple que j'ai
trouvée a été d'attribuer un ID (champ \call{NetNs}) dans la table principale qui gère les processus, à savoir
la table globale de structure \call{EPROCESS}. Cette table est la table utilisée par le système pour stocker
le PID et tout le contexte d'utilisation d'un processus, ça me semble un bon endroit pour y stocker le namespace
qui lui est lié, le seul souci qu'il peut y avoir avec cette solution est que la taille de la structure
\call{EPROCESS} est modifiée. Cette table est utilisée entre autres par \textit{WinDBG} (le débuggeur Microsoft)
et certains développeurs de ReactOS m'ont averti sur le channel IRC de \#reactos que ça \textbf{pourrait} casser
le fonctionnement de \textit{WinDBG}. Je n'ai remarqué aucun problème avec celui-ci durant mon travail.
\newline
Dans mon implémentation, seul le namespace réseau est stocké en dur dans la table, pour étendre cette feature,
remplacer ce champ par une structure pour y stocker l'ID des différents namespaces peut être une solution
intéressante.
\newline
Pour ne pas devoir accéder à cette table depuis un driver/le kernel (tout d'abord parce que cette structure est
opaque d'après la MSDN, et puis surtout par propreté et logique d'utilisation), j'ai fait une fonction qui permet
de récupérer le numéro du namespace réseau courant via \call{IoGetCurrentProcessNs()}. Cet appel est utilisé un peu
partout dans la lib ip et dans le protocol driver tcpip pour prendre une décision sur quelle table (routage, ARP, ...)
utiliser.
\newline
La communication entre le kernel et la stack TCP/IP se fait via l'objet
\linebreak\call{\textbackslash{}Device\textbackslash{}NamespaceNetwork}.
Cet objet est initialisé dans le driver \file{tcpip.sys}
ce qui est loin d'être la meilleure solution, en effet cela implique que le gestionnaire de namespaces réseau
doit attendre que \file{tcpip.sys} soit chargé pour pouvoir dispatcher les opérations du kernel. Dans le cas présent,
c'était le moyen le plus facile de faire communiquer \file{tcpip.sys} et le kernel sans rien modifier d'autre dans
le kernel.
\newline
Pour ne pas devoir créer un syscall par opération, la méthode la plus évidente est de faire un syscall
qui prend une structure en paramètre et une opération tirée d'une liste. Côté kernel, la
liste des opérations (opcode) que j'ai implémentées se trouve dans l'enum suivant:
\begin{listing}[H]
\caption{Extrait de /drivers/network/tcpip/include/dispatch.h}
\begin{ccode}
enum NETNSOPCODE {
KCREATE_NAMESPACE,
KREMOVE_NAMESPACE,
KATTACH_INTERFACE,
KDETACH_INTERFACE,
KSET_MACADDRESS,
KENABLE_SWITCH,
KDISABLE_SWITCH,
};
\end{ccode}
\end{listing}
Cette liste contient le minimum nécessaire pour créer et supprimer un namespace, mais en plus elle permet de
gérer les « virtuals ethernet » (et les interfaces physiques également en réalité) car pour l'instant il n'y avait
aucune commande implémentée pour attribuer une adresse IP ou MAC à une interface depuis la ligne de commande.
Actuellement l'enum est copié/collé à plusieurs endroits dans le code car les dépendances d'includes sont assez
tordues pour pouvoir définir tout comme il faut partout. Il faut faire attention lors de la modification de l'enum
de modifier les fichiers:
\begin{itemize}
\item \file{/drivers/network/tcpip/include/dispatch.h}
\item \file{/include/ndk/netns.h}
\item \file{/include/psdk/netns.h}
\item \file{/ntoskrnl/include/internal/networknamespace.h}
\newline
\end{itemize}
Cet enum contient également la gestion du switch virtuel que j'ai implémenté pour gérer la couche 2 du modèle OSI
(le switch est expliqué section \ref{sub:switch} page \pageref{sub:switch}).
\newline
Le syscall kernel (qui se trouve dans namespace.c) est \call{NtSetNetworkNamespace()}. Il prend en paramètre une
structure \call{NETNSOP} qui contient un ID de namespace, un opcode (l'enum plus haut), une valeur (un \call{int}
utilisé différemment en fonction de l'opcode) et une adresse MAC (qui n'est utilisée qu'avec l'opcode KSET\_MACADDRESS).
Toutes les options sont préfixées par « K » pour différencier facilement la partie kernel-mode
de la partie user-mode.
\begin{figure}[!h]
\caption{Diagramme de création d'un namespace en kernel-space}
\centering
\includegraphics[scale=0.6]{namespace-processing.eps}
\end{figure}
\newpage
(Notez que la partie « Create new TDI Entity List » sera expliquée plus loin).
Pour ce qui est de la partie usermode, les opcode sont définis:
\begin{listing}[H]
\caption{Extrait de /include/psdk/winbase.h}
\begin{ccode}
enum UNETNSOPCODE {
ATTACH_INTERFACE,
DETACH_INTERFACE,
SET_MACADDRESS,
ENABLE_SWITCH,
DISABLE_SWITCH,
};
\end{ccode}
\end{listing}
Comme on peut le voir, on ne peut pas créer de namespace via cet appel, il n'y a que via le \call{CreateProcess()}
qu'on peut créer un nouveau namespace dans l'implémentation actuelle. Ces opcodes sont surtout faits pour interagir
avec les namespaces existants. Les structures et les fonctions ont été crées,
maintenant il reste à faire le lien entre le kernel et le usermode.
%
% SYSCALL
%
\subsubsection{Ajout et linkage d'un syscall dans le kernel}
Lors de la compilation, pour appeler le kernel depuis le usermode, il faut pouvoir compiler et linker
l'exécutable correctement. Pour ça, il faut qu'à la compilation, le compilateur sache où se trouve
l'appel système, comment l'appeler, etc.
\newline
Pour respecter la programmation Windows classique, pour faire un appel du user-mode au kernel-mode, il faut
passer par la Win32 API. La Win32 API se trouve (en partie) dans \file{kernel32.dll} qui lui même appelle
des fonctions de la Native API \file{ntdll.dll}.
\newline
\file{ntdll.dll} est la partie du système qui exporte les appels publics du kernel Windows (\file{ntoskrnl.exe}),
appelés des « Native API ». Le code linké avec \call{ntdll.dll} est appelé « Native Application » et ne dépend
pas de Win32 (ce code peut donc être appelé au début du chargement du système, avant que l'API utilisateur classique
(Win32) soit utilisable, comme par exemple, autochk.exe qui s'occupe de lancer CheckDisk au démarrage).
Kernel32.dll est donc une « Native Application » qui exporte au reste du système des appels
de plus haut niveau (gestion de threads, mémoire, etc.).
\begin{figure}[!h]
\caption{Diagramme de la couche d'appel du kernel à la Win32 API}
\centering
\includegraphics[scale=0.2]{kernel32.eps}
\end{figure}
Concrètement, dans ReactOS, \file{ntdll.dll} et \file{kernel32.dll} ont dans leurs dossiers sources, un
fichier \file{kernel32.spec} et \file{ntdll.spec} qui contient la liste des fonctions à exporter ainsi
que la taille et le nombre de paramètres que la fonction prend.
\begin{listing}[H]
\caption{Code ajouté à ntdll.spec, ntoskrnl.spec et kernel32.spec}
\begin{textcode}
@ stdcall NtSetNetworkNamespace(ptr) ; ntdll.spec
@ stdcall IoGetCurrentProcessNs() ; ntoskrnl.spec
@ stdcall SetNetworkNamespace(ptr) ; kernel32.spec
\end{textcode}
\end{listing}
Il ne reste plus qu'à ajouter les prototypes dans un \file{.h} global (comme \file{winbase.h} par exemple), et
le tour est joué. Maintenant que le syscall est fait et exporté, il reste à l'utiliser.
Tout ces appels ont été utilisés en user-mode avec l'utilitaire en ligne de commande « \file{unshare.exe} »
que j'ai spécialement développé dans le cadre de ce travail.
%
% unshare.exe usermode
%
\subsubsection{Unshare}
Sous Linux, l'application « unshare » permet de lancer une application dans un nouveau namespace (au choix).
Pour l'exemple ci-dessous, on va lancer un \call{bash} dans un nouveau namespace réseau et on va voir que la
liste des interfaces disponibles sur le système ne sera pas la même qu'avant.
\begin{listing}[H]
\caption{Utilisation de « unshare » sous Linux}
\begin{bashcode}
# unshare --help
-m, --mount unshare mounts namespace
-u, --uts unshare UTS namespace (hostname etc)
-i, --ipc unshare System V IPC namespace
-n, --net unshare network namespace
-p, --pid unshare pid namespace
-U, --user unshare user namespace
-f, --fork fork before launching <program>
--mount-proc[=<dir>] mount proc filesystem first (implies --mount)
# ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode [...]
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP [...]
link/ether 8c:89:a5:c8:8a:84 brd ff:ff:ff:ff:ff:ff
\end{bashcode}
\end{listing}
\begin{listing}[H]
\caption{Nouveau namespace avec « unshare » sous Linux}
\begin{bashcode}
# unshare --net bash
# ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
\end{bashcode}
\end{listing}
Le nouveau namespace réseau est volatile (le namespace est détruit une fois que bash est quitté).
J'ai voulu reprendre cette idée de pouvoir créer un namespace via une commande \call{unshare},
mais en plus de ça, cette commande est devenue la commande principale
de gestion des namespaces, des ip, des adresses mac, etc.
Par facilité, l'implémentation de \file{unshare.exe} est clairement un « fourre-tout », l'idée est de supporter
pleinement l'aspect kernel de l'isolation, le nom du programme qui va changer l'adresse IP ou l'adresse MAC de
l'interface a peu d'importance vis-à-vis de l'implémentation dans le système. Pour plus de précisions sur les
utilitaires qui devraient être utilisés, voir section \ref{next:utils} page \pageref{next:utils}.
\newline
Pour ce qui est de la partie volatile, j'ai gardé ce système pour le portage. Cela facilite le développement sans pour
autant enlever des fonctionnalités. Faire du code persistant nécessiterait de sauver les ID des namespaces et les
interfaces qui y sont attachées. Pour garder une persistance, à l'heure actuelle, il faut créer un script
de démarrage qui lance à la manière d'un batch, toutes les commandes
\file{unshare.exe} nécessaires, un peu à la façon d'\call{iptables} sous Linux.
\newline
Le code d'\file{unshare.exe} se trouve dans \file{/base/applications/network/unshare/}
et sa syntaxe est relativement simple, on la verra au cours du développement qui suit
\footnote{Un résumé de la syntaxe se trouve en annexe, page \pageref{a:unshare}}.
Maintenant que les processus peuvent utiliser différents namespaces, voyons comment nous allons exploiter ça dans
le driver \file{tcpip.sys} et comment nous allons trainer les I/O réseaux pour avoir un fonctionnement cohérent et isolé.
\subsection{Implémentation des namespaces dans la stack réseau (tcpip.sys)}
Le fichier \file{tcpip.sys} s'occupe entièrement du traitement réseau dans le système. Même si le résultat
final est un gros fichier binaire qui contient tout, le code, lui, est bien segmenté suivant les différents traitements
(couche OSI 2, 3 et 4).
\newline
Le principe à suivre est simple. Côté usermode, on demande quelque chose au kernel (à un driver) et le système nous
donne une réponse. Cette réponse est généralement une liste d'éléments (liste d'interfaces, routes, adresses, ...).
Il « suffit » donc de trouver où cette liste est générée et d'y ajouter un matching sur le namespace en plus que
le traitement qui est actuellement fait. L'idée est là. Pour ce faire, on va analyser le code qui s'occupe du layer 3.
%
% /lib/drivers/ip/
%
\label{sub:libip}
\subsubsection{Bibliothèque IP}
Comme dit plus haut, la partie TCP/IP du système utilise 2 grandes parties, le code de \file{tcpip.sys} et la bibliothèque
ip qui est statiquement linkée.
\newline
La lib ip qui se trouve dans \file{/lib/drivers/ip/} est structurée ainsi (je n'ai gardé que les fichiers qui ont été utilisés
dans ce travail):
\begin{itemize}
\item \file{/lib/drivers/ip/network}:
\begin{itemize}
\item \textbf{\file{address.c}}:
permet de stocker une adresse ip (ipv4 et ipv6) et de manipuler,
tester et les comparer. Il y a également des fonctions de debug intéressantes dedans
dont une qui permet de formater une adresse pour pouvoir l'afficher en tant que string.
\item \textbf{\file{arp.c}}:
son nom peut être trompeur, il s'agit du handler de paquets ARP et non
de la gestion de la table ARP du système. Les fonctions contenues dans ce fichier s'occupent
soit de forger une requête ARP et de l'envoyer sur le réseau, soit de lire un paquet ARP
et d'en analyser le contenu et traiter (ou pas) la requête.
\item \textbf{\file{icmp.c}}:
son nom l'indique clairement, ce fichier contient les fonctions pour envoyer
ou recevoir un paquet ICMP.
\item \textbf{\file{interface.c}}:
permet de manipuler l'état et des informations sur une interface ip.
\item \textbf{\file{ip.c}}:
la partie principale de la bibliothèque, ce fichier contient ce qu'il faut pour
initialiser la bibliothèque, enregistrer des protocoles, des interfaces et communiquer avec
\file{tcpip.sys}
\item \textbf{\file{loopback.c}}:
tout le code relatif aux interfaces de loopback. L'idée que j'avais du loopback sous Windows
était qu'une interface NDIS (comme tout le reste du système) s'occupait de gérer ça, mais en fait
non, c'est une interface virtuelle de niveau 3 qui s'occupe de tout gérer (en tout cas dans ReactOS).
\item \textbf{\file{neighbor.c}}:
la gestion des voisins (les adresses MAC des voisins, donc la table ARP en définitive).
\item \textbf{\file{receive.c}}:
dispatcheur de paquets reçus.
\item \textbf{\file{router.c}}:
s'occupe de la gestion (ajout, suppression, recherche) de route dans la table de routage.
\newline
\end{itemize}
\item \file{/lib/drivers/ip/transport}:\newline
contient des sous-dossiers (tcp, udp, rawip) contenant la gestion des différents protocoles de couche 4. Toute l'isolation
étant faite après la couche 3, rien de cette partie n'a été modifié, il n'y avait aucune raison d'y toucher.
\newline
\end{itemize}
Bien que ça soit une bibliothèque (en tout cas d'après le path du code), une chose étonnante est que tout les includes se trouvent dans
le dossier de tcpip (dans \file{/drivers/}) et non dans la lib avec le reste. On voit donc très clairement qu'il y a une dépendance mutuelle
entre les deux parties.
%
% IP Address struct
%
\subsubsection{Adresses}
Les adresses IP sont stockées sous forme d'une structure pouvant aussi bien accepter une IPv4 qu'une IPv6.
\begin{listing}[H]
\caption{struct IP\_ADDRESS}
\begin{ccode}
typedef struct IP_ADDRESS {
UCHAR Type;
union {
IPv4_RAW_ADDRESS IPv4Address;
IPv6_RAW_ADDRESS IPv6Address;
} Address;
} IP_ADDRESS, *PIP_ADDRESS;
\end{ccode}
\end{listing}
Le Type défini si il s'agit d'une IPv4 ou une IPv6 (\hexa{0x04} ou \hexa{0x06}, défini par les deux defines \call{IP\_ADDRESS\_V4}
et \call{IP\_ADDRESS\_V6}) puis l'union contient l'adresse en question.
%
% IP Interface struct
%
\subsubsection{Interfaces}
Les adresses IP ne sont pas attribuées à des interfaces NDIS ou des interfaces layer 2 directement. La bibliothèque contient
ses propres interfaces (liées à l'interface de la couche inférieure par un pointeur) et crée donc une indépendance à ses couches inférieures.
Une interface IP est définie comme telle (seul les champs intéressants pour ce travail ont été gardés):
\begin{listing}[H]
\caption{struct IP\_INTERFACE}
\begin{ccode}
typedef struct _IP_INTERFACE {
// [...]
LIST_ENTRY ListEntry;
PVOID Context;
IP_ADDRESS Unicast;
IP_ADDRESS Netmask;
IP_ADDRESS Broadcast;
UNICODE_STRING Name;
UINT Index;
LL_TRANSMIT_ROUTINE Transmit;
// [...]
} IP_INTERFACE, *PIP_INTERFACE;
\end{ccode}
\end{listing}
Les noms de variables sont globalement clairs, mais quelques précisions:
\begin{itemize}
\item \textbf{ListEntry}: champ utilisé par les listes chainées implémentées dans le code
\item \textbf{Context}: un pointeur void vers quelque chose d'intéressant de niveau inférieur.
Dans notre cas, ce pointeur sera le lien vers l'interface layer 2 à laquelle l'interface IP
est attachée.
\item \textbf{Index}: numéro unique d'identification de l'interface. C'est cet index qu'on retrouvera comme
numéro d'interface lors d'un route print par exemple. Pour communiquer avec le usermode, c'est cet index
qui sera utilisé pour spécifier une interface, c'est plus facile que de matcher des noms.
\item \textbf{Transmit}: pointeur de fonction vers un handler qui va dispatcher un paquet pour l'envoyer
vers la couche inférieure.
\newline
\end{itemize}
Cette structure a été adaptée en fonction des différents besoins qui sont expliqués dans ce chapitre.
%
% IP Library Main
%
\subsubsection{IP}
Le point d'entrée et le coeur de la bibliothèque IP. Bien que cette partie n'ait presque pas été modifiée, il est important de
comprendre son fonctionnement pour savoir comment tout est lié ensemble.
C'est dans la routine d'initialisation du driver TCPIP que l'initialisation de la bibliothèque est appelée. Elle met en place
ce qu'il faut pour gérer les listes d'interface et prépare l'enregistrement des protocoles.
\newline
Les protocoles ICMP, TCP et UDP s'enregistrent comme handlers et lors de la réception d'un paquet, une itération sur les handlers
s'effectue pour savoir à qui envoyer le paquet pour le traiter. De cette façon il est très facile d'implémenter un nouveau protocol
car il s'agit de créer un nouveau traitement et l'enregistrer comme les autres sans modifier le code déjà existant.
L'avantage pour moi, c'est qu'il suffit d'adapter le coeur pour que tout ce qui s'y attache puisse
bénéficier de l'isolation de manière transparente, c'est pour ça que la couche 4 n'a pas à être modifiée.
\newline
C'est également dans cette partie de code qu'on ajoute une interface IP. Pour ce faire, il suffit de remplir une structure \call{PLLIP\_BIND\_INFO}
et de la passer à la fonction \call{IPCreateInterface} qui va s'occuper d'ajouter une nouvelle interface à la liste globale. La structure
\call{PLLIP\_BIND\_INFO} est en fait une \call{IP\_INTERFACE} très simplifiée qui ne contient que le minimum pour la lier au reste du système:
le contexte de la couche inférieure, la taille du header de la couche inférieure (pour savoir ce qu'il faut ignorer lors de la réception d'un paquet) et
un pointeur vers une fonction qui permet de préparer un paquet à envoyer.
%
% Loopback Interface
%
\subsubsection{Loopback}
La partie loopback a été un point maître dans l'implémentation car c'est la façon la plus simple de pouvoir tester l'isolation d'interface.
Sous Linux, quand on crée un nouveau namespace, automatiquement une interface de loopback (à l'état down) est ajoutée à ce namespace. Linux
permet une utilisation du réseau sans interface de loopback ce qui n'est pas le cas sous Windows. En effet, pour ce qui est de ReactOS, toutes
les manipulations de recherche d'interface, etc., partent du principe qu'il y a \textbf{TOUJOURS} une interface existante, la loopback. Cela
simplifie le code et le principe, étant donné qu'il ne faut pas prendre en compte le cas où aucune interface n'existe. Le revers de la médaille est
qu'on ne donne pas un accès complet à la couche réseau à l'utilisateur. Il est (sans hack du kernel) impossible de changer l'adresse ip de l'interface
de loopback ni même de désactiver l'interface.
\newline
En partant de ce principe, leur idée (que je trouve justifiée) a été de mettre une variable globale dans la bibliothèque qui contient les informations
à propos de la loopback (un pointeur vers sa structure \call{IP\_INTERFACE} en d'autres termes). C'est là que ça se corse. L'implémentation n'est
\textbf{ABSOLUMENT} pas prévue pour supporter plusieurs interfaces de loopback. Pour avoir une isolation correcte des namespaces, il est normal
d'avoir des loopbacks indépendantes qui peuvent toutes avoir la même adresse IP (typiquement 127.0.0.1/8). Il a donc fallu trouver un moyen
de gérer plusieurs loopbacks (si possible sans réécrire tout ce qui existe déjà...)
\newline
La méthode qui me semble la plus logique est de commencer par remplacer la variable globale par une liste de loopbacks et non par une loopback unique.
En utilisant le système de liste chainée du système, pas besoin de faire grand chose:
\newline
\begin{listing}[H]
\caption{struct LOOPLIST}
\begin{ccode}
typedef struct _LOOPLIST {
LIST_ENTRY ListEntry;
PIP_INTERFACE Loopback;
} LOOPLIST, *PLOOPLIST;
\end{ccode}
\end{listing}
Cette méthode sert à isoler la liste des loopbacks indépendamment du reste du code, mais plus loin, on se rend compte que pour savoir si une interface
précise est une loopback, ils comparent l'adresse de la variable globale à l'interface. Cette méthode (un peu trop naïve) est adaptable en remplacant
la comparaison par un appel de fonction qui itère la liste des loopbacks pour savoir si elle est présente dans la liste ou pas. Cette méthode n'est pas
idéale quand on peut faire beaucoup plus simple: ajouter un flag à la structure \call{IP\_INTERFACE} pour savoir si c'est une loopback ou pas.
De cette façon, on sait directement si une interface précise est une loopback. C'est beaucoup plus facile et efficace qu'une liste. A noter que
j'ai quand même laissé la liste des loopbacks dans le code, ça laisse une abstraction de dépendances (liste de loopbacks, liste d'interfaces).
\newline
Lors de la création d'une loopback, elle s'identifie comme étant dans le namespace 0 par défaut. C'est au code qui a appelé la création de la loopback
de changer son namespace pour la mettre dans le bon lui-même.
\subsubsection{Voisinage}
Le voisinage (la table ARP en d'autres termes) est une table temporaire (les champs qui y sont stockés, y sont en
cache avec un timeout, une fois le timeout dépassé, l'entrée est supprimée) qui fait la correspondance entre
un adresse IP et une adresse MAC (qui a été résolue par le protocol ARP). Cette table est nécessaire pour
envoyer une trame ethernet vu qu'on doit placer l'adresse MAC de destination dans le header ethernet.
\newline
La table de voisinage est faite comme suit:
\begin{listing}[H]
\caption{struct NEIGHBOR\_CACHE\_ENTRY}
\begin{ccode}
typedef struct NEIGHBOR_CACHE_ENTRY {
// [...]
UCHAR State;
UINT EventTimer;
UINT EventCount;
PIP_INTERFACE Interface;
PVOID LinkAddress;
IP_ADDRESS Address;
// [...]
} NEIGHBOR_CACHE_ENTRY, *PNEIGHBOR_CACHE_ENTRY;
\end{ccode}
\end{listing}
\begin{itemize}
\item \textbf{State}: état de la résolution (reachable, stale, delay, incomplete, failed, ...)
\footnote{Voir le fonctionnement du protocole ARP, bon exemple: http://linux-ip.net/html/ether-ARP.html}
\item \textbf{EventTimer}: durée (ticks) depuis le dernier événement
\item \textbf{EventCount}: nombre d'événements
\item \textbf{Interface}: interface depuis laquelle l'entrée ARP a été ajoutée
(donc l'interface par laquelle il faut sortir un paquet vers cette destination)
\item \textbf{LinkAddress}: pointeur vers l'adresse de layer 2 (dans notre cas, une adresse MAC)
\item \textbf{Address}: adresse IP de destination
\newline
\end{itemize}
Pour avoir une idée de comment c'est représenté en mémoire, voilà un diagramme des éléments accessibles depuis
une entrée dans la table ARP du système:
\begin{figure}[!h]
\caption{Diagramme de relation avec une entrée dans la table ARP}
\centering
\includegraphics[scale=0.3]{neighbor-diagram.eps}
\end{figure}
\subsubsection{Routeur}
\label{sub:router}
Pour ce qui est du routage, le système garde une liste de destination (la FIB pour Forwarding Information Base). Cette table contient:
\begin{listing}[H]
\caption{struct FIB\_ENTRY}
\begin{ccode}
typedef struct _FIB_ENTRY {
LIST_ENTRY ListEntry;
// [...]
IP_ADDRESS NetworkAddress;
IP_ADDRESS Netmask;
PNEIGHBOR_CACHE_ENTRY Router;
UINT Metric;
} FIB_ENTRY, *PFIB_ENTRY;
\end{ccode}
\end{listing}
On y a toutes les informations nécessaires: l'adresse de destination avec son masque, l'entrée dans la cache ARP pour y accéder et son metric.
L'entrée dans la table ARP contient un pointeur vers l'interface de sortie. Une fois qu'on a accès à l'interface on a donc accès à son namespace.
Tout ce qu'il nous faut donc. Il reste à modifier le code pour qu'une recherche de route tienne compte du namespace en cours (ou un namespace voulu).
\newline
\label{sub:routerpid}
La fonction \call{RouterGetRoute()} s'occupe de rechercher dans la table de routage, une route vers une destination donnée. Hélas, on ne peut pas juste
s'occuper de comparer les namespaces puis renvoyer ce qu'on a trouvé. Durant le développement, j'ai découvert que lors de l'émission d'un paquet, tout se passe
bien (le namespace étant donné par le processus en cours, ça correspond), mais lors de la réception d'un paquet, le processus en cours d'exécution est \call{SYSTEM}
(pid 4 et de même sous Windows). Du coup, tous les processus de base étant par défaut dans le namespace 0, la réception d'un paquet ne se routait pas correctement
et le paquet se retrouvait drop. Il a donc fallu vérifier si c'est le procesus \call{SYSTEM} qui fait la demande ou pas. Si oui, le système a une vision
globale de la table et non pas uniquement son namespace.
\newline
Pour comparer le processus \call{SYSTEM}, j'ai hard-codé le numéro du PID \hexa{0x04}, ce n'est pas idéale mais je ne sais pas trop comment faire ça de façon plus générique.
J'ai vu dans la doc MSDN qu'il y avait une routine qui s'appelle \call{PsIsSystemThread()} qui pourrait résoudre le problème, mais je n'ai pas eu l'occasion de la tester.
\newline
Lors de la recherche d'une route, il y a un cas particulier qui est traité, c'est le cas où l'adresse de destination
se trouve sur une interface locale. Lors d'une recherche de route vers une destination, une première étape est
de voir si il existe une interface qui contient la destination, si oui on utilise la cache ARP sinon on regarde dans
la table de routage classique.
\begin{figure}[!h]
\caption{Diagramme de recherche d'une route}
\centering
\includegraphics[scale=0.6]{routing-diagram.eps}
\end{figure}
Lors de la recherche OnLink, j'ai dû adapter un peu la recherche en faisant une nouvelle fonction qui prend, en plus,
comme argument l'interface source de la requête. En fait, lors d'une communication inter-namespace (qui sera expliquée
plus en détail dans la partie Switching (section \ref{sub:switch} page \pageref{sub:switch})), la requête source
se fait depuis le PID \hexa{0x04} aussi, alors que la source ne vient pas du monde extérieur mais bien du système.
Il est donc possible de connaître la source et donc de pouvoir préciser la recherche à faire. En effet, sinon
c'est sur toutes les routes et interfaces que la recherche se fait et ça pose problème en cas d'overlapping: quelle
interface choisir vu qu'elles ont la même ip ? En se basant sur l'interface source, il est possible de faire
correspondre les namespaces en sachant lesquels sont inter-connectés (via le contexte Layer 2, expliqué
également plus loin).
\newline
La couche 3 (le fonctionnement et l'utilisation du protocol IP) est maintenant adaptée pour pouvoir gérer
du trafic depuis des namespaces isolés.
\newline
Concrêtement, on a modifié le fonctionnement de la table de routage et adapté les interfaces de loopback.
\newline
Lors du développement, après être arrivé à faire de l'isolation de ping
entre 2 loopbacks dans 2 namespaces différents (les loopbacks n'ont pas d'adresse MAC, il s'agit donc bien de
couche 3 uniquement), une grande étape a été franchie. Cependant, isoler en couche 3 n'est pas suffisant, en effet
pour l'instant, on est capable d'avoir du routage IP isolé, mais le broadcast ethernet ne l'est pas du tout et
le protocole ARP (par exemple) utilise du broadcast ethernet pour pouvoir résoudre les adresses MAC en fonction
des adresses IP. Sans isolation de couche 2, cette communication ne peut se faire correctement.
\newline
Voyons voir comment traiter la couche 2.
%
% lan.c
%
\subsection{Implémentation layer 2 et connexion au layer 3}
Pour poursuivre le processus d'isolation, il faut pouvoir différencier les différents LAN (des Virtuals LAN)
qui sont utilisés. Un namespace correspond à un LAN (avec un domaine de broadcast qui lui est propre). Pour
pouvoir isoler un LAN, il faut pouvoir isoler et dispatcher une trame ethernet vers le bon endroit
(un « VLAN 1 » ne doit pas recevoir des trames de broadcast du \linebreak« VLAN 2 » par exemple).
\newline
Pour y parvenir, on va attaquer une partie plus bas niveau au niveau driver, car on ne va plus travailler uniquement
sur une couche software type bibliothèque de gestion de paquets, mais bien d'un driver qui va communiquer avec
des cartes réseaux, des adresses MAC, etc. (bien entendu, la partie hardware n'entre pas en jeu, tout a été écrit
pour ne pas nécessiter de modification extérieure... si il avait fallu réécrire les drivers des cartes réseaux pour
implémenter les namespaces, l'intérêt serait bien moindre).
\newline
Comme expliqué lors du début de cet ouvrage, Windows et ReactOS utilisent NDIS pour communiquer avec le matériel
réseau. Les drivers Miniport NDIS s'occupent de la partie hardware et font le lien avec la partie NDIS
ProtocolDriver (dans notre cas, TCP/IP).
\newline
Le code du ProtocolDriver TCP/IP se trouve dans le fichier \file{/drivers/network/lan/lan/lan.c} et son point
d'entrée est la fonction \call{LANRegisterProtocol()}. Pendant le chargement du driver \file{tcpip.sys},
cette fonction est appelée et le driver se lie (bind) avec NDIS.
Travailler avec NDIS, globalement, c'est appeler des \call{XxxxRegister}
avec comme paramètre une structure de données qui ne contient que des pointeurs de fonctions avec des prototypes
bien précis, qui sont décrits dans une documentation officielle\footnote{http://www.ndis.com/}.
C'est le cas que ça soit du Miniport ou des ProtocolDriver.
Une fois le ProtocolDriver correctement attaché, NDIS va appeler le callback
\call{LANRegisterAdapter()} pour chaque interface (Miniport) existante (si une nouvelle interface
Plug'n'Play vient à arriver pendant l'exécution du système, cette fonction sera également appelée
avec la nouvelle interface).
\newline
Pour représenter une interface layer 2, au même titre que pour la partie IP, une structure propre aux interfaces
layer 2 est utilisée, nommée \call{LAN\_ADAPTER}:
\begin{listing}[H]
\caption{struct LAN\_ADAPTER}
\begin{ccode}
typedef struct LAN_ADAPTER {
LIST_ENTRY ListEntry;
// [...]
PVOID Context;
NDIS_HANDLE NdisHandle;
NDIS_STATUS NdisStatus;
NDIS_MEDIUM Media;
UCHAR HWAddress[IEEE_802_ADDR_LENGTH];
// [...]
UCHAR HeaderSize;
USHORT MTU;
UINT Speed;
UINT PacketFilter;
// [...]
} LAN_ADAPTER *PLAN_ADAPTER;
\end{ccode}
\end{listing}
Encore une fois, seuls les champs qui sont utilisés dans le cadre de ce développement ont été notés.
Pour ce qui est des précisions:
\begin{itemize}
\item \textbf{Context}: pointeur vers la structure \call{IP\_INTERFACE} qui lui est attachée
(voir section \ref{fig:iplanlink} page \pageref{fig:iplanlink} pour avoir un diagramme
plus clair).
\item \textbf{NdisHandle}: un identificateur unique utilisé par NDIS pour savoir où dispatcher
les demandes à propos de l'interface
\item \textbf{Media}: contient un ID sur le type d'interface dont il s'agit. Seul le média ethernet (802.3)
est supporté dans ReactOS.
\item \textbf{HWAddress}: sans doute le champ le plus important et le plus intéressant: l'adresse MAC de
l'interface
\item \textbf{PacketFilter}: flags NDIS pour filtrer les paquets qu'on va recevoir.
\newline
\end{itemize}
\begin{figure}[!h]
\caption{Diagramme de lien entre LAN\_ADAPTER et NDIS}
\centering
\includegraphics[scale=0.3]{lan_adapter-ndis.eps}
\end{figure}
Petite parenthèse: étant donné que plus loin on va devoir recevoir des trames dont l'adresse
MAC sera l'adresse d'une interface virtuelle, il va falloir activer le mode promiscuous de la carte réseau pour
ne pas que la carte (de façon hardware) drop les trames qui ne lui sont pas directement adressées. Pour cela, il
suffit d'ajouter le flag \call{NDIS\_PACKET\_TYPE\_PROMISCUOUS} au filtre de paquet de l'interface (PacketFilter).
\footnote{Le driver ethernet \textit{pcnet} qui a été implémenté dans ReactOS ne semble pas avoir de handler
permettant d'activer ou pas le mode promiscuous. Voir section \ref{sub:extrapcnet} page \pageref{sub:extrapcnet}}
\newline
Lorsque l'interface Miniport reçoit une trame, elle la forward à NDIS qui s'occupe de forwarder le paquet aux
ProtocolDrivers. Le \call{ReceiveHandler} est alors appelé avec la trame et l'interface source en paramètre.
%
% ReceivePacketHandler
%
\subsubsection{Réception d'une trame dans le driver TCP/IP}
La réception d'une trame est plutôt basique, le driver se charge simplement de regarder le type de data dans le
payload puis de retirer le header ethernet et forwarder le paquet au handler de paquets.
Seul les types IPv4, IPv6 et ARP sont implémentés dans ReactOS.
Une fois cette opération terminée, le driver notifie NDIS en disant que le traitement de trame est terminé et
demande de forwarder le paquet résultant au handler de paquets (\call{ReceivePacketHandler}).
\begin{figure}[!h]
\caption{Diagramme de réception d'un paquet depuis NDIS}
\centering
\includegraphics[scale=0.3]{ip-reception-stage1.eps}
\end{figure}
Une fois que le handler de paquets est appelé, le traitement du paquet est ajouté dans une queue de traitements.
Cette queue est gérée par une implémentation de « Chew » (\call{ChewCreate}), qui prend en paramètres une fonction
et un paramètre (dans ce cas ci, une structure qui contient tout ce qui est nécessaire: paquet, interface, etc.).
\newline
\begin{figure}[!h]
\caption{Diagramme de préparation d'un paquet NDIS reçu}
\centering
\includegraphics[scale=0.3]{ip-reception-stage2.eps}
\end{figure}
Une fois que la queue arrive au paquet en question, le contenu est analysé et extrait du paquet NDIS (qui utilise
un format propre au protocole pour stocker les données dans leur structure, ce n'est pas un simple buffer).
Les statistiques de l'interface sont mises à jour (nombre de paquets/bytes reçus) puis selon le type de payload,
le handler correspondant est appelé (\call{IPReceive} ou \call{ARPReceive}, le reste est droppé).
C'est à partir de ce moment là que la bibliothèque IP prend le relais.
\newline
\begin{figure}[!h]
\caption{Diagramme de réception d'un paquet depuis NDIS vers TCP/IP}
\centering
\includegraphics[scale=0.3]{ip-reception-stage3.eps}
\end{figure}
Ce ProtocolDriver est le premier à avoir un paquet entrant après NDIS, il est du coup également le dernier
à recevoir un paquet sortant.
\newline
%
% LANTransmit
%
\subsubsection{Emission d'une trame depuis le driver TCP/IP}
Chaque interface IP (donc de couche 3) dispose d'un pointeur de fonction vers un callback de transmission. Excepté
pour la loopback dont le code de transmission se trouve dans la bibliothèque IP (il n'a pas de passage via NDIS
pour la loopback dans ReactOS), la fonction de transmission est \call{LANTransmit} qui se trouve dans le
ProtocolDriver TCP/IP.
\newline
Une fois que cette fonction est appelée (elle dispose du paquet et de l'interface source qui veut émettre), elle
se contente de créer une nouvelle trame ethernet, d'y placer le paquet (IP) en payload et de construire l'en-tête
ethernet (MAC source et MAC destination) en fonction des paramètres fournis. Elle met également à jour les
statistiques de l'interface (nombre de paquets/bytes émis) puis forward la trame nouvellement créée à NDIS.
\newline
\begin{figure}[!h]
\caption{Diagramme d'émission d'un paquet depuis TCP/IP vers NDIS}
\centering
\includegraphics[scale=0.3]{LANTransmit.eps}
\end{figure}
Jusque là, il s'agit du fonctionnement d'origine du driver TCP/IP et aucune modification n'y a été vraiment apportée
pour y supporter les namespaces... mais il faut savoir comment le protocole fonctionne avant de le triturer pour y
inclure quelque chose de nouveau. L'étape suivante, qui est l'étape clé dans les containers (et donc les namespaces),
est l'utilisation d'interfaces virtuelles pour faire un pont entre l'intérieur isolé du namespace et l'extérieur.
%
% Virtual Ethernet
%
\subsubsection{Virtual Ethernet}
L'interface virtuelle que j'ai implémentée est une interface de couche 3 qui se trouve dans la bibliothèque IP
et qui se base sur l'interface de loopback (étant donné que la loopback est également une interface virtuelle
où l'émission et la réception de paquet sont communes). Cependant, elle nécessite également d'être liée à la
couche 2 pour disposer d'une adresse MAC.
\newline
Son implémentation se trouve dans \file{/lib/drivers/ip/network/veth.c} et va toucher aussi bien à la couche 3 que
la couche 2.
\newline
Je pars du principe qu'une interface virtuelle, dans le cas dont j'ai besoin, doit être liée à une interface
déjà existante (sinon ça revient au même que de faire une loopback). La première étape est donc d'identifier
l'interface demandée. Vu qu'on ne dispose que de listes linéaires, on parcourt la liste des interfaces existantes
et on compare l'index de l'interface avec l'index passé en paramètre (typiquement, l'index donné lors
de la commande \call{unshare attach <ID>}).
\newline
Ensuite, tout comme une loopback, on demande à la bibliothèque IP d'instancier une
\linebreak\call{IP\_INTERFACE} qu'on va ensuite configurer.
\newline
Le but d'une virtual ethernet est de s'attacher à une interface physique
\footnote{Rien n'empêche le code de s'attacher à une loopback ou même à une
autre virtual ethernet, je ne vois pas dans quel cas cela ne serait pas possible, cependant le code n'a pas été
testé pour, cette utilisation serait donc expérimentale.} (ou plutôt de manière générale à un NDIS Miniport).
Cela veut dire que pour communiquer avec NDIS, il nous faut récupérer le NdisHandler de l'interface à laquelle
on veut se connecter. En fait pour faire encore plus simple, on va tout simplement dupliquer la structure
\call{LAN\_ADAPTER} de l'interface à laquelle on s'attache, et s'y attacher.
L'avantage de ce cas est qu'on dispose exactement du même traitement que l'interface source (émission, réception
de paquets, etc.), et ayant dupliqué la structure, on peut également la modifier... donc changer l'adresse
MAC de l'interface ! Allons-y. Une fois la structure dupliquée, on génère une adresse MAC aléatoire (voir
section \ref{item:macrandom} page \pageref{item:macrandom}) puis on lie la nouvelle \call{LAN\_ADAPTER}
au contexte de notre nouvelle \call{IP\_INTERFACE}.
\label{fig:iplanlink}
\begin{figure}[!h]
\caption{Diagramme d'un lien classique entre IP\_INTERFACE, LAN\_ADAPTER et NDIS}
\centering
\includegraphics[scale=0.3]{ip_interface-lan_adapter-single.eps}
\end{figure}
\begin{figure}[!h]
\caption{Diagramme de lien avec une Virtual Ethernet (instance d'une LAN\_ADAPTER + IP\_INTERFACE)}
\centering
\includegraphics[scale=0.3]{ip_interface-lan_adapter.eps}
\end{figure}
L'interface virtuelle étant attachée à une interface physique, ça veut dire que d'un point de vue
réception de paquets, l'interface source qu'on va recevoir sera l'interface physique et non l'interface
virtuelle (vu que NDIS ne connait pas l'existence de \textbf{nos} interfaces virtuelles).
Il va donc falloir ajouter une couche entre la réception du paquet et son traitement, une couche qui
va consister à aiguiller le paquet vers la bonne direction
\footnote{Attention, la Virtual Ethernet qui a été créée ici n'est pas réellement comparable à une
« veth » sous Linux. Sous Linux, ce type d'interface fonctionne comme une pipe (tout ce qui entre
d'un côté, sort de l'autre), dans notre cas, une Virtual Ethernet nécessite un switch virtuel pour
fonctionner correctement}.
\clearpage
%
% Virtual Switch (switch.sys)
%
\subsubsection{Implémentation d'un switch pour connecter les namespaces}
\label{sub:switch}
Pour pouvoir rediriger une trame dans son namespace adéquat, il va falloir switcher les trames logiciellement.
En effet, chaque namespace ayant des interfaces de niveau 2 (avec des adresses MAC uniques), l'aiguillage va
se faire à ce niveau là.
\newline
\begin{figure}[!h]
\caption{Switching de trames}
\centering
\includegraphics[scale=0.3]{switching.eps}
\end{figure}
Cette implémentation est l'équivalant sous Linux de la commande:
\begin{center}
« \call{ip link add veth0 type veth peer name veth1} »
\end{center}
Sous linux, cela crée une paire d'interface (une pipe, donc tout ce qui entre d'un côté en ressort de l'autre).
Pour connecter un namespace avec le monde extérieur, on place un bout du pipe dans le namespace, et l'autre
bout du pipe dans la partie réseau globale (physique).
\newline
Dans notre cas, on va connecter une interface virtuelle (qui elle est attaché à un namespace) à un switch
virtuel qui va s'occuper de switcher les trames vers différents endroits (un autre namespace ou le monde
extérieur). Le switch n'est réellement qu'un seul gros switch mais il connait les différents
namespaces (« Virtual LAN ») existants et peut donc faire la part des choses.
Voyons maintenant l'implémentation dans le code.
\newline
La première implémentation qui a été faite est d'intégrer un switch virtuel dans la plus basse couche
de la réception de trames dans \file{tcpip.sys} (ReceiveHandler). Le travail du switch était simplement
de comparer l'adresse MAC de destination de la trame et d'adapter l'interface logique source
dans le code pour que les traitements soient cohérents pour la suite. L'idée de base était là, mais
l'implémenter dans \file{tcpip.sys} n'est pas la solution. A terme, l'idée est d'intégrer
un « vrai » (plus complet) switch virtuel (comme par exemple Open vSwitch) qui lui, dispose
de beaucoup plus d'options (802.1Q, etc.). Le but n'étant pas de réécrire un vrai switch,
on va uniquement supporter le switching de base, il y aura donc un certain manque de support plus loin, nous
allons voir ça.
\newline
\begin{listing}[H]
\caption{Prototype de la fonction NDIS s'occupant de la réception d'un paquet}
\begin{ccode}
INT NTAPI ProtocolReceivePacket(
NDIS_HANDLE BindingContext, // LAN_ADAPTER
PNDIS_PACKET NdisPacket // NDIS_PACKET
);
\end{ccode}
\end{listing}
Pour déplacer l'implémentation hors de \file{tcpip.sys} (pour pouvoir changer le switch facilement sans
devoir modifier le coeur du code), l'idée que j'ai retenue est de faire un deuxième ProtocolDriver qui
s'occuperait de switcher puis de renvoyer le résultat à \file{tcpip.sys} comme si de rien n'était pour
lui. C'est ce procédé qui est utilisé sous Windows par les switchs virtuels les plus utilisés, comme
le « VirtualBox Bridged Networking Driver » qui permet d'utiliser des interfaces virtuelles dans VirtualBox.
\newline
\begin{figure}[!h]
\caption{Diagramme montrant la nécessité du switch}
\centering
\includegraphics[scale=0.3]{why-switching.eps}
\end{figure}
Etant donné que tous les ProtocolDrivers bindés à une interface reçoivent la même trame
\footnote{C'est comme ça que fonctionne le driver WinPcap qui permet de sniffer le réseau sous Windows
(utilisé par Wireshark, par exemple). En s'installant comme ProtocolDriver, le driver reçoit une copie
de tout le trafic indépendamment du fonctionnement de TCP/IP. Cela permet également au driver de pouvoir
injecter des paquets aux interfaces auxquelles le ProtocolDriver est bindé}, sous Windows, il suffit
de décocher le « Protocol TCP/IP » de l'interface physique et de laisser le « Protocol Switch » pour arriver
à un switching de plus bas niveau. Cependant, cette partie n'est pas implémentée dans ReactOS. C'est un vrai
casse tête pour pouvoir gérer facilement le binding d'une interface réseau (l'interface graphique
ne supporte même pas la possibilité de cocher ou décocher un ProtocolDriver)
\footnote{Voir screenshots section \ref{an:tcp} page \pageref{an:tcp}}.
\begin{figure}[!h]
\caption{Fonctionnement d'un ProtocolDriver NDIS}
\centering
\includegraphics[scale=0.2]{ndis-protocol-copy.eps}
\end{figure}
Pour pallier à ce problème, je suis parti sur une implémentation de switch qu'on peut activer et désactiver
à la volée (via \call{unshare enable switch} et \call{unshare disable switch}).
\newline
Pour connecter le switch avec la pile TCP/IP existante, il faut faire communiquer les deux drivers entre eux.
L'idée la plus simple et propre est de transférer le paquet entre l'un et l'autre via un
\call{IoCallDriver}, cependant ça veut dire qu'à \textbf{CHAQUE} trame reçue, un appel à l'Object Manager
du kernel va être fait, créer une interruption logiciel, ajouter une entrée dans une queue d'exécution, etc.
Bref, quelque chose de très lourd pour simplement forwarder un paquet entre deux drivers.
\begin{figure}[!h]
\caption{Diagramme d'utilisation de switch.sys}
\centering
\includegraphics[scale=0.3]{enable-switch.eps}
\end{figure}
\newpage
Une autre idée pour implémenter une communication entre les deux est de compiler et linker le switch
avec la bibliothèque IP du système. De cette façon, non seulement je peux utiliser les mêmes structures de données,
mais en plus je pourrais partager des variables globales entre \file{tcpip.sys} et \file{switch.sys} par
le biais de la bibliothèque... mais ce n'est malheureusement pas le cas.
En effet, la bibliothèque est linkée statiquement aux drivers, donc chaque driver à sa propre instance.
Une autre solution est donc à trouver.
\newline
L'un des avantages de se trouver en kernel space c'est qu'on est beaucoup moins restreint quant à
l'utilisation de la mémoire (il n'y a pas cette protection du heap comme en user-mode qui empêche
au code d'accéder à du contenu qui ne lui appartient pas). Du coup, avec une méthode peu commode,
j'ai créé une structure qui va stocker des pointeurs de tcpip.sys et les envoyer à switch.sys via une communication
drivers classique.
\begin{listing}[H]
\caption{struct SWITCH\_NOTIFY}
\begin{ccode}
typedef struct _SWITCH_NOTIFIY {
PCHAR TCPIPEnabled;
VOID (*LANTransmit)(PVOID, PNDIS_PACKET, UINT, PVOID, USHORT, [...]);
NDIS_STATUS NTAPI (*ProtocolReceive)(NDIS_HANDLE, NDIS_HANDLE, [...]);
INT NTAPI (*ProtocolReceivePacket)(NDIS_HANDLE, PNDIS_PACKET);
PLIST_ENTRY AdapterListHead;
PKSPIN_LOCK AdapterListLock;
NDIS_HANDLE GlobalPacketPool;
NDIS_HANDLE GlobalBufferPool;
} SWITCH_NOTIFY, *PSWITCH_NOTIFY;
\end{ccode}
\end{listing}
On voit directement à la longueur du code, qu'il s'agit de pointeurs de fonctions. Le reste, ce sont
des pointeurs classiques vers des variables ou même une copie de certaines variables
(comme les \call{NDIS\_HANDLE} qui ne sont pas nécessaires à déréférencer car fixes).
Pour ce qui est de l'utilisation:
\begin{itemize}
\item \textbf{TCPIPEnabled}: un pointeur vers un flag qui active (ou pas) \file{tcpip.sys}.
Ce flag est utilisé conjointement avec SwitchEnabled qui se trouve dans le code de switch.c
\item \textbf{ProtocolReceive}: pointeur vers la fonction \call{ProtocolReceive} de \file{tcpip.sys}
\item \textbf{ProtocolReceivePacket}: pointeur vers la fonction \call{ProtocolReceivePacket} de \file{tcpip.sys}
\item \textbf{AdapterListHead}: pointeur vers la liste chainée des \call{LAN\_ADAPTER} de \file{tcpip.sys}
\item \textbf{AdapterListLock}: pointeur vers le verrou (mutex) de la liste chainée
\item \textbf{GlobalPacketPool}: copie du handle \call{PacketPool} de \file{tcpip.sys} utilisé par NDIS
(pour émettre un paquet)
\item \textbf{GlobalBufferPool}: copie du handle \call{BufferPool} de \file{tcpip.sys} utilisé par NDIS
(également pour émettre un paquet)
\end{itemize}
Le driver \file{tcpip.sys} est adapté pour que dès qu'une modification d'un de ces champs est provoquée, le
driver recrée cette structure et la ré-envoie au switch pour le tenir informé des nouvelles modifications.
\newline
Une fois que le switch reçoit cette structure, il s'occupe de copier le contenu dans une variable globale. En plus
de ça, il est possible que la mise à jour ait été déclenchée par l'ajout d'une nouvelle interface. Dans ce cas,
par sécurité, si le switch est activé (si le flag \call{SwitchEnabled} est à \call{TRUE}), on va itérer sur
toutes les interfaces et "rerouter" las fonctions de réception et d'émission de l'interface vers le switch.
\begin{listing}[H]
\caption{Reroutage des fonctions de réception et de transmission de paquets de tcpip.sys vers le switch virtuel}
\begin{ccode}
if(SwitchEnabled == TRUE) {
DbgPrint("Switch: rerouting interfaces to switch\n");
CurrentEntry = Linker.AdapterListHead->Flink;
while (CurrentEntry != Linker.AdapterListHead) {
Current = CONTAINING_RECORD(CurrentEntry, LAN_ADAPTER, ListEntry);
((PIP_INTERFACE) Current->Context)->Fallback =
((PIP_INTERFACE) Current->Context)->Transmit;
((PIP_INTERFACE) Current->Context)->Transmit = SwitchTransmit;
CurrentEntry = CurrentEntry->Flink;
}
}
\end{ccode}
\end{listing}
A partir de ce moment là, tout le trafic sortant va passer d'abord par le switch avant de passer par TCP/IP
et le trafic entrant passera par le switch avant de passer dans TCP/IP. Il ne reste plus qu'à faire le traitement
des paquets.
\newline
A noter: même si il n'y a qu'une seule instance du switch virtuel, d'un point de vue logique, nous disposons
d'un switch par « Virtual LAN ». En effet, dès que deux interfaces (virtuelles ou pas) sont connectées/attachées
l'une à l'autre, un point de switching entre les deux s'établit (le switch fait le rapprochement en comparant
le handle NDIS)\footnote{La bonne méthode serait de d'abord créer un Virtual LAN, puis d'y attacher des
interfaces et non pas de créer un Virtual LAN implicitement lors d'une connexion}.
\newline
Lors de la demande de transmission d'un paquet, on reçoit en paramètres le paquet IP et un pointeur
vers une adresse (MAC) de destination.
%
% User-mode linking (ipconfig -> iphlpapi -> tdi -> tcpip.sys)
%
\subsection{Lien entre la couche utilisateur et le driver TCP/IP}
Maintenant que les couches 2 et 3 sont isolées dans le kernel, il faut voir comment faire le lien avec la partie utilisateur.
Je suis parti d'ipconfig (la commande donne l'adresse IP et MAC des interfaces installées sur le système, donc je
suppose que les appels que ce programme fait, sont directement liés au code que j'ai modifié plus haut).
%
% ipconfig
%
\subsubsection{Utilitaires de gestion: ipconfig.exe, route.exe, arp.exe, ping.exe, ...}
Le code source de « ipconfig » se trouve dans \file{/base/applications/network/ipconfig}. Le code semble avoir
été réécrit from scratch mais utilise bien « iphlpapi » (comme sous Windows), pour communiquer avec la couche
réseau du système.
\newline
Dans le header du code, un commentaire dit directement que « renew » et « release » ne sont pas implémentés.
Nous sommes au sommet de la couche d'appel, et on se rend compte d'une chose:
il manque des fonctionnalités, et même de base, mais bon, le principal semble fonctionner.
\newline
Globalement, l'affichage des interfaces ainsi que leurs état, adresse, etc. se font via l'appel
de la fonction \call{GetAdaptersInfo} qui se trouve dans la \call{iphlpapi}. D'après MSDN, cette appel
retourne une liste chainée de structures \call{IP\_ADAPTER\_INFO}.
\newline
Le contenu de la structure de IP\_ADAPTER\_INFO comparé à ce qu'on vient de voir (la séparation layer 3 et 2)
n'est plus respecté, c'est une structure prévue pour répondre à l'utilisateur le plus de choses possible, du
coup on se retrouve avec un mix de tout:
\begin{listing}[H]
\caption{struct IP\_ADAPTER\_INFO (repris de la MSDN)}
\begin{ccode}
typedef struct _IP_ADAPTER_INFO {
struct _IP_ADAPTER_INFO *Next;
char AdapterName[MAX_ADAPTER_NAME_LENGTH + 4];
char Description[MAX_ADAPTER_DESCRIPTION_LENGTH + 4];
BYTE Address[MAX_ADAPTER_ADDRESS_LENGTH];
DWORD Index;
UINT Type;
UINT DhcpEnabled;
PIP_ADDR_STRING CurrentIpAddress;
IP_ADDR_STRING IpAddressList;
IP_ADDR_STRING GatewayList;
IP_ADDR_STRING DhcpServer;
BOOL HaveWins;
IP_ADDR_STRING PrimaryWinsServer;
time_t LeaseExpires;
// [...]
} IP_ADAPTER_INFO, *PIP_ADAPTER_INFO;
\end{ccode}
\end{listing}
On voit bien qu'on dispose d'un tas de choses totalement hétérogène d'un point de vue isolation:
MAC, IP, DHCP, DNS, Gateway, etc. On a donc une concentration et une centralisation des appels
qui se font via \call{iphlpapi}.
%
% iphlpapi
%
\subsubsection{IP Helper API (iphlpapi)}
Cette API permet de questionner le système à propos de sa configuration réseau. Elle ne permet pas d'envoyer
ou recevoir des paquets IP, mais bien uniquement de récupérer (ou configurer) des informations.
\newline
Par exemple, la fonction \call{getInterfaceInfoSet} permet d'établir une liste des interfaces réseaux
disponibles. En réalité, il s'agit d'un helper pour l'utilisation de TDI. Pour établir la liste des interfaces,
iphlpapi va demander à TDI de retourner une liste d'entités puis cette liste va être parcourue et analysée.
C'est également dans cette partie du code que l'interface de loopback va être ignorée, c'est pourquoi
elle n'apparaît pas dans ipconfig (alors qu'elle existe bel et bien).
\newline
Quelle que soit la partie du code de iphlpapi, on remarque un appel récurrent qui est
\linebreak\call{tdiGetEntityIDSet}.
%
% TDI
%
\label{sub:tdi}
\subsubsection{TDI (Transport Dispatch Interface)}
TDI est un protocole utilisé pour communiquer avec la couche de transport de la stack réseau de Windows.
Les « Transport Providers » sont l'implémentation de protocoles réseau tel que TCP/IP, NetBIOS, et AppleTalk.
Durant la compilation et le linkage d'une application user-mode, un client TDI est intégré dans le code.
Ce client permet de faire une passerelle pour les API user-mode des ProtocolDrivers. Typiquement, les
commandes TDI sont: TDI\_SEND, TDI\_CONNECT, TDI\_RECEIVE.
\newline
On trouve TDI à trois endroits dans l'arborescence:
\begin{itemize}
\item \file{/drivers/network/tdi/tdi/}
\item \file{/drivers/network/tcpip/} (avec le reste de TCP/IP)
\item \file{/lib/tdilib/}
\newline
\end{itemize}
La partie qui se trouve dans \file{drivers/tdi} ne contient que du code non-implémenté, certainement pour garder
une compatibilité de symbols avec des applications qui l'utilisent (la partie NetBios n'est absolument
pas gérée par exemple).
\newline
La \call{tdilib} par contre est très petite mais implémentée. Il n'y a pas grand chose derrière.
Lors d'une interrogation via TDI, les étapes suivantes sont effectuées:
\begin{itemize}
\item Ouverture de l'objet \call{\textbackslash{}device\textbackslash{}Xxxx}
\footnote{Variable en fonction de la destination: Tcp, Udp, Raw, ...}
\item Appel de \call{IOCTL\_TCP\_QUERY\_INFORMATION\_EX} sur le driver avec les paramètres
qui ont été fournis à l'appel de TDI (le type d'entité demandé, etc.)
\item Retour de la réponse du driver directement à l'appelant (sans même lire la réponse ou la
formater)
\newline
\end{itemize}
L'objet \call{\textbackslash{}device\textbackslash{}tcp} est géré dans \call{tcpip}
et la liste des entités (TDI Entities) aussi.
Étant donné que toutes les informations sont transmises via ces entités, pour isoler la réponse côté
user-mode, il suffit de modifier la liste d'entités retournée.
\newline
Malheureusement, encore une fois, dans le code de \file{tcpip.sys}, la liste des \call{TDIEntityID} se retrouve
dans une variable globale au code, et cette variable est utilisée pas mal de fois dans le code.
De plus, la structure \call{TDIEntityID} n'est pas un type défini par ReactOS, mais bien par l'implémentation
de TDI, plus d'informations à propos de cette structure se retrouvent dans la documentation MSDN
\footnote{http://msdn.microsoft.com/en-us/library/bb432492(v=vs.85).aspx}. Par sécurité (et propreté, bien que
la propreté puisse être subjective vu ce qui arrive...) j'ai décidé de ne pas modifier cette structure, étant
donné qu'elle est explicitement décrite (ce n'est pas le cas de la structure \call{EPROCESS}
que j'ai modifiée, car elle n'est que partiellement décrite dans la documentation, certaines parties sont cachées).
\newline
Pour pouvoir disposer de plusieurs listes d'entités, j'ai tout simplement créé une liste qui contient des listes d'entités.
Il y a deux variables globales utilisées: \call{EntityList} et \call{EntityCount}. La première variable est
un vecteur d'entités, la deuxième est un entier qui contient le nombre d'éléments du vecteur. Pour ne pas modifier
le code existant, j'ai redéfini ces deux variables par un appel de fonction qui va retourner la liste
correspondante au namespace en cours.
\begin{listing}[H]
\caption{Adaptation de la liste d'entités TDI}
\begin{ccode}
/* Override original global names with functions */
#define EntityList GetEntityList(IoGetCurrentProcessNs())
#define EntityCount GetEntityCount(IoGetCurrentProcessNs())
TDIEntityInfo *GetEntityList(ULONG NamespaceId);
ULONG GetEntityCount(ULONG NamespaceId);
ULONG SetEntityCount(ULONG Count);
NTSTATUS TdiNewNamespace(ULONG NamespaceId);
NTSTATUS TdiRemoveNamespace(ULONG NamespaceId);
\end{ccode}
\end{listing}
Cette méthode (bien que très agressive) fonctionne très bien et propose plusieurs avantages:
\begin{itemize}
\item Les appels déjà existants à \call{EntityList} sont toujours fonctionnels
\item On peut débugger l'appel de la liste vu qu'à chaque accès, une fonction est appelée
\item La liste peut être adaptée plus tard, en changeant juste la fonction appelée
ou en modifiant la fonction \call{GetXxxxxx} directement.
\newline
\end{itemize}
Pour stocker ces différentes listes, on va créer deux nouvelles structures qui vont contenir les informations
nécessaires pour garder différentes listes de TDIEntityInfo.
\begin{listing}[H]
\caption{Structure de données pour stocker plusieurs listes d'entités TDI}
\begin{ccode}
typedef struct _TDINamespaceInfo {
LIST_ENTRY ListEntry; /* Linked list Entry */
ULONG NamespaceId; /* Namespace ID */
TDIEntityInfo *tdiEntry; /* Real EntityList */
} TDINamespaceInfo;
typedef struct _TDINamespaceCount {
LIST_ENTRY ListEntry; /* Linked list Entry */
ULONG NamespaceId; /* Namespace ID */
ULONG CountValue; /* Real EntityCount */
} TDINamespaceCount;
\end{ccode}
\end{listing}
Grâce à ces deux structures, on peut identifier chaque \call{EntityList} et \call{EntityCount} par un ID
qui sera le numéro du namespace.
\newline
Pour ce qui est des deux fonctions (qui remplacent les variables) \call{GetEntityList}, il ne s'agit plus
que d'une itération sur la liste des \call{EntityList} et de retourner celui qui correspond au namespace demandé.
\begin{listing}[H]
\caption{Fonctionnement de GetEntityList}
\begin{ccode}
TDIEntityInfo *GetEntityList(ULONG NamespaceId)
{
TDINamespaceInfo *EntryInfo;
PLIST_ENTRY Entry = __EntityListNS.Flink;
while (Entry != &__EntityListNS) {
EntryInfo = CONTAINING_RECORD(Entry, TDINamespaceInfo, ListEntry);
if (EntryInfo->NamespaceId == NamespaceId)
return EntryInfo->tdiEntry;
Entry = Entry->Flink;
}
return NULL;
}
\end{ccode}
\end{listing}
Pour ce qui est de \call{TdiNewNamespace} et \call{TdiRemoveNamespace}, rappelez-vous, ces fonctions sont
appelées lors de la création d'un namespace (depuis process.c).
\newline
Nous avons le point clé de l'isolation: les applications de configuration et de management
en user-mode (ipconfig, arp, route, ...) utilisent tous \call{iphlpapi} (qui utilise TDI). En réalité, tout
le user-mode utilise TDI pour communiquer avec la couche réseau du système (également Winsock), c'est
grâce à cette modification dans TDI qu'on peut isoler une stack TCP/IP sans aller plus loin que la couche 3.
Grâce à ça, l'implémentation actuelle de UDP, TCP, etc. n'a pas à être modifié.
\begin{figure}[!h]
\caption{Pile d'appels depuis ipconfig jusqu'à tcpip.sys}
\centering
\includegraphics[scale=0.2]{ipconfig-tdi.eps}
\end{figure}
\newpage
\section{Topologies de test}
\subsection{Overlapping d'adresses IP de destination}
Une première démonstration du fonctionnement de l'isolation est un exemple avec de l'overlapping d'IP
de destination. En effet dans cet exemple on va avoir deux processus (ex: deux \file{cmd.exe}) dans deux namespaces
différents qui seront tous les deux attachés à deux interfaces physiques différentes. Chacune des interfaces
physiques est connectée à un routeur derrière, qui ont tout les deux l'adresse IP \call{172.16.0.254/24}. Dans un
environnement classique, il ne peut y avoir qu'une seule route vers \call{172.16.0.254}, cet exemple
montre bien qu'on dispose de deux tables de routage et deux tables ARP différentes pour les deux processus.
\begin{figure}[!h]
\caption{Topologie de test avec overlapping d'adresses IP de destination}
\centering
\includegraphics[scale=0.3]{topology-overlap.eps}
\end{figure}
\subsection{Overlapping d'adresses IP en communication inter-namespaces}
Une autre démonstration est l'isolement inter-namespaces. Dans cet exemple on va faire 4 namespaces qu'on va
regrouper en 2 isolations logiques. On va inter-connecter 2 namespaces entre eux, et les deux autres entre eux.
Pour complexifier le tout, on va faire de l'overlapping dans les deux namespaces logiques.
Le diagramme est plus que nécessaire pour comprendre le principe:
\begin{figure}[!h]
\caption{Topologie de test avec overlapping d'IP inter-namespaces}
\centering
\includegraphics[scale=0.3]{internamespace.eps}
\end{figure}
\newpage
\section{Conclusion}
\subsection{Technique}
En partant d'un OS écrit « from scratch », en analysant le fonctionnement et en lisant le code source
du système, je suis parvenu à implémenter un Proof-Of-Concept relativement poussé d'une isolation
complète d'une stack TCP/IP: les interfaces réseaux, la table de routage et la table ARP sont isolées.
\newline
Du côté du user-mode, les réponses des applications \file{ipconfig.exe}, \file{route.exe},
\file{arp.exe} sont bien isolées également. Un « ipconfig » ne donnera que la liste des interfaces
réseaux de son namespace (et leurs adresses IP et MAC). Un « route print » n'affiche que les
routes de son namespace. Pareil pour la commande « arp -a » qui donnera la table ARP de son namespace, sans
interférer avec les autres.
\newline
En plus de ça, l'isolation va à un tel point que je peux faire une topologie qui supporte de l'overlapping
d'adresses aussi bien sources que de destinations. En effet, dans deux namespaces différents, je peux
pinger la même ip (destination) et voir que le traitement du paquet ne se fait pas de la même façon (routage
précis sur base de l'application source).
\newline
J'ai écrit un simple (mais suffisant pour l'instant) switch virtuel qui s'occupe d'aiguiller les trames
que le système reçoit, de façon à avoir différents domaines de broadcast (sur une certaine limitation: il n'est
pas possible d'avoir plusieurs domaines de broadcast sur une interface physique, cela demande un support
de tagging de paquet, comme fait le 802.1Q).
\newline
Pour finir, j'ai établi un canal de communication entre les drivers (nouveaux et existants) et le user-mode.
J'ai pu traverser toute la couche d'un OS (user-mode vers le kernel-mode, en passant par un driver) et toute
la stack IP d'un système (en se limitant aux couches de liaison et transport).
\newline
J'ai pu intégrer un principe fonctionnel et utilisable dans une topologie de test, il reste cependant pas mal
de travail à faire pour rendre ce système parfaitement fonctionnel et utilisable à grande échelle (à commencer
par remplacer mon pauvre switch de base par un switch plus évolué).
\newpage
\label{next:utils}
\subsection{Recommandations pour la suite}
Un grand nombre de points sont à améliorer ou corriger, certains sont dûs à une limitation d'implémentation,
d'autres à des proof-of-concept qui sont restés dans le code et n'ont jamais été améliorés.
\begin{itemize}
\item Switch virtuel: le \file{switch.sys} qui a été écrit spécialement pour les namespaces n'est pas à garder.
En effet il ne permet qu'un switching très sommaire (sur base d'adresses MAC uniquement) et est
très loin d'être optimisé. De plus il a une forte dépendance à \file{tcpip.sys} et à la lib ip du système.
L'idée qui a été suggérée par mon maitre de stage est d'utiliser un portage de openvswitch pour
Windows. Cependant à l'heure actuelle, l'implémentation est partielle. Pour profiter pleinement
des namespaces, un switch supportant des VLAN semble indispensable pour pouvoir isoler correctement
la couche 2 et 3.
\item Le système utilise globalement des listes linéaires pour stocker toutes les tables existantes. Dans beaucoup
de cas liés aux namespaces, ces listes sont itérées à chaques réception de paquets. L'utilisation d'un meilleur
algorithme ou tout simplement de hashtable pour stocker ces tables semble incontournable pour avoir de meilleures
performances.
\item La communication entre le kernel et le driver réseau se fait par l'objet
\linebreak\call{\textbackslash{}Device\textbackslash{}NamespaceNetwork}, qui est initialisé dans le driver
{tcpip.sys}. Cette pratique est une relique d'implémentation. Mon but a été de faire communiquer
le kernel avec \file{tcpip.sys} rapidement et c'est la première solution qui m'est venue à l'esprit,
cependant maintenant que l'implémentation est plus aboutie, on se rend compte que cette méthode
n'est pas optimale. Seul \file{tcpip.sys} peut bénéficier d'une mise à jour du kernel (être appelé
depuis le kernel).
La solution serait de créer l'objet
\call{\textbackslash{}Device\textbackslash{}NamespaceNetwork}
dans \file{ntoskrnl.exe} (dans la partie \file{networknamespace.c}) et d'y ajouter une
liste de Listeners. Chaque partie du système (driver ou autre) ayant un rapport avec le réseau
pourrait alors s'enregistrer comme « client network namespace » au-près du kernel. De ce fait,
lors d'une demande de création d'un namespace (par exemple), les différents drivers qui ont
une part à jouer dans le namespace (\file{tcpip.sys} doit allouer de nouvelles listes
dans son code par exemple) seraient notifiés depuis le kernel.
\newpage
\begin{figure}[!h]
\caption{Enregistrement des drivers dans le kernel et dispatching d'actions}
\centering
\includegraphics[scale=0.2]{kernel-dispatch-nsop.eps}
\end{figure}
\item Remplacer le hard-code de détection du PID \hexa{0x04} par une fonction qui vérifierait de façon plus générique si
le processus en cours d'exécution est le processus \call{SYSTEM} ou pas.
Voir section \ref{sub:routerpid} page \pageref{sub:routerpid}.
\label{item:macrandom}
\item Faire un tirage aléatoire de l'adresse MAC de la Virtual Ethernet autrement qu'en tirage totalement
aléatoire sur les 48 bits. Par exemple, VirtualBox a son propre OUI et donc toutes leurs adresses
MAC ont le même préfixe. Il est possible qu'un préfixe existe pour ce genre d'utilisation
mais je n'en ai pas trouvé (certaines implémentations d'interfaces TAP font aussi un tirage aléatoire
sur les 48 bits, c'est pourquoi j'ai laissé ça ainsi).
\item Actuellement, toute la configuration et le fonctionnement des namespaces est volatile.
Windows utilise sa base de registre pour y sauver toutes sortes de configurations, etc. Au lieux
de faire un script batch au démarrage de la machine, une implémentation de la sauvegarde de
la configuration (et son chargement au boot) dans la base de registre pourrait être un point
intéressant comparé au fonctionnement général de Windows.
\item Implémenter complètement le support de l'isolation des processus (supporter le fait
d'avoir plusieurs PID les mêmes simultanément, ...)
\label{next:utils}
\item Le portage de la commande « ip » sous Linux, du package « iproute2 ». Ce package et la commande « ip »
supporte presque tout l'aspect de gestion réseau sous Linux. (ip address, ip link, ip route,
ip rule, ip neighbor, ip tunnel, etc.)
\item Faire une extension pour « netsh » qui permettrait de gérer les namespaces depuis la ligne
de commande. En effet, « netsh » est prévu pour supporter des extensions par l'ajout de
fichiers \file{.dll}, un module qui s'occuperait de l'attachement/détachement des interfaces
à un namespace depuis « netsh » semble un choix intéressant.
\item Porter le code et le fonctionnement de ReactOS vers Windows pour en faire une solution
comme « Parallels Containers for Windows », mais avec du code Open Source. Il ne sera plus question
de modifier le fonctionnement du kernel par contre, mais bien d'adapter le fonctionnement
en faisant une sur-couche au système existant.
\item Développer un plugin pour GNS3. Ce programme offre une GUI qui permet de faire une topologie
très simplement. A la base, la GUI sert à configurer des routeurs Cisco pour configurer
une topologie avec l'émulateur \call{dynamips}, mais leur interface graphique est adaptable.
On peut imaginer utiliser l'interface de GNS3 pour configurer les namespaces de la machine et
ainsi construire la topologie et la connexion des containers graphiquement.
\item Hyper-V utilise un switch virtuel qui a été développé par Microsoft. Ce switch est utilisé partout
dans l'hyperviseur de Microsoft et supporte déjà pas mal de fonctionnalités (VLAN, etc.). Il
serait intéressant de remplacer mon implémentation du switch virtuel par la leur et donc
offrir une couche de virtualisation (containers) par dessus leur switch.
\end{itemize}
\newpage
\section{Conclusion personnelle}
\subsection{A propos de mes études}
Étant en option réseau à la Haute École de la Province de Liège, ce sujet est directement lié a mes études.
Bien que la majorité des cours soient basés sur des langages de hauts niveaux, les cours de C et \textbf{surtout}
de réseau m'ont permis d'arriver plus ou moins facilement à bout de ce travail.
\newline
La programmation kernel n'est pas vraiment abordée dans mon cursus, j'ai donc dû trouver et apprendre par moi-même
une grande partie du fonctionnement de Windows. J'ai toujours été intéressé et passionné par Linux (le kernel
et le système GNU/Linux en général), j'ai donc pu me baser sur ce que je connaissais pour le transposer dans une
logique Windows.
\newline
Concrètement, mon développement dans le kernel s'achève avec
\textbf{84} fichiers modifiés,
\textbf{4130} lignes ajoutées et
\textbf{550} lignes supprimées
(il s'agit d'un \call{git diff} entre mon checkout du SVN ReactOS et mon dernier commit dans mon git local)
%
%
% TFE
\subsection{Sujet du TFE}
Ce sujet est particulièrement intéressant car il n'existe aucune alternative libre ou même native à ce
que j'ai conçu/écrit. Le fait de faire quelque chose de neuf et dans un domaine très recherché à l'heure
actuelle (la virtualisation) m'a vraiment plu et m'a appris beaucoup de choses, principalement à propos
du kernel Windows NT.
\newline
Ca me rassure réellement de voir qu'il reste encore des domaines et des sujets où les langages de hauts niveaux
n'ont pas vraiment leurs places. Il n'est pas vraiment concevable à l'heure actuelle de faire un hyperviseur
(faible en demande de ressources) en Java par exemple.
\newline
Je n'avais jamais eu l'occasion de tester réellement les namespaces sous Linux avant de commencer ce stage
et je suis très content d'avoir pu découvrir cette partie de la virtualisation qui est pour moi un concurrent
sérieux aux hyperviseurs de type 1 et 2 à l'heure actuelle où le partage de ressources et la rentabilité
du matériel deviennent un point critique.
\newline
A l'heure actuelle où le « cloud » devient un mot à la mode et que tout le monde s'empresse d'utiliser,
cette technologie demande justement énormément de virtualisation (mettre en ligne du contenu dans un
environnement sécurisé (et donc isolé)). Si il fallait faire une machine virtuelle par utilisateur
d'un système, l'overhead global serait énorme et la rentabilité des ressource ne serait pas optimale.
%
% Retour
%
\subsection{Retour sur le stage}
Je me suis vraiment bien amusé durant mon stage. L'ambiance générale de la boite et dans l'openspace où
je codais était vraiment sympa. De plus, je ne suis absolument pas quelqu'un « du matin », et Level IT
est le genre de société qui offre une certaine flexibilité d'horaire qui me permet, je trouve,
de bien travailler.
\newline
Le courant est (vite) très bien passé avec mon entourage. Je me suis intégré facilement à l'équipe
et j'ai vraiment bien aimé développer dans des conditions qui me permettent d'utiliser mon propre
laptop avec mon système d'exploitation et mon environnement de développement, sans être obligé et
confiné à utiliser des logiciels précis.
%
% Thanks
%
\newpage
\section{Remerciement}
Je souhaite tout d'abord remercier Pierre De Fooz pour m'avoir proposé ce stage. En effet, j'avais demandé
si il était possible de trouver un stage qui impliquait d'écrire du C (n'étant vraiment pas intéressé
par des langages de haut niveau comme C\# et encore moins Java) dans le domaine du réseau.
Il m'a trouvé ce sujet qui ne demande qu'à faire du C, dans un kernel et qui de plus est Open Source.
C'est là exactement tout ce que je recherchais.
\newline
Je tiens à remercier également Olivier Hault, patron de Level IT, qui a toujours été derrière moi, m'a soutenu
et donné des pistes à suivre pour arriver au but.
\newline
Le channel \#reactos du serveur IRC irc.freenode.net, sur lequel j'ai pu avoir de l'aide des développeurs du projet ainsi
que des contacts avec le coordinateur de projet actuel.
\newline
Denis Jasselette, Cyril Paulus et Christine Daniel pour la relecture de cet ouvrage.
\newline
Le channel \#inpres du serveur IRC irc.maxux.net pour ses résidents, toujours prêts à aider et à troller:
Raphael Javaux (RaphaelJ), Lionel Ancia (liBdot4), Benoît Dardenne (morte), Sacha Sokoloff (Darky),
Laurent Doms (Pichet), Ghilan Onkelinxou (ghilan), Zoé (z03), Mathieu Lobet (omlet),
Christophe De Carvalho (Zaibon), Kimberly Scott (woop), William Gathoye (wget), Anthony Binnici (Nemo),
Thomas Van Gysegem (T4g1), Loïc Coenen (jt), Corentin Pazdera (nado), Sarah Sabry (Paglops).
\newline
Tous ceux que je n'aurais pas cité et qui m'ont permis d'arriver au bout de ce projet !
\newpage
\section{Informations complémentaires}
\begin{itemize}
\item Ce document a été rédigé en LaTeX, compilé en PDF avec XeLaTeX sous GNU/Linux (Gentoo).
\item Les diagrammes que j'ai réalisés ont été faits avec https://www.draw.io
\item La coloration syntaxique de ce document est faite via \textit{pygmentize} et \textit{minted}.
Pour bénéficier d'une coloration correcte des types, j'ai dû en ajouter un grand nombre
au fichier source car Microsoft a eu l'excellente idée (je ne sais pas pourquoi à vrai dire) de
redéfinir tous les types du C en majuscule et de ne respecter aucune convention vis-à-vis
des customs types (par exemple faire des typedef type\_t).
\item Source des diagrammes de virtualisation: https://fr.wikipedia.org/wiki/Virtualisation
\end{itemize}
\section{Annexes}
\subsection{Environnement de développement et compilation}
La première chose à faire pour pouvoir utiliser ReactOS depuis les sources, est de cloner le SVN puis compiler
le système complet. Un environnement de compilation (nommé RosBE) a été fait sur mesure pour compiler le système from
scratch. Cet environnement comprend des scripts pour configurer des variables d'environnement et une toolchain
complète (gcc, binutils, ...).
\newline
L'arborescence (principale) du code source de ReactOS se compose ainsi:
\begin{itemize}
\item \file{/base}:
contient la source des applications userspace (ipconfig, calc, autochk, ...)
\item \file{/boot}:
contient le code du bootloader et les fichiers \file{.inf} permettant de construire un registre
de base (pour le livecd ou le bootcd)
\item \file{/cmake}:
contient les fichiers de configuration pour la compilation du système complet
\item \file{/dll}:
contient le code des bibliothèques partagées de Windows (ntdll, directx, le control panel, ...)
ansi que des portages du monde libre (libpng, libjpeg, ...)
\item \file{/drivers}:
contient le code source des drivers qui ont été réécrits pour ReactOS (drivers IDE, USB, Serial, ...)
dont notamment le driver TCP/IP et NDIS.
\item \file{/include}:
contient les .h du ddk, nds, kernel et tous les headers globaux du système
\item \file{/lib}:
contient la source de la RTL (RunTime Library) de Windows et des libs écrites spécialement
pour ReactOS, dont la « libip »
\item \file{/media}:
contient tout ce qui ne se compile pas et qui est nécessaire au système (fichiers inf, fonts,
wallpapers, ...)
\item \file{/ntoskrnl}:
contient le code de \file{ntoskrnl.exe} qui est réellement le coeur du système, c'est le kernel
de Windows. Le contenu est structuré pour séparer la gestion de la mémoire, des processus, etc.
\newline
\end{itemize}
Le système se compile avec cmake et dispose d'une target \textit{bootcd} ou \textit{livecd}.
\begin{itemize}
\item \textit{\call{bootcd}}:
compile tout le système et crée un ISO bootable qui lance une installation
du système (Windows XP like). Installer le système nécessite un disque dur, partitionner,
un bootloader, etc. mais cette version permet d'utiliser le système en read-write
\item \textit{\call{livecd}}:
compile tout le système et crée un ISO bootable qui lance le système sans
l'installer. Cet ISO n'inclut pas l'installeur et n'est utilisable qu'en read-only. Cette
version ne nécessite pas de disque dur. Le livecd est très pratique pour tester des modifications
côté kernel et drivers car durant le boot, le matériel est probé puis installé. Ca
permet de partir d'un système fraîchement installé rapidement.
\end{itemize}
Tout mon travail s'est effectué sur la révision 62083 du SVN. Le code fait un peu plus de 3 millions de lignes
de C et un peu moins de 2 millions de lignes de header.
Ma machine de développement a été un laptop Acer 7730G, avec un Core 2 Duo à 2.2 Ghz et 4 Go de ram.
La compilation du live cd met une grosse demi-heure à compiler.
\subsection{Exécution et débogage}
Pour l'exécution du système, j'ai utilisé la virtualisation et l'émulation. Mon laptop
n'ayant pas de technologie de virtualisation vu l'âge du CPU, les solutions Xen, etc. n'étaient pas
possibles (vu que c'est du Windows qui est virtualisé).
Mon choix s'est limité à VirtualBox et qemu:
\begin{itemize}
\item VirtualBox: hyperviseur de type 2, il m'a surtout permis d'utiliser le système avec
plusieurs cartes réseaux virtuelles bindées à des Debian virtuels pour pouvoir
analyser le trafic entrant et sortant.
\item qemu: je n'ai pas réussi à virtualiser plusieurs interfaces réseaux bindées aux Debian
mais l'utilisation de qemu permet un déboggage du kernel avec l'utilitaire \call{gdb}
à distance. Breakpoint, backtrace, chargements de symboles, etc. tout ce qu'il faut
pour pouvoir avoir la main sur le kernel.
\newline
\end{itemize}
Dans le fichier de configuration \file{/cmake/config.cmake}, j'ai ajouté l'option \call{SEPARATE\_DBG} qui permet
de séparer les symboles de débogage des binaires, ce qui m'a permis de charger les symboles dans \call{gdb} et pouvoir
faire du backtrace, des breakpoints, etc. dans le kernel et les drivers. Cette option ne se trouvait pas dans
la config mais le flag \call{SEPARATE\_DBG} existe et est implémenté dans le code qui s'occupe de compiler
le projet.
\newpage
\subsection{lwIP (A Lightweight TCP/IP stack)}
Ce projet\footnote{https://savannah.nongnu.org/projects/lwip/}
est particulièrement intéressant car il s'agit d'une implémentation d'une stack TCP/IP simple
mais complète, très légère et vraiment indépendante du système derrière, le code pouvant même tourner
sur du matériel embarqué sans OS.
\newline
Le but de lwIP est d'avoir une implémentation la plus complète possible d'une stack TCP/IP tout en
gardant une très faible consommation de ressources.
\newline
Les fonctionnalités principales de lwIP sont:
\begin{itemize}
\item Protocoles: IP, ICMP, UDP, TCP, IGMP, ARP, PPPoS, PPPoE
\item Client DHCP, client DNS, AutoIP/APIPA (Zeroconf), agent SNMP
\item APIs: APIs spécialisés pour systèmes avancés
\item ...
\newline
\end{itemize}
Dans la mailing list de ReactOS\footnote{https://lists.gnu.org/archive/html/lwip-devel/2011-08/msg00010.html},
on peut voir une discussion récente à propos de l'implémentation de
lwIP dans ReactOS comme étant stable et devenue la stack primaire du système, cependant durant tous mes
tests, je n'ai jamais vu cette partie de code être utilisée (leur stack IP par contre bien).
Seule la partie TCP du système utilise lwIP en réalité. Etant en layer 4, ce point n'a pas impacté
mon développement au final.
\newpage
\begin{figure}[!h]
\caption{Couche réseau et utilisation de lwIP}
\centering
\includegraphics[scale=0.3]{diagramme-lwip.eps}
\end{figure}
\newpage
\label{a:unshare}
\subsection{Syntaxe de la commande unshare.exe}
\begin{listing}[H]
\caption{Utilisation de unshare.exe}
\begin{ccode}
unshare // Lance un cmd.exe dans un nouveau namespace
unshare attach <id> // Ajoute une veth liée à l'interface <id>
unshare detach <id> // Détache une interface virtuelle (déchargement)
unshare ip <id> <ipv4> <mask> // Attribue une adresse IPv4 à une interface
unshare mac <id> <mac> // Change l'adresse MAC d'une interface
unshare enable switch // Active le switch layer 2 virtuel
unshare disable switch // Désactive le switch layer 2 virtuel
\end{ccode}
\end{listing}
%
% NDIS Miniport newdev
%
\subsection{Bloquage d'implémentation par le Plug'n'Play manager et NDIS Miniport}
Jusqu'à présent, tout le système a été basé pour attacher une interface virtuelle à une interface
physique\footnote{Concrètement, il faut une interface « physique » (pour ReactOS) par Virtual LAN
que l'ont veut créer.}.
Pour bien faire, il faudrait l'attacher à une interface de type TAP plutôt.
\subsubsection{Interfaces type TAP}
Les interfaces TAP sont des interfaces layer 2 dont la fonction n'est que de forwarder le trafic du
kernel-mode en user-mode.
Ces interfaces sont utilisées par OpenVPN ou encore le réseau TOR. En effet, la partie
intéressante du code se trouve en user-mode (le chiffrement, la négociation de connexions, la gestion
du trafic, le décodage des trames, etc.). L'interface TAP est très simple vu que le driver ne s'occupe que
de faire du passthrough entre le kernel et le user-mode.
\newline
L'avantage d'utiliser ce type d'interface est qu'on dispose d'une carte réseau virtuelle layer 2
sans dépendance hardware.
Il existe un portage pour Windows de l'interface TAP et elle semble fonctionner dans ReactOS, mais
le système ne gère pas pleinement le Plug'n'Play à ce niveau là. En effet, pour pouvoir installer et
instancier une interface TAP dans ReactOS, un reboot du système est nécessaire... autant dire que
ce n'est absolument pas envisageable pour les namespaces qui sont totalement dynamiques et, qui plus
est dans mon implémentation, volatiles.
\newline
J'ai passé beaucoup (trop) de temps sur la tentative de faire fonctionner l'interface TAP pour pouvoir
l'utiliser comme pièce maîtresse du mécanisme d'interface virtuelle.
\newline
\subsubsection{Interfaces virtuelles NDIS}
Les interfaces virtuelles dans NDIS sont des pilotes Miniport qui ne font pas d'appels au hardware
(pas de handlers d'interruption, etc.). Tout s'enregistre de la même façon, excepté que l'appel
à l'instanciation du driver ne se fait pas quand le hardware est détecté (vu qu'il n'y en a pas) mais
bien manuellement ou automatiquement quand NDIS est prêt de son côté.
\newline
Lors de l'enregistrement d'une interface Miniport, la structure de configuration contient
un champ \call{InitializeHandler}. Le problème actuellement dans l'implémentation de NDIS est que
ce handler n'est appelé \textbf{que} lors d'un appel du Plug'n'Play manager « hardware ». Du coup
l'interface TAP (ou n'importe quelle interface sans correspondance hardware) n'est jamais
instanciée (à noter que dans le gestionnaire de périphériques, l'interface apparaît bien et est
notée comme fonctionnelle (mais encore une fois, cette partie n'est sans doute pas bien implémentée)).
\newpage
\subsection{Modifications collatérales}
Durant le développement général, je suis tombé plusieurs fois sur des cas qui ne sont pas directement liés
à l'implémentation des namespaces. Certains cas m'ont bloqué, d'autres sont totalement inutiles mais
intéressants d'un point de vue technique.
\subsubsection{Mode Promiscuous dans le driver pcnet}
\label{sub:extrapcnet}
En regardant le code du driver pcnet, le mode promiscuous est bien défini dans le header du driver
(\file{/drivers/network/dd/pcnet/pcnethw.h}) mais le define \call{CSR15\_PROM} n'est utilisé
nulle part dans le code. Du coup, j'ai forcé son utilisation durant
l'initialisation de l'interface, cela se passe dans la fonction \call{MiInitChip} dans
\file{/drivers/network/dd/pcnet/pcnet.c} ligne 742.
\begin{listing}[H]
\caption{/drivers/network/dd/pcnet/}
\begin{ccode}
//
// pcnethw.h
//
#define CSR15_DRX 0x1 /* disable receiver */
#define CSR15_DTX 0x2 /* disable transmitter */
#define CSR15_LOOP 0x4 /* loopback enable */
// [...]
#define CSR15_DRCVBC 0x4000 /* disable receive broadcast */
#define CSR15_PROM 0x8000 /* promiscuous mode */
//
// pcnet.c MiInitChip(): le CSR15 étant initialisé à 0x00
//
NdisRawWritePortUshort(Adapter->PortOffset + RAP, CSR15);
NdisRawWritePortUshort(Adapter->PortOffset + RDP, CSR15_PROM);
\end{ccode}
\end{listing}
\subsubsection{Changer la couleur du Blue Screen}
Quand on développe dans le kernel, on fini toujours par tomber au moins une (ou 10... ou 50...) fois sur un
blue screen parce qu'on a déréférencé un pointeur NULL (ou qu'on a essayé de charger une font TrueType dans le
kernel par exemple). A force, le bleu du BSOD ça lasse un peu. Puis, n'importe quel geek a toujours voulu
au moins une fois dans sa vie changer la couleur de fond d'un Blue Screen, c'est tellement fun.
\newline
On a l'avantage ici d'avoir le code source du kernel, alors ça doit être facile de changer ça. Suffit de chercher
un peu comment le Blue Screen est généré. Dans le debugger kernel intégré au système, une commande permet de tester
si l'équivalent du \call{panic()} sous Linux fonctionne. C'est \call{bugcheck}. Cette commande génère un Blue Screen
tout simplement. De là, facile de savoir où le code se trouve.
\newline
L'affichage du Blue Screen se trouve dans \file{/ntoskrnl/ke/bug.c}, j'ai dû parcourir le fichier et lire un
peu ce qui s'y trouve pour tomber sur un appel à \call{InbvSolidColorFill}. En changeant un peu les valeurs en
paramètres aléatoirement, j'ai fini par trouver que le dernier paramètre est la couleur de fond. J'ai changé le
\hexa{0x02} en \hexa{0x04} et je me suis retrouvé avec un Blue Screen... vert.
\begin{figure}[!h]
\caption{Green Screen of Death}
\centering
\includegraphics[scale=0.6]{GSOD.eps}
\end{figure}
\newpage
\label{an:tcp}
\subsection{Comparaison des interfaces graphiques de gestion des protocoles}
Comme dit dans le développement, la gestion (activation/désactivation) d'un ProtocolDriver
n'est pas finie et l'interface graphique (par exemple) de ReactOS manque cruellement de possibilités.
\begin{figure}[!h]
\caption{VirtualBox Bridged Networking Driver (NDIS ProtocolDriver)}
\centering
\includegraphics[scale=0.8]{vbox-bridged.eps}
\end{figure}
\begin{figure}[!h]
\caption{Interface graphique non finalisée dans ReactOS}
\centering
\includegraphics[scale=0.6]{reactos-ncpacpl.eps}
\end{figure}
\end{document}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment