Raw Sockets en Linux ejemplo de servidor (capturador de paquetes)

Explicación paso a paso (con un ejemplo): 


0. Inclusiones necesarias.

#include <stdio.h>

#include <stdlib.h>

#include <sys/socket.h>               // Para socket(), bind(), recv()     

#include <unistd.h>                  // Para close()

#include <netinet/in.h>               // Para estructuras de red 

#include <netinet/if_ether.h>          // Para encabezados Ethernet como ETH_P_ALL

#include <netpacket/packet.h>         // Para la estructura sockaddr_ll

#include <arpa/inet.h>                // Para ntohs(). En la red se usa big endian (byte más significativo primero).


Preparamos una pequeña función para imprimir en pantalla las MAC address que procesaremos luego:

void print_mac_address(const unsigned char *mac) {


// '%X' se usa para imprimir hexadecimales. Y '02' limita el numero de caracteres a mostrar.

    printf("%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);    

}


1. Buffer.

Necesitamos un buffer donde almacenar lo recibido (o capturado).

#define BUFFER_SIZE 65536             // Tamaño máximo de un paquete basado en IP (0x10000)


int socketfd;

unsigned char buffer[BUFFER_SIZE];


2. Crear el socket.

socketfd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));

if (socketfd < 0) {

        perror("Error creating socket");

        return 1;

    }

AF_PACKET indica capa 2 (Data Link Layer) permitiendo ver el encabezado Ethernet.

htons porque se crea en el host y se manda esta información a la red.

3. Bucle de escucha. 

Dentro de un loop.

1. Crear las variables necesarias para guardar la dirección del cliente. El tipo de dato de estas variables es el necesario para que las funciones de socket como recvfrom() y sendto() funcionen.

while (1) {

struct sockaddr_ll sender_addr;

socklen_t sender_len = sizeof(sender_addr);


2. Recibir los paquetes Ethernet. Por defecto requiere que el tipo de dato sea ssize_t que es un long int (%ld).

ssize_t packet_size = recvfrom(socketfd, buffer, BUFFER_SIZE, 0, (struct sockaddr *)&sender_addr, &sender_len);

En Raw Sockets puede utilizarse tanto la función recv() como recvfrom(), pero se recomienda más esta última ya que proporciona información sobre la dirección de origen. 

La función devuelve un la cantidad de bytes y es necesario guardarla con el tipo ssize_t.

Los parámetros son:

a. El file descriptor del socket (del servidor obviamente). 

b. El buffer donde guardaremos lo que viene del cliente.

c. El tamaño del buffer

d. Un flag que por lo gral se deja en 0, (tiene opciones avanzadas).

c. La dirección del cliente con cast a un puntero para que sea interpretada como sockaddr 

(en un principio la tuvimos que declarar como sockaddr_ll (capa 2)) 

Nota: en la capa 3 con servidores de socket TCP/UDP el tipo es sockaddr_in


3. Chequeamos que se haya recibido algo.

if (packet_size < 0) {

perror("Error receiving");

break;

}


4. Extraemos la información del paquete Ethernet recibido.

Hasta ahora tenemos lo recibido en el buffer que le indicamos anteriormente a la función recvfrom(). “Todo junto”.

Para interpretar fácilmente esta información nos ayudamos de ethhdr que es una estructura definida en la biblioteca <linux/if_ether.h> en sistemas Linux. Contiene los campos básicos: [MAC_destino] [MAC_origen] [Protocolo].

En C si hacemos un cast a un puntero con un tipo de dato, no estamos cambiando el tipo de dato en si mismo, sino que nos permite decirle al compilador que para este caso interprete este valor como si fuera del tipo de dato al que estamos apuntando.

Un puntero al tipo de datos ethhdr (ethernet header), le dice al compilador que interprete los datos almacenados en buffer con este formato para acceder a ellos.

FF FF FF FF FF FF  00 11 22 33 44 55  08 00

└─ MAC Dest ──┘ └ MAC Origen ┘  └Tipo┘

struct ethhdr *eth = (struct ethhdr *)buffer;

Esto hace que: 

a. los datos en buffer sean tratados como si se fueran del tipo de dato ethhdr 

b. se guarda en un puntero llamado eth la dirección de memoria donde comienzan los datos de buffer 

c. ahora se puede acceder a estos datos como si de un dato tipo ethhdr se tratase, con sus respectivos campos…

     Por ejemplo: 

          ethhdr -> h_dest 

          ethhdr -> h_source 

          ethhdr -> h_proto 

*) ethhdr tiene solo estos tres campos, entonces ¿qué pasa con el resto de lo que hay en ‘buffer’? No son interpretados. El puntero solo interpretara los primeros 14 bytes. Así que no hay problema. 

5. Imprimimos en pantalla cada campo deseado

printf("\n------------------------------------------\n");

printf("Packet received: %ld bytes\n", packet_size);

printf("MAC Source: ");

print_mac_address(eth->h_source);

printf("\nMAC Destination: ");

print_mac_address(eth->h_dest);


// Identificar el protocolo Ethernet

printf("\nProtocol Type: ");

switch (ntohs(eth->h_proto)) {

case ETH_P_IP:

printf("IPv4\n");

break;

case ETH_P_ARP:

printf("ARP\n");

break;

case ETH_P_IPV6:

printf("IPv6\n");

break;

default:

// Revisa el campo del protocolo e imprime en hexadecimal los 4 digitos correspondientes.

        printf("Unknown (0x%04X)\n", ntohs(eth->h_proto));                    

        break;

    }


// Mostrar los primeros bytes del contenido (máximo 10 bytes)

// Los primeros 14 bytes son la cabecera del paquete Ethernet. 

// Asi que el recorremos desde 14 + i ... hasta packet_size - 14 para no salirnos...

// Imprimimos en hexadecimal. 


printf("Payload (first 10 bytes): ");

for (int i = 0; i < 10 && i < packet_size - 14; i++) {                       

    printf("%02X ", buffer[14 + i]);

}

printf("\n------------------------------------------\n");


6. Así los paquetes son capturados y mostrados todos, lo que llenará la pantalla de la terminal muy rápidamente. Como es un simple programa de ejemplo, agregar una pausa al final antes de cerrar el bucle permitirá visualizar mucho mejor (aunque se pierdan muchos paquetes en el medio, no importa en este caso).

    // Pausa para evitar que la salida sea muy rápida

    usleep(500000); // 500ms de espera entre paquetes

} // Cierre del loop “while (1)”


4. Cierre del socket.

close(socketfd);

return 0;