原文作者:Matthew Yacobucci - F5 首席软件工程师转载来源: NGINX 中文官网

作为一门新兴的编程语言,Rust 凭借其丰富成熟的生态系统在业界崭露头角,颇受赞誉。Rust 和 Cargo(其构建系统、工具链接口及软件包管理器)在这个领域内备受推崇和渴望,而 Rust 在 RedMonk 编程语言排名中稳居前 20 位。此外,采用 Rust 的项目通常显示出稳定性和安全相关的编程错误的改进(例如 Android 开发人员分享的间断性改进例子)。
一段时间以来,F5 一直在密切关注 Rust 及其 Rustaceans 社区的这些发展,并注意到在业界的积极倡导下,该语言及其工具链的采用率不断增长。
NGINX 和 Rust 的发展历程
NGINX 和我们 GitHub 的忠实粉丝可能都知道,这并非我们第一次尝试 Rust 的模块开发。在 Kubernetes 的早期和 service mesh(服务网格)的初级阶段,我们就围绕 Rust 开展了一些工作,为 ngx-rust 项目奠定了坚实的基础。
最初,ngx-rust 是为加速与 NGINX 兼容的 Istio 服务网格 产品的开发而创建的。然而,随着时间推移,该项目逐渐被搁置。在此期间,许多社区成员分拆了代码库,或是创建了与 ngx-rust 提供的原始 Rust 绑定 (binding) 示例相关的项目。
现在,我们的 F5 分布式云 Bot 防御团队需要将 NGINX 代理集成至其保护服务中。这就需要构建一个新模块。
这对 NGINX 有何意义?
模块不仅是 NGINX 的核心构建模块,可实现其大部分功能,而且还是 NGINX 用户自定义功能并为特定用例提供支持的最有力方式。
过去,NGINX 仅支持使用 C 语言编写的模块(作为一个用 C 语言编写的项目,必然会选择使用主语言支持模块绑定)。然而,计算机科学和编程语言理论的进步改善了过去的范式,尤其是在内存安全性和准确性方面。这就为像 Rust 等语言的使用铺平了道路,现在这些语言就可用于 NGINX 模块的开发。
如何开始使用 ngx-rust
在了解了 NGINX 和 Rust 的发展历程之后,让我们开始构建一个模块。您可以自由地从源代码开始构建并在本地开发模块,拉取 ngx-rust 源代码以便构建更好的绑定(binding),或者直接从 crates.io 中拉取 crate。
ngx-rust README 文件涵盖贡献指南和本地构建要求,有助于快速入门。ngx-rust 项目仍处于初期开发阶段,但也无妨,我们的目标是在社区的支持下提高质量和改进功能。在本教程中,我们将重点介绍如何创建一个简单的独立模块。您还可以查看 ngx-rust 示例,了解更复杂的用例。
该绑定分为以下两个 crate:
-
nginx-sys 是一个创建于 NGINX 源代码的绑定的 crate。该文件下载 NGINX 源代码和依赖项,并使用 bindgen 代码自动化来创建外部函数接口 (FFI) 绑定。
-
ngx 是实现 Rust 粘合代码、API 和重新导出 nginx-sys 的主要 crate。模块编写程序通过这些符号导入 并于NGINX 交互,而 nginx-sys 的重新导出消除了显示导入的需要。
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 [email protected]:f5yacobucci/ngx-rust-howto.git
这样,您就可以开始开发第一个 NGINX Rust 模块了。构建模块所用的结构、语义及一般方法与使用 C 语言时大同小异。目前,我们采用迭代的方式提供 NGINX 绑定,以便生成可用的绑定并将其交给开发者来创建他们的创新产品。未来,我们将努力打造更出色、更易用的 Rust 体验。
这意味着您首先要结合在 NGINX 中安装和运行所需的指令、上下文及其他要素来构建您的模块。您的模块将是一个简单的处理程序,可以接受或拒绝基于 HTTP 方法的请求,并能够创建可接受单个参数的新指令。我们将分步介绍,您也可以参考 GitHub 上 ngx-rust-howto 代码库中的完整代码。
注:本文重点介绍 Rust 的要点,而不是如何构建 NGINX 模块。如欲了解如何构建其他 NGINX 模块,请参考社区论坛精华帖。通过这些精华帖,您将能够更透彻地了解如何扩展 NGINX(详见下面的“资源”一节)。
模块注册
您可以通过实现 HTTPModule trait(定义了 postconfiguration、preconfiguration、create_main_conf 等所有 NGINX 入口点)来创建 Rust 模块。模块编写程序只需实现其任务所需的函数。该模块将实施 postconfiguration 方法来安装其请求处理程序。
注:若您尚未克隆 ngx-rust-howto 代码库,则可先编辑由 cargo init 创建的 src/lib.rs 文件。
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 模块仅需在访问阶段 NGX_HTTP_ACCESS_PHASE 使用 postconfiguration hook。模块可为 HTTP 请求的各个阶段注册处理程序。
在函数返回之前,您将看到名为 howto_access_handler的阶段处理程序被添加。我们稍后会谈到这一点。目前,只需注意该函数将在请求链中执行处理逻辑。
根据模块类型及其需求,可使用以下注册 hook:
-
preconfiguration
-
postconfiguration
-
create_main_conf
-
init_main_conf
-
create_srv_conf
-
merge_srv_conf
-
create_loc_conf
-
merge_loc_conf
配置状态
现在需要为模块创建存储空间。这些数据包括所需的配置参数或用于处理请求或改变行为的内部状态。具体而言,模块需要持久保存的任何信息均可存入结构中。该 Rust 模块在 location 配置级别使用了一个 ModuleConfig 结构。配置存储必须实现 Merge 和 Default trait。
在上述步骤中定义模块时,您可为主配置、服务器配置及 location 配置设置类型。您在此处开发的 Rust 模块仅支持 location 配置,因此只设置了 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 type,并能够像在 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 模块所需的步骤。不过,您仍需实现在 postconfiguration hook 中设置的阶段处理程序 (howto_access_handler)。
处理程序
处理程序针对每个传入请求进行调用,并执行模块的大部分工作。请求处理程序一直是 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。在构建和运行动态模块时, 请确保使用与您计算机上运行的 NGINX 实例相同的 NGINX_VERSION 进行 ngx-rust 构建。
结语
NGINX 是一个成熟的软件系统,历经多年的发展,包含丰富的特性和用例。与此同时,它还是功能强大的代理、负载均衡器及卓越的 Web 服务器。未来几年,它的市场地位将无可撼动,这促使我们进一步增强其功能,为用户提供新的可能。随着 Rust 在开发人员中日趋流行及其安全问题得以改善,我们很高兴在卓越的 Web 服务器 NGINX 中提供 Rust 选项。
NGINX 的成熟度及其功能丰富的生态系统提供了广泛的 API 选择,ngx-rust 仅仅是其中一例。该项目旨在通过增加更易用的 Rust 接口、构建更多参考模块并提升模块编写的工效来实现改进与扩展。
我们需要您的参与!ngx-rust 项目面向所有人开放,可在 GitHub 上获取。我们希望与 NGINX 社区展开紧密合作,不断改进模块的功能和易用性。您可以亲自试用绑定,体验一番!请在 NGINX Community Slack 频道上联系我们、提交问题或建议,并与我们进行互动。