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
  1. Ajoutez le flag pour créer un processus fils dans un PID namespace distinct
  2. Faites appel à chroot(2) et chdir(2) pour utiliser le shell du rootfs obtenu avec debootstrap
  3. Montez /proc dans le rootfs pour obtenir les bonnes informations sur les processus
  4. 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.

  1. Installer le paquet iproute2 pour obtenir la command ip dans le chroot.
    Indices

    Pour cela vous pouvez utiliser chroot rootfs /bin/bash pour obtenir un shell puis installer le paquet avec apt update puis apt install iproute2.

  2. Une fois le processus fils lancé dans le noveau namespace, créer une paire d’interfaces réseau virtuelles veth à l’aide de la command ip 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().

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

  4. Configurer l’interface réseau à l’aide de ip dans le namespace du père et du fils.

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

Références