---
jupytext:
  text_representation:
    extension: .md
    format_name: myst
    format_version: 0.13
    jupytext_version: 1.16.0
kernelspec:
  name: python3
  display_name: Python 3
---

# 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.

```{code-cell} python
:tags: [hide-input]
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

```{code-cell} python
:tags: [hide-input]
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()
```

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

```{admonition} MTU et fragmentation
:class: note
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

```{code-cell} 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}")
```

---

## 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
```

```{code-cell} python
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']}")
```

---

## 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.

```{code-cell} python
:tags: [hide-input]
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()
```

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

```{code-cell} python
:tags: [hide-input]
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()
```

---

## 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 :

```{code-cell} python
:tags: [hide-input]
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()
```

### Décodage du TCI (Tag Control Information)

```{code-cell} python
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()
```

### 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.

```{admonition} Tempête de broadcast (broadcast storm)
:class: note
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).

```{code-cell} python
:tags: [hide-input]
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()
```

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

```{code-cell} python
:tags: [hide-input]
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()
```

```{admonition} Points clés à retenir
:class: note
- 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.
```
