Conteneurs avec les namespaces sous Linux
Ce TD est à réaliser dans une machine virtuelle Ubuntu. Avec les machines vagrant :
$ vagrant up ubuntu
$ vagrant ssh ubuntu
Objectifs
- Utiliser les interfaces du noyau Linux pour lancer un conteneur
Création « manuelle » d’un conteneur
Nous allons créer et lancer un conteneur sans utiliser Docker pour comprendre les mécanismes utilisés et les interactions avec le noyau Linux.
Création d’une image système
Nous allons utiliser debootstrap
pour créer une racine de système de fichier (le contenu d’une image Docker) pour notre conteneur. Nous utiliserons ici la version focal
(20.04 LTS) de la distribution Ubuntu, dans sa variante minimale :
$ mkdir rootfs
$ sudo debootstrap --variant=minbase focal rootfs
Utilisation de unshare
Pour lancer un shell Bash
dans notre image, nous pouvons utiliser chroot
:
$ sudo chroot rootfs /bin/bash
Mais chroot
« n’isole » que la vue du système de fichier pour une application :
# A l'extérieur du chroot
$ top
$ lsns
# A l'intérieur du chroot
$ mount -t proc proc /proc
$ top
$ ps aux | grep top
$ lsns
$ umount /proc
Pour isoler la vue des processus, il faut utiliser les namespaces avec unshare
:
$ sudo unshare --pid --fork chroot rootfs /bin/bash
$ mount -t proc proc /proc
$ ps aux
$ lsns
Notez que nous utilisons des versions distinctes de la distribution Ubuntu à l’intérieur du conteneur de celle de l’hôte. Dans notre cas, c’est une version stable LTS sur un hôte récent non LTS. En général, on souhaite utiliser une distribution stable bien supportée comme hôte et potentiellement une distribution plus récente dans un conteneur pour bénéficier des dernières évolution d’un langage ou d’un compilateur.
Interface du noyau Linux : namespaces
Nous allons reproduire les commandes ci-dessus à l’aide d’un programme en C faisant directement appel à l’interface du noyau.
Voici un programme qui utilise l’appel système clone(2) pour créer un processus fils :
main.c
(télécharger) :
// Modified from https://lwn.net/Articles/533492/
// Copyright 2013, Michael Kerrisk
// Licensed under GNU General Public License v2 or later
#define _GNU_SOURCE
#include <sched.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdio.h>
#include <sys/mount.h>
/* A simple error-handling function: print an error message based
on the value in 'errno' and terminate the calling process */
#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
} while (0)
/* Start function for cloned child */
static int childFunc(void *arg)
{
char **argv = arg;
// TODO: Mise en place pour le processus fils
execvp(argv[0], &argv[0]);
errExit("execvp");
}
/* Space for child's stack */
#define STACK_SIZE (1024 * 1024)
static char child_stack[STACK_SIZE];
int main(int argc, char *argv[])
{
int flags = 0;
pid_t child_pid;
// TODO: mise en place du flag pour le PID namespace
child_pid = clone(childFunc, child_stack + STACK_SIZE, flags | SIGCHLD, &argv[1]);
if (child_pid == -1)
errExit("clone");
printf("%s: PID of child created by clone() is %ld\n", argv[0], (long) child_pid);
/* Parent falls through to here */
if (waitpid(child_pid, NULL, 0) == -1) /* Wait for child */
errExit("waitpid");
printf("%s: terminating\n", argv[0]);
exit(EXIT_SUCCESS);
}
et un Makefile
(télécharger) :
.PHONY: all clean
all:
gcc -o main -Wall -Wextra main.c
clean:
rm -f main
- Ajoutez le flag pour créer un processus fils dans un PID namespace distinct
- Faites appel à chroot(2) et chdir(2) pour utiliser le shell du rootfs obtenu avec debootstrap
- Montez /proc dans le rootfs pour obtenir les bonnes informations sur les processus
- Validez le résultat avec les commandes :
ps aux
ls -al /proc/self/ns
lsns
Namespace réseau
Nous allons maintenant ajouter le support du namespace réseau.
- Installer le paquet
iproute2
pour obtenir la commandip
dans le chroot.Indices
Pour cela vous pouvez utiliser
chroot rootfs /bin/bash
pour obtenir un shell puis installer le paquet avecapt update
puisapt install iproute2
. Une fois le processus fils lancé dans le noveau namespace, créer une paire d’interfaces réseau virtuelles
veth
à l’aide de la commandip
suivante dans le processus père:ip link add name veth0 type veth peer name veth1 netns [child pid]
Vous pouvez par exemple appeler cette commande à l’aide de la fonction
system()
.Ajouter un appel à
sleep()
dans le processus fils pour qu’il attende que l’interface réseau soit créée par le père dans son namespace avant d’essayer de la configurer.Configurer l’interface réseau à l’aide de
ip
dans le namespace du père et du fils.- Exécuter la command ping depuis le fils pour communiquer avec le père.
Solution
Une solution pour cette section est proposée dans l’article : Linux – Writing a Simple Container App with Namespaces, Liran B. H.