UDP et les ports
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ément | Exemple pour GET /sante |
|---|---|
| Protocole | TCP |
| Adresse source | 172.16.20.5 (le répartiteur) |
| Port source | 39876 |
| Adresse de destination | 172.16.20.11 (sig-app-1) |
| Port de destination | 8000 |
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 :
| Plage | Nom | Attribution |
|---|---|---|
| 0 à 1023 | ports système, ou bien connus | par l'IANA, sur une procédure stricte |
| 1024 à 49151 | ports utilisateur, ou enregistrés | par l'IANA, sur demande |
| 49152 à 65535 | ports dynamiques, privés ou éphémères | jamais 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
| Usage | Port | Pourquoi UDP |
|---|---|---|
| DNS | 53 | une 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) | 123 | quelques paquets, et la mesure du temps de trajet serait faussée par les retransmissions de TCP |
| DHCP | 67, 68 | la machine n'a pas encore d'adresse : elle ne peut pas ouvrir de connexion |
| syslog | 514 | envoyer un journal ne doit jamais bloquer l'application ; une perte est tolérée |
| Voix et vidéo | variable | une donnée en retard est inutile : mieux vaut la perdre que l'attendre |
| QUIC et HTTP/3 | 443 | QUIC réimplémente fiabilité, chiffrement et contrôle de congestion au-dessus d'UDP, dans l'application, pour évoluer sans attendre les noyaux |
| WireGuard | 51820 par défaut | un VPN transporte des paquets IP : la fiabilité est l'affaire des protocoles transportés |
| VXLAN | 4789 | encapsuler 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_DGRAMdemande une socket de datagrammes, c'est-à-dire UDP ; TCP seraitSOCK_STREAM.bind(("127.0.0.1", 9999))associe la socket à l'adresse de bouclage et au port 9999.recvfromrenvoie à 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 -lmontre les sockets UDP liées à une adresse dans cet état. Recv-Qest 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 refusedPuis 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 outLe 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'écoute | Qui 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.11 | ceux qui atteignent la machine par cette adresse, donc par cette interface |
0.0.0.0 | toutes 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 -lunpsur 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églagesnet.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/servicesetgetent servicestraduisent 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 -ltnuliste 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 deniedsous 1024 : il fautCAP_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-WAITexiste.
Sources
- RFC 768, User Datagram Protocol (J. Postel, 1980)
- RFC 6335, IANA Procedures for the Management of the Service Name and Transport Protocol Port Number Registry (BCP 165, 2011)
- RFC 8085, UDP Usage Guidelines (BCP 145, 2017)
- IANA, Service Name and Transport Protocol Port Number Registry
- Linux man-pages, udp(7)
- Linux man-pages, ip(7)
- Linux man-pages, ss(8)
- Documentation du noyau Linux, IP Sysctl (ip_local_port_range, ip_unprivileged_port_start)
- CISA, UDP-Based Amplification Attacks (alerte TA14-017A, révisée en 2019)
- Debian security tracker, CVE-2018-1000115 (memcached, UDP désactivé par défaut en 1.5.6)
- GitHub, February 28th DDoS Incident Report (2018)