使用Rust扩展NGINX(C语言的替代方案)

NGINX | 2023年10月12日

在相对较短的发展历程中,编程语言Rust广受赞誉,并形成了丰富而成熟的生态系统。Rust和Cargo(其构建系统、工具链接口和软件包管理器)都是当前技术领域中备受青睐且广受期待的技术,Rust也一直稳居RedMonk编程语言排名前20名。此外,采用Rust的项目通常在稳定性方面有所提升,与安全相关的编程错误也有所减少(例如,Android开发者讲述了一个颇具说服力的案例,展示了阶段性的显著改进)。

一段时间以来,F5一直热切关注Rust及其Rustacean社区的这些发展。我们通过积极倡导这门语言、其工具链及未来应用,表达了对这些发展的重视。

如今,NGINX也开始切实投入,以满足开发者在日益数字化且愈发重视安全的世界中的期望和需求。我们很高兴地宣布推出ngx-rust项目,这是一种使用Rust语言编写NGINX模块的新方式。Rust开发者们,这个项目正是为你们而来!

NGINX与Rust简史

长期关注NGINX和我们GitHub的读者可能知道,这并不是我们首次开发基于Rust的模块。在Kubernetes发展初期和服务网格兴起之初,我们开展了一些Rust相关工作,为ngx-rust项目奠定了基础。

最初,ngx-rust用于加快基于NGINX开发兼容Istio的服务网格产品。初始原型开发完成后,该项目多年未有改动。在此期间,许多社区成员复刻了该代码库,或受到ngx-rust所提供原始Rust绑定示例的启发,创建了其他项目。

后来,F5 Distributed Cloud Bot Defense团队需要将NGINX代理集成到其防护服务中。为此,需要构建一个新模块。

我们还希望继续扩展Rust产品组合,同时改善开发者体验,满足客户不断变化的需求。为此,我们利用内部创新赞助计划,与ngx-rust的原作者合作,开发了一个经过改进的全新Rust绑定项目。在长期停更后,我们恢复发布ngx-rust crate,并完善了文档,改进了构建易用性,以便社区使用。

这对NGINX意味着什么?

模块是NGINX的核心构建组件,实现了NGINX的大部分功能。模块也是NGINX用户自定义这些功能并为特定用例构建支持的强大方式。

一直以来,NGINX仅支持使用C语言编写的模块(由于项目本身使用C语言编写,支持以宿主语言进行模块绑定是一个简单明了的选择)。然而,计算机科学和编程语言理论的发展改进了以往的范式,尤其是在内存安全性和正确性方面。这为Rust等语言用于NGINX模块开发铺平了道路。

如何开始使用ngx-rust

了解了NGINX和Rust的一些历史后,现在开始构建模块。您可以从源代码构建并在本地开发模块,也可以拉取ngx-rust源代码并协助改进绑定,或者直接从crates.io拉取crate。

要开始使用,请参阅ngx-rust README,其中介绍了贡献指南和本地构建要求。该项目仍处于早期开发阶段,但我们希望在社区的支持下改进质量并完善功能。本教程重点介绍如何创建一个简单的独立模块。还可以参阅ngx-rust examples,了解更复杂的示例。

这些绑定分为两个crate:

  • nginx-sys是一个从NGINX源代码生成绑定的crate。该文件会下载NGINX源代码和依赖项,并使用bindgen代码自动化工具创建外部函数接口(FFI)绑定。
  • ngx 是实现 Rust 粘合代码和 API 的主 crate,并重新导出 nginx-sys。模块开发者可通过这些符号导入 NGINX 并与之交互,而重新导出 nginx-sys 后,无需再显式导入该 crate。

以下说明将初始化一个基本工作区。首先创建一个工作目录并初始化Rust项目:

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

接下来,打开 Cargo.toml 文件并添加以下部分:

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

[dependencies] 
ngx = "0.3.0-beta"

或者,如果想在阅读时查看已完成的模块,可以从Git克隆该模块:

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

至此,您已准备好开始开发第一个NGINX Rust模块。模块的结构、语义和整体构建方法与使用C语言时所需的方法不会有太大差异。目前,我们计划以迭代方式提供NGINX绑定,使其能够生成并投入使用,帮助开发者打造创新成果。未来,我们将致力于提供更完善、更符合Rust惯用方式的开发体验。

这意味着,第一步是构建模块,同时配置在NGINX中安装和运行该模块所需的指令、上下文及其他内容。该模块将是一个简单的处理程序,可根据HTTP方法接受或拒绝请求,并会创建一个接受单个参数的新指令。我们将分步介绍这一过程,完整代码可参阅GitHub上的ngx-rust-howto仓库。

注意:本博客重点介绍Rust的特有内容,而非一般意义上的NGINX模块构建方法。如果您有兴趣构建其他NGINX模块,请参阅社区中的众多精彩讨论。这些讨论还将从更基础的层面说明如何扩展NGINX(更多信息请参阅下方的“资源”部分)。

模块注册

可以通过实现HTTPModule trait 来创建Rust模块。该trait定义了所有NGINX入口点(postconfiguration、preconfiguration、create_main_conf等)。模块开发者只需实现完成其任务所需的函数。此模块将实现postconfiguration方法,以安装其请求处理程序。

注意:如果尚未克隆ngx-rust-howto仓库,可以开始编辑由src/lib.rs文件,该文件由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() 
    } 
} 

Rust模块只需在访问阶段postconfiguration设置一个钩子,即NGX_HTTP_ACCESS_PHASE。模块可以为HTTP请求的不同阶段注册处理程序。有关详细信息,请参阅开发指南。

可以看到,在函数返回前添加了阶段处理程序howto_access_handler。稍后会再介绍它。现在只需注意,该函数将在请求链中执行处理逻辑。

根据模块类型及其需求,可以使用以下注册钩子:

  • preconfiguration
  • postconfiguration
  • create_main_conf
  • init_main_conf
  • create_srv_conf
  • merge_srv_conf
  • create_loc_conf
  • merge_loc_conf

配置状态

现在可以为模块创建存储了。这些数据包括所需的所有配置参数,以及用于处理请求或改变行为的内部状态。也就是说,模块需要持久保存的任何信息都可以放入结构体中并保存。此Rust模块在location配置级别使用ModuleConfig结构体。配置存储必须实现Merge和Defaulttrait。

在上一步定义模块时,可以设置主配置、服务器配置和位置配置的类型。此处开发的Rust模块仅支持位置,因此只设置了LocConf类型。

要为模块创建状态和配置存储,请定义一个结构体并实现Merge trait:

#[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在enabled字段中存储启用或禁用状态,并存储一个HTTP请求方法。处理程序将根据此方法进行检查,以允许或禁止请求。

定义存储后,模块便可创建指令和配置规则,供用户自行设置。NGINX使用ngx_command_t类型和数组,将模块定义的指令注册到核心系统中。

通过FFI绑定,Rust模块开发者可以访问ngx_command_t类型,并能像在C语言中一样注册指令。ngx-rust-howto模块定义了一个接受字符串值的howto指令。在本例中,我们定义一个命令,实现一个setter函数,然后在下一节中将这些命令接入核心系统。请记得使用提供的ngx_command_null!宏终止命令数组。

以下是使用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() 
} 

接入模块

现在已有注册函数、阶段处理程序和配置命令,可以将它们关联起来,并向核心系统公开这些函数。创建一个静态ngx_module_t结构,其中包含对注册函数、阶段处理程序和指令命令的引用。每个模块都必须包含一个类型为ngx_module_t的全局变量。

然后创建上下文和静态模块类型,并使用ngx_modules!宏将其公开。在以下示例中,可以看到如何在commands字段中设置commands,以及如何在ctx字段中设置引用模块注册函数的上下文。对于此模块,其他所有字段实际上都使用默认值。

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

完成上述操作后,设置并注册新Rust模块所需的步骤基本就完成了。不过,仍需实现阶段处理程序(howto_access_handler),该程序已在postconfiguration钩子中设置。

处理程序

每个传入请求都会调用处理程序,模块的大部分工作都由处理程序完成。请求处理程序一直是ngx-rust团队的重点,初期的大部分易用性改进也集中于此。虽然前面的设置步骤要求以类似C语言的风格编写Rust代码,但ngx-rust为请求处理程序提供了更多便利功能和实用工具。

如下例所示,ngx-rust提供宏http_request_handler!,用于接收一个以Request实例为参数调用的Rust闭包。它还提供实用工具,用于获取配置和变量、设置这些变量,以及访问内存、其他NGINX原语和API。

要启动处理程序,请调用该宏,并以Rust闭包的形式提供业务逻辑。对于ngx-rust-howto模块,请检查请求方法,以允许继续处理该请求。

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

至此,您已完成第一个Rust模块!

GitHub 上的ngx-rust-howto仓库在 conf 目录中包含一个 NGINX 配置文件。也可以使用 cargo build 进行构建,将模块二进制文件添加到本地 nginx.conf 的 load_module 指令中,然后使用 NGINX 实例运行该模块。编写本教程时,我们使用的是 NGINX v1.23.3,即 ngx-rust 默认支持的 NGINX_VERSION。构建和运行动态模块时,请确保 ngx-rust 构建所使用的 NGINX_VERSION 与本机运行的 NGINX 实例版本相同。

总结

NGINX是一套成熟的软件系统,经过多年发展,已集成丰富的功能并支持多种使用场景。它是一款功能强大的代理、负载均衡器和广受认可的Web服务器。NGINX未来多年仍将在市场中占有一席之地,这也激励我们继续拓展其功能,为用户提供与之交互的新方式。随着Rust日益受到开发者欢迎,并具备更完善的安全约束,我们很高兴为用户提供将Rust与这款出色的Web服务器结合使用的选择。

不过,NGINX的成熟度和功能丰富的生态系统也带来了庞大的API接口范围,而ngx-rust目前仅触及其中一小部分。该项目旨在通过增加更符合Rust惯用方式的接口、构建更多参考模块,以及改善模块编写体验来持续完善和扩展。

现在正是您参与其中的时候!ngx-rust项目面向所有人开放,并且可在GitHub上获取。我们期待与NGINX社区携手合作,持续提升该模块的功能和易用性。欢迎查看项目并亲自试用这些绑定!也欢迎联系我们、提交问题或PR,并通过NGINX Community Slack频道与我们交流。

资源


分享
标签: F5 NGINX, Tech

相关博客文章

NGINX Plus R26 正式发布
NGINX | 2022/02/15

NGINX Plus R26 正式发布

NGINX Plus R26通过缓存JSON Web密钥集加快JWT验证,并强化TLS握手,从而提升安全性。NGINX JavaScript的新功能包括增强对异步函数的支持,以及实现WebCrypto API。

使用NGINX App Protect DoS保护gRPC应用,抵御严重的DoS攻击
NGINX | 2022/02/10

使用NGINX App Protect DoS保护gRPC应用,抵御严重的DoS攻击

gRPC具备开发人员设计和部署现代应用所需的速度与灵活性,但其开放性也使其极易受到第7层DoS攻击。NGINX App Protect DoS可保护gRPC应用免受此类破坏性攻击。

Ingress Controller 选型指南(第 2 部分):风险与面向未来的规划
NGINX | 2021/09/13

Ingress Controller 选型指南(第 2 部分):风险与面向未来的规划

了解选择错误的Ingress控制器可能带来的风险,以及如何从关键方面着手,让您的选择更好地适应未来需求。

NGINX Plus R19 正式发布
NGINX | 2019/09/03

NGINX Plus R19 正式发布

NGINX Plus R19扩展了监控功能,不仅提供更多指标,还新增了指标分析方式;增加试运行模式,用于测试限速效果;增强键值存储,以支持IP地址范围;并使限速更加动态。

NGINX现已正式加入F5
NGINX | 2019/05/09

NGINX现已正式加入F5

NGINX现已正式成为F5的一部分。Gus Robertson现任F5 NGINX业务部门负责人。他阐述了两家公司在人才、文化和愿景方面融合的价值,以及双方将如何携手增强NGINX的开源产品和商业产品。

使用NGINX和NGINX Plus实现高性能缓存
NGINX | 2016/08/24

使用NGINX和NGINX Plus实现高性能缓存

我将介绍NGINX在内容缓存和性能方面的工作原理,并分享一些控制NGINX内容缓存方式的技巧。