Contexte et motivations
Firezone utilisait initialement boringtun comme implémentation userspace de WireGuard. Après plus de deux ans de déploiement, l’équipe a accumulé plus de 70 pull‑requests : correctifs d’anomalies, ajouts d’API et adaptations aux exigences produit. La nécessité de conserver une compatibilité totale avec l’API upstream devenait difficile, notamment à cause de la gestion du temps et du buffering interne introduits dans boringtun.
Architecture sans‑IO de sanstun
sanstun repose sur le principe sans‑IO : toutes les entrées‑sorties, y compris le temps et la génération d’aléa, sont injectées par l’appelant. Cette séparation élimine les effets de bord, facilite la composition de composants et rend les tests unitaires déterministes. Le modèle expose des fonctions qui reçoivent explicitement un Instant (suffixe _at) pour chaque opération dépendante du temps, évitant ainsi les appels implicites à std::time::now. L’absence de buffering interne supprime la contrainte d’appeler des fonctions dans un ordre particulier, ce qui correspond à la philosophie sans‑IO.
API principale et gestion des paquets
Le point d’entrée public est Interface::receive, qui accepte un datagramme brut, le parse, applique les limites de débit et renvoie un ReceivedMessage contenant une variante d’énumération correspondant aux quatre types de messages WireGuard : handshake initiation, handshake response, cookie reply et encrypted data. Chaque variante est ensuite routée vers le Tunnel concerné via les méthodes suivantes :
Tunnel::handle_handshake_initiation(...)
Tunnel::handle_handshake_response(...)
Tunnel::handle_cookie_reply(...)
Tunnel::begin_decapsulate & Tunnel::commit_decapsulate
Tunnel::begin_encapsulate
Tunnel::handle_timeout & Tunnel::poll_timeout
Tunnel::poll_event
Pour envoyer un paquet, l’application invoque Tunnel::begin_encapsulate. Si aucune session n’existe, le code appelant doit d’abord créer une session en lançant Tunnel::initiate_handshake. Le design évite toute allocation interne superflue et laisse le contrôle du buffering au développeur.
Sécurité et garanties de type
WireGuard repose sur un échange Diffie‑Hellman (DH) modérément coûteux, suivi d’un mécanisme de cookies qui oblige le pair distant à prouver la possession de son adresse IP source. sanstun implémente ce mécanisme en générant automatiquement les réponses de cookie et en appliquant les limites de débit au niveau de l’interface, ce qui empêche les attaques de type amplification par saturation de DH. Grâce aux types Rust, les constructeurs de structures critiques comme HandshakeInitiation sont privés au crate ; seules les fonctions du tunnel peuvent créer ou manipuler ces messages, garantissant que les invariants de protocole (par exemple, l’application du rate‑limiter) sont vérifiés à la compilation. Ainsi, des erreurs telles que l’envoi d’un paquet sans session ou le routage vers le mauvais tunnel sont éliminées avant l’exécution.