urMQTT: budujemy broker MQTT, który jest jednocześnie systemem operacyjnym
Notatki z prac nad urMQTT, unikernelem napisanym w Rust, który sam jest brokerem MQTT: jeden plik ELF z własnym stosem TCP/IP, TLS 1.3, brokerem i SQLite, uruchamiany z jednego obrazu ISO.
Urządzenia w pojazdach, na parkingach i w szafach z elektroniką mają jedną wspólną cechę: wysyłają niewielkie komunikaty, rzadko, ale jest ich bardzo dużo i są stale połączone. Od lat podłączamy je do brokerów MQTT i od lat dręczy nas to samo pytanie. Dlaczego do czegoś tak prostego potrzebujemy całego Linuksa, OpenSSL, środowiska uruchomieniowego jakiegoś języka, SSH, menedżera pakietów i dopiero na końcu brokera? Każda z tych warstw wnosi własne podatności, zużywa własną pamięć i dokłada kolejną rzecz, która może się zepsuć, a flota urządzeń oczekuje od serwera tylko trzech rzeczy: utrzymywania wielu niemal bezczynnych połączeń, niezawodnego uwierzytelniania każdego urządzenia i przekazywania komunikatów bez strat.
Tak powstał urMQTT: unikernel, który jest brokerem MQTT. Jądro, TCP/IP, TLS 1.3, MQTT i wbudowana baza danych w jednym programie binarnym napisanym w Rust, uruchamianym z jednego obrazu ISO. Bez procesów, bez wywołań systemowych, bez powłoki. Pierwszą platformą, na której powstaje i jest testowany, jest aarch64, a ten sam kod zmierza w stronę x86_64, przez UEFI i klasyczny BIOS, oraz prawdziwego sprzętu: celem jest jeden ISO, który uruchamia się na tym wszystkim. Projekt jest open source na licencji MIT lub Apache 2.0, a w tym tekście chcemy zapisać, gdzie jesteśmy, jak jest zbudowany od środka i czego przy okazji się nauczyliśmy.
Co dokładnie oznacza „unikernel"
Typowy broker to program działający na systemie operacyjnym. urMQTT nie ma pod sobą żadnego systemu operacyjnego: sam startuje przez firmware UEFI i bootloader Limine, ustawia własne tablice stron, programuje kontroler przerwań i timer, odnajduje urządzenia virtio przez PCIe, uzyskuje adres przez DHCP i nasłuchuje na jednym porcie. Wszystko, co normalnie robiłyby jądro, sterowniki, stos sieciowy i aplikacja, jest tutaj jednym plikiem ELF działającym na poziomie EL1.
Porównanie z typową instalacją mówi samo za siebie:
| Typowy broker na Linuksie | urMQTT | |
|---|---|---|
| Wystawione usługi | SSH, broker, często także port metryk lub administracji | tylko TCP 8883 (MQTT przez TLS 1.3 z mTLS) |
| Kod w obrazie | jądro, userland, libc, OpenSSL, środowisko uruchomieniowe | jeden plik ELF w Rust (plus dołączony SQLite) |
| Zdalne zarządzanie | powłoka, pliki konfiguracyjne, aktualizacje pakietów | brak. Nowa wersja to nowy ISO |
| Pamięć na bezczynne połączenie | kilobajty (bufory gniazd, obiekty TLS, sterta) | cel ≤ 512 B |
| Model wielordzeniowy | wątki i blokady albo jedna pętla zdarzeń | wątek na rdzeń, bez współdzielonego stanu, obsługa do końca |
| Integralność kodu | możliwe są odwzorowania jednocześnie zapisywalne i wykonywalne | W^X, zablokowane tablice stron, PAC/BTI |
Priorytety są następujące, w tej kolejności: bezpieczeństwo, skalowalność, wydajność wielordzeniowa, mały ślad i niezmienna instalacja. Gdy dwa cele wchodzą w konflikt, wygrywa ten, który jest wyżej na liście.
Gdzie jesteśmy teraz
Projekt jest w fazie pre-alfa, ale zamknęliśmy siedemnaście kamieni milowych, od M0 do M17. Dziś urMQTT obsługuje MQTT 3.1.1 przez TLS 1.3 z obowiązkowymi certyfikatami klienta, a stan przechowuje w SQLite na surowym dysku. W skrócie, co już działa:
- Jądro: własne wektory wyjątków z zdekodowanymi raportami, własne tablice stron z egzekwowanym W^X, strony ochronne, PAC i BTI tam, gdzie procesor je ma, GICv3 i GICv2, timer ogólnego przeznaczenia, SMP do 64 rdzeni.
- Wykrywanie sprzętu z DTB lub ACPI do jednego opisu platformy, z identycznym wynikiem z obu źródeł.
- Sterowniki: PCIe ECAM, MSI-X, virtio-net, virtio-blk, virtio-scsi i virtio-rng oraz CSPRNG zasilany sprzętową entropią.
- Sieć: własne DHCPv4, ARP, ICMP i rygorystyczne IPv4, a do tego własny TCP wyłącznie po stronie serwera: ciasteczka SYN, hierarchiczne koło timerów, NewReno, łączenie ACK, bez stanu TIME_WAIT.
- TLS: wyłącznie TLS 1.3, obowiązkowe mTLS, tożsamość urządzenia z CN lub SAN URI; uzgadnianie obsługuje rustls, a po nim działa własna warstwa rekordów z AES-128-GCM i ChaCha20-Poly1305. Wznawianie sesji biletami PSK, lista unieważnień, kontrola przyjęć już na etapie uzgadniania.
- MQTT: kodek bez kopiowania, fuzzowany i testowany z prawdziwymi klientami mosquitto; QoS 0 i 1, symbole wieloznaczne, wiadomości zatrzymane, testament (LWT), trwałe sesje, ACL ograniczone do
devices/{id}/#, limity szybkości na urządzenie. - Przechowywanie: SQLite wewnątrz jądra przez VFS nad surowym dyskiem, w trybie WAL, ze sprawdzeniem integralności przy starcie; rejestr urządzeń, unieważnienia, wiadomości zatrzymane, trwałe sesje z kolejkami wiadomości dla urządzeń offline; osobny dziennik telemetrii na każdy rdzeń z CRC na zapis i odzyskiwaniem po awarii.
- Eksploatacja i nadzór: timer nadzorczy dla procedur obsługi, kontrola stanu, ograniczone logowanie, syslog i statsd przez UDP, kontrola przyjęć, która przetrwa burze ponownych połączeń.
- Dostawa: bitowo identyczne ponowne kompilacje weryfikowane w CI, ISO i surowy obraz dysku GPT, podpisy minisign, instrukcje dla KVM, Proxmox i Hetzner.
Każdy krok jest oznaczony kamieniem milowym, który go wprowadził, a plik docs/PROGRESS.md w repozytorium jest jedynym źródłem prawdy o tym, co naprawdę działa.
Architektura w trzech obrazach
Cały projekt jest opisany w docs/ARCHITECTURE.md, a powody każdej ważniejszej decyzji zapisane są jako ADR w docs/DECISIONS.md. Poniżej przegląd.
Warstwy
Od góry do dołu: broker (sesje, drzewo tematów, QoS, wiadomości zatrzymane, testamenty, ACL, limity), kodek MQTT, TLS, TCP, warstwa sieciowa, a następnie sterowniki. Obok tej głównej ścieżki znajduje się przechowywanie (SQLite i dziennik telemetrii), a pod spodem zarządzanie pamięcią, wykrywanie platformy i konfiguracja zamieniona w czasie kompilacji na stałe, razem z wbudowanym certyfikatem CA, certyfikatem serwera i kluczem. Jedyne, co leży poza obrazem, to hipernadzorca z urządzeniami virtio.
Wątek na rdzeń, bez współdzielonego stanu
Każdy rdzeń danych prowadzi własną pętlę z własnym stosem TCP, stanami TLS, fragmentem brokera (sesje i drzewo tematów), pamięcią podręczną sterty i partycją telemetrii. Rdzenie nie współdzielą żadnego zmiennego stanu, a jedynie ograniczone pierścienie bez blokad, po jednym dla każdej uporządkowanej pary rdzeni. Najwyższy rdzeń uruchamia SQLite i nic więcej.
Karta sieciowa ma dziś jedną parę kolejek, którą obsługuje rdzeń 0. Kieruje on każdy strumień MQTT, według skrótu Toeplitz jego czwórki, na jeden z rdzeni roboczych i wysyła to, co rdzenie robocze zwrócą. Sesja żyje na swoim rdzeniu macierzystym, wyznaczonym skrótem identyfikatora klienta, więc przejmowanie, testamenty i trwałość nigdy nie przekraczają granic rdzeni. Połączenie, które trafi na inny rdzeń, zachowuje tam swój TCP i TLS, a zwykły MQTT przekazuje rdzeniowi macierzystemu z kontrolą przepływu opartą na kredytach. Każda skierowana wiadomość jest też rozsyłana do pozostałych fragmentów brokera, z których każdy utrzymuje replikę wiadomości zatrzymanych.
Ten model spinają trzy zasady. Obsługa do końca: odebrany pakiet jest przetwarzany w całości na jednym rdzeniu, od TCP przez deszyfrowanie TLS w miejscu, parsowanie MQTT i routing aż do zaszyfrowanej odpowiedzi, bez kolejek zadań i bez kopiowania do buforów aplikacji. Adaptacyjne odpytywanie: pod obciążeniem kolejki odbiorcze są aktywnie odpytywane, a w bezczynności rdzeń wraca do przerwań i WFI; procedury obsługi przerwań jedynie potwierdzają przerwanie i oznaczają pracę. Timer nadzorczy: takt 10 ms na rdzeń wyłapuje procedury obsługi, które przekraczają swój budżet.
Życie jednego PUBLISH
Urządzenie wysyła rekord TLS. Karta sieciowa przekazuje go pętli rdzenia deskryptorem, bez kopiowania. Pętla sprawdza TCP, deszyfruje TLS w miejscu i parsuje MQTT z pożyczonego bufora, po czym w pamięci sprawdza ACL, limit szybkości i ścieżkę przez drzewo tematów. Jeśli temat interesuje inny rdzeń, ten otrzymuje komunikat routingu z odwołaniem do ładunku i identyfikatorem tematu; baza dostaje wpis telemetrii lub aktualizację wiadomości zatrzymanej, w paczce. Odpowiedź PUBACK jest szyfrowana bezpośrednio do wychodzącego bufora i łączona z TCP ACK, więc do urządzenia wychodzi jeden segment. Inny rdzeń kosztuje wiadomość jedno szyfrowanie na subskrybenta w chwili wysyłki.
Ile kosztuje jedno połączenie
Celem jest najwyżej 512 bajtów na bezczynne połączenie TLS, z dążeniem do 256. Gdy połączenie spoczywa, nic dla niego nie jest trzymane w buforze: odebrane dane przetwarzane są bezpośrednio z ramki sieciowej, a bufory wysyłania, rekordów i MQTT istnieją tylko wtedy, gdy coś jest niedokończone lub niepotwierdzone. Jądro samo to mierzy: dokładne liczniki sterty w żądanych bajtach i w blokach, które faktycznie zajmuje, zapisywane co dziesięć sekund jako sterta ponad poziom bazowy na otwarte połączenie.
| Element | Rozmiar | Jak |
|---|---|---|
| Blok kontrolny TCP | 96 B | spakowane flagi, 16-bitowe RTT/RTO, miejsce w kole timerów, bez bufora w bezczynności |
| Miejsce w tablicy TCP | 4–8 B | tylko indeks połączenia, tablica najwyżej w połowie zapełniona |
| Połączenie i stan rekordów TLS 1.3 | 152 B | sekret i numer sekwencyjny na kierunek (40 B); klucz i IV wyprowadzane na rekord |
| Połączenie MQTT | 48 B | tożsamość i testament na stercie tylko wtedy, gdy istnieją |
| Sesja MQTT, forma gorąca | 64 B | jedna subskrypcja w wierszu, internowane identyfikatory węzłów |
Subskrypcja devices/<id>/cmd/# |
0 węzłów drzewa | przechowywana względem identyfikatora sesji, wspólna dla wszystkich urządzeń |
Zmierzone pod HVF z czterema rdzeniami wirtualnymi, z czego trzy robocze, przy czym każde urządzenie utrzymuje połączenie mTLS i jedną subskrypcję z QoS 1, wraz z zaokrągleniami alokatora:
| Połączenia | Bajtów na połączenie |
|---|---|
| 960 | 531 B |
| 9 922 | 494 B |
| 10 197 | 474 B |
Pula ramek sieciowych ma stały rozmiar około 2 MiB, co daje mniej więcej 200 B na połączenie przy dziesięciu tysiącach połączeń i około 2 B przy milionie. Więcej nie udało nam się zmierzyć: sieć użytkownika QEMU na macOS przestała przyjmować połączenia na 10 197, mniej więcej przy granicy 10 240 deskryptorów procesu QEMU. Wartość maleje, gdy stałe części rozkładają się na więcej połączeń, więc nie rośnie wraz z ich liczbą.
Model bezpieczeństwa
Bezpieczeństwo jest pierwszym priorytetem i wygrywa wszystkie konflikty z pozostałymi celami. Model ma kilka warstw:
- Jeden port. TCP 8883, wyłącznie TLS 1.3, obowiązkowe certyfikaty klienta podpisane przez CA wbudowane w obraz. Identyfikator klienta MQTT musi być równy identyfikatorowi urządzenia z certyfikatu.
- Brak wejść. Brak powłoki, wejścia z konsoli, SSH, API administracyjnego, kodu dynamicznego, SQL dostępnego z sieci i systemu plików.
- Bezpieczeństwo pamięci. Rust, z
forbid(unsafe_code)w pakietach protokołów; każdy blokunsafema komentarz// SAFETY:weryfikowany przez lint, a na dane zewnętrzne nie ma paniki: clippy w pakietach na ścieżce danych zakazujeunwrap,expect, indeksowania ipanic!. - Łagodzenie skutków exploitów. W^X jest egzekwowane przez kodowanie deskryptorów i bit
SCTLR_EL1.WXNoraz audytowane przy każdym starcie. Tablice stron po uruchomieniu są tylko do odczytu, bez zapisywalnego aliasu. Dolna połowa przestrzeni adresowej jest wyłączona. Każdy stos ma strony ochronne, a wyjątki działają na osobnym stosie. PAC i BTI włączają się, gdy procesor je obsługuje, co weryfikują testy dymne. - Odporność na nadużycia. Ciasteczka SYN; kontrola przyjęć odrzuca nowe połączenia, przed jakąkolwiek kryptografią, gdy sterta jest niska, gdy baza zalega, gdy zbyt wiele uzgodnień trwa na rdzeń lub na adres źródłowy albo gdy tempo uzgodnień jest zbyt wysokie. Limity szybkości na urządzenie, ograniczone kolejki, okno TCP powiązane z przeciwciśnieniem brokera, ograniczone logowanie, timer nadzorczy i kontrola stanu.
- Niezmienność. Konfiguracja, CA i klucz serwera są wbudowane w obraz. Zmiana czegokolwiek oznacza nowy obraz.
Model zagrożeń jest zapisany w docs/SECURITY.md.
Przechowywanie bez systemu plików
Dane żyją na osobnym dysku, virtio-blk lub virtio-scsi, na którym nie ma systemu plików. Układ jest minimalny i własny:
LBA 0 [ superblok: magia · wersja układu · uuid · tablica regionów · CRC-32C ]
LBA 8, 16 [ tablica rozmiarów plików, sloty A/B (wygrywa najnowszy poprawny) ]
LBA 64 [ główna baza SQLite ½ ][ SQLite WAL ⅛ ][ dziennik 1 MiB ][ dziennik telemetrii: 8 partycji … ]
ostatnie 4 KiB [ przestrzeń do autotestu urządzenia blokowego przy starcie ]
SQLite (dołączona wersja 3.54.1, ze sprawdzeniem skrótu podczas kompilacji) jest kompilowany do jądra za pomocą clang, bez biblioteki standardowej, i używa stałej sterty memsys5. Jego VFS odwzorowuje trzy stałe pliki na regiony dysku. Działa w trybie WAL z locking_mode=EXCLUSIVE, więc nie potrzebuje indeksu w pamięci współdzielonej, oraz z synchronous=FULL; jeśli quick_check przy starcie znajdzie błąd, system zatrzymuje się w bezpieczny sposób. W bazie znajdują się rejestr urządzeń, unieważnienia, wiadomości zatrzymane i trwałe sesje z kolejkami wiadomości QoS 1 dla urządzeń, które nie są połączone.
Rdzeń bazy, ten o najwyższym numerze, uruchamia SQLite i nic więcej. Płaszczyzna danych wysyła mu zmiany stanu przez pierścień SPSC bez blokad; on zbiera je w jedną transakcję co 5 ms lub co 512 operacji i zwraca potwierdzenia drugim pierścieniem. Pusty dysk jest formatowany przy pierwszym starcie; wszystkiego innego nigdy się nie nadpisuje formatowaniem.
Telemetria idzie inną drogą. Każdy przyjęty PUBLISH na tematach z storage.telemetry_topics trafia do partycji dziennika telemetrii swojego rdzenia: pierścienia segmentów z CRC na zapis, osolonego numerem sekwencyjnym segmentu, tak aby stare przebiegi pierścienia nigdy nie pojawiły się ponownie. Rdzenie przekazują zapisy bazie własnym pierścieniem, bez kopiowania, a ona zrzuca je co 5 ms lub co 256 KiB. Przy starcie odzyskiwanie zachowuje każdy zrzucony zapis, odcina niedokończony ogon i kontynuuje w nowym segmencie. Retencja działa według rozmiaru, przez pierścień, i według wieku, przez retention_days. Sprawdziliśmy to z milionem zapisów i z kill -9 w trakcie zapisu.
Liczby
Narzędzie do generowania obciążenia w tools/loadgen robi trzy rzeczy: burzę uzgodnień TLS 1.3 z mTLS i CONNECT, długotrwałe połączenia z PINGREQ, które co sekundę publikują telemetrię, oraz burze ponownych połączeń. make bench na Apple M4 Pro pod HVF daje:
| Rdzenie robocze | uzgodnień/s w gościu | skalowanie | przez sieć użytkownika QEMU (połączeń/s) |
|---|---|---|---|
| 1 | 862 | 1,00× | 789 |
| 2 | 1 724 | 2,00× | 636 |
| 4 | 3 438 | 3,99× | 536 |
Wartość w gościu, gdzie klient i serwer są wewnątrz tej samej maszyny wirtualnej, a każdy rdzeń roboczy pracuje sam dla siebie, pokazuje, że płaszczyzna danych skaluje się liniowo. Wartość sieciowa jest ograniczona jednowątkową siecią użytkownika QEMU, a nie gościem; zapisaliśmy to jako ADR-032.
Burze ponownych połączeń testujemy tak, że wszystkie urządzenia łączą się naraz, ponawiają odrzucone próby z opóźnieniem, a potem wszystkie padają bez DISCONNECT. Parametr heap_limit_mb= w trybie deweloperskim sprawia, że kontrola przyjęć uważa stertę za mniejszą, niż jest, więc burzę większą niż sterta można wypróbować przy niewielu gniazdach. W jednej takiej rundzie z 3 000 urządzeń połączyło się 1 368 w 36 sekund, a 5 619 prób zostało należycie odrzuconych, przy limicie 8 MiB; pamięć po burzy wróciła do poprzedniego stanu.
Jak pracujemy
Projekt prowadzi pisana specyfikacja, PROMPT.md, z planem kamieni milowych, a każdy kamień milowy kończy się tym samym rytuałem: make check i make smoke na zielono, jeden commit z oznaczeniem M<n>, wpis w docs/PROGRESS.md i ADR dla każdej decyzji, która nie jest oczywista. Zasady są krótkie i nie negocjujemy ich: brak paniki na dane zewnętrzne, unsafe tylko w pakietach sprzętowych i FFI oraz zawsze z komentarzem, brak alokacji na gorącej ścieżce, komentarz przy każdym porządku operacji atomowych, każde ograniczenie pochodzi z konfiguracji, a celowo tymczasowe fragmenty zawodzą głośno.
CI robi więcej, niż bylibyśmy w stanie ręcznie: fmt, clippy dla hosta i dla aarch64-unknown-none, typos, actionlint i shellcheck; testy na Linuksie i macOS na arm64; kompilację jądra dla konfiguracji deweloperskiej i produkcyjnej z rozmiarem ELF w podsumowaniu; start pod QEMU ze wszystkimi scenariuszami testu dymnego i zapisem dzienników szeregowych; cargo-deny dla zaleceń, licencji i źródeł; minutę fuzzingu na każdy z jedenastu celów, a nocą dziesięć minut na cel z korpusem w pamięci podręcznej. Do tego CodeQL, OpenSSF Scorecard, Dependabot i wydanie z pochodzeniem SLSA. Każda cudza akcja jest przypięta do pełnego SHA commita.
Powtarzalność rozumiemy dosłownie: make repro buduje HEAD dwukrotnie w świeżych drzewach roboczych i porównuje ELF, ISO i surowy obraz bajt po bajcie. Wydania są podpisane minisign, a artefakty można zweryfikować przez gh attestation verify. Obraz produkcyjny zawiera klucz prywatny serwera, więc sam ISO jest tajemnicą; obrazy dołączone do wydań na GitHubie są deweloperskie, z kluczami tymczasowymi, i służą wyłącznie do wypróbowania.
Czego się nauczyliśmy
Kilka lekcji, które zmieniły plan:
- Własny TCP jednak się opłacił. Prototyp działał na smoltcp i dobrze nam służył do M13. Ale serwer utrzymujący setki tysięcy połączeń nie potrzebuje klienckiego TCP, potrzebuje ciasteczek SYN, koła timerów i stanu o rozmiarze 96 bajtów. W M14 zastąpiliśmy smoltcp własnym stosem wyłącznie po stronie serwera, z testami symulowanych strat i fuzzingiem segmentów.
- Mierzcie pamięć od środka. Szacunki na połączenie były bezużyteczne, dopóki jądro samo nie zaczęło liczyć sterty w blokach alokatora, a nie w żądanych bajtach. Dopiero wtedy zobaczyliśmy, gdzie naprawdę idzie 512 bajtów, i zeszliśmy do 494.
- Narzędzie do testowania jest częścią produktu. Największym ograniczeniem nie było jądro, lecz sieć użytkownika QEMU i liczba portów efemerycznych na komputerze deweloperskim. Dla stu tysięcy połączeń i więcej potrzebny jest
vmnet-sharedna macOS albo Linux z KVM, interfejsem tap i wieloma adresami klientów. - SQLite bez systemu operacyjnego jest możliwy, ale tylko przy rygorystycznych warunkach: stała sterta, wyłączna blokada bez pamięci współdzielonej, VFS, który nie wie o plikach, oraz kontrola przyjęć, która nie pozwala bazie zalegać.
- Burze ponownych połączeń są normalnym stanem, a nie wyjątkiem. Gdy sieć komórkowa padnie i wróci, tysiące urządzeń pukają jednocześnie. Dlatego kontrola przyjęć odrzuca połączenie przed jakąkolwiek kryptografią, a każde odrzucone połączenie kosztuje prawie nic.
Co dalej
Prace po M17 zaplanowano w trzech kierunkach, a kolejność między nimi jest zapisana w docs/PLAN.md. Nic z tego nie zostało jeszcze zbudowane.
- Jeden ISO dla wielu platform. Ten sam plik
urmqtt-<env>-<wersja>.isoi jeden surowy obraz GPT, które startują na aarch64 UEFI, x86_64 UEFI i x86_64 BIOS, w maszynach wirtualnych i na prawdziwym sprzęcie, zaczynając od jednego komputera PC z Intelem. Nadal urządzenie ze wszystkim w środku, powtarzalne i podpisane. - Surowy dysk, bez systemu plików. Specyfikacja formatu na poziomie bajtów, narzędzie hosta
urmqtt-diskwspółdzielące parsery z jądrem, wyrównanie regionów do 1 MiB, rozrost dysku wraz z danymi, discard dla dysków z cienkim przydziałem, migawki A/B bazy, weryfikacja bitów i, opcjonalnie, szyfrowanie AES-256-XTS ze sprzętowym AES. - Udowodnione bezpieczeństwo. Unieważnianie i lista dozwolonych urządzeń w czasie pracy przez podpisany kanał kontrolny, bez nowego obrazu; testowanie różnicowe i zgodnościowe własnego TLS i TCP; KASLR, ochrona stosu, kCFI, MTE i wzmocniony alokator; SQLite wyizolowany na poziomie EL0; Secure Boot; SBOM i cargo-vet; a na końcu pakiet do audytu zewnętrznego.
Platformy, które oferują wyłącznie urządzenia inne niż virtio, takie jak AWS ENA czy Azure MANA, na razie nie są obsługiwane. Raspberry Pi 4 i 5 jako host KVM, Hetzner CAX i Oracle Ampere A1 są udokumentowane i obsługiwane w kodzie, ale nie zostały jeszcze wypróbowane w prawdziwej usłudze.
Wypróbuj
Repozytorium jest na github.com/zeljan-alduk/urMQTT. Z Rust z przypiętym łańcuchem narzędzi nightly i QEMU wystarczą make certs, make run i jeden klient mosquitto, aby wysłać pierwszy PUBLISH przez mTLS na własnym komputerze. Ostrzeżenie z początku obowiązuje także na końcu: to pre-alfa, a wszystko, co działa, jest opisane w docs/PROGRESS.md. Zgłoszenia błędów, pytania o projekt i zgłoszenia dotyczące bezpieczeństwa zgodnie z instrukcją w SECURITY.md są mile widziane. O każdym kolejnym kamieniu milowym damy znać tutaj.
Komentarze
Zasady komentowania