- Chez Firezone, Rust est utilisé pour construire un accès distant sûr et scalable sur des téléphones Android, des ordinateurs MacOS ou des serveurs Linux
- Une bibliothèque de connexion appelée
connlibest utilisée pour gérer les connexions réseau et les tunnels WireGuard - Après plusieurs itérations, l’équipe est arrivée à une conception appelée sans-IO, qui offre des tests rapides et approfondis, une personnalisation poussée et une grande fiabilité
connlib est écrit en Rust et suit une conception sans-IO
- Grâce à la vitesse de Rust et à sa sûreté mémoire, il convient bien à la construction de services réseau
- Utilise notamment le runtime
tokio, les WebSocketstungstenite, l’implémentation WireGuardboringtunet le chiffrement du trafic API avecrustls - La conception sans-IO implémente les protocoles comme des machines à états pures, au lieu d’envoyer et de recevoir des octets via des sockets à plusieurs endroits
Le modèle asynchrone de Rust et le débat sur la « coloration des fonctions »
- Une fonction asynchrone ne peut être appelée que depuis une autre fonction asynchrone
- Si une fonction située profondément dans la pile est asynchrone, toutes les fonctions qui l’appellent doivent elles aussi devenir asynchrones
- Cela peut poser problème à ceux qui veulent écrire du code indépendant du caractère asynchrone ou non de ses dépendances
Présentation de sans-IO
- L’idée centrale de sans-IO ressemble au principe d’inversion des dépendances dans le monde de l’OOP
- La politique (ce qu’il faut faire) ne doit pas dépendre des détails d’implémentation (comment le faire)
- Au lieu de transmettre des données avec la structure
Transmit, on émet unTransmit
Application de l’inversion des dépendances
- Au lieu de transmettre des données avec la structure
Transmit, on émet unTransmit - La boucle d’événements implémente les effets de bord et appelle réellement
UdpSocket::send
Machine à états
- Le diagramme de machine à états d’une requête de binding STUN comporte deux états :
SentetReceived - La machine à états est implémentée en définissant la structure
StunBindinget les fonctions associées
Boucle d’événements
- La boucle d’événements pilote la machine à états et traite les données avec
poll_transmitethandle_input
Abstraction du temps
- Les exigences liées au temps sont traitées via les API
poll_timeoutethandle_timeout
Les prémisses de sans-IO
- La conception sans-IO laisse à l’application la décision de savoir si les dépendances doivent être asynchrones ou non
- La conception sans-IO se compose facilement, fournit une API flexible, se teste aisément et s’accorde bien avec les capacités de Rust
Composition facile
- L’API de
StunBindingpeut s’appliquer à la plupart des protocoles réseau - La bibliothèque
snownetde Firezone combine ICE et WireGuard pour fournir un tunnel IP « magique » qui fonctionne quelle que soit la configuration réseau
API flexible
- Écrire soi-même la boucle d’événements permet d’affiner le code et d’en faciliter la maintenance
Tests rapides
- Le code sans-IO n’a pas d’effets de bord, ce qui le rend très facile à tester
- Chez Firezone, une machine à états de référence est implémentée pour comparer son état à l’état réel de
connlibdans les tests
Cas limites et échecs d’IO
- La conception sans-IO sépare l’implémentation du protocole des effets de bord d’IO réels, ce qui facilite le traitement des cas limites et des erreurs
Rust + sans-IO : un duo parfait ?
- Rust modélise explicitement la propriété et la mutabilité, ce qui s’accorde bien avec la conception sans-IO
- La conception sans-IO utilise librement
&mutpour exprimer les changements d’état et, contrairement à Rustasync, n’emploie que des API synchrones
Inconvénients
- Écrire soi-même la boucle d’événements peut introduire des bugs subtils
- Les workflows séquentiels peuvent demander davantage de code
- Dans la communauté Rust, la conception sans-IO n’est pas encore largement adoptée
Conclusion
- Le code sans-IO peut sembler déroutant au début, mais devient très agréable une fois qu’on s’y habitue
- Rust fournit d’excellents outils pour modéliser des machines à états
- La conception sans-IO impose d’intégrer la gestion des erreurs au traitement des entrées, ce qui donne l’impression d’une bonne manière d’écrire du code réseau
L’avis de GN⁺
- La conception sans-IO s’accorde bien avec le modèle de propriété de Rust et convient donc très bien à l’implémentation de protocoles réseau
- Écrire soi-même la boucle d’événements améliore la flexibilité du code et sa maintenabilité
- La facilité de test aide beaucoup à écrire du code fiable
- En revanche, comme cette approche n’est pas largement utilisée dans la communauté Rust, les bibliothèques associées peuvent manquer
- Lors de l’adoption d’une nouvelle technique, il faut tenir compte de la courbe d’apprentissage et du support de la communauté
1 commentaires
Avis Hacker News
Avant l’introduction de la syntaxe async/await de Rust, il fallait implémenter manuellement des machines à états
En écrivant une bibliothèque VT100, il a réalisé le problème posé par les patterns d’encapsulation en Rust
Comparaison avec une conception qui transmet les données via des channels
Il existe dans l’écosystème Haskell une idée consistant à séparer la logique de l’exécution
tokio::select!a été encapsuléLes fonctions async de Rust sont compilées en machines à états
En exposant l’état, une fonction async peut devenir « pure »
Firezone est un outil remarquable
Ce serait bien que le compilateur puisse convertir automatiquement le code async en sans I/O
Après avoir lu l’article et les commentaires, cela donne l’impression de réinventer le style architectural hexagonal ou ports/adapters
Il se demande si le trafic réel passe par la gateway, ou si elle est utilisée uniquement pour l’établissement de la connexion