Étendre NGINX avec Rust (une alternative au C)

NGINX | 12 octobre 2023

Au cours de son histoire relativement courte, le langage de programmation Rust a reçu des éloges exceptionnels et s’est doté d’un écosystème riche et mature. Rust et Cargo (son système de compilation, son interface de chaîne d’outils et son gestionnaire de paquets) sont tous deux des technologies appréciées et convoitées, Rust occupant durablement une place parmi les 20 premiers langages du classement des langages de programmation de RedMonk. En outre, les projets qui adoptent Rust constatent souvent une amélioration de la stabilité et une diminution des erreurs de programmation liées à la sécurité (les développeurs Android relatent par exemple une histoire éloquente d’amélioration par paliers).

Depuis quelque temps déjà, F5 suit avec enthousiasme ces évolutions autour de Rust et de sa communauté de Rustaceans. Nous avons manifesté notre intérêt en soutenant activement le langage, sa chaîne d’outils et son adoption future.

Chez NGINX, nous nous engageons désormais concrètement afin de répondre aux attentes et aux besoins des développeurs dans un monde toujours 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és, ce projet est pour vous !

Brève histoire de NGINX et de Rust

Les personnes qui suivent de près NGINX et notre dépôt GitHub savent peut-être qu’il ne s’agit pas de notre première mouture de modules basés sur Rust. Durant les premières années de Kubernetes et aux débuts du maillage de services, des travaux autour de Rust ont vu le jour, jetant les bases du projet ngx-rust.

À l’origine, ngx-rust permettait d’accélérer le développement avec NGINX d’un produit de maillage de services compatible avec Istio. Après le développement du prototype initial, ce projet est resté inchangé pendant de nombreuses années. Durant cette période, de nombreux membres de la communauté ont créé des forks du dépôt ou des projets inspirés des exemples originaux de liaisons Rust fournis dans ngx-rust.

Quelques années plus tard, notre équipe F5 Distributed Cloud Bot Defense a dû intégrer les proxys NGINX à ses services de protection. Pour cela, il a fallu développer un nouveau module.

Nous souhaitions également continuer à étoffer notre portefeuille Rust, tout en améliorant l’expérience des développeurs et en répondant à l’évolution des besoins de nos clients. Nous avons donc tiré parti de nos programmes internes de soutien à l’innovation et collaboré avec l’auteur initial de ngx-rust afin de développer un nouveau projet amélioré de liaisons Rust. Après une longue interruption, nous avons repris la publication des crates ngx-rust, avec une documentation enrichie et une ergonomie de compilation améliorée pour la communauté.

Qu’est-ce que cela signifie pour NGINX ?

Les modules sont les composants fondamentaux de NGINX et assurent l’essentiel de ses fonctionnalités. Ils constituent également le moyen le plus puissant pour les utilisateurs de NGINX de personnaliser ces fonctionnalités et de prendre en charge des cas d’usage spécifiques.

Traditionnellement, NGINX ne prenait en charge que les modules écrits en C (pour un projet lui-même écrit en C, la prise en charge des liaisons de modules dans le langage hôte constituait un choix évident et simple). Cependant, les progrès réalisés en informatique et dans la théorie des langages de programmation ont permis de dépasser les paradigmes antérieurs, notamment en matière de sécurité de la mémoire et de fiabilité. Ils ont ainsi ouvert la voie à des langages comme Rust, qui peuvent désormais être utilisés pour développer des modules NGINX.

Bien démarrer avec ngx-rust

Maintenant que nous avons abordé une partie de l’histoire de NGINX et de Rust, commençons à créer un module. Vous pouvez compiler le code source et développer votre module localement, récupérer le code source de ngx-rust et contribuer à améliorer les bindings, ou simplement récupérer le crate depuis crates.io.

Le README de ngx-rust présente les consignes de contribution et les prérequis pour effectuer une compilation locale et démarrer. Le projet n’en est encore qu’à ses débuts, mais nous souhaitons améliorer sa qualité et ses 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 de ngx-rust pour découvrir des cas plus complexes.

Les bindings sont organisés en deux crates :

  • nginx-sys est un crate qui génère des liaisons à partir du code source de NGINX. Le fichier télécharge le code source de NGINX et ses dépendances, puis utilise l’automatisation de code de bindgen pour créer les liaisons de l’interface de fonctions étrangères (FFI).
  • ngx est le crate principal qui implémente le code d’intégration Rust et les API, et qui réexporte nginx-sys. Les auteurs de modules importent ces symboles et les utilisent pour interagir avec NGINX, tandis que la réexportation de nginx-sys évite d’avoir à l’importer explicitement.

Les instructions ci-dessous permettent d’initialiser une structure d’espace de travail. Commencez par créer un répertoire de travail, puis initialisez le projet Rust :

cd $YOUR_DEV_ARENA 
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"] 

[dependencies] 
ngx = "0.3.0-beta"

Sinon, si vous souhaitez consulter le module terminé tout en suivant ce guide, vous pouvez le cloner depuis Git :

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

Et voilà, vous êtes prêt à commencer à développer votre premier module NGINX en Rust. La structure, la sémantique et l’approche générale de la création d’un module ne seront pas très différentes de celles requises avec le langage C. Pour l’instant, nous avons choisi de proposer les liaisons NGINX selon une approche itérative, afin de les générer, de les rendre utilisables et de les mettre à la disposition des développeurs pour qu’ils puissent créer leurs propres solutions innovantes. À l’avenir, nous nous efforcerons d’offrir une expérience Rust plus aboutie et plus idiomatique.

Votre première étape consiste donc à créer votre module ainsi que les directives, le contexte et les autres éléments nécessaires à son installation et à son exécution dans NGINX. Votre module sera un gestionnaire simple capable d’accepter ou de refuser une requête en fonction de la méthode HTTP. Il créera également une nouvelle directive acceptant un seul argument. Nous détaillerons cette procédure étape par étape, mais vous pouvez consulter le code complet dans le dépôt ngx-rust-howto sur GitHub.

Remarque : cet article de blog présente les spécificités de Rust, plutôt que la création de modules NGINX en général. Si vous souhaitez créer d’autres modules NGINX, consultez les nombreuses et excellentes discussions publiées par la communauté. Elles vous permettront également de mieux comprendre les principes fondamentaux de l’extension de NGINX (pour en savoir plus, consultez la section « Ressources » ci-dessous).

Enregistrement du module

Vous pouvez créer votre module Rust en implémentant le trait « HTTPModule », qui définit tous les points d’entrée de NGINX (postconfiguration, preconfiguration, create_main_conf, etc.). Il suffit à l’auteur d’un module d’implémenter les fonctions nécessaires à son fonctionnement. Ce module implémentera la méthode postconfiguration afin d’installer son gestionnaire de requêtes.

Remarque : 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 for Module { 
    type MainConf = (); 
    type SrvConf = (); 
    type LocConf = ModuleConfig; 

    unsafe extern "C" fn postconfiguration(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; 
        if h.is_null() { 
            return core::Status::NGX_ERROR.into(); 
        } 

        // set an Access phase handler 
        *h = Some(howto_access_handler); 
        core::Status::NGX_OK.into() 
    } 
} 

Le module Rust n’a besoin que d’un hook postconfiguration lors de la phase d’accès NGX_HTTP_ACCESS_PHASE. Les modules peuvent enregistrer des gestionnaires pour les 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 que le gestionnaire de phase « howto_access_handler » est ajouté juste avant le retour de la fonction. Nous y reviendrons plus tard. Pour l’instant, retenez simplement qu’il s’agit de la fonction qui exécutera la logique de traitement au cours de la chaîne de requêtes.

Selon le type de votre module et ses besoins, voici les hooks d’enregistrement disponibles :

  • préconfiguration
  • postconfiguration
  • create_main_conf
  • init_main_conf
  • create_srv_conf
  • merge_srv_conf
  • create_loc_conf
  • merge_loc_conf

État de la configuration

Il est maintenant temps de créer un espace de stockage pour votre module. Ces données comprennent tous les paramètres de configuration nécessaires ou l’état interne utilisé pour traiter les requêtes ou modifier le comportement. En substance, toutes les informations que le module doit conserver 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 la configuration doit implémenter les traits Merge and Default.

Lors de la définition de votre module à l’étape précédente, 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 ; seul le type « LocConf » est donc défini.

Pour créer un espace de stockage pour l’état et la configuration de votre module, définissez une structure et implémentez le trait Merge :

#[derive(Debug, Default)] 
struct ModuleConfig { 
    enabled: bool, 
    method: String, 
} 

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

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

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

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

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 auprès du système central.

Grâce aux liaisons FFI, les développeurs 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 chaîne de caractères. Dans ce cas, nous définissons une commande, implémentons une fonction de définition, puis, dans la section suivante, intégrons ces commandes au système central. 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 { 
        name: ngx_string!("howto"), 
        type_: (NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1) as ngx_uint_t, 
        set: Some(ngx_http_howto_commands_set_method), 
        conf: NGX_RS_HTTP_LOC_CONF_OFFSET, 
        offset: 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() 
} 

Intégration dans le module

Maintenant que vous disposez d’une fonction d’enregistrement, d’un gestionnaire de phase et de commandes de configuration, vous pouvez relier tous ces éléments et exposer les fonctions au système principal. Créez une structure statique ngx_module_t contenant des références à vos fonctions d’enregistrement, à vos gestionnaires de phase et à vos 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, puis exposez-les à l’aide de la macro ngx_modules! macro. Dans l’exemple ci-dessous, vous pouvez voir comment commands est défini dans le champ des commandes et comment le contexte faisant référence aux fonctions d’enregistrement des modules est défini dans le champ ctx. Pour ce module, tous les autres champs conservent leurs valeurs par défaut.

#[no_mangle] 
static ngx_http_howto_module_ctx: ngx_http_module_t = ngx_http_module_t { 
    preconfiguration: Some(Module::preconfiguration), 
    postconfiguration: Some(Module::postconfiguration), 
    create_main_conf: Some(Module::create_main_conf), 
    init_main_conf: Some(Module::init_main_conf), 
    create_srv_conf: Some(Module::create_srv_conf), 
    merge_srv_conf: Some(Module::merge_srv_conf), 
    create_loc_conf: Some(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(), 
    name: std::ptr::null_mut(), 
    spare0: 0, 
    spare1: 0, 
    version: nginx_version as ngx_uint_t, 
    signature: NGX_RS_MODULE_SIGNATURE.as_ptr() as *const c_char, 

    ctx: &ngx_http_howto_module_ctx as *const _ as *mut _, 
    commands: unsafe { &ngx_http_howto_commands[0] as *const _ as *mut _ }, 
    type_: NGX_HTTP_MODULE as ngx_uint_t, 

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

    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 aurez pratiquement terminé les étapes nécessaires pour configurer et enregistrer un nouveau module Rust. Cela dit, vous devez encore implémenter le gestionnaire de phase (howto_access_handler) défini dans le hook postconfiguration.

Gestionnaires

Les gestionnaires sont appelés pour chaque requête entrante et effectuent l’essentiel du travail de votre module. L’équipe de ngx-rust s’est principalement concentrée sur les gestionnaires de requêtes, qui ont bénéficié de la majorité des premières améliorations ergonomiques. Alors que les étapes de configuration précédentes nécessitent d’écrire du Rust dans un style proche du C, ngx-rust offre davantage de fonctionnalités pratiques et d’utilitaires pour les gestionnaires de requêtes.

Comme le montre l’exemple ci-dessous, ngx-rust fournit la macro « http_request_handler! », qui accepte une closure Rust appelée avec une instance de « Request ». Il fournit également des utilitaires permettant de récupérer la configuration et les variables, de définir ces variables et d’accéder à la mémoire, à d’autres primitives NGINX et aux API.

Pour lancer une procédure de gestion, invoquez la macro et fournissez votre logique métier sous la forme d’une closure Rust. Pour le module ngx-rust-howto, vérifiez la méthode de la requête afin d’autoriser la poursuite de son traitement.

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("module config is none"); ngx_log_debug_http!(request, "howto module enabled called"); 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, } }); 

Et voilà, vous avez terminé votre premier module Rust !

Le dépôt ngx-rust-howto sur GitHub contient un fichier de configuration NGINX dans le répertoire conf. Vous pouvez également compiler le module (avec cargo build), ajouter son fichier binaire à la directive load_module dans un fichier nginx.conf local, puis 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 prise en charge par défaut par ngx-rust. Lorsque vous compilez et exécutez des modules dynamiques, veillez à utiliser la même valeur de NGINX_VERSION pour les compilations ngx-rust que pour l’instance de NGINX exécutée sur votre machine.

Conclusion

NGINX est un système logiciel mature, enrichi au fil des années de nombreuses fonctionnalités et de multiples cas d’usage. C’est à la fois un proxy performant, un répartiteur de charge et un serveur web de classe mondiale. Sa présence sur le marché est assurée pour de nombreuses années, ce qui renforce notre volonté de développer ses capacités et d’offrir à nos utilisateurs de nouvelles façons d’interagir avec lui. Compte tenu de la popularité de Rust auprès des développeurs et de ses garanties de sécurité renforcées, nous sommes ravis de proposer la possibilité d’utiliser Rust aux côtés du meilleur serveur web au monde.

Cependant, la maturité de NGINX et son écosystème riche en fonctionnalités se traduisent par une vaste surface d’API, dont ngx-rust n’a encore exploré qu’une infime partie. Le projet vise à s’améliorer et à s’étendre en ajoutant des interfaces Rust plus idiomatiques, en développant des modules de référence supplémentaires et en améliorant l’ergonomie de l’écriture de modules.

C’est là que vous intervenez ! Le projet ngx-rust est ouvert à toutes et à tous et disponible sur GitHub. Nous sommes impatients de collaborer avec la communauté NGINX afin de continuer à améliorer les fonctionnalités du module et sa facilité d’utilisation. Découvrez-le et testez vous-même les bindings ! N’hésitez pas non plus à nous contacter, à signaler des problèmes, à soumettre des pull requests et à échanger avec nous sur le canal Slack de la communauté NGINX.

Ressources


Partager
Mots-clés : F5 NGINX, Tech

Articles de blog associés

Tendances actuelles des technologies cloud natives, de l’ingénierie de plateforme et de l’IA
NGINX | 13/10/2025

Tendances actuelles des technologies cloud natives, de l’ingénierie de plateforme et de l’IA

Découvrez les principaux enseignements tirés des réponses à l’enquête annuelle F5 NGINX sur les dernières tendances en matière de technologies cloud natives, de sécurité, d’ingénierie de plateforme et d’IA.

Quels contrôleurs Ingress NGINX sont affectés par les vulnérabilités CVE-2022-4886, CVE-2023-5043 et CVE-2023-5044 ?
NGINX | 03/11/2023

Quels contrôleurs Ingress NGINX sont affectés par les vulnérabilités CVE-2022-4886, CVE-2023-5043 et CVE-2023-5044 ?

Lorsque de nouvelles CVE sont signalées, il est facile de ne plus savoir si elles concernent un outil de contrôleur Ingress NGINX particulier, car il existe plusieurs projets. Cet article présente les CVE et explique comment distinguer les différents projets de contrôleurs Ingress afin de dissiper toute confusion.

Votre API est-elle en temps réel ? Testez sa latence avec l’outil rtapi de NGINX
NGINX | 11/03/2020

Votre API est-elle en temps réel ? Testez sa latence avec l’outil rtapi de NGINX

Votre API répond-elle aux requêtes en moins de 30 ms ? Si ce n’est pas le cas, il ne s’agit pas d’une API en temps réel offrant la faible latence exigée par la grande majorité des organisations. Mesurez la latence de votre API avec l’outil rtapi de NGINX et découvrez comment NGINX peut l’aider à offrir une réactivité en temps réel.

Un testeur d’expressions régulières pour NGINX et NGINX Plus
NGINX | 11/06/2019

Un testeur d’expressions régulières pour NGINX et NGINX Plus

Le testeur d’expressions régulières (regex) pour NGINX et NGINX Plus vous évite de tâtonner en vous indiquant si une expression régulière utilisée dans un bloc location ou map correspond aux valeurs comme prévu. Il génère la configuration appropriée et affiche les valeurs des groupes de capture.

Caches partagés avec les clusters de cache NGINX Plus, partie 1
NGINX | 24/01/2017

Caches partagés avec les clusters de cache NGINX Plus, partie 1

Optimisez la capacité de votre cache web en le répartissant entre plusieurs serveurs de cache web NGINX ou NGINX Plus. Découvrez comment procéder dans cet article.

Introduction aux microservices
NGINX | 19/05/2015

Introduction aux microservices

Les microservices suscitent actuellement beaucoup d’intérêt. Cet article de blog est le premier d’une série de sept consacrée à la conception, au développement et au déploiement de microservices.