Creo que este post puede ser muy interesante y además un buen ejercicio para recordar las capas del modelo TCP/IP. Surge de una necesidad de gestionar a nivel de bit paquetes de red: identificar y parsear cabeceras de las capas L3 y L4 (red y transporte). Esto hecho en C++ desde un host Linux usando la API de sockets estándar pero a un nivel más bajo de lo habitual.
Mi intención es reflejar con claridad las diferencias entre distintos tipos de sockets que podemos crear y qué aporta cada uno, para poder elegir con criterio según las necesidades de cada quien. Primero repasaremos conceptualmente dos o tres tipos de sockets y el nivel de la pila en el que trabajan; después lo aterrizaremos con ejemplos de código en C++ y algún diagrama sencillo para entender exactamente qué bytes llegan a nuestro recvfrom() en cada escenario.
Configurando un socket para recibir datagramas UDP en un puerto concreto
Una necesidad my común como la de recibir datagramas UDP en un puerto concreto puede llevarnos a crear sockets como:
#include <sys/socket.h>
int s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);Donde lo importante está en i) AF_INET: especificando que la comunicación usará direcciones IPv4, en ii) SOCK_DGRAM: especificando que las comunicaciones estarán basadas en datagramas, y en iii) IPPROTO_UDP: que dice al kernel que solo nos interesan datagramas UDP. Se definien así parámetros asociados al direccionamiento IP (capa de red) y al tipo de transporte basado en datagramas. Opcionalmente, también podríamos bindear el socket a un puerto fijo en el que recibir los datagramas:
#include <sys/socket.h>
#include <arpa/inet.h>
int s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(5500);
addr.sin_addr.s_addr = INADDR_ANY;
bind(s, (sockaddr*)&addr, sizeof(addr));Ahora, el proceso podrá recibir los datagramas UDP dirigidos al puerto 5500 mediante funciones como recvfrom(). Si no se utiliza bind(), el sistema asignará automáticamente un puerto cuando el socket se utilice para enviar.
Lo que a todos nos confunde de primeras es que en la aplicación que estemos desarrollando no vamos a recibir un paquete de red con cabeceras IP ni UDP, ni mucho menos de la capa de enlace. Vamos a recibir únicamente el payload de UDP, es decir, los datos de aplicación.
Entendiendo el papel del kernel de Linux
Cuando un datagrama llega a la interfaz de red, el kernel procesa en orden las distintas capas del stack. Primero, elimina la cabecera Ethernet y entrega el paquete al módulo IPv4, donde se valida la cabecera IPv4, reensambla fragmentos si es necesario y, basándose en el campo Protocol, pasa el payload al módulo de transporte, en este caso sería UDP. Este verifica el checksum, demultiplexa por puerto destino y encola en el socket únicamente los datos de aplicación.Como consecuencia, las llamadas a recvfrom() sobre un socket SOCK_DGRAM reciben exclusivamente el payload UDP, ya que el resto de cabeceras han sido consumidas por la pila de red del sistema operativo. Básicamente, hasta llegar a nuestro proceso, cada capa del modelo TCP/IP consume su propia cabecera y hace las comprobaciones y validaciones necesarias antes de enviarlas a la capa superior.
A veces necesitamos las cabeceras IPv4 y UDP, por la razón que sea
Cuando nos encontramos en este punto, lo único que tenemos que hacer es configurar el socket de una manera diferente y tener presente qué papel juega el kernel, nuevamente. Por ejemplo, digamos que queremos obtener el paquete IPv4 completo, con su cabecera y todo. Entonces necesitaremos configurar el socket como SOCK_RAW:
#include <sys/socket.h>
#include <arpa/inet.h>
int s = socket(AF_INET, SOCK_RAW, IPPROTO_UDP);Mediante un socket AF_INET de tipo SOCK_RAW, el kernel entrega al proceso el datagrama IPv4 completo (incluyendo la cabecera IP y la cabecera UDP) permitiendo inspeccionar manualmente los campos de las cabeceras de ambas capas.
A diferencia de los sockets UDP convencionales, en un socket SOCK_RAW la llamada a bind() no realiza demultiplexación por puerto de transporte. El filtrado principal se realiza por protocolo IP, por lo que el proceso recibirá todos los datagramas UDP que lleguen al host, independientemente del puerto destino. No obstante, se puede filtrar manualmente desde el código ignorando paquetes a gusto.
Es importante tener en cuenta que la creación de sockets de tipo SOCK_RAW requiere permisos de sudo en la mayoría de sistemas Linux. Por motivos de seguridad, el kernel restringe este tipo de sockets para evitar que procesos no autorizados puedan inspeccionar o generar tráfico de bajo nivel. En la práctica, esto implica ejecutar la aplicación con sudo o conceder la capacidad CAP_NET_RAW al binario para poder recibir paquetes mediante sockets raw (esto es un rollo porque se corrompe si recompilas).
¿Y para qué iba uno a querer tener las cabeceras de red y transporte en el código? Pues para comprimirlas con SCHC por ejemplo, tal y como explico en: "Un vistazo a SCHC para entender cómo facilita el envío de paquetes IP comprimidos en enlaces restringido".

No hay comentarios:
Publicar un comentario