<?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/de/blog/</link>
    <atom:link href="https://aldo.tech/de/blog/feed.xml" rel="self" type="application/rss+xml"/>
    <description>Notizen aus der Praxis: ÖPNV, Fahrgeldeinzug, Hardware und Software, die unterwegs funktionieren muss.</description>
    <language>de</language>
    <item>
      <title>urMQTT nach dem VPS: SQLite im Benutzermodus, Vergleich mit Mosquitto, Konfiguration als Daten und ein Builder im Browser</title>
      <link>https://aldo.tech/de/blog/urmqtt-config-builder-plaintext-mosquitto/</link>
      <guid isPermaLink="true">https://aldo.tech/de/blog/urmqtt-config-builder-plaintext-mosquitto/</guid>
      <pubDate>Sun, 01 Nov 2026 08:00:00 +0000</pubDate>
      <description>Was nach dem zweiten Text über urMQTT passiert ist: SQLite außerhalb des Kernels isoliert, Messungen gegen Mosquitto auf demselben VPS (auch das, was nicht zu unseren Gunsten ausfiel), gefundene Ursachen des langsamen Einzelflusses, Konfiguration als Daten mit einem Web-Builder für ISOs und einem Emulator im Browser, optionales einfaches MQTT und was noch nicht bewiesen ist.</description>
      <content:encoded><![CDATA[<p>Der zweite Text, <a href="https://aldo.tech/blog/urmqtt-vps-handshake-disk-hardening/">urMQTT auf einem echten VPS</a>, endete mit einer Liste unter "Was als Nächstes kommt": H6 (SQLite außerhalb des Kernels), H7 (Zeitsicherheit), X11 (Vergleich mit Mosquitto auf demselben VPS) und die erste Veröffentlichung. Dieser Text folgt dieser Liste in derselben Reihenfolge und fügt dann zwei Dinge hinzu, die nicht auf der Liste standen, weil sie unterwegs entstanden sind: Konfiguration als Daten, mit einem Web-Builder für ISOs, und optionales einfaches MQTT ohne TLS. Wie zuvor: was wir geplant haben, was wir gebaut haben, was uns überrascht hat und was offen ist. In diesem Text stehen auch einige Ergebnisse, die nicht zugunsten von urMQTT ausfallen; wir haben sie drin gelassen, weil sie Teil der Wahrheit sind.</p>
<p>Der Hinweis vorab bleibt derselbe: Das Projekt ist Pre-Alpha, die erste Veröffentlichung gilt nur für VPS mit KVM, und die Zahlen messen Code, Tests und bestimmte Versuche, nicht die Reife in der Produktion.</p>
<h2 id="wie-das-in-der-zeit-aussah">Wie das in der Zeit aussah</h2>
<p>Seit dem vorigen Text (31.10. um 23:00 Uhr) bis heute (1.11. um 20:41 Uhr) sind im Repository 59 neue Commits dazugekommen, insgesamt 178. Alle sind an einem einzigen Kalendertag entstanden, deshalb gilt dieselbe Warnung wie zuvor: Das Projekt wird mit Claude Code gebaut (die Commits tragen <code>Co-Authored-By: Claude</code>), Plan, Code, Tests und ADRs entstehen in kurzen, geschlossenen Schleifen, und die Zahlen unten sagen etwas über die Menge an Code und Prüfungen aus, nicht darüber, wie viel menschliche Arbeit darin steckt.</p>
<figure><img loading="lazy" alt="Wachstum seit dem vorigen Text: Rust-Zeilen von 89 790 auf 112 408, #[test]-Funktionen von 739 auf 926, ADRs von 104 auf 126 und Commits von 119 auf 178. Jede Zeile hat ihre eigene Skala." src="https://aldo.tech/blog/urmqtt-config-builder-plaintext-mosquitto/img/growth-since.svg" /><figcaption>Wachstum seit dem vorigen Text: Rust-Zeilen von 89 790 auf 112 408, #[test]-Funktionen von 739 auf 926, ADRs von 104 auf 126 und Commits von 119 auf 178. Jede Zeile hat ihre eigene Skala.</figcaption></figure>
<p>Die Zahlen sind mit derselben Methode berechnet wie im letzten Text: alle <code>.rs</code>-Dateien außerhalb von <code>vendor/</code> und <code>fuzz/</code>, <code>#[test]</code>-Funktionen, Überschriften <code>## ADR-</code> in <code>docs/DECISIONS.md</code>.</p>
<table>
<thead>
<tr>
<th>Zeitpunkt</th>
<th>Commit</th>
<th>Zeit</th>
<th style="text-align: right;">Rust-Zeilen</th>
<th style="text-align: right;">Tests</th>
<th style="text-align: right;">ADRs</th>
</tr>
</thead>
<tbody>
<tr>
<td>letzter Text</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 fertig</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 und Release-Mirror</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>einfaches MQTT, Dokumentation</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>heute</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>Es wurde parallel auf mehreren Branches gearbeitet, daher ist die Reihenfolge der Commits nicht strikt die Reihenfolge der Arbeit. Die ADRs sind bis 232 nummeriert, aber die Nummerierung hat Lücken (ein Teil wurde beim Zusammenführen von Branches umbenannt); wir zählen die Überschriften, nicht die größte Nummer. Veröffentlichungen: v0.2.0 (1.11., 18:45 UTC) und v0.2.1 (1.11., 20:22 UTC), beide als Pre-Release gekennzeichnet.</p>
<h2 id="h6-sqlite-im-benutzermodus">H6: SQLite im Benutzermodus</h2>
<p><strong>Was wir geplant haben.</strong> SQLite sind 254 268 Zeilen C, die die Datenbank, das WAL und das Journal von der Festplatte parsen, und die Festplatte kann manipuliert oder verändert sein. Bis dahin lief es im Kernel, mit den Rechten des Kernels. <strong>Was wir gebaut haben</strong> (ADR-210 bis ADR-212): SQLite, seine libc-Teile und der Heap wurden in ein eigenes Programm verschoben, <code>dbworker</code>, das auf EL0 (aarch64) bzw. Ring 3 (x86_64) in einem eigenen Adressraum läuft. Der Kernel linkt SQLite nicht mehr. Der Worker spricht mit dem Kernel über ein gemeinsames Speicherfenster und zwölf Systemaufrufe, und jede zurückgegebene Länge und jeder Offset wird vor der Verwendung geprüft. Ein Fehler im Worker (unzulässiger Zugriff, undefinierte Instruktion, überschrittenes Zeitbudget von 30 s) beendet den Aufruf, der Worker wird neu gestartet und die Datenbank wird aus dem WAL geöffnet wie beim Hochfahren; nach 5 Neustarts pro Minute wird der Speicher für defekt erklärt, statt endlos neu zu starten.</p>
<p>Die Kosten, gemessen auf aarch64 unter HVF, dieselbe VM, Kernel mit eingebautem SQLite gegenüber Kernel mit Worker, je sechs abwechselnde Läufe:</p>
<table>
<thead>
<tr>
<th>Messung</th>
<th>Vorher (im Kernel)</th>
<th>Nachher (Worker)</th>
<th>Unterschied</th>
</tr>
</thead>
<tbody>
<tr>
<td>Ein- und Austritt aus dem Benutzermodus</td>
<td>-</td>
<td>1,1 us pro Runde</td>
<td></td>
</tr>
<tr>
<td><code>db-bench</code>, 5 000 Einfügungen in 50 Transaktionen, bester / Median</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>eine Einfügung pro Transaktion, bester / Median</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>komplette Sicherung in einem Aufruf (23 MiB)</td>
<td>514 ms</td>
<td>635 ms</td>
<td>+23 %</td>
</tr>
</tbody>
</table>
<p>Die Schreibvorgänge auf die Festplatte (1 572) und die Flushes (319) sind identisch, wie es sein muss. Nicht isoliert ist Folgendes: <strong>Der Kernel vertraut weiterhin dem, was der Worker zurückgibt</strong>, nach Prüfung der Form. Ein kompromittierter Worker kann lügen (zum Beispiel eine leere Liste widerrufener Zertifikate zurückgeben und sie damit "entwiderrufen"), aber er kann weder den Speicher des Kernels noch TLS-Schlüssel noch Ticket-Schlüssel lesen. Es gibt kein KPTI, daher sind Lecks vom Typ Spectre/Meltdown nicht verhindert, und wir behaupten dazu nichts; der PAC-Schlüssel wird von EL0 und EL1 geteilt. Auf x86_64 haben wir die Umschaltkosten auf KVM nicht gemessen (unter TCG wäre die Zahl sinnlos), und das Fuzzing von Worker und Protokoll selbst ist nicht abgeschlossen. Die Testsuite für den Speicher (79 Szenarien, jedes lässt SQLite im Worker laufen) läuft auf aarch64 HVF und x86_64 TCG durch, auf aarch64 TCG 76 (eines wird übersprungen, weil TCG kein MSI-X hat).</p>
<p><strong>H7 ist abgeschlossen, aber nicht so, wie wir es angekündigt hatten.</strong> Die Untergrenze der Zeit (ADR-050) und SNTP mit Konsens (T1) decken den Umfang von H7 ab; NTS (RFC 8915) bleibt außerhalb des Umfangs und ist als verbleibendes Risiko vermerkt, nicht als etwas, das funktioniert. Wir halten die Zeitsicherheit nicht für "gelöst", sondern ihren beweisbaren Teil für fertig.</p>
<p><strong>Neue Netzwerkoption.</strong> Contabo vergibt kein DHCP an ein benutzerdefiniertes ISO, daher brauchte der VPS bisher ein anderes Image als die Labor-VM. Jetzt fragt <code>network.mode = "auto"</code> (ADR-214) zuerst DHCP, und wenn innerhalb der Frist (<code>dhcp_timeout_s</code>, Standard 15 s) kein Lease kommt oder er verloren geht, wird die vorgegebene statische Adresse verwendet. Manuell auf QEMU ausprobiert (mit DHCP nutzt es den Lease, ohne Netzwerk fällt es nach 5 s auf die statische Adresse zurück); ein Smoke-Szenario dafür gibt es noch nicht.</p>
<h2 id="x11-vergleich-mit-mosquitto-auf-demselben-vps">X11: Vergleich mit Mosquitto auf demselben VPS</h2>
<p>Dieselbe Art von Maschine wie im letzten Text (Contabo, 4 vCPU AMD EPYC, 8 GiB), Last von einem Linux-Host 15 ms entfernt, gegenseitiges TLS 1.3 mit denselben Zertifikaten auf beiden Seiten, Mosquitto 2.0.21 auf Debian 13 ohne Persistenz. Methode und Entscheidungen stehen in ADR-213, Rohdaten in <code>docs/perf-results/</code>. Median aus drei Läufen.</p>
<figure><img loading="lazy" alt="Vollständige Handshakes pro Sekunde (TLS 1.3 + Clientzertifikat + CONNECT, ohne Sitzungsfortsetzung) bei 32, 64 und 128 Clients: urMQTT 169, 278, 413; Mosquitto 82, 140, 184. Das sind Untergrenzen: Der Lastgenerator war nahe an seiner eigenen Grenze." src="https://aldo.tech/blog/urmqtt-config-builder-plaintext-mosquitto/img/mosquitto.svg" /><figcaption>Vollständige Handshakes pro Sekunde (TLS 1.3 + Clientzertifikat + CONNECT, ohne Sitzungsfortsetzung) bei 32, 64 und 128 Clients: urMQTT 169, 278, 413; Mosquitto 82, 140, 184. Das sind Untergrenzen: Der Lastgenerator war nahe an seiner eigenen Grenze.</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 (Bench-Profil)</td>
<td>169 / 278 / 413</td>
<td>82 / 140 / 184</td>
</tr>
<tr>
<td>dasselbe, mit den ausgelieferten Limits</td>
<td>185 / 262 / 333 (bei 128: 202 bis 286 Fehler, Handshake-Resets)</td>
<td>-</td>
</tr>
<tr>
<td>Speicher pro inaktiver Verbindung (20 000 Verbindungen)</td>
<td>0,50 KB Kernel-Heap (0,66 KB bei 5 000)</td>
<td>26,8 KB Prozesswachstum</td>
</tr>
<tr>
<td>Latenz p50, QoS 0 / QoS 1, 800 Nachrichten/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 Nachrichten/s, zugestellt/s (p50)</td>
<td>4 209 (77 ms)</td>
<td>4 235 (197 ms)</td>
</tr>
<tr>
<td>100 Subscriber, 50 Nachrichten/s: p50</td>
<td>16,6 ms</td>
<td>99 ms</td>
</tr>
<tr>
<td>ein Fluss, QoS 0, 64 B, veröffentlicht/s</td>
<td>2 962 (0 bis 7 733)</td>
<td>22 749</td>
</tr>
</tbody>
</table>
<p>Was das bedeutet und was nicht. Die Handshakes sind <strong>Untergrenzen für beide Server</strong>: Der Lastgenerator ist eine geteilte VM mit 60 bis 70 % gestohlener CPU-Zeit und war bei 128 Threads nahe an seiner eigenen Grenze, während die Arbeitskerne von urMQTT nur zu 12 bis 13 % ausgelastet waren. Das Verhältnis bei 128 Clients (etwa das 2,2-Fache) ist am aussagekräftigsten; bei 32 und 64 misst es überwiegend das Warten der Clients. Die Speicherzahlen zählen nicht dasselbe (Kernel-Heap gegenüber RSS des Prozesses), daher ist "etwa 50-mal weniger" eine Größenordnung, kein genaues Verhältnis. Die Latenz von 14 ms ist eine Netzwerkrunde (etwa 15 ms), also "keine messbare Zeit des Brokers"; die Ausreißer (p99 etwa 100 bis 200 ms) werden bei beiden Brokern von den geteilten Hosts dominiert. Unterschiede unter etwa 20 % behaupten wir nicht.</p>
<p><strong>Was nicht zu unseren Gunsten ausfiel</strong> und in den Tabellen blieb: Ein TCP-Fluss durch urMQTT trug 2- bis 8-mal weniger als durch Mosquitto, mit Schwankungen von null bis 7,7 Tausend Nachrichten pro Sekunde zwischen den Läufen, und der 20-ms-Watchdog des Handlers <strong>hat lebende Verbindungen getrennt</strong>, wenn der Hypervisor die vCPU angehalten hat (in der Produktionseinstellung <code>action = "drop"</code>, 6 bis 8 Verbindungen pro Lauf bei QoS-0-Flut). Beides ist im nächsten Abschnitt beschrieben, denn beides hatte Ursachen in unserem Code.</p>
<h2 id="warum-der-einzelfluss-langsam-war-ursachen-keine-ausreden">Warum der Einzelfluss langsam war: Ursachen, keine Ausreden</h2>
<p>Am einfachsten wäre gewesen, "geteilter VPS" zu sagen. Das haben wir nicht getan, sondern auf demselben VPS und mit derselben Methode nach Ursachen gesucht. Es wurden fünf Ursachen gefunden, vier im Datenpfad und eine im Watchdog (ADR-221 bis ADR-224):</p>
<table>
<thead>
<tr>
<th>Symptom in X11</th>
<th>Ursache in urMQTT</th>
<th>Änderung</th>
</tr>
</thead>
<tbody>
<tr>
<td>ein Fluss 1,9k Nachrichten/s, oft <strong>0 zugestellt</strong></td>
<td><strong>Verlorenes Aufwecken:</strong> Die Ausgabeschleife gab die Liste der bereiten Verbindungen über die soeben geschriebene zurück; ein Subscriber, dessen Ausgabepuffer sich einmal gefüllt hatte, wurde nie wieder bedient, bis ihn ein PINGRESP weckte</td>
<td>behoben (ADR-223)</td>
</tr>
<tr>
<td>einige Prozent Verlust machen aus 48k 14k Nachrichten/s</td>
<td><strong>jedes Segment nach einem verlorenen wurde verworfen</strong> (Empfang nur in Reihenfolge), also kostete die Wiederherstellung eine Netzwerkrunde pro Segment</td>
<td>Warteschlange für Segmente außerhalb der Reihenfolge, SACK (ADR-222)</td>
</tr>
<tr>
<td>Obergrenze 4,4 MB/s bei 15 ms</td>
<td>16-Bit-Fenster, Sendewarteschlange von 64 KiB und ein Slow Start, der ACKs zählte statt Bytes</td>
<td>Fensterskalierung, Zeitstempel, Zählen in Bytes (ADR-222)</td>
</tr>
<tr>
<td>jede Runde eines Flusses, der passt, wartet 0 bis 10 ms</td>
<td><strong>der Dispatcher schlief bis zum nächsten 10-ms-Tick</strong> (virtio-net hat keinen Interrupt für den Empfang)</td>
<td>NIC-Abfrage alle 200 us, solange Verbindungen bestehen (ADR-224)</td>
</tr>
<tr>
<td>1 bis 8 getrennte Verbindungen pro Lauf</td>
<td><strong>der Watchdog rechnete dem Handler die verstrichene Zeit an</strong>, und der VPS hält vCPUs oft und lange an</td>
<td>das Budget ist die Zeit, die der Kern tatsächlich gearbeitet hat (ADR-221)</td>
</tr>
</tbody>
</table>
<p>Die Anhaltephasen sind real: Auf diesem VPS hatten die Kerne 0 bis 2 einen Median von 36 bis 80 verspäteten Ticks pro 5 Sekunden, und die schlimmste Anhaltephase im Fenster lag im Median bei etwa 70 ms und bis zu 0,9 s (Kern 3, Datenbank und Journal: bis zu 1,55 s), während der Host keine gestohlene Zeit meldet (<code>host steal 0 us</code>). Deshalb brauchte der Watchdog eine zweite Uhr, kein größeres Budget.</p>
<figure><img loading="lazy" alt="Vorher und nachher, auf demselben VPS mit derselben Methode, Median aus drei bis sechs Läufen: ein Fluss QoS 0, 64 B: veröffentlicht 1,9k, 23,7k (Mosquitto 29,3k), zugestellt 0 (Median), 12,6k (29,2k). 16 Paare, Flut, 64 B: veröffentlicht 24,7k, 43,2k (31,4k), zugestellt 20,0k, 37,4k (30,7k)." src="https://aldo.tech/blog/urmqtt-config-builder-plaintext-mosquitto/img/before-after.svg" /><figcaption>Vorher und nachher, auf demselben VPS mit derselben Methode, Median aus drei bis sechs Läufen: ein Fluss QoS 0, 64 B: veröffentlicht 1,9k, 23,7k (Mosquitto 29,3k), zugestellt 0 (Median), 12,6k (29,2k). 16 Paare, Flut, 64 B: veröffentlicht 24,7k, 43,2k (31,4k), zugestellt 20,0k, 37,4k (30,7k).</figcaption></figure>
<table>
<thead>
<tr>
<th></th>
<th>X11-Code (7f9517e)</th>
<th>jetzt (86a71ff)</th>
<th>Mosquitto</th>
</tr>
</thead>
<tbody>
<tr>
<td>ein Fluss, QoS 0, 64 B, veröffentlicht / zugestellt pro s</td>
<td>1,9k / 0</td>
<td>23,7k / 12,6k</td>
<td>29,3k / 29,2k</td>
</tr>
<tr>
<td>ein Fluss, 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 Paare, Flut, 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 Paare, Flut, 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>verlorene Verbindungen pro Lauf (16 Paare, 64 B)</td>
<td>13 (7 bis 16)</td>
<td>0 (0 bis 0)</td>
<td>0</td>
</tr>
</tbody>
</table>
<p>Watchdog: 148 getrennte Verbindungen in 30 belasteten Läufen vorher, 0 in 24 Läufen nachher; auf einem lokalen Gerät, das Frames verzögert und verliert, hat ein Fluss von 64 B bei 0,1 % Verlust mit 14,3k Nachrichten/s (Kernel von X11) jetzt 41,0k, und bei 0,5 % von 5,0k auf 17,7k.</p>
<p><strong>Was das nicht sagt.</strong> urMQTT ist bei einem Einzelfluss <strong>weiterhin nicht schneller als Mosquitto</strong>: Bei 64 B veröffentlicht es etwa 80 % von dessen Geschwindigkeit und liefert etwa 40 % zu, weil es QoS 0 verwirft, wenn der Fluss des Subscribers langsamer ist als der des Publishers (Mosquitto reiht ohne Grenze ein). Bei 1 KiB in der 16-Paar-Flut liegt Mosquitto um etwa 15 % vorn, was im Rauschen liegt. Der Generator war wieder eine geteilte VM mit 58 bis 77 % gestohlener Zeit. Die zweite Runde (Heap-Sperren außerhalb des Datenpfads, SACK auf der Sendeseite und anderes) liegt auf dem nicht integrierten Branch <code>perf-single-flow-wip</code>; ihre Zahlen sind noch keine Behauptungen, und wir nennen sie hier nicht.</p>
<h2 id="ein-versprechen-das-wir-nicht-gegeben-aber-eingelost-haben-konfiguration-ist-daten">Ein Versprechen, das wir nicht gegeben, aber eingelöst haben: Konfiguration ist Daten</h2>
<p>Bisher war jede Konfiguration ein neuer Build: Schlüssel und Limits waren im Image eingebaut. C1 dreht das um (ADR-215 bis ADR-219): Der Kernel wird einmal gebaut, und die Konfiguration ist Daten.</p>
<ul>
<li><strong>Der Blob <code>urmqtt-cfg</code></strong> (C1a): eine versionierte, kanonische Darstellung (Felder in aufsteigender Reihenfolge der Tags, SHA-256-Fingerabdruck, optionale getrennte Ed25519-Signatur), 70 Felder mit Standardwerten und Grenzen, ein Decoder, der alles Ungültige ablehnt.</li>
<li><strong>Der Kernel liest ihn als Limine-Modul</strong> (C1b), vor allem anderen, auf aarch64 und x86_64. Es gibt <strong>einen veröffentlichten Kernel für jedes <code>ENV</code> außer <code>dev</code></strong> (die aarch64-ELFs für <code>release</code>, <code>prod</code> und <code>bench</code> haben denselben SHA-256; nur auf aarch64 geprüft). Der veröffentlichte Kernel lehnt eine Entwicklungskonfiguration ab, und ohne Konfiguration schreibt er "refusing to serve" und bedient nichts.</li>
<li><strong>Die installierte Festplatte trägt die Konfiguration</strong>, und das Update behält sie (C1c).</li>
<li><strong><code>urmqtt-config</code> und die ISO-Vorlage</strong> (C1d): <code>pack</code>, <code>show</code>, <code>verify</code>, und <code>pack --iso</code> ersetzt beide Exemplare des Konfigurationsplatzes und 24 Modul-Digests in einem fertigen ISO. Das Ergebnis ist Byte für Byte dasselbe wie das ISO, das <code>mkiso.sh --config</code> baut. Die Vorlage der Version v0.2.0 hat 63 111 168 Bytes.</li>
<li><strong>Vertrauenserklärung</strong> (ADR-219): Ein konfiguriertes Image gibt beim Hochfahren den Fingerabdruck der Konfiguration aus (<code>config: fingerprint ...</code>), und die Signatur ist optional und <strong>standardmäßig ausgeschaltet</strong>. Ein verändertes ISO hat keinen offiziellen Digest; der Fingerabdruck ist die Möglichkeit zu sehen, was darin steckt.</li>
</ul>
<h2 id="der-builder-auf-aldotech">Der Builder auf aldo.tech</h2>
<p>Dasselbe, was <code>urmqtt-config pack --iso</code> tut, tut auch die Webseite, mit demselben Code: die Werkzeugbibliothek, kompiliert zu WebAssembly (1,06 MB, ohne Importe und ohne <code>unsafe</code>-Block). Die Seite ist unter <a href="https://aldo.tech/de/produkte/urmqtt-builder/">aldo.tech/de/produkte/urmqtt-builder/</a> und in allen 15 Sprachen der Website verfügbar (zum Beispiel <a href="https://aldo.tech/proizvodi/urmqtt-builder/">hr</a>).</p>
<ul>
<li><strong>Nichts verlässt den Browser.</strong> Zertifikate, private Schlüssel und das fertige ISO bleiben auf dem Computer; die Seite lädt nur die Release-Dateien von dieser Website, es gibt kein Tracking und keine Skripte von Dritten. Im Live-Test trug keine Anfrage Schlüsselmaterial.</li>
<li><strong>Release-Mirror</strong> (ADR-227): Stündlich (systemd-Timer, niedrige Priorität) kopiert er die Releases von GitHub, prüft jedes vor der Veröffentlichung, und die Seite im Browser prüft die Signatur von <code>SHA256SUMS</code> mit einem in die Seite eingebauten Schlüssel, erst dann schreibt sie die Konfiguration in die Vorlage. Eine fehlgeschlagene Prüfung blockiert den Download.</li>
<li><strong>Einfach als Standard, erweitert bei Bedarf</strong> (ADR-232): Der erste Bildschirm besteht aus drei Schritten (wo es läuft, Ihre Zertifikate, Name des Images). Ein einziges Feld zum Hineinziehen von Dateien sortiert Zertifikat, Schlüssel und CA nach Inhalt. Die erweiterte Ansicht öffnet jedes der 70 Felder, die Suche, Import und Export von TOML; beide Ansichten bearbeiten denselben Zustand. Die Vorlage (63 MB) wird heruntergeladen, während der Benutzer das Formular ausfüllt. Im Live-Test, mit Test-Mirror und Testschlüssel, dauerte der Bau des ISO im Browser 5 bis 18 s.</li>
</ul>
<figure><img loading="lazy" alt="Einfache Ansicht des Builders: drei Schritte, &quot;Wo soll es laufen?&quot;, &quot;Ihre Zertifikate&quot; und &quot;Name des Images&quot;, mit einer einzigen nächsten Aktion in der Seitenleiste." src="https://aldo.tech/blog/urmqtt-config-builder-plaintext-mosquitto/img/builder-simple.png" /><figcaption>Einfache Ansicht des Builders: drei Schritte, &quot;Wo soll es laufen?&quot;, &quot;Ihre Zertifikate&quot; und &quot;Name des Images&quot;, mit einer einzigen nächsten Aktion in der Seitenleiste.</figcaption></figure>
<figure><img loading="lazy" alt="Erweiterte Ansicht, aufgenommen auf dem Test-Mirror mit einem Testschlüssel (Veröffentlichungsdatum und Signaturschlüssel sind nicht echt): Release mit geprüfter Signatur, Vorlagen, jedes Konfigurationsfeld." src="https://aldo.tech/blog/urmqtt-config-builder-plaintext-mosquitto/img/builder-advanced.png" /><figcaption>Erweiterte Ansicht, aufgenommen auf dem Test-Mirror mit einem Testschlüssel (Veröffentlichungsdatum und Signaturschlüssel sind nicht echt): Release mit geprüfter Signatur, Vorlagen, jedes Konfigurationsfeld.</figcaption></figure>
<p><strong>Emulator, optional.</strong> Neben "ISO herunterladen" steht "Im Emulator ausprobieren": QEMU, kompiliert zu WebAssembly, startet das soeben gebaute Image in einem Browser-Tab. Vor dem Klick wird nichts davon heruntergeladen (beim ersten Mal etwa 3 MB). Der Emulator ist nur BIOS, <strong>ohne Netzwerk</strong> (damit der Verkehr des Benutzers nicht über einen Vermittler läuft und das "nichts verlässt den Browser" verletzt), braucht etwa 1,5 GB Speicher und einen Desktop-Browser (getestet mit Chrome; Firefox und Safari nicht). Darin erreicht urMQTT <code>URMQTT READY</code> nach etwa 8 s (mit übersprungenem Countdown) und gibt denselben Konfigurationsfingerabdruck aus wie die Seite, was der Beweis ist, dass genau diese Konfiguration hochgefahren wurde. Das Hochfahren ist etwa 2-mal langsamer als im nativen QEMU auf demselben Mac (0,82 s gegenüber 1,9 s), und die Kryptografie etwa 58-mal langsamer, weil die Hardware-Instruktionen emuliert werden. Deshalb <strong>ziehen wir aus dem Emulator keinerlei Aussagen zur Geschwindigkeit</strong>, und die Seite sagt das vor dem Start. Beim Build mussten vier Bytes in der <code>.wasm</code> gepatcht werden wegen eines Fehlers bei der Übersetzung der Instruktion POPCNT im ursprünglichen Emulator (wir haben ihn noch nicht gemeldet); der Emulator selbst steht unter der GPL, mit Angebot des Quellcodes.</p>
<figure><img loading="lazy" alt="Diagnoseeintrag im Emulator live auf aldo.tech: Der Kernel hat denselben Konfigurationsfingerabdruck ausgegeben wie die Seite, und der Bildschirm zeigt die Diagnose (im Emulator endet sie nach etwa 20 s; ihre Zahlen beschreiben den Emulator, nicht urMQTT)." src="https://aldo.tech/blog/urmqtt-config-builder-plaintext-mosquitto/img/emulator-diagnostic.png" /><figcaption>Diagnoseeintrag im Emulator live auf aldo.tech: Der Kernel hat denselben Konfigurationsfingerabdruck ausgegeben wie die Seite, und der Bildschirm zeigt die Diagnose (im Emulator endet sie nach etwa 20 s; ihre Zahlen beschreiben den Emulator, nicht urMQTT).</figcaption></figure>
<h2 id="einfaches-mqtt-nur-fur-netzwerke-denen-sie-vertrauen">Einfaches MQTT, nur für Netzwerke, denen Sie vertrauen</h2>
<p>Einfaches MQTT (TCP 1883, ohne TLS) ist im IoT sehr verbreitet: Es ist der Standard in Mosquitto, in den meisten Firmwares für Arduino und ESP, in Tasmota, Zigbee2MQTT und Home Assistant, und es wird auch in segmentierten Industrienetzen, hinter VPNs und in privaten LANs verwendet, auch auf Geräten ohne Uhr (Zertifikate brauchen ein Datum), ohne genug Flash oder Prozessor für TLS. Über ein nicht vertrauenswürdiges Netzwerk ist es nicht akzeptabel: Wer auf dem Weg sitzt, liest und verändert den Verkehr. Deshalb ist P1 (ADR-229 bis ADR-231) eine ausdrückliche, begrenzte Wahl, <strong>standardmäßig ausgeschaltet</strong>, und der TLS-Port bleibt; TLS wird dadurch nicht abgeschaltet, der einfache Port kommt zusätzlich.</p>
<ul>
<li><strong>Eine Liste erlaubter Netzwerke ist Pflicht</strong> (<code>allow_from</code>, CIDRs) und wird beim SYN geprüft: Außerhalb der Liste gibt es kein SYN-ACK und keinen Zustand. Das gesamte Internet (<code>0.0.0.0/0</code>) verlangt eine eigene ausdrückliche Option.</li>
<li><strong>Drei Anmeldearten:</strong> Passwort (Standard, wenn eingeschaltet; PBKDF2-HMAC-SHA-256, Salt, 10 000 Iterationen, Vergleich in konstanter Zeit, Sperrung pro Adresse), anonym (eine konfigurierte Identität mit eigener ACL) und nach Adresse (<code>ip</code>). Der Builder hasht das Passwort im Browser und hält nur den Digest im Speicher.</li>
<li><strong>Weniger Rechte als mTLS:</strong> Ein einfacher Client hat nie Service-, Operator- oder Verwaltungsrechte, hat kein <code>$SYS</code> und keine Konsole, hat eigene Sitzungen (<code>$plain/&lt;identität&gt;/&lt;client-id&gt;</code>, nur saubere), und eigene Limits und das Register erlaubter Clients binden ihn weiterhin.</li>
<li><strong>Was verloren geht, steht in <code>SECURITY.md</code>:</strong> Vertraulichkeit, Integrität, Authentizität des Brokers und die Wiedereinspielung einer aufgezeichneten Anmeldung. Das lässt sich durch Konfiguration nicht beheben; so ist es ohne TLS.</li>
</ul>
<p>Gemessen auf einem Laptop (Apple Silicon, HVF, QEMU-Benutzernetzwerk), 16 Paare, 64 B, drei Läufe zu je 10 s: Bei 8 000 Nachrichten pro Sekunde liefern der TLS- und der einfache Pfad alle Nachrichten mit gleicher Latenz (p50 0,17 ms gegenüber 0,13 bis 0,18 ms), neue Verbindungen pro Sekunde: TLS etwa 2 600 bis 2 750, anonym einfach etwa 4 900 bis 7 800, und <strong>die Passwort-Anmeldung ist der teure Teil</strong>: Ein PBKDF2 kostet etwa 2,5 ms, daher ist die Zahl neuer Anmeldungen auf etwa 200 pro Datenkern und Sekunde begrenzt (gemessen 308 bis 342 auf zwei Kernen). Die Flut auf dieser Maschine ist kein stabiles Instrument (20k bis 440k zugestellt pro Sekunde zwischen den Läufen), und <strong>wir ziehen daraus keinen Schluss</strong>. Der Vergleich des einfachen Pfads mit Mosquitto auf dem VPS ist noch nicht gemacht. Tests: Smoke-Gruppe <code>plaintext</code> 7 von 7 auf aarch64 HVF und auf x86_64 TCG, neues Fuzz-Ziel <code>plain_input</code> (2,3 Millionen Läufe ohne Befund).</p>
<h2 id="fehler-die-der-live-betrieb-gefunden-hat">Fehler, die der Live-Betrieb gefunden hat</h2>
<p>Zwei Dinge haben nicht die Tests entdeckt, sondern die erste Inbetriebnahme auf der echten Website:</p>
<ul>
<li>Die Seite suchte die Release-Dateien <strong>ein Verzeichnis zu hoch</strong>, daher antwortete jede Signaturprüfung mit 404 (Korrektur 13af301). Der Test benutzte einen Test-Mirror unter einem anderen Pfad.</li>
<li><strong>Die Tastatur des Emulators erreichte den Gast nie</strong>: Die Ursache war nicht eine falsche Übersetzung der Tasten, sondern dass der Dialog gar nicht auf Tasten hörte (Korrektur in ADR-232, mit einem Boot-Eintrag ohne Tasten).</li>
</ul>
<h2 id="lektionen">Lektionen</h2>
<ul>
<li><strong>Suchen Sie die Ursache, geben Sie nicht der Umgebung die Schuld.</strong> Die meisten Schwächen des Einzelflusses und alle getrennten Verbindungen lagen bei uns; der Hypervisor ist real, aber er musste gemessen und nicht vermutet werden.</li>
<li><strong>Ein Vergleich mit einem anderen Produkt muss auch die Verluste enthalten.</strong> Eine Tabelle, in der nur unsere Siege stehen, wäre kein Vergleich.</li>
<li><strong>Das Messinstrument hat Grenzen.</strong> Ein geteilter Generator mit 60 bis 70 % gestohlener Zeit verändert, was die Zahlen bedeuten; deshalb schreiben wir "Untergrenze" und behaupten keine Unterschiede unter 20 %.</li>
<li><strong>Isolation ist kein Beweis.</strong> SQLite in EL0 verkleinert, was ein Fehler darin anrichten kann, nicht das, wobei es lügen kann.</li>
<li><strong>Eine echte Installation findet, wonach Tests nicht suchen</strong>: einen falsch zusammengesetzten Pfad und Tasten, auf die niemand hört.</li>
</ul>
<h2 id="was-noch-nicht-bewiesen-ist">Was noch nicht bewiesen ist</h2>
<ul>
<li>Der Signaturschlüssel der Releases ist vorerst ein Testschlüssel; v0.2.0 und v0.2.1 sind Pre-Releases, und die Images dazu sind Entwicklungs-Images mit Testzertifikaten (außer der Vorlage, die ohne Konfiguration ist).</li>
<li>13 Übersetzungen der Builder-Seite (und dieses Textes) hat eine KI geschrieben, und sie wurden durch Tests und Stichproben geprüft, nicht von Muttersprachlern.</li>
<li>Der Vergleich mit Mosquitto ist eine Sitzung, ein Pfad und ein geteilter Generator, auf einem geteilten VPS; Unterschiede unter 20 % sind keine Behauptungen.</li>
<li>Der einfache Pfad wurde weder auf dem VPS noch gegen Mosquitto gemessen.</li>
<li>Lighthouse auf der neuen einfachen Ansicht ist nicht bestätigt (alte Ansicht: Barrierefreiheit 95 oder mehr).</li>
<li>Bare Metal, x86_64-Kosten des Workers auf KVM, Firefox und Safari für den Emulator: nicht gemessen.</li>
</ul>
<h2 id="was-als-nachstes-kommt">Was als Nächstes kommt</h2>
<ul>
<li><strong>D1: öffentlicher Demo-Broker.</strong> Den Emulator aus dem Browser können wir nicht öffentlich machen: Er lebt im Tab eines einzelnen Besuchers, ist 20- bis 100-mal langsamer als KVM, hat kein eingehendes Netzwerk und verschwindet mit dem Schließen des Tabs; das Internet in den Browser eines Fremden umzuleiten, wäre außerdem Missbrauch. Möglich ist ein echter Demo-Broker: urMQTT auf einem VPS hinter einem Namen auf aldo.tech, mit veröffentlichter Test-CA, strikter ACL und kleinen Limits. Das ist vorerst nur eine Idee, kein Bau, und braucht eine ausdrückliche Zustimmung zur öffentlichen Bereitstellung.</li>
<li><strong>Zweite Runde für den Einzelfluss</strong> (Branch <code>perf-single-flow-wip</code>): erst, wenn sie auf dem VPS gemessen und zusammengeführt ist.</li>
<li><strong>Den einfachen Pfad gegen Mosquitto auf dem VPS</strong>, Anmeldung und Widerruf von Konten für einfaches MQTT über den signierten Verwaltungskanal.</li>
<li><strong>Nach der Veröffentlichung:</strong> Bare Metal (X12 bis X16), IOMMU und Secure Boot, Festplattenverschlüsselung.</li>
</ul>
<p>Der Code liegt auf <a href="https://github.com/zeljan-alduk/urMQTT">github.com/zeljan-alduk/urMQTT</a>, und <code>docs/PROGRESS.md</code> bleibt die einzige Quelle der Wahrheit darüber, was tatsächlich funktioniert, <code>docs/PERFORMANCE.md</code> darüber, was gemessen wurde. Pre-Alpha, wie zuvor; Fehlermeldungen und Sicherheitsmeldungen nach den Hinweisen in <code>SECURITY.md</code> sind willkommen.</p>]]></content:encoded>
      <category>rust</category>
      <category>mqtt</category>
      <category>iot</category>
      <category>unikernel</category>
      <category>open source</category>
    </item>
    <item>
      <title>urMQTT auf einem echten VPS: ISO, Handshake in 0,24 ms, eigene Festplatte und gehärteter Kernel</title>
      <link>https://aldo.tech/de/blog/urmqtt-vps-handshake-disk-hardening/</link>
      <guid isPermaLink="true">https://aldo.tech/de/blog/urmqtt-vps-handshake-disk-hardening/</guid>
      <pubDate>Sat, 31 Oct 2026 08:00:00 +0000</pubDate>
      <description>Was aus den drei Versprechen des ersten urMQTT-Textes (ein einziges ISO, rohe Festplatte ohne Dateisystem, nachweisbar sicher) nach den ersten vier Entwicklungstagen geworden ist, mit Messungen, Tabellen und einer Liste dessen, was noch nicht fertig ist.</description>
      <content:encoded><![CDATA[<p>Im <a href="https://aldo.tech/blog/urmqtt-unikernel-mqtt-broker/">ersten Text über urMQTT</a> haben wir einen Unikernel beschrieben, der selbst der MQTT-Broker ist, und zwar bis zum Meilenstein M17. Am Ende standen drei Versprechen unter der Überschrift "Was als Nächstes kommt": ein einziges ISO, das auf mehreren Plattformen startet, eine rohe Festplatte ohne Dateisystem und Sicherheit, die sich beweisen lässt, statt nur behauptet zu werden. Nichts davon war damals gebaut. Dieser Text verfolgt, was aus jedem Versprechen geworden ist, in derselben Reihenfolge wie zuvor: was wir geplant haben, was wir gebaut haben, was uns überrascht hat und was noch offen ist. Nebenbei zeigen wir auch, wie lange das gedauert hat, denn das gehört zur Geschichte.</p>
<p>Ein Hinweis vorab: Das Projekt ist weiterhin Pre-Alpha. Die Zahlen in diesem Text messen Code und Tests, nicht die Produktionsreife, und die erste Veröffentlichung ist bewusst auf VPS mit KVM beschränkt. Bare Metal bleibt für später.</p>
<h2 id="wie-das-in-der-zeit-aussah">Wie das in der Zeit aussah</h2>
<p>Der erste Commit entstand am 28. Oktober 2026 um 23:48 Uhr (M0). Bis zum 31. Oktober um 23:00 Uhr sind es 119 Commits im Repository, verteilt auf die Kalendertage als 3, 39, 38 und 39. Wie das möglich ist, sagen wir gleich dazu: Das Projekt wird mit Claude Code gebaut, und die Commits tragen <code>Co-Authored-By: Claude</code>-Trailer. Vier Kalendertage Historie bedeuten nicht, dass eine einzelne Person in vier Tagen 90 000 Zeilen geschrieben hat; sie bedeuten, dass Plan, Implementierung, Tests und ADRs in kurzen, geschlossenen Schleifen entstehen und jeder Schritt mit demselben Prüfritual endet wie zuvor. Die Zahlen unten sagen etwas über die Menge an Code und Tests aus, nicht darüber, wie viel Arbeitsaufwand in der Realität dahintersteckt.</p>
<figure><img loading="lazy" alt="Wachstum des Projekts über vier Tage mit den Meilensteinen von M0 bis heute: Tausende Zeilen Rust (von 1,6 auf 89,8), Anzahl der #[test]-Funktionen (von 27 auf 739) und Anzahl der ADRs (von 13 auf 104), dazu Balken mit den Commits pro Tag (39, 38 und 39 für den 29., 30. und 31. Oktober)." src="https://aldo.tech/blog/urmqtt-vps-handshake-disk-hardening/img/growth.svg" /><figcaption>Wachstum des Projekts über vier Tage mit den Meilensteinen von M0 bis heute: Tausende Zeilen Rust (von 1,6 auf 89,8), Anzahl der #[test]-Funktionen (von 27 auf 739) und Anzahl der ADRs (von 13 auf 104), dazu Balken mit den Commits pro Tag (39, 38 und 39 für den 29., 30. und 31. Oktober).</figcaption></figure>
<p>Das Diagramm zeigt drei Kurven, die fast gemeinsam wachsen, und genau das ist die Absicht: Tests und festgehaltene Entscheidungen begleiten den Code, statt ihm hinterherzuhinken. Der Anstieg ist am 29. Oktober am steilsten, als der gesamte Weg vom Kernel bis zum Broker entsteht. Danach kommen Zeilen langsamer hinzu, aber die Tests wachsen schneller als der Code (27 Tests auf 1,6 Tausend Zeilen am Anfang, 739 auf 89,8 Tausend heute).</p>
<table>
<thead>
<tr>
<th>Meilenstein</th>
<th>Commit</th>
<th>Zeit</th>
<th style="text-align: right;">Rust-Zeilen</th>
<th style="text-align: right;">Tests</th>
<th style="text-align: right;">ADRs</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>heute</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>Als Zeilen zählen alle <code>.rs</code>-Dateien außerhalb von <code>vendor/</code> und <code>fuzz/</code>, als Tests die <code>#[test]</code>-Funktionen. Was welcher Abschnitt hinzugefügt hat, nach Datum aus der Git-Historie:</p>
<table>
<thead>
<tr>
<th>Datum</th>
<th>Meilensteine</th>
<th>Was hinzugekommen ist</th>
</tr>
</thead>
<tbody>
<tr>
<td>29.10.</td>
<td>M0 bis M17</td>
<td>Kernel, Netzwerk, TLS, Broker, SQLite, Mehrkernbetrieb, eigenes TCP, Härtung, reproduzierbares Release</td>
</tr>
<tr>
<td>29.10. abends und 30.10.</td>
<td>X0 bis X10, S1, H2, H3 (X0 beginnt am 29.10. um 20:54)</td>
<td>Port auf x86_64, universelles ISO, eine Festplatte und selbstinstallierendes ISO, TLS-Prüfungen, angreifende Clients</td>
</tr>
<tr>
<td>31.10.</td>
<td>O1 bis O4, T1, PERF, H4, H5, H9, S2 bis S6</td>
<td>Einblick und Konsole über MQTT, Live-Modus und Diagnose, SNTP, Beschleunigungen, Härtung, Verifikation, Lieferkette, Speicherung</td>
</tr>
</tbody>
</table>
<h2 id="versprechen-1-ein-iso-fur-mehrere-plattformen">Versprechen 1: ein ISO für mehrere Plattformen</h2>
<p><strong>Was wir geplant haben.</strong> Eine einzelne Datei, die auf aarch64-UEFI, x86_64-UEFI und x86_64-BIOS startet. <strong>Was wir gebaut haben.</strong> Den Port auf x86_64 (X0 bis X10) und ein universelles ISO mit dem Limine-Bootloader, ein hybrides ISO, von dem alle diese Kombinationen starten; die Smoke-Tests starten es auf vier virtuellen Maschinen. Das Menü hat drei Einträge: urMQTT (Live-Modus, läuft aus dem RAM, schreibt nichts auf die Festplatte), die Installation auf die Festplatte der Maschine, auf der es gestartet wurde (die Installation schreibt GPT und Daten und schaltet die Maschine dann aus) und Diagnose (Hardware-Erkennung und Messungen).</p>
<figure><img loading="lazy" alt="Das Boot-Menü des ISO, auf dem VPS aufgenommen: die Einträge &quot;urMQTT&quot;, &quot;Install urMQTT on this machine's disk&quot; und &quot;Diagnostic: hardware detection and benchmarks&quot; mit einem Countdown bis zum automatischen Start. Die Aufnahme stammt von einem Test-ISO mit längerem Countdown; Standard sind 10 Sekunden." src="https://aldo.tech/blog/urmqtt-vps-handshake-disk-hardening/img/boot-menu.png" /><figcaption>Das Boot-Menü des ISO, auf dem VPS aufgenommen: die Einträge &quot;urMQTT&quot;, &quot;Install urMQTT on this machine's disk&quot; und &quot;Diagnostic: hardware detection and benchmarks&quot; mit einem Countdown bis zum automatischen Start. Die Aufnahme stammt von einem Test-ISO mit längerem Countdown; Standard sind 10 Sekunden.</figcaption></figure>
<p>Das erste echte Ziel war ein VPS und keine physische Maschine: Wir haben entschieden, dass die erste Veröffentlichung nur VPS (KVM) unterstützt und Bare Metal danach kommt. Getestet haben wir auf einem Contabo-VPS mit AMD-EPYC-Prozessor (Familie 23, Zen 1), 4 vCPUs und 8 GiB RAM, mit virtio-net-Netzwerk und virtio-scsi-Festplatte. Der erste Versuch hat uns überrascht: Das als virtuelle CD eingebundene ISO startete den Standardeintrag, das Produktions-Image weigerte sich, von einem schreibgeschützten Medium zu laufen, und setzte sich zurück. So bekamen wir eine Endlosschleife aus Menü, Kernel und Reset, ohne irgendetwas Lesbares. Heute hinterlässt ein fataler Fehler im Produktions-Image seine Meldung 60 Sekunden lang auf der Konsole, bevor der Reset erfolgt, und von einem optischen Medium wird ausgegeben, dass von einem schreibgeschützten Medium gestartet wurde, und dann angehalten. Der Live-Modus im RAM (O3) hat dieses Problem später für den üblichen Fall umgangen.</p>
<p>Der Diagnose-Eintrag ist aus dieser Erfahrung entstanden: Eine Maschine, die wir nicht sehen, müssen wir fragen können, was sie hat. Er gibt CPU, RAM, Netzwerk, Festplatten und PCI-Geräte aus, misst Speicherdurchsatz, Festplatte und Kryptografie und gibt dieselben Messungen als JSON auf dem seriellen Port aus. Das Schreiben auf die Festplatte ist nur im nicht zugewiesenen Bereich erlaubt und erst, nachdem <code>WIPE</code> eingegeben wurde; in der Aufnahme unten wurde das übersprungen.</p>
<figure><img loading="lazy" alt="Diagnose-Bildschirm auf dem Contabo-VPS (AMD EPYC, KVM, 4 vCPU): System, Speicher, Festplatte, PCI-Geräte, Durchsatz und Latenz des RAM sowie Krypto-Messungen, darunter 244 us pro mTLS-Handshake, X25519 48 us, ECDSA-Signatur 35,7 us und Verifikation 107 us." src="https://aldo.tech/blog/urmqtt-vps-handshake-disk-hardening/img/diag-vps.png" /><figcaption>Diagnose-Bildschirm auf dem Contabo-VPS (AMD EPYC, KVM, 4 vCPU): System, Speicher, Festplatte, PCI-Geräte, Durchsatz und Latenz des RAM sowie Krypto-Messungen, darunter 244 us pro mTLS-Handshake, X25519 48 us, ECDSA-Signatur 35,7 us und Verifikation 107 us.</figcaption></figure>
<p><strong>Was offen ist.</strong> Echte Hardware. Nach der ersten Veröffentlichung folgt Bare Metal (USB-Boot, NVMe und AHCI, Netzwerkkarte eines echten Rechners) zusammen mit IOMMU und Secure Boot, denn ohne IOMMU kann ein bösartiges oder fehlerhaftes Gerät den gesamten Speicher beschreiben. In dieser Version behaupten wir dazu nichts.</p>
<h2 id="handshake-von-12-ms-auf-024-ms">Handshake: von 1,2 ms auf 0,24 ms</h2>
<p>Die erste Diagnosemessung auf dem VPS lieferte eine unangenehme Zahl: Der mTLS-Handshake, nur die Kryptografie auf der Serverseite, dauerte 1,2 ms. Auf aarch64 nutzten wir bereits die Hardware-Instruktionen für AES und SHA, aber x86_64 hatte für Public-Key-Verfahren nichts Vergleichbares: RustCrypto ist portabel und constant-time, und damit auch langsam. Drei Änderungen haben das gesenkt.</p>
<ul>
<li>Für P-256 haben wir den <code>nistz256</code>-Assembler aus OpenSSL und ring übernommen (ADR-130), dazu Tabellen mit fester Basis für das Signieren und die Prüfung von CA-Signaturen (ADR-110, ADR-111).</li>
<li>X25519 wurde auf vier 64-Bit-Limbs mit MULX/ADX-Instruktionen umgeschrieben (ADR-131).</li>
<li>Danach haben wir auch den Rest profiliert (ADR-132, ADR-200).</li>
</ul>
<figure><img loading="lazy" alt="Handshake auf dem VPS: 1200 us vorher, 237 us nach Assembler und Tabellen; A/B-Vergleich von memcpy im selben Lauf, 278,8 us mit dem alten, 229,4 us mit dem neuen; sowie vorher und nachher für X25519 (146 auf 48 us), ECDSA-Signatur (673 auf 34 us) und Verifikation (309 auf 90 us)." src="https://aldo.tech/blog/urmqtt-vps-handshake-disk-hardening/img/handshake.svg" /><figcaption>Handshake auf dem VPS: 1200 us vorher, 237 us nach Assembler und Tabellen; A/B-Vergleich von memcpy im selben Lauf, 278,8 us mit dem alten, 229,4 us mit dem neuen; sowie vorher und nachher für X25519 (146 auf 48 us), ECDSA-Signatur (673 auf 34 us) und Verifikation (309 auf 90 us).</figcaption></figure>
<table>
<thead>
<tr>
<th>Messung auf dem VPS (Server, Mikrosekunden)</th>
<th style="text-align: right;">Vorher</th>
<th style="text-align: right;">Nachher</th>
</tr>
</thead>
<tbody>
<tr>
<td>mTLS-Handshake, gesamte Kryptografie</td>
<td style="text-align: right;">1200</td>
<td style="text-align: right;">etwa 240 (237 in der Messung aus dem Diagramm, 244 in der Aufnahme)</td>
</tr>
<tr>
<td>X25519</td>
<td style="text-align: right;">146</td>
<td style="text-align: right;">48</td>
</tr>
<tr>
<td>ECDSA-Signatur</td>
<td style="text-align: right;">673</td>
<td style="text-align: right;">34</td>
</tr>
<tr>
<td>ECDSA-Verifikation</td>
<td style="text-align: right;">309</td>
<td style="text-align: right;">90 (107 in der Aufnahme)</td>
</tr>
<tr>
<td>Handshake, altes memcpy, derselbe Boot</td>
<td style="text-align: right;">278,8</td>
<td style="text-align: right;">229,4 (-17,7%)</td>
</tr>
</tbody>
</table>
<p>Die Zahlen unterscheiden sich von Lauf zu Lauf um etwa 10%, deshalb haben wir eine Spanne statt eines einzelnen Wertes notiert. Insgesamt ist das ungefähr fünfmal weniger CPU-Zeit für den Handshake.</p>
<p>Was wir über memcpy gelernt haben. Als die Kryptografie gefallen war, zeigte das Profil etwas, womit wir nicht gerechnet hatten: Die <code>mem*</code>-Funktionen (memcpy, memmove, memset) verbrauchten 7% des Handshakes, 13 bis 39% des TLS-Record-Pfads (39% bei 64 B, 13% bei 4 KiB) und 17 bis 34% der PUBLISH-Verteilung. Wir haben sie durch SSE2-Assembler für x86_64 ersetzt (ADR-200), mit Differenztests gegen die Referenzimplementierung für alle Größen, Ausrichtungen und Überlappungen. Pro Aufruf sind das 55 bis 75 Zyklen gegenüber 7 bis 12 bei 1 bis 64 Bytes.</p>
<figure><img loading="lazy" alt="Links: Anteil der Serverzeit in den mem*-Funktionen vor der Neufassung (Handshake 7%, TLS-Record 64 B 39%, 256 B 37%, 1 KiB 24%, Verteilung 17 bis 34%). Rechts: um wie viel jeder Pfad danach schneller ist (Handshake 17,7%, 64 B 26,4%, 256 B 19,6%, 1 KiB 16,1%, 4 KiB 6,6%, Verteilung 7 bis 34%)." src="https://aldo.tech/blog/urmqtt-vps-handshake-disk-hardening/img/hotpaths.svg" /><figcaption>Links: Anteil der Serverzeit in den mem*-Funktionen vor der Neufassung (Handshake 7%, TLS-Record 64 B 39%, 256 B 37%, 1 KiB 24%, Verteilung 17 bis 34%). Rechts: um wie viel jeder Pfad danach schneller ist (Handshake 17,7%, 64 B 26,4%, 256 B 19,6%, 1 KiB 16,1%, 4 KiB 6,6%, Verteilung 7 bis 34%).</figcaption></figure>
<table>
<thead>
<tr>
<th>Pfad</th>
<th style="text-align: right;">Anteil mem* vorher</th>
<th style="text-align: right;">Danach schneller</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>Nachrichtenverteilung</td>
<td style="text-align: right;">17 bis 34%</td>
<td style="text-align: right;">7 bis 34%</td>
</tr>
</tbody>
</table>
<p>So liest man das Diagramm: Je kleiner der Record, desto größer der Anteil der Zeit, der ins Kopieren fließt, und desto größer der Gewinn. Für kleine Nachrichten, die den Großteil des MQTT-Verkehrs ausmachen, ist das genau das, was zählt.</p>
<p>Was das nicht bedeutet. Auf nativem x86_64 haben wir OpenSSL 3.5.5 (ADR-130) nur als Referenz für den Assembler gemessen, nicht als Produktvergleich. Den Vergleich Linux plus Mosquitto auf demselben VPS (Aufgabe X11) haben wir noch nicht gemacht. Deshalb behaupten wir nicht, dass urMQTT schneller als Linux ist: Es ist nicht gemessen. Die Zahlen oben vergleichen urMQTT mit sich selbst.</p>
<p>Die Überlast war das zweite Ergebnis. Auf dem VPS, von einem einzelnen Mac aus (RTT 35 ms), schafften 128 Clients 618 Handshakes pro Sekunde, 256 Clients 393 und 512 Clients 67, mit Tausenden abgewiesenen Verbindungen. Das war kein Plateau, sondern ein Zusammenbruch. Die Ursachen haben wir im Code gefunden (zwei davon wurden in QEMU/HVF bestätigt, auf dem VPS ließen sie sich nicht reproduzieren), mehrere gleichzeitig: Eine abgewiesene Verbindung schickte den Client über das nächste Segment mit gültigem SYN-Cookie zurück, die Abweisung per RST sagte dem Client, sofort erneut zu versuchen, und die Annahme war O(Warteschlange). Die Korrektur (ADR-080 bis ADR-082) verwirft ein SYN, das nicht angenommen würde, merkt sich abgewiesene Flows, hält die Annahme in O(1) und zählt die Handshakes pro Kern. Möglich (Hypothese, nicht geprüft) ist, dass ein Teil der Ursache auch auf der Client-Seite lag: Das Lastwerkzeug schloss zuerst, sodass ein einzelner Mac mit 16 384 ephemeren Ports und 30 Sekunden TIME_WAIT auf etwa 550 Verbindungen pro Sekunde begrenzt war. Die echte Obergrenze des Servers haben wir daher noch nicht gemessen.</p>
<h2 id="versprechen-2-rohe-festplatte-aber-sorgfaltig">Versprechen 2: rohe Festplatte, aber sorgfältig</h2>
<p><strong>Was wir geplant haben.</strong> Das eigene Layout ohne Dateisystem beibehalten und all das hinzufügen, was ein Dateisystem sonst liefert: Ausrichtung, Wachstum, Discard, Sicherungskopien und Prüfungen. <strong>Warum kein Dateisystem.</strong> Darüber haben wir diskutiert, und die Entscheidung wurde bestätigt: urMQTT schreibt eine Datenbank, ein Telemetrie-Journal und eigene Metadaten, braucht weder Verzeichnisse noch Berechtigungen noch Dateinamen, und jede zusätzliche Schicht wäre noch mehr Code im vertrauenswürdigen Teil und noch ein Weg, auf dem ein Schreibvorgang unbemerkt verschwinden kann. Die Festplatte ist GPT mit einer ESP-Partition und einer urMQTT-Datenpartition, auf der das eigene Layout liegt.</p>
<p><strong>Was wir gebaut haben (S2 bis S6).</strong></p>
<ul>
<li><strong>Layout v2 (S2).</strong> Festplattentopologie, Ausrichtung auf 1 MiB oder die optimale I/O-Größe, ganze physische Blöcke, ein A/B-Paar von Superblöcken mit Generation (ADR-140). Layout v1 lehnt der Kernel mit einer klaren Meldung ab, und es wird vom Offline-Werkzeug <code>urmqtt-disk migrate</code> migriert (ADR-141): Standard ist ein Probelauf, es wird kopiert und erst dann umgeschaltet, mit Rollback und Fehlerinjektion bei jedem Schreibvorgang.</li>
<li><strong>Wachstum (S3).</strong> Beim Start kann die Festplatte wachsen (sie schrumpft nie), und auf virtio-blk auch zur Laufzeit, über den Konfigurationsänderungs-Interrupt (ADR-142, ADR-143).</li>
<li><strong>Discard und Write-Zeroes (S4).</strong> Thin provisionierte Festplatten geben Speicherplatz an den Host zurück. Im Test fiel eine qcow2-Datei auf dem Host von 62 auf 16 MiB, nachdem Telemetriesegmente abgelaufen waren, und die Wiederherstellung traut dem Inhalt verworfener Bereiche nie.</li>
<li><strong>Sicherungskopie der Datenbank auf der Festplatte (S5).</strong> Eine A/B-Kopie mit der Online-Backup-API von SQLite in Schritten von 16 Seiten: der längste Schritt 4,6 ms gegenüber 445 ms, wenn in einem Aufruf kopiert wird. Commits während des Kopierens hatten ein Maximum von 2,4 ms gegenüber 2,2 ms ohne Kopie. Schlägt <code>quick_check</code> beim Start fehl, wird die Datenbank aus der Kopie wiederhergestellt.</li>
<li><strong>Scrubbing und begrenzter Start (S6).</strong> Der Scrubber liest die Festplatte mit begrenztem I/O-Budget und meldet beschädigte Einträge, und der Start liest nur das jeweils neueste Segment jeder Partition, sodass die Startzeit nicht mit der Größe der Telemetrieregion wächst. Damit haben wir ein Problem gelöst, das wir vorab ausgerechnet hatten: Auf einer großen Festplatte würde eine vollständige Wiederherstellung von Minuten bis zu Dutzenden von Minuten dauern.</li>
</ul>
<p>Die gesamte Gruppe der Smoke-Tests für die Speicherung läuft auf beiden Architekturen durch: 79 erfolgreich, 0 fehlgeschlagen, auf aarch64 unter HVF und auf x86_64 unter TCG. Wir haben auch gemessen, dass S4 die Datenbank nicht verlangsamt hat: 53,3 Tausend Einfügungen pro Sekunde in Batches vorher und nachher, ohne messbaren Unterschied.</p>
<p><strong>Was überrascht hat.</strong> Wie viel von einem "einfachen" Layout auf Randfälle entfällt: virtio-scsi und virtio-blk melden die Topologie auf unterschiedliche Weise, und ein unvollständiger Schreibvorgang mitten in der Migration muss immer mindestens eine gültige Version hinterlassen. Deshalb hat jeder Schreibvorgang auf dem Weg der Migration und des Wachstums die Fehlerinjektion durchlaufen.</p>
<p><strong>Was offen ist.</strong> SQLite-Seiten haben keine Prüfsumme, sodass die Datenbank ein geändertes Bit innerhalb eines Zeilenwerts nicht bemerkt; Telemetrieeinträge und der Inhalt der Kopien haben ein CRC und werden erkannt. Die Festplattenverschlüsselung (S7) ist nicht fertig.</p>
<h2 id="versprechen-3-nachweisbar-sicher-so-weit-wir-es-schaffen">Versprechen 3: nachweisbar sicher, so weit wir es schaffen</h2>
<p><strong>Härtung (H4).</strong> Der Allokator löscht freigegebenen Speicher und prüft ihn bei der Wiederverwendung, erkennt doppelte Freigaben, verknüpft Zeiger der Freiliste per XOR und nullt Schlüssel bei der Freigabe (ADR-170). Stack-Schutz (<code>-Zstack-protector=strong</code>, 1315 Funktionen, und SQLite mit <code>-fstack-protector-strong</code>, ADR-171). Fehlerberichte in der Produktion enthalten weder Register noch Adressen (ADR-172). KASLR auf beiden Architekturen: Der Kernel ist PIE, und Limine verschiebt ihn um etwa 19 Bit, zufällig platziert werden Heap, Stacks und das MMIO-Fenster (ADR-173). Auf aarch64 markiert MTE den Heap (ADR-174), aber nur unter TCG: HVF und x86 haben es nicht. kCFI haben wir bewertet und nicht übernommen, weil der Compiler im festgelegten Nightly mit einem internen Fehler abstürzt; IOMMU wurde zusammen mit Bare Metal zurückgestellt (ADR-175, ADR-176).</p>
<p><strong>Der Preis ist gemessen, nicht angenommen.</strong> Auf dem Handshake-Pfad (<code>dp-bench</code>, aarch64 HVF, volle TLS-1.3- + mTLS- + CONNECT-Handshakes):</p>
<table>
<thead>
<tr>
<th>Schritt</th>
<th style="text-align: right;">1 Kern</th>
<th style="text-align: right;">4 Kerne</th>
<th>Änderung</th>
</tr>
</thead>
<tbody>
<tr>
<td>ohne Härtung</td>
<td style="text-align: right;">1941</td>
<td style="text-align: right;">7620</td>
<td></td>
</tr>
<tr>
<td>+ Allokator</td>
<td style="text-align: right;">1898</td>
<td style="text-align: right;">7429</td>
<td>-2,2% bis -2,5%</td>
</tr>
<tr>
<td>+ Stack-Schutz</td>
<td style="text-align: right;">1845</td>
<td style="text-align: right;">7243</td>
<td>zusammen -4,9% bis -5%</td>
</tr>
<tr>
<td>+ PIE und Markierungen</td>
<td style="text-align: right;">etwa 1800</td>
<td style="text-align: right;">etwa 6750</td>
<td>auf einem ausgelasteten Rechner, verrauscht</td>
</tr>
</tbody>
</table>
<p>Insgesamt liegt der Preis auf dem Handshake-Pfad also bei etwa 5% für die sauber gemessenen Teile, und mit PIE und Markierungen kommen etwa 7% auf einem und etwa 11% auf vier Kernen heraus, doch diese Messung ist verrauscht. Wir halten das für akzeptabel, haben es aber nicht auf dem x86_64-VPS wiederholt.</p>
<p>Es lohnt sich, das mit der Zahl aus dem ersten Text zu vergleichen (862 Handshakes pro Sekunde auf einem Kern). Dasselbe Werkzeug liefert heute, mit der Hardware-Verschlüsselung auf aarch64 (X0b) und einer Reihe von Beschleunigungen, die seitdem hinzugekommen sind, 1941 vor der Härtung. Den Beitrag jeder einzelnen Änderung haben wir nicht isoliert, deshalb behaupten wir zur Ursache nichts.</p>
<p><strong>Wie es geprüft wurde, und was nicht.</strong> H5 hat Miri für eine Teilmenge der Host-Crates, Kani-Beweise, Sanitizer, einen Modell-Fuzzer für die ACL und Abdeckungsberichte hinzugefügt. Kani hat <code>record_len</code> in TLS, <code>parse</code> im TCP und zwei Arithmetiken des Timer-Rads bewiesen. <strong>Nicht bewiesen</strong> (Zeitüberschreitung) sind beide MQTT-Decoder, beide Konfigurationsparser und das Verhalten des Timer-Rads; der CI-Job für Kani blockiert deshalb noch nicht. Miri kann keinen Assembler ausführen. Lieferkette (H9): ein CycloneDX-SBOM mit 215 Komponenten, <code>cargo-vet</code> läuft durch mit 39 vollständig geprüften und 140 Ausnahmen, und cargo-deny ist so konfiguriert, dass Schwachstellenhinweise als CI-Fehler behandelt werden (diese CI-Jobs wurden bisher nur lokal geprüft, nicht auf GitHub, und der Negativtest wurde nicht ausgeführt), dazu <code>VULNERABILITY-HANDLING.md</code>. Der ehrliche Stand: Von 67 in den Kernel gelinkten Versionen von Drittanbieter-Crates sind 53 Ausnahmen und keine Audits. Vet beweist, dass sich die Menge der Abhängigkeiten nicht ohne sichtbaren Unterschied ändern kann, nicht, dass die TCB geprüft wurde. Ein eigenes Audit eines Crates haben wir nicht geschrieben.</p>
<p>SNTP (T1) ist in diesem Sinne eine kleine Sicherheitsgeschichte: Es ist optional, nur zu privaten IPv4-Servern, mit einem maximalen Sprung und der Bedingung, dass die Quellen übereinstimmen (Median, Mehrheit innerhalb einer Sekunde). Eine Uhr, die jeder auf dem Weg verstellen kann, ist keine Uhr, der wir bei der Gültigkeit von Zertifikaten trauen dürfen.</p>
<h2 id="auf-dem-mac-aarch64-nativ-und-x86_64-per-emulation">Auf dem Mac: aarch64 nativ und x86_64 per Emulation</h2>
<p>Wir entwickeln auf dem Mac, und der hat zwei Wege, denselben Code auszuführen, die sich stark unterscheiden.</p>
<table>
<thead>
<tr>
<th></th>
<th>aarch64 unter HVF</th>
<th>x86_64 unter TCG</th>
</tr>
</thead>
<tbody>
<tr>
<td>Was das ist</td>
<td>native Ausführung, Geschwindigkeit nahe am echten Prozessor</td>
<td>QEMU emuliert die Instruktionen</td>
</tr>
<tr>
<td>Handshake im Gast</td>
<td>etwa 0,58 ms (erstmals gemessen um M13); <code>dp-bench</code> heute 1941/s pro Kern</td>
<td>etwa 3,3 ms</td>
</tr>
<tr>
<td>TLS-Smoke-Szenarien</td>
<td>schnell</td>
<td>etwa 15 Minuten pro Szenario</td>
</tr>
<tr>
<td>MTE</td>
<td>nein (gibt es unter HVF nicht)</td>
<td>x86 hat es nicht; die Heap-Markierung funktioniert nur für einen aarch64-Gast unter TCG</td>
</tr>
<tr>
<td>Wofür es dient</td>
<td>echte Messungen</td>
<td>Korrektheit: der x86-Pfad wird geprüft, die Zahlen sind keine Performance</td>
</tr>
</tbody>
</table>
<p>Die Regel ist einfach: Zahlen unter TCG sind Emulation, und wir zitieren sie nicht als Performance. Die x86-Performance messen wir nur auf einem echten VPS. Deshalb stammen die Krypto-Zahlen oben vom Contabo-VPS und nicht vom Mac.</p>
<h2 id="lektionen">Lektionen</h2>
<ul>
<li><strong>Messen Sie auf einer echten Maschine, bevor Sie optimieren.</strong> Unser erster Ansatz war, das zu optimieren, was wir für langsam hielten; erst die Diagnose auf dem VPS zeigte 1,2 ms und wo genau die Zeit hingeht.</li>
<li><strong>Überlast ist ein eigenes Feature.</strong> Ein System, das unter Last schnell ist, kann darüber zusammenbrechen; Abweisen muss billiger sein als Annehmen.</li>
<li><strong>Das Profil überrascht.</strong> Der größte Einzelgewinn für kleine Nachrichten lag nicht in der Kryptografie, sondern im Kopieren von Speicher.</li>
<li><strong>Eine Festplatte ohne Dateisystem erfordert mehr Arbeit, nicht weniger.</strong> Was ein Dateisystem sonst liefert (Ausrichtung, Wachstum, Discard, Kopien, Prüfungen), müssen wir selbst schreiben und prüfen, jeden Schritt mit Fehlerinjektion.</li>
<li><strong>Ehrlichkeit bei den Zahlen.</strong> Speichermessungen auf einem geteilten VPS schwanken zwischen den Läufen um das bis zu 10-Fache und sagen nichts über den Code aus, deshalb nennen wir sie nicht. Die Krypto-Zahlen sind stabil bis auf etwa 10%.</li>
<li><strong>Entwicklungsgeschwindigkeit ist keine Reife.</strong> Tests und ADRs wachsen mit dem Code, aber vier Tage sind kein echter Betrieb.</li>
</ul>
<h2 id="was-als-nachstes-kommt">Was als Nächstes kommt</h2>
<ul>
<li><strong>H6:</strong> SQLite auf EL0-Ebene isoliert, sodass ein Fehler in der Datenbank nicht den Kernel öffnet.</li>
<li><strong>H7:</strong> der Rest der Zeitsicherheit (NTS und Behandlung von Sprüngen); SNTP ist erst der erste Schritt.</li>
<li><strong>X11:</strong> Vergleich mit Linux und Mosquitto auf demselben VPS, erst dann ist es sinnvoll, von relativer Geschwindigkeit zu sprechen.</li>
<li><strong>Release:</strong> die erste öffentliche Veröffentlichung für VPS (KVM), mit signierten Artefakten und Anleitungen.</li>
<li><strong>Nach dem Release:</strong> Bare Metal (X12 bis X16), IOMMU und Secure Boot sowie Festplattenverschlüsselung.</li>
</ul>
<p>Der Code liegt auf <a href="https://github.com/zeljan-alduk/urMQTT">github.com/zeljan-alduk/urMQTT</a>, und <code>docs/PROGRESS.md</code> bleibt die einzige Quelle der Wahrheit darüber, was tatsächlich funktioniert. Pre-Alpha, wie zuvor; Fehlerberichte und Sicherheitsmeldungen nach den Hinweisen in <code>SECURITY.md</code> sind willkommen.</p>]]></content:encoded>
      <category>rust</category>
      <category>mqtt</category>
      <category>iot</category>
      <category>unikernel</category>
      <category>open source</category>
    </item>
    <item>
      <title>urMQTT: Wir bauen einen MQTT-Broker, der zugleich das Betriebssystem ist</title>
      <link>https://aldo.tech/de/blog/urmqtt-unikernel-mqtt-broker/</link>
      <guid isPermaLink="true">https://aldo.tech/de/blog/urmqtt-unikernel-mqtt-broker/</guid>
      <pubDate>Thu, 29 Oct 2026 08:00:00 +0000</pubDate>
      <description>Entwicklungsnotizen zu urMQTT, einem in Rust geschriebenen Unikernel, der selbst der MQTT-Broker ist: ein einziges ELF mit eigenem TCP/IP, TLS 1.3, Broker und SQLite, gestartet von einem einzigen ISO.</description>
      <content:encoded><![CDATA[<p>Geräte in Fahrzeugen, auf Parkplätzen und in Schaltschränken haben eines gemeinsam: Sie senden kleine Nachrichten, selten, aber es sind viele, und sie sind ständig verbunden. Seit Jahren binden wir sie an MQTT-Broker an, und seit Jahren beschäftigt uns dieselbe Frage. Warum brauchen wir für etwas so Einfaches ein komplettes Linux, OpenSSL, die Laufzeitumgebung einer Sprache, SSH, einen Paketmanager und erst dann den Broker? Jede dieser Schichten bringt eigene Schwachstellen mit, verbraucht eigenen Speicher und fügt eine weitere Sache hinzu, die kaputtgehen kann, während eine Geräteflotte vom Server nur drei Dinge braucht: viele nahezu inaktive Verbindungen halten, jedes Gerät zuverlässig authentifizieren und Nachrichten verlustfrei weiterleiten.</p>
<p>So ist <a href="https://github.com/zeljan-alduk/urMQTT">urMQTT</a> entstanden: ein Unikernel, der selbst <em>der</em> MQTT-Broker <em>ist</em>. Kernel, TCP/IP, TLS 1.3, MQTT und eine eingebettete Datenbank in einem einzigen Rust-Binärprogramm, das von einem einzigen ISO gestartet wird. Es gibt keine Prozesse, keine Systemaufrufe, keine Shell. Die erste Plattform, auf der es gebaut und getestet wird, ist aarch64, und derselbe Code geht weiter in Richtung x86_64, über UEFI und klassisches BIOS, und in Richtung echter Hardware: Das Ziel ist ein einziges ISO, das auf all dem startet. Das Projekt ist Open Source unter der Lizenz MIT oder Apache 2.0, und in diesem Text möchten wir festhalten, wo wir stehen, wie es im Inneren aufgebaut ist und was wir unterwegs gelernt haben.</p>
<h2 id="was-unikernel-genau-bedeutet">Was "Unikernel" genau bedeutet</h2>
<p>Ein üblicher Broker ist ein Programm, das auf einem Betriebssystem läuft. urMQTT hat kein Betriebssystem unter sich: Es startet sich selbst über die UEFI-Firmware und den Limine-Bootloader, richtet eigene Seitentabellen ein, programmiert den Interrupt-Controller und den Timer, findet virtio-Geräte über PCIe, bezieht per DHCP eine Adresse und lauscht auf einem einzigen Port. Alles, was sonst Kernel, Treiber, Netzwerkstack und Anwendung leisten, ist hier ein einziges ELF, das auf der Ebene EL1 läuft.</p>
<p>Der Vergleich mit einer typischen Installation spricht für sich:</p>
<table>
<thead>
<tr>
<th></th>
<th>Typischer Broker unter Linux</th>
<th>urMQTT</th>
</tr>
</thead>
<tbody>
<tr>
<td>Exponierte Dienste</td>
<td>SSH, Broker, oft auch ein Port für Metriken oder Administration</td>
<td><strong>nur TCP 8883</strong> (MQTT über TLS 1.3 mit mTLS)</td>
</tr>
<tr>
<td>Code im Image</td>
<td>Kernel, Userland, libc, OpenSSL, Laufzeitumgebung</td>
<td><strong>ein einziges Rust-ELF</strong> (plus gevendortes SQLite)</td>
</tr>
<tr>
<td>Fernverwaltung</td>
<td>Shell, Konfigurationsdateien, Paketupdates</td>
<td><strong>keine.</strong> Eine neue Version ist ein neues ISO</td>
</tr>
<tr>
<td>Speicher pro inaktiver Verbindung</td>
<td>Kilobytes (Socket-Puffer, TLS-Objekte, Heap)</td>
<td><strong>Ziel ≤ 512 B</strong></td>
</tr>
<tr>
<td>Mehrkernmodell</td>
<td>Threads und Sperren oder eine einzelne Ereignisschleife</td>
<td><strong>ein Thread pro Kern, kein gemeinsamer Zustand, Run-to-Completion</strong></td>
</tr>
<tr>
<td>Code-Integrität</td>
<td>Mappings, die zugleich beschreibbar und ausführbar sind, sind möglich</td>
<td><strong>W^X, gesperrte Seitentabellen, PAC/BTI</strong></td>
</tr>
</tbody>
</table>
<p>Die Prioritäten lauten in dieser Reihenfolge: Sicherheit, Skalierbarkeit, Mehrkernleistung, geringer Footprint und unveränderliche Installation. Stehen zwei Ziele im Konflikt, gewinnt dasjenige, das in dieser Liste weiter oben steht.</p>
<h2 id="wo-wir-jetzt-stehen">Wo wir jetzt stehen</h2>
<p>Das Projekt befindet sich in der Pre-Alpha-Phase, aber siebzehn Entwicklungsmeilensteine, von M0 bis M17, sind abgeschlossen. Heute bedient urMQTT MQTT 3.1.1 über TLS 1.3 mit obligatorischen Client-Zertifikaten und hält den Zustand in SQLite auf einer rohen Festplatte. Kurz zusammengefasst, was bereits funktioniert:</p>
<ul>
<li><strong>Kernel:</strong> eigene Ausnahmevektoren mit dekodierten Berichten, eigene Seitentabellen mit durchgesetztem W^X, Guard-Pages, PAC und BTI, wo der Prozessor sie besitzt, GICv3 und GICv2, generischer Timer, SMP bis zu 64 Kernen.</li>
<li><strong>Hardware-Erkennung</strong> aus DTB oder ACPI in einer einzigen Plattformbeschreibung, mit identischem Ergebnis aus beiden Quellen.</li>
<li><strong>Treiber:</strong> PCIe ECAM, MSI-X, virtio-net, virtio-blk, virtio-scsi und virtio-rng sowie ein CSPRNG, der mit Hardware-Entropie initialisiert wird.</li>
<li><strong>Netzwerk:</strong> eigenes DHCPv4, ARP, ICMP und striktes IPv4, dazu ein eigenes TCP nur für die Serverseite: SYN-Cookies, hierarchisches Timer-Rad, NewReno, Zusammenfassen von ACKs, kein TIME_WAIT-Zustand.</li>
<li><strong>TLS:</strong> nur TLS 1.3, obligatorisches mTLS, Geräteidentität aus CN oder SAN-URI; der Handshake läuft über rustls, danach folgt eine eigene Record-Schicht mit AES-128-GCM und ChaCha20-Poly1305. Sitzungswiederaufnahme mit PSK-Tickets, Sperrliste, Admission Control bereits beim Handshake.</li>
<li><strong>MQTT:</strong> Kopierfreier Codec, gefuzzt und mit echten mosquitto-Clients getestet; QoS 0 und 1, Wildcards, Retained Messages, Last Will (LWT), persistente Sitzungen, ACL mit Beschränkung auf <code>devices/{id}/#</code>, Ratenbegrenzung pro Gerät.</li>
<li><strong>Speicherung:</strong> SQLite im Kernel über ein VFS auf der rohen Festplatte, im WAL-Modus, mit Integritätsprüfung beim Start; Geräteregister, Sperrungen, Retained Messages, persistente Sitzungen mit Nachrichtenwarteschlangen für Geräte, die offline sind; pro Kern ein eigenes Telemetrie-Journal mit CRC je Eintrag und Wiederherstellung nach einem Absturz.</li>
<li><strong>Betrieb und Überwachung:</strong> Watchdog-Timer für Handler, Health-Check, begrenztes Logging, Syslog und statsd über UDP, eine Admission Control, die Wiederverbindungsstürme übersteht.</li>
<li><strong>Auslieferung:</strong> bitweise identische Rebuilds, die in der CI geprüft werden, ISO und rohes GPT-Festplattenabbild, minisign-Signaturen, Anleitungen für KVM, Proxmox und Hetzner.</li>
</ul>
<p>Jeder Schritt ist mit dem Meilenstein gekennzeichnet, der ihn gebracht hat, und die Datei <code>docs/PROGRESS.md</code> im Repository ist die einzige Quelle der Wahrheit darüber, was tatsächlich funktioniert.</p>
<h2 id="die-architektur-in-drei-bildern">Die Architektur in drei Bildern</h2>
<p>Das gesamte Design ist in <code>docs/ARCHITECTURE.md</code> beschrieben, und die Gründe für jede wichtigere Entscheidung sind als ADRs in <code>docs/DECISIONS.md</code> festgehalten. Hier ein Überblick.</p>
<h3 id="schichten">Schichten</h3>
<figure><img loading="lazy" alt="Die Schichten von urMQTT: vom Broker über MQTT-Codec, TLS, TCP und Netzwerk bis zu den virtio-Treibern und der Architekturschicht, alles in einem einzigen ELF auf der Ebene EL1." src="https://aldo.tech/blog/urmqtt/img/layers.svg" /><figcaption>Die Schichten von urMQTT: vom Broker über MQTT-Codec, TLS, TCP und Netzwerk bis zu den virtio-Treibern und der Architekturschicht, alles in einem einzigen ELF auf der Ebene EL1.</figcaption></figure>
<p>Von oben nach unten: Broker (Sitzungen, Topic-Baum, QoS, Retained Messages, Last Wills, ACL, Limits), MQTT-Codec, TLS, TCP, Netzwerkschicht und schließlich die Treiber. Neben diesem Hauptpfad steht die Speicherung (SQLite und Telemetrie-Journal), und unter allem liegen Speicherverwaltung, Plattformerkennung und eine Konfiguration, die zur Build-Zeit in Konstanten übersetzt wird, zusammen mit dem eingebetteten CA-Zertifikat, dem Serverzertifikat und dem Schlüssel. Der Hypervisor mit den virtio-Geräten ist das Einzige, was außerhalb des Images liegt.</p>
<h3 id="ein-thread-pro-kern-kein-gemeinsamer-zustand">Ein Thread pro Kern, kein gemeinsamer Zustand</h3>
<figure><img loading="lazy" alt="Ausführungsmodell: Jeder Datenkern hat seine eigene Schleife und seinen eigenen Teil des Brokers, und der höchste Kern führt nur SQLite aus. Die Kerne kommunizieren ausschließlich über SPSC-Ringe." src="https://aldo.tech/blog/urmqtt/img/cores.svg" /><figcaption>Ausführungsmodell: Jeder Datenkern hat seine eigene Schleife und seinen eigenen Teil des Brokers, und der höchste Kern führt nur SQLite aus. Die Kerne kommunizieren ausschließlich über SPSC-Ringe.</figcaption></figure>
<p>Jeder Datenkern dreht seine eigene Schleife mit eigenem TCP-Stack, eigenen TLS-Zuständen, einem eigenen Teil des Brokers (Sitzungen und Topic-Baum), einem Heap-Cache und einer Telemetrie-Partition. Die Kerne teilen keinerlei veränderlichen Zustand, sondern nur begrenzte sperrfreie Ringe, je einen für jedes geordnete Kernpaar. Der höchste Kern führt SQLite aus und sonst nichts.</p>
<p>Die Netzwerkkarte hat heute ein Warteschlangenpaar, das Kern 0 hält. Dieser lenkt jeden MQTT-Fluss anhand des Toeplitz-Hashes seines Vierertupels auf einen der Arbeitskerne und sendet, was die Arbeitskerne zurückgeben. Eine Sitzung lebt auf ihrem Heimatkern, der durch den Hash der Client-ID bestimmt wird, sodass Übernahme, Last Wills und Persistenz nie Kerngrenzen überschreiten. Eine Verbindung, die auf einem anderen Kern landet, behält dort ihr TCP und TLS und reicht den gewöhnlichen MQTT-Verkehr mit kreditbasierter Flusskontrolle an den Heimatkern weiter. Jede weitergeleitete Nachricht wird zudem auf die übrigen Broker-Teile verteilt, von denen jeder eine Replik der Retained Messages hält.</p>
<p>Drei Regeln halten dieses Modell zusammen. <strong>Run-to-Completion:</strong> Ein empfangenes Paket wird vollständig auf einem einzigen Kern verarbeitet, von TCP über das Entschlüsseln von TLS an Ort und Stelle, das Parsen von MQTT und das Routing bis zur verschlüsselten Antwort, ohne Aufgabenwarteschlangen und ohne Kopieren in Anwendungspuffer. <strong>Adaptives Polling:</strong> Unter Last werden die Empfangswarteschlangen aktiv abgefragt, im Leerlauf kehrt der Kern zu Interrupts und <code>WFI</code> zurück; die Interrupt-Handler quittieren den Interrupt nur und markieren die Arbeit. <strong>Watchdog-Timer:</strong> Ein 10-ms-Tick pro Kern fängt Handler ab, die ihr Budget überschreiten.</p>
<h3 id="das-leben-eines-publish">Das Leben eines PUBLISH</h3>
<figure><img loading="lazy" alt="Der Weg einer PUBLISH-Nachricht mit QoS 1: vom TLS-Record auf der Netzwerkkarte über Entschlüsselung, ACL-Prüfung und Routing bis zum PUBACK, das in einem Segment mit dem TCP-ACK verschmolzen wird." src="https://aldo.tech/blog/urmqtt/img/publish.svg" /><figcaption>Der Weg einer PUBLISH-Nachricht mit QoS 1: vom TLS-Record auf der Netzwerkkarte über Entschlüsselung, ACL-Prüfung und Routing bis zum PUBACK, das in einem Segment mit dem TCP-ACK verschmolzen wird.</figcaption></figure>
<p>Das Gerät sendet einen TLS-Record. Die Netzwerkkarte übergibt ihn per Deskriptor ohne Kopieren an die Schleife des Kerns. Die Schleife prüft TCP, entschlüsselt TLS an Ort und Stelle und parst MQTT aus dem geliehenen Puffer, dann prüft sie im Speicher die ACL, die Ratenbegrenzung und den Pfad durch den Topic-Baum. Interessiert sich ein anderer Kern für das Topic, erhält er eine Routing-Nachricht mit einer Referenz auf die Nutzlast und der Topic-Kennung; die Datenbank erhält einen Telemetrieeintrag oder die Aktualisierung einer Retained Message, gebündelt in einem Paket. Das PUBACK wird direkt in den Ausgangspuffer verschlüsselt und mit dem TCP-ACK verschmolzen, sodass nur ein einziges Segment zum Gerät geht. Einen anderen Kern kostet die Nachricht eine Verschlüsselung pro Abonnent im Moment des Sendens.</p>
<h2 id="was-eine-verbindung-kostet">Was eine Verbindung kostet</h2>
<p>Das Ziel sind <strong>höchstens 512 Byte pro inaktiver TLS-Verbindung</strong>, mit der Tendenz zu 256. Solange eine Verbindung ruht, wird für sie nichts in einem Puffer gehalten: Empfangene Daten werden direkt aus dem Netzwerkframe verarbeitet, und die Puffer für Senden, Records und MQTT existieren nur, solange etwas unvollendet oder unbestätigt ist. Der Kernel misst das auch selbst: exakte Heap-Zähler in angeforderten Bytes und in den tatsächlich belegten Blöcken, alle zehn Sekunden geschrieben als Heap über der Grundlinie pro offener Verbindung.</p>
<table>
<thead>
<tr>
<th>Teil</th>
<th style="text-align: right;">Größe</th>
<th>Wie</th>
</tr>
</thead>
<tbody>
<tr>
<td>TCP-Kontrollblock</td>
<td style="text-align: right;">96 B</td>
<td>gepackte Flags, 16-Bit-RTT/RTO, Slot im Timer-Rad, kein Puffer im Leerlauf</td>
</tr>
<tr>
<td>Slot in der TCP-Tabelle</td>
<td style="text-align: right;">4–8 B</td>
<td>nur der Verbindungsindex, Tabelle höchstens halb voll</td>
</tr>
<tr>
<td>Verbindung und Zustand des TLS-1.3-Records</td>
<td style="text-align: right;">152 B</td>
<td>Secret und Sequenz pro Richtung (40 B); Schlüssel und IV werden pro Record abgeleitet</td>
</tr>
<tr>
<td>MQTT-Verbindung</td>
<td style="text-align: right;">48 B</td>
<td>Identität und Last Will liegen nur so lange auf dem Heap, wie sie existieren</td>
</tr>
<tr>
<td>MQTT-Sitzung, heiße Form</td>
<td style="text-align: right;">64 B</td>
<td>ein Abonnement pro Zeile, internierte Knotenkennungen</td>
</tr>
<tr>
<td>Abonnement <code>devices/&lt;id&gt;/cmd/#</code></td>
<td style="text-align: right;">0 Baumknoten</td>
<td>relativ zur Sitzungskennung gespeichert, allen Geräten gemeinsam</td>
</tr>
</tbody>
</table>
<p>Gemessen unter HVF mit vier virtuellen Kernen, davon drei Arbeitskerne, wobei jedes Gerät eine mTLS-Verbindung und ein Abonnement mit QoS 1 hält, einschließlich der Rundung durch den Allokator:</p>
<table>
<thead>
<tr>
<th style="text-align: right;">Verbindungen</th>
<th style="text-align: right;">Bytes pro Verbindung</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>Der Pool der Netzwerkframes ist mit etwa 2 MiB fest, das sind ungefähr 200 B pro Verbindung bei zehntausend Verbindungen und etwa 2 B bei einer Million. Mehr konnten wir nicht messen: Das Benutzer-Netzwerk von QEMU unter macOS hat bei 10 197 aufgehört, Verbindungen anzunehmen, ungefähr an der Grenze von 10 240 Deskriptoren des QEMU-Prozesses. Die Zahl sinkt, je mehr die festen Anteile sich auf mehr Verbindungen verteilen, sie wächst also nicht mit deren Anzahl.</p>
<h2 id="das-sicherheitsmodell">Das Sicherheitsmodell</h2>
<p>Sicherheit ist die erste Priorität und gewinnt jeden Konflikt mit den anderen Zielen. Das Modell hat mehrere Schichten:</p>
<ul>
<li><strong>Ein einziger Port.</strong> TCP 8883, nur TLS 1.3, obligatorische Client-Zertifikate, signiert von der im Image eingebetteten CA. Die MQTT-Client-ID muss der Geräte-ID aus dem Zertifikat entsprechen.</li>
<li><strong>Kein Zugang.</strong> Keine Shell, keine Konsoleneingabe, kein SSH, keine Administrations-API, kein dynamischer Code, kein über das Netzwerk erreichbares SQL und kein Dateisystem.</li>
<li><strong>Speichersicherheit.</strong> Rust, mit <code>forbid(unsafe_code)</code> in den Protokollpaketen; jeder <code>unsafe</code>-Block hat einen Kommentar <code>// SAFETY:</code>, den ein Lint prüft, und bei externer Eingabe gibt es keine Panik: Clippy verbietet in den Paketen auf dem Datenpfad <code>unwrap</code>, <code>expect</code>, Indexierung und <code>panic!</code>.</li>
<li><strong>Abschwächung von Exploits.</strong> W^X wird durch die Kodierung der Deskriptoren und das Bit <code>SCTLR_EL1.WXN</code> durchgesetzt und bei jedem Start auditiert. Die Seitentabellen sind nach dem Start schreibgeschützt, ohne beschreibbaren Alias. Die untere Hälfte des Adressraums ist ausgeschlossen. Jeder Stack hat Guard-Pages, und Ausnahmen laufen auf einem eigenen Stack. PAC und BTI werden aktiviert, wenn der Prozessor sie unterstützt, und das prüfen die Smoke-Tests.</li>
<li><strong>Missbrauchsresistenz.</strong> SYN-Cookies; die Admission Control lehnt neue Verbindungen vor jeglicher Kryptografie ab, wenn der Heap knapp ist, die Datenbank zurückliegt, zu viele Handshakes pro Kern oder pro Quelladresse laufen oder die Handshake-Rate zu hoch ist. Ratenbegrenzung pro Gerät, begrenzte Warteschlangen, ein an den Rückstau des Brokers gekoppeltes TCP-Fenster, begrenztes Logging, Watchdog-Timer und Health-Check.</li>
<li><strong>Unveränderlichkeit.</strong> Konfiguration, CA und Serverschlüssel sind im Image eingebettet. Jede Änderung bedeutet ein neues Image.</li>
</ul>
<p>Das Bedrohungsmodell ist in <code>docs/SECURITY.md</code> festgehalten.</p>
<h2 id="speicherung-ohne-dateisystem">Speicherung ohne Dateisystem</h2>
<p>Die Daten liegen auf einer separaten Festplatte, virtio-blk oder virtio-scsi, auf der es kein Dateisystem gibt. Das Layout ist minimal und eigen:</p>
<pre><code>LBA 0      [ Superblock: Magic · Layout-Version · UUID · Regionstabelle · CRC-32C ]
LBA 8, 16  [ Tabelle der Dateigrößen, Slots A/B (der neueste gültige gewinnt) ]
LBA 64     [ SQLite-Hauptdatenbank ½ ][ SQLite-WAL ⅛ ][ Journal 1 MiB ][ Telemetrie-Journal: 8 Partitionen … ]
letzte 4 KiB [ Bereich für den Selbsttest des Blockgeräts beim Start ]
</code></pre>
<p>SQLite (gevendortes 3.54.1, mit Prüfung des Digests beim Build) wird mit clang ohne Standardbibliothek in den Kernel übersetzt und nutzt einen festen <code>memsys5</code>-Heap. Sein VFS bildet drei feste Dateien auf Regionen der Festplatte ab. Es läuft im WAL-Modus mit <code>locking_mode=EXCLUSIVE</code>, braucht also keinen Index im gemeinsamen Speicher, und mit <code>synchronous=FULL</code>; findet <code>quick_check</code> beim Start einen Fehler, hält sich das System sicher an. In der Datenbank liegen das Geräteregister, die Sperrungen, die Retained Messages und die persistenten Sitzungen mit Warteschlangen für QoS-1-Nachrichten für Geräte, die nicht verbunden sind.</p>
<p>Der Kern für die Datenbank, derjenige mit der höchsten Nummer, führt SQLite aus und sonst nichts. Die Datenebene schickt ihm Zustandsänderungen über einen sperrfreien SPSC-Ring; er sammelt sie alle 5 ms oder alle 512 Operationen in einer Transaktion und gibt die Bestätigungen über einen zweiten Ring zurück. Eine leere Festplatte wird beim ersten Start formatiert; alles andere wird niemals überformatiert.</p>
<p>Die Telemetrie geht einen anderen Weg. Jedes akzeptierte PUBLISH auf Topics aus <code>storage.telemetry_topics</code> landet in der Partition des Telemetrie-Journals seines Kerns: ein Ring aus Segmenten mit CRC pro Eintrag, gesalzen mit der Segmentsequenz, sodass alte Durchläufe des Rings nie wieder auftauchen. Die Kerne übergeben die Einträge über einen eigenen Ring ohne Kopieren an die Datenbank, die sie alle 5 ms oder alle 256 KiB ausschreibt. Beim Start behält die Wiederherstellung jeden ausgeschriebenen Eintrag, schneidet den unvollständigen Rest ab und setzt in einem neuen Segment fort. Die Aufbewahrung erfolgt nach Größe, durch den Ring, und nach Alter, durch <code>retention_days</code>. Wir haben das mit einer Million Einträgen und mit <code>kill -9</code> mitten im Schreiben überprüft.</p>
<h2 id="die-zahlen">Die Zahlen</h2>
<p>Das Lastwerkzeug in <code>tools/loadgen</code> leistet drei Dinge: einen Sturm von TLS-1.3-Handshakes mit mTLS und CONNECT, langlebige Verbindungen mit PINGREQ, die jede Sekunde Telemetrie veröffentlichen, und Wiederverbindungsstürme. <code>make bench</code> auf einem Apple M4 Pro unter HVF liefert:</p>
<table>
<thead>
<tr>
<th>Arbeitskerne</th>
<th>Handshakes/s im Gast</th>
<th>Skalierung</th>
<th>durch das QEMU-Benutzernetzwerk (Verbindungen/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>Die Zahl im Gast, bei der Client und Server innerhalb derselben virtuellen Maschine liegen und jeder Arbeitskern für sich arbeitet, zeigt, dass die Datenebene linear skaliert. Die Netzwerkzahl wird durch das einthreadige Benutzernetzwerk von QEMU begrenzt, nicht durch den Gast; auch das ist als ADR-032 festgehalten.</p>
<p>Wiederverbindungsstürme testen wir, indem sich alle Geräte gleichzeitig verbinden, abgelehnte Versuche mit Verzögerung wiederholen und dann alle ohne DISCONNECT abstürzen. Der Parameter <code>heap_limit_mb=</code> bringt die Admission Control im Entwicklungsmodus dazu, den Heap für kleiner zu halten, als er ist, sodass sich ein Sturm, der größer ist als der Heap, mit wenigen Sockets ausprobieren lässt. In einer solchen Runde mit 3 000 Geräten verbanden sich 1 368 in 36 Sekunden, und 5 619 Versuche wurden ordnungsgemäß abgelehnt, bei einem Limit von 8 MiB; der Speicher kehrte nach dem Sturm zurück.</p>
<h2 id="wie-wir-arbeiten">Wie wir arbeiten</h2>
<p>Das Projekt wird von einer schriftlichen Spezifikation, <code>PROMPT.md</code>, mit einem Meilensteinplan geleitet, und jeder Meilenstein endet mit demselben Ritual: <code>make check</code> und <code>make smoke</code> grün, ein Commit mit der Kennzeichnung <code>M&lt;n&gt;</code>, ein Eintrag in <code>docs/PROGRESS.md</code> und ein ADR für jede Entscheidung, die nicht offensichtlich ist. Die Regeln sind kurz, und wir verhandeln nicht darüber: keine Panik bei externer Eingabe, <code>unsafe</code> nur in Hardware- und FFI-Paketen und immer mit Kommentar, keine Allokationen auf dem heißen Pfad, ein Kommentar zu jeder Reihenfolge atomarer Operationen, jedes Limit kommt aus der Konfiguration, und absichtliche vorläufige Teile scheitern laut.</p>
<p>Die CI leistet mehr, als wir von Hand schaffen würden: fmt, Clippy für den Host und für <code>aarch64-unknown-none</code>, typos, actionlint und shellcheck; Tests unter Linux und macOS auf arm64; den Build des Kernels für die Entwicklungs- und die Produktionskonfiguration mit der ELF-Größe in der Zusammenfassung; den Start unter QEMU mit allen Szenarien des Smoke-Tests und das Sichern der seriellen Logs; cargo-deny für Advisories, Lizenzen und Quellen; eine Minute Fuzzing pro Ziel bei allen elf Zielen und nachts zehn Minuten pro Ziel mit einem zwischengespeicherten Korpus. Dazu CodeQL, OpenSSF Scorecard, Dependabot und eine Veröffentlichung mit SLSA-Provenienz. Jede fremde Action ist auf den vollständigen Commit-SHA festgepinnt.</p>
<p>Reproduzierbarkeit nehmen wir wörtlich: <code>make repro</code> baut HEAD zweimal in frischen Arbeitsbäumen und vergleicht ELF, ISO und rohes Abbild Byte für Byte. Die Releases sind mit minisign signiert, und die Artefakte lassen sich mit <code>gh attestation verify</code> prüfen. Das Produktions-Image enthält den privaten Serverschlüssel, daher ist das ISO selbst ein Geheimnis; die Images bei den Releases auf GitHub sind Entwicklungs-Images mit temporären Schlüsseln und dienen nur zum Ausprobieren.</p>
<h2 id="was-wir-gelernt-haben">Was wir gelernt haben</h2>
<p>Einige Lektionen, die den Plan verändert haben:</p>
<ul>
<li><strong>Ein eigenes TCP hat sich doch gelohnt.</strong> Der Prototyp lief auf smoltcp und hat uns bis M13 gute Dienste geleistet. Aber ein Server, der Hunderttausende Verbindungen hält, braucht kein Client-TCP, er braucht SYN-Cookies, ein Timer-Rad und einen Zustand von 96 Byte. In M14 haben wir smoltcp durch einen eigenen Stack nur für die Serverseite ersetzt, mit Tests unter simuliertem Paketverlust und Fuzzing der Segmente.</li>
<li><strong>Messen Sie den Speicher von innen.</strong> Schätzungen pro Verbindung waren nutzlos, bis der Kernel selbst begann, den Heap in Allokatorblöcken zu zählen und nicht in angeforderten Bytes. Erst dann sahen wir, wohin die 512 Byte tatsächlich gehen, und kamen auf 494 herunter.</li>
<li><strong>Das Testwerkzeug ist Teil des Produkts.</strong> Die größte Einschränkung war nicht der Kernel, sondern das Benutzer-Netzwerk von QEMU und die Zahl der ephemeren Ports auf dem Entwicklungsrechner. Für hunderttausend und mehr Verbindungen braucht man <code>vmnet-shared</code> unter macOS oder Linux mit KVM, einem tap-Interface und mehreren Client-Adressen.</li>
<li><strong>SQLite ohne Betriebssystem ist möglich</strong>, aber nur unter strengen Bedingungen: fester Heap, exklusives Locking ohne gemeinsamen Speicher, ein VFS, das von Dateien nichts weiß, und eine Admission Control, die die Datenbank nicht zurückfallen lässt.</li>
<li><strong>Wiederverbindungsstürme sind der Normalzustand</strong>, keine Ausnahme. Fällt das Mobilfunknetz aus und kehrt zurück, klopfen Tausende Geräte gleichzeitig an. Deshalb lehnt die Admission Control eine Verbindung vor jeglicher Kryptografie ab, und jede abgelehnte Verbindung kostet fast nichts.</li>
</ul>
<h2 id="was-als-nachstes-kommt">Was als Nächstes kommt</h2>
<p>Die Arbeit nach M17 ist in drei Richtungen geplant, und die Reihenfolge dazwischen ist in <code>docs/PLAN.md</code> festgehalten. Nichts davon ist bisher gebaut.</p>
<ul>
<li><strong>Ein ISO für mehrere Plattformen.</strong> Dieselbe Datei <code>urmqtt-&lt;env&gt;-&lt;version&gt;.iso</code> und ein einziges rohes GPT-Abbild, die auf aarch64-UEFI, x86_64-UEFI und x86_64-BIOS starten, in virtuellen Maschinen und auf echter Hardware, beginnend mit einem Intel-PC. Weiterhin ein Appliance-Gerät mit allem darin, reproduzierbar und signiert.</li>
<li><strong>Rohe Festplatte, kein Dateisystem.</strong> Eine Formatspezifikation auf Byte-Ebene, das Host-Werkzeug <code>urmqtt-disk</code>, das die Parser mit dem Kernel teilt, Ausrichtung der Regionen auf 1 MiB, Wachstum der Festplatte mit den Daten, Discard für Thin-Provisioning-Festplatten, A/B-Snapshots der Datenbank, Bit-Prüfung und, optional, Verschlüsselung mit AES-256-XTS mit Hardware-AES.</li>
<li><strong>Nachweislich sicher.</strong> Sperrung und Zulassungsliste von Geräten zur Laufzeit über einen signierten Steuerkanal, ohne neues Image; differenzielles und Konformitätstesten des eigenen TLS und TCP; KASLR, Stack-Schutz, kCFI, MTE und ein gehärteter Allokator; SQLite auf der Ebene EL0 isoliert; Secure Boot; SBOM und cargo-vet; und am Ende ein Paket für die externe Revision.</li>
</ul>
<p>Plattformen, die nur Nicht-virtio-Geräte anbieten, wie AWS ENA oder Azure MANA, werden vorerst nicht unterstützt. Raspberry Pi 4 und 5 als KVM-Host, Hetzner CAX und Oracle Ampere A1 sind dokumentiert und vom Code unterstützt, aber noch nicht auf einem echten Dienst ausprobiert.</p>
<h2 id="probieren-sie-es-aus">Probieren Sie es aus</h2>
<p>Das Repository befindet sich unter <a href="https://github.com/zeljan-alduk/urMQTT">github.com/zeljan-alduk/urMQTT</a>. Mit Rust samt festgepinnter Nightly-Toolchain und QEMU genügen <code>make certs</code>, <code>make run</code> und ein mosquitto-Client für das erste PUBLISH über mTLS auf Ihrem eigenen Rechner. Die Warnung vom Anfang gilt auch am Ende: Dies ist Pre-Alpha, und alles, was funktioniert, steht in <code>docs/PROGRESS.md</code>. Fehlermeldungen, Fragen zum Design und Sicherheitsmeldungen gemäß der Anleitung in <code>SECURITY.md</code> sind willkommen. Über jeden weiteren Meilenstein berichten wir hier.</p>]]></content:encoded>
      <category>rust</category>
      <category>mqtt</category>
      <category>iot</category>
      <category>unikernel</category>
      <category>Open Source</category>
    </item>
    <item>
      <title>Wir starten einen Blog: Notizen aus der Praxis</title>
      <link>https://aldo.tech/de/blog/wir-starten-einen-blog/</link>
      <guid isPermaLink="true">https://aldo.tech/de/blog/wir-starten-einen-blog/</guid>
      <pubDate>Sat, 17 Oct 2026 08:00:00 +0000</pubDate>
      <description>Warum ein Unternehmen, das Terminals, Apps und Elektronik für den ÖPNV baut, zu schreiben beginnt, und worüber Sie hier lesen werden.</description>
      <content:encoded><![CDATA[<p>Das meiste von dem, was wir bauen, sieht niemand. Die Software im Terminal an der Bustür, die Firmware in einem Gerät, das nie ein Display bekommt, der Server, der über Nacht die Transaktionen des Tages in Berichte verwandelt. Wenn alles läuft, läuft es leise. Wenn nicht, merkt es jeder.</p>
<p>In diesem Blog halten wir fest, was wir auf diesem Weg gelernt haben. Keine Marketingtexte, sondern konkrete Notizen: was schiefgegangen ist, wie wir es gelöst haben und was wir heute anders machen würden.</p>
<h2 id="woruber-wir-schreiben-werden">Worüber wir schreiben werden</h2>
<ul>
<li><strong>Fahrgeldeinzug im ÖPNV.</strong> Kontaktlose Karten, SAM-Module, Offline-Betrieb und alles, was ein Terminal in einem Fahrzeug aushalten muss, das zwölf Stunden am Tag rüttelt.</li>
<li><strong>Mobile Apps im Einsatz.</strong> Android und Flutter auf Geräten, die keine gewöhnlichen Telefone sind: mit Kartenlesern, Druckern und Akkus, die eine ganze Schicht durchhalten müssen.</li>
<li><strong>Backend und Flottenüberwachung.</strong> Wie man erfährt, dass ein Gerät im Fahrzeug ein Problem hat, bevor der Fahrer es meldet.</li>
<li><strong>Hardware.</strong> Vom Prototyp auf dem Tisch bis zur Platine in der Serienfertigung, und alle Fehler dazwischen.</li>
<li><strong>Lokale KI-Modelle.</strong> Wo Modelle auf eigener Hardware sinnvoll sind und wo Cloud und API schlicht die bessere Wahl sind.</li>
</ul>
<h2 id="wie-wir-schreiben-werden">Wie wir schreiben werden</h2>
<p>Kurz und konkret, mit Code und Zahlen, wo wir sie haben. Wir veröffentlichen, wenn wir etwas zu sagen haben, nicht nach Plan. Die Beiträge entstehen auf Kroatisch und werden in alle Sprachen dieser Website übersetzt; per <a href="https://aldo.tech/de/blog/feed.xml">RSS</a> bleiben Sie auf dem Laufenden.</p>
<p>Wenn Sie ein Thema interessiert oder Sie an einem ähnlichen Problem arbeiten, schreiben Sie uns an <a href="mailto:info@aldo.tech">info@aldo.tech</a>.</p>]]></content:encoded>
      <category>einstieg</category>
    </item>
  </channel>
</rss>
