Ampliación de NGINX con Rust (una alternativa a C)

NGINX | 12 de octubre de 2023

A lo largo de su relativamente corta historia, el lenguaje de programación Rust ha cosechado elogios excepcionales y ha desarrollado un ecosistema rico y maduro. Tanto Rust como Cargo (su sistema de compilación, interfaz de herramientas y gestor de paquetes) son tecnologías admiradas y deseadas en el sector, y Rust mantiene una posición estable entre los 20 primeros lenguajes de la clasificación de lenguajes de programación de RedMonk. Además, los proyectos que adoptan Rust suelen mostrar mejoras en cuanto a estabilidad y errores de programación relacionados con la seguridad (por ejemplo, los desarrolladores de Android cuentan una historia convincente de mejoras significativas).

F5 lleva tiempo siguiendo con entusiasmo estos avances en torno a Rust y su comunidad de Rustaceans. Hemos mostrado nuestro interés mediante una defensa activa del lenguaje, de su cadena de herramientas y de su adopción futura.

En NGINX, hemos decidido implicarnos de lleno para satisfacer los deseos y necesidades de los desarrolladores en un mundo cada vez más digital y preocupado por la seguridad. Nos complace anunciar el proyecto ngx-rust —una nueva forma de escribir módulos de NGINX con el lenguaje Rust—. Rustáceos, ¡esto es para vosotros!

Breve historia de NGINX y Rust

Quienes sigan de cerca NGINX y nuestro GitHub quizá sepan que esta no es nuestra primera incursión en los módulos basados en Rust. Durante los primeros años de Kubernetes y los inicios de la malla de servicios, se llevaron a cabo algunos trabajos en torno a Rust que sentaron las bases del proyecto ngx-rust.

Originalmente, ngx-rust sirvió para acelerar el desarrollo de un producto de malla de servicios compatible con Istio mediante NGINX. Tras desarrollar el prototipo inicial, el proyecto permaneció sin cambios durante muchos años. Durante ese tiempo, muchos miembros de la comunidad bifurcaron el repositorio o crearon proyectos inspirados en los ejemplos originales de enlaces de Rust incluidos en ngx-rust.

Con el paso del tiempo, nuestro equipo de F5 Distributed Cloud Bot Defense necesitó integrar los proxies de NGINX en sus servicios de protección. Para ello, fue necesario desarrollar un nuevo módulo.

También queríamos seguir ampliando nuestra cartera de Rust, a la vez que mejorábamos la experiencia de los desarrolladores y respondíamos a las necesidades cambiantes de los clientes. Para ello, aprovechamos nuestros programas internos de patrocinio de la innovación y colaboramos con el autor original de ngx-rust para desarrollar un proyecto nuevo y mejorado de enlaces para Rust. Tras una larga pausa, reanudamos la publicación de crates de ngx-rust con documentación mejorada y optimizaciones que facilitan el proceso de compilación para la comunidad.

¿Qué significa esto para NGINX?

Los módulos son los componentes básicos fundamentales de NGINX e implementan la mayor parte de sus funcionalidades. También constituyen la herramienta más potente para que los usuarios de NGINX puedan personalizar dichas funcionalidades y añadir compatibilidad con casos de uso específicos.

Tradicionalmente, NGINX solo ha admitido módulos escritos en C (al tratarse de un proyecto escrito en C, admitir enlaces de módulos en el lenguaje anfitrión era una opción clara y sencilla). Sin embargo, los avances en informática y en la teoría de los lenguajes de programación han permitido superar paradigmas anteriores, especialmente en lo que respecta a la seguridad de la memoria y la corrección. Esto ha allanado el camino para lenguajes como Rust, que ahora pueden utilizarse para desarrollar módulos de NGINX.

Cómo empezar a usar ngx-rust

Ahora que ya hemos repasado parte de la historia de NGINX y Rust, empecemos a crear un módulo. Puedes compilarlo a partir del código fuente y desarrollar el módulo localmente, descargar el código fuente de ngx-rust y ayudar a crear mejores enlaces, o simplemente descargar el paquete desde crates.io.

El README de ngx-rust incluye las directrices para contribuir y los requisitos de compilación local necesarios para empezar. El proyecto aún se encuentra en una fase temprana de desarrollo, pero nuestro objetivo es mejorar su calidad y sus funcionalidades con el apoyo de la comunidad. En este tutorial, nos centramos en la creación de un módulo independiente sencillo. También puedes consultar los ejemplos de ngx-rust para ver casos más complejos.

Los bindings se organizan en dos crates:

  • nginx-sys es un crate que genera enlaces a partir del código fuente de NGINX. El archivo descarga el código fuente de NGINX y sus dependencias, y utiliza la automatización de código de bindgen para crear los enlaces de la interfaz de funciones foráneas (FFI).
  • ngx es el crate principal que implementa el código de enlace de Rust y las API, y reexporta nginx-sys. Los desarrolladores de módulos importan estos símbolos e interactúan con NGINX a través de ellos, mientras que la reexportación de nginx-sys elimina la necesidad de importarlo explícitamente.

Las instrucciones que aparecen a continuación inicializarán un espacio de trabajo básico. Empieza por crear un directorio de trabajo e inicializar el proyecto de Rust:

cd $YOUR_DEV_ARENA 
mkdir ngx-rust-howto 
cd ngx-rust-howto 
cargo init --lib

A continuación, abre el archivo Cargo.toml y añade la siguiente sección:

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

[dependencies] 
ngx = "0.3.0-beta"

Como alternativa, si quieres consultar el módulo terminado mientras sigues la explicación, puedes clonarlo desde Git:

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

Y con esto, ya puedes empezar a desarrollar tu primer módulo de NGINX en Rust. La estructura, la semántica y el enfoque general para crear un módulo no serán muy distintos de los necesarios al utilizar C. Por ahora, nos hemos propuesto ofrecer enlaces de NGINX mediante un enfoque iterativo para generarlos, hacerlos utilizables y ponerlos en manos de los desarrolladores, de modo que puedan crear sus propias soluciones innovadoras. En el futuro, trabajaremos para ofrecer una experiencia con Rust mejor y más idiomática.

Esto significa que el primer paso es crear el módulo junto con las directivas, el contexto y los demás elementos necesarios para instalarlo y ejecutarlo en NGINX. El módulo será un controlador sencillo que podrá aceptar o denegar una solicitud en función del método HTTP y creará una nueva directiva que aceptará un único argumento. Lo explicaremos paso a paso, pero puede consultar el código completo en el repositorio ngx-rust-howto de GitHub.

Nota: Este blog se centra en explicar los aspectos específicos de Rust, en lugar de cómo crear módulos de NGINX en general. Si le interesa crear otros módulos de NGINX, consulte los numerosos y excelentes debates de la comunidad. Estos debates también le ofrecerán una explicación más básica sobre cómo ampliar NGINX (encontrará más información en la sección «Recursos» que aparece más abajo).

Registro del módulo

Puede crear su módulo de Rust implementando el trait «HTTPModule», que define todos los puntos de entrada de NGINX (postconfiguration, preconfiguration, create_main_conf, etc.). Quien desarrolle un módulo solo tiene que implementar las funciones necesarias para su cometido. Este módulo implementará el método postconfiguration para instalar su gestor de solicitudes.

Nota: Si no has clonado el repositorio ngx-rust-howto, puedes empezar a editar el archivo src/lib.rs creado mediante 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() 
    } 
} 

El módulo de Rust solo necesita un hook de postconfiguration en la fase de acceso NGX_HTTP_ACCESS_PHASE. Los módulos pueden registrar controladores para distintas fases de la solicitud HTTP. Para obtener más información al respecto, consulte los detalles en la guía de desarrollo.

Verás que el controlador de fase howto_access_handler se añade justo antes de que la función devuelva el resultado. Volveremos a esto más adelante. Por ahora, basta con señalar que es la función que ejecutará la lógica de gestión durante la cadena de solicitudes.

Según el tipo de módulo y sus necesidades, estos son los hooks de registro disponibles:

  • preconfiguración
  • postconfiguración
  • create_main_conf
  • init_main_conf
  • create_srv_conf
  • merge_srv_conf
  • create_loc_conf
  • merge_loc_conf

Estado de la configuración

Ahora es el momento de crear un almacenamiento para el módulo. Estos datos incluyen los parámetros de configuración necesarios o el estado interno utilizado para procesar solicitudes o modificar el comportamiento. En esencia, cualquier información que el módulo necesite conservar puede almacenarse en estructuras. Este módulo de Rust utiliza una estructura ModuleConfig en el nivel de configuración de ubicación. El almacenamiento de la configuración debe implementar los traits Merge y Default.

Al definir el módulo en el paso anterior, puedes establecer los tipos de las configuraciones principal, de servidor y de ubicación. El módulo de Rust que estás desarrollando aquí solo admite ubicaciones, por lo que únicamente se establece el tipo LocConf.

Para crear el almacenamiento del estado y la configuración de tu módulo, define una estructura e implementa el 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 almacena un estado de activación o desactivación en el campo enabled, junto con un método de solicitud HTTP. El controlador comprobará este método y permitirá o denegará las solicitudes según corresponda.

Una vez definido el almacenamiento, el módulo puede crear directivas y reglas de configuración que los propios usuarios pueden establecer. NGINX utiliza el tipo ngx_command_t y una matriz para registrar en el sistema central las directivas definidas por el módulo.

A través de los enlaces FFI, los desarrolladores de módulos en Rust tienen acceso al tipo ngx_command_t y pueden registrar directivas como lo harían en C. El módulo ngx-rust-howto define una directiva howto que acepta un valor de cadena. En este caso, definimos un comando, implementamos una función de asignación y, a continuación (en la siguiente sección), integramos esos comandos en el sistema central. Recuerde finalizar la matriz de comandos con la macro ngx_command_null! proporcionada.

A continuación se muestra cómo crear una directiva sencilla mediante comandos de 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() 
} 

Integración en el módulo

Ahora que dispone de una función de registro, un controlador de fase y comandos de configuración, puede conectarlo todo y exponer las funciones al sistema principal. Cree una estructura estática ngx_module_t con referencias a sus funciones de registro, controladores de fase y comandos de directivas. Cada módulo debe contener una variable global de tipo ngx_module_t.

A continuación, crea un contexto y un tipo de módulo estático, y expón ambos mediante la macro ngx_modules! macro. En el siguiente ejemplo, puedes ver cómo se establece commands en el campo de comandos y cómo el contexto que hace referencia a las funciones de registro de los módulos se establece en el campo ctx. Para este módulo, todos los demás campos conservan, en la práctica, sus valores predeterminados.

#[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, 
}; 

Después de esto, prácticamente habrás completado los pasos necesarios para configurar y registrar un nuevo módulo de Rust. No obstante, aún debes implementar el controlador de fase (howto_access_handler) que se estableció en el hook postconfiguration.

Manejadores

Los manejadores se invocan para cada solicitud entrante y realizan la mayor parte del trabajo del módulo. El equipo de ngx-rust se ha centrado en los manejadores de solicitudes, que es donde se han aplicado la mayoría de las mejoras ergonómicas iniciales. Aunque los pasos de configuración anteriores requieren escribir Rust con un estilo similar al de C, ngx-rust ofrece más facilidades y utilidades para los manejadores de solicitudes.

Como se muestra en el ejemplo siguiente, ngx-rust proporciona la macro http_request_handler! para aceptar un cierre de Rust al que se llama con una instancia de Request. También proporciona utilidades para obtener la configuración y las variables, establecer dichas variables y acceder a la memoria, a otras primitivas de NGINX y a las API.

Para iniciar el procedimiento de un controlador, invoque la macro y proporcione la lógica de negocio como un cierre de Rust. En el módulo ngx-rust-howto, compruebe el método de la solicitud para permitir que esta continúe procesándose.

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, } }); 

¡Y con esto, has completado tu primer módulo de Rust!

El repositorio ngx-rust-howto de GitHub contiene un archivo de configuración de NGINX en el directorio conf. También puedes compilarlo (con cargo build), añadir el binario del módulo a la directiva load_module de un archivo nginx.conf local y ejecutarlo mediante una instancia de NGINX. Para elaborar este tutorial, utilizamos NGINX v1.23.3, la versión NGINX_VERSION predeterminada compatible con ngx-rust. Al compilar y ejecutar módulos dinámicos, asegúrate de usar la misma versión NGINX_VERSION para las compilaciones de ngx-rust que para la instancia de NGINX que ejecutas en tu equipo.

Conclusión

NGINX es un sistema de software consolidado que incorpora años de funcionalidades y casos de uso. Es un proxy y equilibrador de carga eficaz, además de un servidor web de primer nivel. Su presencia en el mercado está garantizada durante muchos años, lo que refuerza nuestra motivación para ampliar sus capacidades y ofrecer a nuestros usuarios nuevas formas de interactuar con él. Dada la popularidad de Rust entre los desarrolladores y sus mayores garantías de seguridad, nos entusiasma ofrecer la posibilidad de utilizar Rust junto con el mejor servidor web del mundo.

Sin embargo, la madurez de NGINX y su ecosistema repleto de funcionalidades dan lugar a una amplia superficie de API, y ngx-rust apenas ha empezado a explorarla. El proyecto pretende mejorar y crecer mediante la incorporación de interfaces de Rust más idiomáticas, la creación de módulos de referencia adicionales y la mejora de la ergonomía a la hora de escribir módulos.

¡Aquí es donde entras tú! El proyecto ngx-rust está abierto a todo el mundo y está disponible en GitHub. Estamos deseando colaborar con la comunidad de NGINX para seguir mejorando las funcionalidades y la facilidad de uso del módulo. ¡Échale un vistazo y experimenta con los bindings! No dudes en ponerte en contacto con nosotros, abrir incidencias o PR y participar en el canal de Slack de la comunidad de NGINX.

Recursos


Compartir
Etiquetas: F5 NGINX, Tech

Artículos relacionados del blog

Únete a F5 NGINX en AppWorld 2024
NGINX | 04/01/2024

Únete a F5 NGINX en AppWorld 2024

Descubre las numerosas razones para asistir al evento insignia de F5, que incluirá un emocionante anuncio sobre la cartera de productos NGINX.

Rotación de certificados SSL/TLS sin reinicios en NGINX Open Source
NGINX | 26/09/2023

Rotación de certificados SSL/TLS sin reinicios en NGINX Open Source

La función de diccionario compartido permite configurar NGINX sin necesidad de reiniciarlo al renovar los certificados SSL/TLS.

Exposición de APIs en Kubernetes
NGINX | 10/03/2022

Exposición de APIs en Kubernetes

La unidad 2 de Microservices March 2022 se centra en cómo exponer tus API en Kubernetes. Aquí tienes enlaces a los recursos que necesitas: la retransmisión en directo del 14 de marzo, artículos de blog y otros recursos de preparación, y el laboratorio práctico «Protege las API de Kubernetes mediante la limitación de solicitudes».

NGINX Sprint 2.0: visión clara, código renovado y nuevos compromisos con el código abierto
NGINX | 23/08/2021

NGINX Sprint 2.0: visión clara, código renovado y nuevos compromisos con el código abierto

Hoy, en NGINX Sprint 2.0, anunciamos cómo respaldaremos nuestra visión del futuro de las aplicaciones modernas con una mayor presencia en la comunidad de código abierto, incluida la participación en el proyecto comunitario Kubernetes, nuevos proyectos de software de código abierto y la arquitectura de referencia para aplicaciones modernas.

Presentamos NGINX Plus R22
NGINX | 09/06/2020

Presentamos NGINX Plus R22

NGINX Plus R22 incorpora nuevas funcionalidades, como la compatibilidad con OCSP para verificar la revocación de certificados en TLS mutuo, el uso de varios proveedores de identidad OIDC en una configuración, el seguimiento en tiempo real de la limitación de solicitudes y conexiones en el panel de NGINX Plus y mejoras en NGINX JavaScript.

Presentamos NGINX Plus R17
NGINX | 11/12/2018

Presentamos NGINX Plus R17

NGINX Plus R17 incorpora compatibilidad con la limitación de velocidad en dos fases y con TLS 1.3, la versión más reciente del protocolo Transport Layer Security. La configuración de OpenID Connect es más sencilla y NGINX ModSecurity WAF es el doble de rápido que antes. También se ha actualizado el módulo JavaScript de NGINX.