
Ça marchait avant : comment une mise à jour de l’OS a cassé mes sockets Rust
- 23 février 2026
- 6 mins de lecture
- Systèmes d’exploitation , Programmation rust , Débogage et troubleshooting
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.
Bulletin d'information
Abonnez-vous à notre bulletin d'information et restez informé(e).
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
Bulletin d'information
Abonnez-vous à notre bulletin d'information et restez informé(e).
À retenir
localhostn’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.


