Estendendo o NGINX com Rust (uma alternativa ao C)

NGINX | 12 de outubro de 2023

Ao longo de sua história relativamente curta, a linguagem de programação Rust conquistou reconhecimento excepcional e desenvolveu um ecossistema rico e maduro. Tanto Rust quanto Cargo (seu sistema de compilação, interface da cadeia de ferramentas e gerenciador de pacotes) são tecnologias admiradas e desejadas no setor, e Rust mantém uma posição estável entre as 20 principais linguagens do ranking de linguagens de programação da RedMonk. Além disso, projetos que adotam Rust frequentemente apresentam melhorias na estabilidade e uma redução nos erros de programação relacionados à segurança (por exemplo, os desenvolvedores do Android contam uma história convincente de melhorias significativas).

Há algum tempo, a F5 acompanha com entusiasmo esses avanços relacionados ao Rust e à sua comunidade de Rustaceans. Demonstramos nosso interesse por meio da defesa ativa da linguagem, de seu conjunto de ferramentas e de sua adoção futura.

Na NGINX, agora estamos entrando de cabeça para atender aos desejos e às necessidades dos desenvolvedores em um mundo cada vez mais digital e preocupado com a segurança. Temos o prazer de anunciar o projeto ngx-rust — uma nova maneira de escrever módulos do NGINX com a linguagem Rust. Rustaceans, esta é para vocês!

Uma breve história do NGINX e do Rust

Quem acompanha de perto o NGINX e nosso GitHub talvez saiba que esta não é nossa primeira implementação de módulos baseados em Rust. Nos primeiros anos do Kubernetes e no início da era da malha de serviços, surgiram algumas iniciativas em torno do Rust, estabelecendo as bases para o projeto ngx-rust.

Originalmente, o ngx-rust serviu como uma forma de acelerar o desenvolvimento de um produto de malha de serviços compatível com o Istio usando o NGINX. Após o desenvolvimento do protótipo inicial, esse projeto permaneceu inalterado por muitos anos. Durante esse período, muitos membros da comunidade criaram forks do repositório ou projetos inspirados nos exemplos originais de bindings para Rust fornecidos no ngx-rust.

Avançando no tempo, nossa equipe do F5 Distributed Cloud Bot Defense precisou integrar proxies NGINX aos seus serviços de proteção. Para isso, foi necessário desenvolver um novo módulo.

Também queríamos continuar expandindo nosso portfólio de Rust, ao mesmo tempo que aprimorávamos a experiência dos desenvolvedores e atendíamos às necessidades em constante evolução dos clientes. Por isso, aproveitamos nossos patrocínios internos à inovação e trabalhamos com o autor original do ngx-rust para desenvolver um projeto novo e aprimorado de bindings para Rust. Após um longo hiato, retomamos a publicação dos crates do ngx-rust, com documentação aprimorada e melhorias na ergonomia de compilação para uso pela comunidade.

O que isso significa para o NGINX?

Os módulos são os principais componentes do NGINX e implementam a maior parte de suas funcionalidades. Eles também são a maneira mais poderosa de os usuários do NGINX personalizarem essas funcionalidades e criarem suporte para casos de uso específicos.

Tradicionalmente, o NGINX oferecia suporte apenas a módulos escritos em C (como se trata de um projeto escrito em C, oferecer suporte a bindings de módulos na linguagem hospedeira foi uma escolha óbvia e simples). No entanto, os avanços na ciência da computação e na teoria das linguagens de programação aprimoraram paradigmas anteriores, especialmente no que diz respeito à segurança de memória e à correção. Isso abriu caminho para linguagens como Rust, que agora podem ser disponibilizadas para o desenvolvimento de módulos do NGINX.

Como começar a usar o ngx-rust

Agora que já abordamos um pouco da história do NGINX e do Rust, vamos começar a criar um módulo. Você pode compilar a partir do código-fonte e desenvolver seu módulo localmente, baixar o código-fonte do ngx-rust e ajudar a criar bindings melhores ou simplesmente baixar o crate do crates.io.

O README do ngx-rust apresenta as diretrizes de contribuição e os requisitos para compilação local necessários para começar. O projeto ainda está em seus estágios iniciais de desenvolvimento, mas nosso objetivo é melhorar sua qualidade e seus recursos com o apoio da comunidade. Neste tutorial, vamos nos concentrar na criação de um módulo independente simples. Você também pode consultar os exemplos do ngx-rust para ver lições mais avançadas.

Os bindings são organizados em dois crates:

  • nginx-sys é um crate que gera bindings a partir do código-fonte do NGINX. O arquivo baixa o código-fonte do NGINX e as dependências, e usa a automação de código do bindgen para criar os bindings da interface de função estrangeira (FFI).
  • ngx é o crate principal que implementa o código de integração em Rust e as APIs, além de reexportar o nginx-sys. Os autores de módulos importam e interagem com o NGINX por meio desses símbolos, enquanto a reexportação do nginx-sys elimina a necessidade de importá-lo explicitamente.

As instruções abaixo inicializarão um workspace básico. Comece criando um diretório de trabalho e inicializando o projeto Rust:

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

Em seguida, abra o arquivo Cargo.toml e adicione a seguinte seção:

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

[dependencies] 
ngx = "0.3.0-beta"

Como alternativa, se quiser consultar o módulo concluído enquanto acompanha a leitura, você pode cloná-lo do Git:

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

E, com isso, você já está pronto para começar a desenvolver seu primeiro módulo NGINX em Rust. A estrutura, a semântica e a abordagem geral para criar um módulo não serão muito diferentes do que é necessário ao usar C. Por enquanto, decidimos oferecer bindings do NGINX de forma iterativa, para que sejam gerados, possam ser usados e cheguem às mãos dos desenvolvedores, permitindo que criem suas soluções inovadoras. No futuro, trabalharemos para proporcionar uma experiência melhor e mais idiomática em Rust.

Isso significa que a primeira etapa é criar seu módulo juntamente com todas as diretivas, o contexto e os demais elementos necessários para instalá-lo e executá-lo no NGINX. Seu módulo será um manipulador simples capaz de aceitar ou negar uma solicitação com base no método HTTP e criará uma nova diretiva que aceita um único argumento. Abordaremos isso passo a passo, mas você pode consultar o código completo no repositório ngx-rust-howto no GitHub.

Observação: Este blog se concentra em apresentar as particularidades do Rust, e não em explicar como criar módulos do NGINX em geral. Se você tiver interesse em criar outros módulos do NGINX, consulte as excelentes discussões disponíveis na comunidade. Elas também oferecem uma explicação mais aprofundada sobre como estender o NGINX (saiba mais na seção Recursos abaixo).

Registro do módulo

Você pode criar seu módulo Rust implementando a trait HTTPModule, que define todos os pontos de entrada do NGINX (postconfiguration, preconfiguration, create_main_conf, etc.). O autor de um módulo precisa implementar apenas as funções necessárias para sua finalidade. Este módulo implementará o método postconfiguration para instalar seu manipulador de requisições.

Observação: Se você ainda não clonou o repositório ngx-rust-howto, pode começar a editar o arquivo src/lib.rs criado pelo comando 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() 
    } 
} 

O módulo Rust precisa apenas de um hook de postconfiguration na fase de acesso NGX_HTTP_ACCESS_PHASE. Os módulos podem registrar manipuladores para várias fases da solicitação HTTP. Para obter mais informações, consulte os detalhes no guia de desenvolvimento.

Você verá o manipulador de fase howto_access_handler adicionado logo antes do retorno da função. Voltaremos a isso mais adiante. Por enquanto, observe apenas que essa é a função que executará a lógica de processamento durante a cadeia de requisições.

Dependendo do tipo e das necessidades do seu módulo, estes são os hooks de registro disponíveis:

  • pré-configuração
  • pós-configuração
  • create_main_conf
  • init_main_conf
  • create_srv_conf
  • merge_srv_conf
  • create_loc_conf
  • merge_loc_conf

Estado da configuração

Agora é hora de criar o armazenamento para o seu módulo. Esses dados incluem quaisquer parâmetros de configuração necessários ou o estado interno usado para processar solicitações ou alterar o comportamento. Basicamente, todas as informações que o módulo precisa manter podem ser colocadas em estruturas e salvas. Este módulo Rust usa uma estrutura ModuleConfig no nível de configuração de localização. O armazenamento da configuração deve implementar as traits Merge e Default.

Ao definir seu módulo na etapa acima, você pode especificar os tipos das configurações principal, de servidor e de localização. O módulo Rust que você está desenvolvendo aqui oferece suporte apenas a localizações, portanto, somente o tipo LocConf é definido.

Para criar o armazenamento de estado e configuração do seu módulo, defina uma estrutura e implemente o 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 armazena um estado de ativação/desativação no campo enabled, juntamente com um método de requisição HTTP. O manipulador verificará esse método para permitir ou bloquear as requisições.

Depois que o armazenamento for definido, seu módulo poderá criar diretivas e regras de configuração para os próprios usuários definirem. O NGINX usa o tipo ngx_command_t e um array para registrar no sistema principal as diretivas definidas pelo módulo.

Por meio dos bindings FFI, os desenvolvedores de módulos Rust têm acesso ao tipo ngx_command_t e podem registrar diretivas da mesma forma que fariam em C. O módulo ngx-rust-howto define uma diretiva howto que aceita um valor de string. Nesse caso, definimos um comando, implementamos uma função setter e, em seguida (na próxima seção), integramos esses comandos ao sistema principal. Lembre-se de encerrar seu array de comandos com a macro fornecida ngx_command_null!.

Veja como criar uma diretiva simples usando comandos do 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() 
} 

Integrando ao módulo

Agora que você tem uma função de registro, um manipulador de fase e comandos de configuração, pode conectar tudo e expor as funções ao sistema principal. Crie uma estrutura estática ngx_module_t com referências às suas funções de registro, aos manipuladores de fase e aos comandos de diretiva. Todo módulo deve conter uma variável global do tipo ngx_module_t.

Em seguida, crie um contexto e um tipo de módulo estático e exponha-os com a macro ngx_modules! macro. No exemplo abaixo, você pode ver como os commands são definidos no campo de comandos e como o contexto que referencia as funções de registro dos módulos é definido no campo ctx. Para este módulo, todos os outros campos usam, na prática, os valores padrão.

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

Depois disso, você terá praticamente concluído as etapas necessárias para configurar e registrar um novo módulo Rust. No entanto, ainda será preciso implementar o manipulador de fase (howto_access_handler) definido no hook postconfiguration.

Manipuladores

Os manipuladores são chamados para cada solicitação recebida e realizam a maior parte do trabalho do seu módulo. Os manipuladores de solicitações têm sido o foco da equipe do ngx-rust e concentram a maioria das melhorias ergonômicas iniciais. Embora as etapas anteriores de configuração exijam escrever Rust em um estilo semelhante ao C, o ngx-rust oferece mais conveniências e utilitários para os manipuladores de solicitações.

Como mostrado no exemplo abaixo, o ngx-rust fornece a macro http_request_handler! para aceitar uma closure do Rust chamada com uma instância de Request. Ele também fornece utilitários para obter configurações e variáveis, definir essas variáveis e acessar a memória, outros recursos primitivos do NGINX e APIs.

Para iniciar um procedimento de tratamento, invoque a macro e forneça sua lógica de negócios como um closure do Rust. No módulo ngx-rust-howto, verifique o método da solicitação para permitir que ela continue sendo processada.

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

Com isso, você concluiu seu primeiro módulo em Rust!

O repositório ngx-rust-howto no GitHub contém um arquivo de configuração do NGINX no diretório conf. Você também pode compilar (com cargo build), adicionar o binário do módulo à diretiva load_module em um arquivo nginx.conf local e executá-lo usando uma instância do NGINX. Para escrever este tutorial, usamos o NGINX v1.23.3, a NGINX_VERSION padrão compatível com o ngx-rust. Ao compilar e executar módulos dinâmicos, use a mesma NGINX_VERSION nas compilações do ngx-rust e na instância do NGINX em execução na sua máquina.

Conclusão

O NGINX é um sistema de software maduro, que incorpora anos de desenvolvimento de recursos e casos de uso. Ele é um proxy eficiente, um balanceador de carga e um servidor web de nível internacional. Sua presença no mercado está garantida por muitos anos, o que reforça nossa motivação para ampliar seus recursos e oferecer aos usuários novas formas de interagir com ele. Com a popularidade do Rust entre os desenvolvedores e suas restrições de segurança aprimoradas, estamos entusiasmados em oferecer a opção de usar o Rust em conjunto com o melhor servidor web do mundo.

No entanto, a maturidade do NGINX e seu ecossistema repleto de recursos resultam em uma ampla superfície de API, e o ngx-rust ainda está apenas começando a explorá-la. O projeto busca evoluir e se expandir por meio da adição de interfaces Rust mais idiomáticas, da criação de módulos de referência adicionais e do aprimoramento da ergonomia no desenvolvimento de módulos.

É aqui que você entra! O projeto ngx-rust é aberto a todos e está disponível no GitHub. Estamos ansiosos para trabalhar com a comunidade NGINX e continuar aprimorando os recursos e a facilidade de uso do módulo. Confira o projeto e experimente você mesmo os bindings! Entre em contato, abra issues ou PRs e interaja conosco no canal da comunidade NGINX no Slack.

Recursos


Compartilhar
Tags: F5 NGINX, Tech

Publicações relacionadas no blog

Como o F5 NGINX One aprimora o gerenciamento e a observabilidade
NGINX | 10/10/2024

Como o F5 NGINX One aprimora o gerenciamento e a observabilidade

Descubra como o F5 NGINX One aprimora o gerenciamento e a observabilidade de ambientes híbridos e multicloud ao oferecer um console web centralizado baseado em SaaS, com insights em tempo real, conformidade automatizada e gerenciamento simplificado de configurações.

Do OpenAPI ao NGINX como gateway de API usando uma API declarativa
NGINX | 17/06/2024

Do OpenAPI ao NGINX como gateway de API usando uma API declarativa

Descubra como transformar uma definição de esquema OpenAPI em uma configuração NGINX totalmente funcional, operando como um API Gateway com a segurança de um Web Application Firewall e um Portal do Desenvolvedor, usando uma abordagem declarativa de API.

Como gerenciar picos repentinos de tráfego e sobrecarga do servidor
NGINX | 14/07/2021

Como gerenciar picos repentinos de tráfego e sobrecarga do servidor

Picos de tráfego e sobrecarga de servidores são preocupações sérias para varejistas online em qualquer época, mas especialmente durante a “Cyber Season” de compras de fim de ano. Detalhamos como você pode aproveitar as soluções de entrega de aplicações de nível de produção da NGINX para se preparar para aumentos repentinos de tráfego.

Distribuição segura de chaves privadas SSL com o NGINX
NGINX | 02/04/2019

Distribuição segura de chaves privadas SSL com o NGINX

Descrevemos três maneiras progressivamente mais seguras de proteger chaves privadas SSL ao configurar o NGINX para processar tráfego HTTPS: permitir acesso de leitura apenas ao usuário root, criptografar as chaves com senhas armazenadas separadamente e distribuir as senhas a partir de um repositório central.

Agrupando solicitações de API em lotes com NGINX Plus e o módulo JavaScript do NGINX
NGINX | 24/05/2018

Agrupando solicitações de API em lotes com NGINX Plus e o módulo JavaScript do NGINX

O módulo JavaScript do NGINX lançado com o NGINX Plus R15 oferece suporte a subsolicitações, possibilitando uma classe totalmente nova de casos de uso para o NGINX Plus. Este exemplo completo também usa a API do NGINX Plus e os módulos de chave-valor para converter solicitações de API dos clientes em solicitações separadas para as APIs dos microsserviços que compõem um aplicativo.

Como superar o esgotamento de portas efêmeras no NGINX e no NGINX Plus
NGINX | 19/04/2016

Como superar o esgotamento de portas efêmeras no NGINX e no NGINX Plus

Confira dicas para evitar o esgotamento de portas efêmeras com ajustes no kernel do Linux, conexões keepalive e conexões vinculadas dinamicamente a endereços IP locais.