BLOG | NGINX

Extendiendo NGINX con Rust (una alternativa a C)

NGINX - Parte de F5 - horizontal, negro, tipo RGB
Miniatura de Matthew Yacobucci
Mateo Yacobucci
Publicado el 12 de octubre de 2023

A lo largo de su relativamente corta historia, el lenguaje de programación Rust ha cosechado elogios excepcionales junto con un ecosistema rico y maduro. Tanto Rust como Cargo (su sistema de compilación, interfaz de cadena de herramientas y administrador de paquetes) son tecnologías admiradas y deseadas en el panorama, y Rust mantiene una posición estable entre los 20 lenguajes principales del ranking de lenguajes de programación de RedMonk . Además, los proyectos que adoptan Rust a menudo muestran mejoras en la estabilidad y en los errores de programación relacionados con la seguridad (por ejemplo, los desarrolladores de Android cuentan una historia convincente de mejora puntual).

F5 ha estado observando estos desarrollos en torno a Rust y su comunidad de Rustaceans con entusiasmo durante algún tiempo. Hemos tomado nota de ello y promovemos activamente el lenguaje , su cadena de herramientas y su adopción en el futuro.

En NGINX, ahora estamos poniendo algo de piel en el juego para satisfacer los deseos y necesidades de los desarrolladores en un mundo cada vez más digital y consciente de la seguridad. Nos complace anunciar el proyecto ngx-rust : una nueva forma de escribir módulos NGINX con el lenguaje Rust. ¡Rústáceos, esto es para vosotros!

Una breve historia de NGINX y Rust

Los seguidores cercanos de NGINX y nuestro GitHub podrían darse cuenta de que esta no es nuestra primera encarnación de módulos basados en Rust. En los primeros años de Kubernetes y los primeros días de Service Mesh , se manifestó algo de trabajo en torno a Rust, creando las bases para el proyecto ngx-rust.

Originalmente, ngx-rust funcionó como una forma de acelerar el desarrollo de un producto de malla de servicios compatible con Istio con NGINX. Tras el desarrollo del prototipo inicial, este proyecto se mantuvo sin cambios durante muchos años. Durante ese tiempo, muchos miembros de la comunidad bifurcaron el repositorio o crearon proyectos inspirados en los ejemplos de enlaces originales de Rust proporcionados en ngx-rust.

Avanzamos rápidamente y nuestro equipo de F5 Distributed Cloud Bot Defense necesitaba integrar servidores proxy NGINX en sus servicios de protección. Esto requirió construir un nuevo módulo.

También queríamos seguir ampliando nuestro portafolio de Rust al tiempo que mejorábamos la experiencia de los desarrolladores y satisfacíamos las necesidades cambiantes de los clientes. Entonces, aprovechamos nuestros patrocinios de innovación interna y trabajamos con el autor original de ngx-rust para desarrollar un proyecto de enlaces de Rust nuevo y mejorado. Después de una larga pausa, reiniciamos la publicación de cajas ngx-rust con documentación mejorada y mejoras en la ergonomía de la compilación para el uso de la comunidad.

¿Qué significa esto para NGINX?

Los módulos son los componentes básicos de NGINX e implementan la mayor parte de su funcionalidad. Los módulos también son la forma más poderosa en que los usuarios de NGINX pueden personalizar esa funcionalidad y crear soporte para casos de uso específicos.

Tradicionalmente, NGINX solo ha admitido módulos escritos en C (como proyecto escrito en C, admitir enlaces de módulos en el lenguaje host fue una elección clara y sencilla). Sin embargo, los avances en la ciencia informática y en la teoría del lenguaje de programación han mejorado los paradigmas anteriores, especialmente con respecto a la seguridad y corrección de la memoria. Esto ha allanado el camino para lenguajes como Rust, que ahora pueden estar disponibles para el desarrollo de módulos NGINX.

Cómo empezar a usar ngx-rust

Ahora que hemos cubierto parte de la historia de NGINX y Rust, comencemos a construir un módulo. Eres libre de compilar desde la fuente y desarrollar tu módulo localmente, extraer la fuente ngx-rust y ayudar a compilar mejores enlaces, o simplemente extraer el paquete desde crates.io .

El README de ngx-rust cubre las pautas de contribución y los requisitos de compilación local para comenzar. Todavía es temprano y está en su desarrollo inicial, pero nuestro objetivo es mejorar la calidad y las funciones con el apoyo de la comunidad. En este tutorial nos centramos en la creación de un módulo independiente simple. También puedes consultar los ejemplos de ngx-rust para lecciones más complejas.

Las encuadernaciones están organizadas en dos cajas:

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

Las instrucciones a continuación inicializarán un espacio de trabajo esqueleto. Comience creando un directorio de trabajo e inicialice el proyecto Rust:

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

A continuación, abra el archivo Cargo.toml y agregue la siguiente sección:

[lib] 
tipo-de-caja = ["cdylib"] 

[dependencias] 
ngx = "0.3.0-beta"

Alternativamente, si desea ver el módulo completo mientras lee, puede clonarlo desde Git:

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

Y con eso, estás listo para comenzar a desarrollar tu primer módulo NGINX Rust. La estructura, la semántica y el enfoque general para construir un módulo no se verán muy diferentes de lo que es necesario cuando se usa C. Por ahora, nos hemos propuesto ofrecer enlaces NGINX en un enfoque iterativo para generar los enlaces, que sean utilizables y estén en manos de los desarrolladores para crear sus ofertas innovadoras. En el futuro, trabajaremos para construir una experiencia de Rust mejor y más idiomática.

Esto significa que el primer paso es construir el módulo junto con las directivas, el contexto y otros aspectos necesarios para su instalación y ejecución en NGINX. El módulo será un controlador simple que puede aceptar o rechazar una solicitud según el método HTTP y creará una nueva directiva que acepta un solo argumento. Discutiremos esto paso a paso, pero puedes consultar el código completo en el repositorio ngx-rust-howto en GitHub.

Nota:  Este blog se centra en describir los detalles de Rust, en lugar de cómo crear módulos NGINX en general. Si está interesado en crear otros módulos NGINX, consulte las excelentes discusiones que hay en la comunidad. Estas discusiones también le brindarán una explicación más fundamental de cómo extender NGINX (ver más en la sección Recursos a continuación).

Registro del módulo

Puede crear su módulo Rust implementando el rasgo HTTPModule , que define todos los puntos de entrada de NGINX ( postconfiguration , preconfiguration , create_main_conf , etc.). Un escritor de módulos sólo necesita implementar las funciones necesarias para su tarea. Este módulo implementará el método de postconfiguración para instalar su controlador de solicitudes.

Nota:  Si no ha clonado el repositorio ngx-rust-howto , puede comenzar a editar el archivo src/lib.rs creado por cargo init .

Estructura Módulo;

Impl http::HTTPModule para Módulo { 
tipo MainConf = (); 
tipo SrvConf = (); 
tipo LocConf = ModuleConfig; 

unsafe externa "C" función postconfiguración(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 como usize].handlers, ) como *mut ngx_http_handler_pt; 
if h.is_null() { 
devuelve core::Status::NGX_ERROR.into(); } 

// Establecer un controlador de fase de acceso
*h = Some(howto_access_handler); 
core::Status::NGX_OK.into() 
} 
} 

El módulo Rust solo necesita un gancho de postconfiguración en la fase de acceso NGX_HTTP_ACCESS_PHASE . Los módulos pueden registrar controladores para varias fases de la solicitud HTTP. Para obtener más información sobre esto, consulte los detalles en la guía de desarrollo .

Verá el controlador de fase howto_access_handler agregado justo antes de que la función regrese. Volveremos a esto más adelante. Por ahora, solo tenga en cuenta que es la función que realizará la lógica de manejo durante la cadena de solicitud.

Dependiendo del tipo de módulo y sus necesidades, estos son los ganchos de registro disponibles:

  • preconfiguración
  • postconfiguración
  • crear_configuración_principal
  • configuración principal de inicio
  • crear_srv_conf
  • configuración del servidor de fusión
  • crear_loc_conf
  • configuración de ubicación de fusión

Estado de configuración

Ahora es el momento de crear almacenamiento para su módulo. Estos datos incluyen cualquier parámetro de configuración necesario o el estado interno utilizado para procesar solicitudes o alterar el comportamiento. Básicamente, cualquier información que el módulo necesite conservar se puede colocar en estructuras y guardar. Este módulo Rust utiliza una estructura ModuleConfig en el nivel de configuración de ubicación. El almacenamiento de configuración debe implementar las características Merge y Default .

Al definir su módulo en el paso anterior, puede establecer los tipos para sus configuraciones principal, de servidor y de ubicación. El módulo Rust que estás desarrollando aquí solo admite ubicaciones, por lo que solo se configura el tipo LocConf .

Para crear almacenamiento de estado y configuración para su módulo, defina una estructura e implemente el rasgo Merge :

#[derivar(Depurar, Predeterminado)] 
struct ModuleConfig { 
habilitado: bool, 
método: Cadena, 
} 

impl http::Fusionar para 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 activado/desactivado en el campo habilitado, junto con un método de solicitud HTTP. El controlador comprobará este método y permitirá o prohibirá las solicitudes.

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

A través de los enlaces FFI, los escritores de módulos 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. Para este caso, definimos un comando, implementamos una función setter y luego (en la siguiente sección) conectamos esos comandos al sistema central. Recuerde finalizar su matriz de comandos con la macro ngx_command_null! proporcionada.

A continuación se explica cómo crear una directiva simple utilizando comandos NGINX:

#[no_mangle] 
mut estático ngx_http_howto_commands: [ngx_command_t; 2] = [ 
ngx_command_t { 
nombre: ngx_string!("howto"), 
tipo_: (NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1) como ngx_uint_t, 
establecido: Algunos(ngx_http_howto_commands_set_method), 
conf: NGX_RS_HTTP_LOC_CONF_OFFSET, desplazamiento: 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() } 

Conectando el módulo

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

Luego, cree un contexto y un tipo de módulo estático, y expóngalos con la macro ngx_modules!. En el siguiente ejemplo, puede ver cómo se configuran los comandos en el campo de comandos y cómo se configura el contexto que hace referencia a las funciones de registro de los módulos en el campo ctx . Para este módulo, todos los demás campos son valores predeterminados.

#[no_mangle] 
módulo_de_cómo_ngx_http_estático_ctx: módulo_ngx_http_t = módulo_ngx_http_t { 
preconfiguración: Algún(Módulo::preconfiguración), posconfiguración: Algunos(Módulo::postconfiguración),
crear_configuración_principal: Algún(Módulo::create_main_conf), 
init_main_conf: Algún(Módulo::init_main_conf), 
crear_srv_conf: Algunos(Módulo::create_srv_conf), 
merge_srv_conf: Algún(Módulo::merge_srv_conf), 
crear_loc_conf: Algún(Módulo::create_loc_conf), 
merge_loc_conf: Algún(Módulo::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::valor_máximo(), 
index: ngx_uint_t::valor_máximo(), 
name: std::ptr::null_mut(), 
spare0: 0, 
repuesto1: 0, 
versión: nginx_version como ngx_uint_t, 
firma: NGX_RS_MODULE_SIGNATURE.as_ptr() como *const c_char, 

ctx: &ngx_http_howto_module_ctx como *const _ como *mut _, 
comandos: inseguro { &ngx_http_howto_commands[0] como *const _ como *mut _ }, 
tipo_: MÓDULO HTTP NGX como ngx_uint_t,

init_master: Ninguno, 
módulo_init: Ninguno, proceso_de_inicio: Ninguno, 
init_thread: Ninguno, 
salir_hilo: Ninguno, proceso_de_salida: Ninguno, 
exit_master: Ninguno, 

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 Rust. Dicho esto, todavía es necesario implementar el controlador de fase (howto_access_handler) que se configuró en el gancho posterior a la configuración .

Manipuladores

Los controladores se llaman para cada solicitud entrante y realizan la mayor parte del trabajo de su módulo. Los controladores de solicitudes han sido el foco del equipo ngx-rust y son donde se han realizado la mayoría de las mejoras ergonómicas iniciales. Si bien los pasos de configuración anteriores requieren escribir Rust en un estilo similar a C, ngx-rust proporciona más conveniencia y utilidades para los controladores de solicitudes.

Como se ve en el ejemplo a continuación, ngx-rust proporciona la macro http_request_handler! para aceptar un cierre de Rust llamado con una instancia de Solicitud . También proporciona utilidades para obtener configuración y variables, establecer esas variables y acceder a la memoria, otras primitivas NGINX y API.

Para iniciar un procedimiento de controlador, invoque la macro y proporcione su lógica comercial como un cierre de Rust. Para el módulo ngx-rust-howto, verifique el método de la solicitud para permitir que la solicitud 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("La configuración del módulo es ninguna"); 

ngx_log_debug_http!(request, "El módulo howto habilitado fue llamado"); 
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, 
} 
}); 

¡Con esto habrás completado tu primer módulo de Rust!

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

CONCLUSIÓN

NGINX es un sistema de software maduro con años de características y casos de uso incorporados. Es un proxy capaz, un equilibrador de carga y un servidor web de clase mundial. Su presencia en el mercado es segura durante los próximos años, lo que alimenta nuestra motivación para desarrollar sus capacidades y brindar a nuestros usuarios nuevos métodos para interactuar con él. Con la popularidad de Rust entre los desarrolladores y sus restricciones de seguridad mejoradas, estamos entusiasmados de ofrecer la opción de usar Rust junto con el mejor servidor web del mundo.

Sin embargo, la madurez de NGINX y su ecosistema rico en funciones crean una gran superficie de API y ngx-rust apenas ha arañado la superficie. El proyecto tiene como objetivo mejorar y expandirse agregando interfaces Rust más idiomáticas, creando módulos de referencia adicionales y mejorando la ergonomía de los módulos de escritura.

¡Aquí es donde entras tú! El proyecto ngx-rust está abierto a todos y está disponible en GitHub . Estamos ansiosos por trabajar con la comunidad NGINX para seguir mejorando las capacidades y la facilidad de uso del módulo. ¡Pruébalo y experimenta tú mismo con las fijaciones! Y, por favor, comuníquese con nosotros, presente problemas o relaciones públicas e interactúe con nosotros en el canal Slack de la comunidad NGINX .

Recursos


"Esta publicación de blog puede hacer referencia a productos que ya no están disponibles o que ya no reciben soporte. Para obtener la información más actualizada sobre los productos y soluciones F5 NGINX disponibles, explore nuestra familia de productos NGINX . NGINX ahora es parte de F5. Todos los enlaces anteriores de NGINX.com redirigirán a contenido similar de NGINX en F5.com.