*Ça marchait avant* : comment une mise à jour de l’OS a cassé mes sockets Rust

Ça marchait avant : comment une mise à jour de l’OS a cassé mes sockets Rust

Table des matières

Je n’ai changé aucune ligne de code. J’ai simplement mis à jour mon système d’exploitation (OS), et soudain mon outil Rust a cessé de fonctionner.

L’erreur était brutale : Error: Address family not supported by protocol (os error 97).

Je construisais un test distribué simple — un serveur bind sur 0.0.0.0 et un client qui se connecte à localhost. Rien de plus standard.

Mais après la mise à jour de l’OS, le client refusait même de démarrer le handshake. La connexion n’expirait pas. Elle échouait instantanément.

J’ai passé du temps à debugger l’OS et le réseau — avant de réaliser que le problème ne venait pas de là.
C’était les données que mon OS envoyait à mon code.

Le piège de “Localhost”

Voici la réalité : localhost est un nom, pas une IP. On a passé des décennies à penser que localhost == 127.0.0.1.

Mais les OS modernes (Windows 11, macOS récents, et la plupart des distributions Linux) ainsi que les stacks réseau privilégient désormais souvent IPv6.

Dans la plupart des cas, l’OS renvoie à la fois des adresses IPv6 et IPv4 — simplement ordonnées par préférence.

Avant la mise à jour, IPv4 était en premier :

1$ getent ahosts localhost
2127.0.0.1       STREAM localhost
3127.0.0.1       DGRAM  
4127.0.0.1       RAW    
5::1             STREAM
6::1             DGRAM  
7::1             RAW    

Après la mise à jour, IPv6 passe en premier :

1$ getent ahosts localhost
2::1             STREAM localhost
3::1             DGRAM  
4::1             RAW    
5127.0.0.1       STREAM 
6127.0.0.1       DGRAM  
7127.0.0.1       RAW    

Le bug vient du fait de prendre aveuglément le premier résultat :

1let addr = "localhost:8080".to_socket_addrs()?.next().unwrap();
2let stream = TcpStream::connect(addr)?; 

En Rust, on résout généralement les adresses via le trait ToSocketAddrs. Le next().unwrap() est le coupable : il prend juste la première valeur renvoyée par l’OS.

Quand mon client demandait à résoudre localhost, le nouvel OS renvoyait une liste commençant par ::1 (IPv6) au lieu de l’adresse loopback IPv4 que j’attendais.

Il s’est avéré que IPv6 était désactivé sur mon système — ce qui a provoqué l’erreur Address family not supported by protocol (os error 97).

J’ai donc réactivé IPv6 et réessayé. Cette fois, une autre erreur est apparue : Error: Connection refused (os error 111).

Encore plus déroutant.

Un “connection refused” signifie généralement que rien n’écoute sur ce port. Mais j’avais bien un serveur en cours d’exécution — bind sur 0.0.0.0.

La réponse est simple : un client IPv6 ne peut pas se connecter à un serveur IPv4 uniquement.

Cela s’explique par le fait que l’OS maintient des tables de sockets IPv4 et IPv6 complètement séparées.

Quand mon serveur s’est bind sur une adresse IPv4, il s’est enregistré dans la table IPv4. Mais mon client tentait de se connecter en IPv6, donc l’OS cherchait dans la table IPv6. Il n’y a rien trouvé — puisque les deux tables ne se recouvrent pas — et a renvoyé l’erreur.

En interne, tout repose sur la manière dont l’OS identifie les connexions via un 5-tuple (IP source, port source, IP destination, port destination, protocole). Si vous voulez creuser ce modèle, je le détaille ici : Binds and Connections

La solution : arrêter de faire confiance à l’ordre DNS

Pour rendre le code robuste, j’ai dû arrêter de faire confiance à l’ordre des adresses renvoyées par l’OS. Il fallait explicitement sélectionner la famille supportée par mon serveur.

1// Ne prenez pas le premier résultat : choisissez celui qui correspond à votre famille de socket
2let addr = "localhost:8080"
3    .to_socket_addrs()?
4    .find(|a| a.is_ipv4())
5    .expect("No IPv4 address found for localhost");
6
7let stream = TcpStream::connect(addr)?;

Cette solution suppose que votre serveur ne supporte que IPv4. Si votre serveur supporte IPv6, vous devriez plutôt essayer toutes les adresses :

1let addrs: Vec<_> = "localhost:8080"
2    .to_socket_addrs()?
3    .collect();
4
5let stream = TcpStream::connect(addrs.as_slice())?;

Pourquoi Rust ne gère pas ça automatiquement ?

Certains runtimes (comme Go, ou certaines librairies réseau dans d’autres langages) implémentent des stratégies comme “Happy Eyeballs” (RFC 8305).

Ces runtimes résolvent le hostname (localhost) puis tentent de se connecter en IPv4 et IPv6 en parallèle, et utilisent celui qui répond en premier.

En Rust, vous avez un contrôle total — ce qui signifie que vous devez gérer vous-même ce genre de mismatch.

Une meilleure solution côté serveur

Si vous écrivez le serveur, évitez de vous binder sur 0.0.0.0. Cela vous limite à IPv4.

À la place, bindez sur l’adresse IPv6 “any” ::. Sur beaucoup de systèmes, cela crée un socket dual-stack capable d’accepter IPv6 et IPv4 (les adresses IPv4 sont mappées en IPv6, par exemple ::ffff:127.0.0.1).

Cependant, ce comportement dépend de l’OS. Certains systèmes exigent de désactiver explicitement l’option IPV6_V6ONLY pour accepter IPv4.

Ce n’est pas universel — certains systèmes utilisent par défaut des sockets IPv6 uniquement. Plus de détails ici : Binds and Connections

À retenir

  • localhost n’est pas une adresse — c’est une résolution.
  • L’OS peut renvoyer plusieurs adresses — l’ordre compte.
  • .next().unwrap() n’est pas anodin — il encode une hypothèse.
  • IPv4 et IPv6 sont deux mondes séparés, sauf si vous les reliez explicitement.

Si votre code a “soudainement cassé” après une mise à jour de l’OS, il y a de fortes chances que : rien n’ait changé dans votre code — ce sont vos hypothèses qui ont été exposées.

Ce n’était pas un bug réseau.

C’était une mauvaise hypothèse sur la façon dont l’OS résout et route les adresses.

Articles Connexes

Au-delà de 127.0.0.1 : vous possédez 16 millions d'adresses loopback

Au-delà de 127.0.0.1 : vous possédez 16 millions d’adresses loopback

De nombreux développeurs utilisent localhost et 127.0.0.1 de manière interchangeable, en pensant que le loopback se limite à une seule adresse. …

Lire la suite
Règles de Codage de Rust et Ressources Pédagogiques

Règles de Codage de Rust et Ressources Pédagogiques

Quel style de codage dois-je adopter pour mon code Rust afin d’assurer la cohérence avec mes bibliothèques Rust préférées ? Où puis-je apprendre …

Lire la suite
Mise en Place et Utilisation de Rust Hors Ligne pour un Développement Sans Faille : Un Tutoriel Étape par Étape

Mise en Place et Utilisation de Rust Hors Ligne pour un Développement Sans Faille : Un Tutoriel Étape par Étape

[Dernière mise à jour: 23 août 2025]

C’est un processus simple de mettre en place Rust lorsque vous avez accès à Internet, mais que se passe-t-il si vous êtes hors ligne? Rust est …

Lire la suite