Hacerlo rápidamente Introducción a XDP Parte 3 (ICMP Echo Reply Parte 2)
servidor redes programación
Published: 2025-03-06

En el artículo anterior, escribimos un programa para devolver el ICMP Echo Reply usando XDP.
Implementamos un programa que devuelve un ICMP Echo Reply, pero descubrimos que el checksum de ICMP no se estaba calculando correctamente.

En esta ocasión, quiero escribir sobre cómo calcular correctamente el checksum de ICMP y devolverlo.

No me he dejado llevar por la pereza y logré escribir durante dos días seguidos, así que por favor, alénenme a ello.

Checksum de ICMP

  • Estructura del encabezado ICMP
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |      Tipo       |     Código      |          Checksum         |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |           Identificador          |        Número de Secuencia  |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                             Datos                               |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    

El encabezado ICMP incluye un Checksum, y dado que cambiamos el tipo de ICMP Echo Request a ICMP Echo Reply y devolvemos el paquete, es necesario recalcular el valor del Checksum.

El cálculo del checksum de ICMP se realiza sumando el encabezado y todos los datos en unidades de 16 bits, y si se produce un carry, se suma ese valor nuevamente. Finalmente, obtenemos el checksum final al tomar el complemento a uno (inversión de bits).

Este método de cálculo se detalla en RFC 1624. Tenga cuidado si consulta RFC 1141 porque la descripción del cálculo diferencial es incorrecta, y RFC 1624 corrige esto.

Tuve la experiencia de gastar un tiempo infinito consultando RFC 1141 sin darme cuenta de que había sido actualizado.

Asegúrese de leer siempre el texto original de las RFC, no traducciones realizadas por otros… (una lección para mí mismo).

Como se suma en bloques de 16 bits y luego se toma el complemento a uno, en realidad solo necesita tomar las diferencias en las posiciones corregidas en 16 bits, sin necesidad de recalcular todos los datos.

bpf_csum_diff

Cuando se realiza el cálculo del checksum en un programa eBPF, hay una función llamada bpf_csum_diff que está disponible. Usando esta función, se puede calcular el checksum de manera sencilla.

bpf_csum_diff es una función que calcula el cambio en el checksum basado en cambios de 16 bits en un área de memoria específica. Esto permite una actualización eficiente de los checksums ICMP e IP.

Programa para devolver ICMP Echo Reply

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/icmp.h>
#include <arpa/inet.h>

static __always_inline __u16 csum_fold(__u32 csum)
{
    // Sumar el valor superior de 16 bits al inferior de 16 bits
    csum = (csum & 0xffff) + (csum >> 16);
    // Sumar de nuevo el carry
    csum = (csum & 0xffff) + (csum >> 16);
    // Tomar el complemento a uno y devolver el checksum final
    return (__u16)~csum;
}

static __always_inline void swap_eth_addr(__u8 *a, __u8 *b)
{
    __u8 tmp[ETH_ALEN];
    __builtin_memcpy(tmp, a, ETH_ALEN);
    __builtin_memcpy(a, b, ETH_ALEN);
    __builtin_memcpy(b, tmp, ETH_ALEN);
}

static __always_inline void swap_ip_addr(__u32 *a, __u32 *b)
{
    __u32 tmp = *a;
    *a = *b;
    *b = tmp;
}

SEC("xdp")
int xdp_echo_reply(struct xdp_md *ctx)
{
    void *data_end = (void *)(unsigned long)ctx->data_end;
    void *data = (void *)(unsigned long)ctx->data;

    // Obtener el puntero del encabezado Ethernet
    struct ethhdr *eth = data;
    if ((void *)eth + sizeof(*eth) > data_end)
        return XDP_PASS;

    // Verificar si el marco Ethernet contiene un paquete IP
    if (eth->h_proto != htons(ETH_P_IP))
        return XDP_PASS;

    // Obtener el puntero del encabezado IP
    struct iphdr *ip = data + sizeof(*eth);
    if ((void *)ip + sizeof(*ip) > data_end)
        return XDP_PASS;

    // Verificar si el protocolo es ICMP
    if (ip->protocol != IPPROTO_ICMP)
        return XDP_PASS;

    // Obtener el puntero del encabezado ICMP
    struct icmphdr *icmp = (void *)ip + sizeof(*ip);
    if ((void *)icmp + sizeof(*icmp) > data_end)
        return XDP_PASS;

    // Pasar el paquete si no es un ICMP Echo Request
    if (icmp->type != ICMP_ECHO)
        return XDP_PASS;

    // Intercambiar las direcciones MAC de origen y destino
    swap_eth_addr(eth->h_dest, eth->h_source);
    // Intercambiar las direcciones IP de origen y destino
    swap_ip_addr(&ip->saddr, &ip->daddr); 

    // Hacer una copia de icmp
    struct icmphdr icmp_before = *icmp;

    // Cambiar el flag a ICMP Echo Reply
    icmp->type = ICMP_ECHOREPLY;

    // Inicializar el checksum en 0
    icmp->checksum = 0;
    __s64 value = bpf_csum_diff((void *)&icmp_before, sizeof(icmp_before), (void *)icmp, sizeof(*icmp), 0);
    if (value >= 0)
        icmp->checksum = csum_fold(value);

    // Eliminar el paquete ICMP Echo Request
    // return XDP_DROP;
    return XDP_TX;
}

char _license[] SEC("license") = "MIT";
static __always_inline __u16 csum_fold(__u32 csum)
{
    // Sumar el valor superior de 16 bits al inferior de 16 bits
    csum = (csum & 0xffff) + (csum >> 16);
    // Sumar de nuevo el carry
    csum = (csum & 0xffff) + (csum >> 16);
    // Tomar el complemento a uno y devolver el checksum final
    return (__u16)~csum;
}

La función csum_fold realiza el proceso de comprimir un checksum de 32 bits a 16 bits. El valor de retorno de bpf_csum_diff() es de 32 bits y puede ocurrir un carry. Por lo tanto, primero se suma el valor superior de 16 bits al inferior y luego se realiza nuevamente el procesamiento de carry. Finalmente, se obtiene el valor final del checksum tomando el complemento a uno (inversión de bits).

static __always_inline void swap_eth_addr(__u8 *a, __u8 *b)
{
    __u8 tmp[ETH_ALEN];
    __builtin_memcpy(tmp, a, ETH_ALEN);
    __builtin_memcpy(a, b, ETH_ALEN);
    __builtin_memcpy(b, tmp, ETH_ALEN);
}

La función swap_eth_addr es para intercambiar las direcciones MAC del encabezado Ethernet.

static __always_inline void swap_ip_addr(__u32 *a, __u32 *b)
{
    __u32 tmp = *a;
    *a = *b;
    *b = tmp;
}

La función swap_ip_addr es para intercambiar las direcciones IP del encabezado IP.

En eBPF, es necesario realizar el despliegue en línea de las llamadas a funciones, así que se agrega __always_inline.

    // Intercambiar las direcciones MAC de origen y destino
    swap_eth_addr(eth->h_dest, eth->h_source);
    // Intercambiar las direcciones IP de origen y destino
    swap_ip_addr(&ip->saddr, &ip->daddr); 

    // Hacer una copia del encabezado ICMP
    struct icmphdr icmp_before = *icmp;

    // Cambiar el flag a ICMP Echo Reply
    icmp->type = ICMP_ECHOREPLY;

    // Inicializar el checksum en 0
    icmp->checksum = 0;
    __s64 value = bpf_csum_diff((void *)&icmp_before, sizeof(icmp_before), (void *)icmp, sizeof(*icmp), 0);
    if (value >= 0)
        icmp->checksum = csum_fold(value);

Esta parte es diferente de antes.
La función swap_eth_addr y swap_ip_addr simplemente encapsulan el intercambio de direcciones MAC e IP.

Hacemos una copia del icmp y cambiamos icmp->type a ICMP_ECHOREPLY.
Posteriormente, calculamos el checksum.

Inicializamos icmp->checksum a 0 y luego calculamos la diferencia con bpf_csum_diff(). Como el retorno de bpf_csum_diff no tiene carry, utilizamos csum_fold() para hacer la compresión.

Confirmación de funcionamiento

  • Enviar un ping desde el cliente:

    $ ping 192.168.XXX.XXX
    
  • Confirmamos que se recibe el ICMP Echo Reply al enviar un ping desde el cliente.

    $ sudo tcpdump -i eth0 icmp -vvv
    XX:XX:XX.413478 IP (tos 0x0, ttl 64, id 35523, offset 0, flags [DF], proto ICMP (1), length 84)
        192.168.XXX.1 > 192.168.XXX.2: ICMP echo request, id 64346, seq 16, length 64
    XX:XX:XX.414359 IP (tos 0x0, ttl 64, id 35523, offset 0, flags [DF], proto ICMP (1), length 84)
        192.168.XXX.2 > 192.168.XXX.1: ICMP echo reply, id 64346, seq 16, length 64
    

Como se puede confirmar, se resolvió el error de checksum, y se pudo recibir el ICMP Echo Reply.

En realidad…

En realidad, el encabezado IP también tiene un checksum, por lo que en realidad es necesario recalcularlo.

  • Estructura del encabezado IP
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    | Versión |  IHL  |  DSCP  | ECN  |        Longitud Total       |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |         Identificación        |Flags|     Offset de Fragmento   |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |  Tiempo de Vida |   Protocolo    |        Checksum de Encabezado |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                     Dirección IP de Origen                     |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                  Dirección IP de Destino                      |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    |                      Opciones (si las hay)                    |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    

Hay un campo HeaderChecksum, y originalmente es necesario recalcular este valor.

Sin embargo, dado que en este caso solo estamos intercambiando las direcciones IP de origen y destino en 16 bits, el método de cálculo del checksum resulta, en efecto, en que el checksum no cambia.
Esto se debe a la propiedad de que “intercambiar direcciones IP en 16 bits no afecta el checksum como complemento a uno de la suma”. Así que, en este caso, solo tuvimos suerte y no hubo impacto, pero si se cambian otros campos, es necesario recalcular obligatoriamente el checksum de IP.

Si es necesario, el encabezado IP debe recalcularse de manera similar al encabezado ICMP.

Calcular también el checksum del encabezado IP

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/icmp.h>
#include <arpa/inet.h>

static __always_inline __u16 csum_fold(__u32 csum)
{
    // Sumar el valor superior de 16 bits al inferior de 16 bits
    csum = (csum & 0xffff) + (csum >> 16);
    // Sumar de nuevo el carry
    csum = (csum & 0xffff) + (csum >> 16);
    // Tomar el complemento a uno y devolver el checksum final
    return (__u16)~csum;
}

static __always_inline void swap_eth_addr(__u8 *a, __u8 *b)
{
    __u8 tmp[ETH_ALEN];
    __builtin_memcpy(tmp, a, ETH_ALEN);
    __builtin_memcpy(a, b, ETH_ALEN);
    __builtin_memcpy(b, tmp, ETH_ALEN);
}

static __always_inline void swap_ip_addr(__u32 *a, __u32 *b)
{
    __u32 tmp = *a;
    *a = *b;
    *b = tmp;
}

SEC("xdp")
int xdp_echo_reply(struct xdp_md *ctx)
{
    void *data_end = (void *)(unsigned long)ctx->data_end;
    void *data = (void *)(unsigned long)ctx->data;

    // Obtener el puntero del encabezado Ethernet
    struct ethhdr *eth = data;
    if ((void *)eth + sizeof(*eth) > data_end)
        return XDP_PASS;

    // Verificar si el marco Ethernet contiene un paquete IP
    if (eth->h_proto != htons(ETH_P_IP))
        return XDP_PASS;

    // Obtener el puntero del encabezado IP
    struct iphdr *ip = data + sizeof(*eth);
    if ((void *)ip + sizeof(*ip) > data_end)
        return XDP_PASS;

    // Verificar si el protocolo es ICMP
    if (ip->protocol != IPPROTO_ICMP)
        return XDP_PASS;

    // Obtener el puntero del encabezado ICMP
    struct icmphdr *icmp = (void *)ip + sizeof(*ip);
    if ((void *)icmp + sizeof(*icmp) > data_end)
        return XDP_PASS;

    // Pasar el paquete si no es un ICMP Echo Request
    if (icmp->type != ICMP_ECHO)
        return XDP_PASS;

    // Hacer una copia del encabezado IP
    struct iphdr ip_before = *ip;

    // Intercambiar las direcciones MAC de origen y destino
    swap_eth_addr(eth->h_dest, eth->h_source);
    // Intercambiar las direcciones IP de origen y destino
    swap_ip_addr(&ip->saddr, &ip->daddr); 

    // Hacer una copia del encabezado ICMP
    struct icmphdr icmp_before = *icmp;

    // Cambiar el flag a ICMP Echo Reply
    icmp->type = ICMP_ECHOREPLY;

    // Inicializar el checksum en 0
    icmp->checksum = 0;
    __s64 value = bpf_csum_diff((void *)&icmp_before, sizeof(icmp_before), (void *)icmp, sizeof(*icmp), 0);
    if (value >= 0)
        icmp->checksum = csum_fold(value);

    // checksum del encabezado IP
    ip->check = 0;
    value = bpf_csum_diff((void *)&ip_before, sizeof(ip_before), (void *)ip, sizeof(*ip), 0);
    if (value >= 0)
        ip->check = csum_fold(value);

    // Eliminar el paquete ICMP Echo Request
    // return XDP_DROP;
    return XDP_TX;
}

char _license[] SEC("license") = "MIT";

Antes de escribir sobre el encabezado IP, hacemos una copia, y de manera similar al checksum ICMP, calculamos la diferencia con bpf_csum_diff. De esta manera, el checksum del encabezado IP también se calculará correctamente.

Resumen

  • Implementamos un programa XDP que responde a un ICMP Echo Request con un ICMP Echo Reply.
  • Presenté cómo calcular correctamente el checksum de ICMP y devolverlo.
  • El cálculo de checksum requiere simplemente tomar las diferencias en las posiciones corregidas en 16 bits y no es necesario recalcular todos los datos.
  • Usando la función bpf_csum_diff(), se puede realizar el cálculo del checksum de manera sencilla.