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

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

O artigo destina-se a compensar as deficiências de vários materiais no XDP. Primeiro, eles fornecem um código pronto que ignora imediatamente os recursos do XDP: preparado para verificação ou simples demais para causar problemas. Quando você tenta escrever seu próprio código do zero mais tarde, não há compreensão do que fazer com os erros típicos. Em segundo lugar, não cobre maneiras de testar o XDP localmente sem uma VM e hardware, apesar do fato de que eles têm suas próprias armadilhas. O texto é destinado a programadores familiarizados com redes e Linux interessados ​​em XDP e eBPF.

Nesta parte, entenderemos em detalhes como o filtro XDP é montado e como testá-lo, então escreveremos uma versão simples do conhecido mecanismo de cookies SYN no nível de processamento de pacotes. Até formarmos uma "lista branca"
clientes verificados, manter contadores e gerenciar o filtro - logs suficientes.

Vamos escrever em C - isso não está na moda, mas é prático. Todo o código está disponível no GitHub no link ao final e está dividido em commits conforme os passos descritos no artigo.

Aviso Legal. No decorrer do artigo será desenvolvida uma mini-solução para repelir ataques DDoS, pois esta é uma tarefa realista para XDP e minha área. No entanto, o objetivo principal é entender a tecnologia, não é um guia para criar proteção pronta. O código do tutorial não é otimizado e omite algumas nuances.

Uma Breve Visão Geral do XDP

Indicarei apenas os pontos-chave para não duplicar a documentação e os artigos existentes.

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

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

  • Loops (saltos para trás) são proibidos.
  • Há uma pilha de dados, mas nenhuma função (todas as funções C devem ser embutidas).
  • Os acessos à memória fora da pilha e do buffer de pacotes são proibidos.
  • O tamanho do código é limitado, mas na prática isso não é muito significativo.
  • Somente funções especiais do kernel (eBPF helpers) são permitidas.

Desenvolver e instalar um filtro se parece com isto:

  1. código-fonte (ex. kernel.c) compila para objeto (kernel.o) para a arquitetura de máquina virtual eBPF. A partir de outubro de 2019, a compilação para eBPF é suportada pelo Clang e prometida no GCC 10.1.
  2. Se neste código objeto houver chamadas para estruturas do kernel (por exemplo, para tabelas e contadores), em vez de seus IDs haverá zeros, ou seja, esse código não poderá ser executado. Antes de carregar no kernel, esses zeros devem ser substituídos pelos IDs de objetos específicos criados por meio de chamadas do kernel (vincule o código). Você pode fazer isso com utilitários externos ou pode escrever um programa que vinculará e carregará um filtro específico.
  3. O kernel verifica o programa que está sendo carregado. Verifica a ausência de ciclos e a não saída dos limites da embalagem e da pilha. Se o verificador não puder provar que o código está correto, o programa é rejeitado - é preciso agradá-lo.
  4. Após a verificação bem-sucedida, o kernel compila o código do objeto da arquitetura eBPF no código da máquina da arquitetura do sistema (just-in-time).
  5. O programa é anexado à interface e começa a processar os pacotes.

Como o XDP é executado no kernel, a depuração é baseada em logs de rastreamento e, na verdade, em pacotes que o programa filtra ou gera. No entanto, o eBPF mantém o código baixado seguro para o sistema, para que você possa experimentar o XDP diretamente no seu Linux local.

Preparando o Ambiente

montagem

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

  1. Compilar código C para bytecode LLVM (clang -emit-llvm).
  2. Converter bytecode em código de objeto eBPF (llc -march=bpf -filetype=obj).

Ao escrever um filtro, alguns arquivos com funções auxiliares e macros serão úteis de testes de kernel. É importante que correspondam à versão do kernel (KVER). Baixe-os para 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 - arquitetura do sistema. Caminhos e ferramentas podem variar ligeiramente entre as distribuições.

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

A proteção de pilha pode ser desativada (-fno-stack-protector) porque o verificador de código eBPF verifica os limites fora da pilha de qualquer maneira. Você deve ativar as otimizações imediatamente, porque o tamanho do bytecode eBPF é limitado.

Vamos começar com um filtro que passa 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";

Equipe make recolhe xdp_filter.o. Onde você pode testá-lo agora?

bancada de teste

O estande deve incluir duas interfaces: na qual haverá um filtro e de onde os pacotes serão enviados. Estes devem ser dispositivos Linux completos com seus próprios IPs para verificar como os aplicativos regulares funcionam com nosso filtro.

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

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

é xdp-remote и xdp-local — nomes de dispositivos. Sobre xdp-local (192.0.2.1/24) será anexado um filtro, com xdp-remote (192.0.2.2/24) o tráfego de entrada será enviado. Porém, há um problema: as interfaces estão na mesma máquina e o Linux não enviará tráfego para uma delas pela outra. Você pode resolvê-lo com regras complicadas iptables, mas eles terão que alterar os pacotes, o que é inconveniente durante a depuração. É melhor usar namespaces de rede (namespaces de rede, outros netns).

O namespace da rede contém um conjunto de interfaces, tabelas de roteamento e regras do NetFilter que são isoladas de objetos semelhantes em outras redes. Cada processo roda em algum namespace, e somente os objetos deste netns estão disponíveis para ele. Por padrão, o sistema tem um único namespace de rede para todos os objetos, então você pode trabalhar no Linux e não saber sobre netns.

Vamos criar um novo namespace xdp-test e vá para lá xdp-remote.

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

Em seguida, o processo em execução xdp-test, não vai "ver" xdp-local (ficará em netns por padrão) e ao enviar um pacote para 192.0.2.1 irá repassá-lo xdp-remote, porque essa é a única interface em 192.0.2.0/24 disponível para esse processo. Isso também funciona ao contrário.

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

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

Se correr tcpdump -tnevi xdp-local, você pode ver que os pacotes enviados de xdp-test, são entregues a esta interface:

ip netns exec xdp-test   ping 192.0.2.1

É conveniente executar um shell em xdp-test. O repositório possui um script que automatiza o trabalho com o estande, por exemplo, você pode configurar o estande com o comando sudo ./stand up e removê-lo sudo ./stand down.

rastreamento

O filtro é anexado ao dispositivo assim:

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

Ключ -force necessário para vincular um novo programa se outro já estiver vinculado. "Nenhuma notícia é uma boa notícia" não é sobre este comando, a saída é volumosa de qualquer maneira. indicar verbose opcional, mas com ele aparece um relatório sobre o trabalho do verificador de código com a listagem do montador:

Verifier analysis:

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

Destaque o programa da interface:

ip link set dev xdp-local xdp off

No script, estes são os comandos sudo ./stand attach и sudo ./stand detach.

Ao vincular o filtro, você pode garantir que ping continua funcionando, mas o programa funciona? Vamos adicionar logotipos. Função bpf_trace_printk() igual a printf(), mas suporta apenas até três argumentos além do padrão e uma lista limitada de especificadores. Macro bpf_printk() simplifica a chamada.

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

A saída vai para o canal de rastreamento do kernel, que precisa ser ativado:

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

Veja o fluxo de mensagens:

cat /sys/kernel/debug/tracing/trace_pipe

Ambas as equipes fazem uma chamada sudo ./stand log.

O ping agora deve produzir mensagens como esta nele:

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

Se você observar atentamente a saída do verificador, poderá 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 a string de formato são os argumentos imediatos dos comandos da VM:

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

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

Enviando Pacotes XDP

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

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

Lançamos tcpdump em xdp-remote. Ele deve mostrar a solicitação de eco ICMP de entrada e saída idêntica e parar de mostrar a resposta de eco ICMP. Mas não mostra. Acontece que funciona XDP_TX no programa para xdp-local necessáriopara emparelhar interface xdp-remote um programa também foi atribuído, mesmo que estivesse vazio, e foi gerado.

Como eu sabia?

Rastreando o caminho de um pacote no kernel o mecanismo de eventos perf permite, aliás, usar a mesma máquina virtual, ou seja, eBPF é usado para desmontagem com eBPF.

Você deve tirar o bem do mal, porque não há mais nada a fazer com isso.

$ 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

Função veth_xdp_flush_bq() recebe código de erro de veth_xdp_xmit(), onde pesquisar por ENXIO e encontre um comentário.

Restaure o filtro mínimo (XDP_PASS) no arquivo xdp_dummy.c, adicione-o ao Makefile, vincule-o 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 ARP for mostrado, você precisará remover os filtros (isso torna sudo ./stand detach), deixar ping, instale os filtros e tente novamente. O problema é que o filtro XDP_TX afeta o ARP também, e se a pilha
espaços de nomes xdp-test conseguiu "esquecer" o endereço MAC 192.0.2.1, ele não vai conseguir resolver esse IP.

Formulação do problema

Vamos passar para a tarefa indicada: escrever um mecanismo de cookie SYN no XDP.

Até agora, a inundação SYN continua sendo um ataque DDoS popular, cuja essência é a seguinte. Quando uma conexão é estabelecida (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 de endereços falsos na quantidade de milhares por segundo de cada host em uma botnet de vários milhares. O servidor é forçado a alocar recursos imediatamente após a chegada do pacote, mas o libera após um longo timeout, como resultado, a memória ou os limites são esgotados, novas conexões não são aceitas, o serviço fica indisponível.

Se você não aloca recursos no pacote SYN, mas apenas responde com um pacote SYNACK, como o servidor pode entender que o pacote ACK que veio depois pertence ao pacote SYN que não foi salvo? Afinal, um invasor também pode gerar ACKs falsos. A essência do cookie SYN é codificar em seqnum parâmetros de conexão como um hash de endereços, portas e mudança de sal. Se o ACK conseguiu chegar antes da alteração do salt, você pode calcular o hash novamente e comparar com acknum. falso acknum o invasor não pode, pois o salt inclui o segredo e não terá tempo para classificá-lo por causa do canal limitado.

Os cookies SYN foram implementados no kernel do Linux por um longo tempo e podem até ser ativados automaticamente se os SYNs chegarem muito rapidamente e em massa.

Programa educacional sobre handshake TCP

O TCP fornece a transferência de dados como um fluxo de bytes, por exemplo, as solicitações HTTP são transmitidas pelo TCP. O fluxo é transmitido peça por peça em pacotes. Todos os pacotes TCP têm sinalizadores lógicos e números de sequência de 32 bits:

  • A combinação de sinalizadores define a função de um pacote específico. O sinalizador SYN significa que este é o primeiro pacote do remetente na conexão. O sinalizador ACK significa que o remetente recebeu todos os dados de conexão até um byte. acknum. Um pacote pode ter vários sinalizadores e recebe o nome de sua combinação, por exemplo, um pacote SYNACK.

  • O número de sequência (seqnum) especifica o deslocamento no fluxo de dados para o primeiro byte enviado neste pacote. Por exemplo, se no primeiro pacote com X bytes de dados esse número era N, no próximo pacote com novos dados será N+X. No início da conexão, cada parte escolhe esse número aleatoriamente.

  • Número de confirmação (acknum) - o mesmo deslocamento que seqnum, mas não determina o número do byte transmitido, 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 seqnum и acknum. O cliente envia um pacote SYN com seu seqnum = X. O servidor responde com um pacote SYNACK, onde escreve seu próprio seqnum = Y e expõe acknum = X + 1. O cliente responde ao SYNACK com um pacote ACK, onde seqnum = X + 1, acknum = Y + 1. Depois disso, a transferência de dados real começa.

Se o interlocutor não confirmar o recebimento do pacote, o TCP o reenvia por timeout.

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

Primeiro, se um SYNACK ou ACK for perdido, você terá que esperar por um reenvio - o estabelecimento da conexão fica mais lento. Em segundo lugar, no pacote SYN - e somente nele! - várias opções são transmitidas que afetam a operação posterior da conexão. Não se lembrando dos pacotes SYN recebidos, o servidor ignora essas opções, nos pacotes seguintes o cliente não os enviará mais. O TCP pode funcionar neste caso, mas pelo menos no estágio inicial, a qualidade da conexão diminuirá.

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

  • responder a SYN com SYNACK com cookie;
  • responder ACK com RST (interromper a conexão);
  • 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 (*) os pontos em que você precisa gerenciar o estado do sistema são marcados - no primeiro estágio, você pode passar sem eles simplesmente implementando um handshake TCP com a geração de um cookie SYN como um seqnum.

No local (**), enquanto não tivermos mesa, vamos pular o pacote.

Implementação de handshake TCP

Análise de pacote e verificação de 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 não consegui conectar devido a erros relacionados a atomic64_t, tive que copiar as definições necessárias no código.

Todas as funções que são diferenciadas em C para facilitar a leitura devem ser embutidas no local da chamada, pois o verificador eBPF no kernel proíbe retrocessos, ou seja, de fato, loops e chamadas de função.

#define INTERNAL static __attribute__((always_inline))

Macro LOG() desativa a impressão em uma compilação de lançamento.

O programa é um pipeline de funções. Cada um recebe um pacote no qual é destacado um cabeçalho do nível correspondente, por exemplo, process_ether() esperando para ser preenchido ether. 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 é uma ação XDP. Enquanto os manipuladores SYN e ACK permitem a passagem de 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);
}

Presto atenção nas marcações A e B. Se você comentar A, o programa irá construir, mas haverá um erro de verificação ao 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!

Cadeia de caracteres chave invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0): há caminhos de execução quando o décimo terceiro byte desde o início do buffer está fora do pacote. É difícil dizer pela listagem de qual linha estamos falando, mas há um número de instrução (12) e um disassembler que mostra as linhas do código-fonte:

llvm-objdump -S xdp_filter.o | less

Neste caso, aponta para a linha

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

o que deixa claro que o problema é ether. Seria sempre assim.

Responder a SYN

O objetivo neste estágio é gerar um pacote SYNACK correto com um seqnum, que será substituído pelo cookie SYN no futuro. Todas as mudanças ocorrem em process_tcp_syn() e arredores.

Verificando o pacote

Curiosamente, aqui está a linha mais notável, ou melhor, um comentário a ela:

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

Ao escrever a primeira versão do código, foi utilizado o kernel 5.1, para o verificador do qual havia uma diferença entre data_end и (const void*)ctx->data_end. No momento da escrita, o kernel 5.3.1 não tinha esse problema. Talvez o compilador estivesse acessando uma variável local diferentemente de um campo. Moral - em um aninhamento grande, simplificar o código pode ajudar.

Outras verificações de rotina de comprimentos para a glória do verificador; O 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 do pacote

Preencher seqnum и acknum, defina ACK (SYN já 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ços IP e MAC. A biblioteca padrão não está disponível no programa XDP, então memcpy() — uma macro que oculta o Clang intrinsik.

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 é escrito nelas, ou seja, no momento da compilação é desconhecido. Isso é um problema porque o verificador não pulará o loop normal até a variável de limite. 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 pode terminar antes.

noto que existe RFC 1624 sobre como recalcular a soma de verificação parcialmente se apenas as palavras fixas dos pacotes 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 da 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;
}

Embora size verificado pelo código de chamada, a segunda condição de saída é necessária para que o verificador possa provar o fim do loop.

Para palavras de 32 bits, uma versão mais simples é implementada:

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

Na verdade, recalculando as somas de verificação e enviando o pacote de volta:

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;

Função carry() faz uma soma de verificação de uma soma de 32 bits de palavras de 16 bits, de acordo com RFC 791.

Verificação de handshake TCP

O filtro estabelece corretamente uma conexão com netcat, pulando o ACK final, ao qual o Linux respondeu com um pacote RST, já que a pilha de rede não recebeu um SYN - foi convertido para SYNACK e enviado de volta - e do ponto de vista do SO, chegou um pacote que não era relacionados a conexões abertas.

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

É importante verificar os aplicativos completos e observar tcpdump em xdp-remote porque, por exemplo, hping3 não responde a somas de verificação incorretas.

Do ponto de vista do XDP, a verificação em si é trivial. O algoritmo de cálculo é primitivo e provavelmente vulnerável a um invasor sofisticado. O kernel do Linux, por exemplo, usa o criptográfico SipHash, mas sua implementação para o XDP está claramente além do escopo deste artigo.

Apareceu para novos TODOs relacionados à interação externa:

  • O programa XDP não pode armazenar cookie_seed (a parte secreta do sal) em uma variável global, você precisa de um armazenamento de kernel cujo valor será atualizado periodicamente a partir de um gerador confiável.

  • Se o cookie SYN no pacote ACK corresponder, você não precisa imprimir uma mensagem, mas lembre-se do IP do cliente verificado para pular pacotes dele.

Validação por um cliente legítimo:

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

Os logs registraram a passagem do cheque (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

Enquanto não houver uma lista de IPs verificados, não haverá proteção contra a inundação de SYN em si, mas aqui está a reação à inundação de ACK iniciada por este 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

Conclusão

Às vezes, o eBPF em geral e o XDP em particular são apresentados mais como uma ferramenta avançada de administrador do que como uma plataforma de desenvolvimento. De fato, o XDP é uma ferramenta para interferir no processamento de pacotes do kernel e não uma alternativa à pilha do kernel, como o DPDK e outras opções de bypass do kernel. Por outro lado, o XDP permite implementar uma lógica bastante complexa, que, além disso, é fácil de atualizar sem interromper o processamento do tráfego. O verificador não cria grandes problemas, pessoalmente eu não recusaria isso para partes do código do espaço do usuário.

Na segunda parte, se o assunto for interessante, completaremos a tabela de clientes verificados e quebraremos conexões, implementaremos contadores e escreveremos um utilitário userspace para gerenciar o filtro.

Links:

Fonte: habr.com

Adicionar um comentário