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.

Hide code cell source

import matplotlib
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import matplotlib.patheffects as pe
import numpy as np
import pandas as pd
import struct
import seaborn as sns
sns.set_theme(style="whitegrid", palette="muted")
plt.rcParams.update({
    "figure.dpi": 120,
    "font.family": "DejaVu Sans",
    "axes.spines.top": False,
    "axes.spines.right": False,
})

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#

Hide code cell source

fig, ax = plt.subplots(figsize=(14, 3.5))
ax.set_xlim(0, 14)
ax.set_ylim(0, 2)
ax.axis("off")
ax.set_title("Structure d'une trame Ethernet II", fontsize=13, fontweight="bold", pad=12)

champs = [
    ("Préambule\n7 octets", 1.5, "#7f8c8d"),
    ("SFD\n1 octet",        0.5, "#95a5a6"),
    ("MAC destination\n6 octets", 1.5, "#c0392b"),
    ("MAC source\n6 octets",      1.5, "#e74c3c"),
    ("EtherType\n2 octets",       0.8, "#2980b9"),
    ("Données (payload)\n46–1500 octets", 5.5, "#27ae60"),
    ("FCS\n4 octets",             0.7, "#8e44ad"),
]

x = 0.1
for label, width, color in champs:
    rect = mpatches.FancyBboxPatch((x, 0.3), width - 0.05, 1.2,
                                    boxstyle="round,pad=0.04",
                                    edgecolor="#444", facecolor=color, alpha=0.85, linewidth=1.5)
    ax.add_patch(rect)
    ax.text(x + (width - 0.05) / 2, 0.92, label, ha="center", va="center",
            fontsize=8, color="white", fontweight="bold")
    x += width

# Taille totale
ax.annotate("", xy=(13.9, 0.1), xytext=(0.1, 0.1),
            arrowprops=dict(arrowstyle="<->", color="#555555", lw=1.5))
ax.text(7.0, 0.05, "64 à 1518 octets (hors préambule/SFD)",
        ha="center", va="center", fontsize=8.5, color="#555555", fontstyle="italic")

plt.tight_layout()
plt.show()
_images/5cc240a089c1da08d7c9814b63ecf7f13f1e70c48fb324dcac38afd7096cd967.png

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#

  1. ARP Request (broadcast) : « Qui possède l’IP 192.168.1.1 ? Dites-le à 192.168.1.10 »

  2. ARP Reply (unicast) : « C’est moi, 192.168.1.1, et ma MAC est aa:bb:cc:dd:ee:ff »

  3. La réponse est stockée dans le cache ARP pour éviter de redemander.

Hide code cell source

fig, ax = plt.subplots(figsize=(12, 6))
ax.set_xlim(0, 12)
ax.set_ylim(0, 7)
ax.axis("off")
ax.set_title("Fonctionnement du protocole ARP", fontsize=13, fontweight="bold")

# Machines
for x, label, ip in [(2, "PC-A\n(demandeur)", "192.168.1.10"),
                      (10, "PC-B\n(cible)", "192.168.1.20")]:
    rect = mpatches.FancyBboxPatch((x - 0.8, 4.5), 1.6, 1.8,
                                    boxstyle="round,pad=0.1",
                                    edgecolor="#2c3e50", facecolor="#ecf0f1", linewidth=2)
    ax.add_patch(rect)
    ax.text(x, 5.6, label, ha="center", va="center", fontsize=9, fontweight="bold", color="#2c3e50")
    ax.text(x, 4.9, ip, ha="center", va="center", fontsize=8, color="#e74c3c")

# Câble réseau
ax.plot([2.8, 9.2], [5.1, 5.1], color="#7f8c8d", linewidth=3, solid_capstyle="round")

# Étape 1 : ARP Request (broadcast)
ax.annotate("", xy=(9.0, 4.8), xytext=(2.8, 4.8),
            arrowprops=dict(arrowstyle="-|>", color="#e74c3c", lw=2.5))
ax.text(6.0, 5.05,
        "ARP Request (BROADCAST)\n« Qui a 192.168.1.20 ? »",
        ha="center", va="bottom", fontsize=8.5, color="#e74c3c",
        bbox=dict(boxstyle="round,pad=0.3", fc="white", ec="#e74c3c", alpha=0.9))

# Étape 2 : ARP Reply (unicast)
ax.annotate("", xy=(2.8, 4.3), xytext=(9.0, 4.3),
            arrowprops=dict(arrowstyle="-|>", color="#27ae60", lw=2.5))
ax.text(6.0, 3.9,
        "ARP Reply (UNICAST)\n« 192.168.1.20 est à aa:bb:cc:dd:ee:ff »",
        ha="center", va="top", fontsize=8.5, color="#27ae60",
        bbox=dict(boxstyle="round,pad=0.3", fc="white", ec="#27ae60", alpha=0.9))

# Cache ARP
rect_cache = mpatches.FancyBboxPatch((0.3, 0.5), 4.5, 2.8,
                                       boxstyle="round,pad=0.1",
                                       edgecolor="#2980b9", facecolor="#eaf4fb", linewidth=1.5)
ax.add_patch(rect_cache)
ax.text(2.55, 3.1, "Cache ARP de PC-A", ha="center", va="center",
        fontsize=9, fontweight="bold", color="#2980b9")
cache_data = [
    ("192.168.1.1",  "00:1a:2b:3c:4d:5e", "statique"),
    ("192.168.1.20", "aa:bb:cc:dd:ee:ff", "dynamique"),
]
for i, (ip, mac, t) in enumerate(cache_data):
    y = 2.5 - i * 0.7
    ax.text(0.6, y, f"{ip}", fontsize=8, color="#333", va="center")
    ax.text(2.2, y, mac, fontsize=8, color="#333", va="center", family="monospace")
    ax.text(4.1, y, t, fontsize=8, color="#e67e22", va="center")
ax.text(0.6, 3.0, "IP", fontsize=8, color="#555", fontweight="bold", va="center")
ax.text(2.2, 3.0, "MAC", fontsize=8, color="#555", fontweight="bold", va="center")
ax.text(4.1, 3.0, "Type", fontsize=8, color="#555", fontweight="bold", va="center")

plt.tight_layout()
plt.show()
_images/54d478bd83d1b7ff8593505987289b4a5abfa2d1b943011abe7b08826091ca8d.png

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#

Hide code cell source

fig, axes = plt.subplots(1, 3, figsize=(15, 7))
fig.suptitle("Apprentissage MAC et forwarding dans un switch", fontsize=13, fontweight="bold")

def dessiner_switch(ax, titre, etat_table, trame_src=None, trame_dst=None,
                    fleche_src_port=None, fleche_dst_ports=None, note=""):
    ax.set_xlim(0, 10)
    ax.set_ylim(0, 10)
    ax.axis("off")
    ax.set_title(titre, fontsize=10, fontweight="bold", pad=8)

    # Switch central
    rect = mpatches.FancyBboxPatch((3.5, 3.5), 3, 3,
                                    boxstyle="round,pad=0.2",
                                    edgecolor="#2c3e50", facecolor="#bdc3c7", linewidth=2)
    ax.add_patch(rect)
    ax.text(5, 5.0, "Switch", ha="center", va="center",
            fontsize=10, fontweight="bold", color="#2c3e50")

    # Ports et machines
    machines = [
        ("Port 1\nA\naa:bb…", 1.0, 5.0),
        ("Port 2\nB\n11:22…", 9.0, 5.0),
        ("Port 3\nC\nde:ad…", 5.0, 9.0),
    ]
    port_positions = {1: (3.5, 5.0), 2: (6.5, 5.0), 3: (5.0, 6.5)}

    for label, mx, my in machines:
        ax.add_patch(mpatches.FancyBboxPatch((mx - 0.8, my - 0.6), 1.6, 1.2,
                                              boxstyle="round,pad=0.1",
                                              edgecolor="#2980b9", facecolor="#eaf4fb", linewidth=1.5))
        ax.text(mx, my, label, ha="center", va="center", fontsize=7.5, color="#2c3e50")

    # Câbles
    ax.plot([1.8, 3.5], [5.0, 5.0], color="#7f8c8d", linewidth=2)
    ax.plot([6.5, 8.2], [5.0, 5.0], color="#7f8c8d", linewidth=2)
    ax.plot([5.0, 5.0], [6.5, 7.1], color="#7f8c8d", linewidth=2)

    # Table CAM
    ax.add_patch(mpatches.FancyBboxPatch((0.2, 0.3), 9.6, 2.6,
                                          boxstyle="round,pad=0.1",
                                          edgecolor="#27ae60", facecolor="#eafaf1", linewidth=1.5))
    ax.text(5, 2.8, "Table CAM", ha="center", fontsize=9, fontweight="bold", color="#27ae60")
    for i, (mac, port) in enumerate(etat_table):
        couleur = "#e74c3c" if i == len(etat_table) - 1 and etat_table else "#333"
        ax.text(1.0, 2.2 - i * 0.55, mac, fontsize=7.5, color=couleur, va="center")
        ax.text(7.5, 2.2 - i * 0.55, f"Port {port}", fontsize=7.5,
                color=couleur, va="center", fontweight="bold")

    # Flèches de trames
    if fleche_src_port:
        px, py = port_positions[fleche_src_port]
        if fleche_src_port == 1:
            ax.annotate("", xy=(px, py), xytext=(px - 1.5, py),
                        arrowprops=dict(arrowstyle="-|>", color="#e74c3c", lw=2.5))
        else:
            ax.annotate("", xy=(px, py), xytext=(px + 1.5, py),
                        arrowprops=dict(arrowstyle="-|>", color="#e74c3c", lw=2.5))

    if fleche_dst_ports:
        for dp in fleche_dst_ports:
            px, py = port_positions[dp]
            if dp == 2:
                ax.annotate("", xy=(px + 1.5, py), xytext=(px, py),
                            arrowprops=dict(arrowstyle="-|>", color="#27ae60", lw=2.5,
                                           linestyle="dashed"))
            elif dp == 3:
                ax.annotate("", xy=(px, py + 1.3), xytext=(px, py),
                            arrowprops=dict(arrowstyle="-|>", color="#27ae60", lw=2.5,
                                           linestyle="dashed"))
            elif dp == 1:
                ax.annotate("", xy=(px - 1.5, py), xytext=(px, py),
                            arrowprops=dict(arrowstyle="-|>", color="#27ae60", lw=2.5,
                                           linestyle="dashed"))

    if note:
        ax.text(5, 0.05, note, ha="center", va="bottom", fontsize=8,
                color="#555", fontstyle="italic")

# Étape 1 : A envoie, table vide → flooding
dessiner_switch(axes[0], "Étape 1 : Flooding\n(table vide, A → B)",
                etat_table=[("aa:bb:cc…", 1)],
                fleche_src_port=1,
                fleche_dst_ports=[2, 3],
                note="A apprend sur port 1, inonde ports 2 et 3")

# Étape 2 : B répond, A connu
dessiner_switch(axes[1], "Étape 2 : B répond → A\n(A déjà appris)",
                etat_table=[("aa:bb:cc…", 1), ("11:22:33…", 2)],
                fleche_src_port=2,
                fleche_dst_ports=[1],
                note="B appris sur port 2, forwarding direct vers port 1")

# Étape 3 : table complète, communication directe
dessiner_switch(axes[2], "Étape 3 : Table complète\nForwarding intelligent",
                etat_table=[("aa:bb:cc…", 1), ("11:22:33…", 2), ("de:ad:be…", 3)],
                fleche_src_port=1,
                fleche_dst_ports=[2],
                note="Plus de flooding : forwarding unicast direct")

plt.tight_layout()
plt.show()
_images/7c0e3ee4d3ee4a091f303143302a86a8329ec2e1dd6e8a9b1e21e011abc477fc.png

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 :

Hide code cell source

fig, ax = plt.subplots(figsize=(14, 4))
ax.set_xlim(0, 14)
ax.set_ylim(0, 3)
ax.axis("off")
ax.set_title("Trame Ethernet avec tag 802.1Q", fontsize=13, fontweight="bold")

champs_normal = [
    ("MAC dst\n6 oct.", 1.5, "#c0392b"),
    ("MAC src\n6 oct.", 1.5, "#e74c3c"),
    ("EtherType\n2 oct.", 0.9, "#2980b9"),
    ("Payload\n46–1500 oct.", 6.3, "#27ae60"),
    ("FCS\n4 oct.", 0.7, "#8e44ad"),
]

# Trame normale (dessus)
ax.text(0.1, 2.7, "Trame normale :", fontsize=9, color="#555", va="center")
x = 0.8
for label, width, color in champs_normal:
    rect = mpatches.FancyBboxPatch((x, 2.1), width - 0.05, 0.6,
                                    boxstyle="round,pad=0.03", edgecolor="#444",
                                    facecolor=color, alpha=0.8, linewidth=1.2)
    ax.add_patch(rect)
    ax.text(x + (width-0.05)/2, 2.42, label, ha="center", va="center",
            fontsize=7.5, color="white", fontweight="bold")
    x += width

# Trame taguée (dessous)
champs_tag = [
    ("MAC dst\n6 oct.", 1.5, "#c0392b"),
    ("MAC src\n6 oct.", 1.5, "#e74c3c"),
    ("TPID\n0x8100", 0.6, "#f39c12"),
    ("TCI\n(PCP+DEI+VID)", 0.9, "#e67e22"),
    ("EtherType\n2 oct.", 0.9, "#2980b9"),
    ("Payload\n46–1500 oct.", 5.7, "#27ae60"),
    ("FCS\n4 oct.", 0.7, "#8e44ad"),
]

ax.text(0.1, 1.7, "Trame 802.1Q :", fontsize=9, color="#555", va="center")
x = 0.8
for label, width, color in champs_tag:
    rect = mpatches.FancyBboxPatch((x, 0.8), width - 0.05, 0.8,
                                    boxstyle="round,pad=0.03", edgecolor="#444",
                                    facecolor=color, alpha=0.85, linewidth=1.2)
    ax.add_patch(rect)
    ax.text(x + (width-0.05)/2, 1.22, label, ha="center", va="center",
            fontsize=7.5, color="white", fontweight="bold")
    if label.startswith("TPID") or label.startswith("TCI"):
        ax.annotate("", xy=(x + (width-0.05)/2, 1.62),
                    xytext=(x + (width-0.05)/2, 1.58),
                    arrowprops=dict(arrowstyle="-", color="#e67e22", lw=1))
    x += width

# Annotation du tag
ax.annotate("", xy=(3.45, 1.62), xytext=(3.45, 1.62))
ax.add_patch(mpatches.FancyBboxPatch((2.98, 0.15), 1.47, 0.55,
                                      boxstyle="round,pad=0.05",
                                      edgecolor="#e67e22", facecolor="#fef9e7",
                                      linewidth=1.5, linestyle="dashed"))
ax.text(3.72, 0.42, "Tag 802.1Q (4 octets)\nTPID=0x8100 | PCP(3b) | DEI(1b) | VID(12b)",
        ha="center", va="center", fontsize=7.5, color="#e67e22")

plt.tight_layout()
plt.show()
_images/63e0290392a2c0aad06018358ceb7c68699ae4e71031d620a47b53c868559858.png

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#

  1. Élection du Root Bridge : Le switch avec le plus petit BID (Bridge ID = priorité + MAC) devient la racine.

  2. Calcul des chemins : Chaque switch calcule le chemin le moins coûteux vers le root bridge.

  3. Blocage : Les ports créant des boucles sont mis en état Blocking (ne transmettent pas de données).

Hide code cell source

fig, axes = plt.subplots(1, 2, figsize=(14, 7))

def dessiner_topologie_stp(ax, titre, connexions, états_ports, root="SW1"):
    ax.set_xlim(0, 10)
    ax.set_ylim(0, 8)
    ax.axis("off")
    ax.set_title(titre, fontsize=11, fontweight="bold")

    switches = {
        "SW1": (5.0, 7.0),
        "SW2": (1.5, 4.0),
        "SW3": (8.5, 4.0),
        "SW4": (5.0, 1.2),
    }

    # Connexions
    couleur_etat = {"FWD": "#27ae60", "BLK": "#e74c3c", "ROOT": "#2980b9", "DESG": "#27ae60"}
    for (sw_a, sw_b), etat_a, etat_b in connexions:
        x1, y1 = switches[sw_a]
        x2, y2 = switches[sw_b]
        xm, ym = (x1 + x2) / 2, (y1 + y2) / 2
        couleur = "#e74c3c" if "BLK" in [etat_a, etat_b] else "#27ae60"
        style = "dashed" if "BLK" in [etat_a, etat_b] else "solid"
        ax.plot([x1, x2], [y1, y2], color=couleur, linewidth=2.5,
                linestyle=style, zorder=1)
        # Labels d'état
        offset = np.array([y2 - y1, -(x2 - x1)])
        offset = offset / (np.linalg.norm(offset) + 1e-9) * 0.35
        ax.text(x1 + (x2-x1)*0.25 + offset[0], y1 + (y2-y1)*0.25 + offset[1],
                etat_a, ha="center", va="center", fontsize=7.5,
                color=couleur_etat.get(etat_a, "#555"), fontweight="bold",
                bbox=dict(boxstyle="round,pad=0.15", fc="white", ec=couleur_etat.get(etat_a, "#555"), alpha=0.9))
        ax.text(x1 + (x2-x1)*0.75 + offset[0], y1 + (y2-y1)*0.75 + offset[1],
                etat_b, ha="center", va="center", fontsize=7.5,
                color=couleur_etat.get(etat_b, "#555"), fontweight="bold",
                bbox=dict(boxstyle="round,pad=0.15", fc="white", ec=couleur_etat.get(etat_b, "#555"), alpha=0.9))

    # Switches
    for sw, (x, y) in switches.items():
        is_root = (sw == root)
        color = "#f39c12" if is_root else "#2c3e50"
        ax.add_patch(mpatches.FancyBboxPatch((x - 0.7, y - 0.45), 1.4, 0.9,
                                              boxstyle="round,pad=0.1",
                                              edgecolor=color, facecolor="#ecf0f1",
                                              linewidth=3 if is_root else 2, zorder=2))
        ax.text(x, y, sw + (" ★" if is_root else ""), ha="center", va="center",
                fontsize=9.5, fontweight="bold", color=color, zorder=3)

    # Légende
    ax.text(0.2, 0.3, "★ Root Bridge   ─── FWD (Forwarding)   ╌╌╌ BLK (Blocking)",
            fontsize=7.5, color="#555", va="center")

# Topologie sans STP (avec boucle)
connexions_sans_stp = [
    (("SW1","SW2"), "?", "?"),
    (("SW1","SW3"), "?", "?"),
    (("SW2","SW4"), "?", "?"),
    (("SW3","SW4"), "?", "?"),
    (("SW2","SW3"), "?", "?"),  # Lien redondant
]

# Topologie avec STP (arbre couvrant)
connexions_avec_stp = [
    (("SW1","SW2"), "DESG", "ROOT"),
    (("SW1","SW3"), "DESG", "ROOT"),
    (("SW2","SW4"), "DESG", "ROOT"),
    (("SW3","SW4"), "DESG", "BLK"),  # Port bloqué
    (("SW2","SW3"), "DESG", "BLK"),  # Port bloqué
]

dessiner_topologie_stp(axes[0], "Sans STP — Boucles possibles",
                       connexions_sans_stp, {}, root=None)
axes[0].text(5, 0.05, "Risque de broadcast storm !", ha="center", fontsize=9,
             color="#e74c3c", fontweight="bold")

dessiner_topologie_stp(axes[1], "Avec STP (802.1D) — Arbre couvrant",
                       connexions_avec_stp, {}, root="SW1")
axes[1].text(5, 0.05, "Réseau sans boucle, chemins redondants bloqués",
             ha="center", fontsize=9, color="#27ae60", fontweight="bold")

plt.tight_layout()
plt.show()
_images/fa6fb70962718a8e6e259f29d7fb071b9b2ac19e5d1e227e65a3b95ff1941ee7.png

É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é#

Hide code cell source

fig, ax = plt.subplots(figsize=(12, 6))
ax.axis("off")

data = {
    "Concept": [
        "Trame Ethernet", "Adresse MAC", "ARP", "Switch / Table CAM",
        "VLAN 802.1Q", "STP"
    ],
    "Rôle": [
        "Unité de transmission L2 : MAC+EtherType+payload+FCS",
        "Identifiant physique 48 bits unique par interface",
        "Résolution IP → MAC sur le réseau local",
        "Forwarding intelligent L2, apprentissage automatique",
        "Segmentation logique du réseau, isolation broadcast",
        "Élimination des boucles L2, redondance sans tempête",
    ],
    "Détail clé": [
        "MTU=1500 o, min=64 o, EtherType 0x0800=IPv4",
        "OUI (3 o constructeur) + NIC (3 o), bit0=multicast",
        "Broadcast→Unicast, cache ARP, vulnérable au spoofing",
        "Flooding si MAC inconnue, ageing timer ~300 s",
        "TPID=0x8100, VID sur 12 bits (4094 VLANs max)",
        "Root Bridge = plus petit BID, RSTP < 1 s convergence",
    ],
}

df = pd.DataFrame(data)
tbl = ax.table(cellText=df.values, colLabels=df.columns,
               cellLoc="left", loc="center",
               colWidths=[0.18, 0.42, 0.40])
tbl.auto_set_font_size(False)
tbl.set_fontsize(8.5)
tbl.scale(1, 2.2)

for (row, col), cell in tbl.get_celld().items():
    if row == 0:
        cell.set_facecolor("#2c3e50")
        cell.set_text_props(color="white", fontweight="bold")
    elif row % 2 == 0:
        cell.set_facecolor("#eaf4fb")
    else:
        cell.set_facecolor("white")
    cell.set_edgecolor("#cccccc")

ax.set_title("Récapitulatif du chapitre 3", fontsize=13, fontweight="bold", pad=15)
plt.tight_layout()
plt.show()
_images/f8d0eb07d8bafa4264891a1024fe282f549e504e70fc5a34c7ca8942806e9bd2.png

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.