<?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/pt/blog/</link>
    <atom:link href="https://aldo.tech/pt/blog/feed.xml" rel="self" type="application/rss+xml"/>
    <description>Notas do terreno: transporte público, bilhética, hardware e software que têm de funcionar na estrada.</description>
    <language>pt</language>
    <item>
      <title>urMQTT depois do VPS: SQLite em modo utilizador, comparação com o Mosquitto, configuração como dado e um construtor no navegador</title>
      <link>https://aldo.tech/pt/blog/urmqtt-config-builder-plaintext-mosquitto/</link>
      <guid isPermaLink="true">https://aldo.tech/pt/blog/urmqtt-config-builder-plaintext-mosquitto/</guid>
      <pubDate>Sun, 01 Nov 2026 08:00:00 +0000</pubDate>
      <description>O que aconteceu depois do segundo texto sobre o urMQTT: SQLite isolado fora do kernel, medições contra o Mosquitto no mesmo VPS (e o que não correu a nosso favor), causas encontradas para o fluxo único lento, configuração como dado com um construtor web de ISO e um emulador no navegador, MQTT simples opcional e o que ainda não está provado.</description>
      <content:encoded><![CDATA[<p>O segundo texto, <a href="https://aldo.tech/blog/urmqtt-vps-handshake-disk-hardening/">urMQTT num VPS real</a>, terminou com uma lista "O que se segue": H6 (SQLite fora do kernel), H7 (segurança do tempo), X11 (comparação com o Mosquitto no mesmo VPS) e a primeira versão. Este texto segue essa lista pela mesma ordem e depois acrescenta duas coisas que não estavam nela, porque surgiram pelo caminho: a configuração como dado, com um construtor web de ISO, e o MQTT simples opcional, sem TLS. Como antes: o que planeámos, o que construímos, o que nos surpreendeu e o que continua em aberto. Neste texto há também alguns resultados que não favorecem o urMQTT; deixámo-los porque fazem parte da verdade.</p>
<p>A nota prévia mantém-se: o projeto é pré-alfa, a primeira versão destina-se apenas a VPS com KVM, e os números medem código, testes e experiências determinadas, não maturidade em produção.</p>
<h2 id="como-isto-se-desenrolou-no-tempo">Como isto se desenrolou no tempo</h2>
<p>Desde o texto anterior (31.10. às 23:00) até hoje (1.11. às 20:41) o repositório tem 59 commits novos, 178 no total. Todos nasceram num único dia de calendário, pelo que vale o mesmo aviso de antes: o projeto é construído com o Claude Code (os commits trazem <code>Co-Authored-By: Claude</code>), o plano, o código, os testes e os ADR nascem em ciclos curtos e fechados, e os números abaixo falam da quantidade de código e de verificações, não de quanto trabalho humano há neles.</p>
<figure><img loading="lazy" alt="Crescimento desde o texto anterior: linhas de Rust de 89 790 a 112 408, funções #[test] de 739 a 926, ADR de 104 a 126 e commits de 119 a 178. Cada linha tem a sua própria escala." src="https://aldo.tech/blog/urmqtt-config-builder-plaintext-mosquitto/img/growth-since.svg" /><figcaption>Crescimento desde o texto anterior: linhas de Rust de 89 790 a 112 408, funções #[test] de 739 a 926, ADR de 104 a 126 e commits de 119 a 178. Cada linha tem a sua própria escala.</figcaption></figure>
<p>Os números foram calculados com o mesmo método do texto anterior: todos os ficheiros <code>.rs</code> fora de <code>vendor/</code> e <code>fuzz/</code>, funções <code>#[test]</code>, títulos <code>## ADR-</code> em <code>docs/DECISIONS.md</code>.</p>
<table>
<thead>
<tr>
<th>Momento</th>
<th>Commit</th>
<th>Hora</th>
<th style="text-align: right;">Linhas de Rust</th>
<th style="text-align: right;">Testes</th>
<th style="text-align: right;">ADR</th>
</tr>
</thead>
<tbody>
<tr>
<td>texto anterior</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 concluído</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>construtor web e espelho das versões</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 simples, documentação</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>hoje</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>Trabalhou-se em paralelo em vários ramos, pelo que a ordem dos commits não é estritamente a ordem do trabalho. Os ADR estão numerados até 232, mas a numeração tem lacunas (parte foi renumerada ao juntar ramos); contamos os títulos, não o número mais alto. Versões: v0.2.0 (1.11., 18:45 UTC) e v0.2.1 (1.11., 20:22 UTC), ambas marcadas como pre-release.</p>
<h2 id="h6-sqlite-em-modo-utilizador">H6: SQLite em modo utilizador</h2>
<p><strong>O que planeámos.</strong> O SQLite são 254 268 linhas de C que fazem o parsing da base de dados, do WAL e do diário a partir do disco, e o disco pode ser fabricado ou alterado. Até aí corria no kernel, com os privilégios do kernel. <strong>O que construímos</strong> (ADR-210 a ADR-212): o SQLite, as suas partes da libc e o heap foram movidos para um programa separado, <code>dbworker</code>, que corre em EL0 (aarch64) ou ring 3 (x86_64), no seu próprio espaço de endereçamento. O kernel já não liga o SQLite. O worker comunica com o kernel através de uma janela de memória partilhada e de doze chamadas de sistema, e cada comprimento e deslocamento devolvidos é verificado antes de ser usado. Um erro no worker (acesso ilegal, instrução indefinida, orçamento de tempo de 30 s excedido) termina a chamada, o worker é reiniciado e a base de dados é aberta a partir do WAL como num arranque; após 5 reinícios num minuto o armazenamento é declarado avariado, em vez de reiniciar indefinidamente.</p>
<p>O custo, medido em aarch64 sob HVF, a mesma VM, kernel com SQLite incorporado contra kernel com worker, seis arranques alternados de cada:</p>
<table>
<thead>
<tr>
<th>Medição</th>
<th>Antes (no kernel)</th>
<th>Depois (worker)</th>
<th>Diferença</th>
</tr>
</thead>
<tbody>
<tr>
<td>entrada e saída do modo utilizador</td>
<td>-</td>
<td>1,1 us por ciclo</td>
<td></td>
</tr>
<tr>
<td><code>db-bench</code>, 5 000 inserções em 50 transações, melhor / 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>uma inserção por transação, melhor / 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>cópia de segurança completa numa só chamada (23 MiB)</td>
<td>514 ms</td>
<td>635 ms</td>
<td>+23%</td>
</tr>
</tbody>
</table>
<p>As escritas em disco (1 572) e os flushes (319) são idênticos, como tem de ser. O que não está isolado é isto: <strong>o kernel continua a confiar no que o worker devolve</strong>, depois de verificar a forma. Um worker comprometido pode mentir (por exemplo, devolver uma lista vazia de certificados revogados e assim "desrevogá-los"), mas não pode ler a memória do kernel, as chaves TLS nem as chaves dos tickets. Não há KPTI, pelo que as fugas do tipo Spectre/Meltdown não estão impedidas e não afirmamos nada sobre elas; a chave PAC é partilhada por EL0 e EL1. Em x86_64 não medimos o custo da mudança em KVM (sob TCG o número não faria sentido), e o fuzzing do próprio worker e do protocolo ainda não está concluído. O conjunto de testes de armazenamento (79 cenários, cada um a usar o SQLite no worker) passa em aarch64 HVF e x86_64 TCG, e em aarch64 TCG passam 76 (um é ignorado porque o TCG não tem MSI-X).</p>
<p><strong>O H7 está fechado, mas não da forma que anunciámos.</strong> O limite inferior do tempo (ADR-050) e o SNTP com consenso (T1) cobrem o âmbito do H7; o NTS (RFC 8915) fica fora do âmbito e está registado como risco residual, não como algo que funciona. Não consideramos que a segurança do tempo esteja "resolvida", mas sim que a sua parte demonstrável está concluída.</p>
<p><strong>Nova opção de rede.</strong> A Contabo não dá DHCP a um ISO personalizado, pelo que o VPS precisava, até aqui, de uma imagem diferente da da VM de laboratório. Agora <code>network.mode = "auto"</code> (ADR-214) pergunta primeiro ao DHCP e, se não houver concessão dentro do prazo (<code>dhcp_timeout_s</code>, por omissão 15 s) ou se a perder, aplica o endereço estático por omissão. Experimentado manualmente em QEMU (com DHCP usa a concessão, sem rede volta ao endereço estático ao fim de 5 s); ainda não existe um cenário de smoke para isto.</p>
<h2 id="x11-comparacao-com-o-mosquitto-no-mesmo-vps">X11: comparação com o Mosquitto no mesmo VPS</h2>
<p>O mesmo tipo de máquina do texto anterior (Contabo, 4 vCPU AMD EPYC, 8 GiB), carga a partir de um anfitrião Linux a 15 ms de distância, TLS 1.3 mútuo com os mesmos certificados dos dois lados, Mosquitto 2.0.21 em Debian 13 sem persistência. O método e as decisões estão no ADR-213, os dados em bruto em <code>docs/perf-results/</code>. Mediana de três execuções.</p>
<figure><img loading="lazy" alt="Handshakes completos por segundo (TLS 1.3 + certificado de cliente + CONNECT, sem retoma de sessão) com 32, 64 e 128 clientes: urMQTT 169, 278, 413; Mosquitto 82, 140, 184. São limites inferiores: o gerador de carga estava perto do seu próprio limite." src="https://aldo.tech/blog/urmqtt-config-builder-plaintext-mosquitto/img/mosquitto.svg" /><figcaption>Handshakes completos por segundo (TLS 1.3 + certificado de cliente + CONNECT, sem retoma de sessão) com 32, 64 e 128 clientes: urMQTT 169, 278, 413; Mosquitto 82, 140, 184. São limites inferiores: o gerador de carga estava perto do seu próprio limite.</figcaption></figure>
<table>
<thead>
<tr>
<th></th>
<th>urMQTT</th>
<th>Mosquitto 2.0.21</th>
</tr>
</thead>
<tbody>
<tr>
<td>handshakes/s, 32 / 64 / 128 clientes (perfil bench)</td>
<td>169 / 278 / 413</td>
<td>82 / 140 / 184</td>
</tr>
<tr>
<td>o mesmo, com os limites tal como são entregues</td>
<td>185 / 262 / 333 (com 128: 202 a 286 erros, resets de handshake)</td>
<td>-</td>
</tr>
<tr>
<td>memória por ligação inativa (20 000 ligações)</td>
<td>0,50 KB de heap do kernel (0,66 KB com 5 000)</td>
<td>26,8 KB de crescimento do processo</td>
</tr>
<tr>
<td>latência p50, QoS 0 / QoS 1, 800 mensagens/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 mensagens/s, entregues/s (p50)</td>
<td>4 209 (77 ms)</td>
<td>4 235 (197 ms)</td>
</tr>
<tr>
<td>100 subscritores, 50 mensagens/s: p50</td>
<td>16,6 ms</td>
<td>99 ms</td>
</tr>
<tr>
<td>fluxo único, QoS 0, 64 B, publicadas/s</td>
<td>2 962 (0 a 7 733)</td>
<td>22 749</td>
</tr>
</tbody>
</table>
<p>O que isto significa e o que não significa. Os handshakes são <strong>limites inferiores para os dois servidores</strong>: o gerador de carga é uma VM partilhada com 60 a 70% de tempo de CPU roubado e, com 128 threads, estava perto do seu próprio limite, enquanto os núcleos de trabalho do urMQTT estavam ocupados apenas 12 a 13%. A razão com 128 clientes (cerca de 2,2 vezes) é a mais significativa; com 32 e 64 mede sobretudo a espera dos clientes. Os números de memória não contam o mesmo (heap do kernel contra RSS do processo), pelo que "cerca de 50 vezes menos" é uma ordem de grandeza, não uma razão precisa. A latência de 14 ms é uma ida e volta da rede (cerca de 15 ms), ou seja, "sem tempo mensurável do broker"; as caudas (p99 cerca de 100 a 200 ms) são dominadas, nos dois brokers, pelos anfitriões partilhados. Não afirmamos diferenças abaixo de cerca de 20%.</p>
<p><strong>O que não correu a nosso favor</strong> e ficou nas tabelas: um fluxo TCP único através do urMQTT transportou 2 a 8 vezes menos do que através do Mosquitto, com variações de zero a 7,7 mil mensagens por segundo entre execuções, e o watchdog de 20 ms do handler <strong>interrompeu ligações vivas</strong> quando o hipervisor parou um vCPU (na configuração de produção <code>action = "drop"</code>, 6 a 8 ligações por execução com inundação QoS 0). As duas coisas são descritas na secção seguinte, porque tinham causas no nosso código.</p>
<h2 id="porque-e-que-o-fluxo-unico-era-lento-causas-nao-desculpas">Porque é que o fluxo único era lento: causas, não desculpas</h2>
<p>O mais fácil seria dizer "VPS partilhado". Não o fizemos: com o mesmo VPS e o mesmo método, procurámos as causas. Foram encontradas cinco, quatro no caminho de dados e uma no watchdog (ADR-221 a ADR-224):</p>
<table>
<thead>
<tr>
<th>Sintoma no X11</th>
<th>Causa no urMQTT</th>
<th>Alteração</th>
</tr>
</thead>
<tbody>
<tr>
<td>fluxo único 1,9k mensagens/s, muitas vezes <strong>0 entregues</strong></td>
<td><strong>Despertar perdido:</strong> o ciclo de saída devolvia a lista de ligações prontas por cima da que acabara de ser escrita; um subscritor cujo buffer de saída enchesse uma vez nunca mais era servido até um PINGRESP o acordar</td>
<td>corrigido (ADR-223)</td>
</tr>
<tr>
<td>uma perda de alguns por cento transforma 48k em 14k mensagens/s</td>
<td><strong>cada segmento a seguir a um perdido era descartado</strong> (receção apenas por ordem), pelo que a recuperação custava uma ida e volta da rede por segmento</td>
<td>fila de segmentos fora de ordem, SACK (ADR-222)</td>
</tr>
<tr>
<td>teto de 4,4 MB/s a 15 ms</td>
<td>janela de 16 bits, fila de envio de 64 KiB e arranque lento que contava ACK, não bytes</td>
<td>escala da janela, timestamps, contagem de bytes (ADR-222)</td>
</tr>
<tr>
<td>cada ida e volta de um fluxo que cabe espera 0 a 10 ms</td>
<td><strong>o despachante dormia até ao tick seguinte de 10 ms</strong> (o virtio-net não tem interrupção de receção)</td>
<td>polling da NIC a cada 200 us enquanto houver ligações (ADR-224)</td>
</tr>
<tr>
<td>1 a 8 ligações interrompidas por execução</td>
<td><strong>o watchdog cobrava ao handler o tempo decorrido</strong>, e o VPS pára os vCPU com frequência e por muito tempo</td>
<td>o orçamento é o tempo em que o núcleo realmente trabalhou (ADR-221)</td>
</tr>
</tbody>
</table>
<p>As paragens são reais: neste VPS os núcleos 0 a 2 tiveram uma mediana de 36 a 80 ticks atrasados por 5 segundos, e a pior paragem na janela teve mediana de cerca de 70 ms e chegou a 0,9 s (núcleo 3, base de dados e diário: até 1,55 s), enquanto o anfitrião não comunica tempo roubado (<code>host steal 0 us</code>). Por isso o watchdog precisava de outro relógio, não de um orçamento maior.</p>
<figure><img loading="lazy" alt="Antes e depois, no mesmo VPS com o mesmo método, mediana de três a seis execuções: fluxo único QoS 0, 64 B: publicadas 1,9k, 23,7k (Mosquitto 29,3k), entregues 0 (mediana), 12,6k (29,2k). 16 pares, inundação, 64 B: publicadas 24,7k, 43,2k (31,4k), entregues 20,0k, 37,4k (30,7k)." src="https://aldo.tech/blog/urmqtt-config-builder-plaintext-mosquitto/img/before-after.svg" /><figcaption>Antes e depois, no mesmo VPS com o mesmo método, mediana de três a seis execuções: fluxo único QoS 0, 64 B: publicadas 1,9k, 23,7k (Mosquitto 29,3k), entregues 0 (mediana), 12,6k (29,2k). 16 pares, inundação, 64 B: publicadas 24,7k, 43,2k (31,4k), entregues 20,0k, 37,4k (30,7k).</figcaption></figure>
<table>
<thead>
<tr>
<th></th>
<th>código X11 (7f9517e)</th>
<th>agora (86a71ff)</th>
<th>Mosquitto</th>
</tr>
</thead>
<tbody>
<tr>
<td>fluxo único, QoS 0, 64 B, publicadas / entregues por s</td>
<td>1,9k / 0</td>
<td>23,7k / 12,6k</td>
<td>29,3k / 29,2k</td>
</tr>
<tr>
<td>fluxo único, 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 pares, inundação, 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 pares, inundação, 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>ligações perdidas por execução (16 pares, 64 B)</td>
<td>13 (7 a 16)</td>
<td>0 (0 a 0)</td>
<td>0</td>
</tr>
</tbody>
</table>
<p>Watchdog: 148 ligações interrompidas em 30 execuções com carga antes, 0 em 24 execuções depois; num aparelho local que atrasa e perde tramas, com 0,1% de perda um fluxo único de 64 B com 14,3k mensagens/s (kernel do X11) passa a ter 41,0k, e com 0,5% de 5,0k para 17,7k.</p>
<p><strong>O que isto não diz.</strong> O urMQTT <strong>continua a não ser mais rápido do que o Mosquitto num fluxo único</strong>: com 64 B publica cerca de 80% da velocidade dele e entrega cerca de 40%, porque descarta QoS 0 quando o fluxo do subscritor é mais lento do que o do publicador (o Mosquitto, sem limite, põe em fila). Com 1 KiB na inundação de 16 pares o Mosquitto está à frente cerca de 15%, o que está dentro do ruído. O gerador voltou a ser uma VM partilhada com 58 a 77% de tempo roubado. A segunda ronda (bloqueio do heap fora do caminho de dados, SACK do lado do envio e outras coisas) está no ramo não integrado <code>perf-single-flow-wip</code>; os seus números ainda não são afirmações e não os apresentamos aqui.</p>
<h2 id="a-promessa-que-nao-fizemos-mas-cumprimos-a-configuracao-e-dado">A promessa que não fizemos, mas cumprimos: a configuração é dado</h2>
<p>Até agora cada configuração era uma nova compilação: as chaves e os limites estavam incorporados na imagem. O C1 inverte isto (ADR-215 a ADR-219): o kernel é compilado uma vez e a configuração é dado.</p>
<ul>
<li><strong>Blob <code>urmqtt-cfg</code></strong> (C1a): registo versionado e canónico (campos por etiquetas crescentes, impressão digital SHA-256, assinatura Ed25519 separada opcional), 70 campos com valores por omissão e limites, e um descodificador que rejeita tudo o que esteja incorreto.</li>
<li><strong>O kernel lê-o como módulo do Limine</strong> (C1b), antes de qualquer outra coisa, em aarch64 e x86_64. Existe <strong>um único kernel de versão para cada <code>ENV</code> exceto <code>dev</code></strong> (os ELF aarch64 de <code>release</code>, <code>prod</code> e <code>bench</code> têm o mesmo SHA-256; verificado apenas em aarch64). O kernel de versão rejeita uma configuração de desenvolvimento e, sem configuração, escreve "refusing to serve" e não serve nada.</li>
<li><strong>O disco instalado transporta a configuração</strong> e a atualização mantém-na (C1c).</li>
<li><strong><code>urmqtt-config</code> e o modelo de ISO</strong> (C1d): <code>pack</code>, <code>show</code>, <code>verify</code>, e <code>pack --iso</code> substitui as duas cópias do espaço de configuração e os 24 resumos de módulos num ISO já pronto. O resultado é byte a byte igual ao ISO que <code>mkiso.sh --config</code> constrói. O modelo da versão v0.2.0 tem 63 111 168 bytes.</li>
<li><strong>Declaração de confiança</strong> (ADR-219): uma imagem configurada escreve no arranque a impressão digital da configuração (<code>config: fingerprint ...</code>), e a assinatura é opcional e <strong>desativada por omissão</strong>. Um ISO alterado não tem o resumo oficial; a impressão digital é a forma de ver o que está dentro dele.</li>
</ul>
<h2 id="o-construtor-em-aldotech">O construtor em aldo.tech</h2>
<p>O que <code>urmqtt-config pack --iso</code> faz, faz também uma página web, com o mesmo código: a biblioteca de ferramentas compilada para WebAssembly (1,06 MB, sem importações e sem blocos <code>unsafe</code>). A página está em <a href="https://aldo.tech/pt/produtos/urmqtt-builder/">aldo.tech/pt/produtos/urmqtt-builder/</a> e existe nos 15 idiomas do sítio (por exemplo <a href="https://aldo.tech/proizvodi/urmqtt-builder/">hr</a>).</p>
<ul>
<li><strong>Nada sai do navegador.</strong> Os certificados, as chaves privadas e o ISO pronto ficam no computador; a página só descarrega os ficheiros da versão deste sítio, sem rastreamento nem scripts de terceiros. Num teste em produção, nenhum pedido transportou material de chaves.</li>
<li><strong>Espelho das versões</strong> (ADR-227): de hora a hora (systemd timer, baixa prioridade) copia as versões do GitHub, verifica cada uma antes de a publicar, e a página, no navegador, verifica a assinatura de <code>SHA256SUMS</code> com a chave incorporada na página, e só depois escreve a configuração no modelo. Uma verificação falhada bloqueia a transferência.</li>
<li><strong>Simples por omissão, avançado quando necessário</strong> (ADR-232): o primeiro ecrã tem três passos (onde vai funcionar, os seus certificados, o nome da imagem). Um único campo de arrastar ficheiros separa certificado, chave e CA pelo conteúdo. A vista avançada abre cada um dos 70 campos, pesquisa, importação e exportação de TOML; as duas vistas editam o mesmo estado. O modelo (63 MB) é descarregado enquanto o utilizador preenche o formulário. Num ensaio em produção, com um espelho de teste e uma chave de teste, a construção do ISO no navegador demorou 5 a 18 s.</li>
</ul>
<figure><img loading="lazy" alt="Vista simples do construtor: três passos, &quot;Onde vai funcionar?&quot;, &quot;Os seus certificados&quot; e &quot;Nome da imagem&quot;, com uma única ação seguinte na barra lateral." src="https://aldo.tech/blog/urmqtt-config-builder-plaintext-mosquitto/img/builder-simple.png" /><figcaption>Vista simples do construtor: três passos, &quot;Onde vai funcionar?&quot;, &quot;Os seus certificados&quot; e &quot;Nome da imagem&quot;, com uma única ação seguinte na barra lateral.</figcaption></figure>
<figure><img loading="lazy" alt="Vista avançada, capturada num espelho de teste com uma chave de teste (a data da versão e a chave de assinatura não são reais): versão com assinatura verificada, modelos, cada campo da configuração." src="https://aldo.tech/blog/urmqtt-config-builder-plaintext-mosquitto/img/builder-advanced.png" /><figcaption>Vista avançada, capturada num espelho de teste com uma chave de teste (a data da versão e a chave de assinatura não são reais): versão com assinatura verificada, modelos, cada campo da configuração.</figcaption></figure>
<p><strong>Emulador, opcional.</strong> Ao lado de "Descarregar ISO" está "Experimentar no emulador": o QEMU compilado para WebAssembly executa a imagem acabada de construir num separador do navegador. Nada dele é descarregado antes do clique (cerca de 3 MB da primeira vez). O emulador é só BIOS, <strong>sem rede</strong> (para que o tráfego do utilizador não passe por um intermediário e desminta o "nada sai do navegador"), precisa de cerca de 1,5 GB de memória e de um navegador de computador (testado o Chrome; o Firefox e o Safari não). Nele o urMQTT chega a <code>URMQTT READY</code> em cerca de 8 s (com a contagem decrescente ignorada) e escreve a mesma impressão digital da configuração que a página, o que prova que foi arrancada exatamente essa configuração. O arranque é cerca de 2 vezes mais lento do que o QEMU nativo no mesmo Mac (0,82 s contra 1,9 s), e a criptografia cerca de 58 vezes mais lenta porque as instruções de hardware são emuladas. Por isso <strong>não tiramos do emulador nenhuma afirmação sobre velocidade</strong>, e a página diz-o antes do arranque. Na construção foi preciso corrigir quatro bytes no <code>.wasm</code> devido a um erro na tradução da instrução POPCNT no emulador original (ainda não o comunicámos); o próprio emulador está sob GPL, com oferta do código-fonte.</p>
<figure><img loading="lazy" alt="Entrada de diagnóstico no emulador em produção em aldo.tech: o kernel escreveu a mesma impressão digital da configuração que a página, e o ecrã mostra o diagnóstico (no emulador termina em cerca de 20 s; os seus números descrevem o emulador, não o urMQTT)." src="https://aldo.tech/blog/urmqtt-config-builder-plaintext-mosquitto/img/emulator-diagnostic.png" /><figcaption>Entrada de diagnóstico no emulador em produção em aldo.tech: o kernel escreveu a mesma impressão digital da configuração que a página, e o ecrã mostra o diagnóstico (no emulador termina em cerca de 20 s; os seus números descrevem o emulador, não o urMQTT).</figcaption></figure>
<h2 id="mqtt-simples-so-para-redes-em-que-confia">MQTT simples, só para redes em que confia</h2>
<p>O MQTT simples (TCP 1883, sem TLS) é muito comum em IoT: é o predefinido no Mosquitto, na maioria dos firmwares para Arduino e ESP, no Tasmota, no Zigbee2MQTT e no Home Assistant, e usa-se também dentro de redes industriais segmentadas, atrás de VPN e em LAN privadas, incluindo em dispositivos sem relógio (os certificados exigem uma data), sem flash ou processador suficientes para TLS. Numa rede não fiável não é aceitável: quem está no caminho lê e altera o tráfego. Por isso o P1 (ADR-229 a ADR-231) é uma escolha explícita e limitada, <strong>desativada por omissão</strong>, e a porta TLS mantém-se; o TLS não é desligado por isto, a porta simples vem juntamente com ele.</p>
<ul>
<li><strong>A lista de redes permitidas é obrigatória</strong> (<code>allow_from</code>, CIDR) e é verificada no SYN: fora da lista não há SYN-ACK nem estado. A internet inteira (<code>0.0.0.0/0</code>) exige uma opção explícita à parte.</li>
<li><strong>Três modos de autenticação:</strong> palavra-passe (por omissão quando ativado; PBKDF2-HMAC-SHA-256, sal, 10 000 iterações, comparação em tempo constante, bloqueio por endereço), anónimo (uma identidade configurada com a sua própria ACL) e por endereço (<code>ip</code>). O construtor calcula o hash da palavra-passe no navegador e guarda em memória apenas o resumo.</li>
<li><strong>Menos direitos do que o mTLS:</strong> um cliente simples nunca tem direitos de serviço, de operador nem de gestão, não tem <code>$SYS</code> nem consola, tem sessões próprias (<code>$plain/&lt;identidade&gt;/&lt;id do cliente&gt;</code>, apenas limpas), limites próprios, e o registo de permitidos continua a vinculá-lo.</li>
<li><strong>O que se perde está em <code>SECURITY.md</code>:</strong> confidencialidade, integridade, autenticidade do broker e a repetição de um início de sessão gravado. Isto não se corrige com configuração; é assim quando não há TLS.</li>
</ul>
<p>Medido num portátil (Apple silicon, HVF, rede de utilizador do QEMU), 16 pares, 64 B, três execuções de 10 s: com 8 000 mensagens por segundo o caminho TLS e o simples entregam todas as mensagens com a mesma latência (p50 0,17 ms contra 0,13 a 0,18 ms), novas ligações por segundo: TLS cerca de 2 600 a 2 750, simples anónimo cerca de 4 900 a 7 800, e <strong>o início de sessão com palavra-passe é a parte cara</strong>: um PBKDF2 custa cerca de 2,5 ms, pelo que o número de novos inícios de sessão fica limitado a cerca de 200 por núcleo de dados por segundo (medido 308 a 342 em dois núcleos). A inundação nesta máquina não é um instrumento estável (20k a 440k entregues por segundo entre execuções) e <strong>não tiramos conclusões dela</strong>. A comparação do caminho simples com o Mosquitto no VPS ainda não foi feita. Testes: o grupo de smoke <code>plaintext</code> 7 de 7 em aarch64 HVF e em x86_64 TCG, novo alvo de fuzz <code>plain_input</code> (2,3 milhões de execuções sem achados).</p>
<h2 id="erros-que-o-trabalho-em-producao-encontrou">Erros que o trabalho em produção encontrou</h2>
<p>Duas coisas não foram apanhadas pelos testes, mas sim pela primeira ativação no sítio real:</p>
<ul>
<li>A página pedia os ficheiros da versão <strong>um diretório acima do correto</strong>, pelo que cada verificação de assinatura respondia 404 (correção 13af301). O teste usava um espelho de teste noutro caminho.</li>
<li><strong>O teclado do emulador nunca chegava ao convidado</strong>: a causa não era uma tradução incorreta das teclas, mas o facto de o diálogo nem sequer estar a escutar as teclas (correção no ADR-232, com uma entrada de arranque que não precisa de teclas).</li>
</ul>
<h2 id="licoes">Lições</h2>
<ul>
<li><strong>Procurem a causa, não culpem o ambiente.</strong> A maior parte das fraquezas do fluxo único e todas as ligações interrompidas eram nossas; o hipervisor é real, mas era preciso medi-lo, não presumi-lo.</li>
<li><strong>A comparação com outro produto tem de incluir também as perdas.</strong> Uma tabela em que só há vitórias nossas não seria uma comparação.</li>
<li><strong>O instrumento de medição tem limites.</strong> Um gerador partilhado com 60 a 70% de tempo roubado muda o que os números significam; por isso escrevemos "limite inferior" e não afirmamos diferenças abaixo de 20%.</li>
<li><strong>O isolamento não é uma prova.</strong> O SQLite em EL0 reduz o que um erro nele pode fazer, não aquilo sobre que pode mentir.</li>
<li><strong>Uma instalação real encontra o que os testes não pedem</strong>: um caminho mal composto e teclas que ninguém escuta.</li>
</ul>
<h2 id="o-que-ainda-nao-esta-provado">O que ainda não está provado</h2>
<ul>
<li>A chave de assinatura das versões é, por agora, uma chave de teste; a v0.2.0 e a v0.2.1 são pre-release, e as imagens que as acompanham são de desenvolvimento com certificados de teste (exceto o modelo, que não tem configuração).</li>
<li>13 traduções da página do construtor (e deste texto) foram escritas por uma IA e verificadas por testes e amostras, não por falantes nativos.</li>
<li>A comparação com o Mosquitto é uma sessão, um caminho e um gerador partilhado, num VPS partilhado; diferenças abaixo de 20% não são afirmações.</li>
<li>O caminho simples não foi medido no VPS nem contra o Mosquitto.</li>
<li>O Lighthouse na nova vista simples não está confirmado (vista antiga: acessibilidade 95 ou mais).</li>
<li>Bare metal, custo do worker em x86_64 em KVM, Firefox e Safari para o emulador: não medido.</li>
</ul>
<h2 id="o-que-se-segue">O que se segue</h2>
<ul>
<li><strong>D1: broker de demonstração público.</strong> Não podemos tornar público o emulador do navegador: vive no separador de um visitante, é 20 a 100 vezes mais lento do que o KVM, não tem rede de entrada e desaparece quando o separador é fechado; redirecionar a internet para o navegador de outra pessoa seria também um abuso. É possível um verdadeiro broker de demonstração: o urMQTT num VPS atrás de um nome em aldo.tech, com uma CA de teste publicada, uma ACL estrita e pequenos limites. Por agora é apenas uma ideia, não uma construção, e precisa de consentimento explícito para a exposição pública.</li>
<li><strong>Segunda ronda para o fluxo único</strong> (ramo <code>perf-single-flow-wip</code>): só quando estiver medida no VPS e integrada.</li>
<li><strong>O caminho simples contra o Mosquitto no VPS</strong>, início de sessão e revogação de contas para MQTT simples através do canal de gestão assinado.</li>
<li><strong>Depois da versão:</strong> bare metal (X12 a X16), IOMMU e Secure Boot, cifragem do disco.</li>
</ul>
<p>O código está em <a href="https://github.com/zeljan-alduk/urMQTT">github.com/zeljan-alduk/urMQTT</a>, e <code>docs/PROGRESS.md</code> continua a ser a única fonte de verdade sobre o que realmente funciona, <code>docs/PERFORMANCE.md</code> sobre o que foi medido. Pré-alfa, como antes; relatórios de erros e relatórios de segurança, segundo as instruções em <code>SECURITY.md</code>, são bem-vindos.</p>]]></content:encoded>
      <category>rust</category>
      <category>mqtt</category>
      <category>iot</category>
      <category>unikernel</category>
      <category>open source</category>
    </item>
    <item>
      <title>urMQTT num VPS real: ISO, handshake de 0,24 ms, disco próprio e kernel reforçado</title>
      <link>https://aldo.tech/pt/blog/urmqtt-vps-handshake-disk-hardening/</link>
      <guid isPermaLink="true">https://aldo.tech/pt/blog/urmqtt-vps-handshake-disk-hardening/</guid>
      <pubDate>Sat, 31 Oct 2026 08:00:00 +0000</pubDate>
      <description>O que aconteceu às três promessas do primeiro texto sobre o urMQTT (um único ISO, disco em bruto sem sistema de ficheiros, comprovadamente seguro) depois dos primeiros quatro dias de desenvolvimento, com medições, tabelas e uma lista do que ainda não está feito.</description>
      <content:encoded><![CDATA[<p>No <a href="https://aldo.tech/blog/urmqtt-unikernel-mqtt-broker/">primeiro texto sobre o urMQTT</a> descrevemos um unikernel que é um broker MQTT, até ao marco M17, e terminámos com três promessas sob o título "O que se segue": um único ISO que arranca em várias plataformas, um disco em bruto sem sistema de ficheiros e uma segurança que se pode provar, e não apenas afirmar. Nada disso estava então construído. Este texto acompanha o que aconteceu a cada promessa, pela mesma ordem de antes: o que planeámos, o que construímos, o que nos surpreendeu e o que continua em aberto. Pelo caminho mostramos também quanto tempo isto demorou, porque faz parte da história.</p>
<p>Uma nota prévia: o projeto continua pré-alfa. Os números do texto medem código e testes, não maturidade em produção, e a primeira versão está deliberadamente limitada a VPS com KVM. O bare metal fica para mais tarde.</p>
<h2 id="como-isto-se-desenrolou-no-tempo">Como isto se desenrolou no tempo</h2>
<p>O primeiro commit foi feito a 28 de outubro de 2026 às 23:48 (M0). A 31 de outubro às 23:00 o repositório tem 119 commits, distribuídos como 3, 39, 38 e 39 por dia de calendário. Digamos já como isso é possível: o projeto é construído com o Claude Code, e os commits trazem trailers <code>Co-Authored-By: Claude</code>. Quatro dias de calendário de histórico não significam que uma pessoa tenha escrito 90 000 linhas em quatro dias; significam que o plano, a implementação, os testes e os ADR são escritos em ciclos curtos e fechados, e que cada passo termina com o mesmo ritual de verificação de antes. Os números abaixo falam da quantidade de código e de testes, não de quanto de tudo isto foi validado em trabalho real.</p>
<figure><img loading="lazy" alt="Crescimento do projeto ao longo de quatro dias com os marcos de M0 até hoje: milhares de linhas de Rust (de 1,6 a 89,8), número de funções #[test] (de 27 a 739) e número de ADR (de 13 a 104), com colunas de commits por dia (39, 38 e 39 para 29, 30 e 31 de outubro)." src="https://aldo.tech/blog/urmqtt-vps-handshake-disk-hardening/img/growth.svg" /><figcaption>Crescimento do projeto ao longo de quatro dias com os marcos de M0 até hoje: milhares de linhas de Rust (de 1,6 a 89,8), número de funções #[test] (de 27 a 739) e número de ADR (de 13 a 104), com colunas de commits por dia (39, 38 e 39 para 29, 30 e 31 de outubro).</figcaption></figure>
<p>O gráfico mostra três curvas que crescem quase em conjunto, o que é intencional: os testes e as decisões registadas acompanham o código, em vez de ficarem para trás. A inclinação é maior a 29 de outubro, quando nasce todo o caminho do kernel até ao broker. Depois as linhas acrescentam-se mais devagar, mas os testes crescem mais depressa do que o código (27 testes para 1,6 mil linhas no início, 739 para 89,8 mil hoje).</p>
<table>
<thead>
<tr>
<th>Marco</th>
<th>Commit</th>
<th>Hora</th>
<th style="text-align: right;">Linhas de Rust</th>
<th style="text-align: right;">Testes</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>hoje</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>As linhas são todos os ficheiros <code>.rs</code> fora de <code>vendor/</code> e <code>fuzz/</code>, e os testes são funções <code>#[test]</code>. O que cada fase acrescentou, segundo as datas do histórico do git:</p>
<table>
<thead>
<tr>
<th>Data</th>
<th>Marcos</th>
<th>O que foi acrescentado</th>
</tr>
</thead>
<tbody>
<tr>
<td>29.10.</td>
<td>M0 a M17</td>
<td>kernel, rede, TLS, broker, SQLite, vários núcleos, TCP próprio, reforço, versão reprodutível</td>
</tr>
<tr>
<td>29.10. à noite e 30.10.</td>
<td>X0 a X10, S1, H2, H3 (o X0 começa a 29.10. às 20:54)</td>
<td>port para x86_64, ISO universal, um só disco e ISO de autoinstalação, verificações de TLS, clientes atacantes</td>
</tr>
<tr>
<td>31.10.</td>
<td>O1 a O4, T1, PERF, H4, H5, H9, S2 a S6</td>
<td>observabilidade e consola através de MQTT, modo live e diagnóstico, SNTP, acelerações, reforço, verificação, cadeia de abastecimento, armazenamento</td>
</tr>
</tbody>
</table>
<h2 id="promessa-1-um-iso-para-varias-plataformas">Promessa 1: um ISO para várias plataformas</h2>
<p><strong>O que planeámos.</strong> Um único ficheiro que arranca em UEFI aarch64, UEFI x86_64 e BIOS x86_64. <strong>O que construímos.</strong> O port para x86_64 (X0 a X10) e um ISO universal com o bootloader Limine, um ISO híbrido a partir do qual arrancam todas essas combinações; os smoke tests arrancam-no em quatro máquinas virtuais. O menu tem três entradas: urMQTT (modo live, corre a partir da RAM, não escreve nada no disco), instalação no disco da máquina em que está a correr (a instalação escreve o GPT e os dados e depois desliga a máquina) e Diagnóstico (deteção de hardware e medições).</p>
<figure><img loading="lazy" alt="O menu de arranque do ISO, capturado no VPS: entradas &quot;urMQTT&quot;, &quot;Install urMQTT on this machine's disk&quot; e &quot;Diagnostic: hardware detection and benchmarks&quot;, com contagem decrescente até ao arranque automático. A captura é de um ISO de teste com uma contagem mais longa; o valor predefinido é 10 segundos." src="https://aldo.tech/blog/urmqtt-vps-handshake-disk-hardening/img/boot-menu.png" /><figcaption>O menu de arranque do ISO, capturado no VPS: entradas &quot;urMQTT&quot;, &quot;Install urMQTT on this machine's disk&quot; e &quot;Diagnostic: hardware detection and benchmarks&quot;, com contagem decrescente até ao arranque automático. A captura é de um ISO de teste com uma contagem mais longa; o valor predefinido é 10 segundos.</figcaption></figure>
<p>O primeiro alvo real foi um VPS, e não um computador físico: a decisão é que a primeira versão suporte apenas VPS (KVM), e o bare metal vem depois. Testámos num VPS da Contabo com um processador AMD EPYC (família 23, Zen 1), 4 vCPU e 8 GiB de RAM, com rede virtio-net e disco virtio-scsi. A primeira tentativa surpreendeu-nos: o ISO ligado como CD virtual arrancava a entrada predefinida, a imagem de produção recusava-se a servir a partir de um suporte só de leitura e reiniciava, e ficámos com um ciclo infinito de menu, kernel, reinício sem nada legível. Hoje, um erro fatal na imagem de produção deixa uma mensagem na consola durante 60 segundos antes de reiniciar e, a partir de um suporte ótico, escreve que arrancou de um suporte só de leitura e pára. O modo live em RAM (O3) contornou depois esse problema para o caso habitual.</p>
<p>A entrada de diagnóstico nasceu dessa experiência: uma máquina que não vemos tem de poder ser interrogada sobre o que tem. Mostra CPU, RAM, rede, discos e dispositivos PCI, mede a largura de banda da memória, o disco e a criptografia, e escreve as mesmas medições em JSON na porta série. A escrita no disco só é permitida no espaço não atribuído e apenas depois de se escrever <code>WIPE</code>; na captura abaixo isso foi ignorado.</p>
<figure><img loading="lazy" alt="Ecrã de diagnóstico no VPS da Contabo (AMD EPYC, KVM, 4 vCPU): sistema, memória, disco, dispositivos PCI, largura de banda e latência da RAM e medições criptográficas, incluindo 244 us por handshake mTLS, X25519 48 us, assinatura ECDSA 35,7 us e verificação 107 us." src="https://aldo.tech/blog/urmqtt-vps-handshake-disk-hardening/img/diag-vps.png" /><figcaption>Ecrã de diagnóstico no VPS da Contabo (AMD EPYC, KVM, 4 vCPU): sistema, memória, disco, dispositivos PCI, largura de banda e latência da RAM e medições criptográficas, incluindo 244 us por handshake mTLS, X25519 48 us, assinatura ECDSA 35,7 us e verificação 107 us.</figcaption></figure>
<p><strong>O que fica em aberto.</strong> Hardware real. Depois da primeira versão segue-se o bare metal (arranque por USB, NVMe e AHCI, a placa de rede de um computador verdadeiro), juntamente com IOMMU e Secure Boot, porque sem IOMMU um dispositivo malicioso ou avariado pode escrever em toda a memória. Nesta versão não afirmamos nada sobre isso.</p>
<h2 id="o-handshake-de-12-ms-para-024-ms">O handshake: de 1,2 ms para 0,24 ms</h2>
<p>A primeira medição do diagnóstico no VPS deu um número incómodo: o handshake mTLS, apenas a criptografia do lado do servidor, demorava 1,2 ms. No aarch64 já usávamos as instruções de hardware AES e SHA, mas o x86_64 não tinha nada de semelhante para a chave pública: o RustCrypto é portável e constant-time, e por isso também lento. Três alterações baixaram esse valor.</p>
<ul>
<li>Para o P-256 adotámos o assembly <code>nistz256</code> do OpenSSL e do ring (ADR-130), com tabelas de base fixa para a assinatura e para a verificação de assinaturas da CA (ADR-110, ADR-111).</li>
<li>O X25519 foi reescrito com quatro membros de 64 bits e instruções MULX/ADX (ADR-131).</li>
<li>Depois disso, perfilámos o resto (ADR-132, ADR-200).</li>
</ul>
<figure><img loading="lazy" alt="Handshake no VPS: 1200 us antes, 237 us depois do assembly e das tabelas; comparação A/B do memcpy na mesma execução, 278,8 us com o antigo, 229,4 us com o novo; e antes e depois para o X25519 (146 para 48 us), a assinatura ECDSA (673 para 34 us) e a verificação (309 para 90 us)." src="https://aldo.tech/blog/urmqtt-vps-handshake-disk-hardening/img/handshake.svg" /><figcaption>Handshake no VPS: 1200 us antes, 237 us depois do assembly e das tabelas; comparação A/B do memcpy na mesma execução, 278,8 us com o antigo, 229,4 us com o novo; e antes e depois para o X25519 (146 para 48 us), a assinatura ECDSA (673 para 34 us) e a verificação (309 para 90 us).</figcaption></figure>
<table>
<thead>
<tr>
<th>Medição no VPS (servidor, microssegundos)</th>
<th style="text-align: right;">Antes</th>
<th style="text-align: right;">Depois</th>
</tr>
</thead>
<tbody>
<tr>
<td>handshake mTLS, toda a criptografia</td>
<td style="text-align: right;">1200</td>
<td style="text-align: right;">cerca de 240 (237 na medição do gráfico, 244 na captura)</td>
</tr>
<tr>
<td>X25519</td>
<td style="text-align: right;">146</td>
<td style="text-align: right;">48</td>
</tr>
<tr>
<td>assinatura ECDSA</td>
<td style="text-align: right;">673</td>
<td style="text-align: right;">34</td>
</tr>
<tr>
<td>verificação ECDSA</td>
<td style="text-align: right;">309</td>
<td style="text-align: right;">90 (107 na captura)</td>
</tr>
<tr>
<td>handshake, memcpy antigo, mesmo boot</td>
<td style="text-align: right;">278,8</td>
<td style="text-align: right;">229,4 (-17,7%)</td>
</tr>
</tbody>
</table>
<p>Os números variam cerca de 10% de execução para execução, por isso registámos um intervalo em vez de um único valor. No total, isto representa aproximadamente cinco vezes menos tempo de CPU por handshake.</p>
<p>O que aprendemos sobre o memcpy. Quando a criptografia baixou, o perfil mostrou algo que não esperávamos: as funções <code>mem*</code> (memcpy, memmove, memset) consumiam 7% do handshake, 13 a 39% do caminho de um registo TLS (39% para 64 B, 13% para 4 KiB) e 17 a 34% da distribuição de um PUBLISH. Substituímo-las por assembly SSE2 para x86_64 (ADR-200), com testes diferenciais face à implementação de referência para todos os tamanhos, alinhamentos e sobreposições. Por chamada, são 55 a 75 ciclos contra 7 a 12 para 1 a 64 bytes.</p>
<figure><img loading="lazy" alt="Esquerda: parte do tempo do servidor nas funções mem* antes da reescrita (handshake 7%, registo TLS de 64 B 39%, 256 B 37%, 1 KiB 24%, distribuição 17 a 34%). Direita: quanto cada caminho ficou mais rápido depois (handshake 17,7%, 64 B 26,4%, 256 B 19,6%, 1 KiB 16,1%, 4 KiB 6,6%, distribuição 7 a 34%)." src="https://aldo.tech/blog/urmqtt-vps-handshake-disk-hardening/img/hotpaths.svg" /><figcaption>Esquerda: parte do tempo do servidor nas funções mem* antes da reescrita (handshake 7%, registo TLS de 64 B 39%, 256 B 37%, 1 KiB 24%, distribuição 17 a 34%). Direita: quanto cada caminho ficou mais rápido depois (handshake 17,7%, 64 B 26,4%, 256 B 19,6%, 1 KiB 16,1%, 4 KiB 6,6%, distribuição 7 a 34%).</figcaption></figure>
<table>
<thead>
<tr>
<th>Caminho</th>
<th style="text-align: right;">Parte de mem* antes</th>
<th style="text-align: right;">Mais rápido depois</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>registo TLS de 64 B</td>
<td style="text-align: right;">39%</td>
<td style="text-align: right;">26,4%</td>
</tr>
<tr>
<td>registo TLS de 256 B</td>
<td style="text-align: right;">37%</td>
<td style="text-align: right;">19,6%</td>
</tr>
<tr>
<td>registo TLS de 1 KiB</td>
<td style="text-align: right;">24%</td>
<td style="text-align: right;">16,1%</td>
</tr>
<tr>
<td>registo TLS de 4 KiB</td>
<td style="text-align: right;">13%</td>
<td style="text-align: right;">6,6%</td>
</tr>
<tr>
<td>distribuição de mensagens</td>
<td style="text-align: right;">17 a 34%</td>
<td style="text-align: right;">7 a 34%</td>
</tr>
</tbody>
</table>
<p>Como ler o gráfico: quanto mais pequeno é o registo, maior é a parte do tempo que vai para a cópia, e por isso maior é o ganho. Para mensagens pequenas, que são a maioria do tráfego MQTT, é isto que conta.</p>
<p>O que isto não significa. Em x86_64 nativo medimos o OpenSSL 3.5.5 apenas como referência para o assembly (ADR-130), e não como comparação de produtos. A comparação Linux mais Mosquitto no mesmo VPS (tarefa X11) ainda não foi feita. Por isso não afirmamos que o urMQTT é mais rápido do que o Linux: não foi medido. Os números acima comparam o urMQTT consigo próprio.</p>
<p>A sobrecarga foi o segundo resultado. No VPS, a partir de um Mac (RTT de 35 ms), 128 clientes deram 618 handshakes por segundo, 256 clientes deram 393 e 512 clientes deram 67, com milhares de ligações recusadas. Não foi um patamar, foi um colapso. Encontrámos as causas no código (duas foram confirmadas no QEMU/HVF e não puderam ser reproduzidas no VPS), várias em simultâneo: uma ligação recusada devolvia o cliente pelo segmento seguinte que traz um SYN cookie válido, a recusa por RST dizia ao cliente para tentar de novo de imediato, e a receção era O(fila). A correção (ADR-080 a ADR-082) descarta o SYN que não seria aceite, memoriza os fluxos recusados, mantém a receção em O(1) e conta os handshakes por núcleo. É possível (hipótese, não verificada) que parte da causa estivesse também no cliente: a ferramenta de carga fechava primeiro, pelo que um Mac com 16 384 portos efémeros e um TIME_WAIT de 30 segundos ficava limitado a cerca de 550 ligações por segundo. Por isso ainda não medimos o verdadeiro teto do servidor.</p>
<h2 id="promessa-2-disco-em-bruto-mas-cuidadoso">Promessa 2: disco em bruto, mas cuidadoso</h2>
<p><strong>O que planeámos.</strong> Manter a disposição própria sem sistema de ficheiros e acrescentar tudo o que um sistema de ficheiros normalmente dá: alinhamento, crescimento, discard, cópias de segurança e verificações. <strong>Porque não um sistema de ficheiros.</strong> Discutimos o assunto e a decisão foi confirmada: o urMQTT escreve uma base de dados, um diário de telemetria e os seus próprios metadados, não precisa de diretórios, permissões nem nomes de ficheiros, e cada camada adicional seria mais código na parte de confiança e mais uma forma de uma escrita desaparecer em silêncio. O disco é GPT, com uma partição ESP e uma partição de dados do urMQTT, na qual está a disposição própria.</p>
<p><strong>O que construímos (S2 a S6).</strong></p>
<ul>
<li><strong>Disposição v2 (S2).</strong> Topologia do disco, alinhamento a 1 MiB ou ao tamanho ótimo de I/O, blocos físicos inteiros, par A/B de superblocos com geração (ADR-140). A disposição v1 é recusada pelo kernel com uma mensagem clara e migrada pela ferramenta offline <code>urmqtt-disk migrate</code> (ADR-141): por omissão é uma passagem de ensaio, copia-se e só depois se comuta, com reversão e injeção de falhas em cada escrita.</li>
<li><strong>Crescimento (S3).</strong> No arranque o disco pode crescer (nunca encolhe) e, em virtio-blk, também durante a execução, através da interrupção de alteração de configuração (ADR-142, ADR-143).</li>
<li><strong>Discard e write-zeroes (S4).</strong> Os discos de provisionamento dinâmico devolvem o espaço ao anfitrião. Num teste, o ficheiro qcow2 no anfitrião desceu de 62 para 16 MiB depois de expirarem os segmentos de telemetria, e a recuperação nunca confia no conteúdo que foi descartado.</li>
<li><strong>Cópia de segurança da base de dados no disco (S5).</strong> Uma cópia A/B com a API de backup online do SQLite em passos de 16 páginas: o passo mais longo é de 4,6 ms contra 445 ms quando se copia numa única chamada. Os commits durante a cópia tiveram um máximo de 2,4 ms contra 2,2 ms sem ela. Se o <code>quick_check</code> falhar no arranque, a base de dados é restaurada a partir da cópia.</li>
<li><strong>Limpeza e arranque limitado (S6).</strong> O scrubber lê o disco com um orçamento de I/O limitado e comunica os registos danificados, e o arranque lê apenas o segmento mais recente de cada partição, de modo que o tempo de arranque não cresce com o tamanho da região de telemetria. Fechámos assim um problema que tínhamos calculado de antemão: num disco grande, a recuperação completa demoraria de minutos a dezenas de minutos.</li>
</ul>
<p>Todo o grupo de smoke tests de armazenamento passa nas duas arquiteturas: 79 com sucesso, 0 falhados, em aarch64 sob HVF e em x86_64 sob TCG. Medimos também que o S4 não tornou a base de dados mais lenta: 53,3 mil inserções por segundo em lotes antes e depois, sem diferença mensurável.</p>
<p><strong>O que surpreendeu.</strong> Quanto de uma disposição "simples" se vai em casos limite: virtio-scsi e virtio-blk comunicam a topologia de formas diferentes, e uma escrita incompleta a meio de uma migração tem de deixar sempre pelo menos uma versão válida. Por isso cada escrita no caminho da migração e do crescimento passou por injeção de falhas.</p>
<p><strong>O que fica em aberto.</strong> As páginas do SQLite não têm checksum, pelo que a base de dados não deteta um bit alterado dentro do valor de uma linha; os registos de telemetria e o conteúdo das cópias têm CRC e são detetados. A cifragem do disco (S7) não está feita.</p>
<h2 id="promessa-3-comprovadamente-seguro-tanto-quanto-conseguirmos">Promessa 3: comprovadamente seguro, tanto quanto conseguirmos</h2>
<p><strong>Reforço (H4).</strong> O alocador apaga a memória libertada e verifica-a na reutilização, deteta a dupla libertação, liga os ponteiros da lista livre com XOR e zera as chaves ao libertar (ADR-170). Proteção da pilha (<code>-Zstack-protector=strong</code>, 1315 funções, e o SQLite com <code>-fstack-protector-strong</code>, ADR-171). Os relatórios de erro em produção não contêm registos nem endereços (ADR-172). KASLR nas duas arquiteturas: o kernel é PIE e o Limine desloca-o cerca de 19 bits, e o heap, as pilhas e a janela MMIO são colocados aleatoriamente (ADR-173). No aarch64 o MTE marca o heap (ADR-174), mas só sob TCG: o HVF e o x86 não o têm. O kCFI foi avaliado e não o adotámos porque o compilador no nightly fixado falha com um erro interno; o IOMMU foi adiado juntamente com o bare metal (ADR-175, ADR-176).</p>
<p><strong>O custo foi medido, não presumido.</strong> No caminho do handshake (<code>dp-bench</code>, aarch64 HVF, handshakes completos TLS 1.3 + mTLS + CONNECT):</p>
<table>
<thead>
<tr>
<th>Passo</th>
<th style="text-align: right;">1 núcleo</th>
<th style="text-align: right;">4 núcleos</th>
<th>Variação</th>
</tr>
</thead>
<tbody>
<tr>
<td>sem reforço</td>
<td style="text-align: right;">1941</td>
<td style="text-align: right;">7620</td>
<td></td>
</tr>
<tr>
<td>+ alocador</td>
<td style="text-align: right;">1898</td>
<td style="text-align: right;">7429</td>
<td>-2,2% a -2,5%</td>
</tr>
<tr>
<td>+ proteção da pilha</td>
<td style="text-align: right;">1845</td>
<td style="text-align: right;">7243</td>
<td>em conjunto -4,9% a -5%</td>
</tr>
<tr>
<td>+ PIE e marcas</td>
<td style="text-align: right;">cerca de 1800</td>
<td style="text-align: right;">cerca de 6750</td>
<td>num computador carregado, com ruído</td>
</tr>
</tbody>
</table>
<p>No total, o custo no caminho do handshake é portanto de cerca de 5% para as partes medidas de forma limpa e, com PIE e marcas, sai cerca de 7% com um núcleo e cerca de 11% com quatro, mas esta medição tem ruído. Consideramo-lo aceitável, mas não o repetimos no VPS x86_64.</p>
<p>Vale a pena comparar com o número do primeiro texto (862 handshakes por segundo num núcleo). A mesma ferramenta hoje, com o controlador de cifragem por hardware no aarch64 (X0b) e uma série de acelerações que chegaram desde então, dá 1941 antes do reforço. Não isolámos a contribuição de cada alteração, por isso não afirmamos nada sobre a causa.</p>
<p><strong>Como foi verificado, e o que não foi.</strong> O H5 acrescentou o Miri a um subconjunto de crates do host, provas Kani, sanitizers, um model-fuzzer para a ACL e relatórios de cobertura. O Kani provou o <code>record_len</code> no TLS, o <code>parse</code> no TCP e duas aritméticas da roda de temporizadores. <strong>Não está provado</strong> (tempo esgotado): os dois descodificadores MQTT, os dois parsings de configuração e o comportamento da roda de temporizadores; por isso o trabalho de CI do Kani ainda não é bloqueante. O Miri não consegue executar assembly. Cadeia de abastecimento (H9): SBOM CycloneDX com 215 componentes, o <code>cargo-vet</code> passa com 39 totalmente auditados e 140 exceções, e o cargo-deny está configurado para tratar os avisos de vulnerabilidades como falha da CI (esses trabalhos de CI foram até agora verificados só localmente, não no GitHub, e o teste negativo não foi executado), juntamente com o <code>VULNERABILITY-HANDLING.md</code>. O estado honesto: das 67 versões de crates de terceiros ligadas ao kernel, 53 são exceções e não auditorias. O vet prova que o conjunto de dependências não pode mudar sem uma diferença visível, não que o TCB tenha sido revisto. Não escrevemos uma auditoria nossa para nenhum crate.</p>
<p>O SNTP (T1) é, nesse sentido, uma pequena história de segurança: é opcional, apenas para servidores IPv4 privados, com um salto máximo e a condição de as fontes concordarem (mediana, maioria dentro de um segundo). Um relógio que qualquer pessoa no caminho pode deslocar não é um relógio em que possamos confiar para a validade dos certificados.</p>
<h2 id="no-mac-aarch64-nativo-e-x86_64-por-emulacao">No Mac: aarch64 nativo e x86_64 por emulação</h2>
<p>Desenvolvemos num Mac, e este tem duas formas de executar o mesmo código, muito diferentes entre si.</p>
<table>
<thead>
<tr>
<th></th>
<th>aarch64 sob HVF</th>
<th>x86_64 sob TCG</th>
</tr>
</thead>
<tbody>
<tr>
<td>O que é</td>
<td>execução nativa, velocidade próxima da de um processador real</td>
<td>o QEMU emula as instruções</td>
</tr>
<tr>
<td>Handshake no convidado</td>
<td>cerca de 0,58 ms (medido pela primeira vez por volta do M13); <code>dp-bench</code> hoje 1941/s por núcleo</td>
<td>cerca de 3,3 ms</td>
</tr>
<tr>
<td>Cenários de smoke TLS</td>
<td>rápidos</td>
<td>cerca de 15 minutos por cenário</td>
</tr>
<tr>
<td>MTE</td>
<td>não (não existe no HVF)</td>
<td>o x86 não o tem; a marcação do heap só funciona para um convidado aarch64 sob TCG</td>
</tr>
<tr>
<td>Para que serve</td>
<td>medições reais</td>
<td>correção: o caminho x86 é verificado, os números não são desempenho</td>
</tr>
</tbody>
</table>
<p>A regra é simples: os números sob TCG são emulação e não os citamos como desempenho. O desempenho em x86 só o medimos num VPS real. Por isso os números de criptografia acima vêm do VPS da Contabo, e não do Mac.</p>
<h2 id="licoes">Lições</h2>
<ul>
<li><strong>Meçam numa máquina real antes de otimizar.</strong> A primeira abordagem foi otimizar o que pensávamos ser lento; só o diagnóstico no VPS mostrou os 1,2 ms e onde exatamente o tempo se perde.</li>
<li><strong>A sobrecarga é uma funcionalidade à parte.</strong> Um sistema rápido sob carga pode colapsar acima dela; recusar tem de ser mais barato do que aceitar.</li>
<li><strong>O perfil surpreende.</strong> O maior ganho individual para mensagens pequenas não esteve na criptografia, mas na cópia de memória.</li>
<li><strong>Um disco sem sistema de ficheiros dá mais trabalho, não menos.</strong> O que o sistema de ficheiros normalmente dá (alinhamento, crescimento, discard, cópias, verificações) temos de o escrever e verificar nós, cada passo com injeção de falhas.</li>
<li><strong>Honestidade nos números.</strong> As medições de memória num VPS partilhado variam até 10 vezes entre execuções e não dizem nada sobre o código, por isso não as citamos. Os números de criptografia são estáveis até cerca de 10%.</li>
<li><strong>A velocidade de desenvolvimento não é maturidade.</strong> Os testes e os ADR crescem com o código, mas quatro dias não são operação a sério.</li>
</ul>
<h2 id="o-que-se-segue">O que se segue</h2>
<ul>
<li><strong>H6:</strong> SQLite isolado ao nível EL0, para que um erro na base de dados não abra o kernel.</li>
<li><strong>H7:</strong> o resto da segurança do tempo (NTS e tratamento de saltos); o SNTP é apenas o primeiro passo.</li>
<li><strong>X11:</strong> comparação com o Linux e o Mosquitto no mesmo VPS, só então faz sentido falar de velocidade relativa.</li>
<li><strong>Versão:</strong> a primeira publicação pública para VPS (KVM), com artefactos assinados e instruções.</li>
<li><strong>Depois da versão:</strong> bare metal (X12 a X16), IOMMU e Secure Boot, e a cifragem do disco.</li>
</ul>
<p>O código está em <a href="https://github.com/zeljan-alduk/urMQTT">github.com/zeljan-alduk/urMQTT</a>, e o <code>docs/PROGRESS.md</code> continua a ser a única fonte de verdade sobre o que realmente funciona. Pré-alfa, como antes; relatórios de erros e comunicações de segurança, seguindo as instruções em <code>SECURITY.md</code>, são bem-vindos.</p>]]></content:encoded>
      <category>rust</category>
      <category>mqtt</category>
      <category>iot</category>
      <category>unikernel</category>
      <category>open source</category>
    </item>
    <item>
      <title>urMQTT: construímos um broker MQTT que é também um sistema operativo</title>
      <link>https://aldo.tech/pt/blog/urmqtt-unikernel-broker-mqtt/</link>
      <guid isPermaLink="true">https://aldo.tech/pt/blog/urmqtt-unikernel-broker-mqtt/</guid>
      <pubDate>Thu, 29 Oct 2026 08:00:00 +0000</pubDate>
      <description>Notas de desenvolvimento sobre o urMQTT, um unikernel escrito em Rust que é, ele próprio, um broker MQTT: um único ELF com TCP/IP próprio, TLS 1.3, broker e SQLite, arrancado a partir de um único ISO.</description>
      <content:encoded><![CDATA[<p>Os dispositivos instalados em veículos, em parques de estacionamento e em armários de eletrónica têm uma característica em comum: enviam mensagens pequenas, raramente, mas são muitos e estão sempre ligados. Há anos que os ligamos a brokers MQTT e há anos que a mesma pergunta nos incomoda. Porque precisamos de um Linux inteiro, do OpenSSL, do runtime de uma linguagem, de SSH, de um gestor de pacotes e só depois de um broker, para algo tão simples? Cada uma dessas camadas traz as suas próprias vulnerabilidades, consome a sua própria memória e acrescenta mais uma coisa que se pode avariar, e uma frota de dispositivos só precisa de três coisas do servidor: manter muitas ligações quase inativas, autenticar cada dispositivo de forma fiável e transportar as mensagens sem perdas.</p>
<p>Foi assim que nasceu o <a href="https://github.com/zeljan-alduk/urMQTT">urMQTT</a>: um unikernel que <em>é</em> um broker MQTT. Kernel, TCP/IP, TLS 1.3, MQTT e uma base de dados integrada num único binário Rust, que arranca a partir de um único ISO. Não há processos, nem chamadas de sistema, nem shell. A primeira plataforma em que é construído e testado é aarch64, e o mesmo código caminha para x86_64, por UEFI e por BIOS clássico, e para hardware real: o objetivo é um único ISO que arranque em todos eles. O projeto é de código aberto, sob licença MIT ou Apache 2.0, e neste texto queremos registar onde chegámos, como está organizado por dentro e o que aprendemos pelo caminho.</p>
<h2 id="o-que-significa-exatamente-unikernel">O que significa exatamente "unikernel"</h2>
<p>Um broker habitual é um programa que corre sobre um sistema operativo. O urMQTT não tem sistema operativo por baixo: arranca sozinho através do firmware UEFI e do bootloader Limine, configura as suas próprias tabelas de páginas, programa o controlador de interrupções e o temporizador, encontra os dispositivos virtio através de PCIe, obtém um endereço por DHCP e escuta num único porto. Tudo o que normalmente seria feito pelo kernel, pelos controladores, pela pilha de rede e pela aplicação é aqui um único ELF que corre ao nível EL1.</p>
<p>A comparação com uma instalação típica fala por si:</p>
<table>
<thead>
<tr>
<th></th>
<th>Broker típico em Linux</th>
<th>urMQTT</th>
</tr>
</thead>
<tbody>
<tr>
<td>Serviços expostos</td>
<td>SSH, broker, muitas vezes também um porto de métricas ou de administração</td>
<td><strong>apenas TCP 8883</strong> (MQTT sobre TLS 1.3 com mTLS)</td>
</tr>
<tr>
<td>Código na imagem</td>
<td>kernel, userland, libc, OpenSSL, runtime</td>
<td><strong>um único ELF em Rust</strong> (mais o SQLite incorporado)</td>
</tr>
<tr>
<td>Gestão remota</td>
<td>shell, ficheiros de configuração, atualizações de pacotes</td>
<td><strong>nenhuma.</strong> Uma nova versão é um novo ISO</td>
</tr>
<tr>
<td>Memória por ligação inativa</td>
<td>quilobytes (buffers de socket, objetos TLS, heap)</td>
<td><strong>objetivo ≤ 512 B</strong></td>
</tr>
<tr>
<td>Modelo multinúcleo</td>
<td>threads e locks ou um único ciclo de eventos</td>
<td><strong>uma thread por núcleo, sem estado partilhado, processamento até ao fim</strong></td>
</tr>
<tr>
<td>Integridade do código</td>
<td>são possíveis mapeamentos simultaneamente graváveis e executáveis</td>
<td><strong>W^X, tabelas de páginas bloqueadas, PAC/BTI</strong></td>
</tr>
</tbody>
</table>
<p>As prioridades são, por esta ordem: segurança, escalabilidade, desempenho multinúcleo, pequena pegada e instalação imutável. Sempre que dois objetivos entram em conflito, ganha o que está mais acima nesta lista.</p>
<h2 id="onde-estamos-agora">Onde estamos agora</h2>
<p>O projeto está numa fase pré-alfa, mas já fechámos dezassete marcos de desenvolvimento, de M0 a M17. Hoje o urMQTT serve MQTT 3.1.1 sobre TLS 1.3 com certificados de cliente obrigatórios e guarda o estado em SQLite num disco em bruto. Em resumo, o que já funciona:</p>
<ul>
<li><strong>Kernel:</strong> vetores de exceção próprios com relatórios descodificados, tabelas de páginas próprias com W^X imposto, páginas de guarda, PAC e BTI onde o processador os tem, GICv3 e GICv2, temporizador genérico, SMP até 64 núcleos.</li>
<li><strong>Deteção de hardware</strong> a partir de DTB ou ACPI, numa única descrição da plataforma, com resultado idêntico a partir de ambas as fontes.</li>
<li><strong>Controladores:</strong> PCIe ECAM, MSI-X, virtio-net, virtio-blk, virtio-scsi e virtio-rng, e um CSPRNG inicializado com entropia de hardware.</li>
<li><strong>Rede:</strong> DHCPv4, ARP, ICMP e IPv4 estrito próprios, e um TCP próprio apenas do lado do servidor: SYN cookies, roda de temporizadores hierárquica, NewReno, fusão de ACK, sem estado TIME_WAIT.</li>
<li><strong>TLS:</strong> apenas TLS 1.3, mTLS obrigatório, identidade do dispositivo a partir do CN ou do SAN URI; o handshake é feito pelo rustls e, depois dele, por uma camada de registos própria com AES-128-GCM e ChaCha20-Poly1305. Retoma de sessão com tickets PSK, lista de revogação, controlo de admissão logo no handshake.</li>
<li><strong>MQTT:</strong> codec sem cópia, testado com fuzzing e com clientes mosquitto reais; QoS 0 e 1, wildcards, mensagens retidas, testamento (LWT), sessões persistentes, ACL limitada a <code>devices/{id}/#</code>, limites de débito por dispositivo.</li>
<li><strong>Armazenamento:</strong> SQLite dentro do kernel através de um VFS sobre o disco em bruto, em modo WAL, com verificação de integridade no arranque; registo de dispositivos, revogações, mensagens retidas, sessões persistentes com filas de mensagens para dispositivos offline; por núcleo, um diário de telemetria separado com CRC por registo e recuperação após falha.</li>
<li><strong>Operação e supervisão:</strong> watchdog para os handlers, verificação de saúde, registo de logs limitado, syslog e statsd por UDP, admissão que sobrevive a tempestades de religações.</li>
<li><strong>Entrega:</strong> compilações bit a bit idênticas verificadas na CI, ISO e imagem de disco GPT em bruto, assinaturas minisign, instruções para KVM, Proxmox e Hetzner.</li>
</ul>
<p>Cada passo está assinalado com o marco que o trouxe, e o ficheiro <code>docs/PROGRESS.md</code> no repositório é a única fonte de verdade sobre o que realmente funciona.</p>
<h2 id="arquitetura-em-tres-imagens">Arquitetura em três imagens</h2>
<p>Todo o desenho está descrito em <code>docs/ARCHITECTURE.md</code>, e as razões de cada decisão importante estão registadas como ADR em <code>docs/DECISIONS.md</code>. Aqui fica uma visão geral.</p>
<h3 id="camadas">Camadas</h3>
<figure><img loading="lazy" alt="Camadas do urMQTT: do broker, passando pelo codec MQTT, TLS, TCP e rede, até aos controladores virtio e à camada de arquitetura, tudo num único ELF ao nível EL1." src="https://aldo.tech/blog/urmqtt/img/layers.svg" /><figcaption>Camadas do urMQTT: do broker, passando pelo codec MQTT, TLS, TCP e rede, até aos controladores virtio e à camada de arquitetura, tudo num único ELF ao nível EL1.</figcaption></figure>
<p>De cima para baixo: o broker (sessões, árvore de tópicos, QoS, mensagens retidas, testamentos, ACL, limites), o codec MQTT, TLS, TCP, a camada de rede e, por fim, os controladores. Ao lado deste caminho principal está o armazenamento (SQLite e diário de telemetria) e, por baixo de tudo, a gestão de memória, a deteção da plataforma e a configuração, que é convertida em constantes no momento da compilação, juntamente com o certificado da CA, o certificado do servidor e a chave incorporados. O hipervisor com os dispositivos virtio é a única coisa que fica fora da imagem.</p>
<h3 id="uma-thread-por-nucleo-sem-estado-partilhado">Uma thread por núcleo, sem estado partilhado</h3>
<figure><img loading="lazy" alt="Modelo de execução: cada núcleo de dados tem o seu próprio ciclo e a sua parte do broker, e o núcleo mais alto executa apenas o SQLite. Os núcleos comunicam exclusivamente através de anéis SPSC." src="https://aldo.tech/blog/urmqtt/img/cores.svg" /><figcaption>Modelo de execução: cada núcleo de dados tem o seu próprio ciclo e a sua parte do broker, e o núcleo mais alto executa apenas o SQLite. Os núcleos comunicam exclusivamente através de anéis SPSC.</figcaption></figure>
<p>Cada núcleo de dados executa o seu próprio ciclo, com a sua pilha TCP, os seus estados TLS, uma parte do broker (sessões e árvore de tópicos), a cache do heap e a partição de telemetria. Os núcleos não partilham qualquer estado mutável, apenas anéis limitados e sem locks, um para cada par ordenado de núcleos. O núcleo mais alto executa o SQLite e mais nada.</p>
<p>A placa de rede tem hoje um único par de filas, que pertence ao núcleo 0. Este encaminha cada fluxo MQTT para um dos núcleos de trabalho, segundo o hash Toeplitz do respetivo quádruplo, e envia o que os núcleos de trabalho devolvem. A sessão vive no seu núcleo de origem, determinado pelo hash do identificador do cliente, pelo que a tomada de controlo, os testamentos e a persistência nunca atravessam núcleos. Uma ligação que aterre noutro núcleo mantém aí o seu TCP e o seu TLS, mas reencaminha o MQTT simples para o núcleo de origem, com controlo de fluxo baseado em créditos. Cada mensagem encaminhada é também distribuída pelas restantes partes do broker, cada uma das quais mantém uma réplica das mensagens retidas.</p>
<p>Três regras mantêm este modelo coeso. <strong>Processamento até ao fim:</strong> um pacote recebido é tratado por inteiro num único núcleo, do TCP, passando pela decifragem TLS no local, pelo parsing de MQTT e pelo encaminhamento, até à resposta cifrada, sem filas de tarefas e sem cópias para buffers da aplicação. <strong>Polling adaptativo:</strong> sob carga, as filas de receção são consultadas ativamente e, em repouso, o núcleo regressa às interrupções e a <code>WFI</code>; os handlers de interrupção apenas confirmam a interrupção e marcam o trabalho. <strong>Watchdog:</strong> um tique de 10 ms por núcleo apanha os handlers que excedem o seu orçamento.</p>
<h3 id="a-vida-de-um-publish">A vida de um PUBLISH</h3>
<figure><img loading="lazy" alt="O percurso de uma mensagem PUBLISH com QoS 1: do registo TLS na placa de rede, passando pela decifragem, verificação de ACL e encaminhamento, até ao PUBACK fundido com o ACK de TCP num único segmento." src="https://aldo.tech/blog/urmqtt/img/publish.svg" /><figcaption>O percurso de uma mensagem PUBLISH com QoS 1: do registo TLS na placa de rede, passando pela decifragem, verificação de ACL e encaminhamento, até ao PUBACK fundido com o ACK de TCP num único segmento.</figcaption></figure>
<p>O dispositivo envia um registo TLS. A placa de rede entrega-o ao ciclo do núcleo através de um descritor, sem cópia. O ciclo verifica o TCP, decifra o TLS no local e faz o parsing do MQTT a partir do buffer emprestado; depois verifica em memória a ACL, o limite de débito e o caminho pela árvore de tópicos. Se outro núcleo estiver interessado nesse tópico, recebe uma mensagem de encaminhamento com uma referência ao conteúdo e o identificador do tópico; a base de dados recebe um registo de telemetria ou a atualização de uma mensagem retida, em lote. A resposta PUBACK é cifrada diretamente para o buffer de saída e fundida com o ACK de TCP, pelo que ao dispositivo chega um único segmento. A mensagem custa a outro núcleo uma cifragem por subscritor, no momento do envio.</p>
<h2 id="quanto-custa-uma-ligacao">Quanto custa uma ligação</h2>
<p>O objetivo é <strong>no máximo 512 bytes por ligação TLS inativa</strong>, com a ambição de chegar a 256. Enquanto a ligação está em repouso, nada é retido para ela em buffers: os dados recebidos são processados diretamente a partir da trama de rede, e os buffers de envio, de registos e de MQTT só existem enquanto houver algo por concluir ou por confirmar. O kernel mede-o ele próprio: contadores exatos do heap, em bytes pedidos e em blocos ocupados, escritos de dez em dez segundos como heap acima do nível base por ligação aberta.</p>
<table>
<thead>
<tr>
<th>Componente</th>
<th style="text-align: right;">Tamanho</th>
<th>Como</th>
</tr>
</thead>
<tbody>
<tr>
<td>Bloco de controlo TCP</td>
<td style="text-align: right;">96 B</td>
<td>flags compactadas, RTT/RTO de 16 bits, posição na roda de temporizadores, sem buffers em repouso</td>
</tr>
<tr>
<td>Posição na tabela TCP</td>
<td style="text-align: right;">4–8 B</td>
<td>apenas o índice da ligação, tabela no máximo meio cheia</td>
</tr>
<tr>
<td>Ligação e estado de registos TLS 1.3</td>
<td style="text-align: right;">152 B</td>
<td>segredo e sequência por sentido (40 B); chave e IV derivados por registo</td>
</tr>
<tr>
<td>Ligação MQTT</td>
<td style="text-align: right;">48 B</td>
<td>identidade e testamento no heap apenas enquanto existirem</td>
</tr>
<tr>
<td>Sessão MQTT, forma quente</td>
<td style="text-align: right;">64 B</td>
<td>uma subscrição em linha, identificadores de nós internados</td>
</tr>
<tr>
<td>Subscrição <code>devices/&lt;id&gt;/cmd/#</code></td>
<td style="text-align: right;">0 nós da árvore</td>
<td>guardada de forma relativa ao identificador da sessão, comum a todos os dispositivos</td>
</tr>
</tbody>
</table>
<p>Medido sob HVF com quatro núcleos virtuais, três deles de trabalho, em que cada dispositivo mantém uma ligação mTLS e uma subscrição com QoS 1, incluindo o arredondamento do alocador:</p>
<table>
<thead>
<tr>
<th style="text-align: right;">Ligações</th>
<th style="text-align: right;">Bytes por ligação</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>O conjunto de tramas de rede é fixo, com cerca de 2 MiB, o que equivale a aproximadamente 200 B por ligação com dez mil ligações e a cerca de 2 B com um milhão. Não conseguimos medir mais do que isso: a rede de utilizador do QEMU no macOS deixou de aceitar ligações às 10 197, aproximadamente no limite de 10 240 descritores do processo QEMU. O valor desce à medida que as partes fixas se repartem por mais ligações, pelo que não cresce com o seu número.</p>
<h2 id="modelo-de-seguranca">Modelo de segurança</h2>
<p>A segurança é a primeira prioridade e ganha todos os conflitos com os restantes objetivos. O modelo tem várias camadas:</p>
<ul>
<li><strong>Um só porto.</strong> TCP 8883, apenas TLS 1.3, certificados de cliente obrigatórios assinados pela CA incorporada na imagem. O identificador do cliente MQTT tem de ser igual ao identificador do dispositivo constante do certificado.</li>
<li><strong>Sem entrada.</strong> Sem shell, sem entrada pela consola, sem SSH, sem API de administração, sem código dinâmico, sem SQL acessível pela rede e sem sistema de ficheiros.</li>
<li><strong>Segurança de memória.</strong> Rust, com <code>forbid(unsafe_code)</code> nos pacotes de protocolo; cada bloco <code>unsafe</code> tem um comentário <code>// SAFETY:</code> verificado por lint, e perante entrada externa não há panic: o clippy, nos pacotes do caminho de dados, proíbe <code>unwrap</code>, <code>expect</code>, indexação e <code>panic!</code>.</li>
<li><strong>Mitigação de exploração.</strong> O W^X é imposto pela codificação dos descritores e pelo bit <code>SCTLR_EL1.WXN</code>, e é auditado em cada arranque. Depois do arranque, as tabelas de páginas são só de leitura, sem alias gravável. A metade inferior do espaço de endereçamento está excluída. Cada pilha tem páginas de guarda e as exceções correm numa pilha separada. PAC e BTI são ativados quando o processador os suporta, o que é verificado pelos smoke tests.</li>
<li><strong>Resistência ao abuso.</strong> SYN cookies; a admissão recusa novas ligações, antes de qualquer criptografia, quando o heap está baixo, quando a base de dados está atrasada, quando há demasiados handshakes em curso por núcleo ou por endereço de origem, ou quando o ritmo de handshakes é demasiado elevado. Limites de débito por dispositivo, filas limitadas, janela TCP ligada à contrapressão do broker, registo de logs limitado, watchdog e verificação de saúde.</li>
<li><strong>Imutabilidade.</strong> A configuração, a CA e a chave do servidor estão incorporadas na imagem. Alterar seja o que for implica uma nova imagem.</li>
</ul>
<p>O modelo de ameaças está registado em <code>docs/SECURITY.md</code>.</p>
<h2 id="armazenamento-sem-sistema-de-ficheiros">Armazenamento sem sistema de ficheiros</h2>
<p>Os dados vivem num disco separado, virtio-blk ou virtio-scsi, no qual não existe sistema de ficheiros. A disposição é mínima e própria:</p>
<pre><code>LBA 0      [ superbloco: magia · versão da disposição · uuid · tabela de regiões · CRC-32C ]
LBA 8, 16  [ tabela de tamanhos dos ficheiros, posições A/B (vence a mais recente válida) ]
LBA 64     [ base principal SQLite ½ ][ WAL do SQLite ⅛ ][ diário 1 MiB ][ diário de telemetria: 8 partições … ]
últimos 4 KiB [ espaço para a autoverificação do dispositivo de blocos no arranque ]
</code></pre>
<p>O SQLite (3.54.1 incorporado, com verificação do hash na compilação) é compilado para dentro do kernel com o clang, sem biblioteca padrão, e usa um heap <code>memsys5</code> fixo. O seu VFS mapeia três ficheiros fixos para regiões do disco. Funciona em modo WAL com <code>locking_mode=EXCLUSIVE</code>, pelo que não precisa de um índice em memória partilhada, e com <code>synchronous=FULL</code>; se o <code>quick_check</code> no arranque encontrar um erro, o sistema pára em segurança. Na base de dados estão o registo de dispositivos, as revogações, as mensagens retidas e as sessões persistentes, com filas de mensagens QoS 1 para dispositivos que não estão ligados.</p>
<p>O núcleo da base de dados, o de número mais alto, executa o SQLite e mais nada. O plano de dados envia-lhe as alterações de estado através de um anel SPSC sem locks; este agrupa-as numa única transação a cada 5 ms ou a cada 512 operações e devolve as confirmações por outro anel. Um disco vazio é formatado no primeiro arranque; tudo o resto nunca é formatado por cima.</p>
<p>A telemetria segue outro caminho. Cada PUBLISH aceite em tópicos de <code>storage.telemetry_topics</code> vai para a partição do diário de telemetria do seu núcleo: um anel de segmentos com CRC por registo, salgado com a sequência do segmento, de modo que as passagens antigas do anel nunca voltam a aparecer. Os núcleos entregam os registos à base de dados através de um anel próprio, sem cópia, e esta escreve-os a cada 5 ms ou a cada 256 KiB. No arranque, a recuperação mantém todos os registos escritos, corta a cauda incompleta e continua num novo segmento. A retenção é por tamanho, através do anel, e por idade, através de <code>retention_days</code>. Verificámo-lo com um milhão de registos e com <code>kill -9</code> a meio da escrita.</p>
<h2 id="os-numeros">Os números</h2>
<p>A ferramenta de carga em <code>tools/loadgen</code> faz três coisas: uma tempestade de handshakes TLS 1.3 com mTLS e CONNECT, ligações de longa duração com PINGREQ que publicam telemetria a cada segundo, e tempestades de religações. O <code>make bench</code> num Apple M4 Pro sob HVF dá:</p>
<table>
<thead>
<tr>
<th>Núcleos de trabalho</th>
<th>handshakes/s no convidado</th>
<th>escalabilidade</th>
<th>através da rede de utilizador do QEMU (ligações/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>O valor no convidado, em que o cliente e o servidor estão dentro da mesma máquina virtual e cada núcleo de trabalho funciona por si, mostra que o plano de dados escala linearmente. O valor de rede é limitado pela rede de utilizador de uma só thread do QEMU, não pelo convidado; isto também está registado, como ADR-032.</p>
<p>As tempestades de religações testamo-las fazendo com que todos os dispositivos se liguem ao mesmo tempo, repitam as tentativas recusadas com atraso e depois caiam todos sem DISCONNECT. O parâmetro <code>heap_limit_mb=</code> no modo de desenvolvimento faz com que a admissão considere o heap mais pequeno do que é, pelo que uma tempestade maior do que o heap pode ser testada com poucos sockets. Numa ronda destas, com 3 000 dispositivos, ligaram-se 1 368 em 36 segundos e 5 619 tentativas foram devidamente recusadas, com um limite de 8 MiB; a memória voltou ao normal depois da tempestade.</p>
<h2 id="como-trabalhamos">Como trabalhamos</h2>
<p>O projeto é conduzido por uma especificação escrita, <code>PROMPT.md</code>, com o plano de marcos, e cada marco termina com o mesmo ritual: <code>make check</code> e <code>make smoke</code> a verde, um commit com a etiqueta <code>M&lt;n&gt;</code>, uma entrada em <code>docs/PROGRESS.md</code> e um ADR para cada decisão que não seja óbvia. As regras são curtas e não se negoceiam: sem panic perante entrada externa, <code>unsafe</code> apenas nos pacotes de hardware e FFI e sempre com comentário, sem alocações no caminho quente, um comentário junto de cada ordenação de operações atómicas, cada limite vem da configuração, e as partes provisórias intencionais falham de forma ruidosa.</p>
<p>A CI faz mais do que conseguiríamos fazer à mão: fmt, clippy para o host e para <code>aarch64-unknown-none</code>, typos, actionlint e shellcheck; testes em Linux e em macOS em arm64; compilação do kernel para as configurações de desenvolvimento e de produção, com o tamanho do ELF no resumo; arranque sob QEMU com todos os cenários do smoke test e guardando os logs de série; cargo-deny para avisos, licenças e fontes; um minuto de fuzzing em cada um dos onze alvos e, durante a noite, dez minutos por alvo com corpus em cache. Além disso, CodeQL, OpenSSF Scorecard, Dependabot e publicação com proveniência SLSA. Cada action de terceiros está fixada ao SHA completo do commit.</p>
<p>Levamos a reprodutibilidade à letra: o <code>make repro</code> compila o HEAD duas vezes em árvores de trabalho limpas e compara o ELF, o ISO e a imagem em bruto byte a byte. As versões publicadas são assinadas com minisign, e os artefactos podem ser verificados com <code>gh attestation verify</code>. A imagem de produção contém a chave privada do servidor, pelo que o próprio ISO é um segredo; as imagens que acompanham as versões no GitHub são de desenvolvimento, com chaves temporárias, e servem apenas para experimentar.</p>
<h2 id="o-que-aprendemos">O que aprendemos</h2>
<p>Algumas lições que mudaram o plano:</p>
<ul>
<li><strong>Afinal, o TCP próprio compensou.</strong> O protótipo funcionava com o smoltcp e serviu-nos bem até ao M13. Mas um servidor que mantém centenas de milhares de ligações não precisa de um TCP de cliente: precisa de SYN cookies, de uma roda de temporizadores e de um estado de 96 bytes. No M14 substituímos o smoltcp por uma pilha própria apenas do lado do servidor, com testes de perda simulada e fuzzing de segmentos.</li>
<li><strong>Meçam a memória por dentro.</strong> As estimativas por ligação eram inúteis até o kernel começar a contar ele próprio o heap em blocos do alocador, e não em bytes pedidos. Só então vimos para onde vão realmente os 512 bytes e baixámos para 494.</li>
<li><strong>A ferramenta de teste faz parte do produto.</strong> A maior limitação não foi o kernel, mas a rede de utilizador do QEMU e o número de portos efémeros no computador de desenvolvimento. Para cem mil ligações ou mais é preciso <code>vmnet-shared</code> no macOS ou Linux com KVM, interface tap e vários endereços de cliente.</li>
<li><strong>SQLite sem sistema operativo é possível</strong>, mas só em condições estritas: heap fixo, bloqueio exclusivo sem memória partilhada, um VFS que desconhece ficheiros e uma admissão que não deixa a base de dados atrasar-se.</li>
<li><strong>As tempestades de religações são o estado normal</strong>, não a exceção. Quando a rede móvel cai e volta, milhares de dispositivos batem à porta ao mesmo tempo. Por isso a admissão recusa a ligação antes de qualquer criptografia, e cada ligação recusada custa quase nada.</li>
</ul>
<h2 id="o-que-se-segue">O que se segue</h2>
<p>O trabalho depois do M17 está planeado em três direções, e a ordem entre elas está registada em <code>docs/PLAN.md</code>. Nada disto está ainda construído.</p>
<ul>
<li><strong>Um ISO para várias plataformas.</strong> O mesmo ficheiro <code>urmqtt-&lt;env&gt;-&lt;versão&gt;.iso</code> e uma única imagem GPT em bruto que arrancam em UEFI aarch64, UEFI x86_64 e BIOS x86_64, em máquinas virtuais e em hardware real, começando por um PC Intel. Continua a ser um aparelho com tudo lá dentro, reprodutível e assinado.</li>
<li><strong>Disco em bruto, sem sistema de ficheiros.</strong> Especificação do formato ao nível do byte, uma ferramenta de host <code>urmqtt-disk</code> que partilha os parsers com o kernel, alinhamento das regiões a 1 MiB, crescimento do disco com os dados, discard para discos de provisionamento dinâmico, snapshots A/B da base de dados, verificação de bits e, opcionalmente, cifragem AES-256-XTS com AES por hardware.</li>
<li><strong>Comprovadamente seguro.</strong> Revogação e lista de dispositivos permitidos em tempo de execução através de um canal de controlo assinado, sem nova imagem; testes diferenciais e de conformidade do TLS e do TCP próprios; KASLR, proteção da pilha, kCFI, MTE e um alocador reforçado; SQLite isolado ao nível EL0; Secure Boot; SBOM e cargo-vet; e, por fim, um pacote para auditoria externa.</li>
</ul>
<p>As plataformas que oferecem apenas dispositivos não virtio, como o AWS ENA ou o Azure MANA, não são suportadas por agora. O Raspberry Pi 4 e 5 como anfitrião KVM, o Hetzner CAX e o Oracle Ampere A1 estão documentados e suportados no código, mas ainda não foram experimentados num serviço real.</p>
<h2 id="experimente">Experimente</h2>
<p>O repositório está em <a href="https://github.com/zeljan-alduk/urMQTT">github.com/zeljan-alduk/urMQTT</a>. Com o Rust com a toolchain nightly fixada e o QEMU, <code>make certs</code>, <code>make run</code> e um cliente mosquitto bastam para o primeiro PUBLISH sobre mTLS no seu próprio computador. O aviso do início vale também no fim: isto é pré-alfa, e tudo o que funciona está em <code>docs/PROGRESS.md</code>. Relatórios de erros, perguntas sobre o desenho e comunicações de segurança, seguindo as instruções em <code>SECURITY.md</code>, são bem-vindos. Voltaremos a falar aqui de cada marco seguinte.</p>]]></content:encoded>
      <category>rust</category>
      <category>mqtt</category>
      <category>iot</category>
      <category>unikernel</category>
      <category>código aberto</category>
    </item>
    <item>
      <title>Começamos um blog: notas do terreno</title>
      <link>https://aldo.tech/pt/blog/comecamos-um-blog/</link>
      <guid isPermaLink="true">https://aldo.tech/pt/blog/comecamos-um-blog/</guid>
      <pubDate>Sat, 17 Oct 2026 08:00:00 +0000</pubDate>
      <description>Porque é que uma empresa que constrói terminais, aplicações e eletrónica para o transporte público começa a escrever, e sobre o que vai ler aqui.</description>
      <content:encoded><![CDATA[<p>A maior parte do que construímos ninguém vê. O software no terminal à porta do autocarro, o firmware num dispositivo que nunca terá ecrã, o servidor que durante a noite transforma as transações do dia em relatórios. Quando tudo funciona, funciona em silêncio. Quando não funciona, toda a gente repara.</p>
<p>Neste blog registamos o que aprendemos pelo caminho. Não textos de marketing, mas notas concretas: o que correu mal, como o resolvemos e o que faríamos de forma diferente hoje.</p>
<h2 id="sobre-o-que-vamos-escrever">Sobre o que vamos escrever</h2>
<ul>
<li><strong>Bilhética no transporte público.</strong> Cartões sem contacto, módulos SAM, funcionamento sem rede e tudo o que um terminal tem de aguentar num veículo que vibra doze horas por dia.</li>
<li><strong>Aplicações móveis no terreno.</strong> Android e Flutter em dispositivos que não são telemóveis comuns: com leitores de cartões, impressoras e baterias que têm de durar um turno inteiro.</li>
<li><strong>Backend e monitorização da frota.</strong> Como saber que um dispositivo no veículo tem um problema antes de o motorista o comunicar.</li>
<li><strong>Hardware.</strong> Do protótipo na bancada à placa que vai para produção em série, e todos os erros pelo meio.</li>
<li><strong>Modelos de IA locais.</strong> Onde os modelos em hardware próprio fazem sentido, e onde a nuvem e uma API são simplesmente a melhor escolha.</li>
</ul>
<h2 id="como-vamos-escrever">Como vamos escrever</h2>
<p>Curto e concreto, com código e números quando os temos. Publicaremos quando tivermos algo a dizer, não segundo um calendário. Os artigos são escritos em croata e traduzidos para todas as línguas deste site; também pode acompanhar-nos por <a href="https://aldo.tech/pt/blog/feed.xml">RSS</a>.</p>
<p>Se algum tema lhe interessa ou trabalha num problema parecido, escreva-nos para <a href="mailto:info@aldo.tech">info@aldo.tech</a>.</p>]]></content:encoded>
      <category>introducao</category>
    </item>
  </channel>
</rss>
