BLOG | NGINX

Estendendo NGINX com Rust (uma alternativa ao C)

NGINX-Parte-de-F5-horiz-preto-tipo-RGB
Miniatura de Matthew Yacobucci
Mateus Yacobucci
Publicado em 12 de outubro de 2023

Ao longo de sua história relativamente curta, a linguagem de programação Rust recebeu elogios excepcionais, além de um ecossistema rico e maduro. Tanto o Rust quanto o Cargo (seu sistema de construção, interface de cadeia de ferramentas e gerenciador de pacotes) são tecnologias admiradas e desejadas no cenário, com o Rust mantendo 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 mostram melhorias na estabilidade e em erros de programação relacionados à segurança (por exemplo, desenvolvedores Android contam uma história convincente de melhorias pontuais).

A F5 vem observando esses acontecimentos em torno de Rust e sua comunidade de Rustáceos com entusiasmo há algum tempo. Tomamos conhecimento disso com uma defesa ativa da linguagem , seu conjunto de ferramentas e adoção futura.

Na NGINX, estamos agora nos esforçando para satisfazer os desejos e necessidades dos desenvolvedores em um mundo cada vez mais digital e preocupado com a segurança. Estamos felizes em anunciar o projeto ngx-rust – uma nova maneira de escrever módulos NGINX com a linguagem Rust. Rustáceos, esta é para vocês!

Uma breve história do NGINX e do Rust

Seguidores próximos do NGINX e do nosso GitHub podem perceber que esta não é nossa primeira encarnação de módulos baseados em Rust. Nos anos iniciais do Kubernetes e nos primeiros dias do Service Mesh , algum trabalho se manifestou em torno do Rust, criando a base para o projeto ngx-rust.

Originalmente, ngx-rust agia como uma forma de acelerar o desenvolvimento de um produto de service mesh compatível com Istio com NGINX. Após o desenvolvimento do protótipo inicial, este projeto foi deixado inalterado por muitos anos. Durante esse tempo, muitos membros da comunidade bifurcaram o repositório ou criaram projetos inspirados nos exemplos originais de vinculações Rust fornecidos no ngx-rust.

Avançando rapidamente, nossa equipe de defesa de bots em nuvem distribuída da F5 precisava integrar proxies NGINX em seus serviços de proteção. Isso exigiu a construção de um novo módulo.

Também queríamos continuar expandindo nosso portfólio Rust enquanto melhorávamos a experiência do desenvolvedor e atendíamos às necessidades em evolução dos clientes. Então, aproveitamos nossos patrocínios internos de inovação e trabalhamos com o autor original do ngx-rust para desenvolver um novo e aprimorado projeto de vinculações Rust. Após um longo hiato, reiniciamos a publicação de ngx-rust crates com documentação aprimorada e melhorias na ergonomia de construção para uso da comunidade.

O que isso significa para o NGINX?

Os módulos são os principais blocos de construção do NGINX, implementando a maior parte de sua funcionalidade. Os módulos também são a maneira mais poderosa para os usuários do NGINX personalizarem essa funcionalidade e criarem suporte para casos de uso específicos.

Tradicionalmente, o NGINX só oferece suporte a módulos escritos em C (como um projeto escrito em C, oferecer suporte a vinculações de módulos na linguagem host foi uma escolha clara e fácil). No entanto, os avanços na ciência da computação e na teoria da linguagem de programação melhoraram os paradigmas anteriores, especialmente no que diz respeito à segurança e correção da memória. Isso abriu caminho para linguagens como Rust, que agora podem ser disponibilizadas para desenvolvimento de módulos NGINX.

Como começar com ngx-rust

Agora que abordamos um pouco da história do NGINX e do Rust, vamos começar a construir um módulo. Você está livre para construir a partir do código-fonte e desenvolver seu módulo localmente, obter o código-fonte do ngx-rust e ajudar a criar melhores ligações ou simplesmente obter o crate do crates.io .

O README do ngx-rust aborda diretrizes de contribuição e requisitos de compilação local para começar. Ainda é cedo e está em desenvolvimento inicial, mas pretendemos melhorar a qualidade e os recursos com o apoio da comunidade. Neste tutorial, focamos na criação de um módulo independente simples. Você também pode consultar os exemplos do ngx-rust para lições mais complexas.

As encadernações são organizadas em duas caixas:

  • nginx-sys é um crate que gera ligações a partir do código-fonte do NGINX. O arquivo baixa o código-fonte do NGINX, as dependências e usa a automação de código bindgen para criar as ligações da interface de função estrangeira (FFI).
  • ngx é o principal crate que implementa o código Rust glue, APIs e reexporta nginx-sys . Os escritores 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 espaço de trabalho esqueleto. Comece criando um diretório de trabalho e inicialize o projeto Rust:

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

[dependências] 
ngx = "0.3.0-beta"

Alternativamente, se você quiser ver o módulo concluído enquanto lê, ele pode ser clonado do Git:

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

E com isso, você está pronto para começar a desenvolver seu primeiro módulo NGINX Rust. A estrutura, a semântica e a abordagem geral para construir um módulo não parecerão muito diferentes do que é necessário ao usar C. Por enquanto, nos propusemos a oferecer ligações NGINX em uma abordagem iterativa para obter as ligações geradas, utilizáveis e nas mãos dos desenvolvedores para criar suas ofertas inventivas. No futuro, trabalharemos para construir uma experiência Rust melhor e mais idiomática.

Isso significa que seu primeiro passo é construir seu módulo em conjunto com quaisquer diretivas, contexto e outros aspectos necessários para instalar e executar no NGINX. Seu módulo será um manipulador simples que pode aceitar ou negar uma solicitação com base no método HTTP e criará uma nova diretiva que aceita um único argumento. Discutiremos isso em etapas, mas você pode consultar o código completo no repositório ngx-rust-howto no GitHub.

Observação:  Este blog se concentra em descrever os detalhes específicos do Rust, em vez de como construir módulos NGINX em geral. Se você estiver interessado em criar outros módulos NGINX, consulte as muitas discussões excelentes na comunidade. Essas discussões também fornecerão uma explicação mais fundamental de como estender o NGINX (veja mais na seção Recursos abaixo).

Registro do módulo

Você pode criar seu módulo Rust implementando a característica HTTPModule , que define todos os pontos de entrada do NGINX ( postconfiguration , preconfiguration , create_main_conf , etc.). Um escritor de módulo só precisa implementar as funções necessárias para sua tarefa. Este módulo implementará o método postconfiguration para instalar seu manipulador de solicitações.

Observação:  Se você não clonou o repositório ngx-rust-howto , você pode começar a editar o arquivo src/lib.rs criado pelo cargo init .

struct Módulo; 

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

inseguro externo "C" fn postconfiguration(cf: *mut ngx_conf_t) -> ngx_int_t { 
deixe htcf = http::ngx_http_conf_get_module_main_conf(cf, &ngx_http_core_module); 

deixe h = ngx_array_push( 
&mut (*htcf).phases[ngx_http_phases_NGX_HTTP_ACCESS_PHASE como usize].handlers, 
) como *mut ngx_http_handler_pt; 
se h.is_null() { 
retorne core::Status::NGX_ERROR.into(); } 

// define um manipulador de fase de acesso 
*h = Some(howto_access_handler); 
core::Status::NGX_OK.into() 
} 
} 

O módulo Rust precisa apenas de um gancho de pós-configuração na fase de acesso NGX_HTTP_ACCESS_PHASE . Os módulos podem registrar manipuladores para várias fases da solicitação HTTP. Para mais informações sobre isso, veja os detalhes no guia de desenvolvimento .

Você verá o manipulador de fase howto_access_handler adicionado logo antes da função retornar. Voltaremos a isso mais tarde. Por enquanto, observe apenas que é a função que executará a lógica de manipulação durante a cadeia de solicitações.

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

  • pré-configuração
  • pós-configuração
  • criar_configuração_principal
  • init_main_conf
  • criar_srv_conf
  • mesclar_srv_conf
  • criar_loc_conf
  • mesclar_loc_conf

Estado de configuração

Agora é hora de criar armazenamento para 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 comportamento. Essencialmente, qualquer informação que o módulo precise persistir pode ser colocada em estruturas e salva. Este módulo Rust usa uma estrutura ModuleConfig no nível de configuração de localização. O armazenamento de configuração deve implementar as características Merge e Default .

Ao definir seu módulo na etapa acima, você pode definir os tipos para suas configurações principal, de servidor e de local. O módulo Rust que você está desenvolvendo aqui suporta apenas locais, então apenas o tipo LocConf é definido.

Para criar armazenamento de estado e configuração para seu módulo, defina uma estrutura e implemente o traço Merge :

#[derivar(Depuração, Padrão)] 
struct ModuleConfig { 
habilitado: bool, 
método: String, 
} 

impl http::Merge 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 armazena um estado ligado/desligado no campo habilitado, juntamente com um método de solicitação HTTP. O manipulador verificará esse método e permitirá ou proibirá solicitações.

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

Por meio das vinculações FFI, os escritores de módulos Rust têm acesso ao tipo ngx_command_t e podem registrar diretivas como fariam em C. O módulo ngx-rust-howto define uma diretiva howto que aceita um valor de string. Neste caso, definimos um comando, implementamos uma função setter e então (na próxima seção) conectamos esses comandos ao sistema principal. Lembre-se de encerrar sua matriz de comando com a macro ngx_command_null! fornecida.

Veja como criar uma diretiva simples usando comandos NGINX:

#[no_mangle] 
estático mut ngx_http_howto_commands: [ngx_command_t; 2] = [ 
ngx_command_t { 
nome: ngx_string!("howto"), 
tipo_: (NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1) como ngx_uint_t, 
definido: Alguns(ngx_http_howto_commands_set_method), 
conf: NGX_RS_HTTP_LOC_CONF_OFFSET, 
deslocamento: 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 { 
inseguro { 
deixe conf = &mut *(conf como *mut ModuleConfig); 
deixe args = (*(*cf).args).elts como *mut ngx_str_t; 
conf.enabled = true; 
conf.method = (*args.add(1)).to_string(); 
}; 

std::ptr::null_mut() 
} 

Enganchando no módulo

Agora que você tem uma função de registro, um manipulador de fase e comandos para configuração, você 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, manipuladores de fase e comandos de diretiva. Cada 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!. No exemplo abaixo, você pode ver como os comandos são definidos no campo de comandos e o contexto que referencia as funções de registro dos módulos é definido no campo ctx . Para este módulo, todos os outros campos são efetivamente padrões.

#[no_mangle] 
estático ngx_http_howto_module_ctx: ngx_http_module_t = ngx_http_module_t { 
pré-configuração: Alguns(Módulo::pré-configuração),
pós-configuração: Alguns(Módulo::pós-configuração), 
create_main_conf: Alguns(Módulo::create_main_conf), 
init_main_conf: Alguns(Módulo::init_main_conf), 
create_srv_conf: Alguns(Módulo::create_srv_conf), 
merge_srv_conf: Alguns(Módulo::merge_srv_conf), 
create_loc_conf: Alguns(Módulo::create_loc_conf), 
merge_loc_conf: Some(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::max_value(), 
index: ngx_uint_t::max_value(), 
name: std::ptr::null_mut(), 
spare0: 0, 
spare1: 0, 
versão: nginx_version como ngx_uint_t, 
assinatura: 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_: NGX_HTTP_MODULE como ngx_uint_t, 

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

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ê praticamente concluiu as etapas necessárias para configurar e registrar um novo módulo Rust. Dito isso, você ainda precisa implementar o manipulador de fase (howto_access_handler) que foi definido no gancho de pós-configuração .

Manipuladores

Os manipuladores são chamados para cada solicitação recebida e executam a maior parte do trabalho do seu módulo. Os manipuladores de solicitações têm sido o foco da equipe ngx-rust e são onde a maioria das melhorias ergonômicas iniciais foram feitas. Enquanto as etapas de configuração anteriores exigem que o Rust seja escrito em um estilo semelhante ao C, o ngx-rust oferece mais conveniência e utilidades para manipuladores de solicitações.

Como visto no exemplo abaixo, o ngx-rust fornece a macro http_request_handler! para aceitar um fechamento Rust chamado com uma instância Request . Ele também fornece utilitários para obter configuração e variáveis, definir essas variáveis e acessar memória, outros primitivos NGINX e APIs.

Para iniciar um procedimento de manipulador, invoque a macro e forneça sua lógica de negócios como um fechamento Rust. Para o módulo ngx-rust-howto, verifique o método da solicitação para permitir que a solicitação continue o processamento.

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("a configuração do módulo é nenhuma"); 

ngx_log_debug_http!(request, "módulo howto habilitado chamado"); 
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 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 construir (com cargo build ), adicionar o binário do módulo à diretiva load_module em um nginx.conf local e executá-lo usando uma instância do NGINX. Ao escrever este tutorial, usamos o NGINX v1.23.3, o NGINX_VERSION padrão suportado pelo ngx-rust. Ao criar e executar módulos dinâmicos, certifique-se de usar a mesma NGINX_VERSION para compilações ngx-rust que a instância NGINX que você está executando em sua máquina.

Conclusão

O NGINX é um sistema de software maduro com anos de recursos e casos de uso incorporados. É um proxy capaz, balanceador de carga e um servidor web de classe mundial. Sua presença no mercado é certa por muitos anos, o que alimenta nossa motivação para desenvolver suas capacidades e dar aos nossos usuários novos métodos de interação com ele. Com a popularidade do Rust entre os desenvolvedores e suas restrições de segurança aprimoradas, estamos animados em oferecer a opção de usar o Rust junto com o melhor servidor web do mundo.

No entanto, a maturidade e o ecossistema rico em recursos do NGINX criam uma grande área de superfície de API e o ngx-rust apenas arranhou a superfície. O projeto visa melhorar e expandir por meio da adição de interfaces Rust mais idiomáticas, da construção de módulos de referência adicionais e do avanço da ergonomia dos módulos de escrita.

É aqui que você entra! O projeto ngx-rust está aberto a todos e disponível no GitHub . Estamos ansiosos para trabalhar com a comunidade NGINX para continuar melhorando os recursos e a facilidade de uso do módulo. Confira e experimente você mesmo as encadernações! E entre em contato, registre problemas ou RPs e interaja conosco no canal do Slack da Comunidade NGINX .

Recursos


"Esta postagem do blog pode fazer referência a produtos que não estão mais disponíveis e/ou não têm mais suporte. Para obter as informações mais atualizadas sobre os produtos e soluções F5 NGINX disponíveis, explore nossa família de produtos NGINX . O NGINX agora faz parte do F5. Todos os links anteriores do NGINX.com redirecionarão para conteúdo semelhante do NGINX no F5.com."