在相对较短的发展历程中,编程语言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。稍后会再介绍它。现在只需注意,该函数将在请求链中执行处理逻辑。
根据模块类型及其需求,可以使用以下注册钩子:
preconfigurationpostconfigurationcreate_main_confinit_main_confcreate_srv_confmerge_srv_confcreate_loc_confmerge_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频道与我们交流。
资源
相关博客文章
NGINX Plus R26 正式发布
NGINX Plus R26通过缓存JSON Web密钥集加快JWT验证,并强化TLS握手,从而提升安全性。NGINX JavaScript的新功能包括增强对异步函数的支持,以及实现WebCrypto API。
使用NGINX App Protect DoS保护gRPC应用,抵御严重的DoS攻击
gRPC具备开发人员设计和部署现代应用所需的速度与灵活性,但其开放性也使其极易受到第7层DoS攻击。NGINX App Protect DoS可保护gRPC应用免受此类破坏性攻击。
Ingress Controller 选型指南(第 2 部分):风险与面向未来的规划
了解选择错误的Ingress控制器可能带来的风险,以及如何从关键方面着手,让您的选择更好地适应未来需求。
NGINX Plus R19 正式发布
NGINX Plus R19扩展了监控功能,不仅提供更多指标,还新增了指标分析方式;增加试运行模式,用于测试限速效果;增强键值存储,以支持IP地址范围;并使限速更加动态。
NGINX现已正式加入F5
NGINX现已正式成为F5的一部分。Gus Robertson现任F5 NGINX业务部门负责人。他阐述了两家公司在人才、文化和愿景方面融合的价值,以及双方将如何携手增强NGINX的开源产品和商业产品。
