Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Conteneurs avec les namespaces sous Linux

Ce TD est à réaliser dans une machine virtuelle Ubuntu.

A l'INSA-CVL, se connecter à la machine ubuntu-namespaces.

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.

$ sudo apt-get -y -q install debootstrap

Nous utiliserons ici la version jammy (22.04 LTS) de la distribution Ubuntu, dans sa variante minimale :

$ mkdir rootfs
$ sudo debootstrap --variant=minbase jammy 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.

$ sudo apt-get -y -q install make

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)

// Function de départ pour le processus fils cloné
static int childFunc(void *arg)
{
    char **argv = arg;

    // TODO : Mise en place pour le processus fils

    execvp(argv[0], &argv[0]);
    errExit("execvp");
}

// Espace mémoire réservé pour la pile du processus fils
#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);

    // Le processus parent continue ici

    // Attent que le processus fils se termine
    if (waitpid(child_pid, NULL, 0) == -1)
        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

Compilez et essayez l'application avec :

$ make
$ sudo ./main ls
$ sudo ./main ps aux

Namespace PID

Nous allons ajouter la mise en place du namespace PID :

  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 ensuite 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 nouveau 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