Work on TP4
This commit is contained in:
parent
6fd83df1fd
commit
58a228ef2d
13 changed files with 694 additions and 97 deletions
|
|
@ -1,17 +1,163 @@
|
|||
\newpage
|
||||
|
||||
# Utiliser les *namespaces*
|
||||
Les *namespaces*
|
||||
================
|
||||
|
||||
## Introduction
|
||||
|
||||
Les espaces de noms du noyau, *namespaces*, permettent de dupliquer certaines
|
||||
structures du noyau, dans le but de les isoler d'un groupe de processus à un
|
||||
autre.
|
||||
|
||||
On en dénombre 7 depuis Linux 4.6 : `CGroup`, `IPC`, `network`, `mount`, `PID`,
|
||||
`user` et `UTS`.
|
||||
|
||||
|
||||
### `mount` *namespaces*
|
||||
|
||||
Depuis Linux 2.4.19.
|
||||
|
||||
Isole la liste des points de montage.
|
||||
|
||||
Chaque processus d'un namespace différent peut monter, démonter et réorganiser
|
||||
à sa guise les points de montage. Une partition ne sera donc pas nécessairement
|
||||
démonté après un appel à `umount(2)`, elle le sera lorsqu'elle aura
|
||||
effectivement été démontée de chaque *namespace* dans lequel elle était montée.
|
||||
|
||||
|
||||
### `UTS` *namespaces*
|
||||
|
||||
Depuis Linux 2.6.19.
|
||||
|
||||
Isole le nom de machine et son domaine NIS.
|
||||
|
||||
|
||||
### `IPC` *namespaces*
|
||||
|
||||
Depuis Linux 2.6.19.
|
||||
|
||||
Isole les objets IPC et les files de messages POSIX.
|
||||
|
||||
Une fois le *namespace* attaché à un processus, il ne peut alors plus parler
|
||||
qu'avec les autres processus de son *namespace*.
|
||||
|
||||
|
||||
### `PID` *namespaces*
|
||||
|
||||
Depuis Linux 2.6.24.
|
||||
|
||||
Isole la liste des processus et virtualise leurs numéros.
|
||||
|
||||
Une fois dans un espace, le processus ne voit que le sous-arbre de processus
|
||||
également attachés à son espace. Il s'agit d'un sous-ensemble de l'arbre global
|
||||
de PID : les processus de tous les PID *namespaces* apparaissent donc dans
|
||||
l'arbre initial.
|
||||
|
||||
Pour chaque nouvel espace de noms de processus, une nouvelle numérotation est
|
||||
initié ; ainsi, le premier processus de cet espace porte le numéro 1 et aura
|
||||
les mêmes propriétés que le processus `init` usuel ; entre autres, si un
|
||||
processus est rendu orphelin dans ce *namespace*, il devient un fils de ce
|
||||
processus, et non un fils de l'`init` de l'arbre global.
|
||||
|
||||
|
||||
### `network` *namespaces*
|
||||
|
||||
Depuis Linux 2.6.29.
|
||||
|
||||
Fourni une isolation pour toutes les ressources associées aux réseaux : les
|
||||
interfaces, les piles protocolaires IPv4 et IPv6, les tables de routage,
|
||||
pare-feu, ports numérotés, etc.
|
||||
|
||||
Une interface réseau (`eth0`, `wlan0`, ...) ne peut se trouver que dans un seul
|
||||
*namespace* à la fois, mais il est possible de les déplacer.
|
||||
|
||||
Lorsque le *namespace* est libéré (généralement lorsque le dernier processus
|
||||
attaché à cet espace de noms se termine), les interfaces qui le composent sont
|
||||
ramenées dans l'espace initial (et non pas dans l'espace parent, en cas
|
||||
d'imbrication).
|
||||
|
||||
|
||||
### `user` *namespaces*
|
||||
|
||||
Depuis Linux 3.8.
|
||||
|
||||
Isole la liste des utilisateurs, des groupes, leurs identifiants, les
|
||||
capabilities, la racine et le trousseau de clefs du noyau.
|
||||
|
||||
La principale caractéristique est que les identifiants d'utilisateur et de
|
||||
groupe pour un processus peuvent être différent entre l'intérieur et
|
||||
l'extérieur du conteneur. Il est alors possible, alors que l'on est un simple
|
||||
utilisateur à l'extérieur du *namespace*, d'avoir l'UID 0 dans le conteneur.
|
||||
|
||||
|
||||
### `CGroup` *namespaces*
|
||||
|
||||
Depuis Linux 4.6.
|
||||
|
||||
Isole la vue de la racine des *Control Group* en la plaçant sur un
|
||||
sous-groupe de l'arborescence.
|
||||
|
||||
Ainsi, un processus dans un `CGroup` *namespace* ne peut pas voir le contenu
|
||||
des sous-groupes parents (pouvant laisser fuiter des informations sur le reste
|
||||
du système). Cela peut également permettre de faciliter la migration de
|
||||
processus (d'un système à un autre) : l'arborescences des cgroups n'a alors
|
||||
plus d'importance car le processus ne voit que son groupe.
|
||||
|
||||
|
||||
|
||||
|
||||
## Partir dans un nouveau *namespace*
|
||||
|
||||
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
|
||||
nécessaire pour appeler l'appel système `unshare(2)`, puis, tout comme
|
||||
`chroot(1)`, exécuter le programme passé en paramètre.
|
||||
|
||||
En fonction des options qui lui sont passées, `unshare(1)` va créer le/les
|
||||
nouveaux *namespaces* et placer le processus dedans.
|
||||
|
||||
Par exemple, nous pouvons modifier sans crainte le nom de notre machine, si
|
||||
nous sommes passé dans un autre *namespace* `UTS` :
|
||||
|
||||
```shell
|
||||
42sh# hostname --fqdn
|
||||
koala.zoo.paris
|
||||
42sh# sudo unshare -u /bin/bash
|
||||
bash# hostname --fqdn
|
||||
koala.zoo.paris
|
||||
bash# hostname lynx.zoo.paris
|
||||
bash# hostname --fqdn
|
||||
lynx.zoo.paris
|
||||
bash# exit
|
||||
42sh# hostname --fqdn
|
||||
koala.zoo.paris
|
||||
```
|
||||
|
||||
Nous avons pu ici modifier le nom de machine, sans que cela n'affecte notre
|
||||
machine hôte.
|
||||
|
||||
Essayons maintenant avec d'autres options de notre programme pour voir les
|
||||
effets produits : par exemple, comparons un `ip address` à l'extérieur et à
|
||||
l'intérieur d'un `unshare -n`.
|
||||
|
||||
|
||||
## Comparaison de *namespace*
|
||||
|
||||
Écrivez un script ou un programme, `cmpns`, dans le langage courant de votre
|
||||
choix, permettant de déterminer si deux programmes s'exécutent dans les mêmes
|
||||
*namespaces*.
|
||||
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.
|
||||
|
||||
Écrivons un script ou un programme, `cmpns`, permettant de déterminer si deux
|
||||
programmes s'exécutent dans les mêmes *namespaces*.
|
||||
|
||||
|
||||
### Exemples
|
||||
|
||||
```sh
|
||||
42sh$ cmpns $(pgrep influxdb) $(pgrep init)
|
||||
42sh$ ./cmpns $(pgrep influxdb) $(pgrep init)
|
||||
- cgroup: differ
|
||||
- ipc: differ
|
||||
- mnt: differ
|
||||
- net: differ
|
||||
|
|
@ -21,7 +167,8 @@ choix, permettant de déterminer si deux programmes s'exécutent dans les mêmes
|
|||
```
|
||||
|
||||
```sh
|
||||
42sh$ cmpns $(pgrep init) self
|
||||
42sh$ ./cmpns $(pgrep init) self
|
||||
- cgroup: same
|
||||
- ipc: same
|
||||
- mnt: same
|
||||
- net: same
|
||||
|
|
@ -32,102 +179,119 @@ choix, permettant de déterminer si deux programmes s'exécutent dans les mêmes
|
|||
|
||||
Ici, `self` fait référence au processus actuellement exécuté.
|
||||
|
||||
Et pourquoi pas :
|
||||
|
||||
```sh
|
||||
42sh$ unshare -m ./cmpns $$ self
|
||||
- cgroup: same
|
||||
- ipc: same
|
||||
- mnt: differ
|
||||
- net: same
|
||||
- pid: same
|
||||
- user: same
|
||||
- uts: same
|
||||
```
|
||||
|
||||
|
||||
## Rejoindre un *namespace*
|
||||
|
||||
Dans le langage courant de votre choix, écrivez un programme : `setns`,
|
||||
permettant, à la manière de `unshare(1)` et `unshare(2)`, d'utiliser `setns(2)`
|
||||
via votre interpréteur.
|
||||
Rejoindre un *namespace* se fait en utilisant l'appel système `setns(2)`,
|
||||
auquel on passe le *file descriptor* d'un des liens du dossier
|
||||
`/proc/<PID>/ns/` :
|
||||
|
||||
Les options attendues sont :
|
||||
```c
|
||||
#define _GNU_SOURCE
|
||||
#include <fcntl.h>
|
||||
#include <sched.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
* rejoindre un *namespace* IPC : `-i`, `--ipc` ;
|
||||
* rejoindre un *namespace* mount : `-m`, `--mount` ;
|
||||
* rejoindre un *namespace* net : `-n`, `--net` ;
|
||||
* rejoindre un *namespace* PID : `-p`, `--pid` ;
|
||||
* rejoindre un *namespace* UTS : `-u`, `--uts` ;
|
||||
* rejoindre un *namespace* user : `-U`, `--user`.
|
||||
// ./a.out /proc/PID/ns/FILE cmd args...
|
||||
|
||||
### Exemples
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
int fd = open(argv[1], O_RDONLY);
|
||||
if (fd == -1)
|
||||
{
|
||||
perror("open");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
```sh
|
||||
42sh# setns /bin/bash
|
||||
bash# _
|
||||
if (setns(fd, 0) == -1)
|
||||
{
|
||||
perror("setns");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
execvp(argv[2], &argv[2]);
|
||||
|
||||
perror("execve");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
```
|
||||
|
||||
#### IPC and PID Namespaces
|
||||
Dans un shell, on utilisera la commande `nsenter(1)` :
|
||||
|
||||
```sh
|
||||
42sh# setns --ipc=/proc/42/ns/ipc -p /proc/42/ns/pid /bin/echo toto
|
||||
toto
|
||||
```
|
||||
|
||||
#### Net Namespace
|
||||
|
||||
```sh
|
||||
42sh# setns --net=/proc/42/ns/net ip a
|
||||
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default
|
||||
link/loopback 00:00:00:00:00:00 brd 00:00:00:00::00
|
||||
inet 127.0.0.1/8 brd 127.255.255.255 scope lo
|
||||
valid_lft forever preferred_lft
|
||||
inet6 ::1/128 scope host
|
||||
valid_lft forever preferred_lft forever
|
||||
```
|
||||
|
||||
#### UTS Namespace
|
||||
|
||||
```sh
|
||||
42sh# hostname --fqdn
|
||||
koala.zoo.paris
|
||||
42sh# setns --uts=/proc/42/ns/uts hostname --fqdn
|
||||
lynx.zoo.paris
|
||||
```shell
|
||||
42sh# nsenter --uts=/proc/42/ns/uts /bin/bash
|
||||
```
|
||||
|
||||
|
||||
## My Little Container
|
||||
### `docker exec`
|
||||
|
||||
En utilisant le langage courant de votre choix, concevez l'exécutable `mlc`,
|
||||
permettant de lancer une application dans un environnement différent (comme un
|
||||
`chroot`, mais sans permettre de s'en échapper) et avec des privilèges réduits.
|
||||
Si vous avez bien suivi jusque là, vous avez dû comprendre qu'un `docker exec`,
|
||||
n'était donc rien de plus qu'un `nsenter(1)`.
|
||||
|
||||
Votre solution doit créer au moins un nouveau *namespace* mount et PID.
|
||||
Réécrivons, en quelques lignes de shell, la commande `docker exec` ! Pour
|
||||
savoir si vous avez réussi, comparez les sorties des commandes :
|
||||
|
||||
Vous aurez sans doute besoin de : `clone(2)`, `capabilities(7)`, `capset(2)`, `pivot_root(2)`,
|
||||
- `ip address` ;
|
||||
- `hostname` ;
|
||||
- `mount` ;
|
||||
- `pa -aux` ;
|
||||
- ...
|
||||
|
||||
### Exemples
|
||||
|
||||
```sh
|
||||
42sh# ls newroot
|
||||
bin etc home usr root proc var
|
||||
## Durée de vie d'un *namespace*
|
||||
|
||||
42sh# mlc newroot/ /bin/bash
|
||||
bash# ls ../../../
|
||||
bin etc home usr root proc var
|
||||
Le noyau tient à jour un compteur de référence pour chaque *namespace*. Dès
|
||||
qu'une référence tombe à 0, le *namespace* est automatiquement libéré, les
|
||||
points de montage sont démontés, les interfaces réseaux sont réattribués à
|
||||
l'espace de noms initial, ...
|
||||
|
||||
bash# escape_chroot ls
|
||||
bin etc home usr root proc var
|
||||
Ce compteur évolue selon plusieurs critères, et principalement selon le nombre
|
||||
de processus qui l'utilisent. C'est-à-dire que, la plupart du temps, le
|
||||
*namespace* est libéré lorsque le dernier processus s'exécutant dedans se
|
||||
termine.
|
||||
|
||||
bash# ls -ld /proc/[0-9]* | wc -l
|
||||
2
|
||||
Lorsque l'on a besoin de référencer un *namespace* (par exemple pour le faire
|
||||
persister après le dernier processus), on peut utiliser un `mount bind` :
|
||||
|
||||
bash# curl http://www.linuxcontainers.org/ | md5sum
|
||||
0123456789abcdef
|
||||
|
||||
bash# ping 8.8.8.8
|
||||
Operation not permitted
|
||||
```shell
|
||||
42sh# touch /tmp/ns/myrefns
|
||||
42sh# mount --bind /proc/<PID>/ns/mount /tmp/ns/myrefns
|
||||
```
|
||||
|
||||
De cette manière, même si le lien initial n'existe plus (si le `<PID>` s'est
|
||||
terminé), `/tmp/ns/myrefns` pointera toujours au bon endroit.
|
||||
|
||||
## Rendu
|
||||
On peut très bien utiliser directement ce fichier pour obtenir un descripteur
|
||||
de fichier valide vers le *namespace* (pour passer à `setns(2)`).
|
||||
|
||||
Pour chaque exercice de cette partie, vous pouvez rendre un seul fichier s'il
|
||||
s'agit d'un script ; sinon, vous devez rendre une tarball contenant un
|
||||
`Makefile` permettant de générer les éventuels exécutables et/ou un `README`
|
||||
expliquant comment s'en servir.
|
||||
### Faire persister un *namespace*
|
||||
|
||||
Vous devez donc rendre 3 fichiers : `cmpns` ou `cmpns.tar.bz2`, `setns` ou
|
||||
`setns.tar.bz2` et `mlc` ou `mlc.tar.bz2`.
|
||||
Il n'est pas possible de faire persister un namespace d'un reboot à l'autre.
|
||||
|
||||
\vspace{3em}
|
||||
Même en étant attaché à un fichier du disque, il s'agit d'un pointeur vers une
|
||||
structure du noyau, qui ne persistera pas au redémarrage.
|
||||
|
||||
Bon courage !
|
||||
|
||||
## Aller plus loin
|
||||
|
||||
Je vous recommande la lecture des *man* suivants :
|
||||
|
||||
* `namespaces(7)` : introduisant et énumérant les namespaces ;
|
||||
|
||||
Pour tout connaître en détails, [la série d'articles de Michael Kerrisk sur
|
||||
les *namespaces*](https://lwn.net/Articles/531114/) est excellente ! Auquel il
|
||||
faut ajouter [le petit dernier sur le CGroup
|
||||
*namespace*](https://lwn.net/Articles/621006/).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue