virli/tutorial/4/namespaces.md

9.4 KiB

\newpage

Les namespaces

Introduction

Les espaces de noms du noyau, les 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.

S'isoler dans un nouveau namespace

Avec son coquillage

De la même manière que l'on peut utiliser l'appel système chroot(2) depuis un shell via la commande chroot(1), la commande unshare(1) permet de faire le 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 :

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.

Les appels systèmes

L'appel système par excellence pour contrôler l'isolation d'un nouveau processus est clone(2).

L'isolement ou non du processus est faite en fonction des flags qui sont passés à la fonction :

  • CLONE_NEWNS,
  • CLONE_NEWUTS,
  • CLONE_NEWIPC,
  • CLONE_NEWPID,
  • CLONE_NEWNET,
  • CLONE_NEWUSER,
  • CLONE_NEWCGROUP.

On peut bien entendu cumuler un ou plusieurs de ces flags, et les combiner avec d'autres flags attendu par la fonction.

Les mêmes flags sont utilisés lors des appels à unshare(2) ou setns(2).

Pour créer un nouveau processus qui sera à la fois dans un nouvel namespace réseau et dans un nouveau namespace CGroup, on écrirait un code similaire à :

#include <sched.h>

#define STACKSIZE (1024*1024)
static char child_stack[STACKSIZE];

int clone_flags = CLONE_CGROUP | CLONE_NEWNET | SIGCHLD;

pid_t pid = clone(do_execvp,
                  child_stack + STACKSIZE,
				  clone_flags,
				  &args);

Le premier argument est un pointeur sur fonction. Il s'agit de la fonction qui sera appelée par le nouveau processus.

Comparaison de namespace

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

42sh$ ./cmpns $(pgrep influxdb) $(pgrep init)
  - cgroup: differ
  - ipc: differ
  - mnt: differ
  - net: differ
  - pid: differ
  - user: same
  - uts: same
42sh$ ./cmpns $(pgrep init) self
  - cgroup: same
  - ipc: same
  - mnt: same
  - net: same
  - pid: same
  - user: same
  - uts: same

Ici, self fait référence au processus actuellement exécuté.

Et pourquoi pas :

42sh$ unshare -m ./cmpns $$ self
  - cgroup: same
  - ipc: same
  - mnt: differ
  - net: same
  - pid: same
  - user: same
  - uts: same

Rejoindre un namespace

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/ :

#define _GNU_SOURCE
#include <fcntl.h>
#include <sched.h>
#include <stdlib.h>

// ./a.out /proc/PID/ns/FILE cmd args...

int main(int argc, char *argv[])
{
    int fd = open(argv[1], O_RDONLY);
    if (fd == -1)
    {
      perror("open");
      return EXIT_FAILURE;
    }

    if (setns(fd, 0) == -1)
    {
      perror("setns");
      return EXIT_FAILURE;
    }

    execvp(argv[2], &argv[2]);

    perror("execve");
    return EXIT_FAILURE;
}

Dans un shell, on utilisera la commande nsenter(1) :

42sh# nsenter --uts=/proc/42/ns/uts /bin/bash

docker exec

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

Réécrivons, en quelques lignes, la commande docker exec !

Pour savoir si vous avez réussi, comparez les sorties des commandes :

  • ip address ;
  • hostname ;
  • mount ;
  • pa -aux ;
  • ...

Durée de vie d'un namespace

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, ...

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.

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 :

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.

On peut très bien utiliser directement ce fichier pour obtenir un descripteur de fichier valide vers le namespace (pour passer à setns(2)).

Faire persister un namespace

Il n'est pas possible de faire persister un namespace d'un reboot à l'autre.

Même en étant attaché à un fichier du disque, il s'agit d'un pointeur vers une structure du noyau, qui ne persistera pas au redémarrage.

Aller plus loin

Je vous recommande la lecture des man suivants :

  • namespaces(7) : introduisant et énumérant les namespaces ;

Pour tout connaître en détails, la série d'articles de Michael Kerrisk sur les namespaces est excellente ! Auquel il faut ajouter le petit dernier sur le CGroup namespace.

Cet article de Michael Crosby montrant l'utilisation de clone(2).