
Máquina virtual Rust e compilador JIT para programas eBPF
Máquina virtual Rust (espaço de usuário) para eBPF
Este crate contém uma máquina virtual para execução de programas eBPF. BPF, como em Berkeley Packet Filter, é uma linguagem semelhante a assembly inicialmente desenvolvida para sistemas BSD, a fim de filtrar pacotes no kernel com ferramentas como tcpdump para evitar cópias inúteis para o espaço de usuário. Foi portada para Linux, onde evoluiu para eBPF (extended BPF), uma versão mais rápida com mais recursos. Embora os programas BPF sejam originalmente destinados a rodar no kernel, a máquina virtual deste crate permite executá-los em aplicações de espaço de usuário; ela contém um interpretador, um compilador JIT x86_64 para programas eBPF, bem como um desassemblador.
É baseada no software uBPF de Rich Lane, que faz quase o mesmo, mas é escrito em C.
O crate deve compilar e rodar em Linux, MacOS X e Windows, embora o compilador JIT não funcione com Windows no momento.
Este crate está disponível em crates.io, então
deve funcionar imediatamente ao adicioná-lo como dependência no seu arquivo Cargo.toml:
[dependencies]
rbpf = "0.4.1"
Você também pode usar a versão de desenvolvimento deste repositório do GitHub. Isso
deve ser tão simples quanto colocar isto dentro do seu Cargo.toml:
[dependencies]
rbpf = { git = "https://github.com/qmonnet/rbpf" }
Claro, se preferir, você pode cloná-lo localmente, possivelmente modificar o crate,
e então indicar o caminho da sua versão local em Cargo.toml:
[dependencies]
rbpf = { path = "path/to/rbpf" }
Em seguida, indique no seu código-fonte que deseja usar o crate:
extern crate rbpf;
A API está muito bem documentada dentro do código-fonte. Você também deve conseguir acessar uma versão online da documentação aqui, gerada automaticamente a partir da versão do crates.io (pode não estar atualizada com o branch principal). Exemplos e testes unitários também devem ser úteis. Aqui está um resumo de como usar o crate.
Aqui estão os passos a seguir para executar um programa eBPF com o rbpf:
O eBPF foi inicialmente projetado para filtrar pacotes (agora ele tem alguns outros hooks
no kernel Linux, como kprobes, mas isso não é coberto pelo rbpf). Como
consequência, a maioria das instruções de load e store do programa são
realizadas em uma área de memória que representa os dados do pacote. No entanto, no kernel
Linux, o programa eBPF não acessa imediatamente essa área de dados: inicialmente,
ele tem acesso a um struct sk_buff em C, que é um buffer contendo
metadados sobre o pacote—incluindo endereços de memória do início e do
fim da área de dados do pacote. Então o programa primeiro carrega esses ponteiros do
sk_buff, e então pode acessar os dados do pacote.
Esse comportamento pode ser replicado com o rbpf, mas não é obrigatório. Por esse motivo, temos várias structs representando diferentes tipos de máquinas virtuais:
struct EbpfVmMbuffer imita o kernel. Quando o programa é executado, o
endereço fornecido ao seu primeiro registrador eBPF será o endereço de um buffer de metadados
fornecido pelo usuário, e espera-se que ele contenha ponteiros para o
início e o fim da área de memória dos dados do pacote.
struct EbpfVmFixedMbuff tem um propósito: permitir a execução de programas
criados para serem compatíveis com o kernel, enquanto poupa o esforço de manipular
manualmente o buffer de metadados para o usuário. De fato, essa struct tem um buffer
interno estático que é passado para o programa. O usuário precisa indicar os
valores de offset nos quais o programa eBPF espera encontrar o início e o fim
dos dados do pacote no buffer. Ao chamar a função que executa o programa
(com JIT ou não), a struct atualiza automaticamente os endereços nesse
buffer estático, nos offsets indicados, para o início e o fim dos
dados do pacote para os quais o programa é chamado.
struct EbpfVmRaw é para programas que querem executar diretamente nos dados do pacote.
Nenhum buffer de metadados está envolvido, o programa eBPF recebe diretamente o
endereço dos dados do pacote em seu primeiro registrador. Esse é o comportamento do
uBPF.
struct EbpfVmNoData não recebe nenhum dado. O programa eBPF não recebe
nenhum argumento e seu valor de retorno é determinístico. Não tenho tanta certeza de que
haja um caso de uso válido para isso, mas, no mínimo, isso é muito útil para
testes unitários.
Todas essas structs implementam as mesmas funções públicas:
// called with EbpfVmMbuff:: prefix
pub fn new(prog: &'a [u8]) -> Result<EbpfVmMbuff<'a>, Error>
// called with EbpfVmFixedMbuff:: prefix
pub fn new(prog: &'a [u8],
data_offset: usize,
data_end_offset: usize) -> Result<EbpfVmFixedMbuff<'a>, Error>
// called with EbpfVmRaw:: prefix
pub fn new(prog: &'a [u8]) -> Result<EbpfVmRaw<'a>, Error>
// called with EbpfVmNoData:: prefix
pub fn new(prog: &'a [u8]) -> Result<EbpfVmNoData<'a>, Error>
Isto é usado para criar uma nova instância de uma VM. O tipo de retorno depende da
struct a partir da qual a função é chamada. Por exemplo,
rbpf::EbpfVmRaw::new(Some(my_program)) retornaria uma instância de struct rbpf::EbpfVmRaw (envolvida num Result). Quando um programa é carregado, é
verificado com um verificador muito simples (nada próximo do usado para o kernel
Linux). Os utilizadores também podem substituí-lo por um verificador personalizado.
Para struct EbpfVmFixedMbuff, dois argumentos adicionais devem ser passados ao
construtor: data_offset e data_end_offset. São o offset (número de bytes)
no qual os ponteiros para o início e para o fim, respetivamente, da
área de memória dos dados do pacote devem ser armazenados no buffer de metadados
interno cada vez que o programa é executado. Outras structs não usam este mecanismo e
não precisam desses offsets.
// for struct EbpfVmMbuff, struct EbpfVmRaw and struct EbpfVmRawData
pub fn set_program(&mut self, prog: &'a [u8]) -> Result<(), Error>