Escrevemos proteção contra ataques DDoS no XDP. Parte nuclear

A tecnologia EXpress Data Path (XDP) permite o processamento arbitrário de tráfego nas interfaces Linux antes que os pacotes cheguem à pilha de rede do kernel. Aplicação de XDP - proteção contra ataques DDoS (CloudFlare), filtros sofisticados, coleta de estatísticas (Netflix). Os programas XDP são executados pela máquina virtual eBPF, portanto, eles têm restrições tanto em seu código quanto nas funções disponíveis do kernel, dependendo do tipo de filtro.


O artigo pretende preencher as deficiências de vários materiais XDP. Em primeiro lugar, eles fornecem código pronto que ignora imediatamente os recursos do XDP: preparado para verificação ou muito simples para causar problemas. Quando você tenta escrever seu código do zero, não há entendimento do que fazer com erros típicos. Em segundo lugar, os métodos para testar localmente o XDP sem VMs e hardware não são cobertos, apesar de terem suas próprias armadilhas. O texto é destinado a programadores familiarizados com redes e Linux interessados ​​em XDP e eBPF.


Nesta parte, examinaremos em detalhes como o filtro XDP é montado e como testá-lo. Em seguida, escreveremos uma versão simples do conhecido mecanismo de cookie SYN no nível de processamento de pacotes. Embora não formemos uma "lista branca"
clientes verificados, mantém contadores e gerencia o filtro - registros suficientes.


Vamos escrever em C - isso não está na moda, mas é prático. Todo o código está disponível no GitHub através do link no final e é dividido em confirmações de acordo com as etapas descritas no artigo.


Isenção de responsabilidade. Durante o artigo, uma mini-solução será desenvolvida para repelir ataques DDoS, porque essa é uma tarefa realista para XDP e minha área. No entanto, o principal objetivo é lidar com a tecnologia, este não é um guia para criar uma proteção pronta. O código de treinamento não é otimizado e omite algumas nuances.


Visão geral do XDP


Vou descrever apenas os principais pontos para não duplicar a documentação e os artigos existentes.


Portanto, o código do filtro é carregado no kernel. Pacotes de entrada são enviados para o filtro. Como resultado, o filtro deve tomar uma decisão: pular o pacote para o kernel ( XDP_PASS ), descartar o pacote ( XDP_DROP ) ou enviá-lo de volta ( XDP_TX ). O filtro pode alterar o pacote, isso é especialmente verdadeiro para XDP_TX . Você também pode travar o programa ( XDP_ABORTED ) e descartar o pacote, mas este é um análogo do assert(0) para depuração.


A máquina virtual eBPF (Berkley Packet Filter estendida) é especialmente simplificada para que o kernel possa verificar se o código não faz loop e não danifica a memória de outra pessoa. Restrições e verificações agregadas:


  • Ciclos proibidos (retroceder).
  • Há uma pilha de dados, mas nenhuma função (todas as funções C devem ser incorporadas).
  • O acesso à memória fora da pilha e do buffer de pacotes é proibido.
  • O tamanho do código é limitado, mas na prática isso não é muito significativo.
  • As chamadas são permitidas apenas para funções especiais do kernel (auxiliares do eBPF).

O design e a instalação do filtro são assim:


  1. O código fonte (por exemplo, kernel.c ) é compilado no objeto ( kernel.o ) sob a arquitetura da máquina virtual eBPF. A partir de outubro de 2019, a compilação no eBPF é suportada por Clang e prometida no GCC 10.1.
  2. Se nesse código de objeto houver chamadas para estruturas do kernel (por exemplo, tabelas e contadores), zeros serão usados ​​em vez de seus IDs, ou seja, esse código não poderá ser executado. Antes de carregar no kernel, você precisa substituir esses zeros pelo ID de objetos específicos criados por meio de chamadas do kernel (código de link). Você pode fazer isso com utilitários externos ou escrever um programa que vinculará e carregará um filtro específico.
  3. O kernel verifica o programa carregado. A ausência de loops e absenteísmo além dos limites do pacote e da pilha é verificada. Se o verificador não puder provar que o código está correto, o programa será rejeitado - você deve poder agradá-lo.
  4. Após a verificação bem-sucedida, o kernel compila o código do objeto de arquitetura eBPF no código de máquina da arquitetura do sistema (just-in-time).
  5. O programa se conecta à interface e começa a processar pacotes.

Como o XDP funciona no kernel, a depuração é feita por logs de rastreamento e, de fato, por pacotes que o programa filtra ou gera. No entanto, o eBPF fornece segurança para o código carregado para o sistema, para que você possa experimentar o XDP diretamente no Linux local.


Preparação do ambiente


Assembléia


O Clang não pode emitir diretamente o código do objeto para a arquitetura do eBPF, portanto, o processo consiste em duas etapas:


  1. Compile o código C no bytecode do LLVM ( clang -emit-llvm ).
  2. Converta bytecode em código de objeto eBPF ( llc -march=bpf -filetype=obj ).

Ao escrever um filtro, alguns arquivos com funções auxiliares e macros dos testes do kernel são úteis. É importante que eles correspondam à versão do kernel ( KVER ). Faça o download em 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 contém o caminho para os cabeçalhos do kernel, ARCH - a arquitetura do sistema. Caminhos e ferramentas podem variar um pouco entre as distribuições.


Exemplo de diferença para o 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 incluem um diretório com cabeçalhos auxiliares e vários diretórios com cabeçalhos do kernel. O símbolo __KERNEL__ significa que os cabeçalhos UAPI (APIs do espaço do usuário) são definidos para o código do kernel, pois o filtro é executado no kernel.


A proteção da pilha pode ser desativada ( -fno-stack-protector ), porque o verificador de código do eBPF ainda procura uma saída da pilha. A otimização deve ser incluída imediatamente porque o tamanho do bytecode do eBPF é limitado.


Vamos começar com um filtro que ignora todos os pacotes e não faz 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"; 

O comando make xdp_filter.o . Onde testá-lo agora?


Suporte de teste


O suporte deve incluir duas interfaces: nas quais haverá um filtro e a partir dos quais os pacotes serão enviados. Esses devem ser dispositivos Linux completos com seu IP para verificar como aplicativos regulares funcionam com nosso filtro.


Dispositivos como o veth (Ethernet virtual) são adequados para nós: eles são um par de interfaces de rede virtual que são "conectadas" diretamente entre si. Você pode criá-los assim (nesta seção, todos os comandos ip são executados como root ):


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

Aqui xdp-remote e xdp-local são nomes de dispositivos. Um filtro será anexado ao xdp-local (192.0.2.1/24) e o tráfego recebido será enviado do xdp-remote (192.0.2.2/24). No entanto, há um problema: as interfaces estão na mesma máquina e o Linux não envia tráfego para uma delas através da outra. Você pode resolver isso com as regras complicadas do iptables , mas elas terão que alterar os pacotes, o que é inconveniente ao depurar. É melhor usar namespaces de rede (namespaces de rede, a seguir netns).


O espaço para nome da rede contém um conjunto de interfaces, tabelas de roteamento e regras do NetFilter, isoladas de objetos semelhantes em outros netns. Cada processo é executado em um espaço para nome e apenas objetos desses netns são acessíveis a ele. Por padrão, o sistema possui um único namespace de rede para todos os objetos, para que você possa trabalhar no Linux e não conhecer netns.


Crie um novo xdp-test nome do xdp-test e mova o xdp-remote .


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

Então o processo em execução no xdp-test não “verá” o xdp-local (ele permanecerá em netns por padrão) e o enviará via xdp-remote ao enviar um pacote para 192.0.2.1, porque esta é a única interface em 192.0.2.0/ 24 disponíveis para esse processo. Isso também funciona na direção oposta.


Ao se mover entre redes, a interface cai e perde o endereço. Para configurar a interface no netns, você precisa executar o ip ... neste espaço de nome do 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 você pode ver, isso não é diferente da configuração de xdp-local no espaço para nome padrão:


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

Se você executar tcpdump -tnevi xdp-local , poderá ver que os pacotes enviados a partir do xdp-test são entregues a esta interface:


 ip netns exec xdp-test ping 192.0.2.1 

É conveniente executar o shell no xdp-test . Há um script no repositório que automatiza o trabalho com o suporte, por exemplo, você pode configurá-lo com o comando sudo ./stand up e excluí-lo com o comando sudo ./stand down .


Rastrear


O filtro está conectado ao dispositivo da seguinte maneira:


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

A -force necessária para vincular um novo programa se outro já estiver vinculado. “Nenhuma notícia é boa notícia” não é sobre esse comando, a conclusão é, de qualquer forma, volumosa. verbose opcional, mas com ele um relatório aparece no trabalho do verificador de código com a listagem de montagem:


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

Desate o programa da interface:


 ip link set dev xdp-local xdp off 

No script, esses são os sudo ./stand attach e sudo ./stand detach .


Ao anexar um filtro, você pode verificar se o ping continua funcionando, mas o programa funciona? Adicione os logs. A função bpf_trace_printk() é semelhante a printf() , mas suporta apenas até três argumentos, exceto o modelo e uma lista limitada de qualificadores. A macro bpf_printk() simplifica a chamada.


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

A saída vai para o canal de rastreamento do kernel, que você precisa ativar:


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

Exibir fluxo de mensagens:


 cat /sys/kernel/debug/tracing/trace_pipe 

Ambos os comandos fazem uma chamada para sudo ./stand log .


O ping agora deve acionar as seguintes mensagens:


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

Se você observar atentamente a saída do verificador, notará cálculos estranhos:


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

O fato é que os programas eBPF não possuem uma seção de dados; portanto, a única maneira de codificar uma sequência de formato é com os argumentos imediatos dos comandos da VM:


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

Por esse motivo, a saída de depuração aumenta bastante o código resultante.


Enviando pacotes XDP


Vamos mudar o filtro: vamos enviar todos os pacotes recebidos de volta. Isso está incorreto do ponto de vista da rede, pois seria necessário alterar os endereços nos cabeçalhos, mas agora o trabalho é importante em princípio.


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

Execute o tcpdump no xdp-remote . Ele deve mostrar a solicitação de eco ICMP de saída e entrada idêntica e parar de mostrar a resposta de eco do ICMP. Mas não mostra. Acontece que, para o XDP_TX funcionar em um programa no xdp-local é necessário que a interface do xdp-remote seja xdp-remote interface xdp-remote , mesmo que esteja vazia, e deve ser aumentada.


Como eu descobri?

O mecanismo de eventos perf, a propósito, usar a mesma máquina virtual permite rastrear o caminho do pacote no kernel , ou seja, o eBPF é usado para desmontar o eBPF.


Você tem que tirar o bem do mal, porque não há mais nada a fazer dele.

 $ 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]) <...> 

O que é o código 6?


 $ errno 6 ENXIO 6 No such device or address 

A função veth_xdp_flush_bq() recebe um código de erro de veth_xdp_xmit() , onde pesquisamos pelo ENXIO e encontramos um comentário.


Restaure o filtro mínimo ( XDP_PASS ) no arquivo xdp_dummy.c , adicione-o ao Makefile, anexe-o ao xdp-remote :


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

Agora tcpdump mostra o que é esperado:


 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 

Se apenas o ARP for exibido, é necessário remover os filtros (isso é feito por sudo ./stand detach ), iniciar o ping , definir os filtros e tentar novamente. O problema é que o filtro XDP_TX afeta o ARP e se a pilha
xdp-test espaço para nome xdp-test conseguiu "esquecer" o endereço MAC 192.0.2.1; ele não conseguirá resolver esse IP.


Declaração do problema


Vamos seguir para a tarefa declarada: escreva o mecanismo de cookies SYN no XDP.


Até agora, a inundação SYN continua sendo um ataque DDoS popular, cuja essência é a seguinte. Ao estabelecer uma conexão (handshake TCP), o servidor recebe um SYN, aloca recursos para uma conexão futura, responde com um pacote SYNACK e aguarda um ACK. O invasor simplesmente envia pacotes SYN a partir de endereços falsos, na quantidade de milhares por segundo, de cada host, a partir de uma botnet de vários milhares. O servidor é forçado a alocar recursos imediatamente após a chegada do pacote e libera um grande tempo limite, como resultado, a memória ou os limites se esgotam, novas conexões não são aceitas e o serviço não está disponível.


Se você não alocar recursos para o pacote SYN, mas apenas responder com um pacote SYNACK, como o servidor pode entender que o pacote ACK que veio depois se refere a um pacote SYN que não foi salvo? Afinal, um invasor também pode gerar ACKs falsos. A essência do cookie SYN é codificar os parâmetros de seqnum no seqnum como um hash de endereços, portas e alterações no salt. Se o ACK conseguiu chegar antes da mudança de sal, é possível calcular novamente o hash e comparar com o acknum . O atacante não pode falsificar um acknum , já que o sal inclui um segredo e não terá tempo para resolver devido ao canal limitado.


O cookie SYN é implementado há muito tempo no kernel do Linux e pode até ser ativado automaticamente se o SYN chegar muito rapidamente e em massa.


Programa educacional sobre handshake TCP

O TCP fornece transferência de dados como um fluxo de bytes; por exemplo, solicitações HTTP são enviadas por TCP. O fluxo é transmitido em pedaços em pacotes. Todos os pacotes TCP possuem sinalizadores lógicos e números de sequência de 32 bits:


  • A combinação de sinalizadores determina o papel de um pacote específico. O sinalizador SYN significa que este é o primeiro pacote de remetente na conexão. O sinalizador ACK significa que o remetente recebeu todos os dados da conexão antes do byte da conta. Um pacote pode ter vários sinalizadores e é chamado por sua combinação, por exemplo, um pacote SYNACK.


  • O número de sequência (seqnum) define o deslocamento no fluxo de dados para o primeiro byte que é transmitido neste pacote. Por exemplo, se no primeiro pacote com X bytes de dados esse número for N, no próximo pacote com novos dados será N + X. No início da conexão, cada lado seleciona esse número arbitrariamente.


  • Número de confirmação (acknum) - o mesmo deslocamento que seqnum, mas não determina o número de bytes a serem transmitidos, mas o número do primeiro byte do destinatário que o remetente não viu.



No início da conexão, as partes devem concordar com seqnum e acknum . O cliente envia um pacote SYN com seu seqnum = X O servidor responde com um pacote SYNACK, onde grava seu seqnum = Y e define acknum = X + 1 . O cliente responde ao SYNACK com um pacote ACK, em que seqnum = X + 1 , acknum = Y + 1 . Depois disso, a transferência real dos dados começa.


Se o interlocutor não confirmar o recebimento do pacote, o TCP o enviará novamente por tempo limite.


Por que os cookies SYN nem sempre são usados?

Primeiramente, se o SYNACK ou o ACK forem perdidos, você terá que aguardar o reenvio - a conexão está mais lenta. Em segundo lugar, no pacote SYN - e apenas nele! - são transmitidas várias opções que afetam a operação adicional da conexão. Sem lembrar os pacotes SYN recebidos, o servidor ignora essas opções. Nos próximos pacotes, o cliente não os enviará mais. Nesse caso, o TCP pode funcionar, mas pelo menos no estágio inicial, a qualidade da conexão diminuirá.


Em termos de pacotes, um programa XDP deve fazer o seguinte:


  • SYNACK com cookie para responder a SYN;
  • responder ao ACK RST (desconectar);
  • descartar outros pacotes.

Pseudocódigo do algoritmo junto com a análise do pacote:


    Ethernet,  .    IPv4,  .     , (*)    ,  .    TCP,  . (**)   SYN,  SYN-ACK  cookie.   ACK,   acknum   cookie,  .      N  . (*)  RST. (**)     . 

Um (*) indica os pontos nos quais controlar o estado do sistema - no primeiro estágio, você pode ficar sem eles, simplesmente implementando um handshake TCP com a geração de um cookie SYN como seqnum.


No local (**) , enquanto não tivermos uma tabela, pularemos o pacote.


Implementação de handshake TCP


Analise o pacote e verifique o código


Precisamos de estruturas de cabeçalho de rede: Ethernet ( uapi/linux/if_ether.h ), IPv4 ( uapi/linux/ip.h ) e TCP ( uapi/linux/tcp.h ). O último que não consegui conectar devido a erros relacionados ao atomic64_t , tive que copiar as definições necessárias no código.


Todas as funções alocadas em C para facilitar a leitura devem ser incorporadas no local da chamada, já que o verificador eBPF no kernel proíbe transições de volta, ou seja, loops e chamadas de função.


 #define INTERNAL static __attribute__((always_inline)) 

A macro LOG() desativa a impressão na compilação do release.


O programa é um transportador de funções. Cada um recebe um pacote no qual o cabeçalho do nível correspondente é realçado, por exemplo, process_ether() espera que o ether esteja cheio. Com base nos resultados da análise de campo, a função pode transferir o pacote para um nível superior. O resultado da função é a ação XDP. Até agora, os manipuladores SYN e ACK passam todos os pacotes.


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

Chamo a atenção para as verificações marcadas A e B. Se você comentar A, o programa será montado, mas haverá um erro de verificação durante o carregamento:


 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! 

A linha principal é invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0) : existem caminhos de execução quando o décimo terceiro byte do início do buffer está fora do pacote. De acordo com a listagem, é difícil entender de qual linha estamos falando, mas há um número de instrução (12) e um desmontador mostrando as linhas do código-fonte:


 llvm-objdump -S xdp_filter.o | less 

Nesse caso, ele aponta para uma string


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

pelo qual fica claro que o problema está no ether . Seria sempre assim.


Responder a SYN


O objetivo nesse estágio é formar um pacote SYNACK correto com um seqnum fixo, que será substituído por um cookie SYN no futuro. Todas as alterações ocorrem em process_tcp_syn() e na área circundante.


Verificação do pacote


Curiosamente, aqui está a linha mais notável, mais precisamente, um comentário sobre ela:


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

Ao escrever a primeira versão do código, o kernel 5.1 foi usado, para o verificador em que havia uma diferença entre data_end e (const void*)ctx->data_end . Ao escrever o artigo, o kernel 5.3.1 não teve esse problema. Talvez o compilador tenha acessado a variável local de maneira diferente do campo. Código moral simplificado pode ajudar com muitos aninhamentos.


Verificações rotineiras adicionais de comprimentos em homenagem ao verificador; sobre MAX_CSUM_BYTES abaixo.


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

Propagação de pacote


Preencha seqnum e acknum , defina ACK (SYN já está definido):


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

Troque portas TCP, endereço IP e endereço MAC. A biblioteca padrão não está acessível no programa XDP, portanto, memcpy() é uma macro que oculta o Clang intrínseco.


 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álculo da soma de verificação


As somas de verificação IPv4 e TCP exigem a adição de todas as palavras de 16 bits nos cabeçalhos, e o tamanho dos cabeçalhos está escrito nelas, ou seja, no momento da compilação é desconhecido. Isso é um problema porque o verificador não pula um loop regular para um limite de variável. Mas o tamanho dos cabeçalhos é limitado: até 64 bytes cada. Você pode fazer um loop com um número fixo de iterações, que podem terminar antes do previsto.


Observo que existe o RFC 1624 sobre como recalcular parcialmente a soma de verificação se apenas palavras de pacotes fixos forem alteradas. No entanto, o método não é universal e a implementação seria mais difícil de manter.


Função de cálculo de soma de verificação:


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

, size , , .


32- :


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

:


 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; 

carry() 32- 16- , RFC 791.


TCP


netcat , ACK, Linux RST-, SYN — SYNACK - , .


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

tcpdump xdp-remote , , hping3 .



XDP . , , . Linux, , SipHash, XDP .


TODO, :


  • XDP- cookie_seed ( ) , , .


  • SYN cookie ACK- , IP , .



:


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

( flags=0x2 — SYN, flags=0x10 — 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 

IP, SYN flood , ACK flood, :


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

:


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

Conclusão


eBPF XDP , . , XDP — , , DPDK kernel bypass. , XDP , , , . , userspace-.


, , , userspace- .


Referências:


Source: https://habr.com/ru/post/pt473286/


All Articles