<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>ALDO TECH блог</title>
    <link>https://aldo.tech/sr/blog/</link>
    <atom:link href="https://aldo.tech/sr/blog/feed.xml" rel="self" type="application/rss+xml"/>
    <description>Белешке с терена: јавни превоз, наплата, хардвер и софтвер који мора да ради на терену.</description>
    <language>sr</language>
    <item>
      <title>urMQTT после VPS-а: SQLite у корисничком режиму, поређење са Mosquittom, конфигурација као податак и градитељ у прегледачу</title>
      <link>https://aldo.tech/sr/blog/urmqtt-config-builder-plaintext-mosquitto/</link>
      <guid isPermaLink="true">https://aldo.tech/sr/blog/urmqtt-config-builder-plaintext-mosquitto/</guid>
      <pubDate>Sun, 01 Nov 2026 08:00:00 +0000</pubDate>
      <description>Шта се догодило после другог текста о urMQTT-у: SQLite изолован изван језгра, мерења наспрам Mosquitta на истом VPS-у (и оно што није ишло у нашу корист), пронађени узроци спорог једног тока, конфигурација као податак уз веб градитељ ISO-а и емулатор у прегледачу, необавезни обичан MQTT и шта још није доказано.</description>
      <content:encoded><![CDATA[<p>Други текст, <a href="https://aldo.tech/blog/urmqtt-vps-handshake-disk-hardening/">urMQTT на правом VPS-у</a>, завршио се списком „Шта следи”: H6 (SQLite изван језгра), H7 (безбедност времена), X11 (поређење са Mosquittom на истом VPS-у) и прво издање. Овај текст прати тај списак истим редом, а затим додаје две ствари којих на списку није било, јер су се појавиле успут: конфигурацију као податак, уз веб градитељ ISO-а, и необавезни обичан MQTT без TLS-а. Као и раније: шта смо планирали, шта смо изградили, шта нас је изненадило и шта је отворено. У овом тексту има и неколико резултата који не иду у корист urMQTT-а; оставили смо их јер су део истине.</p>
<p>Напомена унапред остаје иста: пројекат је пред-алфа, прво издање је само за VPS-ове са KVM-ом, а бројке мере код, тестове и одређене огледе, не зрелост у продукцији.</p>
<h2 id="_1">Како је то изгледало у времену</h2>
<p>Од прошлог текста (31.10. у 23:00) до данас (1.11. у 20:41) у репозиторијуму је 59 нових commitова, укупно 178. Сви су настали у једном календарском дану, па важи иста опомена као раније: пројекат се гради уз Claude Code (commitови носе <code>Co-Authored-By: Claude</code>), план, код, тестови и ADR-ови настају у кратким затвореним петљама, а бројке испод говоре о количини кода и провера, не о томе колико је људског рада у њима.</p>
<figure><img loading="lazy" alt="Раст од прошлог текста: Rust редова од 89 790 до 112 408, #[test] функција од 739 до 926, ADR-ова од 104 до 126 и commitова од 119 до 178. Сваки ред има сопствену лествицу." src="https://aldo.tech/blog/urmqtt-config-builder-plaintext-mosquitto/img/growth-since.svg" /><figcaption>Раст од прошлог текста: Rust редова од 89 790 до 112 408, #[test] функција од 739 до 926, ADR-ова од 104 до 126 и commitова од 119 до 178. Сваки ред има сопствену лествицу.</figcaption></figure>
<p>Бројке су израчунате истом методом као у прошлом тексту: све <code>.rs</code> датотеке изван <code>vendor/</code> и <code>fuzz/</code>, <code>#[test]</code> функције, наслови <code>## ADR-</code> у <code>docs/DECISIONS.md</code>.</p>
<table>
<thead>
<tr>
<th>Тренутак</th>
<th>Commit</th>
<th>Време</th>
<th style="text-align: right;">Rust редова</th>
<th style="text-align: right;">Тестова</th>
<th style="text-align: right;">ADR-ова</th>
</tr>
</thead>
<tbody>
<tr>
<td>прошли текст</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 готов</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>веб градитељ и огледало издања</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, документација</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>данас</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>Радило се паралелно на више грана, па редослед commitова није стриктно редослед рада. ADR-ови су нумерисани до 232, али нумерација има празнина (део је преименован при спајању грана); бројимо наслове, не највећи број. Издања: v0.2.0 (1.11., 18:45 UTC) и v0.2.1 (1.11., 20:22 UTC), оба означена као pre-release.</p>
<h2 id="h6-sqlite">H6: SQLite у корисничком режиму рада</h2>
<p><strong>Шта смо планирали.</strong> SQLite је 254 268 редова C-а који парсира базу, WAL и дневник са диска, а диск може бити направљен или измењен. Дотад је радио у језгру, са правима језгра. <strong>Шта смо изградили</strong> (ADR-210 до ADR-212): SQLite, његови libc делови и heap премештени су у засебан програм, <code>dbworker</code>, који ради на EL0 (aarch64) односно ring 3 (x86_64), у сопственом адресном простору. Језгро више не линкује SQLite. Радник са језгром разговара кроз заједнички прозор меморије и дванаест системских позива, а свака дужина и померај који се врате проверавају се пре употребе. Грешка у раднику (недопуштен приступ, недефинисана инструкција, прекорачен временски буџет од 30 s) завршава позив, радник се поново покреће и база се отвара из WAL-а као при подизању; након 5 поновних покретања у минуту похрана се проглашава неисправном, а не покреће се у недоглед.</p>
<p>Цена, измерена на aarch64 под HVF-ом, иста VM, језгро са уграђеним SQLite-ом наспрам језгра са радником, шест наизменичних покретања сваке:</p>
<table>
<thead>
<tr>
<th>Мерење</th>
<th>Пре (у језгру)</th>
<th>После (радник)</th>
<th>Разлика</th>
</tr>
</thead>
<tbody>
<tr>
<td>улазак и излазак из корисничког режима</td>
<td>-</td>
<td>1,1 us по кругу</td>
<td></td>
</tr>
<tr>
<td><code>db-bench</code>, 5 000 уметања у 50 трансакција, најбољи / медијан</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>једно уметање по трансакцији, најбољи / медијан</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>цела сигурносна копија једним позивом (23 MiB)</td>
<td>514 ms</td>
<td>635 ms</td>
<td>+23%</td>
</tr>
</tbody>
</table>
<p>Записи на диск (1 572) и flushеви (319) су идентични, како и мора бити. Није изоловано ово: <strong>језгро и даље верује ономе што радник врати</strong>, након провере облика. Компромитовани радник може да лаже (на пример да врати празну листу опозваних сертификата, а тиме их „одопозове”), али не може да чита меморију језгра, TLS кључеве ни кључеве тикета. Нема KPTI-ја, па цурења типа Spectre/Meltdown нису спречена и не тврдимо ништа о њима; PAC кључ деле EL0 и EL1. На x86_64 цену пребацивања нисмо измерили на KVM-у (под TCG-ом би број био бесмислен), а само фазирање радника и протокола није готово. Скуп тестова за похрану (79 сценарија, сваки врти SQLite у раднику) пролази на aarch64 HVF-у и x86_64 TCG-у, на aarch64 TCG-у 76 (један се прескаче јер TCG нема MSI-X).</p>
<p><strong>H7 је затворен, али не онако како смо најавили.</strong> Доња граница времена (ADR-050) и SNTP са консензусом (T1) покривају обим H7; NTS (RFC 8915) остаје изван обима и записан је као преостали ризик, а не као нешто што ради. Не сматрамо да је безбедност времена „решена”, него да је њен доказиви део готов.</p>
<p><strong>Нова мрежна опција.</strong> Contabo не даје DHCP прилагођеном ISO-у, па је VPS дотад тражио другу слику од лабораторијске VM. Сада <code>network.mode = "auto"</code> (ADR-214) прво пита DHCP, а ако закупа нема у задатом року (<code>dhcp_timeout_s</code>, подразумевано 15 s) или га изгуби, примењује подразумевану статичку адресу. Испробано ручно на QEMU-у (са DHCP-ом користи закуп, без мреже се након 5 s враћа на статичку адресу); smoke сценарија за то још нема.</p>
<h2 id="x11-mosquittom-vps-">X11: поређење са Mosquittom на истом VPS-у</h2>
<p>Иста врста машине као у прошлом тексту (Contabo, 4 vCPU AMD EPYC, 8 GiB), оптерећење са Linux домаћина 15 ms даље, узајамни TLS 1.3 са истим сертификатима на обе стране, Mosquitto 2.0.21 на Debianu 13 без трајности. Метода и одлуке су у ADR-213, сирови подаци у <code>docs/perf-results/</code>. Медијан од три покретања.</p>
<figure><img loading="lazy" alt="Потпуна руковања у секунди (TLS 1.3 + клијентски сертификат + CONNECT, без наставка сесије) при 32, 64 и 128 клијената: urMQTT 169, 278, 413; Mosquitto 82, 140, 184. То су доње границе: генератор оптерећења је био близу сопственог ограничења." src="https://aldo.tech/blog/urmqtt-config-builder-plaintext-mosquitto/img/mosquitto.svg" /><figcaption>Потпуна руковања у секунди (TLS 1.3 + клијентски сертификат + CONNECT, без наставка сесије) при 32, 64 и 128 клијената: urMQTT 169, 278, 413; Mosquitto 82, 140, 184. То су доње границе: генератор оптерећења је био близу сопственог ограничења.</figcaption></figure>
<table>
<thead>
<tr>
<th></th>
<th>urMQTT</th>
<th>Mosquitto 2.0.21</th>
</tr>
</thead>
<tbody>
<tr>
<td>руковања/s, 32 / 64 / 128 клијената (bench профил)</td>
<td>169 / 278 / 413</td>
<td>82 / 140 / 184</td>
</tr>
<tr>
<td>исто, са ограничењима каква се испоручују</td>
<td>185 / 262 / 333 (при 128: 202 до 286 грешака, ресети руковања)</td>
<td>-</td>
</tr>
<tr>
<td>меморија по неактивној вези (20 000 веза)</td>
<td>0,50 KB хипа језгра (0,66 KB при 5 000)</td>
<td>26,8 KB раста процеса</td>
</tr>
<tr>
<td>латенција p50, QoS 0 / QoS 1, 800 порука/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 порука/s, испоручено/s (p50)</td>
<td>4 209 (77 ms)</td>
<td>4 235 (197 ms)</td>
</tr>
<tr>
<td>100 претплатника, 50 порука/s: p50</td>
<td>16,6 ms</td>
<td>99 ms</td>
</tr>
<tr>
<td>један ток, QoS 0, 64 B, објављено/s</td>
<td>2 962 (0 до 7 733)</td>
<td>22 749</td>
</tr>
</tbody>
</table>
<p>Шта то значи, а шта не. Руковања су <strong>доње границе за оба сервера</strong>: генератор оптерећења је дељена VM са 60 до 70% украденог процесорског времена и при 128 нити био је близу сопственог ограничења, а urMQTT-ова радна језгра била су заузета само 12 до 13%. Однос при 128 клијената (око 2,2 пута) најсмисленији је; при 32 и 64 углавном мери чекање клијената. Бројке меморије не броје исто (хип језгра наспрам RSS-а процеса), па је „око 50 пута мање” ред величине, не прецизан однос. Латенција од 14 ms је један круг мреже (око 15 ms), дакле „нема мерљивог времена брокера”; репови (p99 око 100 до 200 ms) су код оба брокера доминирани дељеним домаћинима. Разлике испод око 20% не тврдимо.</p>
<p><strong>Шта није ишло у нашу корист</strong> и остало је у табелама: један TCP ток кроз urMQTT носио је 2 до 8 пута мање него кроз Mosquitto, са променама од нуле до 7,7 хиљада порука у секунди међу покретањима, а 20 ms watchdog руковаоца <strong>прекидао је живе везе</strong> кад је хипервизор зауставио vCPU (у продукцијској поставци <code>action = "drop"</code>, 6 до 8 веза по покретању при QoS 0 преплављивању). Обе ствари су описане у следећем одељку јер су имале узроке у нашем коду.</p>
<h2 id="_2">Зашто је један ток био спор: узроци, а не изговори</h2>
<p>Најлакше би било рећи „дељени VPS”. Нисмо, него смо уз исти VPS и исту методу тражили узроке. Пронађено је пет узрока, четири у путу података и један у watchdogу (ADR-221 до ADR-224):</p>
<table>
<thead>
<tr>
<th>Симптом у X11</th>
<th>Узрок у urMQTT-у</th>
<th>Промена</th>
</tr>
</thead>
<tbody>
<tr>
<td>један ток 1,9k порука/s, често <strong>0 испоручено</strong></td>
<td><strong>Изгубљено буђење:</strong> петља излаза враћала је списак спремних веза преко управо исписаног; претплатник чији се излазни међуспремник једном напунио више никад није био опслуживан док га PINGRESP није пробудио</td>
<td>поправљено (ADR-223)</td>
</tr>
<tr>
<td>неколико процената губитка претвара 48k у 14k порука/s</td>
<td><strong>сваки сегмент након изгубљеног био је одбачен</strong> (пријем само по реду), па је опоравак коштао круг мреже по сегменту</td>
<td>ред сегмената изван редоследа, SACK (ADR-222)</td>
</tr>
<tr>
<td>строп 4,4 MB/s при 15 ms</td>
<td>16-битни прозор, ред слања од 64 KiB и спори старт који је бројао ACK-ове, а не бајтове</td>
<td>скалирање прозора, временске ознаке, бројање бајтова (ADR-222)</td>
</tr>
<tr>
<td>сваки круг тока који стане чека 0 до 10 ms</td>
<td><strong>диспечер је спавао до следећег откуцаја од 10 ms</strong> (virtio-net нема прекид за пријем)</td>
<td>испитивање NIC-а сваких 200 us док постоје везе (ADR-224)</td>
</tr>
<tr>
<td>1 до 8 прекинутих веза по покретању</td>
<td><strong>watchdog је руковаоцу наплаћивао протекло време</strong>, а VPS зауставља vCPU-ове често и дуго</td>
<td>буџет је време које је језгро стварно радило (ADR-221)</td>
</tr>
</tbody>
</table>
<p>Заустављања су стварна: на овом VPS-у језгра 0 до 2 имала су медијан 36 до 80 закаснелих откуцаја по 5 секунди, а најгоре заустављање у прозору било је медијан око 70 ms и до 0,9 s (језгро 3, база и дневник: до 1,55 s), док домаћин не јавља украдено време (<code>host steal 0 us</code>). Зато је watchdogу требао други сат, не већи буџет.</p>
<figure><img loading="lazy" alt="Пре и после, на истом VPS-у са истом методом, медијан три до шест покретања: један ток QoS 0, 64 B: објављено 1,9k, 23,7k (Mosquitto 29,3k), испоручено 0 (медијан), 12,6k (29,2k). 16 парова, преплављивање, 64 B: објављено 24,7k, 43,2k (31,4k), испоручено 20,0k, 37,4k (30,7k)." src="https://aldo.tech/blog/urmqtt-config-builder-plaintext-mosquitto/img/before-after.svg" /><figcaption>Пре и после, на истом VPS-у са истом методом, медијан три до шест покретања: један ток QoS 0, 64 B: објављено 1,9k, 23,7k (Mosquitto 29,3k), испоручено 0 (медијан), 12,6k (29,2k). 16 парова, преплављивање, 64 B: објављено 24,7k, 43,2k (31,4k), испоручено 20,0k, 37,4k (30,7k).</figcaption></figure>
<table>
<thead>
<tr>
<th></th>
<th>X11 код (7f9517e)</th>
<th>сада (86a71ff)</th>
<th>Mosquitto</th>
</tr>
</thead>
<tbody>
<tr>
<td>један ток, QoS 0, 64 B, објављено / испоручено у s</td>
<td>1,9k / 0</td>
<td>23,7k / 12,6k</td>
<td>29,3k / 29,2k</td>
</tr>
<tr>
<td>један ток, 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 парова, преплављивање, 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 парова, преплављивање, 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>изгубљене везе по покретању (16 парова, 64 B)</td>
<td>13 (7 до 16)</td>
<td>0 (0 до 0)</td>
<td>0</td>
</tr>
</tbody>
</table>
<p>Watchdog: 148 прекинутих веза у 30 оптерећених покретања пре, 0 у 24 покретања после; на локалној направи која одгађа и губи оквире, при губитку од 0,1% један ток од 64 B са 14,3k порука/s (језгро X11) сада има 41,0k, а при 0,5% са 5,0k на 17,7k.</p>
<p><strong>Шта ово не каже.</strong> urMQTT <strong>и даље није бржи од Mosquitta на једном току</strong>: при 64 B објављује око 80% његове брзине, а испоручује око 40%, јер одбацује QoS 0 кад је ток претплатника спорији од издавачевог (Mosquitto без границе ставља у ред). При 1 KiB у 16-парном преплављивању Mosquitto је испред за око 15%, што је унутар шума. Генератор је поново био дељена VM са 58 до 77% украденог времена. Друга рунда (закључавање хипа изван пута података, SACK на страни слања и друго) је на неинтегрисаној грани <code>perf-single-flow-wip</code>; њене бројке још нису тврдње и овде их не наводимо.</p>
<h2 id="_3">Обећање које нисмо дали, али смо га испунили: конфигурација је податак</h2>
<p>До сада је свака конфигурација била нова градња: кључеви и ограничења били су уграђени у слику. C1 то окреће (ADR-215 до ADR-219): језгро се гради једном, а конфигурација је податак.</p>
<ul>
<li><strong>Блоб <code>urmqtt-cfg</code></strong> (C1a): верзионисан, канонски запис (поља по растућим ознакама, SHA-256 отисак, необавезни одвојени Ed25519 потпис), 70 поља са подразумеваним вредностима и границама, декодер који одбија све неисправно.</li>
<li><strong>Језгро га чита као Limine модул</strong> (C1b), пре било чега другог, на aarch64 и x86_64. Постоји <strong>једно издато језгро за сваки <code>ENV</code> осим <code>dev</code></strong> (aarch64 ELF-ови за <code>release</code>, <code>prod</code> и <code>bench</code> имају исти SHA-256; проверено само на aarch64). Издато језгро одбија развојну конфигурацију, а без конфигурације пише „refusing to serve” и не опслужује ништа.</li>
<li><strong>Инсталирани диск носи конфигурацију</strong> и ажурирање је задржава (C1c).</li>
<li><strong><code>urmqtt-config</code> и шаблон ISO-а</strong> (C1d): <code>pack</code>, <code>show</code>, <code>verify</code>, а <code>pack --iso</code> замењује оба примерка места за конфигурацију и 24 сажетка модула у готовом ISO-у. Резултат је бајт за бајт исти као ISO који <code>mkiso.sh --config</code> гради. Шаблон издања v0.2.0 има 63 111 168 бајтова.</li>
<li><strong>Изјава о поверењу</strong> (ADR-219): конфигурисана слика при подизању исписује отисак конфигурације (<code>config: fingerprint ...</code>), а потпис је необавезан и <strong>искључен по подразумеваном</strong>. Измењени ISO нема службени сажетак; отисак је начин да се види шта је у њему.</li>
</ul>
<h2 id="aldotech">Градитељ на aldo.tech</h2>
<p>Исто што ради <code>urmqtt-config pack --iso</code>, ради и веб страница, истим кодом: библиотека алата преведена у WebAssembly (1,06 MB, без увоза и без <code>unsafe</code> блока). Страница је на <a href="https://aldo.tech/sr/proizvodi/urmqtt-builder/">aldo.tech/sr/proizvodi/urmqtt-builder/</a> и постоји на свих 15 језика сајта (на пример <a href="https://aldo.tech/proizvodi/urmqtt-builder/">hr</a>).</p>
<ul>
<li><strong>Ништа не напушта прегледач.</strong> Сертификати, приватни кључеви и готов ISO остају на рачунару; страница скида само датотеке издања са овог сајта, нема праћења ни скрипти трећих страна. У тесту уживо ниједан захтев није носио кључни материјал.</li>
<li><strong>Огледало издања</strong> (ADR-227): сат по сат (systemd timer, ниског приоритета) копира издања са GitHuba, свако проверава пре објаве, а страница у прегледачу проверава потпис <code>SHA256SUMS</code> кључем уграђеним у страницу, па тек онда уписује конфигурацију у шаблон. Неуспела провера блокира преузимање.</li>
<li><strong>Једноставно по подразумеваном, напредно по потреби</strong> (ADR-232): први екран су три корака (где ради, ваши сертификати, име слике). Једно поље за превлачење датотека разврстава сертификат, кључ и CA по садржају. Напредни приказ отвара свако од 70 поља, претраживање, увоз и извоз TOML-а; оба приказа уређују исто стање. Шаблон (63 MB) скида се док корисник попуњава форму. У проби уживо, са пробним огледалом и пробним кључем, изградња ISO-а у прегледачу трајала је 5 до 18 s.</li>
</ul>
<figure><img loading="lazy" alt="Једноставни приказ градитеља: три корака, „Где ће радити?”, „Ваши сертификати” и „Име слике”, са једном следећом радњом у бочној траци." src="https://aldo.tech/blog/urmqtt-config-builder-plaintext-mosquitto/img/builder-simple.png" /><figcaption>Једноставни приказ градитеља: три корака, „Где ће радити?”, „Ваши сертификати” и „Име слике”, са једном следећом радњом у бочној траци.</figcaption></figure>
<figure><img loading="lazy" alt="Напредни приказ, снимљен на пробном огледалу са пробним кључем (датум издања и кључ потписа нису прави): издање са провереним потписом, шаблони, свако поље конфигурације." src="https://aldo.tech/blog/urmqtt-config-builder-plaintext-mosquitto/img/builder-advanced.png" /><figcaption>Напредни приказ, снимљен на пробном огледалу са пробним кључем (датум издања и кључ потписа нису прави): издање са провереним потписом, шаблони, свако поље конфигурације.</figcaption></figure>
<p><strong>Емулатор, необавезно.</strong> Поред „Преузми ISO” стоји „Испробај у емулатору”: QEMU преведен у WebAssembly покреће управо изграђену слику у картици прегледача. Ништа од њега се не скида пре клика (око 3 MB први пут). Емулатор је BIOS-only, <strong>без мреже</strong> (да саобраћај корисника не би прошао кроз неког посредника и нарушио „ништа не напушта прегледач”), тражи око 1,5 GB меморије и рачунарски прегледач (тестиран Chrome; Firefox и Safari нису). У њему urMQTT стиже до <code>URMQTT READY</code> за око 8 s (са прескоченим одбројавањем) и исписује исти отисак конфигурације као страница, што је доказ да је подигнута управо та конфигурација. Подизање је око 2 пута спорије од изворног QEMU-а на истом Macu (0,82 s наспрам 1,9 s), а криптографија око 58 пута спорија јер су хардверске инструкције емулиране. Зато <strong>из емулатора не извлачимо никакве тврдње о брзини</strong>, а страница то каже пре покретања. При изградњи је требало закрпити четири бајта у <code>.wasm</code>-у због грешке у преводу инструкције POPCNT у изворном емулатору (још је нисмо пријавили); сам емулатор је под GPL-ом, уз понуду изворног кода.</p>
<figure><img loading="lazy" alt="Дијагностички унос у емулатору уживо на aldo.tech: kernel је исписао исти отисак конфигурације као страница, а екран приказује дијагностику (у емулатору завршава за око 20 s; њене бројке описују емулатор, не urMQTT)." src="https://aldo.tech/blog/urmqtt-config-builder-plaintext-mosquitto/img/emulator-diagnostic.png" /><figcaption>Дијагностички унос у емулатору уживо на aldo.tech: kernel је исписао исти отисак конфигурације као страница, а екран приказује дијагностику (у емулатору завршава за око 20 s; њене бројке описују емулатор, не urMQTT).</figcaption></figure>
<h2 id="mqtt">Обичан MQTT, само за мреже којима верујете</h2>
<p>Обичан MQTT (TCP 1883, без TLS-а) врло је чест у IoT-у: подразумеван је у Mosquittu, у већини firmwarea за Arduino и ESP, у Tasmoti, Zigbee2MQTT-у и Home Assistantu, а користи се и унутар сегментираних индустријских мрежа, иза VPN-а и на приватним LAN-овима, па и на уређајима без сата (сертификати траже датум), без довољно flasha или процесора за TLS. Преко непоуздане мреже није прихватљив: ко је на путу, чита и мења саобраћај. Зато је P1 (ADR-229 до ADR-231) изричит, ограничен избор, <strong>искључен по подразумеваном</strong>, а TLS порт остаје; TLS се овим не искључује, обичан порт долази уз њега.</p>
<ul>
<li><strong>Списак дозвољених мрежа је обавезан</strong> (<code>allow_from</code>, CIDR-ови) и проверава се при SYN-у: изван списка нема SYN-ACK-а ни стања. Цео интернет (<code>0.0.0.0/0</code>) тражи засебну изричиту опцију.</li>
<li><strong>Три начина пријаве:</strong> лозинка (подразумевано кад је укључено; PBKDF2-HMAC-SHA-256, со, 10 000 итерација, поређење у сталном времену, закључавање по адреси), анонимно (један конфигурисан идентитет са сопственим ACL-ом) и по адреси (<code>ip</code>). Градитељ лозинку распршује у прегледачу и у меморији држи само сажетак.</li>
<li><strong>Мање права него mTLS:</strong> обичан клијент никад нема сервисна, оператерска ни управљачка права, нема <code>$SYS</code> ни конзоле, има сопствене сесије (<code>$plain/&lt;идентитет&gt;/&lt;id клијента&gt;</code>, само чисте), сопствена ограничења и регистар дозвољених га и даље обавезује.</li>
<li><strong>Шта се губи, пише у <code>SECURITY.md</code>:</strong> поверљивост, целовитост, аутентичност брокера и понављање снимљене пријаве. То се конфигурацијом не може поправити; тако је кад нема TLS-а.</li>
</ul>
<p>Измерено на лаптопу (Apple silicon, HVF, QEMU корисничка мрежа), 16 парова, 64 B, три покретања од 10 s: при 8 000 порука у секунди TLS и обичан пут испоручују све поруке са истом латенцијом (p50 0,17 ms наспрам 0,13 до 0,18 ms), нове везе у секунди: TLS око 2 600 до 2 750, анонимни обичан око 4 900 до 7 800, а <strong>пријава лозинком је скупи део</strong>: један PBKDF2 стоји око 2,5 ms, па се број нових пријава ограничава на око 200 по језгру података у секунди (измерено 308 до 342 на два језгра). Преплављивање на тој машини није стабилан инструмент (20k до 440k испоручених у секунди међу покретањима) и <strong>из њега не извлачимо закључак</strong>. Поређење обичног пута са Mosquittom на VPS-у још није направљено. Тестови: smoke група <code>plaintext</code> 7 од 7 на aarch64 HVF-у и на x86_64 TCG-у, нови fuzz циљ <code>plain_input</code> (2,3 милиона покретања без налаза).</p>
<h2 id="_4">Грешке које је пронашао рад уживо</h2>
<p>Две ствари нису ухватили тестови, него прво укључење на правом сајту:</p>
<ul>
<li>Страница је датотеке издања тражила <strong>један директоријум превисоко</strong>, па је свака провера потписа одговорила 404 (исправак 13af301). Тест је користио пробно огледало на другој путањи.</li>
<li><strong>Тастатура емулатора никад није стигла до госта</strong>: узрок није био лош превод тастера, него то што дијалог уопште није слушао тастере (исправак у ADR-232, уз унос за подизање без тастера).</li>
</ul>
<h2 id="_5">Лекције</h2>
<ul>
<li><strong>Тражите узрок, не окривљујте окружење.</strong> Већина слабости једног тока и све прекинуте везе биле су наше; хипервизор је стваран, али га је требало мерити, не претпостављати.</li>
<li><strong>Поређење са другим производом мора укључити и губитке.</strong> Табела у којој су само наше победе не би била поређење.</li>
<li><strong>Мерни инструмент има границе.</strong> Дељени генератор са 60 до 70% украденог времена мења шта бројке значе; зато пишемо „доња граница” и не тврдимо разлике испод 20%.</li>
<li><strong>Изолација није доказ.</strong> SQLite у EL0 смањује шта грешка у њему може, не оно што може да лаже.</li>
<li><strong>Стварна инсталација налази оно што тестови не траже</strong>: путању која је погрешно сложена и тастере које нико не слуша.</li>
</ul>
<h2 id="_6">Шта још није доказано</h2>
<ul>
<li>Потписни кључ издања за сада је пробни кључ; v0.2.0 и v0.2.1 су pre-release, а слике уз њих развојне уз пробне сертификате (осим шаблона, који је без конфигурације).</li>
<li>13 превода странице градитеља (и овог текста) написао је AI и проверили су их тестови и узорци, не изворни говорници.</li>
<li>Поређење са Mosquittom је једна сесија, један пут и један дељени генератор, на дељеном VPS-у; разлике испод 20% нису тврдње.</li>
<li>Обичан пут није мерен на VPS-у ни против Mosquitta.</li>
<li>Lighthouse на новом једноставном приказу није потврђен (стари приказ: приступачност 95 или више).</li>
<li>Bare metal, x86_64 цена радника на KVM-у, Firefox и Safari за емулатор: није мерено.</li>
</ul>
<h2 id="_7">Шта следи</h2>
<ul>
<li><strong>D1: јавни демо брокер.</strong> Емулатор из прегледача не можемо учинити јавним: живи у картици једног посетиоца, 20 до 100 пута је спорији од KVM-а, нема долазне мреже и нестаје са затварањем картице; преусмеравање интернета у туђи прегледач било би и злоупотреба. Могућ је прави демо брокер: urMQTT на VPS-у иза имена на aldo.tech, са објављеним пробним CA-ом, строгим ACL-ом и малим ограничењима. То је засад само идеја, не градња, и треба изричит пристанак за јавно излагање.</li>
<li><strong>Друга рунда за један ток</strong> (грана <code>perf-single-flow-wip</code>): тек кад је измерена на VPS-у и спојена.</li>
<li><strong>Обичан пут против Mosquitta на VPS-у</strong>, пријава и опозив налога за обичан MQTT преко потписаног управљачког канала.</li>
<li><strong>После издања:</strong> bare metal (X12 до X16), IOMMU и Secure Boot, шифровање диска.</li>
</ul>
<p>Код је на <a href="https://github.com/zeljan-alduk/urMQTT">github.com/zeljan-alduk/urMQTT</a>, а <code>docs/PROGRESS.md</code> и даље је једини извор истине о томе шта стварно ради, <code>docs/PERFORMANCE.md</code> о томе шта је измерено. Пред-алфа, као и раније; пријаве грешака и безбедносне пријаве по упутама у <code>SECURITY.md</code> добродошле су.</p>]]></content:encoded>
      <category>rust</category>
      <category>mqtt</category>
      <category>iot</category>
      <category>unikernel</category>
      <category>open source</category>
    </item>
    <item>
      <title>urMQTT на правом VPS-у: ISO, руковање од 0,24 ms, сопствени диск и ојачано језгро</title>
      <link>https://aldo.tech/sr/blog/urmqtt-vps-handshake-disk-hardening/</link>
      <guid isPermaLink="true">https://aldo.tech/sr/blog/urmqtt-vps-handshake-disk-hardening/</guid>
      <pubDate>Sat, 31 Oct 2026 08:00:00 +0000</pubDate>
      <description>Шта се догодило са три обећања из првог текста о urMQTT-у (један ISO, сирови диск без датотечног система, доказиво безбедно) након прва четири дана развоја, са мерењима, табелама и списком онога што још није готово.</description>
      <content:encoded><![CDATA[<p>У <a href="https://aldo.tech/blog/urmqtt-unikernel-mqtt-broker/">првом тексту о urMQTT-у</a> описали смо уникернел који јесте MQTT брокер, до прекретнице M17, и завршили са три обећања под насловом „Шта следи”: један ISO који се подиже на више платформи, сирови диск без датотечног система и безбедност која може да се докаже, а не само да се тврди. Ништа од тога тада није било изграђено. Овај текст прати шта се догодило са сваким обећањем, истим редом као и раније: шта смо планирали, шта смо изградили, шта нас је изненадило и шта је још отворено. Успут ћемо показати и колико је то трајало, јер је и то део приче.</p>
<p>Једна напомена унапред: пројекат је и даље пред-алфа. Бројке у тексту мере код и тестове, а не зрелост у продукцији, а прва објава је намерно ограничена на VPS-ове са KVM-ом. Bare metal остаје за касније.</p>
<h2 id="_1">Како је то изгледало у времену</h2>
<p>Први commit настао је 28. октобра 2026. у 23:48 (M0). До 31. октобра у 23:00 у репозиторијуму је 119 commitова, распоређених као 3, 39, 38 и 39 по календарским данима. Одмах кажимо и како је то могуће: пројекат се гради уз Claude Code, а commitови носе <code>Co-Authored-By: Claude</code> trailere. Четири календарска дана историје не значе да је једна особа за четири дана написала 90 000 редова; значе да се план, имплементација, тестови и ADR-ови пишу у кратким, затвореним петљама, а сваки корак завршава истим ритуалом провере као и раније. Бројке испод говоре о количини кода и тестова, а не о томе колико је све то спроведено у стварном раду.</p>
<figure><img loading="lazy" alt="Раст пројекта током четири дана са прекретницама од M0 до данас: хиљаде редова Rusta (од 1,6 до 89,8), број #[test] функција (од 27 до 739) и број ADR-ова (од 13 до 104), уз стубце commitова по дану (39, 38 и 39 за 29., 30. и 31. октобар)." src="https://aldo.tech/blog/urmqtt-vps-handshake-disk-hardening/img/growth.svg" /><figcaption>Раст пројекта током четири дана са прекретницама од M0 до данас: хиљаде редова Rusta (од 1,6 до 89,8), број #[test] функција (од 27 до 739) и број ADR-ова (од 13 до 104), уз стубце commitова по дану (39, 38 и 39 за 29., 30. и 31. октобар).</figcaption></figure>
<p>График показује три криве које расту готово заједно, што и јесте намера: тестови и записане одлуке прате код, а не касне за њим. Нагиб је најстрмији 29. октобра, када настаје цео пут од језгра до брокера. Даље се редови додају спорије, али тестови расту брже од кода (27 тестова на 1,6 хиљада редова на почетку, 739 на 89,8 хиљада данас).</p>
<table>
<thead>
<tr>
<th>Прекретница</th>
<th>Commit</th>
<th>Време</th>
<th style="text-align: right;">Rust редова</th>
<th style="text-align: right;">Тестова</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>данас</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>Редови су све <code>.rs</code> датотеке изван <code>vendor/</code> и <code>fuzz/</code>, а тестови су <code>#[test]</code> функције. Шта је који део додао, по датуму из git историје:</p>
<table>
<thead>
<tr>
<th>Датум</th>
<th>Прекретнице</th>
<th>Шта је додато</th>
</tr>
</thead>
<tbody>
<tr>
<td>29.10.</td>
<td>M0 до M17</td>
<td>језгро, мрежа, TLS, брокер, SQLite, више језгара, сопствени TCP, ојачање, репродуцибилно издање</td>
</tr>
<tr>
<td>29.10. увече и 30.10.</td>
<td>X0 до X10, S1, H2, H3 (X0 почиње 29.10. у 20:54)</td>
<td>порт на x86_64, универзални ISO, један диск и самоинсталирајући ISO, TLS провере, нападачки клијенти</td>
</tr>
<tr>
<td>31.10.</td>
<td>O1 до O4, T1, PERF, H4, H5, H9, S2 до S6</td>
<td>увид и конзола преко MQTT-а, живи режим рада и дијагностика, SNTP, убрзања, ојачање, верификација, ланац набавке, похрана</td>
</tr>
</tbody>
</table>
<h2 id="1-iso">Обећање 1: један ISO за више платформи</h2>
<p><strong>Шта смо планирали.</strong> Једна датотека која се подиже на aarch64 UEFI-ју, x86_64 UEFI-ју и x86_64 BIOS-у. <strong>Шта смо изградили.</strong> Порт на x86_64 (X0 до X10) и универзални ISO са Limine bootloaderom, један хибридни ISO са ког се подижу све те комбинације; smoke тестови га подижу на четири виртуелне машине. Мени има три уноса: urMQTT (живи режим рада, ради из RAM-а, ништа не пише на диск), инсталација на диск машине на којој је покренут (инсталација пише GPT и податке, па гаси машину) и Дијагностика (детекција хардвера и мерења).</p>
<figure><img loading="lazy" alt="Boot мени ISO-а снимљен на VPS-у: уноси „urMQTT”, „Install urMQTT on this machine's disk” и „Diagnostic: hardware detection and benchmarks” са одбројавањем до аутоматског подизања. Снимак је са пробног ISO-а са дужим одбројавањем; подразумевано је 10 секунди." src="https://aldo.tech/blog/urmqtt-vps-handshake-disk-hardening/img/boot-menu.png" /><figcaption>Boot мени ISO-а снимљен на VPS-у: уноси „urMQTT”, „Install urMQTT on this machine's disk” и „Diagnostic: hardware detection and benchmarks” са одбројавањем до аутоматског подизања. Снимак је са пробног ISO-а са дужим одбројавањем; подразумевано је 10 секунди.</figcaption></figure>
<p>Прва стварна мета био је VPS, а не физички рачунар: одлука је да прво издање подржава само VPS (KVM), а bare metal долази после њега. Тестирали смо на Contabo VPS-у са AMD EPYC процесором (породица 23, Zen 1), 4 vCPU-а и 8 GiB RAM-а, са virtio-net мрежом и virtio-scsi диском. Први покушај нас је изненадио: ISO прикључен као виртуелни CD подизао је задати унос, продукцијска слика одбила је да служи са медијума само за читање и ресетовала се, па смо добили бесконачну петљу мени, језгро, ресет без иједне читљиве поруке. Данас фатална грешка у продукцијској слици оставља поруку на конзоли 60 секунди пре ресетовања, а са оптичког медијума исписује да је подигнуто са медијума само за читање и стаје. Живи режим рада у RAM-у (O3) касније је тај проблем заобишао за уобичајени случај.</p>
<p>Дијагностички унос настао је из тог искуства: машину коју не видимо морамо моћи да питамо шта има. Исписује CPU, RAM, мрежу, дискове и PCI уређаје, мери пропусност меморије, диск и криптографију, а иста мерења исписује као JSON на серијски порт. Записивање на диск дозвољено је само у нераспоређеном простору и тек након што се укуца <code>WIPE</code>; на снимку испод то је прескочено.</p>
<figure><img loading="lazy" alt="Дијагностички екран на Contabo VPS-у (AMD EPYC, KVM, 4 vCPU): систем, меморија, диск, PCI уређаји, пропусност и латенција RAM-а те крипто-мерења, укључујући 244 us по mTLS руковању, X25519 48 us, ECDSA потпис 35,7 us и проверу 107 us." src="https://aldo.tech/blog/urmqtt-vps-handshake-disk-hardening/img/diag-vps.png" /><figcaption>Дијагностички екран на Contabo VPS-у (AMD EPYC, KVM, 4 vCPU): систем, меморија, диск, PCI уређаји, пропусност и латенција RAM-а те крипто-мерења, укључујући 244 us по mTLS руковању, X25519 48 us, ECDSA потпис 35,7 us и проверу 107 us.</figcaption></figure>
<p><strong>Шта је отворено.</strong> Прави хардвер. После првог издања следи bare metal (USB подизање, NVMe и AHCI, мрежна картица стварног рачунара), заједно са IOMMU-ом и Secure Bootом, јер без IOMMU-а злонамеран или неисправан уређај може писати по целој меморији. У овој верзији о томе не тврдимо ништа.</p>
<h2 id="12-ms-024-ms">Руковање: од 1,2 ms до 0,24 ms</h2>
<p>Прво мерење дијагностике на VPS-у дало је незгодну бројку: mTLS руковање, само криптографија на страни сервера, трајало је 1,2 ms. На aarch64 смо већ користили хардверске AES и SHA инструкције, али x86_64 није имао ништа слично за јавни кључ: RustCrypto је преносив и constant-time, па и спор. Три промене су то спустиле.</p>
<ul>
<li>За P-256 смо преузели <code>nistz256</code> асемблер из OpenSSL-а и ring-а (ADR-130), уз табеле са фиксном базом за потписивање и проверу потписа CA (ADR-110, ADR-111).</li>
<li>X25519 је преписан на четири 64-битна уда са MULX/ADX инструкцијама (ADR-131).</li>
<li>Након тога смо профилисали и остатак (ADR-132, ADR-200).</li>
</ul>
<figure><img loading="lazy" alt="Руковање на VPS-у: 1200 us пре, 237 us после асемблера и табела; A/B поређење memcpy-а на истом покретању, 278,8 us са старим, 229,4 us са новим; те пре и после за X25519 (146 на 48 us), ECDSA потпис (673 на 34 us) и проверу (309 на 90 us)." src="https://aldo.tech/blog/urmqtt-vps-handshake-disk-hardening/img/handshake.svg" /><figcaption>Руковање на VPS-у: 1200 us пре, 237 us после асемблера и табела; A/B поређење memcpy-а на истом покретању, 278,8 us са старим, 229,4 us са новим; те пре и после за X25519 (146 на 48 us), ECDSA потпис (673 на 34 us) и проверу (309 на 90 us).</figcaption></figure>
<table>
<thead>
<tr>
<th>Мерење на VPS-у (server, микросекунде)</th>
<th style="text-align: right;">Пре</th>
<th style="text-align: right;">После</th>
</tr>
</thead>
<tbody>
<tr>
<td>mTLS руковање, цела криптографија</td>
<td style="text-align: right;">1200</td>
<td style="text-align: right;">око 240 (237 у мерењу са графа, 244 на снимку)</td>
</tr>
<tr>
<td>X25519</td>
<td style="text-align: right;">146</td>
<td style="text-align: right;">48</td>
</tr>
<tr>
<td>ECDSA потпис</td>
<td style="text-align: right;">673</td>
<td style="text-align: right;">34</td>
</tr>
<tr>
<td>ECDSA провера</td>
<td style="text-align: right;">309</td>
<td style="text-align: right;">90 (107 на снимку)</td>
</tr>
<tr>
<td>руковање, стари memcpy, исти boot</td>
<td style="text-align: right;">278,8</td>
<td style="text-align: right;">229,4 (-17,7%)</td>
</tr>
</tbody>
</table>
<p>Бројке се разликују по покретању око 10%, па смо записали распон уместо једне вредности. Укупно је то отприлике пет пута мање CPU времена за руковање.</p>
<p>Шта смо научили о memcpy-у. Кад је криптографија пала, профил је показао нешто што нисмо очекивали: функције <code>mem*</code> (memcpy, memmove, memset) трошиле су 7% руковања, 13 до 39% пута TLS записа (39% за 64 B, 13% за 4 KiB) и 17 до 34% разгранавања PUBLISH-а. Заменили смо их SSE2 асемблером за x86_64 (ADR-200), уз диференцијалне тестове према референтној имплементацији за све величине, поравнања и преклапања. По позиву је то 55 до 75 циклуса према 7 до 12 за 1 до 64 бајта.</p>
<figure><img loading="lazy" alt="Лево: удео времена сервера у mem* функцијама пре преписивања (руковање 7%, TLS запис 64 B 39%, 256 B 37%, 1 KiB 24%, разгранавање 17 до 34%). Десно: колико је сваки пут бржи после (руковање 17,7%, 64 B 26,4%, 256 B 19,6%, 1 KiB 16,1%, 4 KiB 6,6%, разгранавање 7 до 34%)." src="https://aldo.tech/blog/urmqtt-vps-handshake-disk-hardening/img/hotpaths.svg" /><figcaption>Лево: удео времена сервера у mem* функцијама пре преписивања (руковање 7%, TLS запис 64 B 39%, 256 B 37%, 1 KiB 24%, разгранавање 17 до 34%). Десно: колико је сваки пут бржи после (руковање 17,7%, 64 B 26,4%, 256 B 19,6%, 1 KiB 16,1%, 4 KiB 6,6%, разгранавање 7 до 34%).</figcaption></figure>
<table>
<thead>
<tr>
<th>Пут</th>
<th style="text-align: right;">Удео mem* пре</th>
<th style="text-align: right;">Брже после</th>
</tr>
</thead>
<tbody>
<tr>
<td>руковање</td>
<td style="text-align: right;">7%</td>
<td style="text-align: right;">17,7%</td>
</tr>
<tr>
<td>TLS запис 64 B</td>
<td style="text-align: right;">39%</td>
<td style="text-align: right;">26,4%</td>
</tr>
<tr>
<td>TLS запис 256 B</td>
<td style="text-align: right;">37%</td>
<td style="text-align: right;">19,6%</td>
</tr>
<tr>
<td>TLS запис 1 KiB</td>
<td style="text-align: right;">24%</td>
<td style="text-align: right;">16,1%</td>
</tr>
<tr>
<td>TLS запис 4 KiB</td>
<td style="text-align: right;">13%</td>
<td style="text-align: right;">6,6%</td>
</tr>
<tr>
<td>разгранавање порука</td>
<td style="text-align: right;">17 до 34%</td>
<td style="text-align: right;">7 до 34%</td>
</tr>
</tbody>
</table>
<p>Читање графика: што је запис мањи, то већи део времена одлази на копирање, па је и добитак већи. За мале поруке, које су већина MQTT саобраћаја, управо то је оно што се рачуна.</p>
<p>Шта ово не значи. На нативном x86_64 смо OpenSSL 3.5.5 (ADR-130) мерили само као референцу за асемблер, а не као поређење производа. Поређење Linux плус Mosquitto на истом VPS-у (задатак X11) још нисмо урадили. Зато не тврдимо да је urMQTT бржи од Linuxа: није измерено. Бројке изнад пореде urMQTT са самим собом.</p>
<p>Преоптерећење је био други резултат. На VPS-у, са једног Maca (RTT 35 ms), 128 клијената дало је 618 руковања у секунди, 256 клијената 393, а 512 клијената 67, уз хиљаде одбијених веза. То није био плато него урушавање. Узроке смо пронашли у коду (два су потврђена у QEMU-у/HVF-у, на VPS-у се нису могли поновити), више њих одједном: одбијена веза враћала је клијента преко следећег сегмента који носи важећи SYN колачић, одбијање је RST-ом говорило клијенту да одмах покуша поново, а пријем је био O(ред). Исправак (ADR-080 до ADR-082) испушта SYN који не би био прихваћен, памти одбијене токове, држи пријем у O(1) и броји руковања по језгру. Могуће је (хипотеза, није проверено) да је део узрока био и на клијенту: алат за оптерећење је затварао први, па је један Mac са 16 384 ефемерних портова и TIME_WAIT-ом од 30 секунди ограничен на око 550 веза у секунди. Зато прави строп сервера још нисмо измерили.</p>
<h2 id="2">Обећање 2: сирови диск, али пажљив</h2>
<p><strong>Шта смо планирали.</strong> Задржати сопствени распоред без датотечног система и додати све оно што датотечни систем иначе даје: поравнање, раст, discard, сигурносне копије и провере. <strong>Зашто не датотечни систем.</strong> О томе смо расправљали и одлука је потврђена: urMQTT пише једну базу, један телеметријски дневник и сопствене метаподатке, не треба му директоријуми, дозволе, ни имена датотека, а сваки додатни слој био би још кода у поверљивом делу и још један начин да запис тихо нестане. Диск је GPT са ESP партицијом и једном urMQTT партицијом података, на којој је сопствени распоред.</p>
<p><strong>Шта смо изградили (S2 до S6).</strong></p>
<ul>
<li><strong>Распоред v2 (S2).</strong> Топологија диска, поравнање на 1 MiB или оптималну величину I/O-а, цели физички блокови, A/B пар суперблокова са генерацијом (ADR-140). Распоред v1 кернел одбија уз јасну поруку, а мигрира га ванмрежни алат <code>urmqtt-disk migrate</code> (ADR-141): задато је пробни пролаз, копира се па тек онда пребацује, уз повраћај и убризгавање грешке на сваком запису.</li>
<li><strong>Раст (S3).</strong> При подизању диск може да порасте (никад се не смањује), а на virtio-blk и за време рада, преко прекида промене конфигурације (ADR-142, ADR-143).</li>
<li><strong>Discard и write-zeroes (S4).</strong> Танко додељени дискови враћају простор домаћину. У тесту је qcow2 датотека на домаћину пала са 62 на 16 MiB након што су истекли телеметријски сегменти, а опоравак никад не верује садржају који је одбачен.</li>
<li><strong>Сигурносна копија базе на диску (S5).</strong> A/B копија помоћу SQLite online backup API-ја у корацима по 16 страница: најдужи корак 4,6 ms наспрам 445 ms кад се копира у једном позиву. Комитови током копије имали су максимум 2,4 ms према 2,2 ms без ње. Ако <code>quick_check</code> при подизању не успе, база се враћа из копије.</li>
<li><strong>Чишћење и ограничено подизање (S6).</strong> Scrubber чита диск уз ограничен I/O буџет и пријављује оштећене записе, а подизање чита само најновији сегмент сваке партиције, тако да време подизања не расте са величином телеметријског региона. Тиме смо затворили проблем који смо унапред израчунали: на великом диску потпуни опоравак трајао би од минута до десетина минута.</li>
</ul>
<p>Цела група smoke тестова за похрану пролази на обе архитектуре: 79 успешних, 0 неуспешних, на aarch64 под HVF-ом и на x86_64 под TCG-ом. Мерили смо и да S4 није успорио базу: 53,3 хиљаде уметања у секунди у пакетима пре и после, без измерљиве разлике.</p>
<p><strong>Шта је изненадило.</strong> Колико од „једноставног” распореда отпада на рубне случајеве: virtio-scsi и virtio-blk пријављују топологију на различите начине, а непотпун запис у сред миграције мора увек оставити бар једну важећу верзију. Зато је сваки запис на путу миграције и раста прошао убризгавање грешке.</p>
<p><strong>Шта је отворено.</strong> SQLite странице немају контролни збир, па промењен бит унутар вредности у реду база неће открити; телеметријски записи и садржај копија имају CRC и откривају се. Шифровање диска (S7) није готово.</p>
<h2 id="3">Обећање 3: доказиво безбедно, колико год успемо</h2>
<p><strong>Ојачање (H4).</strong> Алокатор брише ослобођену меморију и проверава је при поновној употреби, открива двоструко ослобађање, веже показиваче слободне листе XOR-ом и нулира кључеве при ослобађању (ADR-170). Заштита стека (<code>-Zstack-protector=strong</code>, 1315 функција, а SQLite са <code>-fstack-protector-strong</code>, ADR-171). Извештаји о грешкама у продукцији не садрже регистре ни адресе (ADR-172). KASLR на обе архитектуре: кернел је PIE и Limine га помера за око 19 бита, а насумично се постављају heap, стекови и MMIO прозор (ADR-173). На aarch64 MTE означава heap (ADR-174), али само под TCG-ом: HVF и x86 га немају. kCFI смо проценили и нисмо га прихватили јер преводилац на прикаченом nightlyју пада са унутрашњом грешком; IOMMU је одгођен са bare metalом (ADR-175, ADR-176).</p>
<p><strong>Цена је измерена, не претпостављена.</strong> На путу руковања (<code>dp-bench</code>, aarch64 HVF, пуна TLS 1.3 + mTLS + CONNECT руковања):</p>
<table>
<thead>
<tr>
<th>Корак</th>
<th style="text-align: right;">1 језгро</th>
<th style="text-align: right;">4 језгра</th>
<th>Промена</th>
</tr>
</thead>
<tbody>
<tr>
<td>без ојачања</td>
<td style="text-align: right;">1941</td>
<td style="text-align: right;">7620</td>
<td></td>
</tr>
<tr>
<td>+ алокатор</td>
<td style="text-align: right;">1898</td>
<td style="text-align: right;">7429</td>
<td>-2,2% до -2,5%</td>
</tr>
<tr>
<td>+ заштита стека</td>
<td style="text-align: right;">1845</td>
<td style="text-align: right;">7243</td>
<td>заједно -4,9% до -5%</td>
</tr>
<tr>
<td>+ PIE и ознаке</td>
<td style="text-align: right;">око 1800</td>
<td style="text-align: right;">око 6750</td>
<td>на оптерећеном рачунару, шумовито</td>
</tr>
</tbody>
</table>
<p>Свеукупно је дакле цена на путу руковања отприлике 5% за делове мерене чисто, а са PIE-јем и ознакама излази око 7% на једном и око 11% на четири језгра, али то је мерење шумовито. Сматрамо је прихватљивом, али нисмо је поновили на x86_64 VPS-у.</p>
<p>Вреди упоредити са бројком из првог текста (862 руковања у секунди на једном језгру). Исти алат данас, са погоном хардверског криптовања на aarch64 (X0b) и низом убрзања која су стигла од тада, даје 1941 пре ојачања. Нисмо изоловали допринос сваке промене, па о узроку не тврдимо ништа.</p>
<p><strong>Како је проверено, и шта није.</strong> H5 је додао Miri на подскуп host crateова, Kani доказе, санитајзере, модел-фазер за ACL и извештаје о покривености. Kani је доказао <code>record_len</code> у TLS-у, <code>parse</code> у TCP-у и две аритметике точка тајмера. <strong>Није доказано</strong> (истекло време): оба MQTT декодера, оба парсирања конфигурације и понашање точка тајмера; CI посао за Kani зато још не блокира. Miri не може да извршава асемблер. Ланац набавке (H9): CycloneDX SBOM са 215 компоненти, <code>cargo-vet</code> пролази са 39 потпуно ревидираних и 140 изузетака, а cargo-deny је конфигурисан да савете о рањивостима третира као пад CI-ја (ти CI послови до сада су проверени само локално, не на GitHubу, а негативни тест није покренут), уз <code>VULNERABILITY-HANDLING.md</code>. Искрено стање: од 67 кернел-линкованих верзија трећих crateова, 53 су изузеци, а не ревизије. Vet доказује да се скуп зависности не може променити без видљиве разлике, а не да је TCB прегледан. Сопствену ревизију ни једног cratea нисмо написали.</p>
<p>SNTP (T1) је у том смислу мала прича о безбедности: опционалан је, само према приватним IPv4 серверима, са максималним скоком и условом да се извори слажу (медијан, већина унутар једне секунде). Сат који било ко на путу може померити није сат коме смемо веровати код важења сертификата.</p>
<h2 id="macu-aarch64-x86_64">На Macu: aarch64 изворно и x86_64 емулацијом</h2>
<p>Развијамо на Macu, а тај има два начина за покретање истог кода и они су веома различити.</p>
<table>
<thead>
<tr>
<th></th>
<th>aarch64 под HVF-ом</th>
<th>x86_64 под TCG-ом</th>
</tr>
</thead>
<tbody>
<tr>
<td>Шта је то</td>
<td>изворно извршавање, брзина близу стварног процесора</td>
<td>QEMU емулира инструкције</td>
</tr>
<tr>
<td>Руковање у госту</td>
<td>око 0,58 ms (први пут мерено око M13); <code>dp-bench</code> данас 1941/s по језгру</td>
<td>око 3,3 ms</td>
</tr>
<tr>
<td>TLS smoke сценарији</td>
<td>брзи</td>
<td>око 15 минута по сценарију</td>
</tr>
<tr>
<td>MTE</td>
<td>не (нема га на HVF-у)</td>
<td>x86 га нема; означавање heapа ради само за aarch64 госта под TCG-ом</td>
</tr>
<tr>
<td>Чему служи</td>
<td>стварна мерења</td>
<td>исправност: x86 пут се проверава, бројке нису перформансе</td>
</tr>
</tbody>
</table>
<p>Правило је једноставно: бројке под TCG-ом су емулација и не цитирамо их као перформансе. x86 перформансе мерим само на правом VPS-у. Зато криптографске бројке изнад долазе са Contabo VPS-а, а не са Maca.</p>
<h2 id="_2">Лекције</h2>
<ul>
<li><strong>Мерите на стварној машини пре оптимизације.</strong> Први приступ био је оптимизовати оно што смо мислили да је споро; тек је дијагностика на VPS-у показала 1,2 ms и где тачно одлази време.</li>
<li><strong>Преоптерећење је засебна функција.</strong> Систем који је брз под оптерећењем може се урушити изнад њега; одбијање мора бити јефтиније од прихватања.</li>
<li><strong>Профил изненађује.</strong> Највећи појединачни добитак за мале поруке није био у криптографији него у копирању меморије.</li>
<li><strong>Диск без датотечног система тражи више посла, не мање.</strong> Оно што систем датотека иначе даје (поравнање, раст, discard, копије, провере) морамо написати и проверити сами, сваки корак са убризгавањем грешака.</li>
<li><strong>Искреност у бројкама.</strong> Меморијска мерења на дељеном VPS-у варирају до 10 пута међу покретањима и не говоре ништа о коду, па их не наводимо. Крипто бројке су стабилне до око 10%.</li>
<li><strong>Брзина развоја није зрелост.</strong> Тестови и ADR-ови расту уз код, али четири дана нису прави погон.</li>
</ul>
<h2 id="_3">Шта следи</h2>
<ul>
<li><strong>H6:</strong> SQLite изолован на нивоу EL0, тако да грешка у бази не отвара језгро.</li>
<li><strong>H7:</strong> остатак безбедности времена (NTS и обрада скокова); SNTP је тек први корак.</li>
<li><strong>X11:</strong> поређење са Linuxом и Mosquittom на истом VPS-у, тек тада има смисла говорити о релативној брзини.</li>
<li><strong>Издање:</strong> прва јавна објава за VPS (KVM), уз потписане артефакте и упутства.</li>
<li><strong>После издања:</strong> bare metal (X12 до X16), IOMMU и Secure Boot, те шифровање диска.</li>
</ul>
<p>Код је на <a href="https://github.com/zeljan-alduk/urMQTT">github.com/zeljan-alduk/urMQTT</a>, а <code>docs/PROGRESS.md</code> и даље је једини извор истине о томе шта стварно ради. Пред-алфа, као и раније; пријаве грешака и безбедносне пријаве по упутствима у <code>SECURITY.md</code> добродошле су.</p>]]></content:encoded>
      <category>rust</category>
      <category>mqtt</category>
      <category>iot</category>
      <category>unikernel</category>
      <category>open source</category>
    </item>
    <item>
      <title>urMQTT: градимо MQTT брокер који је уједно и оперативни систем</title>
      <link>https://aldo.tech/sr/blog/urmqtt-unikernel-mqtt-broker/</link>
      <guid isPermaLink="true">https://aldo.tech/sr/blog/urmqtt-unikernel-mqtt-broker/</guid>
      <pubDate>Thu, 29 Oct 2026 08:00:00 +0000</pubDate>
      <description>Развојне белешке о urMQTT-у, уникернелу написаном у Rustu који је сам по себи MQTT брокер: један ELF са сопственим TCP/IP-ом, TLS-ом 1.3, брокером и SQLite-ом, покренут са једног ISO-а.</description>
      <content:encoded><![CDATA[<p>Уређаји у возилима, на паркиралиштима и у ормарима са електроником имају једну заједничку особину: шаљу мале поруке, ретко, али их је много и стално су повезани. Годинама их повезујемо на MQTT брокере, и годинама нас мучи исто питање. Зашто за нешто тако једноставно треба цео Linux, OpenSSL, runtime неког језика, SSH, менаџер пакета и тек онда брокер? Сваки од тих слојева доноси сопствене рањивости, троши сопствену меморију и додаје још једну ствар која може да се поквари, а флота уређаја од сервера треба само троје: да држи много готово неактивних веза, да сваки уређај поуздано аутентификује и да поруке пренесе без губитка.</p>
<p>Тако је настао <a href="https://github.com/zeljan-alduk/urMQTT">urMQTT</a>: уникернел који <em>јесте</em> MQTT брокер. Језгро, TCP/IP, TLS 1.3, MQTT и уграђена база података у једном Rust бинарном програму који се покреће са једног ISO-а. Нема процеса, нема системских позива, нема љуске. Прва платформа на којој се гради и тестира је aarch64, а исти код иде ка x86_64, преко UEFI-ја и класичног BIOS-а, и ка правом хардверу: циљ је један ISO који се подиже на свему томе. Пројекат је отвореног кода под лиценцом MIT или Apache 2.0, и у овом тексту желимо да забележимо докле смо стигли, како је изнутра сложен и шта смо успут научили.</p>
<h2 id="_1">Шта тачно значи „уникернел”</h2>
<p>Уобичајени брокер је програм који ради на оперативном систему. urMQTT нема оперативни систем испод себе: сам се подиже кроз UEFI фирмвер и Limine bootloader, поставља сопствене табеле страница, програмира контролер прекида и тајмер, проналази virtio уређаје преко PCIe-а, добија адресу преко DHCP-а и слуша на једном порту. Све што би иначе радили језгро, управљачки програми, мрежни стек и апликација, овде је један ELF који ради на нивоу EL1.</p>
<p>Поређење са типичном инсталацијом говори само за себе:</p>
<table>
<thead>
<tr>
<th></th>
<th>Типичан брокер на Linuxу</th>
<th>urMQTT</th>
</tr>
</thead>
<tbody>
<tr>
<td>Изложени сервиси</td>
<td>SSH, брокер, често и порт за метрике или администрацију</td>
<td><strong>само TCP 8883</strong> (MQTT преко TLS-а 1.3 са mTLS-ом)</td>
</tr>
<tr>
<td>Код у слици</td>
<td>језгро, userland, libc, OpenSSL, runtime</td>
<td><strong>један Rust ELF</strong> (плус вендорисани SQLite)</td>
</tr>
<tr>
<td>Удаљено управљање</td>
<td>љуска, конфигурационе датотеке, ажурирања пакета</td>
<td><strong>ништа.</strong> Нова верзија је нови ISO</td>
</tr>
<tr>
<td>Меморија по неактивној вези</td>
<td>килобајти (међуспремници сокета, TLS објекти, heap)</td>
<td><strong>циљ ≤ 512 B</strong></td>
</tr>
<tr>
<td>Вишејезгарни модел</td>
<td>нити и браве или једна петља догађаја</td>
<td><strong>нит по језгру, без дељеног стања, обрада до краја</strong></td>
</tr>
<tr>
<td>Интегритет кода</td>
<td>могућа су пресликавања која су и уписива и извршна</td>
<td><strong>W^X, закључане табеле страница, PAC/BTI</strong></td>
</tr>
</tbody>
</table>
<p>Приоритети су, тим редом: безбедност, скалабилност, вишејезгарне перформансе, мали отисак и непроменљива инсталација. Кад год су два циља у сукобу, побеђује онај који је виши на тој листи.</p>
<h2 id="_2">Где смо сада</h2>
<p>Пројекат је у пред-алфа фази, али је затворено седамнаест развојних прекретница, од M0 до M17. Данас urMQTT послужује MQTT 3.1.1 преко TLS-а 1.3 са обавезним клијентским сертификатима, а стање чува у SQLite-у на сировом диску. Укратко, шта већ ради:</p>
<ul>
<li><strong>Језгро:</strong> сопствени вектори изузетака са декодираним извештајима, сопствене табеле страница са спроведеним W^X-ом, заштитне странице, PAC и BTI где их процесор има, GICv3 и GICv2, генерички тајмер, SMP до 64 језгра.</li>
<li><strong>Откривање хардвера</strong> из DTB-а или ACPI-ја у један опис платформе, са идентичним резултатом из оба извора.</li>
<li><strong>Управљачки програми:</strong> PCIe ECAM, MSI-X, virtio-net, virtio-blk, virtio-scsi и virtio-rng, те CSPRNG засејан хардверском ентропијом.</li>
<li><strong>Мрежа:</strong> сопствени DHCPv4, ARP, ICMP и строги IPv4, па сопствени TCP само за серверску страну: SYN колачићи, хијерархијски точак тајмера, NewReno, спајање ACK-ова, без TIME_WAIT стања.</li>
<li><strong>TLS:</strong> само TLS 1.3, обавезни mTLS, идентитет уређаја из CN-а или SAN URI-ја; руковање ради rustls, а након њега сопствени слој записа са AES-128-GCM-ом и ChaCha20-Poly1305. Наставак сесије PSK улазницама, листа опозива, контрола пријема већ код руковања.</li>
<li><strong>MQTT:</strong> кодек без копирања, фазиран и тестиран са правим mosquitto клијентима; QoS 0 и 1, џокери, задржане поруке, опорука (LWT), трајне сесије, ACL са ограничењем на <code>devices/{id}/#</code>, ограничења брзине по уређају.</li>
<li><strong>Похрана:</strong> SQLite унутар језгра преко VFS-а над сировим диском, у WAL режиму, са провером интегритета при подизању; регистар уређаја, опозиви, задржане поруке, трајне сесије са редовима порука за уређаје који су offline; по језгру засебан телеметријски дневник са CRC-ом по запису и опоравком након пада.</li>
<li><strong>Погон и надзор:</strong> надзорни тајмер за обрађиваче, здравствена провера, ограничено логовање, syslog и statsd преко UDP-а, пријем који преживљава олује поновних повезивања.</li>
<li><strong>Испорука:</strong> битовно идентичне поновне изградње проверене у CI-ју, ISO и сирова GPT слика диска, minisign потписи, упутства за KVM, Proxmox и Hetzner.</li>
</ul>
<p>Сваки корак је означен прекретницом која га је донела, а датотека <code>docs/PROGRESS.md</code> у репозиторијуму једини је извор истине о томе шта стварно ради.</p>
<h2 id="_3">Архитектура у три слике</h2>
<p>Цео дизајн описан је у <code>docs/ARCHITECTURE.md</code>, а разлози за сваку важнију одлуку записани су као ADR-ови у <code>docs/DECISIONS.md</code>. Овде је преглед.</p>
<h3 id="_4">Слојеви</h3>
<figure><img loading="lazy" alt="Слојеви urMQTT-а: од брокера преко MQTT кодека, TLS-а, TCP-а и мреже до virtio управљачких програма и архитектурног слоја, све у једном ELF-у на нивоу EL1." src="https://aldo.tech/blog/urmqtt/img/layers.svg" /><figcaption>Слојеви urMQTT-а: од брокера преко MQTT кодека, TLS-а, TCP-а и мреже до virtio управљачких програма и архитектурног слоја, све у једном ELF-у на нивоу EL1.</figcaption></figure>
<p>Од врха ка дну: брокер (сесије, стабло тема, QoS, задржане поруке, опоруке, ACL, ограничења), MQTT кодек, TLS, TCP, мрежни слој, па управљачки програми. Поред тог главног пута стоји похрана (SQLite и телеметријски дневник), а испод свега управљање меморијом, откривање платформе и конфигурација која је у време изградње преведена у константе, заједно са уграђеним CA сертификатом, серверским сертификатом и кључем. Хипервизор са virtio уређајима једино је што је изван слике.</p>
<h3 id="_5">Нит по језгру, без дељеног стања</h3>
<figure><img loading="lazy" alt="Модел извршавања: свако податковно језгро има своју петљу и свој део брокера, а највише језгро покреће само SQLite. Језгра комуницирају искључиво SPSC прстеновима." src="https://aldo.tech/blog/urmqtt/img/cores.svg" /><figcaption>Модел извршавања: свако податковно језгро има своју петљу и свој део брокера, а највише језгро покреће само SQLite. Језгра комуницирају искључиво SPSC прстеновима.</figcaption></figure>
<p>Свако податковно језгро врти сопствену петљу са сопственим TCP стеком, TLS стањима, делом брокера (сесије и стабло тема), кеш меморијом heapа и партицијом телеметрије. Језгра не деле никакво променљиво стање, само ограничене прстенове без брава, по један за сваки уређени пар језгара. Највише језгро покреће SQLite и ништа друго.</p>
<p>Мрежна картица данас има један пар редова који држи језгро 0. Оно сваки MQTT ток према Toeplitz хешу његове четворке усмерава на једно од радних језгара и шаље оно што радна језгра врате. Сесија живи на свом домаћем језгру, одређеном хешом идентификатора клијента, па преузимање, опоруке и трајност никад не прелазе језгра. Веза која слети на друго језгро задржава тамо свој TCP и TLS, а обичан MQTT прослеђује домаћем језгру уз контролу тока заснованом на кредитима. Свака усмерена порука разгранава се и на остале делове брокера, од којих сваки држи реплику задржаних порука.</p>
<p>Три правила држе тај модел на окупу. <strong>Обрада до краја:</strong> примљени пакет обрађује се у целости на једном језгру, од TCP-а преко дешифровања TLS-а на месту, парсирања MQTT-а и усмеравања до шифрованог одговора, без редова задатака и без копирања у апликационе међуспремнике. <strong>Прилагодљиво испитивање:</strong> под оптерећењем се редови за пријем активно испитују, а у мировању се језгро враћа на прекиде и <code>WFI</code>; обрађивачи прекида само потврде прекид и означе посао. <strong>Надзорни тајмер:</strong> откуцај од 10 ms по језгру хвата обрађиваче који премаше свој буџет.</p>
<h3 id="publish-">Живот једног PUBLISH-а</h3>
<figure><img loading="lazy" alt="Пут једне поруке PUBLISH са QoS 1: од TLS записа на мрежној картици преко дешифровања, провере ACL-а и усмеравања до PUBACK-а спојеног са TCP ACK-ом у једном сегменту." src="https://aldo.tech/blog/urmqtt/img/publish.svg" /><figcaption>Пут једне поруке PUBLISH са QoS 1: од TLS записа на мрежној картици преко дешифровања, провере ACL-а и усмеравања до PUBACK-а спојеног са TCP ACK-ом у једном сегменту.</figcaption></figure>
<p>Уређај пошаље TLS запис. Мрежна картица га преда петљи језгра дескриптором, без копирања. Петља провери TCP, дешифрује TLS на месту и парсира MQTT из позајмљеног међуспремника, затим у меморији провери ACL, ограничење брзине и пут кроз стабло тема. Ако неко друго језгро занима та тема, добије поруку за усмеравање са референцом на садржај и идентификатором теме; база добије телеметријски запис или ажурирање задржане поруке, у пакету. Одговор PUBACK шифрује се директно у одлазни међуспремник и спаја са TCP ACK-ом, па уређају одлази један сегмент. Друго језгро порука кошта једно шифровање по претплатнику у тренутку слања.</p>
<h2 id="_6">Колико кошта једна веза</h2>
<p>Циљ је <strong>највише 512 бајтова по неактивној TLS вези</strong>, са тежњом ка 256. Док веза мирује, ништа се за њу не држи у међуспремнику: примљени подаци обрађују се директно из мрежног оквира, а међуспремници за слање, записе и MQTT постоје само док је нешто недовршено или непотврђено. Језгро то и само мери: тачни бројачи heapа у траженим бајтовима и у блоковима које заузимају, записани сваких десет секунди као heap изнад основног нивоа по отвореној вези.</p>
<table>
<thead>
<tr>
<th>Део</th>
<th style="text-align: right;">Величина</th>
<th>Како</th>
</tr>
</thead>
<tbody>
<tr>
<td>TCP контролни блок</td>
<td style="text-align: right;">96 B</td>
<td>запаковане заставице, 16-битни RTT/RTO, утор у точку тајмера, без међуспремника у мировању</td>
</tr>
<tr>
<td>Утор у TCP табели</td>
<td style="text-align: right;">4–8 B</td>
<td>само индекс везе, табела највише напола пуна</td>
</tr>
<tr>
<td>Веза и стање TLS 1.3 записа</td>
<td style="text-align: right;">152 B</td>
<td>тајна и секвенца по смеру (40 B); кључ и IV изводе се по запису</td>
</tr>
<tr>
<td>MQTT веза</td>
<td style="text-align: right;">48 B</td>
<td>идентитет и опорука на heapу само док постоје</td>
</tr>
<tr>
<td>MQTT сесија, врућ облик</td>
<td style="text-align: right;">64 B</td>
<td>једна претплата у реду, интернирани идентификатори чворова</td>
</tr>
<tr>
<td>Претплата <code>devices/&lt;id&gt;/cmd/#</code></td>
<td style="text-align: right;">0 чворова стабла</td>
<td>похрањена релативно према идентификатору сесије, заједничка свим уређајима</td>
</tr>
</tbody>
</table>
<p>Измерено под HVF-ом са четири виртуелна језгра, од тога три радна, при чему сваки уређај држи mTLS везу и једну претплату са QoS 1, укључујући заокруживање алокатора:</p>
<table>
<thead>
<tr>
<th style="text-align: right;">Везе</th>
<th style="text-align: right;">Бајтова по вези</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>Базен мрежних оквира фиксан је око 2 MiB, што је отприлике 200 B по вези при десет хиљада веза и око 2 B при милион. Више од тога нисмо могли да измеримо: QEMU-ово корисничко умрежавање на macOS-у престало је да прихвата везе на 10 197, отприлике на граници од 10 240 дескриптора QEMU процеса. Бројка пада како се фиксни делови распоређују на више веза, па не расте са њиховим бројем.</p>
<h2 id="_7">Безбедносни модел</h2>
<p>Безбедност је први приоритет и добија све сукобе са осталим циљевима. Модел има неколико слојева:</p>
<ul>
<li><strong>Један порт.</strong> TCP 8883, само TLS 1.3, обавезни клијентски сертификати потписани CA-ом уграђеним у слику. Идентификатор MQTT клијента мора бити једнак идентификатору уређаја из сертификата.</li>
<li><strong>Нема улаза.</strong> Нема љуске, конзолног уноса, SSH-а, администрацијског API-ја, динамичког кода, SQL-а доступног мрежом ни датотечног система.</li>
<li><strong>Меморијска безбедност.</strong> Rust, са <code>forbid(unsafe_code)</code> у протоколним пакетима; сваки <code>unsafe</code> блок има коментар <code>// SAFETY:</code> који проверава lint, а на спољни унос нема панике: clippy у пакетима на податковном путу забрањује <code>unwrap</code>, <code>expect</code>, индексирање и <code>panic!</code>.</li>
<li><strong>Ублажавање искоришћавања.</strong> W^X спроводи се кодирањем дескриптора и битом <code>SCTLR_EL1.WXN</code> и ревидира при сваком подизању. Табеле страница након подизања су само за читање, без уписивог алијаса. Доња половина адресног простора је искључена. Сваки стек има заштитне странице, а изузеци раде на засебном стеку. PAC и BTI укључују се кад их процесор подржава, и то проверавају димни тестови.</li>
<li><strong>Отпорност на злоупотребу.</strong> SYN колачићи; пријем одбија нове везе, пре икакве криптографије, кад је heap низак, кад база заостаје, кад је превише руковања у току по језгру или по изворној адреси или кад је брзина руковања превисока. Ограничења брзине по уређају, ограничени редови, TCP прозор везан уз противпритисак брокера, ограничено логовање, надзорни тајмер и здравствена провера.</li>
<li><strong>Непроменљивост.</strong> Конфигурација, CA и серверски кључ уграђени су у слику. Промена било чега значи нову слику.</li>
</ul>
<p>Модел претњи записан је у <code>docs/SECURITY.md</code>.</p>
<h2 id="_8">Похрана без датотечног система</h2>
<p>Подаци живе на засебном диску, virtio-blk или virtio-scsi, на којем нема датотечног система. Распоред је минималан и сопствен:</p>
<pre><code>LBA 0      [ суперблок: магија · верзија распореда · uuid · табела региона · CRC-32C ]
LBA 8, 16  [ табела величина датотека, утори A/B (најновији важећи побеђује) ]
LBA 64     [ SQLite главна база ½ ][ SQLite WAL ⅛ ][ дневник 1 MiB ][ телеметријски дневник: 8 партиција … ]
задња 4 KiB [ простор за самопроверу блок-уређаја при подизању ]
</code></pre>
<p>SQLite (вендорисани 3.54.1, са провером сажетка при изградњи) преведен је у језгро clangом, без стандардне библиотеке, и користи фиксни <code>memsys5</code> heap. Његов VFS пресликава три фиксне датотеке на регионе диска. Ради у WAL режиму са <code>locking_mode=EXCLUSIVE</code>, па му не треба индекс у дељеној меморији, и са <code>synchronous=FULL</code>; ако <code>quick_check</code> при подизању нађе грешку, систем се безбедно зауставља. У бази су регистар уређаја, опозиви, задржане поруке и трајне сесије са редовима QoS 1 порука за уређаје који нису повезани.</p>
<p>Језгро за базу, оно са највишим бројем, покреће SQLite и ништа друго. Податковна раван шаље му промене стања преко SPSC прстена без брава; оно их скупља у једну трансакцију сваких 5 ms или сваких 512 операција и потврде враћа другим прстеном. Празан диск форматира се при првом подизању; све друго се никад не форматира преко.</p>
<p>Телеметрија иде другим путем. Сваки прихваћени PUBLISH на темама из <code>storage.telemetry_topics</code> одлази у партицију телеметријског дневника свог језгра: прстен сегмената са CRC-ом по запису, посољеним секвенцом сегмента, тако да се стари пролази прстена никад не појаве поново. Језгра предају записе бази сопственим прстеном, без копирања, а она их исписује сваких 5 ms или сваких 256 KiB. При подизању опоравак задржава сваки исписани запис, одсеца недовршени реп и наставља у новом сегменту. Задржавање је по величини, кроз прстен, и по старости, кроз <code>retention_days</code>. Проверили смо то са милион записа и са <code>kill -9</code> усред писања.</p>
<h2 id="_9">Бројке</h2>
<p>Алат за оптерећење у <code>tools/loadgen</code> ради три ствари: олују TLS 1.3 руковања са mTLS-ом и CONNECT-ом, дуготрајне везе са PINGREQ-ом које сваке секунде објављују телеметрију, и олује поновног повезивања. <code>make bench</code> на Apple M4 Pro под HVF-ом даје:</p>
<table>
<thead>
<tr>
<th>Радна језгра</th>
<th>руковања/s у госту</th>
<th>скалирање</th>
<th>кроз QEMU корисничку мрежу (веза/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>Бројка у госту, где су клијент и сервер унутар исте виртуелне машине и свако радно језгро ради само за себе, показује да податковна раван скалира линеарно. Мрежна бројка ограничена је једнонитним корисничким умрежавањем QEMU-а, не гостом; и то је записано као ADR-032.</p>
<p>Олује поновног повезивања тестирамо тако да се сви уређаји повежу одједном, понављају одбијене покушаје са одгодом и онда сви падну без DISCONNECT-а. Параметар <code>heap_limit_mb=</code> у развојном режиму натера пријем да heap сматра мањим него што јесте, па се олуја већа од heapа може испробати са мало сокета. У једном таквом кругу од 3 000 уређаја повезало се 1 368 за 36 секунди, а 5 619 покушаја било је уредно одбијено, уз ограничење од 8 MiB; меморија се након олује вратила.</p>
<h2 id="_10">Како радимо</h2>
<p>Пројекат води писана спецификација, <code>PROMPT.md</code>, са планом прекретница, и свака прекретница завршава истим ритуалом: <code>make check</code> и <code>make smoke</code> зелени, један commit са ознаком <code>M&lt;n&gt;</code>, запис у <code>docs/PROGRESS.md</code> и ADR за сваку одлуку која није очигледна. Правила су кратка и не преговарамо о њима: нема панике на спољни унос, <code>unsafe</code> само у хардверским и FFI пакетима и увек са коментаром, нема алокација на врућем путу, коментар уз сваки редослед атомских операција, свако ограничење долази из конфигурације, а намерни привремени делови падају гласно.</p>
<p>CI ради више него што бисмо ручно стигли: fmt, clippy за host и за <code>aarch64-unknown-none</code>, typos, actionlint и shellcheck; тестове на Linuxу и macOS-у на arm64; изградњу језгра за развојну и производну конфигурацију са величином ELF-а у сажетку; подизање под QEMU-ом са свим сценаријима димног теста и чување серијских записа; cargo-deny за савете, лиценце и изворе; минут фазинга по свакој од једанаест мета, а ноћу десет минута по мети са кешираним корпусом. Уз то CodeQL, OpenSSF Scorecard, Dependabot и објава са SLSA пореклом. Свака туђа акција закачена је на пуни commit SHA.</p>
<p>Репродуцибилност схватамо дословно: <code>make repro</code> изгради HEAD двапут у свежим радним стаблима и упореди ELF, ISO и сирову слику бајт по бајт. Објаве су потписане minisignом, а артефакти се могу проверити са <code>gh attestation verify</code>. Производна слика садржи приватни кључ сервера, па је сам ISO тајна; слике уз објаве на GitHubу развојне су, са привременим кључевима, и служе само за испробавање.</p>
<h2 id="_11">Шта смо научили</h2>
<p>Неколико лекција које су промениле план:</p>
<ul>
<li><strong>Сопствени TCP ипак се исплатио.</strong> Прототип је радио на smoltcp-у и добро нам је послужио до M13. Али сервер који држи стотине хиљада веза не треба клијентски TCP, треба SYN колачиће, точак тајмера и стање од 96 бајтова. У M14 смо smoltcp заменили сопственим стеком само за серверску страну, са тестовима симулираног губитка и фазингом сегмената.</li>
<li><strong>Мерите меморију изнутра.</strong> Процене по вези биле су бескорисне док језгро није само почело да броји heap у блоковима алокатора, а не у траженим бајтовима. Тек тада смо видели где 512 бајтова стварно одлази и спустили се на 494.</li>
<li><strong>Алат за тестирање је део производа.</strong> Највеће ограничење није било језгро, него QEMU-ово корисничко умрежавање и број ефемерних портова на развојном рачунару. За сто хиљада и више веза треба <code>vmnet-shared</code> на macOS-у или Linux са KVM-ом, tap интерфејсом и више клијентских адреса.</li>
<li><strong>SQLite без оперативног система је могућ</strong>, али само уз строге услове: фиксни heap, ексклузивно закључавање без дељене меморије, VFS који не зна за датотеке, и пријем који бази не дозвољава да заостане.</li>
<li><strong>Олује поновног повезивања су нормално стање</strong>, не изузетак. Кад падне мобилна мрежа и врати се, хиљаде уређаја куца истовремено. Зато пријем одбија везу пре икакве криптографије, а свака одбијена веза кошта готово ништа.</li>
</ul>
<h2 id="_12">Шта следи</h2>
<p>Рад после M17 планиран је у три смера, а редослед међу њима записан је у <code>docs/PLAN.md</code>. Ништа од овога још није изграђено.</p>
<ul>
<li><strong>Један ISO за више платформи.</strong> Иста датотека <code>urmqtt-&lt;env&gt;-&lt;verzija&gt;.iso</code> и једна сирова GPT слика које се подижу на aarch64 UEFI-ју, x86_64 UEFI-ју и x86_64 BIOS-у, у виртуелним машинама и на правом хардверу, почев од једног Intel PC-а. И даље уређај са свиме унутра, репродуцибилан и потписан.</li>
<li><strong>Сирови диск, без датотечног система.</strong> Спецификација формата на нивоу бајта, host алат <code>urmqtt-disk</code> који дели парсере са језгром, поравнање региона на 1 MiB, раст диска са подацима, discard за танко додељене дискове, A/B снимци базе, провера битова и, по избору, шифровање AES-256-XTS са хардверским AES-ом.</li>
<li><strong>Доказиво безбедно.</strong> Опозив и листа дозвољених уређаја за време рада преко потписаног контролног канала, без нове слике; диференцијално и конформацијско тестирање сопственог TLS-а и TCP-а; KASLR, заштита стека, kCFI, MTE и ојачани алокатор; SQLite изолован на нивоу EL0; Secure Boot; SBOM и cargo-vet; и на крају пакет за спољну ревизију.</li>
</ul>
<p>Платформе које нуде само не-virtio уређаје, попут AWS ENA-е или Azure MANA-е, засад нису подржане. Raspberry Pi 4 и 5 као KVM домаћин, Hetzner CAX и Oracle Ampere A1 документовани су и подржани кодом, али још нису испробани на правој услузи.</p>
<h2 id="_13">Испробајте</h2>
<p>Репозиторијум је на <a href="https://github.com/zeljan-alduk/urMQTT">github.com/zeljan-alduk/urMQTT</a>. Уз Rust са прикаченим nightly ланцем алата и QEMU, <code>make certs</code>, <code>make run</code> и један mosquitto клијент довољни су за први PUBLISH преко mTLS-а на сопственом рачунару. Упозорење са почетка вреди и на крају: ово је пред-алфа, и све што ради пише у <code>docs/PROGRESS.md</code>. Пријаве грешака, питања о дизајну и безбедносне пријаве по упутствима у <code>SECURITY.md</code> су добродошли. О свакој следећој прекретници јавићемо се овде.</p>]]></content:encoded>
      <category>rust</category>
      <category>mqtt</category>
      <category>iot</category>
      <category>unikernel</category>
      <category>open source</category>
    </item>
    <item>
      <title>Покрећемо блог: белешке с терена</title>
      <link>https://aldo.tech/sr/blog/pokrecemo-blog/</link>
      <guid isPermaLink="true">https://aldo.tech/sr/blog/pokrecemo-blog/</guid>
      <pubDate>Sat, 17 Oct 2026 08:00:00 +0000</pubDate>
      <description>Зашто фирма која прави терминале, апликације и електронику за јавни превоз почиње да пише, и о чему ћете овде читати.</description>
      <content:encoded><![CDATA[<p>Већину онога што радимо нико не види. Софтвер у терминалу на улазу у аутобус, фирмвер у уређају који никад не добије екран, сервер који ноћу претвара дневне трансакције у извештаје. Кад све ради, ради тихо. Кад не ради, примете сви.</p>
<p>На овом блогу желимо да запишемо оно што смо на том путу научили. Не маркетиншке текстове, него конкретне белешке: шта је пошло наопако, како смо то решили и шта бисмо данас урадили другачије.</p>
<h2 id="_1">О чему ћемо писати</h2>
<ul>
<li><strong>Наплата у јавном превозу.</strong> Бесконтактне картице, SAM модули, рад без мреже и све што терминал мора да поднесе у возилу које се тресе дванаест сати дневно.</li>
<li><strong>Мобилне апликације на терену.</strong> Android и Flutter на уређајима који нису обични телефони: са читачима картица, штампачима и батеријама које морају да издрже целу смену.</li>
<li><strong>Бекенд и надзор флоте.</strong> Како знати да уређај у возилу има проблем пре него што га пријави возач.</li>
<li><strong>Хардвер.</strong> Од прототипа на столу до плочице која иде у серијску производњу, и све грешке између.</li>
<li><strong>Локални AI модели.</strong> Где модели на сопственом хардверу имају смисла, а где су облак и API једноставно бољи избор.</li>
</ul>
<h2 id="_2">Како ћемо писати</h2>
<p>Кратко и конкретно, с кодом и бројкама где их има. Објављиваћемо кад имамо шта да кажемо, а не по распореду. Текстове пишемо на хрватском и преводимо их на све језике сајта, а новости можете пратити и преко <a href="https://aldo.tech/sr/blog/feed.xml">RSS-а</a>.</p>
<p>Ако вас занима нека тема или радите на сличном проблему, јавите нам се на <a href="mailto:info@aldo.tech">info@aldo.tech</a>.</p>]]></content:encoded>
      <category>увод</category>
    </item>
  </channel>
</rss>
