- El NAT IPv4 normalmente distingue el destino de las respuestas usando puertos TCP/UDP, pero el echo de ICMP de
pingno tiene puertos, así que la clave es qué valor usa Linux como clave de mapeo - El experimento recrea una configuración con NAT de 192.168.99.0/24 hacia 10.0.100.0/24 usando namespaces de red para crear
client1,client2,natboxyserver, junto coniptablesMASQUERADE - Al comparar la RFC 792 con capturas de paquetes, se ve que el Identifier y el Sequence Number de ICMP echo se usan para emparejar solicitudes y respuestas, y en la ruta ICMP
SOCK_DGRAMde Linux el puerto local del socket se coloca como ID - Si dos clientes usan el mismo ICMP ID
999, netfilter cambia el ICMP ID a un valor aleatorio en uno de ellos para evitar el conflicto, y en la respuesta lo restaura a la IP e ID originales del cliente - El NAT de Linux, incluso para ICMP sin puertos, guarda el estado de dirección original y de respuesta en un tuple de conntrack, y usa el ICMP ID como clave manipulable para mapear correctamente la respuesta al host interno adecuado
Entorno de pruebas y configuración de NAT
- Se usan namespaces de red para simular varios dispositivos en una sola máquina Linux
- Se crean dos clientes,
natboxcomo router NAT y un servidor, cada uno en su propio namespace, separando la red privada de la red del lado del servidorclient1:192.168.99.1/24client2:192.168.99.2/24- Interfaz interna de
natbox:192.168.99.3/24 - Interfaz externa de
natbox:10.0.100.1/24 server:10.0.100.2/24
- Todo se ejecuta como root en una VM Fedora 38 Server con Linux kernel 6.2.9, usando
ip,iptables,tcpdumpy otras herramientas - Ambos clientes se conectan al bridge
br0, ynatboxse conecta por separado al bridge y alveth pairdel lado del servidor - La ruta por defecto de los clientes se configura hacia
192.168.99.3para que el tráfico al servidor pase pornatbox - En
natbox, el reenvío de paquetes se activa connet.ipv4.ip_forward=1, y se agrega una regla MASQUERADE a la cadenaPOSTROUTINGde la tablanatdeiptablesip netns exec natbox iptables -t nat -A POSTROUTING -o eth1 -j MASQUERADE
NAT de ICMP visto con captura de paquetes
- Se capturan paquetes ICMP con
tcpdump -n icmpen los namespacesclient1yserver - Del lado del cliente se ven un echo request
192.168.99.1 > 10.0.100.2y un echo reply10.0.100.2 > 192.168.99.1 - Del lado del servidor, la misma solicitud aparece con la IP de origen cambiada a
10.0.100.1, confirmando que NAT reescribió la dirección de origen con la IP externa denatbox - Las solicitudes ICMP de distintos clientes tienen distintos campos id
- En el ejemplo,
client1usa ID31428 client2usa ID33391
- En el ejemplo,
- Esta observación muestra que
natboxpuede usar el campo ID para devolver las respuestas ICMP al cliente interno correcto
RFC 792 y el ICMP ID de ping
- ICMP es un protocolo antiguo definido en RFC 792, publicada en 1981
- Los mensajes ICMP echo y echo reply incluyen Type, Code, Checksum, Identifier, Sequence Number y Data
- Type distingue entre echo request y echo reply
- El Type del echo request es
8 - En la cita de la RFC, el echo reply aparece como
1 - Code es
0
- El Type del echo request es
- La RFC 792 explica que Identifier y Sequence Number pueden usarse para emparejar echo request y reply
- El Identifier puede usarse para identificar la sesión, como un puerto en TCP/UDP, y el Sequence Number puede incrementarse con cada echo request
- Como la RFC no define cómo debe elegirse el ID en la práctica, hace falta revisar el código fuente de
ping
Cómo se define el ID en iputils ping
- El comando
pingviene incluido en el paquete iputils - Un comentario cerca de
ping4_send_probeexplica que al crear un ICMP echo request, el campo ID es un número aleatorio y el Sequence Number es un entero creciente - Dentro de
pingexiste el campoidentenstruct ping_rts- Su valor por defecto es
-1 - Puede sobrescribirse con la opción CLI
-eusando un valor entre0yIDENTIFIER_MAX, que es0xFFFF
- Su valor por defecto es
- Si
rts->ident == -1,pinghace bind del socket con tipoSOCK_DGRAMy protocoloIPPROTO_ICMP - Según la descripción de los sockets
IPPROTO_ICMPen Linux, el encabezado ICMP se revisa y ajusta al hacersend(), y el id se establece con el número de puerto local del socket - Si
pingno especifica un puerto de origen, el kernel de Linux elige aleatoriamente un puerto libre, y ese puerto pasa a usarse como el ID del paquete ICMP
Qué pasa cuando choca el mismo ICMP ID
- En ambos clientes se usa
ping -e 999para enviar ping al servidor con el mismo ICMP ID 999 - En la captura del servidor, la solicitud de un cliente conserva el ID
999, pero la del otro aparece con el ID30218 - El dispositivo NAT cambia uno de los IDs para evitar que se repita la misma combinación de IP externa e ICMP ID
- Para encontrar dónde se maneja ese conflicto, se revisa el código en el directorio
net/netfilterde Linux que usa el campo ICMPid
El papel de netfilter, conntrack y NAT
- El subsistema del kernel que implementa las reglas de
iptableses netfilter - Como la regla MASQUERADE es la que realiza el NAT, la implementación del NAT para ICMP también está dentro de netfilter
nf_nat_core.cdenf_nat_setup_infollama aget_unique_tuple, que a su vez termina ennf_nat_l4proto_unique_tuplenf_nat_l4proto_unique_tupletiene un caso paraIPPROTO_ICMPy referenciatuple->src.u.icmp.idnf_nat_proto.cdenf_nat_manip_pktpasa pornf_nat_ipv4_manip_pktyl4proto_manip_pkt, y cuando es ICMP llama aicmp_manip_pkticmp_manip_pktescribe el ID ICMP real en el paquete conhdr->un.echo.id = tuple->src.u.icmp.id
Cómo se representa ICMP en los tuples de conntrack
- En netfilter, una connection no significa solo una conexión TCP; también representa el estado que vincula paquetes salientes y entrantes en protocolos sin conexión como UDP o ICMP
nf_conncontienetuplehash[IP_CT_DIR_MAX]IP_CT_DIR_ORIGINAL: dirección del paquete salienteIP_CT_DIR_REPLY: dirección de la respuesta entrante
- Cada
nf_conntrack_tuple_hashcontiene unnf_conntrack_tupleque identifica la connection - El tuple se divide entre
src, que puede manipularse, ydst, que es inmutablesrccontiene la dirección IP y campos específicos del protocolo- En ICMP, el campo específico del protocolo es
__be16 id dstcontiene la dirección IP sin cambios y eltypeycodede ICMP
- NAT guarda en la connection cómo modificó el paquete saliente, y luego deshace ese cambio cuando llega el paquete de respuesta
Ruta de código para elegir el ICMP ID
- Cuando
natboxrecibe un ICMP echo,nf_nat_setup_infocrea una nueva connection y decide si debe cambiar la IP de origen y el ICMP ID - Después, para cada paquete ICMP,
nf_nat_manip_pktajusta la IP de origen o destino y el ICMP ID según los valores guardados en la connection get_unique_tuplees la ruta principal para elegir un tuple NAT disponiblefind_best_ips_protoreescribe la dirección IP de origennf_nat_used_tuplecomprueba si el tuple ya está en uso y, si no lo está, devuelve el tuple actual sin cambios- Por eso, si los ICMP ID de dos clientes son distintos, el ID también se conserva en los paquetes ya NATeados
- Si el tuple ya está en uso, se llama a
nf_nat_l4proto_unique_tuplepara realizar NAT específico del protocolo
- En ICMP,
tuple->src.u.icmp.idse elige como la clave objetivo de NAT find_free_idgenera un ID aleatorio conget_random_u16(), lo ajusta al rango válido de IDs ICMP y comprueba si ya está en uso- El rango de IDs por defecto cubre todo el espacio de IDs, aunque en una regla MASQUERADE de
iptablespuede especificarse un rango como100-200usando--to-ports - Si no se encuentra un tuple libre, la connection queda con un ID duplicado y después
__nf_conntrack_confirmdetecta la duplicación y descarta el paquete
Verificando el comportamiento del kernel con bpftrace
- Para confirmar el comportamiento de netfilter entendido, se usa
bpftrace - Las funciones del kernel rastreadas son
nf_nat_setup_infoynf_nat_manip_pkt kproberastrea el momento en que se llama a una función, ykretproberastrea el momento en que la función retorna- En
kretprobeno puede accederse directamente a los argumentos de la función, así que en la entrada se guardan en un mapa BPF y en el retorno se leen otra vez struct sk_buffes la estructura con la que el kernel de Linux representa un paquetebswapse usa para convertir del orden de bytes de red, big endian, a little endianntopconvierte una dirección IP en texto- Gracias al BPF Type Format (BTF) de los kernels Linux modernos, el programa BPF puede referenciar estructuras del kernel como
sk_buffynf_connsin incluir headers - Este programa de
bpftracese probó en Linux kernel 6.2.9, y su funcionamiento puede variar en otras versiones del kernel
Resultados del rastreo y conclusión
- Cuando dos clientes envían ping con el mismo ICMP ID
999,nf_nat_setup_infose llama una vez por cada cliente - El primer cliente,
192.168.99.1, conserva el ICMP ID999tanto en el original tuple como en el reply tuple - En el segundo cliente,
192.168.99.2, el ICMP ID del reply tuple se reescribe como32809 nf_nat_manip_pktcambia la IP de origen a10.0.100.1conNF_NAT_MANIP_SRCen el echo request, y en la respuesta restaura la IP de destino a la IP original del cliente conNF_NAT_MANIP_DST- El ICMP ID del paquete de respuesta también se restaura desde el valor NATeado al valor original que había enviado el cliente
- El timeout por defecto del conntrack ICMP puede verse en
/proc/sys/net/netfilter/nf_conntrack_icmp_timeout, y el valor predeterminado observado es 30 segundos - Si el cliente no envía paquetes durante más de 30 segundos, en el siguiente ping
nf_nat_setup_infovuelve a llamarse - El comportamiento del NAT de Linux para
pingtambién está documentado en Netfilter Hacking HOWTO, y la idea central está en los tuples de conntrack y la reescritura del ICMP ID
1 comentarios
Opiniones en Hacker News
Cuando el servidor arranca, empieza a enviar paquetes fijos de solicitud de eco ICMP a la dirección fija 3.3.3.3, esperando que esos paquetes no regresen
3.3.3.3 no es un host accesible ni un objetivo para spoofing. En cambio, cuando el cliente intenta conectarse, conoce la IP del servidor, así que le envía al servidor un paquete ICMP Time Exceeded. Dentro de ese paquete ICMP está el paquete fijo “original” que el servidor enviaba a 3.3.3.3, y este paquete hardcodeado funciona como identificador de pwnat
El cliente básicamente se hace pasar por un salto en Internet y le avisa al servidor que su “ICMP echo request” original no pudo reenviarse. El NAT ve que el paquete dentro del ICMP Time Exceeded coincide con un paquete enviado por el servidor y lo entrega al servidor detrás del NAT; como ahí se incluye todo el encabezado IP del cliente, el servidor puede conocer la dirección IP del cliente
El funcionamiento central de esta herramienta, después de eso, es crear un túnel UDP entre el cliente y el servidor
Sin embargo, por lo que vi al echarle un vistazo, parece asumir que el NAT no reescribe el puerto de origen UDP, así que no creo que funcione en todos los routers. STUN, usado en WebRTC y otros, implementa técnicas más sofisticadas, pero cuando aun así no funciona, hay que usar TURN, que actúa como relay
Es muy probable que el mismo problema aplique al truco del ping a 3.3.3.3. Si el NAT reescribe el identificador del ping, como en el artículo, este truco se rompe
Cuando recibe la respuesta, el router usa ese valor de ID único para entregar la respuesta al dispositivo correcto de la red local
Se puede comprobar con una sola computadora y Wireshark/tcpdump
El artículo en sí es bueno y puede ser revelador para alguien sin ninguna comprensión previa de redes. Pero, en esencia, parece más una forma de armar un laboratorio de redes decente y meterse en el código fuente, en vez de razonarlo directamente
Aprecio muchísimo ejemplos prácticos que se puedan seguir a mano como este, y pienso probarlo yo mismo
Casi el único otro artículo sobre este tema que me resultó fácil de entender fue este de Tailscale. Tiene muchos “ejemplos desarrollados”, lo que deja claro cómo encaja todo
https://tailscale.com/blog/how-nat-traversal-works/
Casualmente, este fin de semana estuve peleándome con Netfilter para activar un proxy transparente en un router OpenWRT
Los materiales básicos de referencia al mirar Netfilter son https://wiki.nftables.org/wiki-nftables/index.php/Main_Page y https://www.netfilter.org/projects/nftables/manpage.html
Pero una solicitud de eco ICMP sí tiene un ID, que en la práctica cumple el mismo papel que un número de puerto de origen
Para hacer NAT correctamente con eco ICMP, hay que remapear el ID en ambos sentidos, igual que se remapea el puerto de origen en UDP
Esto es porque, si una máquina detrás de NAT recibe ping simultáneamente de dos hosts y ambos hosts usan por casualidad el mismo número de solicitud, aparece una ambigüedad
Otra posibilidad es no reescribir el identificador y mantener una lista de máquinas remotas asociadas a cada ID. Si el ID colisiona, la lista tendría más de una dirección IP remota, y cuando llegue una respuesta desde la máquina detrás del NAT, el NAT puede elegir una de la lista, enviarle la respuesta a esa máquina y luego eliminar la entrada
Si hubiera sido IPv6, habría tenido que cambiar todos los nodos de la red y actualizar también el DNS interno
En teoría, podría tener mi propio /48 portable, pero el nuevo ISP tendría que anunciarlo, y aunque mi ISP actual lo hiciera, no es algo común
Cuando se cortó la línea telefónica hace una semana, saqué un MiFi 5G, moví la conexión WAN hacia ahí y bastó con poner un masquerade simple en esa interfaz. No era ideal por la señal débil, pero funcionó
El problema es que, incluso con IPv6, todavía hay que operar dual-stack o usar la abstracción sucia de NAT. Para mí es más trabajo sin beneficio
En el trabajo pasa lo mismo. Tenemos vehículos que usan subredes internas 172.16/12, se conectan y enrutan entre sí, y se enlazan con el exterior mediante varias conexiones VPN. Como muchas veces quedan estacionados bajo tierra y casi no hay señal, la arquitectura consiste en esperar que al menos uno de varios métodos funcione
Al pasarse a IPv6, habría que volver a trasladar los /48. Además, estos vehículos reciben Internet en varios estadios deportivos, y muchos de ellos tienen dificultades incluso para desactivar MITM/443 o levantar el bloqueo de UDP. En un entorno donde llegas a las 10 a. m. del sábado y tiene que funcionar 2 horas después, no sirve
No veo cuál sería el beneficio de negocio de pasarse a doble pila y duplicar la carga de trabajo y el riesgo
https://en.wikipedia.org/wiki/Lindy_effect
https://stackoverflow.com/questions/31857419/how-to-send-a-m...
Lamentablemente, como el sistema operativo procesa ping, las apps en la IP del peer no pueden leer los mensajes
Me pregunto si no será hora de ofrecer hooks en espacio de usuario para algunos de estos servicios, de modo que sea posible un P2P real cuando ambos lados están detrás de NAT. Aunque sea algo como un stream de eventos de solo lectura. A esta altura, todas las barreras que lo impiden se sienten artificiales
Dicho eso, sí he visto estrategias de exfiltración de datos u otras formas de comunicación usando ping. Hoy en día la mayoría de las configuraciones predeterminadas de firewall descartan silenciosamente todo ICMP, incluido ping, así que para P2P me parece casi imposible
idparece ser, en la práctica, equivalente a(sport, dport), pero es de 16 bits, así que el espacio es mucho menor que uno de 32 bitsPero creo que el problema central del NAT hole punching es que, para crear la conexión, se necesita actividad en ambos extremos. Por eso siempre hace falta un servidor de coordinación que le avise a T que el nodo S quiere hablar con él
Aun así, da para pensar. Me pregunto si habría una forma de hacerlo con mensajes de enrutamiento ICMP, por ejemplo unreachable o TTL expired. Cuando haces traceroute a una IP, recibes paquetes de vuelta desde otras IP arbitrarias, y eso por lo general atraviesa NAT
Podríamos imaginar que el host T, que quiere recibir conexiones entrantes, elige una dirección IP “dummy” aleatoria, publica
(IP del router, IP dummy)como su identificador y envía paquetes periódicamente a esa IP dummy. El host S, que quiere hablar con T, podría enviar al router de T un ICMP TTL-expired relativo a esa dirección dummy, y tal vez el router lo vea y lo reenvíe a TClaro que depende de si la dirección IP dentro de los campos ICMP se filtra en el ingreso del mismo modo que las direcciones dentro del encabezado IP
Edición: ya apareció un comentario de nivel superior que apunta a una implementación de esta idea
[1] - https://github.com/yarrick/pingfs
En GitHub puedes anclarlo a una combinación de hash de commit, nombre de archivo y número de línea, pero si la base de código cambia mucho, deja de ser tan útil. En visores web de git menos usados, como git.blender.org, no funcionó bien
https://elixir.bootlin.com/linux/latest/source