tuto4 ready

This commit is contained in:
nemunaire 2022-11-11 10:14:16 +01:00
parent b5de41662b
commit e928733d61
17 changed files with 794 additions and 208 deletions

Binary file not shown.

View File

@ -1,6 +1,6 @@
include ../pandoc-opts.mk include ../pandoc-opts.mk
SOURCES_TUTO = tutorial.md mount.md intro.md setup.md howto.md setns.md setns-samples.md cmpns.md lifetime.md namespaces_more.md networkns.md pidns.md mountns.md userns.md docker-exec.md rendu.md SOURCES_TUTO = tutorial.md mount.md intro.md setup.md lsns.md howto.md setns.md setns-samples.md setns-pid.md cmpns.md lifetime.md namespaces_more.md ../new-page.md networkns.md ../new-page.md pidns.md ../new-page.md mountns.md ../new-page.md userns.md docker-exec.md rendu.md
all: tutorial.pdf all: tutorial.pdf

View File

@ -5,20 +5,23 @@
Les *namespaces* d'un programme sont exposés sous forme de liens symboliques Les *namespaces* d'un programme sont exposés sous forme de liens symboliques
dans le répertoire `/proc/<PID>/ns/`. dans le répertoire `/proc/<PID>/ns/`.
Deux programmes qui partagent un même *namespace* auront un lien vers la même Deux programmes qui partagent un même *namespace* auront un lien vers le même
structure de données. *inode*.
Écrivons un script ou un programme, `cmpns`, permettant de déterminer si deux Écrivons un script, `cmpns`, permettant de déterminer si deux programmes
programmes s'exécutent dans les mêmes *namespaces*. On ignorera les s'exécutent dans les mêmes *namespaces*. On ignorera les *namespace*s
*namespace*s `*_for_children`, car ils ne font pas partie du cycle d'exécution `*_for_children`, car ils ne font pas partie du cycle d'exécution que l'on
que l'on cherche à comparer. cherche à comparer.
En shell, vous aurez besoin de `grep(1)` et de `readlink(1)`.
#### Exemples {.unnumbered} #### Exemples {.unnumbered}
<div lang="en-US"> <div lang="en-US">
``` ```
42sh$ ./cmpns $(pgrep influxdb) $(pgrep init) 42sh$ docker run -d influxdb
42sh$ ./cmpns $(pgrep influxd) $(pgrep init)
- cgroup: differ - cgroup: differ
- ipc: differ - ipc: differ
- mnt: differ - mnt: differ

View File

@ -1,28 +1,27 @@
\newpage
Utiliser les *namespace*s Utiliser les *namespace*s
------------------------- -------------------------
### S'isoler dans un nouveau *namespace* ### S'isoler dans un nouveau *namespace*
Si l'on voit l'isolation procurée par les *namespace*s en parallèle avec les Si l'on voit l'isolation procurée par les *namespace*s comme des machines
machines virtuelles, on peut se dire qu'il suffit d'exécuter un appel système virtuelles, on peut se dire qu'il suffit d'exécuter un appel système pour
pour arriver dans un conteneur bien isolé. Cependant, le choix fait par les 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 développeurs de Linux a été de laisser le choix des espaces de noms dont on
se dissocier. veut se dissocier.
L'intérêt principal de cette approche, exploitée bien après la mise en avant du 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 à 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 des machines virtuelles légères. On retrouve ainsi dans Google Chrome et
nombreuses utilisations des *namespace*s dans le simple but d'accroître la Firefox de nombreuses utilisations des *namespace*s dans le simple but
sécurité de leur navigateur. Ainsi, les threads de rendu n'ont pas accès au d'accroître la sécurité de leur navigateur. Ainsi, les *threads* de rendu n'ont
réseau et sont cloisonnés de manière transparente pour l'utilisateur. pas accès au réseau et sont cloisonnés de manière transparente pour
l'utilisateur.
Nous allons voir dans cette partie plusieurs méthodes pour utiliser ces espaces Nous allons voir dans cette partie plusieurs méthodes pour utiliser ces espaces
de noms. de noms.
#### Dans son shell #### Dans son shell\
De la même manière que l'on peut utiliser l'appel système `chroot(2)` depuis un 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 shell via la commande `chroot(1)`, la commande `unshare(1)` permet de faire le
@ -55,16 +54,16 @@ Nous avons pu ici modifier le nom de la machine, sans que cela n'affecte notre
machine hôte. machine hôte.
#### Les appels système #### Les appels système\
L'appel système par excellence pour contrôler l'isolation d'un nouveau L'appel système par excellence pour contrôler l'isolation d'un nouveau
processus est `clone(2)`. processus est `clone(2)`.
Ce *syscall*, propre à Linux, crée habituellement un nouveau processus (mais Ce *syscall*, propre à Linux, crée habituellement un nouveau processus enfant
aussi des threads) enfant de notre processus courant, comme `fork(2)` (qui lui de notre processus courant (mais il peut aussi créer des *threads*, on va voir
est un appel système POSIX) mais prend en plus de nombreux qu'il fait beaucoup de choses), comme `fork(2)` (qui lui est un appel système
paramètres. L'isolement ou non du processus se fait en fonction des *flags* qui POSIX). Mais il prend en plus de nombreux paramètres. L'isolement ou non du
sont passés : processus se fait en fonction des *flags* qui sont passés. On retrouve donc :
* `CLONE_NEWNS`, * `CLONE_NEWNS`,
* `CLONE_NEWUTS`, * `CLONE_NEWUTS`,
@ -75,12 +74,29 @@ sont passés :
* `CLONE_NEWCGROUP`, * `CLONE_NEWCGROUP`,
* `CLONE_NEWTIME`. * `CLONE_NEWTIME`.
::::: {.more}
Le nom du *flag* `CLONE_NEWNS` est historique et assez peu explicite
contrairement aux autres : il désigne en fait l'espace de nom `mount`.
Au départ, les *namespace*s ont étés pensés pour former un tout : une couche
d'isolation complète pour les processus. Mais lors des développements suivants,
il s'est avéré pratique de pouvoir choisir finement de quels aspects on
souhaitait se dissocier.
C'est ainsi que pour chaque nouveau *namespace*, un nouveau *flag* est
introduit.
:::::
On peut bien entendu cumuler un ou plusieurs de ces *flags*, et les combiner On peut bien entendu cumuler un ou plusieurs de ces *flags*, et les combiner
avec d'autres attendus par `clone(2)`. avec d'autres attendus par `clone(2)`.
Ces 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. que nous verrons plus tard.
::::: {.code}
Pour créer un nouveau processus qui sera à la fois dans un nouvel espace de 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 C noms réseau et dans un nouveau *namespace* `cgroup`, on écrirait un code C
semblable à : semblable à :
@ -105,21 +121,34 @@ pid_t pid = clone(do_execvp, // First function executed by child
Dans cet exemple, le processus fils créé disposera d'un nouvel espace de noms Dans cet exemple, le processus fils créé disposera d'un nouvel espace de noms
pour les *CGroups* et disposera d'une nouvelle pile réseau. pour les *CGroups* et disposera d'une nouvelle pile réseau.
Un exemple complet d'utilisation de `clone(2)` et du *namespace* `UTS` est ::::: {.question}
donné dans le `man` de l'appel système.
#### Quel est le rôle du *flag* `SIGCHLD` ? {-}
\
Lorsque l'on crée un nouveau processus, on ajoute l'option `SIGCHLD` afin
d'être notifié par signal lorsque notre processus fil a terminé son
exécution. Cela permet d'être réveillé de notre `wait(2)`.
:::::
:::::
L'appel système `clone(2)` va donc créer un nouveau processus, ou un nouveau
*thread*, mais parfois on souhaite juste isoler notre processus actuel.
##### `unshare`\ ##### `unshare`\
L'appel système `clone(2)` va créer un nouveau processus, ou un nouveau Lorsque l'on souhaite faire entrer notre processus courant ou notre *thread*
thread. Parfois, on souhaite faire entrer notre processus ou thread en cours dans un nouvel espace de noms, on peut utiliser l'appel système
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 `unshare(2)`. Celui-ci prend uniquement en argument une liste de *flags* des
*namespace*s dont on souhaite se dissocier. *namespace*s dont on souhaite se dissocier.
::::: {.warning} ::::: {.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 ! **Le comportement de `unshare(2)` (ou `unshare(3)`) avec les *namespace*s *PID*
et *Time* n'est pas celui que l'on peut attendre !**\
En effet, après avoir appelé `unshare`, le processus reste tout de même dans 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 à son *namespace* *Time* ou *PID* d'origine, seuls ses enfants (après un appel à
@ -135,4 +164,8 @@ peuvent être différents dans le dossier `/proc/<PID>/ns`.
On ne remarque pas cette bizarrerie avec `clone(2)`, car il crée déjà un 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 nouveau processus, son PID et sa `CLOCK_MONOTONIC` sont directement à la bonne
valeur dès l'exécution de la fonction `fn`. valeur dès l'exécution de la fonction `fn`.
::::: :::::
Nous avons vu comment créer et nous dissocier d'un espace de nom. Maintenant
voyons comment en rejoindre un déjà existant.

View File

@ -10,17 +10,18 @@ notre machine.
Ces fonctionnalités sont très utiles pour éviter les dénis de service, mais nos Ces fonctionnalités sont très utiles pour éviter les dénis de service, mais nos
processus ne sont pas particulièrement isolés du reste du système. On aimerait processus ne sont pas particulièrement isolés du reste du système. On aimerait
maintenant que nos processus n'aient pas accès à l'ensemble des fichiers, ne maintenant que nos processus n'aient pas accès à l'ensemble des fichiers, ne
puissent pas interagir avec les autres processus, avoir sa propre pile puissent pas interagir avec les autres processus, avoir leur propre pile
réseau, ... Voyons maintenant les *namespaces* qui vont nous permettre de faire réseau, ... Voyons maintenant les *namespaces* qui vont nous permettre de faire
cela. cela.
Initiation rapide Présentation des *namespaces*
----------------- -----------------------------
Les espaces de noms du noyau, que l'on appelle *namespaces*, permettent de Les espaces de noms du noyau, que l'on appelle *namespaces*, permettent de
dupliquer certaines structures, habituellement considérées uniques pour le dupliquer certaines structures, habituellement considérées uniques pour le
noyau, dans le but de les isoler d'un groupe de processus à un autre. noyau, dans le but qu'un groupe de processus soit isolé d'autres processus, sur
certains aspects de l'environnement dans lequel il s'exécute.
On en dénombre huit (le dernier ayant été ajouté dans Linux 5.6) : `cgroup`, On en dénombre huit (le dernier ayant été ajouté dans Linux 5.6) : `cgroup`,
`IPC`, `network`, `mount`, `PID`, `time`, `user` et `UTS`. `IPC`, `network`, `mount`, `PID`, `time`, `user` et `UTS`.
@ -33,12 +34,13 @@ que AppArmor, SELinux, Yama, ...).
[^NSDOC]: <https://www.kernel.org/doc/ols/2006/ols2006v1-pages-101-112.pdf> [^NSDOC]: <https://www.kernel.org/doc/ols/2006/ols2006v1-pages-101-112.pdf>
Commençons par passer en revue rapidement les différents *namespaces*.
#### L'espace de noms `mount` {.unnumbered #mount-ns} #### L'espace de noms `mount` {.unnumbered #mount-ns}
Depuis Linux 2.4.19. Depuis Linux 2.4.19.
Cet espace de noms isole la liste des points de montage. Cet espace de noms dissocie la liste des points de montage.
Chaque processus appartenant à un *namespace mount* différent peut monter, 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émonter et réorganiser à sa guise les points de montage, sans que cela n'ait
@ -47,6 +49,9 @@ 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 lorsqu'elle aura effectivement été démontée de chaque *namespace mount* dans
lequel elle était montée. lequel elle était montée.
Il s'agit d'une version améliorée de nos bons vieux `chroot`, puisqu'il n'est
plus possible de s'en échapper en remontant l'arborescence.
#### L'espace de noms `UTS` {.unnumbered #uts-ns} #### L'espace de noms `UTS` {.unnumbered #uts-ns}
@ -136,7 +141,7 @@ Depuis Linux 5.6.
Avec cet espace de noms, il n'est pas possible de virtualiser l'heure d'un de Avec cet espace de noms, il n'est pas possible de virtualiser l'heure d'un de
nos conteneurs (on peut seulement changer le fuseau horaire, puisqu'ils sont nos conteneurs (on peut seulement changer le fuseau horaire, puisqu'ils sont
gérés par la `libc`). Les horloges virtualisations avec ce *namespace* sont les gérés par la `libc`). Les horloges virtualisées avec ce *namespace* sont les
compteurs `CLOCK_MONOTONIC` et `CLOCK_BOOTTIME`. compteurs `CLOCK_MONOTONIC` et `CLOCK_BOOTTIME`.
Lorsque l'on souhaite mesurer un écoulement de temps, la méthode naïve consiste Lorsque l'on souhaite mesurer un écoulement de temps, la méthode naïve consiste

View File

@ -30,6 +30,7 @@ obtenir un descripteur de fichier valide vers le *namespace* (pour passer à
::::: {.question} ::::: {.question}
#### Faire persister un *namespace* ? {-} #### Faire persister un *namespace* ? {-}
\
Il n'est pas possible de faire persister un espace de noms d'un reboot à Il n'est pas possible de faire persister un espace de noms d'un reboot à
l'autre.\ l'autre.\

119
tutorial/4/lsns.md Normal file
View File

@ -0,0 +1,119 @@
Explorons les *namespaces*
--------------------------
Maintenant que nous avons quelques notions de base sur les espaces de nom,
voyons quelles surprises nous réserve notre système...
### Voir les *namespace*s de notre système
La première commande que l'on va utiliser est `lsns(8)`, afin d'afficher tous
les *namespace*s actuels de notre machine.
<div lang="en-US">
```bash
42sh# lsns
NS TYPE NPROCS PID USER COMMAND
4026531834 time 238 1 root /sbin/init
4026531835 cgroup 238 1 root /sbin/init
4026531836 pid 239 1 root /sbin/init
4026531837 user 227 1 root /sbin/init
4026531838 uts 231 1 root /sbin/init
4026531839 ipc 228 1 root /sbin/init
4026531840 net 228 1 root /sbin/init
4026531841 mnt 223 1 root /sbin/init
4026532230 uts 1 227 root ├─/usr/lib/systemd/systemd-udevd
4026532483 mnt 4 366 root ├─/usr/lib/systemd/systemd-userdbd
4026532485 mnt 1 363 systemd-resolve ├─/usr/lib/systemd/systemd-resolved
4026532486 mnt 1 364 systemd-timesync ├─/usr/lib/systemd/systemd-timesyncd
4026532491 mnt 1 381 root ├─/usr/lib/systemd/systemd-logind
4026532569 mnt 1 428 systemd-network └─/usr/lib/systemd/systemd-networkd
4026531862 mnt 1 33 root kdevtmpfs
[...]
```
</div>
Cette commande nous dévoile déjà de nombreuses choses :
- Chaque processus se trouve dans un *namespace* de chaque type : le noyau n'a
pas de notion de « processus hôte » (sans *namespace*) et « processus
contenerisé » (dans un *namespace*). Le processus initial de la machine se
retrouve donc dans des espaces de nom, tout comme les processus d'un conteneur.
- On aperçoit un genre de hiérarchie dans certain cas.
- La première colonne nous renseigne sur l'identifiant du *namespace*.
- `kdevtmpfs` : un *thread* du noyau, s'exécute dans un espace de nom `mnt`
dédié.
Vous verrez surement davantage de processus si vous exécutez cette commande sur
une machine que vous utilisez : chaque conteneur y sera bien entendu listé,
quelque soit la technologie sous-jacente (Docker, podman, CRI-O, snapd, ...),
mais chose plus étonnante, Chrome et Firefox tirent également parti des espaces
de nom pour de la défense en profondeur.
### Voir les *namespace*s d'un processus
Chaque processus lancé est donc rattaché à une liste d'espaces de nom, y
compris s'il est issu du système de base (« l'hôte »).
Nous pouvons dès lors consulter le dossier `/proc/<PID>/ns/` de chaque
processus, pour consulter les différents espaces de nom de nos processus.
Tous les processus ont la même liste de fichiers. Ils sont tous liés à un
espace de noms par *namespace* utilisable avec la version noyau dont on
dispose. D'une machine à l'autre, d'une version du noyau à l'autre, il est
normal d'avoir une liste de *namespace*s différente, mais d'un processus à
l'autre sur un même noyau, nous aurons les mêmes espaces de nom disponibles,
donc les mêmes fichiers.
Ces fichiers sont en fait des liens symboliques un peu particuliers, car ils ne
pointent pas vers une destination "valide" :
<div lang="en-US">
```bash
42sh$ ls -l /proc/self/ns
lrwxrwxrwx 1 nemunaire 1 oct. 23:42 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 nemunaire 1 oct. 23:42 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 nemunaire 1 oct. 23:42 mnt -> 'mnt:[4026531840]'
lrwxrwxrwx 1 nemunaire 1 oct. 23:42 net -> 'net:[4026532008]'
lrwxrwxrwx 1 nemunaire 1 oct. 23:42 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 nemunaire 1 oct. 23:42 pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx 1 nemunaire 1 oct. 23:42 time -> 'time:[4026531834]'
lrwxrwxrwx 1 nemunaire 1 oct. 23:42 time_for_children -> 'time:[4026531834]'
lrwxrwxrwx 1 nemunaire 1 oct. 23:42 user -> 'user:[4026531837]'
lrwxrwxrwx 1 nemunaire 1 oct. 23:42 uts -> 'uts:[4026531838]'
```
</div>
Les liens référencent une structure du noyau résidant en mémoire. Les numéros
entre crochets sont les *inodes* du système de fichiers `nsfs`, que l'on a pu
voir dans la première colonne de `lsns(8)`. Ce sont les identifiants des
espaces de nom.
Ces *inodes* seront les mêmes pour deux processus qui partagent le même espace
de noms : la structure pointée sera identique. Elle sera par contre différente
si l'espace de nom est différent, l'*inode* sera donc différent.
::::: {.question}
##### `*_for_children` {-}
Vous avez peut-être remarqué des fichiers `*_for_children` dans le dossier `ns`
de vos processus. Les espaces de noms *PID* et *Time*, lorsqu'on les change
pour un processus, ne s'appliquent pas directement au processus en cours
d'exécution, la dissociation de *namespace* ne pourra se faire que pour les
processus/threads fils.
`pid_for_children` et `time_for_children` représentent donc les *namespace*s
qui seront attribués aux processus fils lancés par un `clone(2)` ou un
`fork(2)`.
En attendant notre processus courant conserve ses espaces de nom `pid` et
`time`.
:::::

View File

@ -187,7 +187,9 @@ mount -t tmpfs none /mnt/test-shared/foo
``` ```
</div> </div>
Le point de montage apparaît bien sous `/mnt/test-slave/foo`. Par contre : Le point de montage apparaît bien sous `/mnt/test-slave/foo`.
Par contre :
<div lang="en-US"> <div lang="en-US">
```bash ```bash
@ -288,10 +290,15 @@ mount --move /dev /newroot/dev
``` ```
</div> </div>
::::: {.question}
#### Quand a-t-on besoin de déplacer un point de montage ? {-}
\
Cette possibilité s'emploie notamment lorsque l'on souhaite changer la racine Cette possibilité s'emploie notamment lorsque l'on souhaite changer la racine
de notre système de fichiers : par exemple pour passer de l'*initramfs* au de notre système de fichiers : par exemple pour passer de l'*initramfs* au
système démarré, de notre système hôte au système d'un conteneur, ... système démarré, ou encore de notre système hôte au système d'un conteneur, ...
:::::
## Aller plus loin {-} ## Aller plus loin {-}

View File

@ -1,5 +1,3 @@
\newpage
Le *namespace* `mount` Le *namespace* `mount`
---------------------- ----------------------
@ -20,7 +18,7 @@ montage, récursivement, dès que l'on est entré dans notre nouvel espace de
noms. noms.
<div lang="en-US"> <div lang="en-US">
```bash ```
mount --make-rslave / mount --make-rslave /
``` ```
</div> </div>
@ -45,7 +43,7 @@ montage virtuels. Le changement de racine sera donc effectif uniquement dans
cet espace de noms. cet espace de noms.
#### L'environnement #### L'environnement\
Pour pouvoir changer de racine, il est nécessaire que la nouvelle racine soit Pour pouvoir changer de racine, il est nécessaire que la nouvelle racine soit
la racine d'un point de montage, comme l'explique `pivot_root(2)`. En effet, il la racine d'un point de montage, comme l'explique `pivot_root(2)`. En effet, il
@ -55,7 +53,7 @@ ne se trouvait pas à la racine d'une partition au moment du basculement.
Si vous n'avez pas de partition à disposition, vous pouvez utiliser un `tmpfs` : Si vous n'avez pas de partition à disposition, vous pouvez utiliser un `tmpfs` :
<div lang="en-US"> <div lang="en-US">
```bash ```
42sh# mkdir /mnt/newroot 42sh# mkdir /mnt/newroot
42sh# mount -t tmpfs none /mnt/newroot 42sh# mount -t tmpfs none /mnt/newroot
``` ```
@ -75,7 +73,7 @@ Voici les grandes étapes du changement de racine :
3. `pivot_root` ! 3. `pivot_root` !
#### S'isoler #### S'isoler\
Notre but étant de démonter toutes les partitions superflues, nous allons Notre but étant de démonter toutes les partitions superflues, nous allons
devoir nous isoler sur : devoir nous isoler sur :
@ -89,48 +87,54 @@ devoir nous isoler sur :
Isolons-nous : Isolons-nous :
<div lang="en-US"> <div lang="en-US">
```bash ```
42sh# unshare -p -m -f --mount-proc 42sh# unshare -p -m -f --mount-proc
``` ```
</div> </div>
::::: {.warning}
#### Dissocier la propagation des démontages Avant de pouvoir commencer à démonter les partitions, il faut
Attention ! avant de pouvoir commencer à démonter les partitions, il faut
s'assurer que les démontages ne se propagent pas via une politique de *shared s'assurer que les démontages ne se propagent pas via une politique de *shared
mount*. mount*.
:::::
#### Dissocier la propagation des démontages\
Commençons donc par étiqueter tous nos points de montage (de ce *namespace*), Commençons donc par étiqueter tous nos points de montage (de ce *namespace*),
comme esclaves : comme esclaves :
<div lang="en-US"> <div lang="en-US">
```bash ```
42sh# mount --make-rslave / 42sh# mount --make-rslave /
``` ```
</div> </div>
#### Démonter tout ! #### Démonter tout !\
À vous maintenant de démonter vos points d'attache. Il ne devrait vous rester À vous maintenant de démonter vos points d'attache. Il ne devrait vous rester
après cette étape que : `/`, `/dev`, `/sys`, `/proc`, `/run` et leurs fils. après cette étape que : `/`, `/dev`, `/sys`, `/proc`, `/run` et leurs fils.
#### Switch ! #### Switch !\
À ce stade, dans votre console, vous avez plusieurs solutions : utiliser À ce stade, dans votre console, vous avez plusieurs solutions : utiliser
`switch_root(8)` ou `pivot_root(8)`. La première abstrait plus de choses que la `switch_root(8)` ou `pivot_root(8)`. La première abstrait plus de choses que la
seconde. seconde.
##### `switch_root`\ ##### `switch_root` {-}
\
Cette commande s'occupe de déplacer les partitions restantes pour vous, et lance Cette commande s'occupe de déplacer les partitions restantes pour vous, et lance
la première commande (*init*) de votre choix. la première commande (*init*) de votre choix.
##### `pivot_root`\ ##### `pivot_root` {-}
\
Cette commande, plus proche du fonctionnement de l'appel système Cette commande, plus proche du fonctionnement de l'appel système
`pivot_root(2)`, requiert de notre part que nous ayons préalablement déplacé `pivot_root(2)`, requiert de notre part que nous ayons préalablement déplacé
@ -147,9 +151,58 @@ Pour lancer la première commande dans la nouvelle racine, on passe généraleme
par : par :
<div lang="en-US"> <div lang="en-US">
```bash ```
42sh# exec chroot / command 42sh# exec chroot / command
``` ```
</div> </div>
#### Erreurs courantes {-}
\
Voici une liste des erreurs les plus courantes que vous allez sans doute rencontrer :
`EINVAL`
: - La nouvelle racine n'est pas un point de montage.
: - Le chemin où placer l'ancienne racine n'est pas sur la nouvelle racine.
: - Le point de montage de la nouvelle ou l'ancienne racine utilise le type de propagation *shared*.
`EBUSY`
: La nouvelle racine ou le chemin où mettre l'ancienne racine se trouve sur la racine actuelle.
`ENOTDIR`
: La nouvelle racine ou le chemin où mettre l'ancienne racine ne sont pas des dossiers.
`EPERM`
: Le processus appelant ne dispose pas de la capability `CAP_SYS_ADMIN`.
Vous pouvez retrouver toutes les erreurs dans le manuel de `pivot_root(2)`.
#### Assemblage {-}
\
Vous devriez maintenant pouvoir réaliser un script `myswitch_root.sh` qui
enchaîne toutes les étapes précédentes.
On considère préalablement que l'environnement est propice à la réalisation de ce script :
<div lang="en-US">
```
42sh# mkdir -p /mnt/newroot
42sh# mount -t tmpfs none /mnt/newroot
42sh# wget https://dl-cdn.alpinelinux.org/alpine/v3.14/releases/x86_64/alpine-minirootfs-3.14.8-x86_64.tar.gz
42sh# tar xpf alpine-minirootfs-*.tar.gz -C /mnt/newroot
42sh# cd /
```
</div>
Puis on s'attend à le lancer ainsi :
<div lang="en-US">
```
42sh# ~/myswitch_root.sh /mnt/newroot /bin/bash
innewns# _
```
</div>
::::: :::::

View File

@ -1,8 +1,9 @@
\newpage
Le *namespace* `network` {#net-ns} Le *namespace* `network` {#net-ns}
------------------------ ------------------------
Voyons maintenant plus en détail les différents espaces de nom, leurs
caractéristiques et leurs usages ; en commençant par le *namespace* `network`.
### Introduction ### Introduction
L'espace de noms `network`, comme son nom l'indique permet de virtualiser tout L'espace de noms `network`, comme son nom l'indique permet de virtualiser tout
@ -14,16 +15,25 @@ environnement qui n'a plus qu'une interface de *loopback* :
<div lang="en-US"> <div lang="en-US">
``` ```
42sh# unshare -n ip a 42sh# unshare --net ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
``` ```
</div> </div>
::::: {.warning}
Bien que portant le même nom que l'interface de *loopback* de notre Bien que portant le même nom que l'interface de *loopback* de notre
environnement principal, il s'agit bien de deux interfaces isolées l'une de environnement principal, il s'agit bien de deux interfaces isolées l'une de
l'autre. l'autre.
:::::
Afin d'amener du réseau à notre nouvel espace de nom, il va falloir lui
attribuer des interface. En fait, nous allons pouvoir déplacer nos interfaces
réseaux, dans le *namespace* vers lequel elle doit être accessible. Une
interface donnée ne peut se trouver que dans un seul *namespace* à la fois.
Qui dit nouvelle pile réseau, dit également que les ports qui sont assignés Qui dit nouvelle pile réseau, dit également que les ports qui sont assignés
dans l'espace principal, ne le sont plus dans le conteneur : il est donc dans l'espace principal, ne le sont plus dans le conteneur : il est donc
possible de lancer un serveur web sans qu'il n'entre en conflit avec celui d'un possible de lancer un serveur web sans qu'il n'entre en conflit avec celui d'un
@ -38,17 +48,51 @@ La suite d'outils `iproute2` propose une interface simplifiée pour utiliser le
Nous pouvons tout d'abord créer un nouvel espace de noms : Nous pouvons tout d'abord créer un nouvel espace de noms :
<div lang="en-US"> <div lang="en-US">
```bash ```
42sh# ip netns add virli 42sh# ip netns add virli
``` ```
</div> </div>
::::: {.code}
La technique utilisée ici pour avoir des *namespaces* nommés est la même que La technique utilisée ici pour avoir des *namespaces* nommés est la même que
celle que nous avons vue dans celle que nous avons vue dans
[la première partie sur les *namespaces*](#ns-lifetime) : via un `mount --bind` [la première partie sur les *namespaces*](#ns-lifetime) : via un `mount --bind`
dans le dossier `/var/run/netns/`. Cela permet de faire persister le namespace dans le dossier `/var/run/netns/`. Cela permet de faire persister le namespace
malgré le fait que plus aucun processus ne s'y exécute. malgré le fait que plus aucun processus ne s'y exécute.
Nous pouvons créer artificiellement des entrées pour `ip netns` avec les
quelques commandes suivantes :
<div lang="en-US">
```
# On affiche la liste des netns déjà créés
42sh# ip netns
virli
# On crée un fichier pour servir de réceptacle au bind mount
42sh# touch /var/run/netns/foo
# On crée un nouveau namespace net, puis on bind tout de suite le
# fichier de namespace vers le fichier que l'on vient de créer
42sh# unshare --net \
mount --bind /proc/self/ns/net /var/run/netns/foo
# Testons si cela a bien marché
42sh# ip netns
foo virli
42sh# ip netns exec foo ip link
1: lo: <LOOPBACK> mut 65536 qdisc noop state DOWN mode DEFAULT group default
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
```
</div>
Les fichiers utilisés par `ip netns` ne sont donc rien de plus que des
*bind-mount*. Ce qui explique qu'ils soient persistant même sans processus
s'exécutant à l'intérieur.
:::::
Maintenant que notre *namespace* est créé, nous pouvons regarder s'il contient Maintenant que notre *namespace* est créé, nous pouvons regarder s'il contient
des interfaces : des interfaces :
@ -67,7 +111,7 @@ D'ailleurs, cette interface est rapportée comme étant désactivée, activons-l
via la commande : via la commande :
<div lang="en-US"> <div lang="en-US">
```bash ```
42sh# ip netns exec virli ip link set dev lo up 42sh# ip netns exec virli ip link set dev lo up
``` ```
</div> </div>
@ -105,6 +149,171 @@ entre d'un côté sort de l'autre et inversement. La commande précédente a don
créé deux interfaces `veth0` et `veth1` : les paquets envoyés sur `veth0` sont créé deux interfaces `veth0` et `veth1` : les paquets envoyés sur `veth0` sont
donc reçus par `veth1` et les paquets envoyés à `veth1` sont reçus par `veth0`. donc reçus par `veth1` et les paquets envoyés à `veth1` sont reçus par `veth0`.
::::: {.code}
Pour réaliser ces étapes dans un langage de programmation, nous allons passer
par Netlink. Il s'agit d'une interface de communication entre le noyau et
l'espace utilisateur. Il faut commencer par créer une `socket` pour avoir accès
à l'API Netlink, nous pourrons ensuite envoyer des requêtes, comme celle nous
permettant de créer notre interface `veth`.
Un message Netlink a une structure, alignée sur 4 octets, contenant un en-tête
et des données. Le format de l'en-tête est décrit dans le RFC
3549[^RFC3549] :
[^RFC3549]: <https://tools.ietf.org/html/rfc3549>
<div lang="en-US">
```c
struct nlmsghdr {
uint32_t nlmsg_len; // Length of message including header
uint16_t nlmsg_type; // Type of message content
uint16_t nlmsg_flags; // Additional flags
uint32_t nlmsg_seq; // Sequence number
uint32_t nlmsg_pid; // Sender port ID
};
```
</div>
Parmi les fonctionnalités de Netlink, nous allons utiliser le module NIS
(Network Interface Service)[^REF3549NIS]. Il spécifie le format par lequel
doivent commencer les données liées à l'administration d'interfaces réseau.
[^RFC3549NIS]: Network Interface Service Module <https://tools.ietf.org/html/rfc3549#section-2.3.3.1>
<div lang="en-US">
```c
struct ifinfomsg {
uint8_t ifi_family; // AF_UNSPEC
// uint8_t Reserved
uint16_t ifi_type; // Device type
int32_t ifi_index; // Interface index
uint32_t ifi_flags; // Device flags
uint32_t ifi_change; // change mask
};
```
</div>
Le module NIS a besoin que les données soient transmises sous forme d'*attributs
Netlink*. Ces attributs fournissent un moyen de segmenter la charge utile en
sous-sections. Un attribut a une taille et un type, en plus de sa charge utile.
<div lang="en-US">
```c
struct rtattr {
uint16_t rta_len; // Length of option
uint16_t rta_type; // Type of option
// Data follows
};
```
</div>
Le *payload* du message Netlink sera donc transmis comme une liste d'attributs (où
chaque attribut peut à son tour avoir des attributs imbriqués).
<div lang="en-US">
```c
struct rtattr {
unsigned short rta_len;
unsigned short rta_type;
};
```
</div>
En se basant sur du code d'`ip link`[^IPLINK], on peut reconstituer la
communication suivante :
[^IPLINK]: <https://github.com/shemminger/iproute2/blob/main/ip/link_veth.c>
<div lang="en-US">
```c
#define MAX_PAYLOAD 1024
struct nlreq {
struct nlmsghdr hdr; // Netlink message header
struct ifinfomsg msg; // First data filled with NIS module info
char buf[MAX_PAYLOAD]; // Remaining payload
};
int
create_veth(char *ifname, char *peername)
{
// Create the netlink socket
int sock_fd = socket(PF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE);
if (sock_fd < 0)
return -1;
uint16_t flags =
NLM_F_REQUEST // We build a request message
| NLM_F_CREATE // Create a device if it doesn't exist
| NLM_F_EXCL // Do nothing if it already exists
| NLM_F_ACK; // Except an ack as reply or an error
// Initialise request message
struct nl_req req = {
.hdr.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg)),
.hdr.nlmsg_flags = flags,
.hdr.nlmsg_type = RTM_NEWLINK,
.msg.ifi_family = PF_NETLINK,
};
struct nlmsghdr *hdr = &req.hdr;
int maxlen = sizeof(req);
// Attribute r0 with veth info
addattr_l(hdr, maxlen, IFLA_IFNAME, ifname, strlen(ifname) + 1);
// Attribute r1 nested within r1, containing iface info
struct rtattr *linfo =
addattr_nest(n, maxlen, IFLA_LINKINFO);
// Specify the device type is veth
addattr_l(hdr, maxlen, IFLA_INFO_KIND, "veth", 5);
// r2: another nested attribute
struct rtattr *linfodata =
addattr_nest(hdr, maxlen, IFLA_INFO_DATA);
// r3: nested attribute, contains the peer name
struct rtattr *peerinfo =
addattr_nest(n, maxlen, VETH_INFO_PEER);
n->nlmsg_len += sizeof(struct ifinfomsg);
addattr_l(n, maxlen, IFLA_IFNAME, peername, strlen(peername) + 1);
addattr_nest_end(hdr, peerinfo);
addattr_nest_end(hdr, linfodata);
addattr_nest_end(hdr, linfo);
// Send the message
sendmsg(sock_fd, &req, 0);
}
```
</div>
Maintenant que l'on a notre interface virtuelle, nous pouvons envoyer un second
message pour la déplacer dans un nouveau *namespace*[^IPSETNS] :
[^IPSETNS]: <https://github.com/shemminger/iproute2/blob/main/ip/iplink.c#L677>
<div lang="en-US">
```c
// Initialise request message
struct nl_req req = {
.hdr.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg)),
.hdr.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK,
.hdr.nlmsg_type = RTM_NEWLINK,
.msg.ifi_family = PF_NETLINK,
};
addattr_l(&req.hdr, sizeof(req), IFLA_NET_NS_FD, &netns, 4);
addattr_l(&req.hdr, sizeof(req), IFLA_IFNAME,
ifname, strlen(ifname) + 1);
sendmsg(sock_fd, &req, 0);
```
</div>
:::::
Dans cette configuration, ces deux interfaces ne sont pas très utiles, mais si Dans cette configuration, ces deux interfaces ne sont pas très utiles, mais si
l'on place l'une des deux extrémités dans un autre *namespace* `network`, il l'on place l'une des deux extrémités dans un autre *namespace* `network`, il
devient alors possible de réaliser un échange de paquets entre les deux. devient alors possible de réaliser un échange de paquets entre les deux.
@ -112,7 +321,7 @@ devient alors possible de réaliser un échange de paquets entre les deux.
Pour déplacer `veth1` dans notre *namespace* `virli` : Pour déplacer `veth1` dans notre *namespace* `virli` :
<div lang="en-US"> <div lang="en-US">
```bash ```
42sh# ip link set veth1 netns virli 42sh# ip link set veth1 netns virli
``` ```
</div> </div>
@ -120,7 +329,7 @@ Pour déplacer `veth1` dans notre *namespace* `virli` :
Il ne reste maintenant plus qu'à assigner une IP à chacune des interfaces : Il ne reste maintenant plus qu'à assigner une IP à chacune des interfaces :
<div lang="en-US"> <div lang="en-US">
```bash ```
42sh# ip netns exec virli ip a add 10.10.10.42/24 dev veth1 42sh# ip netns exec virli ip a add 10.10.10.42/24 dev veth1
42sh# ip a add 10.10.10.41/24 dev veth0 42sh# ip a add 10.10.10.41/24 dev veth0
``` ```
@ -150,8 +359,9 @@ couches du noyau. Utiliser les interfaces *veth* est plutôt simple et disponibl
partout, mais c'est loin d'être la technique la plus rapide ou la moins partout, mais c'est loin d'être la technique la plus rapide ou la moins
gourmande. gourmande.
Voyons ensemble les autres possibilités à notre disposition.
#### VLAN #### VLAN \
Il est possible d'attribuer juste une interface de VLAN, si l'on a un switch Il est possible d'attribuer juste une interface de VLAN, si l'on a un switch
supportant la technologie [802.1q](https://fr.wikipedia.org/wiki/IEEE_802.1Q) supportant la technologie [802.1q](https://fr.wikipedia.org/wiki/IEEE_802.1Q)
@ -165,8 +375,11 @@ derrière notre machine.
``` ```
</div> </div>
On attribuera alors à chaque conteneur une interface de VLAN différente. Cela
peut donner lieu à une configuration de switch(s) assez complexe.
#### MACVLAN
#### MACVLAN \
<!-- https://hicu.be/bridge-vs-macvlan --> <!-- https://hicu.be/bridge-vs-macvlan -->
@ -176,11 +389,11 @@ MACVLAN. S'il est activé dans votre noyau, vous allez avoir le choix entre l'un
des quatre modes : *private*, VEPA, *bridge* ou *passthru*. des quatre modes : *private*, VEPA, *bridge* ou *passthru*.
Quel que soit le mode choisi, les paquets en provenance d'autres machines et à Quel que soit le mode choisi, les paquets en provenance d'autres machines et à
destination d'un MAC seront délivrés à l'interface possédant la MAC. Les destination d'une MAC seront délivrés à l'interface possédant ladite MAC. Les
différences entre les modes se trouvent au niveau de la communication entre les différences entre les modes se trouvent au niveau de la communication entre les
interfaces. interfaces.
##### VEPA ##### VEPA \
Dans ce mode, tous les paquets sortants sont directement envoyés sur Dans ce mode, tous les paquets sortants sont directement envoyés sur
l'interface Ethernet de sortie, sans qu'aucun routage préalable n'ait été l'interface Ethernet de sortie, sans qu'aucun routage préalable n'ait été
@ -198,7 +411,7 @@ Pour construire une nouvelle interface de ce type :
</div> </div>
##### *Private* ##### *Private* \
À la différence du mode *VEPA*, si un paquet émis par un conteneur à À la différence du mode *VEPA*, si un paquet émis par un conteneur à
destination d'un autre conteneur est réfléchi par un switch, le paquet ne sera destination d'un autre conteneur est réfléchi par un switch, le paquet ne sera
@ -214,12 +427,12 @@ conteneur de la même machine.
</div> </div>
##### *Bridge* ##### *Bridge* \
À l'inverse des modes *VEPA* et *private*, les paquets sont routés selon leur En mode *Bridge*, les paquets sont routés selon leur adresse MAC : si jamais
adresse MAC : si jamais une adresse MAC est connue, le paquet est délivré à une adresse MAC est connue, le paquet est délivré à l'interface MACVLAN
l'interface MACVLAN correspondante ; dans le cas contraire, le paquet est correspondante ; dans le cas contraire, le paquet est envoyé sur l'interface de
envoyé sur l'interface de sortie. sortie.
Pour construire une nouvelle interface de ce type : Pour construire une nouvelle interface de ce type :
@ -230,6 +443,28 @@ Pour construire une nouvelle interface de ce type :
</div> </div>
##### *passthru* \
Enfin, le mode *passthru* permet de récupérer le contrôle sur tout ce qu'il
reste du périphérique initial (notamment pour lui changer sa MAC propre, ou
pour activer le mode de promiscuité).
L'intérêt est surtout de pouvoir donner cette interface à un conteneur ou une
machine virtuelle, sans lui donner un accès complet à l'interface physique (et
notamment aux autres MACVLAN).
On construit l'interface en mode *passthru* de cette façon :
<div lang="en-US">
```
42sh# ip link add link eth0 mac3 type macvlan mode passthru
```
</div>
Une seule interface MACVLAN peut être en mode *passthru* par interface
physique.
### Aller plus loin {-} ### Aller plus loin {-}
Pour approfondir les différentes techniques de routage, je vous Pour approfondir les différentes techniques de routage, je vous

View File

@ -1,5 +1,3 @@
\newpage
Le *namespace* `PID` {#pid-ns} Le *namespace* `PID` {#pid-ns}
-------------------- --------------------
@ -23,13 +21,92 @@ trouve.
Première étape s'isoler : Première étape s'isoler :
<div lang="en-US"> <div lang="en-US">
```bash ```
42sh# unshare --pid --fork /bin/bash 42sh# unshare --pid --fork /bin/bash
inpidns# echo $$
1
``` ```
</div> </div>
Nous utilisons ici l'option `-f`, pour que le passage dans le nouvel espace de ::::: {.question}
noms des PID soit effectif (cf. [Introduction](#pid-ns-intro)).
#### Qu'est-ce qu'il se passe sans l'option `--fork` ? {-}
\
Nous utilisons ici l'option `--fork`, pour que le passage dans le nouvel espace
de noms des PID soit effectif (cf. [Introduction](#pid-ns-intro)). Si on
l'omet, voici ce qu'il se passe :
<div lang="en-US">
```
42sh# unshare --pid /bin/sh
inpidns# echo $$
23456
```
</div>
Ce n'est apparemment pas ce que l'on souhaitait : on est toujours dans l'ancien
*namespace*, puisqu'on a juste `unshare(2)`, sans `fork(2)`. Si l'on va plus
loin et que l'on `fork` en demandant à `sh` d'exécuter un nouveau processus :
<div lang="en-US">
```
42sh# unshare --pid /bin/sh
inpidns# echo $$
34567
inpidns# sh
inpidns# echo $$
1
```
</div>
C'est d'ailleurs le bon moment pour regarder le contenu de notre fichier
`pid_for_children` pour le processus `34567` :
<div lang="en-US">
```
42sh# ls -l /proc/34567/ns/pid*
lrwxrwxrwx 1 nemunaire 1 oct. 23:42 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 nemunaire 1 oct. 23:42 pid_for_children -> 'pid:[4026532993]'
```
</div>
On remarque bien que l'on se trouve dans un entre-deux : `pid` pointe toujours
sur l'*inode* de l'arbre de PID initial, tandis que `pid_for_children` est prêt
à transmettre le nouvel *inode* aux nouveaux processus.
\
#### Pourquoi ça ne fonctionne pas du tout avec `bash` ? {-}
\
Si on utilise `bash` à la place de `sh` dans les exemples précédents, toujours
sans l'option `--fork`, on obtient une erreur plutôt étrange :
<div lang="en-US">
```
42sh# unshare --pid /bin/bash
inpidns# echo $$
65432
inpidns# sh
-bash: fork: Cannot allocate memory
```
</div>
Il se trouve que `bash` commence par `fork(2)` lui-même afin de réaliser un
certain nombre d'opérations. Notre PID 1, le premier PID de notre conteneur, a
donc été alloué à un processus d'initialisation de `bash`, qui s'est terminé
depuis.\
Le comportement du noyau, lorsque le PID 1 se termine, est de lancer un *kernel
panic* (car c'est un processus indispensable, notamment de part son rôle de
parent pour tous les processus orphelin). Au sein d'un *namespace* `PID` qui
n'est pas le *namespace* racine, le noyau appelle la fonction
`disable_pid_allocation` qui retire le *flag* `PIDNS_HASH_ADDING` de l'espace
de nom. Le fait de ne pas avoir `PIDNS_HASH_ADDING` fait retourner `ENOMEM` à
la fonction `alloc_pid` appelée par `fork(2)` et `clone(2)`. On n'a donc pas
d'autre choix que de quitter ce *namespace* pour en recréer un nouveau.
:::::
Un coup d'œil à `top` ou `ps aux` devrait nous montrer que l'on est maintenant Un coup d'œil à `top` ou `ps aux` devrait nous montrer que l'on est maintenant
le seul processus ... pourtant, il n'en est rien, ces deux commandes continuent le seul processus ... pourtant, il n'en est rien, ces deux commandes continuent
@ -45,12 +122,12 @@ notre système initial. Pour s'en sortir, il est nécessaire de s'isoler dans un
*namespace* `mount` séparé. *namespace* `mount` séparé.
#### Double isolation : ajout du *namespace* `mount` ### Double isolation : ajout du *namespace* `mount`
Voici la nouvelle ligne de commande que l'on va utiliser : Voici la nouvelle ligne de commande que l'on va utiliser :
<div lang="en-US"> <div lang="en-US">
```bash ```
42sh# unshare --pid --mount --fork --mount-proc /bin/bash 42sh# unshare --pid --mount --fork --mount-proc /bin/bash
``` ```
</div> </div>
@ -90,11 +167,11 @@ de noms.
[^PR_SET_CHILD_SUBREAPER]: en réalité, ce comportement est lié à la propriété [^PR_SET_CHILD_SUBREAPER]: en réalité, ce comportement est lié à la propriété
`PR_SET_CHILD_SUBREAPER`, qui peut être définie pour n'importe quel processus `PR_SET_CHILD_SUBREAPER`, qui peut être définie pour n'importe quel processus
de l'arborescence. Le processus au PID 1 hérite forcément de cette propriété\ ; de l'arborescence. Le processus au PID 1 hérite forcément de cette propriété ;
il va donc récupérer tous les orphelins, si aucun de leurs parents n'a la il va donc récupérer tous les orphelins, si aucun de leurs parents n'a la
propriété définie. propriété définie.
Lorsque l'on lance un processus via `nsenter(1)` ou `setns(2)`, cela crée un Lorsqu'on lance un processus via `nsenter(1)` ou `setns(2)`, cela crée un
processus qui n'est sans doute pas un fils direct du processus d'`init` de processus qui n'est sans doute pas un fils direct du processus d'`init` de
notre conteneur. Malgré tout, même s'il est affiché comme n'étant pas un fils à notre conteneur. Malgré tout, même s'il est affiché comme n'étant pas un fils à
l'extérieur du conteneur, les propriétés d'`init` sont biens appliquées à l'extérieur du conteneur, les propriétés d'`init` sont biens appliquées à

View File

@ -1,51 +1,65 @@
\newpage \newpage
Projet et rendu Rendu
=============== =====
Est attendu d'ici le TP suivant : Est attendu d'ici le cours suivant :
- le rendu des exercice de ce TP ; - vos réponses à l'évaluation du cours,
- vos réponses à [l'évaluation du cours](https://virli.nemunai.re/quiz/15). - [SRS] tous les exercices de ce TP,
- [GISTRE] l'avancement des paliers du [projet final](https://virli.nemunai.re/project-gistre.pdf).
Pour les GISTRE (et en bonus pour les SRS), [un
projet](https://virli.nemunai.re/project-2.pdf) est à rendre pour le 13
novembre. Consultez les modalités de rendu sur le sujet directement.
## Modalités de rendu
En tant que personnes sensibilisées à la sécurité des échanges électroniques,
vous devrez m'envoyer vos rendus signés avec votre clef PGP.
Un service automatique s'occupe de réceptionner vos rendus, de faire des
vérifications élémentaires et de vous envoyer un accusé de réception (ou de
rejet).
Ce service écoute sur l'adresse <virli@nemunai.re>. C'est donc à cette adresse
et exclusivement à celle-ci que vous devez envoyer vos rendus. Tout rendu
envoyé à une autre adresse et/ou non signé et/ou reçu après la correction ne
sera pas pris en compte.
Afin d'orienter correctement votre rendu, ajoutez une balise `[TP4]` au sujet
de votre courriel. N'hésitez pas à indiquer dans le corps du message votre
ressenti et vos difficultés ou bien alors écrivez votre meilleure histoire
drôle si vous n'avez rien à dire.
Tarball Arborescence attendue (SRS)
------- -------
Tous les exercices de ce TP sont à placer dans une tarball (pas d'archive ZIP, Tous les fichiers identifiés comme étant à rendre sont à placer dans un dépôt
RAR, ...). Git privé, que vous partagerez avec [votre
professeur](https://gitlab.cri.epita.fr/nemunaire/).
Voici une arborescence type (adaptez les extensions et les éventuels Voici une arborescence type (vous pourriez avoir des fichiers supplémentaires) :
fichiers supplémentaires associés au langage que vous aurez choisi
pour chaque exercice) :
<div lang="en-US"> <div lang="en-US">
``` ```
login_x-TP4/cmpns.sh ./cmpns.sh
login_x-TP4/mydocker_exec.sh ./mydocker_exec.sh
login_x-TP4/myswitch_root.sh ./myswitch_root.sh
``` ```
</div> </div>
Votre rendu sera pris en compte en faisant un [tag **signé par votre clef
PGP**](https://lessons.nemunai.re/keys). Consultez les détails du rendu (nom du
tag, ...) sur la page dédiée au projet sur la plateforme de rendu.
::::: {.question}
Si vous utilisez un seul dépôt pour tous vos rendus, vous **DEVRIEZ**
créer une branche distincte pour chaque rendu :
<div lang="en-US">
```
42sh$ git checkout --orphan renduX
42sh$ git reset
42sh$ rm -r *
42sh$ # Créer l'arborescence de rendu ici
```
</div>
Pour retrouver ensuite vos rendus des travaux précédents :
<div lang="en-US">
```
42sh$ git checkout renduY
-- ou --
42sh$ git checkout master
...
```
</div>
Chaque branche est complètement indépendante l'une de l'autre. Vous
pouvez avoir les exercices du TP1 sur `master`, les exercices du TP4
sur `rendu4`, ... ce qui vous permet d'avoir une arborescence
correspondant à ce qui est demandé, sans pour autant perdre votre
travail (ou le rendre plus difficile d'accès).
::::

25
tutorial/4/setns-pid.md Normal file
View File

@ -0,0 +1,25 @@
\
D'une manière similaire à notre dernier exemple, depuis Linux 5.8, `setns(2)`
peut utiliser comme premier argument, un *file descriptor* pointant un
processus (`pidfd`).
Eh oui, outre l'obtention d'un *file descriptor* sur un lien symbolique étrange
d'une structure du noyau, il est possible d'en obtenir un sur un processus :
- soit en réalisant un `open(2)` d'un dossier `/proc/<PID>` ;
- soit en utilisant l'appel système `pidfd_open(2)`, en précisant l'identifiant
du processus dont on souhaite obtenir le *file descriptor* ;
- soit en retour d'un `clone(2)` avec l'option `CLONE_PIDFD`.
<div lang="en-US">
```bash
int fd = pidfd_open(42, 0);
setns(fd, CLONE_NEWUSER | CLONE_NEWNET | CLONE_NEWUTS);
```
</div>
À travers cet exemple, on cherche à récupérer un *file descriptor* pour le
processus 42. On le passe ensuite à `setns(2)` en précisant que l'on ne
souhaite rejoindre que les espaces de noms Utilisateurs, Réseau et UTS (nom de
la machine).

View File

@ -1,5 +1,5 @@
#### Exemple C #### Exemple C {-}
\
Voici un exemple de code C utilisant `setns(2)` : Voici un exemple de code C utilisant `setns(2)` :
<div lang="en-US"> <div lang="en-US">
@ -35,10 +35,58 @@ main(int argc, char *argv[])
``` ```
</div> </div>
Ce programme prend au minimum deux arguments :
- le chemin d'un fichier d'espace de nom que l'on souhaite rejoindre (le chemin
vers le lien symbolique donc) ;
- le programme (et ses arguments) que l'on souhaite souhaite exécuter une fois
que l'on a rejoint l'espace de noms ciblé.
#### Exemple shell Dans un premier temps, on ouvre le fichier passé en paramètre afin d'obtenir un
*file descriptor* de la structure du noyau.
Dans un shell, on utilisera la commande `nsenter(1)` : On passe ensuite ce *file descriptor* en argument de l'appel système
`setns(2)`. Et enfin on exécute la commande données dans les derniers
paramètres.
::::: {.question}
#### Qu'attend le deuxième argument de `setns(2)` ? {-}
\
Il s'agit d'une contrainte sur le type d'espace de nom que l'on souhaite
rejoindre, dans le cas où on ne souhaite pas rejoindre n'importe quel espace de
nom.
Une fois encore, on utilisera les mêmes options `CLONE_NEW*` lorsque l'on
attendra un type particulier d'espace de nom, ou 0 pour autoriser tous les
types.
:::::
::::: {.question}
#### Peut-on connaître le type de *namespace* à partir du *file descriptor* ? {-}
\
Il est possible de récupérer le type d'espace de nom en passant notre *file
descriptor* à `ioctl(2)`, avec le *flag* `NS_GET_NSTYPE` :
<div lang="en-US">
```c
nstype = ioctl(fd, NS_GET_NSTYPE);
if (nstype & CLONE_NEWUTS != 0) {
... // This is a file descriptor to an UTS namespace
}
```
</div>
:::::
#### Exemple shell {-}
\
Dans un shell, nous utiliserons la commande `nsenter(1)` :
<div lang="en-US"> <div lang="en-US">
```bash ```bash
@ -55,3 +103,25 @@ chaton
jument jument
``` ```
</div> </div>
Avec `nsenter(1)`, il est possible de cibler un processus particulier, sans
aller nécessairement faire référence aux fichiers de `/proc`, en utilisant
l'option `--target` :
<div lang="en-US">
```bash
42sh# unshare --uts /bin/bash
inutsns# echo $$
42
inutsns# hostname jument
# Keep this shell active to perform nexts steps, in another shell.
42sh# hostname
chaton
42sh# nsenter --target 42 --uts /bin/bash
inutsns# hostname
jument
```
</div>
Les options supplémentaires `--uts`, `--mount`, `--ipc`, `--pid`, ... ne prennent alors pas d'argument, mais désignent les espaces de noms du processus ciblé que l'on souhaite rejoindre.

View File

@ -1,71 +1,15 @@
\newpage
### Rejoindre un *namespace* ### Rejoindre un *namespace*
Rejoindre un espace de noms se fait en utilisant l'appel système `setns(2)`, ou Rejoindre un espace de noms se fait en utilisant l'appel système `setns(2)`, ou
la commande `nsenter(1)`. Il est nécessaire de donner en argument la commande `nsenter(1)`. Il est nécessaire de donner en argument
respectivement un *file descriptor* ou le chemin vers le fichier, lien respectivement un *file descriptor* ou le chemin vers le fichier, lien
symbolique, représentant l'espace de nom. symbolique, représentant l'espace de nom (dans `/proc/<PID>/ns/...`).
Une particularité de ces fichiers, que l'on ne peut pas afficher (leurs liens
#### Voir les *namespace*s d'un processus ne pointent pas sur des fichiers que l'on peut atteindre), c'est que l'on peut
les ouvrir avec `open(2)` pour obtenir un *file descriptor* que l'on pourra
Chaque processus lancé est rattaché à une liste d'espaces de nom, y compris passer à `setns(2)`.
s'il est issu du système de base (« l'hôte »).
Nous pouvons dès lors consulter le dossier `/proc/<PID>/ns/` de chaque
processus, pour consulter les différents espaces de nom de nos processus.
Tous les processus auront la même liste de fichiers, car ils sont liés à un
espace de noms par *namespace* utilisable avec le noyau. D'une machine à
l'autre, d'une version du noyau à l'autre, il est normal d'avoir une liste de
*namespace*s différents, mais d'un processus à l'autre sur le même noyau, nous
aurons les mêmes.
Ces fichiers sont en fait des liens symboliques un peu particuliers, car ils ne
pointent pas vers une destination valide :
<div lang="en-US">
```bash
42sh$ ls -l /proc/self/ns
lrwxrwxrwx 1 nemunaire 1 oct. 23:42 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 nemunaire 1 oct. 23:42 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 nemunaire 1 oct. 23:42 mnt -> 'mnt:[4026531840]'
lrwxrwxrwx 1 nemunaire 1 oct. 23:42 net -> 'net:[4026532008]'
lrwxrwxrwx 1 nemunaire 1 oct. 23:42 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 nemunaire 1 oct. 23:42 pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx 1 nemunaire 1 oct. 23:42 time -> 'time:[4026531834]'
lrwxrwxrwx 1 nemunaire 1 oct. 23:42 time_for_children -> 'time:[4026531834]'
lrwxrwxrwx 1 nemunaire 1 oct. 23:42 user -> 'user:[4026531837]'
lrwxrwxrwx 1 nemunaire 1 oct. 23:42 uts -> 'uts:[4026531838]'
```
</div>
Les liens référencent une structure du noyau résidant en mémoire. Les numéros
entre crochets seront les mêmes pour deux processus qui partagent le même
espace de noms. La structure pointée sera différente si l'espace de nom est
différent, donc le numéro sera différent.
On ne peut pas afficher tel quel les structures, mais on peut l'ouvrir avec
`open(2)` pour obtenir un *file descriptor* que l'on pourra passer à
`setns(2)`.
Pour les commandes *shell*, il convient de donner en argument le chemin vers le Pour les commandes *shell*, il convient de donner en argument le chemin vers le
lien symbolique : la commande se chargera d'`open(2)` le fichier. lien symbolique : la commande se chargera d'`open(2)` le fichier pour obtenir
le *file descriptor* nécessaire.
::::: {.question}
##### `*_for_children` {-}
Vous avez peut-être remarqué des fichiers `*_for_children` dans le dossier `ns`
de vos processus. Nous verrons par la suite que les espaces de noms *PID* et
*Time*, une fois créés, ne s'appliquent pas directement au processus en cours
d'exécution, la dissociation de *namespace* ne peut se faire que pour les
processus/threads fils.
`pid_for_children` et `time_for_children` représentent donc les *namespace*s
qui seront attribués aux fils lancés après un `unshare(2)` réussi.
:::::

View File

@ -3,7 +3,7 @@ title: Virtualisation légère -- TP n^o^ 4
subtitle: Linux Internals partie 2 subtitle: Linux Internals partie 2
author: Pierre-Olivier *nemunaire* [Mercier]{.smallcaps} author: Pierre-Olivier *nemunaire* [Mercier]{.smallcaps}
institute: EPITA institute: EPITA
date: Jeudi 7 octobre 2021 date: Mercredi 9 novembre 2022
abstract: | abstract: |
Le but de ce second TP sur les mécanismes internes du noyau va nous Le but de ce second TP sur les mécanismes internes du noyau va nous
permettre d'utiliser les commandes et les appels système relatifs permettre d'utiliser les commandes et les appels système relatifs
@ -12,15 +12,8 @@ abstract: |
\vspace{1em} \vspace{1em}
Tous les exercices de ce TP sont à rendre à <virli@nemunai.re> au Les exercices de ce cours sont à rendre au plus tard le mardi 15
plus tard le mercredi 4 novembre 2021 à 23 h 42. Consultez la dernière novembre 2022 à 23 h 42. Consultez les sections matérialisées par un
section de chaque partie pour plus d'informations sur les éléments à bandeau jaunes et un engrenage pour plus d'informations sur les
rendre. Et n'oubliez pas de répondre aux [questions de éléments à rendre.
cours](https://virli.nemunai.re/quiz/13).
En tant que personnes sensibilisées à la sécurité des échanges électroniques,
vous devrez m'envoyer vos rendus signés avec votre clef PGP. Pensez à
[me](https://keys.openpgp.org/search?q=nemunaire%40nemunai.re) faire signer
votre clef et n'hésitez pas à [faire signer la
vôtre](https://www.meetup.com/fr/Paris-certification-de-cles-PGP-et-CAcert/).
... ...

View File

@ -1,5 +1,3 @@
\newpage
Le *namespace* `user` {#user-ns} Le *namespace* `user` {#user-ns}
--------------------- ---------------------
@ -23,8 +21,8 @@ utilisateur, on obtient les privilèges requis pour créer tous les autres types
de *namespaces*. de *namespaces*.
Grâce à cette technique, il est possible de lancer des conteneurs en tant que Grâce à cette technique, il est possible de lancer des conteneurs en tant que
simple utilisateur ; le projet [Singularity](https://sylabs.io/) repose simple utilisateur ; les projets [`podman`](https://podman.io/) et
en grande partie sur cela. [Singularity](https://sylabs.io/), ... reposent en grande partie sur cela.
### Correspondance des utilisateurs et des groupes ### Correspondance des utilisateurs et des groupes
@ -34,7 +32,7 @@ garder dans le nouvel espace, que les utilisateurs et les groupes utiles au
processus, en les renumérotant au passage si besoin. processus, en les renumérotant au passage si besoin.
#### L'utilisateur -2 : *nobody* #### L'utilisateur -2 : *nobody*\
Lorsque l'on arrive dans un nouvel espace, aucun utilisateur ni groupe n'est Lorsque l'on arrive dans un nouvel espace, aucun utilisateur ni groupe n'est
défini. Dans cette situation, tous les identifiants d'utilisateur et de groupe, défini. Dans cette situation, tous les identifiants d'utilisateur et de groupe,
@ -45,13 +43,14 @@ l'utilisateur *nobody* et au groupe *nogroup*.
non-modification d'un paramètre passé en argument d'une fonction. non-modification d'un paramètre passé en argument d'une fonction.
#### `uid_map` et `gid_map` #### `uid_map` et `gid_map`\
Pour établir la correspondance, une fois que l'on a créé le nouveau Pour établir la correspondance, une fois que l'on a créé le nouveau
*namespace*, ces deux fichiers, accessibles dans `/proc/self/`, peuvent être *namespace*, ces deux fichiers, accessibles dans `/proc/self/`, peuvent être
écrits une fois. écrits une fois.
##### `uid_map`\ ##### `uid_map` {-}
\
Sur chaque ligne, on doit indiquer : Sur chaque ligne, on doit indiquer :
@ -89,13 +88,21 @@ l'utilisateur root, dans le conteneur équivaut à l'utilisateur 1000 hors de
l'espace de noms. l'espace de noms.
##### `gid_map`\ ##### `gid_map` {-}
\
Le principe est identique pour ce fichier, mais agit sur les correspondances Le principe est identique pour ce fichier, mais agit sur les correspondances
des groupes au lieu des utilisateurs. des groupes au lieu des utilisateurs.
Il y a cependant un subtilité car il faut écrire la chaîne `deny` dans le
fichier `setgroups` avant de modifier `gid_map`.
### Utilisation de l'espace de noms
### Utilisation basique de l'espace de noms
Avec `unshare(1)`, voici comment, en tant que simple utilisateur, se dissocier
de plusieurs *namespace*s en gardant un environnement cohérent, dans lequel on
devient le super-utilisateur :
<div lang="en-US"> <div lang="en-US">
```bash ```bash