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.

Hide code cell source

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.

Hide code cell source

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()
_images/59ef1d3fe3b56ff054fbdbd08e91a244d771fd7ba9adaa68c6fb3985ea47df3a.png

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

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}")
Décodage d'un en-tête IPv4 :
----------------------------------------
  Version              : 4
  IHL                  : 5 × 4 = 20 octets
  DSCP                 : 0
  ECN                  : 0
  Longueur             : 60 octets
  Identification       : 0x1234
  Flags                : DF
  Frag. Offset         : 0
  TTL                  : 64
  Protocole            : 6 (TCP)
  Checksum             : 0x0000
  Source               : 192.168.1.10
  Destination          : 8.8.8.8

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.

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}")
══════════════════════════════════════════════════
  10.0.0.0/8
  Réseau                 : 10.0.0.0/8
  Adresse réseau         : 10.0.0.0
  Masque                 : 255.0.0.0
  Masque inversé         : 0.255.255.255
  Broadcast              : 10.255.255.255
  Nb hôtes utiles        : 16777214
  Premier hôte           : 10.0.0.1
  Dernier hôte           : 10.255.255.254
  Classe tradition.      : A

══════════════════════════════════════════════════
  172.16.0.0/12
  Réseau                 : 172.16.0.0/12
  Adresse réseau         : 172.16.0.0
  Masque                 : 255.240.0.0
  Masque inversé         : 0.15.255.255
  Broadcast              : 172.31.255.255
  Nb hôtes utiles        : 1048574
  Premier hôte           : 172.16.0.1
  Dernier hôte           : 172.31.255.254
  Classe tradition.      : B

══════════════════════════════════════════════════
  192.168.1.0/24
  Réseau                 : 192.168.1.0/24
  Adresse réseau         : 192.168.1.0
  Masque                 : 255.255.255.0
  Masque inversé         : 0.0.0.255
  Broadcast              : 192.168.1.255
  Nb hôtes utiles        : 254
  Premier hôte           : 192.168.1.1
  Dernier hôte           : 192.168.1.254
  Classe tradition.      : C

══════════════════════════════════════════════════
  192.168.1.64/26
  Réseau                 : 192.168.1.64/26
  Adresse réseau         : 192.168.1.64
  Masque                 : 255.255.255.192
  Masque inversé         : 0.0.0.63
  Broadcast              : 192.168.1.127
  Nb hôtes utiles        : 62
  Premier hôte           : 192.168.1.65
  Dernier hôte           : 192.168.1.126
  Classe tradition.      : sous-réseau

══════════════════════════════════════════════════
  10.10.5.0/29
  Réseau                 : 10.10.5.0/29
  Adresse réseau         : 10.10.5.0
  Masque                 : 255.255.255.248
  Masque inversé         : 0.0.0.7
  Broadcast              : 10.10.5.7
  Nb hôtes utiles        : 6
  Premier hôte           : 10.10.5.1
  Dernier hôte           : 10.10.5.6
  Classe tradition.      : sous-réseau

══════════════════════════════════════════════════
  172.20.0.0/16
  Réseau                 : 172.20.0.0/16
  Adresse réseau         : 172.20.0.0
  Masque                 : 255.255.0.0
  Masque inversé         : 0.0.255.255
  Broadcast              : 172.20.255.255
  Nb hôtes utiles        : 65534
  Premier hôte           : 172.20.0.1
  Dernier hôte           : 172.20.255.254
  Classe tradition.      : B

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

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)")
Découpage de 192.168.1.0/24 en /26 (64 hôtes chacun) :
------------------------------------------------------------
  192.168.1.0/26         → hôtes: 192.168.1.1 – 192.168.1.62
  192.168.1.64/26        → hôtes: 192.168.1.65 – 192.168.1.126
  192.168.1.128/26       → hôtes: 192.168.1.129 – 192.168.1.190
  192.168.1.192/26       → hôtes: 192.168.1.193 – 192.168.1.254

Découpage de 10.0.0.0/8 en /10 :
------------------------------------------------------------
  10.0.0.0/10          (4,194,302 hôtes utiles)
  10.64.0.0/10         (4,194,302 hôtes utiles)
  10.128.0.0/10        (4,194,302 hôtes utiles)
  10.192.0.0/10        (4,194,302 hôtes utiles)

Hide code cell source

# 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()
_images/9715fc5c31ab296c1603ae60028199cb4adb48c1ce29e5e8d3f63ae619b28f81.png

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

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','?')}")
Adresse                        Type                      Complète
───────────────────────────────────────────────────────────────────────────────────────────────
::1                            loopback, privée/unique-local 0000:0000:0000:0000:0000:0000:0000:0001
fe80::1                        link-local, privée/unique-local fe80:0000:0000:0000:0000:0000:0000:0001
2001:db8::1                    privée/unique-local       2001:0db8:0000:0000:0000:0000:0000:0001
ff02::1                        globale, multicast        ff02:0000:0000:0000:0000:0000:0000:0001
fd00::cafe:1                   privée/unique-local       fd00:0000:0000:0000:0000:0000:cafe:0001
2001:4860:4860::8888           globale                   2001:4860:4860:0000:0000:0000:0000:8888

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

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()
Auto-configuration SLAAC (EUI-64) :
------------------------------------------------------------
  MAC     : 00:1a:2b:3c:4d:5e
  EUI-64  : 02:1a:2b:ff:fe:3c:4d:5e
  Adresse : 2001:db8::21a:2bff:fe3c:4d5e

  MAC     : aa:bb:cc:dd:ee:ff
  EUI-64  : a8:bb:cc:ff:fe:dd:ee:ff
  Adresse : 2001:db8::a8bb:ccff:fedd:eeff

  MAC     : 08:00:27:ab:cd:ef
  EUI-64  : 0a:00:27:ff:fe:ab:cd:ef
  Adresse : fd00:cafe::a00:27ff:feab:cdef

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.

Hide code cell source

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()
_images/106be5814dcc1c68433c1dbbdcf7c9decfcd7be86b61237ea2fb6d2f02b25c6f.png

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

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)")
Table de routage :
Réseau                 Next-Hop           Interface    Métrique   Source
────────────────────────────────────────────────────────────────────────────────
  10.1.5.0/24          10.1.5.254         eth2         1          OSPF
  192.168.1.0/24       192.168.1.1        eth3         1          connected
  192.168.2.0/24       192.168.1.254      eth3         2          RIP
  10.1.0.0/16          10.1.0.1           eth2         1          OSPF
  10.0.0.0/8           10.255.255.1       eth1         1          static
  127.0.0.0/8          127.0.0.1          lo           0          connected
  0.0.0.0/0            203.0.113.254      eth0         10         static

Résolution de routes (longest prefix match) :
-------------------------------------------------------
  10.1.5.42          → 10.1.5.0/24            via 10.1.5.254         (OSPF)
  10.1.99.1          → 10.1.0.0/16            via 10.1.0.1           (OSPF)
  10.2.3.4           → 10.0.0.0/8             via 10.255.255.1       (static)
  192.168.2.50       → 192.168.2.0/24         via 192.168.1.254      (RIP)
  8.8.8.8            → 0.0.0.0/0              via 203.0.113.254      (static)
  127.0.0.1          → 127.0.0.0/8            via 127.0.0.1          (connected)

Protocoles de routage#

Routage statique vs dynamique

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#

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}")
ICMP Echo Request construit :
  Hex : 08 00 17 a7 04 d2 00 01 48 65 6c 6c 6f 20 49 43 4d 50 21

  Type         : 8 (Echo Request)
  Code         : 0
  Checksum     : 0x17A7
  ID           : 1234
  Séquence     : 1
  Payload      : Hello ICMP!

Hide code cell source

# 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()
_images/c22fc6fb5b29ed5cc371953a7733cd69901d23b429e0bf5a7850674bc5c8d682.png

Résumé#

Points clés du chapitre 4

  • 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