1 puntos por GN⁺ 2025-03-05 | 1 comentarios | Compartir por WhatsApp
  • Como primer paso de una serie para aprender capas de red construyendo directamente una pila TCP/IP en espacio de usuario en Linux, se implementa desde la gestión de tramas Ethernet de capa 2 hasta la respuesta ARP
  • El tráfico de red de bajo nivel del kernel se recibe mediante un dispositivo TAP, y con el descriptor de archivo devuelto se puede hacer read/write sobre el búfer Ethernet del dispositivo virtual
  • La cabecera Ethernet se maneja con dmac, smac, ethertype y payload, y si el valor de ethertype es 1536 o mayor indica el tipo de payload, mientras que si es menor indica la longitud del payload
  • ARP mapea dinámicamente direcciones de protocolo como las direcciones IPv4 a direcciones MAC de 48 bits, y se implementa con un flujo que responde solicitudes mientras actualiza la tabla de traducción
  • En la prueba con arping, el kernel de Linux reconoce la respuesta ARP de la pila personalizada y agrega una entrada 10.0.0.4 a la caché ARP de la interfaz tap0

Punto de partida de una pila TCP/IP en espacio de usuario

  • El objetivo es implementar una pila TCP/IP mínima en espacio de usuario en Linux para entender con más profundidad las redes y la programación de sistemas
  • TCP se ha vuelto complejo por la acumulación de especificaciones durante más de 30 años, pero sus elementos centrales de implementación pueden acotarse a parseo de cabeceras TCP, máquina de estados, control de congestión y cálculo de timeouts de retransmisión
  • Ethernet e IP tienen menor complejidad que TCP, así que la serie empieza la implementación desde la capa 2

Recibir tráfico Ethernet con un dispositivo TAP

  • Para interceptar tráfico de red de bajo nivel del kernel de Linux se usa un dispositivo TAP de Linux
  • Los dispositivos TUN/TAP se usan con frecuencia cuando aplicaciones de red en espacio de usuario manipulan tráfico L3/L2 respectivamente
    • El tunneling consiste en encapsular un paquete dentro del payload de otro paquete
    • Programas como OpenVPN también usan dispositivos TUN/TAP
  • Como la pila de red se construye desde la capa 2, se necesita un dispositivo TAP y no TUN
  • El dispositivo TAP se crea abriendo /dev/net/tap y usando ioctl(fd, TUNSETIFF, ...)
    • IFF_TAP selecciona un dispositivo TAP
    • IFF_NO_PI evita que se agregue información de paquete innecesaria delante de la trama Ethernet
  • Después de crearlo, con el descriptor de archivo fd devuelto se lee y escribe el búfer Ethernet del dispositivo virtual

Formato de trama Ethernet

  • Ethernet es la tecnología base para conectar computadoras en una LAN, y el primer estándar Ethernet fue publicado en 1980 por Digital Equipment Corporation, Intel y Xerox
  • La primera versión usaba una velocidad de alrededor de 10Mb/s y comunicación half-duplex, por lo que necesitaba un protocolo MAC para regular el flujo de datos
    • En interfaces Ethernet half-duplex, CSMA/CD era necesario como método MAC
    • 100BASE-T usa cableado twisted-pair para permitir comunicación full-duplex y mayor rendimiento
    • Con la adopción generalizada de switches Ethernet, la necesidad de CSMA/CD en gran medida disminuyó
  • El estándar Ethernet es administrado por el grupo de trabajo IEEE 802.3

Cabecera Ethernet usada en la implementación

  • La implementación incluye if_ether.h de Linux para usar el mapeo entre ethertype y valores hexadecimales
  • La cabecera Ethernet se representa en una estructura de C con los siguientes campos
    • dmac: dirección MAC de destino
    • smac: dirección MAC de origen
    • ethertype: longitud o tipo del payload
    • payload: puntero al payload que contiene un paquete ARP o IPv4
  • ethertype es un campo de 2 octetos y su significado cambia según el valor
    • Si el valor es 1536 o mayor, indica un tipo de payload como IPv4 o ARP
    • Si el valor es menor, indica la longitud del payload
  • Las tramas Ethernet pueden llevar etiquetas que representan VLAN o QoS, pero esta implementación excluye las etiquetas de trama
  • Si la longitud del payload es menor que el mínimo requerido de 48 bytes sin etiquetas, se agregan bytes de padding al final
  • Al final del Ethernet Frame Format hay un campo Frame Check Sequence que verifica integridad con CRC, pero esta implementación no lo procesa

Método de parseo de tramas Ethernet

  • El atributo packed en la declaración de la estructura evita que el compilador GNU C optimice el layout de memoria de la estructura con bytes de padding para alineación de datos
  • El parseo en la implementación se hace mediante type casting del búfer a la estructura de protocolo correspondiente
    • Ejemplo: struct eth_hdr *hdr = (struct eth_hdr *) buf;
  • Un método más portable es serializar manualmente los datos del protocolo
    • En ese caso, el compilador podría agregar bytes de padding para cumplir los requisitos de alineación de datos específicos del procesador
  • El procesamiento de tramas Ethernet recibidas sigue un flujo simple
    • Se lee un búfer desde el dispositivo TAP
    • Se inicializa la cabecera Ethernet con init_eth_hdr(buf)
    • handle_frame(&netdev, hdr) decide la siguiente acción según el valor de ethertype

Estructura y función de los paquetes ARP

  • ARP (Address Resolution Protocol) mapea dinámicamente direcciones de protocolo como direcciones IPv4 a direcciones MAC, que son direcciones Ethernet de 48 bits
  • ARP no está limitado solo a IPv4 y puede usarse junto con varios protocolos L3
    • Como ejemplo, CHAOS define una dirección de protocolo de 16 bits
  • En una comunicación LAN normal, aunque se conozca la dirección IP de un servicio, para la transmisión real se necesita una dirección MAC
  • ARP envía una consulta broadcast a la red para que el propietario de esa dirección IP anuncie su dirección de hardware

Cabecera ARP y payload para IPv4

  • La cabecera ARP está compuesta por los siguientes campos
    • hwtype: campo de 2 octetos que indica el tipo de capa de enlace; en Ethernet su valor es 0x0001
    • protype: campo de 2 octetos que indica el tipo de protocolo; en IPv4 su valor es 0x0800
    • hwsize: campo de 1 octeto que indica el tamaño de la dirección de hardware; una dirección MAC ocupa 6 bytes
    • prosize: campo de 1 octeto que indica el tamaño de la dirección de protocolo; una dirección IPv4 ocupa 4 bytes
    • opcode: campo de 2 octetos que indica el tipo de mensaje ARP
  • Los valores de opcode se dividen en cuatro
    • ARP request: 1
    • ARP reply: 2
    • RARP request: 3
    • RARP reply: 4
  • Los datos ARP para IPv4 se manejan con la estructura arp_ipv4
    • smac: dirección MAC del remitente
    • sip: dirección IP del remitente
    • dmac: dirección MAC del receptor
    • dip: dirección IP del receptor

Algoritmo de resolución de direcciones y caché

  • El algoritmo de resolución de direcciones de RFC 826 verifica el tipo de hardware y el tipo de protocolo, luego actualiza la tabla de traducción y, si la dirección objetivo es la propia, genera una respuesta
  • La translation table almacena resultados ARP para que el host pueda consultar en caché entradas que ya posee
  • Esta caché reduce que la red se llene innecesariamente con solicitudes ARP duplicadas
  • El código de implementación está en arp.c

Prueba de respuesta ARP y siguiente paso

  • La prueba final de la implementación ARP consiste en verificar si responde correctamente a las solicitudes
  • Al ejecutar arping -I tap0 10.0.0.4, regresa una respuesta unicast desde 10.0.0.4 con la dirección MAC 00:0C:29:6D:50:25
  • Después, en la salida de arp, aparece la entrada 10.0.0.4 ether 00:0c:29:6d:50:25 tap0 en la caché ARP del kernel de Linux
  • Incluso solo con el procesamiento mínimo de tramas Ethernet y la implementación ARP, se puede comprobar que un dispositivo Ethernet personalizado llena la caché ARP del host Linux
  • El código fuente del proyecto está en GitHub, y el siguiente paso es implementar ping como ICMP echo/reply y el parseo de paquetes IPv4

1 comentarios

 
GN⁺ 2025-03-05
Opiniones en Hacker News
  • Hace unos años hice una pila de red en espacio de usuario en C, y la hice funcionar hasta cierto punto procesando paquetes crudos con una interfaz TUN.
    Ahora incluye una shell sencilla para configurar direcciones IP, rutas, etc., y los paquetes de red se guardan en una estructura híbrida que parece una mezcla de mbuf y sk_buf.
    Sin embargo, después de terminar la implementación de UDP no tuve tiempo ni ganas de implementar TCP, y el código está aquí: https://github.com/cakturk/unet

    • Hace muchísimo tiempo escribí un parser de pcap/tcpdump en bash puro; en ese momento era la única herramienta que sabía usar para escribir “programas”.
      Por supuesto, era probablemente una de las cosas más lentas y frágiles de la historia, pero funcionaba de verdad y fue bastante divertido. Ojalá ese código todavía esté guardado en algún lado.
    • Muchos dispositivos embebidos usan lwip como implementación de TCP/IP.
      El “puerto POSIX” de lwip también obtiene bytes Ethernet crudos desde un dispositivo TUN/TAP de la misma manera.
      https://github.com/lwip-tcpip/lwip/blob/master/contrib/ports...
  • Si se compila un kernel Linux mínimo sin pila TCP/IP, ocupa 400 KB; al incluir la pila TCP/IP, pasa a 800 KB.
    En un proyecto donde solo había que enviar la temperatura, mandé los valores dentro de mensajes UDP hechos a mano desde un pequeño programa en C en espacio de usuario, y así pude reducir mucho el espacio y la complejidad.

    • Desde mi ignorancia, eso me sorprende bastante, pero supongo que no significa que la parte de TCP/IP sea la mitad de todo el código fuente del kernel, ¿no?
    • Me pregunto por qué la pila IP es tan grande. Un binario de 400 KB es bastante código; ¿será porque está muy optimizada para uso en servidores grandes?
  • Si se desactiva ARP, se puede configurar la misma IP en varios servidores de la misma red.
    Si un servidor que actúa como frontend de enrutamiento puede entregar paquetes a la interfaz de red de los servidores backend según la dirección MAC, ese backend se reconocerá como destino y podrá responder directamente al cliente intercambiando las IP de origen/destino. En ese caso no vuelve a pasar por el frontend de enrutamiento.
    O bien, sin desactivar ARP, se puede agregar la dirección IP común como alias en la interfaz loopback para lograr el mismo efecto, permitiendo que el backend se reconozca como destino y evitando conflictos de ARP. Era un truco usado por el balanceador de carga por software IBM WebSphere en los años 90 y 2000.

    • Cisco IOS SLB también puede funcionar de forma parecida. Consiste en agregar la IP virtual como alias en el loopback de cada servidor de la granja.
      La ventaja frente al balanceo de carga L3 más común es que no hace falta reescribir el encabezado del paquete IP.
    • Esto también se conoce como DSR (Direct Server Return): https://www.haproxy.com/blog/layer-4-load-balancing-direct-s...
    • Si desactivas ARP y configuras la misma IP en varios servidores de la misma red, el switch/bridge no podrá aprender las direcciones MAC, por lo que seguirá inundando/broadcasting paquetes por todos los puertos de ese segmento.
      Por eso, si se usa este método, conviene crear una VLAN dedicada.
    • F5 tiene una configuración de proxy ARP, así que no hace falta hacer esto. La desventaja es que muchas veces rompe DHCP.
    • Para este tipo de experimentos de bajo nivel también se puede probar DPDK. ARP viene desactivado por defecto.
  • Hice algo parecido en Python: https://github.com/georgek/notebooks/blob/master/internet.ip...
    Probablemente la calidad del código sea peor y, siendo sincero, el algoritmo de resolución de direcciones me lo inventé. Llegué a hacer ping a hosts de Internet con ICMP.
    Me gusta que esté completamente contenido en un notebook corto. El artículo original omite en el texto muchos detalles que están en el código fuente más grande al que hace referencia.
    Lo hice sin ver este artículo, solo consultando Wikipedia. Pero a partir de TCP la complejidad subió mucho y perdí algo de interés. Como la parte 3 trata ese tema, quizá algún día la lea y lo termine. Si te interesa el networking, creo que es una tarea viable y gratificante para programadores de cualquier nivel.

  • Hace unos años trabajé en instrumentación de plantas nucleares. El desarrollo del lado cliente se hacía en estaciones de trabajo Sun, y de hecho me contrataron gracias a mi experiencia con TCP/IP, que obtuve en el curso de “sistemas operativos” de CMU.
    En cambio, las computadoras de la planta eran minicomputadoras sin pila TCP/IP, así que ese equipo tuvo que crear su propia pila.

  • Aproximadamente al minuto de empezar, el texto dice que “dmac y smac son campos bastante obvios”, pero un lector que no sepa qué son podría abandonar ahí mismo.
    Puede pensar: “este artículo es para gente a la que esos campos le resultan obvios. No es para mí, así que mejor dejo de leer”.

    • La frase completa es “dmac y smac son campos bastante obvios. Contienen las direcciones MAC de las partes que se comunican (destino y origen, respectivamente)”, así que en realidad sí lo explica.
      Además, si es un artículo sobre crear una pila de red, es seguro asumir que el lector sabe algo de redes.
    • Si no se actualizó recién, la oración inmediatamente siguiente explica: “Contienen las direcciones MAC de las partes que se comunican (destino y origen, respectivamente)”.
  • Publicaciones relacionadas:
    Let’s code a TCP/IP stack (2016) - https://news.ycombinator.com/item?id=27654182 - junio de 2021, 49 comentarios
    Let’s code a TCP/IP stack, 1: Ethernet & ARP (2016) - https://news.ycombinator.com/item?id=17316487 - junio de 2018, 47 comentarios
    Let’s Code a TCP/IP Stack: TCP Retransmission - https://news.ycombinator.com/item?id=14701199 - julio de 2017, 30 comentarios
    Let’s code a TCP/IP stack, 1: Ethernet and ARP - https://news.ycombinator.com/item?id=11234229 - marzo de 2016, 49 comentarios

  • No sé de dónde sacó el autor la dirección IP 10.0.0.4 que usa para la prueba de resolución ARP
    ¿Dirección de qué se supone que es? ¿Es un dispositivo ficticio accesible desde el dispositivo Ethernet ficticio creado aquí, o es un dispositivo que realmente existe en la red del autor?

    • No aparece en el artículo, pero es un valor que el autor dejó hardcodeado al inicializar la interfaz: https://github.com/saminiir/level-ip/blob/e9ceb08f01a5499b85...
      Un dispositivo TAP es algo así como un enlace Ethernet emulado por software. Si le envías paquetes, se entregan directamente a un programa en espacio de usuario, y ese programa decide qué dirección IP tendrá y cómo responderá a ARP
      Normalmente, el sistema operativo se encarga de este tipo de cosas, y para agregar una dirección IP a una interfaz se necesitan permisos de root. Lo mismo para abrir un dispositivo TAP. En general, las redes funcionan de manera cooperativa, y un actor malicioso con permisos de root en la red puede hacer cosas malas
  • Según recuerdo, ARP solo funciona en el segmento local. El router completa su propia dirección y reenvía el paquete
    También existe rarp, que es una de las formas de preguntarle a la “red” cuál es tu propia dirección IP. No sé si rarp todavía funciona en entornos reales