Estamos escribiendo protección contra ataques DDoS en XDP. parte nuclear

La tecnología eXpress Data Path (XDP) permite realizar un procesamiento de tráfico aleatorio en interfaces de Linux antes de que los paquetes ingresen a la pila de red del kernel. Aplicación de XDP: protección contra ataques DDoS (CloudFlare), filtros complejos, recopilación de estadísticas (Netflix). Los programas XDP son ejecutados por la máquina virtual eBPF, por lo que tienen restricciones tanto en su código como en las funciones del kernel disponibles según el tipo de filtro.

El artículo pretende cubrir las deficiencias de numerosos materiales sobre XDP. En primer lugar, proporcionan código listo para usar que inmediatamente pasa por alto las funciones de XDP: está preparado para la verificación o es demasiado simple para causar problemas. Cuando intentas escribir tu código desde cero, no tienes idea de qué hacer con los errores típicos. En segundo lugar, no se tratan las formas de probar XDP localmente sin una máquina virtual ni hardware, a pesar de que tienen sus propios inconvenientes. El texto está destinado a programadores familiarizados con las redes y Linux que estén interesados ​​en XDP y eBPF.

En esta parte, entenderemos en detalle cómo se ensambla el filtro XDP y cómo probarlo, luego escribiremos una versión simple del conocido mecanismo de cookies SYN en el nivel de procesamiento de paquetes. Aún no crearemos una “lista blanca”
clientes verificados, mantener contadores y administrar el filtro: suficientes registros.

Escribiremos en C: no está de moda, pero es práctico. Todo el código está disponible en GitHub a través del enlace al final y está dividido en confirmaciones según las etapas descritas en el artículo.

Renuncia. A lo largo de este artículo, desarrollaré una minisolución para protegerme de los ataques DDoS, porque es una tarea realista para XDP y mi área de especialización. Sin embargo, el objetivo principal es comprender la tecnología; esta no es una guía para crear protección ya preparada. El código del tutorial no está optimizado y omite algunos matices.

Breve descripción general de XDP

Resumiré sólo los puntos clave para no duplicar la documentación y los artículos existentes.

Entonces, el código del filtro se carga en el kernel. Los paquetes entrantes se pasan al filtro. Como resultado, el filtro debe tomar una decisión: pasar el paquete al kernel (XDP_PASS), soltar paquete (XDP_DROP) o enviarlo de vuelta (XDP_TX). El filtro puede cambiar el paquete, esto es especialmente cierto para XDP_TX. También puede cancelar el programa (XDP_ABORTED) y restablecer el paquete, pero esto es análogo assert(0) - para depurar.

La máquina virtual eBPF (filtro de paquetes Berkley extendido) se simplifica deliberadamente para que el kernel pueda verificar que el código no se repite y no daña la memoria de otras personas. Restricciones y controles acumulativos:

  • Están prohibidos los bucles (al revés).
  • Hay una pila para datos, pero no funciones (todas las funciones C deben estar integradas).
  • Están prohibidos los accesos a la memoria fuera de la pila y del búfer de paquetes.
  • El tamaño del código es limitado, pero en la práctica no es muy significativo.
  • Sólo se permiten llamadas a funciones especiales del kernel (ayudantes de eBPF).

El diseño e instalación de un filtro se ve así:

  1. Código fuente (por ejemplo kernel.c) se compila en el objeto (kernel.o) para la arquitectura de la máquina virtual eBPF. A partir de octubre de 2019, Clang admite la compilación en eBPF y se promete en GCC 10.1.
  2. Si este código objeto contiene llamadas a estructuras del núcleo (por ejemplo, tablas y contadores), sus ID se reemplazan por ceros, lo que significa que dicho código no se puede ejecutar. Antes de cargar en el kernel, debe reemplazar estos ceros con las ID de objetos específicos creados a través de llamadas al kernel (vincule el código). Puede hacer esto con utilidades externas o puede escribir un programa que vincule y cargue un filtro específico.
  3. El kernel verifica el programa cargado. Se comprueba la ausencia de ciclos y la imposibilidad de superar los límites de paquetes y pilas. Si el verificador no puede demostrar que el código es correcto, el programa se rechaza; debe poder complacerlo.
  4. Después de una verificación exitosa, el kernel compila el código objeto de la arquitectura eBPF en código de máquina para la arquitectura del sistema (justo a tiempo).
  5. El programa se conecta a la interfaz y comienza a procesar paquetes.

Dado que XDP se ejecuta en el kernel, la depuración se lleva a cabo mediante registros de seguimiento y, de hecho, paquetes que el programa filtra o genera. Sin embargo, eBPF garantiza que el código descargado sea seguro para el sistema, por lo que puede experimentar con XDP directamente en su Linux local.

Preparando el Ambiente

asamblea

Clang no puede producir directamente código objeto para la arquitectura eBPF, por lo que el proceso consta de dos pasos:

  1. Compile código C en código de bytes LLVM (clang -emit-llvm).
  2. Convertir código de bytes a código objeto eBPF (llc -march=bpf -filetype=obj).

Al escribir un filtro, serán útiles un par de archivos con funciones auxiliares y macros. de las pruebas del kernel. Es importante que coincidan con la versión del kernel (KVER). Descárgalos a 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 BASE

Makefile para Arch Linux (kernel 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 ./*.o

KDIR contiene la ruta a los encabezados del kernel, ARCH — arquitectura del sistema. Las rutas y herramientas pueden variar ligeramente entre distribuciones.

Ejemplo de diferencias para Debian 10 (kernel 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 conecte un directorio con encabezados auxiliares y varios directorios con encabezados del kernel. Símbolo __KERNEL__ significa que los encabezados UAPI (API de espacio de usuario) están definidos para el código del kernel, ya que el filtro se ejecuta en el kernel.

La protección de la pila se puede desactivar (-fno-stack-protector), porque el verificador de código eBPF aún busca violaciones de pilas fuera de límites. Vale la pena activar las optimizaciones de inmediato, porque el tamaño del código de bytes de eBPF es limitado.

Comencemos con un filtro que pasa 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";

Equipo make recoge xdp_filter.o. ¿Dónde probarlo ahora?

Banco de pruebas

El stand debe incluir dos interfaces: en la que habrá un filtro y desde donde se enviarán los paquetes. Deben ser dispositivos Linux completos con sus propias IP para poder comprobar cómo funcionan las aplicaciones normales con nuestro filtro.

Los dispositivos del tipo veth (Ethernet virtual) son adecuados para nosotros: son un par de interfaces de red virtuales "conectadas" directamente entre sí. Puedes crearlos así (en esta sección todos los comandos ip se llevan a cabo desde root):

ip link add xdp-remote type veth peer name xdp-local

es xdp-remote и xdp-local — nombres de dispositivos. En xdp-local (192.0.2.1/24) se adjuntará un 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 a una de ellas a través de la otra. Puedes resolver esto con reglas complicadas. iptables, pero tendrán que cambiar los paquetes, lo cual resulta inconveniente para la depuración. Es mejor utilizar espacios de nombres de red (en adelante netns).

Un espacio de nombres de red contiene un conjunto de interfaces, tablas de enrutamiento y reglas de NetFilter que están aisladas de objetos similares en otras redes. Cada proceso se ejecuta en un espacio de nombres y solo tiene acceso a los objetos de esa red. De forma predeterminada, el sistema tiene un único espacio de nombres de red para todos los objetos, por lo que puede trabajar en Linux y no saber nada de netns.

Creemos un nuevo espacio de nombres. xdp-test y muévelo allí xdp-remote.

ip netns add xdp-test
ip link set dev xdp-remote netns xdp-test

Entonces el proceso que se ejecuta en xdp-test, no "verá" xdp-local (permanecerá en netns por defecto) y al enviar un paquete a 192.0.2.1 lo pasará por xdp-remoteporque es la única interfaz en 192.0.2.0/24 accesible para este proceso. Esto también funciona en la dirección opuesta.

Al moverse entre redes, la interfaz se cae y pierde su dirección. Para configurar la interfaz en netns, debe ejecutar ip ... en este espacio de nombres de comando 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 up

Como puede ver, esto no es diferente de la configuración. xdp-local en el espacio de nombres predeterminado:

    ip address add 192.0.2.1/24 dev xdp-local
    ip link set xdp-local up

Si tu corres tcpdump -tnevi xdp-local, puede ver que los paquetes enviados desde xdp-test, se entregan a esta interfaz:

ip netns exec xdp-test   ping 192.0.2.1

Es conveniente lanzar un shell en xdp-test. El repositorio tiene un script que automatiza el trabajo con el stand, por ejemplo, puedes configurar el stand con el comando sudo ./stand up y borrarlo sudo ./stand down.

Rastreo

El filtro está asociado con el dispositivo de esta manera:

ip -force link set dev xdp-local xdp object xdp_filter.o verbose

Llave -force necesario para vincular un nuevo programa si ya hay otro vinculado. “No hay noticias son buenas noticias” no se trata de este comando, la conclusión es voluminosa en cualquier caso. indicar verbose opcional, pero con él aparece un informe sobre el trabajo del verificador de código con un listado del ensamblado:

Verifier analysis:

0: (b7) r0 = 2
1: (95) exit

Desvincula el programa de la interfaz:

ip link set dev xdp-local xdp off

En el script estos son comandos. sudo ./stand attach и sudo ./stand detach.

Al colocar un filtro, puede asegurarse de que ping continúa ejecutándose, pero ¿funciona el programa? Agreguemos registros. Función bpf_trace_printk() Similar a printf(), pero solo admite hasta tres argumentos distintos del patrón y una lista limitada de especificadores. Macro bpf_printk() simplifica la llamada.

   SEC("prog")
   int xdp_main(struct xdp_md* ctx) {
+      bpf_printk("got packet: %pn", ctx);
       return XDP_PASS;
   }

La salida va al canal de seguimiento del kernel, que debe habilitarse:

echo -n 1 | sudo tee /sys/kernel/debug/tracing/options/trace_printk

Ver hilo de mensajes:

cat /sys/kernel/debug/tracing/trace_pipe

Ambos comandos hacen una llamada. sudo ./stand log.

Ping ahora debería generar mensajes como este:

<...>-110930 [004] ..s1 78803.244967: 0: got packet: 00000000ac510377

Si observa de cerca el resultado del verificador, 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#6
<...>

El hecho es que los programas eBPF no tienen una sección de datos, por lo que la única forma de codificar una cadena de formato son los argumentos inmediatos de los comandos de VM:

$ python -c "import binascii; print(bytes(reversed(binascii.unhexlify('0a7025203a74656b63617020746f67'))))"
b'got packet: %pn'

Por esta razón, la salida de depuración sobrecarga enormemente el código resultante.

Envío de paquetes XDP

Cambiemos el filtro: dejemos que devuelva todos los paquetes entrantes. Esto es incorrecto desde el punto de vista de la red, ya que sería necesario cambiar las direcciones en los encabezados, pero ahora el trabajo, en principio, es importante.

       bpf_printk("got packet: %pn", ctx);
-      return XDP_PASS;
+      return XDP_TX;
   }

Lanzamos tcpdump en xdp-remote. Debería mostrar una solicitud de eco ICMP entrante y saliente idéntica y dejar de mostrar una respuesta de eco ICMP. Pero no se nota. Resulta que por trabajo XDP_TX en el programa de xdp-local necesarioa la interfaz de par xdp-remote También se le asignó un programa, aunque estuviera vacío, y se le levantó.

¿Cómo supe esto?

Rastrear la ruta de un paquete en el kernel El mecanismo de eventos de rendimiento permite, por cierto, utilizar la misma máquina virtual, es decir, eBPF se utiliza para desmontajes con eBPF.

Debes sacar el bien del mal, porque no hay nada más con qué sacarlo.

$ 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é es el código 6?

$ errno 6
ENXIO 6 No such device or address

Función veth_xdp_flush_bq() recibe un código de error de veth_xdp_xmit(), donde buscar por ENXIO y encuentra el comentario.

Restablezcamos el filtro mínimo (XDP_PASS) en archivo xdp_dummy.c, agréguelo al Makefile, vincúlelo a xdp-remote:

ip netns exec remote 
    ip link set dev int xdp object dummy.o

Ahora tcpdump muestra lo que se espera:

62:57:8e:70:44:64 > 26:0e:25:37:8f:96, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 64, id 13762, offset 0, flags [DF], proto ICMP (1), length 84)
    192.0.2.2 > 192.0.2.1: ICMP echo request, id 46966, seq 1, length 64
62:57:8e:70:44:64 > 26:0e:25:37:8f:96, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 64, id 13762, offset 0, flags [DF], proto ICMP (1), length 84)
    192.0.2.2 > 192.0.2.1: ICMP echo request, id 46966, seq 1, length 64

Si en su lugar solo se muestran los ARP, deberá eliminar los filtros (esto no sudo ./stand detach), Déjalo ir ping, luego configura los filtros y vuelve a intentarlo. El problema es que el filtro XDP_TX válido tanto en ARP como si la pila
espacios de nombres xdp-test logró “olvidar” la dirección MAC 192.0.2.1, no podrá resolver esta IP.

Formulación del problema

Pasemos a la tarea indicada: escribir un mecanismo de cookies SYN en XDP.

SYN Flood sigue siendo un ataque DDoS popular, cuya esencia es la siguiente. Cuando se establece una conexión (apretón de manos TCP), el servidor recibe un SYN, asigna recursos para la conexión futura, responde con un paquete SYNACK y espera un ACK. El atacante simplemente envía miles de paquetes SYN por segundo desde direcciones falsificadas desde cada host en una botnet de varios miles de personas. El servidor se ve obligado a asignar recursos inmediatamente después de la llegada del paquete, pero los libera después de un tiempo de espera prolongado; como resultado, la memoria o los límites se agotan, no se aceptan nuevas conexiones y el servicio no está disponible.

Si no asigna recursos en función del paquete SYN, sino que solo responde con un paquete SYNACK, ¿cómo puede entonces el servidor entender que el paquete ACK que llegó más tarde se refiere a un paquete SYN que no se guardó? Después de todo, un atacante también puede generar ACK falsos. El objetivo de la cookie SYN es codificarla en seqnum parámetros de conexión como un hash de direcciones, puertos y sal cambiante. Si el ACK logró llegar antes de que se cambiara la sal, puedes calcular el hash nuevamente y compararlo con acknum. Fragua acknum el atacante no puede, ya que la sal incluye el secreto y no tendrá tiempo de analizarlo debido a un canal limitado.

La cookie SYN se implementó durante mucho tiempo en el kernel de Linux e incluso puede habilitarse automáticamente si las SYN llegan demasiado rápido y en masa.

Programa educativo sobre el protocolo de enlace TCP

TCP proporciona transmisión de datos como un flujo de bytes; por ejemplo, las solicitudes HTTP se transmiten a través de TCP. La transmisión se transmite en pedazos en paquetes. Todos los paquetes TCP tienen indicadores lógicos y números de secuencia de 32 bits:

  • La combinación de banderas determina la función de un paquete en particular. El indicador SYN indica que este es el primer paquete del remitente en la conexión. El indicador ACK significa que el remitente ha recibido todos los datos de conexión hasta el byte acknum. Un paquete puede tener varios indicadores y se llama por su combinación, por ejemplo, un paquete SYNACK.

  • El número de secuencia (seqnum) especifica 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 datos nuevos será N+X. Al inicio de la conexión, cada lado elige este número al azar.

  • Número de acuse de recibo (acknum): el mismo desplazamiento que seqnum, pero no determina el número del byte que se transmite, sino el número del primer byte del destinatario, que el remitente no vio.

Al inicio de la conexión, las partes deben acordar seqnum и acknum. El cliente envía un paquete SYN con su seqnum = X. El servidor responde con un paquete SYNACK, donde registra su seqnum = Y y expone acknum = X + 1. El cliente responde a SYNACK con un paquete ACK, donde seqnum = X + 1, acknum = Y + 1. Después de esto comienza la transferencia de datos propiamente dicha.

Si el interlocutor no acusa recibo del paquete, TCP lo reenvía después de un tiempo de espera.

¿Por qué no siempre se utilizan cookies SYN?

En primer lugar, si se pierde SYNACK o ACK, tendrá que esperar a que se envíe nuevamente; la configuración de la conexión se ralentizará. En segundo lugar, en el paquete SYN, ¡y solo en él! — se transmiten una serie de opciones que afectan el funcionamiento posterior de la conexión. Sin recordar los paquetes SYN entrantes, el servidor ignora estas opciones; el cliente no las enviará en los siguientes paquetes. TCP puede funcionar en este caso, pero al menos en la etapa inicial la calidad de la conexión disminuirá.

En términos de paquetes, un programa XDP debe hacer lo siguiente:

  • responder a SYN con SYNACK con una cookie;
  • responder a ACK con RST (desconectar);
  • deseche los paquetes restantes.

Pseudocódigo del algoritmo junto con el análisis del paquete:

Если это не Ethernet,
    пропустить пакет.
Если это не IPv4,
    пропустить пакет.
Если адрес в таблице проверенных,               (*)
        уменьшить счетчик оставшихся проверок,
        пропустить пакет.
Если это не TCP,
    сбросить пакет.     (**)
Если это SYN,
    ответить SYN-ACK с cookie.
Если это ACK,
    если в acknum лежит не cookie,
        сбросить пакет.
    Занести в таблицу адрес с N оставшихся проверок.    (*)
    Ответить RST.   (**)
В остальных случаях сбросить пакет.

Uno (*) Los puntos donde necesita administrar el estado del sistema están marcados; en la primera etapa, puede prescindir de ellos simplemente implementando un protocolo de enlace TCP con la generación de una cookie SYN como número secuencial.

En el instante (**), mientras no tengamos mesa, nos saltaremos el paquete.

Implementación del protocolo de enlace TCP

Analizando el paquete y verificando el código.

Necesitaremos estructuras de encabezado de red: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) y TCP (uapi/linux/tcp.h). No pude conectar este último debido a errores relacionados con atomic64_t, Tuve que copiar las definiciones necesarias en el código.

Todas las funciones que están resaltadas en C para facilitar la lectura deben estar alineadas en el punto de llamada, ya que el verificador eBPF en el kernel prohíbe el retroceso, es decir, de hecho, bucles y llamadas a funciones.

#define INTERNAL static __attribute__((always_inline))

Macro LOG() deshabilita la impresión en la versión de lanzamiento.

El programa es un transportador de funciones. Cada uno recibe un paquete en el que se resalta el encabezado de nivel correspondiente, por ejemplo, process_ether() espera que se llene ether. Según los resultados del análisis de campo, la función puede pasar el paquete a un nivel superior. El resultado de la función es la acción XDP. Por ahora, los controladores SYN y ACK pasan 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; /* malformed packet */
    }

    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);
}

Llamo su atención sobre las marcas marcadas A y B. Si comenta A, el programa se compilará, pero habrá un error de verificación al cargar:

Verifier analysis:

<...>
11: (7b) *(u64 *)(r10 -48) = r1
12: (71) r3 = *(u8 *)(r7 +13)
invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0)
R7 offset is outside of the packet
processed 11 insns (limit 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0

Error fetching program/map!

Cadena de claves invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0): Hay rutas de ejecución cuando el decimotercer byte desde el principio del búfer está fuera del paquete. Es difícil entender en el listado de qué línea estamos hablando, 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 | less

En este caso apunta a la línea

LOG("Ether(proto=0x%x)", bpf_ntohs(ether->h_proto));

lo que deja claro que el problema es ether. Siempre sería así.

Responder a SYN

El objetivo en esta etapa es generar un paquete SYNACK correcto con un valor fijo seqnum, que será sustituida en el futuro por la cookie SYN. Todos los cambios ocurren en process_tcp_syn() y zonas aledañas.

Verificación del paquete

Curiosamente, aquí está la línea más notable, o mejor dicho, su comentario:

/* 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 kernel 5.1, para cuyo verificador había una diferencia entre data_end и (const void*)ctx->data_end. Al momento de escribir este artículo, el kernel 5.3.1 no tenía este problema. Es posible que el compilador estuviera accediendo a una variable local de manera diferente a un campo. Moraleja de la historia: simplificar el código puede ayudar cuando hay mucho anidamiento.

Lo siguiente son las comprobaciones rutinarias de longitud para gloria del verificador; oh MAX_CSUM_BYTES abajo

const u32 ip_len = ip->ihl * 4;
if ((void*)ip + ip_len > data_end) {
    return XDP_DROP; /* malformed packet */
}
if (ip_len > MAX_CSUM_BYTES) {
    return XDP_ABORTED; /* implementation limitation */
}

const u32 tcp_len = tcp->doff * 4;
if ((void*)tcp + tcp_len > (void*)ctx->data_end) {
    return XDP_DROP; /* malformed packet */
}
if (tcp_len > MAX_CSUM_BYTES) {
    return XDP_ABORTED; /* implementation limitation */
}

Desplegando el paquete

Rellene seqnum и acknum, configure ACK (SYN ya está configurado):

const u32 cookie = 42;
tcp->ack_seq = bpf_htonl(bpf_ntohl(tcp->seq) + 1);
tcp->seq = bpf_htonl(cookie);
tcp->ack = 1;

Intercambie puertos TCP, direcciones IP y direcciones MAC. No se puede acceder a la biblioteca estándar desde el programa XDP, por lo que memcpy() - una macro que oculta los intrínsecos 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);

Nuevo cálculo de sumas de control

Las sumas de comprobación de IPv4 y TCP requieren la adición de todas las palabras de 16 bits en los encabezados y el tamaño de los encabezados está escrito en ellos, es decir, se desconoce en el momento de la compilación. Esto es un problema porque el verificador no omitirá el bucle normal hasta la variable de límite. Pero el tamaño de los encabezados es limitado: hasta 64 bytes cada uno. Puede crear un bucle con un número fijo de iteraciones, que puede finalizar antes de tiempo.

observo que hay RFC 1624 sobre cómo recalcular parcialmente la suma de comprobación si solo se cambian las palabras fijas de los paquetes. Sin embargo, el método no es universal y su implementación sería más difícil de mantener.

Función de cálculo de suma de comprobació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 size verificado por el código de llamada, la segunda condición de salida es necesaria para que el verificador pueda probar la finalización del ciclo.

Para palabras de 32 bits, se implementa una versión más sencilla:

INTERNAL u32
sum16_32(u32 v) {
    return (v >> 16) + (v & 0xffff);
}

En realidad, recalculando las sumas de verificación y enviando el paquete de regreso:

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 << 8;
tcp->check = 0;
tcp_csum += sum16(tcp, tcp_len, data_end);
tcp->check = carry(tcp_csum);

return XDP_TX;

Función carry() realiza una suma de comprobación a partir de una suma de 32 bits de palabras de 16 bits, según RFC 791.

Verificación del protocolo de enlace TCP

El filtro establece correctamente una conexión con netcat, faltando el ACK final, al que Linux respondió con un paquete RST, ya que la pila de red no recibió SYN (se convirtió a SYNACK y se envió de regreso) y desde el punto de vista del sistema operativo, llegó un paquete que no estaba relacionado con la apertura. conexiones.

$ sudo ip netns exec xdp-test   nc -nv 192.0.2.1 6666
192.0.2.1 6666: Connection reset by peer

Es importante consultar con aplicaciones completas y observar. tcpdump en xdp-remote porque, por ejemplo, hping3 no responde a sumas de verificación incorrectas.

Desde el punto de vista de XDP, la verificación en sí es trivial. El algoritmo de cálculo es primitivo y probablemente vulnerable a un atacante sofisticado. El kernel de Linux, por ejemplo, utiliza el sistema criptográfico SipHash, pero su implementación para XDP está claramente fuera del alcance de este artículo.

Introducido para nuevos TODO relacionados con la comunicación externa:

  • El programa XDP no puede almacenar cookie_seed (la parte secreta de la sal) en una variable global, necesita almacenamiento en el kernel, cuyo valor se actualizará periódicamente desde un generador confiable.

  • Si la cookie SYN coincide en el paquete ACK, no necesita imprimir un mensaje, pero recuerde la IP del cliente verificado para continuar pasando paquetes desde él.

Verificación de cliente legítimo:

$ sudoip netns exec xdp-test   nc -nv 192.0.2.1 6666
192.0.2.1 6666: Connection reset by peer

Los registros muestran que la verificación pasó (flags=0x2 - este es SYN, flags=0x10 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 matches for client 20200c0

Si bien no hay una lista de IP verificadas, no habrá protección contra la inundación SYN en sí, pero aquí está la reacción a una inundación ACK iniciada por el siguiente comando:

sudo ip netns exec xdp-test   hping3 --flood -A -s 1111 -p 2222 192.0.2.1

Entradas de registro:

Ether(proto=0x800)
  IP(src=0x15bd11a dst=0x15bd11e proto=6)
    TCP(sport=3236 dport=2222 flags=0x10)
      cookie mismatch

Conclusión

En ocasiones eBPF en general y XDP en particular se presentan más como una herramienta de administración avanzada que como una plataforma de desarrollo. De hecho, XDP es una herramienta para interferir con el procesamiento de paquetes por parte del kernel, y no una alternativa a la pila del kernel, como DPDK y otras opciones de omisión del kernel. Por otro lado, XDP permite implementar una lógica bastante compleja que, además, es fácil de actualizar sin interrumpir el procesamiento del tráfico. El verificador no crea grandes problemas; personalmente, no lo rechazaría para partes del código del 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

Añadir un comentario