Chapitre 3 — La couche liaison de données#
La couche liaison (couche 2 OSI) organise les bits en trames, gère l’accès au support partagé et assure une communication fiable entre deux nœuds directement connectés. C’est ici que vivent les adresses MAC, les switches et les VLANs.
La trame Ethernet#
Ethernet (IEEE 802.3) est le protocole de couche liaison dominant dans les réseaux locaux filaires depuis les années 1980.
Structure d’une trame Ethernet II#
Description des champs#
Champ |
Taille |
Rôle |
|---|---|---|
Préambule |
7 octets |
Synchronisation horloge : alternance 10101010 × 7 |
SFD (Start Frame Delimiter) |
1 octet |
Marqueur de début de trame : 10101011 |
MAC destination |
6 octets |
Adresse physique du destinataire |
MAC source |
6 octets |
Adresse physique de l’émetteur |
EtherType |
2 octets |
Protocole encapsulé : 0x0800=IPv4, 0x86DD=IPv6, 0x0806=ARP |
Données (payload) |
46–1500 octets |
Données de couche supérieure (paquet IP, etc.) |
FCS (Frame Check Sequence) |
4 octets |
CRC-32 pour détecter les erreurs de transmission |
MTU et fragmentation
La taille maximale du payload Ethernet est 1500 octets : c’est le MTU (Maximum Transmission Unit). Si un paquet IP est plus grand, il doit être fragmenté avant encapsulation. Les trames jumbo (jusqu’à 9000 octets de payload) sont supportées dans certains réseaux d’entreprise et data centers.
Construction d’une trame Ethernet en Python#
import struct
def crc32(data: bytes) -> int:
"""Calcule un CRC-32 simple (algorithme standard Ethernet)."""
crc = 0xFFFFFFFF
for byte in data:
crc ^= byte
for _ in range(8):
if crc & 1:
crc = (crc >> 1) ^ 0xEDB88320
else:
crc >>= 1
return crc ^ 0xFFFFFFFF
def construire_trame_ethernet(mac_dst: str, mac_src: str,
ethertype: int, payload: bytes) -> bytes:
"""Construit une trame Ethernet II complète avec FCS."""
def parse_mac(mac: str) -> bytes:
return bytes(int(x, 16) for x in mac.split(":"))
en_tete = parse_mac(mac_dst) + parse_mac(mac_src) + struct.pack(">H", ethertype)
trame_sans_fcs = en_tete + payload
fcs = crc32(trame_sans_fcs)
return trame_sans_fcs + struct.pack("<I", fcs) # FCS en little-endian
def decoder_trame_ethernet(trame: bytes) -> dict:
"""Décode les champs d'une trame Ethernet II."""
def bytes_to_mac(b: bytes) -> str:
return ":".join(f"{x:02x}" for x in b)
mac_dst = bytes_to_mac(trame[0:6])
mac_src = bytes_to_mac(trame[6:12])
ethertype = struct.unpack(">H", trame[12:14])[0]
payload = trame[14:-4]
fcs = struct.unpack("<I", trame[-4:])[0]
fcs_calc = crc32(trame[:-4])
return {
"MAC destination": mac_dst,
"MAC source": mac_src,
"EtherType": f"0x{ethertype:04X}",
"Taille payload": len(payload),
"FCS reçu": f"0x{fcs:08X}",
"FCS calculé": f"0x{fcs_calc:08X}",
"FCS valide": fcs == fcs_calc,
}
# Exemple : trame Ethernet transportant un payload IPv4 fictif
payload_ipv4 = b"\x45\x00\x00\x28" + b"\x00" * 36 # En-tête IPv4 factice
trame = construire_trame_ethernet(
mac_dst="ff:ff:ff:ff:ff:ff", # Broadcast
mac_src="aa:bb:cc:dd:ee:ff",
ethertype=0x0800, # IPv4
payload=payload_ipv4
)
print(f"Trame construite ({len(trame)} octets) :")
print(f" Hex : {trame.hex(' ')}\n")
champs = decoder_trame_ethernet(trame)
for cle, val in champs.items():
print(f" {cle:<22} : {val}")
Trame construite (58 octets) :
Hex : ff ff ff ff ff ff aa bb cc dd ee ff 08 00 45 00 00 28 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 11 85 c3 3a
MAC destination : ff:ff:ff:ff:ff:ff
MAC source : aa:bb:cc:dd:ee:ff
EtherType : 0x0800
Taille payload : 40
FCS reçu : 0x3AC38511
FCS calculé : 0x3AC38511
FCS valide : True
Adresses MAC#
Une adresse MAC (Media Access Control) est un identifiant unique sur 48 bits (6 octets) gravé dans la carte réseau (NIC) lors de sa fabrication.
Structure d’une adresse MAC#
OUI (3 octets) NIC-specific (3 octets)
┌─────────────────┐ ┌─────────────────────┐
│ Constructeur │ │ Numéro de série │
│ aa : bb : cc │ : │ dd : ee : ff │
└─────────────────┘ └─────────────────────┘
bit b0=0 : unicast / b0=1 : multicast
bit b1=0 : globalement unique / b1=1 : local
def analyser_mac(mac: str) -> dict:
"""Analyse une adresse MAC et retourne ses propriétés."""
octets = [int(x, 16) for x in mac.split(":")]
premier_octet = octets[0]
oui = ":".join(f"{x:02X}" for x in octets[:3])
nic = ":".join(f"{x:02X}" for x in octets[3:])
est_multicast = bool(premier_octet & 0x01)
est_local = bool(premier_octet & 0x02)
est_broadcast = all(o == 0xFF for o in octets)
return {
"Adresse": mac,
"OUI": oui,
"NIC": nic,
"Type": "broadcast" if est_broadcast else ("multicast" if est_multicast else "unicast"),
"Portée": "locale (LAA)" if est_local else "universelle (UAA)",
}
adresses_test = [
"ff:ff:ff:ff:ff:ff", # Broadcast
"01:00:5e:00:00:01", # Multicast IPv4 (224.0.0.1)
"33:33:00:00:00:01", # Multicast IPv6
"00:1a:2b:3c:4d:5e", # Unicast universel
"02:42:ac:11:00:03", # Docker (administrativement assigné)
]
print(f"{'Adresse MAC':<22} {'Type':<12} {'Portée':<20} {'OUI':<10} {'NIC'}")
print("─" * 85)
for mac in adresses_test:
info = analyser_mac(mac)
print(f"{info['Adresse']:<22} {info['Type']:<12} {info['Portée']:<20} "
f"{info['OUI']:<10} {info['NIC']}")
Adresse MAC Type Portée OUI NIC
─────────────────────────────────────────────────────────────────────────────────────
ff:ff:ff:ff:ff:ff broadcast locale (LAA) FF:FF:FF FF:FF:FF
01:00:5e:00:00:01 multicast universelle (UAA) 01:00:5E 00:00:01
33:33:00:00:00:01 multicast locale (LAA) 33:33:00 00:00:01
00:1a:2b:3c:4d:5e unicast universelle (UAA) 00:1A:2B 3C:4D:5E
02:42:ac:11:00:03 unicast locale (LAA) 02:42:AC 11:00:03
ARP — Address Resolution Protocol#
L’ARP (RFC 826) résout les adresses IP en adresses MAC sur un réseau local. Avant d’envoyer un paquet IP à 192.168.1.1, une machine doit connaître son adresse MAC.
Fonctionnement d’ARP#
ARP Request (broadcast) : « Qui possède l’IP 192.168.1.1 ? Dites-le à 192.168.1.10 »
ARP Reply (unicast) : « C’est moi, 192.168.1.1, et ma MAC est aa:bb:cc:dd:ee:ff »
La réponse est stockée dans le cache ARP pour éviter de redemander.
ARP Gratuitous et ARP Poisoning#
ARP Gratuitous : Une machine annonce sa propre IP en ARP Request (src = dst = sa propre IP). Utilisé au démarrage pour détecter les conflits d’IP ou pour mettre à jour les caches après un changement de MAC.
ARP Poisoning (attaque) : Un attaquant envoie de faux ARP Reply pour associer son adresse MAC à l’IP d’une victime. Résultat : le trafic de la victime est redirigé vers l’attaquant (Man-in-the-Middle). Contre-mesure : ARP inspection dynamique sur les switches (DAI — Dynamic ARP Inspection).
Switches et apprentissage MAC#
Un switch Ethernet est un équipement de couche 2 qui transmet les trames de façon intelligente, en apprenant les adresses MAC de chaque port.
Table CAM (Content Addressable Memory)#
Le switch maintient une table CAM associant adresses MAC et ports :
Adresse MAC |
Port |
VLAN |
Âge (s) |
|---|---|---|---|
aa:bb:cc:dd:ee:ff |
1 |
10 |
15 |
11:22:33:44:55:66 |
3 |
10 |
42 |
de:ad:be:ef:ca:fe |
2 |
20 |
5 |
Processus d’apprentissage#
VLAN (Virtual LAN)#
Les VLANs (IEEE 802.1Q) permettent de segmenter logiquement un réseau physique en plusieurs réseaux virtuels isolés, sans avoir besoin de matériel séparé.
Avantages des VLANs#
Isolation : le trafic VLAN 10 (comptabilité) ne peut pas atteindre VLAN 20 (R&D) sans routage explicite
Sécurité : limitation de la propagation des broadcasts et des attaques L2
Flexibilité : regroupement logique indépendant de la localisation physique
Performance : réduction des domaines de broadcast
Tag 802.1Q#
Un tag VLAN de 4 octets est inséré dans la trame Ethernet entre le champ MAC source et le champ EtherType :
Décodage du TCI (Tag Control Information)#
def decoder_tci(tci_16bits: int) -> dict:
"""
Décode le champ TCI du tag 802.1Q.
TCI = PCP (3 bits) | DEI (1 bit) | VID (12 bits)
"""
pcp = (tci_16bits >> 13) & 0x07 # Priority Code Point
dei = (tci_16bits >> 12) & 0x01 # Drop Eligible Indicator
vid = tci_16bits & 0x0FFF # VLAN Identifier (0–4095)
pcp_labels = {
0: "Best Effort", 1: "Background", 2: "Excellent Effort",
3: "Critical Apps", 4: "Video < 100ms", 5: "Video < 10ms",
6: "Internetwork Control", 7: "Network Control",
}
return {
"TCI (hex)": f"0x{tci_16bits:04X}",
"PCP": f"{pcp} ({pcp_labels.get(pcp, '?')})",
"DEI": f"{dei} ({'éligible au drop' if dei else 'non éligible'})",
"VLAN ID": vid,
"VLAN réservé": vid in (0, 4095),
}
exemples_tci = [0x0001, 0x000A, 0xA014, 0xE064]
for tci in exemples_tci:
info = decoder_tci(tci)
print(f"TCI = 0x{tci:04X} ({tci:016b}b)")
for k, v in info.items():
print(f" {k:<18} : {v}")
print()
TCI = 0x0001 (0000000000000001b)
TCI (hex) : 0x0001
PCP : 0 (Best Effort)
DEI : 0 (non éligible)
VLAN ID : 1
VLAN réservé : False
TCI = 0x000A (0000000000001010b)
TCI (hex) : 0x000A
PCP : 0 (Best Effort)
DEI : 0 (non éligible)
VLAN ID : 10
VLAN réservé : False
TCI = 0xA014 (1010000000010100b)
TCI (hex) : 0xA014
PCP : 5 (Video < 10ms)
DEI : 0 (non éligible)
VLAN ID : 20
VLAN réservé : False
TCI = 0xE064 (1110000001100100b)
TCI (hex) : 0xE064
PCP : 7 (Network Control)
DEI : 0 (non éligible)
VLAN ID : 100
VLAN réservé : False
Ports Access et Trunk#
Type de port |
VLAN |
Utilisation |
|---|---|---|
Access |
Un seul VLAN |
Connexion d’un équipement terminal (PC, imprimante) |
Trunk |
Plusieurs VLANs taggés |
Lien entre deux switches, switch→routeur |
Hybrid |
Mix taggé/non taggé |
Certains constructeurs (Huawei, etc.) |
Spanning Tree Protocol (STP)#
Le problème des boucles L2#
Dans un réseau avec plusieurs switches et des chemins redondants, les trames peuvent tourner indéfiniment (absence de TTL en L2). Un broadcast storm peut saturer tout le réseau en quelques millisecondes.
Tempête de broadcast (broadcast storm)
Sans STP, une trame broadcast entre dans une boucle et est dupliquée exponentiellement. En moins d’une seconde, le réseau peut être entièrement saturé. STP (IEEE 802.1D) résout ce problème en bloquant logiquement certains ports pour éliminer les boucles.
Fonctionnement de STP#
Élection du Root Bridge : Le switch avec le plus petit BID (Bridge ID = priorité + MAC) devient la racine.
Calcul des chemins : Chaque switch calcule le chemin le moins coûteux vers le root bridge.
Blocage : Les ports créant des boucles sont mis en état Blocking (ne transmettent pas de données).
États STP d’un port#
État |
Durée typique |
Description |
|---|---|---|
Disabled |
— |
Port administrativement désactivé |
Blocking |
Indéfini |
Reçoit les BPDUs, ne transmet pas de données |
Listening |
15 s |
Écoute les BPDUs, prépare la transition |
Learning |
15 s |
Apprend les adresses MAC, pas encore de forwarding |
Forwarding |
Indéfini |
État normal : transmet et reçoit |
RSTP (Rapid STP, 802.1w) converge en moins d’une seconde contre 30–50 secondes pour STP classique.
Résumé#
Points clés à retenir
Une trame Ethernet contient les MAC dst/src, l’EtherType et un FCS (CRC-32) pour la détection d’erreurs.
L”ARP résout les IP en MAC par un broadcast; le cache ARP est vulnérable à l’ARP poisoning.
Un switch apprend les MAC sur ses ports et transmet (forwarding) ou inonde (flooding) selon sa table CAM.
Les VLANs (802.1Q) segmentent logiquement le réseau; les ports trunk transportent plusieurs VLANs taggés.
Le STP (Spanning Tree Protocol) élimine les boucles L2 en bloquant certains ports; RSTP converge en < 1 s.