Avec l’émergence de technologies de containérisation légères comme Docker, il est devenu très simple et habituel de réaliser des tests divers et variés sur un ensemble isolé de processus. Si les outils sur lesquels reposent les containers sont multiples, l’un des piliers de ce procédé est l’utilisation de namespaces Linux.
Ces derniers ne datent pas d’hier ! Le terme « name spaces » a été introduit pour la première fois en 1992, il s’agit alors de ceux du Plan 9 de Bell Labs. Côté Linux c’est en 2002 qu’Al Viro implémente avec le patch 2.4.19 une première idée de ce que seront les namespaces. Première puisque ce qui était à l’origine prévu pour n’être qu’un élément finira par être (à ce jour) un ensemble de 7 namespaces, chacun introduits au fur et à mesure de patchs du kernel à partir de 2006.
L’idée est simple: un ensemble de ressources systèmes est enveloppé et tous les processus de cet ensemble ont la sensation (les processus ont une âme après tout) d’être isolés du reste du système. A l’initialisation de ce dernier, un namespace de chaque type est créé et utilisé par tous les processus, qui peuvent créer ou rejoindre d’autres namespaces. Le type en question faisant parti de la liste ci-dessous.
PID
A l’origine, les processus Linux n’étaient seulement qu’organisés sous forme d’arbre. Chaque processus possède donc un père, et le père de tous, de PID 1, est init
. Celui-ci se trouve au sommet de l’arbre. Tous ces processus ont donc bien évidemment un droit d’inspection sur les autres processus, et peuvent même les tuer !
Avec les namespaces a été introduit la notion d’isolation de groupes de processus. Un processus appartenant à un groupe n’est plus en mesure d’interagir de quelconque manière avec un processus d’un autre groupe. Les processus enfants d’un namespace ne connaissent pas l’existence de leurs « vrais » processus parents, à l’inverse des parents qui ont une vue complète des enfants du namespace. Il est bien entendu possible pour un processus enfant présent dans un namespace de lui même avoir un namespace pour enfant.
Tout cela implique, brièvement, que deux processus peuvent avoir le même PID si ils ne sont pas dans le même namespace, le père du namespace étant d’ailleurs de PID 1 pour ses enfants, comme init
.
Les PID namespaces ont été introduits avec la version 2.6.24
Network
Les processus du namespace ont un ensemble de ressources réseau isolées, adresse IP, table de routage IP, répertoire /proc/net
, ports… Est donc visible un ensemble d’interfaces différentes des autres namespaces (même la loopback
).
Un interêt que l’on peut voir à ce namespace est lié à la containérisation. Chaque container ayant son propre réseau virtuel, il devient alors possible que celui-ci ait aussi un ensemble d’applications liées à plusieurs de ses ports. Par exemple, il serait possible d’avoir plusieurs serveurs web conteneurisés sur le même hôte, avec chaque port 80/443 dans son propre network namespace (par conteneur donc).
Les network namespaces ont été introduits avec la version 2.6.24 et fortement remaniés à la 2.6.29
Mount
Toutes les données liées aux points de montage (quelles partitions disque sont montées, où, leur permission…) du système peuvent être clonées en un namespace et modifiées par la suite sans affecter l’original. Un processus que l’on clone offrira donc une copie identique de son arbre de fichiers au nouveau processus et les changements sur l’un ne se répercuteront pas sur l’autre. Comme pour tout namespace, la liste des points de montage vue par les processus internes est isolée du système. En d’autres termes, chaque mount namespace a sa propre vue de la liste de points de montage qu’il peut manipuler. C’est assez proche de l’idée du chroot()
jail, mais bien plus sécurisé parce que réellement isolé (cela dit…).
Cependant avoir des hiérarchies de fichiers distinctes et confinées peut parfois être trop restrictif. Imaginons qu’un device soit monté après clonage sur l’objet original cloné, le clone n’aura pas accès à ce montage. Si besoin, iI faudrait donc le monter de nouveau dans le clone. C’est ainsi qu’a été inventé (entre autres utilisations complexes…) la propagation entre mount namespaces.
Si deux objets montés ont une relation partagée, un événement qui a lieu sur l’un peut se répercuter sur l’autre, on parle de shared mount. Si deux objets montés ont une relation de maître-esclave, la répercussion ne se fera que dans un sens (du maître à l’esclave seulement), on parle de slave mount. Si un objet ne reçoit ni ne partage, on parle de private mount. Enfin si un objet partage les caractéristiques du private mount et que l’on ne peut rien y attacher, on parle de unbindable mount. Les informations relatives au point de montage se trouvent dans /proc/(PID)/mountinfo
du PID correspondant.
Les mount namespaces ont été introduits avec la version 2.4.19 du kernel.
User
Par leurs UID et GID, les processus en question peuvent être isolés via un user namespace. En d’autres termes, l’UID
et GID
d’un processus sera différent entre son namespace et l’extérieur. L’une des principales utilité à cette fonction est la possibilité qu’un utilisateur n’ait pas les mêmes privilèges au sein du système hôte que dans son namespace: typiquement, avoir un UID
de 0 (root) dans le namespace. A noter qu’un utilisateur sans privilège particulier a la possibilité de créer un user namespace, chose unique comme nous le verrons par la suite.
Ces derniers ont été introduits avec le patch 2.6.23 et ont été fortement complétés avec le 3.8
UNIX Time-sharing System
Possibilité d’isoler nom de domaine et hôte (donné par la commande uname
) du reste du système avec les appels systèmes sethostname()
et setdomainname()
. Un interêt peut être celui de vouloir des noms uniques ou différents de l’hôte pour certaines applications, à tout hasard, un container.
Ils ont été introduit avec le patch 2.6.30
Inter Processus Communication
Chaque namespace IPC à son propre ensemble de ressources de communications inter processeur basé sur SYSTEM V
ou POSIX
message queue filesystem, qui ne dépend plus des chemins des fichiers.
Leur introduction s’est faite avec la version 2.6.19
cgroup
Un cgroup namespace possède son propre répertoire root cgroup.
Pour plus d’informations concernant les cgroups, je vous renvoie vers l’article de Quentin.
Dernier namespace a avoir été ajouté, lors de la version 4.6
Implémentation
L’implémentation des namespaces au sein du kernel (v5.5.5) est tout simplement faite à base de structures. On a ainsi pour chaque namespace correspondant un fichier header présent dans /include/linux
et l’accès à ces structures est géré par la structure nsproxy du fichier header /include/linux/nsproxy.h
Ci-dessous, l’aperçu de cette structure.
/*
* A structure to contain pointers to all per-process
* namespaces - fs (mount), uts, network, sysvipc, etc.
*
* The pid namespace is an exception -- it's accessed using
* task_active_pid_ns. The pid namespace here is the
* namespace that children will use.
*
* 'count' is the number of tasks holding a reference.
* The count for each namespace, then, will be the number
* of nsproxies pointing to it, not the number of tasks.
*
* The nsproxy is shared by tasks which share all namespaces.
* As soon as a single namespace is cloned or unshared, the
* nsproxy is copied.
*/
struct nsproxy {
atomic_t count;
struct uts_namespace *uts_ns;
struct ipc_namespace *ipc_ns;
struct mnt_namespace *mnt_ns;
struct pid_namespace *pid_ns_for_children;
struct net *net_ns;
struct cgroup_namespace *cgroup_ns;
};
extern struct nsproxy init_nsproxy;
L’API namespaces repose sur 3 appels systèmes:
- clone pour créer un nouveau processus.
- setns pour autoriser le processus à rejoindre un namespace existant.
- unshare pour déplacer le processus dans un nouveau namespace.
Les fonctions appelées suite à ces appels systèmes se trouvent toutes dans /kernel/nsproxy.c
Pour clone:
/*
* called from clone. This now handles copy for nsproxy and all
* namespaces therein.
*/
int copy_namespaces(unsigned long flags, struct task_struct *tsk)
{ ... }
Pour unshare:
/*
* Called from unshare. Unshare all the namespaces part of nsproxy.
* On success, returns the new nsproxy.
*/
int unshare_nsproxy_namespaces(unsigned long unshare_flags,
struct nsproxy **new_nsp, struct cred *new_cred, struct fs_struct *new_fs)
{ ... }
Et enfin pour setns:
SYSCALL_DEFINE2(setns, int, fd, int, nstype)
{ ... }
En complément de ces appels existent 7 flags, chacun étant évidemment utilisé pour spécifier sur quel namespace appliquer l’appel.
Flag | Capability nécessaire |
CLONE_NEWNS | CAP_SYS_ADMIN |
CLONE_NEWUTS | CAP_SYS_ADMIN |
CLONE_NEWIPC | CAP_SYS_ADMIN |
CLONE_NEWUSER | Aucune |
CLONE_NEWPID | CAP_SYS_ADMIN |
CLONE_NEWNET | CAP_SYS_ADMIN |
CLONE_NEWCGROUP | CAP_SYS_ADMIN |
Comme mentionner précédemment, on notera le cas spécial de l’absence de privilège nécessaire pour créer un user namespace. Si en théorie tout est fait pour que cette fonctionnalité soit sécurisée, le fait qu’une partie du code pensée pour le root devienne tout d’un coup accessible à un simple utilisateur pose bien des problèmes, en témoigne les nombreuses failles de sécurité liées à ce namespace comme celle-ci ou bien cette autre.
(Plus amusant, on notera le _NEWNS
pour les mount namespaces, premier namespace introduit comme étant alors le supposé seul et ayant donc un nom général).
Manipulation
La manipulation des namespaces se faisant par appels systèmes, cette dernière peut se faire de multiples manières : ligne de commande, code d’assez bas niveau orienté système comme le C ou le Go…
Amusons nous un peu avec le network namespace et ce facilement grâce à la commande ip
.
Tout comme les humains, un namespace a besoin d’un nom, le notre s’appellera darlene
, les namespaces féminins étant trop peu représentés.
Pour créer darlene
, rien de plus simple:
Cependant, ping l’adresse locale de darlene
permet de voir que rien n’est prêt à l’emploi, la loopback
elle même est DOWN
.
La création du namespace n’est en effet que le début, il va désormais falloir s’attaquer à l’assignement d’interfaces puis par la suite à la configuration de la connectivité du réseau.
Commençons par donner vie à la loopback
.
Il s’agit ensuite comme dit plus haut d’ajouter un peu de connectivité. Pour se faire nous allons assigner un réseau virtuel ethernet (veth
), fonctionnant par paire (a noter que l’on pourrait tout à fait faire le choix d’y ajouter un réseau physique via un bridge), pour que tout paquet arrivant à l’un arrive aussi à l’autre. L’idée étant qu’un membre de la paire soit lié au namespace tandis que l’autre est connecté au « vrai monde », le global namespace.
Il est temps d’assigner un membre de la paire à darlene
.
L’interface est encore une fois DOWN
, occupons nous en.
Nous y sommes presque, il ne reste plus qu’à donner une plage d’adresses à veth1
et à le mettre en route.
C’est enfin prêt, une connectivité a été établie entre darlene
et le namespace global. De nombreuses choses restes encore à faire, on pourrait par exemple connecter darlene
à internet.
Présenter une manipulation détaillée de tous les namespaces dans cet article serait beaucoup trop long, mais il est important d’avoir à l’esprit que l’exemple donné ci-dessus est similaire aux opérations réalisées pour la containérisation, pensez-y la prochaine fois que vous lancerez Docker !
Ping : Namespaces Linux et relation avec Docker – Sysblog