Aller au contenu

IPv6

200 Pratiquer ⏱ 1 h 30 reseaulinuxipv6

À la fin, vous saurez

  • Écrire et lire une adresse IPv6 sous sa forme canonique, et la développer
  • Reconnaître une adresse globale, de lien local, locale unique, multicast ou de bouclage
  • Expliquer comment une machine obtient ses adresses par SLAAC, et ce qui la distingue de DHCPv6
  • Décrire la découverte des voisins (NDP) et ce qu'elle remplace par rapport à IPv4
  • Configurer un service pour qu'il écoute en IPv4 et en IPv6, et l'appeler par une URL IPv6
  • Adapter un pare-feu à IPv6 sans casser ICMPv6

Prérequis

Testé avec iproute2 6.1.0 iputils 20240117 noyau 7.0 (HWE) python 3.12.3 ubuntu 24.04 , vérifié le 5 octobre 2026

Pourquoi

La leçon précédente a montré comment l'Internet en IPv4 tient malgré la pénurie d'adresses : en traduisant, à la box, chez l'opérateur, à la passerelle du cloud. Cela fonctionne, mais à un prix : les connexions entrantes deviennent impossibles sans configuration, les adresses n'identifient plus les machines, les tables de traduction se remplissent, les ports s'épuisent, et chaque couche de NAT ajoute une panne possible.

IPv6 règle le problème à la racine : ses adresses font 128 bits au lieu de 32. Le nombre d'adresses possibles, 2 puissance 128, est si grand qu'on peut donner à chaque réseau local un bloc de 2 puissance 64 adresses, et à chaque abonné des dizaines de milliers de réseaux. Chaque machine peut de nouveau avoir une adresse publique, unique, joignable de bout en bout.

Ce n'est plus un protocole d'avenir. Le baromètre de l'Arcep publié en 2025 indique qu'à la fin de 2024, 87 % des clients des réseaux fixes grand public en France avaient IPv6 activé, et 70 % sur le mobile ; l'autorité prévoit que la quasi-totalité des clients grand public seront connectés en IPv6 d'ici fin 2027. Selon l'Internet Society, la part des accès aux services de Google en IPv6 natif a dépassé 50 % pour la première fois le 28 mars 2026, et la France figure parmi les pays où l'adoption est la plus forte, autour de 73 %.

Le retard est côté serveurs : toujours selon l'Arcep, seuls 35 % des sites web français étaient accessibles en IPv6 fin 2024. Autrement dit, la plupart des utilisatrices de Signalements arrivent probablement en IPv6 jusqu'à leur box, et c'est l'application qui les oblige à revenir en IPv4. Pour une équipe qui exploite des services, IPv6 n'est plus une option : c'est le protocole que parlent déjà la majorité de ses clients, avec ses règles propres, différentes d'IPv4 sur des points qui comptent pour la sécurité.

Les concepts

Une adresse de 128 bits

Une adresse IPv6 s'écrit en huit groupes de 16 bits, chacun noté en hexadécimal sur quatre chiffres, séparés par des deux-points :

2001:0db8:0042:0001:0000:0000:0000:0011

Cette forme complète est illisible. La RFC 5952 de 2010 fixe une forme canonique, que les outils doivent produire et que les humains devraient écrire :

  1. Supprimer les zéros en tête de chaque groupe : 0db8 devient db8, 0042 devient 42, 0000 devient 0.
  2. Remplacer la plus longue suite de groupes nuls par ::, une seule fois dans l'adresse (sinon on ne saurait plus combien de groupes chaque :: remplace).
  3. Ne pas utiliser :: pour un seul groupe nul.
  4. En cas d'égalité, compresser la première suite.
  5. Écrire les lettres hexadécimales en minuscules.

L'adresse ci-dessus s'écrit donc 2001:db8:42:1::11. Le préfixe 2001:db8::/32 est réservé à la documentation, comme 192.0.2.0/24 en IPv4 : tous les exemples de ce cours l'utilisent.

Comme en IPv4, on écrit un réseau avec la longueur de son préfixe : 2001:db8:42:1::/64 désigne les adresses dont les 64 premiers bits valent 2001:db8:42:1. La notation des masques en décimal pointé (255.255.255.0) n'existe pas en IPv6.

Les types d'adresses

IPv6 n'a pas de diffusion générale (broadcast). Ce qu'IPv4 faisait par diffusion, IPv6 le fait par multicast, en ne dérangeant que les machines concernées. Les principales catégories, définies par la RFC 4291 et ses compléments :

TypePréfixePortéeExemple
Globale (global unicast)2000::/3 actuellement attribuéInternet2001:db8:42:1::11
Lien local (link-local)fe80::/10le lien seulement, jamais routéefe80::ff:fe00:1
Locale unique (Unique Local Address, ULA)fc00::/7, en pratique fd00::/8un site, jamais routée sur Internetfd3c:9a1e:52b0:1::11
Multicastff00::/8selon le champ de portéeff02::1 (tous les nœuds du lien)
Bouclage::1/128la machine::1
Non spécifiée::/128aucune:: (« toutes les adresses » à l'écoute)
IPv4 mappée::ffff:0:0/96représentation interne::ffff:203.0.113.10

Trois points surprennent quand on vient d'IPv4 :

  • Chaque interface a toujours une adresse de lien local, en fe80::, créée automatiquement dès que l'interface est active, même sans aucun routeur. Elle sert aux échanges de voisinage et à joindre son routeur. Elle n'est valable que sur un lien : la même adresse fe80::1 peut exister sur chaque interface d'une machine, et il faut préciser l'interface pour l'utiliser (fe80::1%ens2).
  • Une interface a plusieurs adresses en même temps : une de lien local, une ou plusieurs globales, parfois des temporaires. C'est normal.
  • Les ULA jouent le rôle des adresses RFC 1918, avec une différence importante : la RFC 4193 impose que leurs 40 bits d'identifiant global soient tirés au hasard, jamais choisis à la main ni attribués en séquence. Deux organisations qui fusionnent leurs réseaux n'auront donc presque jamais de collision, contrairement à deux réseaux 192.168.1.0/24.

Le /64 et l'autoconfiguration

En IPv6, un réseau local (un lien, un sous-réseau) a presque toujours un préfixe de 64 bits. Les 64 bits restants forment l'identifiant d'interface. Cette règle n'est pas une recommandation d'élégance : elle conditionne l'autoconfiguration.

Le mécanisme s'appelle SLAAC (Stateless Address Autoconfiguration, RFC 4862). Il se déroule ainsi :

  1. L'interface s'active et se fabrique une adresse de lien local.
  2. La machine envoie une sollicitation de routeur en multicast à tous les routeurs du lien.
  3. Le routeur répond par une annonce de routeur (Router Advertisement, RA) qui contient le préfixe du lien (par exemple 2001:db8:42:1::/64), sa propre adresse de lien local comme passerelle, et des durées de validité. Il envoie aussi ces annonces régulièrement, sans qu'on les lui demande.
  4. La machine colle son identifiant d'interface de 64 bits derrière le préfixe, vérifie que personne n'utilise déjà l'adresse obtenue (détection de doublon), et s'en sert.

Personne n'a attribué l'adresse : le routeur a donné un préfixe, la machine a choisi sa partie. D'où « sans état » : aucun serveur ne tient de registre des adresses distribuées.

Comment se choisit l'identifiant d'interface

Trois méthodes coexistent, et elles n'ont pas les mêmes conséquences :

  • EUI-64, la méthode historique : l'identifiant est tiré de l'adresse MAC de la carte réseau, en insérant ff:fe au milieu et en inversant un bit. La MAC 52:54:00:12:34:56 donne l'identifiant 5054:ff:fe12:3456. Défaut majeur : la même carte a le même identifiant sur tous les réseaux, ce qui permet de suivre un appareil de réseau en réseau, et révèle son fabricant.
  • Les identifiants stables et opaques (RFC 7217) : l'identifiant est calculé par une fonction de hachage à partir du préfixe, de l'interface et d'un secret propre à la machine. Il est stable sur un réseau donné (pratique pour un serveur), mais change quand on change de réseau, et ne révèle rien de la carte.
  • Les adresses temporaires (RFC 8981, qui remplace la RFC 4941) : des identifiants aléatoires, renouvelés régulièrement, que la machine préfère pour ses connexions sortantes. Par défaut, une adresse temporaire est préférée pendant un jour et valable deux jours. Elles protègent la vie privée d'un poste nomade ; un serveur n'en a pas besoin.

Sur la machine de test, Ubuntu active les adresses temporaires par un fichier installé par le paquet procps :

$ cat /etc/sysctl.d/10-ipv6-privacy.conf | grep -v '^#'
net.ipv6.conf.all.use_tempaddr = 2
net.ipv6.conf.default.use_tempaddr = 2

La valeur 2 signifie : créer des adresses temporaires et les préférer pour les connexions sortantes. Un gestionnaire de réseau (NetworkManager sur un poste, systemd-networkd ou netplan sur un serveur) peut appliquer sa propre politique par interface.

SLAAC ou DHCPv6

IPv6 a aussi son DHCP, DHCPv6, qui attribue des adresses depuis un serveur et en garde la trace, comme en IPv4. L'annonce de routeur dit aux machines quoi faire, par deux drapeaux : M (managed, « demandez votre adresse à DHCPv6 ») et O (other, « l'adresse par SLAAC, mais les autres informations, comme les serveurs DNS, par DHCPv6 »). Les annonces de routeur peuvent aussi transporter directement les serveurs DNS (option RDNSS), ce qui permet de se passer complètement de DHCPv6.

Une particularité déroute les équipes réseau : DHCPv6 ne donne pas de route par défaut. La passerelle vient toujours des annonces de routeur. Un réseau « tout DHCPv6 » a donc quand même besoin d'annonces.

La découverte des voisins

En IPv4, pour envoyer un paquet à 172.16.20.12 sur le même réseau, une machine demande à tout le lien, par diffusion ARP, « qui a cette adresse ? ». IPv6 remplace ARP par le protocole de découverte des voisins (Neighbor Discovery Protocol, NDP, RFC 4861), bâti sur ICMPv6. Ses messages principaux :

MessageType ICMPv6Rôle
Sollicitation de routeur (RS)133« y a-t-il un routeur ? »
Annonce de routeur (RA)134préfixe, passerelle, drapeaux M et O
Sollicitation de voisin (NS)135« quelle est l'adresse MAC de cette adresse IPv6 ? » ; détection de doublon
Annonce de voisin (NA)136la réponse
Redirection137« pour cette destination, passez plutôt par tel routeur »

La sollicitation de voisin n'est pas diffusée à tout le lien : elle part vers une adresse multicast dite de nœud sollicité, construite à partir des 24 derniers bits de l'adresse cherchée (ff02::1:ff00:11 pour 2001:db8:42:1::11). Seules les machines dont l'adresse se termine ainsi l'écoutent, ce qui épargne les autres. La table des voisins se lit avec ip -6 neigh, comme la table ARP avec ip -4 neigh.

Un en-tête plus simple

L'en-tête IPv6 fait 40 octets fixes, contre 20 à 60 en IPv4. La RFC 8200 le réduit à huit champs : version, classe de trafic, étiquette de flux, longueur des données, en-tête suivant, limite de sauts (l'équivalent du TTL), adresses source et destination. Trois disparitions comptent :

  • Plus de somme de contrôle dans l'en-tête IP : les couches liaison et transport en ont déjà. Les routeurs n'ont donc plus à la recalculer à chaque saut.
  • Plus de fragmentation par les routeurs : seule la source fragmente. Un routeur qui reçoit un paquet trop gros pour le lien suivant le jette et renvoie un message ICMPv6 « paquet trop gros » (Packet Too Big). La MTU minimale d'un lien IPv6 est de 1 280 octets.
  • Plus d'options dans l'en-tête : les fonctions facultatives passent dans des en-têtes d'extension chaînés par le champ « en-tête suivant » (options saut par saut, routage, fragmentation, authentification, chiffrement), dans un ordre recommandé par la RFC 8200.

Double pile et Happy Eyeballs

La transition ne se fait pas en un jour : pendant des années, la plupart des machines parlent les deux protocoles, avec une adresse IPv4 et des adresses IPv6 sur les mêmes interfaces. C'est la double pile (dual stack).

Un client en double pile qui veut joindre signalements.exemple.fr demande au DNS les deux types d'enregistrements : A (IPv4) et AAAA (IPv6). S'il obtient les deux, lequel utiliser ? Essayer IPv6, puis IPv4 en cas d'échec, ferait attendre de longues secondes chaque fois que le chemin IPv6 est cassé quelque part. La RFC 8305, dite Happy Eyeballs version 2, organise une course : le client préfère IPv6, mais lance une tentative IPv4 si IPv6 n'a pas abouti au bout d'un court délai (250 millisecondes par défaut, entre 100 millisecondes et 2 secondes), et garde la première connexion qui réussit. Les navigateurs et la plupart des bibliothèques modernes l'implémentent.

Conséquence pour l'exploitant : un IPv6 cassé côté serveur ne se voit presque pas depuis un navigateur, qui se rabat silencieusement sur IPv4, mais se voit très bien depuis des clients plus simples (un script, un objet connecté, un autre serveur) qui essaient l'adresse IPv6 et attendent. Publier un enregistrement AAAA engage à servir correctement en IPv6.

En pratique

Lire ses adresses

Sur la machine de test, l'interface de bouclage montre l'adresse ::1 :

$ ip -6 addr show dev lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    inet6 ::1/128 scope host noprefixroute 
       valid_lft forever preferred_lft forever
  • inet6 ::1/128 : l'adresse et la longueur de son préfixe.
  • scope host : la portée. On verra scope link pour une adresse fe80::, et scope global pour une adresse globale ou locale unique.
  • valid_lft et preferred_lft : les durées de vie de validité et de préférence. Pour une adresse obtenue par SLAAC, elles décroissent et sont rafraîchies par chaque annonce de routeur ; une adresse temporaire en fin de préférence passe en deprecated et n'est plus utilisée pour de nouvelles connexions.

Sur une machine réelle, ip -6 addr show dev ens2 liste en plus l'adresse de lien local de l'interface, et, si un routeur annonce un préfixe, une ou plusieurs adresses scope global, éventuellement marquées temporary et dynamic. ip -6 route montre une route fe80::/64 par interface, le préfixe du lien, et une route par défaut default via fe80::... : la passerelle IPv6 est désignée par son adresse de lien local, celle que le routeur utilise dans ses annonces.

Le bouclage répond comme en IPv4 :

$ ping -c 3 ::1
PING ::1 (::1) 56 data bytes
64 bytes from ::1: icmp_seq=1 ttl=64 time=0.059 ms
64 bytes from ::1: icmp_seq=2 ttl=64 time=0.055 ms
64 bytes from ::1: icmp_seq=3 ttl=64 time=0.066 ms

--- ::1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2040ms
rtt min/avg/max/mdev = 0.055/0.060/0.066/0.004 ms

ping affiche ttl par habitude ; en IPv6, c'est la limite de sauts.

Compresser et développer avec Python

Le module ipaddress de la bibliothèque standard de Python applique les règles de la RFC 5952. Ce petit script, notation.py, affiche la forme canonique de quelques écritures :

import ipaddress

for texte in ["2001:0db8:0000:0000:0000:0000:0000:0001",
              "2001:db8:0:0:1:0:0:1",
              "2001:DB8:0:0:0:0:2:1",
              "2001:db8:0:1:0:0:0:11"]:
    print(f"{texte:42} -> {ipaddress.IPv6Address(texte)}")

print(ipaddress.IPv6Address("2001:db8::1").exploded)
$ python3 notation.py
2001:0db8:0000:0000:0000:0000:0000:0001    -> 2001:db8::1
2001:db8:0:0:1:0:0:1                       -> 2001:db8::1:0:0:1
2001:DB8:0:0:0:0:2:1                       -> 2001:db8::2:1
2001:db8:0:1:0:0:0:11                      -> 2001:db8:0:1::11
2001:0db8:0000:0000:0000:0000:0000:0001

Chaque ligne illustre une règle. La deuxième a deux suites de deux groupes nuls : la première est compressée. La troisième passe en minuscules. La quatrième compresse la suite de trois zéros, plus longue que le zéro isolé, qui reste écrit 0. La dernière ligne, avec exploded, développe l'adresse en huit groupes de quatre chiffres : c'est la forme à utiliser pour comparer deux adresses en tant que texte, ou pour les trier.

Le même module classe les adresses. Le script categories.py :

import ipaddress

for texte in ["2001:db8:42::11", "fe80::1", "fd3c:9a1e:52b0:1::11",
              "ff02::1", "::1", "::ffff:203.0.113.10"]:
    a = ipaddress.IPv6Address(texte)
    print(f"{texte:22} lien local={a.is_link_local!s:5} "
          f"privée={a.is_private!s:5} multicast={a.is_multicast!s:5} "
          f"boucle={a.is_loopback}")
$ python3 categories.py
2001:db8:42::11        lien local=False privée=True  multicast=False boucle=False
fe80::1                lien local=True  privée=True  multicast=False boucle=False
fd3c:9a1e:52b0:1::11   lien local=False privée=True  multicast=False boucle=False
ff02::1                lien local=False privée=False multicast=True  boucle=False
::1                    lien local=False privée=True  multicast=False boucle=True
::ffff:203.0.113.10    lien local=False privée=True  multicast=False boucle=False

La première ligne mérite un regard : 2001:db8:42::11 ressemble à une adresse globale, mais Python la déclare « privée ». C'est exact : 2001:db8::/32 est réservé à la documentation et n'est pas joignable sur Internet. Le module suit le registre des adresses à usage spécial de l'IANA, et non la seule forme de l'adresse.

Enfin, la taille des blocs donne le vertige :

$ python3 -c "
import ipaddress
n = ipaddress.IPv6Network('2001:db8:42::/48')
print(n.num_addresses)
print(sum(1 for _ in n.subnets(new_prefix=64)))
print(list(n.subnets(new_prefix=64))[:3])"
1208925819614629174706176
65536
[IPv6Network('2001:db8:42::/64'), IPv6Network('2001:db8:42:1::/64'), IPv6Network('2001:db8:42:2::/64')]

Un /48, une taille souvent attribuée à un site d'entreprise, contient 65 536 réseaux /64. On ne découpe donc plus les sous-réseaux à l'économie comme en IPv4 (leçon 4) : chaque réseau reçoit son /64, et le plan d'adressage se lit dans le quatrième groupe (:1::, :2::...).

Fabriquer un identifiant EUI-64

Pour voir ce qu'une adresse EUI-64 révèle, ce script, eui64.py, applique la règle à une adresse MAC :

import ipaddress

def eui64(prefixe, mac):
    octets = bytearray(int(x, 16) for x in mac.split(":"))
    octets[0] ^= 0x02                      # inverser le bit universel/local
    iid = octets[:3] + b"\xff\xfe" + octets[3:]
    reseau = ipaddress.IPv6Network(prefixe)
    return reseau[int.from_bytes(iid, "big")]

print(eui64("fe80::/64", "02:00:00:00:00:01"))
print(eui64("2001:db8:42:1::/64", "52:54:00:12:34:56"))
$ python3 eui64.py
fe80::ff:fe00:1
2001:db8:42:1:5054:ff:fe12:3456

Le motif ff:fe au milieu de l'identifiant trahit une adresse EUI-64, et les trois premiers octets de la MAC (52:54:00, le préfixe utilisé par QEMU et KVM pour les cartes virtuelles) restent lisibles : on devine qu'il s'agit d'une machine virtuelle.

Écouter en IPv4 et en IPv6

Un serveur qui écoute sur 0.0.0.0 n'accepte que l'IPv4. Pour servir les deux, on écoute sur ::, l'adresse non spécifiée d'IPv6 : sous Linux, par défaut, une socket IPv6 en écoute sur :: accepte aussi les connexions IPv4, qui lui apparaissent sous forme d'adresses IPv4 mappées. Démonstration avec quatre serveurs web de test :

$ python3 -m http.server 8001 --bind 127.0.0.1 &
$ python3 -m http.server 8002 --bind 0.0.0.0 &
$ python3 -m http.server 8003 --bind :: &
$ python3 -m http.server 8004 --bind ::1 &
$ ss -ltn '( sport >= :8001 and sport <= :8004 )'
State  Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0      5          127.0.0.1:8001      0.0.0.0:*          
LISTEN 0      5            0.0.0.0:8002      0.0.0.0:*          
LISTEN 0      5                  *:8003            *:*          
LISTEN 0      5              [::1]:8004         [::]:*          

ss affiche *:8003 pour la socket qui accepte les deux familles, et [::1]:8004 pour celle qui n'écoute que sur le bouclage IPv6. Dans une adresse suivie d'un port, l'adresse IPv6 est entre crochets : c'est la notation recommandée par la RFC 5952, la seule qui ne soit pas ambiguë, puisque le deux-points sert déjà de séparateur dans l'adresse. Les URL suivent la même règle :

$ curl -s -o /dev/null -w '%{http_code} %{remote_ip}\n' http://127.0.0.1:8003/
200 127.0.0.1
$ curl -s -o /dev/null -w '%{http_code} %{remote_ip}\n' 'http://[::1]:8003/'
200 ::1
$ curl -s -o /dev/null -w '%{http_code} %{remote_ip}\n' 'http://[::1]:8002/'
000 
$ curl -s -o /dev/null -w '%{http_code} %{remote_ip}\n' http://localhost:8004/
200 ::1

Le serveur sur :: répond aux deux familles ; celui sur 0.0.0.0 refuse l'IPv6 (000 : aucune réponse HTTP, curl a échoué). La dernière ligne montre que localhost se résout aussi en ::1 : un service qui n'écoute que sur 127.0.0.1 peut donc sembler « ne pas répondre sur localhost » quand le client essaie IPv6 en premier.

Ce que voit le serveur quand un client IPv4 se connecte sur une socket :: :

import socket
srv = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
print("IPV6_V6ONLY par défaut :", srv.getsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY))
srv.bind(("::", 8005)); srv.listen()
for cible in ("127.0.0.1", "::1"):
    c = socket.create_connection((cible, 8005))
    conn, adresse = srv.accept()
    print("connexion depuis", cible, "-> vue par le serveur :", adresse)
    conn.close(); c.close()
IPV6_V6ONLY par défaut : 0
connexion depuis 127.0.0.1 -> vue par le serveur : ('::ffff:127.0.0.1', 34030, 0, 0)
connexion depuis ::1 -> vue par le serveur : ('::1', 55118, 0, 0)

Le client IPv4 apparaît comme ::ffff:127.0.0.1. Ce comportement dépend de l'option de socket IPV6_V6ONLY, dont la valeur par défaut vient du réglage net.ipv6.bindv6only (0 par défaut, d'après la documentation du noyau : les adresses IPv4 mappées sont acceptées). Avec IPV6_V6ONLY à 1, la même socket refuse les clients IPv4, et la tentative depuis 127.0.0.1 échoue avec ConnectionRefusedError [Errno 111] Connection refused sur la machine de test.

Pour Signalements, --bind [::]:8000 suffit donc à Gunicorn pour servir les deux familles, sur un système où bindv6only vaut 0. Méfiez-vous de la combinaison qui paraît plus explicite, --bind 0.0.0.0:8000 --bind [::]:8000 : la socket sur :: couvre déjà l'IPv4 du port 8000, et la seconde ouverture échoue. Sur la machine de test, ouvrir :: sur un port déjà tenu par une socket 0.0.0.0 donne OSError [Errno 98] Address already in use, et le code de Gunicorn ne positionne pas IPV6_V6ONLY qui l'éviterait. Les journaux et le code qui analysent l'adresse du client doivent enfin s'attendre à des formes ::ffff:a.b.c.d.

IPv6 chez Scaleway

La documentation de Scaleway décrit un support inégal selon les produits, ce qui est typique des fournisseurs :

  • Instances : jusqu'à cinq adresses IPv6 publiques flexibles par instance, chacune étant en réalité un préfixe /64 routé vers l'instance, configuré par SLAAC.
  • Réseaux privés : toujours en double pile, avec un bloc IPv4 et un bloc IPv6 /64 attribué automatiquement.
  • Répartiteurs de charge : une adresse IPv6 flexible possible, mais pas d'IPv6 seule ; ils parlent aux instances en IPv4 ou en IPv6.
  • Passerelles publiques : pas d'IPv6 à la date de vérification. Les routes gérées vers une passerelle ne sont créées qu'en IPv4.

Pour l'architecture du cours, cela veut dire qu'on peut publier un enregistrement AAAA pour le répartiteur de Signalements, et que les clientes en IPv6 joindront l'API sans traduction. En revanche, sig-app-1, sans adresse publique, sort vers Internet en IPv4 par la passerelle : ses adresses IPv6 du réseau privé ne lui donnent pas d'accès Internet en IPv6.

Sous le capot

La détection de doublon

Avant d'utiliser une adresse qu'elle vient de se fabriquer, une machine vérifie que personne ne l'utilise déjà (Duplicate Address Detection, RFC 4862) : elle envoie une sollicitation de voisin pour cette adresse, depuis l'adresse non spécifiée ::. Si quelqu'un répond, l'adresse est en conflit et n'est pas utilisée. Pendant cette vérification, l'adresse est marquée tentative dans ip -6 addr ; un service qui tente d'écouter sur une adresse encore tentative échoue (« Cannot assign requested address »). C'est une cause classique de service qui ne démarre pas juste après le démarrage de la machine, et l'une des raisons d'être de la cible network-online.target vue dans le cours Linux : premiers pas.

Ce que deviennent les fonctions d'IPv4

Fonction en IPv4En IPv6
ARP (diffusion)NDP, sollicitation de voisin en multicast (ICMPv6)
Diffusion généralemulticast (ff02::1 tous les nœuds, ff02::2 tous les routeurs)
DHCP pour l'adresse et la passerelleSLAAC (passerelle par les annonces de routeur), DHCPv6 en option
Fragmentation par les routeursfragmentation à la source seulement, découverte de la MTU obligatoire en pratique
Somme de contrôle de l'en-tête IPsupprimée
TTLlimite de sauts (hop limit)
NAT pour économiser les adressesinutile ; chaque réseau a son /64

La délégation de préfixe

Comment la box de l'utilisatrice obtient-elle les préfixes de son réseau domestique ? L'opérateur lui délègue un bloc plus large qu'un /64, par exemple un /56, avec DHCPv6 et l'option de délégation de préfixe (Prefix Delegation). La box découpe ce bloc en /64 pour chacun de ses réseaux (filaire, Wi-Fi, invités), et les annonce par des annonces de routeur. Si le portable reçoit le préfixe 2001:db8:a1b2:c301::/64, il se fabrique une adresse dans ce bloc, et c'est cette adresse que voit le répartiteur de Signalements. Plus de NAT : le serveur connaît l'adresse réelle du portable, ou du moins son adresse temporaire du moment.

Pièges courants

Bloquer tout ICMPv6. C'est l'erreur la plus grave, héritée de l'habitude de « bloquer le ping » en IPv4. En IPv6, NDP et l'autoconfiguration sont de l'ICMPv6 : sans eux, une machine ne trouve ni ses voisins ni son routeur. Et sans les messages « paquet trop gros », puisque les routeurs ne fragmentent plus, les connexions qui transportent de gros paquets restent bloquées. La RFC 4890 liste les messages à ne jamais filtrer : destination injoignable, paquet trop gros, temps dépassé (code 0), problème de paramètre (codes 1 et 2), demande et réponse d'écho pour le trafic qui traverse le pare-feu, et, pour le trafic destiné à la machine elle-même, les messages de découverte des voisins et des routeurs (types 133 à 136).

Un enregistrement AAAA publié, un service qui n'écoute pas en IPv6. Le service écoute sur 0.0.0.0, le DNS annonce une adresse IPv6 : les navigateurs se rabattent sur IPv4 grâce à Happy Eyeballs, les autres clients échouent ou attendent. Vérifiez avec curl -6, depuis l'extérieur.

Oublier les crochets. http://2001:db8::1:8000/sante est ambiguë et refusée. Il faut http://[2001:db8::1]:8000/sante. Même règle dans les fichiers de configuration qui attendent une adresse et un port.

Comparer des adresses comme du texte. 2001:db8::1 et 2001:0db8:0:0:0:0:0:1 sont la même adresse. Une liste d'autorisation qui compare des chaînes laissera passer ou refusera à tort. Normalisez avec une bibliothèque (ipaddress en Python) avant de comparer.

Bannir une adresse plutôt qu'un préfixe. Une machine peut changer d'adresse temporaire chaque jour, et dispose de 2 puissance 64 adresses dans son /64. Une limite de débit ou un bannissement par adresse IPv6 individuelle est inefficace : on raisonne par /64, voire par /56 ou /48.

Utiliser une adresse de lien local sans interface. ping fe80::1 échoue tant qu'on ne précise pas l'interface : ping fe80::1%ens2.

Sécurité

La fin de l'illusion du NAT. Derrière une box IPv4, une machine domestique n'était pas joignable de l'extérieur, par effet de bord du NAT (leçon 7). En IPv6, elle a une adresse globale : seule une règle de pare-feu explicite l'empêche d'être jointe. La plupart des box filtrent par défaut les connexions entrantes en IPv6, mais un serveur dans le cloud, lui, est exposé dès qu'il a une adresse IPv6 publique. Ce qui protégeait par accident doit être remplacé par une politique écrite.

Le pare-feu doit couvrir les deux familles. Un pare-feu configuré en IPv4 seulement laisse IPv6 grand ouvert. Avec nftables, une table de la famille inet traite IPv4 et IPv6 avec les mêmes règles ; avec les anciens outils, iptables ne concerne que l'IPv4 et ip6tables doit être écrit à part. Les groupes de sécurité des fournisseurs ont, de même, des règles distinctes par famille. Vérifiez depuis l'extérieur, en IPv6, ce qui répond vraiment.

Les annonces de routeur usurpées. N'importe quelle machine d'un lien peut envoyer des annonces de routeur et se proclamer passerelle par défaut de tout le réseau, ou distribuer un préfixe pirate. C'est l'équivalent IPv6 d'un serveur DHCP pirate. Les commutateurs d'entreprise proposent une protection dédiée (RA Guard), et un serveur qui ne doit pas apprendre sa configuration par annonces peut les refuser (accept_ra, dont la documentation du noyau décrit les valeurs).

Les adresses qui en disent trop. Une adresse EUI-64 révèle la carte réseau et permet de suivre un appareil. Les identifiants stables et opaques (RFC 7217) pour les serveurs, et les adresses temporaires (RFC 8981) pour les postes, évitent ces fuites.

Le balayage devient plus difficile, pas impossible. On ne peut pas parcourir un /64 adresse par adresse comme un /24 en IPv4. Mais des adresses prévisibles (::1, ::11, des adresses dérivées de la MAC) et les adresses publiées dans le DNS ou dans les journaux de certificats restent faciles à trouver. Ne comptez pas sur l'obscurité.

En production

  • Double pile d'abord. Commencez par le point d'entrée public : un enregistrement AAAA pour le répartiteur de charge, testé depuis un client IPv6. Les échanges internes peuvent rester en IPv4 tant que les services gérés de votre fournisseur ne suivent pas.
  • Journalisez l'adresse IPv6 complète des clients, sous forme canonique, et adaptez les outils d'analyse (expressions régulières, colonnes de base de données) à des adresses de 39 caractères, ou plus avec une adresse IPv4 mappée.
  • Surveillez IPv6 séparément. Une sonde de supervision qui ne teste que l'IPv4 ne verra jamais une panne IPv6, que Happy Eyeballs masque aux navigateurs mais pas aux autres clients.
  • Pensez aux limites par préfixe pour les protections anti-abus, et aux listes d'autorisation écrites en préfixes.
  • Le plan d'adressage change de nature : on n'économise plus, on structure. Un /64 par réseau, un bloc par environnement ou par site, et un document qui les recense.

Exercices

1. Forme canonique (niveau 100). Écrivez sous forme canonique : 2001:0DB8:0000:0000:0008:0800:200C:417A, ff02:0000:0000:0000:0000:0000:0000:0002, 2001:db8:0:0:1:0:0:0, 0:0:0:0:0:0:0:1. Vérifiez avec python3 -c "import ipaddress; print(ipaddress.IPv6Address('...'))".

Solution

2001:db8::8:800:200c:417a (zéros en tête supprimés, minuscules, la suite de deux groupes nuls compressée) ; ff02::2 (l'adresse multicast de tous les routeurs du lien) ; 2001:db8:0:0:1:: (deux suites de zéros, de longueurs deux et trois : on compresse la plus longue, celle de la fin) ; ::1. Python donne les mêmes résultats.

2. Reconnaître une adresse (niveau 100). Pour chaque adresse, donnez son type et dites si elle peut être jointe depuis Internet : fe80::5054:ff:fe12:3456, fd3c:9a1e:52b0:1::11, 2001:db8:42:1::11, ff02::1:ff00:11. Que pouvez-vous dire de la première ?

Solution

fe80::5054:ff:fe12:3456 : lien local, jamais routée ; son identifiant contient ff:fe, c'est un EUI-64 dérivé de la MAC 52:54:00:12:34:56, préfixe utilisé par QEMU et KVM, donc probablement une machine virtuelle. fd3c:9a1e:52b0:1::11 : adresse locale unique (ULA), privée à un site. 2001:db8:42:1::11 : forme globale, mais dans le préfixe de documentation, donc injoignable en réalité. ff02::1:ff00:11 : multicast de nœud sollicité, utilisé par NDP pour trouver la machine dont l'adresse se termine par 00:11, portée lien local.

3. Le service invisible (niveau 200). Signalements a désormais un enregistrement AAAA sur son répartiteur. Une partenaire signale que son script d'intégration, lancé depuis un serveur en double pile, met 75 secondes à échouer, alors que son navigateur fonctionne. Expliquez, et dites comment vous le vérifieriez.

Solution

Le chemin IPv6 vers le service est cassé (frontend IPv6 absent sur le répartiteur, règle de filtrage, route manquante). Le navigateur implémente Happy Eyeballs : il lance IPv4 après quelques centaines de millisecondes et l'utilisatrice ne voit rien. Le script utilise une bibliothèque qui essaie la première adresse renvoyée par le DNS, en IPv6, attend l'expiration de la tentative de connexion TCP, puis seulement passe à IPv4 (ou échoue). Vérification : curl -6 -v https://signalements.exemple.fr/sante et curl -4 -v ... depuis une machine qui a IPv6, pour comparer ; puis contrôler que le répartiteur a bien un frontend en IPv6 et que les règles de filtrage IPv6 autorisent le port 443.

4. Écouter correctement (niveau 200). Lancez python3 -m http.server 8010 --bind :: puis, dans un autre terminal, testez http://127.0.0.1:8010/ et http://[::1]:8010/. Relancez ensuite le serveur avec --bind 0.0.0.0, et refaites les deux tests. Expliquez la différence, puis indiquez comment écrire le paramètre --bind de Gunicorn pour Signalements afin qu'il serve les deux familles sur le port 8000.

Solution

Avec ::, les deux tests répondent : sous Linux, net.ipv6.bindv6only vaut 0 par défaut, donc une socket IPv6 en écoute sur :: accepte aussi l'IPv4, sous forme d'adresses ::ffff:127.0.0.1. Avec 0.0.0.0, seul l'IPv4 répond ; [::1] échoue (connexion refusée). Pour Gunicorn : --bind [::]:8000, sur un système où bindv6only vaut 0, ce qui est le cas par défaut. Ne pas ajouter --bind 0.0.0.0:8000 : les deux sockets se disputeraient l'IPv4 du port 8000 et la seconde échouerait avec « Address already in use ». L'adresse IPv6 s'écrit entre crochets.

5. Le pare-feu oublié (niveau 200). Un serveur Ubuntu a un pare-feu écrit avec iptables qui n'autorise que les ports 22 et 443. L'hébergeur active IPv6 sur l'instance. Quels risques, et que corrigez-vous ?

Solution

Les règles iptables ne s'appliquent qu'à l'IPv4 : en IPv6, tous les ports en écoute sur :: deviennent joignables (base de données, interface d'administration, Gunicorn sur 8000), puisque la plupart des services écoutent sur :: et acceptent les deux familles. Correction : réécrire la politique dans une table nftables de famille inet, qui couvre les deux protocoles avec les mêmes règles, politique d'entrée drop, autorisation des connexions établies, des ports voulus, et des messages ICMPv6 indispensables (RFC 4890, dont les types 133 à 136 pour la découverte des voisins). Vérifier depuis l'extérieur avec un client IPv6. Ne pas « régler » le problème en bloquant tout ICMPv6.

Récapitulatif

  • IPv6 donne des adresses de 128 bits, assez pour que chaque machine ait de nouveau une adresse publique. Fin 2024, 87 % des clients fixes français avaient IPv6, mais seuls 35 % des sites web le servaient.
  • Forme canonique (RFC 5952) : pas de zéros en tête, :: pour la plus longue suite de zéros (la première en cas d'égalité, jamais pour un seul groupe), minuscules ; crochets autour de l'adresse quand on ajoute un port.
  • Types : globale (2000::/3), lien local (fe80::/10, toujours présente), locale unique (fd00::/8, identifiant aléatoire), multicast (ff00::/8), bouclage (::1). Pas de diffusion générale.
  • Un réseau local est un /64 ; les machines s'autoconfigurent par SLAAC à partir des annonces de routeur ; DHCPv6 est optionnel et ne donne pas la passerelle.
  • NDP, sur ICMPv6, remplace ARP et découvre routeurs et voisins.
  • En-tête fixe de 40 octets, sans somme de contrôle, pas de fragmentation par les routeurs : ICMPv6 « paquet trop gros » est indispensable.
  • Double pile et Happy Eyeballs masquent un IPv6 cassé aux navigateurs, pas aux autres clients. Écouter sur :: sert les deux familles sous Linux par défaut.
  • Sans NAT, la sécurité repose sur un pare-feu explicite qui couvre les deux familles, sans bloquer l'ICMPv6 nécessaire.

Pour aller plus loin

  • La RFC 4291, qui décrit l'architecture d'adressage, et la RFC 5952, courte, à garder sous la main.
  • Le baromètre annuel de l'Arcep sur la transition vers IPv6, pour suivre l'état du déploiement en France, opérateur par opérateur.
  • La RFC 4890, pour écrire une politique de filtrage ICMPv6 correcte.
  • La leçon suivante, qui monte d'une couche : les ports, et le plus simple des protocoles de transport, UDP.
Voir ma constellation →

Sources