博客 | NGINX

使用 Rust(C 的替代品)扩展 NGINX

NGINX-F5-horiz-black-type-RGB 的一部分
Matthew Yacobucci 缩略图
马修·雅科布奇
2023 年 10 月 12 日发布

在相对较短的历史中,编程语言Rust获得了非凡的赞誉以及丰富而成熟的生态系统。 Rust 和 Cargo(其构建系统、工具链接口和包管理器)都是业界推崇和期望的技术,其中 Rust 在RedMonk 编程语言排名的前 20 种语言中占据稳定地位。 此外,采用 Rust 的项目通常在稳定性和与安全性相关的编程错误方面有所改进(例如,Android 开发人员讲述了一个引人注目的间断改进故事)。

F5 一直兴奋地关注着 Rust 及其 Rustaceans 社区的发展。 我们已注意到对该语言、其工具链和采用的积极倡导

在 NGINX,我们正在投入一些精力来满足开发人员在日益数字化和安全意识日益增强的世界中的需求。 我们很高兴地宣布ngx-rust 项目——一种使用 Rust 语言编写 NGINX 模块的新方法。 Rustaceans,这个是给你的!

NGINX 和 Rust 简史

NGINX 和我们的GitHub的密切关注者可能会意识到这不是我们第一次推出基于 Rust 的模块。 在Kubernetes服务网格发展的初期,围绕 Rust 开展了一些工作,为 ngx-rust 项目奠定了基础。

ngx-rust 最初是为了加速开发与 NGINX 兼容的 Istio 服务网格产品而存在的,在开发出最初的原型之后,这个项目很多年都没有改变过。 在此期间,许多社区成员分叉了存储库或创建了受 ngx-rust 中提供的原始 Rust 绑定示例启发的项目。

快进,我们的F5 分布式云机器人防御团队需要将 NGINX 代理集成到其保护服务中。 这需要构建一个新的模块。

我们还希望继续扩大我们的 Rust 产品组合,同时改善开发人员体验并满足客户不断变化的需求。 因此,我们利用内部创新赞助并与原始 ngx-rust 作者合作开发了一个新的和改进的 Rust 绑定项目。 经过长时间的中断后,我们重新开始发布 ngx-rust 板条箱,并增强了文档和改进,以构建符合人体工程学的设计供社区使用。

这对 NGINX 意味着什么?

模块是 NGINX 的核心构建块,实现其大部分功能。 模块也是 NGINX 用户自定义功能和构建特定用例支持的最强大方式。

NGINX 传统上仅支持用 C 编写的模块(作为用 C 编写的项目,支持宿主语言中的模块绑定是一个明确而简单的选择)。 然而,计算机科学和编程语言理论的进步已经改进了过去的范式,特别是在内存安全性和正确性方面。 这为 Rust 等语言铺平了道路,现在这些语言可以用于 NGINX 模块开发。

如何开始使用 ngx-rust

现在了解了 NGINX 和 Rust 的一些历史,让我们开始构建一个模块。 您可以自由地从源代码构建并在本地开发模块,拉取ngx-rust源代码并帮助构建更好的绑定,或者简单地从crates.io拉取板条箱。

ngx-rust README涵盖了贡献指南和本地构建要求。 它仍处于早期阶段和初步开发阶段,但我们的目标是通过社区支持来提高质量和功能。 在本教程中,我们重点介绍如何创建一个简单的独立模块。 您还可以查看ngx-rust 示例,了解更复杂的课程。

绑定被组织成两个包:

  • nginx-sys是一个从 NGINX源代码生成绑定的包。 该文件下载 NGINX源代码、依赖项,并使用bindgen代码自动化创建外部函数接口 (FFI) 绑定。
  • ngx 是实现 Rust 粘合代码、API 和重新导出nginx-sys 的主要包。 模块编写器通过这些符号导入 NGINX 并与之交互,而nginx-sys的重新导出则无需明确导入。

下面的说明将初始化一个骨架工作区。 首先创建工作目录并初始化 Rust 项目:

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

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

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

[依赖项] 
ngx = "0.3.0-beta"

或者,如果您想在阅读时查看完整的模块,可以从 Git 克隆它:

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

这样,您就可以开始开发您的第一个 NGINX Rust 模块了。 构建模块的结构、语义和一般方法与使用 C 时所需的方法看起来没有太大区别。目前,我们已着手以迭代方式提供 NGINX 绑定,以生成、使用绑定,并将其交到开发人员手中,以创建他们的创新产品。 将来,我们将致力于打造更好、更惯用的 Rust 体验。

这意味着您的第一步是构建模块,并结合在 NGINX 中安装和运行所需的任何指令、上下文和其他方面。您的模块将是一个简单的处理程序,可以根据 HTTP 方法接受或拒绝请求,并且它将创建一个接受单个参数的新指令。 我们将分步讨论这个问题,但您可以参考 GitHub 上的ngx-rust-howto repo 中的完整代码。

笔记: 本博客主要介绍 Rust 的具体细节,而不是如何构建 NGINX 模块。 如果您有兴趣构建其他 NGINX 模块,请参考社区中的许多精彩讨论。 这些讨论还将为您提供有关如何扩展 NGINX 的更基本的解释(请参阅下面的资源部分了解更多内容)。

模块注册

您可以通过实现HTTPModule特征来创建 Rust 模块,该特征定义了所有 NGINX 入口点( postconfigurationpreconfigurationcreate_main_conf等)。 模块编写者仅需实现其任务所需的功能。 该模块将实现后配置方法来安装其请求处理程序。

笔记: 如果您尚未克隆ngx-rust-howto repo,您可以开始编辑由cargo init创建的src/lib.rs文件。

结构模块;

为模块实现 http::HTTPModule { 
类型 MainConf = (); 
类型 SrvConf = (); 
类型 LocConf = ModuleConfig; 

不安全的外部“C”fn postconfiguration(cf:*mut ngx_conf_t)-> ngx_int_t { 
让 htcf = http::ngx_http_conf_get_module_main_conf(cf,&ngx_http_core_module);

让 h = ngx_array_push(
&mut (*htcf).phases[ngx_http_phases_NGX_HTTP_ACCESS_PHASE as usize].handlers,
) as *mut ngx_http_handler_pt;
如果 h.is_null() { 
返回 core::Status::NGX_ERROR.into();
} 

// 设置 Access 阶段处理程序 
*h = Some(howto_access_handler); 
core::Status::NGX_OK.into() 
} 
} 

Rust 模块只需要在访问阶段NGX_HTTP_ACCESS_PHASE进行一个后配置钩子。 模块可以为 HTTP 请求的各个阶段注册处理程序。 有关这方面的更多信息,请参阅开发指南中的详细信息。

您将看到在函数返回之前添加了阶段处理程序howto_access_handler 。 我们稍后再讨论这个问题。 现在,只需注意它是在请求链中执行处理逻辑的函数。

根据您的模块类型及其需求,可用的注册挂钩如下:

  • 預設配置
  • 后配置
  • 创建主配置文件
  • 初始化主配置文件
  • 创建 SRV 配置文件
  • 合并服务端
  • 创建位置配置
  • 合并位置_conf

配置状态

现在是时候为您的模块创建存储了。 这些数据包括所需的任何配置参数或用于处理请求或改变行为的内部状态。 本质上,模块需要保留的任何信息都可以放入结构中并保存。 这个 Rust 模块在位置配置级别使用ModuleConfig结构。 配置存储必须实现合并和默认特征。

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

要为您的模块创建状态和配置存储,请定义一个结构并实现合并特征:

#[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在启用字段中存储开/关状态,以及 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)作为 ngx_uint_t,设置: 一些(ngx_http_howto_commands_set_method), 
conf: NGX_RS_HTTP_LOC_CONF_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!宏公开它们。 在下面的示例中,您可以看到如何在命令字段中设置命令,以及如何在ctx字段中设置引用模块注册函数的上下文。 对于此模块,所有其他字段实际上都是默认的。

#[no_mangle] 
static ngx_http_howto_module_ctx: ngx_http_module_t = ngx_http_module_t { 
预配置: 一些(模块::预配置), 
后配置: 一些(模块::后配置), 
create_main_conf: 一些(模块::create_main_conf), 
init_main_conf: 一些(模块::init_main_conf), 
create_srv_conf: 一些(模块::create_srv_conf), 
merge_srv_conf: 一些(模块::merge_srv_conf), 
create_loc_conf: 一些(模块::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, 
备用1: 0, 
版本:nginx_version as ngx_uint_t, 
签名: NGX_RS_MODULE_SIGNATURE.as_ptr() 作为 *const c_char,

ctx:&ngx_http_howto_module_ctx 作为 *const _ 作为 *mut _,
命令:不安全{&ngx_http_howto_commands[0] 作为 *const _ 作为 *mut _},
type_: NGX_HTTP_MODULE 作为 ngx_uint_t,

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

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)

处理程序

每个传入请求都会调用处理程序,并执行模块的大部分工作。 请求处理程序一直是 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("模块配置为无"); 

ngx_log_debug_http!(request, "howto 模块已启用并调用"); 
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 repo 在 conf 目录中包含一个 NGINX 配置文件。 您还可以构建(使用cargo build ),将模块二进制文件添加到本地 nginx.conf 中的load_module指令,然后使用 NGINX 实例运行它。在编写本教程时,我们使用了 NGINX v1.23.3,这是 ngx-rust 支持的默认 NGINX_VERSION。 在构建和运行动态模块时,请确保对 ngx-rust 构建使用与您在机器上运行的 NGINX 实例相同的NGINX_VERSION

结论

NGINX 是一个成熟的软件系统,内置了多年的功能和用例。 它是一个功能强大的代理、负载平衡器和世界一流的网络服务器。 未来几年它在市场上的存在是肯定的,这激励我们不断增强其功能,并为用户提供与之互动的新方法。 随着 Rust 在开发人员中的流行及其安全约束的改进,我们很高兴能够提供与世界上最好的 Web 服务器一起使用 Rust 的选项。

然而,NGINX 的成熟度和功能丰富的生态系统都创建了庞大的 API 表面积,而 ngx-rust 仅触及了表面。 该项目旨在通过添加更多惯用的 Rust 接口、构建额外的参考模块以及提高编写模块的人体工程学来改进和扩展。

这就是你进来的地方! ngx-rust 项目对所有人开放,可在 GitHub 上获取。 我们渴望与 NGINX 社区合作,不断改进该模块的功能和易用性。 检查一下并亲自试验一下绑定! 请联系我们、提交问题或 PR,并通过NGINX 社区 Slack 频道与我们互动。

资源


“这篇博文可能引用了不再可用和/或不再支持的产品。 有关 F5 NGINX 产品和解决方案的最新信息,请探索我们的NGINX 产品系列。 NGINX 现在是 F5 的一部分。 所有之前的 NGINX.com 链接都将重定向至 F5.com 上的类似 NGINX 内容。”