<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Blog ALDO TECH</title>
    <link>https://aldo.tech/fr/blog/</link>
    <atom:link href="https://aldo.tech/fr/blog/feed.xml" rel="self" type="application/rss+xml"/>
    <description>Notes de terrain : transport public, billettique, matériel et logiciel qui doivent fonctionner sur la route.</description>
    <language>fr</language>
    <item>
      <title>urMQTT après le VPS : SQLite en mode utilisateur, comparaison avec Mosquitto, configuration comme donnée et constructeur dans le navigateur</title>
      <link>https://aldo.tech/fr/blog/urmqtt-config-builder-plaintext-mosquitto/</link>
      <guid isPermaLink="true">https://aldo.tech/fr/blog/urmqtt-config-builder-plaintext-mosquitto/</guid>
      <pubDate>Sun, 01 Nov 2026 08:00:00 +0000</pubDate>
      <description>Ce qui s&#x27;est passé après le deuxième article sur urMQTT : SQLite isolé hors du noyau, mesures face à Mosquitto sur le même VPS (et ce qui n&#x27;a pas joué en notre faveur), causes trouvées de la lenteur d&#x27;un seul flux, configuration comme donnée avec un constructeur web d&#x27;ISO et un émulateur dans le navigateur, MQTT en clair facultatif et ce qui n&#x27;est toujours pas démontré.</description>
      <content:encoded><![CDATA[<p>Le deuxième article, <a href="https://aldo.tech/blog/urmqtt-vps-handshake-disk-hardening/">urMQTT sur un vrai VPS</a>, se terminait par une liste « La suite » : H6 (SQLite hors du noyau), H7 (sécurité du temps), X11 (comparaison avec Mosquitto sur le même VPS) et la première publication. Cet article suit cette liste dans le même ordre, puis ajoute deux choses qui n'y figuraient pas, parce qu'elles sont apparues en chemin : la configuration comme donnée, avec un constructeur web d'ISO, et un MQTT en clair facultatif, sans TLS. Comme avant : ce que nous avions prévu, ce que nous avons construit, ce qui nous a surpris et ce qui reste ouvert. Cet article contient aussi quelques résultats qui ne vont pas dans le sens d'urMQTT ; nous les avons conservés parce qu'ils font partie de la vérité.</p>
<p>La précision d'emblée reste la même : le projet est en pré-alpha, la première publication ne concerne que les VPS avec KVM, et les chiffres mesurent du code, des tests et certaines expériences précises, pas une maturité en production.</p>
<h2 id="a-quoi-cela-ressemblait-dans-le-temps">À quoi cela ressemblait dans le temps</h2>
<p>Depuis l'article précédent (31 octobre à 23:00) jusqu'à aujourd'hui (1er novembre à 20:41), le dépôt compte 59 nouveaux commits, 178 au total. Tous sont nés en un seul jour calendaire, donc la même mise en garde qu'avant s'applique : le projet se construit avec Claude Code (les commits portent <code>Co-Authored-By: Claude</code>), le plan, le code, les tests et les ADR naissent en boucles courtes et fermées, et les chiffres ci-dessous parlent de la quantité de code et de vérifications, pas de la part de travail humain qu'ils contiennent.</p>
<figure><img loading="lazy" alt="Croissance depuis l'article précédent : lignes de Rust de 89 790 à 112 408, fonctions #[test] de 739 à 926, ADR de 104 à 126 et commits de 119 à 178. Chaque ligne a sa propre échelle." src="https://aldo.tech/blog/urmqtt-config-builder-plaintext-mosquitto/img/growth-since.svg" /><figcaption>Croissance depuis l'article précédent : lignes de Rust de 89 790 à 112 408, fonctions #[test] de 739 à 926, ADR de 104 à 126 et commits de 119 à 178. Chaque ligne a sa propre échelle.</figcaption></figure>
<p>Les chiffres sont calculés avec la même méthode que dans l'article précédent : tous les fichiers <code>.rs</code> hors de <code>vendor/</code> et <code>fuzz/</code>, les fonctions <code>#[test]</code>, les titres <code>## ADR-</code> dans <code>docs/DECISIONS.md</code>.</p>
<table>
<thead>
<tr>
<th>Moment</th>
<th>Commit</th>
<th>Heure</th>
<th style="text-align: right;">Lignes de Rust</th>
<th style="text-align: right;">Tests</th>
<th style="text-align: right;">ADR</th>
</tr>
</thead>
<tbody>
<tr>
<td>article précédent</td>
<td>794f607</td>
<td>31.10. 23:00</td>
<td style="text-align: right;">89 790</td>
<td style="text-align: right;">739</td>
<td style="text-align: right;">104</td>
</tr>
<tr>
<td>H6 terminé</td>
<td>a0423cc</td>
<td>1.11. 03:59</td>
<td style="text-align: right;">95 078</td>
<td style="text-align: right;">754</td>
<td style="text-align: right;">107</td>
</tr>
<tr>
<td>X11</td>
<td>a29f49b</td>
<td>1.11. 07:56</td>
<td style="text-align: right;">95 579</td>
<td style="text-align: right;">754</td>
<td style="text-align: right;">108</td>
</tr>
<tr>
<td>constructeur web et miroir des publications</td>
<td>582783f</td>
<td>1.11. 16:49</td>
<td style="text-align: right;">105 645</td>
<td style="text-align: right;">848</td>
<td style="text-align: right;">122</td>
</tr>
<tr>
<td>MQTT en clair, documentation</td>
<td>8f959f1</td>
<td>1.11. 19:05</td>
<td style="text-align: right;">112 408</td>
<td style="text-align: right;">926</td>
<td style="text-align: right;">125</td>
</tr>
<tr>
<td>aujourd'hui</td>
<td>d21e3ec</td>
<td>1.11. 20:41</td>
<td style="text-align: right;">112 408</td>
<td style="text-align: right;">926</td>
<td style="text-align: right;">126</td>
</tr>
</tbody>
</table>
<p>On a travaillé en parallèle sur plusieurs branches, donc l'ordre des commits n'est pas strictement l'ordre du travail. Les ADR sont numérotés jusqu'à 232, mais la numérotation a des trous (une partie a été renumérotée lors de la fusion de branches) ; nous comptons les titres, pas le plus grand numéro. Publications : v0.2.0 (1er novembre, 18:45 UTC) et v0.2.1 (1er novembre, 20:22 UTC), toutes deux marquées comme pré-publication.</p>
<h2 id="h6-sqlite-en-mode-utilisateur">H6 : SQLite en mode utilisateur</h2>
<p><strong>Ce que nous avions prévu.</strong> SQLite, c'est 254 268 lignes de C qui analysent la base, le WAL et le journal depuis le disque, et le disque peut être fabriqué ou modifié. Jusque-là, il tournait dans le noyau, avec les droits du noyau. <strong>Ce que nous avons construit</strong> (ADR-210 à ADR-212) : SQLite, ses parties de libc et son heap ont été déplacés dans un programme séparé, <code>dbworker</code>, qui s'exécute en EL0 (aarch64) ou en ring 3 (x86_64), dans son propre espace d'adressage. Le noyau ne lie plus SQLite. Le worker dialogue avec le noyau par une fenêtre de mémoire partagée et douze appels système, et chaque longueur et chaque décalage renvoyés sont vérifiés avant usage. Une erreur dans le worker (accès interdit, instruction non définie, budget de temps de 30 s dépassé) termine l'appel, le worker redémarre et la base se rouvre à partir du WAL comme au démarrage ; après 5 redémarrages en une minute, le stockage est déclaré défectueux au lieu de redémarrer indéfiniment.</p>
<p>Le coût, mesuré sur aarch64 sous HVF, avec la même VM, noyau avec SQLite intégré contre noyau avec worker, six exécutions alternées de chaque :</p>
<table>
<thead>
<tr>
<th>Mesure</th>
<th>Avant (dans le noyau)</th>
<th>Après (worker)</th>
<th>Différence</th>
</tr>
</thead>
<tbody>
<tr>
<td>entrée et sortie du mode utilisateur</td>
<td>-</td>
<td>1,1 us par aller-retour</td>
<td></td>
</tr>
<tr>
<td><code>db-bench</code>, 5000 insertions en 50 transactions, meilleur / médiane</td>
<td>61,9k / 58,4k op/s</td>
<td>54,7k / 53,3k op/s</td>
<td>-11,6% / -8,7%</td>
</tr>
<tr>
<td>une insertion par transaction, meilleur / médiane</td>
<td>2319 / 2148 op/s</td>
<td>2307 / 2126 op/s</td>
<td>-0,5% / -1%</td>
</tr>
<tr>
<td>sauvegarde complète en un seul appel (23 MiB)</td>
<td>514 ms</td>
<td>635 ms</td>
<td>+23%</td>
</tr>
</tbody>
</table>
<p>Les écritures sur disque (1572) et les flushs (319) sont identiques, comme il se doit. Ce qui n'est pas isolé : <strong>le noyau continue de faire confiance à ce que le worker renvoie</strong>, après vérification de la forme. Un worker compromis peut mentir (par exemple renvoyer une liste vide de certificats révoqués, et ainsi les « dé-révoquer »), mais il ne peut lire ni la mémoire du noyau, ni les clés TLS, ni les clés de ticket. Il n'y a pas de KPTI, donc les fuites de type Spectre/Meltdown ne sont pas empêchées et nous n'affirmons rien à leur sujet ; la clé PAC est partagée par EL0 et EL1. Sur x86_64, nous n'avons pas mesuré le coût de la bascule sur KVM (sous TCG le chiffre n'aurait aucun sens), et le fuzzing du worker et du protocole n'est pas terminé. Le groupe de tests de stockage (79 scénarios, chacun fait tourner SQLite dans le worker) passe sur aarch64 HVF et x86_64 TCG, et 76 sur aarch64 TCG (un est ignoré parce que TCG n'a pas MSI-X).</p>
<p><strong>H7 est clos, mais pas comme nous l'avions annoncé.</strong> La borne inférieure du temps (ADR-050) et SNTP avec consensus (T1) couvrent le périmètre de H7 ; NTS (RFC 8915) reste hors périmètre et est consigné comme risque résiduel, et non comme quelque chose qui fonctionne. Nous ne considérons pas que la sécurité du temps est « résolue », mais que sa partie démontrable est terminée.</p>
<p><strong>Une nouvelle option réseau.</strong> Contabo ne donne pas de DHCP à un ISO personnalisé, donc jusque-là le VPS exigeait une autre image que la VM de labo. Désormais <code>network.mode = "auto"</code> (ADR-214) interroge d'abord DHCP et, si aucun bail n'arrive dans le délai fixé (<code>dhcp_timeout_s</code>, 15 s par défaut) ou si le bail est perdu, applique l'adresse statique indiquée. Essayé à la main sur QEMU (avec DHCP il utilise le bail, sans réseau il revient à l'adresse statique après 5 s) ; il n'existe pas encore de scénario de smoke test pour cela.</p>
<h2 id="x11-comparaison-avec-mosquitto-sur-le-meme-vps">X11 : comparaison avec Mosquitto sur le même VPS</h2>
<p>Le même type de machine que dans l'article précédent (Contabo, 4 vCPU AMD EPYC, 8 GiB), charge depuis un hôte Linux à 15 ms, TLS 1.3 mutuel avec les mêmes certificats des deux côtés, Mosquitto 2.0.21 sur Debian 13 sans persistance. La méthode et les décisions sont dans l'ADR-213, les données brutes dans <code>docs/perf-results/</code>. Médiane de trois exécutions.</p>
<figure><img loading="lazy" alt="Poignées de main complètes par seconde (TLS 1.3 + certificat client + CONNECT, sans reprise de session) à 32, 64 et 128 clients : urMQTT 169, 278, 413 ; Mosquitto 82, 140, 184. Ce sont des bornes inférieures : le générateur de charge était proche de sa propre limite." src="https://aldo.tech/blog/urmqtt-config-builder-plaintext-mosquitto/img/mosquitto.svg" /><figcaption>Poignées de main complètes par seconde (TLS 1.3 + certificat client + CONNECT, sans reprise de session) à 32, 64 et 128 clients : urMQTT 169, 278, 413 ; Mosquitto 82, 140, 184. Ce sont des bornes inférieures : le générateur de charge était proche de sa propre limite.</figcaption></figure>
<table>
<thead>
<tr>
<th></th>
<th>urMQTT</th>
<th>Mosquitto 2.0.21</th>
</tr>
</thead>
<tbody>
<tr>
<td>poignées de main/s, 32 / 64 / 128 clients (profil bench)</td>
<td>169 / 278 / 413</td>
<td>82 / 140 / 184</td>
</tr>
<tr>
<td>idem, avec les limites telles que livrées</td>
<td>185 / 262 / 333 (à 128 : 202 à 286 erreurs, réinitialisations de poignée de main)</td>
<td>-</td>
</tr>
<tr>
<td>mémoire par connexion inactive (20 000 connexions)</td>
<td>0,50 KB de heap du noyau (0,66 KB à 5000)</td>
<td>26,8 KB de croissance du processus</td>
</tr>
<tr>
<td>latence p50, QoS 0 / QoS 1, 800 messages/s, 64 B</td>
<td>14,0 / 14,8 ms</td>
<td>21,4 / 39,5 ms</td>
</tr>
<tr>
<td>QoS 1, 4800 messages/s, livrés/s (p50)</td>
<td>4209 (77 ms)</td>
<td>4235 (197 ms)</td>
</tr>
<tr>
<td>100 abonnés, 50 messages/s : p50</td>
<td>16,6 ms</td>
<td>99 ms</td>
</tr>
<tr>
<td>un seul flux, QoS 0, 64 B, publiés/s</td>
<td>2962 (0 à 7733)</td>
<td>22 749</td>
</tr>
</tbody>
</table>
<p>Ce que cela signifie, et ce que cela ne signifie pas. Les poignées de main sont des <strong>bornes inférieures pour les deux serveurs</strong> : le générateur de charge est une VM partagée avec 60 à 70% de temps processeur volé et, à 128 threads, il était proche de sa propre limite, alors que les cœurs de travail d'urMQTT n'étaient occupés qu'à 12 à 13%. Le rapport à 128 clients (environ 2,2 fois) est le plus significatif ; à 32 et 64, on mesure surtout l'attente des clients. Les chiffres de mémoire ne comptent pas la même chose (heap du noyau contre RSS du processus), donc « environ 50 fois moins » est un ordre de grandeur, pas un rapport précis. La latence de 14 ms est un aller-retour réseau (environ 15 ms), donc « pas de temps de broker mesurable » ; les queues (p99 autour de 100 à 200 ms) sont dominées chez les deux brokers par des hôtes partagés. Nous n'affirmons pas les différences inférieures à environ 20%.</p>
<p><strong>Ce qui n'a pas joué en notre faveur</strong> et est resté dans les tableaux : un seul flux TCP à travers urMQTT portait 2 à 8 fois moins qu'à travers Mosquitto, avec des variations de zéro à 7,7 milliers de messages par seconde entre les exécutions, et le watchdog de 20 ms du handler <strong>coupait des connexions vivantes</strong> lorsque l'hyperviseur arrêtait un vCPU (dans la configuration de production <code>action = "drop"</code>, 6 à 8 connexions par exécution lors d'un déluge QoS 0). Les deux sont décrits dans la section suivante, car ils avaient des causes dans notre code.</p>
<h2 id="pourquoi-un-seul-flux-etait-lent-des-causes-pas-des-excuses">Pourquoi un seul flux était lent : des causes, pas des excuses</h2>
<p>Le plus simple aurait été de dire « VPS partagé ». Nous ne l'avons pas fait : avec le même VPS et la même méthode, nous avons cherché des causes. Cinq ont été trouvées, quatre dans le chemin des données et une dans le watchdog (ADR-221 à ADR-224) :</p>
<table>
<thead>
<tr>
<th>Symptôme dans X11</th>
<th>Cause dans urMQTT</th>
<th>Changement</th>
</tr>
</thead>
<tbody>
<tr>
<td>un flux 1,9k messages/s, souvent <strong>0 livré</strong></td>
<td><strong>Réveil perdu :</strong> la boucle de sortie écrasait la liste des connexions prêtes par celle qui venait d'être écrite ; un abonné dont le tampon de sortie s'était rempli une fois n'était plus jamais servi jusqu'à ce qu'un PINGRESP le réveille</td>
<td>corrigé (ADR-223)</td>
</tr>
<tr>
<td>quelques pour cent de perte font passer 48k à 14k messages/s</td>
<td><strong>chaque segment après un segment perdu était rejeté</strong> (réception seulement dans l'ordre), donc la récupération coûtait un aller-retour réseau par segment</td>
<td>file de segments hors ordre, SACK (ADR-222)</td>
</tr>
<tr>
<td>plafond de 4,4 MB/s à 15 ms</td>
<td>fenêtre de 16 bits, file d'envoi de 64 KiB et slow start qui comptait des ACK et non des octets</td>
<td>mise à l'échelle de la fenêtre, horodatages, comptage en octets (ADR-222)</td>
</tr>
<tr>
<td>chaque tour d'un flux qui tient dans la fenêtre attend 0 à 10 ms</td>
<td><strong>le dispatcher dormait jusqu'au tick suivant de 10 ms</strong> (virtio-net n'a pas d'interruption de réception)</td>
<td>interrogation de la NIC toutes les 200 us tant qu'il existe des connexions (ADR-224)</td>
</tr>
<tr>
<td>1 à 8 connexions coupées par exécution</td>
<td><strong>le watchdog facturait au handler le temps écoulé</strong>, et le VPS arrête les vCPU souvent et longtemps</td>
<td>le budget est le temps que le cœur a réellement travaillé (ADR-221)</td>
</tr>
</tbody>
</table>
<p>Les arrêts sont réels : sur ce VPS, les cœurs 0 à 2 avaient une médiane de 36 à 80 ticks en retard par 5 secondes, et l'arrêt le plus long dans la fenêtre était d'une médiane d'environ 70 ms et jusqu'à 0,9 s (cœur 3, base et journal : jusqu'à 1,55 s), alors que l'hôte ne signale pas de temps volé (<code>host steal 0 us</code>). C'est pourquoi le watchdog avait besoin d'une autre horloge, pas d'un budget plus grand.</p>
<figure><img loading="lazy" alt="Avant et après, sur le même VPS avec la même méthode, médiane de trois à six exécutions : un flux QoS 0, 64 B : publiés 1,9k, 23,7k (Mosquitto 29,3k), livrés 0 (médiane), 12,6k (29,2k). 16 paires, déluge, 64 B : publiés 24,7k, 43,2k (31,4k), livrés 20,0k, 37,4k (30,7k)." src="https://aldo.tech/blog/urmqtt-config-builder-plaintext-mosquitto/img/before-after.svg" /><figcaption>Avant et après, sur le même VPS avec la même méthode, médiane de trois à six exécutions : un flux QoS 0, 64 B : publiés 1,9k, 23,7k (Mosquitto 29,3k), livrés 0 (médiane), 12,6k (29,2k). 16 paires, déluge, 64 B : publiés 24,7k, 43,2k (31,4k), livrés 20,0k, 37,4k (30,7k).</figcaption></figure>
<table>
<thead>
<tr>
<th></th>
<th>code X11 (7f9517e)</th>
<th>maintenant (86a71ff)</th>
<th>Mosquitto</th>
</tr>
</thead>
<tbody>
<tr>
<td>un flux, QoS 0, 64 B, publiés / livrés par s</td>
<td>1,9k / 0</td>
<td>23,7k / 12,6k</td>
<td>29,3k / 29,2k</td>
</tr>
<tr>
<td>un flux, QoS 0, 1 KiB</td>
<td>0 / 0</td>
<td>3,9k / 2,3k</td>
<td>5,4k / 5,4k</td>
</tr>
<tr>
<td>16 paires, déluge, QoS 0, 64 B</td>
<td>24,7k / 20,0k</td>
<td>43,2k / 37,4k</td>
<td>31,4k / 30,7k</td>
</tr>
<tr>
<td>16 paires, déluge, QoS 0, 1 KiB</td>
<td>3,7k / 2,9k</td>
<td>5,0k / 4,5k</td>
<td>5,9k / 5,7k</td>
</tr>
<tr>
<td>connexions perdues par exécution (16 paires, 64 B)</td>
<td>13 (7 à 16)</td>
<td>0 (0 à 0)</td>
<td>0</td>
</tr>
</tbody>
</table>
<p>Watchdog : 148 connexions coupées en 30 exécutions chargées avant, 0 en 24 exécutions après ; sur un dispositif local qui retarde et perd des trames, avec 0,1% de perte, un flux de 64 B à 14,3k messages/s (noyau X11) en a maintenant 41,0k, et avec 0,5%, 5,0k passent à 17,7k.</p>
<p><strong>Ce que cela ne dit pas.</strong> urMQTT <strong>n'est toujours pas plus rapide que Mosquitto sur un seul flux</strong> : à 64 B il publie environ 80% de sa vitesse et livre environ 40%, parce qu'il rejette du QoS 0 quand le flux de l'abonné est plus lent que celui de l'éditeur (Mosquitto, sans limite, met en file). À 1 KiB, dans le déluge à 16 paires, Mosquitto est devant d'environ 15%, ce qui est dans le bruit. Le générateur était de nouveau une VM partagée avec 58 à 77% de temps volé. Le deuxième tour (verrouillage du heap hors du chemin des données, SACK côté envoi et autres) se trouve sur la branche non intégrée <code>perf-single-flow-wip</code> ; ses chiffres ne sont pas encore des affirmations et nous ne les donnons pas ici.</p>
<h2 id="une-promesse-que-nous-navions-pas-faite-mais-que-nous-avons-tenue-la-configuration-est-une-donnee">Une promesse que nous n'avions pas faite, mais que nous avons tenue : la configuration est une donnée</h2>
<p>Jusqu'ici, chaque configuration était une nouvelle construction : les clés et les limites étaient intégrées à l'image. C1 inverse cela (ADR-215 à ADR-219) : le noyau est construit une fois, et la configuration est une donnée.</p>
<ul>
<li><strong>Le blob <code>urmqtt-cfg</code></strong> (C1a) : un enregistrement versionné et canonique (champs par étiquettes croissantes, empreinte SHA-256, signature Ed25519 détachée facultative), 70 champs avec valeurs par défaut et bornes, un décodeur qui rejette tout ce qui est invalide.</li>
<li><strong>Le noyau le lit comme module Limine</strong> (C1b), avant tout le reste, sur aarch64 et x86_64. Il existe <strong>un seul noyau publié pour chaque <code>ENV</code> sauf <code>dev</code></strong> (les ELF aarch64 pour <code>release</code>, <code>prod</code> et <code>bench</code> ont le même SHA-256 ; vérifié seulement sur aarch64). Le noyau publié rejette la configuration de développement, et sans configuration il écrit « refusing to serve » et ne sert rien.</li>
<li><strong>Le disque installé porte la configuration</strong> et la mise à jour la conserve (C1c).</li>
<li><strong><code>urmqtt-config</code> et le modèle d'ISO</strong> (C1d) : <code>pack</code>, <code>show</code>, <code>verify</code>, et <code>pack --iso</code> remplace les deux copies de l'emplacement de configuration et les 24 condensats de modules dans un ISO prêt. Le résultat est identique octet pour octet à l'ISO que construit <code>mkiso.sh --config</code>. Le modèle de la publication v0.2.0 fait 63 111 168 octets.</li>
<li><strong>La déclaration de confiance</strong> (ADR-219) : l'image configurée écrit au démarrage l'empreinte de la configuration (<code>config: fingerprint ...</code>), et la signature est facultative et <strong>désactivée par défaut</strong>. Un ISO modifié n'a pas de condensat officiel ; l'empreinte est le moyen de voir ce qu'il contient.</li>
</ul>
<h2 id="le-constructeur-sur-aldotech">Le constructeur sur aldo.tech</h2>
<p>Ce que fait <code>urmqtt-config pack --iso</code>, la page web le fait aussi, avec le même code : la bibliothèque de l'outil compilée en WebAssembly (1,06 MB, sans imports et sans bloc <code>unsafe</code>). La page se trouve à <a href="https://aldo.tech/fr/produits/urmqtt-builder/">aldo.tech/fr/produits/urmqtt-builder/</a> et existe dans les 15 langues du site (par exemple <a href="https://aldo.tech/proizvodi/urmqtt-builder/">hr</a>).</p>
<ul>
<li><strong>Rien ne quitte le navigateur.</strong> Les certificats, les clés privées et l'ISO fini restent sur l'ordinateur ; la page ne télécharge que les fichiers de publication depuis ce site, sans suivi ni scripts tiers. Lors du test en direct, aucune requête ne portait de matériel de clé.</li>
<li><strong>Le miroir des publications</strong> (ADR-227) : heure par heure (timer systemd, basse priorité), il copie les publications depuis GitHub, chacune est vérifiée avant publication, et la page vérifie dans le navigateur la signature de <code>SHA256SUMS</code> avec la clé intégrée à la page, puis seulement écrit la configuration dans le modèle. Une vérification échouée bloque le téléchargement.</li>
<li><strong>Simple par défaut, avancé au besoin</strong> (ADR-232) : le premier écran compte trois étapes (où il tourne, vos certificats, le nom de l'image). Un seul champ de dépôt de fichiers trie le certificat, la clé et la CA selon leur contenu. La vue avancée ouvre chacun des 70 champs, la recherche, l'import et l'export de TOML ; les deux vues éditent le même état. Le modèle (63 MB) se télécharge pendant que l'utilisateur remplit le formulaire. Lors d'un essai en direct, avec un miroir de test et une clé de test, la construction de l'ISO dans le navigateur a pris 5 à 18 s.</li>
</ul>
<figure><img loading="lazy" alt="Vue simple du constructeur : trois étapes, « Où va-t-il tourner ? », « Vos certificats » et « Nommez votre image », avec une seule action suivante dans la barre latérale." src="https://aldo.tech/blog/urmqtt-config-builder-plaintext-mosquitto/img/builder-simple.png" /><figcaption>Vue simple du constructeur : trois étapes, « Où va-t-il tourner ? », « Vos certificats » et « Nommez votre image », avec une seule action suivante dans la barre latérale.</figcaption></figure>
<figure><img loading="lazy" alt="Vue avancée, capturée sur un miroir de test avec une clé de test (la date de publication et la clé de signature ne sont pas réelles) : publication avec signature vérifiée, modèles, chaque champ de configuration." src="https://aldo.tech/blog/urmqtt-config-builder-plaintext-mosquitto/img/builder-advanced.png" /><figcaption>Vue avancée, capturée sur un miroir de test avec une clé de test (la date de publication et la clé de signature ne sont pas réelles) : publication avec signature vérifiée, modèles, chaque champ de configuration.</figcaption></figure>
<p><strong>L'émulateur, facultatif.</strong> À côté de « Télécharger l'ISO » se trouve « Essayer dans l'émulateur » : QEMU compilé en WebAssembly fait démarrer l'image qui vient d'être construite dans un onglet du navigateur. Rien de lui n'est téléchargé avant le clic (environ 3 MB la première fois). L'émulateur est BIOS seulement, <strong>sans réseau</strong> (pour que le trafic de l'utilisateur ne passe par aucun intermédiaire et n'enfreigne pas « rien ne quitte le navigateur »), demande environ 1,5 GB de mémoire et un navigateur de bureau (Chrome testé ; Firefox et Safari non). Dedans, urMQTT atteint <code>URMQTT READY</code> en environ 8 s (compte à rebours ignoré) et écrit la même empreinte de configuration que la page, ce qui prouve que c'est bien cette configuration qui a démarré. Le démarrage est environ 2 fois plus lent que QEMU natif sur le même Mac (0,82 s contre 1,9 s), et la cryptographie environ 58 fois plus lente parce que les instructions matérielles sont émulées. C'est pourquoi <strong>nous ne tirons de l'émulateur aucune affirmation sur la vitesse</strong>, et la page le dit avant le lancement. À la construction, il a fallu corriger quatre octets dans le <code>.wasm</code> à cause d'une erreur de traduction de l'instruction POPCNT dans l'émulateur d'origine (nous ne l'avons pas encore signalée) ; l'émulateur lui-même est sous GPL, avec une offre de code source.</p>
<figure><img loading="lazy" alt="Entrée de diagnostic dans l'émulateur en direct sur aldo.tech : le noyau a écrit la même empreinte de configuration que la page, et l'écran affiche le diagnostic (dans l'émulateur il se termine en environ 20 s ; ses chiffres décrivent l'émulateur, pas urMQTT)." src="https://aldo.tech/blog/urmqtt-config-builder-plaintext-mosquitto/img/emulator-diagnostic.png" /><figcaption>Entrée de diagnostic dans l'émulateur en direct sur aldo.tech : le noyau a écrit la même empreinte de configuration que la page, et l'écran affiche le diagnostic (dans l'émulateur il se termine en environ 20 s ; ses chiffres décrivent l'émulateur, pas urMQTT).</figcaption></figure>
<h2 id="mqtt-en-clair-seulement-pour-les-reseaux-de-confiance">MQTT en clair, seulement pour les réseaux de confiance</h2>
<p>MQTT en clair (TCP 1883, sans TLS) est très répandu dans l'IoT : c'est le réglage par défaut de Mosquitto, de la plupart des firmwares pour Arduino et ESP, de Tasmota, de Zigbee2MQTT et de Home Assistant, et il est utilisé aussi dans des réseaux industriels segmentés, derrière des VPN et sur des LAN privés, y compris sur des appareils sans horloge (les certificats exigent une date), sans assez de flash ou de processeur pour TLS. Sur un réseau non fiable, il n'est pas acceptable : qui est sur le chemin lit et modifie le trafic. C'est pourquoi P1 (ADR-229 à ADR-231) est un choix explicite et limité, <strong>désactivé par défaut</strong>, et le port TLS reste ; TLS n'est pas désactivé par cela, le port en clair vient en plus.</p>
<ul>
<li><strong>La liste des réseaux autorisés est obligatoire</strong> (<code>allow_from</code>, CIDR) et vérifiée au SYN : hors de la liste, il n'y a ni SYN-ACK ni état. Tout Internet (<code>0.0.0.0/0</code>) exige une option explicite distincte.</li>
<li><strong>Trois modes d'authentification :</strong> mot de passe (par défaut quand c'est activé ; PBKDF2-HMAC-SHA-256, sel, 10 000 itérations, comparaison en temps constant, verrouillage par adresse), anonyme (une identité configurée avec son propre ACL) et par adresse (<code>ip</code>). Le constructeur hache le mot de passe dans le navigateur et ne conserve en mémoire que le condensat.</li>
<li><strong>Moins de droits qu'avec mTLS :</strong> un client en clair n'a jamais de droits de service, d'opérateur ou de gestion, pas de <code>$SYS</code> ni de console, il a ses propres sessions (<code>$plain/&lt;identité&gt;/&lt;id du client&gt;</code>, seulement propres), ses propres limites, et le registre des appareils autorisés continue de s'appliquer à lui.</li>
<li><strong>Ce qui est perdu est écrit dans <code>SECURITY.md</code> :</strong> confidentialité, intégrité, authenticité du broker et rejeu d'une authentification enregistrée. La configuration ne peut pas y remédier ; c'est ainsi quand il n'y a pas de TLS.</li>
</ul>
<p>Mesuré sur un portable (Apple silicon, HVF, réseau utilisateur QEMU), 16 paires, 64 B, trois exécutions de 10 s : à 8000 messages par seconde, le chemin TLS et le chemin en clair livrent tous les messages avec la même latence (p50 0,17 ms contre 0,13 à 0,18 ms), nouvelles connexions par seconde : TLS environ 2600 à 2750, anonyme en clair environ 4900 à 7800, et <strong>l'authentification par mot de passe est la partie coûteuse</strong> : un PBKDF2 coûte environ 2,5 ms, donc le nombre de nouvelles authentifications est limité à environ 200 par cœur de données et par seconde (mesuré : 308 à 342 sur deux cœurs). Le déluge sur cette machine n'est pas un instrument stable (20k à 440k livrés par seconde selon les exécutions) et <strong>nous n'en tirons aucune conclusion</strong>. La comparaison du chemin en clair avec Mosquitto sur le VPS n'est pas encore faite. Tests : le groupe de smoke tests <code>plaintext</code> 7 sur 7 sur aarch64 HVF et sur x86_64 TCG, nouvelle cible de fuzz <code>plain_input</code> (2,3 millions d'exécutions sans résultat).</p>
<h2 id="les-erreurs-trouvees-par-lusage-en-direct">Les erreurs trouvées par l'usage en direct</h2>
<p>Deux choses n'ont pas été attrapées par les tests, mais par la première mise en service sur le vrai site :</p>
<ul>
<li>La page cherchait les fichiers de publication <strong>un répertoire trop haut</strong>, donc chaque vérification de signature répondait 404 (correctif 13af301). Le test utilisait un miroir de test à un autre chemin.</li>
<li><strong>Le clavier de l'émulateur n'arrivait jamais à l'invité</strong> : la cause n'était pas une mauvaise traduction des touches, mais le fait que la boîte de dialogue n'écoutait pas du tout les touches (correctif dans l'ADR-232, avec une entrée de démarrage sans touches).</li>
</ul>
<h2 id="lecons">Leçons</h2>
<ul>
<li><strong>Cherchez la cause, n'accusez pas l'environnement.</strong> La plupart des faiblesses d'un seul flux et toutes les connexions coupées étaient les nôtres ; l'hyperviseur est réel, mais il fallait le mesurer, pas le supposer.</li>
<li><strong>La comparaison avec un autre produit doit inclure aussi les pertes.</strong> Un tableau où il n'y a que nos victoires ne serait pas une comparaison.</li>
<li><strong>L'instrument de mesure a des limites.</strong> Un générateur partagé avec 60 à 70% de temps volé change ce que les chiffres signifient ; c'est pourquoi nous écrivons « borne inférieure » et n'affirmons pas les différences inférieures à 20%.</li>
<li><strong>L'isolation n'est pas une preuve.</strong> SQLite en EL0 réduit ce qu'une erreur en lui peut faire, pas ce qu'il peut mentir.</li>
<li><strong>Une installation réelle trouve ce que les tests ne cherchent pas</strong> : un chemin mal composé et des touches que personne n'écoute.</li>
</ul>
<h2 id="ce-qui-nest-pas-encore-demontre">Ce qui n'est pas encore démontré</h2>
<ul>
<li>La clé de signature des publications est pour l'instant une clé de test ; v0.2.0 et v0.2.1 sont des pré-publications, et les images qui les accompagnent sont des images de développement avec des certificats de test (sauf le modèle, qui est sans configuration).</li>
<li>13 traductions de la page du constructeur (et de cet article) ont été écrites par une IA et vérifiées par des tests et des échantillons, pas par des locuteurs natifs.</li>
<li>La comparaison avec Mosquitto est une session, un chemin et un générateur partagé, sur un VPS partagé ; les différences inférieures à 20% ne sont pas des affirmations.</li>
<li>Le chemin en clair n'a pas été mesuré sur le VPS ni face à Mosquitto.</li>
<li>Lighthouse sur la nouvelle vue simple n'est pas confirmé (ancienne vue : accessibilité 95 ou plus).</li>
<li>Bare metal, coût du worker x86_64 sur KVM, Firefox et Safari pour l'émulateur : non mesuré.</li>
</ul>
<h2 id="la-suite">La suite</h2>
<ul>
<li><strong>D1 : un broker de démonstration public.</strong> Nous ne pouvons pas rendre public l'émulateur du navigateur : il vit dans l'onglet d'un seul visiteur, il est 20 à 100 fois plus lent que KVM, n'a pas de réseau entrant et disparaît à la fermeture de l'onglet ; rediriger Internet vers le navigateur de quelqu'un d'autre serait aussi un abus. Un vrai broker de démonstration est possible : urMQTT sur un VPS derrière un nom sur aldo.tech, avec une CA de test publiée, un ACL strict et de petites limites. Pour l'instant ce n'est qu'une idée, pas une construction, et cela exige un accord explicite pour l'exposition publique.</li>
<li><strong>Le deuxième tour pour un seul flux</strong> (branche <code>perf-single-flow-wip</code>) : seulement quand il aura été mesuré sur le VPS et fusionné.</li>
<li><strong>Le chemin en clair face à Mosquitto sur le VPS</strong>, enregistrement et révocation de compte pour MQTT en clair via le canal de gestion signé.</li>
<li><strong>Après la publication :</strong> bare metal (X12 à X16), IOMMU et Secure Boot, chiffrement du disque.</li>
</ul>
<p>Le code est sur <a href="https://github.com/zeljan-alduk/urMQTT">github.com/zeljan-alduk/urMQTT</a>, et <code>docs/PROGRESS.md</code> reste la seule source de vérité sur ce qui fonctionne réellement, <code>docs/PERFORMANCE.md</code> sur ce qui a été mesuré. Pré-alpha, comme avant ; les rapports de bogues et les signalements de sécurité, selon les instructions de <code>SECURITY.md</code>, sont les bienvenus.</p>]]></content:encoded>
      <category>rust</category>
      <category>mqtt</category>
      <category>iot</category>
      <category>unikernel</category>
      <category>open source</category>
    </item>
    <item>
      <title>urMQTT sur un vrai VPS : ISO, poignée de main de 0,24 ms, disque propre et noyau durci</title>
      <link>https://aldo.tech/fr/blog/urmqtt-vps-handshake-disk-hardening/</link>
      <guid isPermaLink="true">https://aldo.tech/fr/blog/urmqtt-vps-handshake-disk-hardening/</guid>
      <pubDate>Sat, 31 Oct 2026 08:00:00 +0000</pubDate>
      <description>Ce qu&#x27;il est advenu des trois promesses du premier article sur urMQTT (un seul ISO, un disque brut sans système de fichiers, une sécurité démontrable) après les quatre premiers jours de développement, avec mesures, tableaux et liste de ce qui reste à faire.</description>
      <content:encoded><![CDATA[<p>Dans <a href="https://aldo.tech/blog/urmqtt-unikernel-mqtt-broker/">le premier article sur urMQTT</a>, nous avons décrit l'unikernel qui est un broker MQTT, jusqu'au jalon M17, et nous avons conclu par trois promesses sous le titre « La suite » : un seul ISO qui démarre sur plusieurs plateformes, un disque brut sans système de fichiers et une sécurité qui se démontre au lieu de simplement s'affirmer. Rien de tout cela n'était encore construit. Cet article suit le sort de chaque promesse, dans le même ordre qu'avant : ce que nous avions prévu, ce que nous avons construit, ce qui nous a surpris et ce qui reste ouvert. Nous montrerons aussi, en chemin, combien de temps tout cela a pris, car cela fait partie de l'histoire.</p>
<p>Une précision d'emblée : le projet reste en pré-alpha. Les chiffres de cet article mesurent du code et des tests, pas une maturité en production, et la première publication est volontairement limitée aux VPS avec KVM. Le bare metal viendra plus tard.</p>
<h2 id="a-quoi-cela-ressemblait-dans-le-temps">À quoi cela ressemblait dans le temps</h2>
<p>Le premier commit date du 28 octobre 2026 à 23:48 (M0). Au 31 octobre à 23:00, le dépôt compte 119 commits, répartis en 3, 39, 38 et 39 par jour calendaire. Disons tout de suite comment c'est possible : le projet se construit avec Claude Code, et les commits portent des trailers <code>Co-Authored-By: Claude</code>. Quatre jours calendaires d'historique ne signifient pas qu'une seule personne a écrit 90 000 lignes en quatre jours ; cela signifie que le plan, l'implémentation, les tests et les ADR s'écrivent en boucles courtes et fermées, et que chaque étape se termine par le même rituel de vérification qu'auparavant. Les chiffres ci-dessous parlent de la quantité de code et de tests, pas du temps de travail réel que tout cela représente.</p>
<figure><img loading="lazy" alt="Croissance du projet sur quatre jours avec les jalons de M0 à aujourd'hui : milliers de lignes de Rust (de 1,6 à 89,8), nombre de fonctions #[test] (de 27 à 739) et nombre d'ADR (de 13 à 104), avec des barres de commits par jour (39, 38 et 39 pour les 29, 30 et 31 octobre)." src="https://aldo.tech/blog/urmqtt-vps-handshake-disk-hardening/img/growth.svg" /><figcaption>Croissance du projet sur quatre jours avec les jalons de M0 à aujourd'hui : milliers de lignes de Rust (de 1,6 à 89,8), nombre de fonctions #[test] (de 27 à 739) et nombre d'ADR (de 13 à 104), avec des barres de commits par jour (39, 38 et 39 pour les 29, 30 et 31 octobre).</figcaption></figure>
<p>Le graphique montre trois courbes qui montent presque ensemble, ce qui est voulu : les tests et les décisions consignées suivent le code au lieu de rester à la traîne. La pente est la plus raide le 29 octobre, quand naît tout le chemin qui va du noyau au broker. Ensuite, les lignes s'ajoutent plus lentement, mais les tests croissent plus vite que le code (27 tests pour 1,6 millier de lignes au début, 739 pour 89,8 milliers aujourd'hui).</p>
<table>
<thead>
<tr>
<th>Jalon</th>
<th>Commit</th>
<th>Heure</th>
<th style="text-align: right;">Lignes de Rust</th>
<th style="text-align: right;">Tests</th>
<th style="text-align: right;">ADR</th>
</tr>
</thead>
<tbody>
<tr>
<td>M0</td>
<td>9412a9c</td>
<td>28.10. 23:48</td>
<td style="text-align: right;">1 592</td>
<td style="text-align: right;">27</td>
<td style="text-align: right;">13</td>
</tr>
<tr>
<td>M5</td>
<td>a61083b</td>
<td>29.10. 01:32</td>
<td style="text-align: right;">9 314</td>
<td style="text-align: right;">88</td>
<td style="text-align: right;">24</td>
</tr>
<tr>
<td>M9</td>
<td>4be8cc9</td>
<td>29.10. 02:21</td>
<td style="text-align: right;">15 729</td>
<td style="text-align: right;">149</td>
<td style="text-align: right;">28</td>
</tr>
<tr>
<td>M11</td>
<td>4800904</td>
<td>29.10. 03:43</td>
<td style="text-align: right;">22 737</td>
<td style="text-align: right;">180</td>
<td style="text-align: right;">30</td>
</tr>
<tr>
<td>M13</td>
<td>a558d5e</td>
<td>29.10. 05:22</td>
<td style="text-align: right;">26 556</td>
<td style="text-align: right;">193</td>
<td style="text-align: right;">32</td>
</tr>
<tr>
<td>M17</td>
<td>7df4f98</td>
<td>29.10. 08:48</td>
<td style="text-align: right;">33 504</td>
<td style="text-align: right;">232</td>
<td style="text-align: right;">36</td>
</tr>
<tr>
<td>S1</td>
<td>ecf72ec</td>
<td>30.10. 00:46</td>
<td style="text-align: right;">41 910</td>
<td style="text-align: right;">299</td>
<td style="text-align: right;">45</td>
</tr>
<tr>
<td>X8</td>
<td>fb1504a</td>
<td>30.10. 19:08</td>
<td style="text-align: right;">56 341</td>
<td style="text-align: right;">456</td>
<td style="text-align: right;">59</td>
</tr>
<tr>
<td>PERF</td>
<td>76f7c36</td>
<td>31.10. 14:19</td>
<td style="text-align: right;">72 511</td>
<td style="text-align: right;">618</td>
<td style="text-align: right;">76</td>
</tr>
<tr>
<td>aujourd'hui</td>
<td>HEAD</td>
<td>31.10. 23:00</td>
<td style="text-align: right;">89 790</td>
<td style="text-align: right;">739</td>
<td style="text-align: right;">104</td>
</tr>
</tbody>
</table>
<p>Les lignes sont tous les fichiers <code>.rs</code> hors de <code>vendor/</code> et <code>fuzz/</code>, et les tests sont des fonctions <code>#[test]</code>. Ce que chaque étape a apporté, par date d'après l'historique git :</p>
<table>
<thead>
<tr>
<th>Date</th>
<th>Jalons</th>
<th>Ce qui a été ajouté</th>
</tr>
</thead>
<tbody>
<tr>
<td>29.10.</td>
<td>M0 à M17</td>
<td>noyau, réseau, TLS, broker, SQLite, multicœur, TCP maison, durcissement, publication reproductible</td>
</tr>
<tr>
<td>29.10. au soir et 30.10.</td>
<td>X0 à X10, S1, H2, H3 (X0 commence le 29.10. à 20:54)</td>
<td>portage vers x86_64, ISO universel, un seul disque et ISO auto-installant, contrôles TLS, clients hostiles</td>
</tr>
<tr>
<td>31.10.</td>
<td>O1 à O4, T1, PERF, H4, H5, H9, S2 à S6</td>
<td>observabilité et console via MQTT, mode live et diagnostic, SNTP, accélérations, durcissement, vérification, chaîne d'approvisionnement, stockage</td>
</tr>
</tbody>
</table>
<h2 id="promesse-1-un-seul-iso-pour-plusieurs-plateformes">Promesse 1 : un seul ISO pour plusieurs plateformes</h2>
<p><strong>Ce que nous avions prévu.</strong> Un seul fichier qui démarre sur UEFI aarch64, UEFI x86_64 et BIOS x86_64. <strong>Ce que nous avons construit.</strong> Le portage vers x86_64 (X0 à X10) et un ISO universel avec le bootloader Limine, un ISO hybride unique d'où démarrent toutes ces combinaisons ; les tests de fumée le démarrent sur quatre machines virtuelles. Le menu comporte trois entrées : urMQTT (mode live, exécuté depuis la RAM, rien n'est écrit sur le disque), l'installation sur le disque de la machine sur laquelle il tourne (l'installation écrit le GPT et les données, puis éteint la machine) et Diagnostic (détection du matériel et mesures).</p>
<figure><img loading="lazy" alt="Le menu de démarrage de l'ISO, capturé sur le VPS : entrées « urMQTT », « Install urMQTT on this machine's disk » et « Diagnostic: hardware detection and benchmarks », avec un compte à rebours avant le démarrage automatique. La capture provient d'un ISO de test avec un compte à rebours plus long ; la valeur par défaut est de 10 secondes." src="https://aldo.tech/blog/urmqtt-vps-handshake-disk-hardening/img/boot-menu.png" /><figcaption>Le menu de démarrage de l'ISO, capturé sur le VPS : entrées « urMQTT », « Install urMQTT on this machine's disk » et « Diagnostic: hardware detection and benchmarks », avec un compte à rebours avant le démarrage automatique. La capture provient d'un ISO de test avec un compte à rebours plus long ; la valeur par défaut est de 10 secondes.</figcaption></figure>
<p>La première cible réelle a été un VPS, et non un ordinateur physique : la décision est que la première publication ne prend en charge que les VPS (KVM), et que le bare metal viendra après. Nous avons testé sur un VPS Contabo avec un processeur AMD EPYC (famille 23, Zen 1), 4 vCPU et 8 GiB de RAM, avec un réseau virtio-net et un disque virtio-scsi. Le premier essai nous a surpris : l'ISO attaché comme CD virtuel démarrait l'entrée par défaut, l'image de production refusait de fonctionner depuis un support en lecture seule et se réinitialisait, et nous nous sommes retrouvés dans une boucle infinie menu, noyau, réinitialisation sans rien de lisible. Aujourd'hui, une erreur fatale dans l'image de production laisse un message sur la console pendant 60 secondes avant la réinitialisation, et depuis un support optique elle affiche qu'elle a démarré depuis un support en lecture seule, puis s'arrête. Le mode live en RAM (O3) a ensuite contourné ce problème pour le cas courant.</p>
<p>L'entrée de diagnostic est née de cette expérience : une machine que nous ne voyons pas, nous devons pouvoir l'interroger sur ce qu'elle contient. Elle affiche le CPU, la RAM, le réseau, les disques et les périphériques PCI, mesure le débit mémoire, le disque et la cryptographie, et écrit les mêmes mesures en JSON sur le port série. L'écriture sur le disque n'est permise que dans l'espace non alloué, et seulement après avoir saisi <code>WIPE</code> ; sur la capture ci-dessous, cette étape a été sautée.</p>
<figure><img loading="lazy" alt="Écran de diagnostic sur le VPS Contabo (AMD EPYC, KVM, 4 vCPU) : système, mémoire, disque, périphériques PCI, débit et latence de la RAM et mesures cryptographiques, dont 244 us par poignée de main mTLS, X25519 48 us, signature ECDSA 35,7 us et vérification 107 us." src="https://aldo.tech/blog/urmqtt-vps-handshake-disk-hardening/img/diag-vps.png" /><figcaption>Écran de diagnostic sur le VPS Contabo (AMD EPYC, KVM, 4 vCPU) : système, mémoire, disque, périphériques PCI, débit et latence de la RAM et mesures cryptographiques, dont 244 us par poignée de main mTLS, X25519 48 us, signature ECDSA 35,7 us et vérification 107 us.</figcaption></figure>
<p><strong>Ce qui reste ouvert.</strong> Le vrai matériel. Après la première publication viendra le bare metal (démarrage USB, NVMe et AHCI, carte réseau d'un vrai ordinateur), avec l'IOMMU et Secure Boot, car sans IOMMU un périphérique malveillant ou défectueux peut écrire dans toute la mémoire. Dans cette version, nous n'affirmons rien à ce sujet.</p>
<h2 id="la-poignee-de-main-de-12-ms-a-024-ms">La poignée de main : de 1,2 ms à 0,24 ms</h2>
<p>La première mesure du diagnostic sur le VPS a donné un chiffre gênant : la poignée de main mTLS, uniquement la cryptographie côté serveur, durait 1,2 ms. Sur aarch64, nous utilisions déjà les instructions matérielles AES et SHA, mais x86_64 n'avait rien de comparable pour la clé publique : RustCrypto est portable et à temps constant, donc lent aussi. Trois changements l'ont fait baisser.</p>
<ul>
<li>Pour P-256, nous avons repris l'assembleur <code>nistz256</code> d'OpenSSL et de ring (ADR-130), avec des tables à base fixe pour la signature et la vérification des signatures de la CA (ADR-110, ADR-111).</li>
<li>X25519 a été réécrit sur quatre membres de 64 bits avec les instructions MULX/ADX (ADR-131).</li>
<li>Nous avons ensuite profilé tout le reste (ADR-132, ADR-200).</li>
</ul>
<figure><img loading="lazy" alt="Poignée de main sur le VPS : 1200 us avant, 237 us après l'assembleur et les tables ; comparaison A/B de memcpy sur le même démarrage, 278,8 us avec l'ancien, 229,4 us avec le nouveau ; et avant et après pour X25519 (146 à 48 us), la signature ECDSA (673 à 34 us) et la vérification (309 à 90 us)." src="https://aldo.tech/blog/urmqtt-vps-handshake-disk-hardening/img/handshake.svg" /><figcaption>Poignée de main sur le VPS : 1200 us avant, 237 us après l'assembleur et les tables ; comparaison A/B de memcpy sur le même démarrage, 278,8 us avec l'ancien, 229,4 us avec le nouveau ; et avant et après pour X25519 (146 à 48 us), la signature ECDSA (673 à 34 us) et la vérification (309 à 90 us).</figcaption></figure>
<table>
<thead>
<tr>
<th>Mesure sur le VPS (serveur, microsecondes)</th>
<th style="text-align: right;">Avant</th>
<th style="text-align: right;">Après</th>
</tr>
</thead>
<tbody>
<tr>
<td>poignée de main mTLS, toute la cryptographie</td>
<td style="text-align: right;">1200</td>
<td style="text-align: right;">environ 240 (237 dans la mesure du graphique, 244 sur la capture)</td>
</tr>
<tr>
<td>X25519</td>
<td style="text-align: right;">146</td>
<td style="text-align: right;">48</td>
</tr>
<tr>
<td>signature ECDSA</td>
<td style="text-align: right;">673</td>
<td style="text-align: right;">34</td>
</tr>
<tr>
<td>vérification ECDSA</td>
<td style="text-align: right;">309</td>
<td style="text-align: right;">90 (107 sur la capture)</td>
</tr>
<tr>
<td>poignée de main, ancien memcpy, même boot</td>
<td style="text-align: right;">278,8</td>
<td style="text-align: right;">229,4 (-17,7%)</td>
</tr>
</tbody>
</table>
<p>Les chiffres varient d'environ 10% d'une exécution à l'autre, c'est pourquoi nous avons noté une fourchette plutôt qu'une valeur unique. Au total, cela représente environ cinq fois moins de temps CPU par poignée de main.</p>
<p>Ce que nous avons appris sur memcpy. Quand la cryptographie a chuté, le profil a montré quelque chose d'inattendu : les fonctions <code>mem*</code> (memcpy, memmove, memset) consommaient 7% de la poignée de main, 13 à 39% du chemin d'un enregistrement TLS (39% pour 64 B, 13% pour 4 KiB) et 17 à 34% de la distribution des PUBLISH. Nous les avons remplacées par de l'assembleur SSE2 pour x86_64 (ADR-200), avec des tests différentiels contre l'implémentation de référence pour toutes les tailles, tous les alignements et tous les chevauchements. Par appel, cela donne 55 à 75 cycles contre 7 à 12 pour 1 à 64 octets.</p>
<figure><img loading="lazy" alt="À gauche : part du temps du serveur passée dans les fonctions mem* avant la réécriture (poignée de main 7%, enregistrement TLS 64 B 39%, 256 B 37%, 1 KiB 24%, distribution 17 à 34%). À droite : de combien chaque chemin est plus rapide après (poignée de main 17,7%, 64 B 26,4%, 256 B 19,6%, 1 KiB 16,1%, 4 KiB 6,6%, distribution 7 à 34%)." src="https://aldo.tech/blog/urmqtt-vps-handshake-disk-hardening/img/hotpaths.svg" /><figcaption>À gauche : part du temps du serveur passée dans les fonctions mem* avant la réécriture (poignée de main 7%, enregistrement TLS 64 B 39%, 256 B 37%, 1 KiB 24%, distribution 17 à 34%). À droite : de combien chaque chemin est plus rapide après (poignée de main 17,7%, 64 B 26,4%, 256 B 19,6%, 1 KiB 16,1%, 4 KiB 6,6%, distribution 7 à 34%).</figcaption></figure>
<table>
<thead>
<tr>
<th>Chemin</th>
<th style="text-align: right;">Part de mem* avant</th>
<th style="text-align: right;">Plus rapide après</th>
</tr>
</thead>
<tbody>
<tr>
<td>poignée de main</td>
<td style="text-align: right;">7%</td>
<td style="text-align: right;">17,7%</td>
</tr>
<tr>
<td>enregistrement TLS 64 B</td>
<td style="text-align: right;">39%</td>
<td style="text-align: right;">26,4%</td>
</tr>
<tr>
<td>enregistrement TLS 256 B</td>
<td style="text-align: right;">37%</td>
<td style="text-align: right;">19,6%</td>
</tr>
<tr>
<td>enregistrement TLS 1 KiB</td>
<td style="text-align: right;">24%</td>
<td style="text-align: right;">16,1%</td>
</tr>
<tr>
<td>enregistrement TLS 4 KiB</td>
<td style="text-align: right;">13%</td>
<td style="text-align: right;">6,6%</td>
</tr>
<tr>
<td>distribution des messages</td>
<td style="text-align: right;">17 à 34%</td>
<td style="text-align: right;">7 à 34%</td>
</tr>
</tbody>
</table>
<p>Lecture du graphique : plus l'enregistrement est petit, plus la part du temps consacrée à la copie est grande, et plus le gain est important. Pour les petits messages, qui forment l'essentiel du trafic MQTT, c'est ce qui compte.</p>
<p>Ce que cela ne veut pas dire. Sur x86_64 natif, nous n'avons mesuré OpenSSL 3.5.5 (ADR-130) que comme référence pour l'assembleur, et non comme comparaison de produits. La comparaison Linux plus Mosquitto sur le même VPS (tâche X11) n'a pas encore été faite. Nous n'affirmons donc pas qu'urMQTT est plus rapide que Linux : ce n'est pas mesuré. Les chiffres ci-dessus comparent urMQTT à lui-même.</p>
<p>La surcharge a été le second résultat. Sur le VPS, depuis un seul Mac (RTT 35 ms), 128 clients ont donné 618 poignées de main par seconde, 256 clients 393, et 512 clients 67, avec des milliers de connexions refusées. Ce n'était pas un plateau mais un effondrement. Nous avons trouvé les causes dans le code (deux sont confirmées dans QEMU/HVF, mais n'ont pas pu être reproduites sur le VPS), plusieurs à la fois : une connexion refusée renvoyait le client sur le segment suivant portant un cookie SYN valide, le refus par RST disait au client de réessayer aussitôt, et la réception était en O(file). Le correctif (ADR-080 à ADR-082) laisse tomber un SYN qui ne serait pas accepté, mémorise les flux refusés, maintient la réception en O(1) et compte les poignées de main par cœur. Il est possible (hypothèse, non vérifiée) qu'une partie de la cause se soit trouvée aussi côté client : l'outil de charge fermait en premier, si bien qu'un seul Mac, avec 16 384 ports éphémères et un TIME_WAIT de 30 secondes, était limité à environ 550 connexions par seconde. Nous n'avons donc pas encore mesuré le vrai plafond du serveur.</p>
<h2 id="promesse-2-un-disque-brut-mais-soigneux">Promesse 2 : un disque brut, mais soigneux</h2>
<p><strong>Ce que nous avions prévu.</strong> Garder notre propre disposition sans système de fichiers, et ajouter tout ce qu'un système de fichiers apporte d'habitude : alignement, croissance, discard, sauvegardes et vérifications. <strong>Pourquoi pas un système de fichiers.</strong> Nous en avons débattu et la décision est confirmée : urMQTT écrit une seule base, un seul journal de télémétrie et ses propres métadonnées, n'a besoin ni de répertoires, ni de permissions, ni de noms de fichiers, et chaque couche supplémentaire serait du code de plus dans la partie de confiance et une manière de plus de perdre un enregistrement en silence. Le disque est un GPT avec une partition ESP et une partition de données urMQTT, sur laquelle se trouve la disposition propre au projet.</p>
<p><strong>Ce que nous avons construit (S2 à S6).</strong></p>
<ul>
<li><strong>Disposition v2 (S2).</strong> Topologie du disque, alignement sur 1 MiB ou sur la taille d'E/S optimale, blocs physiques entiers, paire A/B de superblocs avec génération (ADR-140). La disposition v1 est refusée par le noyau avec un message clair, et l'outil hors ligne <code>urmqtt-disk migrate</code> la migre (ADR-141) : par défaut c'est un passage à blanc, on copie puis on bascule seulement ensuite, avec retour arrière et injection de fautes à chaque écriture.</li>
<li><strong>Croissance (S3).</strong> Au démarrage, le disque peut grandir (jamais rétrécir), et sur virtio-blk aussi pendant l'exécution, via l'interruption de changement de configuration (ADR-142, ADR-143).</li>
<li><strong>Discard et write-zeroes (S4).</strong> Les disques à allocation dynamique rendent l'espace à l'hôte. Dans un test, le fichier qcow2 sur l'hôte est passé de 62 à 16 MiB après l'expiration des segments de télémétrie, et la récupération ne fait jamais confiance au contenu qui a été rejeté.</li>
<li><strong>Sauvegarde de la base sur le disque (S5).</strong> Une copie A/B avec l'API de sauvegarde en ligne de SQLite par pas de 16 pages : l'étape la plus longue dure 4,6 ms contre 445 ms quand on copie en un seul appel. Les commits pendant la copie ont eu un maximum de 2,4 ms contre 2,2 ms sans elle. Si <code>quick_check</code> échoue au démarrage, la base est restaurée à partir de la copie.</li>
<li><strong>Nettoyage et démarrage borné (S6).</strong> Le scrubber lit le disque avec un budget d'E/S limité et signale les enregistrements corrompus, et le démarrage ne lit que le segment le plus récent de chaque partition, si bien que le temps de démarrage ne croît pas avec la taille de la région de télémétrie. Nous avons ainsi fermé un problème que nous avions calculé à l'avance : sur un grand disque, une récupération complète aurait pris de quelques minutes à plusieurs dizaines de minutes.</li>
</ul>
<p>Tout le groupe de tests de fumée du stockage passe sur les deux architectures : 79 réussis, 0 échoués, sur aarch64 sous HVF et sur x86_64 sous TCG. Nous avons aussi mesuré que S4 n'a pas ralenti la base : 53,3 milliers d'insertions par seconde par lots avant et après, sans différence mesurable.</p>
<p><strong>Ce qui a surpris.</strong> La part des cas limites dans une disposition « simple » : virtio-scsi et virtio-blk signalent la topologie de manières différentes, et une écriture incomplète au milieu d'une migration doit toujours laisser au moins une version valide. C'est pourquoi chaque écriture sur le chemin de migration et de croissance a subi une injection de fautes.</p>
<p><strong>Ce qui reste ouvert.</strong> Les pages SQLite n'ont pas de somme de contrôle, donc la base ne détectera pas un bit modifié à l'intérieur d'une valeur d'une ligne ; les enregistrements de télémétrie et le contenu des copies ont un CRC et sont détectés. Le chiffrement du disque (S7) n'est pas terminé.</p>
<h2 id="promesse-3-demontrablement-sur-autant-que-nous-y-parvenons">Promesse 3 : démontrablement sûr, autant que nous y parvenons</h2>
<p><strong>Durcissement (H4).</strong> L'allocateur efface la mémoire libérée et la vérifie à la réutilisation, détecte les double libérations, lie les pointeurs de la liste libre par XOR et met les clés à zéro à la libération (ADR-170). Protection de la pile (<code>-Zstack-protector=strong</code>, 1315 fonctions, et SQLite avec <code>-fstack-protector-strong</code>, ADR-171). Les rapports d'erreur en production ne contiennent ni registres ni adresses (ADR-172). KASLR sur les deux architectures : le noyau est PIE et Limine le déplace d'environ 19 bits, tandis que le heap, les piles et la fenêtre MMIO sont placés au hasard (ADR-173). Sur aarch64, MTE marque le heap (ADR-174), mais seulement sous TCG : HVF et x86 n'en disposent pas. Nous avons évalué kCFI sans le retenir, car le compilateur du nightly épinglé plante avec une erreur interne ; l'IOMMU est reporté avec le bare metal (ADR-175, ADR-176).</p>
<p><strong>Le coût est mesuré, pas supposé.</strong> Sur le chemin de la poignée de main (<code>dp-bench</code>, aarch64 HVF, poignées de main complètes TLS 1.3 + mTLS + CONNECT) :</p>
<table>
<thead>
<tr>
<th>Étape</th>
<th style="text-align: right;">1 cœur</th>
<th style="text-align: right;">4 cœurs</th>
<th>Variation</th>
</tr>
</thead>
<tbody>
<tr>
<td>sans durcissement</td>
<td style="text-align: right;">1941</td>
<td style="text-align: right;">7620</td>
<td></td>
</tr>
<tr>
<td>+ allocateur</td>
<td style="text-align: right;">1898</td>
<td style="text-align: right;">7429</td>
<td>-2,2% à -2,5%</td>
</tr>
<tr>
<td>+ protection de la pile</td>
<td style="text-align: right;">1845</td>
<td style="text-align: right;">7243</td>
<td>ensemble -4,9% à -5%</td>
</tr>
<tr>
<td>+ PIE et marquage</td>
<td style="text-align: right;">environ 1800</td>
<td style="text-align: right;">environ 6750</td>
<td>sur une machine chargée, bruité</td>
</tr>
</tbody>
</table>
<p>Au total, le coût sur le chemin de la poignée de main est donc d'environ 5% pour les éléments mesurés proprement, et avec PIE et le marquage on obtient environ 7% sur un cœur et environ 11% sur quatre, mais cette mesure est bruitée. Nous la jugeons acceptable, mais nous ne l'avons pas reproduite sur le VPS x86_64.</p>
<p>Il vaut la peine de comparer avec le chiffre du premier article (862 poignées de main par seconde sur un cœur). Le même outil aujourd'hui, avec le pilote de chiffrement matériel sur aarch64 (X0b) et la série d'accélérations arrivées depuis, donne 1941 avant le durcissement. Nous n'avons pas isolé la contribution de chaque changement, donc nous n'affirmons rien sur la cause.</p>
<p><strong>Comment cela a été vérifié, et ce qui ne l'a pas été.</strong> H5 a ajouté Miri sur un sous-ensemble de crates de l'hôte, des preuves Kani, des sanitizers, un model-fuzzer pour les ACL et des rapports de couverture. Kani a prouvé <code>record_len</code> dans TLS, <code>parse</code> dans TCP et deux arithmétiques de la roue de timers. <strong>Non prouvés</strong> (délai dépassé) : les deux décodeurs MQTT, les deux analyses de configuration et le comportement de la roue de timers ; le job CI de Kani n'est donc pas encore bloquant. Miri ne peut pas exécuter d'assembleur. Chaîne d'approvisionnement (H9) : SBOM CycloneDX de 215 composants, <code>cargo-vet</code> passe avec 39 audités complètement et 140 exemptions, et cargo-deny est configuré pour traiter les avis de vulnérabilité comme un échec de la CI (ces jobs CI n'ont été vérifiés jusqu'ici qu'en local, pas sur GitHub, et le test négatif n'a pas été exécuté), avec <code>VULNERABILITY-HANDLING.md</code>. L'état honnête : sur 67 versions de crates tierces liées au noyau, 53 sont des exemptions et non des audits. Vet prouve que l'ensemble des dépendances ne peut pas changer sans différence visible, pas que le TCB a été examiné. Nous n'avons écrit notre propre audit d'aucun crate.</p>
<p>SNTP (T1) est dans ce sens une petite histoire de sécurité : il est facultatif, uniquement vers des serveurs IPv4 privés, avec un saut maximal et à la condition que les sources s'accordent (médiane, majorité à moins d'une seconde). Une horloge que n'importe qui sur le chemin peut décaler n'est pas une horloge à laquelle nous pouvons nous fier pour la validité des certificats.</p>
<h2 id="sur-le-mac-aarch64-en-natif-et-x86_64-par-emulation">Sur le Mac : aarch64 en natif et x86_64 par émulation</h2>
<p>Nous développons sur un Mac, et celui-ci offre deux manières d'exécuter le même code, très différentes l'une de l'autre.</p>
<table>
<thead>
<tr>
<th></th>
<th>aarch64 sous HVF</th>
<th>x86_64 sous TCG</th>
</tr>
</thead>
<tbody>
<tr>
<td>Ce que c'est</td>
<td>exécution native, vitesse proche d'un vrai processeur</td>
<td>QEMU émule les instructions</td>
</tr>
<tr>
<td>Poignée de main dans l'invité</td>
<td>environ 0,58 ms (première mesure vers M13) ; <code>dp-bench</code> aujourd'hui 1941/s par cœur</td>
<td>environ 3,3 ms</td>
</tr>
<tr>
<td>Scénarios de fumée TLS</td>
<td>rapides</td>
<td>environ 15 minutes par scénario</td>
</tr>
<tr>
<td>MTE</td>
<td>non (absent de HVF)</td>
<td>x86 n'en a pas ; le marquage du heap ne fonctionne que pour un invité aarch64 sous TCG</td>
</tr>
<tr>
<td>À quoi cela sert</td>
<td>mesures réelles</td>
<td>exactitude : le chemin x86 est vérifié, les chiffres ne sont pas des performances</td>
</tr>
</tbody>
</table>
<p>La règle est simple : les chiffres sous TCG sont de l'émulation et nous ne les citons pas comme des performances. Nous ne mesurons les performances x86 que sur un vrai VPS. C'est pourquoi les chiffres de cryptographie ci-dessus viennent du VPS Contabo, et non du Mac.</p>
<h2 id="lecons">Leçons</h2>
<ul>
<li><strong>Mesurez sur une vraie machine avant d'optimiser.</strong> Notre première approche consistait à optimiser ce que nous croyions lent ; ce n'est que le diagnostic sur le VPS qui a montré 1,2 ms et où le temps passe exactement.</li>
<li><strong>La surcharge est une fonctionnalité à part.</strong> Un système rapide sous charge peut s'effondrer au-delà ; refuser doit coûter moins cher qu'accepter.</li>
<li><strong>Le profil surprend.</strong> Le plus grand gain isolé pour les petits messages ne se trouvait pas dans la cryptographie mais dans la copie de mémoire.</li>
<li><strong>Un disque sans système de fichiers demande plus de travail, pas moins.</strong> Ce qu'un système de fichiers apporte d'habitude (alignement, croissance, discard, copies, vérifications) nous devons l'écrire et le vérifier nous-mêmes, chaque étape avec injection de fautes.</li>
<li><strong>L'honnêteté dans les chiffres.</strong> Les mesures de mémoire sur un VPS partagé varient jusqu'à 10 fois d'une exécution à l'autre et ne disent rien du code, nous ne les citons donc pas. Les chiffres cryptographiques sont stables à environ 10%.</li>
<li><strong>La vitesse de développement n'est pas la maturité.</strong> Les tests et les ADR croissent avec le code, mais quatre jours ne font pas un vrai fonctionnement en production.</li>
</ul>
<h2 id="la-suite">La suite</h2>
<ul>
<li><strong>H6 :</strong> SQLite isolé au niveau EL0, de sorte qu'une erreur dans la base n'ouvre pas le noyau.</li>
<li><strong>H7 :</strong> le reste de la sécurité du temps (NTS et traitement des sauts) ; SNTP n'est qu'un premier pas.</li>
<li><strong>X11 :</strong> comparaison avec Linux et Mosquitto sur le même VPS ; ce n'est qu'alors qu'il a un sens de parler de vitesse relative.</li>
<li><strong>Publication :</strong> première publication publique pour VPS (KVM), avec artefacts signés et instructions.</li>
<li><strong>Après la publication :</strong> bare metal (X12 à X16), IOMMU et Secure Boot, ainsi que le chiffrement du disque.</li>
</ul>
<p>Le code est sur <a href="https://github.com/zeljan-alduk/urMQTT">github.com/zeljan-alduk/urMQTT</a>, et <code>docs/PROGRESS.md</code> reste l'unique source de vérité sur ce qui fonctionne réellement. Pré-alpha, comme avant ; les rapports de bogues et les signalements de sécurité, selon les indications de <code>SECURITY.md</code>, sont les bienvenus.</p>]]></content:encoded>
      <category>rust</category>
      <category>mqtt</category>
      <category>iot</category>
      <category>unikernel</category>
      <category>open source</category>
    </item>
    <item>
      <title>urMQTT : nous construisons un broker MQTT qui est aussi un système d&#x27;exploitation</title>
      <link>https://aldo.tech/fr/blog/urmqtt-unikernel-broker-mqtt/</link>
      <guid isPermaLink="true">https://aldo.tech/fr/blog/urmqtt-unikernel-broker-mqtt/</guid>
      <pubDate>Thu, 29 Oct 2026 08:00:00 +0000</pubDate>
      <description>Notes de développement sur urMQTT, un unikernel écrit en Rust qui est lui-même un broker MQTT : un seul ELF avec sa propre pile TCP/IP, TLS 1.3, broker et SQLite, démarré depuis un seul ISO.</description>
      <content:encoded><![CDATA[<p>Les appareils embarqués dans les véhicules, sur les parkings et dans les armoires électroniques ont une caractéristique commune : ils envoient de petits messages, rarement, mais ils sont très nombreux et toujours connectés. Depuis des années, nous les raccordons à des brokers MQTT, et depuis des années la même question nous tracasse. Pourquoi faut-il un Linux complet, OpenSSL, le runtime d'un langage, SSH, un gestionnaire de paquets, et seulement ensuite un broker, pour une tâche aussi simple ? Chacune de ces couches apporte ses propres vulnérabilités, consomme sa propre mémoire et ajoute un élément de plus susceptible de tomber en panne, alors qu'une flotte d'appareils n'attend d'un serveur que trois choses : tenir un grand nombre de connexions presque inactives, authentifier chaque appareil de façon fiable et acheminer les messages sans perte.</p>
<p>C'est ainsi qu'est né <a href="https://github.com/zeljan-alduk/urMQTT">urMQTT</a> : un unikernel qui <em>est</em> un broker MQTT. Le noyau, la pile TCP/IP, TLS 1.3, MQTT et une base de données intégrée tiennent dans un seul binaire Rust qui démarre depuis un seul ISO. Pas de processus, pas d'appels système, pas de shell. La première plateforme sur laquelle il est construit et testé est aarch64, et le même code se dirige vers x86_64, par UEFI et BIOS classique, et vers du matériel réel : l'objectif est un seul ISO qui démarre sur tous. Le projet est open source sous licence MIT ou Apache 2.0, et dans cet article nous souhaitons consigner où nous en sommes, comment il est construit en interne et ce que nous avons appris en chemin.</p>
<h2 id="ce-que-signifie-exactement-unikernel">Ce que signifie exactement « unikernel »</h2>
<p>Un broker classique est un programme qui s'exécute sur un système d'exploitation. urMQTT n'a aucun système d'exploitation en dessous de lui : il s'amorce lui-même via le firmware UEFI et le bootloader Limine, met en place ses propres tables de pages, programme le contrôleur d'interruptions et le timer, découvre les périphériques virtio via PCIe, obtient une adresse par DHCP et écoute sur un seul port. Tout ce que feraient habituellement le noyau, les pilotes, la pile réseau et l'application tient ici dans un seul ELF qui s'exécute au niveau EL1.</p>
<p>La comparaison avec une installation typique parle d'elle-même :</p>
<table>
<thead>
<tr>
<th></th>
<th>Broker typique sous Linux</th>
<th>urMQTT</th>
</tr>
</thead>
<tbody>
<tr>
<td>Services exposés</td>
<td>SSH, broker, souvent aussi un port de métriques ou d'administration</td>
<td><strong>uniquement TCP 8883</strong> (MQTT sur TLS 1.3 avec mTLS)</td>
</tr>
<tr>
<td>Code dans l'image</td>
<td>noyau, userland, libc, OpenSSL, runtime</td>
<td><strong>un seul ELF Rust</strong> (plus SQLite en version vendorisée)</td>
</tr>
<tr>
<td>Gestion à distance</td>
<td>shell, fichiers de configuration, mises à jour de paquets</td>
<td><strong>aucune.</strong> Une nouvelle version est un nouvel ISO</td>
</tr>
<tr>
<td>Mémoire par connexion inactive</td>
<td>des kilo-octets (tampons de socket, objets TLS, heap)</td>
<td><strong>objectif ≤ 512 B</strong></td>
</tr>
<tr>
<td>Modèle multicœur</td>
<td>threads et verrous, ou une seule boucle d'événements</td>
<td><strong>un thread par cœur, aucun état partagé, traitement de bout en bout</strong></td>
</tr>
<tr>
<td>Intégrité du code</td>
<td>mappages à la fois inscriptibles et exécutables possibles</td>
<td><strong>W^X, tables de pages verrouillées, PAC/BTI</strong></td>
</tr>
</tbody>
</table>
<p>Les priorités sont, dans cet ordre : sécurité, scalabilité, performances multicœurs, faible empreinte et installation immuable. Chaque fois que deux objectifs entrent en conflit, celui qui figure plus haut dans cette liste l'emporte.</p>
<h2 id="ou-nous-en-sommes">Où nous en sommes</h2>
<p>Le projet est en pré-alpha, mais dix-sept jalons de développement, de M0 à M17, sont bouclés. Aujourd'hui, urMQTT sert MQTT 3.1.1 sur TLS 1.3 avec des certificats clients obligatoires, et conserve son état dans SQLite sur un disque brut. En bref, voici ce qu'il fait déjà :</p>
<ul>
<li><strong>Noyau :</strong> vecteurs d'exceptions propres avec rapports décodés, tables de pages propres avec W^X appliqué, pages de garde, PAC et BTI là où le processeur les possède, GICv3 et GICv2, timer générique, SMP jusqu'à 64 cœurs.</li>
<li><strong>Découverte du matériel</strong> à partir du DTB ou d'ACPI vers une description de plateforme unique, avec un résultat identique depuis les deux sources.</li>
<li><strong>Pilotes :</strong> PCIe ECAM, MSI-X, virtio-net, virtio-blk, virtio-scsi et virtio-rng, ainsi qu'un CSPRNG initialisé avec de l'entropie matérielle.</li>
<li><strong>Réseau :</strong> DHCPv4, ARP, ICMP et IPv4 strict maison, puis un TCP maison côté serveur uniquement : cookies SYN, roue de timers hiérarchique, NewReno, fusion des ACK, aucun état TIME_WAIT.</li>
<li><strong>TLS :</strong> TLS 1.3 uniquement, mTLS obligatoire, identité de l'appareil issue du CN ou d'un SAN URI ; la poignée de main est assurée par rustls, puis une couche d'enregistrements maison avec AES-128-GCM et ChaCha20-Poly1305. Reprise de session par tickets PSK, liste de révocation, contrôle d'admission dès la poignée de main.</li>
<li><strong>MQTT :</strong> codec sans copie, fuzzé et testé avec de vrais clients mosquitto ; QoS 0 et 1, jokers, messages retenus, testament (LWT), sessions persistantes, ACL limitées à <code>devices/{id}/#</code>, limites de débit par appareil.</li>
<li><strong>Stockage :</strong> SQLite dans le noyau via un VFS sur disque brut, en mode WAL, avec vérification d'intégrité au démarrage ; registre des appareils, révocations, messages retenus, sessions persistantes avec files de messages pour les appareils hors ligne ; par cœur, un journal de télémétrie distinct avec CRC par enregistrement et récupération après un plantage.</li>
<li><strong>Exploitation et supervision :</strong> timer de surveillance pour les gestionnaires, contrôle de santé, journalisation limitée, syslog et statsd sur UDP, une admission qui résiste aux tempêtes de reconnexions.</li>
<li><strong>Livraison :</strong> reconstructions identiques au bit près vérifiées en CI, ISO et image disque GPT brute, signatures minisign, instructions pour KVM, Proxmox et Hetzner.</li>
</ul>
<p>Chaque étape est marquée par le jalon qui l'a apportée, et le fichier <code>docs/PROGRESS.md</code> du dépôt est la seule source de vérité sur ce qui fonctionne réellement.</p>
<h2 id="larchitecture-en-trois-images">L'architecture en trois images</h2>
<p>La conception complète est décrite dans <code>docs/ARCHITECTURE.md</code>, et les raisons de chaque décision importante sont consignées sous forme d'ADR dans <code>docs/DECISIONS.md</code>. Voici un aperçu.</p>
<h3 id="les-couches">Les couches</h3>
<figure><img loading="lazy" alt="Les couches d'urMQTT : du broker, en passant par le codec MQTT, TLS, TCP et le réseau, jusqu'aux pilotes virtio et à la couche d'architecture, le tout dans un seul ELF au niveau EL1." src="https://aldo.tech/blog/urmqtt/img/layers.svg" /><figcaption>Les couches d'urMQTT : du broker, en passant par le codec MQTT, TLS, TCP et le réseau, jusqu'aux pilotes virtio et à la couche d'architecture, le tout dans un seul ELF au niveau EL1.</figcaption></figure>
<p>Du haut vers le bas : le broker (sessions, arbre des sujets, QoS, messages retenus, testaments, ACL, limites), le codec MQTT, TLS, TCP, la couche réseau, puis les pilotes. À côté de ce chemin principal se trouve le stockage (SQLite et le journal de télémétrie), et sous le tout la gestion de la mémoire, la découverte de la plateforme et la configuration compilée en constantes au moment de la construction, avec le certificat CA, le certificat du serveur et sa clé intégrés. L'hyperviseur avec ses périphériques virtio est la seule chose qui reste hors de l'image.</p>
<h3 id="un-thread-par-cur-aucun-etat-partage">Un thread par cœur, aucun état partagé</h3>
<figure><img loading="lazy" alt="Modèle d'exécution : chaque cœur de données a sa propre boucle et sa propre part du broker, et le cœur le plus élevé n'exécute que SQLite. Les cœurs communiquent exclusivement par des anneaux SPSC." src="https://aldo.tech/blog/urmqtt/img/cores.svg" /><figcaption>Modèle d'exécution : chaque cœur de données a sa propre boucle et sa propre part du broker, et le cœur le plus élevé n'exécute que SQLite. Les cœurs communiquent exclusivement par des anneaux SPSC.</figcaption></figure>
<p>Chaque cœur de données fait tourner sa propre boucle avec sa propre pile TCP, ses états TLS, sa part du broker (sessions et arbre des sujets), son cache de heap et sa partition de télémétrie. Les cœurs ne partagent aucun état modifiable, seulement des anneaux bornés sans verrou, un pour chaque paire ordonnée de cœurs. Le cœur le plus élevé exécute SQLite et rien d'autre.</p>
<p>La carte réseau n'a aujourd'hui qu'une paire de files, détenue par le cœur 0. Il oriente chaque flux MQTT vers l'un des cœurs de travail d'après le hash Toeplitz de son quadruplet, et envoie ce que les cœurs de travail renvoient. Une session vit sur son cœur d'attache, déterminé par le hash de l'identifiant du client, de sorte que la prise de contrôle, les testaments et la persistance ne franchissent jamais les cœurs. Une connexion qui atterrit sur un autre cœur y conserve son TCP et son TLS, et transmet le MQTT en clair au cœur d'attache avec un contrôle de flux fondé sur des crédits. Chaque message routé est en outre diffusé vers les autres parts du broker, dont chacune détient une réplique des messages retenus.</p>
<p>Trois règles maintiennent ce modèle. <strong>Traitement de bout en bout :</strong> un paquet reçu est traité intégralement sur un seul cœur, du TCP au message chiffré de réponse en passant par le déchiffrement TLS sur place, l'analyse MQTT et le routage, sans files de tâches et sans copie dans des tampons applicatifs. <strong>Interrogation adaptative :</strong> sous charge, les files de réception sont interrogées activement, et au repos le cœur retourne aux interruptions et à <code>WFI</code> ; les gestionnaires d'interruptions se contentent d'acquitter l'interruption et de marquer le travail. <strong>Timer de surveillance :</strong> un tick de 10 ms par cœur attrape les gestionnaires qui dépassent leur budget.</p>
<h3 id="la-vie-dun-publish">La vie d'un PUBLISH</h3>
<figure><img loading="lazy" alt="Le chemin d'un message PUBLISH en QoS 1 : de l'enregistrement TLS sur la carte réseau, en passant par le déchiffrement, la vérification des ACL et le routage, jusqu'au PUBACK fusionné avec l'ACK TCP dans un seul segment." src="https://aldo.tech/blog/urmqtt/img/publish.svg" /><figcaption>Le chemin d'un message PUBLISH en QoS 1 : de l'enregistrement TLS sur la carte réseau, en passant par le déchiffrement, la vérification des ACL et le routage, jusqu'au PUBACK fusionné avec l'ACK TCP dans un seul segment.</figcaption></figure>
<p>L'appareil envoie un enregistrement TLS. La carte réseau le remet à la boucle du cœur par un descripteur, sans copie. La boucle vérifie le TCP, déchiffre le TLS sur place et analyse le MQTT depuis un tampon emprunté, puis contrôle en mémoire l'ACL, la limite de débit et le chemin dans l'arbre des sujets. Si un autre cœur s'intéresse à ce sujet, il reçoit un message de routage avec une référence au contenu et l'identifiant du sujet ; la base reçoit un enregistrement de télémétrie ou une mise à jour de message retenu, par lots. La réponse PUBACK est chiffrée directement dans le tampon sortant et fusionnée avec l'ACK TCP, de sorte qu'un seul segment part vers l'appareil. Pour un autre cœur, le message ne coûte qu'un chiffrement par abonné au moment de l'envoi.</p>
<h2 id="ce-que-coute-une-connexion">Ce que coûte une connexion</h2>
<p>L'objectif est <strong>au plus 512 octets par connexion TLS inactive</strong>, avec une ambition de 256. Tant qu'une connexion est au repos, rien n'est conservé en tampon pour elle : les données reçues sont traitées directement depuis la trame réseau, et les tampons d'envoi, d'enregistrement et MQTT n'existent que tant que quelque chose est inachevé ou non acquitté. Le noyau le mesure lui-même : des compteurs exacts du heap, en octets demandés et en blocs occupés, consignés toutes les dix secondes sous la forme du heap au-dessus du niveau de base par connexion ouverte.</p>
<table>
<thead>
<tr>
<th>Élément</th>
<th style="text-align: right;">Taille</th>
<th>Comment</th>
</tr>
</thead>
<tbody>
<tr>
<td>Bloc de contrôle TCP</td>
<td style="text-align: right;">96 B</td>
<td>drapeaux empaquetés, RTT/RTO sur 16 bits, emplacement dans la roue de timers, aucun tampon au repos</td>
</tr>
<tr>
<td>Emplacement dans la table TCP</td>
<td style="text-align: right;">4–8 B</td>
<td>simple indice de connexion, table remplie au plus à moitié</td>
</tr>
<tr>
<td>Connexion et état d'enregistrement TLS 1.3</td>
<td style="text-align: right;">152 B</td>
<td>secret et séquence par sens (40 B) ; clé et IV dérivés par enregistrement</td>
</tr>
<tr>
<td>Connexion MQTT</td>
<td style="text-align: right;">48 B</td>
<td>identité et testament sur le heap uniquement tant qu'ils existent</td>
</tr>
<tr>
<td>Session MQTT, forme chaude</td>
<td style="text-align: right;">64 B</td>
<td>un abonnement en ligne, identifiants de nœuds internés</td>
</tr>
<tr>
<td>Abonnement <code>devices/&lt;id&gt;/cmd/#</code></td>
<td style="text-align: right;">0 nœud d'arbre</td>
<td>stocké relativement à l'identifiant de session, commun à tous les appareils</td>
</tr>
</tbody>
</table>
<p>Mesuré sous HVF avec quatre cœurs virtuels, dont trois de travail, chaque appareil tenant une connexion mTLS et un abonnement en QoS 1, arrondis de l'allocateur compris :</p>
<table>
<thead>
<tr>
<th style="text-align: right;">Connexions</th>
<th style="text-align: right;">Octets par connexion</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: right;">960</td>
<td style="text-align: right;">531 B</td>
</tr>
<tr>
<td style="text-align: right;">9 922</td>
<td style="text-align: right;">494 B</td>
</tr>
<tr>
<td style="text-align: right;">10 197</td>
<td style="text-align: right;">474 B</td>
</tr>
</tbody>
</table>
<p>Le pool de trames réseau est fixe, d'environ 2 MiB, soit à peu près 200 B par connexion à dix mille connexions et environ 2 B à un million. Nous n'avons pas pu mesurer davantage : le réseau en mode utilisateur de QEMU sous macOS a cessé d'accepter des connexions à 10 197, à peu près à la limite de 10 240 descripteurs du processus QEMU. Le chiffre diminue à mesure que les parts fixes sont réparties sur davantage de connexions, il ne croît donc pas avec leur nombre.</p>
<h2 id="le-modele-de-securite">Le modèle de sécurité</h2>
<p>La sécurité est la première priorité et l'emporte sur tous les autres objectifs en cas de conflit. Le modèle comporte plusieurs couches :</p>
<ul>
<li><strong>Un seul port.</strong> TCP 8883, uniquement TLS 1.3, certificats clients obligatoires signés par la CA intégrée à l'image. L'identifiant du client MQTT doit être égal à l'identifiant de l'appareil figurant dans le certificat.</li>
<li><strong>Aucune entrée.</strong> Pas de shell, pas de saisie console, pas de SSH, pas d'API d'administration, pas de code dynamique, pas de SQL accessible par le réseau et pas de système de fichiers.</li>
<li><strong>Sûreté mémoire.</strong> Rust, avec <code>forbid(unsafe_code)</code> dans les paquets de protocole ; chaque bloc <code>unsafe</code> porte un commentaire <code>// SAFETY:</code> vérifié par un lint, et aucune panique sur une entrée externe : clippy interdit <code>unwrap</code>, <code>expect</code>, l'indexation et <code>panic!</code> dans les paquets du chemin de données.</li>
<li><strong>Atténuation de l'exploitation.</strong> W^X est appliqué par l'encodage des descripteurs et par le bit <code>SCTLR_EL1.WXN</code>, et audité à chaque démarrage. Après le démarrage, les tables de pages sont en lecture seule, sans alias inscriptible. La moitié basse de l'espace d'adressage est désactivée. Chaque pile a des pages de garde, et les exceptions s'exécutent sur une pile distincte. PAC et BTI sont activés lorsque le processeur les prend en charge, ce que vérifient les tests de fumée.</li>
<li><strong>Résistance aux abus.</strong> Cookies SYN ; l'admission refuse de nouvelles connexions, avant toute cryptographie, lorsque le heap est bas, que la base prend du retard, que trop de poignées de main sont en cours par cœur ou par adresse source, ou que le débit de poignées de main est trop élevé. Limites de débit par appareil, files bornées, fenêtre TCP liée à la contre-pression du broker, journalisation limitée, timer de surveillance et contrôle de santé.</li>
<li><strong>Immuabilité.</strong> La configuration, la CA et la clé du serveur sont intégrées à l'image. Changer quoi que ce soit signifie une nouvelle image.</li>
</ul>
<p>Le modèle de menaces est consigné dans <code>docs/SECURITY.md</code>.</p>
<h2 id="un-stockage-sans-systeme-de-fichiers">Un stockage sans système de fichiers</h2>
<p>Les données vivent sur un disque distinct, virtio-blk ou virtio-scsi, sur lequel il n'y a pas de système de fichiers. La disposition est minimale et propre au projet :</p>
<pre><code>LBA 0      [ superbloc : magic · version de la disposition · uuid · table des régions · CRC-32C ]
LBA 8, 16  [ table des tailles de fichiers, emplacements A/B (le plus récent valide l'emporte) ]
LBA 64     [ base principale SQLite ½ ][ WAL SQLite ⅛ ][ journal 1 MiB ][ journal de télémétrie : 8 partitions … ]
derniers 4 KiB [ zone d'autotest du périphérique bloc au démarrage ]
</code></pre>
<p>SQLite (3.54.1 vendorisé, avec vérification du condensat à la construction) est compilé dans le noyau avec clang, sans bibliothèque standard, et utilise un heap <code>memsys5</code> fixe. Son VFS fait correspondre trois fichiers fixes à des régions du disque. Il fonctionne en mode WAL avec <code>locking_mode=EXCLUSIVE</code>, il n'a donc pas besoin d'index en mémoire partagée, et avec <code>synchronous=FULL</code> ; si <code>quick_check</code> trouve une erreur au démarrage, le système s'arrête en toute sécurité. La base contient le registre des appareils, les révocations, les messages retenus et les sessions persistantes avec des files de messages QoS 1 pour les appareils qui ne sont pas connectés.</p>
<p>Le cœur dédié à la base, celui qui porte le numéro le plus élevé, exécute SQLite et rien d'autre. Le plan de données lui envoie les changements d'état par un anneau SPSC sans verrou ; il les regroupe en une transaction toutes les 5 ms ou toutes les 512 opérations et renvoie les acquittements par un second anneau. Un disque vide est formaté au premier démarrage ; tout le reste n'est jamais reformaté par-dessus.</p>
<p>La télémétrie suit une autre voie. Chaque PUBLISH accepté sur les sujets de <code>storage.telemetry_topics</code> part dans la partition du journal de télémétrie de son cœur : un anneau de segments avec CRC par enregistrement, salé par le numéro de séquence du segment, de sorte que les anciens passages de l'anneau ne réapparaissent jamais. Les cœurs transmettent les enregistrements à la base par leur propre anneau, sans copie, et celle-ci les écrit toutes les 5 ms ou tous les 256 KiB. Au démarrage, la récupération conserve chaque enregistrement écrit, tronque la queue inachevée et reprend dans un nouveau segment. La rétention se fait par la taille, à travers l'anneau, et par l'âge, via <code>retention_days</code>. Nous l'avons vérifié avec un million d'enregistrements et avec <code>kill -9</code> en pleine écriture.</p>
<h2 id="les-chiffres">Les chiffres</h2>
<p>L'outil de charge dans <code>tools/loadgen</code> fait trois choses : une tempête de poignées de main TLS 1.3 avec mTLS et CONNECT, des connexions de longue durée avec PINGREQ qui publient de la télémétrie chaque seconde, et des tempêtes de reconnexions. <code>make bench</code> sur un Apple M4 Pro sous HVF donne :</p>
<table>
<thead>
<tr>
<th>Cœurs de travail</th>
<th>poignées de main/s dans l'invité</th>
<th>passage à l'échelle</th>
<th>via le réseau utilisateur QEMU (connexions/s)</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>862</td>
<td>1,00×</td>
<td>789</td>
</tr>
<tr>
<td>2</td>
<td>1 724</td>
<td>2,00×</td>
<td>636</td>
</tr>
<tr>
<td>4</td>
<td>3 438</td>
<td>3,99×</td>
<td>536</td>
</tr>
</tbody>
</table>
<p>Le chiffre dans l'invité, où le client et le serveur se trouvent dans la même machine virtuelle et où chaque cœur de travail fonctionne seul de son côté, montre que le plan de données passe à l'échelle de façon linéaire. Le chiffre réseau est limité par le réseau en mode utilisateur monothread de QEMU, et non par l'invité ; cela aussi est consigné, sous la référence ADR-032.</p>
<p>Nous testons les tempêtes de reconnexions en connectant tous les appareils en même temps, en répétant avec un délai les tentatives refusées, puis en les faisant tous tomber sans DISCONNECT. Le paramètre <code>heap_limit_mb=</code> en mode développement force l'admission à considérer le heap plus petit qu'il ne l'est, ce qui permet de tester une tempête plus grande que le heap avec peu de sockets. Lors d'un tour de ce type avec 3 000 appareils, 1 368 se sont connectés en 36 secondes, et 5 619 tentatives ont été proprement refusées, avec une limite de 8 MiB ; la mémoire est revenue à son niveau après la tempête.</p>
<h2 id="notre-facon-de-travailler">Notre façon de travailler</h2>
<p>Le projet est piloté par une spécification écrite, <code>PROMPT.md</code>, avec un plan de jalons, et chaque jalon se termine par le même rituel : <code>make check</code> et <code>make smoke</code> au vert, un commit portant l'étiquette <code>M&lt;n&gt;</code>, une entrée dans <code>docs/PROGRESS.md</code> et un ADR pour chaque décision qui n'est pas évidente. Les règles sont courtes et non négociables : pas de panique sur une entrée externe, <code>unsafe</code> uniquement dans les paquets matériels et FFI et toujours avec un commentaire, pas d'allocations sur le chemin chaud, un commentaire pour chaque ordre d'opérations atomiques, chaque limite provient de la configuration, et les éléments provisoires volontaires échouent bruyamment.</p>
<p>La CI fait plus que ce que nous pourrions faire à la main : fmt, clippy pour l'hôte et pour <code>aarch64-unknown-none</code>, typos, actionlint et shellcheck ; des tests sous Linux et macOS sur arm64 ; la construction du noyau pour la configuration de développement et de production avec la taille de l'ELF dans le résumé ; le démarrage sous QEMU avec tous les scénarios de test de fumée et la conservation des journaux série ; cargo-deny pour les avis, les licences et les sources ; une minute de fuzzing sur chacune des onze cibles, et la nuit dix minutes par cible avec un corpus mis en cache. S'y ajoutent CodeQL, OpenSSF Scorecard, Dependabot et une publication avec provenance SLSA. Chaque action tierce est épinglée sur un SHA de commit complet.</p>
<p>Nous prenons la reproductibilité au pied de la lettre : <code>make repro</code> construit HEAD deux fois dans des arbres de travail neufs et compare l'ELF, l'ISO et l'image brute octet par octet. Les publications sont signées avec minisign, et les artefacts peuvent être vérifiés avec <code>gh attestation verify</code>. L'image de production contient la clé privée du serveur, l'ISO lui-même est donc un secret ; les images jointes aux publications sur GitHub sont des images de développement, avec des clés temporaires, et ne servent qu'à essayer le projet.</p>
<h2 id="ce-que-nous-avons-appris">Ce que nous avons appris</h2>
<p>Quelques leçons qui ont changé le plan :</p>
<ul>
<li><strong>Un TCP maison s'est finalement avéré rentable.</strong> Le prototype fonctionnait avec smoltcp et nous a bien servi jusqu'à M13. Mais un serveur qui tient des centaines de milliers de connexions n'a pas besoin d'un TCP client : il lui faut des cookies SYN, une roue de timers et un état de 96 octets. Avec M14, nous avons remplacé smoltcp par notre propre pile uniquement côté serveur, avec des tests de perte simulée et du fuzzing de segments.</li>
<li><strong>Mesurez la mémoire de l'intérieur.</strong> Les estimations par connexion étaient inutilisables tant que le noyau ne s'est pas mis à compter lui-même le heap en blocs de l'allocateur plutôt qu'en octets demandés. C'est alors seulement que nous avons vu où partent réellement les 512 octets, et nous sommes descendus à 494.</li>
<li><strong>L'outil de test fait partie du produit.</strong> La plus grande limite n'était pas le noyau, mais le réseau en mode utilisateur de QEMU et le nombre de ports éphémères sur la machine de développement. Pour cent mille connexions et plus, il faut <code>vmnet-shared</code> sous macOS ou Linux avec KVM, une interface tap et plusieurs adresses clientes.</li>
<li><strong>SQLite sans système d'exploitation est possible</strong>, mais seulement à des conditions strictes : un heap fixe, un verrouillage exclusif sans mémoire partagée, un VFS qui ne connaît pas les fichiers, et une admission qui ne laisse pas la base prendre du retard.</li>
<li><strong>Les tempêtes de reconnexions sont l'état normal</strong>, pas l'exception. Quand le réseau mobile tombe puis revient, des milliers d'appareils frappent à la porte en même temps. C'est pourquoi l'admission refuse la connexion avant toute cryptographie, et chaque connexion refusée ne coûte presque rien.</li>
</ul>
<h2 id="la-suite">La suite</h2>
<p>Le travail après M17 est prévu dans trois directions, et l'ordre entre elles est consigné dans <code>docs/PLAN.md</code>. Rien de tout cela n'est encore construit.</p>
<ul>
<li><strong>Un seul ISO pour plusieurs plateformes.</strong> Un même fichier <code>urmqtt-&lt;env&gt;-&lt;version&gt;.iso</code> et une image GPT brute qui démarrent sur UEFI aarch64, UEFI x86_64 et BIOS x86_64, dans des machines virtuelles et sur du matériel réel, en commençant par un PC Intel. Toujours une appliance avec tout à l'intérieur, reproductible et signée.</li>
<li><strong>Un disque brut, sans système de fichiers.</strong> Une spécification du format au niveau de l'octet, un outil hôte <code>urmqtt-disk</code> qui partage les analyseurs avec le noyau, un alignement des régions sur 1 MiB, la croissance du disque avec les données, discard pour les disques à allocation dynamique, des instantanés A/B de la base, une vérification des bits et, en option, un chiffrement AES-256-XTS avec AES matériel.</li>
<li><strong>Sûreté démontrable.</strong> Révocation et liste d'appareils autorisés à l'exécution par un canal de contrôle signé, sans nouvelle image ; tests différentiels et de conformité de notre TLS et de notre TCP ; KASLR, protection de la pile, kCFI, MTE et un allocateur durci ; SQLite isolé au niveau EL0 ; Secure Boot ; SBOM et cargo-vet ; et enfin un dossier pour un audit externe.</li>
</ul>
<p>Les plateformes qui ne proposent que des périphériques non virtio, comme AWS ENA ou Azure MANA, ne sont pas prises en charge pour l'instant. Raspberry Pi 4 et 5 comme hôte KVM, Hetzner CAX et Oracle Ampere A1 sont documentés et pris en charge dans le code, mais n'ont pas encore été essayés sur un vrai service.</p>
<h2 id="essayez-le">Essayez-le</h2>
<p>Le dépôt se trouve sur <a href="https://github.com/zeljan-alduk/urMQTT">github.com/zeljan-alduk/urMQTT</a>. Avec Rust et sa chaîne d'outils nightly épinglée, ainsi que QEMU, <code>make certs</code>, <code>make run</code> et un client mosquitto suffisent pour un premier PUBLISH sur mTLS sur votre propre machine. L'avertissement du début vaut aussi pour la fin : il s'agit d'un pré-alpha, et tout ce qui fonctionne est consigné dans <code>docs/PROGRESS.md</code>. Les rapports de bogues, les questions de conception et les signalements de sécurité, selon les indications de <code>SECURITY.md</code>, sont les bienvenus. Nous reviendrons ici à chaque nouveau jalon.</p>]]></content:encoded>
      <category>rust</category>
      <category>mqtt</category>
      <category>iot</category>
      <category>unikernel</category>
      <category>open source</category>
    </item>
    <item>
      <title>Nous lançons un blog : notes de terrain</title>
      <link>https://aldo.tech/fr/blog/nous-lancons-un-blog/</link>
      <guid isPermaLink="true">https://aldo.tech/fr/blog/nous-lancons-un-blog/</guid>
      <pubDate>Sat, 17 Oct 2026 08:00:00 +0000</pubDate>
      <description>Pourquoi une entreprise qui construit des terminaux, des applications et de l&#x27;électronique pour les transports publics se met à écrire, et ce que vous lirez ici.</description>
      <content:encoded><![CDATA[<p>L'essentiel de ce que nous construisons est invisible. Le logiciel du terminal à la porte du bus, le firmware d'un appareil qui n'aura jamais d'écran, le serveur qui transforme chaque nuit les transactions de la journée en rapports. Quand tout fonctionne, c'est silencieux. Quand ça ne fonctionne pas, tout le monde le remarque.</p>
<p>Sur ce blog, nous notons ce que nous avons appris en chemin. Pas de textes marketing, mais des notes concrètes : ce qui a mal tourné, comment nous l'avons résolu et ce que nous ferions autrement aujourd'hui.</p>
<h2 id="de-quoi-nous-allons-parler">De quoi nous allons parler</h2>
<ul>
<li><strong>La billettique dans les transports publics.</strong> Cartes sans contact, modules SAM, fonctionnement hors ligne et tout ce qu'un terminal doit supporter dans un véhicule qui vibre douze heures par jour.</li>
<li><strong>Les applications mobiles sur le terrain.</strong> Android et Flutter sur des appareils qui ne sont pas des téléphones ordinaires : avec lecteurs de cartes, imprimantes et batteries qui doivent tenir toute une journée de service.</li>
<li><strong>Le backend et la supervision de flotte.</strong> Comment savoir qu'un appareil dans un véhicule a un problème avant que le conducteur ne le signale.</li>
<li><strong>Le matériel.</strong> Du prototype sur l'établi à la carte qui part en production en série, et toutes les erreurs entre les deux.</li>
<li><strong>Les modèles d'IA en local.</strong> Où les modèles sur votre propre matériel ont du sens, et où le cloud et une API sont tout simplement le meilleur choix.</li>
</ul>
<h2 id="comment-nous-allons-ecrire">Comment nous allons écrire</h2>
<p>Court et concret, avec du code et des chiffres quand nous en avons. Nous publierons quand nous aurons quelque chose à dire, pas selon un calendrier. Les articles sont écrits en croate et traduits dans toutes les langues de ce site ; vous pouvez aussi nous suivre par <a href="https://aldo.tech/fr/blog/feed.xml">RSS</a>.</p>
<p>Si un sujet vous intéresse ou si vous travaillez sur un problème similaire, écrivez-nous à <a href="mailto:info@aldo.tech">info@aldo.tech</a>.</p>]]></content:encoded>
      <category>introduction</category>
    </item>
  </channel>
</rss>
