2 puntos por GN⁺ 2023-09-11 | 1 comentarios | Compartir por WhatsApp
  • El NAT IPv4 normalmente distingue el destino de las respuestas usando puertos TCP/UDP, pero el echo de ICMP de ping no 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, natbox y server, junto con iptables MASQUERADE
  • 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_DGRAM de 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, natbox como router NAT y un servidor, cada uno en su propio namespace, separando la red privada de la red del lado del servidor
    • client1: 192.168.99.1/24
    • client2: 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, tcpdump y otras herramientas
  • Ambos clientes se conectan al bridge br0, y natbox se conecta por separado al bridge y al veth pair del lado del servidor
  • La ruta por defecto de los clientes se configura hacia 192.168.99.3 para que el tráfico al servidor pase por natbox
  • En natbox, el reenvío de paquetes se activa con net.ipv4.ip_forward=1, y se agrega una regla MASQUERADE a la cadena POSTROUTING de la tabla nat de iptables
    • ip 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 icmp en los namespaces client1 y server
  • Del lado del cliente se ven un echo request 192.168.99.1 > 10.0.100.2 y un echo reply 10.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 de natbox
  • Las solicitudes ICMP de distintos clientes tienen distintos campos id
    • En el ejemplo, client1 usa ID 31428
    • client2 usa ID 33391
  • Esta observación muestra que natbox puede 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
  • 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 ping viene incluido en el paquete iputils
  • Un comentario cerca de ping4_send_probe explica 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 ping existe el campo ident en struct ping_rts
    • Su valor por defecto es -1
    • Puede sobrescribirse con la opción CLI -e usando un valor entre 0 y IDENTIFIER_MAX, que es 0xFFFF
  • Si rts->ident == -1, ping hace bind del socket con tipo SOCK_DGRAM y protocolo IPPROTO_ICMP
  • Según la descripción de los sockets IPPROTO_ICMP en Linux, el encabezado ICMP se revisa y ajusta al hacer send(), y el id se establece con el número de puerto local del socket
  • Si ping no 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 999 para 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 ID 30218
  • 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/netfilter de Linux que usa el campo ICMP id

El papel de netfilter, conntrack y NAT

  • El subsistema del kernel que implementa las reglas de iptables es 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.c de nf_nat_setup_info llama a get_unique_tuple, que a su vez termina en nf_nat_l4proto_unique_tuple
  • nf_nat_l4proto_unique_tuple tiene un caso para IPPROTO_ICMP y referencia tuple->src.u.icmp.id
  • nf_nat_proto.c de nf_nat_manip_pkt pasa por nf_nat_ipv4_manip_pkt y l4proto_manip_pkt, y cuando es ICMP llama a icmp_manip_pkt
  • icmp_manip_pkt escribe el ID ICMP real en el paquete con hdr->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_conn contiene tuplehash[IP_CT_DIR_MAX]
    • IP_CT_DIR_ORIGINAL: dirección del paquete saliente
    • IP_CT_DIR_REPLY: dirección de la respuesta entrante
  • Cada nf_conntrack_tuple_hash contiene un nf_conntrack_tuple que identifica la connection
  • El tuple se divide entre src, que puede manipularse, y dst, que es inmutable
    • src contiene la dirección IP y campos específicos del protocolo
    • En ICMP, el campo específico del protocolo es __be16 id
    • dst contiene la dirección IP sin cambios y el type y code de 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 natbox recibe un ICMP echo, nf_nat_setup_info crea 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_pkt ajusta la IP de origen o destino y el ICMP ID según los valores guardados en la connection
  • get_unique_tuple es la ruta principal para elegir un tuple NAT disponible
    • find_best_ips_proto reescribe la dirección IP de origen
    • nf_nat_used_tuple comprueba 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_tuple para realizar NAT específico del protocolo
  • En ICMP, tuple->src.u.icmp.id se elige como la clave objetivo de NAT
  • find_free_id genera un ID aleatorio con get_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 iptables puede especificarse un rango como 100-200 usando --to-ports
  • Si no se encuentra un tuple libre, la connection queda con un ID duplicado y después __nf_conntrack_confirm detecta 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_info y nf_nat_manip_pkt
  • kprobe rastrea el momento en que se llama a una función, y kretprobe rastrea el momento en que la función retorna
  • En kretprobe no 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_buff es la estructura con la que el kernel de Linux representa un paquete
  • bswap se usa para convertir del orden de bytes de red, big endian, a little endian
  • ntop convierte 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_buff y nf_conn sin incluir headers
  • Este programa de bpftrace se 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_info se llama una vez por cada cliente
  • El primer cliente, 192.168.99.1, conserva el ICMP ID 999 tanto 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 como 32809
  • nf_nat_manip_pkt cambia la IP de origen a 10.0.100.1 con NF_NAT_MANIP_SRC en el echo request, y en la respuesta restaura la IP de destino a la IP original del cliente con NF_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_info vuelve a llamarse
  • El comportamiento del NAT de Linux para ping tambié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

 
GN⁺ 2023-09-11
Opiniones en Hacker News
  • Puede que te interese https://samy.pl/pwnat/
    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
    • En resumen, el truco de hacer ping a 3.3.3.3 es una forma de que un servidor detrás de NAT conozca la dirección IP de un cliente detrás de NAT, sin necesitar un servidor sin NAT como https://ifconfig.co
      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 un dispositivo de la red local hace ping a un dispositivo en Internet, el router que realiza NAT cambia la dirección de origen del ping por su propia IP pública y reescribe el campo ID del paquete ICMP con un valor único
    Cuando recibe la respuesta, el router usa ese valor de ID único para entregar la respuesta al dispositivo correcto de la red local
    • Para verlo mejor, piensa en cómo el sistema operativo distingue distintas conversaciones ICMP dirigidas al mismo destino
      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
    • Si llevas esta idea un poco más lejos, en la práctica estás convirtiendo un protocolo sin estado en uno con estado
    • De todos modos, ping también necesita esa información de estado para emparejar solicitudes y respuestas
    • Me pregunto por qué no se usa la IP privada de origen en lugar de un “valor único”
    • Me pregunto si ese ID está en el encabezado ICMP o si pertenece a la parte de IP
  • Me alegra ver que un artículo del tipo “cómo funciona” baja por las capas de abstracción hasta meterse en el código fuente. La explicación es buena y trae mucha información
    • Yo también venía a decir esto. El ruteo y las redes todavía me confunden, y la mayoría de los artículos al respecto suelen sentirse demasiado abstractos
      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/
  • Buen artículo
    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
  • Como ICMP no tiene puertos, NAT no necesita lidiar con el problema de devolver una respuesta de eco ICMP al puerto correcto
    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
  • NAT es una abstracción realmente sucia. IPv4 debería desaparecer
    • En mi Internet de casa tengo dispositivos en varias subredes del rango 192.168. Hace poco cambié de ISP, cambió el AS al que pertenece mi casa y recibí una nueva dirección IPv4, pero solo tuve que actualizar en el router WAN el reenvío del tráfico entrante hacia la nueva IP
      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

  • No está claro que IPv6 vaya a resolver este problema. Técnicamente sí, pero los principales proveedores ya están dando solo /64 a usuarios residenciales y cobrando mucho por /48 “empresariales”, lo que ya está llevando a NAT en IPv6 o a subdividir aún más los /64. Se supone que no debería hacerse así
  • Entonces CG-NAT les va a gustar todavía menos
  • Hay que tener presente el efecto Lindy. Es la observación de que la vida futura de algo que no se desgasta, como una tecnología o una idea, es proporcional a su edad actual; y como IPv4 ya es viejo, es muy probable que siga existiendo bastante tiempo más
    https://en.wikipedia.org/wiki/Lindy_effect
  • IPv6 también debería desaparecer. Tuvo tiempo de sobra para imponerse, pero siguió estancado
  • Me pregunto si se podría abusar de ping para enviar mensajes cortos en redes P2P basadas en UDP, manejando el cruce de NAT sin un servidor central. Parece que alguien ya resolvió la parte de los mensajes
    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
    • Una corrección técnica menor: ping no es UDP, sino ICMP
      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
    • Es una idea interesante. id parece 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 bits
      Pero 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 T
      Claro 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
    • Ya existe: https://samy.pl/pwnat/
    • No es exactamente lo que buscabas, pero eso de abusar de ping me recordó a pingfs. Le da una definición completamente nueva a la computación en la nube
      [1] - https://github.com/yarrick/pingfs
    • A medida que aumente la adopción de IPv6, todos tendrán una IP enrutable públicamente y podrán evitar NAT por completo, así que estos problemas deberían disminuir
  • Al escribir entradas de blog como esta, es muy frustrante lo difícil que es enlazar a líneas específicas de código y lograr que ese enlace siga vivo y siendo útil con el paso del tiempo
    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
  • En resumen, dentro de los paquetes ICMP hay un campo id, y Netfilter reconoce los paquetes o tramas ICMP como un “caso especial”