<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Blog ALDO TECH</title>
    <link>https://aldo.tech/it/blog/</link>
    <atom:link href="https://aldo.tech/it/blog/feed.xml" rel="self" type="application/rss+xml"/>
    <description>Appunti dal campo: trasporto pubblico, bigliettazione, hardware e software che devono funzionare su strada.</description>
    <language>it</language>
    <item>
      <title>urMQTT dopo il VPS: SQLite in modalità utente, confronto con Mosquitto, configurazione come dato e builder nel browser</title>
      <link>https://aldo.tech/it/blog/urmqtt-config-builder-plaintext-mosquitto/</link>
      <guid isPermaLink="true">https://aldo.tech/it/blog/urmqtt-config-builder-plaintext-mosquitto/</guid>
      <pubDate>Sun, 01 Nov 2026 08:00:00 +0000</pubDate>
      <description>Che cosa è successo dopo il secondo articolo su urMQTT: SQLite isolato fuori dal kernel, misure contro Mosquitto sullo stesso VPS (e ciò che non è andato a nostro favore), le cause trovate per il flusso singolo lento, la configurazione come dato con un builder web di ISO e un emulatore nel browser, l&#x27;MQTT in chiaro facoltativo e ciò che non è ancora dimostrato.</description>
      <content:encoded><![CDATA[<p>Il secondo articolo, <a href="https://aldo.tech/blog/urmqtt-vps-handshake-disk-hardening/">urMQTT su un vero VPS</a>, si chiudeva con l'elenco "Che cosa viene dopo": H6 (SQLite fuori dal kernel), H7 (sicurezza del tempo), X11 (confronto con Mosquitto sullo stesso VPS) e la prima pubblicazione. Questo articolo segue quell'elenco nello stesso ordine e poi aggiunge due cose che nell'elenco non c'erano, perché sono comparse lungo la strada: la configurazione come dato, con un builder web di ISO, e l'MQTT in chiaro facoltativo, senza TLS. Come prima: che cosa avevamo pianificato, che cosa abbiamo costruito, che cosa ci ha sorpreso e che cosa è aperto. In questo articolo ci sono anche alcuni risultati che non vanno a favore di urMQTT; li abbiamo lasciati perché fanno parte della verità.</p>
<p>L'avvertenza iniziale resta la stessa: il progetto è pre-alfa, la prima pubblicazione è solo per VPS con KVM, e i numeri misurano codice, test ed esperimenti precisi, non la maturità in produzione.</p>
<h2 id="come-si-presenta-nel-tempo">Come si presenta nel tempo</h2>
<p>Dall'articolo precedente (31.10. alle 23:00) a oggi (1.11. alle 20:41) il repository ha 59 nuovi commit, in tutto 178. Sono nati tutti in un solo giorno di calendario, quindi vale la stessa avvertenza di prima: il progetto è costruito con Claude Code (i commit portano <code>Co-Authored-By: Claude</code>), piano, codice, test e ADR nascono in cicli brevi e chiusi, e i numeri qui sotto parlano della quantità di codice e di verifiche, non di quanto lavoro umano ci sia dietro.</p>
<figure><img loading="lazy" alt="Crescita dall'articolo precedente: righe di Rust da 89 790 a 112 408, funzioni #[test] da 739 a 926, ADR da 104 a 126 e commit da 119 a 178. Ogni riga ha una propria scala." src="https://aldo.tech/blog/urmqtt-config-builder-plaintext-mosquitto/img/growth-since.svg" /><figcaption>Crescita dall'articolo precedente: righe di Rust da 89 790 a 112 408, funzioni #[test] da 739 a 926, ADR da 104 a 126 e commit da 119 a 178. Ogni riga ha una propria scala.</figcaption></figure>
<p>I numeri sono calcolati con lo stesso metodo dell'articolo precedente: tutti i file <code>.rs</code> fuori da <code>vendor/</code> e <code>fuzz/</code>, le funzioni <code>#[test]</code>, i titoli <code>## ADR-</code> in <code>docs/DECISIONS.md</code>.</p>
<table>
<thead>
<tr>
<th>Momento</th>
<th>Commit</th>
<th>Ora</th>
<th style="text-align: right;">Righe di Rust</th>
<th style="text-align: right;">Test</th>
<th style="text-align: right;">ADR</th>
</tr>
</thead>
<tbody>
<tr>
<td>articolo precedente</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 concluso</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>builder web e mirror delle release</td>
<td>582783f</td>
<td>1.11. 16:49</td>
<td style="text-align: right;">105 645</td>
<td style="text-align: right;">848</td>
<td style="text-align: right;">122</td>
</tr>
<tr>
<td>MQTT in chiaro, documentazione</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>oggi</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>Si è lavorato in parallelo su più branch, quindi l'ordine dei commit non è rigorosamente l'ordine del lavoro. Gli ADR sono numerati fino a 232, ma la numerazione ha dei vuoti (una parte è stata rinominata durante l'unione dei branch); contiamo i titoli, non il numero più alto. Le release: v0.2.0 (1.11., 18:45 UTC) e v0.2.1 (1.11., 20:22 UTC), entrambe contrassegnate come pre-release.</p>
<h2 id="h6-sqlite-in-modalita-utente">H6: SQLite in modalità utente</h2>
<p><strong>Che cosa avevamo pianificato.</strong> SQLite è fatto di 254 268 righe di C che analizzano il database, il WAL e il journal dal disco, e il disco può essere stato fabbricato o modificato. Finora girava nel kernel, con i privilegi del kernel. <strong>Che cosa abbiamo costruito</strong> (da ADR-210 a ADR-212): SQLite, le sue parti di libc e il suo heap sono stati spostati in un programma separato, <code>dbworker</code>, che gira in EL0 (aarch64) ovvero ring 3 (x86_64), in uno spazio di indirizzi proprio. Il kernel non linka più SQLite. Il worker parla con il kernel attraverso una finestra di memoria condivisa e dodici chiamate di sistema, e ogni lunghezza e ogni offset restituiti vengono controllati prima dell'uso. Un errore nel worker (accesso non consentito, istruzione non definita, budget di tempo di 30 s superato) termina la chiamata, il worker viene riavviato e il database viene riaperto dal WAL come all'avvio; dopo 5 riavvii in un minuto lo storage viene dichiarato guasto, invece di riavviarsi all'infinito.</p>
<p>Il costo, misurato su aarch64 con HVF, stessa VM, kernel con SQLite incorporato contro kernel con worker, sei avvii alternati per ciascuno:</p>
<table>
<thead>
<tr>
<th>Misura</th>
<th>Prima (nel kernel)</th>
<th>Dopo (worker)</th>
<th>Differenza</th>
</tr>
</thead>
<tbody>
<tr>
<td>ingresso e uscita dalla modalità utente</td>
<td>-</td>
<td>1,1 us per giro</td>
<td></td>
</tr>
<tr>
<td><code>db-bench</code>, 5 000 inserimenti in 50 transazioni, migliore / mediana</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>un inserimento per transazione, migliore / mediana</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>copia di sicurezza completa con una sola chiamata (23 MiB)</td>
<td>514 ms</td>
<td>635 ms</td>
<td>+23%</td>
</tr>
</tbody>
</table>
<p>Le scritture su disco (1 572) e i flush (319) sono identici, come deve essere. Ciò che non è isolato è questo: <strong>il kernel continua a fidarsi di ciò che il worker restituisce</strong>, dopo il controllo della forma. Un worker compromesso può mentire (per esempio restituire una lista vuota di certificati revocati, e quindi "annullarne" la revoca), ma non può leggere la memoria del kernel, le chiavi TLS né le chiavi dei ticket. Non c'è KPTI, quindi le fughe di tipo Spectre/Meltdown non sono impedite e non affermiamo nulla al riguardo; la chiave PAC è condivisa tra EL0 ed EL1. Su x86_64 il costo del passaggio non lo abbiamo misurato su KVM (sotto TCG il numero sarebbe privo di senso), e il fuzzing del worker e del protocollo stesso non è concluso. La suite di test per lo storage (79 scenari, ognuno fa girare SQLite nel worker) passa su aarch64 HVF e su x86_64 TCG, su aarch64 TCG ne passano 76 (uno viene saltato perché TCG non ha MSI-X).</p>
<p><strong>H7 è chiuso, ma non come avevamo annunciato.</strong> Il limite inferiore del tempo (ADR-050) e l'SNTP con consenso (T1) coprono il perimetro di H7; NTS (RFC 8915) resta fuori dal perimetro ed è annotato come rischio residuo, non come qualcosa che funziona. Non riteniamo che la sicurezza del tempo sia "risolta", ma che sia finita la sua parte dimostrabile.</p>
<p><strong>Una nuova opzione di rete.</strong> Contabo non dà DHCP a un ISO personalizzato, quindi finora il VPS richiedeva un'immagine diversa da quella della VM di laboratorio. Ora <code>network.mode = "auto"</code> (ADR-214) interroga prima il DHCP e, se il lease non arriva nel tempo previsto (<code>dhcp_timeout_s</code>, predefinito 15 s) o lo perde, applica l'indirizzo statico indicato. Provato a mano su QEMU (con il DHCP usa il lease, senza rete dopo 5 s torna all'indirizzo statico); per questo non esiste ancora uno scenario smoke.</p>
<h2 id="x11-confronto-con-mosquitto-sullo-stesso-vps">X11: confronto con Mosquitto sullo stesso VPS</h2>
<p>Lo stesso tipo di macchina dell'articolo precedente (Contabo, 4 vCPU AMD EPYC, 8 GiB), carico da un host Linux a 15 ms di distanza, TLS 1.3 reciproco con gli stessi certificati da entrambe le parti, Mosquitto 2.0.21 su Debian 13 senza persistenza. Metodo e decisioni sono in ADR-213, i dati grezzi in <code>docs/perf-results/</code>. Mediana di tre esecuzioni.</p>
<figure><img loading="lazy" alt="Handshake completi al secondo (TLS 1.3 + certificato client + CONNECT, senza ripresa della sessione) con 32, 64 e 128 client: urMQTT 169, 278, 413; Mosquitto 82, 140, 184. Sono limiti inferiori: il generatore di carico era vicino al proprio limite." src="https://aldo.tech/blog/urmqtt-config-builder-plaintext-mosquitto/img/mosquitto.svg" /><figcaption>Handshake completi al secondo (TLS 1.3 + certificato client + CONNECT, senza ripresa della sessione) con 32, 64 e 128 client: urMQTT 169, 278, 413; Mosquitto 82, 140, 184. Sono limiti inferiori: il generatore di carico era vicino al proprio limite.</figcaption></figure>
<table>
<thead>
<tr>
<th></th>
<th>urMQTT</th>
<th>Mosquitto 2.0.21</th>
</tr>
</thead>
<tbody>
<tr>
<td>handshake/s, 32 / 64 / 128 client (profilo bench)</td>
<td>169 / 278 / 413</td>
<td>82 / 140 / 184</td>
</tr>
<tr>
<td>idem, con i limiti come vengono distribuiti</td>
<td>185 / 262 / 333 (con 128: da 202 a 286 errori, reset degli handshake)</td>
<td>-</td>
</tr>
<tr>
<td>memoria per connessione inattiva (20 000 connessioni)</td>
<td>0,50 KB di heap del kernel (0,66 KB con 5 000)</td>
<td>26,8 KB di crescita del processo</td>
</tr>
<tr>
<td>latenza p50, QoS 0 / QoS 1, 800 messaggi/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 messaggi/s, consegnati/s (p50)</td>
<td>4 209 (77 ms)</td>
<td>4 235 (197 ms)</td>
</tr>
<tr>
<td>100 sottoscrittori, 50 messaggi/s: p50</td>
<td>16,6 ms</td>
<td>99 ms</td>
</tr>
<tr>
<td>flusso singolo, QoS 0, 64 B, pubblicati/s</td>
<td>2 962 (da 0 a 7 733)</td>
<td>22 749</td>
</tr>
</tbody>
</table>
<p>Che cosa significa, e che cosa no. Gli handshake sono <strong>limiti inferiori per entrambi i server</strong>: il generatore di carico è una VM condivisa con il 60-70% di tempo di CPU rubato e con 128 thread era vicino al proprio limite, mentre i core di lavoro di urMQTT erano occupati solo per il 12-13%. Il rapporto con 128 client (circa 2,2 volte) è il più significativo; con 32 e 64 misura per lo più l'attesa dei client. I numeri di memoria non contano la stessa cosa (heap del kernel contro RSS del processo), quindi "circa 50 volte meno" è un ordine di grandezza, non un rapporto preciso. La latenza di 14 ms è un giro di rete (circa 15 ms), cioè "nessun tempo misurabile del broker"; le code (p99 circa 100-200 ms) sono dominate in entrambi i broker dagli host condivisi. Le differenze sotto circa il 20% non le affermiamo.</p>
<p><strong>Che cosa non è andato a nostro favore</strong> ed è rimasto nelle tabelle: un singolo flusso TCP attraverso urMQTT portava da 2 a 8 volte meno che attraverso Mosquitto, con variazioni da zero a 7,7 mila messaggi al secondo tra un'esecuzione e l'altra, e il watchdog da 20 ms del gestore <strong>interrompeva connessioni vive</strong> quando l'hypervisor fermava una vCPU (nella configurazione di produzione <code>action = "drop"</code>, da 6 a 8 connessioni per esecuzione con il flood QoS 0). Entrambe le cose sono descritte nella sezione seguente perché avevano cause nel nostro codice.</p>
<h2 id="perche-il-flusso-singolo-era-lento-cause-non-scuse">Perché il flusso singolo era lento: cause, non scuse</h2>
<p>La cosa più facile sarebbe stata dire "VPS condiviso". Non l'abbiamo fatto: con lo stesso VPS e lo stesso metodo abbiamo cercato le cause. Ne sono state trovate cinque, quattro nel percorso dei dati e una nel watchdog (da ADR-221 a ADR-224):</p>
<table>
<thead>
<tr>
<th>Sintomo in X11</th>
<th>Causa in urMQTT</th>
<th>Modifica</th>
</tr>
</thead>
<tbody>
<tr>
<td>flusso singolo 1,9k messaggi/s, spesso <strong>0 consegnati</strong></td>
<td><strong>Risveglio perso:</strong> il ciclo di uscita restituiva l'elenco delle connessioni pronte sopra quello appena scritto; un sottoscrittore il cui buffer di uscita si era riempito una volta non veniva più servito finché un PINGRESP non lo svegliava</td>
<td>corretto (ADR-223)</td>
</tr>
<tr>
<td>qualche punto percentuale di perdita trasforma 48k in 14k messaggi/s</td>
<td><strong>ogni segmento dopo uno perso veniva scartato</strong> (ricezione solo in ordine), quindi il recupero costava un giro di rete per segmento</td>
<td>coda dei segmenti fuori ordine, SACK (ADR-222)</td>
</tr>
<tr>
<td>tetto di 4,4 MB/s a 15 ms</td>
<td>finestra a 16 bit, coda di invio da 64 KiB e slow start che contava gli ACK, non i byte</td>
<td>scalatura della finestra, timestamp, conteggio dei byte (ADR-222)</td>
</tr>
<tr>
<td>ogni giro del flusso che sta nella finestra aspetta da 0 a 10 ms</td>
<td><strong>il dispatcher dormiva fino al tick successivo da 10 ms</strong> (virtio-net non ha interrupt di ricezione)</td>
<td>polling della NIC ogni 200 us finché ci sono connessioni (ADR-224)</td>
</tr>
<tr>
<td>da 1 a 8 connessioni interrotte per esecuzione</td>
<td><strong>il watchdog addebitava al gestore il tempo trascorso</strong>, e il VPS ferma le vCPU spesso e a lungo</td>
<td>il budget è il tempo in cui il core ha effettivamente lavorato (ADR-221)</td>
</tr>
</tbody>
</table>
<p>Le interruzioni sono reali: su questo VPS i core da 0 a 2 hanno avuto una mediana da 36 a 80 tick in ritardo ogni 5 secondi, e l'interruzione peggiore nella finestra aveva una mediana di circa 70 ms e fino a 0,9 s (core 3, database e journal: fino a 1,55 s), mentre l'host non riporta tempo rubato (<code>host steal 0 us</code>). Per questo il watchdog aveva bisogno di un secondo orologio, non di un budget più grande.</p>
<figure><img loading="lazy" alt="Prima e dopo, sullo stesso VPS con lo stesso metodo, mediana di tre-sei esecuzioni: flusso singolo QoS 0, 64 B: pubblicati 1,9k, 23,7k (Mosquitto 29,3k), consegnati 0 (mediana), 12,6k (29,2k). 16 coppie, flood, 64 B: pubblicati 24,7k, 43,2k (31,4k), consegnati 20,0k, 37,4k (30,7k)." src="https://aldo.tech/blog/urmqtt-config-builder-plaintext-mosquitto/img/before-after.svg" /><figcaption>Prima e dopo, sullo stesso VPS con lo stesso metodo, mediana di tre-sei esecuzioni: flusso singolo QoS 0, 64 B: pubblicati 1,9k, 23,7k (Mosquitto 29,3k), consegnati 0 (mediana), 12,6k (29,2k). 16 coppie, flood, 64 B: pubblicati 24,7k, 43,2k (31,4k), consegnati 20,0k, 37,4k (30,7k).</figcaption></figure>
<table>
<thead>
<tr>
<th></th>
<th>codice X11 (7f9517e)</th>
<th>ora (86a71ff)</th>
<th>Mosquitto</th>
</tr>
</thead>
<tbody>
<tr>
<td>flusso singolo, QoS 0, 64 B, pubblicati / consegnati al s</td>
<td>1,9k / 0</td>
<td>23,7k / 12,6k</td>
<td>29,3k / 29,2k</td>
</tr>
<tr>
<td>flusso singolo, 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 coppie, flood, 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 coppie, flood, 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>connessioni perse per esecuzione (16 coppie, 64 B)</td>
<td>13 (da 7 a 16)</td>
<td>0 (da 0 a 0)</td>
<td>0</td>
</tr>
</tbody>
</table>
<p>Watchdog: 148 connessioni interrotte in 30 esecuzioni sotto carico prima, 0 in 24 esecuzioni dopo; su un apparecchio locale che ritarda e perde frame, con una perdita dello 0,1% un flusso singolo da 64 B con 14,3k messaggi/s (kernel X11) ora arriva a 41,0k, e con lo 0,5% da 5,0k a 17,7k.</p>
<p><strong>Che cosa questo non dice.</strong> urMQTT <strong>non è ancora più veloce di Mosquitto su un singolo flusso</strong>: a 64 B pubblica circa l'80% della sua velocità e consegna circa il 40%, perché scarta il QoS 0 quando il flusso del sottoscrittore è più lento di quello di chi pubblica (Mosquitto, senza limite, mette in coda). A 1 KiB nel flood a 16 coppie Mosquitto è avanti di circa il 15%, il che rientra nel rumore. Il generatore era di nuovo una VM condivisa con il 58-77% di tempo rubato. Il secondo giro (blocco dell'heap fuori dal percorso dei dati, SACK lato invio e altro) è sul branch non integrato <code>perf-single-flow-wip</code>; i suoi numeri non sono ancora affermazioni e qui non li riportiamo.</p>
<h2 id="una-promessa-che-non-avevamo-fatto-ma-che-abbiamo-mantenuto-la-configurazione-e-un-dato">Una promessa che non avevamo fatto, ma che abbiamo mantenuto: la configurazione è un dato</h2>
<p>Finora ogni configurazione era una nuova compilazione: chiavi e limiti erano incorporati nell'immagine. C1 lo rovescia (da ADR-215 a ADR-219): il kernel si compila una volta sola e la configurazione è un dato.</p>
<ul>
<li><strong>Blob <code>urmqtt-cfg</code></strong> (C1a): una registrazione versionata e canonica (campi per tag crescente, impronta SHA-256, firma Ed25519 separata facoltativa), 70 campi con valori predefiniti e limiti, un decodificatore che rifiuta tutto ciò che non è valido.</li>
<li><strong>Il kernel lo legge come modulo Limine</strong> (C1b), prima di qualsiasi altra cosa, su aarch64 e x86_64. Esiste <strong>un solo kernel pubblicato per ogni <code>ENV</code> tranne <code>dev</code></strong> (gli ELF aarch64 per <code>release</code>, <code>prod</code> e <code>bench</code> hanno lo stesso SHA-256; verificato solo su aarch64). Il kernel pubblicato rifiuta la configurazione di sviluppo e, senza configurazione, scrive "refusing to serve" e non serve nulla.</li>
<li><strong>Il disco installato porta la configurazione</strong> e l'aggiornamento la conserva (C1c).</li>
<li><strong><code>urmqtt-config</code> e il modello ISO</strong> (C1d): <code>pack</code>, <code>show</code>, <code>verify</code>, e <code>pack --iso</code> sostituisce entrambe le copie dello slot di configurazione e i 24 digest dei moduli in un ISO già pronto. Il risultato è identico byte per byte all'ISO che <code>mkiso.sh --config</code> costruisce. Il modello della release v0.2.0 è di 63 111 168 byte.</li>
<li><strong>Dichiarazione di fiducia</strong> (ADR-219): l'immagine configurata, all'avvio, stampa l'impronta della configurazione (<code>config: fingerprint ...</code>), mentre la firma è facoltativa e <strong>disattivata per impostazione predefinita</strong>. Un ISO modificato non ha il digest ufficiale; l'impronta è il modo per vedere che cosa contiene.</li>
</ul>
<h2 id="il-builder-su-aldotech">Il builder su aldo.tech</h2>
<p>Ciò che fa <code>urmqtt-config pack --iso</code> lo fa anche la pagina web, con lo stesso codice: la libreria dello strumento compilata in WebAssembly (1,06 MB, senza import e senza blocchi <code>unsafe</code>). La pagina è su <a href="https://aldo.tech/it/prodotti/urmqtt-builder/">aldo.tech/it/prodotti/urmqtt-builder/</a> ed esiste in tutte le 15 lingue del sito (per esempio <a href="https://aldo.tech/proizvodi/urmqtt-builder/">hr</a>).</p>
<ul>
<li><strong>Nulla lascia il browser.</strong> Certificati, chiavi private e l'ISO finito restano sul computer; la pagina scarica soltanto i file della release da questo sito, senza tracciamento e senza script di terze parti. Nel test dal vivo nessuna richiesta portava materiale di chiavi.</li>
<li><strong>Mirror delle release</strong> (ADR-227): ogni ora (timer systemd, a bassa priorità) copia le release da GitHub, ognuna viene verificata prima della pubblicazione, e la pagina nel browser verifica la firma di <code>SHA256SUMS</code> con la chiave incorporata nella pagina, e solo dopo scrive la configurazione nel modello. Una verifica fallita blocca il download.</li>
<li><strong>Semplice per impostazione predefinita, avanzato quando serve</strong> (ADR-232): la prima schermata ha tre passaggi (dove gira, i vostri certificati, il nome dell'immagine). Un solo campo di trascinamento ordina certificato, chiave e CA in base al contenuto. La vista avanzata apre ciascuno dei 70 campi, la ricerca, l'importazione e l'esportazione di TOML; le due viste modificano lo stesso stato. Il modello (63 MB) viene scaricato mentre l'utente compila il modulo. In una prova dal vivo, con mirror di prova e chiave di prova, la creazione dell'ISO nel browser è durata da 5 a 18 s.</li>
</ul>
<figure><img loading="lazy" alt="Vista semplice del builder: tre passaggi, &quot;Dove verrà eseguito?&quot;, &quot;I vostri certificati&quot; e &quot;Dare un nome all'immagine&quot;, con una sola azione successiva nella barra laterale." src="https://aldo.tech/blog/urmqtt-config-builder-plaintext-mosquitto/img/builder-simple.png" /><figcaption>Vista semplice del builder: tre passaggi, &quot;Dove verrà eseguito?&quot;, &quot;I vostri certificati&quot; e &quot;Dare un nome all'immagine&quot;, con una sola azione successiva nella barra laterale.</figcaption></figure>
<figure><img loading="lazy" alt="Vista avanzata, catturata su un mirror di prova con una chiave di prova (la data della release e la chiave di firma non sono quelle vere): release con firma verificata, modelli, ogni campo della configurazione." src="https://aldo.tech/blog/urmqtt-config-builder-plaintext-mosquitto/img/builder-advanced.png" /><figcaption>Vista avanzata, catturata su un mirror di prova con una chiave di prova (la data della release e la chiave di firma non sono quelle vere): release con firma verificata, modelli, ogni campo della configurazione.</figcaption></figure>
<p><strong>L'emulatore, facoltativo.</strong> Accanto a "Scarica ISO" c'è "Prova nell'emulatore": QEMU compilato in WebAssembly avvia l'immagine appena creata in una scheda del browser. Nulla di esso viene scaricato prima del clic (circa 3 MB la prima volta). L'emulatore è solo BIOS, <strong>senza rete</strong> (perché il traffico dell'utente non passi attraverso un intermediario e non violi il "nulla lascia il browser"), richiede circa 1,5 GB di memoria e un browser per computer (testato Chrome; Firefox e Safari no). Al suo interno urMQTT arriva a <code>URMQTT READY</code> in circa 8 s (saltando il conto alla rovescia) e stampa la stessa impronta di configurazione della pagina, il che prova che è stata avviata proprio quella configurazione. L'avvio è circa 2 volte più lento del QEMU nativo sullo stesso Mac (0,82 s contro 1,9 s), e la crittografia circa 58 volte più lenta perché le istruzioni hardware sono emulate. Per questo <strong>dall'emulatore non ricaviamo alcuna affermazione sulla velocità</strong>, e la pagina lo dice prima dell'avvio. Durante la compilazione è stato necessario correggere quattro byte nel <code>.wasm</code> a causa di un errore nella traduzione dell'istruzione POPCNT nell'emulatore originale (non l'abbiamo ancora segnalato); l'emulatore stesso è sotto GPL, con offerta del codice sorgente.</p>
<figure><img loading="lazy" alt="Voce diagnostica nell'emulatore dal vivo su aldo.tech: il kernel ha stampato la stessa impronta di configurazione della pagina, e lo schermo mostra la diagnostica (nell'emulatore finisce in circa 20 s; i suoi numeri descrivono l'emulatore, non urMQTT)." src="https://aldo.tech/blog/urmqtt-config-builder-plaintext-mosquitto/img/emulator-diagnostic.png" /><figcaption>Voce diagnostica nell'emulatore dal vivo su aldo.tech: il kernel ha stampato la stessa impronta di configurazione della pagina, e lo schermo mostra la diagnostica (nell'emulatore finisce in circa 20 s; i suoi numeri descrivono l'emulatore, non urMQTT).</figcaption></figure>
<h2 id="mqtt-in-chiaro-solo-per-reti-di-cui-ci-si-fida">MQTT in chiaro, solo per reti di cui ci si fida</h2>
<p>L'MQTT in chiaro (TCP 1883, senza TLS) è molto diffuso nell'IoT: è l'impostazione predefinita di Mosquitto, nella maggior parte dei firmware per Arduino ed ESP, in Tasmota, Zigbee2MQTT e Home Assistant, e viene usato anche all'interno di reti industriali segmentate, dietro una VPN e su LAN private, e anche su dispositivi senza orologio (i certificati richiedono una data), senza abbastanza flash o processore per il TLS. Su una rete non affidabile non è accettabile: chi è sul percorso legge e modifica il traffico. Per questo P1 (da ADR-229 a ADR-231) è una scelta esplicita e limitata, <strong>disattivata per impostazione predefinita</strong>, e la porta TLS resta; il TLS non viene disattivato da questa opzione, la porta in chiaro si aggiunge.</p>
<ul>
<li><strong>L'elenco delle reti consentite è obbligatorio</strong> (<code>allow_from</code>, CIDR) e viene controllato al SYN: fuori dall'elenco non c'è SYN-ACK né stato. L'intera internet (<code>0.0.0.0/0</code>) richiede un'opzione esplicita a parte.</li>
<li><strong>Tre modalità di accesso:</strong> password (predefinita quando è attivo; PBKDF2-HMAC-SHA-256, salt, 10 000 iterazioni, confronto a tempo costante, blocco per indirizzo), anonimo (una identità configurata con un proprio ACL) e per indirizzo (<code>ip</code>). Il builder calcola l'hash della password nel browser e in memoria tiene solo il digest.</li>
<li><strong>Meno diritti che con mTLS:</strong> un client in chiaro non ha mai diritti di servizio, di operatore né di gestione, non ha <code>$SYS</code> né console, ha proprie sessioni (<code>$plain/&lt;identità&gt;/&lt;id client&gt;</code>, solo pulite), propri limiti e il registro dei consentiti continua a vincolarlo.</li>
<li><strong>Che cosa si perde, lo dice <code>SECURITY.md</code>:</strong> riservatezza, integrità, autenticità del broker e la riproduzione di un accesso registrato. Questo non si può correggere con la configurazione; è così quando non c'è il TLS.</li>
</ul>
<p>Misurato su un laptop (Apple silicon, HVF, rete utente QEMU), 16 coppie, 64 B, tre esecuzioni da 10 s: a 8 000 messaggi al secondo il percorso TLS e quello in chiaro consegnano tutti i messaggi con la stessa latenza (p50 0,17 ms contro da 0,13 a 0,18 ms), nuove connessioni al secondo: TLS circa 2 600-2 750, anonimo in chiaro circa 4 900-7 800, e <strong>l'accesso con password è la parte costosa</strong>: un PBKDF2 costa circa 2,5 ms, quindi il numero di nuovi accessi è limitato a circa 200 per core dati al secondo (misurati da 308 a 342 su due core). Il flood su quella macchina non è uno strumento stabile (da 20k a 440k consegnati al secondo tra un'esecuzione e l'altra) e <strong>non ne ricaviamo conclusioni</strong>. Il confronto del percorso in chiaro con Mosquitto su VPS non è ancora stato fatto. Test: gruppo smoke <code>plaintext</code> 7 su 7 su aarch64 HVF e su x86_64 TCG, nuovo target fuzz <code>plain_input</code> (2,3 milioni di esecuzioni senza risultati).</p>
<h2 id="gli-errori-trovati-dalluso-dal-vivo">Gli errori trovati dall'uso dal vivo</h2>
<p>Due cose non le hanno trovate i test, ma la prima messa in funzione sul sito vero:</p>
<ul>
<li>La pagina cercava i file della release <strong>una directory troppo in alto</strong>, quindi ogni verifica della firma rispondeva 404 (correzione 13af301). Il test usava un mirror di prova su un altro percorso.</li>
<li><strong>La tastiera dell'emulatore non è mai arrivata al guest</strong>: la causa non era una cattiva traduzione dei tasti, ma il fatto che la finestra di dialogo non ascoltava affatto i tasti (correzione in ADR-232, con una voce di avvio che non richiede tasti).</li>
</ul>
<h2 id="lezioni">Lezioni</h2>
<ul>
<li><strong>Cercate la causa, non incolpate l'ambiente.</strong> La maggior parte delle debolezze del flusso singolo e tutte le connessioni interrotte erano nostre; l'hypervisor è reale, ma andava misurato, non presupposto.</li>
<li><strong>Il confronto con un altro prodotto deve includere anche le perdite.</strong> Una tabella con solo le nostre vittorie non sarebbe un confronto.</li>
<li><strong>Lo strumento di misura ha dei limiti.</strong> Un generatore condiviso con il 60-70% di tempo rubato cambia il significato dei numeri; per questo scriviamo "limite inferiore" e non affermiamo differenze sotto il 20%.</li>
<li><strong>L'isolamento non è una dimostrazione.</strong> SQLite in EL0 riduce ciò che un errore al suo interno può fare, non ciò su cui può mentire.</li>
<li><strong>Un'installazione reale trova ciò che i test non cercano</strong>: un percorso composto male e tasti che nessuno ascolta.</li>
</ul>
<h2 id="che-cosa-non-e-ancora-dimostrato">Che cosa non è ancora dimostrato</h2>
<ul>
<li>La chiave di firma della release per ora è una chiave di prova; v0.2.0 e v0.2.1 sono pre-release, e le immagini che le accompagnano sono di sviluppo con certificati di prova (tranne il modello, che è senza configurazione).</li>
<li>13 traduzioni della pagina del builder (e di questo articolo) sono state scritte da un'AI e verificate con test e campioni, non da madrelingua.</li>
<li>Il confronto con Mosquitto è una sola sessione, un solo percorso e un solo generatore condiviso, su un VPS condiviso; le differenze sotto il 20% non sono affermazioni.</li>
<li>Il percorso in chiaro non è stato misurato su VPS né contro Mosquitto.</li>
<li>Lighthouse sulla nuova vista semplice non è confermato (vista precedente: accessibilità 95 o più).</li>
<li>Bare metal, costo del worker x86_64 su KVM, Firefox e Safari per l'emulatore: non misurato.</li>
</ul>
<h2 id="che-cosa-viene-dopo">Che cosa viene dopo</h2>
<ul>
<li><strong>D1: broker demo pubblico.</strong> L'emulatore del browser non possiamo renderlo pubblico: vive nella scheda di un solo visitatore, è da 20 a 100 volte più lento di KVM, non ha rete in ingresso e sparisce con la chiusura della scheda; far passare internet nel browser di qualcun altro sarebbe anche un abuso. Un vero broker demo è possibile: urMQTT su un VPS dietro un nome su aldo.tech, con una CA di prova pubblicata, un ACL severo e piccoli limiti. Per ora è solo un'idea, non un lavoro in corso, e richiede un consenso esplicito per l'esposizione pubblica.</li>
<li><strong>Secondo giro per il flusso singolo</strong> (branch <code>perf-single-flow-wip</code>): solo quando sarà misurato su VPS e unito.</li>
<li><strong>Percorso in chiaro contro Mosquitto su VPS</strong>, registrazione e revoca dell'account per l'MQTT in chiaro attraverso il canale di gestione firmato.</li>
<li><strong>Dopo la pubblicazione:</strong> bare metal (da X12 a X16), IOMMU e Secure Boot, cifratura del disco.</li>
</ul>
<p>Il codice è su <a href="https://github.com/zeljan-alduk/urMQTT">github.com/zeljan-alduk/urMQTT</a>, e <code>docs/PROGRESS.md</code> resta l'unica fonte di verità su ciò che funziona davvero, <code>docs/PERFORMANCE.md</code> su ciò che è stato misurato. Pre-alfa, come prima; segnalazioni di errori e di sicurezza secondo le istruzioni in <code>SECURITY.md</code> sono benvenute.</p>]]></content:encoded>
      <category>rust</category>
      <category>mqtt</category>
      <category>iot</category>
      <category>unikernel</category>
      <category>open source</category>
    </item>
    <item>
      <title>urMQTT su un vero VPS: ISO, handshake da 0,24 ms, disco proprio e kernel rinforzato</title>
      <link>https://aldo.tech/it/blog/urmqtt-vps-handshake-disk-hardening/</link>
      <guid isPermaLink="true">https://aldo.tech/it/blog/urmqtt-vps-handshake-disk-hardening/</guid>
      <pubDate>Sat, 31 Oct 2026 08:00:00 +0000</pubDate>
      <description>Che cosa è successo alle tre promesse del primo articolo su urMQTT (un solo ISO, disco raw senza file system, sicurezza dimostrabile) dopo i primi quattro giorni di sviluppo, con misure, tabelle e l&#x27;elenco di ciò che non è ancora finito.</description>
      <content:encoded><![CDATA[<p>Nel <a href="https://aldo.tech/blog/urmqtt-unikernel-mqtt-broker/">primo articolo su urMQTT</a> abbiamo descritto un unikernel che è un broker MQTT, fino alla milestone M17, e abbiamo chiuso con tre promesse sotto il titolo "Che cosa viene dopo": un solo ISO che si avvia su più piattaforme, un disco raw senza file system e una sicurezza che si possa dimostrare, non solo dichiarare. Allora nulla di tutto questo era stato costruito. Questo articolo segue che cosa è successo a ciascuna promessa, nello stesso ordine di prima: che cosa avevamo pianificato, che cosa abbiamo costruito, che cosa ci ha sorpreso e che cosa è ancora aperto. Lungo la strada mostreremo anche quanto tempo è servito, perché fa parte della storia.</p>
<p>Una premessa: il progetto è ancora pre-alfa. I numeri nel testo misurano codice e test, non la maturità in produzione, e la prima pubblicazione è volutamente limitata ai VPS con KVM. Il bare metal resta per dopo.</p>
<h2 id="come-si-presenta-nel-tempo">Come si presenta nel tempo</h2>
<p>Il primo commit risale al 28 ottobre 2026 alle 23:48 (M0). Al 31 ottobre alle 23:00 il repository conta 119 commit, distribuiti come 3, 39, 38 e 39 per giorno di calendario. Diciamo subito anche come sia possibile: il progetto è costruito con Claude Code, e i commit portano trailer <code>Co-Authored-By: Claude</code>. Quattro giorni di calendario di storia non significano che una persona abbia scritto 90 000 righe in quattro giorni; significano che piano, implementazione, test e ADR vengono scritti in cicli brevi e chiusi, e che ogni passo termina con lo stesso rituale di verifica di prima. I numeri qui sotto parlano della quantità di codice e di test, non di quanto lavoro reale ci sia dietro tutto questo.</p>
<figure><img loading="lazy" alt="Crescita del progetto nell'arco di quattro giorni con le milestone da M0 a oggi: migliaia di righe di Rust (da 1,6 a 89,8), numero di funzioni #[test] (da 27 a 739) e numero di ADR (da 13 a 104), con le barre dei commit per giorno (39, 38 e 39 per il 29, 30 e 31 ottobre)." src="https://aldo.tech/blog/urmqtt-vps-handshake-disk-hardening/img/growth.svg" /><figcaption>Crescita del progetto nell'arco di quattro giorni con le milestone da M0 a oggi: migliaia di righe di Rust (da 1,6 a 89,8), numero di funzioni #[test] (da 27 a 739) e numero di ADR (da 13 a 104), con le barre dei commit per giorno (39, 38 e 39 per il 29, 30 e 31 ottobre).</figcaption></figure>
<p>Il grafico mostra tre curve che crescono quasi insieme, ed è proprio l'intenzione: test e decisioni scritte seguono il codice, non restano indietro. La pendenza è massima il 29 ottobre, quando nasce l'intero percorso dal kernel al broker. In seguito le righe si aggiungono più lentamente, ma i test crescono più in fretta del codice (27 test su 1,6 migliaia di righe all'inizio, 739 su 89,8 migliaia oggi).</p>
<table>
<thead>
<tr>
<th>Milestone</th>
<th>Commit</th>
<th>Ora</th>
<th style="text-align: right;">Righe di Rust</th>
<th style="text-align: right;">Test</th>
<th style="text-align: right;">ADR</th>
</tr>
</thead>
<tbody>
<tr>
<td>M0</td>
<td>9412a9c</td>
<td>28.10. 23:48</td>
<td style="text-align: right;">1 592</td>
<td style="text-align: right;">27</td>
<td style="text-align: right;">13</td>
</tr>
<tr>
<td>M5</td>
<td>a61083b</td>
<td>29.10. 01:32</td>
<td style="text-align: right;">9 314</td>
<td style="text-align: right;">88</td>
<td style="text-align: right;">24</td>
</tr>
<tr>
<td>M9</td>
<td>4be8cc9</td>
<td>29.10. 02:21</td>
<td style="text-align: right;">15 729</td>
<td style="text-align: right;">149</td>
<td style="text-align: right;">28</td>
</tr>
<tr>
<td>M11</td>
<td>4800904</td>
<td>29.10. 03:43</td>
<td style="text-align: right;">22 737</td>
<td style="text-align: right;">180</td>
<td style="text-align: right;">30</td>
</tr>
<tr>
<td>M13</td>
<td>a558d5e</td>
<td>29.10. 05:22</td>
<td style="text-align: right;">26 556</td>
<td style="text-align: right;">193</td>
<td style="text-align: right;">32</td>
</tr>
<tr>
<td>M17</td>
<td>7df4f98</td>
<td>29.10. 08:48</td>
<td style="text-align: right;">33 504</td>
<td style="text-align: right;">232</td>
<td style="text-align: right;">36</td>
</tr>
<tr>
<td>S1</td>
<td>ecf72ec</td>
<td>30.10. 00:46</td>
<td style="text-align: right;">41 910</td>
<td style="text-align: right;">299</td>
<td style="text-align: right;">45</td>
</tr>
<tr>
<td>X8</td>
<td>fb1504a</td>
<td>30.10. 19:08</td>
<td style="text-align: right;">56 341</td>
<td style="text-align: right;">456</td>
<td style="text-align: right;">59</td>
</tr>
<tr>
<td>PERF</td>
<td>76f7c36</td>
<td>31.10. 14:19</td>
<td style="text-align: right;">72 511</td>
<td style="text-align: right;">618</td>
<td style="text-align: right;">76</td>
</tr>
<tr>
<td>oggi</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>Le righe sono tutti i file <code>.rs</code> al di fuori di <code>vendor/</code> e <code>fuzz/</code>, i test sono funzioni <code>#[test]</code>. Che cosa ha aggiunto ciascuna fase, in base alla data nella storia di git:</p>
<table>
<thead>
<tr>
<th>Data</th>
<th>Milestone</th>
<th>Che cosa è stato aggiunto</th>
</tr>
</thead>
<tbody>
<tr>
<td>29.10.</td>
<td>da M0 a M17</td>
<td>kernel, rete, TLS, broker, SQLite, più core, TCP proprio, rinforzo, rilascio riproducibile</td>
</tr>
<tr>
<td>29.10. sera e 30.10.</td>
<td>da X0 a X10, S1, H2, H3 (X0 inizia il 29.10. alle 20:54)</td>
<td>port su x86_64, ISO universale, un solo disco e ISO di auto-installazione, controlli TLS, client ostili</td>
</tr>
<tr>
<td>31.10.</td>
<td>da O1 a O4, T1, PERF, H4, H5, H9, da S2 a S6</td>
<td>visibilità e console tramite MQTT, modalità live e diagnostica, SNTP, ottimizzazioni, rinforzo, verifica, supply chain, archiviazione</td>
</tr>
</tbody>
</table>
<h2 id="promessa-1-un-solo-iso-per-piu-piattaforme">Promessa 1: un solo ISO per più piattaforme</h2>
<p><strong>Che cosa avevamo pianificato.</strong> Un unico file che si avvia su UEFI aarch64, UEFI x86_64 e BIOS x86_64. <strong>Che cosa abbiamo costruito.</strong> Il port su x86_64 (da X0 a X10) e un ISO universale con il bootloader Limine, un unico ISO ibrido da cui si avviano tutte queste combinazioni; gli smoke test lo avviano su quattro macchine virtuali. Il menu ha tre voci: urMQTT (modalità live, gira dalla RAM, non scrive nulla sul disco), installazione sul disco della macchina su cui è avviato (l'installazione scrive GPT e dati, poi spegne la macchina) e Diagnostica (rilevamento dell'hardware e misure).</p>
<figure><img loading="lazy" alt="Il menu di avvio dell'ISO, catturato sul VPS: le voci &quot;urMQTT&quot;, &quot;Install urMQTT on this machine's disk&quot; e &quot;Diagnostic: hardware detection and benchmarks&quot; con il conto alla rovescia per l'avvio automatico. L'immagine proviene da un ISO di prova con un conto alla rovescia più lungo; il valore predefinito è 10 secondi." src="https://aldo.tech/blog/urmqtt-vps-handshake-disk-hardening/img/boot-menu.png" /><figcaption>Il menu di avvio dell'ISO, catturato sul VPS: le voci &quot;urMQTT&quot;, &quot;Install urMQTT on this machine's disk&quot; e &quot;Diagnostic: hardware detection and benchmarks&quot; con il conto alla rovescia per l'avvio automatico. L'immagine proviene da un ISO di prova con un conto alla rovescia più lungo; il valore predefinito è 10 secondi.</figcaption></figure>
<p>Il primo bersaglio reale è stato un VPS, non un computer fisico: la decisione è che la prima versione supporti solo VPS (KVM), mentre il bare metal arriva dopo. Abbiamo provato su un VPS Contabo con processore AMD EPYC (famiglia 23, Zen 1), 4 vCPU e 8 GiB di RAM, con rete virtio-net e disco virtio-scsi. Il primo tentativo ci ha sorpreso: l'ISO collegato come CD virtuale avviava la voce predefinita, l'immagine di produzione si è rifiutata di servire da un supporto di sola lettura e si è riavviata, così abbiamo ottenuto un ciclo infinito di menu, kernel, reset senza nulla di leggibile. Oggi un errore fatale nell'immagine di produzione lascia un messaggio sulla console per 60 secondi prima del riavvio e, da un supporto ottico, segnala che l'avvio è avvenuto da un supporto di sola lettura e si ferma. La modalità live in RAM (O3) ha poi aggirato il problema per il caso comune.</p>
<p>La voce Diagnostica è nata da questa esperienza: una macchina che non vediamo dobbiamo poterla interrogare su ciò che ha. Stampa CPU, RAM, rete, dischi e dispositivi PCI, misura la banda della memoria, il disco e la crittografia, e le stesse misure le emette come JSON sulla porta seriale. La scrittura su disco è consentita solo nello spazio non allocato e solo dopo aver digitato <code>WIPE</code>; nell'immagine qui sotto è stata saltata.</p>
<figure><img loading="lazy" alt="Schermata di diagnostica su un VPS Contabo (AMD EPYC, KVM, 4 vCPU): sistema, memoria, disco, dispositivi PCI, banda e latenza della RAM e misure crittografiche, tra cui 244 us per handshake mTLS, X25519 48 us, firma ECDSA 35,7 us e verifica 107 us." src="https://aldo.tech/blog/urmqtt-vps-handshake-disk-hardening/img/diag-vps.png" /><figcaption>Schermata di diagnostica su un VPS Contabo (AMD EPYC, KVM, 4 vCPU): sistema, memoria, disco, dispositivi PCI, banda e latenza della RAM e misure crittografiche, tra cui 244 us per handshake mTLS, X25519 48 us, firma ECDSA 35,7 us e verifica 107 us.</figcaption></figure>
<p><strong>Che cosa è ancora aperto.</strong> L'hardware reale. Dopo la prima versione arriva il bare metal (avvio da USB, NVMe e AHCI, scheda di rete di un computer vero), insieme a IOMMU e Secure Boot, perché senza IOMMU un dispositivo malevolo o difettoso può scrivere su tutta la memoria. In questa versione non affermiamo nulla al riguardo.</p>
<h2 id="lhandshake-da-12-ms-a-024-ms">L'handshake: da 1,2 ms a 0,24 ms</h2>
<p>La prima misura della diagnostica sul VPS ha dato un numero scomodo: l'handshake mTLS, solo la crittografia lato server, richiedeva 1,2 ms. Su aarch64 usavamo già le istruzioni hardware AES e SHA, ma x86_64 non aveva nulla di simile per la chiave pubblica: RustCrypto è portabile e constant-time, e quindi anche lento. Tre modifiche lo hanno ridotto.</p>
<ul>
<li>Per P-256 abbiamo adottato l'assembly <code>nistz256</code> di OpenSSL e ring (ADR-130), con tabelle a base fissa per la firma e per la verifica delle firme della CA (ADR-110, ADR-111).</li>
<li>X25519 è stato riscritto su quattro arti a 64 bit con le istruzioni MULX/ADX (ADR-131).</li>
<li>Dopodiché abbiamo profilato anche il resto (ADR-132, ADR-200).</li>
</ul>
<figure><img loading="lazy" alt="Handshake sul VPS: 1200 us prima, 237 us dopo assembly e tabelle; confronto A/B di memcpy nella stessa esecuzione, 278,8 us con il vecchio, 229,4 us con il nuovo; e prima e dopo per X25519 (da 146 a 48 us), firma ECDSA (da 673 a 34 us) e verifica (da 309 a 90 us)." src="https://aldo.tech/blog/urmqtt-vps-handshake-disk-hardening/img/handshake.svg" /><figcaption>Handshake sul VPS: 1200 us prima, 237 us dopo assembly e tabelle; confronto A/B di memcpy nella stessa esecuzione, 278,8 us con il vecchio, 229,4 us con il nuovo; e prima e dopo per X25519 (da 146 a 48 us), firma ECDSA (da 673 a 34 us) e verifica (da 309 a 90 us).</figcaption></figure>
<table>
<thead>
<tr>
<th>Misura sul VPS (server, microsecondi)</th>
<th style="text-align: right;">Prima</th>
<th style="text-align: right;">Dopo</th>
</tr>
</thead>
<tbody>
<tr>
<td>handshake mTLS, tutta la crittografia</td>
<td style="text-align: right;">1200</td>
<td style="text-align: right;">circa 240 (237 nella misura del grafico, 244 nell'immagine)</td>
</tr>
<tr>
<td>X25519</td>
<td style="text-align: right;">146</td>
<td style="text-align: right;">48</td>
</tr>
<tr>
<td>firma ECDSA</td>
<td style="text-align: right;">673</td>
<td style="text-align: right;">34</td>
</tr>
<tr>
<td>verifica ECDSA</td>
<td style="text-align: right;">309</td>
<td style="text-align: right;">90 (107 nell'immagine)</td>
</tr>
<tr>
<td>handshake, vecchio memcpy, stesso boot</td>
<td style="text-align: right;">278,8</td>
<td style="text-align: right;">229,4 (-17,7%)</td>
</tr>
</tbody>
</table>
<p>I numeri variano di circa il 10% da un'esecuzione all'altra, quindi abbiamo annotato un intervallo anziché un singolo valore. Nel complesso è circa un quinto del tempo di CPU per un handshake.</p>
<p>Che cosa abbiamo imparato su memcpy. Quando la crittografia è scesa, il profilo ha mostrato qualcosa che non ci aspettavamo: le funzioni <code>mem*</code> (memcpy, memmove, memset) consumavano il 7% dell'handshake, dal 13 al 39% del percorso dei record TLS (39% per 64 B, 13% per 4 KiB) e dal 17 al 34% della distribuzione dei PUBLISH. Le abbiamo sostituite con assembly SSE2 per x86_64 (ADR-200), con test differenziali rispetto all'implementazione di riferimento per tutte le dimensioni, gli allineamenti e le sovrapposizioni. Per chiamata sono da 55 a 75 cicli contro da 7 a 12 per 1–64 byte.</p>
<figure><img loading="lazy" alt="A sinistra: la quota di tempo del server nelle funzioni mem* prima della riscrittura (handshake 7%, record TLS 64 B 39%, 256 B 37%, 1 KiB 24%, distribuzione 17–34%). A destra: di quanto è più veloce ciascun percorso dopo (handshake 17,7%, 64 B 26,4%, 256 B 19,6%, 1 KiB 16,1%, 4 KiB 6,6%, distribuzione 7–34%)." src="https://aldo.tech/blog/urmqtt-vps-handshake-disk-hardening/img/hotpaths.svg" /><figcaption>A sinistra: la quota di tempo del server nelle funzioni mem* prima della riscrittura (handshake 7%, record TLS 64 B 39%, 256 B 37%, 1 KiB 24%, distribuzione 17–34%). A destra: di quanto è più veloce ciascun percorso dopo (handshake 17,7%, 64 B 26,4%, 256 B 19,6%, 1 KiB 16,1%, 4 KiB 6,6%, distribuzione 7–34%).</figcaption></figure>
<table>
<thead>
<tr>
<th>Percorso</th>
<th style="text-align: right;">Quota di mem* prima</th>
<th style="text-align: right;">Più veloce dopo</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>record TLS 64 B</td>
<td style="text-align: right;">39%</td>
<td style="text-align: right;">26,4%</td>
</tr>
<tr>
<td>record TLS 256 B</td>
<td style="text-align: right;">37%</td>
<td style="text-align: right;">19,6%</td>
</tr>
<tr>
<td>record TLS 1 KiB</td>
<td style="text-align: right;">24%</td>
<td style="text-align: right;">16,1%</td>
</tr>
<tr>
<td>record TLS 4 KiB</td>
<td style="text-align: right;">13%</td>
<td style="text-align: right;">6,6%</td>
</tr>
<tr>
<td>distribuzione dei messaggi</td>
<td style="text-align: right;">dal 17 al 34%</td>
<td style="text-align: right;">dal 7 al 34%</td>
</tr>
</tbody>
</table>
<p>Come leggere il grafico: più piccolo è il record, maggiore è la parte di tempo dedicata alla copia, e quindi maggiore è il guadagno. Per i messaggi piccoli, che sono la maggior parte del traffico MQTT, è questo che conta.</p>
<p>Che cosa questo non significa. Su x86_64 nativo abbiamo misurato OpenSSL 3.5.5 solo come riferimento per l'assembly (ADR-130), non come confronto fra prodotti. Il confronto Linux più Mosquitto sullo stesso VPS (attività X11) non l'abbiamo ancora fatto. Per questo non affermiamo che urMQTT sia più veloce di Linux: non è stato misurato. I numeri qui sopra confrontano urMQTT con se stesso.</p>
<p>Il sovraccarico è stato il secondo risultato. Sul VPS, da un singolo Mac (RTT 35 ms), 128 client hanno prodotto 618 handshake al secondo, 256 client 393 e 512 client 67, con migliaia di connessioni rifiutate. Non era un plateau, ma un crollo. Le cause le abbiamo trovate nel codice (due sono confermate in QEMU/HVF, sul VPS non si sono potute riprodurre), più di una insieme: una connessione rifiutata riportava il client oltre il segmento successivo che portava un SYN cookie valido, il rifiuto con RST diceva al client di riprovare subito, e la ricezione era O(coda). La correzione (da ADR-080 a ADR-082) scarta il SYN che non sarebbe stato accettato, ricorda i flussi rifiutati, mantiene la ricezione in O(1) e conta gli handshake per core. È possibile (ipotesi, non verificata) che parte della causa fosse anche sul client: lo strumento di carico chiudeva per primo, per cui un singolo Mac con 16 384 porte effimere e un TIME_WAIT di 30 secondi era limitato a circa 550 connessioni al secondo. Per questo il vero limite del server non l'abbiamo ancora misurato.</p>
<h2 id="promessa-2-disco-raw-ma-con-cura">Promessa 2: disco raw, ma con cura</h2>
<p><strong>Che cosa avevamo pianificato.</strong> Mantenere il layout proprio senza file system e aggiungere tutto ciò che un file system di solito offre: allineamento, crescita, discard, copie di sicurezza e verifiche. <strong>Perché non un file system.</strong> Ne abbiamo discusso e la decisione è confermata: urMQTT scrive un solo database, un solo log di telemetria e metadati propri, non servono directory, permessi né nomi di file, e ogni strato in più sarebbe altro codice nella parte fidata e un altro modo in cui una scrittura può sparire in silenzio. Il disco è GPT con una partizione ESP e una partizione dati urMQTT, su cui sta il layout proprio.</p>
<p><strong>Che cosa abbiamo costruito (da S2 a S6).</strong></p>
<ul>
<li><strong>Layout v2 (S2).</strong> Topologia del disco, allineamento a 1 MiB o alla dimensione I/O ottimale, blocchi fisici interi, coppia A/B di superblocchi con generazione (ADR-140). Il layout v1 il kernel lo rifiuta con un messaggio chiaro, e lo migra lo strumento offline <code>urmqtt-disk migrate</code> (ADR-141): per impostazione predefinita è una prova a secco, si copia e solo dopo si passa alla nuova versione, con ripristino e iniezione di errori a ogni scrittura.</li>
<li><strong>Crescita (S3).</strong> All'avvio il disco può crescere (mai ridursi), e su virtio-blk anche durante il funzionamento, tramite l'interrupt di cambio di configurazione (ADR-142, ADR-143).</li>
<li><strong>Discard e write-zeroes (S4).</strong> I dischi thin provisioned restituiscono lo spazio all'host. In un test il file qcow2 sull'host è sceso da 62 a 16 MiB dopo la scadenza dei segmenti di telemetria, e il ripristino non si fida mai del contenuto che è stato scartato.</li>
<li><strong>Copia di sicurezza del database su disco (S5).</strong> Copia A/B con l'API di backup online di SQLite a passi di 16 pagine: il passo più lungo 4,6 ms contro 445 ms quando si copia in una sola chiamata. I commit durante la copia hanno avuto un massimo di 2,4 ms contro 2,2 ms senza copia. Se <code>quick_check</code> all'avvio fallisce, il database viene ripristinato dalla copia.</li>
<li><strong>Scrubbing e avvio limitato (S6).</strong> Lo scrubber legge il disco con un budget I/O limitato e segnala i record danneggiati, mentre l'avvio legge solo il segmento più recente di ogni partizione, così il tempo di avvio non cresce con la dimensione della regione di telemetria. Con questo abbiamo chiuso un problema che avevamo calcolato in anticipo: su un disco grande il ripristino completo richiederebbe da minuti a decine di minuti.</li>
</ul>
<p>L'intero gruppo di smoke test per l'archiviazione passa su entrambe le architetture: 79 riusciti, 0 falliti, su aarch64 sotto HVF e su x86_64 sotto TCG. Abbiamo misurato anche che S4 non ha rallentato il database: 53,3 mila inserimenti al secondo in batch prima e dopo, senza differenza misurabile.</p>
<p><strong>Che cosa ci ha sorpreso.</strong> Quanto di un layout "semplice" sia fatto di casi limite: virtio-scsi e virtio-blk riportano la topologia in modi diversi, e una scrittura incompleta a metà migrazione deve sempre lasciare almeno una versione valida. Per questo ogni scrittura sul percorso di migrazione e di crescita è passata dall'iniezione di errori.</p>
<p><strong>Che cosa è ancora aperto.</strong> Le pagine di SQLite non hanno checksum, quindi un bit modificato all'interno di un valore in una riga il database non lo rileva; i record di telemetria e il contenuto delle copie hanno un CRC e vengono individuati. La cifratura del disco (S7) non è finita.</p>
<h2 id="promessa-3-sicurezza-dimostrabile-per-quanto-ci-riesca">Promessa 3: sicurezza dimostrabile, per quanto ci riesca</h2>
<p><strong>Rinforzo (H4).</strong> L'allocatore azzera la memoria liberata e la verifica al riutilizzo, rileva il double free, collega i puntatori della free list con XOR e azzera le chiavi alla liberazione (ADR-170). Protezione dello stack (<code>-Zstack-protector=strong</code>, 1315 funzioni, mentre SQLite con <code>-fstack-protector-strong</code>, ADR-171). I report di errore in produzione non contengono registri né indirizzi (ADR-172). KASLR su entrambe le architetture: il kernel è PIE e Limine lo sposta di circa 19 bit, mentre heap, stack e finestra MMIO sono posizionati in modo casuale (ADR-173). Su aarch64 MTE marca l'heap (ADR-174), ma solo sotto TCG: HVF e x86 non ce l'hanno. kCFI lo abbiamo valutato e non l'abbiamo adottato perché il compilatore sul nightly fissato va in crash con un errore interno; IOMMU è rinviato insieme al bare metal (ADR-175, ADR-176).</p>
<p><strong>Il costo è misurato, non presunto.</strong> Sul percorso dell'handshake (<code>dp-bench</code>, aarch64 HVF, handshake completi TLS 1.3 + mTLS + CONNECT):</p>
<table>
<thead>
<tr>
<th>Passo</th>
<th style="text-align: right;">1 core</th>
<th style="text-align: right;">4 core</th>
<th>Variazione</th>
</tr>
</thead>
<tbody>
<tr>
<td>senza rinforzo</td>
<td style="text-align: right;">1941</td>
<td style="text-align: right;">7620</td>
<td></td>
</tr>
<tr>
<td>+ allocatore</td>
<td style="text-align: right;">1898</td>
<td style="text-align: right;">7429</td>
<td>da -2,2% a -2,5%</td>
</tr>
<tr>
<td>+ protezione dello stack</td>
<td style="text-align: right;">1845</td>
<td style="text-align: right;">7243</td>
<td>insieme da -4,9% a -5%</td>
</tr>
<tr>
<td>+ PIE e marcatura</td>
<td style="text-align: right;">circa 1800</td>
<td style="text-align: right;">circa 6750</td>
<td>su un computer carico, rumoroso</td>
</tr>
</tbody>
</table>
<p>Nel complesso, quindi, il costo sul percorso dell'handshake è circa il 5% per le parti misurate in modo pulito, e con PIE e marcatura risulta circa il 7% su un core e circa l'11% su quattro, ma questa misura è rumorosa. La riteniamo accettabile, ma non l'abbiamo ripetuta sul VPS x86_64.</p>
<p>Vale la pena confrontarlo con il numero del primo articolo (862 handshake al secondo su un solo core). Lo stesso strumento oggi, con il motore di cifratura hardware su aarch64 (X0b) e una serie di ottimizzazioni arrivate da allora, dà 1941 prima del rinforzo. Non abbiamo isolato il contributo di ogni modifica, quindi sulla causa non affermiamo nulla.</p>
<p><strong>Come è stato verificato, e che cosa no.</strong> H5 ha aggiunto Miri su un sottoinsieme di crate host, dimostrazioni con Kani, sanitizer, un model-fuzzer per le ACL e report di copertura. Kani ha dimostrato <code>record_len</code> in TLS, <code>parse</code> in TCP e due aritmetiche della timer wheel. <strong>Non è dimostrato</strong> (tempo scaduto): entrambi i decoder MQTT, entrambi i parsing della configurazione e il comportamento della timer wheel; il job CI per Kani quindi non è ancora bloccante. Miri non può eseguire assembly. Supply chain (H9): SBOM CycloneDX con 215 componenti, <code>cargo-vet</code> passa con 39 revisioni complete e 140 eccezioni, e cargo-deny è configurato per trattare gli avvisi di vulnerabilità come fallimento della CI (questi job CI finora sono stati provati solo in locale, non su GitHub, e il test negativo non è stato eseguito), insieme a <code>VULNERABILITY-HANDLING.md</code>. Lo stato onesto: delle 67 versioni di crate di terzi collegate al kernel, 53 sono eccezioni, non revisioni. Vet dimostra che l'insieme delle dipendenze non può cambiare senza una differenza visibile, non che il TCB sia stato revisionato. Una nostra revisione di un crate non l'abbiamo scritta.</p>
<p>SNTP (T1) è in questo senso una piccola storia di sicurezza: è opzionale, solo verso server IPv4 privati, con salto massimo e con la condizione che le fonti concordino (mediana, maggioranza entro un secondo). Un orologio che chiunque sul percorso può spostare non è un orologio di cui ci si possa fidare per la validità dei certificati.</p>
<h2 id="sul-mac-aarch64-nativo-e-x86_64-in-emulazione">Sul Mac: aarch64 nativo e x86_64 in emulazione</h2>
<p>Sviluppiamo su un Mac, e questo ha due modi di eseguire lo stesso codice, molto diversi tra loro.</p>
<table>
<thead>
<tr>
<th></th>
<th>aarch64 sotto HVF</th>
<th>x86_64 sotto TCG</th>
</tr>
</thead>
<tbody>
<tr>
<td>Che cos'è</td>
<td>esecuzione nativa, velocità vicina a quella di un processore reale</td>
<td>QEMU emula le istruzioni</td>
</tr>
<tr>
<td>Handshake nel guest</td>
<td>circa 0,58 ms (misurato la prima volta intorno a M13); <code>dp-bench</code> oggi 1941/s per core</td>
<td>circa 3,3 ms</td>
</tr>
<tr>
<td>Scenari smoke TLS</td>
<td>veloci</td>
<td>circa 15 minuti per scenario</td>
</tr>
<tr>
<td>MTE</td>
<td>no (su HVF non c'è)</td>
<td>x86 non ce l'ha; la marcatura dell'heap funziona solo per un guest aarch64 sotto TCG</td>
</tr>
<tr>
<td>A che cosa serve</td>
<td>misure reali</td>
<td>correttezza: il percorso x86 viene verificato, i numeri non sono prestazioni</td>
</tr>
</tbody>
</table>
<p>La regola è semplice: i numeri sotto TCG sono emulazione e non li citiamo come prestazioni. Le prestazioni x86 le misuriamo solo su un VPS vero. Per questo i numeri crittografici qui sopra vengono dal VPS Contabo, non dal Mac.</p>
<h2 id="lezioni">Lezioni</h2>
<ul>
<li><strong>Misurate su una macchina reale prima di ottimizzare.</strong> Il primo approccio è stato ottimizzare ciò che pensavamo fosse lento; solo la diagnostica sul VPS ha mostrato 1,2 ms e dove va a finire esattamente il tempo.</li>
<li><strong>Il sovraccarico è una funzionalità a sé.</strong> Un sistema veloce sotto carico può crollare oltre di esso; rifiutare deve costare meno che accettare.</li>
<li><strong>Il profilo sorprende.</strong> Il maggior guadagno singolo per i messaggi piccoli non era nella crittografia, ma nella copia della memoria.</li>
<li><strong>Un disco senza file system richiede più lavoro, non meno.</strong> Ciò che un file system di solito offre (allineamento, crescita, discard, copie, verifiche) dobbiamo scriverlo e verificarlo da soli, ogni passo con iniezione di errori.</li>
<li><strong>Onestà nei numeri.</strong> Le misure di memoria su un VPS condiviso variano fino a 10 volte tra le esecuzioni e non dicono nulla sul codice, quindi non le riportiamo. I numeri crittografici sono stabili entro circa il 10%.</li>
<li><strong>La velocità di sviluppo non è maturità.</strong> Test e ADR crescono insieme al codice, ma quattro giorni non sono un vero esercizio.</li>
</ul>
<h2 id="che-cosa-viene-dopo">Che cosa viene dopo</h2>
<ul>
<li><strong>H6:</strong> SQLite isolato a livello EL0, così che un errore nel database non apra il kernel.</li>
<li><strong>H7:</strong> il resto della sicurezza del tempo (NTS e gestione dei salti); SNTP è solo il primo passo.</li>
<li><strong>X11:</strong> confronto con Linux e Mosquitto sullo stesso VPS, solo allora ha senso parlare di velocità relativa.</li>
<li><strong>Rilascio:</strong> la prima pubblicazione per VPS (KVM), con artefatti firmati e istruzioni.</li>
<li><strong>Dopo il rilascio:</strong> bare metal (da X12 a X16), IOMMU e Secure Boot, e cifratura del disco.</li>
</ul>
<p>Il codice è su <a href="https://github.com/zeljan-alduk/urMQTT">github.com/zeljan-alduk/urMQTT</a>, e <code>docs/PROGRESS.md</code> resta l'unica fonte di verità su ciò che funziona davvero. Pre-alfa, come prima; segnalazioni di errori e di sicurezza secondo le indicazioni in <code>SECURITY.md</code> sono benvenute.</p>]]></content:encoded>
      <category>rust</category>
      <category>mqtt</category>
      <category>iot</category>
      <category>unikernel</category>
      <category>open source</category>
    </item>
    <item>
      <title>urMQTT: costruiamo un broker MQTT che è anche un sistema operativo</title>
      <link>https://aldo.tech/it/blog/urmqtt-unikernel-broker-mqtt/</link>
      <guid isPermaLink="true">https://aldo.tech/it/blog/urmqtt-unikernel-broker-mqtt/</guid>
      <pubDate>Thu, 29 Oct 2026 08:00:00 +0000</pubDate>
      <description>Note di sviluppo su urMQTT, un unikernel scritto in Rust che è esso stesso un broker MQTT: un unico ELF con TCP/IP, TLS 1.3, broker e SQLite propri, avviato da un solo ISO.</description>
      <content:encoded><![CDATA[<p>I dispositivi installati nei veicoli, nei parcheggi e negli armadi elettrici hanno una caratteristica in comune: inviano messaggi piccoli, di rado, ma sono molti e restano sempre connessi. Da anni li colleghiamo a broker MQTT, e da anni ci poniamo la stessa domanda. Perché per una cosa così semplice servono un intero Linux, OpenSSL, il runtime di un linguaggio, SSH, un gestore di pacchetti e solo alla fine il broker? Ciascuno di questi strati porta con sé le proprie vulnerabilità, consuma la propria memoria e aggiunge un altro elemento che può guastarsi, mentre una flotta di dispositivi chiede al server soltanto tre cose: mantenere molte connessioni quasi inattive, autenticare ogni dispositivo in modo affidabile e trasportare i messaggi senza perderli.</p>
<p>Così è nato <a href="https://github.com/zeljan-alduk/urMQTT">urMQTT</a>: un unikernel che <em>è</em> un broker MQTT. Kernel, TCP/IP, TLS 1.3, MQTT e un database integrato in un unico programma binario Rust che si avvia da un solo ISO. Nessun processo, nessuna chiamata di sistema, nessuna shell. La prima piattaforma su cui viene costruito e collaudato è aarch64, e lo stesso codice è in cammino verso x86_64, tramite UEFI e BIOS classico, e verso l'hardware reale: l'obiettivo è un solo ISO che si avvii su tutti. Il progetto è open source con licenza MIT o Apache 2.0, e in questo articolo vogliamo mettere per iscritto a che punto siamo, com'è fatto al suo interno e che cosa abbiamo imparato lungo la strada.</p>
<h2 id="che-cosa-significa-esattamente-unikernel">Che cosa significa esattamente "unikernel"</h2>
<p>Un broker tradizionale è un programma che gira su un sistema operativo. urMQTT non ha alcun sistema operativo sotto di sé: si avvia da solo attraverso il firmware UEFI e il bootloader Limine, imposta le proprie tabelle delle pagine, programma il controller degli interrupt e il timer, individua i dispositivi virtio tramite PCIe, ottiene un indirizzo via DHCP e ascolta su una sola porta. Tutto ciò che di solito fanno kernel, driver, stack di rete e applicazione, qui è un unico ELF eseguito a livello EL1.</p>
<p>Il confronto con un'installazione tipica parla da sé:</p>
<table>
<thead>
<tr>
<th></th>
<th>Broker tipico su Linux</th>
<th>urMQTT</th>
</tr>
</thead>
<tbody>
<tr>
<td>Servizi esposti</td>
<td>SSH, broker, spesso anche una porta per metriche o amministrazione</td>
<td><strong>solo TCP 8883</strong> (MQTT su TLS 1.3 con mTLS)</td>
</tr>
<tr>
<td>Codice nell'immagine</td>
<td>kernel, userland, libc, OpenSSL, runtime</td>
<td><strong>un solo ELF Rust</strong> (più SQLite incluso nel repository)</td>
</tr>
<tr>
<td>Gestione remota</td>
<td>shell, file di configurazione, aggiornamenti dei pacchetti</td>
<td><strong>nessuna.</strong> Una nuova versione è un nuovo ISO</td>
</tr>
<tr>
<td>Memoria per connessione inattiva</td>
<td>kilobyte (buffer dei socket, oggetti TLS, heap)</td>
<td><strong>obiettivo ≤ 512 B</strong></td>
</tr>
<tr>
<td>Modello multicore</td>
<td>thread e lock oppure un unico event loop</td>
<td><strong>un thread per core, nessuno stato condiviso, elaborazione completa sullo stesso core</strong></td>
</tr>
<tr>
<td>Integrità del codice</td>
<td>sono possibili mappature contemporaneamente scrivibili ed eseguibili</td>
<td><strong>W^X, tabelle delle pagine bloccate, PAC/BTI</strong></td>
</tr>
</tbody>
</table>
<p>Le priorità sono, in quest'ordine: sicurezza, scalabilità, prestazioni multicore, ingombro ridotto e installazione immutabile. Quando due obiettivi entrano in conflitto, prevale quello che sta più in alto nell'elenco.</p>
<h2 id="a-che-punto-siamo">A che punto siamo</h2>
<p>Il progetto è in fase pre-alfa, ma sono state chiuse diciassette milestone di sviluppo, da M0 a M17. Oggi urMQTT serve MQTT 3.1.1 su TLS 1.3 con certificati client obbligatori e conserva lo stato in SQLite su disco raw. In breve, ciò che già funziona:</p>
<ul>
<li><strong>Kernel:</strong> vettori di eccezione propri con report decodificati, tabelle delle pagine proprie con W^X applicato, pagine di guardia, PAC e BTI dove il processore li supporta, GICv3 e GICv2, timer generico, SMP fino a 64 core.</li>
<li><strong>Rilevamento dell'hardware</strong> da DTB o ACPI in un'unica descrizione della piattaforma, con risultato identico da entrambe le fonti.</li>
<li><strong>Driver:</strong> PCIe ECAM, MSI-X, virtio-net, virtio-blk, virtio-scsi e virtio-rng, più un CSPRNG inizializzato con entropia hardware.</li>
<li><strong>Rete:</strong> DHCPv4, ARP, ICMP e IPv4 rigoroso, tutti propri, e poi un TCP proprio solo lato server: SYN cookie, timer wheel gerarchica, NewReno, coalescenza degli ACK, nessuno stato TIME_WAIT.</li>
<li><strong>TLS:</strong> solo TLS 1.3, mTLS obbligatorio, identità del dispositivo ricavata dal CN o dal SAN URI; l'handshake è gestito da rustls, dopodiché subentra un livello di record proprio con AES-128-GCM e ChaCha20-Poly1305. Ripresa della sessione con ticket PSK, lista di revoca, controllo di ammissione già in fase di handshake.</li>
<li><strong>MQTT:</strong> codec zero-copy, sottoposto a fuzzing e testato con veri client mosquitto; QoS 0 e 1, wildcard, messaggi retained, last will (LWT), sessioni persistenti, ACL limitate a <code>devices/{id}/#</code>, limiti di frequenza per dispositivo.</li>
<li><strong>Archiviazione:</strong> SQLite dentro il kernel tramite un VFS su disco raw, in modalità WAL, con verifica dell'integrità all'avvio; registro dei dispositivi, revoche, messaggi retained, sessioni persistenti con code di messaggi per i dispositivi offline; per ogni core un log di telemetria separato con CRC per record e ripristino dopo un crash.</li>
<li><strong>Esercizio e sorveglianza:</strong> timer watchdog per i gestori, health check, logging limitato, syslog e statsd su UDP, un'ammissione che resiste alle tempeste di riconnessioni.</li>
<li><strong>Distribuzione:</strong> build ripetute bit per bit e verificate in CI, ISO e immagine disco GPT raw, firme minisign, istruzioni per KVM, Proxmox e Hetzner.</li>
</ul>
<p>Ogni passo è contrassegnato dalla milestone che lo ha portato, e il file <code>docs/PROGRESS.md</code> nel repository è l'unica fonte di verità su ciò che funziona davvero.</p>
<h2 id="larchitettura-in-tre-immagini">L'architettura in tre immagini</h2>
<p>L'intera progettazione è descritta in <code>docs/ARCHITECTURE.md</code>, mentre le ragioni di ogni decisione importante sono registrate come ADR in <code>docs/DECISIONS.md</code>. Ecco una panoramica.</p>
<h3 id="gli-strati">Gli strati</h3>
<figure><img loading="lazy" alt="Gli strati di urMQTT: dal broker, passando per il codec MQTT, TLS, TCP e la rete, fino ai driver virtio e allo strato architetturale, il tutto in un unico ELF a livello EL1." src="https://aldo.tech/blog/urmqtt/img/layers.svg" /><figcaption>Gli strati di urMQTT: dal broker, passando per il codec MQTT, TLS, TCP e la rete, fino ai driver virtio e allo strato architetturale, il tutto in un unico ELF a livello EL1.</figcaption></figure>
<p>Dall'alto verso il basso: il broker (sessioni, albero dei topic, QoS, messaggi retained, will, ACL, limiti), il codec MQTT, TLS, TCP, lo strato di rete e infine i driver. Accanto a questo percorso principale si trova l'archiviazione (SQLite e log di telemetria), e sotto a tutto la gestione della memoria, il rilevamento della piattaforma e la configurazione, compilata in costanti al momento della build, insieme al certificato CA, al certificato del server e alla chiave incorporati. L'hypervisor con i dispositivi virtio è l'unica cosa che resta fuori dall'immagine.</p>
<h3 id="un-thread-per-core-nessuno-stato-condiviso">Un thread per core, nessuno stato condiviso</h3>
<figure><img loading="lazy" alt="Modello di esecuzione: ogni core dati ha il proprio ciclo e la propria porzione di broker, mentre il core con il numero più alto esegue soltanto SQLite. I core comunicano esclusivamente tramite anelli SPSC." src="https://aldo.tech/blog/urmqtt/img/cores.svg" /><figcaption>Modello di esecuzione: ogni core dati ha il proprio ciclo e la propria porzione di broker, mentre il core con il numero più alto esegue soltanto SQLite. I core comunicano esclusivamente tramite anelli SPSC.</figcaption></figure>
<p>Ogni core dati esegue un proprio ciclo con il proprio stack TCP, i propri stati TLS, una porzione del broker (sessioni e albero dei topic), la cache dell'heap e la partizione di telemetria. I core non condividono alcuno stato mutabile, ma soltanto anelli limitati e privi di lock, uno per ogni coppia ordinata di core. Il core con il numero più alto esegue SQLite e nient'altro.</p>
<p>La scheda di rete ha oggi una sola coppia di code, gestita dal core 0. Questo smista ogni flusso MQTT su uno dei core di lavoro in base all'hash Toeplitz della sua quadrupla e invia ciò che i core di lavoro restituiscono. Una sessione vive sul proprio core di appartenenza, determinato dall'hash dell'identificatore del client, così che take-over, will e persistenza non attraversino mai i core. Una connessione che atterra su un altro core vi mantiene il proprio TCP e TLS, ma inoltra l'MQTT in chiaro al core di appartenenza con un controllo di flusso basato su crediti. Ogni messaggio instradato viene inoltre distribuito alle altre porzioni del broker, ciascuna delle quali tiene una replica dei messaggi retained.</p>
<p>Tre regole tengono insieme questo modello. <strong>Elaborazione completa sullo stesso core:</strong> un pacchetto ricevuto viene elaborato per intero su un solo core, dal TCP alla decifratura TLS sul posto, dal parsing MQTT all'instradamento fino alla risposta cifrata, senza code di task e senza copie nei buffer applicativi. <strong>Polling adattivo:</strong> sotto carico le code di ricezione vengono interrogate attivamente, mentre a riposo il core torna agli interrupt e a <code>WFI</code>; i gestori degli interrupt si limitano a confermare l'interrupt e a segnalare il lavoro. <strong>Watchdog:</strong> un tick da 10 ms per core intercetta i gestori che superano il proprio budget.</p>
<h3 id="la-vita-di-un-publish">La vita di un PUBLISH</h3>
<figure><img loading="lazy" alt="Il percorso di un messaggio PUBLISH con QoS 1: dal record TLS sulla scheda di rete, passando per la decifratura, il controllo ACL e l'instradamento, fino al PUBACK unito al TCP ACK in un unico segmento." src="https://aldo.tech/blog/urmqtt/img/publish.svg" /><figcaption>Il percorso di un messaggio PUBLISH con QoS 1: dal record TLS sulla scheda di rete, passando per la decifratura, il controllo ACL e l'instradamento, fino al PUBACK unito al TCP ACK in un unico segmento.</figcaption></figure>
<p>Il dispositivo invia un record TLS. La scheda di rete lo consegna al ciclo del core tramite un descrittore, senza copie. Il ciclo verifica il TCP, decifra il TLS sul posto e analizza l'MQTT da un buffer preso in prestito, poi controlla in memoria l'ACL, il limite di frequenza e il percorso nell'albero dei topic. Se un altro core è interessato a quel topic, riceve un messaggio di instradamento con il riferimento al payload e l'identificatore del topic; il database riceve un record di telemetria o l'aggiornamento di un messaggio retained, in batch. La risposta PUBACK viene cifrata direttamente nel buffer in uscita e unita al TCP ACK, così al dispositivo parte un solo segmento. Per l'altro core il messaggio costa una cifratura per sottoscrittore al momento dell'invio.</p>
<h2 id="quanto-costa-una-connessione">Quanto costa una connessione</h2>
<p>L'obiettivo è <strong>al massimo 512 byte per connessione TLS inattiva</strong>, con la tensione verso i 256. Finché la connessione è a riposo, per essa non viene trattenuto alcun buffer: i dati ricevuti vengono elaborati direttamente dal frame di rete, e i buffer per invio, record e MQTT esistono solo finché qualcosa è incompleto o non confermato. Il kernel misura da sé anche questo: contatori esatti dell'heap, in byte richiesti e in blocchi occupati, registrati ogni dieci secondi come heap oltre il livello di base per connessione aperta.</p>
<table>
<thead>
<tr>
<th>Componente</th>
<th style="text-align: right;">Dimensione</th>
<th>Come</th>
</tr>
</thead>
<tbody>
<tr>
<td>Blocco di controllo TCP</td>
<td style="text-align: right;">96 B</td>
<td>flag compattati, RTT/RTO a 16 bit, slot nella timer wheel, nessun buffer a riposo</td>
</tr>
<tr>
<td>Slot nella tabella TCP</td>
<td style="text-align: right;">4–8 B</td>
<td>solo l'indice della connessione, tabella piena al massimo a metà</td>
</tr>
<tr>
<td>Connessione e stato dei record TLS 1.3</td>
<td style="text-align: right;">152 B</td>
<td>segreto e sequenza per direzione (40 B); chiave e IV derivati per record</td>
</tr>
<tr>
<td>Connessione MQTT</td>
<td style="text-align: right;">48 B</td>
<td>identità e will sull'heap solo finché esistono</td>
</tr>
<tr>
<td>Sessione MQTT, forma hot</td>
<td style="text-align: right;">64 B</td>
<td>una sottoscrizione in linea, identificatori dei nodi internati</td>
</tr>
<tr>
<td>Sottoscrizione <code>devices/&lt;id&gt;/cmd/#</code></td>
<td style="text-align: right;">0 nodi dell'albero</td>
<td>memorizzata in forma relativa all'identificatore di sessione, condivisa da tutti i dispositivi</td>
</tr>
</tbody>
</table>
<p>Misurato sotto HVF con quattro core virtuali, di cui tre di lavoro, con ogni dispositivo che mantiene una connessione mTLS e una sottoscrizione QoS 1, compreso l'arrotondamento dell'allocatore:</p>
<table>
<thead>
<tr>
<th style="text-align: right;">Connessioni</th>
<th style="text-align: right;">Byte per connessione</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>Il pool dei frame di rete è fisso, di circa 2 MiB, ovvero circa 200 B per connessione con diecimila connessioni e circa 2 B con un milione. Oltre questo non siamo riusciti a misurare: il networking utente di QEMU su macOS ha smesso di accettare connessioni a 10 197, più o meno al limite di 10 240 descrittori del processo QEMU. La cifra scende man mano che le parti fisse si ripartiscono su più connessioni, quindi non cresce con il loro numero.</p>
<h2 id="il-modello-di-sicurezza">Il modello di sicurezza</h2>
<p>La sicurezza è la prima priorità e vince tutti i conflitti con gli altri obiettivi. Il modello è composto da più strati:</p>
<ul>
<li><strong>Una sola porta.</strong> TCP 8883, solo TLS 1.3, certificati client obbligatori firmati dalla CA incorporata nell'immagine. L'identificatore del client MQTT deve coincidere con l'identificatore del dispositivo presente nel certificato.</li>
<li><strong>Nessun punto d'ingresso.</strong> Nessuna shell, nessun input da console, nessun SSH, nessuna API di amministrazione, nessun codice dinamico, nessun SQL raggiungibile dalla rete e nessun file system.</li>
<li><strong>Sicurezza della memoria.</strong> Rust, con <code>forbid(unsafe_code)</code> nei crate di protocollo; ogni blocco <code>unsafe</code> ha un commento <code>// SAFETY:</code> verificato da un lint, e sull'input esterno non ci sono panic: clippy nei crate del percorso dati vieta <code>unwrap</code>, <code>expect</code>, l'indicizzazione e <code>panic!</code>.</li>
<li><strong>Mitigazione dello sfruttamento.</strong> W^X è applicato tramite la codifica dei descrittori e il bit <code>SCTLR_EL1.WXN</code>, e viene riverificato a ogni avvio. Dopo l'avvio le tabelle delle pagine sono di sola lettura, senza alias scrivibili. La metà inferiore dello spazio di indirizzamento è esclusa. Ogni stack ha pagine di guardia e le eccezioni girano su uno stack separato. PAC e BTI si attivano quando il processore li supporta, e lo verificano gli smoke test.</li>
<li><strong>Resistenza agli abusi.</strong> SYN cookie; l'ammissione rifiuta le nuove connessioni, prima di qualsiasi crittografia, quando l'heap è basso, quando il database è in ritardo, quando ci sono troppi handshake in corso per core o per indirizzo di origine, oppure quando la frequenza degli handshake è troppo alta. Limiti di frequenza per dispositivo, code limitate, finestra TCP legata alla contropressione del broker, logging limitato, watchdog e health check.</li>
<li><strong>Immutabilità.</strong> Configurazione, CA e chiave del server sono incorporate nell'immagine. Cambiare qualsiasi cosa significa una nuova immagine.</li>
</ul>
<p>Il modello delle minacce è descritto in <code>docs/SECURITY.md</code>.</p>
<h2 id="archiviazione-senza-file-system">Archiviazione senza file system</h2>
<p>I dati risiedono su un disco separato, virtio-blk o virtio-scsi, privo di file system. Il layout è minimale e proprio:</p>
<pre><code>LBA 0      [ superblocco: magic · versione del layout · uuid · tabella delle regioni · CRC-32C ]
LBA 8, 16  [ tabella delle dimensioni dei file, slot A/B (vince il più recente valido) ]
LBA 64     [ database principale SQLite ½ ][ WAL SQLite ⅛ ][ journal 1 MiB ][ log di telemetria: 8 partizioni … ]
ultimi 4 KiB [ area per l'autoverifica del dispositivo a blocchi all'avvio ]
</code></pre>
<p>SQLite (3.54.1 incluso nel repository, con verifica dell'hash in fase di build) è compilato nel kernel con clang, senza libreria standard, e usa un heap <code>memsys5</code> di dimensione fissa. Il suo VFS mappa tre file fissi su regioni del disco. Funziona in modalità WAL con <code>locking_mode=EXCLUSIVE</code>, quindi non gli serve un indice in memoria condivisa, e con <code>synchronous=FULL</code>; se <code>quick_check</code> rileva un errore all'avvio, il sistema si ferma in modo sicuro. Nel database si trovano il registro dei dispositivi, le revoche, i messaggi retained e le sessioni persistenti con le code di messaggi QoS 1 per i dispositivi non connessi.</p>
<p>Il core del database, quello con il numero più alto, esegue SQLite e nient'altro. Il piano dati gli invia le variazioni di stato tramite un anello SPSC privo di lock; lui le raggruppa in un'unica transazione ogni 5 ms o ogni 512 operazioni e restituisce le conferme con un secondo anello. Un disco vuoto viene formattato al primo avvio; in ogni altro caso non si formatta mai sopra ciò che esiste.</p>
<p>La telemetria segue un altro percorso. Ogni PUBLISH accettato sui topic indicati in <code>storage.telemetry_topics</code> finisce nella partizione del log di telemetria del proprio core: un anello di segmenti con CRC per record, salato con la sequenza del segmento, in modo che i vecchi giri dell'anello non ricompaiano mai. I core passano i record al database con un anello proprio, senza copie, e questo li scrive ogni 5 ms o ogni 256 KiB. All'avvio il ripristino conserva ogni record scritto, tronca la coda incompleta e riprende in un nuovo segmento. La conservazione avviene per dimensione, attraverso l'anello, e per età, attraverso <code>retention_days</code>. Lo abbiamo verificato con un milione di record e con <code>kill -9</code> nel bel mezzo della scrittura.</p>
<h2 id="i-numeri">I numeri</h2>
<p>Lo strumento di carico in <code>tools/loadgen</code> fa tre cose: una tempesta di handshake TLS 1.3 con mTLS e CONNECT, connessioni di lunga durata con PINGREQ che pubblicano telemetria ogni secondo, e tempeste di riconnessioni. <code>make bench</code> su un Apple M4 Pro sotto HVF dà:</p>
<table>
<thead>
<tr>
<th>Core di lavoro</th>
<th>handshake/s nel guest</th>
<th>scalabilità</th>
<th>tramite rete utente QEMU (connessioni/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>Il dato nel guest, dove client e server si trovano nella stessa macchina virtuale e ogni core di lavoro opera per conto proprio, mostra che il piano dati scala in modo lineare. Il dato di rete è limitato dal networking utente a thread singolo di QEMU, non dal guest; anche questo è registrato come ADR-032.</p>
<p>Le tempeste di riconnessioni le proviamo facendo connettere tutti i dispositivi contemporaneamente, ripetendo con attesa i tentativi rifiutati e facendoli poi cadere tutti senza DISCONNECT. Il parametro <code>heap_limit_mb=</code> in modalità di sviluppo induce l'ammissione a ritenere l'heap più piccolo di quanto sia, così una tempesta più grande dell'heap si può provare con pochi socket. In uno di questi round con 3 000 dispositivi, 1 368 si sono connessi in 36 secondi e 5 619 tentativi sono stati rifiutati in modo ordinato, con un limite di 8 MiB; dopo la tempesta la memoria è tornata al livello iniziale.</p>
<h2 id="come-lavoriamo">Come lavoriamo</h2>
<p>Il progetto è guidato da una specifica scritta, <code>PROMPT.md</code>, con il piano delle milestone, e ogni milestone si chiude con lo stesso rituale: <code>make check</code> e <code>make smoke</code> verdi, un commit con l'etichetta <code>M&lt;n&gt;</code>, una voce in <code>docs/PROGRESS.md</code> e un ADR per ogni decisione non ovvia. Le regole sono brevi e non negoziabili: nessun panic sull'input esterno, <code>unsafe</code> solo nei crate hardware e FFI e sempre con un commento, nessuna allocazione sul percorso caldo, un commento per ogni ordinamento delle operazioni atomiche, ogni limite proviene dalla configurazione, e le parti provvisorie intenzionali falliscono in modo rumoroso.</p>
<p>La CI fa più di quanto riusciremmo a fare a mano: fmt, clippy per l'host e per <code>aarch64-unknown-none</code>, typos, actionlint e shellcheck; test su Linux e macOS su arm64; build del kernel per la configurazione di sviluppo e di produzione con la dimensione dell'ELF nel riepilogo; avvio sotto QEMU con tutti gli scenari degli smoke test e salvataggio dei log seriali; cargo-deny per advisory, licenze e sorgenti; un minuto di fuzzing per ciascuno degli undici target, e di notte dieci minuti per target con corpus in cache. A ciò si aggiungono CodeQL, OpenSSF Scorecard, Dependabot e un rilascio con provenienza SLSA. Ogni action di terze parti è bloccata sull'SHA completo del commit.</p>
<p>La riproducibilità la prendiamo alla lettera: <code>make repro</code> compila HEAD due volte in alberi di lavoro puliti e confronta ELF, ISO e immagine raw byte per byte. I rilasci sono firmati con minisign e gli artefatti si possono verificare con <code>gh attestation verify</code>. L'immagine di produzione contiene la chiave privata del server, quindi l'ISO stesso è un segreto; le immagini allegate ai rilasci su GitHub sono di sviluppo, con chiavi temporanee, e servono solo per fare prove.</p>
<h2 id="che-cosa-abbiamo-imparato">Che cosa abbiamo imparato</h2>
<p>Alcune lezioni che hanno cambiato il piano:</p>
<ul>
<li><strong>Un TCP proprio alla fine è valso la pena.</strong> Il prototipo girava su smoltcp e ci è stato utile fino a M13. Ma un server che mantiene centinaia di migliaia di connessioni non ha bisogno di un TCP lato client: servono SYN cookie, una timer wheel e uno stato di 96 byte. In M14 abbiamo sostituito smoltcp con uno stack proprio solo lato server, con test di perdita simulata e fuzzing dei segmenti.</li>
<li><strong>La memoria va misurata dall'interno.</strong> Le stime per connessione erano inutili finché il kernel non ha iniziato a contare da sé l'heap in blocchi dell'allocatore anziché in byte richiesti. Solo allora abbiamo visto dove vanno davvero a finire i 512 byte e siamo scesi a 494.</li>
<li><strong>Lo strumento di test fa parte del prodotto.</strong> Il limite maggiore non era il kernel, ma il networking utente di QEMU e il numero di porte effimere sul computer di sviluppo. Per centomila connessioni e oltre servono <code>vmnet-shared</code> su macOS oppure Linux con KVM, interfaccia tap e più indirizzi client.</li>
<li><strong>SQLite senza sistema operativo è possibile</strong>, ma solo a condizioni rigorose: heap fisso, lock esclusivo senza memoria condivisa, un VFS che non conosce i file, e un'ammissione che non lascia accumulare ritardo al database.</li>
<li><strong>Le tempeste di riconnessioni sono la normalità</strong>, non l'eccezione. Quando la rete mobile cade e poi torna, migliaia di dispositivi bussano nello stesso istante. Per questo l'ammissione rifiuta la connessione prima di qualsiasi crittografia, e ogni connessione rifiutata costa quasi nulla.</li>
</ul>
<h2 id="che-cosa-viene-dopo">Che cosa viene dopo</h2>
<p>Il lavoro successivo a M17 è pianificato in tre direzioni, e l'ordine tra di esse è registrato in <code>docs/PLAN.md</code>. Nulla di quanto segue è ancora stato realizzato.</p>
<ul>
<li><strong>Un solo ISO per più piattaforme.</strong> Lo stesso file <code>urmqtt-&lt;env&gt;-&lt;versione&gt;.iso</code> e un'unica immagine GPT raw che si avviano su UEFI aarch64, UEFI x86_64 e BIOS x86_64, in macchine virtuali e su hardware reale, a partire da un PC Intel. Sempre un appliance con tutto al proprio interno, riproducibile e firmato.</li>
<li><strong>Disco raw, senza file system.</strong> Una specifica del formato a livello di byte, lo strumento host <code>urmqtt-disk</code> che condivide i parser con il kernel, allineamento delle regioni a 1 MiB, crescita del disco con i dati, discard per i dischi thin provisioned, snapshot A/B del database, verifica dei bit e, a scelta, cifratura AES-256-XTS con AES hardware.</li>
<li><strong>Sicurezza dimostrabile.</strong> Revoca e lista dei dispositivi consentiti a runtime tramite un canale di controllo firmato, senza una nuova immagine; test differenziali e di conformità del TLS e del TCP propri; KASLR, protezione dello stack, kCFI, MTE e un allocatore rinforzato; SQLite isolato a livello EL0; Secure Boot; SBOM e cargo-vet; e infine un pacchetto per la revisione esterna.</li>
</ul>
<p>Le piattaforme che offrono soltanto dispositivi non virtio, come AWS ENA o Azure MANA, per ora non sono supportate. Raspberry Pi 4 e 5 come host KVM, Hetzner CAX e Oracle Ampere A1 sono documentati e supportati nel codice, ma non sono ancora stati provati su un servizio reale.</p>
<h2 id="provatelo">Provatelo</h2>
<p>Il repository si trova su <a href="https://github.com/zeljan-alduk/urMQTT">github.com/zeljan-alduk/urMQTT</a>. Con Rust e la toolchain nightly fissata, QEMU, <code>make certs</code>, <code>make run</code> e un client mosquitto bastano per il primo PUBLISH su mTLS sul proprio computer. L'avvertenza iniziale vale anche alla fine: questo è pre-alfa, e tutto ciò che funziona è scritto in <code>docs/PROGRESS.md</code>. Segnalazioni di errori, domande sulla progettazione e segnalazioni di sicurezza secondo le indicazioni in <code>SECURITY.md</code> sono benvenute. Vi aggiorneremo qui a ogni prossima milestone.</p>]]></content:encoded>
      <category>rust</category>
      <category>mqtt</category>
      <category>iot</category>
      <category>unikernel</category>
      <category>open source</category>
    </item>
    <item>
      <title>Apriamo un blog: appunti dal campo</title>
      <link>https://aldo.tech/it/blog/apriamo-un-blog/</link>
      <guid isPermaLink="true">https://aldo.tech/it/blog/apriamo-un-blog/</guid>
      <pubDate>Sat, 17 Oct 2026 08:00:00 +0000</pubDate>
      <description>Perché un&#x27;azienda che costruisce terminali, app ed elettronica per il trasporto pubblico inizia a scrivere, e di cosa leggerete qui.</description>
      <content:encoded><![CDATA[<p>La maggior parte di ciò che costruiamo non la vede nessuno. Il software nel terminale alla porta dell'autobus, il firmware in un dispositivo che non avrà mai uno schermo, il server che di notte trasforma le transazioni della giornata in report. Quando tutto funziona, funziona in silenzio. Quando non funziona, se ne accorgono tutti.</p>
<p>Su questo blog annotiamo ciò che abbiamo imparato lungo la strada. Non testi di marketing, ma appunti concreti: cosa è andato storto, come l'abbiamo risolto e cosa faremmo diversamente oggi.</p>
<h2 id="di-cosa-scriveremo">Di cosa scriveremo</h2>
<ul>
<li><strong>Bigliettazione nel trasporto pubblico.</strong> Carte contactless, moduli SAM, funzionamento offline e tutto ciò che un terminale deve sopportare in un veicolo che vibra dodici ore al giorno.</li>
<li><strong>App mobili sul campo.</strong> Android e Flutter su dispositivi che non sono telefoni comuni: con lettori di carte, stampanti e batterie che devono reggere un intero turno.</li>
<li><strong>Backend e monitoraggio della flotta.</strong> Come sapere che un dispositivo a bordo ha un problema prima che lo segnali l'autista.</li>
<li><strong>Hardware.</strong> Dal prototipo sul banco alla scheda che va in produzione di serie, e tutti gli errori nel mezzo.</li>
<li><strong>Modelli di IA in locale.</strong> Dove i modelli sul proprio hardware hanno senso, e dove il cloud e un'API sono semplicemente la scelta migliore.</li>
</ul>
<h2 id="come-scriveremo">Come scriveremo</h2>
<p>In modo breve e concreto, con codice e numeri dove li abbiamo. Pubblicheremo quando avremo qualcosa da dire, non secondo un calendario. Gli articoli sono scritti in croato e tradotti in tutte le lingue di questo sito; potete seguirci anche via <a href="https://aldo.tech/it/blog/feed.xml">RSS</a>.</p>
<p>Se vi interessa un argomento o state lavorando a un problema simile, scriveteci a <a href="mailto:info@aldo.tech">info@aldo.tech</a>.</p>]]></content:encoded>
      <category>introduzione</category>
    </item>
  </channel>
</rss>
