Ping n’est pas un diagnostic. C’est un tunnel. L’écho ICMP (la requête qu’une machine envoie et la réponse qu’elle reçoit en retour) portera n’importe quels octets que vous y mettez, dans les deux sens, sur un protocole que la plupart des pare-feu laissent passer sans inspecter et que la plupart des systèmes de journalisation comptent plutôt qu’ils ne lisent. Un réseau qui « n’autorise que le ping » a déjà un VPN complet et chiffrable vers l’extérieur, et quiconque peut atteindre ce réseau peut en piloter un.

C’est vieux, et c’est documenté. Loki1 a publié la technique dans Phrack 49 en 1996. Ptunnel2 transporte des sessions TCP entières dans du ping depuis vingt ans et est à un paquet de distance sur n’importe quelle machine Linux. Pas de faille du jour zéro. Le protocole qui se comporte exactement comme spécifié.

Voilà la cause : la norme, pas un bug dans le produit de quiconque. Chaque hôte de la planète est tenu de prendre les données d’une requête d’écho et de les rendre telles quelles, inchangées. Des données arbitraires en entrée, les mêmes en sortie : c’est tout ce dont un tunnel a besoin, et c’est imposé depuis 1981.

Le correctif est un seul changement étroit. Jetez l’écho ICMP, requête et réponse, en IPv4 et IPv6, à la frontière, et gardez chaque autre message ICMP. Les erreurs — Time Exceeded, Packet Too Big, Destination Unreachable — sont porteuses ; bloquez-les et vous cassez la découverte du MTU de chemin et traceroute pour rien. Donc ce n’est pas « bloquer l’ICMP ». C’est jeter le seul message qui est un risque, et garder ceux qui portent la vérité.

Ce que votre politique de sortie a réellement autorisé

Si vous exploitez un réseau où le sortant est filtré (et si ce n’est pas le cas, c’est une autre conversation), alors ceci est la facture d’une ligne dedans. La politique qui dit « bloquer tout le sortant, autoriser l’ICMP parce qu’on a besoin de pinguer des choses » n’est pas une politique de sortie. C’est un tunnel complet dont la paperasse est classée sous « diagnostic ». Tout ce qui est à l’intérieur et sait envoyer une requête d’écho et lire la réponse peut déplacer des données vers n’importe quoi à l’extérieur qui y répond, au débit que la liaison supportera, et au-delà de chaque contrôle de contenu que vous avez acheté. Dans les journaux, c’est quelqu’un qui vérifie si l’internet marche, et rien d’autre. Les logiciels malveillants livrent ça depuis des années pour cette raison exacte. Discret, standard, et d’habitude déjà autorisé.

C’est pourquoi c’est un canal de premier choix pour quiconque est déjà à l’intérieur et ne devrait pas y être. C’est l’installation dans la durée, pas le casse rapide. Quelqu’un qui cherche une voie d’entrée et de sortie qui dure évite le port qui déclenche une alerte. Il monte un tunnel sur l’écho et le laisse tourner des mois. Un hôte qui pingue beaucoup est un hôte que personne ne surveille. Un réseau bidirectionnel jusque dans le parc : commande à l’entrée, données à la sortie, sur le seul protocole que personne ne limite en débit, ne surveille par alerte, ni ne lit. Et c’est chiffré, comme n’importe quel vrai outil le chiffre, si bien que sur le fil la charge utile n’est que les octets d’apparence aléatoire qu’un ping porte de toute façon, et l’inspection de contenu n’a rien à lire. La taille et le rythme peuvent encore le trahir auprès de quelqu’un qui regarde vraiment. Regarder à l’intérieur du paquet, non.

Et la machine qui le pilote n’a pas à appartenir à quelqu’un qui travaille là. Il suffit d’une chose qui puisse atteindre votre réseau et poser un ping sur l’internet, et atteindre votre réseau est plus facile que personne n’aime l’admettre. Le WiFi porte au-delà des murs, dans le couloir, le parking, l’appartement du dessus. La prise ethernet dans le mur de la salle de réunion, ou de l’accueil, ou du bureau vide près de la fenêtre, est très souvent active et parlera à tout ce que vous y branchez, sans 802.1X qui demande qui vous êtes. Un signal à portée, ou une prise que personne n’a verrouillée. Voilà le droit d’entrée. Pas de badge, pas de compte, pas d’invitation.

Imaginez le visiteur que vous avez, lui, invité. Il est en réunion, agréable, prend des notes, contribue. Son portable, non. À la seconde où il a atteint votre réseau, par les airs ou par la prise sous la table, il était à l’intérieur du mur, et si ce réseau sait émettre une requête d’écho, il a une voie de sortie. Rien n’a été déposé sur votre parc. Pas de compte, pas de privilège sur quoi que ce soit qui vous appartient, rien que vos agents de poste puissent attraper, parce que vos agents ne sont pas sur sa machine. La personne est en face de vous. Le trafic sort par votre porte d’entrée, chiffré, indiscernable d’un portable qui pingue plus qu’il ne devrait.

Un visiteur à portée du signal, ou sur une prise ouverte, obtient un tunnel vers l'extérieur à travers un pare-feu qui ne voit que des pingsUn visiteur à portée, ou sur une prise ouverte, obtient une route vers l'extérieurà l'intérieur du mur — votre réseaule WiFi atteintle couloir,le parking,l'appartement du dessusportable du visiteurpas de badge, pas de compteune prise murale activepas de 802.1X pour vous identifierpare-feu de frontièrene voit que des pingsserveursur l'internetrequête d'écho →← réponse d'échon'importe quel trafic IP, chiffré, à l'intérieur des pings
Le droit d’entrée est l’accès au réseau — un signal WiFi à portée, ou une prise murale active sans 802.1X — et la sortie est l’écho à travers la frontière. Pour le pare-feu, c’est un hôte qui pingue ; à l’intérieur de ces pings, il y a n’importe quel trafic IP, chiffré, à destination d’un serveur sur l’internet.

Les deux choses vers lesquelles vous allez vous tourner n’aident pas. Le NAT est une table de traduction, pas un filtre : une requête d’écho d’un hôte interne ouvre une correspondance indexée sur l’id ICMP, que le NAT traite exactement comme il traite un port, et la réponse revient par là comme n’importe quel autre flux. J’ai fait tourner un client derrière mon propre NAT domestique et il n’a jamais remarqué que le NAT était là. Un VLAN invité séparé ne vaut pas mieux. La ségrégation garde le visiteur hors de vos serveurs, mais elle ne fait rien pour le garder hors de l’internet, et l’internet, atteint par un ping, est toute l’exigence. Un seul contrôle touche à ça : ce réseau laisse-t-il l’écho sortir ?

Vous ne réglez pas ça par l’inspection, et vous ne le réglez pas par la ségrégation. Vous fermez la porte. Tout ce qui suit explique pourquoi la norme la laisse ouverte, trois façons de bâtir le tunnel pour que l’affirmation tienne sur plus que ma parole, et le seul changement qui la ferme.

La partie de l’ICMP qui ne vous doit rien d’utile

Commençons par ce que la norme dit vraiment, parce que tout l’argument repose sur une phrase et que les gens l’écartent d’un revers de main sans la lire.

Une requête d’écho porte un champ de données. En IPv4, la RFC 7923 dit clairement que « the data received in the echo message must be returned in the echo reply message ». IPv6 a resserré la formulation plutôt que de la relâcher : la RFC 44434 définit le champ comme « zero or more octets of arbitrary data » puis exige qu’il « MUST be returned entirely and unmodified in the ICMPv6 Echo Reply message ».

Lisez ça en exploitant et c’est un keepalive. Lisez-le en quelqu’un qui déplace des données et c’est un cadeau. La norme oblige chaque hôte joignable à accepter un bloc d’octets que vous choisissez et à vous les renvoyer tels quels, inchangés, sur demande. Longueur arbitraire. Contenu arbitraire. Pas de poignée de main, pas de port, pas d’application au bout d’en face qui doive consentir à quoi que ce soit. Le noyau le fait, avant qu’aucun processus de l’espace utilisateur n’ait son mot à dire.

Et c’est les deux familles. Passer à IPv6 ne range pas ça. Ça l’ouvre plus grand. La RFC 792 disait que les données « must be returned » ; la RFC 4443 dit qu’elles « MUST be returned entirely and unmodified », c’est la même porte tenue ouverte par une serrure plus solide. À ce titre, le canal existe sur l’écho ICMP comme sur l’écho ICMPv6, et une règle qui le ferme sur une famille et pas l’autre n’a rien fermé. Le tunnel se déplace juste vers la famille que vous avez laissée ouverte. Quoi que vous fassiez à ce sujet, faites-le sur ip et ip6 ensemble.

Rien d’autre dans le protocole ne fait ça. Un Time Exceeded porte l’en-tête du paquet qui est mort et rien de plus. Un Destination Unreachable est un compte rendu sur quelque chose qui a déjà eu lieu. Ces messages vous disent des faits sur le réseau. L’écho porte tout ce que vous y mettez, dans les deux sens, et appelle ça un diagnostic.

Les erreurs sont porteuses. L’écho, non.

C’est la distinction que la bande du « bloquez juste l’ICMP » ne fait jamais, et c’est tout le propos, alors je vais la faire une fois et proprement.

Bloquer les erreurs ICMP casse le réseau en silence. Filtrez Packet Too Big et vous tuez la découverte du MTU de chemin : la poignée de main s’achève, les petits transferts marchent, et tout ce qui porte un paquet de taille pleine se bloque pour toujours sans rien dans les journaux. En IPv6, ce n’est même pas une affaire de goût. Les routeurs ne fragmentent pas, alors la RFC 48905 liste Packet Too Big parmi les messages qu’un pare-feu « must not drop » et prévient que sans lui « parts of the Internet will become inaccessible ». Filtrez Time Exceeded et vous cassez traceroute, que la RFC 18126 nomme comme la raison d’être de l’obligation du message au départ. Ce ne sont pas des options. Ce sont le retour qui rend le réseau autocorrecteur, et j’ai couvert le coût de leur perte en détail la dernière fois.

Maintenant jetez l’écho et allez chercher ce qui a cassé. ping à travers la frontière cesse de marcher. Voilà la liste. Toute la liste.

La découverte du MTU de chemin s’en moque, parce qu’elle tourne sur Packet Too Big, qui est une erreur. Traceroute s’en moque, parce que traceroute -T et -U marchent en TCP et UDP et lisent les erreurs qui reviennent. Aucun d’eux n’envoie d’écho. La découverte de voisins en IPv6 s’en moque, parce que ce sont les types 133 à 137, et vous les gardez ou le segment meurt. L’astuce du TTL inverse du billet précédent marche sur n’importe quelle réponse, et une poignée de main TCP vous en donne une. Tout ce qui faisait marcher le diagnostic du billet précédent continue de marcher, parce que pas une seule mesure là-dedans n’envoyait de requête d’écho.

Les deux moitiés du protocole ne pourraient donc pas être moins semblables. Une moitié est le réseau qui vous dit la vérité sur lui-même, et vous la cassez à vos risques. L’autre moitié est un service de renvoi de charge utile imposé qui se trouve s’appeler un diagnostic, et la chose la plus forte qu’on puisse dire pour le garder ouvert, c’est que ping est pratique. C’est pratique. C’est aussi la seule partie de l’ICMP qu’un attaquant peut piloter, et je peux vous montrer exactement avec quoi ils la pilotent.

Gardez les erreurs ICMP, à débit limité ; ne jetez que l'écho, dans les deux sens et les deux famillesDeux moitiés d'un protocole, et une seule est un risqueGARDERlimiter le débit, jamais bloquerDestination Unreachableporte le pourquoi d'un paquet non livréPacket Too Bigla découverte du MTU de chemin en dépendTime Exceededtraceroute en dépendParameter Problemsignale un en-tête malforméDécouverte de voisins 133–137IPv6 : le segment local en dépendCassez-les et le réseau casse en silence.JETERles deux sens, les deux famillesEcho requesttype 8 (IPv4) · type 128 (IPv6)Echo replytype 0 (IPv4) · type 129 (IPv6)Rien n'en a besoin sauf lapingcommande.Tout ce qu'un attaquant piloteà travers l'ICMP passe par eux.Un tunnel laissé ouvert sur une famille n'est pas fermé.
Les erreurs ICMP sont porteuses : la découverte du MTU de chemin, traceroute et la découverte de voisins IPv6 en dépendent toutes, et les bloquer casse le réseau en silence. L’écho est la seule partie dont rien ne dépend sauf ping, et la seule qu’un attaquant peut piloter. Jetez celle-là, gardez le reste, sur les deux familles.

La preuve : un VPN fait de ping

L’outil est Hans7, écrit par Friedrich Schöller. Sa propre description tient en une ligne : il « makes it possible to tunnel IPv4 through ICMP echo packets, so you could call it a ping tunnel. » Il monte une interface tun à chaque bout, leur donne des adresses, et déplace chaque paquet entre elles à l’intérieur de l’écho ICMP. Pour le réseau au milieu, c’est quelqu’un qui pingue un serveur et le serveur qui répond. Pour moi, c’est une route.

Un paquet IP entier voyage à l'intérieur du champ de données d'un seul pingUn paquet entier voyage à l'intérieur d'un seul pingLe paquet que votre session SSH envoieen-tête IPsrc / dsten-tête TCPport 22données applicativesvos frappes, votre fichierHans copie tout le paquet dans le champ de données d'échoLe paquet qui part sur le filen-tête IPvous vers serveurrequête d'écho ICMPtype 8 · id · seqchamp de données d'échole paquet entier ci-dessus, inchangéPour le pare-feu de frontière, c'est une requête d'écho, et le journal note un ping.Dans le champ de données, il y a un paquet à destination de partout où le bout d'en face peut atteindre.La norme oblige le bout d'en face à renvoyer ces données telles quelles, si bien que la réponse est un paquet elle aussi.
Chaque paquet que le tunnel porte est copié dans le champ de données d’une requête d’écho ICMP. La norme oblige le bout d’en face à renvoyer ces données inchangées, si bien que la réponse porte un paquet elle aussi. Le pare-feu compte un ping ; la charge utile va partout où le bout d’en face sait router.

Je l’ai fait tourner sur ma propre ligne, un serveur sur une adresse publique et un client derrière mon NAT domestique, et j’ai poussé du vrai trafic à travers. Pas des pings synthétiques avec un drapeau posé. Une connexion SSH et un téléchargement de fichier, à cheval dans l’écho.

C’est à un paquet de distance là où j’ai fait tourner le serveur, et ça se compile depuis les sources de Schöller partout ailleurs. Le serveur doit être sous Linux et a besoin de root, parce qu’ouvrir un équipement tun et un socket ICMP brut le demandent tous les deux. La syntaxe est délibérément réduite :

# on the server (public IP), pick the tunnel network and a password
hans -s 10.8.0.0 -p '<password>'
# server takes 10.8.0.1; clients are handed 10.8.0.2 and up

# on the client, point it at the server's public address
hans -c <server-public-ip> -p '<password>'

Une fois les deux bouts levés, il y a une nouvelle interface de chaque côté avec une adresse sur le réseau du tunnel, et elle se comporte comme n’importe quelle autre liaison point à point :

Rien dans cette session ne sait qu’elle est à l’intérieur du ping. SSH ouvre une connexion TCP vers 10.8.0.1, le noyau la route par tun0, Hans enveloppe chaque paquet en charge utile d’une requête d’écho, et le bout d’en face le déballe et le remet à son propre tun0. La réponse revient en charge utile d’une réponse d’écho. Pour SSH, il parle sur une liaison ordinaire. Pour le pare-feu, personne n’a ouvert quoi que ce soit. Un hôte se fait pinguer.

À quoi ça ressemble sur le fil

C’est la partie qui met fin au débat, alors regardez la vue depuis le pare-feu plutôt que la mienne.

Ce qui se passe vraiment, et ce que le pare-feu journalise, ne sont pas la même imageUn tunnel, deux imagesCe qui se passe vraimentvotre hôtetun0 · 10.8.0.2le serveurtun0 · 10.8.0.1partout où ilpeut atteindreSSH, téléchargementsrouté vers l'extérieurn'importe quel trafic IP, par le tunnelen paquets ordinairesCe que le pare-feu de frontière voit et journalisevotre hôte198.51.100.9le serveur192.0.2.7requête d'échoréponse d'échoicmp echo request 198.51.100.9 > 192.0.2.7icmp echo reply 192.0.2.7 > 198.51.100.9icmp echo request 198.51.100.9 > 192.0.2.7... comptés comme des pings, contenu non luMême fil. Le pare-feu ne voit jamais les paquets à l'intérieur.
Le tunnel déplace du vrai trafic entre deux hôtes puis vers partout où le serveur peut atteindre. À la frontière, ce n’est que requête d’écho et réponse d’écho — le pare-feu journalise des pings et ne voit jamais les paquets portés à l’intérieur.

Asseyez-vous sur l’interface externe avec tcpdump et capturez uniquement l’ICMP pendant que la session SSH tourne. Aucun TCP vers le port 22 ne traverse la frontière. Ce qui traverse, c’est requête d’écho et réponse d’écho, dans les deux sens, chacune plus grosse qu’un vrai ping parce qu’elle porte une tranche de segment TCP dans sa charge utile :

# on the boundary, watch only ICMP echo while traffic runs over the tunnel
tcpdump -ni <wan-iface> 'icmp[icmptype] = icmp-echo or icmp[icmptype] = icmp-echoreply'

Lisez les deux choses qui comptent dans cette capture. D’abord les longueurs de charge utile : un ping normal envoie 56 octets et chaque ligne a la même taille, alors que celles-ci varient et sont grosses, parce que la taille de la chose que vous déplacez transparaît dans la taille du ping. Ensuite le débit : un ping de diagnostic, c’est un par seconde, et là c’est un déluge, parce qu’il déplace un fichier. Ni l’un ni l’autre n’est caché. Les deux sont en pleine vue sur un protocole que personne ne regarde.

Et c’est tout le propos. Pas que ce soit malin, ou dur à repérer une fois qu’on regarde. Presque personne ne regarde, parce que la boîte est configurée pour autoriser l’ICMP, les journaux le comptent comme des pings, et l’alerte a été réglée pour les ports que quelqu’un a pensé à surveiller. Le trafic sort en ayant l’air d’un contrôle de santé, et le contrôle de santé est une route vers partout où le serveur peut atteindre.

Un vrai VPN à tunnel complet, monté à la main

Hans prouve que le canal est là, mais il fait l’interface et l’adressage pour vous et vous rend une route, donc il ne vous montre pas tout à fait ce que vous avez bâti. Pour voir que c’est un VPN au sens plein — tout le trafic de la machine qui sort par le ping, pas une liaison entre deux hôtes nommés — assemblez-le à la main. icmptunnel8, de Dhaval Kapil, est celui pour ça, et sa propre description tient en une ligne : « Transparently tunnel your IP traffic through ICMP echo and reply packets. » Même idée, équipement tun et charges utiles d’écho, mais vous posez la plomberie vous-même et rien ne se cache dans un binaire.

Sur le serveur, vous démarrez le tunnel, montez l’interface, puis faites la chose qui vend la mèche : dire au noyau d’arrêter de répondre aux pings lui-même, pour que ses propres réponses d’écho ne se battent pas avec celles que le tunnel envoie.

sudo ./icmptunnel -s 10.0.1.1                 # server mode; creates tun0, then blocks
# from a second shell, bring the interface up (iproute2, not the net-tools the repo ships)
sudo ip addr add 10.0.1.1/24 dev tun0
sudo ip addr add 2001:db8:1::1/64 dev tun0
sudo ip link set tun0 mtu 1472 up             # 1500 − 20 (IP) − 8 (ICMP); see below
# stop the kernel replying to pings — echo now belongs to the tunnel
sudo sysctl -w net.ipv4.icmp_echo_ignore_all=1
sudo sysctl -w net.ipv6.icmp.echo_ignore_all=1
# let the server route the client's packets onward
sudo sysctl -w net.ipv4.ip_forward=1

Relisez la ligne icmp_echo_ignore_all, parce qu’elle dit plus qu’elle n’en a l’air. Ce bouton est celui du noyau, et il est livré comme une défense : posez-le et, selon les mots du noyau, il « will ignore all ICMP ECHO requests sent to it »9, si bien qu’un exploitant peut retirer un hôte du radar du ping entièrement. Il est sur les deux familles désormais. net.ipv4.icmp_echo_ignore_all depuis des années, et net.ipv6.icmp.echo_ignore_all ajouté plus tard pour aligner.10 Le tunnel bascule cet interrupteur défensif dans l’autre sens : le noyau ne répondant plus à l’écho lui-même, ses deux bouts sont libres d’utiliser l’écho comme transport pur. Ce qui est le signe. Les gens qui ont écrit la pile traitent déjà l’écho comme quelque chose qu’un hôte peut raisonnablement refuser — la règle de ce billet fait ce même choix une fois, à la frontière, pour chaque hôte derrière elle.

Sur le client, vous montez l’interface puis pointez la route par défaut dedans. Pas un hôte atteint par le tunnel. Tout.

sudo ./icmptunnel -c <server-public-ip>       # client mode; creates tun0
sudo ip addr add 10.0.1.2/24 dev tun0
sudo ip addr add 2001:db8:1::2/64 dev tun0
sudo ip link set tun0 mtu 1472 up
# keep the route to the server itself OUT of the tunnel...
sudo ip route add <server-public-ip> via <gateway> dev <iface>
# ...then send everything else down it
sudo ip route replace default dev tun0

Les client.sh et server.sh du projet vont encore chercher ifconfig et route de net-tools. Les commandes ip ci-dessus sont les équivalents iproute2 et font le même travail. Cette route vers le serveur est la ligne que les gens oublient : gardez-la hors du tunnel, sinon les paquets d’écho qui portent le tunnel essaient de voyager par le tunnel, et rien ne sort. Tout le reste passe maintenant par tun0, enveloppé dans l’écho, et atteint le serveur. Qu’il aille plus loin est un choix de routage. Une seule règle de masquerade sur le serveur le poserait sur l’internet public sous l’adresse propre du serveur, et elle n’est délibérément pas ici, parce que le tunnel est la chose montrée et qu’il n’en a pas besoin.

Voilà un VPN à tunnel complet, bâti à partir du ping en une poignée de commandes. Chaque paquet que le client envoie — web, DNS, SSH, tout — est capturé dans tun0 et sort en requête d’écho vers le serveur, et les réponses reviennent en réponses d’écho. Une machine sur un réseau qui « n’autorise que l’ICMP » vient de remettre tout son trafic sortant à une boîte à l’extérieur, et la frontière a journalisé un hôte qui aime pinguer.

Écrire le vôtre en Python

Voici la partie qui devrait troubler quiconque espère se défendre contre ça en repérant un outil. Ni Hans ni icmptunnel ne fait quoi que ce soit que vous ne pourriez pas écrire vous-même en un après-midi. Ouvrez un équipement tun, enveloppez chaque paquet en charge utile d’une requête d’écho, déballez ceux qui reviennent. C’est tout le mécanisme, et le tunnel nu fait environ soixante lignes de Python de bibliothèque standard avec rien du tout à installer. La version ci-dessous ajoute une chose par-dessus : elle chiffre la charge utile, et c’est la seule partie qui tire une dépendance.

#!/usr/bin/env python3
# pingvpn.py — a VPN tunnel over ICMP echo, in one short file.
#
# Not a product. It exists to show the channel is trivial to rebuild, so a
# defence that hunts for a known tool is chasing the wrong thing entirely.
# Linux, needs root (a tun device and a raw ICMP socket both do), and the
# `cryptography` package for the AES (pip install cryptography).
#
#   server:  sudo python3 pingvpn.py --server
#   client:  sudo python3 pingvpn.py --client <server-public-ip>
#
# The payload is encrypted with AES-128-GCM under a pre-shared key before it
# goes on the wire, so what a firewall sees in the echo data is random bytes —
# the same as a real ping's padding, and nothing for content inspection to read.

import argparse
import fcntl
import hashlib
import os
import select
import socket
import struct
import sys

from cryptography.hazmat.primitives.ciphers.aead import AESGCM

TUNSETIFF    = 0x400454CA
IFF_TUN      = 0x0001
IFF_NO_PI    = 0x1000
MAGIC        = 0x4954          # 'IT' in the id field, so we ignore real pings
ECHO_REQUEST = 8
ECHO_REPLY   = 0

PSK   = b"change-me-to-a-shared-secret"          # pre-shared secret, both ends
KEY   = hashlib.sha256(PSK).digest()[:16]        # 128-bit key -> AES-128-GCM
AEAD  = AESGCM(KEY)


def open_tun(name=b"tun0"):
    fd = os.open("/dev/net/tun", os.O_RDWR)
    fcntl.ioctl(fd, TUNSETIFF, struct.pack("16sH", name, IFF_TUN | IFF_NO_PI))
    return fd


def encrypt(data):                                # -> nonce || ciphertext+tag
    nonce = os.urandom(12)
    return nonce + AEAD.encrypt(nonce, data, None)


def decrypt(blob):                                # raises on a packet that is not ours
    return AEAD.decrypt(blob[:12], blob[12:], None)


def checksum(data):
    if len(data) % 2:
        data += b"\x00"
    total = sum(struct.unpack("!%dH" % (len(data) // 2), data))
    total = (total >> 16) + (total & 0xFFFF)
    total += total >> 16
    return ~total & 0xFFFF


def build_echo(icmp_type, payload):
    head = struct.pack("!BBHHH", icmp_type, 0, 0, MAGIC, 0)
    csum = checksum(head + payload)
    return struct.pack("!BBHHH", icmp_type, 0, csum, MAGIC, 0) + payload


def main():
    ap = argparse.ArgumentParser(description="a VPN tunnel over ICMP echo")
    group = ap.add_mutually_exclusive_group(required=True)
    group.add_argument("--server", action="store_true")
    group.add_argument("--client", metavar="SERVER_IP")
    args = ap.parse_args()

    out_type = ECHO_REQUEST if args.client else ECHO_REPLY
    in_type  = ECHO_REPLY   if args.client else ECHO_REQUEST
    peer     = args.client        # None on the server until a client is seen

    tun  = open_tun()
    sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP)
    print("tun0 created. bring it up with an address and route, then send traffic.",
          file=sys.stderr)

    while True:
        readable, _, _ = select.select([tun, sock], [], [])

        if tun in readable:                       # a packet wants to leave this host
            packet = os.read(tun, 65535)
            if peer:
                sock.sendto(build_echo(out_type, encrypt(packet)), (peer, 0))

        if sock in readable:                      # something arrived over ICMP
            data, _ = sock.recvfrom(65535)
            ihl  = (data[0] & 0x0F) * 4           # skip the IP header the kernel adds
            icmp = data[ihl:]
            if len(icmp) < 8 or icmp[0] != in_type or icmp[4:6] != struct.pack("!H", MAGIC):
                continue
            try:
                packet = decrypt(icmp[8:])        # wrong key or a real ping -> skip
            except Exception:
                continue
            if args.server:
                peer = socket.inet_ntoa(data[12:16])   # reply to whoever sent
            os.write(tun, packet)                 # hand the carried packet to the stack


if __name__ == "__main__":
    main()

C’est tout le tunnel. Il ouvre tun0 et un socket ICMP brut et fait la navette des paquets entre eux : ce qui quitte l’hôte est enveloppé en requête d’écho, ou en réponse d’écho sur le serveur, et envoyé au bout d’en face ; ce qui arrive par l’ICMP est déballé et remis à la pile. Le champ id est figé sur une valeur pour qu’il enjambe les vrais pings, et le serveur apprend où répondre depuis la source du premier paquet qu’il voit.

Le chiffrement est le point sur lequel s’attarder, parce que c’est ce que font les vrais outils et c’est pourquoi vous n’attraperez pas ça en regardant à l’intérieur du paquet. La charge utile est scellée avec AES-128-GCM sous une clé prépartagée avant d’être enveloppée, si bien que les octets dans les données d’écho sont indiscernables du remplissage aléatoire qu’un vrai ping porte. Retirez les deux lignes de crypto et le tunnel tourne encore sur la seule bibliothèque standard — la seule raison pour laquelle il a besoin de pip install cryptography, c’est l’AES, et l’AES est justement la partie qui transforme un tunnel lisible en tunnel illisible. Montez-le de la même façon qu’avant, moins le masquerade. Vous n’en avez pas besoin pour prouver le point :

# server: start it (creates tun0, then blocks), then configure from the same shell
sudo python3 pingvpn.py --server &
sudo ip addr add 10.9.0.1/24 dev tun0
sudo ip addr add 2001:db8:9::1/64 dev tun0
sudo ip link set tun0 mtu 1400 up
sudo sysctl -w net.ipv4.icmp_echo_ignore_all=1
sudo sysctl -w net.ipv6.icmp.echo_ignore_all=1

# client
sudo python3 pingvpn.py --client <server-public-ip> &
sudo ip addr add 10.9.0.2/24 dev tun0
sudo ip addr add 2001:db8:9::2/64 dev tun0
sudo ip link set tun0 mtu 1400 up
sudo sysctl -w net.ipv4.icmp_echo_ignore_all=1
sudo sysctl -w net.ipv6.icmp.echo_ignore_all=1
sudo ip route add <server-public-ip> via <gateway> dev <iface>
sudo ip route replace default dev tun0

La raison de l’écrire en entier n’est pas l’outil. C’est que l’outil est jetable. Un court fichier, aucune dépendance jusqu’à ce que vous ajoutiez le chiffrement, et chaque copie que quelqu’un tape a l’air un peu différente sur le fil. Donc une signature qui attrape celui-ci n’attrape rien la semaine suivante. Vous ne pouvez pas vous sortir de ça par le blocage en nommant le logiciel, parce qu’il n’y a pas de logiciel à nommer.

Ça n’a même pas besoin d’un shell. La logique, c’est des octets en entrée, des octets en sortie, donc ça se porte sur tout ce qui sait ouvrir un socket. Même en WebAssembly : le bac à sable du navigateur refuse normalement d’emblée un socket brut à une page, mais là où cette barrière est levée (un navigateur à qui l’utilisateur a donné la permission, sur une machine où l’utilisateur tourne avec assez de privilège pour qu’un socket brut soit ouvert), le même court programme tourne dans un onglet. Ce qui est la moitié inconfortable de la chose. Vous ne pouvez pas faire confiance à vos utilisateurs ici. L’hôte qui pilote le tunnel est à l’intérieur, tenu par quelqu’un que vous avez décidé être sûr parce qu’il siège derrière le pare-feu, et le pare-feu est la chose qu’on tunnelise. La confiance de périmètre suppose que la menace est hors du mur. Celle-ci commence à l’intérieur, à chaque fois.

Attention au MTU, et au plancher de 1280 d’IPv6

Le tunnel n’est pas gratuit sur le fil. Chaque paquet que vous portez gagne un en-tête IP externe et un en-tête ICMP avant de partir, donc l’interface interne doit siéger sous ce que le chemin peut porter. En IPv4, ça ne coûte presque rien à l’attaquant. Posez le MTU interne bas (le 1472 d’icmptunnel, c’est 1500 moins 20 pour l’en-tête IP externe et 8 pour l’en-tête ICMP, et le Python ci-dessus descend à 1400 pour la marge), et là où le nombre est encore faux, IPv4 fragmente le paquet trop grand et le réassemble au bout d’en face plutôt que de le jeter. Entre le plancher bas que vous pouvez choisir et la fragmentation qui recouvre le reste, le tunnel tourne sur presque n’importe quel chemin. Cette souplesse est justement ce qui fait de l’IPv4 l’endroit confortable pour faire ça.

La fragmentation et la marge de MTU d'IPv4 laissent le tunnel tourner partout ; IPv6 n'a ni l'une ni l'autre, donc il échoue ferméPourquoi il tourne partout en IPv4 et échoue fermé en IPv6IPv4en-tête IP20 oICMP8 opaquet interneMTU du tun 1472 oTrop grand ? IPv4 fragmente et réassemble — le tunnel tourne sur presque n'importe quel chemin.IPv6en-tête IPv640 oICMPv68 opaquet internene peut descendre sous 1280 oplancher dur 1280 o (RFC 8200)Trop grand ? Les routeurs IPv6 le jettent, sans fragmentation — le tunnel échoue fermé.Les replis d'IPv4 — un plancher que vous choisissez, la fragmentation pour le reste — sont ce qui rend le tunnel fiable.IPv6 n'a ni l'un ni l'autre, donc l'attaque qui tourne presque partout en IPv4 est fragile en IPv6. La famille la plus sûre ici.Packet Too Big — une erreur que vous gardez, pas un écho — est ce qui laisse un émetteur trouver la taille qui rentre. Pas à l'échelle.
Le tunnel perd un en-tête IP et ICMP externe sur chaque paquet. IPv4 vous laisse raboter le MTU interne et fragmente ce qui est encore trop grand, si bien qu’il tourne presque partout ; IPv6 pose un plancher dur de 1280 octets et ses routeurs ne fragmentent pas, donc un paquet trop grand est jeté et le tunnel échoue fermé — ce qui fait de l’IPv6 la famille la plus sûre ici.

IPv6 est moins indulgent, et pour une fois c’est de votre côté. Le plancher est dur : la RFC 820011 §5 exige que « every link in the Internet have an MTU of 1280 octets or greater », et les routeurs IPv6 ne fragmentent pas en transit. Ça retire les deux replis d’IPv4 d’un coup, et ça mord le tunnel dans les deux sens. Faites-le tourner sur de l’ICMPv6 et chaque lien du chemin doit porter 1280, donc un chemin qui descend en dessous fait tomber le transport, et il n’y a pas de rabotage sous le plancher comme on peut en IPv4. Portez de l’IPv6 à l’intérieur du tunnel et vous rencontrez le même mur par l’autre côté : l’interface interne ne peut pas descendre sous 1280 non plus, tandis que l’enveloppe ICMPv6 externe, 40 octets d’en-tête et 8 d’ICMPv6, dépense déjà le budget, donc le chemin doit disposer de 1280 plus la surcharge. Ratez-le à l’un ou l’autre endroit et le paquet est jeté, pas rogné pour rentrer : le tunnel se lève, les petites choses marchent, tout ce qui est de taille pleine se bloque. Donc IPv6 est la famille la plus sûre ici, pas la plus risquée. L’attaque qui tourne sur presque tout en IPv4 est fragile en IPv6, et échoue fermée. Plus sûre n’est pas la même chose que sûre, attention : le canal d’écho est ouvert en IPv6 aussi, donc la règle le jette toujours sur les deux familles — l’attaquant ne peut simplement pas s’appuyer sur IPv6 comme il s’appuie sur IPv4.

La récupération de ça est l’erreur ICMP que vous avez pris soin de garder. Packet Too Big est ce qui laisse un émetteur trouver la taille qui marche, et c’est une erreur, pas un écho, donc la règle que ce billet défend le laisse tranquille. Jetez le tunnel et gardez le diagnostic — c’est tout le montage, et le MTU est un endroit de plus où il gagne sa place.

La règle, resserrée à l’écho

Le billet précédent donnait le jeu de règles de transit complet : autoriser les erreurs sous une limitation de débit, jeter l’écho. Je ne vais pas le réimprimer. Le changement que ce billet défend est une seule paire de lignes, et c’est la paire qui fait le travail :

# permit the ICMP errors — these are load-bearing, keep them
ip protocol icmp icmp type { destination-unreachable, time-exceeded, parameter-problem } \
    limit rate 100/second accept
ip6 nexthdr ipv6-icmp icmpv6 type { destination-unreachable, packet-too-big, \
    time-exceeded, parameter-problem } limit rate 100/second accept

# drop the tunnel — echo, both directions, both families
ip  protocol icmp     icmp   type { echo-request, echo-reply } drop
ip6 nexthdr ipv6-icmp icmpv6 type { echo-request, echo-reply } drop

La règle n’est pas celle de Linux, cependant. C’est la même intention sur n’importe quel pare-feu digne du nom : autoriser les erreurs, plafonner leur débit, jeter l’écho dans les deux sens sur les deux familles. Alors la voici dans les dialectes que vous tenez plus probablement.

pf des BSD (pfSense, OPNsense, OpenBSD, FreeBSD) :

# permit the errors; block echo both directions, both families
pass  in proto icmp  icmp-type  { unreach, timex, paramprob }
pass  in proto icmp6 icmp6-type { unreach, toobig, timex, paramprob, \
                                  routersol, routeradv, neighbrsol, neighbradv }
block in proto icmp  icmp-type  { echoreq, echorep }
block in proto icmp6 icmp6-type { echoreq, echorep }

Gardez les types de découverte de voisins sur la ligne icmp6 ; ce sont ceux qui font tomber le segment si vous les perdez.

Cisco IOS (ACL étendues, appliquées au bord) :

ip access-list extended ICMP-EDGE
 permit icmp any any unreachable
 permit icmp any any time-exceeded
 permit icmp any any parameter-problem
 deny   icmp any any echo
 deny   icmp any any echo-reply
!
ipv6 access-list ICMP6-EDGE
 permit icmp any any packet-too-big
 permit icmp any any unreachable
 permit icmp any any time-exceeded
 permit icmp any any parameter-problem
 permit icmp any any nd-ns
 permit icmp any any nd-na
 deny   icmp any any echo-request
 deny   icmp any any echo-reply

En IPv4, le type d’écho est echo ; en IPv6, c’est echo-request. La limitation de débit vit dans le CoPP, pas dans l’ACL.

Juniper Junos (filtre de pare-feu ; le filtre inet6 reflète ça et garde la découverte de voisins) :

firewall family inet filter icmp-edge {
    term errors {
        from { protocol icmp; icmp-type [ unreachable time-exceeded parameter-problem ]; }
        then { policer icmp-cap; accept; }
    }
    term drop-echo {
        from { protocol icmp; icmp-type [ echo-request echo-reply ]; }
        then discard;
    }
}

MikroTik RouterOS — jetez les deux types d’écho, puis acceptez le reste de l’ICMP, ce qui garde les erreurs et, en v6, la découverte de voisins :

/ip firewall filter
add chain=forward protocol=icmp icmp-options=8:0 action=drop comment="echo request"
add chain=forward protocol=icmp icmp-options=0:0 action=drop comment="echo reply"
add chain=forward protocol=icmp action=accept comment="keep the errors (add limit= to cap)"
/ipv6 firewall filter
add chain=forward protocol=icmpv6 icmp-options=128:0 action=drop comment="echo request"
add chain=forward protocol=icmpv6 icmp-options=129:0 action=drop comment="echo reply"
add chain=forward protocol=icmpv6 action=accept comment="keep errors and ND"

Syntaxe différente, une seule règle. Autorisez les messages qui portent la vérité, jetez celui qui porte vos octets.

Deux mises en garde, dont les deux vous mordront si vous survolez.

Jetez l’écho à la frontière, pas sur le fil entre un hôte et son propre routeur. En IPv6, la découverte de voisins est de l’ICMP — types 133 à 137, de nd-router-solicit à nd-redirect — et c’est la façon dont le segment fait le travail qu’ARP fait en IPv4. Portez un rejet d’écho sur une chaîne lien-local ou hôte sans garder ceux-là et le segment cesse de marcher en quelques minutes, et ça n’aura pas l’air d’un défaut de pare-feu. Filtrez l’écho là où le trafic quitte votre réseau, et laissez les chaînes internes tranquilles.

Jetez-le dans les deux sens et il cesse d’être utile dans un sens comme dans l’autre. Bloquer seulement la requête empêche vos hôtes d’être pingués mais laisse encore une réponse sortir, et un tunnel peut être bâti sur les seules réponses avec un peu plus d’effort. Jetez requête et réponse, en IPv4 et IPv6, et le canal est fermé des deux côtés.

Ce que jeter le ping vous coûte vraiment

Soyez honnête sur la perte, parce qu’un contrôle que vous avez survendu est un contrôle que quelqu’un annule discrètement la première fois qu’il gêne.

Vous perdez ping à travers la frontière. Voilà le coût, énoncé en entier. C’est un vrai coût. ping est le réflexe, il est dans les doigts de tout le monde, et le lendemain de la mise en service quelqu’un dira que l’internet est en panne parce que son ping vers la passerelle expire. Il n’est pas en panne. Vous avez dit à la passerelle d’arrêter de répondre à une requête qui n’a jamais été qu’une commodité.

Le test de joignabilité n’a pas besoin de l’écho. Une connexion TCP vers un port que vous savez ouvert vous dit que l’hôte est levé et que le chemin marche, et ça vous en dit plus qu’un ping parce que ça prouve qu’un service a répondu, pas juste un noyau :

# "is it up and reachable?" without sending a single echo
nc -zv <host> 443           # did the TCP handshake complete?
traceroute -T -p 443 <host> # walk the path on TCP, read the errors back

Les deux vont à cheval sur les erreurs ICMP que vous avez gardées et le TCP qu’un vrai service parle déjà. Aucun n’envoie d’écho. L’échange honnête est donc celui-ci : vous abandonnez l’outil le moins instructif de la boîte, celui qui répond « un noyau veut-il répondre » et rien de plus, et en échange vous fermez la seule partie de l’ICMP qu’un attaquant peut transformer en route hors de votre réseau. Le diagnostic vers lequel vous vous tournez vraiment un mauvais jour est tout de l’autre côté de la ligne, intact.

Le Fisher-Price OS (Windows) n’est pas une échappatoire à ça

Si vous exploitez une boutique Fisher-Price OS (Windows), vous lisez peut-être ceci comme le problème de quelqu’un d’autre. Ce n’en est pas un. Le risque est dans le protocole, pas dans le système d’exploitation. La norme oblige chaque hôte qui répond à un ping à vous rendre vos octets, et elle ne s’arrête pas pour demander d’abord ce que fait tourner l’hôte.

Une boîte sous ce système fait un très bon bout de tunnel. Hans livre un client Windows, donc une machine interne peut être le bout client et faire sortir son trafic dans l’écho comme n’importe quelle autre. Ce qu’elle ne peut pas facilement être, c’est le bout serveur, parce que ça veut un équipement tun et un socket ICMP brut tenus ouverts, et sur ce système seuls les membres du groupe Administrateurs peuvent créer des sockets de type SOCK_RAW12 — les mots de Microsoft. Donc le serveur siège sur un vrai système d’exploitation, comme le mien, et la boîte derrière le pare-feu qui pingue discrètement sa sortie est celle qu’on vous a dit être sûre parce qu’elle était à l’intérieur.

C’est le point pour quiconque le défend. La règle de frontière se moque de ce que fait tourner l’intérieur. Jetez l’écho là où le trafic quitte le réseau et chaque hôte derrière est couvert — ceux que vous administrez, et celui dont on vous a assuré qu’il se cachait derrière le NAT. Je ne fais pas tourner la chose, et je ne vais pas faire croire que le tunnel la respecte. Le correctif est la même règle, au même endroit, quel que soit le système sur lequel le point terminal a démarré.

Si c’est la boîte elle-même que vous durcissez (la boîte, pas le réseau), PowerShell l’empêchera au moins de répondre à un ping ou d’en émettre pour son propre compte :

foreach ($t in 8,0)     { New-NetFirewallRule -DisplayName "Drop ICMPv4 Echo $t" -Protocol ICMPv4 -IcmpType $t -Direction Inbound -Action Block }
foreach ($t in 128,129) { New-NetFirewallRule -DisplayName "Drop ICMPv6 Echo $t" -Protocol ICMPv6 -IcmpType $t -Direction Inbound -Action Block }

Ajoutez des jumelles -Direction Outbound pour l’empêcher de tendre la main en client de tunnel plutôt que de rester là en cible, et laissez tranquilles les types d’erreur et de découverte de voisins ICMPv6, pour la raison qu’on les laisse tranquilles partout ailleurs sur cette page. Mais ne le prenez pas pour le contrôle. Un pare-feu d’hôte n’est pas un pare-feu de transit. Il durcit la boîte et ne change rien à la frontière, et la frontière est là où ça se ferme vraiment.

Posez la question à qui exploite votre frontière, aujourd’hui

Vous avez sans doute lu jusqu’ici en pensant à un réseau qui n’est pas le vôtre à changer. La plupart des gens le sont. Alors la chose utile à faire n’est pas de vous tourner vers le pare-feu, c’est de poser la question à qui le tient — votre propre équipe réseau, votre prestataire, ou l’éditeur dont la boîte siège au bord — et de la poser aujourd’hui, parce que c’est une porte ouverte depuis 1996 et une semaine de plus est un choix.

Demandez franchement, et demandez en vous attendant à un regard vide, parce que je parierais tout le PIB du Royaume-Uni que personne là-bas n’a accordé à l’écho une seconde pensée. C’est le seul paquet que tout le monde autorise et que personne ne possède, et une règle sans propriétaire est justement celle qui est restée fausse pendant des années sans personne pour le remarquer.

Demandez-leur quatre choses, dans ces mots, pour qu’il n’y ait pas de place pour opiner du chef sans rien changer :

  • Jetons-nous la requête et la réponse d’écho ICMP à la frontière, en IPv4 et IPv6 à la fois ? Pas « autorisons-nous l’ICMP » — le message précis, les deux sens, les deux familles. Si la réponse est une seule famille, ce n’est pas une réponse, parce que le tunnel se déplace juste vers l’autre.
  • Autorisons-nous encore les erreurs ICMP ? Destination Unreachable, Time Exceeded, Parameter Problem, et Packet Too Big en IPv6, sous une limitation de débit plutôt qu’un blocage. S’ils ne savent pas dire, il y a une chance sur deux qu’ils aient soit tout laissé ouvert soit tout bloqué, et les deux sont faux.
  • Avons-nous gardé la découverte de voisins IPv6 sur les chaînes internes ? Types 133 à 137. C’est celle qui transforme « on a durci l’ICMP » en un segment qui meurt en silence une semaine plus tard, et c’est l’erreur qu’un changement précipité commet.
  • Montrez-moi la règle. Pas une déclaration de politique, la vraie ligne sur la vraie boîte, et la date à laquelle elle a été posée. Un contrôle que personne ne peut pointer du doigt est un contrôle qui n’est pas là.

La boîte sera-t-elle arrivée de l’éditeur avec ça déjà fermé ? Elle ne le sera pas. Le défaut presque partout est de laisser passer l’écho et de le noter comme un compteur de paquets, ce qui est toute la raison pour laquelle le tunnel marche. Il n’exploite pas un bug, il utilise la configuration livrée en standard. Personne ne ferme une porte dont on ne lui a jamais dit qu’elle était ouverte.

La raison d’insister sur la formulation, c’est que le correctif paresseux à ce billet est « d’accord, on bloquera l’ICMP », et ce correctif est pire que le trou. Il casse la découverte du MTU de chemin et traceroute, il vous fera poursuivre des transferts bloqués sans rien dans les journaux, et en IPv6 il retire des parties de l’internet de la table entièrement. Si la personne à qui vous demandez se tourne vers ça, arrêtez-la. L’instruction est étroite exprès : jeter l’écho, garder les erreurs, garder la découverte de voisins. À ce titre, quiconque exploite des pare-feu pour vivre devrait pouvoir faire ce changement en un après-midi et vous dire que c’est fait.

Et s’ils sont un prestataire que vous payez pour exploiter ça : un fournisseur qui ne sait pas vous dire votre propre posture écho-et-erreurs de tête, ou qui répond « on bloque l’ICMP » comme si c’était l’option sûre, vient de vous dire quelque chose sur le reste du parc. La prochaine fois qu’un défaut siégera entre deux réseaux qui disent tous deux être propres, rappelez-vous lequel des deux ne savait pas décrire ce que son propre pare-feu fait à un ping.

Le diagnostic qui n’en fut jamais un

Ping a fait un bon parcours. Mike Muuss l’a écrit en 1983 pour vérifier si un hôte répondait, l’a nommé d’après le sonar, et il a si bien fait ce seul travail qu’il est devenu la première chose vers laquelle tout le monde se tourne et la dernière que quiconque questionne. Quarante ans après, c’est de la mémoire musculaire, et la mémoire musculaire est justement la façon dont un risque survit. Personne ne réexamine la chose qu’il a toujours faite.

Mais regardez ce que c’est vraiment, dépouillé de l’habitude. Un message qui ne porte aucun fait sur le réseau, auquel chaque hôte est obligé de répondre avec vos propres octets rendus, qui tourne sur un protocole que les contrôles n’inspectent pas et que les journaux ne lisent pas. Tout ce qui le fait paraître inoffensif — ce n’est qu’un diagnostic, ce n’est qu’un keepalive, tout le monde l’autorise — est la même chose qui en fait la voie la plus propre hors d’un réseau filtré qui existe. L’industrie bloque les erreurs ICMP, qui portent la vérité et cassent le réseau quand elles disparaissent, et laisse passer l’écho, qui porte tout ce que vous y chargez. Elle a le protocole exactement à l’envers.

Le correctif n’est pas malin. Gardez les messages qui vous disent la vérité, et jetez celui qui rend juste vos octets. Ça vous coûte une commande à laquelle vous n’aviez de toute façon aucune raison de faire confiance pour un diagnostic, et ça ferme une porte qui est restée ouverte depuis 1996, documentée dans un fanzine de hackers, empaquetée depuis deux décennies, et laissée grande ouverte parce que la fermer voudrait dire que quelqu’un ne pourrait pas ping. Sachez ce qu’une chose coûte, pas ce qu’elle est affichée. Ping est affiché à rien. Il vous coûte le seul canal que vous ne pouvez pas voir.


  1. Loki, Phrack 49 — a command channel carried inside ICMP echo payloads, 1996. ↩︎

  2. Ptunnel — carries a TCP session inside ICMP echo. ↩︎

  3. RFC 792 — ICMP : « the data received in the echo message must be returned in the echo reply message ». ↩︎

  4. RFC 4443 — ICMPv6 : echo data « MUST be returned entirely and unmodified in the ICMPv6 Echo Reply message ». ↩︎

  5. RFC 4890 — the ICMPv6 messages a firewall must not drop. ↩︎

  6. RFC 1812 — router requirements : Time Exceeded is a MUST, named for traceroute. ↩︎

  7. Hans — IP over ICMP echo tunnel, by Friedrich Schöller. ↩︎

  8. icmptunnel — tunnels IP traffic through ICMP echo and reply packets, by Dhaval Kapil. ↩︎

  9. noyau Linux — IP sysctl — icmp_echo_ignore_all : « the kernel will ignore all ICMP ECHO requests sent to it ». ↩︎

  10. Linux commit e6f86b0f — « ipv6: Add icmp_echo_ignore_all support for ICMPv6 » — the IPv6 equivalent, by Virgile Jarry. ↩︎

  11. RFC 8200 §5 — IPv6 requires every link to have an MTU of at least 1280 octets. ↩︎

  12. Microsoft — TCP/IP raw sockets — « only members of the Administrators group can create sockets of type SOCK_RAW ». ↩︎