
Máquina virtual de Rust y compilador JIT para programas eBPF
Máquina virtual Rust (espacio de usuario) para eBPF
Este crate contiene una máquina virtual para la ejecución de programas eBPF. BPF, como en Berkeley Packet Filter, es un lenguaje similar al ensamblador desarrollado inicialmente para sistemas BSD, con el fin de filtrar paquetes en el kernel con herramientas como tcpdump para evitar copias inútiles al espacio de usuario. Fue portado a Linux, donde evolucionó a eBPF (extended BPF), una versión más rápida con más características. Mientras que los programas BPF están originalmente destinados a ejecutarse en el kernel, la máquina virtual de este crate permite ejecutarlos en aplicaciones de espacio de usuario; contiene un intérprete, un compilador JIT x86_64 para programas eBPF, así como un desensamblador.
Está basado en el software uBPF de Rich Lane, que hace casi lo mismo, pero está escrito en C.
Se supone que el crate compila y se ejecuta en Linux, MacOS X y Windows, aunque el compilador JIT no funciona con Windows en este momento.
Este crate está disponible en crates.io, por lo que
debería funcionar sin problemas añadiéndolo como dependencia en tu archivo Cargo.toml:
[dependencies]
rbpf = "0.4.1"
También puedes usar la versión de desarrollo de este repositorio de GitHub. Esto
debería ser tan simple como poner esto dentro de tu Cargo.toml:
[dependencies]
rbpf = { git = "https://github.com/qmonnet/rbpf" }
Por supuesto, si lo prefieres, puedes clonarlo localmente, modificar el crate si quieres, e indicar la ruta de tu versión local en Cargo.toml:
[dependencies]
rbpf = { path = "path/to/rbpf" }
Luego indica en tu código fuente que quieres usar el crate:
extern crate rbpf;
La API está bastante bien documentada dentro del código fuente. También deberías poder acceder a una versión en línea de la documentación desde aquí, generada automáticamente a partir de la versión de crates.io (puede que no esté actualizada con la rama principal). Ejemplos y pruebas unitarias también deberían resultar útiles. Aquí hay un resumen de cómo usar el crate.
Estos son los pasos a seguir para ejecutar un programa eBPF con rbpf:
eBPF fue diseñado inicialmente para filtrar paquetes (ahora tiene otros hooks
en el kernel de Linux, como kprobes, pero esto no está cubierto por rbpf). Como
consecuencia, la mayoría de las instrucciones de carga y almacenamiento del programa se
realizan sobre un área de memoria que representa los datos del paquete. Sin embargo, en el kernel
de Linux, el programa eBPF no accede inmediatamente a esta área de datos: inicialmente,
tiene acceso a una struct sk_buff de C en su lugar, que es un búfer que contiene
metadatos sobre el paquete—incluyendo las direcciones de memoria del inicio y del
final del área de datos del paquete. Así que el programa primero carga esos punteros desde
el sk_buff, y luego puede acceder a los datos del paquete.
Este comportamiento puede replicarse con rbpf, pero no es obligatorio. Por esta razón, tenemos varias structs que representan diferentes tipos de máquinas virtuales:
struct EbpfVmMbuffer imita al kernel. Cuando se ejecuta el programa, la
dirección proporcionada a su primer registro eBPF será la dirección de un búfer de metadatos
proporcionado por el usuario, y se espera que contenga punteros al
inicio y al final del área de memoria de datos del paquete.
struct EbpfVmFixedMbuff tiene un propósito: permitir la ejecución de programas
creados para ser compatibles con el kernel, mientras se ahorra el esfuerzo de manejar
manualmente el búfer de metadatos para el usuario. De hecho, esta struct tiene un búfer
interno estático que se pasa al programa. El usuario tiene que indicar los
valores de offset en los que el programa eBPF espera encontrar el inicio y el final
de los datos del paquete en el búfer. Al llamar a la función que ejecuta el programa
(con JIT o no), la struct actualiza automáticamente las direcciones en este
búfer estático, en los offsets indicados, para el inicio y el final de los
datos del paquete sobre los que se llama al programa.
struct EbpfVmRaw es para programas que quieren ejecutarse directamente sobre los datos del paquete.
No hay búfer de metadatos involucrado, el programa eBPF recibe directamente la
dirección de los datos del paquete en su primer registro. Este es el comportamiento de
uBPF.
struct EbpfVmNoData no toma ningún dato. El programa eBPF no toma ningún
argumento y su valor de retorno es determinista. No estoy tan seguro de que
haya un caso de uso válido para eso, pero si nada más, esto es muy útil para
las pruebas unitarias.
Todas estas structs implementan las mismas funciones 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>
Se utiliza para crear una nueva instancia de una VM. El tipo de retorno depende de la estructura desde la cual se llama a la función. Por ejemplo, rbpf::EbpfVmRaw::new(Some(my_program)) devolvería una instancia de struct rbpf::EbpfVmRaw (envuelta en un Result). Cuando se carga un programa, se verifica con un verificador muy simple (nada parecido al del kernel de Linux). Los usuarios también pueden reemplazarlo con un verificador personalizado.