Una introducción rápida a Ultra XDP Parte 2 (sección eBPF Map)
servidor red programación
Lastmod: 2025-03-06
Published: 2024-09-17

Resumen

En el artículo anterior, escribimos un programa que utiliza el programa XDP para DROPAR todos los paquetes.

Ahora, vamos a escribir un programa que utiliza el Map de eBPF para contar los paquetes DROPEADOS.

Esta vez, el enfoque es experimentar rápidamente con XDP + eBPF Map. Por lo tanto, omitiremos explicaciones detalladas sobre eBPF y otros tipos de Maps que no utilizaremos.

Configuración del entorno

Este artículo se basa en Ubuntu 22.04. Recomendamos probar en máquinas virtuales como VM para controlar la comunicación de NIC con XDP.

Para la configuración, consulte el artículo anterior.

eBPF Map

eBPF Map es una estructura de datos utilizada para compartir datos entre el espacio del núcleo y el espacio de usuario. Esto permite que el programa XDP actualice datos en tiempo real y que el espacio de usuario los lea.

Esta vez, vamos a escribir un programa que cuenta el número de paquetes DROPEADOS por XDP.

Tipos de eBPF Map

Hay varios tipos de eBPF Map.
Los detalles están disponibles en Documentación del núcleo de Linux BPF Map.
Usaremos los siguientes tipos:

  • BPF_MAP_TYPE_HASH
    • Un Map que representa una tabla hash.
    • Puede almacenar pares de clave y valor hash.

Usaremos la dirección IP de origen de los paquetes como clave y almacenaremos el número de paquetes DROPEADOS como valor.

Creación de eBPF Map

eBPF Map se define dentro del programa eBPF de la siguiente manera:

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __type(key, uint32_t);
    __type(value, uint64_t);
    __uint(max_entries, 1024);
} icmp_count_map SEC(".maps");
  • type : Especifica el tipo del Map.
  • key : Especifica el tipo de la clave. Como usaremos direcciones IPv4 como clave, especificamos uint32_t.
  • value : Especifica el tipo de valor. Como almacenaremos el número de paquetes DROPEADOS, especificamos uint64_t.
  • max_entries : Especifica el número máximo de entradas que se pueden almacenar en el Map.
    • En BPF_MAP_TYPE_HASH, intentar añadir más entradas de las permitidas resultará en un error.
  • icmp_count_map : Especifica el nombre del Map.
  • SEC(".maps") : Especifica la sección en la que se define el Map.

Operación de eBPF Map

eBPF Map se opera desde el programa eBPF de la siguiente manera:

  • bpf_map_lookup_elem : Obtiene el valor correspondiente a la clave desde el Map.
  • bpf_map_update_elem : Añade un par de clave-valor al Map.
  • bpf_map_delete_elem : Elimina el valor correspondiente a la clave desde el Map.
  • bpf_map_get_next_key : Obtiene la siguiente clave del Map.

Escribamos un programa XDP

Programa utilizando eBPF Map

A continuación, se muestra un programa que utiliza eBPF Map.

#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>

// Definición de eBPF Map
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __type(key, uint32_t);
    __type(value, uint64_t);
    __uint(max_entries, 1024);
} icmp_count_map SEC(".maps");

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

    // Obtener el puntero de la cabecera 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 de la cabecera 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 de la cabecera ICMP
    struct icmphdr *icmp = (void *)ip + sizeof(*ip);
    if ((void *)icmp + sizeof(*icmp) > data_end)
        return XDP_PASS;

    // Extraer la dirección IP de origen
    uint32_t src_ip = ip->saddr;

    // Buscar en icmp_count_map utilizando la dirección IP de origen
    uint64_t *count = bpf_map_lookup_elem(&icmp_count_map, &src_ip);

    // Si el puntero de tipo u64 no es NULL, incrementar el conteo
    if (count) {
        __sync_fetch_and_add(count, 1);
    } else {
        // Si es NULL, añadir a icmp_count_map
        bpf_map_update_elem(&icmp_count_map, &src_ip, &(uint64_t){1}, BPF_ANY);
    }  

    // DROPAR el paquete ICMP
    return XDP_DROP;
}

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

Compilación y carga

Compilamos xdp.c y cargamos el programa XDP.

$ clang -O2 -target bpf -g -c xdp.c -o xdp.o
$ sudo ip link set dev eth0 xdp obj xdp.o sec xdp

Consultemos el Map desde el espacio de usuario

Configuración del pin para acceder al map

Para que el espacio de usuario pueda consultar el eBPF Map, lo pinamos.
De este modo, se puede acceder al Map desde el espacio de usuario.

  • Obtener el id del map del programa BPF

    $ sudo bpftool map
    16: hash  name icmp_count_map  flags 0x0
            key 4B  value 8B  max_entries 1024  memlock 86144B
            btf_id 25
    
    • 16 es el id del Map.
  • Configuración del pin en el Map

    $ sudo bpftool map pin id 16 /sys/fs/bpf/xdp/icmp_count_map
    

Escribamos un programa de espacio de usuario para obtener el eBPF Map

Ya que hemos almacenado datos en el eBPF Map, consultemos el Map desde el espacio de usuario.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <linux/bpf.h>
#include <linux/if_link.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>

int main(int argc, char **argv) {
    int map_fd = bpf_obj_get("/sys/fs/bpf/xdp/icmp_count_map");
    if (map_fd < 0) {
        perror("bpf_obj_get");
        return 1;
    }

    struct bpf_map_info map_info = {};
    uint32_t info_len = sizeof(map_info);
    if (bpf_obj_get_info_by_fd(map_fd, &map_info, &info_len)) {
        perror("bpf_obj_get_info_by_fd");
        return 1;
    }

    // Mostrar la información del Map
    printf("Nombre del Map: %s\n", map_info.name);
    printf("Tipo del Map: %d\n", map_info.type);
    printf("Tamaño de clave del Map: %d\n", map_info.key_size);
    printf("Tamaño de valor del Map: %d\n", map_info.value_size);
    printf("Máximo de entradas del Map: %d\n", map_info.max_entries);

    // Obtener todas las entradas
    uint32_t key, next_key;
    uint64_t value;
    key = next_key = 0;
    while (bpf_map_get_next_key(map_fd, &key, &next_key) == 0) {
        key = next_key; // Usar la clave actualizada
        if (bpf_map_lookup_elem(map_fd, &key, &value) == 0) {
            // Convertir la dirección IP a una cadena
            char ip_str[INET_ADDRSTRLEN];
            inet_ntop(AF_INET, &key, ip_str, sizeof(ip_str));
            printf("Clave: %s, Valor: %lu\n", ip_str, value);
        }
        key = next_key; // Prepararse para la siguiente iteración
    }

    return 0;
}
  • bpf_obj_get : Obtiene el descriptor de archivo del Map que ha sido pinado.

    • Especificamos el Map pinado en /sys/fs/bpf/xdp/icmp_count_map.
  • bpf_obj_get_info_by_fd : Obtiene información sobre el Map.

    • Obtenemos el nombre del Map, tipo, tamaño de clave, tamaño de valor y el número máximo de entradas.
  • bpf_map_get_next_key : Obtiene la siguiente clave del Map.

    • key : Clave actual
    • next_key : Siguiente clave
  • bpf_map_lookup_elem : Obtiene el valor correspondiente a la clave desde el Map.

Compilación y ejecución

$ gcc -o map_info map_info.c -lbpf
$ sudo ./map_info
Nombre del Map: icmp_count_map
Tipo del Map: 1
Tamaño de clave del Map: 4
Tamaño de valor del Map: 8
Máximo de entradas del Map: 1024
Clave: 192.168.XX.10, Valor: 320
Clave: 192.168.XX.11, Valor: 2307

Esto mostrará las direcciones IP de origen de los paquetes ICMP DROPEADOS y su número de conteo.

Limpieza

  • Despinado del Map

    $ sudo rm -iv /sys/fs/bpf/xdp/icmp_count_map
    
  • Desmontaje del programa XDP

    $ sudo ip link set dev eth0 xdp off
    

Cargar xdp y programa para consultar el map

Cargar XDP y configurar el pin para acceder al Map usando el comando ip cada vez puede ser tedioso.
Un programa en segundo plano puede obtener el descriptor de archivo del Map cuando se carga el programa XDP,
lo que permite acceder al Map sin configurar el pin.

Además, al finalizar, podemos descargar el programa XDP, evitando olvidar descargarlo o desvincular el pin.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <linux/bpf.h>
#include <linux/if_link.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>
#include <unistd.h>
#include <net/if.h>

int main(int argc, char **argv) {
    struct bpf_object *obj;
    struct bpf_map *map;
    struct bpf_program *prog;
    struct bpf_link *link = NULL;
    int map_fd, prog_fd;
    int err;

    if (argc != 2) {
        fprintf(stderr, "Uso: %s <interface>\n", argv[0]);
        return 1;
    }

    // Cargar programa xdp
    obj = bpf_object__open_file("./xdp.o", NULL);
    if (libbpf_get_error(obj)) {
        perror("Fallo al abrir el objeto BPF");
        return 1;
    }

    if (bpf_object__load(obj)) {
        perror("Fallo al cargar el objeto BPF");
        return 1;
    }

    // Obtener el programa
    prog = bpf_object__find_program_by_name(obj, "xdp_drop_icmp");
    if (libbpf_get_error(prog)) {
        perror("Fallo al encontrar programa BPF por nombre");
        return 1;
    }

    // Obtener el Map
    map = bpf_object__find_map_by_name(obj, "icmp_count_map");
    if (!map) {
        perror("Fallo al encontrar Map BPF por nombre");
        return 1;
    }

    map_fd = bpf_map__fd(map);
    if (map_fd < 0) {
        perror("Fallo al obtener fd del map");
        return 1;
    }

    // Adjuntar el programa XDP a eth0
    const char* if_name = argv[1];
    int ifindex = if_nametoindex(if_name);
    if (ifindex == 0) {
        perror("Fallo al obtener el índice de la interfaz");
        return 1;
    }

    link = bpf_program__attach_xdp(prog, ifindex);
    if (libbpf_get_error(link)) {
        perror("Fallo al adjuntar el programa XDP");
        return 1;
    }

    // Mostrar el estado del mapa cada segundo en un bucle infinito
    while (1) {
        // Línea divisoria
        printf("==============================\n");
        // Mostrar información del Map
        struct bpf_map_info map_info = {};
        uint32_t info_len = sizeof(map_info);
        if (bpf_obj_get_info_by_fd(map_fd, &map_info, &info_len)) {
            perror("Fallo al obtener información del mapa");
            return 1;
        }

        printf("Nombre del Map: %s\n", map_info.name);
        printf("Tipo del Map: %d\n", map_info.type);
        printf("Tamaño de clave del Map: %d\n", map_info.key_size);
        printf("Tamaño de valor del Map: %d\n", map_info.value_size);
        printf("Máximo de entradas del Map: %d\n", map_info.max_entries);

        // Obtener todas las entradas
        uint32_t key, next_key;
        uint64_t value;
        key = next_key = 0;
        while (bpf_map_get_next_key(map_fd, &key, &next_key) == 0) {
            key = next_key; // Usar la clave actualizada
            if (bpf_map_lookup_elem(map_fd, &key, &value) == 0) {
                // Convertir la dirección IP a una cadena
                char ip_str[INET_ADDRSTRLEN];
                inet_ntop(AF_INET, &key, ip_str, sizeof(ip_str));
                printf("Clave: %s, Valor: %lu\n", ip_str, value);
            }
            key = next_key; // Preparar para la próxima iteración
        }
        sleep(1);  // Bucle con un intervalo de 1 segundo
    }

    return 0;
}
  • bpf_object__open_file : Abre el objeto BPF desde un archivo.
    • Especificamos el archivo de objeto compilado de XDP.
  • bpf_object__load : Carga el objeto BPF.
    • Cargar el objeto BPF nos permite obtener programas y Maps.
  • bpf_object__find_program_by_name : Obtiene el programa BPF por nombre.
    • Especificamos el nombre del programa XDP, utilizando el nombre de la función justo después de sec(“xdp”).
  • bpf_object__find_map_by_name : Obtiene el Map BPF por nombre.
    • Especificamos el nombre del Map.
  • bpf_map__fd : Obtiene el descriptor de archivo del Map.
  • bpf_program__attach_xdp : Adjunta el programa XDP a la interfaz especificada.

Compilación y ejecución

  • Compilación

    $ gcc -o map_monitor map_monitor.c -lbpf
    
  • Ejecución

    ./map_monitor eth0
    Nombre del Map: icmp_count_map
    Tipo del Map: 1
    Tamaño de clave del Map: 4
    Tamaño de valor del Map: 8
    Máximo de entradas del Map: 1024
    Clave: 192.168.XX.2, Valor: 4
    ==============================
    Nombre del Map: icmp_count_map
    Tipo del Map: 1
    Tamaño de clave del Map: 4
    Tamaño de valor del Map: 8
    Máximo de entradas del Map: 1024
    Clave: 192.168.XX.2, Valor: 5
    ^C
    

    Salimos con Ctrl+C.

Al finalizar, el programa XDP se desmontará automáticamente.

Conclusión

Hemos escrito un programa que cuenta el número de paquetes DROPEADOS por el programa XDP.
El uso de un Map permite la intercomunicación de datos entre programas eBPF y programas del espacio de usuario,
lo que amplía las aplicaciones de XDP.

Notas finales

Realmente, por razones de rendimiento, deberíamos usar BPF_MAP_TYPE_PERCPU_HASH en lugar de BPF_MAP_TYPE_HASH,
pero por simplicidad, utilizamos BPF_MAP_TYPE_HASH esta vez.

Próximos Pasos (Actualizado: 06/03/2025)

Guía rápida: Introducción a Super XDP Parte 3 (ICMP Echo Reply - Parte 1)