Resumen
Este es un artículo sobre cómo tocar rápidamente XDP.
Por lo tanto, omitiré las explicaciones detalladas sobre eBPF y otros temas.
XDP es un marco para procesar paquetes en la etapa más temprana de la pila de red del núcleo de Linux, y permite insertar programas directamente en la tarjeta de interfaz de red (NIC) utilizando eBPF.
Al operar paquetes en la etapa más temprana de la pila de red del núcleo de Linux, se puede procesar de manera más rápida que filtros como iptables.
En resumen, puedes tener la noción de que los programas XDP se pueden adjuntar a la interfaz de la NIC usando eBPF.
Configuración del entorno
Se asume Ubuntu 22.04.
Para controlar la comunicación de la NIC con XDP, se recomienda probar en una máquina virtual o similar.
Instalación de paquetes necesarios
$ sudo apt install build-essential clang llvm gcc-multilib libbpf-dev
Usaremos clang en lugar de gcc para compilar eBPF (XDP), y es necesario instalar libbpf-dev porque se requieren archivos de encabezado específicos para eBPF.
gcc-multilib se necesita más adelante para usar asm/types.h
.
Vamos a empezar: DROPA TODOS LOS PAQUETES
Verificación de la interfaz
Primero, comprobemos si hay actividad en la interfaz.
$ sudo tcpdump -i eht0 -n
Normalmente, si está conectado a un switch, debería haber tráfico ARP, entre otros.
Si no hay actividad, intenta enviar un ping desde el exterior para verificar que se están enviando paquetes.
Preparación del programa XDP
XDP para DROPA todo el tráfico
- xdp.c
#include <linux/bpf.h> #include <bpf/bpf_helpers.h> SEC("xdp") int xdp_drop(struct xdp_md *ctx) { return XDP_DROP; } char _license[] SEC("license") = "MIT";
#include <linux/bpf.h>
- Archivo de encabezado que proporciona funcionalidades relacionadas con BPF (Berkeley Packet Filter) en Linux.
#include <bpf/bpf_helpers.h>
- Archivo de encabezado que proporciona funciones auxiliares necesarias para escribir programas eBPF.
SEC("xdp")
- Macro que especifica el tipo de programa eBPF de la función definida a continuación.
- Especificando “xdp”, indicamos que la función que se define a continuación es un programa XDP.
int xdp_drop(struct xdp_md *ctx)
- La función xdp_drop se llamará cada vez que se reciba un paquete.
En este caso, el programa devolveráreturn XDP_DROP;
, lo que resultará en que todos los paquetes se DROPEEN.
- La función xdp_drop se llamará cada vez que se reciba un paquete.
char _license[] SEC("license") = "MIT";
- Los programas eBPF requieren una especificación de licencia para ejecutarse.
- Si se ejecutan funciones derivadas de GPL, se producirá un error si la licencia no es GPL.
Compilación
Compilación
$ clang -O2 -target bpf -c xdp.c -o xdp.o
Al especificar
bpf
como target en la compilación, se generará un objeto para eBPF.
Adjuntar a la interfaz
Adjuntar a la interfaz
$ sudo ip link set dev eth0 xdp obj xdp.o sec xdp
Verificación de paquetes
$ sudo tcpdump -i eth0 -n
Ahora no deberían llegar los paquetes ARP, ICMP, etc.
Dado que XDP se procesa antes que tcpdump, no podrás observar los paquetes a través de tcpdump.
Desadjuntar XDP
$ sudo ip link set dev eth0 xdp off
Con esto, el programa XDP que DROPA todos los paquetes se ha deshabilitado,
y ahora la comunicación debería estar funcionando.
Profundizando un poco más (DROPA ICMP)
DROPA todos los paquetes no es muy útil, ya que es casi lo mismo que ip link set down dev eth0
,
así que intentemos usar XDP para DROPA solo ICMP.
- xdp.c
#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> 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; // DROPA el paquete ICMP return XDP_DROP; } char _license[] SEC("license") = "MIT";
En ctx, podemos extraer punteros del paquete y usarlos para el procesamiento.
- data
- Puntero a los datos del paquete.
- data_end
- Puntero al final de data.
struct ethhdr *eth = data;
if ((void *)eth + sizeof(*eth) > data_end)
return XDP_PASS;
Asignamos el puntero de cabecera Ethernet a data.
Si el puntero eth más el tamaño de la cabecera es mayor que data_end,
significa que el tamaño del paquete es menor que la cabecera Ethernet, así que no lo procesamos y lo pasamos al Kernel.
if (eth->h_proto != htons(ETH_P_IP))
return XDP_PASS;
Comprobamos la cabecera eth y verificamos que h_proto sea ETH_P_IP (protocolo IPv4 0x0800). htons
es una macro que convierte el endianness de la máquina (orden de bytes de host) al endianness del paquete (orden de bytes de red). htons
significa “host to network short”.
Si el protocolo contenido en el paquete Ethernet no es IPv4, devolvemos XDP_PASS
y lo pasamos al Kernel.
struct iphdr *ip = data + sizeof(*eth);
if ((void *)ip + sizeof(*ip) > data_end)
return XDP_PASS;
Estamos obteniendo el puntero de la cabecera IP.
Agregamos el tamaño de la cabecera Ethernet al puntero inicial para obtener el puntero al inicio de la cabecera IP.
De igual manera, verificamos si el puntero de la cabecera IP más su tamaño no excede data_end. Si excede, no es una cabecera IP y pasamos el control al kernel con XDP_PASS
.
if (ip->protocol != IPPROTO_ICMP)
return XDP_PASS;
Verificamos que el protocolo en el paquete IP sea IPPROTO_ICMP (1).
Cualquier otro protocolo se pasará al kernel mediante XDP_PASS
.
struct icmphdr *icmp = (void *)ip + sizeof(*ip);
if ((void *)icmp + sizeof(*icmp) > data_end)
return XDP_PASS;
Obtenemos el puntero de la cabecera ICMP.
Sumamos el tamaño de la cabecera IP al puntero de la cabecera IP para obtener el puntero a la cabecera ICMP.
También verificamos que no exceda data_end.
Dado que en este punto, el paquete que no ha pasado XDP_PASS
es ICMP, podemos lograr nuestro propósito devolviendo XDP_DROP
para el paquete.
Por último
Puede parecer un poco difícil porque debe escribirse en C, pero al intentarlo, creo que es un logro bastante simple.