2 points par GN⁺ 2023-09-11 | 1 commentaires | Partager sur WhatsApp
  • Le NAT IPv4 distingue généralement la cible des réponses à l’aide des ports TCP/UDP, mais l’echo ICMP de ping n’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/24 vers 10.0.100.0/24 avec des espaces de noms réseau, en créant client1, client2, natbox et server, ainsi qu’une règle iptables MASQUERADE
  • 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_DGRAM de 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, natbox jouant 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é serveur
    • client1 : 192.168.99.1/24
    • client2 : 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, et natbox est 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.3 afin que le trafic vers le serveur passe par natbox
  • Sur natbox, le forwarding de paquets est activé avec net.ipv4.ip_forward=1, et une règle MASQUERADE est ajoutée à la chaîne POSTROUTING de la table nat d’iptables
    • ip 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 icmp dans les espaces de noms client1 et server
  • Côté client, on voit une echo request 192.168.99.1 > 10.0.100.2 et une echo reply 10.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 de natbox
  • Les requêtes ICMP provenant de clients différents ont des champs id différents
    • dans l’exemple, client1 a l’ID 31428
    • client2 a l’ID 33391
  • Cette observation montre que natbox peut 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
  • 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 ping fait partie du paquet iputils
  • Un commentaire près de ping4_send_probe explique 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, ping possède un champ ident dans struct ping_rts
    • sa valeur par défaut est -1
    • il peut être remplacé via l’option CLI -e par une valeur comprise entre 0 et IDENTIFIER_MAX, soit 0xFFFF
  • Si rts->ident == -1, ping associe le socket avec le type SOCK_DGRAM et le protocole IPPROTO_ICMP
  • D’après la description des sockets Linux IPPROTO_ICMP, l’en-tête ICMP est vérifié et nettoyé lors du send(), et l’id est défini à partir du numéro de port local du socket
  • Si ping ne 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 999 est 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é en 30218
  • 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/netfilter du code Linux les parties qui utilisent le champ ICMP id

Rôle de netfilter, conntrack et NAT

  • Le sous-système du noyau qui implémente les règles iptables est netfilter
  • Comme la règle MASQUERADE effectue le NAT, l’implémentation du NAT ICMP se trouve elle aussi dans netfilter
  • nf_nat_setup_info dans nf_nat_core.c appelle get_unique_tuple, qui mène ensuite à nf_nat_l4proto_unique_tuple
  • nf_nat_l4proto_unique_tuple contient un cas IPPROTO_ICMP et référence tuple->src.u.icmp.id
  • nf_nat_manip_pkt dans nf_nat_proto.c passe par nf_nat_ipv4_manip_pkt et l4proto_manip_pkt, puis appelle icmp_manip_pkt dans le cas ICMP
  • icmp_manip_pkt écrit l’ID ICMP réel dans le paquet via hdr->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_conn contient tuplehash[IP_CT_DIR_MAX]
    • IP_CT_DIR_ORIGINAL : direction du paquet sortant
    • IP_CT_DIR_REPLY : direction de la réponse entrante
  • Chaque nf_conntrack_tuple_hash contient un nf_conntrack_tuple qui identifie la connection
  • Le tuple est séparé entre src, modifiable, et dst, immuable
    • src contient l’adresse IP et des champs spécifiques au protocole
    • pour l’ICMP, le champ spécifique au protocole est __be16 id
    • dst contient l’adresse IP non modifiée ainsi que l’ICMP type et code
  • 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 natbox reçoit un echo ICMP, nf_nat_setup_info cré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_pkt définit l’IP source ou destination ainsi que l’ID ICMP en fonction des valeurs stockées dans la connection
  • get_unique_tuple est le chemin central qui choisit un tuple NAT disponible
    • find_best_ips_proto réécrit l’adresse IP source
    • nf_nat_used_tuple vé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_tuple est appelé pour effectuer le NAT spécifique au protocole
  • En ICMP, tuple->src.u.icmp.id est choisi comme clé ciblée par le NAT
  • find_free_id génère un ID aléatoire avec get_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-200 dans la règle iptables MASQUERADE avec --to-ports
  • Si aucun tuple libre n’est trouvé, un ID en doublon reste attaché à la connection, et __nf_conntrack_confirm dé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_info et nf_nat_manip_pkt
  • Un kprobe trace le moment où la fonction est appelée, et un kretprobe trace 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_buff est la structure utilisée par le noyau Linux pour représenter un paquet
  • bswap sert à convertir l’ordre des octets réseau, big endian, en little endian
  • ntop convertit 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_buff ou nf_conn sans inclure de fichiers d’en-tête
  • Ce programme bpftrace a é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_info est appelé une fois pour chaque client
  • Pour le premier client 192.168.99.1, les tuples original et reply conservent tous deux l’ID ICMP 999
  • Pour le second client 192.168.99.2, l’ID ICMP du tuple reply est réécrit en 32809
  • nf_nat_manip_pkt remplace l’IP source par 10.0.100.1 dans l’echo request avec NF_NAT_MANIP_SRC, puis restaure l’IP de destination vers l’IP d’origine du client dans la réponse avec NF_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_info est rappelé au ping suivant
  • Le comportement du NAT Linux pour ping est é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

 
GN⁺ 2023-09-11
Commentaires Hacker News
  • https://samy.pl/pwnat/ pourrait vous intéresser
    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
    • En résumé, l’astuce consistant à pinguer 3.3.3.3 permet à un serveur derrière un NAT de connaître l’adresse IP d’un client derrière un NAT, sans avoir besoin d’un serveur non NATé comme https://ifconfig.co
      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 un appareil du réseau local envoie un ping vers un appareil sur Internet, le routeur qui fait le NAT remplace l’adresse source du ping par sa propre IP publique et réécrit le champ ID du paquet ICMP avec une valeur unique
    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
    • Pour mieux voir les choses, il faut se demander comment le système d’exploitation distingue différentes conversations ICMP à destination du même hôte
      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
    • En poussant un peu plus cette idée, cela revient à transformer un protocole sans état en protocole avec état
    • De toute façon, ping a besoin de ces informations d’état pour associer requêtes et réponses
    • Je me demande pourquoi ne pas utiliser l’IP privée source plutôt qu’une « valeur unique »
    • Je me demande si cet ID se trouve dans l’en-tête ICMP, ou s’il relève plutôt de la partie IP
  • C’est réjouissant de voir un article du genre « comment ça marche » descendre les couches d’abstraction jusqu’au code source. Les explications sont bonnes et riches en informations
    • J’allais dire la même chose. Le routage et le réseau restent encore confus pour moi, et les articles sur le sujet me paraissent généralement trop abstraits
      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/
  • Bon article
    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
  • ICMP n’a pas de ports, donc il n’y a pas à traiter le problème du renvoi d’une réponse echo ICMP vers le bon port par le NAT
    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
  • NAT est vraiment une abstraction crasseuse. IPv4 devrait disparaître
    • Sur mon accès Internet domestique, j’ai des appareils répartis sur plusieurs sous-réseaux en 192.168. Il y a quelque temps, en changeant de FAI, l’AS auquel mon domicile appartient a changé et j’ai reçu une nouvelle adresse IPv4, mais il m’a suffi de mettre à jour, sur le routeur WAN, la redirection du trafic entrant vers la nouvelle IP
      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

  • Je ne suis pas sûr qu’IPv6 résolve ce problème. Techniquement oui, mais les grands fournisseurs ne donnent déjà qu’un /64 aux particuliers et facturent cher les /48 « business », ce qui mène déjà à du NAT IPv6 ou à un redécoupage supplémentaire des /64. Ce n’est pas censé fonctionner comme ça
  • Du coup, on finira par détester encore plus le CG-NAT
  • Il faut garder en tête l’effet Lindy. C’est l’observation selon laquelle la durée de vie future d’une chose qui ne s’use pas, comme une technologie ou une idée, est proportionnelle à son âge actuel ; IPv4 étant ancien, il a de bonnes chances de rester encore assez longtemps
    https://en.wikipedia.org/wiki/Lindy_effect
  • IPv6 devrait aussi disparaître. Il a eu largement le temps de s’imposer, mais il continue de stagner
  • Je me demande s’il serait possible d’abuser de ping pour envoyer de courts messages dans du réseau P2P basé sur UDP, tout en gérant la traversée de NAT sans serveur central. Quelqu’un semble déjà avoir trouvé la partie « message »
    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
    • Petite correction technique : ping n’est pas de l’UDP, mais de l’ICMP
      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
    • Idée intéressante. Le champ id semble en fait équivalent à (sport, dport), mais il est sur 16 bits, donc l’espace est beaucoup plus petit que 32 bits
      Mais 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 à T
      Bien 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
    • Ça existe déjà : https://samy.pl/pwnat/
    • Ce n’est pas exactement ce que je cherchais, mais l’idée d’abuser de ping m’a fait penser à pingfs. Ça donne une toute nouvelle définition au cloud computing
      [1] - https://github.com/yarrick/pingfs
    • Avec une adoption plus large d’IPv6, tout le monde aura une IP routable publiquement et pourra éviter complètement le NAT, donc ce genre de problème diminuera
  • Quand on écrit ce genre de billet de blog, c’est agaçant de voir à quel point il est difficile de lier une ligne de code précise tout en faisant en sorte que le lien reste vivant et utile dans le temps
    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
    • Pour le code du noyau Linux, on peut utiliser elixir afin de pointer au moins vers une version précise. Si l’on veut un minimum de persistance, il suffit d’utiliser une version LTS
      https://elixir.bootlin.com/linux/latest/source
  • En résumé, il y a un champ id dans les paquets ICMP, et Netfilter reconnaît les paquets ou trames ICMP comme un « cas particulier »