
Macchina virtuale Rust e compilatore JIT per programmi eBPF
Macchina virtuale Rust (user-space) per eBPF
Questo crate contiene una macchina virtuale per l'esecuzione di programmi eBPF. BPF, ovvero Berkeley Packet Filter, è un linguaggio simile all'assembly inizialmente sviluppato per i sistemi BSD, al fine di filtrare i pacchetti nel kernel con strumenti come tcpdump così da evitare copie inutili verso lo user-space. È stato portato su Linux, dove si è evoluto in eBPF (extended BPF), una versione più veloce con più funzionalità. Mentre i programmi BPF sono originariamente pensati per essere eseguiti nel kernel, la macchina virtuale di questo crate consente di eseguirli in applicazioni user-space; contiene un interprete, un compilatore JIT x86_64 per programmi eBPF, nonché un disassembler.
È basato sul software uBPF di Rich Lane, che fa quasi la stessa cosa, ma è scritto in C.
Il crate dovrebbe compilare ed essere eseguito su Linux, MacOS X e Windows, sebbene il compilatore JIT non funzioni con Windows al momento.
Questo crate è disponibile su crates.io, quindi
dovrebbe funzionare out of the box aggiungendolo come dipendenza nel tuo file Cargo.toml:
[dependencies]
rbpf = "0.4.1"
Puoi anche usare la versione di sviluppo da questo repository GitHub. Questo
dovrebbe essere semplice come inserire quanto segue nel tuo Cargo.toml:
[dependencies]
rbpf = { git = "https://github.com/qmonnet/rbpf" }
Naturalmente, se preferisci, puoi clonarlo localmente, eventualmente modificare il crate,
e poi indicare il percorso della tua versione locale in Cargo.toml:
[dependencies]
rbpf = { path = "path/to/rbpf" }
Poi indica nel tuo codice sorgente che desideri utilizzare il crate:
extern crate rbpf;
L'API è piuttosto ben documentata all'interno del codice sorgente. Dovresti anche essere in grado di accedere a una versione online della documentazione da qui, generata automaticamente dalla versione su crates.io (potrebbe non essere aggiornata con il branch principale). Anche gli Esempi e i test unitari dovrebbero rivelarsi utili. Ecco un riepilogo di come usare il crate.
Ecco i passaggi da seguire per eseguire un programma eBPF con rbpf:
eBPF è stato inizialmente progettato per filtrare i pacchetti (ora ha altri hook
nel kernel Linux, come le kprobe, ma questo non è coperto da rbpf). Di
conseguenza, la maggior parte delle istruzioni di load e store del programma vengono
eseguite su un'area di memoria che rappresenta i dati del pacchetto. Tuttavia, nel kernel
Linux, il programma eBPF non accede immediatamente a quest'area dati: inizialmente,
ha accesso a una struct sk_buff in C, che è un buffer contenente
metadati sul pacchetto—inclusi gli indirizzi di memoria dell'inizio e della
fine dell'area dati del pacchetto. Quindi il programma prima carica quei puntatori dallo
sk_buff, e poi può accedere ai dati del pacchetto.
Questo comportamento può essere replicato con rbpf, ma non è obbligatorio. Per questo motivo, abbiamo diverse struct che rappresentano diversi tipi di macchine virtuali:
struct EbpfVmMbuffer imita il kernel. Quando il programma viene eseguito,
l'indirizzo fornito al suo primo registro eBPF sarà l'indirizzo di un buffer di metadati
fornito dall'utente, che ci si aspetta contenga puntatori all'inizio e alla
fine dell'area di memoria dei dati del pacchetto.
struct EbpfVmFixedMbuff ha uno scopo: abilitare l'esecuzione di programmi
creati per essere compatibili con il kernel, risparmiando allo stesso tempo lo sforzo di gestire manualmente
il buffer di metadati per l'utente. Infatti, questa struct ha un buffer interno
statico che viene passato al programma. L'utente deve indicare i
valori di offset ai quali il programma eBPF si aspetta di trovare l'inizio e la fine
dei dati del pacchetto nel buffer. Quando si chiama la funzione che esegue il programma
(JIT o meno), la struct aggiorna automaticamente gli indirizzi in questo
buffer statico, agli offset stabiliti, per l'inizio e la fine dei
dati del pacchetto su cui il programma viene chiamato.
struct EbpfVmRaw è per i programmi che vogliono essere eseguiti direttamente sui dati del pacchetto.
Non è coinvolto alcun buffer di metadati, il programma eBPF riceve direttamente
l'indirizzo dei dati del pacchetto nel suo primo registro. Questo è il comportamento di
uBPF.
struct EbpfVmNoData non accetta alcun dato. Il programma eBPF non prende
alcun argomento e il suo valore di ritorno è deterministico. Non sono così sicuro che
ci sia un caso d'uso valido per questo, ma se non altro, è molto utile per
i test unitari.
Tutte queste struct implementano le stesse funzioni pubbliche:
// 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>
Questo viene utilizzato per creare una nuova istanza di una VM. Il tipo restituito dipende dalla struct da cui viene chiamata la funzione. Ad esempio,
rbpf::EbpfVmRaw::new(Some(my_program)) restituirebbe un'istanza di struct rbpf::EbpfVmRaw (racchiusa in un Result). Quando un programma viene caricato, viene
verificato con un verificatore molto semplice (nulla di paragonabile a quello per il kernel
Linux). Gli utenti possono anche sostituirlo con un verificatore personalizzato.