Quando preciso capturar alguns pacotes usando tcpdump
, uso comandos como:
tcpdump -i eth0 "dst host 192.168.1.0"
Eu sempre acho que a parte do dst host 192.168.1.0 é algo chamado BPF, Berkeley Packet Filter. Para mim, é uma linguagem simples para filtrar pacotes de rede. Mas hoje meu colega de quarto me diz que o BPF pode ser usado para capturar informações de desempenho. De acordo com sua descrição, é como a ferramenta perfmon
no Windows. É verdade? É o mesmo BPF que mencionei no início da pergunta?
O que é BPF?
BPF (ou mais comumente, a versão estendida, eBPF ) é uma linguagem que foi originalmente usada exclusivamente para filtrar pacotes, mas é capaz de muito mais. No Linux, ele pode ser usado para muitas outras coisas, incluindo filtros de chamadas do sistema para segurança e monitoramento de desempenho, como você apontou. Embora o Windows tenha adicionado suporte eBPF , não é isso que o utilitário do Windows
perfmon
usa. O Windows adicionou suporte apenas para compatibilidade com utilitários não Windows que dependem do suporte do SO para eBPF.Os programas eBPF não são executados no espaço do usuário. Em vez disso, o aplicativo cria e envia um programa eBPF para o kernel, que o executa. Na verdade, é um código de máquina para um processador virtual implementado na forma de um interpretador no kernel, embora também possa usar a compilação JIT para melhorar consideravelmente o desempenho. O programa tem acesso a algumas interfaces básicas no kernel, incluindo aquelas relacionadas a desempenho e rede. O programa eBPF então se comunica com o kernel para fornecer os resultados computacionais (como descartar um pacote).
Restrições aos programas eBPF
Para proteger contra ataques de negação de serviço ou travamentos acidentais, o kernel primeiro verifica o código antes de ser compilado. Antes de ser executado, o código passa por várias verificações importantes:
O programa consiste em não mais de 4096 instruções no total para usuários sem privilégios.
Saltos para trás não podem ocorrer, com exceção de loops limitados e chamadas de função.
Não há instruções que são sempre inalcançáveis.
O resultado é que o verificador deve ser capaz de provar que o programa eBPF é interrompido. Ele não encontrou uma solução para o problema da parada , é claro, e é por isso que só aceita programas que sabe que vão parar. Para isso, representa o programa como um grafo acíclico direcionado . Além disso, ele tenta evitar vazamentos de informações e acesso à memória fora dos limites, impedindo que o valor real de um ponteiro seja revelado enquanto ainda permite que operações limitadas sejam executadas nele:
Ponteiros não podem ser comparados, armazenados ou retornados como um valor que pode ser examinado.
A aritmética de ponteiro só pode ser feita em um escalar (um valor não derivado de um ponteiro).
Nenhuma aritmética de ponteiro pode resultar em apontar para fora do mapa de memória designado.
O verificador é bastante complexo e faz muito mais, embora ele mesmo tenha sido a fonte de sérios bugs de segurança , pelo menos quando o syscall não está desabilitado para usuários sem privilégios .
bpf(2)
Visualizando o código
O
dst host 192.168.1.0
componente do comando não é BPF. Essa é apenas a sintaxe usada pelotcpdump
. No entanto, o comando que você dá é usado para gerar um programa BPF que é então enviado ao kernel. Observe que não é o eBPF que é usado neste caso, mas o cBPF mais antigo. Existem várias diferenças importantes entre os dois (embora o kernel converta internamente cBPF em eBPF). O-d
sinalizador pode ser usado para ver o código cBPF que deve ser enviado ao kernel:Filtros mais complicados resultam em bytecodes mais complicados. Experimente alguns dos exemplos na página de manual e anexe o
-d
sinalizador para ver qual bytecode seria carregado no kernel. Para entender como ler a desmontagem, consulte a documentação do filtro BPF . Se estiver lendo um programa eBPF, você deve dar uma olhada no conjunto de instruções eBPF para a CPU virtual.Entendendo o código
Para simplificar, vou supor que você especificou um IP de destino de 192.168.1.1 em vez de 192.168.1.0 e queria corresponder apenas ao IPv4, o que reduz um pouco o código, pois não precisa mais lidar com o IPv6:
Vamos ver o que o bytecode acima realmente faz . Cada vez que um pacote é recebido na interface especificada, o bytecode BPF é executado. O conteúdo do pacote (incluindo o cabeçalho Ethernet, se aplicável) é colocado em um buffer ao qual o código BPF tem acesso. Se o pacote corresponder ao filtro, o código retornará o tamanho do buffer de captura (262144 bytes por padrão), caso contrário, retornará 0.
Vamos supor que você esteja executando este filtro e ele receba um pacote enviando uma mensagem ICMP com uma carga vazia de 192.168.1.142 a 192.168.1.1. O MAC de origem é aa:aa:aa:aa:aa:aa e o MAC de destino é bb:bb:bb:bb:bb:bb. O conteúdo do quadro Ethernet, em hexadecimal, é:
A primeira instrução é
ldh [12]
. Isso carrega uma meia palavra (dois bytes) localizada em um deslocamento de 12 bytes no pacote no registrador A. Este é o valor 0x0800 (lembre-se que os dados da rede são sempre big-endian). A segunda instrução éjeq #0x800
, que irá comparar um imediato com o valor no registrador A. Se forem iguais, saltará para a instrução 2, caso contrário, 5. O valor 0x800 nesse deslocamento no quadro Ethernet especifica o protocolo IPv4. Como a comparação é avaliada como verdadeira, o código agora pula para a instrução 2. Se a carga útil não fosse IPv4, ela teria saltado para 5.A instrução 2 (a terceira) é
ld [30]
. Isso carrega uma palavra inteira de 4 bytes com um deslocamento de 30 no registrador A. Em nosso quadro Ethernet, este é 0xc0a80101. A próxima instrução,jeq #0xc0a80101
, comparará um imediato com o conteúdo do registrador A e saltará para 4 se verdadeiro, caso contrário 5. Este valor é o endereço de destino (0xc0a80101 é a representação big-endian de 192.168.1.1). Os valores realmente correspondem, então o contador do programa agora está definido como 4.A instrução 4 é
ret #262144
. Isso encerra o programa BPF e retorna o inteiro 262144 para o programa de chamada. Isso informa ao programa chamador,tcpdump
neste caso, que o pacote foi capturado pelo filtro, então ele solicita o conteúdo do pacote do kernel, o decodifica mais detalhadamente e grava as informações em seu terminal. Se o endereço de destino não correspondesse ao que o filtro estava procurando ou o tipo de protocolo não fosse IPv4, o código teria saltado para a instrução 5, onde seria encontradoret #0
. Isso teria encerrado o filtro sem uma correspondência.Isso tudo é apenas uma maneira de retornar 262144 se a meia palavra no deslocamento 12 no pacote for 0x800 E a palavra no deslocamento 30 for 0xc0a80101 e retornar 0 caso contrário. Como tudo isso é feito no kernel (opcionalmente após ser convertido em código de máquina nativo pelo mecanismo JIT), não são necessárias trocas de contexto caras ou buffers de passagem entre o kernelspace e o userspace, portanto, o filtro é rápido .
Exemplos mais avançados
O código BPF não se limita a ser usado por
tcpdump
. Vários outros utilitários podem usá-lo. Você pode até criar uma regra iptables com um filtro BPF usando oxt_bpf
módulo! No entanto, você deve ter cuidado ao gerar o bytecode comtcpdump -ddd
porque espera consumir um cabeçalho de camada 2, enquanto o iptables não. Tudo o que você precisa fazer para torná-los compatíveis é ajustar os deslocamentos.Além disso, são fornecidas várias funções auxiliares que fornecem informações que não podem ser obtidas pela leitura do conteúdo bruto do pacote, como o comprimento do pacote, o deslocamento inicial da carga útil, a CPU na qual o pacote foi recebido, a marca NetFilter, etc. a documentação do filtro:
As extensões BPF suportadas são:
Por exemplo, para corresponder a todos os pacotes recebidos na CPU 3, você pode fazer:
Observe que isso está usando a sintaxe do assembly BPF compatível com
bpf_asm
, enquanto as outras listagens de assembly aqui estão usando atcpdump
sintaxe. A principal diferença é que a sintaxe do primeiro usa rótulos nomeados, enquanto a sintaxe BPF do último rotula cada instrução com um número de linha. Este assembly se traduz no seguinte bytecode (instruções de delimitação de vírgulas):Isso pode ser usado com
iptables
o uso doxt_bpf
módulo:Isso pulará para a cadeia de destino
CPU3
para qualquer pacote recebido nessa CPU.Se isso parece poderoso, lembre-se de que tudo isso é cBPF. Embora o cBPF seja traduzido em eBPF internamente, tudo isso não é nada comparado ao que o eBPF bruto pode fazer!
Para maiores informações
Eu recomendo que você leia este artigo para entender como
tcpdump
usa o cBPF.Depois de ler isso, leia esta explicação de como
tcpdump
transforma expressões em bytecode.Se você quiser aprender tudo sobre isso, você sempre pode conferir o código-fonte !
Para complementar a boa resposta do @forest, talvez possamos elaborar um pouco como esses programas são executados.
cBPF, como usado pelo tcpdump, tem poucos ganchos: ele pode ser anexado a sockets , para ser executado quando um pacote chega (é isso que o tcpdump faz, para filtrar os pacotes recebidos no socket, e passar apenas os desejados para o espaço do usuário ), ou eles podem ser anexados aos ganchos seccomp , para fazer alguma filtragem nas chamadas do sistema e seus argumentos.
Uma das características importantes do eBPF é que ele pode ser anexado a uma seleção mais ampla de ganchos no kernel (embora não faça seccomp). Para redes, existem sockets , mas também ganchos TC (controle de tráfego), XDP (ganchos de nível de driver para redes rápidas) ou alguns outros. Com relação à sua pergunta: os programas também podem ser anexados a tracepoints no kernel (ganchos pré-definidos em algumas funções específicas, por exemplo, syscalls ou funções “importantes” no kernel), ou em testes do kernel ( kprobes ), tornando-os capazes de rastrear qualquer função no kernel (desde que não tenha sido embutida no momento da compilação). Em seguida, outros tiposexistem, por exemplo, LSM para casos de uso de segurança.
O rastreamento geralmente depende de pontos de rastreamento ou kprobes para anexar um programa eBPF a uma função e executá-lo toda vez que essa função é chamada no kernel. O programa pode acessar os argumentos da função ou (se estiver anexado na saída) ao valor de retorno. Através do uso de mapas , áreas especiais de memória do kernel, como arrays ou mapas de hash, dedicados ao compartilhamento de dados entre programas eBPF e/ou espaço do usuário, os programas podem coletar métricas ou compartilhar estados entre execuções consecutivas.
Por exemplo, opensnoop do BCC será anexado aos pontos de rastreamento na entrada e na saída das
open()
eopenat()
syscalls. Na entrada, ele coleta o caminho do arquivo que está sendo aberto e o PID do processo que o abre e o armazena em um mapa de hash. Quando a syscall é encerrada, o segundo probe coleta o valor de retorno e, com base no PID, atualiza a entrada relevante no mapa de hash. Em seguida, o espaço do usuário pode coletar e despejar todas as entradas do mapa de hash para mostrar quais arquivos foram abertos por quais processos e quais foram os valores de retorno.https://ebpf.io/ é um bom lugar para começar a usar o eBPF.