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
dnspythonpour des requêtes avancéesDécouvrir DNSSEC, DoH et DoT
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()
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()
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()
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()
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()
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()
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()
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()