tuto4 ready
This commit is contained in:
parent
b5de41662b
commit
e928733d61
Binary file not shown.
@ -1,6 +1,6 @@
|
||||
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
|
||||
|
@ -5,20 +5,23 @@
|
||||
Les *namespaces* d'un programme sont exposés sous forme de liens symboliques
|
||||
dans le répertoire `/proc/<PID>/ns/`.
|
||||
|
||||
Deux programmes qui partagent un même *namespace* auront un lien vers la même
|
||||
structure de données.
|
||||
Deux programmes qui partagent un même *namespace* auront un lien vers le même
|
||||
*inode*.
|
||||
|
||||
Écrivons un script ou un programme, `cmpns`, permettant de déterminer si deux
|
||||
programmes s'exécutent dans les mêmes *namespaces*. On ignorera les
|
||||
*namespace*s `*_for_children`, car ils ne font pas partie du cycle d'exécution
|
||||
que l'on cherche à comparer.
|
||||
Écrivons un script, `cmpns`, permettant de déterminer si deux programmes
|
||||
s'exécutent dans les mêmes *namespaces*. On ignorera les *namespace*s
|
||||
`*_for_children`, car ils ne font pas partie du cycle d'exécution que l'on
|
||||
cherche à comparer.
|
||||
|
||||
En shell, vous aurez besoin de `grep(1)` et de `readlink(1)`.
|
||||
|
||||
|
||||
#### Exemples {.unnumbered}
|
||||
|
||||
<div lang="en-US">
|
||||
```
|
||||
42sh$ ./cmpns $(pgrep influxdb) $(pgrep init)
|
||||
42sh$ docker run -d influxdb
|
||||
42sh$ ./cmpns $(pgrep influxd) $(pgrep init)
|
||||
- cgroup: differ
|
||||
- ipc: differ
|
||||
- mnt: differ
|
||||
|
@ -1,28 +1,27 @@
|
||||
\newpage
|
||||
|
||||
Utiliser les *namespace*s
|
||||
-------------------------
|
||||
|
||||
### S'isoler dans un nouveau *namespace*
|
||||
|
||||
Si l'on voit l'isolation procurée par les *namespace*s en parallèle avec les
|
||||
machines virtuelles, 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.
|
||||
Si l'on voit l'isolation procurée par les *namespace*s comme des machines
|
||||
virtuelles, 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.
|
||||
|
||||
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.
|
||||
des machines virtuelles légères. On retrouve ainsi dans Google Chrome et
|
||||
Firefox 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.
|
||||
|
||||
Nous allons voir dans cette partie plusieurs méthodes pour utiliser ces espaces
|
||||
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
|
||||
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.
|
||||
|
||||
|
||||
#### Les appels système
|
||||
#### Les appels système\
|
||||
|
||||
L'appel système par excellence pour contrôler l'isolation d'un nouveau
|
||||
processus est `clone(2)`.
|
||||
|
||||
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 :
|
||||
Ce *syscall*, propre à Linux, crée habituellement un nouveau processus enfant
|
||||
de notre processus courant (mais il peut aussi créer des *threads*, on va voir
|
||||
qu'il fait beaucoup de choses), comme `fork(2)` (qui lui est un appel système
|
||||
POSIX). Mais il prend en plus de nombreux paramètres. L'isolement ou non du
|
||||
processus se fait en fonction des *flags* qui sont passés. On retrouve donc :
|
||||
|
||||
* `CLONE_NEWNS`,
|
||||
* `CLONE_NEWUTS`,
|
||||
@ -75,12 +74,29 @@ sont passés :
|
||||
* `CLONE_NEWCGROUP`,
|
||||
* `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
|
||||
avec d'autres attendus par `clone(2)`.
|
||||
|
||||
Ces mêmes *flags* sont utilisés lors des appels à `unshare(2)` ou `setns(2)`,
|
||||
que nous verrons plus tard.
|
||||
|
||||
::::: {.code}
|
||||
|
||||
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
|
||||
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
|
||||
pour les *CGroups* et disposera d'une nouvelle pile réseau.
|
||||
|
||||
Un exemple complet d'utilisation de `clone(2)` et du *namespace* `UTS` est
|
||||
donné dans le `man` de l'appel système.
|
||||
::::: {.question}
|
||||
|
||||
#### 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`\
|
||||
|
||||
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
|
||||
Lorsque l'on souhaite faire entrer notre processus courant ou notre *thread*
|
||||
dans un nouvel espace de noms, on peut utiliser l'appel système
|
||||
`unshare(2)`. Celui-ci prend uniquement en argument une liste de *flags* des
|
||||
*namespace*s dont on souhaite se dissocier.
|
||||
|
||||
::::: {.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
|
||||
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
|
||||
nouveau processus, son PID et sa `CLOCK_MONOTONIC` sont directement à la bonne
|
||||
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.
|
||||
|
@ -10,17 +10,18 @@ notre machine.
|
||||
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
|
||||
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
|
||||
cela.
|
||||
|
||||
|
||||
Initiation rapide
|
||||
-----------------
|
||||
Présentation des *namespaces*
|
||||
-----------------------------
|
||||
|
||||
Les espaces de noms du noyau, que l'on appelle *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.
|
||||
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`,
|
||||
`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>
|
||||
|
||||
Commençons par passer en revue rapidement les différents *namespaces*.
|
||||
|
||||
#### L'espace de noms `mount` {.unnumbered #mount-ns}
|
||||
|
||||
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,
|
||||
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
|
||||
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}
|
||||
|
||||
@ -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
|
||||
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`.
|
||||
|
||||
Lorsque l'on souhaite mesurer un écoulement de temps, la méthode naïve consiste
|
||||
|
@ -30,6 +30,7 @@ obtenir un descripteur de fichier valide vers le *namespace* (pour passer à
|
||||
|
||||
::::: {.question}
|
||||
#### Faire persister un *namespace* ? {-}
|
||||
\
|
||||
|
||||
Il n'est pas possible de faire persister un espace de noms d'un reboot à
|
||||
l'autre.\
|
||||
|
119
tutorial/4/lsns.md
Normal file
119
tutorial/4/lsns.md
Normal 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`.
|
||||
|
||||
:::::
|
@ -187,7 +187,9 @@ mount -t tmpfs none /mnt/test-shared/foo
|
||||
```
|
||||
</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">
|
||||
```bash
|
||||
@ -288,10 +290,15 @@ mount --move /dev /newroot/dev
|
||||
```
|
||||
</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
|
||||
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 {-}
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
\newpage
|
||||
|
||||
Le *namespace* `mount`
|
||||
----------------------
|
||||
|
||||
@ -20,7 +18,7 @@ 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>
|
||||
@ -45,7 +43,7 @@ montage virtuels. Le changement de racine sera donc effectif uniquement dans
|
||||
cet espace de noms.
|
||||
|
||||
|
||||
#### L'environnement
|
||||
#### L'environnement\
|
||||
|
||||
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
|
||||
@ -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` :
|
||||
|
||||
<div lang="en-US">
|
||||
```bash
|
||||
```
|
||||
42sh# mkdir /mnt/newroot
|
||||
42sh# mount -t tmpfs none /mnt/newroot
|
||||
```
|
||||
@ -75,7 +73,7 @@ Voici les grandes étapes du changement de racine :
|
||||
3. `pivot_root` !
|
||||
|
||||
|
||||
#### S'isoler
|
||||
#### S'isoler\
|
||||
|
||||
Notre but étant de démonter toutes les partitions superflues, nous allons
|
||||
devoir nous isoler sur :
|
||||
@ -89,48 +87,54 @@ devoir nous isoler sur :
|
||||
Isolons-nous :
|
||||
|
||||
<div lang="en-US">
|
||||
```bash
|
||||
```
|
||||
42sh# unshare -p -m -f --mount-proc
|
||||
```
|
||||
</div>
|
||||
|
||||
::::: {.warning}
|
||||
|
||||
#### Dissocier la propagation des démontages
|
||||
|
||||
Attention ! avant de pouvoir commencer à démonter les partitions, il faut
|
||||
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
|
||||
mount*.
|
||||
|
||||
:::::
|
||||
|
||||
|
||||
#### Dissocier la propagation des démontages\
|
||||
|
||||
Commençons donc par étiqueter tous nos points de montage (de ce *namespace*),
|
||||
comme esclaves :
|
||||
|
||||
<div lang="en-US">
|
||||
```bash
|
||||
```
|
||||
42sh# mount --make-rslave /
|
||||
```
|
||||
</div>
|
||||
|
||||
|
||||
#### Démonter tout !
|
||||
#### Démonter tout !\
|
||||
|
||||
À 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.
|
||||
|
||||
|
||||
#### Switch !
|
||||
#### Switch !\
|
||||
|
||||
À 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
|
||||
seconde.
|
||||
|
||||
|
||||
##### `switch_root`\
|
||||
##### `switch_root` {-}
|
||||
\
|
||||
|
||||
Cette commande s'occupe de déplacer les partitions restantes pour vous, et lance
|
||||
la première commande (*init*) de votre choix.
|
||||
|
||||
|
||||
##### `pivot_root`\
|
||||
##### `pivot_root` {-}
|
||||
\
|
||||
|
||||
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é
|
||||
@ -147,9 +151,58 @@ Pour lancer la première commande dans la nouvelle racine, on passe généraleme
|
||||
par :
|
||||
|
||||
<div lang="en-US">
|
||||
```bash
|
||||
```
|
||||
42sh# exec chroot / command
|
||||
```
|
||||
</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>
|
||||
|
||||
|
||||
:::::
|
||||
|
@ -1,8 +1,9 @@
|
||||
\newpage
|
||||
|
||||
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
|
||||
|
||||
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">
|
||||
```
|
||||
42sh# unshare -n ip a
|
||||
42sh# unshare --net ip a
|
||||
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
|
||||
```
|
||||
</div>
|
||||
|
||||
::::: {.warning}
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
@ -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 :
|
||||
|
||||
<div lang="en-US">
|
||||
```bash
|
||||
```
|
||||
42sh# ip netns add virli
|
||||
```
|
||||
</div>
|
||||
|
||||
::::: {.code}
|
||||
|
||||
La technique utilisée ici pour avoir des *namespaces* nommés est la même que
|
||||
celle que nous avons vue dans
|
||||
[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
|
||||
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
|
||||
des interfaces :
|
||||
|
||||
@ -67,7 +111,7 @@ D'ailleurs, cette interface est rapportée comme étant désactivée, activons-l
|
||||
via la commande :
|
||||
|
||||
<div lang="en-US">
|
||||
```bash
|
||||
```
|
||||
42sh# ip netns exec virli ip link set dev lo up
|
||||
```
|
||||
</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
|
||||
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
|
||||
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.
|
||||
@ -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` :
|
||||
|
||||
<div lang="en-US">
|
||||
```bash
|
||||
```
|
||||
42sh# ip link set veth1 netns virli
|
||||
```
|
||||
</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 :
|
||||
|
||||
<div lang="en-US">
|
||||
```bash
|
||||
```
|
||||
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
|
||||
```
|
||||
@ -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
|
||||
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
|
||||
supportant la technologie [802.1q](https://fr.wikipedia.org/wiki/IEEE_802.1Q)
|
||||
@ -165,8 +375,11 @@ derrière notre machine.
|
||||
```
|
||||
</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 -->
|
||||
|
||||
@ -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*.
|
||||
|
||||
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
|
||||
interfaces.
|
||||
|
||||
##### VEPA
|
||||
##### VEPA \
|
||||
|
||||
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é
|
||||
@ -198,7 +411,7 @@ Pour construire une nouvelle interface de ce type :
|
||||
</div>
|
||||
|
||||
|
||||
##### *Private*
|
||||
##### *Private* \
|
||||
|
||||
À 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
|
||||
@ -214,12 +427,12 @@ conteneur de la même machine.
|
||||
</div>
|
||||
|
||||
|
||||
##### *Bridge*
|
||||
##### *Bridge* \
|
||||
|
||||
À l'inverse des modes *VEPA* et *private*, les paquets sont routés selon leur
|
||||
adresse MAC : si jamais une adresse MAC est connue, le paquet est délivré à
|
||||
l'interface MACVLAN correspondante ; dans le cas contraire, le paquet est
|
||||
envoyé sur l'interface de sortie.
|
||||
En mode *Bridge*, les paquets sont routés selon leur adresse MAC : si jamais
|
||||
une adresse MAC est connue, le paquet est délivré à l'interface MACVLAN
|
||||
correspondante ; dans le cas contraire, le paquet est envoyé sur l'interface de
|
||||
sortie.
|
||||
|
||||
Pour construire une nouvelle interface de ce type :
|
||||
|
||||
@ -230,6 +443,28 @@ Pour construire une nouvelle interface de ce type :
|
||||
</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 {-}
|
||||
|
||||
Pour approfondir les différentes techniques de routage, je vous
|
||||
|
@ -1,5 +1,3 @@
|
||||
\newpage
|
||||
|
||||
Le *namespace* `PID` {#pid-ns}
|
||||
--------------------
|
||||
|
||||
@ -23,13 +21,92 @@ trouve.
|
||||
Première étape s'isoler :
|
||||
|
||||
<div lang="en-US">
|
||||
```bash
|
||||
```
|
||||
42sh# unshare --pid --fork /bin/bash
|
||||
inpidns# echo $$
|
||||
1
|
||||
```
|
||||
</div>
|
||||
|
||||
Nous utilisons ici l'option `-f`, pour que le passage dans le nouvel espace de
|
||||
noms des PID soit effectif (cf. [Introduction](#pid-ns-intro)).
|
||||
::::: {.question}
|
||||
|
||||
#### 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
|
||||
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é.
|
||||
|
||||
|
||||
#### Double isolation : ajout du *namespace* `mount`
|
||||
### Double isolation : ajout du *namespace* `mount`
|
||||
|
||||
Voici la nouvelle ligne de commande que l'on va utiliser :
|
||||
|
||||
<div lang="en-US">
|
||||
```bash
|
||||
```
|
||||
42sh# unshare --pid --mount --fork --mount-proc /bin/bash
|
||||
```
|
||||
</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`, 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
|
||||
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
|
||||
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 à
|
||||
|
@ -1,51 +1,65 @@
|
||||
\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](https://virli.nemunai.re/quiz/15).
|
||||
|
||||
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.
|
||||
- vos réponses à l'évaluation du cours,
|
||||
- [SRS] tous les exercices de ce TP,
|
||||
- [GISTRE] l'avancement des paliers du [projet final](https://virli.nemunai.re/project-gistre.pdf).
|
||||
|
||||
|
||||
Tarball
|
||||
Arborescence attendue (SRS)
|
||||
-------
|
||||
|
||||
Tous les exercices de ce TP sont à placer dans une tarball (pas d'archive ZIP,
|
||||
RAR, ...).
|
||||
Tous les fichiers identifiés comme étant à rendre sont à placer dans un dépôt
|
||||
Git privé, que vous partagerez avec [votre
|
||||
professeur](https://gitlab.cri.epita.fr/nemunaire/).
|
||||
|
||||
Voici une arborescence type (adaptez les extensions et les éventuels
|
||||
fichiers supplémentaires associés au langage que vous aurez choisi
|
||||
pour chaque exercice) :
|
||||
Voici une arborescence type (vous pourriez avoir des fichiers supplémentaires) :
|
||||
|
||||
<div lang="en-US">
|
||||
```
|
||||
login_x-TP4/cmpns.sh
|
||||
login_x-TP4/mydocker_exec.sh
|
||||
login_x-TP4/myswitch_root.sh
|
||||
./cmpns.sh
|
||||
./mydocker_exec.sh
|
||||
./myswitch_root.sh
|
||||
```
|
||||
</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
25
tutorial/4/setns-pid.md
Normal 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).
|
@ -1,5 +1,5 @@
|
||||
#### Exemple C
|
||||
|
||||
#### Exemple C {-}
|
||||
\
|
||||
Voici un exemple de code C utilisant `setns(2)` :
|
||||
|
||||
<div lang="en-US">
|
||||
@ -35,10 +35,58 @@ main(int argc, char *argv[])
|
||||
```
|
||||
</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">
|
||||
```bash
|
||||
@ -55,3 +103,25 @@ chaton
|
||||
jument
|
||||
```
|
||||
</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.
|
||||
|
@ -1,71 +1,15 @@
|
||||
\newpage
|
||||
|
||||
### Rejoindre un *namespace*
|
||||
|
||||
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
|
||||
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/...`).
|
||||
|
||||
|
||||
#### Voir les *namespace*s d'un processus
|
||||
|
||||
Chaque processus lancé est 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 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)`.
|
||||
Une particularité de ces fichiers, que l'on ne peut pas afficher (leurs liens
|
||||
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
|
||||
passer à `setns(2)`.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
::::: {.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.
|
||||
|
||||
:::::
|
||||
lien symbolique : la commande se chargera d'`open(2)` le fichier pour obtenir
|
||||
le *file descriptor* nécessaire.
|
||||
|
@ -3,7 +3,7 @@ title: Virtualisation légère -- TP n^o^ 4
|
||||
subtitle: Linux Internals partie 2
|
||||
author: Pierre-Olivier *nemunaire* [Mercier]{.smallcaps}
|
||||
institute: EPITA
|
||||
date: Jeudi 7 octobre 2021
|
||||
date: Mercredi 9 novembre 2022
|
||||
abstract: |
|
||||
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
|
||||
@ -12,15 +12,8 @@ abstract: |
|
||||
|
||||
\vspace{1em}
|
||||
|
||||
Tous les exercices de ce TP sont à rendre à <virli@nemunai.re> au
|
||||
plus tard le mercredi 4 novembre 2021 à 23 h 42. Consultez la dernière
|
||||
section de chaque partie pour plus d'informations sur les éléments à
|
||||
rendre. Et n'oubliez pas de répondre aux [questions de
|
||||
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/).
|
||||
Les exercices de ce cours sont à rendre au plus tard le mardi 15
|
||||
novembre 2022 à 23 h 42. Consultez les sections matérialisées par un
|
||||
bandeau jaunes et un engrenage pour plus d'informations sur les
|
||||
éléments à rendre.
|
||||
...
|
||||
|
@ -1,5 +1,3 @@
|
||||
\newpage
|
||||
|
||||
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*.
|
||||
|
||||
Grâce à cette technique, il est possible de lancer des conteneurs en tant que
|
||||
simple utilisateur ; le projet [Singularity](https://sylabs.io/) repose
|
||||
en grande partie sur cela.
|
||||
simple utilisateur ; les projets [`podman`](https://podman.io/) et
|
||||
[Singularity](https://sylabs.io/), ... reposent en grande partie sur cela.
|
||||
|
||||
|
||||
### 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.
|
||||
|
||||
|
||||
#### L'utilisateur -2 : *nobody*
|
||||
#### L'utilisateur -2 : *nobody*\
|
||||
|
||||
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,
|
||||
@ -45,13 +43,14 @@ l'utilisateur *nobody* et au groupe *nogroup*.
|
||||
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
|
||||
*namespace*, ces deux fichiers, accessibles dans `/proc/self/`, peuvent être
|
||||
écrits une fois.
|
||||
|
||||
##### `uid_map`\
|
||||
##### `uid_map` {-}
|
||||
\
|
||||
|
||||
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.
|
||||
|
||||
|
||||
##### `gid_map`\
|
||||
##### `gid_map` {-}
|
||||
\
|
||||
|
||||
Le principe est identique pour ce fichier, mais agit sur les correspondances
|
||||
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">
|
||||
```bash
|
||||
|
Loading…
Reference in New Issue
Block a user