urMQTT auf einem echten ARM64-VPS: kein Bildschirm, kein Hardware-Zufallszahlengenerator und ein Handshake, der zu langsam war
Was nach dem Text zu v0.3.0 geschah: der erste Lauf auf einem echten aarch64-KVM-Server (Installation auf dessen Datenträger, ein Stromausfall, Datenträgerwerte), ein Diagnose-Eintrag, der über das Netz berichtet, weil die Konsole unsichtbar ist, was er über den Zufallszahlengenerator herausgefunden hat, ein P-256-Assembler für ARM64, der volle Handshakes etwa 1,5-mal häufiger machte, und die Fehler, die wir unterwegs gemacht haben.

Der vorige Text, urMQTT v0.3.0, endete mit vier Punkten unter „Wie es weitergeht": eine Entscheidung zum öffentlichen Demo-Broker (D1), Fuzzing der PCI- und virtio-Parser und ein Test für die gesperrten Seitentabellen, ein Schlüsseldialog auf der Builder-Seite und Bare Metal. Keiner der vier wurde in dieser Runde erledigt, und das sagen wir zuerst. Stattdessen haben wir getan, was die ganze Zeit gefehlt hat: Wir haben urMQTT auf einem echten ARM64-Server unter KVM laufen lassen, und fast alles Folgende stammt von dieser einen Maschine. Der erste Punkt der alten Liste, CI, wurde aufgeräumt (der frühere Text und die Release Notes, die sie als rot bezeichneten, wurden berichtigt)
Der Hinweis vorab bleibt derselbe: Das Projekt ist Pre-Alpha, die Zahlen messen Code, Tests und bestimmte Versuche auf einer Maschine, nicht die Reife im Produktivbetrieb. Wo eine Zahl nicht auf der Zielmaschine gemessen wurde, steht das dabei.
Wie es sich zeitlich darstellte
Seit dem letzten Text (3. November, 02:43) bis heute (4. November, 03:18) hat main 29 neue Commits, insgesamt 244. Wie zuvor entsteht das Projekt mit Claude Code (die Commits tragen Co-Authored-By: Claude). Die Zahlen sind nach der Methode der letzten beiden Texte berechnet (alle .rs-Dateien außerhalb von vendor/ und fuzz/, #[test]-Funktionen, Überschriften ## ADR- in docs/DECISIONS.md); die Methode liefert am alten Commit dieselben Zahlen wie damals.
| Zeitpunkt | Commit | Zeit | Zeilen Rust | Tests | ADRs | Commits |
|---|---|---|---|---|---|---|
| letzter Text | f1a7fbc | 3. Nov., 02:43 | 115.148 | 962 | 133 | 215 |
| heute | efddf6e | 4. Nov., 03:18 | 117.084 | 978 | 134 | 244 |
Wachstum: +1.936 Zeilen, +16 Tests, +1 ADR. Wieder wenig neuer Code; der P-256-Assembler selbst (1.509 Zeilen) ist eingebunden (vendored) und zählt nicht zur Zeilenzahl.
Eine zweite echte Maschine
Bisher hatte das Projekt einen echten Server: einen Contabo-VPS mit AMD EPYC (x86_64, KVM), der für den Leistungsvergleich mit Mosquitto diente. ARM64 lief nur in QEMU, mit Hardwarebeschleunigung auf einem Mac und ohne sie in CI. Die neue Maschine ist ein netcup VPS 500 ARM G12.5: ein Neoverse-N1-Kern mit etwa 3 GHz, 2 vCPUs, 3,9 GiB RAM, eine virtio-Platte mit 64 GiB, QEMU/KVM mit UEFI-Firmware, eine virtio-Netzwerkkarte. Alles lief über die REST-API des Anbieters mit einem Kommandozeilen-Client: Wir laden die ISO hoch, hängen sie als virtuelle CD ein, stellen das Bootgerät um und schalten die Maschine aus und wieder ein.
Was wir darauf getan haben, der Reihe nach:
- Zuerst ein Snapshot des ursprünglichen Systems. Ein Online-Snapshot ist mit UEFI nicht möglich, daher wurde der Server ausgeschaltet und ein Offline-Snapshot erstellt.
- Installation. Der Installer der ISO schrieb das Image auf den Datenträger (der 30-Sekunden-Countdown eingerechnet, insgesamt etwa 40 Sekunden) und schaltete die Maschine wie vorgesehen selbst aus.
- Der Start vom installierten Datenträger bis zu einem TLS-Handshake dauerte etwa 35 Sekunden.
- Eine Retained-Nachricht überstand einen harten Stromausfall. Wir haben sie veröffentlicht, den Strom über die API getrennt, neu gestartet und sie zurückgelesen.
- Der installierte Datenträger, gemessen vom Diagnose-Eintrag (nur Lesen; der Schreibtest wurde übersprungen, weil das installierte Layout keinen unzugewiesenen Platz lässt, was beabsichtigt ist): sequenzielles Lesen 332 MiB/s, zufällige 4-KiB-Lesevorgänge 22.147 pro Sekunde mit p50 10,4 µs, p90 28,3 µs, p99 671 µs, Maximum 9,9 ms (2.000 Lesevorgänge).
Fünf Punkte sind keine Testsuite: Das ist eine Maschine, jeweils ein Lauf, und der Installationspfad wurde einmal auf dem echten Datenträger durchlaufen. Was es zeigt, ist, dass der eigene virtio-blk-Treiber des Kernels, der Installer und der Start auf einem echten Cloud-Gast funktionieren und nicht nur in einem Laboremulator.
Kein Bildschirm, also geht die Diagnose über das Netz
Der Diagnose-Eintrag aus v0.3.0 zeichnet seine Werte auf den Framebuffer. Auf diesem Server sagt die Konsole des Anbieters „Display output is not active": Die virtuelle Maschine hat eine virtio-Grafikkarte, für die wir keinen Treiber haben, und der serielle Port ist nicht freigegeben. Die Werte gab es, nur konnte sie niemand lesen.
Die Lösung ist ein Boot-Wort, diag-serve: Die Maschine führt dieselben Messungen aus, startet dann als normaler Live-Broker und veröffentlicht den Bericht als ein JSON-Dokument in der Retained-Nachricht $SYS/diag/report, die nur ein Operator-Zertifikat lesen darf. Ein kleines Werkzeug, urmqtt-diag-png, zeichnet aus diesem JSON das Dashboard mit demselben Code, den der Kernel auf dem Bildschirm benutzt (dasselbe Layout und dieselbe 8x8-Schrift), sodass das Bild der Bildschirm ist, den die Maschine gezeigt hätte:

Zwei Dinge wurden unterwegs dem Bildschirm hinzugefügt, weil sie uns fehlten: der CPU-Takt (der PMU-Zykluszähler gegen die Zeitbasis: hier 2,95 bis 2,98 GHz; wo nur ein Timer mit fester Rate lesbar ist, steht „not measurable", statt die Frequenz des Timers als die der CPU auszugeben) und ein Feld für den Zufallszahlengenerator.
Woraus der Zufallszahlengenerator besteht
Das Feld nennt, woraus der Generator initialisiert (geseedet) wurde, und testet frische Bytes aus jeder Hardwarequelle. Auf dieser Maschine steht dort:
- virtio-rng: kein Gerät.
RNDR(der eigene Befehl der CPU): auf dieser CPU nicht verfügbar. - Der EFI-Zufallszahlengenerator des Bootloaders: 8 von 8 Werten ungleich null.
- Der Rückfall auf CPU-Timing-Jitter (ADR-038): 1.024 Stichproben zu je 64 Schritten über den PMU-Zykluszähler, 3 Bit pro Stichprobe geschätzt, 768 Bit angerechnet (ein Viertel der Schätzung, beabsichtigt), Gesundheitstests bestanden.
- Die Ausgabe des Generators, ein Plausibilitätstest über 4.096 Byte: 49,6 % Einsen, der längste Lauf 16 Bit. Das ist eine Prüfung, dass nichts hängt oder verzerrt ist, keine statistische Zertifizierung.
Der Generator auf diesem Server beruht also auf dem Jitter-Rückfall und auf dem, was die Firmware übergeben hat. Genau das beschreibt ADR-038, und es ist die schwächste Initialisierung, die der Kernel akzeptiert. Wir nennen es nicht defekt: Die angerechneten Bits liegen über der Schwelle, die der Kernel verlangt (256), und der Schätzer ist konservativ. Wir können auch nicht beweisen, dass es so gut ist wie eine Hardwarequelle. Ob ein Produktiv-Image den Start ohne Hardwarequelle verweigern soll, sofern der Betreiber es nicht ausdrücklich zulässt, ist eine offene Frage an den Projektinhaber (sie steht in der Roadmap, unten). In der Praxis ist es auch eine Frage an den Anbieter: Ein Hardware-Zufallsgerät für die virtuelle Maschine würde sie klären.
Ein Handshake, der zu langsam war
Der Diagnose-Eintrag zeigte auch, was ein voller gegenseitiger TLS-Handshake auf dem Server kostet: 1,08 ms. Fast alles davon ist Public-Key-Arithmetik: zwei ECDSA-P-256-Prüfungen (das Clientzertifikat und der Nachweis des Clients) mit je etwa 508 µs, X25519 176 µs, eine ECDSA-Signatur 141 µs. Auf x86_64 hatten wir das früher gelöst (ADR-130, der OpenSSL/Intel-Assembler: P-256 drei- bis viermal schneller); auf ARM64 lief weiterhin der portable RustCrypto-Code.
Dasselbe haben wir für ARM64 getan (ADR-255). Der Assembler ist OpenSSLs ARMv8-nistz256 (Apache-2.0), aus ring 0.17.14 übernommen und mit Herkunft und jeder mechanischen Änderung dokumentiert eingebunden (vendored). Was in den Details zählte:
- Nur Ganzzahlen. Der Assembler benutzt keine NEON-Register. Die zwei Funktionen zur Tabellenauswahl, die das Original mit NEON umsetzt, haben wir weggelassen und durch konstantzeitige maskierte Scans in Rust ersetzt (sie kosten höchstens 1,4 µs einer Signatur von 12 µs).
- Sprungschutz. Der Kernel wird mit BTI und Pointer Authentication gebaut. Jeder der 8 exportierten Einstiegspunkte hat einen Landing Pad, im Disassembly des Kernels geprüft, und die sechs, die das Link-Register sichern, signieren es.
- Tests, denn das ist Kryptografie in der Basis, der wir vertrauen. Die gesamte x86_64-Suite läuft jetzt auch auf aarch64-Linux: Körper- und Skalararithmetik gegen eine Schulbuch-Referenz, die ganze Generatortabelle (2.368 Einträge) gegen RustCrypto, Signaturen Bit für Bit gleich denen von RustCrypto, Prüfurteile, ECDH mit Eingaben außerhalb der Kurve und mit null, und die Known-Answer-Tests, die bei jedem Start über die Produktivpfade laufen. In einer virtuellen Maschine mit vier Kernen unter einem Interrupt-Sturm liefen 59.526 Known-Answer-Runden mit 0 Abweichungen.
- Ein Zeittest mit Kontrolle und ein Signal, das unser eigener Test war. Eine absichtlich undichte Multiplikation erreicht |t| von 482 bis 1.755, der Test sieht also ein Leck in dieser Größenordnung. Auf einem Apple-Kern in einem Container erreichte ECDH 0,2 bis 2,1, Signieren 0,5 bis 1,9, Schlüsselerzeugung 0,8 bis 5,5. Auf dem arm64-Serverkern des CI-Runners erreichte das ECDSA-Signieren in drei Versuchen |t| = 8,6, 9,1 und 8,3, über unserer Grenze von 7, während ECDH (3,9) und Schlüsselerzeugung (1,9) auf derselben Maschine bestanden. Ein Ergebnis, das sich wiederholt, ist kein Rauschen, also schrieben wir es als offenen Befund auf (A255-W1) und hoben die Grenze für diesen einen Fall einige Stunden lang auf 12 an, damit der Build durchlaufen konnte. Dann fanden wir die Ursache, und es war das Testgerüst: Der Fall signierte eine feste Nachricht, sodass der dünn besetzte Schlüssel jedes Mal dieselbe Signatur erzeugte und die zufälligen Schlüssel jedes Mal eine andere, und das DER-Kodieren einer wiederholten Signatur ist schneller (|t| = 49 bis 57 allein für die Kodierung). Mit einer zufälligen Nachricht pro Messung in beiden Klassen erreicht der Fall auf dem Runner (einer virtuellen Neoverse-N2-Maschine) 1,4 und 1,8, die Nonce-Erzeugung allein 2,7 und 1,1, und die Grenze liegt wieder bei 7. Eines bleibt offen (A255-W2): Das Prozessor-Flag, das die Multiplikatorlaufzeit auf Arm-Kernen datenunabhängig macht (DIT), wird nirgends gesetzt, weder vom Kernel noch um die hostseitigen Aufrufe herum. Keine Messung zeigt einen Unterschied, aber die virtuelle Maschine des Runners stellt das Merkmal nicht bereit, also konnte der Test auch keinen zeigen.
Was es gebracht hat
Zuerst der Mikrobenchmark auf der Maschine, auf der wir entwickelt haben, einem Apple M4 Pro (nicht die Ziel-CPU, die Verhältnisse auf dem Server können daher abweichen), derselbe Code, den der Handshake aufruft, vorher und nachher:
| Operation | vorher (µs) | nachher (µs) | Verhältnis |
|---|---|---|---|
| Schlüsselerzeugung | 30,8 | 7,0 | 4,4x |
| ECDH P-256 | 135,2 | 25,0 | 5,4x |
| ECDSA-Signatur | 34,7 | 11,6 | 3,0x |
| ECDSA-Prüfung (unbekannter Schlüssel) | 147,4 | 29,1 | 5,1x |
| CA-Prüfung (registrierter Aussteller) | 54,2 | 9,6 | 5,6x |
| X25519 (unveränderter Code) | 20,4 | 20,1 | 1,0x |
| Handshake-Schätzung (Summe des Obigen) | 292 | 104 | 2,8x |
Dann die Wirklichkeit: derselbe Lastgenerator, dieselbe Methode wie im früheren Leistungstext (voller TLS-1.3-Handshake mit Clientzertifikat, ohne Wiederaufnahme, MQTT CONNECT, der Server schließt zuerst), ausgeführt von einer Maschine 9 ms vom Server entfernt, gegen denselben netcup-VPS, vor und nach der Änderung. Das Benchmark-Image hat gelockerte Handshake-Grenzen, wie im x86_64-Vergleich; drei Wiederholungen, die Zahl ist der Median.
| Threads | vorher (Verbindungen/s) | nachher (Verbindungen/s) | Verhältnis | Fehler |
|---|---|---|---|---|
| 32 | 848 (408-855) | 1.317 (957-1.317) | 1,55x | 0 |
| 64 | 827 (815-839) | 1.398 (1.352-1.416) | 1,69x | 0 |
| 128 | 778 (714-788) | 1.316 (1.313-1.383) | 1,69x | 0 |
| TLS-Anteil einer Verbindung, p50, 32 Threads | 18,5 ms | 13 ms |
Volle Handshakes pro Sekunde stiegen um etwa das 1,55- bis 1,7-Fache, nicht um das Dreifache, das die Arithmetik allein ergäbe. Der Grund ist einfach und sagenswert: Wir haben P-256 beschleunigt, während X25519 (176 µs), der TCP/IP-Stack, die Record-Schicht und MQTT unverändert sind, sodass der verbleibende Anteil an Gewicht gewinnt (Amdahlsches Gesetz). Der nächste Kandidat ist X25519.
Zur Orientierung neben dem x86_64-Server aus dem früheren Text. Das ist kein Wettrennen: andere CPUs, eine andere Kernzahl (4 gegen 2), ein anderer Abstand zum Generator (15 ms gegen 9 ms) und in beiden Fällen ein geteilter Lasthost, der während der Läufe 5 bis 60 % seiner eigenen CPU eingebüßt hat. Die vergleichbare Spalte sind die CPU-Kosten des Servers pro Handshake:
| Contabo, x86_64 EPYC (asm) | netcup, ARM64 N1 vorher | netcup, ARM64 N1 nachher | |
|---|---|---|---|
| Server-CPU pro Handshake | 0,26 ms (die Konsole des Brokers) | 1,08 ms (Diagnose) | 0,55 ms (Diagnose, ein Lauf, siehe unten) |
| Handshakes/s, 32 Threads | 169 (Bench) | 848 | 1.317 |
| Handshakes/s, 64 Threads | 278 (Bench) | 827 | 1.398 |
| Handshakes/s, 128 Threads | 413 (Bench) | 778 | 1.316 |
Am selben Tag lief der Diagnose-Eintrag noch einmal auf dieser Maschine, mit dem Assembler (in einem Build, der auch den Grafiktreiber aus dem Update am Ende enthält): Die CPU-Kosten des Servers für einen gegenseitigen TLS-Handshake betrugen 553 µs gegenüber 1,08 ms zuvor, 1,95-mal weniger. Die ECDSA-Verifikation dauerte 192 µs (zuvor 508 µs), das Signieren 78 µs (141 µs) und X25519 209 µs (176 µs; dieser Code ist unverändert, und der CPU-Takt dieses Laufs lag bei 2,58 GHz gegenüber 2,95 bis 2,98 GHz in den früheren, sodass sich einzelne Werte auf dieser geteilten Maschine um Dutzende Prozent verschieben). Es ist ein einziger Lauf auf einem geteilten Host. Dass die Handshake-Rate um das 1,55- bis 1,7-Fache stieg, während die eigenen Kosten des Servers pro Handshake um das 1,95-Fache sanken, passt zusammen: Der Rest einer Verbindung sind der Stack, der Generator und das Netz. Das Rauschen zwischen den Läufen ist in den Rohdateien sichtbar: Jeder Satz von dreien hat einen Ausreißer (408 vorher und 957 nachher bei 32 Threads), als der Lasthost den Großteil seiner CPU an andere virtuelle Maschinen verlor. Bei 128 Threads ist auf beiden Maschinen der Generator an seiner Grenze, nicht der Server.
Der Rest der Suite auf dieser Maschine
Dieselbe Suite wie für den x86_64-Server (Durchsatz, Latenz, Fan-out, ruhende Verbindungen) lief vollständig gegen den neuen Build. Für diese Zeilen gibt es kein „vorher": Der Lauf wurde angehalten, um den Assembler zu bauen, es ist also der Zustand nach der Änderung, und der Assembler berührt diese Pfade nicht, außer bei den Handshakes, die die Verbindungen öffnen. Eine Tatsache erklärt das meiste des Folgenden: Diese virtuelle Maschine hat zwei vCPUs. Kern 0 erledigt die Netzwerkkarte, das Dispatching und die gesamte MQTT- und TLS-Arbeit, Kern 1 ist der Datenbankkern und war durchgehend zu 2 bis 3 % ausgelastet. Der x86_64-Server hatte vier vCPUs und zwei Arbeitskerne. Alles Folgende ist deshalb die Leistung des Brokers auf einem Kern.
| Ruhende Verbindungen | 5.000 | 10.000 | 20.000 |
|---|---|---|---|
| verbunden | 5.000 von 5.000 in 4,3 s | 10.000 in 9,3 s | 20.000 in 21,2 s |
| Wachstum des Kernel-Heaps pro Verbindung | 0,34 KB | 0,41 KB | 0,43 KB |
| Fehler | 0 | 0 | 0 |
(0,50 bis 0,66 KB pro Verbindung auf dem x86_64-Server beim H7-Commit. Der Heap-Wert ist $SYS/broker/memory/used minus sein Wert vor der ersten Verbindung.)
| Latenz bei mäßiger Rate (8 Paare x 100 Nachrichten/s, 64 B) | p50 | p99 |
|---|---|---|
| QoS 0 | 4,0 ms | 6,9 ms |
| QoS 1 | 4,1 ms | 7,0 ms |
| Fan-out (1 Publisher, 64 B) | zugestellt pro Sekunde | p50 | p99 |
|---|---|---|---|
| 100 Subscriber, QoS 0, 50 Nachr./s | 5.000 (alle) | 5,9 ms | 8,6 ms |
| 100 Subscriber, QoS 1, 50 Nachr./s | 5.000 (alle) | 6,3 ms | 10,0 ms |
| 1.000 Subscriber, QoS 0, 10 Nachr./s | 8.805 von 10.000 | 207 ms | 5,4 s |
| Flood, 16 Paare, Publisher so schnell, wie der Broker annimmt | veröffentlicht/s | zugestellt/s | p50 / p99 | verlorene Verbindungen |
|---|---|---|---|---|
| QoS 0, 64 B | 231.317 | 85.140 | 1,36 s / 2,8 s | 0 |
| QoS 0, 1 KiB | 62.110 | 25.506 | 336 ms / 873 ms | 0 |
| QoS 1, 64 B | 107.349 | 55.965 | 21 ms / 29 ms | 0 |
| QoS 1, 1 KiB | 51.369 | 25.054 | 47 ms / 60 ms | 0 |
| QoS 1, 64 B, 64 Paare | 130.462 | 69.411 | 69 ms / 87 ms | 0 |
Wie man es liest. Die Latenz bei mäßiger Rate ist der Beitrag des Brokers plus ein Weg zum Generator: Der Generator ist diesem Server näher als der im x86_64-Vergleich, die beiden Latenzspalten sind also nicht vergleichbar. Der QoS-0-Flood zeigt das übliche Bild eines überlasteten einzelnen Kerns: Der Broker nimmt 231.000 Nachrichten pro Sekunde an und stellt 85.000 zu (den Rest verwerfen die Back-Pressure-Regeln, dafür sind sie da) mit Latenzen in Sekunden, und keine Verbindung ging verloren; auf dem x86_64-Server kostete derselbe Flood pro Lauf mehrere Verbindungen wegen des 20-ms-Watchdogs (ADR-221 hat seither geändert, wie dieses Budget gezählt wird). Die Zeile mit 1.000 Subscribern hat denselben Charakter wie zuvor, Tails in Sekunden, teils vom Generator selbst. Die Zahlen beim H7-Commit auf dem x86_64-Server stehen im früheren Text; wir stellen sie nicht in eine Tabelle mit diesen, weil sich der Code zwischen den beiden Messungen im Datenpfad geändert hat (ADR-221 bis ADR-224) und sich die Maschinen in allem unterscheiden.
Fehler, die wir selbst gefunden haben
- Das Bild sagte „no disks found", und der Datenträger stand im Bericht. Die erste Version von
urmqtt-diag-pnglas das Felddisksgar nicht. Der Inhaber des Projekts sah sich das Bild an, sagte, es sehe falsch aus, und hatte recht. Behoben, und ein Test liest jetzt einen Datenträger aus dem JSON zurück. - Wir haben den Installer zu früh für fertig erklärt. Der Zustand des Servers ging zweimal auf „off": einmal am Ende des Aus- und Einschaltens und einmal, als der Installer fertig war. Unser Skript hielt das erste für das zweite, hängte die ISO aus und startete das alte System, das dann die alte Versionsnummer meldete und uns einen Fehler im Installer suchen ließ, den es nicht gab. Das Skript wartet jetzt zuerst auf einen frischen Start.
- Fortschrittshistorie ging verloren. Eine Analyse für die Roadmap ergab, dass der Commit für den Meilenstein S6
docs/PROGRESS.md(1.763 Zeilen, Meilensteine M0 bis S5) durch einen eigenen Eintrag mit 26 Zeilen ersetzt hatte. Es blieb tagelang unbemerkt, obwohl die README diese Datei „die Quelle der Wahrheit" nennt. Aus der Historie wiederhergestellt. - CI schlug nach dem Assembler fehl, an zwei Stellen. Drei Smoke-Tests hatten die Größe des Boot-Images fest eingetragen („10 MiB"), die mit dem größeren Kernel auf 11 MiB wuchs; sie nehmen die Größe nicht mehr an. Der andere Fehlschlag war der oben beschriebene Zeittest: Beim ersten Mal sah er auf einem geteilten Runner wie Rauschen aus, also ließen wir den Test dreimal wiederholen; er schlug in allen drei Versuchen fehl, und so erfuhren wir, dass es ein Signal war, und das Signal erwies sich als unser eigener Test (er signierte jedes Mal dieselbe Nachricht). Wir haben diese eine Grenze für einige Stunden angehoben, das offen gesagt und sie wieder gesenkt, sobald die Ursache behoben war.
- Die Dokumentation zählt drei Boot-Wörter, und der Code hat vier (
diag-serve), und die Meilensteintabelle in der README zeigt die Arbeit der letzten Woche noch als geplant. Beides steht in der Roadmap als zu behebende Punkte.
Eine Roadmap, mit Fragen
Zum ersten Mal gibt es eine Roadmap: etwa 40 Punkte in drei Phasen (v0.4 Härtung und Betrieb; Protokoll und Netz; Scale-out, mehr Clouds und Bare Metal), jeder mit einer aus einem Dokument entnommenen Begründung, einem Aufwand, einem Risiko und einer Prüfmöglichkeit, dazu 16 Fragen, die nur der Inhaber beantworten kann, und eine Liste dessen, was außerhalb des Umfangs liegt. Einiges sagt sie offen: Nur MQTT 3.1.1 wird unterstützt, ein QoS-2-Publish schließt die Verbindung, es gibt kein IPv6 (der Anbieter vergibt ein /64), kein WebSocket und keine ausgehende Verbindung vom Broker, und es gibt keine Treiber für die Netzwerk- und Speichergeräte der großen Clouds.
Was noch nicht bewiesen ist
- Die CPU-Kosten pro Handshake mit dem Assembler wurden auf dem Neoverse-Kern nur ein einziges Mal gemessen (553 µs gegenüber 1,08 ms zuvor, bei niedrigerem Takt, auf einem geteilten Host); das 1,55- bis 1,7-Fache stammt aus der Rate voller Handshakes und das 3- bis 5-Fache von einem Apple-Kern. Die x86_64-Seite des Refactorings (der in eine eigene Datei verschobene Code) wurde typgeprüft und gebaut, aber ihre Unit-Tests liefen nicht, und der x86_64-Kernel wurde danach nicht gestartet: Die x86_64-Suiten laufen nicht in der CI, sie werden von Hand auf dem Contabo-VPS ausgeführt, und das ist für diese Änderung noch nicht geschehen.
- Konstante Laufzeit: Für P-256-Schlüsselerzeugung, ECDH und Signieren (den Schlüssel und die Nonce) fand der statistische Test keine Abhängigkeit von einem dünn besetzten Geheimnis. Er lief auf einem Apple-Kern und auf dem CI-Runner (einer virtuellen Neoverse-N2-Maschine), nicht auf dem Neoverse-N1-Kern bei netcup, und nie mit gesetztem DIT-Flag auf einem Serverkern (A255-W2, offen: Der Kernel setzt es nicht). Für X25519 oder Ed25519 lief er nicht. Ein Black-Box-Test begrenzt nur, was er bei seiner Stichprobengröße auflösen kann.
- Ein Server, ein Anbieter, eine Installation. Mehrere Ergebnisse (Installation, ein Stromausfall) sind ein einzelner Lauf. Außer bei den Handshakes haben die Leistungszeilen kein „vorher", und eine Maschine mit zwei vCPUs gibt dem Broker einen Datenkern.
- Der Zufallszahlengenerator auf diesem Server wird, wie oben beschrieben, aus Jitter und aus der Firmware initialisiert.
- Der Signaturschlüssel ist weiterhin ein Testschlüssel, die Release-Images sind Entwicklungs-Builds, und die Übersetzungen dieses Textes hat eine KI geschrieben, sie wurden nicht von Muttersprachlern geprüft.
- Kein unabhängiges Audit, kein Bare Metal, keine IOMMU.
Wie es weitergeht
- X25519 im Assembler für ARM64 (es sind 176 µs des verbleibenden Handshakes) und die Wiederholung der Messung pro Handshake auf dem Neoverse-Kern, mehr als einmal.
- Eine Entscheidung zum Standard des Zufallszahlengenerators: den Start ohne Hardwarequelle verweigern, sofern der Betreiber nichts anderes sagt, oder den Jitter-Rückfall behalten und das laut sagen.
- Die alte Liste: eine Entscheidung zu D1, Fuzzing der PCI- und virtio-Parser, der Schlüsseldialog auf der Builder-Seite.
- Aus der Roadmap, für das nächste Release: ein echter Signaturschlüssel und eine Produktiv-PKI, MQTT 5.0 und QoS 2.
Der Code liegt auf github.com/zeljan-alduk/urMQTT; docs/PROGRESS.md bleibt die Quelle der Wahrheit darüber, was funktioniert, docs/PERFORMANCE.md darüber, was gemessen wurde (Abschnitt 10 enthält die Zahlen des Assemblers), docs/AUDIT.md darüber, was nicht bewiesen ist. Pre-Alpha, wie zuvor; Fehlerberichte und Sicherheitsmeldungen nach den Anweisungen in SECURITY.md sind willkommen.
Update, 4. November, später am selben Tag. Vier Dinge aus den Stunden nach der Veröffentlichung dieses Textes.
- Der fehlende Wert. Der Diagnose-Eintrag lief auf dem netcup-Server noch einmal mit dem Assembler und lieferte die Zahl, die in der ersten Tabelle fehlt: 553 µs Server-CPU pro gegenseitigem TLS-Handshake gegenüber 1,08 ms zuvor (Einzelheiten unter dieser Tabelle).
- Das Signier-Signal war unser Test. Die Ursache des oben beschriebenen Zeitsignals wurde im Testgerüst gefunden und behoben (es signierte jedes Mal dieselbe Nachricht); die Grenze liegt wieder bei 7. Offen bleibt, dass das DIT-Flag nirgends gesetzt wird (A255-W2).
-
Das Bildschirmproblem ist gelöst, und es liegt auf
main. Wir haben einen Treiber für die virtio-Grafikkarte geschrieben (nur 2D, ADR-256), geschaltet über einen Konfigurationsschlüssel, der ins Image eingebrannt ist (console.virtio_gpu, standardmäßigoff,autoundonbrauchen Konfigurationsformat 5). Mitonzeigt der Reiter „Screen" des Anbieters das Diagnose-Dashboard live auf dem netcup-Server, dasselbe Bild wie oben, einschließlich des letzten Bildschirms mit „press any key". Sein Fuzz-Target fand in der ersten Version einen echten Fehler: Eine Display-Info-Antwort, die vier Bytes kürzer war als in der Spezifikation, wurde als gültig akzeptiert (A256-W1); ein echtes Gerät sendet immer die ganze Antwort, der Server war also nicht betroffen, und der Parser weist die kurze jetzt ab. Was wir nicht verifiziert haben: dass das dunkle Display wirklich daher kam, dass die Firmware die Karte bei der Übergabe an den Kernel zurücksetzt (wahrscheinlich, nicht bestätigt), das Zusammenspiel mit Secure Boot und x86_64 auf einer echten Maschine (es lief nur unter Emulation). Eine bekannte Grenze (A256-W2): Die Einstellungautokann einen lebenden emulierten Framebuffer neben einer virtio-Karte nicht von einem toten unterscheiden, also nimmt sie in dieser einen Konfiguration die virtio-Karte. Der Treiber bringt einen Parser für die Antworten des Hypervisors in den Kernel, weshalb er aus ist, solange die Konfiguration ihn nicht verlangt. -
Das Release. Das alles steckt in v0.4.0, am selben Abend als Pre-Release veröffentlicht (GitHub-Release). Die Builder-Seite dieser Website bietet es als Erstes an; es braucht Konfigurationsformat 6, und die älteren Vorlagen (v0.2.0 bis v0.3.0) lehnen die neuen Schlüssel mit einer Meldung ab, die auf ein neueres Release verweist. Die Grenzen von v0.3.0 bleiben: Der Signaturschlüssel ist weiterhin ein Testschlüssel, und die Images sind Entwicklungs-Builds mit Testzertifikaten. Die CI auf dem Release-Commit war erst grün, nachdem ein Job erneut ausgeführt wurde (ein Client-Test in einem Smoke-Szenario lief einmal in ein Timeout und bestand beim zweiten Mal). Was wir an den veröffentlichten Dateien geprüft haben: die minisign-Signatur, die Prüfsummen, die Build-Attestierungen des Kernels und der SBOM sowie die Vorlage mit den neuen Schlüsseln (v0.4.0 akzeptiert sie, v0.3.0 lehnt sie ab). Der Treiber für die neuen virtio-Geräte (Konsole, Balloon, Input, vsock) ist im Release enthalten, aber standardmäßig aus und lief nicht auf einem echten Server.
Kommentare
Kommentarregeln