
Machine virtuelle Rust et compilateur JIT pour programmes eBPF
Machine virtuelle Rust (espace utilisateur) pour eBPF
Ce crate contient une machine virtuelle pour l'exécution de programmes eBPF. BPF, pour Berkeley Packet Filter, est un langage de type assembleur initialement développé pour les systèmes BSD, afin de filtrer les paquets dans le noyau avec des outils tels que tcpdump, évitant ainsi des copies inutiles vers l'espace utilisateur. Il a été porté sur Linux, où il a évolué vers eBPF (extended BPF), une version plus rapide avec davantage de fonctionnalités. Bien que les programmes BPF soient à l'origine destinés à s'exécuter dans le noyau, la machine virtuelle de ce crate permet de les exécuter dans des applications en espace utilisateur ; elle contient un interpréteur, un compilateur JIT x86_64 pour les programmes eBPF, ainsi qu'un désassembleur.
Il est basé sur le logiciel uBPF de Rich Lane, qui fait presque la même chose, mais est écrit en C.
Le crate est censé compiler et s'exécuter sur Linux, MacOS X et Windows, bien que le compilateur JIT ne fonctionne pas avec Windows pour le moment.
Ce crate est disponible sur crates.io, il
devrait donc fonctionner directement en l'ajoutant comme dépendance dans votre fichier Cargo.toml :
[dependencies]
rbpf = "0.4.1"
Vous pouvez également utiliser la version de développement depuis ce dépôt GitHub. Cela
devrait être aussi simple que de mettre ceci dans votre Cargo.toml :
[dependencies]
rbpf = { git = "https://github.com/qmonnet/rbpf" }
Bien sûr, si vous préférez, vous pouvez le cloner localement, éventuellement modifier le crate,
puis indiquer le chemin de votre version locale dans Cargo.toml :
[dependencies]
rbpf = { path = "path/to/rbpf" }
Indiquez ensuite dans votre code source que vous souhaitez utiliser le crate :
extern crate rbpf;
L'API est plutôt bien documentée dans le code source. Vous devriez également pouvoir accéder à une version en ligne de la documentation depuis ici, générée automatiquement à partir de la version crates.io (peut ne pas être à jour avec la branche principale). Les exemples et les tests unitaires devraient également s'avérer utiles. Voici un résumé de la façon d'utiliser le crate.
Voici les étapes à suivre pour exécuter un programme eBPF avec rbpf :
eBPF a été initialement conçu pour filtrer les paquets (il possède désormais d'autres points d'accroche dans le noyau Linux, tels que les kprobes, mais cela n'est pas couvert par rbpf). Par conséquent, la plupart des instructions de chargement et de stockage du programme sont effectuées sur une zone mémoire représentant les données du paquet. Cependant, dans le noyau Linux, le programme eBPF n'accède pas immédiatement à cette zone de données : initialement, il a accès à une struct sk_buff en C à la place, qui est un tampon contenant des métadonnées sur le paquet—y compris les adresses mémoire du début et de la fin de la zone de données du paquet. Le programme charge donc d'abord ces pointeurs depuis la sk_buff, puis peut accéder aux données du paquet.
Ce comportement peut être reproduit avec rbpf, mais il n'est pas obligatoire. Pour cette raison, nous avons plusieurs structs représentant différents types de machines virtuelles :
struct EbpfVmMbuffer imite le noyau. Lorsque le programme est exécuté, l'adresse fournie à son premier registre eBPF sera l'adresse d'un tampon de métadonnées fourni par l'utilisateur, et qui est censé contenir des pointeurs vers le début et la fin de la zone mémoire des données du paquet.
struct EbpfVmFixedMbuff a un seul objectif : permettre l'exécution de programmes créés pour être compatibles avec le noyau, tout en épargnant à l'utilisateur l'effort de gérer manuellement le tampon de métadonnées. En fait, cette struct possède un tampon interne statique qui est passé au programme. L'utilisateur doit indiquer les valeurs de décalage auxquelles le programme eBPF s'attend à trouver le début et la fin des données du paquet dans le tampon. Lors de l'appel de la fonction qui exécute le programme (JITté ou non), la struct met automatiquement à jour les adresses dans ce tampon statique, aux décalages indiqués, pour le début et la fin des données du paquet sur lesquelles le programme est appelé.
struct EbpfVmRaw est destinée aux programmes qui veulent s'exécuter directement sur les données du paquet. Aucun tampon de métadonnées n'est impliqué, le programme eBPF reçoit directement l'adresse des données du paquet dans son premier registre. C'est le comportement d'uBPF.
struct EbpfVmNoData ne prend aucune donnée. Le programme eBPF ne prend aucun argument et sa valeur de retour est déterministe. Je ne suis pas certain qu'il existe un cas d'usage valide pour cela, mais à défaut d'autre chose, c'est très utile pour les tests unitaires.
Toutes ces structs implémentent les mêmes fonctions publiques :
// 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>
Ceci est utilisé pour créer une nouvelle instance d'une VM. Le type de retour dépend de la structure à partir de laquelle la fonction est appelée. Par exemple, rbpf::EbpfVmRaw::new(Some(my_program)) renverrait une instance de struct rbpf::EbpfVmRaw (encapsulée dans un Result). Lorsqu'un programme est chargé, il est vérifié avec un vérificateur très simple (rien de comparable à celui du noyau Linux). Les utilisateurs peuvent également le remplacer par un vérificateur personnalisé.