Work on TP4

This commit is contained in:
nemunaire 2016-10-19 05:24:05 +02:00
commit 58a228ef2d
13 changed files with 694 additions and 97 deletions

View file

@ -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/).