Tuto 4 ready

This commit is contained in:
nemunaire 2021-10-31 20:51:17 +01:00
commit c960136430
18 changed files with 536 additions and 340 deletions

View file

@ -1,142 +1,28 @@
\newpage
Les espaces de noms -- *namespaces* {#namespaces}
===================================
Utiliser les *namespace*s
-------------------------
## Introduction
### S'isoler dans un nouveau *namespace*
Les espaces de noms du noyau, les *namespaces*, permettent de
dupliquer certaines structures, habituellement considérées uniques
pour le noyau, dans le but de les isoler d'un groupe de processus à un
autre.
Si l'on voit l'isolation procurée par les *namespace*s en parallèle avec les
machines virtuelle, on peut se dire qu'il suffit d'exécuter un appel système
pour arriver dans un conteneur bien isolé. Cependant, le choix fait par les
développeurs de Linux a été de laisser le choix des espaces de noms dont on veut
se dissocier.
On en dénombre sept (le dernier ayant été ajouté dans Linux 4.6) : `cgroup`,
`IPC`, `network`, `mount`, `PID`, `user` et `UTS`.
L'intérêt principal de cette approche, exploitée bien après la mise en avant du
concept, est que l'utilisation des *namespace*s ne se limite pas seulement à
des machines virtuelles légères. On retrouve ainsi dans Google Chrome de
nombreuses utilisations des *namespace*s dans le simple but d'accroître la
sécurité de leur navigateur. Ainsi, les threads de rendu n'ont pas accès au
réseau et sont cloisonnés de manière transparente pour l'utilisateur.
La notion d'espace de noms est relativement nouvelle et a été intégrée
progressivement au sein du noyau Linux. Aussi, toutes les structures
ne sont pas encore *containerisables* :
[le document fondateur](https://www.kernel.org/doc/ols/2006/ols2006v1-pages-101-112.pdf)
parle ainsi d'isoler les périphériques, ou encore l'horloge. Pour ce
dernier,
[un patch a même déjà été proposé](https://lwn.net/Articles/766089/).
### L'espace de noms `mount` {#mount-ns}
Depuis Linux 2.4.19.
Cet espace de noms isole la liste des points de montage.
Chaque processus appartenant à un *namespace mount* différent peut monter,
démonter et réorganiser à sa guise les points de montage, sans que cela n'ait
d'impact sur les processus hors de cet espace de noms. Une partition ne sera
donc pas nécessairement démontée après un appel à `umount(2)`, elle le sera
lorsqu'elle aura effectivement été démontée de chaque *namespace mount* dans
lequel elle était montée.
Attention il convient cependant de prendre garde aux types de liaison existant
entre vos points de montage (voir la partie sur
[les particularités des points de montage](#mount)), car les montages et
démontages pourraient alors être répercutés dans l'espace de noms parent.
Une manière rapide pour s'assurer que nos modifications ne sortiront pas de
notre *namespace* est d'appliquer le type esclave à l'ensemble de nos points de
montage, récursivement, dès que l'on est entré dans notre nouvel espace de
noms.
<div lang="en-US">
```bash
mount --make-rslave /
```
</div>
Nous allons voir dans cette partie plusieurs méthodes pour utiliser ces espaces
de noms.
### L'espace de noms `UTS` {#uts-ns}
Depuis Linux 2.6.19.
Cet espace de noms isole le nom de machine et son domaine NIS.
### L'espace de noms `IPC` {#ipc-ns}
Depuis Linux 2.6.19.
Cet espace de noms isole les objets IPC et les files de messages POSIX.
Une fois le *namespace* attaché à un processus, il ne peut alors plus parler
qu'avec les autres processus de son espace de noms (lorsque ceux-ci passent par
l'API IPC du noyau).
### L'espace de noms `PID`
Depuis Linux 2.6.24.
Cet espace de noms isole la liste des processus et virtualise leurs numéros.
Une fois dans un espace, le processus ne voit que le sous-arbre de processus
également attachés à son espace. Il s'agit d'un sous-ensemble de l'arbre global
de PID : les processus de tous les PID *namespaces* apparaissent donc dans
l'arbre initial.
Pour chaque nouvel espace de noms de processus, une nouvelle numérotation est
initiée. Ainsi, le premier processus de cet espace porte le numéro 1 et aura
les mêmes propriétés que le processus `init` usuel\ ; entre autre, si un
processus est rendu orphelin dans ce *namespace*, il devient un fils de ce
processus, et non un fils de l'`init` de l'arbre global.
### L'espace de nom `network`
Depuis Linux 2.6.29.
Cet espace de noms fournit une isolation pour toutes les ressources associées
aux réseaux : les interfaces, les piles protocolaires IPv4 et IPv6, les tables
de routage, règles pare-feu, ports numérotés, etc.
Une interface réseau (`eth0`, `wlan0`, ...) ne peut se trouver que dans un seul
espace de noms à la fois. Il est par contre possible de les déplacer.
Lorsque le *namespace* est libéré (généralement lorsque le dernier processus
attaché à cet espace de noms se termine), les interfaces qui le composent sont
ramenées dans l'espace initial/racine (et non pas dans l'espace parent, en cas
d'imbrication).
### L'espace de noms `user`
Depuis Linux 3.8.
Cet espace de noms isole la liste des utilisateurs, des groupes, leurs
identifiants, les *capabilities*, la racine et le trousseau de clefs du noyau.
La principale caractéristique est que les identifiants d'utilisateur et de
groupe pour un processus peuvent être différents entre l'intérieur et
l'extérieur de l'espace de noms. Il est donc possible, alors que l'on est un
simple utilisateur à l'extérieur du *namespace*, d'avoir l'UID 0 dans le
conteneur.
### L'espace de noms `cgroup` {#cgroup-ns}
Depuis Linux 4.6.
Cet espace de noms filtre l'arborescence des *Control Group* en changeant la
racine de l'arborescence des cgroups. Au sein d'un *namespace*, la racine vue
correspond en fait à un sous-groupe de l'arborescence globale.
Ainsi, un processus dans un `CGroup` *namespace* ne peut pas voir le contenu
des sous-groupes parents (pouvant laisser fuiter des informations sur le reste
du système). Cela peut également permettre de faciliter la migration de
processus (d'un système à un autre) : l'arborescence des cgroups n'a alors
plus d'importance car le processus ne voit que son groupe.
## S'isoler dans un nouveau *namespace*
### Avec son coquillage
#### Avec son coquillage\
De la même manière que l'on peut utiliser l'appel système `chroot(2)` depuis un
shell via la commande `chroot(1)`, la commande `unshare(1)` permet de faire le
@ -169,13 +55,16 @@ Nous avons pu ici modifier le nom de la machine, sans que cela n'affecte notre
machine hôte.
### Les appels systèmes
#### Les appels systèmes\
L'appel système par excellence pour contrôler l'isolation d'un nouveau
processus est `clone(2)`.
L'isolement ou non du processus est faite en fonction des `flags` qui sont
passés à la fonction :
Ce *syscall*, propre à Linux, crée habituellement un nouveau processus (mais
aussi des threads) enfant de notre processus courant, comme `fork(2)` (qui lui
est un appel système POSIX) mais prend en plus de nombreux
paramètres. L'isolement ou non du processus se fait en fonction des *flags* qui
sont passés :
* `CLONE_NEWNS`,
* `CLONE_NEWUTS`,
@ -183,16 +72,18 @@ passés à la fonction :
* `CLONE_NEWPID`,
* `CLONE_NEWNET`,
* `CLONE_NEWUSER`,
* `CLONE_NEWCGROUP`.
* `CLONE_NEWCGROUP`,
* `CLONE_NEWTIME`.
On peut bien entendu cumuler un ou plusieurs de ces `flags`, et les combiner
avec d'autres `flags` attendu par la fonction.
On peut bien entendu cumuler un ou plusieurs de ces *flags*, et les combiner
avec d'autres attendus par `clone(2)`.
Les mêmes `flags` sont utilisés lors des appels à `unshare(2)` ou `setns(2)`.
Ces mêmes *flags* sont utilisés lors des appels à `unshare(2)` ou `setns(2)`,
que nous verrons plus tard.
Pour créer un nouveau processus qui sera à la fois dans un nouvel espace de
noms réseau et dans un nouveau *namespace* `cgroup`, on écrirait un code
similaire à :
noms réseau et dans un nouveau *namespace* `cgroup`, on écrirait un code C
semblable à :
<div lang="en-US">
```c
@ -217,102 +108,30 @@ Un exemple complet d'utilisation de `clone(2)` et du *namespace* `UTS` est
donné dans le `man` de l'appel système.
## Rejoindre un *namespace*
##### `unshare`\
Rejoindre un espace de noms se fait en utilisant l'appel système `setns(2)`,
auquel on passe le *file descriptor* d'un des liens du dossier
`/proc/<PID>/ns/` :
L'appel système `clone(2)` va créer un nouveau processus, ou un nouveau
thread. Parfois, on souhaite faire entrer notre processus ou thread en cours
dans un nouvel espace de noms. Dans ce cas, on utilisera l'appel système
`unshare(2)`. Celui-ci prend uniquement en argument une liste de *flags* des
*namespace*s dont on souhaite se dissocier.
<div lang="en-US">
```c
#define _GNU_SOURCE
#include <fcntl.h>
#include <sched.h>
#include <stdlib.h>
::::: {.warning}
Le comportement de `unshare(2)` (ou `unshare(3)`) avec les *namespace*s *PID*
et *Time* n'est pas celui que l'on peut attendre !
// ./a.out /proc/PID/ns/FILE cmd args...
En effet, après avoir appelé `unshare`, le processus reste tout de même dans
son *namespace* *Time* ou *PID* d'origine, seuls ses enfants (après un appel à
`fork(2)` ou `clone(2)` par exemple) seront dans le nouveau *namespace*.
int
main(int argc, char *argv[])
{
int fd = open(argv[1], O_RDONLY);
if (fd == -1)
{
perror("open");
return EXIT_FAILURE;
}
Cela poserait trop de problème de faire changer le PID d'un processus en cours
d'exécution, de même qu'il serait impensable que la `CLOCK_MONOTONIC` puisse
faire un saut en avant ou en arrière. Alors le choix qui a été fait est que
seuls les fils créés après l'appel à `unshare(2)` seront concrètement dans le
nouveau *namespace*. C'est dans cette situation que `pid` et `pid_for_children`
peuvent être différents dans le dossier `/proc/<PID>/ns`.
if (setns(fd, 0) == -1)
{
perror("setns");
return EXIT_FAILURE;
}
execvp(argv[2], &argv[2]);
perror("execve");
return EXIT_FAILURE;
}
```
</div>
Dans un shell, on utilisera la commande `nsenter(1)` :
<div lang="en-US">
```bash
42sh# nsenter --uts=/proc/42/ns/uts /bin/bash
```
</div>
## Durée de vie d'un *namespace* {#ns-lifetime}
Le noyau tient à jour un compteur de références pour chaque *namespace*. Dès
qu'une référence tombe à 0, l'espace de noms est automatiquement libéré, les
points de montage sont démontés, les interfaces réseaux sont réattribués à
l'espace de noms initial, ...
Ce compteur évolue selon plusieurs critères, et principalement selon le nombre
de processus qui l'utilise. C'est-à-dire que, la plupart du temps, le
*namespace* est libéré lorsque le dernier processus s'exécutant dedans se
termine.
Lorsque l'on a besoin de référencer un *namespace* (par exemple pour le faire
persister après le dernier processus), on peut utiliser un `mount bind` :
<div lang="en-US">
```bash
42sh# touch /tmp/ns/myrefns
42sh# mount --bind /proc/<PID>/ns/mount /tmp/ns/myrefns
```
</div>
De cette manière, même si le lien initial n'existe plus (si le `<PID>` s'est
terminé), `/tmp/ns/myrefns` pointera toujours au bon endroit.
On peut très bien utiliser directement ce fichier pour obtenir un descripteur
de fichier valide vers le *namespace* (pour passer à `setns(2)`).
### Faire persister un *namespace*
Il n'est pas possible de faire persister un espace de noms d'un reboot à
l'autre.
Même en étant attaché à un fichier du disque, il s'agit d'un pointeur vers une
structure du noyau, qui ne persistera pas au redémarrage.
## Aller plus loin {-}
Je vous recommande la lecture des *man* suivants :
* `namespaces(7)` : introduisant et énumérant les *namespaces* ;
Pour tout connaître en détails, [la série d'articles de Michael Kerrisk sur
les *namespaces*](https://lwn.net/Articles/531114/) est excellente ! Auquel il
faut ajouter [le petit dernier sur le `cgroup`
*namespace*](https://lwn.net/Articles/621006/).
[Cet article de Michael Crosby montrant l'utilisation de clone(2)](https://web.archive.org/web/20190206073558/http://crosbymichael.com/creating-containers-part-1.html)
est également des plus intéressants, pour ce qui concerne la programmation
plus bas-niveau.
On ne remarque pas cette bizarrerie avec `clone(2)`, car il crée déjà un
nouveau processus, son PID et sa `CLOCK_MONOTONIC` sont directement à la bonne
valeur dès l'exécution de la fonction `fn`.
:::::