Aller au contenu

UDP et les ports

100 Comprendre ⏱ 1 h 10 reseaulinuxudp

À la fin, vous saurez

  • Expliquer le rôle des ports et identifier un échange par son quintuplet
  • Distinguer ports bien connus, enregistrés et éphémères, et retrouver le service associé à un port
  • Décrire l'en-tête UDP et ce qu'UDP ne garantit pas
  • Lister les sockets en écoute avec ss et interpréter l'adresse d'écoute (0.0.0.0, 127.0.0.1, adresse précise)
  • Diagnostiquer un port déjà utilisé, un port privilégié et un port fermé en UDP
  • Expliquer le principe d'une attaque par amplification et la responsabilité de l'exploitant d'un service UDP

Prérequis

Testé avec iproute2 6.1.0 netcat-openbsd 1.226 noyau 7.0 (HWE) python 3.12.3 ubuntu 24.04 , vérifié le 5 octobre 2026

Pourquoi

La requête GET /sante est arrivée jusqu'à sig-app-1, à l'adresse 172.16.20.11. Mais sur cette machine tournent plusieurs programmes qui communiquent par le réseau : Gunicorn, qui sert l'API sur le port 8000 ; le serveur SSH, sur le port 22 ; le client de synchronisation de l'heure ; peut-être un agent de supervision. L'adresse IP désigne une machine. Il faut un second niveau d'adressage pour désigner un programme sur cette machine : ce sont les ports, et c'est le rôle de la couche transport.

Cette couche a deux protocoles principaux. TCP, fiable et orienté connexion, fait l'objet des leçons 10 et 11. Cette leçon commence par le plus simple, UDP, qui fait presque uniquement ce travail d'adressage par ports, et presque rien d'autre. Sa simplicité en fait le transport du DNS, de la synchronisation de l'heure, de la téléphonie, des VPN modernes et désormais de HTTP/3.

Les ports sont aussi la source de pannes très quotidiennes : un service qui refuse de démarrer parce que « l'adresse est déjà utilisée », un service qui écoute mais que personne ne peut joindre parce qu'il n'écoute que sur 127.0.0.1, un port inférieur à 1024 qu'un compte de service n'a pas le droit d'ouvrir. Et UDP, mal exploité, transforme un serveur en arme : en 2018, GitHub a encaissé une attaque de 1,35 térabit par seconde lancée à travers des serveurs memcached qu'oubliaient d'autres exploitants.

Les concepts

Le port, adresse d'un programme

Un port est un nombre de 16 bits, de 0 à 65 535, présent dans l'en-tête de TCP et d'UDP. Chaque segment ou datagramme porte un port source et un port de destination. Le système d'exploitation remet les données reçues au programme qui s'est associé au port de destination.

Ports TCP et ports UDP sont deux espaces distincts : le port 53 en UDP et le port 53 en TCP sont deux portes différentes, qu'un même programme (un serveur DNS) ou deux programmes différents peuvent ouvrir.

La socket et le quintuplet

Un programme ne manipule pas directement des ports : il demande au noyau une socket, un point de communication auquel il associe une adresse et un port locaux (opération bind), et à travers lequel il envoie et reçoit des données. Pour le programme, une socket est un descripteur de fichier comme un autre, sur lequel il lit et écrit.

Un échange réseau est identifié sans ambiguïté par cinq valeurs, le quintuplet (5-tuple) :

ÉlémentExemple pour GET /sante
ProtocoleTCP
Adresse source172.16.20.5 (le répartiteur)
Port source39876
Adresse de destination172.16.20.11 (sig-app-1)
Port de destination8000

Deux connexions vers le même service (même adresse et même port de destination) se distinguent par leur port source. C'est ce qui permet à un serveur d'avoir des milliers de clients sur un seul port d'écoute, à un navigateur d'ouvrir six connexions vers le même site, et à une box de distinguer les connexions des appareils du foyer (leçon 7). Les répartiteurs de charge, le suivi de connexion du noyau et les journaux de flux des fournisseurs cloud raisonnent tous en quintuplets.

Ports bien connus, enregistrés, éphémères

L'IANA tient le registre des numéros de port. La RFC 6335 de 2011 découpe l'espace en trois plages :

PlageNomAttribution
0 à 1023ports système, ou bien connuspar l'IANA, sur une procédure stricte
1024 à 49151ports utilisateur, ou enregistréspar l'IANA, sur demande
49152 à 65535ports dynamiques, privés ou éphémèresjamais attribués

Un serveur écoute sur un port fixe et connu à l'avance : 22 pour SSH, 53 pour DNS, 443 pour HTTPS, 5432 pour PostgreSQL. Un client, lui, n'a pas besoin d'un port particulier : le noyau lui en attribue un, libre, au moment où il envoie son premier paquet. C'est un port éphémère, qui ne vit que le temps de l'échange.

Linux n'utilise pas la plage éphémère de la RFC 6335. Sa plage par défaut, documentée par la documentation du noyau, va de 32768 à 60999 :

$ sysctl net.ipv4.ip_local_port_range
net.ipv4.ip_local_port_range = 32768	60999

Soit 28 232 ports disponibles pour les connexions sortantes vers une même destination. Cette limite compte pour une machine qui ouvre énormément de connexions vers un même service (un mandataire, un répartiteur), et c'est l'équivalent, côté machine, de l'épuisement des ports d'un NAT.

Le fichier /etc/services fait correspondre noms et numéros, pour que les outils puissent afficher https au lieu de 443. Il est installé par le paquet netbase sur Ubuntu et Debian :

$ grep -wE "^(domain|ntp|syslog|bootps|bootpc|https)" /etc/services
domain		53/tcp				# Domain Name Server
domain		53/udp
bootps		67/udp
bootpc		68/udp
ntp		123/udp				# Network Time Protocol
https		443/tcp				# http protocol over TLS/SSL
https		443/udp				# HTTP/3
syslog		514/udp

On y lit déjà les services qui reposent sur UDP : le DNS, DHCP (bootps et bootpc, du nom de son ancêtre BOOTP), la synchronisation de l'heure, la journalisation à distance, et HTTPS en UDP pour HTTP/3. getent interroge la même base :

$ getent services 53/udp
domain                53/udp
$ getent services 8000/tcp
$ echo $?
2

Le port 8000 de Gunicorn n'a pas de nom : il n'est pas dans le fichier. Un port n'a pas besoin d'être enregistré pour être utilisé ; l'enregistrement évite seulement les collisions entre logiciels publics.

UDP : le minimum

UDP (User Datagram Protocol) est décrit par la RFC 768, signée par Jon Postel en 1980. Elle tient en trois pages. L'en-tête UDP fait 8 octets et contient quatre champs de 16 bits :

 0      7 8     15 16    23 24    31
+--------+--------+--------+--------+
|   Port source   | Port destination|
+--------+--------+--------+--------+
|    Longueur     | Somme contrôle  |
+--------+--------+--------+--------+
|              Données ...
+-----------------------------------+
  • Port source : facultatif en principe (zéro s'il n'est pas utilisé), en pratique toujours rempli pour recevoir la réponse.
  • Port de destination : le programme visé.
  • Longueur : en-tête compris, donc au moins 8.
  • Somme de contrôle : calculée sur les données, l'en-tête UDP et un pseudo-en-tête qui reprend les adresses IP source et destination et le protocole. Une valeur nulle signifie, en IPv4, « pas de somme de contrôle » ; en IPv6, elle est obligatoire, puisque l'en-tête IPv6 n'en a plus (leçon 8).

C'est tout. Il n'y a ni numéro de séquence, ni accusé de réception, ni fenêtre, ni connexion. La RFC 768 le dit en une phrase : la remise et la protection contre les doublons ne sont pas garanties. Un datagramme UDP peut :

  • se perdre sans que personne ne le sache ;
  • arriver en double ;
  • arriver dans le désordre par rapport aux autres ;
  • être tronqué à la lecture, si le programme lit avec un tampon trop petit.

En revanche, UDP préserve les frontières des messages : un datagramme envoyé arrive entier ou pas du tout, en un seul bloc. TCP, à l'inverse, transporte un flux d'octets sans frontières.

Qui choisit UDP, et pourquoi

UsagePortPourquoi UDP
DNS53une question, une réponse : ouvrir une connexion coûterait plus que l'échange ; le client réessaie s'il n'a pas de réponse
NTP (heure)123quelques paquets, et la mesure du temps de trajet serait faussée par les retransmissions de TCP
DHCP67, 68la machine n'a pas encore d'adresse : elle ne peut pas ouvrir de connexion
syslog514envoyer un journal ne doit jamais bloquer l'application ; une perte est tolérée
Voix et vidéovariableune donnée en retard est inutile : mieux vaut la perdre que l'attendre
QUIC et HTTP/3443QUIC réimplémente fiabilité, chiffrement et contrôle de congestion au-dessus d'UDP, dans l'application, pour évoluer sans attendre les noyaux
WireGuard51820 par défautun VPN transporte des paquets IP : la fiabilité est l'affaire des protocoles transportés
VXLAN4789encapsuler des trames Ethernet entre hyperviseurs (réseaux privés du cloud)

Le point commun : soit l'échange est trop court pour justifier une connexion, soit l'application préfère gérer elle-même ce dont elle a besoin. Choisir UDP, c'est accepter cette responsabilité. La RFC 8085, qui rassemble les recommandations d'usage d'UDP, en fait la liste : contrôler la congestion pour ne pas saturer le réseau, ne pas envoyer de datagrammes plus gros que la MTU du chemin, gérer soi-même pertes, doublons et désordre si l'on en a besoin.

Ce que voit un pare-feu

TCP a une ouverture et une fermeture de connexion explicites : un pare-feu à état voit le début et la fin d'un échange. UDP n'en a pas. Pour filtrer UDP « à état », le suivi de connexion de Linux simule une connexion : le premier datagramme sortant crée une entrée, les datagrammes en sens inverse sur le même quintuplet sont considérés comme des réponses, et l'entrée expire après un délai sans trafic (30 secondes par défaut pour un échange isolé, 120 secondes pour un flux, leçon 7). Pour une application qui doit garder une correspondance ouverte à travers un NAT ou un pare-feu, la RFC 8085 recommande des messages de maintien, mais pas plus d'un toutes les 15 secondes.

Le port fermé

Que se passe-t-il quand un datagramme UDP arrive sur un port où personne n'écoute ? Il n'y a pas de connexion à refuser. Le noyau de la machine de destination renvoie un message ICMP « port injoignable » (port unreachable, type 3 code 3 en IPv4). Ce message n'est pas garanti : il peut être filtré en chemin, ou limité en débit par le noyau émetteur. Le silence ne prouve donc rien en UDP : un port qui ne répond pas peut être fermé, filtré, ou ouvert sur un service qui a choisi de ne pas répondre à ce message-là.

En pratique

Les sorties de cette section ont été capturées sur une machine Ubuntu 24.04, avec LC_ALL=C.UTF-8, dans un répertoire de travail jetable. Tous les échanges restent sur l'interface de bouclage.

Un serveur UDP en dix lignes

Écrivez serveur_udp.py, qui renvoie en majuscules tout ce qu'il reçoit :

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(("127.0.0.1", 9999))
print("en écoute sur", s.getsockname(), flush=True)
while True:
    donnees, expediteur = s.recvfrom(2048)
    print("reçu", donnees, "de", expediteur, flush=True)
    s.sendto(donnees.upper(), expediteur)
  • SOCK_DGRAM demande une socket de datagrammes, c'est-à-dire UDP ; TCP serait SOCK_STREAM.
  • bind(("127.0.0.1", 9999)) associe la socket à l'adresse de bouclage et au port 9999.
  • recvfrom renvoie à la fois les données et l'adresse de l'expéditeur : puisqu'il n'y a pas de connexion, chaque datagramme doit dire d'où il vient, et chaque réponse dire où elle va (sendto).

Lancez-le en arrière-plan, et regardez la socket avec ss :

$ python3 serveur_udp.py > serveur.log 2>&1 &
$ ss -uan 'sport = :9999'
State  Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
UNCONN 0      0          127.0.0.1:9999      0.0.0.0:*          
  • -u : sockets UDP ; -a : toutes, y compris celles en attente ; -n : numéros bruts, sans traduction par /etc/services. Le filtre 'sport = :9999' limite l'affichage à ce port local.
  • L'état est UNCONN (unconnected) : une socket UDP n'est jamais « en écoute » au sens de TCP, elle est simplement prête à recevoir. ss -l montre les sockets UDP liées à une adresse dans cet état.
  • Recv-Q est la file des datagrammes reçus mais pas encore lus par le programme. Une valeur qui grandit indique un programme qui ne suit pas, et des datagrammes qui finiront jetés quand la file sera pleine.

Envoyez-lui un datagramme avec nc (la version OpenBSD, celle d'Ubuntu), en mode UDP :

$ echo "bonjour" | nc -u -w1 127.0.0.1 9999
BONJOUR
$ cat serveur.log
en écoute sur ('127.0.0.1', 9999)
reçu b'bonjour\n' de ('127.0.0.1', 45979)

-u passe en UDP, -w1 fait quitter nc après une seconde sans activité (sans cela, il attendrait indéfiniment, puisqu'en UDP rien ne signale la fin de l'échange). Le serveur a vu le datagramme arriver depuis le port 45979 : le port éphémère que le noyau a attribué à nc, dans la plage 32768 à 60999.

Le port fermé, vu de l'application

Envoyons maintenant un datagramme vers le port 9998, où personne n'écoute, de deux façons. D'abord avec une socket UDP « connectée », c'est-à-dire associée par connect à un destinataire unique :

import socket
c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
c.settimeout(2)
c.connect(("127.0.0.1", 9998))
print("adresse locale choisie par le noyau :", c.getsockname())
c.send(b"y a quelqu'un ?")
try:
    print(c.recv(2048))
except OSError as e:
    print(type(e).__name__, e)
adresse locale choisie par le noyau : ('127.0.0.1', 57316)
ConnectionRefusedError [Errno 111] Connection refused

Puis avec une socket non connectée, en sendto :

import socket
c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
c.settimeout(2)
c.sendto(b"y a quelqu'un ?", ("127.0.0.1", 9998))
try:
    print(c.recvfrom(2048))
except OSError as e:
    print(type(e).__name__, e)
TimeoutError timed out

Le noyau a reçu dans les deux cas le message ICMP « port injoignable ». Il le remonte comme une erreur ECONNREFUSED à la socket connectée, puisqu'il sait à quel échange il se rapporte. Pour la socket non connectée, qui pourrait parler à n'importe qui, il ne le signale pas sans option particulière (IP_RECVERR, décrite dans udp(7)) : le programme attend jusqu'à l'expiration de son délai. Le premier comportement explique le « Connection refused » que l'on obtient parfois en UDP, protocole sans connexion ; le second explique pourquoi beaucoup de clients UDP se contentent d'un délai d'attente.

Remarquez aussi la première ligne : sans bind explicite, connect a suffi pour que le noyau choisisse un port éphémère (57316) et l'adresse locale.

Le port déjà utilisé

Le serveur tourne toujours sur 9999. Tentons d'ouvrir le même port :

$ python3 -c "
import socket
s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
try: s.bind(('127.0.0.1',9999))
except OSError as e: print(type(e).__name__, e)"
OSError [Errno 98] Address already in use

EADDRINUSE, l'erreur que ip(7) décrit comme une tentative de lier une adresse déjà utilisée. Pour un service qui ne démarre pas avec ce message, la question est toujours : qui tient déjà ce port ? La réponse s'obtient avec l'option -p de ss, qui affiche le processus propriétaire (il faut les droits de root pour voir les processus des autres comptes) :

$ sudo ss -ulpn 'sport = :9999'
$ sudo ss -tlpn 'sport = :8000'

La colonne Process donne le nom du programme, son PID et le descripteur de fichier de la socket. Arrêtez ensuite le serveur de test (kill %1).

Le port privilégié

$ python3 -c "
import socket
s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
try:
    s.bind(('127.0.0.1',514))
except OSError as e: print(type(e).__name__, e)"
PermissionError [Errno 13] Permission denied

Un compte ordinaire ne peut pas ouvrir un port inférieur à 1024. Selon ip(7), seul un processus doté de la capacité CAP_NET_BIND_SERVICE le peut, et la limite elle-même est réglable :

$ sysctl net.ipv4.ip_unprivileged_port_start
net.ipv4.ip_unprivileged_port_start = 1024

C'est l'une des raisons pour lesquelles Gunicorn écoute sur le port 8000, sous un compte sans privilège, derrière un répartiteur qui expose le port 443. Si un service doit vraiment écouter sur un port bas sans tourner en root, on lui donne la seule capacité nécessaire (directive AmbientCapabilities=CAP_NET_BIND_SERVICE d'une unité systemd), plutôt que d'abaisser la limite pour toute la machine (glossaire : capability).

Sur quelle adresse écouter

L'adresse passée à bind décide qui peut joindre le service, indépendamment de tout pare-feu :

Adresse d'écouteQui peut joindre le service
127.0.0.1 (ou ::1)seulement les programmes de la machine elle-même
une adresse précise, comme 172.16.20.11ceux qui atteignent la machine par cette adresse, donc par cette interface
0.0.0.0toutes les adresses IPv4 de la machine, sur toutes ses interfaces, présentes et futures
::toutes les adresses IPv6 et, par défaut sous Linux, IPv4 aussi (leçon 8)

Pour lister tous les services en écoute d'une machine, en TCP et en UDP, avec numéros bruts :

$ ss -ltnu

C'est la première commande à lancer sur un serveur que l'on découvre : chaque ligne dont l'adresse locale est 0.0.0.0, * ou [::] est un service potentiellement joignable depuis le réseau. Sur sig-app-1, après la leçon 6 du cours cloud, on attend Gunicorn sur l'adresse privée 172.16.20.11:8000 et SSH sur le port 22, et rien d'autre d'exposé.

Le quintuplet, vu par ss

Pour voir des quintuplets, ouvrons deux connexions TCP vers un même serveur, sur le bouclage, avec ce script quintuplets.py :

import socket, subprocess

serveur = socket.socket()
serveur.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
serveur.bind(("127.0.0.1", 9101))
serveur.listen()
clients = [socket.create_connection(("127.0.0.1", 9101)) for _ in range(2)]
acceptees = [serveur.accept()[0] for _ in range(2)]
subprocess.run(["ss", "-tan", "( sport = :9101 or dport = :9101 )"])
$ python3 quintuplets.py
State  Recv-Q Send-Q Local Address:Port  Peer Address:Port Process
LISTEN 0      128        127.0.0.1:9101       0.0.0.0:*           
ESTAB  0      0          127.0.0.1:9101     127.0.0.1:43164       
ESTAB  0      0          127.0.0.1:43162    127.0.0.1:9101        
ESTAB  0      0          127.0.0.1:43164    127.0.0.1:9101        
ESTAB  0      0          127.0.0.1:9101     127.0.0.1:43162       

Cinq sockets pour deux échanges : la socket d'écoute (LISTEN), puis, pour chaque connexion, une socket côté client et une côté serveur, puisque les deux extrémités sont sur la même machine. Les deux connexions ont le même protocole, les mêmes adresses et le même port de destination ; seuls les ports éphémères des clients, 43162 et 43164, les distinguent.

Sous le capot

Comment le noyau choisit un port éphémère

Quand un programme envoie sans avoir lié sa socket, le noyau choisit un port libre dans ip_local_port_range. Il ne les prend pas dans l'ordre : il part d'une position pseudo-aléatoire (pour une connexion TCP, calculée à partir d'un secret et des adresses en jeu), puis cherche un port libre. Les ports source sont ainsi difficiles à prévoir, ce qui compte pour la sécurité : la RFC 8085 recommande des ports source aléatoires pour qu'un attaquant hors du chemin ne puisse pas deviner le quintuplet d'un échange et y injecter des données. C'est précisément ce qui protège les résolveurs DNS contre l'empoisonnement de cache : une réponse falsifiée doit tomber sur le bon port source, parmi des dizaines de milliers.

Où vont les datagrammes reçus

Quand un datagramme arrive, le noyau cherche une socket UDP dont l'adresse et le port locaux correspondent à sa destination, en préférant la plus précise : une socket liée à 127.0.0.1:9999 passe avant une socket liée à 0.0.0.0:9999, et une socket connectée à un expéditeur précis passe avant les autres. S'il n'en trouve aucune, il jette le datagramme et renvoie le message ICMP « port injoignable ». S'il en trouve une mais que sa file de réception est pleine, il jette le datagramme sans rien dire à personne : l'émetteur ne le saura jamais. Ces pertes silencieuses se comptent dans les statistiques du noyau (compteurs RcvbufErrors de /proc/net/snmp, ou nstat), à surveiller sur un serveur DNS ou un collecteur de journaux.

Pas de fragmentation souhaitée

Un datagramme UDP plus gros que la MTU du chemin doit être fragmenté par IP (leçon 6). Or la perte d'un seul fragment fait perdre tout le datagramme, et beaucoup de pare-feu jettent les fragments. D'après udp(7), Linux fait par défaut la découverte de la MTU du chemin pour UDP, et renvoie l'erreur EMSGSIZE à l'application qui envoie un datagramme trop gros. La RFC 8085 demande de ne pas dépasser la MTU du chemin, ou, à défaut, 576 octets en IPv4 et 1 280 octets en IPv6. C'est la même prudence qui limitait historiquement les réponses DNS en UDP à 512 octets, et qui fait basculer en TCP les réponses trop longues.

Pièges courants

« Connection refused » sur un port UDP. Ce n'est pas une connexion refusée, puisqu'il n'y en a pas : c'est un message ICMP « port injoignable » remonté à une socket connectée. Le service n'écoute pas sur ce port, ou pas sur cette adresse.

Le silence pris pour un succès. nc -u qui ne dit rien, un client DNS qui attend : en UDP, l'absence de réponse peut venir d'un port fermé dont l'ICMP est filtré, d'un pare-feu, d'une perte, ou d'un service qui ignore la requête. Testez avec un vrai client du protocole (dig pour le DNS, chronyc pour NTP), qui sait reconnaître une réponse.

Un service qui écoute sur 127.0.0.1 et « ne répond pas ». Il répond, mais seulement localement. ss -ltnu montre l'adresse d'écoute ; c'est la première chose à regarder avant d'accuser le réseau.

Un service qui écoute sur 0.0.0.0 sans qu'on le sache. L'inverse est plus dangereux : une base de données ou un cache installé avec sa configuration par défaut, accessible sur toutes les interfaces. Le pare-feu protège peut-être, mais la règle saine est d'écouter sur l'adresse la plus restreinte qui suffit.

Address already in use au redémarrage. Un ancien processus tient encore le port (ss -lpn), ou, en TCP, des connexions récemment fermées le gardent réservé quelques instants : les serveurs positionnent pour cela l'option SO_REUSEADDR, que ip(7) décrit (leçon 10).

Confondre port et service. Le port 443 ouvert ne prouve pas qu'un serveur HTTPS répond derrière, et un service peut écouter sur n'importe quel port. Le numéro est une convention, pas une garantie.

Sécurité

Les attaques par amplification

UDP n'a pas de poignée de main : rien ne vérifie que l'adresse source d'un datagramme est bien celle de l'émetteur. Un attaquant peut donc envoyer une petite requête à un serveur UDP en usurpant l'adresse de sa victime. Le serveur répond, avec une réponse bien plus grosse que la requête, à la victime. Avec des milliers de serveurs ainsi abusés, l'attaquant fait converger vers la victime un trafic bien supérieur à ce qu'il émet lui-même : c'est une attaque par amplification.

L'agence américaine de cybersécurité, la CISA, publie les facteurs d'amplification (le rapport entre la taille de la réponse et celle de la requête) des protocoles les plus abusés : de 28 à 54 pour le DNS, 556,9 pour NTP avec une ancienne commande de supervision, 358,8 pour CharGEN, et de 10 000 à 51 000 pour memcached. C'est ce dernier qui a servi contre GitHub le 28 février 2018 : selon le rapport d'incident de GitHub, l'attaque a culminé à 1,35 térabit par seconde et 126,9 millions de paquets par seconde, via des serveurs memcached exposés sur Internet avec UDP activé. Le site a été indisponible cinq minutes, puis par intermittence quelques minutes encore.

Les victimes de ces attaques sont des tiers ; les complices involontaires sont des exploitants qui ont laissé un service UDP répondre à n'importe qui. D'où trois règles :

  • N'exposez pas un service UDP qui n'a pas à l'être. Un cache memcached, un résolveur DNS, un serveur NTP ou un agent SNMP n'a aucune raison d'écouter sur une adresse publique : liez-le à l'adresse privée ou au bouclage.
  • Désactivez ce qui ne sert pas. memcached a désactivé UDP par défaut à partir de sa version 1.5.6, publiée en 2018 en réponse à ces attaques (vulnérabilité CVE-2018-1000115) ; vérifiez la configuration des versions que vous déployez, et ss -lunp sur vos serveurs.
  • Un résolveur DNS ne répond qu'à ses clients. Un résolveur « ouvert », qui répond aux requêtes récursives de tout Internet, est un amplificateur. Le cours DNS en profondeur y revient.

Côté opérateurs, la parade structurelle est le filtrage des adresses source usurpées à l'entrée des réseaux (BCP 38) : un réseau ne devrait laisser sortir que des paquets dont l'adresse source lui appartient.

Le moindre service

Chaque port en écoute est une surface d'attaque. Sur un serveur, faites l'inventaire avec ss -ltnup, et pour chaque ligne demandez-vous : ce service doit-il tourner ? doit-il écouter sur cette adresse ? Un service désinstallé ne peut pas être attaqué ; un service lié au bouclage ne peut l'être que depuis la machine.

En production

  • Inventoriez les ports de chaque machine et comparez-les à ce qui est attendu, idéalement de façon automatique : un nouveau port en écoute est un changement à expliquer.
  • Supervisez les pertes UDP sur les serveurs qui en reçoivent beaucoup (DNS, syslog, métriques en UDP comme StatsD) : les erreurs de tampon de réception signalent des pertes silencieuses. Augmenter le tampon (SO_RCVBUF, réglages net.core.rmem_*) ou ajouter des lecteurs règle la plupart des cas.
  • Méfiez-vous de syslog en UDP pour des journaux qui comptent (sécurité, audit) : une perte n'est jamais signalée. Préférez TCP, avec TLS, ou un agent qui accuse réception.
  • Dans le cloud, les groupes de sécurité et les listes de contrôle d'accès ont des règles par protocole : ouvrir le port 53 en TCP n'ouvre pas le port 53 en UDP, et inversement. Les journaux de flux (flow logs) des fournisseurs, quand ils existent, s'expriment en quintuplets.
  • Les répartiteurs de charge UDP existent mais sont moins courants que pour TCP : sans connexion, ils répartissent par hachage du quintuplet, et un client qui change de port source change de serveur.

Exercices

1. Lire un quintuplet (niveau 100). L'utilisatrice ouvre deux onglets sur l'API de Signalements. Le répartiteur reçoit deux connexions venues de 198.51.100.7. Qu'est-ce qui les distingue ? Écrivez les deux quintuplets possibles.

Solution

Le port source, attribué par la box (après traduction, leçon 7). Par exemple : (TCP, 198.51.100.7, 40312, 203.0.113.10, 443) et (TCP, 198.51.100.7, 40313, 203.0.113.10, 443). Protocole, adresses et port de destination sont identiques ; le port source suffit à séparer les deux échanges.

2. Le service introuvable (niveau 100). Sur sig-app-1, ss -ltn affiche la ligne LISTEN 0 2048 127.0.0.1:8000 0.0.0.0:*. Le répartiteur déclare l'instance hors service. Expliquez, et corrigez.

Solution

Gunicorn n'écoute que sur l'interface de bouclage : seuls les programmes de sig-app-1 peuvent le joindre. Le répartiteur, qui se connecte à 172.16.20.11:8000 depuis le réseau privé, n'atteint aucune socket et reçoit un refus de connexion. Correction : lancer Gunicorn avec --bind 172.16.20.11:8000 (l'adresse privée, ce qui est le plus restrictif), puis vérifier avec ss -ltn que l'adresse d'écoute a changé et que la vérification de santé du répartiteur passe.

3. Le bon outil (niveau 100). Expliquez pourquoi echo test | nc -u -w1 172.16.20.12 123 qui ne renvoie rien ne permet pas de conclure qu'un serveur NTP fonctionne, ni qu'il ne fonctionne pas. Que feriez-vous à la place ?

Solution

test n'est pas une requête NTP valide : un serveur NTP en marche l'ignore, sans répondre. Et si le port était fermé, le message ICMP « port injoignable » pourrait être filtré en chemin, et nc en mode non connecté ne le signalerait pas forcément. Dans les deux cas, on observe le même silence. Il faut un vrai client du protocole : chronyc sources sur une machine synchronisée sur ce serveur, ou un outil d'interrogation NTP comme ntpdate -q ou sntp s'il est installé. Pour savoir si le port est ouvert, ss -lunp sur le serveur lui-même.

4. L'amplificateur (niveau 200). Un audit externe signale que sig-app-2 répond sur le port UDP 11211 depuis Internet. Expliquez le risque, et listez les vérifications et corrections à faire, dans l'ordre.

Solution

11211 est le port de memcached. Exposé en UDP, il peut servir d'amplificateur dans une attaque par déni de service contre un tiers (facteur de 10 000 à 51 000 selon la CISA), en plus de laisser lire et modifier le cache. Vérifications : sudo ss -lunp 'sport = :11211' pour voir quel processus écoute et sur quelle adresse ; la configuration de memcached (adresse d'écoute, UDP activé ou non) ; les règles du groupe de sécurité et du pare-feu de l'hôte. Corrections : lier memcached au bouclage ou à l'adresse privée, désactiver UDP s'il ne sert pas, fermer le port dans le groupe de sécurité, puis vérifier depuis l'extérieur que le port ne répond plus. Enfin, chercher pourquoi la machine avait une adresse publique joignable alors que l'architecture du cours n'en prévoit pas.

5. Les ports éphémères (niveau 200). Un mandataire sur une machine Linux ouvre une nouvelle connexion TCP vers 172.16.20.11:8000 pour chaque requête, et la ferme aussitôt. À fort trafic, de nouvelles connexions échouent avec « Cannot assign requested address ». Expliquez, avec les valeurs par défaut vues dans la leçon, et proposez deux corrections.

Solution

Toutes les connexions vers la même destination (même adresse, même port) ne se distinguent que par le port source. La plage éphémère de Linux offre 28 232 ports (32768 à 60999). Chaque connexion fermée par le mandataire garde son port réservé un moment dans l'état TIME-WAIT (leçon 10). Si le mandataire ouvre plus de connexions que la plage n'en peut contenir sur cette durée, le noyau ne trouve plus de port libre : EADDRNOTAVAIL. Corrections : réutiliser les connexions (maintien de connexion HTTP, pool de connexions vers les instances), et, en complément, élargir net.ipv4.ip_local_port_range ou répartir sur plusieurs adresses de destination ou source.

Récapitulatif

  • La couche transport adresse des programmes grâce aux ports (16 bits). TCP et UDP ont chacun leur espace de ports.
  • Un échange est identifié par son quintuplet : protocole, adresse et port source, adresse et port de destination. Le port source distingue les connexions vers un même service.
  • Ports système (0 à 1023, privilégiés sous Linux), enregistrés (1024 à 49151), dynamiques (49152 à 65535) ; Linux attribue ses ports éphémères entre 32768 et 60999. /etc/services et getent services traduisent noms et numéros.
  • UDP : en-tête de 8 octets, pas de connexion, pas de garantie de remise, d'ordre ni d'unicité, mais des messages aux frontières préservées. Il sert au DNS, à NTP, à DHCP, à syslog, à la voix, à QUIC et HTTP/3, à WireGuard, à VXLAN.
  • Un port UDP fermé répond par un ICMP « port injoignable », que seule une socket connectée remonte en « Connection refused » ; le silence ne prouve rien.
  • ss -ltnu liste les services en écoute ; l'adresse d'écoute (127.0.0.1, adresse précise, 0.0.0.0, ::) décide qui peut les joindre. EADDRINUSE : le port est pris ; Permission denied sous 1024 : il faut CAP_NET_BIND_SERVICE.
  • Un service UDP exposé sans raison peut devenir un amplificateur d'attaques (memcached contre GitHub en 2018, 1,35 Tbit/s).

Pour aller plus loin

  • La RFC 768, trois pages, à lire en entier une fois.
  • La RFC 8085, pour qui écrit une application au-dessus d'UDP : congestion, taille des messages, maintien à travers les NAT.
  • Le registre des ports de l'IANA, pour vérifier à quoi correspond un port inconnu (sans oublier qu'un port ne prouve rien).
  • La leçon suivante, qui passe à TCP : la poignée de main, les états d'une connexion, et pourquoi TIME-WAIT existe.
Voir ma constellation →

Sources