- Le NAT IPv4 distingue généralement la cible des réponses à l’aide des ports TCP/UDP, mais l’echo ICMP de
pingn’a pas de port ; la question centrale est donc de savoir quelle valeur Linux utilise comme clé de mapping - L’expérience reproduit une configuration NAT de
192.168.99.0/24vers10.0.100.0/24avec des espaces de noms réseau, en créantclient1,client2,natboxetserver, ainsi qu’une règleiptablesMASQUERADE - En comparant la RFC 792 et des captures de paquets, on constate que l’Identifier et le Sequence Number de l’echo ICMP servent à faire correspondre requêtes et réponses, et que sur le chemin ICMP
SOCK_DGRAMde Linux, le port local du socket devient l’ID - Si deux clients utilisent le même ID ICMP
999, netfilter modifie l’ID ICMP en une valeur aléatoire pour l’un d’eux afin d’éviter une collision, puis le rétablit vers l’IP et l’ID d’origine du client dans la réponse - Même pour l’ICMP sans port, le NAT Linux enregistre l’état des directions aller et retour dans un tuple conntrack et utilise l’ID ICMP comme clé modifiable pour mapper correctement la réponse vers l’hôte interne adéquat
Environnement de test et configuration NAT
- Pour simuler plusieurs machines sur une seule machine Linux, l’auteur utilise des espaces de noms réseau
- Deux clients,
natboxjouant le rôle de routeur NAT, et un serveur sont créés chacun dans un espace de noms distinct, avec séparation entre le réseau privé et le réseau côté serveurclient1:192.168.99.1/24client2:192.168.99.2/24- interface interne de
natbox:192.168.99.3/24 - interface externe de
natbox:10.0.100.1/24 server:10.0.100.2/24
- Les commandes
ip,iptables,tcpdump, etc. sont exécutées en root sur une VM Fedora 38 Server avec le noyau Linux 6.2.9 - Les deux clients sont connectés au pont
br0, etnatboxest relié d’un côté au pont et de l’autre à une paire veth côté serveur - La route par défaut des clients est définie sur
192.168.99.3afin que le trafic vers le serveur passe parnatbox - Sur
natbox, le forwarding de paquets est activé avecnet.ipv4.ip_forward=1, et une règle MASQUERADE est ajoutée à la chaînePOSTROUTINGde la tablenatd’iptablesip netns exec natbox iptables -t nat -A POSTROUTING -o eth1 -j MASQUERADE
NAT ICMP observé via capture de paquets
- Des paquets ICMP sont capturés avec
tcpdump -n icmpdans les espaces de nomsclient1etserver - Côté client, on voit une echo request
192.168.99.1 > 10.0.100.2et une echo reply10.0.100.2 > 192.168.99.1 - Côté serveur, l’adresse source de la même requête apparaît modifiée en
10.0.100.1, ce qui confirme que le NAT a réécrit l’adresse source avec l’IP externe denatbox - Les requêtes ICMP provenant de clients différents ont des champs id différents
- dans l’exemple,
client1a l’ID31428 client2a l’ID33391
- dans l’exemple,
- Cette observation montre que
natboxpeut exploiter le champ ID pour renvoyer les réponses ICMP vers le bon client interne
RFC 792 et l’ID ICMP de ping
- ICMP est un ancien protocole défini dans la RFC 792, publiée en 1981
- Les messages ICMP echo et echo reply contiennent Type, Code, Checksum, Identifier, Sequence Number et Data
- Type distingue l’echo request de l’echo reply
- le Type d’une echo request est
8 - dans l’extrait cité de la RFC, l’echo reply est indiqué comme
1 - Code vaut
0
- le Type d’une echo request est
- La RFC 792 explique que l’Identifier et le Sequence Number peuvent être utilisés pour faire correspondre echo request et echo reply
- L’Identifier peut servir à identifier une session, un peu comme les ports en TCP/UDP, tandis que le Sequence Number peut être incrémenté à chaque echo request
- Comme la RFC ne précise pas comment choisir concrètement l’ID, il faut examiner le code source de l’implémentation
ping
Comment l’ID est déterminé dans iputils ping
- La commande
pingfait partie du paquet iputils - Un commentaire près de
ping4_send_probeexplique qu’au moment de construire une ICMP echo request, le champ ID est un nombre aléatoire et le Sequence Number un entier croissant - En interne,
pingpossède un champidentdansstruct ping_rts- sa valeur par défaut est
-1 - il peut être remplacé via l’option CLI
-epar une valeur comprise entre0etIDENTIFIER_MAX, soit0xFFFF
- sa valeur par défaut est
- Si
rts->ident == -1,pingassocie le socket avec le typeSOCK_DGRAMet le protocoleIPPROTO_ICMP - D’après la description des sockets Linux
IPPROTO_ICMP, l’en-tête ICMP est vérifié et nettoyé lors dusend(), et l’id est défini à partir du numéro de port local du socket - Si
pingne spécifie pas de port source, le noyau Linux choisit un port libre aléatoirement, et ce port semble alors être utilisé comme ID du paquet ICMP
Quand le même ID ICMP entre en collision
- Sur les deux clients,
ping -e 999est utilisé pour envoyer des pings au serveur avec le même ID ICMP 999 - Dans la capture côté serveur, la requête d’un client conserve l’ID
999, tandis que celle de l’autre voit son ID modifié en30218 - Le dispositif NAT change donc l’ID d’un côté afin d’éviter qu’une même combinaison IP externe + ID ICMP n’entre en collision
- Pour localiser le traitement de cette collision, l’auteur inspecte dans le répertoire
net/netfilterdu code Linux les parties qui utilisent le champ ICMPid
Rôle de netfilter, conntrack et NAT
- Le sous-système du noyau qui implémente les règles
iptablesest netfilter - Comme la règle MASQUERADE effectue le NAT, l’implémentation du NAT ICMP se trouve elle aussi dans netfilter
nf_nat_setup_infodansnf_nat_core.cappelleget_unique_tuple, qui mène ensuite ànf_nat_l4proto_unique_tuplenf_nat_l4proto_unique_tuplecontient un casIPPROTO_ICMPet référencetuple->src.u.icmp.idnf_nat_manip_pktdansnf_nat_proto.cpasse parnf_nat_ipv4_manip_pktetl4proto_manip_pkt, puis appelleicmp_manip_pktdans le cas ICMPicmp_manip_pktécrit l’ID ICMP réel dans le paquet viahdr->un.echo.id = tuple->src.u.icmp.id
Comment l’ICMP est représenté dans les tuples conntrack
- Dans netfilter, une connection ne signifie pas seulement une connexion TCP ; pour des protocoles sans connexion comme UDP ou ICMP, cela désigne aussi l’état qui relie les paquets sortants et entrants
nf_conncontienttuplehash[IP_CT_DIR_MAX]IP_CT_DIR_ORIGINAL: direction du paquet sortantIP_CT_DIR_REPLY: direction de la réponse entrante
- Chaque
nf_conntrack_tuple_hashcontient unnf_conntrack_tuplequi identifie la connection - Le tuple est séparé entre
src, modifiable, etdst, immuablesrccontient l’adresse IP et des champs spécifiques au protocole- pour l’ICMP, le champ spécifique au protocole est
__be16 id dstcontient l’adresse IP non modifiée ainsi que l’ICMPtypeetcode
- Le NAT enregistre dans la connection la façon dont il a modifié le paquet sortant, puis inverse cette transformation sur le paquet de réponse
Le chemin de code qui choisit l’ID ICMP
- Quand
natboxreçoit un echo ICMP,nf_nat_setup_infocrée une nouvelle connection et décide s’il faut modifier l’IP source et l’ID ICMP - Ensuite, pour chaque paquet ICMP,
nf_nat_manip_pktdéfinit l’IP source ou destination ainsi que l’ID ICMP en fonction des valeurs stockées dans la connection get_unique_tupleest le chemin central qui choisit un tuple NAT disponiblefind_best_ips_protoréécrit l’adresse IP sourcenf_nat_used_tuplevérifie si le tuple est déjà utilisé ; si ce n’est pas le cas, le tuple courant est renvoyé tel quel- c’est pour cela que si les ID ICMP des deux clients sont différents, l’ID est conservé aussi dans les paquets NATés
- si le tuple est déjà pris,
nf_nat_l4proto_unique_tupleest appelé pour effectuer le NAT spécifique au protocole
- En ICMP,
tuple->src.u.icmp.idest choisi comme clé ciblée par le NAT find_free_idgénère un ID aléatoire avecget_random_u16(), l’ajuste dans la plage valide des ID ICMP, puis vérifie s’il est déjà utilisé- Par défaut, la plage d’ID couvre toute la plage possible, mais on peut définir une plage comme
100-200dans la règleiptablesMASQUERADE avec--to-ports - Si aucun tuple libre n’est trouvé, un ID en doublon reste attaché à la connection, et
__nf_conntrack_confirmdétecte ensuite le doublon et abandonne le paquet
Vérification du comportement du noyau avec bpftrace
- Pour vérifier le fonctionnement de netfilter tel qu’il l’a compris, l’auteur utilise
bpftrace - Les fonctions du noyau suivies sont
nf_nat_setup_infoetnf_nat_manip_pkt - Un
kprobetrace le moment où la fonction est appelée, et unkretprobetrace le moment où elle retourne - Dans un
kretprobe, on ne peut pas accéder directement aux arguments de fonction ; les arguments sont donc stockés dans une map BPF à l’entrée puis relus à la sortie struct sk_buffest la structure utilisée par le noyau Linux pour représenter un paquetbswapsert à convertir l’ordre des octets réseau, big endian, en little endianntopconvertit une adresse IP en chaîne de caractères- Grâce au BPF Type Format (BTF) des noyaux Linux récents, le programme BPF peut référencer des structures du noyau comme
sk_buffounf_connsans inclure de fichiers d’en-tête - Ce programme
bpftracea été testé sur Linux kernel 6.2.9, et son fonctionnement peut varier selon la version du noyau
Résultats du traçage et conclusion
- Lorsque les deux clients envoient un ping avec le même ID ICMP
999,nf_nat_setup_infoest appelé une fois pour chaque client - Pour le premier client
192.168.99.1, les tuples original et reply conservent tous deux l’ID ICMP999 - Pour le second client
192.168.99.2, l’ID ICMP du tuple reply est réécrit en32809 nf_nat_manip_pktremplace l’IP source par10.0.100.1dans l’echo request avecNF_NAT_MANIP_SRC, puis restaure l’IP de destination vers l’IP d’origine du client dans la réponse avecNF_NAT_MANIP_DST- L’ID ICMP du paquet de réponse est lui aussi restauré, de la valeur NATée vers la valeur initialement envoyée par le client
- Le timeout conntrack ICMP par défaut peut être consulté dans
/proc/sys/net/netfilter/nf_conntrack_icmp_timeout; la valeur par défaut observée est de 30 secondes - Si le client n’envoie aucun paquet pendant plus de 30 secondes,
nf_nat_setup_infoest rappelé au ping suivant - Le comportement du NAT Linux pour
pingest également documenté dans le Netfilter Hacking HOWTO, et l’essentiel repose sur les tuples conntrack et la réécriture de l’ID ICMP
1 commentaires
Commentaires Hacker News
Quand le serveur démarre, il commence à envoyer des paquets ICMP echo request fixes vers l’adresse fixe 3.3.3.3, en s’attendant à ce qu’ils ne reviennent pas
3.3.3.3 n’est ni un hôte joignable ni une cible à usurper. À la place, quand le client veut se connecter, il connaît l’IP du serveur et lui envoie donc un paquet ICMP Time Exceeded. À l’intérieur de ce paquet ICMP se trouve le paquet fixe « d’origine » que le serveur envoyait à 3.3.3.3, et ce paquet codé en dur sert d’identifiant à pwnat
Le client fait en quelque sorte semblant d’être un saut sur Internet et informe le serveur que sa requête « ICMP echo request » d’origine n’a pas pu être transmise. Le NAT constate que le paquet contenu dans l’ICMP Time Exceeded correspond à un paquet envoyé par le serveur et le transmet au serveur derrière le NAT ; comme l’en-tête IP complet du client est inclus, le serveur peut ainsi connaître l’adresse IP du client
Le fonctionnement essentiel de cet outil consiste ensuite à créer un tunnel UDP entre le client et le serveur
Cela dit, d’après ce que j’ai survolé, il semble supposer que le NAT ne réécrit pas le port source UDP, donc ça ne fonctionnera probablement pas avec tous les routeurs. STUN, utilisé notamment par WebRTC, met en œuvre des techniques plus sophistiquées, mais lorsque ça ne suffit pas, il faut tout de même utiliser TURN comme relais
Le même problème a de fortes chances de s’appliquer à l’astuce du ping vers 3.3.3.3. Si, comme dans l’article, le NAT réécrit l’identifiant du ping, cette astuce casse
Quand il reçoit la réponse, le routeur utilise cette valeur d’ID unique pour transmettre la réponse au bon appareil du réseau local
Un seul ordinateur et Wireshark/tcpdump suffisent pour le vérifier
L’article est bon en soi et peut être une révélation pour quelqu’un qui n’avait aucune compréhension du réseau. Mais fondamentalement, cela ressemble davantage à une manière de monter un vrai labo réseau et de fouiller dans les sources qu’à un raisonnement mené par soi-même
Je suis vraiment reconnaissant pour ce type d’exemple que l’on peut suivre pas à pas, et j’ai l’intention de le refaire moi-même
Le seul autre article ou presque qui m’ait bien aidé à comprendre ce sujet est celui de Tailscale. Il contient beaucoup « d’exemples détaillés », ce qui rend clair la façon dont l’ensemble s’emboîte
https://tailscale.com/blog/how-nat-traversal-works/
Par coïncidence, ce week-end, je me suis battu avec Netfilter pour activer un proxy transparent sur un routeur OpenWRT
Les références de base à consulter pour Netfilter sont https://wiki.nftables.org/wiki-nftables/index.php/Main_Page et https://www.netfilter.org/projects/nftables/manpage.html
Mais les requêtes echo ICMP ont un ID, qui joue en pratique le rôle d’un numéro de port source
Pour NATer correctement l’echo ICMP, il faut remapper cet ID dans les deux sens, comme on remappe le port source d’UDP
En effet, si une machine derrière le NAT reçoit simultanément des ping de deux hôtes, et que ces deux hôtes utilisent par hasard le même numéro de requête, cela devient ambigu
Une autre possibilité consiste à ne pas réécrire l’identifiant et à maintenir une liste des machines distantes associées à chaque ID. Si l’ID entre en collision, la liste contient plusieurs adresses IP distantes ; quand une réponse arrive de la machine derrière le NAT, celui-ci peut choisir une entrée de la liste, envoyer la réponse à cette machine, puis supprimer l’entrée
Avec IPv6, il aurait fallu changer tous les nœuds du réseau et mettre à jour le DNS interne
En théorie, je pourrais avoir mon propre /48 portable, mais il faudrait que le nouveau FAI l’annonce, et même si mon FAI actuel le faisait, ce n’est pas courant
Il y a une semaine, quand la ligne téléphonique a été coupée, j’ai sorti un MiFi 5G et basculé la connexion WAN dessus ; il a suffi d’activer un simple masquerade sur cette interface. Le signal était faible, donc ce n’était pas idéal, mais ça fonctionnait
Le problème, c’est que même avec IPv6, il faut toujours exploiter une double pile ou utiliser cette abstraction NAT crasseuse. Pour moi, cela ajoute du travail sans bénéfice
Côté professionnel, c’est pareil. Des véhicules utilisant des sous-réseaux internes en 172.16/12 se connectent et routent entre eux, et communiquent avec l’extérieur via diverses connexions VPN. Comme ils sont souvent garés en sous-sol avec très peu de signal, l’architecture consiste à espérer qu’au moins une des différentes méthodes fonctionne
Si on passe à IPv6, il faudra à nouveau déplacer des /48. En plus, ces véhicules reçoivent Internet dans plusieurs stades, dont beaucoup ont déjà du mal à désactiver le MITM/443 ou même à lever le blocage de l’UDP. Ça ne marche pas dans un environnement où l’on arrive le samedi à 10 h et où tout doit fonctionner deux heures plus tard
Je ne vois pas quel bénéfice business il y aurait à passer en double pile pour doubler la charge de travail et les risques
https://en.wikipedia.org/wiki/Lindy_effect
https://stackoverflow.com/questions/31857419/how-to-send-a-m...
Malheureusement, ping est géré par le système d’exploitation, donc les applications sur l’IP du pair ne peuvent pas lire les messages
Je me demande s’il ne serait pas temps de fournir des hooks en espace utilisateur pour certains de ces services, afin de permettre un vrai P2P lorsque les deux côtés sont derrière un NAT. Au minimum, quelque chose comme un flux d’événements en lecture seule. À ce stade, tous les obstacles semblent artificiels
Cela dit, j’ai déjà vu des stratégies d’exfiltration de données utilisant ping, ou d’autres modes de communication. Aujourd’hui, la plupart des configurations par défaut des pare-feu jettent silencieusement tout l’ICMP, y compris ping, donc ça me semble presque impossible pour du P2P
idsemble en fait équivalent à(sport, dport), mais il est sur 16 bits, donc l’espace est beaucoup plus petit que 32 bitsMais il me semble que le problème central du NAT hole punching, c’est qu’il faut une activité des deux extrémités pour établir la connexion. C’est pourquoi il faut toujours un serveur de coordination pour indiquer à T que le nœud S veut lui parler
Cela donne quand même matière à réfléchir. Je me demande s’il y aurait un moyen de faire ça avec des messages de routage ICMP, par exemple unreachable ou TTL expired. Quand on fait un traceroute vers une IP, on reçoit des paquets depuis diverses autres IP arbitraires, et ça traverse généralement le NAT
On peut imaginer qu’un hôte T qui veut recevoir des connexions entrantes choisisse une adresse IP « factice » aléatoire, publie
(IP du routeur, IP factice)comme identifiant, puis envoie périodiquement des paquets vers cette IP factice. Un hôte S qui veut parler à T pourrait envoyer au routeur de T un ICMP TTL-expired concernant cette adresse factice, et le routeur pourrait le voir et le transmettre à TBien sûr, cela dépend de la question de savoir si l’adresse IP à l’intérieur des champs ICMP est soumise au filtrage d’entrée comme l’adresse dans l’en-tête IP
Édition : il y a déjà un commentaire de premier niveau qui pointe vers une implémentation de cette idée
[1] - https://github.com/yarrick/pingfs
Avec GitHub, on peut bien lier une combinaison hash de commit, nom de fichier et numéro de ligne, mais si la base de code change beaucoup, ce n’est pas très utile. Sur des interfaces web git moins courantes comme git.blender.org, ça ne fonctionnait pas bien
https://elixir.bootlin.com/linux/latest/source