BLOG | NGINX

Extension de NGINX avec Rust (une alternative à C)

NGINX-Partie-de-F5-horiz-black-type-RGB
Vignette de Matthew Yacobucci
Matthieu Yacobucci
Publié le 12 octobre 2023

Au cours de son histoire relativement courte, le langage de programmation Rust a recueilli des distinctions exceptionnelles ainsi qu'un écosystème riche et mature. Rust et Cargo (son système de construction, son interface de chaîne d'outils et son gestionnaire de paquets) sont des technologies admirées et souhaitées dans le paysage, Rust occupant une position stable dans le top 20 des langages du classement des langages de programmation de RedMonk . De plus, les projets qui adoptent Rust montrent souvent une amélioration de la stabilité et des erreurs de programmation liées à la sécurité (à titre d’exemple, les développeurs Android racontent une histoire convaincante d’amélioration ponctuée).

F5 observe depuis un certain temps avec enthousiasme ces développements autour de Rust et de sa communauté de Rustacéens. Nous en avons pris note en défendant activement le langage , sa chaîne d'outils et son adoption à l'avenir.

Chez NGINX, nous nous engageons désormais à satisfaire les souhaits et les besoins des développeurs dans un monde de plus en plus numérique et soucieux de la sécurité. Nous sommes ravis d'annoncer le projet ngx-rust – une nouvelle façon d'écrire des modules NGINX avec le langage Rust. Rustacéens, celui-ci est pour vous !

Un bref historique de NGINX et Rust

Les adeptes proches de NGINX et de notre GitHub pourraient se rendre compte que ce n'est pas notre première incarnation de modules basés sur Rust. Au cours des premières années de Kubernetes et des premiers jours du service mesh , certains travaux se sont concentrés sur Rust, créant les bases du projet ngx-rust.

À l'origine, ngx-rust servait à accélérer le développement d'un produit de service mesh compatible Istio avec NGINX. Après le développement du prototype initial, ce projet est resté inchangé pendant de nombreuses années. Pendant cette période, de nombreux membres de la communauté ont bifurqué le référentiel ou créé des projets inspirés des exemples de liaisons Rust originaux fournis dans ngx-rust.

Avance rapide et notre équipe F5 Distributed Cloud Bot Defense a dû intégrer les proxys NGINX dans ses services de protection. Cela a nécessité la construction d’un nouveau module.

Nous souhaitions également continuer à élargir notre portefeuille Rust tout en améliorant l’expérience des développeurs et en répondant aux besoins évolutifs des clients. Nous avons donc tiré parti de nos parrainages d’innovation internes et travaillé avec l’auteur original de ngx-rust pour développer un projet de liaisons Rust nouveau et amélioré. Après une longue pause, nous avons redémarré la publication des caisses ngx-rust avec une documentation améliorée et des améliorations pour créer une ergonomie destinée à l'utilisation communautaire.

Qu'est-ce que cela signifie pour NGINX ?

Les modules sont les éléments de base de NGINX, implémentant la plupart de ses fonctionnalités. Les modules constituent également le moyen le plus puissant pour les utilisateurs de NGINX de personnaliser cette fonctionnalité et de créer un support pour des cas d'utilisation spécifiques.

NGINX a traditionnellement uniquement pris en charge les modules écrits en C (en tant que projet écrit en C, la prise en charge des liaisons de modules dans le langage hôte était un choix clair et facile). Cependant, les progrès de l’informatique et de la théorie des langages de programmation ont amélioré les paradigmes passés, notamment en ce qui concerne la sécurité et l’exactitude de la mémoire. Cela a ouvert la voie à des langages comme Rust, qui peuvent désormais être mis à disposition pour le développement de modules NGINX.

Comment démarrer avec ngx-rust

Maintenant que nous avons couvert une partie de l'histoire de NGINX et de Rust, commençons à créer un module. Vous êtes libre de construire à partir de la source et de développer votre module localement, d'extraire la source de ngx-rust et d'aider à créer de meilleures liaisons, ou simplement d'extraire la caisse de crates.io .

Le fichier README de ngx-rust couvre les directives de contribution et les exigences de construction locales pour commencer. C'est encore tôt et dans sa phase de développement initial, mais nous visons à améliorer la qualité et les fonctionnalités avec le soutien de la communauté. Dans ce tutoriel, nous nous concentrons sur la création d'un module indépendant simple. Vous pouvez également consulter les exemples ngx-rust pour des leçons plus complexes.

Les reliures sont organisées en deux caisses :

  • nginx-sys est une caisse qui génère des liaisons à partir du code source NGINX. Le fichier télécharge le code source NGINX, les dépendances et utilise l'automatisation du code bindgen pour créer les liaisons d'interface de fonction étrangère (FFI).
  • ngx est la caisse principale qui implémente le code de colle Rust, les API et réexporte nginx-sys . Les auteurs de modules importent et interagissent avec NGINX via ces symboles tandis que la réexportation de nginx-sys supprime la nécessité de l'importer explicitement.

Les instructions ci-dessous initialiseront un espace de travail squelette. Commencez par créer un répertoire de travail et initialisez le projet Rust :

cd $VOTRE_ARÈNE_DEV 
mkdir ngx-rust-howto 
cd ngx-rust-howto 
cargo init --lib

Ensuite, ouvrez le fichier Cargo.toml et ajoutez la section suivante :

[lib] 
crate-type = ["cdylib"] 

[dépendances] 
ngx = "0.3.0-beta"

Alternativement, si vous souhaitez voir le module terminé pendant que vous lisez, il peut être cloné depuis Git :

cd $YOUR_DEV_ARENA 
git clone git@github.com:f5yacobucci/ngx-rust-howto.git

Et avec cela, vous êtes prêt à commencer à développer votre premier module NGINX Rust. La structure, la sémantique et l’approche générale de la construction d’un module ne seront pas très différentes de ce qui est nécessaire lors de l’utilisation de C. Pour l’instant, nous avons décidé de proposer des liaisons NGINX dans une approche itérative pour que les liaisons soient générées, utilisables et entre les mains des développeurs pour créer leurs offres inventives. À l’avenir, nous travaillerons à créer une expérience Rust meilleure et plus idiomatique.

Cela signifie que votre première étape consiste à construire votre module en tandem avec toutes les directives, le contexte et les autres aspects requis pour l'installation et l'exécution dans NGINX. Votre module sera un simple gestionnaire qui peut accepter ou refuser une requête en fonction de la méthode HTTP, et il créera une nouvelle directive qui accepte un seul argument. Nous en discuterons par étapes, mais vous pouvez vous référer au code complet dans le référentiel ngx-rust-howto sur GitHub.

Note: Ce blog se concentre sur la description des spécificités de Rust, plutôt que sur la façon de créer des modules NGINX en général. Si vous êtes intéressé par la création d'autres modules NGINX, veuillez vous référer aux nombreuses et excellentes discussions au sein de la communauté. Ces discussions vous donneront également une explication plus fondamentale sur la façon d’étendre NGINX (voir plus dans la section Ressources ci-dessous).

Inscription au module

Vous pouvez créer votre module Rust en implémentant le trait HTTPModule , qui définit tous les points d'entrée NGINX ( postconfiguration , preconfiguration , create_main_conf , etc.). Un rédacteur de module n’a besoin d’implémenter que les fonctions nécessaires à sa tâche. Ce module implémentera la méthode de postconfiguration pour installer son gestionnaire de requêtes.

Note: Si vous n'avez pas cloné le dépôt ngx-rust-howto , vous pouvez commencer à modifier le fichier src/lib.rs créé par cargo init .

struct Module; 

impl http::HTTPModule pour Module { 
type MainConf = (); 
type SrvConf = (); 
type LocConf = ModuleConfig; 

fn postconfiguration externe "C" non sécurisé(cf: *mut ngx_conf_t) -> ngx_int_t { 
let htcf = http::ngx_http_conf_get_module_main_conf(cf, &ngx_http_core_module); 

let h = ngx_array_push( 
&mut (*htcf).phases[ngx_http_phases_NGX_HTTP_ACCESS_PHASE as usize].handlers, 
) as *mut ngx_http_handler_pt; 
si h.is_null() { 
return core::Status::NGX_ERROR.into(); 
} 

// définir un gestionnaire de phase d'accès 
*h = Some(howto_access_handler); 
core::Status::NGX_OK.into() 
} 
} 

Le module Rust n'a besoin que d'un hook de postconfiguration lors de la phase d'accès NGX_HTTP_ACCESS_PHASE . Les modules peuvent enregistrer des gestionnaires pour différentes phases de la requête HTTP. Pour plus d'informations à ce sujet, consultez les détails dans le guide de développement .

Vous verrez le gestionnaire de phase howto_access_handler ajouté juste avant le retour de la fonction. Nous y reviendrons plus tard. Pour l'instant, notez simplement que c'est la fonction qui exécutera la logique de gestion pendant la chaîne de requête.

En fonction de votre type de module et de ses besoins, voici les hooks d'enregistrement disponibles :

  • préconfiguration
  • post-configuration
  • créer_main_conf
  • init_main_conf
  • créer_srv_conf
  • merge_srv_conf
  • créer_loc_conf
  • merge_loc_conf

État de la configuration

Il est maintenant temps de créer un stockage pour votre module. Ces données incluent tous les paramètres de configuration requis ou l’état interne utilisé pour traiter les demandes ou modifier le comportement. Essentiellement, toutes les informations dont le module a besoin pour persister peuvent être placées dans des structures et enregistrées. Ce module Rust utilise une structure ModuleConfig au niveau de la configuration de l'emplacement. Le stockage de configuration doit implémenter les caractéristiques Merge et Default .

Lors de la définition de votre module à l’étape ci-dessus, vous pouvez définir les types de vos configurations principales, de serveur et d’emplacement. Le module Rust que vous développez ici ne prend en charge que les emplacements, donc seul le type LocConf est défini.

Pour créer un stockage d'état et de configuration pour votre module, définissez une structure et implémentez la fonction Merge :

#[derive(Debug, Default)] 
struct ModuleConfig { 
activé : bool, 
méthode : Chaîne, 
} 

impl http::Merge pour ModuleConfig { 
fn merge(&mut self, prev: &ModuleConfig) -> Result<(), MergeConfigError> { 
si prev.enabled { 
self.enabled = true; 
} 

si self.method.is_empty() { 
self.method = String::from(si !prev.method.is_empty() { 
&prev.method 
} else { 
"" 
}); 
} 

si self.enabled && self.method.is_empty() { 
return Err(MergeConfigError::NoValue); 
} 
Ok(()) 
} 
} 

ModuleConfig stocke un état activé/désactivé dans le champ activé, ainsi qu'une méthode de requête HTTP. Le gestionnaire vérifiera cette méthode et autorisera ou interdira les demandes.

Une fois le stockage défini, votre module peut créer des directives et des règles de configuration que les utilisateurs peuvent définir eux-mêmes. NGINX utilise le type ngx_command_t et un tableau pour enregistrer les directives définies par le module sur le système principal.

Grâce aux liaisons FFI, les auteurs de modules Rust ont accès au type ngx_command_t et peuvent enregistrer des directives comme ils le feraient en C. Le module ngx-rust-howto définit une directive howto qui accepte une valeur de chaîne. Pour ce cas, nous définissons une commande, implémentons une fonction setter, puis (dans la section suivante) connectons ces commandes au système principal. N'oubliez pas de terminer votre tableau de commandes avec la macro ngx_command_null! fournie.

Voici comment créer une directive simple à l’aide des commandes NGINX :

#[no_mangle] 
static mut ngx_http_howto_commands: [ngx_command_t; 2] = [ 
ngx_command_t { 
nom: ngx_string!("howto"), 
type_: (NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1) comme ngx_uint_t, 
définir : Certains (ngx_http_howto_commands_set_method), 
conf : NGX_RS_HTTP_LOC_CONF_OFFSET, 
décalage : 0, 
post: std::ptr::null_mut(), 
}, 
ngx_null_command!(), 
]; 

#[no_mangle] 
extern "C" fn ngx_http_howto_commands_set_method( 
cf: *mut ngx_conf_t, 
_cmd: *mut ngx_command_t, 
conf: *mut c_void, 
) -> *mut c_char { 
unsafe { 
let conf = &mut *(conf as *mut ModuleConfig); 
let args = (*(*cf).args).elts as *mut ngx_str_t; 
conf.enabled = true; 
conf.method = (*args.add(1)).to_string(); 
}; 

std::ptr::null_mut() 
} 

Accrochage dans le module

Maintenant que vous disposez d’une fonction d’enregistrement, d’un gestionnaire de phase et de commandes de configuration, vous pouvez tout assembler et exposer les fonctions au système principal. Créez une structure ngx_module_t statique avec des références à vos fonctions d'enregistrement, gestionnaires de phase et commandes de directive. Chaque module doit contenir une variable globale de type ngx_module_t .

Créez ensuite un contexte et un type de module statique, et exposez-les avec la macro ngx_modules!. Dans l'exemple ci-dessous, vous pouvez voir comment les commandes sont définies dans le champ commandes et le contexte référençant les fonctions d'enregistrement des modules est défini dans le champ ctx . Pour ce module, tous les autres champs sont effectivement des valeurs par défaut.

#[no_mangle] 
static ngx_http_howto_module_ctx: ngx_http_module_t = ngx_http_module_t { 
préconfiguration : Certains(Module::preconfiguration), 
postconfiguration : Certains(Module::postconfiguration), 
create_main_conf : Certains(Module::create_main_conf), 
init_main_conf : Certains(Module::init_main_conf), 
create_srv_conf : Certains(Module::create_srv_conf), 
merge_srv_conf : Certains(Module::merge_srv_conf), 
create_loc_conf : Certains(Module::create_loc_conf), 
merge_loc_conf : Some(Module::merge_loc_conf), 
}; 

ngx_modules!(ngx_http_howto_module); 

#[no_mangle] 
pub static mut ngx_http_howto_module: ngx_module_t = ngx_module_t { 
ctx_index: ngx_uint_t::max_value(), 
index: ngx_uint_t::max_value(), 
nom: std::ptr::null_mut(), 
spare0: 0, 
spare1 : 0, 
version : nginx_version comme ngx_uint_t, 
signature : NGX_RS_MODULE_SIGNATURE.as_ptr() comme *const c_char, 

ctx : &ngx_http_howto_module_ctx comme *const _ comme *mut _, 
commandes : non sécurisé { &ngx_http_howto_commands[0] comme *const _ comme *mut _ }, 
type_ : NGX_HTTP_MODULE comme ngx_uint_t, 

init_master : Aucun, 
init_module : Aucun, 
init_process : Aucun, 
init_thread : Aucun, 
exit_thread : Aucun, 
exit_process : Aucun, 
exit_master : Aucun, 

spare_hook0 : 0, 
spare_hook1 : 0, 
spare_hook2 : 0, 
spare_hook3 : 0, 
spare_hook4 : 0, 
spare_hook5 : 0, 
spare_hook6 : 0, 
spare_hook7 : 0, 
}; 

Après cela, vous avez pratiquement terminé les étapes nécessaires pour configurer et enregistrer un nouveau module Rust. Cela dit, vous devez toujours implémenter le gestionnaire de phase (howto_access_handler) qui a été défini dans le hook de postconfiguration .

Gestionnaires

Les gestionnaires sont appelés pour chaque demande entrante et effectuent la plupart du travail de votre module. Les gestionnaires de requêtes ont été au centre des préoccupations de l’équipe ngx-rust et sont l’endroit où la majorité des améliorations ergonomiques initiales ont été apportées. Alors que les étapes de configuration précédentes nécessitent l'écriture de Rust dans un style de type C, ngx-rust offre plus de commodité et d'utilitaires pour les gestionnaires de requêtes.

Comme le montre l'exemple ci-dessous, ngx-rust fournit la macro http_request_handler! pour accepter une fermeture Rust appelée avec une instance de Request . Il fournit également des utilitaires pour obtenir la configuration et les variables, définir ces variables et accéder à la mémoire, à d'autres primitives NGINX et aux API.

Pour lancer une procédure de gestionnaire, appelez la macro et fournissez votre logique métier sous forme de fermeture Rust. Pour le module ngx-rust-howto, vérifiez la méthode de la requête pour permettre à la requête de continuer à être traitée.

http_request_handler!(howto_access_handler, |request: &mut http::Request| { 
let co = unsafe { request.get_module_loc_conf::(&ngx_http_howto_module) }; 
let co = co.expect("la configuration du module est nulle"); 

ngx_log_debug_http!(request, "module howto activé appelé"); 
match co.enabled { 
true => { 
let method = request.method(); 
if method.as_str() == co.method { 
return core::Status::NGX_OK; 
} 
http::HTTPStatus::FORBIDDEN.into() 
} 
false => core::Status::NGX_OK, 
} 
}); 

Avec cela, vous avez terminé votre premier module Rust !

Le référentiel ngx-rust-howto sur GitHub contient un fichier de configuration NGINX dans le répertoire conf. Vous pouvez également compiler (avec cargo build ), ajouter le binaire du module à la directive load_module dans un nginx.conf local et l'exécuter à l'aide d'une instance de NGINX. Pour rédiger ce tutoriel, nous avons utilisé NGINX v1.23.3, la version NGINX_VERSION par défaut prise en charge par ngx-rust. Lors de la création et de l'exécution de modules dynamiques, assurez-vous d'utiliser la même NGINX_VERSION pour les builds ngx-rust que l'instance NGINX que vous exécutez sur votre machine.

Conclusion

NGINX est un système logiciel mature avec des années de fonctionnalités et de cas d'utilisation intégrés. Il s'agit d'un proxy performant, d'un équilibreur de charge et d'un serveur Web de classe mondiale. Sa présence sur le marché est certaine pour les années à venir, ce qui alimente notre motivation à développer ses capacités et à offrir à nos utilisateurs de nouvelles méthodes pour interagir avec lui. Avec la popularité de Rust parmi les développeurs et ses contraintes de sécurité améliorées, nous sommes ravis d’offrir la possibilité d’utiliser Rust avec le meilleur serveur Web au monde.

Cependant, la maturité de NGINX et son écosystème riche en fonctionnalités créent tous deux une grande surface d’API et ngx-rust n’a fait qu’effleurer la surface. Le projet vise à s'améliorer et à s'étendre en ajoutant des interfaces Rust plus idiomatiques, en créant des modules de référence supplémentaires et en faisant progresser l'ergonomie des modules d'écriture.

C'est ici que vous intervenez ! Le projet ngx-rust est ouvert à tous et disponible sur GitHub . Nous sommes impatients de travailler avec la communauté NGINX pour continuer à améliorer les capacités et la facilité d'utilisation du module. Découvrez-le et expérimentez vous-même les fixations ! Et n'hésitez pas à nous contacter, à signaler des problèmes ou des relations publiques et à nous interagir sur le canal Slack de la communauté NGINX .

Ressources


« Cet article de blog peut faire référence à des produits qui ne sont plus disponibles et/ou qui ne sont plus pris en charge. Pour obtenir les informations les plus récentes sur les produits et solutions F5 NGINX disponibles, explorez notre famille de produits NGINX . NGINX fait désormais partie de F5. Tous les liens NGINX.com précédents redirigeront vers un contenu NGINX similaire sur F5.com."