---
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 4 — La couche réseau : IP et routage

La couche réseau (couche 3 OSI) est le niveau où les paquets voyagent de leur source à leur destination à travers des réseaux hétérogènes. Elle définit l'adressage logique (IP), le routage et la fragmentation.

```{code-cell} python
:tags: [hide-input]
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import numpy as np
import pandas as pd
import ipaddress
import struct
import socket
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,
})
```

## IPv4 — Structure de l'en-tête

L'en-tête IPv4 fait **20 octets** minimum (sans options). Chaque champ a un rôle précis dans le routage et la fragmentation des paquets.

```{code-cell} python
:tags: [hide-input]
fig, ax = plt.subplots(figsize=(14, 8))
ax.set_xlim(0, 32)
ax.set_ylim(0, 11)
ax.axis("off")
ax.set_title("Structure de l'en-tête IPv4 (20 octets minimum, 32 bits par ligne)",
             fontsize=13, fontweight="bold", pad=12)

# Chaque ligne = 32 bits = 4 octets
lignes = [
    # (y, liste de (label, bits_largeur, color))
    (9.5, [("Version\n4 bits", 4, "#e74c3c"),
           ("IHL\n4 bits",    4, "#c0392b"),
           ("DSCP\n6 bits",   6, "#e67e22"),
           ("ECN\n2 bits",    2, "#f39c12"),
           ("Longueur totale\n16 bits", 16, "#f1c40f")]),
    (7.5, [("Identification\n16 bits", 16, "#27ae60"),
           ("Flags\n3 bits",  3, "#2ecc71"),
           ("Fragment Offset\n13 bits", 13, "#1abc9c")]),
    (5.5, [("TTL\n8 bits",    8, "#2980b9"),
           ("Protocole\n8 bits", 8, "#3498db"),
           ("Checksum en-tête\n16 bits", 16, "#5dade2")]),
    (3.5, [("Adresse IP source\n32 bits", 32, "#8e44ad")]),
    (1.5, [("Adresse IP destination\n32 bits", 32, "#9b59b6")]),
]

for y, champs in lignes:
    x = 0
    total_bits = sum(c[1] for c in champs)
    for label, bits, color in champs:
        width = (bits / 32) * 32
        rect = mpatches.FancyBboxPatch((x + 0.05, y + 0.05), width - 0.1, 1.7,
                                        boxstyle="round,pad=0.05",
                                        edgecolor="#444", facecolor=color, alpha=0.85, linewidth=1.2)
        ax.add_patch(rect)
        ax.text(x + width/2, y + 0.95, label, ha="center", va="center",
                fontsize=7.5, color="white", fontweight="bold")
        x += width

# Numéros de bits en haut
ax.text(0, 10.7, "0", ha="left", fontsize=8, color="#555")
ax.text(8, 10.7, "8", ha="center", fontsize=8, color="#555")
ax.text(16, 10.7, "16", ha="center", fontsize=8, color="#555")
ax.text(24, 10.7, "24", ha="center", fontsize=8, color="#555")
ax.text(32, 10.7, "31", ha="right", fontsize=8, color="#555")
ax.axhline(10.5, color="#cccccc", linewidth=0.8, linestyle="--")

# Annotations
annotations = [
    ("Version = 4\n(IPv4)", 2, 9.5 + 0.95, "#e74c3c"),
    ("TTL : durée de\nvie du paquet", -1, 5.5 + 0.95, "#2980b9"),
    ("Protocole : 6=TCP\n17=UDP, 1=ICMP", 35, 5.5 + 0.95, "#3498db"),
]
for text, x_off, y_pos, color in annotations:
    if x_off < 0:
        ax.text(-0.5, y_pos, text, ha="right", va="center", fontsize=7,
                color=color, bbox=dict(boxstyle="round", fc="white", ec=color, alpha=0.8))
    elif x_off > 33:
        ax.text(32.5, y_pos, text, ha="left", va="center", fontsize=7,
                color=color, bbox=dict(boxstyle="round", fc="white", ec=color, alpha=0.8))

plt.tight_layout()
plt.show()
```

### Description des champs IPv4

| Champ | Taille | Valeur / Rôle |
|-------|--------|---------------|
| **Version** | 4 bits | 4 pour IPv4 |
| **IHL** | 4 bits | Internet Header Length en mots de 32 bits (min=5 → 20 octets) |
| **DSCP** | 6 bits | Differentiated Services : priorité QoS |
| **ECN** | 2 bits | Explicit Congestion Notification |
| **Longueur totale** | 16 bits | Taille de l'en-tête + données (max 65 535 octets) |
| **Identification** | 16 bits | Identifiant pour rassembler les fragments |
| **Flags** | 3 bits | Bit DF (Don't Fragment), MF (More Fragments) |
| **Fragment Offset** | 13 bits | Position du fragment dans le paquet original |
| **TTL** | 8 bits | Time To Live : décrémenté de 1 par chaque routeur (max 255) |
| **Protocole** | 8 bits | Protocole encapsulé : 1=ICMP, 6=TCP, 17=UDP |
| **Checksum** | 16 bits | Somme de contrôle de l'en-tête uniquement |
| **IP source** | 32 bits | Adresse de l'émetteur |
| **IP destination** | 32 bits | Adresse du destinataire |

```{code-cell} python
def decoder_entete_ipv4(data: bytes) -> dict:
    """Décode les 20 premiers octets d'un en-tête IPv4."""
    if len(data) < 20:
        raise ValueError("Données trop courtes pour un en-tête IPv4")

    version_ihl   = data[0]
    version       = (version_ihl >> 4) & 0x0F
    ihl           = version_ihl & 0x0F
    dscp_ecn      = data[1]
    dscp          = (dscp_ecn >> 2) & 0x3F
    ecn           = dscp_ecn & 0x03
    longueur      = struct.unpack(">H", data[2:4])[0]
    identification= struct.unpack(">H", data[4:6])[0]
    flags_offset  = struct.unpack(">H", data[6:8])[0]
    flags         = (flags_offset >> 13) & 0x07
    offset        = flags_offset & 0x1FFF
    ttl           = data[8]
    protocole     = data[9]
    checksum      = struct.unpack(">H", data[10:12])[0]
    src           = socket.inet_ntoa(data[12:16])
    dst           = socket.inet_ntoa(data[16:20])

    protos = {1: "ICMP", 6: "TCP", 17: "UDP", 41: "IPv6", 89: "OSPF"}
    flags_str = []
    if flags & 0x02: flags_str.append("DF")
    if flags & 0x01: flags_str.append("MF")

    return {
        "Version":       version,
        "IHL":           f"{ihl} × 4 = {ihl*4} octets",
        "DSCP":          dscp,
        "ECN":           ecn,
        "Longueur":      f"{longueur} octets",
        "Identification": f"0x{identification:04X}",
        "Flags":         ", ".join(flags_str) if flags_str else "aucun",
        "Frag. Offset":  offset,
        "TTL":           ttl,
        "Protocole":     f"{protocole} ({protos.get(protocole, '?')})",
        "Checksum":      f"0x{checksum:04X}",
        "Source":        src,
        "Destination":   dst,
    }

# Exemple d'en-tête IPv4 : paquet TCP de 192.168.1.10 vers 8.8.8.8, TTL=64
en_tete = struct.pack(">BBHHHBBH4s4s",
    0x45,        # Version=4, IHL=5
    0x00,        # DSCP=0, ECN=0
    60,          # Longueur totale
    0x1234,      # Identification
    0x4000,      # Flags: DF, Offset=0
    64,          # TTL
    6,           # Protocole: TCP
    0x0000,      # Checksum (factice)
    socket.inet_aton("192.168.1.10"),
    socket.inet_aton("8.8.8.8"),
)

print("Décodage d'un en-tête IPv4 :")
print("-" * 40)
for champ, valeur in decoder_entete_ipv4(en_tete).items():
    print(f"  {champ:<20} : {valeur}")
```

---

## CIDR et sous-réseaux

### Notation CIDR

La notation **CIDR** (Classless Inter-Domain Routing) exprime un réseau par son adresse de base et le nombre de bits du masque : `192.168.1.0/24`.

Le masque `/24` signifie que les 24 premiers bits identifient le réseau et les 8 bits restants identifient les hôtes.

```{code-cell} python
import ipaddress

def analyser_reseau(notation_cidr: str) -> dict:
    """Analyse un réseau CIDR et retourne toutes ses propriétés."""
    net = ipaddress.ip_network(notation_cidr, strict=False)
    return {
        "Réseau":           str(net),
        "Adresse réseau":   str(net.network_address),
        "Masque":           str(net.netmask),
        "Masque inversé":   str(net.hostmask),
        "Broadcast":        str(net.broadcast_address),
        "Nb hôtes utiles":  net.num_addresses - 2 if net.prefixlen < 31 else net.num_addresses,
        "Premier hôte":     str(net.network_address + 1) if net.prefixlen < 31 else str(net.network_address),
        "Dernier hôte":     str(net.broadcast_address - 1) if net.prefixlen < 31 else str(net.broadcast_address),
        "Classe tradition.": "A" if net.prefixlen <= 8 else "B" if net.prefixlen <= 16 else "C" if net.prefixlen <= 24 else "sous-réseau",
    }

reseaux = ["10.0.0.0/8", "172.16.0.0/12", "192.168.1.0/24",
           "192.168.1.64/26", "10.10.5.0/29", "172.20.0.0/16"]

for r in reseaux:
    info = analyser_reseau(r)
    print(f"\n{'═'*50}")
    print(f"  {r}")
    for k, v in info.items():
        print(f"  {k:<22} : {v}")
```

### Découpage en sous-réseaux (subnetting)

```{code-cell} python
def decouper_reseau(reseau: str, nouveau_prefixe: int) -> list:
    """Découpe un réseau en sous-réseaux de taille identique."""
    net = ipaddress.ip_network(reseau, strict=False)
    sous_reseaux = list(net.subnets(new_prefix=nouveau_prefixe))
    return sous_reseaux

print("Découpage de 192.168.1.0/24 en /26 (64 hôtes chacun) :")
print("-" * 60)
for sr in decouper_reseau("192.168.1.0/24", 26):
    print(f"  {str(sr):<22} → hôtes: {str(sr.network_address + 1)}"
          f" – {str(sr.broadcast_address - 1)}")

print("\nDécoupage de 10.0.0.0/8 en /10 :")
print("-" * 60)
for sr in decouper_reseau("10.0.0.0/8", 10):
    n = ipaddress.ip_network(str(sr))
    print(f"  {str(sr):<20} ({n.num_addresses - 2:>8,} hôtes utiles)")
```

```{code-cell} python
:tags: [hide-input]
# Visualisation d'un plan d'adressage d'entreprise
fig, ax = plt.subplots(figsize=(12, 7))
ax.set_xlim(0, 12)
ax.set_ylim(0, 8)
ax.axis("off")
ax.set_title("Plan d'adressage CIDR — Réseau d'entreprise 10.0.0.0/8", fontsize=13, fontweight="bold")

services = [
    ("DMZ",             "10.0.1.0/24",  "#e74c3c", 0.5, 6.5),
    ("Serveurs",        "10.0.2.0/23",  "#e67e22", 3.0, 6.5),
    ("RH",              "10.1.0.0/24",  "#f39c12", 5.5, 6.5),
    ("Comptabilité",    "10.1.1.0/24",  "#27ae60", 8.0, 6.5),
    ("R&D",             "10.2.0.0/22",  "#2980b9", 0.5, 3.5),
    ("Wi-Fi invités",   "10.3.0.0/24",  "#8e44ad", 3.0, 3.5),
    ("Infrastructure",  "10.254.0.0/16","#7f8c8d", 5.5, 3.5),
    ("IoT",             "10.4.0.0/22",  "#16a085", 8.0, 3.5),
]

for nom, cidr, color, x, y in services:
    net = ipaddress.ip_network(cidr)
    nb_hotes = net.num_addresses - 2
    rect = mpatches.FancyBboxPatch((x, y), 2.4, 1.4,
                                    boxstyle="round,pad=0.1",
                                    edgecolor=color, facecolor=color, alpha=0.15, linewidth=2)
    ax.add_patch(rect)
    rect2 = mpatches.FancyBboxPatch((x, y + 0.85), 2.4, 0.55,
                                     boxstyle="round,pad=0.05",
                                     edgecolor=color, facecolor=color, alpha=0.7, linewidth=1.5)
    ax.add_patch(rect2)
    ax.text(x + 1.2, y + 1.13, nom, ha="center", va="center",
            fontsize=9, fontweight="bold", color="white")
    ax.text(x + 1.2, y + 0.55, cidr, ha="center", va="center",
            fontsize=8.5, color=color, fontweight="bold")
    ax.text(x + 1.2, y + 0.2, f"{nb_hotes:,} hôtes max", ha="center", va="center",
            fontsize=7.5, color="#555")

# Réseau principal
ax.add_patch(mpatches.FancyBboxPatch((4.5, 1.2), 3.0, 0.9,
                                      boxstyle="round,pad=0.1",
                                      edgecolor="#2c3e50", facecolor="#2c3e50", alpha=0.85, linewidth=2))
ax.text(6.0, 1.65, "Backbone — 10.0.0.0/8", ha="center", va="center",
        fontsize=10, fontweight="bold", color="white")

plt.tight_layout()
plt.show()
```

---

## IPv6

IPv6 (RFC 2460) répond à l'épuisement des adresses IPv4. Il utilise des adresses de **128 bits** (contre 32 bits pour IPv4), soit 2¹²⁸ ≈ 3.4 × 10³⁸ adresses.

### Format d'adresse IPv6

Une adresse IPv6 est écrite en **8 groupes de 4 chiffres hexadécimaux** séparés par `:` :

```
2001:0db8:0000:0042:0000:8a2e:0370:7334
```

**Règles d'abréviation** :
1. Omettre les zéros initiaux dans chaque groupe : `2001:db8:0:42:0:8a2e:370:7334`
2. Remplacer une suite de groupes nuls par `::` (une seule fois) : `2001:db8:0:42::8a2e:370:7334`

### Types d'adresses IPv6

| Type | Préfixe | Exemple | Usage |
|------|---------|---------|-------|
| **Unicast global** | `2000::/3` | `2001:db8::1` | Adressage Internet public |
| **Unicast link-local** | `fe80::/10` | `fe80::1` | Communication sur le lien local uniquement |
| **Unicast unique-local** | `fc00::/7` | `fd00::1` | Équivalent des IP privées RFC 1918 |
| **Multicast** | `ff00::/8` | `ff02::1` | Diffusion vers un groupe |
| **Loopback** | `::1/128` | `::1` | Interface de bouclage |
| **Non spécifiée** | `::/128` | `::` | Source avant l'attribution d'une adresse |

```{code-cell} python
import ipaddress

def analyser_ipv6(adresse: str) -> dict:
    """Analyse une adresse IPv6."""
    try:
        addr = ipaddress.ip_address(adresse)
    except ValueError as e:
        return {"erreur": str(e)}

    types = []
    if addr.is_loopback:       types.append("loopback")
    if addr.is_link_local:     types.append("link-local")
    if addr.is_private:        types.append("privée/unique-local")
    if addr.is_global:         types.append("globale")
    if addr.is_multicast:      types.append("multicast")
    if addr.is_unspecified:    types.append("non spécifiée")

    return {
        "Adresse compressée":    str(addr),
        "Adresse complète":      addr.exploded,
        "Type(s)":               ", ".join(types) if types else "inconnu",
        "Version":               addr.version,
    }

adresses_test = [
    "::1",
    "fe80::1",
    "2001:db8::1",
    "ff02::1",
    "fd00::cafe:1",
    "2001:4860:4860::8888",  # Google DNS
]

print(f"{'Adresse':<30} {'Type':<25} {'Complète'}")
print("─" * 95)
for a in adresses_test:
    info = analyser_ipv6(a)
    print(f"{info.get('Adresse compressée','?'):<30} "
          f"{info.get('Type(s)','?'):<25} "
          f"{info.get('Adresse complète','?')}")
```

### Auto-configuration SLAAC

SLAAC (Stateless Address Autoconfiguration, RFC 4862) permet à une interface IPv6 de se configurer automatiquement sans serveur DHCP :

1. Le routeur envoie des **Router Advertisements (RA)** avec le préfixe réseau
2. L'hôte combine ce préfixe avec son identifiant d'interface (dérivé de la MAC via EUI-64 ou généré aléatoirement)
3. Une vérification de duplication (DAD) est effectuée en multicast

```{code-cell} python
def eui64_depuis_mac(mac: str) -> str:
    """
    Génère l'identifiant d'interface EUI-64 depuis une adresse MAC 48 bits.
    Règle : insertion de ff:fe au milieu et inversion du bit Universal/Local.
    """
    octets = [int(x, 16) for x in mac.split(":")]
    octets[0] ^= 0x02  # Inversion du bit U/L
    eui64 = octets[:3] + [0xff, 0xfe] + octets[3:]
    return ":".join(f"{x:02x}" for x in eui64)

def slaac_adresse(prefixe_reseau: str, mac: str) -> str:
    """Calcule l'adresse SLAAC à partir du préfixe réseau et de la MAC."""
    iid = eui64_depuis_mac(mac)
    net = ipaddress.ip_network(prefixe_reseau, strict=False)
    # Combiner le préfixe /64 avec l'IID
    prefixe_int = int(net.network_address)
    iid_bytes = bytes(int(x, 16) for x in iid.split(":"))
    iid_int = int.from_bytes(iid_bytes, "big")
    adresse_int = prefixe_int | iid_int
    return str(ipaddress.ip_address(adresse_int))

exemples = [
    ("2001:db8::/64",   "00:1a:2b:3c:4d:5e"),
    ("2001:db8::/64",   "aa:bb:cc:dd:ee:ff"),
    ("fd00:cafe::/64",  "08:00:27:ab:cd:ef"),
]

print("Auto-configuration SLAAC (EUI-64) :")
print("-" * 60)
for prefixe, mac in exemples:
    iid = eui64_depuis_mac(mac)
    adresse = slaac_adresse(prefixe, mac)
    print(f"  MAC     : {mac}")
    print(f"  EUI-64  : {iid}")
    print(f"  Adresse : {adresse}")
    print()
```

---

## NAT — Network Address Translation

Le NAT permet à plusieurs machines avec des adresses IP privées de partager une (ou quelques) adresse(s) IP publique(s).

### NAT Masquerade / PAT

Le **PAT** (Port Address Translation), aussi appelé NAT overload ou masquerade, est la forme la plus courante. Il utilise les numéros de ports TCP/UDP pour distinguer les connexions.

```{code-cell} python
:tags: [hide-input]
fig, ax = plt.subplots(figsize=(14, 8))
ax.set_xlim(0, 14)
ax.set_ylim(0, 8)
ax.axis("off")
ax.set_title("NAT Masquerade (PAT) — Translation d'adresses et de ports",
             fontsize=13, fontweight="bold")

# Réseau privé (gauche)
ax.add_patch(mpatches.FancyBboxPatch((0.1, 2.0), 3.5, 5.0,
                                      boxstyle="round,pad=0.2",
                                      edgecolor="#27ae60", facecolor="#eafaf1", linewidth=2))
ax.text(1.85, 6.8, "Réseau privé\n192.168.1.0/24", ha="center", fontsize=9,
        fontweight="bold", color="#27ae60")

pcs = [
    ("PC-A", "192.168.1.10", 2.5, 5.5),
    ("PC-B", "192.168.1.20", 2.5, 4.2),
    ("PC-C", "192.168.1.30", 2.5, 2.9),
]
for nom, ip, x, y in pcs:
    ax.add_patch(mpatches.FancyBboxPatch((x - 1.0, y - 0.4), 2.0, 0.8,
                                          boxstyle="round,pad=0.1",
                                          edgecolor="#2980b9", facecolor="#eaf4fb", linewidth=1.5))
    ax.text(x, y, f"{nom}\n{ip}", ha="center", va="center",
            fontsize=8, color="#2c3e50", fontweight="bold")

# Routeur NAT
ax.add_patch(mpatches.FancyBboxPatch((4.5, 3.5), 2.0, 1.8,
                                      boxstyle="round,pad=0.2",
                                      edgecolor="#e67e22", facecolor="#fef9e7", linewidth=2.5))
ax.text(5.5, 4.8, "Routeur NAT", ha="center", fontsize=9, fontweight="bold", color="#e67e22")
ax.text(5.5, 4.2, "IP priv. : 192.168.1.1\nIP pub. : 203.0.113.1",
        ha="center", fontsize=7.5, color="#555")

# Internet (droite)
ax.add_patch(mpatches.FancyBboxPatch((8.0, 2.0), 5.8, 5.0,
                                      boxstyle="round,pad=0.2",
                                      edgecolor="#2980b9", facecolor="#eaf4fb", linewidth=2))
ax.text(10.9, 6.8, "Internet", ha="center", fontsize=9, fontweight="bold", color="#2980b9")

# Serveur web
ax.add_patch(mpatches.FancyBboxPatch((9.5, 4.0), 2.5, 1.0,
                                      boxstyle="round,pad=0.1",
                                      edgecolor="#e74c3c", facecolor="white", linewidth=1.5))
ax.text(10.75, 4.5, "Serveur web\n93.184.216.34:80", ha="center", va="center",
        fontsize=8, color="#e74c3c", fontweight="bold")

# Connexions
for nom, ip, x, y in pcs:
    ax.annotate("", xy=(4.5, 4.5), xytext=(x + 1.0, y),
                arrowprops=dict(arrowstyle="-|>", color="#7f8c8d", lw=1.5))

ax.annotate("", xy=(8.0, 4.5), xytext=(6.5, 4.5),
            arrowprops=dict(arrowstyle="<->", color="#e67e22", lw=2.5))

ax.annotate("", xy=(9.5, 4.5), xytext=(8.0, 4.5),
            arrowprops=dict(arrowstyle="-|>", color="#2980b9", lw=1.5))

# Table NAT
ax.add_patch(mpatches.FancyBboxPatch((0.1, 0.1), 13.8, 1.7,
                                      boxstyle="round,pad=0.1",
                                      edgecolor="#e67e22", facecolor="#fef9e7", linewidth=1.5))
ax.text(7.0, 1.65, "Table NAT (PAT)", ha="center", fontsize=9, fontweight="bold", color="#e67e22")

entrees_nat = [
    ("192.168.1.10:54321", "203.0.113.1:10001", "93.184.216.34:80", "TCP", "ESTABLISHED"),
    ("192.168.1.20:54322", "203.0.113.1:10002", "93.184.216.34:80", "TCP", "ESTABLISHED"),
    ("192.168.1.30:54323", "203.0.113.1:10003", "93.184.216.34:443","TCP", "SYN_SENT"),
]

colonnes = ["Source privée", "Source traduite (NAT)", "Destination", "Proto", "État"]
for i, col in enumerate(colonnes):
    ax.text(0.5 + i * 2.7, 1.35, col, fontsize=7, fontweight="bold", color="#555")
for j, (src_priv, src_nat, dst, proto, etat) in enumerate(entrees_nat):
    y_row = 1.0 - j * 0.28
    for i, val in enumerate([src_priv, src_nat, dst, proto, etat]):
        ax.text(0.5 + i * 2.7, y_row, val, fontsize=7, color="#333")

plt.tight_layout()
plt.show()
```

### Avantages et inconvénients du NAT

| Aspect | Pour | Contre |
|--------|------|--------|
| Économie d'adresses | Partage d'une IP publique entre N hôtes | Brise le modèle end-to-end d'Internet |
| Sécurité | Cache la topologie interne | Les connexions entrantes nécessitent du port forwarding |
| Déploiement | Simple, transparent | Complique les protocoles embarquant des IPs (FTP, SIP) |
| Performance | — | Surcoût de traitement (état, translation) |

---

## Routage IP

### Table de routage

Un routeur décide du prochain saut (next hop) pour chaque paquet en consultant sa **table de routage** selon la règle du **plus long préfixe** (longest prefix match).

```{code-cell} python
import ipaddress

class TableRoutage:
    """Simulation d'une table de routage avec longest prefix match."""

    def __init__(self):
        self.routes = []

    def ajouter_route(self, reseau: str, next_hop: str, interface: str,
                       metrique: int = 1, source: str = "static"):
        net = ipaddress.ip_network(reseau, strict=False)
        self.routes.append({
            "réseau":     net,
            "next_hop":   next_hop,
            "interface":  interface,
            "métrique":   metrique,
            "source":     source,
        })
        # Tri par longueur de préfixe décroissante
        self.routes.sort(key=lambda r: r["réseau"].prefixlen, reverse=True)

    def router(self, ip_dest: str) -> dict | None:
        """Applique le longest prefix match."""
        dest = ipaddress.ip_address(ip_dest)
        for route in self.routes:
            if dest in route["réseau"]:
                return route
        return None

    def afficher(self):
        print(f"{'Réseau':<22} {'Next-Hop':<18} {'Interface':<12} {'Métrique':<10} {'Source'}")
        print("─" * 80)
        for r in self.routes:
            print(f"  {str(r['réseau']):<20} {r['next_hop']:<18} "
                  f"{r['interface']:<12} {r['métrique']:<10} {r['source']}")

# Exemple de table de routage d'un routeur d'entreprise
table = TableRoutage()
table.ajouter_route("0.0.0.0/0",        "203.0.113.254", "eth0", metrique=10, source="static")
table.ajouter_route("10.0.0.0/8",       "10.255.255.1",  "eth1", metrique=1,  source="static")
table.ajouter_route("10.1.0.0/16",      "10.1.0.1",      "eth2", metrique=1,  source="OSPF")
table.ajouter_route("10.1.5.0/24",      "10.1.5.254",    "eth2", metrique=1,  source="OSPF")
table.ajouter_route("192.168.1.0/24",   "192.168.1.1",   "eth3", metrique=1,  source="connected")
table.ajouter_route("192.168.2.0/24",   "192.168.1.254", "eth3", metrique=2,  source="RIP")
table.ajouter_route("127.0.0.0/8",      "127.0.0.1",     "lo",   metrique=0,  source="connected")

print("Table de routage :")
table.afficher()

print("\nRésolution de routes (longest prefix match) :")
print("-" * 55)
tests = ["10.1.5.42", "10.1.99.1", "10.2.3.4", "192.168.2.50",
         "8.8.8.8", "127.0.0.1"]
for ip in tests:
    route = table.router(ip)
    if route:
        print(f"  {ip:<18} → {str(route['réseau']):<22} via {route['next_hop']:<18} ({route['source']})")
    else:
        print(f"  {ip:<18} → PAS DE ROUTE (paquet dropped)")
```

### Protocoles de routage

```{admonition} Routage statique vs dynamique
:class: note
Le **routage statique** est configuré manuellement par l'administrateur. Simple mais ne s'adapte pas aux pannes. Le **routage dynamique** utilise des protocoles qui échangent des informations de topologie et recalculent les routes automatiquement.
```

| Protocole | Type | Algorithme | Métrique | Usage |
|-----------|------|-----------|----------|-------|
| **RIP v2** | IGP, Distance Vector | Bellman-Ford | Nombre de sauts (max 15) | Petits réseaux (obsolescent) |
| **OSPF** | IGP, Link State | Dijkstra | Bande passante (coût) | Réseaux d'entreprise |
| **IS-IS** | IGP, Link State | Dijkstra | Coût | FAI, backbone |
| **EIGRP** | IGP, Hybrid | DUAL | Bande passante + délai | Réseaux Cisco |
| **BGP-4** | EGP, Path Vector | Best-path | Attributs de politique | Inter-AS, Internet |

---

## ICMP — Ping et Traceroute

**ICMP** (Internet Control Message Protocol, RFC 792) transporte des messages de contrôle et d'erreur pour IP. Il est encapsulé directement dans les paquets IP (protocole 1).

### Messages ICMP courants

| Type | Code | Description |
|------|------|-------------|
| 0 | 0 | Echo Reply (réponse ping) |
| 3 | 0–15 | Destination Unreachable |
| 8 | 0 | Echo Request (ping) |
| 11 | 0 | Time Exceeded (TTL expiré — utilisé par traceroute) |
| 12 | 0 | Parameter Problem |

### Simulation de ping et traceroute

```{code-cell} python
import struct
import socket

def construire_icmp_echo_request(identifiant: int, sequence: int,
                                   payload: bytes = b"Hello ICMP!") -> bytes:
    """
    Construit un paquet ICMP Echo Request (Type=8, Code=0).
    Le checksum est calculé correctement.
    """
    type_icmp = 8
    code      = 0
    checksum  = 0
    en_tete   = struct.pack(">BBHHH", type_icmp, code, checksum, identifiant, sequence)
    paquet    = en_tete + payload

    # Calcul du checksum Internet (RFC 1071)
    if len(paquet) % 2:
        paquet += b'\x00'
    total = 0
    for i in range(0, len(paquet), 2):
        mot = (paquet[i] << 8) + paquet[i+1]
        total += mot
    while total >> 16:
        total = (total & 0xFFFF) + (total >> 16)
    checksum = ~total & 0xFFFF

    en_tete_final = struct.pack(">BBHHH", type_icmp, code, checksum, identifiant, sequence)
    return en_tete_final + payload

def decoder_icmp(data: bytes) -> dict:
    """Décode un paquet ICMP."""
    type_icmp, code, checksum, ident, seq = struct.unpack(">BBHHH", data[:8])
    types_icmp = {
        0: "Echo Reply", 3: "Destination Unreachable",
        8: "Echo Request", 11: "Time Exceeded",
    }
    return {
        "Type":      f"{type_icmp} ({types_icmp.get(type_icmp, '?')})",
        "Code":      code,
        "Checksum":  f"0x{checksum:04X}",
        "ID":        ident,
        "Séquence":  seq,
        "Payload":   data[8:].decode("ascii", errors="replace"),
    }

# Construction d'un Echo Request
paquet = construire_icmp_echo_request(identifiant=1234, sequence=1)
print("ICMP Echo Request construit :")
print(f"  Hex : {paquet.hex(' ')}\n")

decodage = decoder_icmp(paquet)
for k, v in decodage.items():
    print(f"  {k:<12} : {v}")
```

```{code-cell} python
:tags: [hide-input]
# Visualisation d'un traceroute simulé
fig, ax = plt.subplots(figsize=(12, 7))
ax.set_xlim(0, 14)
ax.set_ylim(0, 8)
ax.axis("off")
ax.set_title("Fonctionnement de Traceroute (TTL incrémental)", fontsize=13, fontweight="bold")

# Nœuds
noeuds = [
    ("Source\n192.168.1.10", 0.8, 4.0, "#27ae60"),
    ("R1 (FAI)\n10.0.0.1",   3.5, 4.0, "#2980b9"),
    ("R2 (WAN)\n10.1.0.1",   6.5, 4.0, "#2980b9"),
    ("R3 (Transit)\n10.2.0.1",9.5, 4.0, "#2980b9"),
    ("Dest.\n93.184.216.34",  12.5, 4.0, "#e74c3c"),
]

for label, x, y, color in noeuds:
    ax.add_patch(mpatches.FancyBboxPatch((x - 0.8, y - 0.5), 1.6, 1.0,
                                          boxstyle="round,pad=0.1",
                                          edgecolor=color, facecolor="#ecf0f1", linewidth=2))
    ax.text(x, y, label, ha="center", va="center", fontsize=8, fontweight="bold", color=color)

# Liens
for i in range(len(noeuds) - 1):
    x1 = noeuds[i][1] + 0.8
    x2 = noeuds[i+1][1] - 0.8
    y  = 4.0
    ax.plot([x1, x2], [y, y], color="#7f8c8d", linewidth=2.5)

# Paquets TTL=1,2,3 et réponse ICMP Time Exceeded
ttls = [
    (1, 0.8, 3.5, "#e74c3c",  "TTL=1\n→ R1"),
    (2, 0.8, 3.5, "#e67e22",  "TTL=2\n→ R2"),
    (3, 0.8, 3.5, "#f39c12",  "TTL=3\n→ R3"),
    (4, 0.8, 3.5, "#27ae60",  "TTL=4\n→ Dest"),
]

y_levels = [6.5, 5.8, 5.1, 4.7]
x_retours = [3.5, 6.5, 9.5, 12.5]

for i, ((ttl, xs, ys, color, label), y_tir, x_ret) in enumerate(
        zip(ttls, y_levels, x_retours)):
    # Paquet aller
    ax.annotate("",
                xy=(x_ret - 0.8, y_tir),
                xytext=(xs + 0.8, y_tir),
                arrowprops=dict(arrowstyle="-|>", color=color, lw=1.8))
    ax.text((xs + 0.8 + x_ret - 0.8) / 2, y_tir + 0.12, label,
            ha="center", va="bottom", fontsize=7.5, color=color)
    # Réponse ICMP Time Exceeded (sauf le dernier qui reçoit Echo Reply)
    msg_retour = "ICMP Time Exceeded" if ttl < 4 else "ICMP Echo Reply"
    ax.annotate("",
                xy=(0.8 + 0.8, y_tir - 0.4),
                xytext=(x_ret - 0.8, y_tir - 0.4),
                arrowprops=dict(arrowstyle="-|>", color=color, lw=1.2, linestyle="dashed"))
    ax.text((xs + 0.8 + x_ret - 0.8) / 2, y_tir - 0.52, msg_retour,
            ha="center", va="top", fontsize=7, color=color, fontstyle="italic")

plt.tight_layout()
plt.show()
```

---

## Résumé

```{admonition} Points clés du chapitre 4
:class: note
- L'**en-tête IPv4** fait 20 octets minimum et contient les adresses src/dst, le TTL, le protocole encapsulé et les bits de fragmentation.
- **CIDR** (`/24`, `/26`…) remplace les classes A/B/C historiques et permet un adressage flexible.
- **IPv6** utilise 128 bits (contre 32), l'auto-configuration SLAAC remplace DHCP pour le cas simple.
- **NAT/PAT** partage une IP publique entre plusieurs hôtes privés grâce aux ports TCP/UDP.
- Le **longest prefix match** détermine le next-hop dans la table de routage.
- **ICMP** fournit les messages de contrôle : ping (Echo Request/Reply) et traceroute (TTL + Time Exceeded).
```

| Concept | Taille | Exemple |
|---------|--------|---------|
| En-tête IPv4 | 20–60 octets | version, TTL, protocole, src, dst |
| Adresse IPv4 | 32 bits | 192.168.1.1/24 |
| Adresse IPv6 | 128 bits | 2001:db8::1/64 |
| TTL initial | 64 ou 128 | Linux=64, Windows=128 |
| MTU Ethernet | 1500 octets | fragmentation si > MTU |
