La tecnología eXpress Data Path (XDP) permite realizar un procesamiento arbitrario del tráfico en las interfaces de Linux antes de que los paquetes lleguen a la pila de red del núcleo. El uso de XDP incluye la protección contra ataques DDoS (CloudFlare), filtros complejos y recopilación de estadísticas (Netflix). Los programas XDP se ejecutan en la máquina virtual eBPF, por lo que tienen limitaciones tanto en su código como en las funciones del núcleo disponibles, dependiendo del tipo de filtro.
Este artículo tiene como objetivo cubrir las deficiencias de numerosos materiales sobre XDP. En primer lugar, estos proporcionan código listo que elude las particularidades de XDP: están adaptados para la verificación o son demasiado simples para causar problemas. Al intentar escribir su propio código desde cero, no hay comprensión sobre cómo manejar los errores característicos. En segundo lugar, no se abordan las maneras de probar XDP localmente sin VM y 'hardware', a pesar de que tienen sus propias 'trampas'. El texto está dirigido a programadores familiarizados con redes y Linux, que están interesados en XDP y eBPF.
En esta sección, analizaremos en detalle cómo se construye un filtro XDP y cómo probarlo, y luego escribiremos una variante simple del conocido mecanismo SYN cookies en el nivel de procesamiento de paquetes. Por ahora, no formaremos una 'lista blanca'
de clientes verificados, llevaremos contadores y gestionaremos el filtro: con los registros es suficiente.
Escribiremos en C: no es moderno, pero es práctico. Todo el código está disponible en GitHub a través del enlace al final y dividido en commits por etapas, descritas en el artículo.
Descargo de responsabilidad. A lo largo del artículo, se desarrollará una mini-solución para mitigar ataques DDoS, ya que es una tarea realista para XDP y mi área de especialización. Sin embargo, el objetivo principal es comprender la tecnología; esto no es una guía para crear una protección lista. El código de ejemplo no está optimizado y omite algunos matices.
Resumen breve de XDP
Solo mencionaré los puntos clave para no duplicar la documentación y los artículos existentes.
Así, se carga código del filtro en el núcleo. Se pasan los paquetes entrantes al filtro. Como resultado, el filtro debe tomar una decisión: permitir el paquete en el núcleo (XDP_PASS), descartar el paquete (XDP_DROP) o enviarlo de vuelta (XDP_TX). El filtro puede modificar el paquete, lo cual es especialmente relevante para XDP_TX. También se puede interrumpir el programa de manera abrupta (XDP_ABORTED) y descartar el paquete, pero esto es análogo a assert(0) — para depuración.
La máquina virtual eBPF (Extended Berkley Packet Filter) ha sido diseñada para ser sencilla, de modo que el núcleo pueda verificar que el código no entra en bucle ni daña la memoria ajena. Limitaciones y comprobaciones generales:
- Se prohíben los ciclos (transiciones hacia atrás).
- Hay una pila para los datos, pero no hay funciones (todas las funciones C deben estar integradas).
- Se prohíben los accesos a la memoria fuera de la pila y del búfer de paquetes.
- El tamaño del código está limitado, pero en la práctica esto no es muy relevante.
- Se permiten solo llamadas a funciones especiales del núcleo (ayudantes de eBPF).
El desarrollo e instalación del filtro se ve así:
- El código fuente (por ejemplo,
kernel.c) se compila en un objeto (kernel.o) para la arquitectura de la máquina virtual eBPF. A partir de octubre de 2019, la compilación en eBPF es compatible con Clang y está prometida en GCC 10.1. - Si este código objeto contiene accesos a estructuras del núcleo (por ejemplo, tablas y contadores), los ID son ceros, es decir, no se puede ejecutar tal código. Antes de cargarlo en el núcleo, es necesario reemplazar estos ceros por los ID de los objetos específicos creados a través de llamadas al núcleo (enlazar el código). Esto se puede hacer con utilidades externas o se puede escribir un programa que enlazará y cargará el filtro específico.
- El núcleo verifica el programa que se va a cargar. Se comprueba la ausencia de ciclos y el no exceder los límites del paquete y de la pila. Si el verificador no puede demostrar que el código es correcto, el programa es rechazado; hay que saber complacerlo.
- Después de una verificación exitosa, el núcleo compila el código objeto de la arquitectura eBPF en código máquina de la arquitectura del sistema (just-in-time).
- El programa se asocia a la interfaz y comienza a procesar paquetes.
Dado que XDP opera en el núcleo, la depuración se realiza mediante los registros de trazado y, en sí, a través de los paquetes que el programa filtra o genera. Sin embargo, eBPF proporciona seguridad al código cargado para el sistema, por lo que se puede experimentar con XDP directamente en Linux local.
Preparación del entorno
Compilación
Clang no puede emitir directamente código objeto para la arquitectura eBPF, por lo que el proceso consta de dos pasos:
- Compilar el código en C a bytecode LLVM (
clang -emit-llvm). - Convertir el bytecode en código objeto eBPF (
llc -march=bpf -filetype=obj).
Al escribir un filtro, será útil tener algunos archivos con funciones y macroas de apoyo . Es importante que sean compatibles con la versión del núcleo (KVER). Descargamos en helpers/:
export KVER=v5.3.7
export BASE=https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/plain/tools/testing/selftests/bpf
wget -P helpers --content-disposition "${BASE}/bpf_helpers.h?h=${KVER}" "${BASE}/bpf_endian.h?h=${KVER}"
unset KVER BASEMakefile para Arch Linux (núcleo 5.3.7):
CLANG ?= clang
LLC ?= llc
KDIR ?= /lib/modules/$(shell uname -r)/build
ARCH ?= $(subst x86_64,x86,$(shell uname -m))
CFLAGS =
-Ihelpers
-I$(KDIR)/include
-I$(KDIR)/include/uapi
-I$(KDIR)/include/generated/uapi
-I$(KDIR)/arch/$(ARCH)/include
-I$(KDIR)/arch/$(ARCH)/include/generated
-I$(KDIR)/arch/$(ARCH)/include/uapi
-I$(KDIR)/arch/$(ARCH)/include/generated/uapi
-D__KERNEL__
-fno-stack-protector -O2 -g
xdp_%.o: xdp_%.c Makefile
$(CLANG) -c -emit-llvm $(CFLAGS) $< -o - |
$(LLC) -march=bpf -filetype=obj -o $@
.PHONY: all clean
all: xdp_filter.o
clean:
rm -f ./*.oKDIR contiene la ruta a los encabezados del núcleo, ARCH — la arquitectura del sistema. Las rutas y herramientas pueden variar un poco entre distribuciones.
Ejemplo de diferencias para Debian 10 (núcleo 4.19.67)
# другая команда
CLANG ?= clang
LLC ?= llc-7
# другой каталог
KDIR ?= /usr/src/linux-headers-$(shell uname -r)
ARCH ?= $(subst x86_64,x86,$(shell uname -m))
# два дополнительных каталога -I
CFLAGS =
-Ihelpers
-I/usr/src/linux-headers-4.19.0-6-common/include
-I/usr/src/linux-headers-4.19.0-6-common/arch/$(ARCH)/include
# далее без измененийCFLAGS incluyen el directorio con los encabezados auxiliares y varios directorios con los encabezados del núcleo. El símbolo __KERNEL__ indica que los encabezados de UAPI (userspace API) se definen para el código del núcleo, ya que el filtro se ejecuta en el núcleo.
Se puede desactivar la protección de pila (-fno-stack-protector), porque el verificador de código eBPF verifica de todos modos el acceso fuera de los límites de la pila. Es recomendable activar las optimizaciones desde el principio, ya que el tamaño del bytecode eBPF está limitado.
Comenzaremos con un filtro que permite todos los paquetes y no hace nada:
#include <uapi/linux/bpf.h>
#include <bpf_helpers.h>
SEC("prog")
int xdp_main(struct xdp_md* ctx) {
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";Comando make recoge xdp_filter.o. ¿Dónde lo probamos ahora?
Comenzamos las pruebas del esquema de HA con el desarrollo de un
El entorno debe incluir dos interfaces: una donde estará el filtro y otra desde la que se enviarán los paquetes. Deben ser dispositivos Linux completos con sus IP, para verificar cómo funcionan las aplicaciones normales con nuestro filtro.
Los dispositivos tipo veth (Ethernet virtual) son adecuados: son un par de interfaces de red virtuales "conectadas" entre sí de forma directa. Se pueden crear así (en esta sección todos los comandos ip se ejecutan desde root):
ip link add xdp-remote type veth peer name xdp-localAquí xdp-remote y xdp-local — son los nombres de los dispositivos. En xdp-local (192.0.2.1/24) se unirá el filtro, con xdp-remote (192.0.2.2/24) se enviará el tráfico entrante. Sin embargo, hay un problema: las interfaces están en la misma máquina, y Linux no enviará tráfico de una a otra. Esto se puede resolver con reglas ingeniosas, iptablespero tendrán que modificar los paquetes, lo que es incómodo para la depuración. Es mejor usar espacios de nombres de red (network namespaces, en adelante netns).
El espacio de nombres de red contiene un conjunto de interfaces, tablas de enrutamiento y reglas de NetFilter, aisladas de los objetos similares en otros netns. Cada proceso opera en un espacio de nombres específico, y solo tiene acceso a los objetos de ese netns. Por defecto, hay un único espacio de nombres de red para todos los objetos en el sistema, por lo que es posible trabajar en Linux sin conocer netns.
Crearemos un nuevo espacio de nombres xdp-test y lo moveremos allí xdp-remote.
ip netns add xdp-test
ip link set dev xdp-remote netns xdp-testEntonces el proceso que se ejecuta en xdp-test, no podrá "ver" xdp-local (se mantendrá en el netns por defecto) y al enviar un paquete a 192.0.2.1, lo transmitirá a través de xdp-remote, porque esta es la única interfaz en 192.0.2.0/24 disponible para este proceso. Esto también funciona en la dirección opuesta.
Al moverse entre netns, la interfaz se baja y pierde su dirección. Para configurar la interfaz en netns, es necesario ejecutar ip ... en este espacio de nombres de comandos ip netns exec:
ip netns exec xdp-test
ip address add 192.0.2.2/24 dev xdp-remote
ip netns exec xdp-test
ip link set xdp-remote upComo se puede ver, esto no difiere de la configuración xdp-local en el espacio de nombres por defecto:
ip address add 192.0.2.1/24 dev xdp-local
ip link set xdp-local upSi ejecutas tcpdump -tnevi xdp-local, podrás ver que los paquetes enviados desde xdp-test, se entregan en esta interfaz:
ip netns exec xdp-test ping 192.0.2.1Es conveniente iniciar un shell en xdp-test. En el repositorio hay un script que automatiza el trabajo con el stand, por ejemplo, puedes configurar el stand con el comando sudo ../stand up y eliminarlo sudo .../stand down.
Trazado
El filtro se vincula al dispositivo de la siguiente manera:
ip -force link set dev xdp-local xdp object xdp_filter.o verboseClave -force es necesario para vincular un nuevo programa si otro ya está vinculado. "No news is good news" no se aplica a este comando, la salida es voluminosa en cualquier caso. Especificar verbose no es obligatorio, pero trae un informe del trabajo del verificador de código con un listado en ensamblador:
Análisis del verificador:
0: (b7) r0 = 2
1: (95) exitDesvincular el programa de la interfaz:
ip link set dev xdp-local xdp offEn el script estas son las órdenes sudo .../stand attach y sudo .../stand detach.
Una vez vinculado el filtro, puedes asegurarte de que ping sigue funcionando, pero ¿lo hace el programa? Agreguemos registros. La función es similar a printf(), pero soporta solo hasta tres argumentos, además de la plantilla, y una lista restringida de especificadores. El macro bpf_printk() facilita la llamada.
SEC("prog")
int xdp_main(struct xdp_md* ctx) {
+ bpf_printk("got packet: %pn", ctx);
return XDP_PASS;
}La salida se dirige al canal de trazado del núcleo, que debe ser habilitado:
echo -n 1 | sudo tee /sys/kernel/debug/tracing/options/trace_printkVer el flujo de mensajes:
cat /sys/kernel/debug/tracing/trace_pipeAmbos comandos hacen una llamada sudo ./stand log.
El ping ahora debería generar mensajes como estos:
-110930 [004] ..s1 78803.244967: 0: got packet: 00000000ac510377Si se observa la salida del verificador, se pueden notar cálculos extraños:
0: (bf) r3 = r1
1: (18) r1 = 0xa7025203a7465
3: (7b) *(u64 *)(r10 -8) = r1
4: (18) r1 = 0x6b63617020746f67
6: (7b) *(u64 *)(r10 -16) = r1
7: (bf) r1 = r10
8: (07) r1 += -16
9: (b7) r2 = 16
10: (85) call bpf_trace_printk#6El problema es que los programas en eBPF no tienen una sección de datos, así que la única manera de codificar la cadena de formato es a través de los argumentos inmediatos de las instrucciones de la máquina virtual:
$ python -c "import binascii; print(bytes(reversed(binascii.unhexlify('0a7025203a74656b63617020746f67'))))"
b'got packet: %pn'Por esta razón, la salida de depuración inflará considerablemente el código final.
Envío de paquetes XDP
Cambiemos el filtro: debe enviar todos los paquetes entrantes de regreso. Esto es incorrecto desde el punto de vista de la red, ya que deberían cambiarse las direcciones en los encabezados, pero lo importante ahora es que funcione.
bpf_printk("got packet: %pn", ctx);
- return XDP_PASS;
+ return XDP_TX;
}Iniciamos tcpdump en xdp-remote. Debe mostrar solicitudes de eco ICMP salientes e entrantes idénticas y dejar de mostrar respuestas de eco ICMP. Pero no lo hace. Resulta que para que funcione XDP_TX en el programa en xdp-local , para que la interfaz correspondiente xdp-remote también tenga un programa asignado, aunque sea vacío, y que esté activada.
¿Cómo lo supe?
se permite mediante el mecanismo de eventos de rendimiento, que utiliza la misma máquina virtual, es decir, se aplica eBPF para analizar eBPF.
Debes hacer el bien del mal, porque no hay otra forma de hacerlo.
$ sudo perf trace --call-graph dwarf -e 'xdp:*'
0.000 ping/123455 xdp:xdp_bulk_tx:ifindex=19 action=TX sent=0 drops=1 err=-6
veth_xdp_flush_bq ([veth])
veth_xdp_flush_bq ([veth])
veth_poll ([veth])¿Qué significa el código 6?
$ errno 6
ENXIO 6 No hay tal dispositivo o direcciónLa función veth_xdp_flush_bq() recibe el código de error de veth_xdp_xmit(), donde se busca ENXIO y se encuentra un comentario.
Restablezcamos el filtro mínimo (XDP_PASS) en el archivo xdp_dummy.c, lo añadimos al Makefile, lo vinculamos a xdp-remote:
ip netns exec remote
ip link set dev int xdp object dummy.oAhora tcpdump muestra lo que se espera:
62:57:8e:70:44:64 > 26:0e:25:37:8f:96, ethertype IPv4 (0x0800), longitud 98: (tos 0x0, ttl 64, id 13762, offset 0, flags [DF], proto ICMP (1), longitud 84)
192.0.2.2 > 192.0.2.1: solicitud de eco ICMP, id 46966, seq 1, longitud 64
62:57:8e:70:44:64 > 26:0e:25:37:8f:96, ethertype IPv4 (0x0800), longitud 98: (tos 0x0, ttl 64, id 13762, offset 0, flags [DF], proto ICMP (1), longitud 84)
192.0.2.2 > 192.0.2.1: solicitud de eco ICMP, id 46966, seq 1, longitud 64Si solo se muestran ARP en su lugar, es necesario quitar los filtros (esto lo hace sudo .../stand detach), dejar pasar ping, luego establecer los filtros y probar nuevamente. El problema es que el filtro XDP_TX también se aplica a ARP, y si la pila
del espacio de nombres xdp-test ha 'olvidado' la dirección MAC 192.0.2.1, no podrá resolver esta IP.
Planteamiento del problema
Pasemos a la tarea declarada: escribir en XDP el mecanismo SYN cookies.
Hasta ahora, el ataque DDoS más popular sigue siendo el SYN flood, cuyo principio es el siguiente. Durante el establecimiento de la conexión (TCP handshake), el servidor recibe un SYN, asigna recursos para la futura conexión, responde con un paquete SYNACK y espera un ACK. El atacante simplemente envía paquetes SYN desde direcciones falsificadas en una cantidad de miles por segundo desde cada host de una botnet de miles de dispositivos. El servidor se ve obligado a asignar recursos inmediatamente al recibir el paquete, y los libera tras un gran tiempo de espera, lo que resulta en el agotamiento de la memoria o límites, no se aceptan nuevas conexiones, el servicio no está disponible.
Si no se asignan recursos por cada paquete SYN, sino que solo se responde con un paquete SYNACK, ¿cómo puede el servidor entender que el paquete ACK, que llega más tarde, corresponde al paquete SYN que no se guardó? Después de todo, el atacante también puede generar ACK falsos. La esencia de SYN cookies es codificar en seqnum los parámetros de la conexión como un hash de direcciones, puertos y una sal cambiante. Si el ACK llega antes de que cambie la sal, se puede calcular el hash nuevamente y compararlo con acknum. El atacante no puede falsificarlo, ya que la sal incluye un secreto, y no podrá hacer pruebas debido a un canal limitado. acknum SYN cookies se ha implementado desde hace tiempo en el núcleo de Linux y incluso puede activarse automáticamente si los SYN llegan demasiado rápido y en masa.
Introducción al TCP handshake
TCP proporciona la transmisión de datos como un flujo de bytes, por ejemplo, las solicitudes HTTP se transmiten sobre TCP. El flujo se envía en fragmentos dentro de paquetes. Todos los paquetes TCP tienen banderas lógicas y números de secuencia de 32 bits:
TCP garantiza la transmisión de datos como un flujo de bytes; por ejemplo, las solicitudes HTTP se envían a través de TCP. El flujo se transmite en fragmentos dentro de paquetes. Todos los paquetes TCP tienen banderas lógicas y números de secuencia de 32 bits:
La combinación de flags determina el papel de un paquete específico. La bandera SYN significa que este es el primer paquete del remitente en la conexión. La bandera ACK significa que el remitente ha recibido todos los datos de conexión hasta el byte.
acknum. Un paquete puede tener varias banderas y se denomina según su combinación, por ejemplo, el paquete SYNACK.El número de secuencia (seqnum) define el desplazamiento en el flujo de datos para el primer byte que se transmite en este paquete. Por ejemplo, si en el primer paquete con X bytes de datos este número era N, en el siguiente paquete con nuevos datos será N+X. Al inicio de la conexión, cada parte elige este número de forma aleatoria.
El número de acuse de recibo (acknum) es un desplazamiento igual al seqnum, pero determina no el número de byte transmitido, sino el número del primer byte del destinatario que el remitente no ha visto.
Al inicio de la conexión, las partes deben acordar seqnum y acknum. El cliente envía un paquete SYN con su seqnum = X. El servidor responde con un paquete SYNACK, en el cual escribe su seqnum = Y y establece acknum = X + 1. El cliente responde al SYNACK con un paquete ACK, donde seqnum = X + 1, acknum = Y + 1. Después de esto, comienza la transmisión de datos propiamente dicha.
Si la contraparte no confirma la recepción del paquete, TCP lo vuelve a enviar tras un tiempo de espera.
¿Por qué no se utilizan siempre las cookies SYN?
En primer lugar, si se pierde el SYNACK o el ACK, se tendrá que esperar el reenvío, lo que ralentiza el establecimiento de la conexión. En segundo lugar, en el paquete SYN — ¡y solo en él! — se envían una serie de opciones que afectan el funcionamiento posterior de la conexión. Al no almacenar los paquetes SYN entrantes, el servidor ignora estas opciones, en los siguientes paquetes el cliente ya no las enviará. TCP puede funcionar así, pero al menos en la etapa inicial, la calidad de la conexión disminuirá.
Desde el punto de vista de los paquetes, el programa XDP debe hacer lo siguiente:
- responder al SYN con un SYNACK con cookie;
- responder al ACK con un RST (romper la conexión);
- desechar los demás paquetes.
Pseudocódigo del algoritmo junto con el análisis del paquete:
Si no es Ethernet,
omitir el paquete.
Si no es IPv4,
omitir el paquete.
Si la dirección está en la tabla de verificación, (*)
reducir el contador de verificaciones restantes,
omitir el paquete.
Si no es TCP,
eliminar el paquete. (**)
Si es SYN,
responder con SYN-ACK y una cookie.
Si es ACK,
si en acknum no hay cookie,
eliminar el paquete.
Registrar en la tabla la dirección con N verificaciones restantes. (*)
Responder RST. (**)
En los demás casos eliminar el paquete.Uno (*) señaladas las partes en las que es necesario gestionar el estado del sistema; en la primera fase se puede prescindir de ellas, simplemente implementando el apretón de manos TCP generando una cookie SYN como seqnum.
En su lugar (**), mientras no tengamos una tabla, estaremos omitiendo el paquete.
Implementación del apretón de manos TCP
Análisis del paquete y verificación del código
Necesitaremos estructuras para los encabezados de red: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) y TCP (uapi/linux/tcp.h). El último no pude conectarlo debido a errores relacionados con atomic64_t, tuve que copiar las definiciones necesarias en el código.
Todas las funciones que en C se definen para facilitar la lectura, deben ser incorporadas en el lugar de la llamada, ya que el verificador eBPF en el núcleo prohíbe los saltos hacia atrás, es decir, en esencia, bucles y llamadas de funciones.
#define INTERNAL static __attribute__((always_inline))Macro LOG() desactiva la impresión en la compilación de lanzamiento.
El programa consiste en una canalización de funciones. Cada una toma un paquete, en el que está reservado el encabezado del nivel correspondiente, por ejemplo, process_ether() espera que esté lleno ether. Según los resultados del análisis de los campos, la función puede pasar el paquete al nivel superior. El resultado del trabajo de la función es una acción XDP. Por ahora, los manejadores SYN y ACK omiten todos los paquetes.
struct Packet {
struct xdp_md* ctx;
struct ethhdr* ether;
struct iphdr* ip;
struct tcphdr* tcp;
};
INTERNAL int process_tcp_syn(struct Packet* packet) { return XDP_PASS; }
INTERNAL int process_tcp_ack(struct Packet* packet) { return XDP_PASS; }
INTERNAL int process_tcp(struct Packet* packet) { ... }
INTERNAL int process_ip(struct Packet* packet) { ... }
INTERNAL int
process_ether(struct Packet* packet) {
struct ethhdr* ether = packet->ether;
LOG("Ether(proto=0x%x)", bpf_ntohs(ether->h_proto));
if (ether->h_proto != bpf_ntohs(ETH_P_IP)) {
return XDP_PASS;
}
// B
struct iphdr* ip = (struct iphdr*)(ether + 1);
if ((void*)(ip + 1) > (void*)packet->ctx->data_end) {
return XDP_DROP; /* paquete malformado */
}
packet->ip = ip;
return process_ip(packet);
}
SEC("prog")
int xdp_main(struct xdp_md* ctx) {
struct Packet packet;
packet.ctx = ctx;
// A
struct ethhdr* ether = (struct ethhdr*)(void*)ctx->data;
if ((void*)(ether + 1) > (void*)ctx->data_end) {
return XDP_PASS;
}
packet.ether = ether;
return process_ether(&packet);
}Presto atención a las verificaciones marcadas como A y B. Si comentas A, el programa se compilará, pero al cargar habrá un error de verificación:
Análisis del verificador:
11: (7b) *(u64 *)(r10 -48) = r1
12: (71) r3 = *(u8 *)(r7 +13)
acceso inválido al paquete, off=13 tamaño=1, R7(id=0,off=0,r=0)
El desplazamiento de R7 está fuera del paquete
procesadas 11 instrucciones (límite 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0
Error al obtener el programa/mapa!Cadena clave acceso inválido al paquete, off=13 tamaño=1, R7(id=0,off=0,r=0): hay caminos de ejecución donde el decimotercer byte desde el comienzo del búfer está fuera del paquete. La lista es complicada de entender, pero hay un número de instrucción (12) y un desensamblador que muestra las líneas del código fuente:
llvm-objdump -S xdp_filter.o | lessEn este caso, se refiere a la línea
LOG("Ether(proto=0x%x)", bpf_ntohs(ether->h_proto));en la que es claro que el problema está en ether. Siempre sería así.
Respuesta a SYN
El objetivo en esta etapa es formar un paquete SYNACK correcto con un seqnum, que posteriormente se reemplazará por un cookie SYN. Todos los cambios ocurren en process_tcp_syn() y sus alrededores.
Verificación del paquete
Curiosamente, aquí está la línea más notable, más bien, un comentario sobre ella:
/* Required to verify checksum calculation */
const void* data_end = (const void*)ctx->data_end;Al escribir la primera versión del código, se utilizó el núcleo 5.1, cuya diferencia para el verificador era entre data_end y (const void*)ctx->data_end. Al escribir este artículo, el núcleo 5.3.1 no tenía tal problema. Puede que el compilador tratara la variable local de manera diferente a un campo. La moraleja es que con una gran profundidad, simplificar el código puede ayudar.
A continuación, revisiones rutinarias de longitudes en honor al verificador; sobre MAX_CSUM_BYTES más abajo.
const u32 ip_len = ip->ihl * 4;
if ((void*)ip + ip_len > data_end) {
return XDP_DROP; /* paquete malformado */
}
if (ip_len > MAX_CSUM_BYTES) {
return XDP_ABORTED; /* limitación de implementación */
}
const u32 tcp_len = tcp->doff * 4;
if ((void*)tcp + tcp_len > (void*)ctx->data_end) {
return XDP_DROP; /* paquete malformado */
}
if (tcp_len > MAX_CSUM_BYTES) {
return XDP_ABORTED; /* limitación de implementación */
}Reinversión del paquete
Llenamos seqnum y acknum, se establece ACK (SYN ya establecido):
const u32 cookie = 42;
tcp->ack_seq = bpf_htonl(bpf_ntohl(tcp->seq) + 1);
tcp->seq = bpf_htonl(cookie);
tcp->ack = 1;Intercambiamos los puertos TCP, direcciones IP y direcciones MAC. La biblioteca estándar no está disponible desde el programa XDP, por lo que memcpy() es un macro que oculta el intrínseco de Clang.
const u16 temp_port = tcp->source;
tcp->source = tcp->dest;
tcp->dest = temp_port;
const u32 temp_ip = ip->saddr;
ip->saddr = ip->daddr;
ip->daddr = temp_ip;
struct ethhdr temp_ether = *ether;
memcpy(ether->h_dest, temp_ether.h_source, ETH_ALEN);
memcpy(ether->h_source, temp_ether.h_dest, ETH_ALEN);Recalculo de sumas de verificación
Las sumas de verificación de IPv4 y TCP requieren la suma de todas las palabras de 16 bits en los encabezados, y el tamaño de los encabezados está escrito en ellos, es decir, en el momento de la compilación no se conoce. Este es un problema, porque el validador no permitirá un bucle normal hasta la variable de límite. Sin embargo, el tamaño de los encabezados está limitado: hasta 64 bytes cada uno. Se puede hacer un bucle con un número fijo de iteraciones que puede terminar anticipadamente.
Cabe señalar que hay sobre cómo recalcular la suma de verificación parcialmente, si solo se han cambiado palabras fijas de los paquetes. Sin embargo, el método no es universal y sería más difícil de mantener la implementación.
Función de cálculo de la suma de verificación:
#define MAX_CSUM_WORDS 32
#define MAX_CSUM_BYTES (MAX_CSUM_WORDS * 2)
INTERNAL u32
sum16(const void* data, u32 size, const void* data_end) {
u32 s = 0;
#pragma unroll
for (u32 i = 0; i < MAX_CSUM_WORDS; i++) {
if (2*i >= size) {
return s; /* normal exit */
}
if (data + 2*i + 1 + 1 > data_end) {
return 0; /* should be unreachable */
}
s += ((const u16*)data)[i];
}
return s;
}A pesar de que tamaño verificado por el código que llama, se requiere una segunda condición de salida para que el validador pueda demostrar la terminación del bucle.
Para las palabras de 32 bits, se implementó una versión más simple:
INTERNAL u32
sum16_32(u32 v) {
return (v >> 16) + (v & 0xffff);
}En realidad, el recálculo de las sumas de verificación y el envío del paquete de vuelta:
ip->check = 0;
ip->check = carry(sum16(ip, ip_len, data_end));
u32 tcp_csum = 0;
tcp_csum += sum16_32(ip->saddr);
tcp_csum += sum16_32(ip->daddr);
tcp_csum += 0x0600;
tcp_csum += tcp_len <check = 0;
tcp_csum += sum16(tcp, tcp_len, data_end);
tcp->check = carry(tcp_csum);
return XDP_TX;La función carry() convierte una suma de 32 bits de palabras de 16 bits en una suma de verificación, según la RFC 791.
Verificación del saludo TCP
El filtro establece correctamente la conexión con netcat, omitiendo el ACK final, al cual Linux respondió con un paquete RST, ya que la pila de red no recibió el SYN: se modificó a un SYNACK y se envió de vuelta, y desde el punto de vista del sistema operativo, el paquete llegó sin relación a las conexiones abiertas.
$ sudo ip netns exec xdp-test nc -nv 192.0.2.1 6666
192.0.2.1 6666: Conexión restablecida por el parEs importante probar con aplicaciones completas y observar tcpdump en xdp-remote porque, por ejemplo, hping3 no reacciona a sumas de verificación incorrectas.
SYN cookie
Desde la perspectiva de XDP, la propia verificación es trivial. El algoritmo de cálculo es primitivo y, probablemente, vulnerable a un atacante sofisticado. El núcleo de Linux, por ejemplo, utiliza SipHash criptográfico, pero su implementación para XDP claramente está más allá del alcance del artículo.
Apareció para nuevas tareas TODO relacionadas con la interacción externa:
El programa XDP no puede almacenar
cookie_seed(la parte secreta de la sal) en una variable global, se requiere almacenamiento en el núcleo, cuyo valor se actualizará periódicamente desde un generador confiable.Al coincidir SYN cookie en el paquete ACK, no se debe imprimir un mensaje, sino recordar la IP del cliente verificado para luego permitir los paquetes de él.
Verificación por un cliente legítimo:
$ sudo ip netns exec xdp-test nc -nv 192.0.2.1 6666
192.0.2.1 6666: Conexión restablecida por el peerEn los registros se ha registrado el paso de la verificación (flags=0x2 — esto es SYN, flags=0x10 — esto es ACK):
Ether(proto=0x800)
IP(src=0x20e6e11a dst=0x20e6e11e proto=6)
TCP(sport=50836 dport=6666 flags=0x2)
Ether(proto=0x800)
IP(src=0xfe2cb11a dst=0xfe2cb11e proto=6)
TCP(sport=50836 dport=6666 flags=0x10)
cookie coincide para el cliente 20200c0Mientras no haya una lista de IPs verificadas, no habrá protección contra inundaciones SYN, pero aquí está la reacción ante una inundación ACK, que se inicia con el siguiente comando:
sudo ip netns exec xdp-test hping3 --flood -A -s 1111 -p 2222 192.0.2.1Entradas en el registro:
Ether(proto=0x800)
IP(src=0x15bd11a dst=0x15bd11e proto=6)
TCP(sport=3236 dport=2222 flags=0x10)
cookie no coincideConclusión
A veces, eBPF en general y XDP en particular se perciben más como una herramienta para administradores avanzados que como una plataforma para desarrollo. De hecho, XDP es una herramienta de intervención en el procesamiento de paquetes por parte del núcleo, y no una alternativa a la pila del núcleo, como DPDK y otras variantes de bypass del núcleo. Por otro lado, XDP permite implementar lógica bastante compleja, que además se puede actualizar fácilmente sin interrumpir el procesamiento del tráfico. El verificador no crea grandes problemas, yo personalmente no renunciaría a algo así para partes del código de espacio de usuario.
En la segunda parte, si el tema es interesante, completaremos la tabla de clientes verificados y desconexiones, implementaremos contadores y escribiremos una utilidad de espacio de usuario para gestionar el filtro.
Enlaces:
Fuente: habr.com
