Pourquoi des couches
Pourquoi
Camille, développeuse dans l'équipe qui exploite Signalements, tape dans son terminal :
$ curl http://203.0.113.10/sante
{"etat": "ok"}
Une ligne part, une ligne revient, en quelques dizaines de millisecondes. Entre les deux, la requête a quitté un ordinateur portable branché en Wi-Fi sur la box d'un appartement, traversé le réseau de son fournisseur d'accès, plusieurs réseaux d'opérateurs, l'entrée du centre de données de Scaleway, un répartiteur de charge, un réseau privé virtuel, et atteint un processus Gunicorn sur la machine sig-app-1. Chacun de ces réseaux utilise un matériel différent, a été construit par une équipe différente, à une époque différente. Aucun ne sait rien des autres. Et pourtant, cela marche, des milliards de fois par seconde à l'échelle de la planète.
Le jour où cela ne marche plus, Camille voit un message : Connection refused, Connection timed out, No route to host, Could not resolve host. Ces quatre erreurs ne viennent pas du même endroit et ne se diagnostiquent pas de la même façon. Sans une image claire de ce qui se passe entre le programme et le câble, on essaie des choses au hasard : on redémarre le service, on désactive le pare-feu, on change de réseau Wi-Fi. Avec cette image, on sait à quel étage chercher.
Cette image, c'est le modèle en couches. Il est l'idée d'organisation la plus importante des réseaux, et ce cours entier s'en sert comme d'une carte. Cette leçon la pose, suit la requête de Camille octet par octet, et montre où ces couches vivent dans une machine Linux.
Les concepts
Le problème à résoudre
Faire communiquer deux programmes à travers un réseau demande de résoudre une liste de problèmes très différents :
- transformer des bits en signal électrique, en lumière ou en onde radio, et inversement ;
- désigner une machine parmi toutes celles qui partagent un même câble ou une même borne Wi-Fi ;
- trouver un chemin entre deux machines qui ne sont pas sur le même réseau ;
- livrer les données au bon programme parmi tous ceux qui tournent sur la machine ;
- remettre les données dans l'ordre, retransmettre ce qui s'est perdu, ne pas noyer un destinataire plus lent ;
- donner un sens aux octets : une requête, une réponse, un code d'état.
Un seul programme qui ferait tout cela serait ingérable, et devrait être réécrit à chaque nouveau type de câble. L'idée des couches est de découper : chaque couche résout une partie du problème, rend un service à la couche du dessus, et utilise le service de la couche du dessous sans savoir comment il est rendu. Le navigateur ignore s'il est en Wi-Fi ou en fibre ; la carte Wi-Fi ignore si elle transporte du HTTP ou de la vidéo.
Les quatre couches du modèle TCP/IP
La description de référence de l'architecture d'Internet est la RFC 1122, publiée en octobre 1989 sous la direction de Robert Braden. Elle décrit ce qu'un hôte (une machine qui n'est pas un routeur) doit implémenter, et l'organise en quatre couches :
| Couche | Rôle | Protocoles typiques | Ce qu'elle désigne |
|---|---|---|---|
| Application | le sens des échanges | HTTP, DNS, SSH, SMTP | une ressource, un nom |
| Transport | une communication de bout en bout entre deux programmes | TCP, UDP | un port |
| Internet | acheminer un paquet d'un réseau à l'autre, jusqu'à la machine de destination | IP (v4 et v6), ICMP | une adresse IP |
| Lien | transmettre une trame à une machine voisine, sur un même réseau local | Ethernet, Wi-Fi (802.11) | une adresse MAC |
Retenez la dernière colonne : chaque couche a son propre système d'adresses, et l'une des tâches du réseau est de passer de l'un à l'autre (un nom vers une adresse IP avec le DNS, une adresse IP vers une adresse MAC avec ARP, leçon 2).
Deux couches méritent qu'on insiste :
- La couche internet est la seule qui traverse tout. Un routeur lit l'en-tête IP pour décider où envoyer le paquet ; il ne regarde normalement ni le port, ni le contenu HTTP. IP est un service sans connexion et sans garantie : chaque paquet voyage seul, peut se perdre, arriver en double ou dans le désordre. La RFC 1122 parle de service de datagrammes « au mieux » (best effort).
- La couche transport n'existe que sur les deux machines aux extrémités. C'est là que TCP reconstruit, à partir de paquets non fiables, un flux d'octets fiable et ordonné (leçons 10 et 11). Les routeurs du milieu n'y participent pas.
L'encapsulation
Comment une couche « utilise le service » de celle du dessous ? En confiant ses données à la couche inférieure, qui les enveloppe dans son propre en-tête, puis les confie à son tour à la couche du dessous. C'est l'encapsulation : chaque couche traite tout ce qu'elle reçoit d'en haut comme une charge utile opaque, et n'ajoute que ce dont elle a besoin.
flowchart TB
A["Application : GET /sante HTTP/1.1 ... (82 octets)"]
T["Transport : en-tête TCP | données HTTP"]
I["Internet : en-tête IP | en-tête TCP | données HTTP"]
L["Lien : en-tête Ethernet | en-tête IP | en-tête TCP | données HTTP | FCS"]
A --> T --> I --> L
À l'arrivée, le chemin est inverse : chaque couche lit son en-tête, décide à qui confier le reste, et le retire. On parle de désencapsulation. Pour décider « à qui confier le reste », chaque en-tête contient un champ qui désigne le protocole du dessus :
- l'en-tête Ethernet contient un EtherType (
0x0800pour IPv4,0x86DDpour IPv6,0x0806pour ARP) ; - l'en-tête IP contient un numéro de protocole (
6pour TCP,17pour UDP,1pour ICMP) ; - l'en-tête TCP contient un port de destination, qui désigne le programme (
8000pour Gunicorn).
Ces valeurs sont visibles dans les en-têtes C du noyau Linux, que l'on trouve sur toute machine où les en-têtes de développement sont installés :
$ grep -E "define ETH_P_(IP|ARP|IPV6)\b" /usr/include/linux/if_ether.h
#define ETH_P_IP 0x0800 /* Internet Protocol packet */
#define ETH_P_ARP 0x0806 /* Address Resolution packet */
#define ETH_P_IPV6 0x86DD /* IPv6 over bluebook */
Les noms des unités
Chaque couche a un nom pour ce qu'elle transmet. La RFC 1122 les définit, et les employer correctement évite des malentendus dans un ticket d'incident :
| Couche | Unité | Définition (RFC 1122, section 1.3.3) |
|---|---|---|
| Lien | trame (frame) | en-tête de lien suivi d'un paquet |
| Internet | paquet (packet) ou datagramme IP | en-tête IP suivi de données (un datagramme entier ou un fragment) |
| Transport, TCP | segment | en-tête TCP suivi de données de l'application |
| Transport, UDP | datagramme (UDP) | en-tête UDP suivi de données |
| Application | message | ce que l'application envoie, ici une requête HTTP |
Dans la conversation courante, on dit souvent « paquet » pour tout. Ce n'est pas grave tant que l'on sait, en cas de besoin, préciser : « la trame est arrivée, mais le paquet a été rejeté par le pare-feu » décrit une situation exacte.
Ce que pèsent les en-têtes
Chaque enveloppe a un coût. Les tailles minimales à retenir :
| En-tête | Taille | Source |
|---|---|---|
| Ethernet | 14 octets (2 adresses MAC de 6 octets, 2 octets d'EtherType), plus 4 octets de somme de contrôle (FCS) à la fin | if_ether.h : ETH_HLEN 14, ETH_FCS_LEN 4 |
| IPv4 | 20 octets sans options | RFC 791 |
| IPv6 | 40 octets, taille fixe | RFC 8200 |
| TCP | 20 octets sans options ; 32 octets sous Linux avec l'option d'horodatage, active par défaut | RFC 9293, tcp(7) |
| UDP | 8 octets | RFC 768 |
Sur un câble Ethernet, chaque trame est en outre précédée d'un préambule de 8 octets et suivie d'un silence obligatoire équivalent à 12 octets. Une trame Ethernet transporte au plus 1 500 octets de paquet (ETH_DATA_LEN 1500) : c'est la MTU (Maximum Transmission Unit) d'Ethernet, qui reviendra à la leçon 6.
Le modèle OSI, et pourquoi on parle encore de « couche 7 »
Vous rencontrerez aussi un modèle à sept couches, le modèle OSI (Open Systems Interconnection), publié par l'ISO en 1983 (norme ISO 7498) :
| OSI | TCP/IP | |
|---|---|---|
| 7 | Application | Application |
| 6 | Présentation | Application |
| 5 | Session | Application |
| 4 | Transport | Transport |
| 3 | Réseau | Internet |
| 2 | Liaison de données | Lien |
| 1 | Physique | Lien (et matériel) |
L'histoire de ces deux modèles est celle d'une rivalité. Dans les années 1980, les grands opérateurs de télécommunications et les États soutenaient OSI, conçu en comité, et les protocoles qui allaient avec. Le gouvernement américain imposa même des produits conformes à OSI pour ses administrations à partir d'août 1990. Pendant ce temps, TCP/IP, issu de l'ARPANET et des universités, fonctionnait déjà, était disponible gratuitement dans Unix BSD, et se diffusait. L'historien Andrew Russell rapporte la formule d'un défenseur d'Internet de l'époque, Einar Stefferud : « OSI est un beau rêve, et TCP/IP est en train de le vivre ». Au milieu des années 1990, les protocoles OSI avaient pratiquement disparu.
Le vocabulaire d'OSI, lui, a survécu. Quand on dit qu'un répartiteur de charge travaille « en couche 4 », on veut dire qu'il répartit des connexions TCP sans lire leur contenu ; « en couche 7 », qu'il lit les requêtes HTTP et peut décider selon le chemin ou un en-tête. Un commutateur « de niveau 2 » aiguille des trames selon les adresses MAC ; un routeur « de niveau 3 » aiguille des paquets selon les adresses IP. Ces numéros viennent d'OSI, même quand les protocoles sont ceux de TCP/IP. Les couches 5 et 6 d'OSI n'ont pas d'équivalent distinct dans TCP/IP : leurs fonctions (sessions, chiffrement, encodage) sont prises en charge par l'application ou par des bibliothèques comme TLS, dont on dit, faute de mieux, qu'il est « entre la couche 4 et la couche 7 ».
Note
La RFC 1122 elle-même prévient que le découpage strict en couches est un modèle imparfait : les protocoles de couches différentes interagissent de façons complexes, et de bonnes conceptions « cassent » parfois volontairement la séparation. TCP, par exemple, doit connaître les adresses IP pour calculer sa somme de contrôle. Les couches sont une carte, pas un règlement.
Le principe de bout en bout
Pourquoi IP est-il si simple, sans garantie de livraison, alors que l'on aurait pu construire un réseau qui garantit tout ? La réponse tient dans un article de 1984, End-to-End Arguments in System Design, de Jerome Saltzer, David Reed et David Clark, au MIT. Leur argument, en substance : une fonction ne peut être réalisée complètement et correctement qu'avec la connaissance de l'application, aux extrémités de la communication ; la fournir dans le réseau lui-même est donc impossible, et une version partielle dans le réseau ne se justifie que comme amélioration des performances.
Leur exemple est le transfert de fichier « soigneux ». Pour que le fichier arrive intact sur le disque de B, il faut se protéger d'un disque défaillant en A, d'une erreur de copie dans un tampon mémoire en A ou en B, d'un paquet corrompu ou perdu sur le réseau. Un réseau parfaitement fiable ne protège que du troisième risque. Seule une vérification de bout en bout, par exemple une somme de contrôle du fichier calculée en A et vérifiée en B après écriture, protège de tous. Et si l'on fait cette vérification de toute façon, la fiabilité du réseau devient une simple optimisation.
Ce raisonnement a façonné Internet :
- le cœur du réseau (IP, les routeurs) reste simple et sans état : il achemine des paquets, au mieux ;
- l'intelligence (fiabilité, ordre, contrôle de débit, chiffrement) est aux extrémités, dans TCP, TLS et les applications.
C'est ce qui permet d'inventer un nouveau protocole applicatif sans demander la permission à aucun opérateur, et c'est pour cela que l'on représente souvent l'architecture d'Internet comme un sablier : beaucoup de technologies de lien en bas, beaucoup d'applications en haut, et un seul protocole au milieu, IP, que tout le monde parle.
flowchart TB
subgraph haut["Applications"]
direction LR
h1["HTTP"]
h2["DNS"]
h3["SSH"]
h4["SMTP"]
h5["..."]
end
subgraph milieu["Transport"]
direction LR
t1["TCP"]
t2["UDP"]
t3["QUIC (sur UDP)"]
end
ip["IP"]
subgraph bas["Liens"]
direction LR
b1["Ethernet"]
b2["Wi-Fi"]
b3["Fibre, 4G, 5G"]
b4["..."]
end
haut --> milieu --> ip --> bas
Pour vous, développeur ou exploitant, la conséquence est concrète : le réseau ne vous doit rien. Un paquet peut se perdre, une connexion peut être coupée au milieu d'une requête, un équipement intermédiaire peut redémarrer. Une application robuste réessaie, vérifie, et rend ses opérations rejouables sans dommage.
Une brève histoire
| Date | Étape |
|---|---|
| 1969 | Premiers nœuds de l'ARPANET, réseau de recherche financé par l'agence ARPA du ministère américain de la Défense |
| 1974 | Vinton Cerf et Robert Kahn publient la conception d'un protocole pour interconnecter des réseaux différents, l'ancêtre de TCP/IP |
| Septembre 1981 | Publication des RFC 791 (IP) et 793 (TCP), éditées par Jon Postel |
| 1er janvier 1983 | Date fixée par la RFC 801 pour basculer tout l'ARPANET de l'ancien protocole NCP vers TCP/IP. C'est souvent retenu comme la naissance d'Internet |
| Mai 1983 | L'ISO publie le modèle de référence OSI (ISO 7498) |
| Octobre 1989 | RFC 1122 et 1123, exigences pour les hôtes |
| Milieu des années 1990 | Les protocoles OSI sont abandonnés ; TCP/IP est partout |
| Août 2022 | RFC 9293, nouvelle spécification de TCP, qui remplace la RFC 793 et intègre quarante ans de corrections |
L'IETF et les RFC
Les protocoles d'Internet sont normalisés par l'IETF (Internet Engineering Task Force), une organisation ouverte où participent des individus, pas des pays. Sa devise officieuse vient d'une présentation de David Clark en 1992, citée dans la RFC 7282 : « Nous rejetons les rois, les présidents et les votes. Nous croyons au consensus approximatif et au code qui tourne. »
Ses documents s'appellent des RFC (Request for Comments), un nom modeste hérité de 1969. Quelques clés pour les lire :
- Une RFC ne change jamais. Une correction donne une nouvelle RFC, qui en « met à jour » (Updates) ou en « remplace » (Obsoletes) une autre. L'en-tête de chaque RFC l'indique. TCP était décrit par la RFC 793 de 1981, il l'est désormais par la RFC 9293 ; une page web qui cite encore la 793 n'est pas fausse, mais elle est ancienne.
- Toutes les RFC ne sont pas des normes. Le champ Category dit s'il s'agit d'une norme (Standards Track), d'une bonne pratique (Best Current Practice, BCP), d'une information, d'une expérience, ou parfois d'une plaisanterie publiée un 1er avril.
- Les mots en majuscules ont un sens précis. La BCP 14 (RFC 2119, complétée par la RFC 8174 de mai 2017) définit
MUST(obligatoire),SHOULD(recommandé, sauf bonne raison documentée),MAY(facultatif), et leurs négations. La RFC 8174 précise que ces mots n'ont ce sens que lorsqu'ils sont écrits en capitales : unshoulden minuscules est de l'anglais ordinaire. - Le site de référence est
rfc-editor.org, qui donne aussi les errata connus de chaque RFC.
En pratique
Nous allons suivre la requête de Camille couche par couche, puis observer une vraie connexion TCP sur votre machine, sans aucun accès au réseau extérieur : tout se passe sur l'interface locale (127.0.0.1). Les sorties ont été produites sur Ubuntu 24.04 avec LC_ALL=C.UTF-8.
Écrire la requête
Préparez un répertoire de travail et un faux point de santé, puis lancez un serveur HTTP minimal, celui de la bibliothèque standard de Python, sur le port 8000 :
$ mkdir -p ~/tcpip && cd ~/tcpip
$ echo '{"etat": "ok"}' > sante
$ python3 -m http.server 8000 --bind 127.0.0.1 &
--bind 127.0.0.1 le fait écouter uniquement sur l'interface locale : il est injoignable depuis une autre machine (leçon 3). Demandez maintenant à curl de montrer exactement ce qu'il envoie :
$ curl -s --trace-ascii trace.txt http://127.0.0.1:8000/sante
{"etat": "ok"}
$ sed -n '/Send header/,/^== Info/p' trace.txt
=> Send header, 82 bytes (0x52)
0000: GET /sante HTTP/1.1
0015: Host: 127.0.0.1:8000
002b: User-Agent: curl/8.5.0
0043: Accept: */*
0050:
== Info: HTTP 1.0, assume close after body
Voilà la couche application : 82 octets de texte, quatre lignes terminées chacune par un retour chariot et un saut de ligne, et une ligne vide qui marque la fin des en-têtes. Les nombres à gauche sont des positions en hexadécimal (0x15 = 21 : la deuxième ligne commence au 22e octet). Le serveur de Python répond en HTTP/1.0 et ferme la connexion après la réponse, d'où la dernière ligne d'information de curl.
Dérouler l'encapsulation
Sur le chemin réel de Camille, vers 203.0.113.10, le message HTTP est confié à TCP, qui ajoute son en-tête ; puis à IP ; puis au pilote de la carte réseau. Faisons les comptes pour ces 82 octets, avec les en-têtes que Linux utilise par défaut :
| Couche | Ajoute | Taille cumulée | Champ qui désigne la couche du dessus |
|---|---|---|---|
| Application (HTTP) | 82 octets de requête | 82 | |
| Transport (TCP) | 32 octets (20 + 12 d'horodatage) | 114 | port de destination 80 (ou 8000 sur sig-app-1) |
| Internet (IPv4) | 20 octets | 134 | protocole 6 (TCP) |
| Lien (Ethernet) | 14 octets d'en-tête + 4 de FCS | 152 | EtherType 0x0800 (IPv4) |
| Sur le câble | 8 de préambule + 12 de silence | 172 |
Sur les 172 « octets de temps » occupés sur le câble, 82 seulement sont la requête : moins de la moitié. Pour une requête aussi courte, les enveloppes coûtent plus cher que la lettre. À l'inverse, un segment TCP plein (1 448 octets de données dans 1 500 octets de paquet) occupe 1 538 octets sur le câble, soit 94 % d'efficacité. Retenez l'ordre de grandeur : beaucoup de petits messages coûtent cher, ce qui explique les efforts de HTTP/2 et HTTP/3 pour regrouper les échanges, et l'intérêt de grouper les écritures dans une application.
Tip
L'option d'horodatage TCP est active quand /proc/sys/net/ipv4/tcp_timestamps vaut 1, la valeur par défaut. Vérifiez-le avec cat /proc/sys/net/ipv4/tcp_timestamps : c'est la raison pour laquelle, sur un lien Ethernet, les segments TCP de Linux transportent 1 448 octets de données et non 1 460.
Observer une connexion vivante
Une connexion TCP ne dure souvent que quelques millisecondes : trop peu pour l'observer. Ouvrons-en une à la main avec Python, gardons-la ouverte quelques secondes avant d'envoyer la requête, et regardons-la pendant ce temps avec ss (socket statistics), l'outil d'iproute2 qui remplace l'ancien netstat :
$ python3 - <<'EOF' &
import socket, time
s = socket.create_connection(("127.0.0.1", 8000))
time.sleep(3)
s.sendall(b"GET /sante HTTP/1.1\r\nHost: 127.0.0.1:8000\r\n\r\n")
print(s.recv(4096).decode())
EOF
$ ss -tnp 'sport = :8000 or dport = :8000'
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
ESTAB 0 0 127.0.0.1:8000 127.0.0.1:50196 users:(("python3",pid=663506,fd=4))
ESTAB 0 0 127.0.0.1:50196 127.0.0.1:8000 users:(("python3",pid=664221,fd=3))
Les options : -t pour TCP seulement, -n pour afficher les ports en chiffres plutôt qu'en noms de services, -p pour afficher le processus (seulement pour vos propres processus, sauf en root). Le filtre entre apostrophes garde les sockets dont le port source ou destination est 8000.
Lisez les deux lignes : une connexion TCP a deux extrémités, et comme le client et le serveur sont sur la même machine, on voit les deux. Côté serveur, la socket locale est 127.0.0.1:8000 et le pair 127.0.0.1:50196 ; côté client, c'est l'inverse. Le port 50196 n'a été choisi par personne : le noyau l'a pris dans la plage des ports éphémères, 32768 à 60999 par défaut (/proc/sys/net/ipv4/ip_local_port_range, leçon 9). Le couple d'adresses et de ports des deux extrémités, avec le protocole, identifie la connexion de façon unique : c'est le quintuplet (protocole, adresse source, port source, adresse destination, port destination), que l'on retrouvera dans le NAT (leçon 7) et dans les pare-feu à état.
Le serveur a d'ailleurs deux sockets : celle qui écoute (état LISTEN, descripteur 3), et celle de cette connexion (descripteur 4), créée par le noyau quand la connexion a été acceptée. ss -ltn montre les premières.
Ce que le noyau sait
ss lit ses informations dans le noyau. La vieille interface texte, toujours présente, est /proc/net/tcp. Pendant que la connexion était ouverte :
$ head -1 /proc/net/tcp
sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
$ grep 1F40 /proc/net/tcp
14: 0100007F:1F40 00000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 18357213 1 0000000000000000 100 0 0 10 0
55: 0100007F:1F40 0100007F:C414 01 00000000:00000000 00:00000000 00000000 1000 0 18370755 1 0000000000000000 20 0 0 10 -1
60: 0100007F:C414 0100007F:1F40 01 00000000:00000000 00:00000000 00000000 1000 0 18372748 1 0000000000000000 20 0 0 10 -1
61: 0100007F:DD42 0100007F:1F40 06 00000000:00000000 03:00000B06 00000000 0 0 0 3 0000000000000000
Tout est en hexadécimal :
1F40vaut 8000,C414vaut 50196 : on retrouve nos ports.0100007Fest l'adresse127.0.0.1, écrite à l'envers. Le noyau affiche l'entier de 32 bits tel qu'il est rangé en mémoire sur un processeur x86, dont l'ordre des octets (little-endian) est l'inverse de l'ordre du réseau (big-endian, l'octet de poids fort d'abord).7F 00 00 01lu dans l'ordre du réseau, c'est 127.0.0.1. Ce genre de détail est la raison d'être des fonctionshtonsetntohlen C.- La colonne
stest l'état :0ApourLISTEN,01pourESTABLISHED,06pourTIME_WAIT. La dernière ligne est une connexion précédente vers le port 8000, déjà fermée, que le noyau garde un moment enTIME_WAIT(leçon 10). uidest l'utilisateur propriétaire de la socket,inodel'identifiant de la socket, que l'on retrouve dans/proc/<pid>/fd/.
Vous pouvez vérifier les conversions avec Python :
$ python3 -c "print(0x1F40, 0xC414, 0x0100007F)"
8000 50196 16777343
Arrêtez le serveur avec kill %1 (ou fg puis Ctrl+C).
Sous le capot
Où vit la pile. Dans Linux, comme dans la plupart des systèmes d'exploitation, les couches transport, internet et une partie de la couche lien sont dans le noyau. L'application ne fabrique pas d'en-tête TCP ni de paquet IP : elle demande au noyau une socket (appel système socket(2)), lui demande de se connecter (connect(2)) ou d'écouter (bind(2), listen(2), accept(2)), puis lit et écrit des octets (read, write, send, recv). Le noyau découpe, ajoute les en-têtes, retransmet, réordonne. La couche lien est partagée entre le noyau et le pilote de la carte, qui programme le matériel ; la carte elle-même calcule souvent la somme de contrôle Ethernet et, sur les cartes récentes, une partie des sommes de contrôle IP et TCP (offload).
flowchart TB
app["Application (curl, Gunicorn)<br/>espace utilisateur"]
sock["Socket : socket(), connect(), send(), recv()"]
tcp["TCP / UDP"]
ip["IP : routage, fragmentation"]
neigh["Voisinage : ARP, NDP"]
drv["Pilote de la carte"]
nic["Carte réseau (matériel)"]
app --> sock
subgraph noyau["Noyau Linux"]
sock --> tcp --> ip --> neigh --> drv
end
drv --> nic
Ce découpage a deux conséquences pratiques. D'abord, une application ne voit jamais les paquets : pour les observer, il faut un outil qui demande au noyau une copie de ce qui passe sur l'interface (tcpdump, leçon 12), ce qui exige des privilèges. Ensuite, beaucoup de réglages du réseau sont des réglages du noyau, visibles et modifiables sous /proc/sys/net/ : la plage de ports éphémères, les délais d'ARP, le transfert de paquets entre interfaces.
Pourquoi le serveur a deux sockets. Une socket en écoute ne transporte aucune donnée : elle sert de file d'attente pour les connexions entrantes. Quand un client se connecte, le noyau réalise la poignée de main TCP tout seul, puis range la connexion établie dans la file. accept(2) la retire de la file et rend un nouveau descripteur. C'est pour cela que l'on voit un LISTEN et un ESTAB côte à côte, et que la colonne Send-Q d'une socket en écoute indique en fait la taille maximale de cette file (5 pour le petit serveur de Python, avec ss -ltn).
L'interface locale n'est pas un raccourci magique. Le trafic vers 127.0.0.1 traverse bien la pile TCP et IP du noyau, avec ses en-têtes ; seule la couche lien est simulée par l'interface lo, qui « renvoie » chaque paquet à la machine elle-même. C'est ce qui rend l'expérience de cette leçon représentative.
Pièges courants
Confondre les erreurs des différentes couches. Could not resolve host : le nom n'a pas été traduit en adresse (DNS, application). No route to host ou Network is unreachable : la couche internet ne sait pas où envoyer le paquet (routage, leçon 5). Connection timed out : les paquets partent, mais rien ne revient (filtrage silencieux, machine éteinte, route de retour absente). Connection refused : la machine a répondu, mais aucun programme n'écoute sur ce port (transport). Rien que cette lecture divise le temps de diagnostic.
Croire que le réseau est fiable. C'est la première des « illusions de l'informatique répartie » souvent attribuées à Peter Deutsch. Le principe de bout en bout le dit autrement : la garantie est aux extrémités. Un appel réseau dans une application doit avoir un délai d'attente, une politique de nouvelle tentative et une réponse prévue à l'échec.
Lire « couche 4 » et « couche 7 » comme des notions vagues. Un répartiteur L4 ne voit que des connexions TCP : il ne peut pas router selon le chemin /sante, ni ajouter un en-tête X-Forwarded-For, ni terminer TLS. Un répartiteur L7 le peut, mais il lit vos requêtes, donc il doit déchiffrer le trafic. Le choix a des conséquences sur la sécurité et les fonctionnalités, comme on l'a vu avec le répartiteur de Scaleway dans le cours Le cloud : les fondamentaux.
Citer une RFC remplacée. Avant d'appuyer un argument sur une RFC, regardez son en-tête : si elle est Obsoleted by une autre, lisez la plus récente. Et vérifiez sa catégorie : une RFC Informational n'oblige personne.
Oublier les octets d'en-tête dans un calcul de débit. Un lien à 1 Gbit/s ne transporte pas 125 Mo/s de données utiles : préambules, en-têtes et accusés de réception en prennent une part, d'autant plus grande que les messages sont petits.
Sécurité
Le modèle en couches éclaire plusieurs réalités de la sécurité :
- Chaque couche a ses attaques. Usurpation d'adresse MAC ou empoisonnement ARP en couche 2 (leçon 2), usurpation d'adresse IP en couche 3, inondation de demandes de connexion TCP en couche 4, injections et contournements d'authentification en couche 7. Un pare-feu qui ne filtre que les adresses et les ports (couches 3 et 4) ne voit rien d'une attaque dans une requête HTTP valide.
- Les en-têtes ne sont pas authentifiés. Ni IP, ni TCP, ni Ethernet ne prouvent l'identité de l'expéditeur : une adresse source s'écrit comme on veut, et ce sont les équipements du réseau qui, au mieux, filtrent les incohérences. L'authentification et la confidentialité relèvent d'un protocole conçu pour cela, de bout en bout : TLS (cours TLS et PKI), SSH, ou un tunnel chiffré.
- Le principe de bout en bout s'applique au chiffrement. Un réseau « sûr » (un réseau privé d'entreprise, un réseau privé virtuel chez un fournisseur de cloud) ne remplace pas le chiffrement entre les applications : il protège de certaines menaces, pas d'un équipement compromis au milieu ni d'une erreur de configuration. C'est l'idée qui sous-tend le zero trust.
- Les métadonnées restent visibles. Même avec TLS, les en-têtes IP et TCP circulent en clair : qui parle à qui, quand, combien. Le chiffrement protège le contenu, pas le fait de communiquer.
En production
- Diagnostiquez de bas en haut, ou de haut en bas, mais par couche. Le lien est-il actif ? L'adresse est-elle configurée ? La route existe-t-elle ? Le port répond-il ? L'application répond-elle correctement ? La leçon 12 transforme cette idée en méthode.
- Les équipements du milieu ne sont pas tous des routeurs simples. Pare-feu, traducteurs d'adresses, répartiteurs de charge et proxys lisent ou modifient des couches qui, en théorie, ne les regardent pas. Ces middleboxes expliquent bien des comportements surprenants, comme une connexion inactive coupée au bout de quelques minutes par un pare-feu à état qui a oublié son existence.
- Les petites requêtes coûtent cher à grande échelle. Une API qui fait dix appels de cent octets au lieu d'un appel de mille paie dix fois les en-têtes, les allers-retours et souvent les poignées de main.
- Chez Lyneko, l'application Signalements est exposée par un répartiteur de charge HTTP (couche 7) qui termine TLS et transmet les requêtes en clair aux instances dans un réseau privé, comme dans le cours Le cloud : les fondamentaux : un choix de couche qui détermine ce que l'on peut journaliser, router et protéger.
Exercices
1. Quelle couche ? (niveau 100). Pour chacune des situations suivantes, dites quelle couche est en cause et quelle adresse ou quel identifiant est concerné : (a) curl affiche Could not resolve host: api.signalements.example ; (b) curl affiche Connection refused vers 203.0.113.10:80 ; (c) le voyant de la carte réseau de sig-app-1 est éteint ; (d) un répartiteur de charge renvoie la requête /admin vers un autre groupe de serveurs que /sante.
Solution
(a) Application : le nom n'a pas pu être traduit en adresse IP, le DNS est en cause (ou le nom est faux). (b) Transport : la machine 203.0.113.10 a répondu, mais rien n'écoute sur le port 80 ; le paquet est bien arrivé, la couche internet fonctionne. (c) Lien, et même en dessous (physique en OSI) : rien ne peut passer, quelle que soit l'adresse. (d) Application (couche 7 en vocabulaire OSI) : seul un équipement qui lit la requête HTTP peut distinguer /admin de /sante.
2. Compter les octets (niveau 100). Une application envoie un message de 1 000 octets par UDP sur IPv4 et Ethernet. Combien d'octets fait la trame (FCS comprise) ? Combien d'octets de temps occupe-t-elle sur le câble ? Même question avec TCP sous Linux (horodatage actif).
Solution
UDP : 1 000 + 8 (UDP) + 20 (IPv4) + 14 (Ethernet) + 4 (FCS) = 1 046 octets de trame, et 1 046 + 8 + 12 = 1 066 octets de temps sur le câble. TCP : 1 000 + 32 + 20 + 14 + 4 = 1 070 octets de trame, 1 090 sur le câble. Dans les deux cas, le paquet IP (1 028 ou 1 052 octets) tient sous la MTU de 1 500 : pas de découpage.
3. Lire /proc/net/tcp (niveau 100). Une ligne de /proc/net/tcp contient 0B14A8C0:0016 6401A8C0:D2F0 01. Traduisez-la : adresses, ports, état.
Solution
Adresse locale 0B14A8C0 : les octets à l'envers donnent C0 A8 14 0B, soit 192.168.20.11. Port local 0x0016 = 22 (SSH). Adresse distante 6401A8C0 donne C0 A8 01 64, soit 192.168.1.100. Port distant 0xD2F0 = 54000, un port éphémère. État 01 : ESTABLISHED. C'est une session SSH établie depuis 192.168.1.100 vers cette machine. Vérification : python3 -c "import ipaddress; print(ipaddress.ip_address(bytes.fromhex('0B14A8C0')[::-1]))".
4. Bout en bout (niveau 100). L'équipe propose de supprimer la vérification de la somme SHA-256 des sauvegardes de la base de Signalements, au motif que le transfert vers le stockage objet se fait en HTTPS, « donc sans erreur possible ». Que répondez-vous, avec l'argument de Saltzer, Reed et Clark ?
Solution
HTTPS protège le transfert sur le réseau (intégrité et confidentialité entre les deux extrémités de la connexion TLS), mais pas le reste du chemin : la lecture du fichier sur le disque source, une erreur dans le programme de sauvegarde, une copie en mémoire, l'écriture côté stockage, une corruption ultérieure. Seule une vérification entre les vraies extrémités, la somme calculée sur la sauvegarde d'origine et vérifiée sur le fichier relu après dépôt, couvre tous ces risques. TLS est une optimisation utile, pas une preuve que la sauvegarde est intacte. On garde la vérification, et on la complète par un test de restauration.
Récapitulatif
- Le modèle TCP/IP découpe la communication en quatre couches (RFC 1122) : lien (adresse MAC, trame), internet (adresse IP, paquet), transport (port, segment TCP ou datagramme UDP), application (message).
- Chaque couche encapsule les données de la couche du dessus dans son en-tête, et indique qui doit les traiter ensuite : EtherType, numéro de protocole, port.
- Tailles à retenir : Ethernet 14 + 4, IPv4 20, IPv6 40, TCP 20 (32 sous Linux avec horodatage), UDP 8 ; MTU Ethernet 1 500 octets.
- Le modèle OSI à sept couches a perdu la bataille des protocoles mais gagné celle du vocabulaire : « couche 2 », « couche 4 », « couche 7 ».
- Le principe de bout en bout garde le cœur du réseau simple et place la fiabilité, l'ordre et le chiffrement aux extrémités : le réseau ne garantit rien, l'application doit le prévoir.
- Sous Linux, la pile réseau est dans le noyau ; les applications passent par des sockets ;
sset/proc/net/tcpmontrent l'état des connexions. - Les RFC font foi : regarder si elles sont remplacées, leur catégorie, et lire
MUSTetSHOULDen capitales selon la BCP 14.
Pour aller plus loin
- La RFC 1122, sections 1.1 et 1.3 : une vingtaine de pages qui posent l'architecture, toujours d'actualité.
- L'article de Saltzer, Reed et Clark, douze pages, l'un des textes les plus cités de l'histoire des réseaux.
- TCP/IP Illustrated, Volume 1, de Kevin Fall et W. Richard Stevens, chapitre 1, pour une présentation de l'architecture par ceux qui l'ont le mieux expliquée.
- La leçon suivante, qui descend à la couche la plus basse que l'on manipule au quotidien : Ethernet et la traduction des adresses IP en adresses MAC.
Sources
- RFC 1122, Requirements for Internet Hosts, Communication Layers (R. Braden, octobre 1989)
- J. H. Saltzer, D. P. Reed, D. D. Clark, End-to-End Arguments in System Design (ACM Transactions on Computer Systems, 1984)
- RFC 801, NCP/TCP Transition Plan (J. Postel, novembre 1981)
- A. L. Russell, OSI: The Internet That Wasn't (IEEE Spectrum, 2013)
- RFC 2119 et RFC 8174 (BCP 14), mots-clés des exigences
- RFC 7282, On Consensus and Humming in the IETF (P. Resnick, 2014)
- K. R. Fall, W. R. Stevens, TCP/IP Illustrated, Volume 1, 2e édition (Addison-Wesley, 2011), chapitre 1
- Linux man-pages : ip(7), tcp(7), socket(7), ss(8)
- Linux, en-têtes du noyau : include/uapi/linux/if_ether.h