Aller au contenu

ss : trier les problèmes de connexion sous Linux

Utilisez la commande ss pour voir ce qui écoute sur un serveur Linux, filtrer les connexions par état ou par port et lire les états TCP afin de savoir si la faute revient au réseau, à l'autre bout ou à votre propre application. Vous trouverez aussi l'équivalent ss des commandes netstat que vous connaissez déjà.

Les administrateurs Linux habitués à netstat peuvent se servir de ss pour voir chaque connexion ouverte et trouver pourquoi l’une d’elles échoue, et cet article vous montre comment le lire. Vous l’avez peut-être découvert à la dure, quand netstat -tlnp a répondu command not found. Les distributions récentes et les images de conteneurs laissent souvent netstat de côté, alors que ss est livré avec iproute2, le paquet réseau standard. Il demande les données de sockets directement au noyau, donc il reste rapide même sur un hôte chargé de milliers de connexions. Un socket, ici, c’est l’une des deux extrémités d’une connexion réseau, et chaque socket TCP passe par des états nommés pendant qu’il s’ouvre et se ferme. C’est la colonne d’état qui rend ss si utile. Une pile de sockets SYN-SENT (des tentatives de connexion qui attendent encore une réponse) pointe souvent vers le réseau ou un pare-feu. Une pile de CLOSE-WAIT (l’autre bout a raccroché, mais votre programme n’a jamais fermé le sien) pointe vers votre propre code. TIME-WAIT, un court délai que le noyau garde après une fermeture propre, est normal et demande rarement une correction.

Ce que vous allez accomplir

  • Remplacer chaque réflexe netstat par son équivalent ss, et comprendre pourquoi le changement a eu lieu.
  • Lister ce qui écoute, sur quelle adresse, sous quel PID — et attraper la fameuse erreur du « lié à 127.0.0.1 » avant qu’elle vous coûte une heure.
  • Filtrer les sockets par état TCP et par adresse ou port, au lieu de tout passer dans grep.
  • Lire une accumulation d’états et nommer le composant fautif : le réseau, le pair, ou votre propre code.
  • Lire correctement Recv-Q et Send-Q sur une ligne LISTEN, ce que la plupart des gens ne font pas.

Prérequis

  • Le binaire ss, du paquet iproute2. Il est installé d’office sur pratiquement toutes les distributions actuelles. S’il manque : apt install iproute2 sur Debian et Ubuntu, dnf install iproute sur Fedora et RHEL, apk add iproute2 sur Alpine.
  • Root, ou sudo, pour voir les noms de processus. Sans privilège, la liste des sockets s’affiche quand même au complet, mais la colonne users:(...) reste vide pour les sockets qui ne vous appartiennent pas. Ce ne sont pas les données qui manquent, ce sont vos permissions.
  • Les différences entre distributions, honnêtement : netstat vit dans net-tools, en pratique non maintenu en amont depuis des années. Debian et Ubuntu l’empaquettent encore mais ne l’installent plus dans les images minimales ou de conteneur ; RHEL 8 et plus l’ont retiré de l’installation par défaut ; bien des images de base ne l’ont jamais eu. ss, lui, sera là.
  • Note de version : ss suit la version d’iproute2 — ss -V affiche quelque chose comme ss utility, iproute2-6.1.0. Les colonnes sont stables depuis des années ; sur les noyaux avant 4.x, quelques champs de -i manquent, mais rien ici n’en dépend.

Comment ça marche

netstat ouvre /proc/net/tcp, /proc/net/tcp6, /proc/net/udp et compagnie, les lit comme du texte et analyse des adresses encodées en hexadécimal, ligne par ligne. Pour rattacher les noms de processus, il parcourt ensuite chaque répertoire /proc/<pid>/fd/ de la machine et fait un stat sur chaque descripteur pour faire correspondre les numéros d’inode aux sockets. Sur un portable au repos, c’est invisible. Sur un proxy occupé avec 200 000 connexions établies, c’est une charge mesurable — et pire, le fichier est régénéré pendant que vous le lisez, donc ce que vous obtenez est étalé dans le temps plutôt qu’un instantané propre.

ss ouvre une socket NETLINK_SOCK_DIAG et interroge directement la pile réseau, avec une requête qui peut porter un filtre. Le noyau renvoie des enregistrements binaires. Deux choses en découlent. C’est beaucoup plus rapide, parce qu’il n’y a pas de texte à analyser et que le filtrage se fait côté noyau. Et ça peut rapporter des données qui n’apparaissent nulle part dans /proc — fenêtre de congestion, RTT lissé, compteurs de retransmission, état des minuteries, mémoire des sockets. C’est ce qu’exposent -i, -o et -m, et c’est pourquoi ss répond à des questions que netstat ne pouvait structurellement pas traiter.

Le vrai gain, c’est la colonne d’état. Chaque socket TCP se trouve dans un état défini par la machine à états du protocole, et chaque état signifie qu’une chose précise s’est produite et qu’une chose précise est attendue. Quand un état s’accumule bien au-delà des autres, cet état nomme la panne.

Six fiches d'états de sockets TCP montrant ce que signifient LISTEN, SYN-SENT, ESTAB, FIN-WAIT, CLOSE-WAIT et TIME-WAIT, et si la faute revient à votre application, au pair ou au réseau

Lisez cette carte comme un tableau de responsabilités. SYN-SENT veut dire que les paquets ne se rendent pas — le problème du réseau. CLOSE-WAIT veut dire que le pair a raccroché et que votre processus n’a jamais appelé close() — le problème de votre code. TIME-WAIT veut dire que le noyau fait exactement ce que la RFC lui dicte — le problème de personne.

Étape par étape

1. Voir ce qui écoute, et à qui ça appartient. Cette seule commande remplace netstat -tlnp et c’est celle que vous taperez le plus :

sudo ss -tlnp
State   Recv-Q  Send-Q  Local Address:Port  Peer Address:Port  Process
LISTEN  0       511           0.0.0.0:80          0.0.0.0:*     users:(("nginx",pid=1041,fd=6),("nginx",pid=1040,fd=6))
LISTEN  0       4096        127.0.0.1:5432        0.0.0.0:*     users:(("postgres",pid=892,fd=5))
LISTEN  0       511              [::]:80             [::]:*     users:(("nginx",pid=1041,fd=7),("nginx",pid=1040,fd=7))
LISTEN  0       4096        127.0.0.1:6379        0.0.0.0:*     users:(("redis-server",pid=744,fd=6))

Les options se décomposent proprement : -t TCP seulement, -l seulement ce qui écoute, -n numérique (ne pas transformer :80 en :http), -p le processus propriétaire. Lisez la colonne Local Address attentivement — 0.0.0.0:80 accepte de partout, 127.0.0.1:5432 seulement depuis cette machine. Cette distinction est la cause la plus fréquente du « le service roule mais rien ne réussit à s’y connecter ».

2. Tout voir, pas juste ce qui écoute. Remplacez -l par -a :

sudo ss -tanp

-a veut dire à la fois ce qui écoute et ce qui n’écoute pas, donc vous obtenez les lignes LISTEN plus tous les ESTAB, TIME-WAIT et CLOSE-WAIT. Sur un hôte occupé, ça fait des milliers de lignes — d’où les deux étapes suivantes.

3. Prendre le pouls de la machine en une ligne. Avant de lire des sockets une par une, lisez les totaux :

ss -s
Total: 1243
TCP:   892 (estab 214, closed 601, orphaned 0, timewait 598)

Transport Total     IP        IPv6
RAW       1         0         1
UDP       12        8         4
TCP       291       270       21
INET      304       278       26
FRAG      0         0         0

C’est votre porte d’entrée du triage. 214 établies et 598 en timewait sur un hôte qui sert du trafic, c’est banal. 214 établies et 40 000 en close-wait, c’est un bogue — et vous le savez avant d’avoir lu une seule ligne de socket.

4. Filtrer par état plutôt que de greper. Le filtre d’état se fait côté noyau et il est exact :

ss -tn state established
ss -tn state time-wait
ss -tn state syn-sent
sudo ss -tnp state close-wait
Recv-Q  Send-Q  Local Address:Port   Peer Address:Port  Process
0       0       10.0.1.14:8080       10.0.1.7:51044     users:(("app",pid=2214,fd=57))
0       0       10.0.1.14:8080       10.0.1.7:51046     users:(("app",pid=2214,fd=58))

Remarquez ce qui a changé : avec un filtre state, il n’y a plus de colonne State, puisque ce serait la même valeur sur chaque ligne. Les noms acceptés sont les états TCP écrits en minuscules avec des traits d’union — established, syn-sent, syn-recv, fin-wait-1, fin-wait-2, time-wait, closed, close-wait, last-ack, listening, closing — plus les raccourcis de groupe all, connected (tout sauf listening et closed), synchronized (connected moins syn-sent) et bucket (les états de minisockets, soit time-wait et syn-recv).

Pour compter au lieu de lire, passez le tout dans wc -l (moins l’en-tête) :

ss -tn state close-wait | wc -l

5. Filtrer par adresse et par port. Les expressions de filtre vont entre apostrophes simples pour que le shell n’y touche pas :

ss -tn 'dport = :443'
ss -tn 'sport = :5432'
ss -tn 'src 10.0.0.0/8'
ss -tn state established '( dport = :5432 or dport = :6379 )'

dport est le port distant sur une connexion sortante, sport le port local. src et dst prennent un hôte ou un bloc CIDR. La forme avec or exige les parenthèses, et les espaces autour du = font partie de la syntaxe.

6. Regarder à l’intérieur de la connexion. Quand une connexion existe mais qu’elle a l’air malade, -i affiche la vision qu’en a le noyau :

sudo ss -tinp state established '( dport = :5432 )'
Recv-Q  Send-Q  Local Address:Port  Peer Address:Port  Process
0       0       10.0.1.14:44120     10.0.1.9:5432      users:(("app",pid=2214,fd=31))
	 cubic wscale:7,7 rto:204 rtt:0.412/0.121 ato:40 mss:1448 pmtu:1500
	 cwnd:10 bytes_sent:2841 bytes_acked:2842 bytes_received:19422
	 segs_out:41 segs_in:39 send 281Mbps lastsnd:12 lastrcv:12 lastack:12
	 pacing_rate 562Mbps delivery_rate 48.2Mbps retrans:0/0 rcv_space:14480

rtt:0.412/0.121, c’est le temps d’aller-retour lissé et sa variance, en millisecondes. cwnd, c’est la fenêtre de congestion en segments. retrans:0/0, ce sont les retransmissions courantes et totales — un deuxième chiffre qui monte veut dire que le chemin perd des paquets, une panne réseau déguisée en lenteur applicative. lastrcv, ce sont les millisecondes depuis la dernière réception sur la socket, ce qui identifie discrètement une connexion figée.

7. Le reste du coffre à outils. D’autres options à garder dans les doigts :

sudo ss -tonp state established    # -o ajoute timer:(keepalive,29sec,0)
sudo ss -tmnp state established    # -m ajoute skmem:(r0,rb131072,t0,tb87040,...)
sudo ss -tenp state established    # -e ajoute uid, inode, témoin de socket
sudo ss -xa                        # sockets de domaine Unix
ss -uan                            # sockets UDP

-o, c’est ce qui vous permet de distinguer « inactive mais en santé, le keepalive roule » de « coincée en retransmission » : le nom de la minuterie sera keepalive dans le premier cas, et on avec un compteur de retransmissions qui monte dans le second. -m montre l’usage des tampons de socket, ce qui confirme si un Recv-Q traduit une vraie pression mémoire ou juste un soubresaut.

Valider que ça fonctionne

Trois vérifications, dans l’ordre.

Confirmez que le port écoute sur l’adresse que vous croyez. N’acceptez pas le « c’est parti » du gestionnaire de services :

sudo ss -tlnp 'sport = :8080'

Regardez Local Address. 0.0.0.0:8080 ou *:8080 accepte les connexions distantes ; 127.0.0.1:8080 non. Si vous attendez du trafic distant et que vous voyez la boucle locale, arrêtez-vous ici — vous avez trouvé le bogue.

Confirmez que le PID est bien celui que vous pensez. Prenez le pid dans la colonne users:(...) et vérifiez-le :

ps -p 2214 -o pid,user,cmd

Si le propriétaire est une vieille instance qui n’est jamais sortie, voilà pourquoi votre service redémarré n’arrive pas à se lier.

Confirmez qu’une vraie connexion atteint ESTAB. Depuis une machine cliente, ouvrez une connexion et surveillez l’état côté serveur :

ss -tn state established '( sport = :8080 )'

Une ligne qui apparaît veut dire que la négociation s’est terminée et que l’application a accepté — une affirmation bien plus forte que « le port est ouvert ».

Dépannage

Aucun nom de processus dans la sortie. Vous avez lancé ss -tlnp et la colonne Process est vide. Vous n’êtes pas root. Le noyau ne dira pas à un utilisateur sans privilège quel processus détient une socket qui ne lui appartient pas. Relancez avec sudo. Il n’y a rien d’autre de brisé.

Service lié à 127.0.0.1, les connexions distantes figent. ss -tlnp affiche 127.0.0.1:8080. Le service roule et le port écoute, pourtant les clients distants obtiennent une expiration plutôt qu’un refus : le SYN arrive, ne correspond à aucune socket en écoute sur cette interface, et il est jeté. Corrigez ça dans la configuration de l’application, pas dans le pare-feu — liez à 0.0.0.0, ou à l’adresse d’interface que vous voulez servir — puis revérifiez avec ss -tlnp.

Accumulation de CLOSE-WAIT. Des milliers de lignes dans ss -tnp state close-wait, toutes sous le même PID. CLOSE-WAIT veut dire que le pair a envoyé un FIN et que le noyau attend que votre processus appelle close(). Il n’y a aucun délai d’expiration — le noyau attend indéfiniment, en gardant un descripteur de fichier tout ce temps-là. C’est un bogue dans votre code : un chemin qui retourne sans fermer, un defer conn.Close() oublié, un pool qui ne récupère jamais. Le redémarrage qui « règle » ça vous achète des heures, pas une solution. Si vous voyez aussi accept: too many open files, la fuite a atteint votre plafond de descripteurs — voyez trop de fichiers ouverts et ulimit.

Accumulation de SYN-SENT. Des lignes dans ss -tn state syn-sent qui restent plantées là pendant des secondes. Votre hôte a envoyé un SYN et rien n’est revenu. Voici la distinction qui compte : une connexion refusée renvoie un RST instantané et ne traîne jamais en SYN-SENT. Une socket coincée ici veut dire le silence — un pare-feu qui jette au lieu de refuser, un groupe de sécurité sans règle correspondante, une route vers nulle part, ou un pair qui n’existe plus. Déboguez le chemin, pas l’application. Si la destination venait d’un nom d’hôte, confirmez d’abord que vous avez résolu vers l’adresse attendue — dig pour le dépannage DNS couvre cette moitié-là.

Panique du TIME-WAIT. Quelqu’un lance ss -s, voit timewait 24000, et ouvre un incident. Presque toujours : tout va bien. TIME-WAIT, c’est le côté qui a fermé en premier qui garde le quadruplet une soixantaine de secondes pour que des paquets en retard ne soient pas pris pour une nouvelle connexion. C’est de la rigueur, pas une fuite — chaque entrée coûte un peu de mémoire noyau et aucun descripteur de fichier. Ça ne devient un vrai problème que si vous épuisez la plage de ports éphémères locaux vers une seule destination. Vérifiez sysctl net.ipv4.ip_local_port_range avant d’y croire. Tournez-vous ensuite vers la réutilisation de connexions plutôt que vers le billet de blogue qui vous dit d’activer tcp_tw_recycle, retiré du noyau en 4.12 parce qu’il brisait le NAT.

Lire correctement la file d’écoute

C’est le chiffre le plus mal lu de toute la sortie de ss.

Schéma des deux files d'écoute : la file SYN qui garde les connexions à moitié ouvertes et la file d'accept qui garde les négociations terminées, avec un exemple de sortie ss -ltn montrant Recv-Q comme profondeur actuelle de la file d'accept et Send-Q comme son maximum

Sur une ligne ESTAB, Recv-Q et Send-Q sont des comptes d’octets : les données reçues mais pas encore lues par l’application, et les données écrites mais pas encore acquittées. Sur une ligne LISTEN, ils veulent dire tout autre chose. Recv-Q, c’est le nombre de connexions établies qui attendent dans la file d’accept que votre application appelle accept(). Send-Q, c’est le maximum que cette file peut contenir — min(backlog, net.core.somaxconn), où backlog est l’argument passé par l’application à listen(). Ni l’un ni l’autre n’est un compte d’octets.

Donc une ligne LISTEN qui affiche Recv-Q 0, Send-Q 511, c’est un écouteur en santé avec une file de 511 de profondeur et rien en attente. Une ligne qui affiche Recv-Q 128, Send-Q 128, c’est une file d’accept pleine — et pleine veut dire que le noyau jette des connexions en ce moment même. Les clients voient des blocages et des expirations pendant que votre service a l’air parfaitement vivant.

Confirmez les rejets au lieu de les déduire :

nstat -az TcpExtListenOverflows TcpExtListenDrops
#kernel
TcpExtListenOverflows           1420               0.0
TcpExtListenDrops               1420               0.0

Un compteur qui monte pendant que vous regardez, c’est une preuve. Le correctif est en amont du noyau : votre application n’appelle pas accept() assez vite — trop peu de travailleurs, des travailleurs bloqués sur quelque chose de lent, une boucle d’événements calée. Augmenter somaxconn et le backlog de l’application achète de la profondeur de file, ce qui convertit des rejets en latence. Ça vaut la peine pendant que vous réglez la vraie cause, mais ce n’est pas la cause.

Quand ce n’est pas la bonne approche

ss vous montre des sockets — la comptabilité que le noyau tient des connexions. Il ne vous montre pas de paquets. Quand la question est « qu’est-ce qu’il y a vraiment sur le fil », c’est le territoire de tcpdump. Une socket en SYN-SENT vous dit que rien n’est revenu, mais seule une capture vous dit si le SYN est bel et bien sorti de la machine, par quelle interface, ou si un RST est arrivé et s’est fait jeter quelque part entre les deux.

ss ne connaît pas non plus le NAT. Sur un routeur, un hôte Docker, ou n’importe quoi qui fait du masquerade, la table des sockets et la table de suivi de connexions sont deux choses différentes — utilisez conntrack -L pour voir les quadruplets traduits, et rappelez-vous que l’épuisement de conntrack produit ses propres rejets, qui n’apparaissent jamais dans ss.

Enfin, ss est limité à son espace de noms réseau. Lancez-le sur l’hôte et vous ne verrez pas les sockets d’un conteneur en mode bridge — vous obtenez la vue de l’hôte, un ensemble complètement différent. Utilisez nsenter -t <pid> -n ss -tlnp, ou lancez ss dans le conteneur.

La page de manuel ss(8) est courte et vaut la peine d’être lue une fois d’un bout à l’autre.

Si c’est le genre de travail que vous préféreriez ne plus jamais porter, c’est le quotidien d’Opérations Cloud — réservez une revue d’infrastructure.