Escrivim protecció contra atacs DDoS a XDP. Part nuclear

La tecnologia eXpress Data Path (XDP) permet que el processament de trànsit aleatori es realitzi a les interfícies de Linux abans que els paquets entrin a la pila de xarxa del nucli. Aplicació de XDP: protecció contra atacs DDoS (CloudFlare), filtres complexos, recollida d'estadístiques (Netflix). Els programes XDP els executa la màquina virtual eBPF, de manera que tenen restriccions tant al seu codi com a les funcions disponibles del nucli segons el tipus de filtre.

L'article pretén omplir les mancances de nombrosos materials sobre XDP. En primer lloc, proporcionen codi ja fet que passa immediatament per alt les característiques de XDP: està preparat per a la verificació o és massa senzill per causar problemes. Quan intenteu escriure el vostre codi des de zero, no teniu ni idea de què fer amb els errors típics. En segon lloc, les maneres de provar XDP localment sense una màquina virtual i maquinari no estan cobertes, malgrat que tenen els seus propis inconvenients. El text està pensat per a programadors familiaritzats amb xarxes i Linux que estiguin interessats en XDP i eBPF.

En aquesta part, entendrem detalladament com es munta el filtre XDP i com provar-lo, després escriurem una versió senzilla del conegut mecanisme de cookies SYN a nivell de processament de paquets. Encara no crearem una "llista blanca".
clients verificats, mantenir comptadors i gestionar el filtre: prou registres.

Escriurem en C: no està de moda, però és pràctic. Tot el codi està disponible a GitHub a través de l'enllaç al final i es divideix en commits segons les etapes descrites a l'article.

Descàrrec de responsabilitat. Al llarg d'aquest article, desenvoluparé una mini solució per evitar atacs DDoS, perquè aquesta és una tasca realista per a XDP i la meva àrea d'experiència. Tanmateix, l'objectiu principal és entendre la tecnologia; aquesta no és una guia per crear una protecció ja feta. El codi tutorial no està optimitzat i omet alguns matisos.

Breu descripció de XDP

Esbossaré només els punts clau per no duplicar la documentació i els articles existents.

Per tant, el codi del filtre es carrega al nucli. Els paquets entrants es passen al filtre. Com a resultat, el filtre ha de prendre una decisió: passar el paquet al nucli (XDP_PASS), deixa anar el paquet (XDP_DROP) o enviar-lo de tornada (XDP_TX). El filtre pot canviar el paquet, això és especialment cert per a XDP_TX. També podeu avortar el programa (XDP_ABORTED) i restablir el paquet, però això és anàleg assert(0) - per a la depuració.

La màquina virtual eBPF (Extended Berkley Packet Filter) es simplifica deliberadament perquè el nucli pugui comprovar que el codi no es fa en bucle i no danya la memòria d'altres persones. Restriccions i controls acumulatius:

  • Els bucles (enrere) estan prohibits.
  • Hi ha una pila per a dades, però no hi ha funcions (totes les funcions C han d'estar integrades).
  • Es prohibeixen els accessos a la memòria fora de la pila i el buffer de paquets.
  • La mida del codi és limitada, però a la pràctica això no és gaire significatiu.
  • Només es permeten les trucades a funcions especials del nucli (ajudants eBPF).

Dissenyar i instal·lar un filtre és el següent:

  1. Codi font (ex kernel.c) es compila en objecte (kernel.o) per a l'arquitectura de màquina virtual eBPF. A partir de l'octubre de 2019, la compilació a eBPF compta amb el suport de Clang i es compromet a GCC 10.1.
  2. Si aquest codi d'objecte conté crides a estructures del nucli (per exemple, taules i comptadors), els seus ID se substituiran per zeros, la qual cosa significa que aquest codi no es pot executar. Abans de carregar al nucli, heu de substituir aquests zeros amb els ID d'objectes específics creats mitjançant trucades al nucli (enllaç del codi). Podeu fer-ho amb utilitats externes, o podeu escriure un programa que vinculi i carregarà un filtre específic.
  3. El nucli verifica el programa carregat. Es comprova l'absència de cicles i la fallada per superar els límits de paquets i pila. Si el verificador no pot demostrar que el codi és correcte, el programa es rebutja; heu de poder agradar-lo.
  4. Després de la verificació correcta, el nucli compila el codi d'objecte de l'arquitectura eBPF en codi màquina per a l'arquitectura del sistema (just a temps).
  5. El programa s'adjunta a la interfície i comença a processar els paquets.

Com que XDP s'executa al nucli, la depuració es porta a terme mitjançant registres de traça i, de fet, paquets que filtra o genera el programa. Tanmateix, eBPF garanteix que el codi descarregat sigui segur per al sistema, de manera que podeu experimentar amb XDP directament al vostre Linux local.

Preparant el Medi Ambient

assemblea

Clang no pot produir directament codi objecte per a l'arquitectura eBPF, de manera que el procés consta de dos passos:

  1. Compileu el codi C al bytecode LLVM (clang -emit-llvm).
  2. Converteix bytecode en codi d'objecte eBPF (llc -march=bpf -filetype=obj).

Quan escriu un filtre, seran útils un parell de fitxers amb funcions auxiliars i macros de les proves del nucli. És important que coincideixin amb la versió del nucli (KVER). Descarrega'ls 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 per a Arch Linux (nucli 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 conté el camí a les capçaleres del nucli, ARCH - Arquitectura del sistema. Els camins i les eines poden variar lleugerament entre distribucions.

Exemple de diferències per a Debian 10 (nucli 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 connecteu un directori amb capçaleres auxiliars i diversos directoris amb capçaleres del nucli. Símbol __KERNEL__ significa que les capçaleres UAPI (API d'espai d'usuari) es defineixen per al codi del nucli, ja que el filtre s'executa al nucli.

La protecció de la pila es pot desactivar (-fno-stack-protector), perquè el verificador de codi eBPF encara comprova si hi ha infraccions fora dels límits de la pila. Val la pena activar les optimitzacions immediatament, perquè la mida del bytecode eBPF és limitada.

Comencem amb un filtre que passa tots els paquets i no fa res:

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

Equip make col·lecciona xdp_filter.o. On provar-ho ara?

banc de proves

L'estand ha d'incloure dues interfícies: sobre la qual hi haurà un filtre i des de la qual s'enviaran els paquets. Aquests han de ser dispositius Linux complets amb les seves pròpies IP per comprovar com funcionen les aplicacions habituals amb el nostre filtre.

Els dispositius del tipus veth (Ethernet virtual) són adequats per a nosaltres: es tracta d'un parell d'interfícies de xarxa virtuals "connectades" directament entre si. Podeu crear-los així (en aquesta secció totes les ordres ip es duen a terme des de root):

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

Aquí xdp-remote и xdp-local - noms de dispositius. Encès xdp-local (192.0.2.1/24) s'adjuntarà un filtre, amb xdp-remote (192.0.2.2/24) s'enviarà el trànsit entrant. Tanmateix, hi ha un problema: les interfícies es troben a la mateixa màquina i Linux no enviarà trànsit a una d'elles a través de l'altra. Podeu resoldre això amb regles complicades iptables, però hauran de canviar de paquet, cosa que és inconvenient per a la depuració. És millor utilitzar espais de noms de xarxa (d'ara endavant netns).

Un espai de noms de xarxa conté un conjunt d'interfícies, taules d'encaminament i regles de NetFilter que estan aïllades d'objectes similars en altres netns. Cada procés s'executa en un espai de noms i només té accés als objectes d'aquest netns. Per defecte, el sistema té un únic espai de noms de xarxa per a tots els objectes, de manera que podeu treballar a Linux i no saber sobre netns.

Creem un nou espai de noms xdp-test i traslladar-lo allà xdp-remote.

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

A continuació, el procés s'executa xdp-test, no "veuran" xdp-local (per defecte romandrà a netns) i en enviar un paquet a 192.0.2.1 el passarà xdp-remoteperquè és l'única interfície a 192.0.2.0/24 accessible per a aquest procés. Això també funciona en sentit contrari.

Quan es mou entre netns, la interfície baixa i perd la seva adreça. Per configurar la interfície a netns, cal que executeu ip ... en aquest espai de noms d'ordres 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

Com podeu veure, això no és diferent de la configuració xdp-local a l'espai de noms predeterminat:

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

Si corres tcpdump -tnevi xdp-local, podeu veure que els paquets enviats des de xdp-test, s'entreguen a aquesta interfície:

ip netns exec xdp-test   ping 192.0.2.1

És convenient llançar un shell xdp-test. El repositori té un script que automatitza el treball amb l'estand; per exemple, podeu configurar l'estand amb l'ordre sudo ./stand up i eliminar-lo sudo ./stand down.

Traçat

El filtre està associat amb el dispositiu de la següent manera:

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

Clau -force necessari per enllaçar un programa nou si ja n'hi ha un altre. "Cap notícia és una bona notícia" no tracta d'aquesta comanda, la conclusió és voluminosa en tot cas. indicar verbose opcional, però amb ell apareix un informe sobre el treball del verificador de codi amb una llista de muntatge:

Verifier analysis:

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

Desenllaça el programa de la interfície:

ip link set dev xdp-local xdp off

A l'script, aquestes són ordres sudo ./stand attach и sudo ./stand detach.

Si adjunteu un filtre, podeu assegurar-vos-ho ping continua funcionant, però funciona el programa? Afegim registres. Funció bpf_trace_printk() semblant a printf(), però només admet fins a tres arguments diferents del patró i una llista limitada d'especificadors. Macro bpf_printk() simplifica la trucada.

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

La sortida va al canal de traça del nucli, que s'ha d'habilitar:

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

Visualitza el fil del missatge:

cat /sys/kernel/debug/tracing/trace_pipe

Aquestes dues ordres fan una trucada sudo ./stand log.

Ping hauria d'activar missatges com aquest:

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

Si observeu de prop la sortida del verificador, notareu càlculs estranys:

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 fet és que els programes eBPF no tenen una secció de dades, de manera que l'única manera de codificar una cadena de format són els arguments immediats de les ordres de VM:

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

Per aquest motiu, la sortida de depuració augmenta molt el codi resultant.

Enviament de paquets XDP

Canviem el filtre: deixem que enviï tots els paquets entrants. Això és incorrecte des del punt de vista de la xarxa, ja que caldria canviar les adreces a les capçaleres, però ara el treball en principi és important.

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

Llançament tcpdump en xdp-remote. Hauria de mostrar la mateixa sol·licitud d'eco ICMP sortint i entrant i deixar de mostrar la resposta d'eco ICMP. Però no es veu. Resulta que per feina XDP_TX al programa on xdp-local necessaria la interfície de parella xdp-remote també se li va assignar un programa, encara que fos buit, i es va criar.

Com vaig saber això?

Traceu el camí d'un paquet al nucli El mecanisme d'esdeveniments perf permet, per cert, utilitzar la mateixa màquina virtual, és a dir, eBPF s'utilitza per a desmuntatges amb eBPF.

Heu de fer el bé del mal, perquè no hi ha res més per fer-ho.

$ 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è és el codi 6?

$ errno 6
ENXIO 6 No such device or address

Funció veth_xdp_flush_bq() rep un codi d'error de veth_xdp_xmit(), on cerca ENXIO i troba el comentari.

Restaurem el filtre mínim (XDP_PASS) al fitxer xdp_dummy.c, afegiu-lo al Makefile, enllaçeu-lo xdp-remote:

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

Ara tcpdump mostra el que s'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 només es mostren els ARP, haureu d'eliminar els filtres (això sí sudo ./stand detach), deixar anar ping, després configura els filtres i torna-ho a provar. El problema és que el filtre XDP_TX vàlid tant en ARP com si la pila
espais de noms xdp-test va aconseguir "oblidar" l'adreça MAC 192.0.2.1, no podrà resoldre aquesta IP.

Declaració de problemes

Passem a la tasca indicada: escriure un mecanisme de cookies SYN a XDP.

SYN flood continua sent un atac DDoS popular, l'essència del qual és la següent. Quan s'estableix una connexió (TCP handshake), el servidor rep un SYN, assigna recursos per a la futura connexió, respon amb un paquet SYNACK i espera un ACK. L'atacant simplement envia milers de paquets SYN per segon des d'adreces falsificades de cada host en una botnet de milers de persones. El servidor es veu obligat a assignar recursos immediatament a l'arribada del paquet, però els allibera després d'un temps d'espera important; com a resultat, la memòria o els límits s'esgoten, no s'accepten connexions noves i el servei no està disponible.

Si no assigneu recursos basats en el paquet SYN, sinó que només responeu amb un paquet SYNACK, com pot el servidor entendre que el paquet ACK que va arribar més tard fa referència a un paquet SYN que no s'ha desat? Després de tot, un atacant també pot generar ACK falsos. El punt de la galeta SYN és codificar-la seqnum paràmetres de connexió com un hash d'adreces, ports i sal canviant. Si l'ACK ha aconseguit arribar abans que es canviés la sal, podeu tornar a calcular el hash i comparar-lo amb acknum. Forjar acknum l'atacant no pot, ja que la sal inclou el secret, i no tindrà temps d'esbrinar-lo a causa d'un canal limitat.

La galeta SYN fa temps que s'ha implementat al nucli de Linux i fins i tot es pot activar automàticament si els SYN arriben massa ràpidament i en massa.

Programa educatiu sobre encaixada de mans TCP

TCP proporciona transmissió de dades com un flux de bytes, per exemple, les sol·licituds HTTP es transmeten a través de TCP. El flux es transmet a trossos en paquets. Tots els paquets TCP tenen indicadors lògics i números de seqüència de 32 bits:

  • La combinació de senyals determina el paper d'un paquet concret. El senyalador SYN indica que aquest és el primer paquet del remitent a la connexió. El senyalador ACK significa que el remitent ha rebut totes les dades de connexió fins al byte acknum. Un paquet pot tenir diversos indicadors i s'anomena per la seva combinació, per exemple, un paquet SYNACK.

  • El número de seqüència (seqnum) especifica el desplaçament en el flux de dades per al primer byte que es transmet en aquest paquet. Per exemple, si en el primer paquet amb X bytes de dades aquest número era N, en el següent paquet amb dades noves serà N+X. Al principi de la connexió, cada costat tria aquest número aleatòriament.

  • Número de reconeixement (acknum): el mateix desplaçament que seqnum, però no determina el número del byte que s'està transmetent, sinó el número del primer byte del destinatari, que l'emissor no va veure.

Al començament de la connexió, les parts s'han d'acordar seqnum и acknum. El client envia un paquet SYN amb el seu seqnum = X. El servidor respon amb un paquet SYNACK, on ​​enregistra el seu seqnum = Y i exposa acknum = X + 1. El client respon a SYNACK amb un paquet ACK, on seqnum = X + 1, acknum = Y + 1. Després d'això, comença la transferència de dades real.

Si el peer no reconeix la recepció del paquet, TCP el torna a enviar després d'un temps d'espera.

Per què no s'utilitzen sempre les galetes SYN?

En primer lloc, si es perd SYNACK o ACK, haureu d'esperar que s'enviï de nou; la configuració de la connexió s'alentirà. En segon lloc, al paquet SYN, i només en ell! — es transmeten una sèrie d'opcions que afecten el funcionament posterior de la connexió. Sense recordar els paquets SYN entrants, el servidor ignora aquestes opcions; el client no les enviarà als paquets següents. TCP pot funcionar en aquest cas, però almenys en l'etapa inicial la qualitat de la connexió disminuirà.

Des de la perspectiva dels paquets, un programa XDP ha de fer el següent:

  • respondre a SYN amb SYNACK amb una galeta;
  • respondre a ACK amb RST (desconnectar);
  • descartar els paquets restants.

Pseudocodi de l'algorisme juntament amb l'anàlisi de paquets:

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

Un (*) Es marquen els punts on necessiteu gestionar l'estat del sistema; en la primera etapa, podeu prescindir d'ells simplement implementant un encaix TCP amb la generació d'una galeta SYN com a seqnum.

En el lloc (**), mentre no tinguem taula, ens saltarem el paquet.

Implementació de l'enllaç TCP

Analitzant el paquet i verificant el codi

Necessitarem estructures de capçalera de xarxa: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) i TCP (uapi/linux/tcp.h). No he pogut connectar aquest últim a causa d'errors relacionats amb atomic64_t, vaig haver de copiar les definicions necessàries al codi.

Totes les funcions que es destaquen en C per a la seva llegibilitat s'han d'alinear al punt de trucada, ja que el verificador eBPF del nucli prohibeix el retrocés, és a dir, de fet, els bucles i les trucades de funcions.

#define INTERNAL static __attribute__((always_inline))

Macro LOG() desactiva la impressió a la versió de la versió.

El programa és un transportador de funcions. Cadascun rep un paquet en el qual es destaca la capçalera de nivell corresponent, per exemple, process_ether() espera que s'ompli ether. A partir dels resultats de l'anàlisi de camp, la funció pot passar el paquet a un nivell superior. El resultat de la funció és l'acció XDP. De moment, els controladors SYN i ACK passen tots els paquets.

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

Crido la vostra atenció sobre les comprovacions marcades amb A i B. Si comenteu A, el programa es construirà, però hi haurà un error de verificació en carregar:

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!

Corda clau invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0): Hi ha camins d'execució quan el tretzè byte des de l'inici de la memòria intermèdia està fora del paquet. És difícil entendre a partir de la llista de quina línia estem parlant, però hi ha un número d'instrucció (12) i un desmuntador que mostra les línies del codi font:

llvm-objdump -S xdp_filter.o | less

En aquest cas apunta a la línia

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

que deixa clar que el problema és ether. Sempre seria així.

Respon a SYN

L'objectiu en aquesta etapa és generar un paquet SYNACK correcte amb un paquet fix seqnum, que serà substituïda en el futur per la galeta SYN. Tots els canvis es produeixen a process_tcp_syn() i zones circumdants.

Verificació del paquet

Curiosament, aquí hi ha la línia més notable, o millor dit, el seu comentari:

/* Required to verify checksum calculation */
const void* data_end = (const void*)ctx->data_end;

En escriure la primera versió del codi, es va utilitzar el nucli 5.1, per al verificador del qual hi havia una diferència entre data_end и (const void*)ctx->data_end. En el moment d'escriure, el nucli 5.3.1 no tenia aquest problema. És possible que el compilador accedís a una variable local de manera diferent a un camp. Moral de la història: simplificar el codi pot ajudar quan hi ha molta nidificació.

A continuació hi ha les comprovacions rutinàries de longitud per a la glòria del verificador; O MAX_CSUM_BYTES a continuació

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 */
}

Desplegant el paquet

omplir seqnum и acknum, establiu ACK (SYN ja està configurat):

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

Canvia els ports TCP, l'adreça IP i les adreces MAC. La biblioteca estàndard no és accessible des del programa XDP, per tant memcpy() — una macro que amaga els intrínsecs 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);

Recàlcul de sumes de control

Les sumes de comprovació IPv4 i TCP requereixen l'addició de totes les paraules de 16 bits a les capçaleres, i la mida de les capçaleres s'escriu en elles, és a dir, desconeguda en temps de compilació. Aquest és un problema perquè el verificador no saltarà el bucle normal a la variable límit. Però la mida de les capçaleres és limitada: fins a 64 bytes cadascuna. Podeu fer un bucle amb un nombre fix d'iteracions, que pot acabar abans d'hora.

Noto que n'hi ha RFC 1624 sobre com tornar a calcular parcialment la suma de comprovació si només es canvien les paraules fixes dels paquets. Tanmateix, el mètode no és universal i la implementació seria més difícil de mantenir.

Funció de càlcul de suma de verificació:

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

Encara que size verificat pel codi de trucada, la segona condició de sortida és necessària perquè el verificador pugui demostrar la finalització del bucle.

Per a paraules de 32 bits, s'implementa una versió més senzilla:

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

De fet, recalculant les sumes de comprovació i enviant el paquet de tornada:

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ó carry() fa una suma de comprovació a partir d'una suma de 32 bits de paraules de 16 bits, segons RFC 791.

Verificació de l'enllaç TCP

El filtre estableix correctament una connexió amb netcat, faltava l'ACK final, al qual Linux va respondre amb un paquet RST, ja que la pila de xarxa no va rebre SYN -es va convertir a SYNACK i es va tornar a enviar- i des del punt de vista del sistema operatiu va arribar un paquet que no estava relacionat amb l'obertura. connexions.

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

És important comprovar amb aplicacions completes i observar tcpdump en xdp-remote perquè, per exemple, hping3 no respon a sumes de control incorrectes.

Des del punt de vista de XDP, la verificació en si és trivial. L'algoritme de càlcul és primitiu i probablement vulnerable a un atacant sofisticat. El nucli de Linux, per exemple, utilitza el criptogràfic SipHash, però la seva implementació per a XDP està clarament fora de l'abast d'aquest article.

Introduït per a noves TODO relacionades amb la comunicació externa:

  • El programa XDP no pot emmagatzemar cookie_seed (la part secreta de la sal) en una variable global, necessiteu emmagatzematge al nucli, el valor del qual s'actualitzarà periòdicament des d'un generador fiable.

  • Si la galeta SYN coincideix amb el paquet ACK, no cal que imprimiu cap missatge, però recordeu la IP del client verificat per continuar passant paquets d'aquest.

Verificació legítima del client:

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

Els registres mostren que el control ha passat (flags=0x2 - Això és SYN, flags=0x10 és 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

Tot i que no hi ha cap llista d'IP verificades, no hi haurà protecció contra la inundació SYN en si, però aquí teniu la reacció a una inundació ACK llançada per l'ordre següent:

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

Entrades de registre:

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

Conclusió

De vegades, eBPF en general i XDP en particular es presenten més com una eina avançada d'administrador que com una plataforma de desenvolupament. De fet, XDP és una eina per interferir amb el processament de paquets pel nucli, i no una alternativa a la pila del nucli, com DPDK i altres opcions de bypass del nucli. D'altra banda, XDP permet implementar una lògica força complexa, que, a més, és fàcil d'actualitzar sense interrupcions en el processament del trànsit. El verificador no crea grans problemes; personalment, no ho rebutjaria per a parts del codi de l'espai d'usuari.

A la segona part, si el tema és interessant, completarem la taula de clients verificats i desconnexions, implementarem comptadors i redactarem una utilitat d'espai d'usuari per gestionar el filtre.

Enllaços:

Font: www.habr.com

Afegeix comentari