Lab avec UML

· Read in about 11 min · (2155 words)

Il m’arrive très souvent de monter des laboratoires avec des VMs pour faire des tests réseaux. Pour faire des laboratoires “jetables”, j’utilisais avant QEMU/KVM, mais c’était lourd à monter pour faire mes tests, puis j’ai découvert User-Mode Linux (UML).

UML va permettre de rapidement exécuter des machines virtuelles dans un système Linux et cela me convient parfaitement pour faire mes tests sur OvS. Je vais donc vous présenter cette technologie.

User-Mode Linux

UML est un outil qui permet d’exécuter un noyau linux dans un User-Space, c’est-à-dire qu’il sera considéré comme un processus. Cet outil présente différent avantages:

  • Léger, car ne nécessite pas toute une pile de virtualisation, comme QEMU
  • Si UML plante, elle ne fait pas planter le système hôte
  • S’appuie sur le noyau du système

Installation d’UML

Pour utiliser UML, il est nécessaire d’installer le paquet user-mode-linux et nous aurons la commande linux ou vmlinux.

Mon système étant sous ArchLinux, la plupart des chemins que vous verrez s’appuie sur mon environnement. Si vous utilisez d’autres distributions, modifier ces valeurs pour correspondre à votre environnement.

Création d’une première VM

Pour démarrer notre instance UML, il suffit de saisir la commande vmlinux ou linux:

Core dump limits :
	soft - NONE
	hard - NONE
Checking that ptrace can change system call numbers...OK
Checking syscall emulation patch for ptrace...OK
Checking advanced syscall emulation patch for ptrace...OK
Checking environment variables for a tempdir...none found
Checking if /dev/shm is on tmpfs...OK
Checking PROT_EXEC mmap in /dev/shm...OK
Adding 2056192 bytes to physical memory to account for exec-shield gap
Linux version 5.5.11-1-usermodelinux (geoffrey@geoffrey-pc) (gcc version 9.2.0 (GCC)) #1 Tue Mar 24 08:25:31 CET 2020
Built 1 zonelists, mobility grouping on.  Total pages: 8575
Kernel command line: root=98:0
Dentry cache hash table entries: 8192 (order: 4, 65536 bytes, linear)
Inode-cache hash table entries: 4096 (order: 3, 32768 bytes, linear)
mem auto-init: stack:off, heap alloc:off, heap free:off
Memory: 26812K/34776K available (3150K kernel code, 804K rwdata, 1020K rodata, 130K init, 175K bss, 7964K reserved, 0K cma-reserved)
NR_IRQS: 16
clocksource: timer: mask: 0xffffffffffffffff max_cycles: 0x1cd42e205, max_idle_ns: 881590404426 ns
Calibrating delay loop... 7669.35 BogoMIPS (lpj=38346752)
pid_max: default: 32768 minimum: 301
Mount-cache hash table entries: 512 (order: 0, 4096 bytes, linear)
Mountpoint-cache hash table entries: 512 (order: 0, 4096 bytes, linear)
*** VALIDATE tmpfs ***
*** VALIDATE proc ***
*** VALIDATE cgroup1 ***
*** VALIDATE cgroup2 ***
Checking that host ptys support output SIGIO...Yes
Checking that host ptys support SIGIO on close...No, enabling workaround
devtmpfs: initialized
random: get_random_u32 called from bucket_table_alloc+0x129/0x154 with crng_init=0
umid_file_name : buffer too short
clocksource: jiffies: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 19112604462750000 ns
futex hash table entries: 256 (order: 0, 6144 bytes, linear)
NET: Registered protocol family 16
clocksource: Switched to clocksource timer
VFS: Disk quotas dquot_6.6.0
VFS: Dquot-cache hash table entries: 512 (order 0, 4096 bytes)
*** VALIDATE ramfs ***
NET: Registered protocol family 2
tcp_listen_portaddr_hash hash table entries: 256 (order: 0, 4096 bytes, linear)
TCP established hash table entries: 512 (order: 0, 4096 bytes, linear)
TCP bind hash table entries: 512 (order: 0, 4096 bytes, linear)
TCP: Hash tables configured (established 512 bind 512)
UDP hash table entries: 256 (order: 1, 8192 bytes, linear)
UDP-Lite hash table entries: 256 (order: 1, 8192 bytes, linear)
NET: Registered protocol family 1
printk: console [stderr0] disabled
mconsole (version 2) initialized on /home/geoffrey/.uml/i78dgi/mconsole
Checking host MADV_REMOVE support...OK
workingset: timestamp_bits=62 max_order=13 bucket_order=0
io scheduler mq-deadline registered
io scheduler kyber registered
NET: Registered protocol family 10
Segment Routing with IPv6
sit: IPv6, IPv4 and MPLS over IPv4 tunneling driver
NET: Registered protocol family 17
Initialized stdio console driver
Console initialized on /dev/tty0
printk: console [tty0] enabled
Initializing software serial port version 1
printk: console [mc-1] enabled
Failed to initialize ubd device 0 :Couldn't determine size of device's file
epollctl add err fd 1, Operation not permitted
VFS: Cannot open root device "98:0" or unknown-block(98,0): error -6
Please append a correct "root=" boot option; here are the available partitions:
Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(98,0)
CPU: 0 PID: 1 Comm: swapper Not tainted 5.5.11-1-usermodelinux #1
Stack:
 61c2bd40 6031bee4 60069000 603abd77
 60069078 00000000 61c2bd50 6031bf29
 61c2be70 6003920a 61c2bd70 61c2bdf0
Call Trace:
 [<60069078>] ? printk+0x0/0x94
 [<6001e273>] show_stack+0x13b/0x155
 [<6031bee4>] ? dump_stack_print_info+0xdf/0xe8
 [<60069000>] ? kmsg_dump_rewind_nolock+0x16/0x35
 [<60069078>] ? printk+0x0/0x94
 [<6031bf29>] dump_stack+0x2a/0x2c
 [<6003920a>] panic+0x18c/0x3be
 [<6031d4de>] ? klist_next+0x0/0xd3
 [<6003907e>] ? panic+0x0/0x3be
 [<60069078>] ? printk+0x0/0x94
 [<600023f7>] mount_root+0x0/0xba
 [<60324b48>] ? strncmp+0x0/0x1f
 [<60324b48>] ? strncmp+0x0/0x1f
 [<6001b6ef>] ? do_one_initcall+0x0/0x1d0
 [<600024ad>] mount_root+0xb6/0xba
 [<6000264e>] prepare_namespace+0x19d/0x1f1
 [<60001eaf>] kernel_init_freeable+0x212/0x221
 [<60069078>] ? printk+0x0/0x94
 [<6032b0f1>] kernel_init+0x27/0x136
 [<6001cfe9>] new_thread_handler+0x81/0xb2

Cependant, nous avons un kernel panic, car le système cherche le root fs: Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(98,0).

Pour permettre à UML de booter correctement, nous devons monter notre système de fichier. Pour cela, nous allons voir deux méthodes: la première c’est d’utiliser un rootfs, c’est-à-dire de monter tout un système de fichier complet et la seconde méthode est d’utiliser hostfs qui va monter le système de fichier de l’hôte. Ce système de fichier est propre à UML.

Démarrer une instance via un rootfs

Pour monter notre instance UML via rootfs, nous devons créer dans un sous-répertoire notre système. Les exemples qui suivent illustre la création d’un système avec ArchLinux.

Pour les utilisateurs Debian, vous pouvez utiliser debootstrap pour installer un système dans un sous-répertoire

Préparation du file system

Pour permettre de démarrer notre instance UML en rootfs, nous devons créer notre file system. Pour réaliser cette opération, nous allons créer un fichier de 1G:

$ dd if=/dev/zero of=test-block count=1024 bs=1048576
$ ll -lah test-block
-rw-r--r-- 1 geoffrey geoffrey 1.0G Mar 29 07:50 test-block
$ mke2fs test-block 
mke2fs 1.45.4 (23-Sep-2019)
Discarding device blocks: done                            
Creating filesystem with 307200 1k blocks and 76912 inodes
Filesystem UUID: f786dc4c-7bac-4d36-8208-ea726dfc409e
Superblock backups stored on blocks: 
	8193, 24577, 40961, 57345, 73729, 204801, 221185

Allocating group tables: done                            
Writing inode tables: done                            
Writing superblocks and filesystem accounting information: done 

$ file test-block 
test-block: Linux rev 1.0 ext2 filesystem data, UUID=f786dc4c-7bac-4d36-8208-ea726dfc409e (large files)

Maintenant que nous avons créé un file system en ext2, nous allons monter notre système:

# mount -o loop test-block /mnt/
# mkdir -p /mnt/var/pacman
# pacman -Sy base -r /mnt
# cd /mnt/dev && mknod --mode=660 ubd0 b 98 0
# chown root:disk ubd0
# cat etc/fstab
/dev/ubd0 / ext2 defaults 0 0
# umount /mnt
Démarrage de notre instance

Nous pouvons maintenant nous connecter à la machine UML et faire un test:

$ vmlinux ubda=test-block mem=256m
sh-5.0# free -m
free[243]: segfault at 552acd2000 ip 0000000040070202 sp 0000007fbf88dca0 error 6 in libprocps.so.7.1.0[4006c000+11000]

Comme le montre le résultat de free -m, nous avons un segmentation fault. En effet, il manque la partition /proc. Nous devons la monter ainsi que d’autres partitions pour pouvoir faire nos tests dessus:

sh-5.0# mount -t proc proc /proc
sh-5.0# mount -t sysfs sysfs /sys
sh-5.0# mount -t tmpfs tmpfs /var/run -o rw,nosuid,nodev
sh-5.0# mount -t tmpfs tmpfs /var/log -o rw,nosuid,nodev
sh-5.0# free -m
              total        used        free      shared  buff/cache   available
Mem:            246           3         240           0           2         238
Swap:             0           0           0
sh-5.0# fdisk -l
Disk /dev/ubda: 1 GiB, 10737418240 bytes, 2048 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes

Nous montons les systèmes de fichier virtuel procfs et sysfs respectivement dans /proc et /sys, puis les répertoires /var/[log,run] dans un système fichier temporaire tmpfs.

Voilà, notre système est prêt à être utilisé, néanmoins, nous n’allons pas trop nous attarder sur cette partie, car honnêtement, je n’utilise pas le rootfs, car je souhaite surtout monter mon laboratoire le plus rapidement possible pour mes tests.

Démarrer une instance via hostfs

La seconde méthode est d’utiliser hostfs pour monter le système de fichier de l’hôte, par ailleurs, nous allons demander à UML d’utiliser directement l’interpréteur shell pour avoir une console:

$ vmlinux init=/bin/sh rootfstype=hostfs mem=256m user=${USER}
[...]
Segment Routing with IPv6
sit: IPv6, IPv4 and MPLS over IPv4 tunneling driver
NET: Registered protocol family 17
Initialized stdio console driver
Console initialized on /dev/tty0
printk: console [tty0] enabled
Initializing software serial port version 1
printk: console [mc-1] enabled
Failed to initialize ubd device 0 :Couldn't determine size of device's file
VFS: Mounted root (hostfs filesystem) readonly on device 0:12.
devtmpfs: mounted
This architecture does not have kernel memory protection.
Run /bin/sh as init process
sh: cannot set terminal process group (-1): Inappropriate ioctl for device
sh: no job control in this shell
sh-5.0#

Nous montons ensuite les partitions /proc et /sys:

sh-5.0# mount -t proc proc /proc
sh-5.0# mount -t sysfs sysfs /sys
sh-5.0# mount -t tmpfs tmpfs /var/run -o rw,nosuid,nodev
sh-5.0# mount -t tmpfs tmpfs /var/log -o rw,nosuid,nodev
sh-5.0# mount -t hostfs hostfs /home/${user}/lab-test -o /home/${user}/lab-test

Comme vous pouvez le constater, les autres systèmes de fichier, comme /etc ne sont qu’en lecture seule, ce qui va fournir une petite couche de sécurité et éviter de décrire notre système.

Par ailleurs, on monte notre /home/${user} dans un système de fichier hostfs en lecture et écriture:

sh-5.0# mount -t hostfs hostfs /home/${user}/lab-test/ -o /home/${user}/lab-test/
sh-5.0# touch /home/${user}/lab-test/toto

Communication en réseau

Maintenant que nous pouvons créer et démarrer une instance UML, nous allons lui ajouter dans un réseau. Pour cela, je vais m’appuyer sur Open vSwitch.

Après avoir installé le paquet OvS pour votre distribution, démarrer le service. Pour m’a part, je ne démarre pas via un initd ou systemd, mais depuis les commandes ovsdb-server et ovs-vswitch. Voici les commandes que j’utilise pour démarrer le service OvS:

ovsdb-server --pidfile=/run/openvswitch/ovsdb-server.pid --detach --remote=punix:/run/openvswitch/db.sock
ovs-vswitchd --pidfile --detach unix:/run/openvswitch/db.sock
ovs-vsctl --db=unix:/run/openvswitch/db.sock --no-wait init

Maintenant qu’il est démarré, nous allons créer un bridge et une interface tap. Une interface tap est une interface créée dans le User-Space et va permettre de transmettre les données envoyées par le noyau vers l’application qui utilise cette interface. Pour utiliser ces interfaces virtuelles, il faut installer le paquet uml_utilities.

L’exemple ci-dessous va permettre de créer un switch virtuel sw0 ainsi qu’une interface TAP tap0:

# Create a new bridge
ovs-vsctl --db=unix:/run/openvswitch/db.sock add-br sw0

# Create a new TAP interface
tunctl -b -u ${USER} -t tap0

# Connect TAP to bridge
ovs-vsctl --db=unix:/run/openvswitch/db.sock add-port sw0 tap0

Nous pouvons maintenant connecter notre instance UML à l’interface tap:

$ vmlinux init=/bin/sh rootfstype=hostfs eth0=tuntap,tap0 mem=256m
[...]
$ ip link set eth0 up
$ ip addr add 192.168.2.2/24 dev eth0

Vous pouvez maintenant démarrer d’autres instances et ils pourront communiquer entre eux via le réseau, puisqu’ils sont connectés sur le même OvS.

Script complet

Voici le script complet que j’utilise pour démarrer une instance UML.

#!/bin/sh

# Start and configure OvS
init_ovs() {
  if [ ! -f /run/openvswitch/ovsdb-server.pid ]; then
    sudo mkdir /run/openvswitch
  	sudo ovsdb-server --pidfile=/run/openvswitch/ovsdb-server.pid --detach --remote=punix:/run/openvswitch/db.sock
  	sudo ovs-vswitchd --pidfile --detach unix:/run/openvswitch/db.sock
    sudo ovs-vsctl --db=unix:/run/openvswitch/db.sock --no-wait init
  fi
}

configure_ovs() {
  echo "Create OpenFlow's rules"
  sudo ovs-vsctl --db=unix:/run/openvswitch/db.sock set bridge sw0 protocols=OpenFlow10,OpenFlow13
  sudo ovs-ofctl -O OpenFlow13 add-flow sw0 in_port=1,actions=output:2
  sudo ovs-ofctl -O OpenFlow13 add-flow sw0 in_port=2,actions=output:1
}

add_tap_to_ovs(){
  # Check if bridge exist
  if [ ! -d /sys/class/net/$1 ]; then
  	sudo ovs-vsctl --db=unix:/run/openvswitch/db.sock --no-wait add-br $1
  fi
 
  echo "Add $2 to $1" 
  sudo ovs-vsctl --db=unix:/run/openvswitch/db.sock add-port $1 $2
}

new_tap() {
  switch=$1
  if [ ! -d /sys/class/net/$2 ]; then
    echo "Create a new tap $2"
    sudo tunctl -b -u ${USER} -t $2
  fi

  # Add tap to ovs
  add_tap_to_ovs $switch $2
}

start_vm() {
  vmlinux init=/bin/sh rootfstype=hostfs eth0=tuntap,$2 hostname=$1 user=${USER}
  cleanup $2 $3
}

cleanup() {
  sudo ovs-vsctl --db=unix:/run/openvswitch/db.sock del-port $1 $2
}

case $1 in
  "start")
    if [ $# -gt 3  ]; then
      name=$2
      switch=$3
      tap=$4

      echo ":::: Configuring OvS"
  	  init_ovs
      configure_ovs
  	  new_tap $switch $tap
  	  
      echo ":::: Starting $name"
  	  start_vm $name $switch $tap
    else
      echo "Please, you must specify 3 arguments: <name> <switch> <tap>";
    fi  
    ;;
  "config")
    # When we are in UML, we configure it
    echo ":::: Configure $hostname"
    
    export PATH=/usr/local/bin:/usr/bin:/bin:/sbin:/usr/local/sbin:/usr/sbin
    mount -t proc proc /proc
    mount -t sysfs sysfs /sys
    mount -t tmpfs tmpfs /var/run -o rw,nosuid,nodev
    mount -t tmpfs tmpfs /var/log -o rw,nosuid,nodev
    mount -o bind /usr/lib/uml/modules /lib/modules
    mount -t hostfs hostfs /home/${user}/lab-test -o /home/${user}/lab-test
 
    ip link set eth0 up 
    case $hostname in
      "vm0") 
        ip addr add 192.168.2.2/24 dev eth0
        ;;
      "vm1")
        ip addr add 192.168.2.3/24 dev eth0
        ;; 
     esac
    ;;
esac

Ce script prend différents arguments. Si nous souhaitons démarrer une nouvelle instance UML, nous devons spécifier l’argument start ainsi que les arguments du nom de la VM, le switch et l’interface:

./exec.sh start vm0 sw0 tap0

Après avoir exécuté ce script, je suis dans la machine UML. A cette étape-là, je dois la configurer, pour cela, je re-exécute le script mais en spécifiant l’argument config:

sh-5.0# ./home/geoffrey/GIT/lab-test/exec.sh config
:::: Configure vm0

Conclusion

J’utilise souvent Open vSwitch et faire des tests de configuration et j’ai besoin de monter rapidement un laboratoire et faire mes tests de performance, UML satisfait à mes besoins pour l’instant. Cependant UML peut aussi montrer ces limites, donc j’utilise aussi QEMU pour faire d’autres tests qui peuvent être plus poussés, j’en parlerais d’ailleurs dans un autre article.

Pour ceux qui veulent approfondir cette technologie, voici quelques sources qui peuvent vous intéresser: