<?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>ALDO TECH blog</title>
    <link>https://aldo.tech/nl/blog/</link>
    <atom:link href="https://aldo.tech/nl/blog/feed.xml" rel="self" type="application/rss+xml"/>
    <description>Notities uit de praktijk: openbaar vervoer, tariefinning, hardware en software die onderweg moeten werken.</description>
    <language>nl</language>
    <item>
      <title>urMQTT na de VPS: SQLite in gebruikersmodus, vergelijking met Mosquitto, configuratie als data en een builder in de browser</title>
      <link>https://aldo.tech/nl/blog/urmqtt-config-builder-plaintext-mosquitto/</link>
      <guid isPermaLink="true">https://aldo.tech/nl/blog/urmqtt-config-builder-plaintext-mosquitto/</guid>
      <pubDate>Sun, 01 Nov 2026 08:00:00 +0000</pubDate>
      <description>Wat er gebeurde na het tweede artikel over urMQTT: SQLite buiten de kernel geïsoleerd, metingen tegen Mosquitto op dezelfde VPS (inclusief wat niet in ons voordeel uitviel), gevonden oorzaken van de trage enkele stroom, configuratie als data met een web-builder voor ISO&#x27;s en een emulator in de browser, optionele gewone MQTT en wat nog niet bewezen is.</description>
      <content:encoded><![CDATA[<p>Het tweede artikel, <a href="https://aldo.tech/blog/urmqtt-vps-handshake-disk-hardening/">urMQTT op een echte VPS</a>, eindigde met een lijst "Wat volgt": H6 (SQLite buiten de kernel), H7 (tijdveiligheid), X11 (vergelijking met Mosquitto op dezelfde VPS) en de eerste release. Dit artikel volgt die lijst in dezelfde volgorde en voegt dan twee dingen toe die niet op de lijst stonden, omdat ze onderweg opdoken: configuratie als data, met een web-builder voor ISO's, en optionele gewone MQTT zonder TLS. Net als eerder: wat we hadden gepland, wat we hebben gebouwd, wat ons verraste en wat openstaat. In dit artikel staan ook enkele resultaten die niet in het voordeel van urMQTT uitvallen; we hebben ze laten staan omdat ze deel van de waarheid zijn.</p>
<p>De opmerking vooraf blijft dezelfde: het project is pre-alfa, de eerste release is alleen voor VPS'en met KVM, en de cijfers meten code, tests en bepaalde experimenten, geen rijpheid in productie.</p>
<h2 id="hoe-het-er-in-de-tijd-uitzag">Hoe het er in de tijd uitzag</h2>
<p>Sinds het vorige artikel (31.10. om 23:00) tot vandaag (1.11. om 20:41) zijn er 59 nieuwe commits in de repository bijgekomen, in totaal 178. Ze zijn allemaal in één kalenderdag ontstaan, dus dezelfde waarschuwing als eerder geldt: het project wordt gebouwd met Claude Code (de commits dragen <code>Co-Authored-By: Claude</code>), plan, code, tests en ADR's ontstaan in korte, gesloten lussen, en de cijfers hieronder zeggen iets over de hoeveelheid code en controles, niet over hoeveel menselijk werk erin zit.</p>
<figure><img loading="lazy" alt="Groei sinds het vorige artikel: regels Rust van 89.790 naar 112.408, #[test]-functies van 739 naar 926, ADR's van 104 naar 126 en commits van 119 naar 178. Elke rij heeft een eigen schaal." src="https://aldo.tech/blog/urmqtt-config-builder-plaintext-mosquitto/img/growth-since.svg" /><figcaption>Groei sinds het vorige artikel: regels Rust van 89.790 naar 112.408, #[test]-functies van 739 naar 926, ADR's van 104 naar 126 en commits van 119 naar 178. Elke rij heeft een eigen schaal.</figcaption></figure>
<p>De cijfers zijn berekend met dezelfde methode als in het vorige artikel: alle <code>.rs</code>-bestanden buiten <code>vendor/</code> en <code>fuzz/</code>, <code>#[test]</code>-functies, koppen <code>## ADR-</code> in <code>docs/DECISIONS.md</code>.</p>
<table>
<thead>
<tr>
<th>Moment</th>
<th>Commit</th>
<th>Tijdstip</th>
<th style="text-align: right;">Regels Rust</th>
<th style="text-align: right;">Tests</th>
<th style="text-align: right;">ADR's</th>
</tr>
</thead>
<tbody>
<tr>
<td>vorig artikel</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 klaar</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>web-builder en releasespiegel</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>gewone MQTT, documentatie</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>vandaag</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>Er is parallel op meerdere branches gewerkt, dus de volgorde van de commits is niet strikt de volgorde van het werk. De ADR's zijn genummerd tot 232, maar de nummering heeft gaten (een deel is hernoemd bij het samenvoegen van branches); we tellen de koppen, niet het hoogste nummer. Releases: v0.2.0 (1.11., 18:45 UTC) en v0.2.1 (1.11., 20:22 UTC), beide gemarkeerd als pre-release.</p>
<h2 id="h6-sqlite-in-gebruikersmodus">H6: SQLite in gebruikersmodus</h2>
<p><strong>Wat we hadden gepland.</strong> SQLite is 254.268 regels C die de database, de WAL en het journaal van schijf parsen, en de schijf kan worden gemaakt of gewijzigd. Tot dan toe draaide het in de kernel, met de rechten van de kernel. <strong>Wat we hebben gebouwd</strong> (ADR-210 tot ADR-212): SQLite, zijn libc-onderdelen en de heap zijn verplaatst naar een apart programma, <code>dbworker</code>, dat draait op EL0 (aarch64) respectievelijk ring 3 (x86_64), in een eigen adresruimte. De kernel linkt SQLite niet meer. De worker praat met de kernel via een gedeeld geheugenvenster en twaalf systeemaanroepen, en elke lengte en elke offset die terugkomt wordt gecontroleerd voordat ze worden gebruikt. Een fout in de worker (ongeoorloofde toegang, ongedefinieerde instructie, overschreden tijdbudget van 30 s) beëindigt de aanroep, de worker wordt opnieuw gestart en de database wordt uit de WAL geopend zoals bij het opstarten; na 5 herstarts per minuut wordt de opslag defect verklaard en wordt er niet eindeloos herstart.</p>
<p>De prijs, gemeten op aarch64 onder HVF, dezelfde VM, een kernel met ingebouwde SQLite tegenover een kernel met de worker, zes afwisselende runs van elk:</p>
<table>
<thead>
<tr>
<th>Meting</th>
<th>Ervoor (in de kernel)</th>
<th>Erna (worker)</th>
<th>Verschil</th>
</tr>
</thead>
<tbody>
<tr>
<td>binnengaan en verlaten van de gebruikersmodus</td>
<td>-</td>
<td>1,1 us per rondgang</td>
<td></td>
</tr>
<tr>
<td><code>db-bench</code>, 5.000 inserts in 50 transacties, beste / mediaan</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>één insert per transactie, beste / mediaan</td>
<td>2.319 / 2.148 op/s</td>
<td>2.307 / 2.126 op/s</td>
<td>-0,5 % / -1 %</td>
</tr>
<tr>
<td>volledige back-up in één aanroep (23 MiB)</td>
<td>514 ms</td>
<td>635 ms</td>
<td>+23 %</td>
</tr>
</tbody>
</table>
<p>Schrijfacties naar schijf (1.572) en flushes (319) zijn identiek, zoals het hoort. Dit is niet geïsoleerd: <strong>de kernel vertrouwt nog steeds wat de worker teruggeeft</strong>, na controle van de vorm. Een gecompromitteerde worker kan liegen (bijvoorbeeld een lege lijst van ingetrokken certificaten teruggeven en ze daarmee weer "ongedaan intrekken"), maar kan het geheugen van de kernel, de TLS-sleutels en de ticketsleutels niet lezen. Er is geen KPTI, dus lekken van het type Spectre/Meltdown worden niet voorkomen en we beweren er niets over; de PAC-sleutel wordt gedeeld door EL0 en EL1. Op x86_64 hebben we de schakelkosten niet gemeten op KVM (onder TCG zou het getal zinloos zijn), en het fuzzen van de worker en het protocol zelf is niet af. De testreeks voor opslag (79 scenario's, elk draait SQLite in de worker) slaagt op aarch64 HVF en x86_64 TCG, op aarch64 TCG 76 (één wordt overgeslagen omdat TCG geen MSI-X heeft).</p>
<p><strong>H7 is afgesloten, maar niet zoals we hadden aangekondigd.</strong> De ondergrens voor de tijd (ADR-050) en SNTP met consensus (T1) dekken de reikwijdte van H7; NTS (RFC 8915) blijft buiten de reikwijdte en is vastgelegd als resterend risico, niet als iets wat werkt. We vinden niet dat tijdveiligheid "opgelost" is, maar dat het aantoonbare deel ervan klaar is.</p>
<p><strong>Nieuwe netwerkoptie.</strong> Contabo geeft geen DHCP aan een aangepaste ISO, dus de VPS vroeg tot dan toe om een andere image dan de lab-VM. Nu vraagt <code>network.mode = "auto"</code> (ADR-214) eerst DHCP, en als er binnen de ingestelde tijd (<code>dhcp_timeout_s</code>, standaard 15 s) geen lease is of die verloren gaat, past het het ingestelde vaste adres toe. Handmatig geprobeerd in QEMU (met DHCP gebruikt het de lease, zonder netwerk valt het na 5 s terug op het vaste adres); een smoke-scenario daarvoor is er nog niet.</p>
<h2 id="x11-vergelijking-met-mosquitto-op-dezelfde-vps">X11: vergelijking met Mosquitto op dezelfde VPS</h2>
<p>Hetzelfde type machine als in het vorige artikel (Contabo, 4 vCPU AMD EPYC, 8 GiB), belasting vanaf een Linux-host op 15 ms afstand, wederzijdse TLS 1.3 met dezelfde certificaten aan beide kanten, Mosquitto 2.0.21 op Debian 13 zonder persistentie. De methode en de beslissingen staan in ADR-213, de ruwe data in <code>docs/perf-results/</code>. Mediaan van drie runs.</p>
<figure><img loading="lazy" alt="Volledige handshakes per seconde (TLS 1.3 + clientcertificaat + CONNECT, zonder sessiehervatting) bij 32, 64 en 128 clients: urMQTT 169, 278, 413; Mosquitto 82, 140, 184. Dit zijn ondergrenzen: de belastinggenerator zat dicht bij zijn eigen limiet." src="https://aldo.tech/blog/urmqtt-config-builder-plaintext-mosquitto/img/mosquitto.svg" /><figcaption>Volledige handshakes per seconde (TLS 1.3 + clientcertificaat + CONNECT, zonder sessiehervatting) bij 32, 64 en 128 clients: urMQTT 169, 278, 413; Mosquitto 82, 140, 184. Dit zijn ondergrenzen: de belastinggenerator zat dicht bij zijn eigen limiet.</figcaption></figure>
<table>
<thead>
<tr>
<th></th>
<th>urMQTT</th>
<th>Mosquitto 2.0.21</th>
</tr>
</thead>
<tbody>
<tr>
<td>handshakes/s, 32 / 64 / 128 clients (benchprofiel)</td>
<td>169 / 278 / 413</td>
<td>82 / 140 / 184</td>
</tr>
<tr>
<td>idem, met de limieten zoals ze worden geleverd</td>
<td>185 / 262 / 333 (bij 128: 202 tot 286 fouten, handshake-resets)</td>
<td>-</td>
</tr>
<tr>
<td>geheugen per inactieve verbinding (20.000 verbindingen)</td>
<td>0,50 KB kernel-heap (0,66 KB bij 5.000)</td>
<td>26,8 KB procesgroei</td>
</tr>
<tr>
<td>latentie p50, QoS 0 / QoS 1, 800 berichten/s, 64 B</td>
<td>14,0 / 14,8 ms</td>
<td>21,4 / 39,5 ms</td>
</tr>
<tr>
<td>QoS 1, 4.800 berichten/s, afgeleverd/s (p50)</td>
<td>4.209 (77 ms)</td>
<td>4.235 (197 ms)</td>
</tr>
<tr>
<td>100 abonnees, 50 berichten/s: p50</td>
<td>16,6 ms</td>
<td>99 ms</td>
</tr>
<tr>
<td>één stroom, QoS 0, 64 B, gepubliceerd/s</td>
<td>2.962 (0 tot 7.733)</td>
<td>22.749</td>
</tr>
</tbody>
</table>
<p>Wat dit betekent en wat niet. De handshakes zijn <strong>ondergrenzen voor beide servers</strong>: de belastinggenerator is een gedeelde VM met 60 tot 70 % gestolen processortijd en zat bij 128 threads dicht bij zijn eigen limiet, terwijl de werkcores van urMQTT slechts 12 tot 13 % bezet waren. De verhouding bij 128 clients (ongeveer 2,2 keer) is het zinvolst; bij 32 en 64 meet ze voornamelijk het wachten van de clients. De geheugencijfers tellen niet hetzelfde (kernel-heap tegenover RSS van het proces), dus "ongeveer 50 keer minder" is een orde van grootte, geen precieze verhouding. De latentie van 14 ms is één netwerkrondgang (ongeveer 15 ms), dus "geen meetbare tijd van de broker"; de staarten (p99 ongeveer 100 tot 200 ms) worden bij beide brokers gedomineerd door de gedeelde hosts. Verschillen onder ongeveer 20 % beweren we niet.</p>
<p><strong>Wat niet in ons voordeel uitviel</strong> en in de tabellen is gebleven: één TCP-stroom door urMQTT droeg 2 tot 8 keer minder dan door Mosquitto, met schommelingen van nul tot 7,7 duizend berichten per seconde tussen runs, en de watchdog van 20 ms voor de handler <strong>verbrak levende verbindingen</strong> wanneer de hypervisor een vCPU stopzette (in de productie-opstelling <code>action = "drop"</code>, 6 tot 8 verbindingen per run bij QoS 0-overbelasting). Beide zijn beschreven in de volgende paragraaf, omdat ze hun oorzaken in onze eigen code hadden.</p>
<h2 id="waarom-de-enkele-stroom-traag-was-oorzaken-geen-excuses">Waarom de enkele stroom traag was: oorzaken, geen excuses</h2>
<p>Het makkelijkst zou zijn om "gedeelde VPS" te zeggen. Dat hebben we niet gedaan; we hebben op dezelfde VPS en met dezelfde methode naar oorzaken gezocht. Er zijn er vijf gevonden, vier in het datapad en één in de watchdog (ADR-221 tot ADR-224):</p>
<table>
<thead>
<tr>
<th>Symptoom in X11</th>
<th>Oorzaak in urMQTT</th>
<th>Wijziging</th>
</tr>
</thead>
<tbody>
<tr>
<td>één stroom 1,9k berichten/s, vaak <strong>0 afgeleverd</strong></td>
<td><strong>Verloren wake-up:</strong> de uitvoerlus gaf de lijst van gereedstaande verbindingen terug over de net geschreven lijst heen; een abonnee wiens uitgaande buffer eenmaal vol was geraakt werd nooit meer bediend totdat een PINGRESP hem wekte</td>
<td>gerepareerd (ADR-223)</td>
</tr>
<tr>
<td>een paar procent verlies verandert 48k in 14k berichten/s</td>
<td><strong>elk segment na een verloren segment werd weggegooid</strong> (ontvangst alleen in volgorde), dus herstel kostte één netwerkrondgang per segment</td>
<td>wachtrij voor segmenten buiten volgorde, SACK (ADR-222)</td>
</tr>
<tr>
<td>plafond van 4,4 MB/s bij 15 ms</td>
<td>16-bits venster, verzendwachtrij van 64 KiB en een langzame start die ACK's telde in plaats van bytes</td>
<td>vensterschaling, tijdstempels, tellen in bytes (ADR-222)</td>
</tr>
<tr>
<td>elke rondgang van een stroom die past wacht 0 tot 10 ms</td>
<td><strong>de dispatcher sliep tot de volgende tik van 10 ms</strong> (virtio-net heeft geen interrupt voor ontvangst)</td>
<td>de NIC elke 200 us pollen zolang er verbindingen zijn (ADR-224)</td>
</tr>
<tr>
<td>1 tot 8 verbroken verbindingen per run</td>
<td><strong>de watchdog rekende de verstreken tijd aan de handler aan</strong>, en de VPS zet vCPU's vaak en lang stil</td>
<td>het budget is de tijd die de core daadwerkelijk heeft gewerkt (ADR-221)</td>
</tr>
</tbody>
</table>
<p>De stilstanden zijn echt: op deze VPS hadden cores 0 tot 2 een mediaan van 36 tot 80 vertraagde tikken per 5 seconden, en de ergste stilstand in het venster was mediaan ongeveer 70 ms en tot 0,9 s (core 3, database en journaal: tot 1,55 s), terwijl de host geen gestolen tijd meldt (<code>host steal 0 us</code>). Daarom had de watchdog een tweede klok nodig, geen groter budget.</p>
<figure><img loading="lazy" alt="Ervoor en erna, op dezelfde VPS met dezelfde methode, mediaan van drie tot zes runs: één stroom QoS 0, 64 B: gepubliceerd 1,9k, 23,7k (Mosquitto 29,3k), afgeleverd 0 (mediaan), 12,6k (29,2k). 16 paren, overbelasting, 64 B: gepubliceerd 24,7k, 43,2k (31,4k), afgeleverd 20,0k, 37,4k (30,7k)." src="https://aldo.tech/blog/urmqtt-config-builder-plaintext-mosquitto/img/before-after.svg" /><figcaption>Ervoor en erna, op dezelfde VPS met dezelfde methode, mediaan van drie tot zes runs: één stroom QoS 0, 64 B: gepubliceerd 1,9k, 23,7k (Mosquitto 29,3k), afgeleverd 0 (mediaan), 12,6k (29,2k). 16 paren, overbelasting, 64 B: gepubliceerd 24,7k, 43,2k (31,4k), afgeleverd 20,0k, 37,4k (30,7k).</figcaption></figure>
<table>
<thead>
<tr>
<th></th>
<th>X11-code (7f9517e)</th>
<th>nu (86a71ff)</th>
<th>Mosquitto</th>
</tr>
</thead>
<tbody>
<tr>
<td>één stroom, QoS 0, 64 B, gepubliceerd / afgeleverd per s</td>
<td>1,9k / 0</td>
<td>23,7k / 12,6k</td>
<td>29,3k / 29,2k</td>
</tr>
<tr>
<td>één stroom, 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 paren, overbelasting, 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 paren, overbelasting, 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>verloren verbindingen per run (16 paren, 64 B)</td>
<td>13 (7 tot 16)</td>
<td>0 (0 tot 0)</td>
<td>0</td>
</tr>
</tbody>
</table>
<p>Watchdog: 148 verbroken verbindingen in 30 belaste runs ervoor, 0 in 24 runs erna; op een lokaal apparaat dat frames vertraagt en laat vallen heeft bij 0,1 % verlies één stroom van 64 B met 14,3k berichten/s (kernel X11) nu 41,0k, en bij 0,5 % gaat het van 5,0k naar 17,7k.</p>
<p><strong>Wat dit niet zegt.</strong> urMQTT is <strong>nog steeds niet sneller dan Mosquitto op één stroom</strong>: bij 64 B publiceert het ongeveer 80 % van zijn snelheid en levert het ongeveer 40 % af, omdat het QoS 0 weggooit wanneer de stroom van de abonnee trager is dan die van de uitgever (Mosquitto zet zonder limiet in de wachtrij). Bij 1 KiB in de overbelasting met 16 paren loopt Mosquitto ongeveer 15 % voor, wat binnen de ruis valt. De generator was opnieuw een gedeelde VM met 58 tot 77 % gestolen tijd. De tweede ronde (heap-locking buiten het datapad, SACK aan de verzendkant en meer) staat op de niet-geïntegreerde branch <code>perf-single-flow-wip</code>; de cijfers daarvan zijn nog geen beweringen en we noemen ze hier niet.</p>
<h2 id="een-belofte-die-we-niet-hadden-gedaan-maar-toch-zijn-nagekomen-configuratie-is-data">Een belofte die we niet hadden gedaan, maar toch zijn nagekomen: configuratie is data</h2>
<p>Tot nu toe was elke configuratie een nieuwe build: sleutels en limieten waren in de image ingebakken. C1 draait dat om (ADR-215 tot ADR-219): de kernel wordt één keer gebouwd en de configuratie is data.</p>
<ul>
<li><strong>Blob <code>urmqtt-cfg</code></strong> (C1a): een geversioneerde, canonieke codering (velden op oplopende tags, SHA-256-vingerafdruk, optionele losse Ed25519-handtekening), 70 velden met standaardwaarden en grenzen, een decoder die alles wat ongeldig is afwijst.</li>
<li><strong>De kernel leest hem als Limine-module</strong> (C1b), vóór al het andere, op aarch64 en x86_64. Er is <strong>één uitgegeven kernel voor elke <code>ENV</code> behalve <code>dev</code></strong> (de aarch64-ELF's voor <code>release</code>, <code>prod</code> en <code>bench</code> hebben dezelfde SHA-256; alleen op aarch64 gecontroleerd). De uitgegeven kernel wijst een ontwikkelconfiguratie af, en zonder configuratie schrijft hij "refusing to serve" en bedient niets.</li>
<li><strong>De geïnstalleerde schijf draagt de configuratie</strong> en een update behoudt haar (C1c).</li>
<li><strong><code>urmqtt-config</code> en de ISO-sjabloon</strong> (C1d): <code>pack</code>, <code>show</code>, <code>verify</code>, en <code>pack --iso</code> vervangt beide exemplaren van de configuratieplek en 24 modulesamenvattingen in een kant-en-klare ISO. Het resultaat is byte voor byte gelijk aan de ISO die <code>mkiso.sh --config</code> bouwt. De sjabloon van release v0.2.0 is 63.111.168 bytes groot.</li>
<li><strong>Vertrouwensverklaring</strong> (ADR-219): een geconfigureerde image schrijft bij het opstarten de vingerafdruk van de configuratie uit (<code>config: fingerprint ...</code>), en de handtekening is optioneel en <strong>standaard uitgeschakeld</strong>. Een gewijzigde ISO heeft geen officiële samenvatting; de vingerafdruk is de manier om te zien wat erin zit.</li>
</ul>
<h2 id="de-builder-op-aldotech">De builder op aldo.tech</h2>
<p>Wat <code>urmqtt-config pack --iso</code> doet, doet ook de webpagina, met dezelfde code: de bibliotheek van de tool gecompileerd naar WebAssembly (1,06 MB, zonder imports en zonder <code>unsafe</code>-blok). De pagina staat op <a href="https://aldo.tech/nl/producten/urmqtt-builder/">aldo.tech/nl/producten/urmqtt-builder/</a> en bestaat in alle 15 talen van de site (bijvoorbeeld <a href="https://aldo.tech/proizvodi/urmqtt-builder/">hr</a>).</p>
<ul>
<li><strong>Er verlaat niets de browser.</strong> Certificaten, privésleutels en de kant-en-klare ISO blijven op de computer; de pagina downloadt alleen releasebestanden van deze site, zonder tracking en zonder scripts van derden. In de test live droeg geen enkel verzoek sleutelmateriaal.</li>
<li><strong>Releasespiegel</strong> (ADR-227): elk uur (systemd-timer, lage prioriteit) kopieert hij de releases van GitHub, controleert elke vóór publicatie, en de pagina in de browser controleert de handtekening van <code>SHA256SUMS</code> met een in de pagina ingebouwde sleutel en schrijft pas daarna de configuratie in de sjabloon. Een mislukte controle blokkeert het downloaden.</li>
<li><strong>Eenvoudig als standaard, geavanceerd als dat nodig is</strong> (ADR-232): het eerste scherm bestaat uit drie stappen (waar het draait, uw certificaten, naam van de image). Eén veld om bestanden in te slepen sorteert certificaat, sleutel en CA op inhoud. De geavanceerde weergave opent elk van de 70 velden, zoeken, en import en export van TOML; beide weergaven bewerken dezelfde toestand. De sjabloon (63 MB) wordt gedownload terwijl de gebruiker het formulier invult. In een proef live, met een proefspiegel en een proefsleutel, duurde het bouwen van de ISO in de browser 5 tot 18 s.</li>
</ul>
<figure><img loading="lazy" alt="Eenvoudige weergave van de builder: drie stappen, &quot;Waar gaat het draaien?&quot;, &quot;Uw certificaten&quot; en &quot;Geef uw image een naam&quot;, met één volgende actie in de zijbalk." src="https://aldo.tech/blog/urmqtt-config-builder-plaintext-mosquitto/img/builder-simple.png" /><figcaption>Eenvoudige weergave van de builder: drie stappen, &quot;Waar gaat het draaien?&quot;, &quot;Uw certificaten&quot; en &quot;Geef uw image een naam&quot;, met één volgende actie in de zijbalk.</figcaption></figure>
<figure><img loading="lazy" alt="Geavanceerde weergave, opgenomen op een proefspiegel met een proefsleutel (de releasedatum en de ondertekeningssleutel zijn niet echt): een release met geverifieerde handtekening, sjablonen, elk veld van de configuratie." src="https://aldo.tech/blog/urmqtt-config-builder-plaintext-mosquitto/img/builder-advanced.png" /><figcaption>Geavanceerde weergave, opgenomen op een proefspiegel met een proefsleutel (de releasedatum en de ondertekeningssleutel zijn niet echt): een release met geverifieerde handtekening, sjablonen, elk veld van de configuratie.</figcaption></figure>
<p><strong>Emulator, optioneel.</strong> Naast "ISO downloaden" staat "Uitproberen in de emulator": QEMU gecompileerd naar WebAssembly draait de net gebouwde image in een browsertabblad. Er wordt niets van gedownload vóór de klik (ongeveer 3 MB de eerste keer). De emulator is alleen BIOS, <strong>zonder netwerk</strong> (zodat het verkeer van de gebruiker niet via een tussenpersoon loopt en "er verlaat niets de browser" niet wordt geschaad), vraagt ongeveer 1,5 GB geheugen en een desktopbrowser (getest met Chrome; Firefox en Safari niet). Daarin bereikt urMQTT <code>URMQTT READY</code> in ongeveer 8 s (met overgeslagen aftelling) en schrijft het dezelfde vingerafdruk van de configuratie uit als de pagina, wat het bewijs is dat juist die configuratie is opgestart. Het opstarten is ongeveer 2 keer langzamer dan in het oorspronkelijke QEMU op dezelfde Mac (0,82 s tegenover 1,9 s), en de cryptografie ongeveer 58 keer langzamer omdat de hardware-instructies worden geëmuleerd. Daarom <strong>trekken we uit de emulator geen enkele bewering over snelheid</strong>, en de pagina zegt dat vóór het starten. Bij het bouwen moesten vier bytes in de <code>.wasm</code> worden gepatcht vanwege een fout in de vertaling van de POPCNT-instructie in het oorspronkelijke QEMU (die hebben we nog niet gemeld); de emulator zelf valt onder de GPL, met aanbod van de broncode.</p>
<figure><img loading="lazy" alt="Diagnostische invoer in de emulator live op aldo.tech: de kernel heeft dezelfde vingerafdruk van de configuratie uitgeschreven als de pagina, en het scherm toont de diagnostiek (in de emulator eindigt ze na ongeveer 20 s; de cijfers ervan beschrijven de emulator, niet urMQTT)." src="https://aldo.tech/blog/urmqtt-config-builder-plaintext-mosquitto/img/emulator-diagnostic.png" /><figcaption>Diagnostische invoer in de emulator live op aldo.tech: de kernel heeft dezelfde vingerafdruk van de configuratie uitgeschreven als de pagina, en het scherm toont de diagnostiek (in de emulator eindigt ze na ongeveer 20 s; de cijfers ervan beschrijven de emulator, niet urMQTT).</figcaption></figure>
<h2 id="gewone-mqtt-alleen-voor-netwerken-die-u-vertrouwt">Gewone MQTT, alleen voor netwerken die u vertrouwt</h2>
<p>Gewone MQTT (TCP 1883, zonder TLS) is in IoT heel gangbaar: het is de standaard in Mosquitto, in de meeste firmware voor Arduino en ESP, in Tasmota, Zigbee2MQTT en Home Assistant, en wordt ook gebruikt binnen gesegmenteerde industriële netwerken, achter VPN's en op privé-LAN's, en zelfs op apparaten zonder klok (certificaten vragen een datum), zonder genoeg flash of processor voor TLS. Over een onbetrouwbaar netwerk is het niet aanvaardbaar: wie onderweg zit, leest en wijzigt het verkeer. Daarom is P1 (ADR-229 tot ADR-231) een uitdrukkelijke, beperkte keuze, <strong>standaard uitgeschakeld</strong>, en blijft de TLS-poort bestaan; TLS wordt hiermee niet uitgeschakeld, de gewone poort komt er naast.</p>
<ul>
<li><strong>Een lijst van toegestane netwerken is verplicht</strong> (<code>allow_from</code>, CIDR's) en wordt bij de SYN gecontroleerd: buiten de lijst is er geen SYN-ACK en geen toestand. Het hele internet (<code>0.0.0.0/0</code>) vraagt een aparte, uitdrukkelijke optie.</li>
<li><strong>Drie aanmeldmethoden:</strong> wachtwoord (standaard wanneer ingeschakeld; PBKDF2-HMAC-SHA-256, salt, 10.000 iteraties, vergelijking in constante tijd, vergrendeling per adres), anoniem (één geconfigureerde identiteit met een eigen ACL) en per adres (<code>ip</code>). De builder hasht het wachtwoord in de browser en bewaart in het geheugen alleen de samenvatting.</li>
<li><strong>Minder rechten dan mTLS:</strong> een gewone client heeft nooit service-, operator- of beheerrechten, geen <code>$SYS</code> en geen console, heeft eigen sessies (<code>$plain/&lt;identiteit&gt;/&lt;client-id&gt;</code>, alleen schone), en eigen limieten en het register van toegestane apparaten blijven voor hem gelden.</li>
<li><strong>Wat verloren gaat, staat in <code>SECURITY.md</code>:</strong> vertrouwelijkheid, integriteit, authenticiteit van de broker en herhaling van een opgenomen aanmelding. Dat is met configuratie niet te herstellen; zo is het zonder TLS.</li>
</ul>
<p>Gemeten op een laptop (Apple silicon, HVF, gebruikersnetwerk van QEMU), 16 paren, 64 B, drie runs van 10 s: bij 8.000 berichten per seconde leveren het TLS-pad en het gewone pad alle berichten af met dezelfde latentie (p50 0,17 ms tegenover 0,13 tot 0,18 ms), nieuwe verbindingen per seconde: TLS ongeveer 2.600 tot 2.750, anoniem gewoon ongeveer 4.900 tot 7.800, en <strong>aanmelden met wachtwoord is het dure deel</strong>: één PBKDF2 kost ongeveer 2,5 ms, dus het aantal nieuwe aanmeldingen wordt beperkt tot ongeveer 200 per datacore per seconde (gemeten 308 tot 342 op twee cores). Overbelasting op die machine is geen stabiel instrument (20k tot 440k afgeleverd per seconde tussen runs) en <strong>we trekken er geen conclusie uit</strong>. Een vergelijking van het gewone pad met Mosquitto op de VPS is nog niet gedaan. Tests: smoke-groep <code>plaintext</code> 7 van 7 op aarch64 HVF en op x86_64 TCG, nieuw fuzzdoel <code>plain_input</code> (2,3 miljoen runs zonder bevindingen).</p>
<h2 id="fouten-die-het-gebruik-live-aan-het-licht-bracht">Fouten die het gebruik live aan het licht bracht</h2>
<p>Twee dingen werden niet door de tests gevonden, maar door de eerste inschakeling op de echte site:</p>
<ul>
<li>De pagina zocht de releasebestanden <strong>één map te hoog</strong>, waardoor elke controle van de handtekening met 404 antwoordde (reparatie 13af301). De test gebruikte een proefspiegel op een ander pad.</li>
<li><strong>Het toetsenbord van de emulator bereikte de gast nooit</strong>: de oorzaak was niet een slechte vertaling van toetsen, maar dat het dialoogvenster helemaal niet naar toetsen luisterde (reparatie in ADR-232, met een opstartinvoer zonder toetsen).</li>
</ul>
<h2 id="lessen">Lessen</h2>
<ul>
<li><strong>Zoek de oorzaak, geef de omgeving niet de schuld.</strong> De meeste zwakheden van de enkele stroom en alle verbroken verbindingen waren de onze; de hypervisor is echt, maar moest worden gemeten, niet verondersteld.</li>
<li><strong>Een vergelijking met een ander product moet ook de verliezen omvatten.</strong> Een tabel waarin alleen onze overwinningen staan, zou geen vergelijking zijn.</li>
<li><strong>Een meetinstrument heeft grenzen.</strong> Een gedeelde generator met 60 tot 70 % gestolen tijd verandert wat de cijfers betekenen; daarom schrijven we "ondergrens" en beweren we geen verschillen onder 20 %.</li>
<li><strong>Isolatie is geen bewijs.</strong> SQLite in EL0 verkleint wat een fout erin kan doen, niet wat het kan liegen.</li>
<li><strong>Een echte installatie vindt wat tests niet zoeken</strong>: een pad dat verkeerd in elkaar is gezet en toetsen waar niemand naar luistert.</li>
</ul>
<h2 id="wat-nog-niet-bewezen-is">Wat nog niet bewezen is</h2>
<ul>
<li>De ondertekeningssleutel van de releases is voorlopig een proefsleutel; v0.2.0 en v0.2.1 zijn pre-releases, en de images erbij zijn ontwikkelimages met proefcertificaten (behalve de sjabloon, die zonder configuratie is).</li>
<li>13 vertalingen van de builderpagina (en van dit artikel) zijn door een AI geschreven en gecontroleerd met tests en steekproeven, niet door moedertaalsprekers.</li>
<li>De vergelijking met Mosquitto is één sessie, één pad en één gedeelde generator, op een gedeelde VPS; verschillen onder 20 % zijn geen beweringen.</li>
<li>Het gewone pad is niet gemeten op de VPS en niet tegen Mosquitto.</li>
<li>Lighthouse op de nieuwe eenvoudige weergave is niet bevestigd (oude weergave: toegankelijkheid 95 of hoger).</li>
<li>Bare metal, de x86_64-prijs van de worker op KVM, Firefox en Safari voor de emulator: niet gemeten.</li>
</ul>
<h2 id="wat-volgt">Wat volgt</h2>
<ul>
<li><strong>D1: openbare demobroker.</strong> De emulator uit de browser kunnen we niet openbaar maken: hij leeft in het tabblad van één bezoeker, is 20 tot 100 keer langzamer dan KVM, heeft geen inkomend netwerk en verdwijnt wanneer het tabblad wordt gesloten; het internet doorsturen naar de browser van een ander zou ook misbruik zijn. Een echte demobroker is mogelijk: urMQTT op een VPS achter een naam op aldo.tech, met een gepubliceerde proef-CA, een strikte ACL en kleine limieten. Dat is voorlopig alleen een idee, geen bouwwerk, en vraagt uitdrukkelijke toestemming voor openbare blootstelling.</li>
<li><strong>Tweede ronde voor de enkele stroom</strong> (branch <code>perf-single-flow-wip</code>): pas wanneer ze op de VPS is gemeten en samengevoegd.</li>
<li><strong>Het gewone pad tegen Mosquitto op de VPS</strong>, aanmelding en intrekking van accounts voor gewone MQTT via een ondertekend beheerkanaal.</li>
<li><strong>Na de release:</strong> bare metal (X12 tot X16), IOMMU en Secure Boot, schijfversleuteling.</li>
</ul>
<p>De code staat op <a href="https://github.com/zeljan-alduk/urMQTT">github.com/zeljan-alduk/urMQTT</a>, en <code>docs/PROGRESS.md</code> blijft de enige bron van waarheid over wat echt werkt, <code>docs/PERFORMANCE.md</code> over wat is gemeten. Pre-alfa, zoals eerder; foutmeldingen en beveiligingsmeldingen volgens de aanwijzingen in <code>SECURITY.md</code> zijn welkom.</p>]]></content:encoded>
      <category>rust</category>
      <category>mqtt</category>
      <category>iot</category>
      <category>unikernel</category>
      <category>open source</category>
    </item>
    <item>
      <title>urMQTT op een echte VPS: ISO, handshake van 0,24 ms, eigen schijf en geharde kernel</title>
      <link>https://aldo.tech/nl/blog/urmqtt-vps-handshake-disk-hardening/</link>
      <guid isPermaLink="true">https://aldo.tech/nl/blog/urmqtt-vps-handshake-disk-hardening/</guid>
      <pubDate>Sat, 31 Oct 2026 08:00:00 +0000</pubDate>
      <description>Wat er na de eerste vier dagen ontwikkeling is gebeurd met de drie beloften uit het eerste artikel over urMQTT (één ISO, een rauwe schijf zonder bestandssysteem, aantoonbaar veilig), met metingen, tabellen en een lijst van wat nog niet af is.</description>
      <content:encoded><![CDATA[<p>In <a href="https://aldo.tech/blog/urmqtt-unikernel-mqtt-broker/">het eerste artikel over urMQTT</a> beschreven we een unikernel die zelf een MQTT-broker is, tot en met mijlpaal M17, en we eindigden met drie beloften onder de kop "Wat volgt": één ISO die op meerdere platformen opstart, een rauwe schijf zonder bestandssysteem en veiligheid die je kunt aantonen in plaats van alleen te beweren. Niets daarvan was toen gebouwd. Dit artikel volgt wat er met elke belofte is gebeurd, in dezelfde volgorde als eerder: wat we hadden gepland, wat we hebben gebouwd, wat ons verraste en wat nog openstaat. Onderweg laten we ook zien hoeveel tijd het heeft gekost, want ook dat hoort bij het verhaal.</p>
<p>Eén opmerking vooraf: het project is nog steeds pre-alfa. De cijfers in dit artikel meten code en tests, geen rijpheid in productie, en de eerste release is bewust beperkt tot VPS'en met KVM. Bare metal komt later.</p>
<h2 id="hoe-het-er-in-de-tijd-uitzag">Hoe het er in de tijd uitzag</h2>
<p>De eerste commit is van 28 oktober 2026 om 23:48 (M0). Op 31 oktober om 23:00 telt de repository 119 commits, verdeeld als 3, 39, 38 en 39 per kalenderdag. Meteen ook hoe dat mogelijk is: het project wordt gebouwd met Claude Code, en de commits dragen <code>Co-Authored-By: Claude</code>-trailers. Vier kalenderdagen geschiedenis betekenen niet dat één persoon in vier dagen 90.000 regels heeft geschreven; ze betekenen dat plan, implementatie, tests en ADR's in korte, gesloten lussen worden geschreven, en dat elke stap eindigt met hetzelfde controleritueel als eerder. De cijfers hieronder zeggen iets over de hoeveelheid code en tests, niet over hoeveel daarvan in de praktijk is beproefd.</p>
<figure><img loading="lazy" alt="Groei van het project over vier dagen met mijlpalen van M0 tot nu: duizenden regels Rust (van 1,6 naar 89,8), het aantal #[test]-functies (van 27 naar 739) en het aantal ADR's (van 13 naar 104), met staafjes voor commits per dag (39, 38 en 39 op 29, 30 en 31 oktober)." src="https://aldo.tech/blog/urmqtt-vps-handshake-disk-hardening/img/growth.svg" /><figcaption>Groei van het project over vier dagen met mijlpalen van M0 tot nu: duizenden regels Rust (van 1,6 naar 89,8), het aantal #[test]-functies (van 27 naar 739) en het aantal ADR's (van 13 naar 104), met staafjes voor commits per dag (39, 38 en 39 op 29, 30 en 31 oktober).</figcaption></figure>
<p>De grafiek toont drie krommen die bijna samen stijgen, en dat is ook de bedoeling: tests en vastgelegde beslissingen volgen de code in plaats van erachteraan te lopen. De helling is het steilst op 29 oktober, toen het hele pad van kernel naar broker ontstond. Daarna komen er langzamer regels bij, maar de tests groeien sneller dan de code (27 tests op 1,6 duizend regels aan het begin, 739 op 89,8 duizend nu).</p>
<table>
<thead>
<tr>
<th>Mijlpaal</th>
<th>Commit</th>
<th>Tijdstip</th>
<th style="text-align: right;">Regels Rust</th>
<th style="text-align: right;">Tests</th>
<th style="text-align: right;">ADR's</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>vandaag</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>De regels zijn alle <code>.rs</code>-bestanden buiten <code>vendor/</code> en <code>fuzz/</code>, de tests zijn <code>#[test]</code>-functies. Wat elk onderdeel heeft toegevoegd, volgens de datum in de git-geschiedenis:</p>
<table>
<thead>
<tr>
<th>Datum</th>
<th>Mijlpalen</th>
<th>Wat er is toegevoegd</th>
</tr>
</thead>
<tbody>
<tr>
<td>29.10.</td>
<td>M0 tot M17</td>
<td>kernel, netwerk, TLS, broker, SQLite, meerdere cores, eigen TCP, hardening, reproduceerbare release</td>
</tr>
<tr>
<td>29.10. 's avonds en 30.10.</td>
<td>X0 tot X10, S1, H2, H3 (X0 begint op 29.10. om 20:54)</td>
<td>port naar x86_64, universele ISO, één schijf en zelfinstallerende ISO, TLS-controles, aanvallende clients</td>
</tr>
<tr>
<td>31.10.</td>
<td>O1 tot O4, T1, PERF, H4, H5, H9, S2 tot S6</td>
<td>inzicht en console via MQTT, live-modus en diagnostiek, SNTP, versnellingen, hardening, verificatie, toeleveringsketen, opslag</td>
</tr>
</tbody>
</table>
<h2 id="belofte-1-een-iso-voor-meerdere-platformen">Belofte 1: één ISO voor meerdere platformen</h2>
<p><strong>Wat we hadden gepland.</strong> Eén bestand dat opstart op aarch64 UEFI, x86_64 UEFI en x86_64 BIOS. <strong>Wat we hebben gebouwd.</strong> De port naar x86_64 (X0 tot X10) en een universele ISO met de Limine-bootloader, één hybride ISO waarmee al die combinaties opstarten; smoke-tests starten hem op vier virtuele machines. Het menu heeft drie items: urMQTT (live-modus, draait vanuit RAM, schrijft niets naar schijf), installatie op de schijf van de machine waarop hij draait (de installatie schrijft GPT en gegevens en zet de machine daarna uit) en Diagnostiek (hardwaredetectie en metingen).</p>
<figure><img loading="lazy" alt="Het bootmenu van de ISO, opgenomen op de VPS: de items &quot;urMQTT&quot;, &quot;Install urMQTT on this machine's disk&quot; en &quot;Diagnostic: hardware detection and benchmarks&quot; met een aftelling tot automatisch opstarten. De opname komt van een test-ISO met een langere aftelling; standaard is 10 seconden." src="https://aldo.tech/blog/urmqtt-vps-handshake-disk-hardening/img/boot-menu.png" /><figcaption>Het bootmenu van de ISO, opgenomen op de VPS: de items &quot;urMQTT&quot;, &quot;Install urMQTT on this machine's disk&quot; en &quot;Diagnostic: hardware detection and benchmarks&quot; met een aftelling tot automatisch opstarten. De opname komt van een test-ISO met een langere aftelling; standaard is 10 seconden.</figcaption></figure>
<p>Het eerste echte doel was een VPS en geen fysieke computer: we hebben besloten dat de eerste release alleen VPS'en (KVM) ondersteunt en dat bare metal daarna komt. We testten op een Contabo-VPS met een AMD EPYC-processor (familie 23, Zen 1), 4 vCPU's en 8 GiB RAM, met een virtio-net-netwerk en een virtio-scsi-schijf. De eerste poging verraste ons: de ISO, gekoppeld als virtuele cd, startte het standaarditem op, de productie-image weigerde vanaf een alleen-lezen medium te draaien en herstartte, en we kregen een eindeloze lus van menu, kernel en reset zonder iets leesbaars. Tegenwoordig laat een fatale fout in de productie-image 60 seconden lang een bericht op de console staan voordat er wordt herstart, en vanaf een optisch medium meldt hij dat hij van een alleen-lezen medium is opgestart en stopt. De live-modus in RAM (O3) heeft dat probleem later voor het gewone geval omzeild.</p>
<p>Het diagnostiekitem is uit die ervaring voortgekomen: een machine die we niet kunnen zien, moeten we kunnen vragen wat erin zit. Het toont CPU, RAM, netwerk, schijven en PCI-apparaten, meet de doorvoer van geheugen, schijf en cryptografie, en schrijft dezelfde metingen als JSON naar de seriële poort. Schrijven naar schijf is alleen toegestaan in niet-toegewezen ruimte en pas nadat <code>WIPE</code> is ingetypt; op de opname hieronder is dat overgeslagen.</p>
<figure><img loading="lazy" alt="Diagnostiekscherm op een Contabo-VPS (AMD EPYC, KVM, 4 vCPU): systeem, geheugen, schijf, PCI-apparaten, doorvoer en latentie van RAM en cryptografische metingen, waaronder 244 us per mTLS-handshake, X25519 48 us, ECDSA-ondertekening 35,7 us en verificatie 107 us." src="https://aldo.tech/blog/urmqtt-vps-handshake-disk-hardening/img/diag-vps.png" /><figcaption>Diagnostiekscherm op een Contabo-VPS (AMD EPYC, KVM, 4 vCPU): systeem, geheugen, schijf, PCI-apparaten, doorvoer en latentie van RAM en cryptografische metingen, waaronder 244 us per mTLS-handshake, X25519 48 us, ECDSA-ondertekening 35,7 us en verificatie 107 us.</figcaption></figure>
<p><strong>Wat openstaat.</strong> Echte hardware. Na de eerste release volgt bare metal (opstarten via USB, NVMe en AHCI, de netwerkkaart van een echte computer), samen met IOMMU en Secure Boot, want zonder IOMMU kan een kwaadwillend of defect apparaat over het hele geheugen schrijven. In deze versie beweren we daarover niets.</p>
<h2 id="handshake-van-12-ms-naar-024-ms">Handshake: van 1,2 ms naar 0,24 ms</h2>
<p>De eerste meting van de diagnostiek op de VPS leverde een vervelend getal op: de mTLS-handshake, alleen de cryptografie aan de serverkant, duurde 1,2 ms. Op aarch64 gebruikten we al hardware-instructies voor AES en SHA, maar x86_64 had niets vergelijkbaars voor publieke sleutels: RustCrypto is overdraagbaar en constant-time, en dus ook traag. Drie wijzigingen brachten dat omlaag.</p>
<ul>
<li>Voor P-256 hebben we de <code>nistz256</code>-assembly uit OpenSSL en ring overgenomen (ADR-130), met tabellen met vaste basis voor het ondertekenen en voor het verifiëren van CA-handtekeningen (ADR-110, ADR-111).</li>
<li>X25519 is herschreven naar vier 64-bits limbs met MULX/ADX-instructies (ADR-131).</li>
<li>Daarna hebben we ook de rest geprofileerd (ADR-132, ADR-200).</li>
</ul>
<figure><img loading="lazy" alt="Handshake op de VPS: 1200 us ervoor, 237 us na assembly en tabellen; een A/B-vergelijking van memcpy in dezelfde run, 278,8 us met de oude en 229,4 us met de nieuwe; en ervoor en erna voor X25519 (146 naar 48 us), ECDSA-ondertekening (673 naar 34 us) en verificatie (309 naar 90 us)." src="https://aldo.tech/blog/urmqtt-vps-handshake-disk-hardening/img/handshake.svg" /><figcaption>Handshake op de VPS: 1200 us ervoor, 237 us na assembly en tabellen; een A/B-vergelijking van memcpy in dezelfde run, 278,8 us met de oude en 229,4 us met de nieuwe; en ervoor en erna voor X25519 (146 naar 48 us), ECDSA-ondertekening (673 naar 34 us) en verificatie (309 naar 90 us).</figcaption></figure>
<table>
<thead>
<tr>
<th>Meting op de VPS (server, microseconden)</th>
<th style="text-align: right;">Ervoor</th>
<th style="text-align: right;">Erna</th>
</tr>
</thead>
<tbody>
<tr>
<td>mTLS-handshake, alle cryptografie</td>
<td style="text-align: right;">1200</td>
<td style="text-align: right;">circa 240 (237 in de meting van de grafiek, 244 op de opname)</td>
</tr>
<tr>
<td>X25519</td>
<td style="text-align: right;">146</td>
<td style="text-align: right;">48</td>
</tr>
<tr>
<td>ECDSA-ondertekening</td>
<td style="text-align: right;">673</td>
<td style="text-align: right;">34</td>
</tr>
<tr>
<td>ECDSA-verificatie</td>
<td style="text-align: right;">309</td>
<td style="text-align: right;">90 (107 op de opname)</td>
</tr>
<tr>
<td>handshake, oude memcpy, dezelfde boot</td>
<td style="text-align: right;">278,8</td>
<td style="text-align: right;">229,4 (-17,7%)</td>
</tr>
</tbody>
</table>
<p>De cijfers verschillen per run ongeveer 10%, dus hebben we een bereik genoteerd in plaats van één waarde. In totaal is dat ongeveer vijf keer minder CPU-tijd voor de handshake.</p>
<p>Wat we over memcpy hebben geleerd. Toen de cryptografie was gedaald, liet het profiel iets zien wat we niet verwachtten: de <code>mem*</code>-functies (memcpy, memmove, memset) kostten 7% van de handshake, 13 tot 39% van het pad van een TLS-record (39% voor 64 B, 13% voor 4 KiB) en 17 tot 34% van de verdeling van een PUBLISH. We hebben ze vervangen door SSE2-assembly voor x86_64 (ADR-200), met differentiële tests tegen de referentie-implementatie voor alle groottes, uitlijningen en overlappingen. Per aanroep is dat 55 tot 75 cycli tegen 7 tot 12 voor 1 tot 64 bytes.</p>
<figure><img loading="lazy" alt="Links: aandeel van de tijd van de server in mem*-functies vóór de herschrijving (handshake 7%, TLS-record 64 B 39%, 256 B 37%, 1 KiB 24%, verdeling 17 tot 34%). Rechts: hoeveel sneller elk pad erna is (handshake 17,7%, 64 B 26,4%, 256 B 19,6%, 1 KiB 16,1%, 4 KiB 6,6%, verdeling 7 tot 34%)." src="https://aldo.tech/blog/urmqtt-vps-handshake-disk-hardening/img/hotpaths.svg" /><figcaption>Links: aandeel van de tijd van de server in mem*-functies vóór de herschrijving (handshake 7%, TLS-record 64 B 39%, 256 B 37%, 1 KiB 24%, verdeling 17 tot 34%). Rechts: hoeveel sneller elk pad erna is (handshake 17,7%, 64 B 26,4%, 256 B 19,6%, 1 KiB 16,1%, 4 KiB 6,6%, verdeling 7 tot 34%).</figcaption></figure>
<table>
<thead>
<tr>
<th>Pad</th>
<th style="text-align: right;">Aandeel mem* ervoor</th>
<th style="text-align: right;">Sneller erna</th>
</tr>
</thead>
<tbody>
<tr>
<td>handshake</td>
<td style="text-align: right;">7%</td>
<td style="text-align: right;">17,7%</td>
</tr>
<tr>
<td>TLS-record 64 B</td>
<td style="text-align: right;">39%</td>
<td style="text-align: right;">26,4%</td>
</tr>
<tr>
<td>TLS-record 256 B</td>
<td style="text-align: right;">37%</td>
<td style="text-align: right;">19,6%</td>
</tr>
<tr>
<td>TLS-record 1 KiB</td>
<td style="text-align: right;">24%</td>
<td style="text-align: right;">16,1%</td>
</tr>
<tr>
<td>TLS-record 4 KiB</td>
<td style="text-align: right;">13%</td>
<td style="text-align: right;">6,6%</td>
</tr>
<tr>
<td>verdeling van berichten</td>
<td style="text-align: right;">17 tot 34%</td>
<td style="text-align: right;">7 tot 34%</td>
</tr>
</tbody>
</table>
<p>De grafiek lezen: hoe kleiner het record, hoe groter het deel van de tijd dat naar kopiëren gaat, en dus hoe groter de winst. Voor kleine berichten, het grootste deel van het MQTT-verkeer, is dat waar het om gaat.</p>
<p>Wat dit niet betekent. Op native x86_64 hebben we alleen OpenSSL 3.5.5 gemeten als referentie voor de assembly (ADR-130), niet als productvergelijking. De vergelijking van Linux plus Mosquitto op dezelfde VPS (taak X11) hebben we nog niet gedaan. Daarom beweren we niet dat urMQTT sneller is dan Linux: dat is niet gemeten. De cijfers hierboven vergelijken urMQTT met zichzelf.</p>
<p>Overbelasting was het tweede resultaat. Op de VPS, vanaf één Mac (RTT 35 ms), leverden 128 clients 618 handshakes per seconde op, 256 clients 393 en 512 clients 67, met duizenden geweigerde verbindingen. Dat was geen plateau maar een instorting. De oorzaken hebben we in de code gevonden (twee zijn bevestigd in QEMU/HVF, op de VPS konden ze niet worden nagebootst), meerdere tegelijk: een geweigerde verbinding stuurde de client via het volgende segment met een geldige SYN-cookie weer naar binnen, het weigeren met een RST zei de client meteen opnieuw te proberen, en de ontvangst was O(wachtrij). De fix (ADR-080 tot ADR-082) laat een SYN vallen die toch niet zou worden geaccepteerd, onthoudt geweigerde stromen, houdt de ontvangst in O(1) en telt handshakes per core. Mogelijk (hypothese, niet gecontroleerd) lag een deel van de oorzaak ook bij de client: de belastingstool sloot als eerste af, waardoor één Mac met 16.384 efemere poorten en een TIME_WAIT van 30 seconden beperkt was tot ongeveer 550 verbindingen per seconde. Daarom hebben we het echte plafond van de server nog niet gemeten.</p>
<h2 id="belofte-2-een-rauwe-schijf-maar-zorgvuldig">Belofte 2: een rauwe schijf, maar zorgvuldig</h2>
<p><strong>Wat we hadden gepland.</strong> Onze eigen indeling zonder bestandssysteem behouden en alles toevoegen wat een bestandssysteem normaal biedt: uitlijning, groei, discard, back-ups en controles. <strong>Waarom geen bestandssysteem.</strong> Daarover hebben we gediscussieerd en het besluit is bevestigd: urMQTT schrijft één database, één telemetriejournaal en zijn eigen metadata, heeft geen mappen, rechten of bestandsnamen nodig, en elke extra laag zou meer code in het vertrouwde deel zijn en nog een manier waarop een schrijfactie stilletjes verdwijnt. De schijf is GPT met een ESP-partitie en één urMQTT-gegevenspartitie, met daarop onze eigen indeling.</p>
<p><strong>Wat we hebben gebouwd (S2 tot S6).</strong></p>
<ul>
<li><strong>Indeling v2 (S2).</strong> Schijftopologie, uitlijning op 1 MiB of de optimale I/O-grootte, hele fysieke blokken, een A/B-paar superblokken met een generatie (ADR-140). Indeling v1 wordt door de kernel geweigerd met een duidelijke melding, en gemigreerd door de offline tool <code>urmqtt-disk migrate</code> (ADR-141): standaard is dat een proefrun, er wordt eerst gekopieerd en pas daarna omgeschakeld, met rollback en foutinjectie bij elke schrijfactie.</li>
<li><strong>Groei (S3).</strong> Bij het opstarten kan de schijf groeien (nooit krimpen), en op virtio-blk ook tijdens bedrijf, via de interrupt voor configuratiewijziging (ADR-142, ADR-143).</li>
<li><strong>Discard en write-zeroes (S4).</strong> Thin-provisioned schijven geven ruimte terug aan de host. In een test daalde het qcow2-bestand op de host van 62 naar 16 MiB nadat telemetriesegmenten waren verlopen, en het herstel vertrouwt nooit op inhoud die is weggegooid.</li>
<li><strong>Back-up van de database op schijf (S5).</strong> Een A/B-kopie met de online backup-API van SQLite in stappen van 16 pagina's: de langste stap is 4,6 ms tegen 445 ms wanneer in één aanroep wordt gekopieerd. Commits tijdens het kopiëren hadden een maximum van 2,4 ms tegen 2,2 ms zonder kopie. Als <code>quick_check</code> bij het opstarten faalt, wordt de database uit de kopie hersteld.</li>
<li><strong>Scrubben en begrensd opstarten (S6).</strong> De scrubber leest de schijf met een begrensd I/O-budget en meldt beschadigde records, en het opstarten leest alleen het nieuwste segment van elke partitie, zodat de opstarttijd niet meegroeit met de grootte van het telemetriegebied. Daarmee hebben we een probleem opgelost dat we vooraf hadden uitgerekend: op een grote schijf zou volledig herstel minuten tot tientallen minuten duren.</li>
</ul>
<p>De hele groep smoke-tests voor opslag slaagt op beide architecturen: 79 geslaagd, 0 mislukt, op aarch64 onder HVF en op x86_64 onder TCG. We hebben ook gemeten dat S4 de database niet heeft vertraagd: 53,3 duizend inserts per seconde in batches ervoor en erna, zonder meetbaar verschil.</p>
<p><strong>Wat ons verraste.</strong> Hoeveel van een "eenvoudige" indeling uit randgevallen bestaat: virtio-scsi en virtio-blk melden de topologie op verschillende manieren, en een onvolledige schrijfactie midden in een migratie moet altijd minstens één geldige versie achterlaten. Daarom is elke schrijfactie op het pad van migratie en groei door foutinjectie gegaan.</p>
<p><strong>Wat openstaat.</strong> SQLite-pagina's hebben geen controlesom, dus een gewijzigde bit binnen een waarde in een rij ontdekt de database niet; telemetrierecords en de inhoud van kopieën hebben een CRC en worden wel ontdekt. Schijfversleuteling (S7) is niet af.</p>
<h2 id="belofte-3-aantoonbaar-veilig-voor-zover-we-het-redden">Belofte 3: aantoonbaar veilig, voor zover we het redden</h2>
<p><strong>Hardening (H4).</strong> De allocator wist vrijgegeven geheugen en controleert het bij hergebruik, detecteert dubbel vrijgeven, bindt de pointers van de vrije lijst met XOR en nult sleutels bij het vrijgeven (ADR-170). Stackbescherming (<code>-Zstack-protector=strong</code>, 1315 functies, en SQLite met <code>-fstack-protector-strong</code>, ADR-171). Foutrapporten in productie bevatten geen registers of adressen (ADR-172). KASLR op beide architecturen: de kernel is PIE en Limine verschuift hem met ongeveer 19 bits, en heap, stacks en het MMIO-venster worden willekeurig geplaatst (ADR-173). Op aarch64 markeert MTE de heap (ADR-174), maar alleen onder TCG: HVF en x86 hebben het niet. kCFI hebben we beoordeeld en niet overgenomen omdat de compiler op de vastgezette nightly crasht met een interne fout; IOMMU is uitgesteld samen met bare metal (ADR-175, ADR-176).</p>
<p><strong>De prijs is gemeten, niet aangenomen.</strong> Op het handshake-pad (<code>dp-bench</code>, aarch64 HVF, volledige TLS 1.3 + mTLS + CONNECT-handshakes):</p>
<table>
<thead>
<tr>
<th>Stap</th>
<th style="text-align: right;">1 core</th>
<th style="text-align: right;">4 cores</th>
<th>Wijziging</th>
</tr>
</thead>
<tbody>
<tr>
<td>zonder hardening</td>
<td style="text-align: right;">1941</td>
<td style="text-align: right;">7620</td>
<td></td>
</tr>
<tr>
<td>+ allocator</td>
<td style="text-align: right;">1898</td>
<td style="text-align: right;">7429</td>
<td>-2,2% tot -2,5%</td>
</tr>
<tr>
<td>+ stackbescherming</td>
<td style="text-align: right;">1845</td>
<td style="text-align: right;">7243</td>
<td>samen -4,9% tot -5%</td>
</tr>
<tr>
<td>+ PIE en tags</td>
<td style="text-align: right;">circa 1800</td>
<td style="text-align: right;">circa 6750</td>
<td>op een belaste computer, ruis</td>
</tr>
</tbody>
</table>
<p>In totaal is de prijs op het handshake-pad dus ongeveer 5% voor de onderdelen die schoon zijn gemeten, en met PIE en tags komt het uit op ongeveer 7% op één en ongeveer 11% op vier cores, al is die meting ruisachtig. We vinden dat acceptabel, maar we hebben het niet herhaald op de x86_64-VPS.</p>
<p>Het is de moeite waard om dit te vergelijken met het cijfer uit het eerste artikel (862 handshakes per seconde op één core). Dezelfde tool geeft vandaag, met de hardwareversnelde cryptografie op aarch64 (X0b) en een reeks versnellingen die sindsdien zijn gekomen, 1941 vóór hardening. We hebben de bijdrage van elke wijziging niet geïsoleerd, dus over de oorzaak beweren we niets.</p>
<p><strong>Hoe het is gecontroleerd, en wat niet.</strong> H5 voegde Miri toe op een deel van de host-crates, Kani-bewijzen, sanitizers, een model-fuzzer voor de ACL en dekkingsrapporten. Kani heeft <code>record_len</code> in TLS bewezen, <code>parse</code> in TCP en twee rekenkundige onderdelen van het timerwiel. <strong>Niet bewezen</strong> (time-out): beide MQTT-decoders, beide configuratieparsers en het gedrag van het timerwiel; de CI-taak voor Kani blokkeert daarom nog niet. Miri kan geen assembly uitvoeren. Toeleveringsketen (H9): een CycloneDX-SBOM met 215 componenten, <code>cargo-vet</code> slaagt met 39 volledig beoordeelde en 140 uitzonderingen, en cargo-deny is zo geconfigureerd dat advisories over kwetsbaarheden als CI-fout tellen (die CI-taken zijn tot nu toe alleen lokaal gecontroleerd, niet op GitHub, en de negatieve test is niet uitgevoerd), plus <code>VULNERABILITY-HANDLING.md</code>. De eerlijke stand: van 67 kernel-gelinkte versies van crates van derden zijn er 53 uitzonderingen en geen beoordelingen. Vet bewijst dat de verzameling afhankelijkheden niet zonder zichtbaar verschil kan veranderen, niet dat de TCB is nagekeken. Een eigen beoordeling van geen enkele crate hebben we geschreven.</p>
<p>SNTP (T1) is in dat opzicht een klein veiligheidsverhaal: het is optioneel, alleen naar privé-IPv4-servers, met een maximale sprong en de voorwaarde dat de bronnen het eens zijn (mediaan, meerderheid binnen één seconde). Een klok die iedereen op het pad kan verzetten, is geen klok die we mogen vertrouwen bij de geldigheid van certificaten.</p>
<h2 id="op-de-mac-aarch64-native-en-x86_64-via-emulatie">Op de Mac: aarch64 native en x86_64 via emulatie</h2>
<p>We ontwikkelen op een Mac, en die heeft twee manieren om dezelfde code uit te voeren, die erg verschillend zijn.</p>
<table>
<thead>
<tr>
<th></th>
<th>aarch64 onder HVF</th>
<th>x86_64 onder TCG</th>
</tr>
</thead>
<tbody>
<tr>
<td>Wat het is</td>
<td>native uitvoering, snelheid dicht bij een echte processor</td>
<td>QEMU emuleert instructies</td>
</tr>
<tr>
<td>Handshake in de gast</td>
<td>circa 0,58 ms (voor het eerst gemeten rond M13); <code>dp-bench</code> vandaag 1941/s per core</td>
<td>circa 3,3 ms</td>
</tr>
<tr>
<td>TLS-smoke-scenario's</td>
<td>snel</td>
<td>circa 15 minuten per scenario</td>
</tr>
<tr>
<td>MTE</td>
<td>nee (HVF heeft het niet)</td>
<td>x86 heeft het niet; heap-tagging werkt alleen voor een aarch64-gast onder TCG</td>
</tr>
<tr>
<td>Waarvoor</td>
<td>echte metingen</td>
<td>juistheid: het x86-pad wordt gecontroleerd, de cijfers zijn geen prestaties</td>
</tr>
</tbody>
</table>
<p>De regel is eenvoudig: cijfers onder TCG zijn emulatie en die citeren we niet als prestaties. x86-prestaties meten we alleen op een echte VPS. Daarom komen de cryptografiecijfers hierboven van de Contabo-VPS en niet van de Mac.</p>
<h2 id="lessen">Lessen</h2>
<ul>
<li><strong>Meet op een echte machine voordat je optimaliseert.</strong> Onze eerste aanpak was optimaliseren wat we traag dachten te zijn; pas de diagnostiek op de VPS liet 1,2 ms zien en waar de tijd precies heen gaat.</li>
<li><strong>Overbelasting is een aparte eigenschap.</strong> Een systeem dat onder belasting snel is, kan erboven instorten; weigeren moet goedkoper zijn dan accepteren.</li>
<li><strong>Het profiel verrast.</strong> De grootste afzonderlijke winst voor kleine berichten zat niet in de cryptografie maar in het kopiëren van geheugen.</li>
<li><strong>Een schijf zonder bestandssysteem vraagt meer werk, niet minder.</strong> Wat een bestandssysteem normaal biedt (uitlijning, groei, discard, kopieën, controles) moeten we zelf schrijven en controleren, elke stap met foutinjectie.</li>
<li><strong>Eerlijkheid in cijfers.</strong> Geheugenmetingen op een gedeelde VPS variëren tot 10 keer tussen runs en zeggen niets over de code, dus noemen we ze niet. De cryptocijfers zijn stabiel tot ongeveer 10%.</li>
<li><strong>Ontwikkelsnelheid is geen rijpheid.</strong> Tests en ADR's groeien mee met de code, maar vier dagen zijn geen echte productie.</li>
</ul>
<h2 id="wat-volgt">Wat volgt</h2>
<ul>
<li><strong>H6:</strong> SQLite geïsoleerd op EL0-niveau, zodat een fout in de database de kernel niet openzet.</li>
<li><strong>H7:</strong> de rest van de tijdveiligheid (NTS en het afhandelen van sprongen); SNTP is slechts een eerste stap.</li>
<li><strong>X11:</strong> vergelijking met Linux en Mosquitto op dezelfde VPS, pas dan is het zinvol om over relatieve snelheid te praten.</li>
<li><strong>Release:</strong> de eerste openbare release voor VPS (KVM), met ondertekende artefacten en instructies.</li>
<li><strong>Na de release:</strong> bare metal (X12 tot X16), IOMMU en Secure Boot, en schijfversleuteling.</li>
</ul>
<p>De code staat op <a href="https://github.com/zeljan-alduk/urMQTT">github.com/zeljan-alduk/urMQTT</a>, en <code>docs/PROGRESS.md</code> blijft de enige bron van waarheid over wat er werkelijk werkt. Pre-alfa, net als eerder; foutmeldingen en beveiligingsmeldingen volgens de instructies in <code>SECURITY.md</code> zijn welkom.</p>]]></content:encoded>
      <category>rust</category>
      <category>mqtt</category>
      <category>iot</category>
      <category>unikernel</category>
      <category>open source</category>
    </item>
    <item>
      <title>urMQTT: we bouwen een MQTT-broker die tegelijk het besturingssysteem is</title>
      <link>https://aldo.tech/nl/blog/urmqtt-unikernel-mqtt-broker/</link>
      <guid isPermaLink="true">https://aldo.tech/nl/blog/urmqtt-unikernel-mqtt-broker/</guid>
      <pubDate>Thu, 29 Oct 2026 08:00:00 +0000</pubDate>
      <description>Ontwikkelaantekeningen over urMQTT, een in Rust geschreven unikernel dat zelf een MQTT-broker is: één ELF met eigen TCP/IP, TLS 1.3, broker en SQLite, opgestart vanaf één ISO.</description>
      <content:encoded><![CDATA[<p>Apparaten in voertuigen, op parkeerterreinen en in schakelkasten hebben één eigenschap gemeen: ze sturen kleine berichten, zelden, maar er zijn er veel en ze zijn voortdurend verbonden. Al jaren verbinden we ze met MQTT-brokers, en al jaren houdt ons dezelfde vraag bezig. Waarom hebben we voor iets zo eenvoudigs een compleet Linux nodig, OpenSSL, de runtime van een programmeertaal, SSH, een pakketbeheerder en pas dan de broker? Elk van die lagen brengt eigen kwetsbaarheden mee, verbruikt eigen geheugen en voegt weer iets toe wat kapot kan gaan, terwijl een vloot apparaten van een server maar drie dingen nodig heeft: veel vrijwel inactieve verbindingen vasthouden, elk apparaat betrouwbaar authenticeren en berichten zonder verlies doorgeven.</p>
<p>Zo is <a href="https://github.com/zeljan-alduk/urMQTT">urMQTT</a> ontstaan: een unikernel die zelf <em>de</em> MQTT-broker <em>is</em>. Kernel, TCP/IP, TLS 1.3, MQTT en een ingebouwde database in één Rust-binary die vanaf één ISO wordt gestart. Geen processen, geen systeemaanroepen, geen shell. Het eerste platform waarop het wordt gebouwd en getest is aarch64, en dezelfde code gaat richting x86_64, via UEFI en klassieke BIOS, en richting echte hardware: het doel is één ISO die op al die platformen opstart. Het project is open source onder de licentie MIT of Apache 2.0, en in dit artikel leggen we vast waar we staan, hoe het van binnen in elkaar zit en wat we onderweg hebben geleerd.</p>
<h2 id="wat-unikernel-precies-betekent">Wat "unikernel" precies betekent</h2>
<p>Een gebruikelijke broker is een programma dat op een besturingssysteem draait. urMQTT heeft geen besturingssysteem onder zich: het start zichzelf op via UEFI-firmware en de Limine-bootloader, zet eigen paginatabellen op, programmeert de interruptcontroller en de timer, vindt virtio-apparaten via PCIe, krijgt een adres via DHCP en luistert op één poort. Alles wat normaal door de kernel, de stuurprogramma's, de netwerkstack en de applicatie wordt gedaan, is hier één ELF dat op EL1-niveau draait.</p>
<p>Een vergelijking met een typische installatie zegt genoeg:</p>
<table>
<thead>
<tr>
<th></th>
<th>Typische broker op Linux</th>
<th>urMQTT</th>
</tr>
</thead>
<tbody>
<tr>
<td>Blootgestelde diensten</td>
<td>SSH, broker, vaak ook een poort voor metrics of beheer</td>
<td><strong>alleen TCP 8883</strong> (MQTT over TLS 1.3 met mTLS)</td>
</tr>
<tr>
<td>Code in de image</td>
<td>kernel, userland, libc, OpenSSL, runtime</td>
<td><strong>één Rust-ELF</strong> (plus gevendord SQLite)</td>
</tr>
<tr>
<td>Beheer op afstand</td>
<td>shell, configuratiebestanden, pakketupdates</td>
<td><strong>niets.</strong> Een nieuwe versie is een nieuwe ISO</td>
</tr>
<tr>
<td>Geheugen per inactieve verbinding</td>
<td>kilobytes (socketbuffers, TLS-objecten, heap)</td>
<td><strong>doel ≤ 512 B</strong></td>
</tr>
<tr>
<td>Multicoremodel</td>
<td>threads en locks of één eventloop</td>
<td><strong>één thread per core, geen gedeelde toestand, run-to-completion</strong></td>
</tr>
<tr>
<td>Code-integriteit</td>
<td>mappings die zowel beschrijfbaar als uitvoerbaar zijn, zijn mogelijk</td>
<td><strong>W^X, vergrendelde paginatabellen, PAC/BTI</strong></td>
</tr>
</tbody>
</table>
<p>De prioriteiten zijn, in deze volgorde: veiligheid, schaalbaarheid, multicoreprestaties, kleine footprint en een onveranderlijke installatie. Wanneer twee doelen botsen, wint het doel dat hoger op deze lijst staat.</p>
<h2 id="waar-we-nu-staan">Waar we nu staan</h2>
<p>Het project bevindt zich in de pre-alfafase, maar zeventien ontwikkelmijlpalen, van M0 tot en met M17, zijn afgerond. Vandaag bedient urMQTT MQTT 3.1.1 over TLS 1.3 met verplichte clientcertificaten en bewaart het zijn toestand in SQLite op een rauwe schijf. In het kort wat al werkt:</p>
<ul>
<li><strong>Kernel:</strong> eigen exceptievectoren met gedecodeerde rapporten, eigen paginatabellen met afgedwongen W^X, guard pages, PAC en BTI waar de processor ze heeft, GICv3 en GICv2, generieke timer, SMP tot 64 cores.</li>
<li><strong>Hardwaredetectie</strong> uit DTB of ACPI naar één platformbeschrijving, met een identiek resultaat uit beide bronnen.</li>
<li><strong>Stuurprogramma's:</strong> PCIe ECAM, MSI-X, virtio-net, virtio-blk, virtio-scsi en virtio-rng, plus een CSPRNG die met hardware-entropie wordt geïnitialiseerd.</li>
<li><strong>Netwerk:</strong> eigen DHCPv4, ARP, ICMP en strikt IPv4, daarnaast een eigen TCP alleen voor de serverkant: SYN-cookies, hiërarchisch timerwiel, NewReno, samenvoegen van ACK's, geen TIME_WAIT-toestand.</li>
<li><strong>TLS:</strong> alleen TLS 1.3, verplichte mTLS, apparaatidentiteit uit de CN of de SAN-URI; de handshake wordt door rustls afgehandeld, daarna volgt een eigen recordlaag met AES-128-GCM en ChaCha20-Poly1305. Sessiehervatting met PSK-tickets, een intrekkingslijst, toelatingscontrole al tijdens de handshake.</li>
<li><strong>MQTT:</strong> codec zonder kopiëren, gefuzzd en getest met echte mosquitto-clients; QoS 0 en 1, wildcards, retained berichten, last will (LWT), persistente sessies, ACL met beperking tot <code>devices/{id}/#</code>, snelheidslimieten per apparaat.</li>
<li><strong>Opslag:</strong> SQLite in de kernel via een VFS bovenop een rauwe schijf, in WAL-modus, met een integriteitscontrole bij het opstarten; apparaatregister, intrekkingen, retained berichten, persistente sessies met berichtenwachtrijen voor offline apparaten; per core een afzonderlijk telemetriejournaal met een CRC per record en herstel na een crash.</li>
<li><strong>Bedrijf en bewaking:</strong> een watchdogtimer voor handlers, een gezondheidscontrole, begrensde logging, syslog en statsd via UDP, een toelatingslaag die stormen van heropgebouwde verbindingen overleeft.</li>
<li><strong>Oplevering:</strong> bitgewijs identieke herbouwen, gecontroleerd in CI, ISO en rauw GPT-schijfimage, minisign-handtekeningen, handleidingen voor KVM, Proxmox en Hetzner.</li>
</ul>
<p>Elke stap is gemarkeerd met de mijlpaal die hem heeft opgeleverd, en het bestand <code>docs/PROGRESS.md</code> in de repository is de enige bron van waarheid over wat er werkelijk werkt.</p>
<h2 id="de-architectuur-in-drie-plaatjes">De architectuur in drie plaatjes</h2>
<p>Het volledige ontwerp staat beschreven in <code>docs/ARCHITECTURE.md</code>, en de redenen achter elke belangrijke beslissing zijn vastgelegd als ADR's in <code>docs/DECISIONS.md</code>. Hier volgt een overzicht.</p>
<h3 id="lagen">Lagen</h3>
<figure><img loading="lazy" alt="De lagen van urMQTT: van de broker via de MQTT-codec, TLS, TCP en het netwerk tot de virtio-stuurprogramma's en de architectuurlaag, alles in één ELF op EL1-niveau." src="https://aldo.tech/blog/urmqtt/img/layers.svg" /><figcaption>De lagen van urMQTT: van de broker via de MQTT-codec, TLS, TCP en het netwerk tot de virtio-stuurprogramma's en de architectuurlaag, alles in één ELF op EL1-niveau.</figcaption></figure>
<p>Van boven naar beneden: de broker (sessies, onderwerpenboom, QoS, retained berichten, last wills, ACL, limieten), de MQTT-codec, TLS, TCP, de netwerklaag en dan de stuurprogramma's. Naast dat hoofdpad staat de opslag (SQLite en het telemetriejournaal), en onder alles het geheugenbeheer, de platformdetectie en de configuratie die tijdens de build in constanten is omgezet, samen met het ingebouwde CA-certificaat, het servercertificaat en de sleutel. De hypervisor met de virtio-apparaten is het enige wat buiten de image valt.</p>
<h3 id="een-thread-per-core-geen-gedeelde-toestand">Eén thread per core, geen gedeelde toestand</h3>
<figure><img loading="lazy" alt="Het uitvoeringsmodel: elke datacore heeft een eigen lus en een eigen deel van de broker, en de hoogste core draait alleen SQLite. De cores communiceren uitsluitend via SPSC-ringen." src="https://aldo.tech/blog/urmqtt/img/cores.svg" /><figcaption>Het uitvoeringsmodel: elke datacore heeft een eigen lus en een eigen deel van de broker, en de hoogste core draait alleen SQLite. De cores communiceren uitsluitend via SPSC-ringen.</figcaption></figure>
<p>Elke datacore draait een eigen lus met een eigen TCP-stack, eigen TLS-toestanden, een eigen deel van de broker (sessies en onderwerpenboom), een heapcache en een telemetriepartitie. De cores delen geen enkele veranderlijke toestand, alleen begrensde lock-vrije ringen, één voor elk geordend paar cores. De hoogste core draait SQLite en niets anders.</p>
<p>De netwerkkaart heeft op dit moment één paar wachtrijen, dat door core 0 wordt beheerd. Die stuurt elke MQTT-stroom op basis van de Toeplitz-hash van zijn viertal naar een van de werkcores en verstuurt wat de werkcores teruggeven. Een sessie leeft op haar thuiscore, bepaald door de hash van de client-ID, zodat overname, last wills en persistentie nooit cores overschrijden. Een verbinding die op een andere core terechtkomt, houdt daar haar TCP en TLS en stuurt gewone MQTT door naar de thuiscore, met flowcontrol op basis van credits. Elk doorgestuurd bericht wordt bovendien verspreid naar de overige delen van de broker, die elk een replica van de retained berichten bijhouden.</p>
<p>Drie regels houden dat model bijeen. <strong>Run-to-completion:</strong> een ontvangen pakket wordt volledig op één core afgehandeld, van TCP via TLS-ontsleuteling ter plekke, MQTT-parsing en routering tot het versleutelde antwoord, zonder taakwachtrijen en zonder kopieën naar applicatiebuffers. <strong>Adaptieve polling:</strong> onder belasting worden de ontvangstwachtrijen actief gepold, en in rust keert de core terug naar interrupts en <code>WFI</code>; interrupthandlers bevestigen alleen de interrupt en markeren werk. <strong>Watchdogtimer:</strong> een tik van 10 ms per core vangt handlers op die hun budget overschrijden.</p>
<h3 id="het-leven-van-een-publish">Het leven van één PUBLISH</h3>
<figure><img loading="lazy" alt="Het pad van één PUBLISH-bericht met QoS 1: van het TLS-record op de netwerkkaart via ontsleuteling, ACL-controle en routering naar de PUBACK die in één segment met de TCP-ACK wordt samengevoegd." src="https://aldo.tech/blog/urmqtt/img/publish.svg" /><figcaption>Het pad van één PUBLISH-bericht met QoS 1: van het TLS-record op de netwerkkaart via ontsleuteling, ACL-controle en routering naar de PUBACK die in één segment met de TCP-ACK wordt samengevoegd.</figcaption></figure>
<p>Het apparaat verstuurt een TLS-record. De netwerkkaart geeft het via een descriptor zonder kopiëren aan de lus van de core. De lus controleert TCP, ontsleutelt TLS ter plekke en parset MQTT uit de geleende buffer, en controleert daarna in het geheugen de ACL, de snelheidslimiet en het pad door de onderwerpenboom. Als een andere core in dat onderwerp geïnteresseerd is, krijgt die een routeringsbericht met een verwijzing naar de payload en de onderwerp-ID; de database krijgt een telemetrierecord of een update van een retained bericht, gebundeld. Het PUBACK-antwoord wordt rechtstreeks in de uitgaande buffer versleuteld en samengevoegd met de TCP-ACK, zodat er één segment naar het apparaat gaat. Voor een andere core kost het bericht één versleuteling per abonnee op het moment van verzenden.</p>
<h2 id="wat-een-verbinding-kost">Wat een verbinding kost</h2>
<p>Het doel is <strong>maximaal 512 bytes per inactieve TLS-verbinding</strong>, met 256 als streven. Zolang een verbinding rust, wordt er niets voor haar in een buffer bewaard: ontvangen gegevens worden rechtstreeks uit het netwerkframe verwerkt, en de buffers voor verzenden, records en MQTT bestaan alleen zolang er iets onafgemaakt of onbevestigd is. De kernel meet dat zelf: exacte heaptellers in gevraagde bytes en in de blokken die ze innemen, elke tien seconden vastgelegd als heap boven het basisniveau per open verbinding.</p>
<table>
<thead>
<tr>
<th>Onderdeel</th>
<th style="text-align: right;">Grootte</th>
<th>Hoe</th>
</tr>
</thead>
<tbody>
<tr>
<td>TCP-controleblok</td>
<td style="text-align: right;">96 B</td>
<td>gepakte vlaggen, 16-bits RTT/RTO, slot in het timerwiel, geen buffers in rust</td>
</tr>
<tr>
<td>Slot in de TCP-tabel</td>
<td style="text-align: right;">4–8 B</td>
<td>alleen de verbindingsindex, tabel hoogstens half vol</td>
</tr>
<tr>
<td>Verbinding en toestand van het TLS 1.3-record</td>
<td style="text-align: right;">152 B</td>
<td>geheim en volgnummer per richting (40 B); sleutel en IV worden per record afgeleid</td>
</tr>
<tr>
<td>MQTT-verbinding</td>
<td style="text-align: right;">48 B</td>
<td>identiteit en last will op de heap alleen zolang ze bestaan</td>
</tr>
<tr>
<td>MQTT-sessie, hete vorm</td>
<td style="text-align: right;">64 B</td>
<td>één abonnement in de rij, geïnterneerde knoop-ID's</td>
</tr>
<tr>
<td>Abonnement <code>devices/&lt;id&gt;/cmd/#</code></td>
<td style="text-align: right;">0 knopen in de boom</td>
<td>relatief aan de sessie-ID opgeslagen, gedeeld door alle apparaten</td>
</tr>
</tbody>
</table>
<p>Gemeten onder HVF met vier virtuele cores, waarvan drie werkcores, waarbij elk apparaat een mTLS-verbinding en één abonnement met QoS 1 vasthoudt, inclusief afronding door de allocator:</p>
<table>
<thead>
<tr>
<th style="text-align: right;">Verbindingen</th>
<th style="text-align: right;">Bytes per verbinding</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>De pool met netwerkframes is vast ongeveer 2 MiB groot, wat neerkomt op circa 200 B per verbinding bij tienduizend verbindingen en circa 2 B bij een miljoen. Verder konden we niet meten: de gebruikersnetwerkmodus van QEMU op macOS accepteerde bij 10.197 geen verbindingen meer, ongeveer op de grens van 10.240 descriptors van het QEMU-proces. Het getal daalt naarmate de vaste delen over meer verbindingen worden verdeeld, en groeit dus niet met hun aantal.</p>
<h2 id="het-beveiligingsmodel">Het beveiligingsmodel</h2>
<p>Veiligheid is de eerste prioriteit en wint alle conflicten met de andere doelen. Het model heeft meerdere lagen:</p>
<ul>
<li><strong>Eén poort.</strong> TCP 8883, alleen TLS 1.3, verplichte clientcertificaten die zijn ondertekend door de in de image ingebouwde CA. De MQTT-client-ID moet gelijk zijn aan de apparaat-ID uit het certificaat.</li>
<li><strong>Geen ingang.</strong> Geen shell, geen console-invoer, geen SSH, geen beheer-API, geen dynamische code, geen via het netwerk bereikbare SQL en geen bestandssysteem.</li>
<li><strong>Geheugenveiligheid.</strong> Rust, met <code>forbid(unsafe_code)</code> in de protocolpakketten; elk <code>unsafe</code>-blok heeft een <code>// SAFETY:</code>-commentaar dat door een lint wordt gecontroleerd, en bij externe invoer treedt er geen panic op: clippy verbiedt in de pakketten op het datapad <code>unwrap</code>, <code>expect</code>, indexering en <code>panic!</code>.</li>
<li><strong>Beperking van exploits.</strong> W^X wordt afgedwongen via de codering van descriptors en het bit <code>SCTLR_EL1.WXN</code> en bij elke start gecontroleerd. De paginatabellen zijn na het opstarten alleen-lezen, zonder beschrijfbare alias. De onderste helft van de adresruimte is uitgesloten. Elke stack heeft guard pages en exceptions draaien op een aparte stack. PAC en BTI worden ingeschakeld wanneer de processor ze ondersteunt, en dat wordt door smoke-tests gecontroleerd.</li>
<li><strong>Weerstand tegen misbruik.</strong> SYN-cookies; de toelatingslaag weigert nieuwe verbindingen, vóór enige cryptografie, wanneer de heap laag is, de database achterloopt, er per core of per bronadres te veel handshakes lopen of de handshake-snelheid te hoog is. Snelheidslimieten per apparaat, begrensde wachtrijen, een TCP-venster dat aan de tegendruk van de broker is gekoppeld, begrensde logging, een watchdogtimer en een gezondheidscontrole.</li>
<li><strong>Onveranderlijkheid.</strong> Configuratie, CA en serversleutel zijn in de image ingebouwd. Iets wijzigen betekent een nieuwe image.</li>
</ul>
<p>Het dreigingsmodel is vastgelegd in <code>docs/SECURITY.md</code>.</p>
<h2 id="opslag-zonder-bestandssysteem">Opslag zonder bestandssysteem</h2>
<p>De gegevens leven op een aparte schijf, virtio-blk of virtio-scsi, waarop geen bestandssysteem staat. De indeling is minimaal en eigen:</p>
<pre><code>LBA 0      [ superblok: magic · indelingsversie · uuid · regiotabel · CRC-32C ]
LBA 8, 16  [ tabel met bestandsgroottes, slots A/B (de nieuwste geldige wint) ]
LBA 64     [ SQLite-hoofddatabase ½ ][ SQLite WAL ⅛ ][ journaal 1 MiB ][ telemetriejournaal: 8 partities … ]
laatste 4 KiB [ ruimte voor de zelftest van het blokapparaat bij het opstarten ]
</code></pre>
<p>SQLite (gevendord 3.54.1, met controle van de hash tijdens de build) is met clang in de kernel gecompileerd, zonder standaardbibliotheek, en gebruikt een vaste <code>memsys5</code>-heap. Zijn VFS koppelt drie vaste bestanden aan schijfregio's. Het draait in WAL-modus met <code>locking_mode=EXCLUSIVE</code>, zodat het geen index in gedeeld geheugen nodig heeft, en met <code>synchronous=FULL</code>; als <code>quick_check</code> bij het opstarten een fout vindt, stopt het systeem veilig. In de database staan het apparaatregister, intrekkingen, retained berichten en persistente sessies met wachtrijen van QoS 1-berichten voor apparaten die niet verbonden zijn.</p>
<p>De core voor de database, die met het hoogste nummer, draait SQLite en niets anders. Het datavlak stuurt haar toestandswijzigingen via een lock-vrije SPSC-ring; zij bundelt ze elke 5 ms of elke 512 operaties in één transactie en stuurt de bevestigingen via een tweede ring terug. Een lege schijf wordt bij de eerste start geformatteerd; al het andere wordt nooit overschreven.</p>
<p>Telemetrie gaat langs een andere weg. Elke geaccepteerde PUBLISH op onderwerpen uit <code>storage.telemetry_topics</code> gaat naar de partitie van het telemetriejournaal van zijn core: een ring van segmenten met een CRC per record, gezouten met het volgnummer van het segment, zodat oude rondes van de ring nooit opnieuw opduiken. De cores geven records via een eigen ring zonder kopiëren aan de database door, die ze elke 5 ms of elke 256 KiB wegschrijft. Bij het opstarten behoudt het herstel elk weggeschreven record, snijdt het onvoltooide einde af en gaat verder in een nieuw segment. Bewaren gebeurt op grootte, via de ring, en op leeftijd, via <code>retention_days</code>. We hebben dit getest met een miljoen records en met <code>kill -9</code> midden in het schrijven.</p>
<h2 id="de-cijfers">De cijfers</h2>
<p>De belastingstool in <code>tools/loadgen</code> doet drie dingen: een storm van TLS 1.3-handshakes met mTLS en CONNECT, langlevende verbindingen met PINGREQ die elke seconde telemetrie publiceren, en reconnect-stormen. <code>make bench</code> op een Apple M4 Pro onder HVF geeft:</p>
<table>
<thead>
<tr>
<th>Werkcores</th>
<th>handshakes/s in de gast</th>
<th>schaling</th>
<th>via het gebruikersnetwerk van QEMU (verbindingen/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>Het getal in de gast, waar client en server binnen dezelfde virtuele machine zitten en elke werkcore voor zichzelf werkt, laat zien dat het datavlak lineair schaalt. Het netwerkgetal wordt begrensd door het enkelvoudig-threadige gebruikersnetwerk van QEMU, niet door de gast; ook dat is vastgelegd als ADR-032.</p>
<p>Reconnect-stormen testen we door alle apparaten tegelijk te laten verbinden, geweigerde pogingen met uitstel te herhalen en ze daarna allemaal zonder DISCONNECT te laten wegvallen. De parameter <code>heap_limit_mb=</code> laat de toelatingslaag in de ontwikkelmodus de heap kleiner inschatten dan hij is, zodat een storm die groter is dan de heap met weinig sockets kan worden uitgeprobeerd. In één zo'n ronde met 3.000 apparaten maakten er 1.368 verbinding in 36 seconden, en 5.619 pogingen werden netjes geweigerd, met een limiet van 8 MiB; na de storm keerde het geheugen terug.</p>
<h2 id="hoe-we-werken">Hoe we werken</h2>
<p>Het project wordt geleid door een geschreven specificatie, <code>PROMPT.md</code>, met een mijlpalenplan, en elke mijlpaal eindigt met hetzelfde ritueel: <code>make check</code> en <code>make smoke</code> groen, één commit met het label <code>M&lt;n&gt;</code>, een aantekening in <code>docs/PROGRESS.md</code> en een ADR voor elke beslissing die niet vanzelfsprekend is. De regels zijn kort en we onderhandelen er niet over: geen panic bij externe invoer, <code>unsafe</code> alleen in hardware- en FFI-pakketten en altijd met commentaar, geen allocaties op het hete pad, commentaar bij elke volgorde van atomaire operaties, elke limiet komt uit de configuratie, en bewuste tijdelijke onderdelen falen luid.</p>
<p>CI doet meer dan wij met de hand zouden halen: fmt, clippy voor de host en voor <code>aarch64-unknown-none</code>, typos, actionlint en shellcheck; tests op Linux en macOS op arm64; een build van de kernel voor de ontwikkel- en productieconfiguratie met de grootte van de ELF in de samenvatting; opstarten onder QEMU met alle smoke-testscenario's en het bewaren van seriële logs; cargo-deny voor advisories, licenties en bronnen; een minuut fuzzing per elk van de elf doelen, en 's nachts tien minuten per doel met een gecachet corpus. Daarnaast CodeQL, OpenSSF Scorecard, Dependabot en publicatie met SLSA-herkomst. Elke actie van derden is vastgezet op een volledige commit-SHA.</p>
<p>Reproduceerbaarheid nemen we letterlijk: <code>make repro</code> bouwt HEAD tweemaal in verse werkbomen en vergelijkt de ELF, de ISO en het rauwe image byte voor byte. Releases zijn ondertekend met minisign en de artefacten kunnen worden gecontroleerd met <code>gh attestation verify</code>. De productie-image bevat de privésleutel van de server, dus de ISO zelf is een geheim; de images bij de releases op GitHub zijn ontwikkelimages, met tijdelijke sleutels, en dienen alleen om te proberen.</p>
<h2 id="wat-we-hebben-geleerd">Wat we hebben geleerd</h2>
<p>Een paar lessen die het plan hebben veranderd:</p>
<ul>
<li><strong>Een eigen TCP bleek toch de moeite waard.</strong> Het prototype draaide op smoltcp en heeft ons tot en met M13 goed gediend. Maar een server die honderdduizenden verbindingen vasthoudt, heeft geen client-TCP nodig, maar SYN-cookies, een timerwiel en een toestand van 96 bytes. In M14 hebben we smoltcp vervangen door een eigen stack alleen voor de serverkant, met tests met gesimuleerd pakketverlies en fuzzing van segmenten.</li>
<li><strong>Meet het geheugen van binnenuit.</strong> Schattingen per verbinding waren nutteloos totdat de kernel zelf de heap ging tellen in allocatorblokken in plaats van in gevraagde bytes. Pas toen zagen we waar die 512 bytes werkelijk blijven en kwamen we uit op 494.</li>
<li><strong>Testgereedschap is deel van het product.</strong> De grootste beperking was niet de kernel, maar de gebruikersnetwerkmodus van QEMU en het aantal efemere poorten op de ontwikkelcomputer. Voor honderdduizend verbindingen of meer heeft u <code>vmnet-shared</code> op macOS nodig, of Linux met KVM, een tap-interface en meerdere clientadressen.</li>
<li><strong>SQLite zonder besturingssysteem is mogelijk</strong>, maar alleen onder strenge voorwaarden: een vaste heap, exclusieve vergrendeling zonder gedeeld geheugen, een VFS die niets van bestanden weet, en een toelatingslaag die de database niet laat achterlopen.</li>
<li><strong>Reconnect-stormen zijn de normale toestand</strong>, geen uitzondering. Wanneer een mobiel netwerk uitvalt en terugkomt, kloppen duizenden apparaten tegelijk aan. Daarom weigert de toelatingslaag een verbinding vóór enige cryptografie, en kost elke geweigerde verbinding vrijwel niets.</li>
</ul>
<h2 id="wat-volgt">Wat volgt</h2>
<p>Het werk na M17 is in drie richtingen gepland, en de volgorde daartussen staat in <code>docs/PLAN.md</code>. Niets hiervan is al gebouwd.</p>
<ul>
<li><strong>Eén ISO voor meerdere platformen.</strong> Hetzelfde bestand <code>urmqtt-&lt;env&gt;-&lt;versie&gt;.iso</code> en één rauw GPT-image die opstarten op aarch64 UEFI, x86_64 UEFI en x86_64 BIOS, in virtuele machines en op echte hardware, te beginnen met één Intel-pc. Nog steeds een appliance met alles erin, reproduceerbaar en ondertekend.</li>
<li><strong>Rauwe schijf, zonder bestandssysteem.</strong> Een formaatspecificatie op byteniveau, de hosttool <code>urmqtt-disk</code> die de parsers met de kernel deelt, uitlijning van regio's op 1 MiB, groei van de schijf met de gegevens, discard voor thin-provisioned schijven, A/B-snapshots van de database, bitcontrole en, naar keuze, versleuteling met AES-256-XTS met hardware-AES.</li>
<li><strong>Aantoonbaar veilig.</strong> Intrekking en een lijst met toegestane apparaten tijdens bedrijf via een ondertekend controlekanaal, zonder nieuwe image; differentiële tests en conformiteitstests van de eigen TLS en TCP; KASLR, stackbescherming, kCFI, MTE en een geharde allocator; SQLite geïsoleerd op EL0-niveau; Secure Boot; SBOM en cargo-vet; en tot slot een pakket voor een externe audit.</li>
</ul>
<p>Platformen die alleen niet-virtio-apparaten aanbieden, zoals AWS ENA of Azure MANA, worden voorlopig niet ondersteund. Raspberry Pi 4 en 5 als KVM-host, Hetzner CAX en Oracle Ampere A1 zijn gedocumenteerd en in code ondersteund, maar nog niet getest op een echte dienst.</p>
<h2 id="probeer-het-uit">Probeer het uit</h2>
<p>De repository staat op <a href="https://github.com/zeljan-alduk/urMQTT">github.com/zeljan-alduk/urMQTT</a>. Met Rust met een vastgezette nightly-toolchain en QEMU zijn <code>make certs</code>, <code>make run</code> en één mosquitto-client genoeg voor de eerste PUBLISH over mTLS op uw eigen computer. De waarschuwing van het begin geldt ook aan het eind: dit is pre-alfa, en alles wat werkt staat in <code>docs/PROGRESS.md</code>. Foutmeldingen, vragen over het ontwerp en beveiligingsmeldingen volgens de instructies in <code>SECURITY.md</code> zijn welkom. Over elke volgende mijlpaal laten we hier van ons horen.</p>]]></content:encoded>
      <category>rust</category>
      <category>mqtt</category>
      <category>iot</category>
      <category>unikernel</category>
      <category>open source</category>
    </item>
    <item>
      <title>We starten een blog: notities uit de praktijk</title>
      <link>https://aldo.tech/nl/blog/we-starten-een-blog/</link>
      <guid isPermaLink="true">https://aldo.tech/nl/blog/we-starten-een-blog/</guid>
      <pubDate>Sat, 17 Oct 2026 08:00:00 +0000</pubDate>
      <description>Waarom een bedrijf dat terminals, apps en elektronica voor het openbaar vervoer bouwt begint te schrijven, en waarover u hier zult lezen.</description>
      <content:encoded><![CDATA[<p>Het meeste van wat we bouwen ziet niemand. De software in de terminal bij de busdeur, de firmware in een apparaat dat nooit een scherm krijgt, de server die 's nachts een dag aan transacties omzet in rapporten. Als alles werkt, werkt het stil. Als het niet werkt, merkt iedereen het.</p>
<p>Op dit blog schrijven we op wat we onderweg hebben geleerd. Geen marketingteksten, maar concrete notities: wat er misging, hoe we het hebben opgelost en wat we vandaag anders zouden doen.</p>
<h2 id="waarover-we-gaan-schrijven">Waarover we gaan schrijven</h2>
<ul>
<li><strong>Tariefinning in het openbaar vervoer.</strong> Contactloze kaarten, SAM-modules, offline werken en alles wat een terminal moet doorstaan in een voertuig dat twaalf uur per dag schudt.</li>
<li><strong>Mobiele apps in het veld.</strong> Android en Flutter op apparaten die geen gewone telefoons zijn: met kaartlezers, printers en accu's die een hele dienst moeten volhouden.</li>
<li><strong>Backend en vlootbewaking.</strong> Hoe je weet dat een apparaat in een voertuig een probleem heeft voordat de chauffeur het meldt.</li>
<li><strong>Hardware.</strong> Van een prototype op de werkbank tot een printplaat die in serieproductie gaat, en alle fouten daartussen.</li>
<li><strong>Lokale AI-modellen.</strong> Waar modellen op eigen hardware zinvol zijn, en waar de cloud en een API gewoon de betere keuze zijn.</li>
</ul>
<h2 id="hoe-we-gaan-schrijven">Hoe we gaan schrijven</h2>
<p>Kort en concreet, met code en cijfers waar we die hebben. We publiceren als we iets te zeggen hebben, niet volgens een schema. De berichten worden in het Kroatisch geschreven en vertaald naar alle talen van deze site; via <a href="https://aldo.tech/nl/blog/feed.xml">RSS</a> blijft u op de hoogte.</p>
<p>Interesseert een onderwerp u, of werkt u aan een vergelijkbaar probleem? Schrijf ons op <a href="mailto:info@aldo.tech">info@aldo.tech</a>.</p>]]></content:encoded>
      <category>intro</category>
    </item>
  </channel>
</rss>
