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

# DNS : résolution de noms

Le DNS (Domain Name System) est le carnet d'adresses distribué d'Internet. Sans lui, accéder à `python.org` impliquerait de mémoriser une adresse IP. Conçu en 1983 par Paul Mockapetris (RFC 882/883, remplacés par les RFC 1034/1035), le DNS est un système hiérarchique, distribué et très extensible. Chaque requête DNS est un voyage à travers une délégation de responsabilités finement conçue.

```{admonition} Objectifs du chapitre
:class: note
- Comprendre l'architecture DNS : résolveur, serveurs récursifs, root servers, TLD, autoritaires
- Maîtriser les types d'enregistrements : A, AAAA, CNAME, MX, NS, TXT, PTR, SRV, CAA
- Analyser la structure binaire d'un message DNS
- Comprendre le cache, le TTL et le DNS poisoning
- Utiliser `dnspython` pour des requêtes avancées
- Découvrir DNSSEC, DoH et DoT
```

```{code-cell} python3
:tags: [hide-input]
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.patches import FancyBboxPatch
import numpy as np
import pandas as pd
import seaborn as sns
import struct
import socket
import random

sns.set_theme(style="whitegrid", palette="muted")
plt.rcParams.update({
    "figure.dpi": 110,
    "axes.titlesize": 13,
    "axes.labelsize": 11,
    "font.family": "sans-serif",
})
```

## Architecture DNS

Le DNS repose sur une hiérarchie de serveurs qui se délèguent mutuellement la responsabilité de portions de l'espace de noms.

```{code-cell} python3
fig, ax = plt.subplots(figsize=(13, 7))
ax.set_xlim(0, 14)
ax.set_ylim(0, 9)
ax.axis("off")
ax.set_title("Architecture DNS — Hiérarchie et résolution", fontsize=14, fontweight="bold")

def dns_box(ax, x, y, w, h, title, subtitle, color, fontsize=9):
    r = FancyBboxPatch((x, y), w, h, boxstyle="round,pad=0.12",
                       linewidth=1.8, edgecolor=color, facecolor=color, alpha=0.88)
    ax.add_patch(r)
    ax.text(x + w/2, y + h*0.62, title, ha="center", va="center",
            fontsize=fontsize, fontweight="bold", color="white")
    ax.text(x + w/2, y + h*0.2, subtitle, ha="center", va="center",
            fontsize=7, color="white", alpha=0.92)

def dns_arrow(ax, x1, y1, x2, y2, label, color="#555", style="->"):
    ax.annotate("", xy=(x2, y2), xytext=(x1, y1),
                arrowprops=dict(arrowstyle=style, color=color, lw=1.8))
    mx, my = (x1+x2)/2, (y1+y2)/2
    ax.text(mx + 0.1, my + 0.15, label, ha="center", fontsize=7.5, color=color)

# Client / Stub Resolver
dns_box(ax, 0.3, 4.0, 2.4, 0.9, "Application\n(navigateur)", "stub resolver", "#C96DD8")

# Résolveur récursif
dns_box(ax, 3.5, 4.0, 2.8, 0.9, "Résolveur récursif", "FAI / 8.8.8.8 / 1.1.1.1", "#4C9BE8")

# Root servers
dns_box(ax, 8, 7.5, 3, 0.9, "Root Name Servers", "13 clusters : a.root-servers.net…\n→ connaissent les TLD", "#E87A4C")

# TLD servers
dns_box(ax, 3.5, 7.5, 2.8, 0.9, "Serveurs TLD", ".com .fr .org .net\n→ délèguent aux NS", "#F0C040")
dns_box(ax, 8, 5.7, 3, 0.9, "Serveurs TLD\n(autoritaires TLD)", ".com, .fr, .org", "#F0C040")

# Serveur autoritaire
dns_box(ax, 3.5, 5.7, 2.8, 0.9, "Serveur Autoritaire", "ns1.example.com\n→ connaît les RR", "#54B87A")

# Cache
dns_box(ax, 0.3, 6.5, 2.4, 0.8, "Cache DNS\n(résolveur)", "TTL — Réponses mémorisées", "#888888")

# Flèches de résolution
dns_arrow(ax, 2.7, 4.45, 3.5, 4.45, "1. Requête", "#C96DD8")
dns_arrow(ax, 6.3, 4.7, 8.0, 7.5, "2. Vers root\n(si pas en cache)", "#4C9BE8")
dns_arrow(ax, 8.0, 7.5, 6.3, 7.5, "3. Délég. TLD", "#E87A4C")
dns_arrow(ax, 6.3, 7.5, 6.3, 5.7, "4. Vers TLD", "#F0C040")
dns_arrow(ax, 6.3, 5.7, 6.3, 4.9, "5. Délég. auth.", "#F0C040")
dns_arrow(ax, 3.5, 5.7, 6.3, 5.7, "→ autoritaire", "#F0C040")
dns_arrow(ax, 3.5, 5.0, 6.3, 4.5, "6. Réponse finale", "#54B87A")
dns_arrow(ax, 3.5, 4.1, 2.7, 4.1, "7. Réponse", "#4C9BE8")

# Cache
ax.annotate("", xy=(1.5, 6.5), xytext=(4.0, 4.9),
            arrowprops=dict(arrowstyle="<->", color="#888888", lw=1.5, linestyle="dashed"))
ax.text(2.2, 5.7, "cache", fontsize=8, color="#888888", style="italic")

ax.text(7, 0.5,
        "Les 13 clusters root servers sont distribués géographiquement via anycast.\n"
        "ICANN supervise la zone racine. IANA maintient la liste des TLD (~1500).",
        ha="center", fontsize=9, color="#333333",
        bbox=dict(facecolor="#F0F4F8", edgecolor="#AAAAAA", boxstyle="round,pad=0.3"))

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

## Résolution récursive vs itérative

```{code-cell} python3
fig, axes = plt.subplots(1, 2, figsize=(14, 6))

for ax, titre, mode in zip(axes,
    ["Résolution RÉCURSIVE\n(vue du client)", "Résolution ITÉRATIVE\n(vue du résolveur récursif)"],
    ["recursive", "iterative"]):

    ax.set_xlim(0, 10)
    ax.set_ylim(0, 9)
    ax.axis("off")
    ax.set_title(titre, fontweight="bold")

    entites = [("Client", 1, "#C96DD8"), ("Résolveur", 3.5, "#4C9BE8"),
               ("Root", 6.5, "#E87A4C"), ("TLD .com", 8.5, "#F0C040")]
    for nom, x, c in entites:
        r = FancyBboxPatch((x - 0.7, 8.3), 1.4, 0.6, boxstyle="round,pad=0.08",
                           linewidth=1.5, edgecolor=c, facecolor=c, alpha=0.9)
        ax.add_patch(r)
        ax.text(x, 8.6, nom, ha="center", va="center", fontsize=8.5,
                fontweight="bold", color="white")
        ax.plot([x, x], [0.5, 8.3], "--", color="#CCCCCC", lw=1)

    if mode == "recursive":
        etapes = [
            (1, 3.5, 7.5, "1. python.org ?", "#C96DD8", "->"),
            (3.5, 6.5, 6.5, "2. python.org ?", "#4C9BE8", "->"),
            (6.5, 8.5, 5.5, "3. python.org ?", "#E87A4C", "->"),
            (8.5, 6.5, 4.5, "4. ns1.python.org", "#F0C040", "<-"),
            (6.5, 3.5, 3.5, "5. ns1.python.org", "#E87A4C", "<-"),
            (3.5, 1, 2.5, "6. 151.101.x.x", "#4C9BE8", "<-"),
        ]
    else:  # iterative
        etapes = [
            (1, 3.5, 7.5, "1. python.org ?", "#C96DD8", "->"),
            (3.5, 6.5, 6.5, "2. Qui gère .org ?", "#4C9BE8", "->"),
            (6.5, 3.5, 5.5, "3. ns1.org!", "#E87A4C", "<-"),
            (3.5, 8.5, 4.5, "4. python.org ?", "#4C9BE8", "->"),
            (8.5, 3.5, 3.5, "5. 151.101.x.x!", "#F0C040", "<-"),
            (3.5, 1, 2.5, "6. 151.101.x.x", "#4C9BE8", "<-"),
        ]

    for src_x, dst_x, y, label, color, direction in etapes:
        ax.annotate("", xy=(dst_x, y), xytext=(src_x, y),
                    arrowprops=dict(arrowstyle=direction, color=color, lw=1.8))
        ax.text((src_x + dst_x)/2, y + 0.18, label, ha="center",
                fontsize=7.5, color=color, fontweight="bold")

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

## Types d'enregistrements DNS

```{code-cell} python3
types_rr = pd.DataFrame({
    "Type": ["A", "AAAA", "CNAME", "MX", "NS", "TXT", "PTR", "SRV", "CAA", "SOA"],
    "Description": [
        "Adresse IPv4", "Adresse IPv6",
        "Alias canonique", "Serveur de mail (avec priorité)",
        "Serveur de noms autoritaire", "Texte libre (SPF, DKIM, vérification…)",
        "Reverse DNS (IP → nom)", "Service avec port et priorité",
        "Certificate Authority Authorization", "Start of Authority (métadonnées zone)"
    ],
    "Exemple de valeur": [
        "151.101.65.69", "2a04:4e42:600::313",
        "www.example.com. → example.com.", "10 mail.example.com.",
        "ns1.example.com.", "v=spf1 include:_spf.google.com ~all",
        "69.65.101.151.in-addr.arpa. → pypi.org.", "_http._tcp.example.com. 443",
        "0 issue \"letsencrypt.org\"", "ns1.example.com. admin.example.com. 2024…"
    ],
    "RFC": [1035, 3596, 1035, 1035, 1035, 1035, 1035, 2782, 6844, 1035]
})

print(types_rr.to_string(index=False))
```

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

# ── Fréquence d'utilisation des types ────────────────────────────────────────
ax1 = axes[0]
types = ["A", "AAAA", "CNAME", "MX", "TXT", "NS", "PTR", "SRV", "CAA", "SOA"]
freq = [100, 75, 85, 60, 80, 55, 40, 25, 30, 10]  # fréquence relative estimée
colors_t = plt.cm.tab10(np.linspace(0, 1, len(types)))
bars = ax1.barh(types[::-1], freq[::-1], color=colors_t[::-1], edgecolor="white")
ax1.set_xlabel("Utilisation relative (%)")
ax1.set_title("Fréquence d'utilisation des types DNS", fontweight="bold")
ax1.set_xlim(0, 115)
ax1.grid(axis="x", alpha=0.4)
for bar, v in zip(bars, freq[::-1]):
    ax1.text(v + 1, bar.get_y() + bar.get_height()/2, f"{v}%",
             va="center", fontsize=9)

# ── Diagramme enregistrement MX ──────────────────────────────────────────────
ax2 = axes[1]
ax2.set_xlim(0, 10)
ax2.set_ylim(0, 7)
ax2.axis("off")
ax2.set_title("Enregistrement MX — Priorités", fontweight="bold")

domain = "example.com"
ax2.text(5, 6.5, f"Zone DNS : {domain}", ha="center", fontsize=11,
         fontweight="bold", color="#2C3E50")

mx_records = [
    (10, "mail1.example.com", "#4C9BE8", "Primaire"),
    (20, "mail2.example.com", "#54B87A", "Secondaire"),
    (30, "fallback.provider.com", "#E87A4C", "Fallback externe"),
]
for i, (prio, host, color, label) in enumerate(mx_records):
    y = 5.0 - i * 1.3
    r = FancyBboxPatch((1.5, y - 0.3), 7, 0.65, boxstyle="round,pad=0.1",
                       linewidth=1.5, edgecolor=color, facecolor=color, alpha=0.85)
    ax2.add_patch(r)
    ax2.text(3.5, y + 0.02, f"MX {prio:3d}  {host}", ha="center", va="center",
             fontsize=9.5, fontweight="bold", color="white")
    ax2.text(8.8, y + 0.02, label, ha="right", va="center", fontsize=8.5, color=color,
             bbox=dict(facecolor="white", edgecolor=color, boxstyle="round,pad=0.1"))

ax2.text(5, 1.0, "Le serveur SMTP essaie d'abord la priorité la plus basse (10),\n"
         "puis 20, puis 30 si les serveurs précédents sont indisponibles.",
         ha="center", fontsize=8.5, color="#333",
         bbox=dict(facecolor="#F0F4F8", edgecolor="#AAAAAA", boxstyle="round,pad=0.3"))

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

## Structure d'un message DNS

```{code-cell} python3
import struct

def parse_dns_message(data: bytes) -> dict:
    """Parse un message DNS brut (structure simplifiée)."""
    if len(data) < 12:
        raise ValueError("Message DNS trop court")

    # En-tête (12 octets)
    (txid, flags, qdcount, ancount, nscount, arcount) = struct.unpack("!HHHHHH", data[:12])

    qr     = (flags >> 15) & 0x1     # 0=Requête, 1=Réponse
    opcode = (flags >> 11) & 0xF
    aa     = (flags >> 10) & 0x1     # Authoritative Answer
    tc     = (flags >> 9) & 0x1      # TrunCated
    rd     = (flags >> 8) & 0x1      # Recursion Desired
    ra     = (flags >> 7) & 0x1      # Recursion Available
    rcode  = flags & 0xF             # 0=NOERROR, 3=NXDOMAIN

    rcodes = {0: "NOERROR", 1: "FORMERR", 2: "SERVFAIL",
              3: "NXDOMAIN", 5: "REFUSED"}

    return {
        "transaction_id": f"0x{txid:04X}",
        "type": "Réponse" if qr else "Requête",
        "opcode": opcode,
        "authoritative": bool(aa),
        "truncated": bool(tc),
        "recursion_desired": bool(rd),
        "recursion_available": bool(ra),
        "rcode": rcodes.get(rcode, f"?{rcode}"),
        "questions": qdcount,
        "answers": ancount,
        "authority": nscount,
        "additional": arcount,
    }

def build_dns_query(domain: str, qtype: int = 1) -> bytes:
    """Construit une requête DNS complète."""
    txid = random.randint(0, 65535)
    flags = 0x0100  # Requête standard, RD=1
    qdcount = 1
    header = struct.pack("!HHHHHH", txid, flags, qdcount, 0, 0, 0)

    qname = b""
    for label in domain.split("."):
        enc = label.encode("ascii")
        qname += bytes([len(enc)]) + enc
    qname += b"\x00"

    question = struct.pack("!HH", qtype, 1)  # qtype, qclass=IN
    return header + qname + question

# Construire et analyser une requête DNS pour python.org
query = build_dns_query("python.org")
parsed = parse_dns_message(query)

print("=== Requête DNS pour python.org (type A) ===")
print(f"Octets totaux : {len(query)}")
print(f"En-tête (12 octets) : {query[:12].hex(' ')}")
print()
for k, v in parsed.items():
    print(f"  {k:<25} = {v}")
```

```{code-cell} python3
fig, axes = plt.subplots(1, 2, figsize=(14, 5.5))

# ── Structure du message DNS ─────────────────────────────────────────────────
ax = axes[0]
ax.set_xlim(0, 10)
ax.set_ylim(0, 9)
ax.axis("off")
ax.set_title("Structure d'un message DNS", fontweight="bold")

sections = [
    (7.5, 1.0, "En-tête (12 octets)",
     "ID | QR | Opcode | AA | TC | RD | RA | RCODE\nQDCOUNT | ANCOUNT | NSCOUNT | ARCOUNT",
     "#4C9BE8"),
    (5.5, 0.9, "Section Question",
     "QNAME (domaine encodé) | QTYPE | QCLASS", "#54B87A"),
    (3.8, 0.9, "Section Réponse",
     "NAME | TYPE | CLASS | TTL | RDLENGTH | RDATA", "#E87A4C"),
    (2.1, 0.9, "Section Autorité (NS)",
     "Enregistrements NS de référence", "#C96DD8"),
    (0.4, 0.9, "Section Additionnelle",
     "Glue records (adresses des NS)", "#888888"),
]

y = 8.3
for h, dh, title, subtitle, color in sections:
    r = FancyBboxPatch((0.5, y - dh), 9, dh, boxstyle="round,pad=0.08",
                       linewidth=1.5, edgecolor=color, facecolor=color, alpha=0.85)
    ax.add_patch(r)
    ax.text(5, y - dh/2 + 0.15, title, ha="center", va="center",
            fontsize=9.5, fontweight="bold", color="white")
    ax.text(5, y - dh/2 - 0.2, subtitle, ha="center", va="center",
            fontsize=7.5, color="white", alpha=0.9)
    y -= dh + 0.15

# ── Flags de l'en-tête DNS ───────────────────────────────────────────────────
ax2 = axes[1]
ax2.set_xlim(0, 16)
ax2.set_ylim(-0.5, 4.5)
ax2.axis("off")
ax2.set_title("Champ Flags de l'en-tête DNS (16 bits)", fontweight="bold")

flags_bits = [
    ("QR", 1, "#E87A4C", "Requête(0)/Réponse(1)"),
    ("Opcode", 4, "#4C9BE8", "0=Query 1=iQuery 2=Status"),
    ("AA", 1, "#54B87A", "Authoritative Answer"),
    ("TC", 1, "#C96DD8", "TrunCated"),
    ("RD", 1, "#F0C040", "Recursion Desired"),
    ("RA", 1, "#4C9BE8", "Recursion Available"),
    ("Z", 1, "#AAAAAA", "Réservé"),
    ("RCODE", 4, "#E87A4C", "0=OK 3=NXDOMAIN"),
]

x = 0
for i, (name, width, color, desc) in enumerate(flags_bits):
    r = FancyBboxPatch((x + 0.05, 2.8), width - 0.1, 0.9,
                       boxstyle="round,pad=0.05", linewidth=1,
                       edgecolor="white", facecolor=color, alpha=0.85)
    ax2.add_patch(r)
    ax2.text(x + width/2, 3.25, name, ha="center", va="center",
             fontsize=8 if width > 1 else 7, fontweight="bold", color="white")
    ax2.text(x + width/2, 2.3, f"{width} bit{'s' if width > 1 else ''}",
             ha="center", fontsize=7, color="#555555")
    ax2.text(x + width/2, 1.8, desc, ha="center", fontsize=6.5, color="#333333",
             wrap=True, rotation=0 if width > 2 else 0)
    x += width

ax2.axhline(2.8, color="#AAAAAA", lw=0.5, xmin=0, xmax=1)
ax2.text(8, 4.3, "Bits 0–15", ha="center", fontsize=10, color="#333333")

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

## TTL, cache et DNS poisoning

```{code-cell} python3
import time

# Simulation d'un cache DNS avec TTL
class SimpleDNSCache:
    """Cache DNS basique avec expiration par TTL."""

    def __init__(self):
        self._cache: dict = {}  # domain → (ip, expire_at)

    def set(self, domain: str, ip: str, ttl: int) -> None:
        expire_at = time.monotonic() + ttl
        self._cache[domain] = (ip, expire_at)
        print(f"[CACHE] {domain:<30} → {ip:<20} TTL={ttl}s "
              f"(expire dans {ttl}s)")

    def get(self, domain: str) -> str | None:
        if domain not in self._cache:
            print(f"[CACHE] MISS   : {domain}")
            return None
        ip, expire_at = self._cache[domain]
        remaining = expire_at - time.monotonic()
        if remaining <= 0:
            del self._cache[domain]
            print(f"[CACHE] EXPIRED: {domain} (TTL expiré)")
            return None
        print(f"[CACHE] HIT    : {domain} → {ip} (TTL restant: {remaining:.1f}s)")
        return ip

    def negative_cache(self, domain: str, ttl: int = 60) -> None:
        """Cache une réponse NXDOMAIN (negative caching — RFC 2308)."""
        expire_at = time.monotonic() + ttl
        self._cache[domain] = ("NXDOMAIN", expire_at)
        print(f"[CACHE] NXDOMAIN: {domain} mis en cache {ttl}s")

    def stats(self) -> None:
        print(f"\n[CACHE] Entrées actives : {len(self._cache)}")
        for domain, (ip, exp) in self._cache.items():
            remaining = max(0, exp - time.monotonic())
            print(f"  {domain:<35} → {ip:<20} TTL restant: {remaining:.1f}s")


# Démonstration
cache = SimpleDNSCache()
print("=== Démonstration cache DNS ===\n")
cache.set("python.org", "151.101.65.69", 300)
cache.set("pypi.org", "151.101.108.223", 60)
cache.set("docs.python.org", "151.101.65.69", 120)
cache.negative_cache("inexistant.example.com", 30)

print()
cache.get("python.org")
cache.get("pypi.org")
cache.get("unknown.example.com")
cache.get("inexistant.example.com")
cache.stats()
```

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

# ── Cycle de vie d'un enregistrement DNS ─────────────────────────────────────
ax = axes[0]
ttl_values = [3600, 300, 60, 5, 86400, 0]
domains = ["A record\nstatique", "CDN\ndynamique", "Résolveur\nlocal",
           "Roundrobin\napplicatif", "DMARC/SPF\nstable", "TTL=0\n(no cache)"]
colors_ttl = ["#4C9BE8" if t > 1000 else
              "#54B87A" if t > 100 else
              "#F0C040" if t > 10 else
              "#E87A4C" for t in ttl_values]
colors_ttl[-1] = "#C96DD8"
bars = ax.bar(domains, ttl_values, color=colors_ttl, edgecolor="white", width=0.6)
ax.set_yscale("log")
ax.set_ylabel("TTL (secondes) — échelle logarithmique")
ax.set_title("TTL typiques selon le type d'enregistrement", fontweight="bold")
ax.grid(axis="y", alpha=0.4)
for bar, v in zip(bars, ttl_values):
    lbl = f"{v}s" if v > 0 else "no-cache"
    ax.text(bar.get_x() + bar.get_width()/2, max(bar.get_height() * 1.3, 2),
            lbl, ha="center", fontsize=9, fontweight="bold")
ax.set_ylim(0.5, 200000)

# ── DNS poisoning ─────────────────────────────────────────────────────────────
ax2 = axes[1]
ax2.set_xlim(0, 10)
ax2.set_ylim(0, 8)
ax2.axis("off")
ax2.set_title("DNS Cache Poisoning (Attaque Kaminsky)", fontweight="bold")

etapes_poison = [
    (3.5, 7.0, "1. Résolveur\ninterroge Root/TLD", "#4C9BE8"),
    (3.5, 5.3, "2. Attaquant envoie\nde fausses réponses\n(flood de TxID)", "#E87A4C"),
    (3.5, 3.6, "3. Si TxID deviné :\ncache empoisonné\nbad.com → IP malveillante", "#E87A4C"),
    (3.5, 1.9, "4. Victimes redirigées\nvers serveur malveillant", "#C96DD8"),
]
for x, y, txt, color in etapes_poison:
    r = FancyBboxPatch((x - 2.5, y - 0.55), 5, 0.9, boxstyle="round,pad=0.1",
                       linewidth=1.5, edgecolor=color, facecolor=color, alpha=0.85)
    ax2.add_patch(r)
    ax2.text(x, y - 0.1, txt, ha="center", va="center", fontsize=8.5,
             fontweight="bold", color="white")

ax2.text(8.5, 4.5, "Solution :\nDNSSEC\n(signatures\nRRSIG)", ha="center",
         fontsize=9, fontweight="bold", color="#54B87A",
         bbox=dict(facecolor="#F0FFF4", edgecolor="#54B87A", boxstyle="round,pad=0.3"))

for y1, y2 in [(6.45, 5.85), (4.75, 4.15), (2.95, 2.35)]:
    ax2.annotate("", xy=(3.5, y2), xytext=(3.5, y1),
                arrowprops=dict(arrowstyle="->", color="#555", lw=1.5))

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

## DNSSEC

DNSSEC ajoute une couche de signatures cryptographiques aux enregistrements DNS pour garantir leur authenticité et intégrité.

```{code-cell} python3
dnssec_records = pd.DataFrame({
    "Type DNSSEC": ["DNSKEY", "RRSIG", "DS", "NSEC / NSEC3", "CDS / CDNSKEY"],
    "Rôle": [
        "Clé publique de la zone (KSK et ZSK)",
        "Signature d'un Resource Record Set",
        "Delegation Signer — empreinte du DNSKEY fils dans zone parente",
        "Preuve d'inexistence (NXDOMAIN authenticated)",
        "Mise à jour automatique des clés (RFC 7344)"
    ],
    "Analogue TLS": [
        "Clé publique du certificat",
        "Signature du certificat par la CA",
        "Empreinte du certificat dans la chaîne",
        "—",
        "Renouvellement automatique"
    ]
})

print(dnssec_records.to_string(index=False))
```

## DoH et DoT : confidentialité des requêtes

```{code-cell} python3
fig, ax = plt.subplots(figsize=(13, 4.5))
ax.set_xlim(0, 13)
ax.set_ylim(0, 6)
ax.axis("off")
ax.set_title("DNS traditionnel vs DoT vs DoH — Confidentialité", fontsize=12, fontweight="bold")

modes = [
    (1.5, "DNS\nclassique\n(UDP/53)", "Pas de chiffrement\nISP voit toutes\nles requêtes",
     "#E87A4C", "✗ Conf.\n✗ Auth."),
    (5, "DoT\n(DNS over TLS)\nport 853", "TLS 1.3\nRequêtes chiffrées\nPort distinct",
     "#4C9BE8", "✓ Conf.\n✓ Auth."),
    (9, "DoH\n(DNS over HTTPS)\nport 443", "DNS dans HTTP/2\nTrafic indiscernable\ndu HTTPS normal",
     "#54B87A", "✓ Conf.\n✓ Auth.\n✓ Bypass filtre"),
]

for x, title, desc, color, verdict in modes:
    r = FancyBboxPatch((x - 1.6, 1.5), 3.2, 4.1, boxstyle="round,pad=0.2",
                       linewidth=2, edgecolor=color, facecolor=color, alpha=0.85)
    ax.add_patch(r)
    ax.text(x, 5.1, title, ha="center", va="center", fontsize=9.5,
            fontweight="bold", color="white")
    ax.text(x, 3.4, desc, ha="center", va="center", fontsize=8.5, color="white")
    ax.text(x, 1.9, verdict, ha="center", va="center", fontsize=8.5,
            fontweight="bold", color="white")

ax.text(6.5, 0.5, "RFC 7858 (DoT) — RFC 8484 (DoH) — Résolveurs publics : 1.1.1.1, 8.8.8.8, 9.9.9.9",
        ha="center", fontsize=9, color="#555555")

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

## Code Python avec dnspython

```{code-cell} python3
try:
    import dns.resolver
    import dns.reversename
    import dns.message
    import dns.query
    import dns.rdatatype
    DNS_AVAILABLE = True
except ImportError:
    DNS_AVAILABLE = False
    print("dnspython non installé — illustrations avec socket stdlib")

if DNS_AVAILABLE:
    resolver = dns.resolver.Resolver()
    resolver.timeout = 5
    resolver.lifetime = 10

    # ── Résolution A / AAAA ───────────────────────────────────────────────────
    print("=== Résolution A (IPv4) ===")
    try:
        answers = resolver.resolve("python.org", "A")
        for rdata in answers:
            print(f"  python.org A → {rdata.address}  (TTL {answers.ttl}s)")
    except Exception as e:
        print(f"  Erreur : {e}")

    print("\n=== Résolution AAAA (IPv6) ===")
    try:
        answers = resolver.resolve("python.org", "AAAA")
        for rdata in answers:
            print(f"  python.org AAAA → {rdata.address}")
    except Exception as e:
        print(f"  Erreur : {e}")

    # ── MX records ────────────────────────────────────────────────────────────
    print("\n=== Enregistrements MX ===")
    try:
        answers = resolver.resolve("python.org", "MX")
        for rdata in sorted(answers, key=lambda r: r.preference):
            print(f"  MX {rdata.preference:3d} → {rdata.exchange}")
    except Exception as e:
        print(f"  Erreur : {e}")

    # ── TXT records ───────────────────────────────────────────────────────────
    print("\n=== Enregistrements TXT ===")
    try:
        answers = resolver.resolve("python.org", "TXT")
        for rdata in answers:
            for txt in rdata.strings:
                decoded = txt.decode("utf-8", errors="replace")
                print(f"  TXT → {decoded[:80]}{'…' if len(decoded) > 80 else ''}")
    except Exception as e:
        print(f"  Erreur : {e}")
```

```{code-cell} python3
if DNS_AVAILABLE:
    # ── Reverse DNS (PTR) ─────────────────────────────────────────────────────
    print("=== Reverse DNS (PTR) ===")
    test_ips = ["8.8.8.8", "1.1.1.1", "151.101.65.69"]
    for ip in test_ips:
        try:
            rev_name = dns.reversename.from_address(ip)
            answers = resolver.resolve(rev_name, "PTR")
            for rdata in answers:
                print(f"  {ip:<20} → {rdata.target}")
        except Exception as e:
            print(f"  {ip:<20} → Erreur : {e}")

    # ── NS records ────────────────────────────────────────────────────────────
    print("\n=== Serveurs de noms autoritaires (NS) ===")
    try:
        answers = resolver.resolve("python.org", "NS")
        for rdata in answers:
            print(f"  NS → {rdata.target}")
    except Exception as e:
        print(f"  Erreur : {e}")

    # ── Inspection complète d'une réponse DNS ─────────────────────────────────
    print("\n=== Inspection d'une réponse DNS brute ===")
    try:
        qname = dns.name.from_text("python.org.")
        request = dns.message.make_query(qname, dns.rdatatype.A)
        response = dns.query.udp(request, "8.8.8.8", timeout=5)
        print(f"  Transaction ID : {response.id}")
        print(f"  Flags          : {dns.flags.to_text(response.flags)}")
        print(f"  Réponses       : {len(response.answer)} section(s)")
        for rrset in response.answer:
            print(f"  RRset : {rrset.name} TTL={rrset.ttl} type={rrset.rdtype}")
            for rr in rrset:
                print(f"    → {rr}")
    except Exception as e:
        print(f"  Erreur : {e}")
else:
    # Fallback avec socket stdlib
    print("=== Résolution basique avec socket.getaddrinfo() ===")
    for host in ["python.org", "pypi.org", "docs.python.org"]:
        try:
            results = socket.getaddrinfo(host, 80, socket.AF_INET)
            ip = results[0][4][0]
            print(f"  {host:<30} → {ip}")
        except Exception as e:
            print(f"  {host:<30} → Erreur : {e}")
```

```{code-cell} python3
# Visualisation : temps de résolution DNS simulé
import random

def simulate_dns_resolution_times(domain: str, n: int = 100, seed: int = 42) -> dict:
    """Simule les temps de résolution selon la présence en cache."""
    rng = random.Random(seed)
    cached_times = [max(0.1, rng.gauss(0.5, 0.15)) for _ in range(n)]    # < 1 ms
    recursive_times = [max(5, rng.gauss(50, 20)) for _ in range(n)]      # ~50 ms
    return {"cached_ms": cached_times, "recursive_ms": recursive_times}

sims = simulate_dns_resolution_times("python.org")

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

ax1 = axes[0]
ax1.hist(sims["cached_ms"], bins=30, color="#54B87A", alpha=0.8,
         label=f"Cache HIT (moy={np.mean(sims['cached_ms']):.2f} ms)")
ax1.hist(sims["recursive_ms"], bins=30, color="#E87A4C", alpha=0.8,
         label=f"Récursif (moy={np.mean(sims['recursive_ms']):.1f} ms)")
ax1.set_xlabel("Temps de résolution (ms)")
ax1.set_ylabel("Fréquence")
ax1.set_title("Distribution des temps de résolution DNS", fontweight="bold")
ax1.legend(fontsize=9)
ax1.grid(alpha=0.4)

ax2 = axes[1]
scenarios = ["Cache local\n(stub)", "Résolveur FAI\n(cache chaud)", "Résolveur\n(cache froid)",
             "Root → TLD\n→ Autoritaire"]
medians = [0.5, 5, 50, 200]
p95s = [1.2, 15, 120, 450]
x_s = np.arange(len(scenarios))
ax2.bar(x_s - 0.2, medians, 0.35, label="Médiane (ms)", color="#4C9BE8", alpha=0.85)
ax2.bar(x_s + 0.2, p95s, 0.35, label="P95 (ms)", color="#E87A4C", alpha=0.85)
ax2.set_yscale("log")
ax2.set_ylabel("Latence (ms) — log")
ax2.set_title("Latences DNS selon la couche de cache", fontweight="bold")
ax2.set_xticks(x_s)
ax2.set_xticklabels(scenarios, fontsize=8.5)
ax2.legend(fontsize=9)
ax2.grid(axis="y", alpha=0.4)
for i, (m, p) in enumerate(zip(medians, p95s)):
    ax2.text(i - 0.2, m * 1.4, f"{m}", ha="center", fontsize=8)
    ax2.text(i + 0.2, p * 1.4, f"{p}", ha="center", fontsize=8)

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

## Résumé

```{code-cell} python3
fig, ax = plt.subplots(figsize=(12, 5.5))
ax.axis("off")
ax.set_title("Récapitulatif — DNS", fontsize=14, fontweight="bold", pad=15)

resume = [
    ["Architecture", "Client → Résolveur récursif → Root → TLD → Autoritaire"],
    ["Types d'enregistrements", "A, AAAA, CNAME, MX, NS, TXT, PTR, SRV, CAA, SOA"],
    ["TTL", "Durée de validité en cache ; positif (RR) ou négatif (NXDOMAIN - RFC 2308)"],
    ["DNS poisoning", "Injection de fausses réponses dans le cache (attaque Kaminsky)"],
    ["DNSSEC", "Signatures RRSIG + clés DNSKEY — chaîne de confiance jusqu'à la racine"],
    ["DoT (port 853)", "DNS over TLS — chiffrement de la requête DNS"],
    ["DoH (port 443)", "DNS over HTTPS — indiscernable du trafic HTTPS normal"],
    ["dnspython", "Bibliothèque Python complète : resolver, query UDP/TCP, message parsing"],
    ["socket.getaddrinfo()", "Résolution simple en stdlib Python — délègue au resolver OS"],
]

table = ax.table(
    cellText=resume,
    colLabels=["Concept", "Description"],
    cellLoc="left",
    loc="center",
    colWidths=[0.25, 0.65]
)
table.auto_set_font_size(False)
table.set_fontsize(9.5)
table.scale(1, 1.8)

for j in range(2):
    table[0, j].set_facecolor("#2C3E50")
    table[0, j].set_text_props(color="white", fontweight="bold")
for i in range(1, len(resume) + 1):
    for j in range(2):
        if i % 2 == 0:
            table[i, j].set_facecolor("#F5F7FA")
        if j == 0:
            table[i, j].set_text_props(fontweight="bold", color="#2C3E50", fontsize=9)

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