3 Punkte von GN⁺ 2024-07-05 | 1 Kommentare | Auf WhatsApp teilen
  • 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 connlib genutzt, 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, tungstenite WebSockets, die boringtun-WireGuard-Implementierung und rustls zur 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 Transmit zu senden, gibt der Code Transmit-Werte aus

Anwendung der Abhängigkeitsumkehr

  • Anstatt Daten mithilfe der Struktur Transmit zu senden, gibt der Code Transmit-Werte aus
  • Die Event-Loop implementiert die Seiteneffekte und ruft tatsächlich UdpSocket::send auf

Zustandsmaschine

  • Das Zustandsdiagramm für eine STUN-Binding-Request besitzt zwei Zustände: Sent und Received
  • Die Zustandsmaschine wird durch Definition der Struktur StunBinding und der zugehörigen Funktionen implementiert

Event-Loop

  • Die Event-Loop treibt die Zustandsmaschine an und verarbeitet Daten mit poll_transmit und handle_input

Zeitabstraktion

  • Zeitbasierte Anforderungen werden mit den APIs poll_timeout und handle_timeout verarbeitet

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 StunBinding lässt sich auf die meisten Netzwerkprotokolle anwenden
  • Firezones Bibliothek snownet kombiniert 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 connlib vergleichen

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 &mut frei, um Zustandsänderungen auszudrücken, und nutzt im Gegensatz zu async Rust 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

 
GN⁺ 2024-07-05
Hacker-News-Kommentare
  • Vor der Einführung der async/await-Syntax in Rust wurden Zustandsautomaten manuell implementiert

    • Dank der async/await-Syntax in Rust hat sich die Produktivität deutlich verbessert
    • Rust-async wird in einen automatischen Zustandsautomaten umgewandelt und speichert Werte an I/O-Punkten
  • Beim Schreiben einer VT100-Bibliothek wurde ein Problem mit Rusts Kapselungsmustern erkannt

    • Die Fixierung auf Kapselung verursacht Probleme
    • Es erinnert daran, dass Computer Maschinen sind, die Eingaben verarbeiten, Daten transformieren und Ausgaben erzeugen
  • Im Vergleich zu einem Design, das Daten über Channels überträgt

    • Der Code wird komplexer
    • Nachrichtentypen müssen manuell implementiert werden
    • Der Sender muss explizit bereitgestellt werden
    • Wenn die Netzwerkübertragung fehlschlägt, erhält man kein Ergebnis
    • Es gibt jedoch auch praktische Vorteile
  • Im Haskell-Ökosystem gibt es die Idee, Logik und Ausführung zu trennen

    • Es wird nicht erwähnt, wie der Aufruf von tokio::select! gekapselt wurde
    • Es bestand Interesse an der Implementierung gekapselter Funktionen im Sans-IO-Stil
  • Rust-async-Funktionen werden zu Zustandsautomaten kompiliert

    • Es wird gefragt, ob es Versuche gab, Sans-IO und async zu kombinieren
    • Die Hauptprobleme sind Benutzbarkeit und der Umgang mit Pin
  • Wenn der Zustand offengelegt wird, können async-Funktionen „rein“ werden

    • Es wurde versucht, OpenSSL an asynchrones Rust zu binden
  • Firezone ist ein erstaunliches Tool

    • Es wurde ein ähnliches Muster wie in rust-libp2p entdeckt
  • Es wäre gut, wenn der Compiler async-Code automatisch in Sans-IO umwandeln könnte

    • Eine manuelle Umwandlung ist fehleranfällig
  • Nach 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