Dans les séries américaines, on appelle ce genre d’histoire un crossover, les premières occurrences ont démarré dans Linux Pratique, puis une partie plus profonde sur l’amincissement d’un noyau NetBSD, pas nécessairement utile pour la finalité de notre produit, a fait son apparition dans GNU/Linux Magazine. Aujourd’hui, nous allons apprendre à construire un système BSD UNIX, NetBSD, From Scratch.
Un petit rappel s’impose. L’objectif final de cette expérience est de créer des micro-services à la serverless, autrement dit des machines virtuelles qui démarrent en moins d’une seconde avec le service désiré (serveur web, mail, proxy IRC…) afin d’obtenir une isolation totale entre les différents services, modulo d’éventuels problèmes de sécurité impliquant l’hyperviseur.
Dans GNU/Linux Magazine N°265 [1], nous avons appris comment charcut^Wmodifier un noyau NetBSD pour faire démarrer celui-ci en quelques centaines de millisecondes. Dans le cadre de cet article, nous utiliserons un outil tierce partie, confkerndev [2] qui réalise cette opération de façon simple et rapide.
1. Un disque plein de zéros
Tel l’artiste peintre nous allons nécessiter une toile vierge pour réaliser notre expérience ; cette dernière prend la forme d’un fichier qui aura la taille du disque virtuel que nous allons utiliser et sera rempli de zéros, littéralement. Le but de l’opération est de créer un périphérique de type « bloc », soit un disque dur logique, que le noyau considèrera comme tel, et de l’affubler de tous les éléments nécessaires au fonctionnement d’un système d’exploitation.
Une telle opération nécessite l’utilisation du programme dd(1), véritable couteau suisse de la manipulation de fichiers bruts, en voici la syntaxe :
Explication : on utilise en entrée le périphérique /dev/zero qui renvoie uniquement des zéros et on envoie le résultat dans le fichier disque.img, avec une taille de bloc de 1 mégaoctet, 10 fois, ce qui créera un fichier rempli de zéros de 10 mégaoctets. On aurait parfaitement pu produire le même résultat en utilisant bs=10M count=1 ou encore bs=100K count=100.
Munis de cette matière brute, nous allons maintenant la « formater », ou plus précisément, ordonner ce disque pour que le système d’exploitation puisse y inscrire des données et le retrouver, l’organiser donc. Puisque nous menons notre expérience sous GNU/Linux, nous réalisons cette opération à l’aide de mke2fs, car en effet, même si le système cible est NetBSD, ce dernier est parfaitement capable de lire, et même démarrer, sur un système de fichiers ext2, propre au noyau Linux. Un bémol néanmoins, le noyau NetBSD ne supporte pas les dernières additions à ce format, comme par exemple la journalisation, il faut dont ajouter l’option -O none à la ligne de commandes pour formater notre image au format ext2 sans aucune fonctionnalité supplémentaire :
Maintenant que nous disposons d’un périphérique de type bloc formaté, nous pouvons le monter ! GNU/Linux implémente une fonctionnalité permettant d’« attacher » un fichier à un périphérique virtuel, le loopback device. Ce tour de passe-passe est effectué en coulisses par la commande mount lorsqu’elle est appelée avec l’option -o loop :
Un disque ma foi parfaitement normal.
2. Aménagement
Nous avons créé le contenant, il est temps de s’occuper du contenu. NetBSD, fidèle héritier des systèmes BSD UNIX, est composé de deux sources d’outils, le système de base (base system) et les paquets tiers (pkgsrc). Au contraire de GNU/Linux, le système de base ne se présente pas sous forme de paquets de tel ou tel format, il s’agit simplement d’archives au format tar.gz qu’on appelle des sets. Ces sets contiennent les outils de base qui constituent un système d’exploitation NetBSD, pensez par exemple à ls, df, ps, du… mais également aux serveurs fondamentaux tels que postfix, bind ou encore sshd. Le contenu de ces sets est référencé dans le répertoire /etc/mtree, chaque set étant associé à un fichier set.<categorie>, dans ces derniers, on trouve le type, le mode, la taille et le hash sha256 du fichier concerné, par exemple pour set.base :
Ces archives se téléchargent simplement sur les divers miroirs du projet NetBSD, ou plus rapidement sur le CDN NetBSD [3], gracieusement proposé par Fastly.
Pour cet article, nous travaillerons avec la version 9.3 de NetBSD, aussi nous récupèrerons les sets à cette adresse https://cdn.netbsd.org/pub/NetBSD/NetBSD-9.3/i386/binary/sets/.
Parmi tous ces sets, il en est un particulier : rescue.tgz. Ce set est peuplé avec les outils les plus communs destinés à réparer un système en mauvais état, ces outils sont liés statiquement afin d’en diminuer les dépendances sur des fichiers dispersés sur un disque potentiellement endommagé, et réduits à leur plus simple expression, c’est-à-dire qu’ils ne sont affublés d’aucune fonctionnalité superflue.
Une autre particularité est que tous ces outils sont en réalité compilés en un seul binaire dont la fonctionnalité sera déterminée par argv[0], soit le nom du programme appelé, comme le réalise la trousse à outils busybox [4] sous GNU/Linux. En conséquence, le contenu de rescue.tgz se résume à une centaine d’outils qui sont tous des liens vers /bin/[ dont la taille est de… 7Mo :
Pour les plus curieux, l’outil utilisé pour générer cette trousse à outils se nomme crunchgen(1) et se trouve dans /usr/bin dans n’importe quel système NetBSD.
Peuplons donc notre disque vierge avec le contenu de ce set :
Ceci va extraire le set dans le répertoire mnt/rescue.
L’objectif de cette expérience étant de faire démarrer un système sur ce disque virtuel, nous avons besoin d’un fichier /etc/rc, qui est exécuté par le premier processus démarré par le noyau, init. Le code source du noyau NetBSD en charge de l’appel à init [5] montre le tableau suivant :
Tableau parcouru par la fonction start_init 10 lignes plus loin, qui exécutera la première occurrence existante de la liste. Comme on peut le voir, /rescue/init fait partie des possibilités, aussi, inutile de copier ce dernier dans /sbin.
On peut constater l’appel au fichier /etc/rc susnommé dans le code source d’init [6], nous allons créer ce fichier de démarrage avec un contenu basique :
Dans ce script :
- on s’assure que le chemin vers l’interpréteur est bien /rescue (mais on pourrait aussi bien créer un lien de /rescue/sh vers /bin/sh) ;
- on déclare la variable $HOME pour les programmes qui en auront besoin ;
- on s’assure d’ajouter /rescue au $PATH ;
- on crée un masque de création de fichiers classique, 022, soit u=rwx,g=rx,o=rx ;
- on mount tous les systèmes de fichiers ;
- on affiche un texte « témoin » ;
- on exécute sh, qui est dans le $PATH précédemment déclaré.
Le point 5 implique que nous devons créer un fichier /etc/fstab afin d’indiquer à mount quelles sont les partitions disponibles :
Le disque que nous allons monter au démarrage de cette machine virtuelle est de type VirtIO [7], ces périphériques sont présentés avec le nom ld[0-9][a-z] où [0-9] est le numéro du disque et [a-z] la partition. Ici, nous montons la première partition du premier disque.
Mais ceci implique encore un ajout dans notre disque virtuel, au minimum la création du fichier de périphérique correspondant. Cette opération est réalisée avec le programme mknod(1) :
Quelques explications s’imposent. On invoque le programme mknod(1) dont le but est de créer des fichiers spéciaux, des fichiers de périphériques de type bloc (typiquement des disques ou équivalents) ou caractère (typiquement des périphériques dont on lit le contenu séquentiellement, comme par exemple un port série). Ici, on indique que le mode du fichier sera 640 (u=rw,g=r,o=), que son nom est ld0a, qu’il s’agit d’un périphérique de type bloc et que les identifiants major et minor du périphérique sont respectivement 19 et 0. Le premier chiffre, le major, indique le numéro affecté par le noyau au périphérique dont nous souhaitons créer l’interface. Ce chiffre, sous GNU/Linux, est indiqué par le fichier virtuel /proc/devices, sous NetBSD, c’est la commande sysctl kern.drivers qui indique quels sont les nombres liés au pilote, ici ld(4). Et j’ai bien dit les nombres, car les systèmes héritiers de BSD UNIX ont deux modes d’accès possibles pour manipuler un disque, le mode brut (raw), qui permet d’accéder au disque de façon directe, octet par octet, et le mode bloc, de plus haut niveau, dans lequel le disque est lu par blocs de taille fixe, à l’aide de fonctionnalités de plus haut niveau mises en place par le noyau.
La commande sysctl kern.drivers (sur un système NetBSD) nous indique que le pilote ld répond à deux majors :
Dans la plupart des systèmes BSD UNIX, on préfère aujourd’hui utiliser la commande doas au lieu de sudo, car plus simple et plus lisible. Le premier ne bénéficie cependant pas des innombrables capacités du dernier.
69 est le major du périphérique brut, et 19 celui du périphérique bloc, que l’on souhaite utiliser pour monter notre partition. Finalement, le minor est simplement le numéro du périphérique, ici 0 puisqu’il s’agit du premier disque sur ce système vierge.
Avant de lancer QEMU, il conviendra de démonter le disque virtuel afin que toutes les modifications apportées soient bien inscrites, cela se fera simplement avec la commande umount mnt.
3. Punch it Chewie!
Le noyau que nous utiliserons est un noyau NetBSD à qui on a fait subir une cure d’amaigrissement forcée, et dont le détail de la réalisation est expliqué dans GLMF n°265 ; en deux mots, nous pouvons réduire son temps de chargement à un dixième de secondes en désactivant tous les pilotes de périphériques inutiles dans un environnement virtualisé.Pour cet article, nous nous contenterons d’utiliser confkerndev, œuvre d’un certain Denis Bodor, qui réalise cette tâche en quelques instants. La procédure est la suivante :
Télécharger un noyau NetBSD et le décompresser :
Comme nous l’avons expliqué dans l’article dédié à cette opération dans GNU/Linux Magazine n°265, seuls les noyaux i386 supportent l’appel direct avec le paramètre -kernel de QEMU.
Préparer confkerndev puis lui passer en paramètre le noyau à opérer ainsi que les pilotes de périphériques à conserver :
La liste des pilotes de périphériques est la suivante :
Spartiate.
Bien ! Ces prérequis installés sur notre mini système, pouvons-nous le démarrer ?
La machine virtuelle sera prise en charge par l’inénarrable QEMU qu’on lancera avec l’option -enable-kvm afin de disposer de l’accélération matérielle pour la virtualisation grâce au module noyau kvm. Nous n’aurons pas besoin de plus de 256Mo. La console, l’élément basique et indispensable nécessaire à l’affichage, est passée en paramètre au noyau NetBSD, on déclare que cette dernière sera disponible sur le port série (com). On déclare également que le système de fichiers racine (root filesystem) sera disponible sur le périphérique ld0a. On indique à QEMU que le port série sera redirigé vers stdio (la sortie standard) en préfixant cette dernière par mon: de façon à gérer l’appui sur Control-C et ne pas quitter l’émulateur quand cette séquence est pressée. On indique à QEMU qu’il n’y aura pas de sortie vidéo avec l’option -display none et finalement le chemin et le mode d’accès du disque virtuel à utiliser :
Résultat ?
Bruit de Millenium Falcon qui rate son passage en vitesse lumière...
Ok, ok, premier test de décollage raté, MAIS plusieurs choses sont à noter, premièrement on visualise bien que c’est /rescue/init qui a été utilisé pour démarrer, ensuite, un indice clé, ce kernel panic est lié à la sortie du programme init, et plus particulièrement une sortie avec un code de retour 11. Or l’analyse du code source de init [8] nous montre le code suivant :
Dans le header include/paths.h, on peut lire :
Et effectivement, nous n’avons pas créé le périphérique console. Comme pour le périphérique disque, nous créons ce dernier à l’aide de mknod(1) :
Et cette fois :
Victoire, nous avons atterri sur le shell appelé depuis /etc/rc en moins d’une seconde, en consommant à peine 7Mo ! À partir d’ici, plus de limite, nous pouvons construire l’OS qui nous plaît, brindille par brindille.
4. MicroVM ?
Nous sommes en possession d’une base de travail assez solide et reproductible pour envisager son utilisation réelle, en effet, ce mini système d’exploitation met pratiquement le même temps à démarrer qu’un simple programme, le surcoût de faire s’exécuter le service de notre choix dans /etc/rc serait négligeable, et permettrait d’isoler chaque service non pas à l’aide de simples namespaces à l’instar de Docker, mais d’une machine virtuelle complète.
La technique n’est pas nouvelle, c’est à peu près de cette façon que les services « serverless » d’AWS Lambda fonctionnent, en démarrant en une fraction de seconde des micromachines virtuelles qui exécutent du code qui leur est fourni. La solution créée et utilisée par AWS, Firecracker [9], est par ailleurs libre, son code source est disponible sur un dépôt GitHub dédié [10].
Afin de rendre cette microVM totalement opérationnelle avec un logiciel classique, il nous manque encore plusieurs entrées dans le répertoire /dev, pensez par exemple à null, zero, random, mais aussi stdin, stdout, stderr… Heureusement pour nous, nous allons retrouver tous ces périphériques indispensables dans un fichier habituellement invoqué par l’installateur NetBSD, /dev/MAKEDEV, qui comme son nom l’indique, crée les devices. Ce fichier est un script shell dans lequel on trouve un case intéressant pour notre scénario minimaliste :
Le cas std crée en effet les périphériques « standards » dont un système d’exploitation UNIX classique a besoin.
Armés de ces informations et de notre précédente expérience, nous disposons de tout le nécessaire pour nous fendre d’un petit script shell qui servira à générer une image disque minimale. Nous allons ajouter quelques raffinements :
- le script doit être portable, il peut fonctionner sous GNU/Linux ou NetBSD ;
- pour d’éventuels futurs portages, il devra être POSIX [11] ;
- on peut lui passer des informations en paramètre.
Les paramètres en question :
- la taille de l’image ;
- son nom ;
- le ou les set(s) à utiliser ;
- un nom de service, qui nous servira à personnaliser l’image créée.
La portabilité porte essentiellement les aspects suivants :
- le contenu de la fstab ;
- la syntaxe de dd ;
- la gestion du loopback.
Voici le script en question :
En regard de nos expériences durant cet article, les spécificités que nous avons énumérées prennent la forme suivante.
On crée sur la machine servant à générer l’image, au niveau du script, un répertoire etc qui contiendra un fichier fstab.<OS>, soit fstab.Linux ou fstab.NetBSD. Ceci permettra de différencier le système de fichiers à créer et à monter.
On crée également un répertoire service qui va contenir des personnalisations relatives à une image. Par exemple, on voudra certainement un fichier /etc/rc différent, et potentiellement des entrées dans /etc spécifiques. Mais comme on aura probablement besoin d’avoir des troncs communs, un répertoire service/common contiendra des scripts qui seront copiés dans /etc/include sur la cible et qu’on pourra inclure.
On se donne également la possibilité de réaliser des opérations post-installation, en exécutant tout script présent dans service/<nom du service>/postinst, par exemple :
Attention : Le script est lancé avec l’utilisateur root, ce qui signifie que les opérations post-installation également, prenez bien garde à utiliser des chemins relatifs et non absolus !
Une utilisation typique du script prendra la forme :
Ce qui aura pour effet de générer une image base.img, utilisera les personnalisations créées dans service/base, générera un disque de 300Mo et y extraira le contenu des sets base et etc.
Ce script, et l’ensemble des prérequis sont disponibles dans le dépôt GitLab du projet mksmolnb [12] dont je suis l’auteur.
Conclusion
Notre preuve de concept fonctionne, mais nécessite maintenant un peu de travail pour la rendre exploitable en production, les étapes que nous avons suivies sont maintenant automatisées à l’aide d’un script, reste à trouver une manière élégante de déployer un service utile sur les microVM ainsi créées.
Nous utiliserons pour réaliser ces tâches les outils utilisés depuis la nuit des temps par les divers contributeurs des systèmes BSD à travers les âges, sh et make… mais ça, ce sera pour la prochaine fois ;)
Références
[1] iMil, « SmolBSD : un système UNIX de 7 mégaoctets qui démarre en moins d’une seconde », GNU/Linux Magazine n°265, septembre 2023 :
https://connect.ed-diamond.com/gnu-linux-magazine/glmf-265/smolbsd-un-systeme-unix-de-7-megaoctets-qui-demarre-en-moins-d-une-seconde
[2] https://gitlab.com/0xDRRB/confkerndev
[5] https://github.com/NetBSD/src/blob/trunk/sys/kern/init_main.c#L948
[6] https://github.com/NetBSD/src/blob/trunk/sbin/init/init.c#L922
[7] https://wiki.libvirt.org/Virtio.html
[8] https://github.com/NetBSD/src/blob/trunk/sbin/init/init.c#L1742
[9] https://firecracker-microvm.github.io/
[10] https://github.com/firecracker-microvm/firecracker/