Diagnostiquer un problème réseau
Pourquoi
« Le réseau ne marche pas » est la phrase la moins informative de l'exploitation. Elle peut vouloir dire qu'un nom ne se résout pas, qu'une route manque, qu'un pare-feu jette des paquets, qu'un service écoute sur la mauvaise adresse, qu'un paquet trop gros disparaît en chemin, ou que l'application elle-même est saturée. Chacune de ces causes a des symptômes distincts, et des remèdes qui n'ont rien en commun.
Face à cette phrase, deux attitudes s'opposent. La première consiste à essayer : redémarrer le service, ouvrir un port « pour voir », désactiver le pare-feu, relancer la machine. Parfois cela marche, et l'on ne sait pas pourquoi ; souvent cela empire, parce que l'on a changé trois choses à la fois, dont une ouvre une brèche que personne ne refermera. La seconde consiste à interroger : formuler une hypothèse, la vérifier par une commande dont on sait lire la réponse, et avancer. Elle paraît plus lente ; elle est presque toujours plus rapide, et elle laisse une trace que l'on pourra relire au prochain incident.
Cette dernière leçon rassemble tout le cours en une méthode. Elle s'appuie sur les mécanismes vus jusqu'ici (adresses et routes, ARP, ICMP et MTU, NAT, ports, états de TCP) et sur les outils que l'on trouve sur n'importe quel serveur Linux. Elle se termine sur cinq pannes de Signalements, inspirées de situations que toute équipe d'exploitation rencontre.
Les concepts
Une méthode plutôt qu'une liste de commandes
Un diagnostic est une suite d'hypothèses que l'on confirme ou que l'on élimine. Trois règles le rendent efficace :
- Décrire précisément le symptôme. Qui n'arrive pas à joindre quoi, depuis où, par quel protocole et quel port, depuis quand, avec quel message exact ? « Le répartiteur marque
sig-app-1hors service depuis 14 h 10,sig-app-2est sain,curldepuissig-app-1vers127.0.0.1:8000/santerépond 200 » contient déjà la moitié de la réponse. - Avancer dans un ordre. Deux ordres fonctionnent :
- de bas en haut, en suivant les couches du cours : le lien, puis IP et la route, puis le port et TCP, puis l'application. Chaque étape suppose la précédente ;
- par dichotomie : on coupe le chemin en deux (« est-ce que ça passe jusqu'au répartiteur ? ») et l'on recommence dans la moitié fautive. C'est la méthode la plus rapide sur un long chemin, à condition d'avoir des points d'observation au milieu.
- Ne changer qu'une chose à la fois, et noter ce que l'on change. Un changement qui ne corrige rien est annulé avant le suivant.
Les questions, dans l'ordre
Pour une connexion d'un client vers un service, les questions suivantes couvrent l'immense majorité des pannes. Chacune a sa commande :
| Question | Commande | Leçon |
|---|---|---|
| Le nom se résout-il, et vers la bonne adresse ? | getent hosts nom, resolvectl query nom | cours DNS en profondeur |
| Ai-je une route vers cette adresse, par quelle interface et quelle source ? | ip route get adresse | 5 |
| Le lien est-il actif, le prochain saut répond-il à ARP ? | ip -br link, ip neigh | 2 |
| Les paquets atteignent-ils la destination, et reviennent-ils ? | ping, tracepath, mtr | 6 |
| Le service écoute-t-il, sur le bon port et la bonne adresse ? | ss -ltnp sur le serveur | 9, 10 |
| Un filtrage s'interpose-t-il (groupe de sécurité, pare-feu, ACL) ? | règles du fournisseur, nft list ruleset | 7 |
| La connexion TCP s'établit-elle ? | nc -vz, curl -v | 10 |
| L'application répond-elle, et en combien de temps ? | curl -w, journaux de l'application | cours HTTP et les API web |
Avec l'habitude, on commence souvent par la question du milieu (la connexion TCP s'établit-elle ?), parce que sa réponse coupe l'espace des causes en deux : si elle s'établit, tout ce qui est en dessous fonctionne.
Lire les messages d'erreur
Les messages que remontent les programmes viennent presque tous d'un code d'erreur du noyau (errno), renvoyé par connect(2) ou par la résolution de noms. Chacun dit qui a répondu, et donc où chercher :
| Message | Code | D'où il vient | Causes typiques |
|---|---|---|---|
Connection refused | ECONNREFUSED | La destination a répondu au SYN par un RST | Rien n'écoute sur ce port, ou l'application écoute sur une autre adresse ; plus rarement un pare-feu qui rejette par RST |
Connection timed out | ETIMEDOUT | Personne n'a répondu au SYN, après les retransmissions | Groupe de sécurité ou pare-feu qui jette, machine éteinte, route absente au retour, file accept saturée |
No route to host | EHOSTUNREACH | Une erreur ICMP « hôte injoignable » est revenue, ou la machine elle-même n'a pas obtenu de réponse ARP | Destination absente du réseau local, pare-feu qui rejette avec ICMP (le réglage par défaut de firewalld produit ce message) |
Network is unreachable | ENETUNREACH | La pile locale n'a trouvé aucune route | Pas de route par défaut, adresse d'une famille non configurée (IPv6 sans route IPv6), interface désactivée |
Name or service not known | EAI_NONAME | La résolution de noms a échoué : aucune connexion n'a même été tentée | Nom mal écrit, enregistrement DNS absent, mauvais résolveur |
Temporary failure in name resolution | EAI_AGAIN | Le résolveur n'a pas pu répondre | Serveur DNS injoignable, souvent un problème de réseau sortant |
Deux remarques importantes. D'abord, un délai dépassé ne prouve pas que le SYN s'est perdu : la réponse a pu se perdre au retour, par exemple quand la route de retour passe par un autre chemin que l'aller, ou qu'un filtrage à état ne voit qu'un sens. Ensuite, beaucoup de programmes reformulent ces messages : curl affiche « Couldn't connect to server » avec un code de sortie 7 pour un refus comme pour une absence de route, et le code 28 pour un délai. Quand le message n'est pas clair, nc -v affiche le texte exact du noyau.
Les outils et ce qu'ils regardent
ip(paquetiproute2) montre l'état de la machine : interfaces (ip -br link,ip -br addr), routes (ip route, et surtoutip route get, qui donne la décision de routage pour une adresse précise), voisins ARP et NDP (ip neigh).ssmontre les sockets : celles qui écoutent (-l), les connexions et leurs états (-a), les processus propriétaires (-p), les informations internes de TCP (-i, leçon 11).nc(netcat) ouvre une connexion TCP ou UDP brute :nc -vz hôte portteste l'établissement sans rien envoyer.curlfait une vraie requête HTTP, et sait mesurer chacune de ses phases.ping,tracepath,tracerouteetmtrtestent le chemin avec ICMP ou UDP (leçon 6).mtrcombinetracerouteetpingen continu, et affiche les pertes et les temps à chaque saut.tcpdumpcapture les paquets eux-mêmes : c'est l'arbitre final, quand les autres outils se contredisent.
Capturer avec tcpdump
tcpdump écoute une interface et affiche (ou enregistre) les paquets qui correspondent à un filtre. Le filtre est l'essentiel : sur un serveur actif, une capture sans filtre noie l'information et coûte cher en processeur. La syntaxe des filtres est décrite dans pcap-filter(7) :
$ sudo tcpdump -ni any 'host 172.16.20.5 and tcp port 8000'
$ sudo tcpdump -ni eth0 'tcp port 8000 and tcp[tcpflags] & (tcp-syn|tcp-rst) != 0'
$ sudo tcpdump -ni any -c 200 -w signalements.pcap 'tcp port 8000'
$ tcpdump -nr signalements.pcap
-nn'essaie pas de convertir les adresses et les ports en noms (ce qui ralentirait la capture et ajouterait des requêtes DNS à ce que l'on observe).-i anyécoute toutes les interfaces (pseudo-interface propre à Linux) ;-i eth0une seule. Sur un répartiteur ou une passerelle, comparer deux interfaces dit si un paquet entre sans ressortir.- Le premier filtre ne garde que le trafic entre le répartiteur et le port 8000 ; le deuxième, les seuls segments
SYNetRST(ouvertures et refus), ce qui suffit souvent et ne capture aucune donnée applicative. -c 200s'arrête après 200 paquets ;-wenregistre les paquets bruts dans un fichier, que l'on relit avec-rou que l'on ouvre dans Wireshark sur son poste.
La page de manuel de tcpdump décrit le format d'une ligne TCP :
src > dst: Flags [tcpflags], seq data-seqno, ack ackno, win window, urg urgent, options [opts], length lenet l'illustre par le début d'une session, dont voici les quatre premières lignes, citées telles quelles :
IP rtsg.1023 > csam.login: Flags [S], seq 768512:768512, win 4096, opts [mss 1024]
IP csam.login > rtsg.1023: Flags [S.], seq 947648:947648, ack 768513, win 4096, opts [mss 1024]
IP rtsg.1023 > csam.login: Flags [.], ack 1, win 4096
IP rtsg.1023 > csam.login: Flags [P.], seq 1:2, ack 1, win 4096, length 1On y reconnaît la poignée de main de la leçon 10 : [S] est un SYN, [S.] un SYN+ACK (le point note l'ACK), [.] un acquittement seul, [P.] des données avec PSH. Un [R] ou [R.] est un RST, [F.] un FIN. Après les premiers segments, tcpdump affiche des numéros de séquence relatifs (ack 1, seq 1:2) pour rester lisible ; l'option -S affiche les numéros absolus. Avec ces seules lettres, on lit la plupart des diagnostics : un [S] répété sans réponse est un filtrage ; un [S] suivi d'un [R.] est un refus ; une poignée de main complète suivie d'un silence met en cause l'application ou la taille des paquets.
En pratique
Les sorties de cette section ont été produites sur une machine Ubuntu 24.04, avec LC_ALL=C, sur l'interface locale ou sans qu'aucun paquet ne quitte la machine. tcpdump, qui exige les droits d'administration, n'y est pas exécuté : ses commandes sont décrites plus haut.
Qui écoute, et où
Sur le serveur, la première vérification est presque toujours celle-ci :
$ ss -ltnp 'sport = :8000'
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0 5 127.0.0.1:8000 0.0.0.0:* users:(("python3",pid=666826,fd=3))
-lles sockets en écoute,-tTCP,-nen chiffres,-ple processus propriétaire (pour un processus d'un autre compte, il fautsudo).- La colonne qui compte est
Local Address:127.0.0.1:8000n'accepte que les connexions venues de la machine elle-même. Le répartiteur, qui arrive par172.16.20.11, recevra un refus. Les formes attendues pour un service joignable depuis le réseau privé sont172.16.20.11:8000(l'adresse privée seule) ou0.0.0.0:8000(toutes les adresses IPv4).
Tester l'établissement de la connexion
Depuis le client, nc -vz répond à la question du milieu :
$ nc -vz 127.0.0.1 8000
Connection to 127.0.0.1 8000 port [tcp/*] succeeded!
-zétablit la connexion puis la ferme sans rien envoyer ;-vaffiche le résultat ;-w 3fixerait un délai maximal de 3 secondes, indispensable quand on soupçonne un filtrage.
Quand la pile ne trouve aucune route, l'échec est local et immédiat. Cette machine n'a pas de route IPv6 par défaut ; une connexion vers une adresse de documentation IPv6 donne :
$ nc -vz -w 3 2001:db8::1 8000
nc: connect to 2001:db8::1 port 8000 (tcp) failed: Network is unreachable
Aucun paquet n'est parti : c'est la table de routage locale qui a répondu, ce que confirmerait ip route get 2001:db8::1. Une erreur de résolution, elle, survient avant même toute tentative :
$ python3 -c '
import socket
try: socket.getaddrinfo("sig-app-1.exemple.invalid", 8000)
except socket.gaierror as e: print(e.errno, e.strerror)'
-2 Name or service not known
Le domaine .invalid est réservé (RFC 6761) pour ne jamais exister : c'est le moyen sûr de provoquer cette erreur.
Distinguer un refus d'un délai, en mesurant
Le refus de la leçon 10 était immédiat (« after 0 ms »). Pour obtenir un délai sans quitter la machine, on peut reprendre la file accept saturée de la leçon 10 : un serveur avec un backlog de 0 qui n'appelle jamais accept(), déjà occupé par une connexion. Le SYN suivant est ignoré, comme il le serait par un pare-feu qui jette :
$ curl -sS --connect-timeout 3 http://127.0.0.1:8004/sante; echo "code=$?"
curl: (28) Failed to connect to 127.0.0.1 port 8004 after 3002 ms: Timeout was reached
code=28
--connect-timeout 3 limite l'attente de l'établissement à 3 secondes ; sans lui, curl attendrait la fin des retransmissions du SYN (environ deux minutes). Le code de sortie 28 signale un délai dépassé, le code 7 un échec de connexion.
Mesurer les phases d'une requête
Quand la connexion s'établit mais que « c'est lent », curl sait dire où le temps passe, avec l'option -w (write-out) et ses variables :
$ curl -sS -o /dev/null -w 'dns=%{time_namelookup} connexion=%{time_connect} tls=%{time_appconnect} premier_octet=%{time_starttransfer} total=%{time_total} code=%{http_code}\n' http://127.0.0.1:8000/sante
dns=0.000052 connexion=0.000331 tls=0.000000 premier_octet=0.006325 total=0.006401 code=200
Les temps sont en secondes, cumulés depuis le début de la requête :
time_namelookup: fin de la résolution du nom ;time_connect: fin de la poignée de main TCP ;time_connect - time_namelookupest donc environ un RTT ;time_appconnect: fin de la négociation TLS (0 en HTTP clair) ;time_starttransfer: arrivée du premier octet de la réponse ; l'écart avec l'étape précédente est essentiellement le temps de traitement du serveur ;time_total: fin du transfert.
Ici, sur l'interface locale, la connexion prend 0,3 ms et le serveur 6 ms : le réseau n'est pas en cause. Depuis le poste de l'équipe, à travers le répartiteur, la même mesure sépare en une commande un problème de DNS, de réseau, de TLS ou d'application.
Suivre le détail d'un échange
curl -v affiche chaque étape de la requête, préfixée par * (informations de curl), > (ce qui est envoyé) et < (ce qui est reçu) :
$ curl -sv http://127.0.0.1:8000/sante 2>&1
* Trying 127.0.0.1:8000...
* Connected to 127.0.0.1 (127.0.0.1) port 8000
> GET /sante HTTP/1.1
> Host: 127.0.0.1:8000
> User-Agent: curl/8.5.0
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Server: SimpleHTTP/0.6 Python/3.12.3
< Date: Mon, 05 Oct 2026 09:48:45 GMT
< Content-type: application/octet-stream
< Content-Length: 3
< Last-Modified: Mon, 05 Oct 2026 09:39:32 GMT
<
{ [3 bytes data]
* Closing connection
ok
Le serveur de cet essai est python3 -m http.server, qui répond en HTTP/1.0. -s supprime la barre de progression ; 2>&1 réunit la trace (écrite sur la sortie d'erreur) et le corps de la réponse (ok, sur la sortie standard), et, les deux flux n'étant pas vidés au même moment, le corps apparaît ici en dernier. { [3 bytes data] note la réception des 3 octets du corps. La ligne Connected to prouve que TCP est établi ; si la commande s'arrête juste après, sans <, c'est le serveur qui ne répond pas, et non le réseau.
Le chemin : ip route get et mtr
ip route get <adresse> affiche la décision de routage que la machine prendrait pour cette destination, sans envoyer de paquet. La réponse tient en une ligne : l'adresse, puis via suivi de la passerelle (absent si la destination est sur un réseau directement connecté), dev suivi de l'interface de sortie, et src suivi de l'adresse source qui sera utilisée. C'est la commande qui répond à « par où part ce paquet, et avec quelle adresse ? », bien plus fiable que la lecture de la table entière.
mtr -n <destination> envoie des sondes en continu et affiche, pour chaque saut, le taux de pertes et les temps (moyen, meilleur, pire). Une perte qui apparaît à un saut et se propage à tous les suivants est réelle ; une perte affichée à un seul saut intermédiaire, mais pas aux suivants, vient le plus souvent d'un routeur qui limite ses réponses ICMP, et ne gêne pas le trafic qui le traverse.
Sous le capot
D'où viennent les messages. Quand un programme appelle connect(2), la pile choisit une route (sinon ENETUNREACH), résout l'adresse du prochain saut par ARP (sans réponse après quelques essais : EHOSTUNREACH), envoie le SYN et attend. Un RST en retour produit ECONNREFUSED ; un message ICMP « destination injoignable » est traduit selon son type (hôte, réseau, port, interdit administrativement) ; l'absence de toute réponse après les retransmissions produit ETIMEDOUT. La résolution de noms est faite avant, en espace utilisateur, par la bibliothèque C (getaddrinfo), qui renvoie ses propres codes (EAI_*) : c'est pourquoi une erreur de nom n'a pas de numéro errno.
Ce que voit tcpdump. La capture se fait au niveau du pilote de l'interface, par une socket spéciale (AF_PACKET) : tcpdump voit les paquets entrants avant le pare-feu de la machine, et les paquets sortants après que la pile les a construits. Un SYN visible dans la capture mais qui ne reçoit jamais de SYN+ACK a donc pu être jeté par le pare-feu local ; un SYN absent de la capture n'est jamais arrivé jusqu'à l'interface. Le filtre est compilé en un petit programme (BPF) exécuté dans le noyau, pour que seuls les paquets utiles soient copiés vers tcpdump.
Les ports éphémères comme ressource. Chaque connexion sortante vers une même destination (adresse et port) consomme un port local de la plage net.ipv4.ip_local_port_range, soit 28 232 ports de 32768 à 60999 par défaut. Un port reste occupé tant que la connexion existe, y compris en TIME-WAIT pendant 60 secondes. Un client qui ouvre et ferme plus de 28 000 connexions par minute vers la même destination épuise la plage : connect() échoue alors avec EADDRNOTAVAIL, « Cannot assign requested address ».
Pièges courants
Conclure de ping. Beaucoup de pare-feu et de groupes de sécurité bloquent ICMP. Un ping sans réponse ne prouve pas que la machine est injoignable en TCP ; un ping qui répond ne prouve pas que le port 8000 est ouvert. Testez le protocole et le port réellement utilisés, avec nc -vz ou curl.
Tester depuis le mauvais endroit. curl http://127.0.0.1:8000/sante sur le serveur ne teste ni le réseau, ni le pare-feu, ni l'adresse d'écoute. Testez depuis là où vient le trafic réel (le répartiteur, une autre instance du réseau privé), ou au moins vers l'adresse privée du serveur.
Désactiver le pare-feu « pour voir ». Si cela corrige, vous savez que le pare-feu est en cause, mais vous ne savez pas quelle règle, et la tentation de ne pas le réactiver est forte. Lisez plutôt les règles et leurs compteurs (sudo nft list ruleset affiche les compteurs des règles qui en ont), et ajoutez une règle précise.
Capturer sans filtre. Une capture tcpdump -i any sans filtre sur une machine active sature le terminal, perd des paquets (signalé à la fin par « packets dropped by kernel ») et enregistre au passage des données qui n'avaient rien à faire dans un fichier.
Oublier le chemin de retour. Un SYN peut arriver et sa réponse partir par une autre interface ou une autre passerelle, où un filtrage à état la jette faute d'avoir vu l'aller. Capturez des deux côtés, ou sur les deux interfaces d'un équipement.
Changer plusieurs choses à la fois. Le correctif qui « a marché » après trois modifications simultanées ne dit pas laquelle comptait ; les deux autres resteront, et l'une d'elles sera la cause du prochain incident.
Sécurité
- Une capture contient des données. Tout ce qui passe en clair (HTTP entre le répartiteur et Gunicorn, requêtes SQL sans TLS) apparaît dans une capture : en-têtes d'authentification, cookies de session, données personnelles des signalements. Une capture est donc soumise aux mêmes règles que les données qu'elle contient, RGPD compris. Filtrez au plus juste (les seuls
SYNetRSTsuffisent souvent), limitez la taille capturée par paquet (-s 96ne garde que les en-têtes), et supprimez le fichier une fois l'analyse terminée. - Capturer est un privilège. tcpdump exige les capacités
CAP_NET_RAWetCAP_NET_ADMIN, c'est-à-dire en pratiquesudo. Ne donnez pas ces capacités de façon permanente au binaire, et ne laissez pas un groupe d'utilisateurs capturer librement : qui capture voit les secrets des autres. tcpdump abandonne d'ailleurs ses privilèges après avoir ouvert l'interface (option-Z, comptetcpdumppar défaut sur Debian et Ubuntu). - Chiffrez le dernier saut. Que le trafic entre le répartiteur et les instances soit lisible par quiconque capture sur le réseau privé est un choix, pas une fatalité ; le cours TLS et PKI montre comment le chiffrer de bout en bout quand les exigences l'imposent.
- Ne laissez pas d'ouverture de diagnostic. Une règle « temporaire » ouverte à
0.0.0.0/0pendant un incident, unnc -loublié qui écoute sur un port, un service lancé à la main sur0.0.0.0: faites-en l'inventaire en fin d'incident, avecss -ltnpet la liste des règles.
En production
- Préparez les points d'observation. La dichotomie suppose de pouvoir regarder au milieu du chemin : journaux et métriques du répartiteur (état des backends, codes de réponse), compteurs TCP des instances (
nstat), vérifications de santé. Une plateforme qui n'expose rien oblige à tout deviner. - Écrivez les diagnostics courants dans un runbook. Les cinq scénarios ci-dessous, avec leurs commandes, valent mieux dans la documentation de l'équipe que dans la mémoire d'une personne. Le cours Réduire le toil : runbooks et automatisation en fait une pratique.
- Faites tester le chemin réel par la supervision. Une sonde qui appelle
https://signalements.exemple.fr/santedepuis l'extérieur, et une autre qui teste chaque backend depuis le réseau privé, disent tout de suite quelle moitié du chemin est en cause. - Gardez une trace. Notez pendant l'incident ce que vous avez vérifié, ce que vous avez changé et à quelle heure. Le postmortem (cours Postmortems sans reproche) se rédige à partir de ces notes.
Exercices
Les exercices de cette leçon sont cinq pannes de Signalements. Pour chacune, formulez votre hypothèse et les commandes qui la vérifient avant d'ouvrir la solution. Rappel des adresses : sig-app-1 172.16.20.11, sig-app-2 172.16.20.12, répartiteur 172.16.20.5 côté privé, base 172.16.20.30:5432, réseau privé 172.16.20.0/22.
1. Le backend hors service (niveau 200). Après une mise à jour, le répartiteur marque sig-app-1 hors service. Sur sig-app-1, systemctl status signalements est actif et curl http://127.0.0.1:8000/sante répond 200. Depuis sig-app-2, nc -vz 172.16.20.11 8000 répond immédiatement « Connection refused ».
Solution
Le refus est immédiat : la machine est joignable, mais rien n'écoute sur 172.16.20.11:8000. ss -ltnp 'sport = :8000' sur sig-app-1 montre 127.0.0.1:8000 : la mise à jour a changé l'option --bind de Gunicorn (ou la variable qui la fixe). Correction : lier Gunicorn à l'adresse privée (--bind 172.16.20.11:8000) ou à 0.0.0.0:8000 si le pare-feu de l'hôte protège le port, redémarrer, puis vérifier depuis sig-app-2 avec nc -vz et attendre que le répartiteur repasse le backend en service. Le test depuis 127.0.0.1 ne pouvait pas révéler cette panne.
2. Le silence (niveau 200). Depuis ce matin, toutes les requêtes vers sig-app-1 échouent par délai dépassé ; sig-app-2 fonctionne. Le service écoute bien sur 172.16.20.11:8000. Hier soir, une personne a « durci » le pare-feu de l'hôte.
Solution
Délai dépassé avec un service qui écoute sur la bonne adresse : un filtrage jette les paquets. On le confirme par une capture des seuls SYN et RST sur sig-app-1 : sudo tcpdump -ni any 'tcp port 8000 and tcp[tcpflags] & (tcp-syn|tcp-rst) != 0'. Des [S] venus de 172.16.20.5 apparaissent, répétés, sans [S.] en réponse : les paquets arrivent sur l'interface (tcpdump les voit avant le pare-feu) et sont jetés par le pare-feu local. sudo nft list ruleset montre la règle fautive (politique drop en entrée sans exception pour 172.16.20.0/22 sur le port 8000). On ajoute une règle précise, on vérifie, et l'on documente.
3. La base injoignable depuis une seule instance (niveau 200). sig-app-2, recréée hier, ne joint plus sig-db : psql échoue par délai dépassé, alors que sig-app-1 fonctionne. ip -br addr sur sig-app-2 montre l'adresse 172.16.24.12/22 sur l'interface privée.
Solution
172.16.24.12/22 n'appartient pas au réseau 172.16.20.0/22 (qui va de 172.16.20.0 à 172.16.23.255) : sig-app-2 a été rattachée à un autre réseau privé, ou ce réseau a été recréé avec une autre plage (leçon 4). ip route get 172.16.20.30 montre que le paquet part par la route par défaut (vers la passerelle) au lieu de l'interface privée, ou qu'il n'y a pas de chemin. Et même si un chemin existait, la base n'accepte peut-être que les connexions de son réseau. Correction : rattacher sig-app-2 au bon réseau privé, vérifier son adresse et la route, puis tester avec nc -vz -w 3 172.16.20.30 5432 avant psql.
4. Les petites requêtes passent, les grosses non (niveau 200). Depuis le poste d'un agent en télétravail, à travers le VPN de la mairie, GET /sante fonctionne, mais l'envoi d'un signalement avec une photo reste bloqué indéfiniment. Depuis le bureau, tout fonctionne.
Solution
Les petits paquets passent, les gros disparaissent, et seulement sur un chemin qui ajoute une encapsulation (le VPN) : c'est le trou noir MTU de la leçon 6. Le tunnel réduit la MTU du chemin, un routeur jette les paquets trop gros avec le drapeau « ne pas fragmenter », et le message ICMP qui devrait en avertir l'émetteur est filtré quelque part. La poignée de main et les petites requêtes passent ; les segments pleins de la photo, jamais. Vérification : tracepath vers le service depuis le poste, ou ping -M do -s 1400 puis des tailles décroissantes. Corrections : laisser passer les messages ICMP « fragmentation nécessaire » (ICMPv6 « paquet trop gros » en IPv6), ou réduire la MSS annoncée sur l'équipement du tunnel (MSS clamping).
5. « Cannot assign requested address » (niveau 200). Un script de reprise de données envoie un signalement par requête à l'API interne de sig-app-1, en ouvrant une nouvelle connexion HTTP à chaque fois, environ 600 par seconde. Au bout d'une minute, il échoue avec Cannot assign requested address. ss -s sur la machine du script montre près de 28 000 connexions en TIME-WAIT.
Solution
Le script épuise ses ports éphémères : chaque connexion vers 172.16.20.11:8000 consomme un port local, qui reste occupé 60 secondes en TIME-WAIT après la fermeture (le script ferme en premier, leçon 10). 600 connexions par seconde pendant 60 secondes font 36 000 ports, plus que les 28 232 de la plage par défaut : connect() échoue avec EADDRNOTAVAIL. La vraie correction est de réutiliser les connexions (une session HTTP avec keep-alive, comme requests.Session en Python) : quelques connexions suffisent alors pour tout le débit, et le gain de latence est important (leçon 11). Élargir ip_local_port_range ou activer tcp_tw_reuse ne fait que repousser le problème.
Récapitulatif
- Un diagnostic est une suite d'hypothèses vérifiées : décrire précisément le symptôme, avancer couche par couche ou par dichotomie, ne changer qu'une chose à la fois.
- Les questions dans l'ordre : nom, route (
ip route get), lien et voisin (ip neigh), écoute sur le bon port et la bonne adresse (ss -ltnp), filtrage, établissement TCP (nc -vz), réponse de l'application (curl -v,curl -w). - Les messages disent qui a répondu :
refused(unRSTde la destination),timed out(personne),No route to host(ICMP ou ARP),Network is unreachable(la pile locale),Name or service not known(la résolution, avant toute connexion). tcpdump -navec un filtre précis est l'arbitre final ;[S]répétés sans réponse : filtrage ;[S]puis[R.]: refus ; poignée de main puis silence : application ou MTU.- Les ports éphémères (28 232 par défaut) s'épuisent avec les connexions jetables :
Cannot assign requested address, à corriger en réutilisant les connexions. - Une capture contient des données : filtrer, tronquer, supprimer, et réserver ce droit à peu de personnes.
Pour aller plus loin
- Les pages de manuel
tcpdump(1)etpcap-filter(7), dont les exemples couvrent la plupart des filtres utiles. - La méthode USE de Brendan Gregg (utilisation, saturation, erreurs), qui donne un ordre de questions applicable aux ressources réseau comme aux autres.
- TCP/IP Illustrated, Volume 1, pour s'entraîner à lire des captures commentées.
- Le cours Le réseau sous Linux : nftables, namespaces, bridges, pour construire et diagnostiquer des réseaux virtuels sur une seule machine, et le cours Performance et diagnostic sous Linux pour aller plus loin dans la mesure.
- Le lab du cours, qui fait monter un petit réseau dans une machine virtuelle, puis le casser pour observer un refus, un délai dépassé et un problème de MTU.
Sources
- tcpdump, page de manuel tcpdump(1)
- tcpdump, page de manuel pcap-filter(7)
- Linux man-pages, ip-route(8), ip-neighbour(8) et ss(8)
- Linux man-pages, connect(2) et errno(3)
- curl, documentation : --write-out et codes de sortie
- RFC 1191, Path MTU Discovery, et RFC 4821, Packetization Layer Path MTU Discovery
- Documentation du noyau Linux, IP Sysctl (ip_local_port_range, tcp_tw_reuse)
- Brendan Gregg, The USE Method
- W. Richard Stevens, Kevin R. Fall, TCP/IP Illustrated, Volume 1 (2e éd., 2011)