eBPF Summit 2024

eBPF Documentation

O eBPF é uma tecnologia revolucionária com origens no kernel Linux que pode executar programas em sandbox em um contexto privilegiado, como o kernel do sistema operacional. Ele é usado para estender com segurança e eficiência as capacidades do kernel sem a necessidade de alterar o código-fonte do kernel ou carregar módulos do kernel.

Historicamente, o sistema operacional sempre foi um local ideal para implementar funcionalidades de observabilidade, segurança e rede, devido à habilidade privilegiada do kernel de supervisionar e controlar todo o sistema. Ao mesmo tempo, evoluir o kernel do sistema operacional é difícil devido ao seu papel central e aos altos requisitos de estabilidade e segurança. Como resultado, a taxa de inovação no nível do sistema operacional tradicionalmente tem sido menor em comparação com as funcionalidades implementadas fora do sistema operacional.

Visão geral

O eBPF muda fundamentalmente essa fórmula. Ao permitir a execução de programas isolados dentro do sistema operacional, os desenvolvedores de aplicativos podem executar programas eBPF para adicionar capacidades adicionais ao sistema operacional em tempo de execução. O sistema operacional, em seguida, garante segurança e eficiência de execução, como se estivesse compilado de forma nativa, com o auxílio de um compilador Just-In-Time (JIT) e um mecanismo de verificação. Isso levou a uma onda de projet os baseados em eBPF, abrangendo uma ampla variedade de casos de uso, incluindo funcionalidades de rede, observabilidade e segurança de próxima geração.

Hoje, o eBPF é amplamente utilizado para impulsionar uma ampla variedade de casos de uso: fornecer rede de alto desempenho e balanceamento de carga em data centers modernos e ambientes nativos em nuvem, extrair dados de observabilidade de segurança de granularidade fina com baixa sobrecarga, ajudar os desenvolvedores de aplicativos a rastrear aplicativos, fornecer insights para solucionar problemas de desempenho, aplicação preventiva e aplicação de segurança de contêiner em tempo de execução, e muito mais. As possibilidades são infinitas, e a inovação que o eBPF está desbloqueando apenas começou.

O eBPF.io é um espaço para todos aprenderem e colaborarem no tópico do eBPF. O eBPF é uma comunidade aberta e todos podem participar e compartilhar. Se você deseja ler uma primeira introdução ao eBPF, encontrar materiais de leitura adicionais ou dar seus primeiros passos para se tornar um colaborador dos principais projetos do eBPF, o eBPF.io irá ajudá-lo no caminho.

Originalmente, BPF significava Berkeley Packet Filter, mas agora que o eBPF (BPF estendido) pode fazer muito mais do que filtragem de pacotes, o acrônimo não faz mais sentido. O eBPF agora é considerado um termo independente que não representa mais nada. No código-fonte do Linux, o termo BPF persiste, e nas ferramentas e na documentação, os termos BPF e eBPF são geralmente usados de forma intercambiável. O BPF original é às vezes referido como cBPF (BPF clássico) para distingui-lo do eBPF.

A abelha é o logotipo oficial do eBPF e foi originalmente criada por Vadim Shchekoldin. No primeiro eBPF Summit, houve uma votação e a abelha foi nomeada de eBee. (Para detalhes sobre usos aceitáveis do logotipo, consulte as Diretrizes de Marca da Linux Foundation.)

Os próximos capítulos são uma rápida introdução ao eBPF. Se você deseja aprender mais sobre o eBPF, consulte o Guia de Referência eBPF & XDP. Seja um desenvolvedor que deseja criar um programa eBPF ou esteja interessado em aproveitar uma solução que usa o eBPF, é útil entender os conceitos básicos e a arquitetura.

Os programas eBPF são baseados em eventos e são executados quando o kernel ou um aplicativo passa por um determinado ponto de ancoragem (hook). Os pontos de ancoragem pré-definidos incluem chamadas do sistema, entrada/saída de funções, pontos de rastreamento do kernel, eventos de rede e vários outros.

Syscall hook

Se um gancho predeterminado não existe para uma necessidade em particular , É possível criar um kernel probe (kprobe)ou usuário probe (uprobe) para anexar programas eBPF praticamente em qualquer lugar do kernel ou aplicações do usuário.

Hook overview

Em muitos cenários, eBPF não é usado diretamente mas sim indiretamente através de projetos como Cilium, bcc ou bpftrace que fornecem uma abstração em cima do eBPF e não necessitam de um programa de escrita diretamente, mas ao invés disso oferecem a habilidade de especificar definições baseadas nas intenções que são implementadas com eBPF.

Clang

Se não houver abstração de nível superior disponível, os programas precisam ser escritos diretamente. O kernel Linux espera que os programas eBPF sejam carregados na forma de bytecode. Embora seja possível escrever bytecode diretamente, a prática de desenvolvimento mais comum é utilizar uma suíte de compiladores como o LLVM para compilar código pseudo-C em bytecode eBPF.

Quando o gancho desejado for identificado, o programa eBPF pode ser carregado no kernel Linux usando a chamada de sistema bpf. Isso é tipicamente feito usando uma das bibliotecas disponíveis para eBPF. A próxima seção oferece uma introdução às ferramentas de desenvolvimento disponíveis.

Go

Conforme o programa é carregado no kernel Linux, ele passa por duas etapas antes de ser anexado ao gancho solicitado:

A etapa de verificação garante que o programa eBPF seja seguro para ser executado. Ela valida se o programa atende a várias condições, por exemplo:

Carregamento
  • O processo que carrega o programa eBPF possui as capacidades (privilégios) necessárias. A menos que o eBPF não privilegiado esteja habilitado, apenas processos com privilégios podem carregar programas eBPF.
  • O programa não causa falhas ou prejudica o sistema.
  • O programa é executado sempre até o final (ou seja, o programa não fica em loop para sempre, impedindo o processamento adicional).

A etapa de compilação Just-in-Time (JIT) traduz o bytecode genérico do programa para o conjunto de instruções específico da máquina para otimizar a velocidade de execução do programa. Isso faz com que os programas eBPF sejam executados com a mesma eficiência do código do kernel compilado nativamente ou do código carregado como um módulo do kernel.

Um aspecto vital dos programas eBPF é a capacidade de compartilhar informações coletadas e armazenar estado. Para esse propósito, os programas eBPF podem usar o conceito de mapas eBPF para armazenar e recuperar dados em um amplo conjunto de estruturas de dados. Os mapas eBPF podem ser acessados a partir de programas eBPF, bem como de aplicativos no espaço do usuário por meio de uma chamada de sistema.

Arquitetura de Mapas

A seguir, está uma lista incompleta de tipos de mapas suportados para dar uma compreensão da diversidade em estruturas de dados. Para vários tipos de mapas, estão disponíveis variações compartilhadas e por CPU.

  • Tabelas de hash, matrizes
  • LRU (Menos Recentemente Usado)
  • Buffer circular (Ring Buffer)
  • Rastreamento de Pilha
  • LPM (Correspondência do Prefixo Mais Longo)
  • ...

Os programas eBPF não podem chamar funções do kernel arbitrariamente. Permitir isso vincularia os programas eBPF a versões específicas do kernel e complicaria a compatibilidade dos programas. Em vez disso, os programas eBPF podem fazer chamadas de função para funções de assistência, que são uma API bem conhecida e estável oferecida pelo kernel.

Assistente

O conjunto de chamadas de assistência disponíveis está em constante evolução. Exemplos de chamadas de assistência disponíveis:

  • Gerar números aleatórios
  • Obter data e hora atual
  • Acesso a mapas eBPF
  • Obter contexto de processo/cgroup
  • Manipular pacotes de rede e lógica de encaminhamento

Os programas eBPF são componíveis com o conceito de chamadas de cauda (tail calls) e funções. As chamadas de função permitem definir e chamar funções dentro de um programa eBPF. As chamadas de cauda podem chamar e executar outro programa eBPF e substituir o contexto de execução, semelhante ao funcionamento da chamada de sistema execve() para processos regulares.

Chamada de Cauda

Com grande poder também vem grande responsabilidade.

O eBPF é uma tecnologia incrivelmente poderosa e agora está no centro de muitos componentes críticos da infraestrutura de software. Durante o desenvolvimento do eBPF, a segurança do eBPF foi o aspecto mais crucial ao ser considerado para inclusão no kernel Linux. A segurança do eBPF é garantida por várias camadas:

Privilégios Exigidos

A menos que o eBPF não privilegiado esteja habilitado, todos os processos que pretendem carregar programas eBPF no kernel Linux devem ser executados em modo privilegiado (root) ou requerer a capacidade CAP_BPF. Isso significa que programas não confiáveis não podem carregar programas eBPF.

Se o eBPF não privilegiado estiver habilitado, processos não privilegiados podem carregar certos programas eBPF sujeitos a um conjunto reduzido de funcionalidades e com acesso limitado ao kernel.

Verificador

Se um processo tem permissão para carregar um programa eBPF, todos os programas ainda passam pelo verificador do eBPF. O verificador do eBPF garante a segurança do próprio programa. Isso significa, por exemplo:

  • Os programas são validados para garantir que sempre sejam executados até o final, por exemplo, um programa eBPF nunca pode bloquear ou ficar em loop para sempre. Os programas eBPF podem conter loops limitados, mas o programa só é aceito se o verificador puder garantir que o loop contém uma condição de saída que é garantida de se tornar verdadeira.
  • Os programas não podem usar variáveis não inicializadas ou acessar a memória fora dos limites.
  • Os programas devem se adequar aos requisitos de tamanho do sistema. Não é possível carregar programas eBPF arbitrariamente grandes.
  • Os programas devem ter uma complexidade finita. O verificador avaliará todos os caminhos de execução possíveis e deve ser capaz de concluir a análise dentro dos limites do limite superior configurado.

O verificador é uma ferramenta de segurança que verifica se os programas são seguros para serem executados. Não é uma ferramenta de segurança que inspeciona o que os programas estão fazendo.

Fortalecimento

Após a conclusão bem-sucedida da verificação, o programa eBPF passa por um processo de fortalecimento de acordo com se o programa é carregado a partir de um processo privilegiado ou não privilegiado. Essa etapa inclui:

  • Proteção da execução do programa: A memória do kernel que contém um programa eBPF é protegida e tornada somente leitura. Se, por qualquer motivo, seja por um bug do kernel ou manipulação maliciosa, for feita uma tentativa de modificar o programa eBPF, o kernel irá travar em vez de permitir a execução do programa corrompido/manipulado.
  • Mitigação contra Spectre: Em CPUs especulativas, pode haver previsões erradas de ramificações que deixam efeitos colaterais observáveis que podem ser extraídos por meio de um canal paralelo. Para citar alguns exemplos: programas eBPF mascaram o acesso à memória para redirecionar o acesso em instruções transitórias para áreas controladas, o verificador também segue caminhos do programa acessíveis apenas sob execução especulativa e o compilador JIT emite Retpolines caso chamadas de cauda não possam ser convertidas em chamadas diretas.
  • Cegamento constante: Todas as constantes no código são cegadas para evitar ataques de JIT spraying. Isso impede que atacantes injetem código executável como constantes que, na presença de outro bug do kernel, poderiam permitir que um atacante salte para a seção de memória do programa eBPF para executar código.

Contexto de Execução Abstraído

Os programas eBPF não podem acessar diretamente a memória do kernel arbitrariamente. O acesso a dados e estruturas de dados que estão fora do contexto do programa deve ser feito por meio de chamadas de assistência (helper calls) do eBPF. Isso garante acesso consistente aos dados e torna qualquer acesso desse tipo sujeito aos privilégios do programa eBPF, ou seja, um programa eBPF em execução só pode modificar os dados de certas estruturas de dados se a modificação puder ser garantida como segura. Um programa eBPF não pode modificar aleatoriamente estruturas de dados no kernel.

Vamos começar com uma analogia. Você se lembra do GeoCities? Há 20 anos, as páginas da web costumavam ser escritas quase exclusivamente em linguagem de marcação estática (HTML). Uma página da web era basicamente um documento com um aplicativo (navegador) capaz de exibi-lo. Ao olhar as páginas da web hoje, elas se tornaram aplicativos completos e a tecnologia baseada na web substituiu a maioria dos aplicativos escritos em linguagens que requerem compilação. O que possibilitou essa evolução?

Geocities

A resposta curta é a programabilidade com a introdução do JavaScript. Isso desencadeou uma revolução maciça, resultando em navegadores evoluindo para sistemas quase independentes.

Por que a evolução aconteceu? Os programadores não estavam mais limitados pelos usuários que executavam versões específicas do navegador. Em vez de convencer órgãos de padronização de que era necessário uma nova tag HTML, a disponibilidade dos blocos de construção necessários desvinculou o ritmo de inovação do navegador subjacente da aplicação em execução. Claro, isso é um pouco simplificado, pois o HTML evoluiu ao longo do tempo e contribuiu para o sucesso, mas a evolução do HTML por si só não teria sido suficiente.

Antes de aplicar esse exemplo ao eBPF, vamos analisar alguns aspectos-chave que foram vitais para a introdução do JavaScript:

  • Segurança: O código não confiável é executado no navegador do usuário. Isso foi resolvido isolando os programas JavaScript e abstraindo o acesso aos dados do navegador.
  • Entrega Contínua: A evolução da lógica do programa deve ser possível sem exigir o envio constante de novas versões do navegador. Isso foi resolvido fornecendo os blocos de construção de baixo nível corretos, suficientes para construir lógica arbitrária.
  • Desempenho: A programabilidade deve ser fornecida com um mínimo de sobrecarga. Isso foi resolvido com a introdução de um compilador Just-in-Time (JIT). Para todos os itens acima, encontramos contrapartes exatas no eBPF pelas mesmas razões.

Agora, voltemos ao eBPF. Para entender o impacto da programabilidade do eBPF no kernel Linux, é útil ter uma compreensão de alto nível da arquitetura do kernel Linux e como ela interage com aplicativos e o hardware.

Arquitetura do Kernel

O principal objetivo do kernel Linux é abstrair o hardware ou hardware virtual e fornecer uma API consistente (chamadas de sistema) que permite que os aplicativos sejam executados e compartilhem os recursos. Para isso, um amplo conjunto de subsistemas e camadas é mantido para distribuir essas responsabilidades. Cada subsistema geralmente permite algum nível de configuração para atender a diferentes necessidades dos usuários. Se um comportamento desejado não puder ser configurado, uma alteração no kernel será necessária, historicamente, deixando duas opções:

Suporte Nativo

  1. Alterar o código-fonte do kernel e convencer a comunidade do kernel Linux de que a alteração é necessária.
  2. Aguardar vários anos para que a nova versão do kernel se torne uma commodity.

Módulo do Kernel

  1. Escrever um módulo do kernel
  2. Atualizá-lo regularmente, pois cada lançamento do kernel pode quebrá-lo
  3. Correr o risco de corromper o kernel Linux devido à falta de limites de segurança

Com o eBPF, uma nova opção está disponível que permite reprogramar o comportamento do kernel Linux sem exigir alterações no código-fonte do kernel ou o carregamento de um módulo do kernel. Em muitos aspectos, isso é muito semelhante à forma como o JavaScript e outras linguagens de script permitiram a evolução de sistemas que se tornaram difíceis ou caros de serem alterados.

Existem várias ferramentas de desenvolvimento disponíveis para ajudar no desenvolvimento e gerenciamento de programas eBPF. Todas elas atendem a diferentes necessidades dos usuários:

bcc

O BCC é um framework que permite aos usuários escrever programas em Python com programas eBPF incorporados neles. O framework é direcionado principalmente para casos de uso que envolvem perfilamento/tracing de aplicativos e sistemas, onde um programa eBPF é usado para coletar estatísticas ou gerar eventos, e um programa correspondente no espaço do usuário coleta os dados e os exibe de forma legível para humanos. A execução do programa Python gerará o bytecode eBPF e o carregará no kernel.

bcc

bpftrace

O bpftrace é uma linguagem de rastreamento de alto nível para eBPF Linux e está disponível em kernels Linux recentes (4.x). O bpftrace usa o LLVM como backend para compilar scripts para bytecode eBPF e faz uso do BCC para interagir com o subsistema eBPF Linux, bem como com as capacidades existentes de rastreamento do Linux: rastreamento dinâmico do kernel (kprobes), rastreamento dinâmico em nível de usuário (uprobes) e tracepoints. A linguagem bpftrace é inspirada por awk, C e rastreadores predecessores, como DTrace e SystemTap.

bpftrace

Biblioteca eBPF Go

A biblioteca eBPF Go fornece uma biblioteca eBPF genérica que desacopla o processo de obtenção do bytecode eBPF e o carregamento e gerenciamento de programas eBPF. Os programas eBPF são tipicamente criados escrevendo em uma linguagem de alto nível e, em seguida, usando o compilador clang/LLVM para compilá-los em bytecode eBPF.

Go

Biblioteca C/C++ libbpf

A biblioteca libbpf é uma biblioteca eBPF genérica baseada em C/C++ que ajuda a desacoplar o carregamento de arquivos de objeto eBPF gerados pelo compilador clang/LLVM no kernel e geralmente abstrai a interação com a chamada de sistema BPF, fornecendo APIs de biblioteca fáceis de usar para aplicativos.

Libbpf

Se você deseja saber mais sobre eBPF, continue lendo usando os seguintes materiais adicionais:

Generic

Deep Dives

Cilium

Hubble