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

# Réseaux modernes

Les réseaux informatiques n'ont jamais évolué aussi vite. En quelques années, le software-defined networking a bouleversé la façon dont les réseaux de data centers sont construits ; eBPF a rendu le noyau Linux programmable sans recompilation ; VXLAN permet de superposer des réseaux virtuels sur n'importe quelle infrastructure IP ; et Kubernetes a créé ses propres abstractions réseau. Ce chapitre couvre les technologies qui définissent l'état de l'art des réseaux de 2025.

```{code-cell} python
:tags: [hide-input]

import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import numpy as np
import pandas as pd
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted")
```

## SD-WAN — Software-Defined Wide Area Network

Le **SD-WAN** sépare le **plan de contrôle** (décisions d'acheminement centralisées) du **plan de données** (transport effectif des paquets), à l'image de ce que SDN fait pour les réseaux locaux.

### Architecture

```{code-cell} python
fig, ax = plt.subplots(figsize=(12, 6))
ax.set_xlim(0, 12)
ax.set_ylim(0, 7)
ax.axis('off')
ax.set_title("Architecture SD-WAN — séparation plan de contrôle / données", fontsize=13, fontweight='bold', pad=12)

# Contrôleur central
ax.add_patch(mpatches.FancyBboxPatch((4, 5.5), 4, 1.2,
             boxstyle="round,pad=0.15", facecolor='#4575b4', alpha=0.2,
             edgecolor='#4575b4', linewidth=2.5))
ax.text(6, 6.1, "SD-WAN Controller (cloud)", ha='center', va='center',
        fontsize=11, color='#4575b4', fontweight='bold')
ax.text(6, 5.7, "Visibilité centralisée · Politiques QoS · Routage applicatif",
        ha='center', va='center', fontsize=8.5, color='#4575b4')

# Sites distants
sites = [
    (1.5, 3, "Siège\n(HQ)"),
    (5.5, 3, "Agence\n(Branch)"),
    (9.5, 3, "Data Center\n(Cloud)"),
]
for x, y, label in sites:
    ax.add_patch(mpatches.FancyBboxPatch((x-1.1, y-0.5), 2.2, 1.0,
                 boxstyle="round,pad=0.1", facecolor='#1a9850', alpha=0.2,
                 edgecolor='#1a9850', linewidth=2))
    ax.text(x, y, label, ha='center', va='center', fontsize=9, color='#1a9850', fontweight='bold')

# CPE SD-WAN (équipements edge)
for x, y in [(1.5, 4.2), (5.5, 4.2), (9.5, 4.2)]:
    ax.add_patch(mpatches.FancyBboxPatch((x-0.6, y-0.25), 1.2, 0.55,
                 boxstyle="round,pad=0.05", facecolor='#d73027', alpha=0.2,
                 edgecolor='#d73027', linewidth=1.5))
    ax.text(x, y+0.05, "CPE SD-WAN", ha='center', va='center', fontsize=7.5, color='#d73027', fontweight='bold')

# Flèches contrôleur ↔ CPE (plan de contrôle)
for x in [1.5, 5.5, 9.5]:
    ax.annotate("", xy=(x, 4.5), xytext=(6, 5.5),
                arrowprops=dict(arrowstyle='<->', color='#4575b4', lw=1.5, linestyle='dashed'))

# Tunnels overlay (plan de données)
liens_wan = [
    (2.6, 4.2, 4.4, 4.2, "MPLS"),
    (2.6, 3.8, 4.4, 3.8, "Internet"),
    (6.6, 4.2, 8.4, 4.2, "4G/5G"),
    (6.6, 3.8, 8.4, 3.8, "Internet"),
]
for x1, y1, x2, y2, label in liens_wan:
    ax.plot([x1, x2], [y1, y2], '-', color='#888888', linewidth=1.5, alpha=0.7)
    ax.text((x1+x2)/2, y1+0.12, label, ha='center', fontsize=7.5, color='#555555')

ax.text(6, 1.8,
        "Le contrôleur sélectionne dynamiquement le meilleur lien WAN\n"
        "(MPLS, Internet, 4G/5G) selon la QoS applicative et les coûts.",
        ha='center', va='center', fontsize=9, color='#333333',
        bbox=dict(boxstyle='round,pad=0.4', facecolor='#f0f8ff', edgecolor='#4575b4'))

plt.tight_layout()
plt.savefig('_static/sdwan_archi.png', dpi=100, bbox_inches='tight')
plt.show()
```

---

## eBPF — Berkeley Packet Filter étendu

**eBPF** (*extended Berkeley Packet Filter*) est une technologie révolutionnaire du noyau Linux qui permet d'exécuter des programmes sandboxés dans le noyau sans modifier son code source ni charger de modules.

### Principes fondamentaux

```{code-cell} python
fig, ax = plt.subplots(figsize=(12, 6))
ax.set_xlim(0, 12)
ax.set_ylim(0, 7)
ax.axis('off')
ax.set_title("Architecture eBPF — du code utilisateur au noyau Linux", fontsize=13, fontweight='bold', pad=12)

# Espace utilisateur
ax.add_patch(mpatches.FancyBboxPatch((0.3, 4.5), 5.4, 2.2,
             boxstyle="round,pad=0.2", facecolor='#4575b4', alpha=0.08,
             edgecolor='#4575b4', linewidth=2))
ax.text(3, 6.4, "Espace utilisateur", ha='center', fontsize=11, color='#4575b4', fontweight='bold')

# Espace noyau
ax.add_patch(mpatches.FancyBboxPatch((0.3, 0.3), 11.4, 3.9,
             boxstyle="round,pad=0.2", facecolor='#d73027', alpha=0.05,
             edgecolor='#d73027', linewidth=2))
ax.text(6, 3.8, "Noyau Linux (kernel space)", ha='center', fontsize=11, color='#d73027', fontweight='bold')

# Composants espace utilisateur
for x, y, label, col in [
    (1.8, 5.6, "Programme C\n(source eBPF)", '#4575b4'),
    (3.5, 5.6, "Compilateur\n(Clang/LLVM)", '#4575b4'),
    (5.3, 5.6, "Bytecode\neBPF (.o)", '#4575b4'),
]:
    ax.add_patch(mpatches.FancyBboxPatch((x-0.7, y-0.4), 1.4, 0.8,
                 boxstyle="round,pad=0.05", facecolor=col, alpha=0.2,
                 edgecolor=col, linewidth=1.5))
    ax.text(x, y+0.05, label, ha='center', va='center', fontsize=8, color=col, fontweight='bold')
    if x < 5.3:
        ax.annotate("", xy=(x+0.85, y), xytext=(x+0.7, y),
                    arrowprops=dict(arrowstyle='->', color=col, lw=1.5))

# Vérificateur
ax.add_patch(mpatches.FancyBboxPatch((6.5, 4.7), 2.5, 1.0,
             boxstyle="round,pad=0.1", facecolor='#1a9850', alpha=0.2,
             edgecolor='#1a9850', linewidth=2))
ax.text(7.75, 5.2, "Vérificateur\n(safety checker)", ha='center', va='center',
        fontsize=9, color='#1a9850', fontweight='bold')

# Chargement syscall bpf()
ax.annotate("", xy=(7.75, 4.7), xytext=(5.3, 5.2),
            arrowprops=dict(arrowstyle='->', color='#555555', lw=1.8))
ax.text(6.5, 5.1, "bpf()\nsyscall", ha='center', fontsize=8, color='#555555', style='italic')

# Points d'accroche dans le noyau
hooks = [
    (1.5, 2.5, "XDP\n(avant stack réseau)"),
    (3.5, 2.5, "tc ingress/egress\n(Traffic Control)"),
    (5.5, 2.5, "kprobes /\ntracepointss"),
    (7.5, 2.5, "Socket\nfilter"),
    (9.5, 2.5, "Sécurité LSM\n(SELinux-like)"),
]
for x, y, label in hooks:
    ax.add_patch(mpatches.FancyBboxPatch((x-0.8, y-0.45), 1.6, 0.9,
                 boxstyle="round,pad=0.05", facecolor='#d73027', alpha=0.18,
                 edgecolor='#d73027', linewidth=1.5))
    ax.text(x, y+0.05, label, ha='center', va='center', fontsize=7.5, color='#d73027', fontweight='bold')
    ax.annotate("", xy=(x, y+0.45), xytext=(7.75, 4.7),
                arrowprops=dict(arrowstyle='->', color='#1a9850', lw=1.2, linestyle='dotted'))

# Maps
ax.add_patch(mpatches.FancyBboxPatch((9.0, 1.2), 2.2, 0.9,
             boxstyle="round,pad=0.1", facecolor='#984ea3', alpha=0.2,
             edgecolor='#984ea3', linewidth=1.5))
ax.text(10.1, 1.65, "BPF Maps\n(données partagées)", ha='center', va='center',
        fontsize=8, color='#984ea3', fontweight='bold')

plt.tight_layout()
plt.savefig('_static/ebpf_archi.png', dpi=100, bbox_inches='tight')
plt.show()
```

### XDP — eXpress Data Path

XDP est le point d'accroche eBPF le plus bas dans la pile réseau Linux. Il s'exécute **avant même l'allocation d'un `sk_buff`**, offrant des performances proches de DPDK tout en restant intégré au noyau.

```c
// Exemple de programme XDP en C (illustratif)
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <arpa/inet.h>

// Map pour compter les paquets par IP source
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 65536);
    __type(key,   __u32);   // IP source
    __type(value, __u64);   // compteur
} paquets_par_ip SEC(".maps");

SEC("xdp")
int xdp_firewall(struct xdp_md *ctx) {
    void *data     = (void *)(long)ctx->data;
    void *data_end = (void *)(long)ctx->data_end;

    struct ethhdr *eth = data;
    if ((void *)(eth + 1) > data_end) return XDP_PASS;
    if (eth->h_proto != htons(ETH_P_IP)) return XDP_PASS;

    struct iphdr *ip = (void *)(eth + 1);
    if ((void *)(ip + 1) > data_end) return XDP_PASS;

    __u32 src = ip->saddr;
    __u64 *compteur = bpf_map_lookup_elem(&paquets_par_ip, &src);
    if (compteur) {
        (*compteur)++;
        // Bloquer si > 1 000 000 paquets
        if (*compteur > 1000000) return XDP_DROP;
    } else {
        __u64 un = 1;
        bpf_map_update_elem(&paquets_par_ip, &src, &un, BPF_ANY);
    }
    return XDP_PASS;
}
```

```{admonition} Actions XDP
:class: note
Un programme XDP retourne une action qui détermine le destin du paquet :
- `XDP_PASS` : transmettre à la pile réseau Linux normale.
- `XDP_DROP` : éliminer le paquet immédiatement (DDoS mitigation ultra-rapide).
- `XDP_TX` : renvoyer le paquet sur la même interface (réflecteur).
- `XDP_REDIRECT` : rediriger vers une autre interface ou un socket AF_XDP.
```

---

## DPDK — Data Plane Development Kit

DPDK est un ensemble de bibliothèques permettant le **bypass complet du noyau Linux** pour le traitement de paquets haute performance.

### Comparaison des performances

```{code-cell} python
fig, ax = plt.subplots(figsize=(10, 5))

méthodes = ['Kernel\nstandard', 'AF_PACKET\n(v3)', 'eBPF/XDP\n(native)', 'DPDK\n(poll mode)']
débit_mpps = [1.5, 3.5, 14.0, 29.0]  # Millions de paquets par seconde (64 octets)
latence_µs  = [50, 30, 5, 1.5]       # Latence en µs

x = np.arange(len(méthodes))
width = 0.35

ax2 = ax.twinx()

bars1 = ax.bar(x - width/2, débit_mpps, width, color='#4575b4', alpha=0.8,
               label='Débit (Mpps, 64B)', edgecolor='white')
bars2 = ax2.bar(x + width/2, latence_µs, width, color='#d73027', alpha=0.8,
                label='Latence (µs)', edgecolor='white')

ax.set_xticks(x)
ax.set_xticklabels(méthodes, fontsize=11)
ax.set_ylabel("Débit (Millions de paquets/s)", fontsize=11, color='#4575b4')
ax2.set_ylabel("Latence (µs)", fontsize=11, color='#d73027')
ax.set_title("Performances de traitement de paquets\n(CPU 3 GHz, paquets 64 octets)", fontsize=12, fontweight='bold')

for bar, val in zip(bars1, débit_mpps):
    ax.text(bar.get_x() + bar.get_width()/2, bar.get_height()+0.2,
            f"{val} Mpps", ha='center', fontsize=9, color='#4575b4', fontweight='bold')
for bar, val in zip(bars2, latence_µs):
    ax2.text(bar.get_x() + bar.get_width()/2, bar.get_height()+0.3,
             f"{val} µs", ha='center', fontsize=9, color='#d73027', fontweight='bold')

lines1, labels1 = ax.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax.legend(lines1 + lines2, labels1 + labels2, loc='upper left', fontsize=10)

plt.tight_layout()
plt.savefig('_static/dpdk_perf.png', dpi=100, bbox_inches='tight')
plt.show()
```

---

## Data center networking — topologie spine-leaf

La topologie **spine-leaf** est devenue le standard des data centers modernes. Elle remplace la hiérarchie traditionnelle (cœur / distribution / accès) par une architecture **2 couches** offrant des chemins à faible latence et un débit uniforme.

### Principes

- **Leaf switches** : connectés aux serveurs. Chaque leaf est connecté à **tous** les spines.
- **Spine switches** : interconnectent tous les leaves. Aucun spine ne se connecte à un autre spine.
- **ECMP** (*Equal-Cost Multi-Path*) : le trafic est réparti équitablement sur tous les chemins disponibles.
- Tout chemin feuille-à-feuille traverse exactement **2 sauts** : leaf → spine → leaf.

```{code-cell} python
try:
    import networkx as nx
    HAS_NX = True
except ImportError:
    HAS_NX = False

fig, ax = plt.subplots(figsize=(13, 7))
ax.set_xlim(-1, 13)
ax.set_ylim(-1, 7)
ax.axis('off')
ax.set_title("Topologie Spine-Leaf — data center moderne", fontsize=14, fontweight='bold', pad=12)

# Spines (4 switches)
n_spines = 4
n_leaves = 8
spine_y  = 5.5
leaf_y   = 2.0

spine_xs = np.linspace(1.5, 10.5, n_spines)
leaf_xs  = np.linspace(0.5, 11.5, n_leaves)

# Dessiner les connexions spine-leaf (toutes)
for sx in spine_xs:
    for lx in leaf_xs:
        ax.plot([sx, lx], [spine_y - 0.35, leaf_y + 0.35],
                '-', color='#aaaaaa', linewidth=0.8, alpha=0.5, zorder=1)

# Spines
for i, sx in enumerate(spine_xs):
    ax.add_patch(plt.Rectangle((sx-0.65, spine_y-0.35), 1.3, 0.7,
                 facecolor='#4575b4', alpha=0.25, edgecolor='#4575b4', linewidth=2.5,
                 zorder=3))
    ax.text(sx, spine_y, f"Spine {i+1}", ha='center', va='center',
            fontsize=8.5, color='#4575b4', fontweight='bold', zorder=4)

# Leaves + serveurs
for i, lx in enumerate(leaf_xs):
    ax.add_patch(plt.Rectangle((lx-0.55, leaf_y-0.3), 1.1, 0.6,
                 facecolor='#1a9850', alpha=0.25, edgecolor='#1a9850', linewidth=2,
                 zorder=3))
    ax.text(lx, leaf_y, f"Leaf {i+1}", ha='center', va='center',
            fontsize=7.5, color='#1a9850', fontweight='bold', zorder=4)

    # Serveurs sous chaque leaf
    for j in range(2):
        sx_srv = lx + (j - 0.5) * 0.7
        ax.add_patch(mpatches.FancyBboxPatch((sx_srv - 0.25, 0.4), 0.5, 0.6,
                     boxstyle="round,pad=0.05", facecolor='#d73027', alpha=0.18,
                     edgecolor='#d73027', linewidth=1.5, zorder=3))
        ax.text(sx_srv, 0.7, f"S{i*2+j+1}", ha='center', va='center',
                fontsize=6.5, color='#d73027', fontweight='bold', zorder=4)
        ax.plot([lx, sx_srv], [leaf_y - 0.3, 1.0], '-', color='#d73027',
                linewidth=0.8, alpha=0.6, zorder=2)

# Légende ECMP
ax.text(6, -0.5,
        "ECMP : tout chemin Leaf A → Leaf B utilise exactement 2 sauts\n"
        "Le trafic est équilibré sur les 4 chemins disponibles (4 spines)",
        ha='center', va='center', fontsize=9.5, color='#333333',
        bbox=dict(boxstyle='round,pad=0.4', facecolor='#f0f8ff', edgecolor='#4575b4'))

# Légende icônes
for x, label, col in [(0.5, "Spine", '#4575b4'), (2.5, "Leaf", '#1a9850'), (4.5, "Serveur", '#d73027')]:
    ax.add_patch(mpatches.FancyBboxPatch((x-0.3, 6.1), 0.6, 0.45,
                 boxstyle="round,pad=0.05", facecolor=col, alpha=0.25, edgecolor=col, linewidth=1.5))
    ax.text(x+0.5, 6.33, label, va='center', fontsize=9, color=col, fontweight='bold')

plt.tight_layout()
plt.savefig('_static/spine_leaf.png', dpi=100, bbox_inches='tight')
plt.show()
```

### MLAG et zero-oversubscription

- **MLAG** (*Multi-Chassis Link Aggregation*) : regroupe des ports sur deux switches physiques distincts, éliminant le SPoF.
- **Zero oversubscription** : ratio de contention de 1:1 — aucun goulot d'étranglement entre les couches. Impossible à grande échelle sans budgets considérables, mais visé pour les workloads HPC.

---

## VXLAN — overlay réseau L2 sur L3

**VXLAN** (*Virtual eXtensible LAN*) encapsule des trames Ethernet dans des paquets UDP, permettant de créer des réseaux virtuels L2 qui s'étendent au-delà des domaines L3.

### Encapsulation

```{code-cell} python
fig, ax = plt.subplots(figsize=(12, 5))
ax.set_xlim(0, 12)
ax.set_ylim(0, 6)
ax.axis('off')
ax.set_title("Encapsulation VXLAN — trame L2 dans UDP/IP", fontsize=13, fontweight='bold', pad=12)

couches = [
    (0.2, 4.5, 1.6, "Ethernet\nextérieur\n(14 o)", '#555555'),
    (2.0, 4.5, 1.6, "IP extérieur\n(VTEP src→dst)\n(20 o)", '#d73027'),
    (3.8, 4.5, 1.2, "UDP\ndport=4789\n(8 o)", '#f46d43'),
    (5.2, 4.5, 1.2, "VXLAN\nHeader\nVNI (8 o)", '#fdae61'),
    (6.6, 4.5, 1.6, "Ethernet\nintérieur\n(14 o)", '#4575b4'),
    (8.4, 4.5, 1.6, "IP intérieur\n(VM src→dst)\n(20 o)", '#74add1'),
    (10.2, 4.5, 1.5, "Payload\n(TCP/UDP…)", '#abd9e9'),
]
for x, y, w, label, col in couches:
    ax.add_patch(mpatches.FancyBboxPatch((x, y), w, 1.0,
                 boxstyle="round,pad=0.05", facecolor=col, alpha=0.3,
                 edgecolor=col, linewidth=2))
    ax.text(x + w/2, y + 0.5, label, ha='center', va='center',
            fontsize=8, color=col, fontweight='bold')

# Accolades groupées
for x1, x2, y_br, label, col in [
    (0.2,  4.8, 3.8, "Encapsulation réseau physique\n(ajoutée par le VTEP source)", '#d73027'),
    (5.2,  6.0, 3.2, "VXLAN header\n(VNI = identifiant réseau virtuel)", '#fdae61'),
    (6.6, 11.7, 3.8, "Trame originale du tenant\n(VM source → VM destination)", '#4575b4'),
]:
    ax.annotate("", xy=(x1, y_br + 0.3), xytext=(x2, y_br + 0.3),
                arrowprops=dict(arrowstyle='|-|', color=col, lw=1.5))
    ax.text((x1+x2)/2, y_br - 0.0, label, ha='center', va='top', fontsize=8, color=col)

# VNI
ax.text(5.8, 2.0,
        "VNI (VXLAN Network Identifier) — 24 bits → 16 millions de segments virtuels possibles\n"
        "VTEP (VXLAN Tunnel End Point) : l'équipement (physique ou logiciel) qui encapsule/décapsule",
        ha='center', va='center', fontsize=9, color='#333333',
        bbox=dict(boxstyle='round,pad=0.4', facecolor='#f5f5f5', edgecolor='#888888'))

plt.tight_layout()
plt.savefig('_static/vxlan_encap.png', dpi=100, bbox_inches='tight')
plt.show()
```

### BGP EVPN — plan de contrôle pour VXLAN

**BGP EVPN** (*Ethernet VPN*, RFC 7432) est le standard de facto pour distribuer les informations d'accessibilité MAC/IP entre les VTEPs d'un réseau VXLAN. Il remplace l'apprentissage flood-and-learn par un plan de contrôle centralisé.

Types de routes EVPN :
- **Type 2** : MAC/IP Advertisement — distribue les adresses MAC et IP des VMs.
- **Type 3** : Inclusive Multicast — indique les VTEPs participants à chaque VNI.
- **Type 5** : IP Prefix Route — routage inter-VNI (IP routing entre segments).

---

## Kubernetes networking

Kubernetes ajoute plusieurs couches d'abstraction réseau au-dessus de Linux.

### Modèle réseau Kubernetes

```{code-cell} python
fig, ax = plt.subplots(figsize=(13, 7))
ax.set_xlim(0, 13)
ax.set_ylim(0, 8)
ax.axis('off')
ax.set_title("Architecture réseau Kubernetes", fontsize=14, fontweight='bold', pad=12)

# Nœuds
for node_x, node_label in [(1, "Node 1"), (7, "Node 2")]:
    ax.add_patch(mpatches.FancyBboxPatch((node_x - 0.2, 0.5), 5.5, 6.5,
                 boxstyle="round,pad=0.2", facecolor='#f0f0f0',
                 edgecolor='#888888', linewidth=2, zorder=1))
    ax.text(node_x + 2.5, 6.7, node_label, ha='center', fontsize=11,
            fontweight='bold', color='#555555')

    # Pods
    for i, (pod_x, pod_label, pod_col) in enumerate([
        (node_x + 0.5, f"Pod A{i+1}\n10.0.{node_x}.{i+2}/16", '#4575b4'),
        (node_x + 2.5, f"Pod B{i+1}\n10.0.{node_x}.{i+4}/16", '#1a9850'),
    ]):
        ax.add_patch(mpatches.FancyBboxPatch((pod_x - 0.4, 1.8), 1.6, 1.2,
                     boxstyle="round,pad=0.1", facecolor=pod_col, alpha=0.2,
                     edgecolor=pod_col, linewidth=2, zorder=3))
        ax.text(pod_x + 0.4, 2.4, pod_label, ha='center', va='center',
                fontsize=7.5, color=pod_col, fontweight='bold', zorder=4)

        # Veth pair vers cbr0
        ax.plot([pod_x + 0.4, pod_x + 0.4], [1.8, 1.2], '-',
                color=pod_col, linewidth=1.5, alpha=0.7, zorder=2)

    # bridge CNI (cbr0 / cni0)
    ax.add_patch(mpatches.FancyBboxPatch((node_x + 0.1, 0.7), 4.5, 0.55,
                 boxstyle="round,pad=0.05", facecolor='#984ea3', alpha=0.2,
                 edgecolor='#984ea3', linewidth=2, zorder=3))
    ax.text(node_x + 2.35, 0.975, "CNI bridge (cbr0) — réseau pod", ha='center',
            va='center', fontsize=8, color='#984ea3', fontweight='bold', zorder=4)

    # kube-proxy / iptables rules
    ax.add_patch(mpatches.FancyBboxPatch((node_x + 0.1, 3.2), 4.5, 0.7,
                 boxstyle="round,pad=0.05", facecolor='#d73027', alpha=0.15,
                 edgecolor='#d73027', linewidth=1.5, zorder=3))
    ax.text(node_x + 2.35, 3.55, "kube-proxy (iptables/IPVS)\nService → Pod translation",
            ha='center', va='center', fontsize=7.5, color='#d73027', fontweight='bold')

# Services
services = [
    (2.5, 5.0, "ClusterIP Service\n(10.96.0.1:80)", '#4575b4'),
    (7.5, 5.0, "NodePort Service\n(:30080)", '#1a9850'),
    (10.5, 5.0, "LoadBalancer\n(IP externe)", '#d73027'),
]
for x, y, label, col in [(2.5, 5.0, "ClusterIP\n10.96.0.1:80", '#4575b4'),
                          (7.5, 5.0, "NodePort\n:30080", '#1a9850')]:
    ax.add_patch(mpatches.FancyBboxPatch((x-0.8, y-0.35), 1.6, 0.7,
                 boxstyle="round,pad=0.1", facecolor=col, alpha=0.2,
                 edgecolor=col, linewidth=2, zorder=3))
    ax.text(x, y, label, ha='center', va='center', fontsize=8.5, color=col, fontweight='bold')

# Ingress
ax.add_patch(mpatches.FancyBboxPatch((5, 6.8), 3, 0.8,
             boxstyle="round,pad=0.1", facecolor='#ff7f00', alpha=0.2,
             edgecolor='#ff7f00', linewidth=2))
ax.text(6.5, 7.2, "Ingress Controller (Nginx / Traefik)\nHTTPS → Services",
        ha='center', va='center', fontsize=8.5, color='#ff7f00', fontweight='bold')

# Internet
ax.add_patch(plt.Circle((11.5, 7.2), 0.5, facecolor='#4575b4', alpha=0.2,
             edgecolor='#4575b4', linewidth=2))
ax.text(11.5, 7.2, "🌐", ha='center', va='center', fontsize=16)
ax.annotate("", xy=(8, 7.2), xytext=(11, 7.2),
            arrowprops=dict(arrowstyle='->', color='#4575b4', lw=2))

plt.tight_layout()
plt.savefig('_static/k8s_networking.png', dpi=100, bbox_inches='tight')
plt.show()
```

### Types de Services Kubernetes

| Type | Accessibilité | Description |
|------|--------------|-------------|
| `ClusterIP` | Interne cluster uniquement | IP virtuelle, routée par kube-proxy/IPVS |
| `NodePort` | Externe via node IP:port | Ouvre un port (30000-32767) sur chaque nœud |
| `LoadBalancer` | Externe via LB cloud | Provisionne un LB cloud (AWS ELB, GCP LB…) |
| `ExternalName` | DNS alias | Redirige vers un FQDN externe via CNAME |

### CNI — Container Network Interface

Les plugins CNI implémentent la connectivité réseau des pods :

```{code-cell} python
cni_data = {
    'Plugin': ['Flannel', 'Calico', 'Cilium', 'Weave Net', 'Canal'],
    'Modèle réseau': ['VXLAN/host-gw', 'BGP/VXLAN', 'eBPF/VXLAN', 'VXLAN/Fast Datapath', 'VXLAN+Calico'],
    'NetworkPolicy': ['Non', 'Oui', 'Oui (avancé)', 'Oui', 'Oui'],
    'Performance': ['★★★', '★★★★', '★★★★★', '★★★', '★★★★'],
    'Cas d\'usage': ['Simple, débutant', 'Prod, sécurité', 'Haute perf, eBPF', 'On-premise', 'Flannel+Calico'],
}

fig, ax = plt.subplots(figsize=(12, 3.5))
ax.axis('off')

df_cni = pd.DataFrame(cni_data)
table = ax.table(cellText=df_cni.values, colLabels=df_cni.columns,
                 cellLoc='center', loc='center',
                 colWidths=[0.12, 0.22, 0.16, 0.14, 0.30])
table.auto_set_font_size(False)
table.set_fontsize(9)
table.scale(1, 2.2)

for (row, col), cell in table.get_celld().items():
    if row == 0:
        cell.set_facecolor('#2c7bb6')
        cell.set_text_props(color='white', fontweight='bold')
    elif row % 2 == 0:
        cell.set_facecolor('#f0f8ff')
    cell.set_edgecolor('#dddddd')

ax.set_title("Plugins CNI pour Kubernetes", fontsize=12, fontweight='bold', pad=18)
plt.tight_layout()
plt.savefig('_static/cni_comparison.png', dpi=100, bbox_inches='tight')
plt.show()
```

---

## SR-IOV et virtualisation réseau

**SR-IOV** (*Single Root I/O Virtualization*) permet à une carte réseau physique d'exposer plusieurs **fonctions virtuelles** (VF) directement à des machines virtuelles ou des containers, éliminant le goulot d'étranglement de l'hyperviseur.

```{code-cell} python
fig, axes = plt.subplots(1, 2, figsize=(13, 5))

# SR-IOV vs virtio
ax1 = axes[0]
ax1.set_xlim(0, 10)
ax1.set_ylim(0, 7)
ax1.axis('off')
ax1.set_title("SR-IOV : bypass de l'hyperviseur", fontsize=11, fontweight='bold')

couches_virtio = [
    (0.5, 5.8, "VM 1\n(guest OS)", '#4575b4'),
    (0.5, 4.3, "Virtual\nNIC (virtio)", '#74add1'),
    (0.5, 2.8, "Hyperviseur\n(vSwitch)", '#d73027'),
    (0.5, 1.3, "Carte réseau\nphysique (PF)", '#1a9850'),
]
for x, y, label, col in couches_virtio:
    ax1.add_patch(mpatches.FancyBboxPatch((x, y), 3.5, 0.9,
                 boxstyle="round,pad=0.1", facecolor=col, alpha=0.2,
                 edgecolor=col, linewidth=1.5))
    ax1.text(x+1.75, y+0.45, label, ha='center', va='center', fontsize=8.5, color=col, fontweight='bold')
    if y > 1.3:
        ax1.annotate("", xy=(x+1.75, y), xytext=(x+1.75, y+0.9+0.05),
                    arrowprops=dict(arrowstyle='<->', color='#888888', lw=1.5))

ax1.text(2.25, 0.4, "Modèle paravirtualisé (virtio)\nLatence : ~50 µs", ha='center',
         fontsize=8, color='#333333', style='italic')

couches_sriov = [
    (5.5, 5.8, "VM 1\n(guest OS)", '#4575b4'),
    (5.5, 4.3, "VF (Virtual\nFunction)", '#74add1'),
    (5.5, 1.3, "PF + VFs\n(SR-IOV NIC)", '#1a9850'),
]
for x, y, label, col in couches_sriov:
    ax1.add_patch(mpatches.FancyBboxPatch((x, y), 3.5, 0.9,
                 boxstyle="round,pad=0.1", facecolor=col, alpha=0.2,
                 edgecolor=col, linewidth=1.5))
    ax1.text(x+1.75, y+0.45, label, ha='center', va='center', fontsize=8.5, color=col, fontweight='bold')

ax1.plot([7.25, 7.25], [4.3, 2.2], '-', color='#1a9850', linewidth=2)
ax1.annotate("", xy=(7.25, 2.2), xytext=(7.25, 4.3),
            arrowprops=dict(arrowstyle='->', color='#1a9850', lw=2))
ax1.text(7.6, 3.2, "Direct\nDMA", ha='left', fontsize=8, color='#1a9850', fontweight='bold')

ax1.add_patch(mpatches.FancyBboxPatch((5.5, 2.7), 3.5, 0.9,
             boxstyle="round,pad=0.1", facecolor='#dddddd', alpha=0.3,
             edgecolor='#aaaaaa', linewidth=1, linestyle='dashed'))
ax1.text(7.25, 3.15, "Hyperviseur\n(bypassed)", ha='center', va='center',
         fontsize=8, color='#aaaaaa', style='italic')

ax1.text(7.25, 0.4, "SR-IOV\nLatence : ~2-5 µs", ha='center',
         fontsize=8, color='#333333', style='italic')

# SmartNIC / DPU
ax2 = axes[1]
ax2.set_xlim(0, 10)
ax2.set_ylim(0, 6)
ax2.axis('off')
ax2.set_title("SmartNIC / DPU — offload vers le réseau", fontsize=11, fontweight='bold')

composants_dpu = [
    (1.5, 5, "Serveur\nhôte (x86)", '#4575b4'),
    (5, 5,   "DPU / SmartNIC\n(ARM cores + FPGA)", '#d73027'),
    (8.5, 5, "Réseau\n(fabric)", '#1a9850'),
]
for x, y, label, col in composants_dpu:
    ax2.add_patch(mpatches.FancyBboxPatch((x-0.9, y-0.45), 1.8, 0.9,
                 boxstyle="round,pad=0.1", facecolor=col, alpha=0.2,
                 edgecolor=col, linewidth=2))
    ax2.text(x, y, label, ha='center', va='center', fontsize=8.5, color=col, fontweight='bold')

for x1, x2 in [(2.4, 4.1), (5.9, 7.6)]:
    ax2.annotate("", xy=(x2, 5), xytext=(x1, 5),
                arrowprops=dict(arrowstyle='<->', color='#555555', lw=2))

fonctions_offload = [
    "OVS/vSwitch (virtual switching)",
    "Firewall & ACL stateful",
    "IPsec / WireGuard encryption",
    "RDMA / RoCE processing",
    "Telemetry & monitoring",
    "SR-IOV VF management",
]
ax2.text(5, 3.8, "Fonctions offloadées sur le DPU :", ha='center', fontsize=9,
         fontweight='bold', color='#333333')
for i, func in enumerate(fonctions_offload):
    ax2.text(5, 3.3 - i * 0.42, f"• {func}", ha='center', fontsize=8.5, color='#444444')

plt.tight_layout()
plt.savefig('_static/sriov_dpu.png', dpi=100, bbox_inches='tight')
plt.show()
```

---

## Tendances : P4, 400GbE, réseaux optiques

### P4 — Programming Protocol-Independent Packet Processors

P4 est un langage de programmation de la couche de traitement de paquets (**plan de données programmable**). Contrairement à eBPF qui programme le noyau Linux, P4 programme directement les ASICs et FPGAs des équipements réseau.

```{code-cell} python
# Comparaison des approches de data plane programmable

approches = {
    'eBPF/XDP': {
        'Abstraction': 'Noyau Linux',
        'Cible':       'CPU Intel/AMD',
        'Perf (Mpps)': 14,
        'Flexibilité': 9,
        'Simplicité':  8,
    },
    'DPDK': {
        'Abstraction': 'Userspace Linux',
        'Cible':       'CPU (poll-mode)',
        'Perf (Mpps)': 30,
        'Flexibilité': 10,
        'Simplicité':  5,
    },
    'P4/FPGA': {
        'Abstraction': 'FPGA/ASIC',
        'Cible':       'Tofino, FPGA',
        'Perf (Mpps)': 6400,
        'Flexibilité': 8,
        'Simplicité':  4,
    },
    'ASIC fixe': {
        'Abstraction': 'Silicium',
        'Cible':       'Broadcom Trident',
        'Perf (Mpps)': 12800,
        'Flexibilité': 1,
        'Simplicité':  9,
    },
}

fig, axes = plt.subplots(1, 2, figsize=(13, 5))

# Radar chart — Flexibilité vs Simplicité
ax1 = axes[0]
noms = list(approches.keys())
flex = [v['Flexibilité'] for v in approches.values()]
simp = [v['Simplicité']  for v in approches.values()]
perfs_log = [np.log10(v['Perf (Mpps)']) * 3 for v in approches.values()]

cols_rdr = ['#4575b4', '#1a9850', '#d73027', '#984ea3']
for nom, f, s, p_log, col in zip(noms, flex, simp, perfs_log, cols_rdr):
    ax1.scatter(f, s, s=p_log*80, color=col, alpha=0.7, edgecolor='white',
                linewidth=2, zorder=3, label=nom)
    ax1.text(f+0.1, s+0.15, nom, fontsize=9, color=col, fontweight='bold')

ax1.set_xlabel("Flexibilité (1=fixe, 10=totale)", fontsize=11)
ax1.set_ylabel("Facilité d'utilisation", fontsize=11)
ax1.set_title("Flexibilité vs Facilité\n(taille = performances Mpps)", fontsize=11, fontweight='bold')
ax1.set_xlim(0, 11)
ax1.set_ylim(0, 11)
ax1.grid(True, alpha=0.3)

# Évolution des vitesses d'interface
ax2 = axes[1]
années_eth = [1983, 1995, 1999, 2002, 2007, 2010, 2014, 2018, 2021, 2024]
débits_eth = [0.01, 0.1, 1, 10, 10, 40, 100, 400, 400, 800]
étiquettes = ['10M', '100M', '1G', '10G', '10G', '40G', '100G', '400G', '400G', '800G']

ax2.semilogy(années_eth, débits_eth, 'o-', color='#4575b4', linewidth=2.5,
             markersize=8, markerfacecolor='white', markeredgewidth=2.5)
for x, y, lab in zip(années_eth[::2], débits_eth[::2], étiquettes[::2]):
    ax2.annotate(lab, (x, y), textcoords='offset points', xytext=(5, 5),
                 fontsize=9, color='#4575b4', fontweight='bold')

ax2.set_xlabel("Année", fontsize=11)
ax2.set_ylabel("Débit (Gbps, échelle log)", fontsize=11)
ax2.set_title("Évolution des vitesses Ethernet", fontsize=12, fontweight='bold')
ax2.yaxis.set_major_formatter(plt.FuncFormatter(lambda y, _: f"{y:.0f}" if y >= 1 else f"{y*1000:.0f}M"))

plt.tight_layout()
plt.savefig('_static/trends_networking.png', dpi=100, bbox_inches='tight')
plt.show()
```

### Réseaux optiques reconfigurables

Les réseaux **ROADM** (*Reconfigurable Optical Add-Drop Multiplexer*) permettent de router des longueurs d'onde individuelles dans un réseau WDM (*Wavelength Division Multiplexing*) sans conversion optique-électronique-optique (O-E-O).

**400ZR** est le standard cohérent pour les liaisons point-à-point 400G sur 80 km, utilisé dans les interconnexions de data centers.

---

## Simulation topologie spine-leaf avec graphe

```{code-cell} python
# Simulation et visualisation d'une topologie spine-leaf
# Sans dépendance NetworkX — utilisation de matplotlib seul

def simuler_routing_spine_leaf(n_spines: int = 3, n_leaves: int = 6,
                                 serveurs_par_leaf: int = 4) -> dict:
    """
    Simule une topologie spine-leaf et calcule :
    - Le nombre de chemins entre toutes les paires de leaves.
    - Le nombre maximum de sauts.
    """
    # Tous les leaves sont connectés à tous les spines
    # Chemins entre leaf_i et leaf_j : passe par chaque spine (n_spines chemins)
    paires_leaves = n_leaves * (n_leaves - 1) // 2
    total_chemins = paires_leaves * n_spines

    return {
        'spines':            n_spines,
        'leaves':            n_leaves,
        'serveurs_par_leaf': serveurs_par_leaf,
        'total_serveurs':    n_leaves * serveurs_par_leaf,
        'liens_spine_leaf':  n_spines * n_leaves,
        'paires_serveurs':   (n_leaves * serveurs_par_leaf) *
                              (n_leaves * serveurs_par_leaf - 1) // 2,
        'sauts_max':         2,  # toujours 2 pour spine-leaf
        'chemins_par_paire': n_spines,
        'total_chemins':     total_chemins,
    }


# Analyse scalabilité
configs = [(2,4), (3,6), (4,8), (4,16), (8,32), (16,64)]
résultats = [simuler_routing_spine_leaf(s, l, 8) for s, l in configs]
df_scale = pd.DataFrame(résultats)

fig, axes = plt.subplots(1, 3, figsize=(13, 5))
fig.suptitle("Scalabilité de la topologie Spine-Leaf", fontsize=14, fontweight='bold')

configs_labels = [f"{s}S/{l}L" for s, l in configs]

# Total serveurs
axes[0].bar(configs_labels, df_scale['total_serveurs'], color='#4575b4', alpha=0.8, edgecolor='white')
axes[0].set_title("Total serveurs", fontsize=11, fontweight='bold')
axes[0].set_ylabel("Nombre de serveurs")
axes[0].tick_params(axis='x', rotation=45)
for i, v in enumerate(df_scale['total_serveurs']):
    axes[0].text(i, v+1, str(v), ha='center', fontsize=9)

# Liens totaux
axes[1].bar(configs_labels, df_scale['liens_spine_leaf'], color='#1a9850', alpha=0.8, edgecolor='white')
axes[1].set_title("Liens Spine-Leaf", fontsize=11, fontweight='bold')
axes[1].set_ylabel("Nombre de liens")
axes[1].tick_params(axis='x', rotation=45)
for i, v in enumerate(df_scale['liens_spine_leaf']):
    axes[1].text(i, v+0.5, str(v), ha='center', fontsize=9)

# Chemins par paire
axes[2].bar(configs_labels, df_scale['chemins_par_paire'], color='#d73027', alpha=0.8, edgecolor='white')
axes[2].set_title("Chemins ECMP par paire de leaves", fontsize=11, fontweight='bold')
axes[2].set_ylabel("Nombre de chemins")
axes[2].tick_params(axis='x', rotation=45)
axes[2].set_ylim(0, max(df_scale['chemins_par_paire']) + 3)
for i, v in enumerate(df_scale['chemins_par_paire']):
    axes[2].text(i, v+0.1, str(v), ha='center', fontsize=9)

plt.tight_layout()
plt.savefig('_static/spine_leaf_scalability.png', dpi=100, bbox_inches='tight')
plt.show()

print("Analyse scalabilité Spine-Leaf :")
print(df_scale[['spines', 'leaves', 'total_serveurs', 'liens_spine_leaf',
                 'chemins_par_paire', 'sauts_max']].to_string(index=False))
```

---

## Résumé du chapitre

```{code-cell} python
fig, ax = plt.subplots(figsize=(12, 7))
ax.axis('off')

données_résumé = [
    ['SD-WAN', 'WAN', 'Séparation ctrl/données\nOverlay sur Internet', 'Flexibilité WAN\nQoS applicative'],
    ['eBPF/XDP', 'Noyau Linux', 'Programmation kernel\nsandboxée', '14 Mpps\nObservabilité sans overhead'],
    ['DPDK', 'Userspace', 'Bypass noyau\nPoll-mode drivers', '30+ Mpps\nNFV, télécoms'],
    ['Spine-Leaf', 'Data center L2/L3', '2 couches + ECMP\nZero SPoF', 'Latence uniforme\nFacilité de scale'],
    ['VXLAN/BGP EVPN', 'Overlay L2/L3', 'L2 sur UDP/IP\nPlan de contrôle BGP', '16M VNI\nMulti-tenant DC'],
    ['Kubernetes CNI', 'Container', 'Pod réseau flat\nServices + Ingress', 'Cilium (eBPF)\nCalico (BGP)'],
    ['SR-IOV / DPU', 'Virtualisation', 'NIC → VMs directement\nOffload vers DPU', '2–5 µs latence\nLibère le CPU hôte'],
    ['P4', 'Data plane ASIC', 'Protocoles définis par prog.\nFPGA / Tofino', '6400+ Mpps\nReconfigurabilité'],
]
cols = ['Technologie', 'Domaine', 'Principe', 'Bénéfices clés']

table = ax.table(cellText=données_résumé, colLabels=cols,
                 cellLoc='center', loc='center',
                 colWidths=[0.18, 0.17, 0.33, 0.28])
table.auto_set_font_size(False)
table.set_fontsize(8.5)
table.scale(1, 2.1)

for (row, col), cell in table.get_celld().items():
    if row == 0:
        cell.set_facecolor('#2c7bb6')
        cell.set_text_props(color='white', fontweight='bold')
    elif row % 2 == 0:
        cell.set_facecolor('#f5f8fc')
    cell.set_edgecolor('#dddddd')

ax.set_title("Synthèse — technologies de réseaux modernes", fontsize=13, fontweight='bold', pad=20)
plt.tight_layout()
plt.savefig('_static/modern_networks_summary.png', dpi=100, bbox_inches='tight')
plt.show()
```

```{admonition} Points clés du chapitre
:class: tip
- **SD-WAN** découple la logique de routage WAN du matériel et permet de combiner MPLS, Internet et 4G/5G selon des politiques applicatives.
- **eBPF** est la révolution silencieuse du noyau Linux : il permet d'instrumenter, de filtrer et de rediriger les paquets avec une performance proche du matériel, sans modifier le noyau. **XDP** est son point d'accroche le plus bas.
- **DPDK** pousse le concept plus loin en contournant entièrement le noyau ; utilisé pour les routeurs logiciels, les telcos NFV, les pare-feux haute performance.
- **Spine-leaf** est la topologie standard des data centers modernes : 2 sauts max, ECMP sur tous les chemins, passage à l'échelle horizontal aisé.
- **VXLAN** encapsule du L2 dans de l'UDP pour créer des segments virtuels sur n'importe quelle infrastructure IP ; **BGP EVPN** en est le plan de contrôle standard.
- **Kubernetes** abstrait le réseau en trois niveaux : pods (flat L3), Services (ClusterIP/NodePort/LB), Ingress. Les CNI (**Cilium**, Calico, Flannel) implémentent ces abstractions.
- **SR-IOV** et les **SmartNIC/DPU** déportent le traitement réseau hors du CPU principal, libérant des ressources pour les charges applicatives.
- **P4** représente le futur du data plane programmable : les protocoles eux-mêmes peuvent être redéfinis sans changer le silicium.
```
