Ping no es un diagnóstico. Es un túnel. El echo de ICMP (la petición que una máquina envía y la respuesta que recibe de vuelta) llevará cualquier byte que le pongas, en ambas direcciones, sobre un protocolo que la mayoría de los firewalls dejan pasar sin inspeccionar y que la mayoría de los sistemas de registro cuentan en vez de leer. Una red que «solo permite ping» ya tiene un VPN completo y cifrable hacia fuera, y cualquiera que pueda alcanzar esa red puede conducir uno.

Esto es viejo, y está documentado. Loki1 publicó la técnica en Phrack 49 en 1996. Ptunnel2 lleva sesiones TCP enteras dentro de ping desde hace veinte años y está a un paquete de distancia en cualquier caja Linux. Ningún zero-day. El protocolo comportándose exactamente como se especifica.

Esa es la causa: el estándar, no un fallo en el producto de nadie. Cada host de la tierra está obligado a tomar los datos de una petición de echo y devolvértelos tal cual, sin cambiar. Datos arbitrarios dentro, los mismos datos fuera: eso es todo lo que un túnel necesita, y está impuesto desde 1981.

El arreglo es un cambio estrecho. Descarta el echo de ICMP, petición y respuesta, en IPv4 e IPv6, en el borde, y conserva cualquier otro mensaje ICMP. Los errores — Time Exceeded, Packet Too Big, Destination Unreachable — son portantes; bloquéalos y rompes el descubrimiento de MTU del camino y traceroute para nada. Así que esto no es «bloquear ICMP». Es descartar el único mensaje que es un riesgo, y conservar los que llevan la verdad.

Qué permitió de verdad tu política de salida

Si gestionas una red donde la salida está filtrada (y si no, eso es otra conversación), entonces esta es la factura de una línea de ella. La política que dice «bloquea todo lo saliente, permite ICMP porque necesitamos hacer ping a cosas» no es una política de salida. Es un túnel completo con el papeleo archivado bajo diagnósticos. Cualquier cosa del interior que pueda enviar una petición de echo y leer la respuesta puede mover datos a cualquier lugar del exterior que responda una, al ritmo que aguante el enlace, y más allá de cada control de contenido que compraste. En los registros es alguien comprobando si internet funciona, y nada más. El malware ha enviado esto durante años por esa razón exacta. Silencioso, estándar, y normalmente ya permitido.

Por eso es un canal de primera elección para cualquiera que ya esté dentro y no debería. Es la ocupación, no el asalto. Alguien que busca una entrada y salida que dure evita el puerto que dispara una alerta. Levantan un túnel sobre echo y lo dejan corriendo durante meses. Un host que hace mucho ping es un host que nadie vigila. Red de dos vías hacia el patrimonio: comando dentro, datos fuera, sobre el único protocolo que nadie limita en tasa, alerta, ni lee. Y está cifrado, como lo cifra cualquier herramienta real, así que en el cable la carga son los bytes de aspecto aleatorio que un ping lleva de todos modos y la inspección de contenido no tiene nada que leer. El tamaño y la temporización todavía pueden delatarlo ante alguien que de verdad mire. Asomarse dentro del paquete no.

Y la máquina que lo conduce no tiene por qué pertenecer a nadie que trabaje ahí. Todo lo que hace falta es algo que pueda alcanzar tu red y poner un ping en internet, y alcanzar tu red es más fácil de lo que a nadie le gusta admitir. El WiFi llega más allá de las paredes, al pasillo, al aparcamiento, al piso de arriba. El puerto ethernet de la pared de la sala de reuniones, o de recepción, o del escritorio vacío junto a la ventana, está muy a menudo vivo y hablará con cualquier cosa que enchufes, sin ningún 802.1X preguntando quién eres. Una señal a tiro, o un enchufe que nadie bloqueó. Ese es el precio de entrada. Sin credencial, sin cuenta, sin invitación.

Imagina al visitante que sí invitaste. Está en la reunión, agradable, tomando notas, aportando. Su portátil no. En el momento en que alcanzó tu red, por el aire o por el puerto bajo la mesa, estuvo dentro de la pared, y si esa red puede emitir una petición de echo, tiene una salida. Nada se soltó en tu patrimonio. Ninguna cuenta, ningún privilegio sobre nada tuyo, nada para que tus agentes de endpoint atrapen, porque tus agentes no están en su máquina. La persona está al otro lado de la mesa. El tráfico sale por tu puerta principal, cifrado, indistinguible de un portátil que hace más ping del que debería.

Un visitante a tiro de señal, o en un puerto abierto, consigue un túnel hacia fuera a través de un firewall que solo ve pingsUn visitante a tiro, o en un puerto abierto, consigue una ruta hacia fueradentro de la pared — tu redel WiFi llegaal pasillo,aparcamiento,piso de arribaportátil del visitantesin credencial, sin cuentaun puerto de pared vivosin 802.1X preguntando quién eresfirewall de bordesolo ve pingsservidoren internetpetición de echo →← respuesta de echocualquier tráfico IP, cifrado, viajando dentro de los pings
El precio de entrada es acceso a la red — una señal WiFi a tiro, o un puerto de pared vivo sin 802.1X — y la salida es echo a través del borde. Para el firewall es un host que hace ping; dentro de esos pings hay cualquier tráfico IP, cifrado, con destino a un servidor de internet.

Las dos cosas a las que recurrirás no ayudan. NAT es una tabla de traducción, no un filtro: una petición de echo de un host interno abre un mapeo indexado por el id de ICMP, que NAT trata exactamente como trata a un puerto, y la respuesta vuelve por él como cualquier otro flujo. Ejecuté un cliente detrás de mi propio NAT doméstico y nunca notó que el NAT estaba ahí. Una VLAN de invitados aparte no es mejor. La segregación mantiene al visitante fuera de tus servidores, pero no hace nada por mantenerlo fuera de internet, e internet, alcanzado con un ping, es todo el requisito. Un control toca esto: ¿deja esa red salir el echo?

Esto no se inspecciona para quitarlo, y no se segrega para quitarlo. Cierras la puerta. Todo lo que viene después es por qué el estándar la deja abierta, tres formas de construir el túnel para que la afirmación se sostenga en más que mi palabra, y el único cambio que la cierra.

La parte de ICMP que no te debe nada útil

Empieza por lo que el estándar dice de verdad, porque todo el argumento descansa en una frase y la gente la descarta sin leerla.

Una petición de echo lleva un campo de datos. En IPv4, la RFC 7923 dice llanamente que «the data received in the echo message must be returned in the echo reply message». IPv6 apretó la redacción en vez de aflojarla: la RFC 44434 define el campo como «zero or more octets of arbitrary data» y luego exige que «MUST be returned entirely and unmodified in the ICMPv6 Echo Reply message».

Léelo como operador y es un keepalive. Léelo como alguien que mueve datos y es un regalo. El estándar obliga a cada host alcanzable a aceptar un bloque de bytes que tú eliges y a devolvértelos tal cual, sin cambiar, bajo demanda. Longitud arbitraria. Contenido arbitrario. Sin handshake, sin puerto, sin ninguna aplicación en el otro extremo que tenga que acordar nada. El kernel lo hace, antes de que ningún proceso de userland lo mire.

Y son ambas familias. Pasarse a IPv6 no arregla esto. Lo abre más. La RFC 792 decía que los datos «must be returned»; la RFC 4443 dice que «MUST be returned entirely and unmodified», que es la misma puerta con un cerrojo más fuerte sujetándola abierta. Como tal, el canal existe en el echo de ICMP y en el echo de ICMPv6 por igual, y una regla que lo cierra en una familia y no en la otra no ha cerrado nada. El túnel simplemente se mueve a la familia que dejaste abierta. Hagas lo que hagas con esto, hazlo en ip e ip6 juntos.

Nada más en el protocolo hace esto. Un Time Exceeded lleva la cabecera del paquete que murió y nada más. Un Destination Unreachable es un informe sobre algo que ya pasó. Esos mensajes te cuentan hechos sobre la red. El echo lleva lo que le pongas, en ambas direcciones, y se hace llamar diagnóstico.

Los errores son portantes. El echo no.

Esta es la distinción que la gente del «bloquea ICMP a secas» nunca hace, y es todo el asunto, así que la haré una vez y bien.

Bloquear los errores de ICMP rompe la red en silencio. Filtra Packet Too Big y matas el descubrimiento de MTU del camino: el handshake se completa, las transferencias pequeñas funcionan, y cualquier cosa que lleve un paquete de tamaño completo se cuelga para siempre sin nada en los registros. En IPv6 eso ni siquiera es cuestión de gusto. Los routers no fragmentan, así que la RFC 48905 lista Packet Too Big entre los mensajes que un firewall «must not drop» y advierte que sin él «parts of the Internet will become inaccessible». Filtra Time Exceeded y rompes traceroute, que la RFC 18126 nombra como la razón por la que el mensaje es obligatorio en primer lugar. Estos no son opcionales. Son la retroalimentación que hace que la red se autocorrija, y cubrí el coste de perderlos con detalle la última vez.

Ahora descarta el echo y ve a buscar qué se rompió. ping a través del límite deja de funcionar. Esa es la lista. La lista entera.

El descubrimiento de MTU del camino no se inmuta, porque corre sobre Packet Too Big, que es un error. Traceroute no se inmuta, porque traceroute -T y -U recorren TCP y UDP y leen los errores que vuelven. Ninguno de ellos envía un echo. El descubrimiento de vecinos en IPv6 no se inmuta, porque eso son los tipos 133 a 137, y esos los conservas o el segmento muere. El truco de la TTL invertida del post anterior funciona sobre cualquier respuesta, y un handshake TCP te da una. Todo lo que hacía funcionar los diagnósticos del post anterior sigue funcionando, porque ni una sola medición de él enviaba una petición de echo.

Así que las dos mitades del protocolo no podrían parecerse menos. Una mitad es la red contándote la verdad sobre sí misma, y la rompes bajo tu propio riesgo. La otra mitad es un servicio impuesto de devolución de carga que resulta que se llama diagnóstico, y lo más fuerte que nadie puede decir a favor de mantenerlo abierto es que ping es cómodo. Es cómodo. Es también la única parte de ICMP que un atacante puede conducir, y puedo enseñarte exactamente con qué la conducen.

Conserva los errores de ICMP, con límite de tasa; descarta solo el echo, en ambas direcciones y ambas familiasDos mitades de un protocolo, y solo una es un riesgoCONSERVARlimita la tasa, nunca bloqueesDestination Unreachablelleva por qué un paquete no se pudo entregarPacket Too Bigel descubrimiento de MTU del camino depende de élTime Exceededtraceroute depende de élParameter Probleminforma de una cabecera malformadaDescubrimiento de vecinos 133–137IPv6: el segmento local depende de élRómpelos y la red se rompe en silencio.DESCARTARambas direcciones y familiasEcho requesttype 8 (IPv4) · type 128 (IPv6)Echo replytype 0 (IPv4) · type 129 (IPv6)Nada necesita esto salvo el comandoping.Todo lo que un atacante conducea través de ICMP pasa por ellos.Un túnel dejado abierto en una familia no está cerrado.
Los errores de ICMP son portantes: el descubrimiento de MTU del camino, traceroute y el descubrimiento de vecinos de IPv6 dependen todos de ellos, y bloquearlos rompe la red en silencio. El echo es la única parte de la que nada depende salvo ping, y la única parte que un atacante puede conducir. Descarta eso, conserva el resto, en ambas familias.

Demostrándolo: un VPN hecho de ping

La herramienta es Hans7, escrita por Friedrich Schöller. Su propia descripción es una línea: «makes it possible to tunnel IPv4 through ICMP echo packets, so you could call it a ping tunnel.» Levanta una interfaz tun en cada extremo, les da direcciones, y mueve cada paquete entre ellas dentro del echo de ICMP. Para la red del medio es alguien haciendo ping a un servidor y el servidor respondiendo. Para mí es una ruta.

Un paquete IP entero viaja dentro del campo de datos de un solo pingUn paquete entero viaja dentro de un solo pingEl paquete que tu sesión SSH envíacabecera IPsrc / dstcabecera TCPport 22datos de aplicacióntus pulsaciones, tu archivoHans copia el paquete entero en el campo de datos de echoEl paquete que sale por el cablecabecera IPtú al servidorpetición de echo de ICMPtype 8 · id · seqcampo de datos de echoel paquete entero de arriba, sin cambiarPara el firewall de borde esto es una petición de echo, y el registro anota un ping.Dentro del campo de datos hay un paquete con destino a cualquier lugar que el otro extremo pueda alcanzar.El estándar exige que el otro extremo devuelva esos datos tal cual, así que la respuesta también es un paquete.
Cada paquete que el túnel lleva se copia en el campo de datos de una petición de echo de ICMP. El estándar obliga al otro extremo a devolver esos datos sin cambiar, así que la respuesta también lleva un paquete. El firewall cuenta un ping; la carga va a cualquier lugar que el otro extremo pueda enrutar.

Lo ejecuté a través de mi propia línea, un servidor en una dirección pública y un cliente detrás de mi NAT doméstico, y empujé tráfico real por él. No pings sintéticos con una bandera puesta. Un inicio de sesión SSH y una descarga de archivo, viajando dentro del echo.

Está a un paquete de distancia donde ejecuté el servidor, y se compila desde el código de Schöller en cualquier otro sitio. El servidor tiene que ser Linux y necesita root, porque abrir un dispositivo tun y un socket ICMP en bruto lo requieren ambos. La sintaxis es deliberadamente pequeña:

# 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>'

Una vez que ambos extremos están arriba hay una nueva interfaz en cada lado con una dirección en la red del túnel, y se comporta como cualquier otro enlace punto a punto:

Nada de esa sesión sabe que está dentro de ping. SSH abre una conexión TCP a 10.8.0.1, el kernel la enruta por tun0, Hans envuelve cada paquete como la carga de una petición de echo, y el otro extremo lo desenvuelve y se lo da a su propio tun0. La respuesta vuelve como la carga de una respuesta de echo. En lo que a SSH respecta, está hablando por un enlace corriente. En lo que al firewall respecta, nadie abrió nada. A un host le están haciendo ping.

Qué pinta tiene en el cable

Esta es la parte que termina el argumento, así que mira la vista del firewall en vez de la mía.

Lo que de verdad pasa, y lo que el firewall registra, no son la misma imagenUn túnel, dos imágenesLo que de verdad pasatu hosttun0 · 10.8.0.2el servidortun0 · 10.8.0.1a cualquier sitioque pueda alcanzarSSH, descargasenrutado fueracualquier tráfico IP, por el túnelcomo paquetes corrientesLo que el firewall de borde ve y registratu host198.51.100.9el servidor192.0.2.7petición de echorespuesta de echoicmp 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... contados como pings, contenido sin leerEl mismo cable. El firewall nunca ve los paquetes de dentro.
El túnel mueve tráfico real entre dos hosts y luego hacia fuera a cualquier lugar que el servidor pueda alcanzar. En el borde solo es petición de echo y respuesta de echo — el firewall registra pings y nunca ve los paquetes que llevan dentro.

Siéntate en la interfaz exterior con tcpdump y captura solo ICMP mientras corre la sesión SSH. Ningún TCP al puerto 22 cruza el límite. Lo que cruza es petición de echo y respuesta de echo, ida y vuelta, cada una más gorda que un ping real porque lleva una porción de un segmento TCP en su carga:

# 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'

Lee las dos cosas que importan en esa captura. Primero, las longitudes de carga: un ping normal envía 56 bytes y cada línea es del mismo tamaño, mientras que estas varían y son grandes, porque el tamaño de la cosa que mueves se filtra al tamaño del ping. Segundo, el ritmo: un ping de diagnóstico es uno por segundo, y esto es una inundación, porque está moviendo un archivo. Ninguno está oculto. Ambos están a la vista sobre un protocolo que nadie está mirando.

Y ese es todo el asunto. No que esto sea ingenioso, ni difícil de detectar una vez que miras. Casi nadie mira, porque la caja está configurada para permitir ICMP, los registros lo cuentan como pings, y la alerta se ajustó para los puertos que alguien se acordó de preocuparse. El tráfico sale con pinta de comprobación de salud y la comprobación de salud es una ruta a cualquier lugar que el servidor pueda alcanzar.

Un VPN de túnel completo en condiciones, construido a mano

Hans demuestra que el canal está ahí, pero te hace la interfaz y el direccionamiento y te devuelve una ruta, así que no te enseña del todo lo que has construido. Para ver que esto es un VPN en el sentido pleno — el tráfico de toda la máquina saliendo por ping, no un enlace entre dos hosts con nombre — móntalo a mano. icmptunnel8, de Dhaval Kapil, es el indicado para eso, y su propia descripción es una sola línea: «Transparently tunnel your IP traffic through ICMP echo and reply packets.» La misma idea, dispositivo tun y cargas de echo, pero pones la fontanería tú mismo y nada se esconde dentro de un binario.

En el servidor arrancas el túnel, levantas la interfaz, y luego haces lo que delata todo el juego: le dices al kernel que deje de responder pings él mismo, para que sus propias respuestas de echo no peleen con las que el túnel está enviando.

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

Lee la línea de icmp_echo_ignore_all otra vez, porque dice más de lo que parece. Ese mando es del propio kernel, y viene como una defensa: ponlo y, en palabras del kernel, «will ignore all ICMP ECHO requests sent to it»9, de modo que un operador puede quitar un host del radar de ping por completo. Está en ambas familias ahora. net.ipv4.icmp_echo_ignore_all desde hace años, y net.ipv6.icmp.echo_ignore_all añadido más tarde para igualar.10 El túnel acciona ese interruptor defensivo por la razón opuesta: con el kernel ya sin responder al echo él mismo, sus dos extremos son libres de usar el echo como transporte puro. Que es la señal. La gente que escribió la pila ya trata el echo como algo que un host puede razonablemente rechazar — la regla de este post toma esa misma decisión una vez, en el borde, por cada host detrás de él.

En el cliente levantas la interfaz y luego apuntas la ruta por defecto por ella. No un host alcanzado a través del túnel. Todo.

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

Los propios client.sh y server.sh del proyecto todavía recurren a ifconfig y route de net-tools. Los comandos ip de arriba son los equivalentes de iproute2 y hacen el mismo trabajo. Esa ruta al servidor es la línea que la gente olvida: mantenla fuera del túnel, o los paquetes de echo que llevan el túnel intentan viajar por el túnel, y nada sale. Todo lo demás ahora va por tun0, envuelto en echo, y alcanza el servidor. Si va más lejos es una elección de enrutamiento. Una sola regla de masquerade en el servidor lo pondría en internet público bajo la propia dirección del servidor, y deliberadamente no está aquí, porque el túnel es lo que se está mostrando y no lo necesita.

Eso es un VPN de túnel completo, construido de ping en un puñado de comandos. Cada paquete que el cliente envía — web, DNS, SSH, todo — se captura en tun0 y sale como una petición de echo al servidor, y las respuestas vuelven como respuestas de echo. Una máquina en una red que «solo permite ICMP» acaba de entregar todo su tráfico saliente a una caja de fuera, y el borde registró un host al que le gusta hacer ping.

Escribiendo el tuyo en Python

Aquí está la parte que debería inquietar a cualquiera que espere defenderse de esto detectando una herramienta. Ni Hans ni icmptunnel hacen nada que no pudieras escribir tú mismo en una tarde. Abre un dispositivo tun, envuelve cada paquete como la carga de una petición de echo, desenvuelve los que vuelven. Ese es todo el mecanismo, y el túnel pelado son unas sesenta líneas de Python de la librería estándar sin nada que instalar en absoluto. La versión de abajo añade una cosa encima: cifra la carga, y esa es la única parte que arrastra una dependencia.

#!/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()

Ese es el túnel entero. Abre tun0 y un socket ICMP en bruto y transporta paquetes entre ellos: lo que sale del host se envuelve como una petición de echo, o una respuesta de echo en el servidor, y se envía al otro extremo; lo que llega por ICMP se desenvuelve y se devuelve a la pila. El campo id está fijado a un valor para que pase por encima de los pings reales, y el servidor aprende a dónde responder por el origen del primer paquete que ve.

El cifrado es la parte en la que merece la pena detenerse, porque es lo que hacen las herramientas reales y es por qué no atraparás esto mirando dentro del paquete. La carga se sella con AES-128-GCM bajo una clave precompartida antes de envolverse, así que los bytes en los datos del echo son indistinguibles del relleno aleatorio que un ping real lleva. Quita las dos líneas de cripto y el túnel sigue corriendo sobre nada más que la librería estándar — la única razón por la que necesita pip install cryptography es el AES, y el AES es exactamente la parte que convierte un túnel legible en uno ilegible. Levántalo de la misma forma que antes, menos el masquerade. No lo necesitas para demostrar el punto:

# 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 razón para escribirlo entero no es la herramienta. Es que la herramienta es desechable. Un archivo corto, sin dependencias hasta que añades el cifrado, y cada copia que alguien teclea tiene una pinta un poco distinta en el cable. Así que una firma que atrapa esta no atrapa nada la semana que viene. No puedes bloquear tu salida de esto nombrando el software, porque no hay software que nombrar.

Ni siquiera necesita un shell. La lógica es bytes dentro, bytes fuera, así que se porta a cualquier cosa que pueda abrir un socket. Incluso WebAssembly: el sandbox del navegador normalmente le niega a una página un socket en bruto de plano, pero donde esa barrera se levanta (un navegador con el permiso concedido por el usuario, en una máquina donde el usuario corre con privilegio suficiente para abrir un socket en bruto) el mismo programa corto corre dentro de una pestaña. Que es la mitad incómoda de esto. No puedes confiar en tus usuarios aquí. El host que conduce el túnel está en el interior, en manos de alguien que decidiste que era seguro porque se sienta detrás del firewall, y el firewall es la cosa que se está tunelizando. La confianza de perímetro asume que la amenaza está fuera de la pared. Esta empieza dentro de ella, siempre.

Cuidado con el MTU, y el suelo de 1280 de IPv6

El túnel no es gratis en el cable. Cada paquete que llevas gana una cabecera IP externa y una cabecera ICMP antes de salir, así que la interfaz interna tiene que quedar por debajo de lo que el camino puede llevar. En IPv4 eso le cuesta al atacante casi nada. Pon el MTU interno bajo (el 1472 de icmptunnel es 1500 menos 20 por la cabecera IP externa y 8 por la cabecera ICMP, y el Python de arriba baja a 1400 por holgura), y donde el número todavía esté mal, IPv4 fragmenta el paquete sobredimensionado y lo reensambla en el otro extremo en vez de descartarlo. Entre el suelo bajo que puedes elegir y la fragmentación tapando el resto, el túnel corre por casi cualquier camino. Esa flexibilidad es exactamente lo que hace de IPv4 el sitio cómodo para hacer esto.

La fragmentación y el margen de MTU de IPv4 dejan al túnel correr en cualquier sitio; IPv6 no tiene ninguna, así que falla cerradoPor qué corre en cualquier sitio en IPv4 y falla cerrado en IPv6IPv4IP hdr20 BICMP8 Bpaquete internoMTU del tun 1472 B¿Demasiado grande? IPv4 fragmenta y reensambla — el túnel corre por casi cualquier camino.IPv6IPv6 hdr40 BICMPv68 Bpaquete internono puede bajar de 1280 Bsuelo duro de 1280 B (RFC 8200)¿Demasiado grande? Los routers IPv6 lo descartan, sin fragmentación — el túnel falla cerrado.Los recursos de IPv4 — un suelo que puedes elegir, fragmentación para el resto — son lo que hace fiable el túnel.IPv6 no tiene ninguno, así que el ataque que corre casi en cualquier sitio en IPv4 es frágil en IPv6. La familia más segura aquí.Packet Too Big — un error que conservas, no un echo — es lo que le deja a un emisor encontrar el tamaño que cabe. Sin escala.
El túnel pierde una cabecera IP externa y una ICMP de cada paquete. IPv4 te deja recortar el MTU interno y fragmenta lo que siga siendo demasiado grande, así que corre casi en cualquier sitio; IPv6 pone un suelo duro de 1280 bytes y sus routers no fragmentan, así que un paquete sobredimensionado se descarta y el túnel falla cerrado — lo que hace de IPv6 la familia más segura aquí.

IPv6 es menos indulgente, y por una vez eso está de tu lado. El suelo es duro: la RFC 820011 §5 exige que «every link in the Internet have an MTU of 1280 octets or greater», y los routers de IPv6 no fragmentan en tránsito. Eso quita ambas redes de IPv4 a la vez, y muerde al túnel en ambas direcciones. Ejecútalo sobre ICMPv6 y cada enlace del camino tiene que llevar 1280, así que un camino que baje de eso tira abajo el transporte, y no hay recortar por debajo del suelo como puedes en IPv4. Lleva IPv6 dentro del túnel y te topas con la misma pared por el otro lado: la interfaz interna tampoco puede bajar de 1280, mientras que el envoltorio ICMPv6 externo, 40 bytes de cabecera y 8 de ICMPv6, ya está gastando el presupuesto, así que el camino tiene que sobrar 1280 más la sobrecarga. Fállalo en cualquier lugar y el paquete se descarta, no se recorta para caber: el túnel se levanta, las cosas pequeñas funcionan, cualquier cosa de tamaño completo se cuelga. Así que IPv6 es la familia más segura aquí, no la más arriesgada. El ataque que corre por casi cualquier cosa en IPv4 es frágil en IPv6, y falla cerrado. Más seguro no es lo mismo que seguro, eso sí: el canal de echo está abierto en IPv6 también, así que la regla lo descarta igual en ambas familias — el atacante simplemente no puede apoyarse en IPv6 como se apoya en IPv4.

La recuperación de eso es el error de ICMP que tuviste cuidado de conservar. Packet Too Big es lo que le deja a un emisor encontrar el tamaño que funciona, y es un error, no un echo, así que la regla que este post defiende lo deja en paz. Descarta el túnel y conserva los diagnósticos — ese es todo el diseño, y el MTU es un sitio más donde se gana el sueldo.

La regla, estrechada al echo

El post anterior dio el conjunto de reglas de tránsito completo: permite los errores bajo un límite de tasa, descarta el echo. No voy a reimprimirlo. El cambio que este post defiende es un par de líneas, y es el par que hace el trabajo:

# 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 regla no es de Linux, eso sí. Es la misma intención en cualquier firewall que se precie: permite los errores, cierra su tasa, descarta el echo en ambos sentidos en ambas familias. Así que aquí está en los dialectos que es más probable que tengas en la mano.

BSD pf (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 }

Conserva los tipos de descubrimiento de vecinos en la línea icmp6; esos son los que tiran abajo el segmento si los pierdes.

Cisco IOS (ACL extendidas, aplicadas en el borde):

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 el tipo de echo es echo; en IPv6 es echo-request. El límite de tasa vive en CoPP, no en la ACL.

Juniper Junos (firewall filter; el filtro inet6 refleja esto y conserva el descubrimiento de vecinos):

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 — descarta los dos tipos de echo, luego acepta el resto de ICMP, lo que conserva los errores y, en v6, el descubrimiento de vecinos:

/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"

Sintaxis distinta, una regla. Permite los mensajes que llevan la verdad, descarta el que lleva tus bytes.

Dos precauciones, ambas de las cuales te morderán si lees por encima.

Descarta el echo en el borde, no en el cable entre un host y su propio router. En IPv6, el descubrimiento de vecinos es ICMP — tipos 133 a 137, de nd-router-solicit a nd-redirect — y es cómo el segmento hace el trabajo que ARP hace en IPv4. Lleva un descarte de echo a una cadena de enlace local o de host sin conservar esos y el segmento deja de funcionar en minutos, y no parecerá un fallo del firewall. Filtra el echo donde el tráfico sale de tu red, y deja las cadenas internas en paz.

Descártalo en ambas direcciones y deja de ser útil de ninguna manera. Bloquear solo la petición impide que se haga ping a tus hosts pero todavía deja salir una respuesta, y un túnel se puede construir solo con respuestas con un poco más de esfuerzo. Descarta petición y respuesta, en IPv4 e IPv6, y el canal queda cerrado en ambos sentidos.

Qué te cuesta de verdad descartar ping

Sé honesto sobre la pérdida, porque un control que has sobrevendido es un control que alguien revierte calladamente la primera vez que resulta inconveniente.

Pierdes ping a través del límite. Ese es el coste, dicho entero. Es un coste real. ping es el reflejo, está en los dedos de todos, y el día después de que envíes esto alguien dirá que internet está caído porque su ping a la puerta de enlace da timeout. No está caído. Le dijiste a la puerta de enlace que dejara de responder una petición que solo fue nunca una comodidad.

Probar la alcanzabilidad no necesita el echo. Una conexión TCP a un puerto que sabes que está abierto te dice que el host está arriba y el camino funciona, y te dice más que un ping porque prueba que un servicio respondió, no solo un kernel:

# "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

Ambos viajan sobre los errores de ICMP que conservaste y el TCP que un servicio real ya habla. Ninguno envía un echo. Así que el trato honesto es este: renuncias a la herramienta menos informativa de la caja, la que responde «¿está un kernel dispuesto a responder?» y nada más, y a cambio cierras la única parte de ICMP que un atacante puede convertir en una ruta fuera de tu red. Los diagnósticos a los que de verdad recurres en un mal día están todos al otro lado de la línea, intactos.

El Fisher-Price OS (Windows) no es una salida de esto

Si gestionas una tienda de Fisher-Price OS (Windows) quizá estés leyendo esto como el problema de otro. No lo es. El riesgo está en el protocolo, no en el sistema operativo. El estándar obliga a cada host que responde un ping a devolverte tus bytes, y no se detiene a preguntar qué corre el host primero.

Una caja con ese SO hace un extremo de túnel perfectamente bueno. Hans envía un cliente de Windows, así que una máquina interna puede ser el extremo cliente y pasar su tráfico fuera dentro del echo como cualquier otra. Lo que no puede ser fácilmente es el extremo servidor, porque eso quiere un dispositivo tun y un socket ICMP en bruto abiertos, y en ese SO solo los miembros del grupo Administradores pueden crear sockets de tipo SOCK_RAW12 — palabras de la propia Microsoft. Así que el servidor se sienta en un sistema operativo de verdad, como el mío, y la caja detrás del firewall que calladamente hace ping para salir es la que te dijeron que era segura porque estaba en el interior.

Ese es el punto para cualquiera que lo defienda. La regla del borde no le importa qué corre el interior. Descarta el echo donde el tráfico sale de la red y cada host detrás de él queda cubierto — los que administras, y el que te aseguraron que se escondía detrás de NAT. No ejecuto la cosa, y no voy a pretender que el túnel la respeta. El arreglo es la misma regla, en el mismo sitio, en lo que sea que el endpoint haya arrancado.

Si la caja en sí es lo que estás endureciendo (la caja, no la red), PowerShell al menos hará que deje de responder o emitir un ping por su cuenta:

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 }

Añade gemelas -Direction Outbound para impedir que salga como cliente de túnel en vez de quedarse ahí como el objetivo, y deja los tipos de error y de descubrimiento de vecinos de ICMPv6 en paz, por la razón por la que se dejan en paz en todo el resto de esta página. Pero no lo confundas con el control. Un firewall de host no es un firewall de tránsito. Endurece la caja y no cambia nada del borde, y el borde es donde esto de verdad se cierra.

Plantéalo hoy con quien gestione tu borde

Probablemente has leído hasta aquí pensando en una red que no es tuya para cambiar. La mayoría de la gente lo está. Así que lo útil que hacer no es recurrir al firewall, es ponerle la pregunta a quien lo tiene — tu propio equipo de red, tu MSP, o el fabricante cuya caja se sienta en el borde — y ponerla hoy, porque esto ha sido una puerta abierta desde 1996 y una semana más de ello es una elección.

Pregunta llanamente, y pregunta esperando una cara en blanco, porque me apostaría todo el PIB del Reino Unido a que nadie ahí le ha dado al echo un segundo pensamiento. Es el único paquete que todo el mundo permite y nadie posee, y una regla sin dueño es exactamente la que ha estado mal puesta durante años sin nadie que lo note.

Pregúntales cuatro cosas, con estas palabras, para que no haya sitio para asentir y no cambiar nada:

  • ¿Descartamos la petición de echo y la respuesta de echo de ICMP en el borde, en IPv4 e IPv6 los dos? No «¿permitimos ICMP?» — el mensaje específico, ambas direcciones, ambas familias. Si la respuesta es una sola familia, no es respuesta, porque el túnel simplemente se mueve a la otra.
  • ¿Seguimos permitiendo los errores de ICMP? Destination Unreachable, Time Exceeded, Parameter Problem, y Packet Too Big en IPv6, bajo un límite de tasa en vez de un bloqueo. Si no saben decirlo, hay tantas probabilidades de que hayan dejado todo abierto como de que hayan bloqueado el lote, y ambas están mal.
  • ¿Hemos conservado el descubrimiento de vecinos de IPv6 en las cadenas internas? Tipos 133 a 137. Este es el que convierte «endurecimos ICMP» en un segmento que muere calladamente una semana después, y es el error que comete un cambio con prisas.
  • Enséñame la regla. No una declaración de política, la línea real en la caja real, y la fecha en que se puso. Un control al que nadie puede señalar es un control que no está ahí.

¿Habrá llegado la caja del fabricante con esto ya cerrado? No habrá. El valor por defecto casi en todas partes es dejar pasar el echo y registrarlo como un conteo de paquetes, que es toda la razón por la que el túnel funciona. No está explotando un fallo, está usando la configuración que se envía de serie. Nadie cierra una puerta que nunca le dijeron que estaba abierta.

La razón para insistir en la redacción es que el arreglo perezoso a este post es «vale, bloquearemos ICMP», y ese arreglo es peor que el agujero. Rompe el descubrimiento de MTU del camino y traceroute, te tendrá persiguiendo transferencias colgadas sin nada en los registros, y en IPv6 quita partes de internet de la mesa por completo. Si la persona a la que preguntas recurre a eso, párala. La instrucción es estrecha a propósito: descarta el echo, conserva los errores, conserva el descubrimiento de vecinos. Como tal, cualquiera que gestione firewalls para ganarse la vida debería poder hacer ese cambio en una tarde y decirte que está hecho.

Y si son un MSP al que pagas para gestionar esto: un proveedor que no sabe decirte de memoria tu propia postura de echo-y-errores, o que responde «bloqueamos ICMP» como si eso fuera la opción segura, acaba de decirte algo sobre el resto del patrimonio. La próxima vez que un fallo se siente entre dos redes y ambas digan que están limpias, recuerda cuál de ellas no supo describir qué le hace su propio firewall a un ping.

El diagnóstico que nunca fue un diagnóstico

Ping ha tenido una buena racha. Mike Muuss lo escribió en 1983 para comprobar si un host respondía, lo nombró por el sonar, e hizo ese único trabajo tan bien que se convirtió en lo primero a lo que todos recurren y lo último que nadie cuestiona. Cuarenta años después es memoria muscular, y la memoria muscular es exactamente cómo sobrevive un riesgo. Nadie reexamina la cosa que siempre ha hecho.

Pero mira lo que de verdad es, despojado de la costumbre. Un mensaje que no lleva ningún hecho sobre la red, que cada host está obligado a responder con tus propios bytes devueltos, que corre sobre un protocolo que los controles no inspeccionan y los registros no leen. Todo lo que lo hace sentir inofensivo — solo es un diagnóstico, solo es un keepalive, todo el mundo lo permite — es lo mismo que lo hace la forma más limpia que existe de salir de una red filtrada. La industria bloquea los errores de ICMP, que llevan la verdad y rompen la red cuando faltan, y deja pasar el echo, que lleva lo que sea que le cargues. Tiene el protocolo exactamente del revés.

El arreglo no es ingenioso. Conserva los mensajes que te cuentan la verdad, y descarta el que solo devuelve tus bytes. Te cuesta un comando en el que no tenías por qué confiar para un diagnóstico de todos modos, y cierra una puerta que ha estado abierta desde 1996, documentada en una revista hacker, empaquetada durante dos décadas, y dejada de par en par porque cerrarla significaría que alguien no podría hacer ping. Sabe lo que cuesta una cosa, no lo que tiene de precio. Ping tiene precio de nada. Te cuesta el único canal que no puedes ver.


  1. Loki, Phrack 49 — un canal de comando llevado dentro de las cargas de echo de ICMP, 1996. ↩︎

  2. Ptunnel — lleva una sesión TCP dentro del echo de ICMP. ↩︎

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

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

  5. RFC 4890 — los mensajes de ICMPv6 que un firewall no debe descartar. ↩︎

  6. RFC 1812 — requisitos de routers: Time Exceeded es un MUST, nombrado por traceroute. ↩︎

  7. Hans — túnel de IP sobre echo de ICMP, de Friedrich Schöller. ↩︎

  8. icmptunnel — tuneliza tráfico IP a través de paquetes de echo y respuesta de ICMP, de Dhaval Kapil. ↩︎

  9. kernel de 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» — el equivalente de IPv6, de Virgile Jarry. ↩︎

  11. RFC 8200 §5 — IPv6 exige que cada enlace tenga un MTU de al menos 1280 octetos. ↩︎

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