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.

Objectifs du chapitre

  • 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

Hide code cell source

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.

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()
_images/16a2865f2f314a26baf8a0d9225aaccd1706d8d9e1e9285a18991732ccaa22a1.png

Résolution récursive vs itérative#

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()
_images/189a9ae74e5f18aa15ce1ff37fdf9b5e48a31b7dcba2cb0ac4847dd1ed1ce5eb.png

Types d’enregistrements DNS#

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

Structure d’un message DNS#

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}")
=== Requête DNS pour python.org (type A) ===
Octets totaux : 28
En-tête (12 octets) : 91 7b 01 00 00 01 00 00 00 00 00 00

  transaction_id            = 0x917B
  type                      = Requête
  opcode                    = 0
  authoritative             = False
  truncated                 = False
  recursion_desired         = True
  recursion_available       = False
  rcode                     = NOERROR
  questions                 = 1
  answers                   = 0
  authority                 = 0
  additional                = 0
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()
_images/b45550ac59a5f6232085bdc09b51c42190eaf251acd4f822273219290110b48f.png

TTL, cache et DNS poisoning#

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()
=== Démonstration cache DNS ===

[CACHE] python.org                     → 151.101.65.69        TTL=300s (expire dans 300s)
[CACHE] pypi.org                       → 151.101.108.223      TTL=60s (expire dans 60s)
[CACHE] docs.python.org                → 151.101.65.69        TTL=120s (expire dans 120s)
[CACHE] NXDOMAIN: inexistant.example.com mis en cache 30s

[CACHE] HIT    : python.org → 151.101.65.69 (TTL restant: 300.0s)
[CACHE] HIT    : pypi.org → 151.101.108.223 (TTL restant: 60.0s)
[CACHE] MISS   : unknown.example.com
[CACHE] HIT    : inexistant.example.com → NXDOMAIN (TTL restant: 30.0s)

[CACHE] Entrées actives : 4
  python.org                          → 151.101.65.69        TTL restant: 300.0s
  pypi.org                            → 151.101.108.223      TTL restant: 60.0s
  docs.python.org                     → 151.101.65.69        TTL restant: 120.0s
  inexistant.example.com              → NXDOMAIN             TTL restant: 30.0s
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()
_images/d0af36cfdc7be6c131e20a5188b3c516a1a64219d67a56675f4221cfb71261b2.png

DNSSEC#

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

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))
  Type DNSSEC                                                           Rôle                           Analogue TLS
       DNSKEY                           Clé publique de la zone (KSK et ZSK)             Clé publique du certificat
        RRSIG                             Signature d'un Resource Record Set      Signature du certificat par la CA
           DS Delegation Signer — empreinte du DNSKEY fils dans zone parente Empreinte du certificat dans la chaîne
 NSEC / NSEC3                  Preuve d'inexistence (NXDOMAIN authenticated)                                      —
CDS / CDNSKEY                    Mise à jour automatique des clés (RFC 7344)             Renouvellement automatique

DoH et DoT : confidentialité des requêtes#

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()
_images/58033bde6d326c10e25b327441c40edec281c0aea6f0ec7891ed79b049246995.png

Code Python avec dnspython#

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}")
dnspython non installé — illustrations avec socket stdlib
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}")
=== Résolution basique avec socket.getaddrinfo() ===
  python.org                     → 151.101.0.223
  pypi.org                       → 151.101.192.223
  docs.python.org                → 151.101.0.223
# 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()
_images/424660359f2a416585e6fa9b2b7e02c3068d79ca9862ed76c4fc4d8464aad65e.png

Résumé#

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()
_images/66254df2a75388b1ab2f5fdd23b0e4271ab9e0f43719812df267cf1065caccf9.png