- Bei Firezone wird Rust verwendet, um skalierbaren und sicheren Remote-Zugriff auf Android-Telefonen, MacOS-Computern oder Linux-Servern aufzubauen
- Dafür wird eine Verbindungsbibliothek namens
connlibgenutzt, die Netzwerkverbindungen und WireGuard-Tunnel verwaltet - Nach mehreren Iterationen gelangte man zu einem Design namens sans-IO, das schnelle und gründliche Tests, tiefe Anpassbarkeit und hohe Zuverlässigkeit bietet
connlib ist in Rust geschrieben und folgt einem sans-IO-Design
- Dank der Geschwindigkeit und Speichersicherheit von Rust eignet es sich gut für den Aufbau von Netzwerkdiensten
- Verwendet werden unter anderem die
tokio-Runtime,tungsteniteWebSockets, dieboringtun-WireGuard-Implementierung undrustlszur Verschlüsselung des API-Traffics - Das sans-IO-Design implementiert Protokolle als reine Zustandsmaschine, anstatt an mehreren Stellen Bytes über Sockets zu senden und zu empfangen
Rusts asynchrones Modell und die Debatte um "function coloring"
- Asynchrone Funktionen können nur von anderen asynchronen Funktionen aufgerufen werden
- Befindet sich eine Funktion tief innerhalb asynchronen Codes, müssen auch alle aufrufenden Funktionen asynchron werden
- Das kann für Menschen zum Problem werden, die Code schreiben möchten, der gegenüber der Frage gleichgültig ist, ob Abhängigkeiten asynchron sind oder nicht
Einführung in sans-IO
- Die Grundidee von sans-IO ähnelt dem Prinzip der Umkehrung von Abhängigkeiten aus der OOP-Welt
- Die Richtlinie (was getan werden soll) sollte nicht von Implementierungsdetails (wie es getan wird) abhängen
- Anstatt Daten mithilfe der Struktur
Transmitzu senden, gibt der CodeTransmit-Werte aus
Anwendung der Abhängigkeitsumkehr
- Anstatt Daten mithilfe der Struktur
Transmitzu senden, gibt der CodeTransmit-Werte aus - Die Event-Loop implementiert die Seiteneffekte und ruft tatsächlich
UdpSocket::sendauf
Zustandsmaschine
- Das Zustandsdiagramm für eine STUN-Binding-Request besitzt zwei Zustände:
SentundReceived - Die Zustandsmaschine wird durch Definition der Struktur
StunBindingund der zugehörigen Funktionen implementiert
Event-Loop
- Die Event-Loop treibt die Zustandsmaschine an und verarbeitet Daten mit
poll_transmitundhandle_input
Zeitabstraktion
- Zeitbasierte Anforderungen werden mit den APIs
poll_timeoutundhandle_timeoutverarbeitet
Die Prämissen von sans-IO
- Das sans-IO-Design verlagert die Entscheidung, ob Abhängigkeiten asynchron sein sollen, in die Anwendung
- Das sans-IO-Design ist leicht kombinierbar, bietet eine flexible API, ist gut testbar und passt gut zu den Eigenschaften von Rust
Einfache Kombination
- Die API von
StunBindinglässt sich auf die meisten Netzwerkprotokolle anwenden - Firezones Bibliothek
snownetkombiniert ICE und WireGuard, um einen "magischen" IP-Tunnel bereitzustellen, der unabhängig von der Netzwerkkonfiguration funktioniert
Flexible API
- Wer die Event-Loop selbst schreibt, kann den Code feiner abstimmen und leichter warten
Schnelle Tests
- sans-IO-Code hat keine Seiteneffekte und ist daher sehr leicht zu testen
- Bei Firezone wird eine Referenz-Zustandsmaschine implementiert, um Tests durchzuführen, die den tatsächlichen Zustand von
connlibvergleichen
Edge Cases und IO-Fehler
- Das sans-IO-Design trennt die Protokollimplementierung von tatsächlichen IO-Seiteneffekten und erleichtert dadurch die Behandlung von Edge Cases und Fehlern
Rust + sans-IO: ein perfektes Paar?
- Rust modelliert Ownership und Veränderlichkeit explizit und passt daher gut zu einem sans-IO-Design
- Das sans-IO-Design verwendet
&mutfrei, um Zustandsänderungen auszudrücken, und nutzt im Gegensatz zuasyncRust nur synchrone APIs
Nachteile
- Wer die Event-Loop selbst schreibt, kann subtile Bugs einbauen
- Sequentielle Workflows können mehr Code erfordern
- In der Rust-Community ist das sans-IO-Design noch nicht weit verbreitet
Fazit
- sans-IO-Code wirkt anfangs ungewohnt, macht aber viel Freude, sobald man sich daran gewöhnt hat
- Rust bietet hervorragende Werkzeuge, um Zustandsmaschinen zu modellieren
- Das sans-IO-Design erzwingt, dass Fehlerbehandlung Teil der Eingabeverarbeitung ist, und wirkt dadurch wie die richtige Art, Netzwerkcode zu schreiben
Meinung von GN⁺
- Das sans-IO-Design passt gut zu Rusts Ownership-Modell und eignet sich daher sehr gut für die Implementierung von Netzwerkprotokollen
- Wer die Event-Loop selbst schreibt, verbessert die Flexibilität des Codes und erleichtert die Wartung
- Die gute Testbarkeit hilft enorm dabei, stabilen Code zu schreiben
- Da das Muster in der Rust-Community jedoch nicht weit verbreitet ist, könnte es an entsprechenden Bibliotheken fehlen
- Bei der Einführung neuer Techniken sollten Lernkurve und Community-Support berücksichtigt werden
1 Kommentare
Hacker-News-Kommentare
Vor der Einführung der
async/await-Syntax in Rust wurden Zustandsautomaten manuell implementiertasync/await-Syntax in Rust hat sich die Produktivität deutlich verbessertasyncwird in einen automatischen Zustandsautomaten umgewandelt und speichert Werte an I/O-PunktenBeim Schreiben einer VT100-Bibliothek wurde ein Problem mit Rusts Kapselungsmustern erkannt
Im Vergleich zu einem Design, das Daten über Channels überträgt
Im Haskell-Ökosystem gibt es die Idee, Logik und Ausführung zu trennen
tokio::select!gekapselt wurdeRust-
async-Funktionen werden zu Zustandsautomaten kompiliertasynczu kombinierenPinWenn der Zustand offengelegt wird, können
async-Funktionen „rein“ werdenFirezone ist ein erstaunliches Tool
Es wäre gut, wenn der Compiler
async-Code automatisch in Sans-IO umwandeln könnteNach dem Lesen des Artikels und der Kommentare wirkt es so, als würde der Stil der hexagonalen Architektur bzw. Ports/Adapters neu erfunden
Es stellt sich die Frage, ob echter Traffic durch das Gateway läuft oder ob es nur für den Verbindungsaufbau verwendet wird