【CSDN 萨德基】你最常见的合作开发词汇是何种呢?近日,一位著眼于 Linux 操控性和开放源码智能化基准测试的应用软件技师 Michael Larabel 在一则该文中表示,在 Cloudflare,她们已经开始用 Rust 撰写的代替方案来替代 Nginx,但 Cloudflare 的基础建设非常巨大,并且有很多不同的服务项目在有所作为。最后她们是怎样来撰写的呢?一起上看该文~
校对 | 天山山脉木 白眉林| 女王彧公司出品 | CSDN(ID:CSDNnews)在 Cloudflare,技师们所花大量的时间解构或重新重写原有机能。最近在合作开发两个可以代替内部 cf-html 的组件,它在核心逆向互联网全权中,被称作 FL(Front Line)。cf-html 是负责导出和重写 HTML 的架构,它从中文网站根源流入中文网站访客。从 Cloudflare 早期开始,就提供了一些机能,这些机能将在飞行中为你重写互联网允诺的积极响应体。以此种方式撰写的第两个机能是用 JavaScript 代替电子邮件地址,然后在互联网应用程序中查阅时读取该电子邮件地址。由于机器通常无法评估结果 JavaScript,这有利于防止从中文网站上掠取电子邮件地址。
FL 是 Cloudflare 绝大部分应用基础建设方法论运转的地方性,主要由 Lua 脚本词汇撰写的标识符组成,它作为 OpenResty 的一部分在 Nginx 其内运转。为了间接与 Nginx 交会,部分(如cf-html)是用 C 和 C++ 等词汇撰写。过去,在 Cloudflare 有很多这样的 OpenResty 服务项目,但 FL 是惟一的剩下的服务项目之一,因为技师们把其他组件转移到了 Workers 或如前所述 Rust 的全权上。
当 HTTP 允诺通过互联网,尤其是 FL 做了什么姿势时,几乎所有的目光都集中在允诺抵达客户的根源之前出现的事情。这是绝大部分业务方法论出现的地方性:防火墙准则、工人和路由器决定都出现在允诺中。但从技师的角度上看,很多有意思的工作出现在积极响应上,因此技师们将 HTML 积极响应从圆心流往给中文网站访客。
处理此种情况的方法论,在两个动态的 Nginx 组件中,并在 Nginx 的积极响应体冷却系统阶段运转。cf-html 使用两个INS13ZD HTML 导出器来相匹配某一的 HTML 条码和内容,称作 Lazy HTML 或 lhtml,它和 cf-html 机能的绝大部分方法论都是用 Ragel 状态机引擎撰写的。
所以,她们已经开始用内部的 Rust 撰写的代替方案来替代 Nginx,但 Cloudflare 的基础建设非常巨大,并且有很多不同的服务项目在有所作为。
内存安全性
所有的 cf-html 方法论都是用 C 词汇撰写,因此容易受到困扰很多大型 C 标识符库的内存损坏问题的影响。2017 年,当团队试图代替部分 cf-html 时,这导致了两个安全漏洞。FL 从内存中读取任意数据并将其附加到积极响应体。这可能包括同时通过 FL 的其他允诺的数据,此安全事件被广泛称作 Cloudbleach。
自这一事件出现以来,Cloudflare 实施了一系列政策和保障措施,以确保此类事件不再出现。尽管多年来在 cf-html 上进行了工作,但架构上几乎没有实现新机能,而且技师们现在对 FL(以及互联网上运转的任何其他进程)中出现的崩溃非常敏感,尤其是在可以通过积极响应反映数据的部分。
目前,FL 平台团队已经收到越来越多的系统允诺,她们可以方便地使用该系统来查阅和重写积极响应体数据。同时,另两个团队已经开始为 Workers 合作开发两个新的积极响应体导出和重写架构,称作 lol-HTML 或低输出延迟 HTML。lol html 不仅比 Lazy HTML 更快、更高效,而且目前作为 Worker 界面的一部分,它已经在正式生产中使用,并且是用 Rust 撰写的。在处理内存方面,它比 C 词汇安全得多。因此,它是两个理想的代替品。
因此,技师们开始研究两个用 Rust 撰写的新架构,该架构将包含 lol-HTML,并允许其他团队撰写积极响应体导出机能,而不会造成大量安全问题的威胁。新系统被称作 ROFL 或 Response Overseer for FL,它是两个完全用 Rust 撰写的全新 Nginx 组件。截至目前,ROFL 每秒处理数百万个积极响应,操控性与 cf-html 相当。在构建 ROFL 时,技师们已经能够弃用 Cloudflare 整个标识符库中最可怕的标识符之一,同时为 Cloudflare 的团队提供两个强大的系统,她们可以用来撰写需要导出和重写积极响应体数据的机能。
用 Rust 撰写 Nginx 组件
在撰写新组件时,技师们了解了很多 Nginx 的工作原理,以及如何让它与 Rust 对话。Nginx 没有提供太多用 C 词汇以外的词汇撰写组件的文档,因此技师需要做一些工作来确定如何用选择的词汇撰写 Nginx 组件。开始时,技师们大量使用了 nginx-rs 项目中的部分标识符,尤其是缓冲区和内存池的处理。虽然在 Rus t中撰写完整的 Nginx 组件是两个漫长的过程,但有几个关键点使整个过程成为可能,并值得讨论。
其中第两个是生成 Rust 绑定,以便 Nginx 可以与之通信。为此,技师们根据 Nginx 头文件中的符号定义,使用 Rust 的库 Bindgen 构建 FFI 绑定。要将其添加到原有的 Rust 项目中,首先要删除两个 Nginx 的副本并对其进行配置。理想情况下,这将在两个简单的脚本或 Makefile 中完成,但手动完成时,它看起来像这样:
$ git clone –depth=1 https://github.com/nginx/nginx.git$ cd nginx$ ./auto/configure –without-http_rewrite_module –without-http_gzip_module在 Nginx 处于正确状态的情况下,需要在 Rust 项目中创建两个文件,以便在组件构建时自动生成绑定。现在,将在构建中添加必要的参数,并使用 Bindgen 生成文件。对于参数,只需要包含头文件的目录,以便 clang 执行其任务。其次,可以将它们与一些 allowlist 参数一起输入 Bindgen,这样它就知道应该生成绑定的内容,以及可以忽略的内容。在顶部添加一些样板标识符,整个文件如下所示:
use std::env;use std::path::PathBuf;fn main() {println!(“cargo:rerun-if-changed=build.rs”); let clang_args = [ “-Inginx/objs/”, “-Inginx/src/core/”,“-Inginx/src/event/”, “-Inginx/src/event/modules/”, “-Inginx/src/os/unix/”, “-Inginx/src/http/”,“-Inginx/src/http/modules/” ]; let bindings = bindgen::Builder::default() .header(“wrapper.h”) .layout_tests(false).allowlist_type(“ngx_.*”) .allowlist_function(“ngx_.*”) .allowlist_var(“NGX_.*|ngx_.*|nginx_.*”).parse_callbacks(Box::new(bindgen::CargoCallbacks)) .clang_args(clang_args) .generate().expect(“Unable to generate bindings”); let out_path = PathBuf::from(env::var(“OUT_DIR”).unwrap());bindings.write_to_file(out_path.join(“bindings.rs”)) .expect(“Unable to write bindings.”);}希望这一切都是不言自明的。Bindgen 遍历 Nginx 源标识符,并在 Rust 中生成两个等效构造,并将其导入到项目中。此外,Bindgen 在 Nginx 中的几个符号存在问题,技师们需要为其修复。应包含以下内容:
#include const char* NGX_RS_MODULE_SIGNATURE = NGX_MODULE_SIGNATURE;const size_t NGX_RS_HTTP_LOC_CONF_OFFSET = NGX_HTTP_LOC_CONF_OFFSET;在 Cargo.toml 文件的一节中设置了此项并设置了 Bindgen,就可以开始构建了。
$ cargo buildCompiling rust-nginx-module v0.1.0 (/Users/sam/cf-repos/rust-nginx-module)Finished dev [unoptimized + debuginfo] target(s) in 4.70s幸运的是,我们应该在 target/debug/build 目录中看到两个名为 bindings.rs 的文件,其中包含所有 Nginx 符号的 Rust 定义。
$ find target -name bindings.rstarget/debug/build/rust-nginx-module-c5504dc14560ecc1/out/bindings.rs$ head target/debug/build/rust-nginx-module-c5504dc14560ecc1/out/bindings.rs/* automatically generated by rust-bindgen 0.61.0 */[…]为了能够在项目中使用它们,可以将它们包含在将调用的目录下的新文件中:
$ cat > src/bindings.rsinclude!(concat!(env!(“OUT_DIR”), “/bindings.rs”));有了该集合,只需将通常的导入添加到文件的顶部,就可以从 Rust 访问 Nginx 构造。与手动编码相比,这不仅使 Nginx 和 Rust 组件之间的接口出现错误的可能性要小得多,而且在 Rust 中构建组件时,可以使用它来检查 Nginx 中的东西的结构,并需要大量的腿部工作来设置一切。这确实证明了很多 Rust 库(如 Bindgen)的质量,这样的工作可以用很少的时间就可以完成。
一旦构建了 Rust 库,下一步就是将其连接到 Nginx 中。大多数 Nginx 组件都是动态校对的。也就是说,该组件作为整个 Nginx 校对的一部分进行校对。然而,自 Nginx 1.9.11 以来开始支持动态组件,这些组件是单独校对的,然后使用文件中的指令读取。这就是技师们需要用来构建 ROFL 的地方性,这样就可以在 Nginx 启动时单独校对并读取库。找到正确的格式以便从文档中找到必要的符号是很困难的,尽管可以使用单独的配置文件来设置一些元数据,但最好将其作为组件的一部分读取,以保持整洁。幸运的是,通过 Nginx 标识符库不需要花太多时间就可以找到调用的位置。
因此,之后只需确保相关符号存在的情况。
use std::os::raw::c_char;use std::ptr;#[no_mangle]pub static mut ngx_modules: [*const ngx_module_t; 2] = [ unsafe { rust_nginx_module as *const ngx_module_t }, ptr::null()];#[no_mangle]pub static mut ngx_module_type: [*const c_char; 2] = [“HTTP_FILTER\0”.as_ptr() as *const c_char, ptr::null()];#[no_mangle]pub static mut ngx_module_names: [*const c_char; 2] = [“rust_nginx_module\0”.as_ptr() as *const c_char, ptr::null()];在撰写 Nginx 组件时,确保其相对于其他组件的顺序正确是至关重要的。当 Nginx 启动时,动态组件被读取,这意味着它们(可能与直觉相反)是第两个运转积极响应的组件。通过指定组件相对于 gunzip 组件的顺序来确保组件在 gzip 解压缩后运转是必不可少的,否则您可能所花费大量时间盯着无法打印的字符流,且想知道为什么没有看到预期的积极响应。幸运的是,这也可以通过查阅 Nginx 源标识符并确保组件中存在相关实体来解决。下面是可以设置的示例:
pub static mut ngx_module_order: [*const c_char; 3] = [ “rust_nginx_module\0”.as_ptr() as *const c_char,“ngx_http_headers_more_filter_module\0”.as_ptr() as *const c_char, ptr::null()];本质上说,技师们希望组件恰好在组件之前运转,这应该允许它在预期的位置运转。
Nginx 和 OpenResty 的两个怪癖是,在处理 HTTP 积极响应时,它对调用外部服务项目不那么友好。它不是作为 OpenRestyLua 架构的一部分提供,尽管它会使处理允诺的积极响应阶段变得更加容易。我们无论如何都可以做到这一点,但这意味着必须分叉 Nginx 和 OpenResty,这将带来一些挑战。因此,从 Nginx 处理 HTTP 允诺到通过积极响应流传输状态,这些年来花了很多时间来思考如何传递状态,技师们的很多方法论都是围绕此种工作方式构建的。
对于 ROFL,这意味着为了确定是否需要为积极响应应用某个特性,需要在允诺中找出这一点,然后将该信息传递给积极响应,以便知道要激活哪些特性。为此,需要使用 Nginx 为您提供的两个实用程序。借助前面生成的文件,可以查阅结构的定义,其中包含与给定允诺相关的所有状态:
#[repr(C)]#[derive(Debug, Copy, Clone)]pub struct ngx_http_request_s { pub signature: u32, pub connection: *mut ngx_connection_t,pub ctx: *mut *mut ::std::os::raw::c_void, pub main_conf: *mut *mut ::std::os::raw::c_void,pub srv_conf: *mut *mut ::std::os::raw::c_void, pub loc_conf: *mut *mut ::std::os::raw::c_void,pub read_event_handler: ngx_http_event_handler_pt, pub write_event_handler: ngx_http_event_handler_pt, pub cache: *mut ngx_http_cache_t,pub upstream: *mut ngx_http_upstream_t, pub upstream_states: *mut ngx_array_t, pub pool: *mut ngx_pool_t,pub header_in: *mut ngx_buf_t, pub headers_in: ngx_http_headers_in_t, pub headers_out: ngx_http_headers_out_t,pub request_body: *mut ngx_http_request_body_t,[…]}正如 Nginx 合作开发指南所提到的,它是两个可以存储与允诺相关联的任何值的地方性,该值应该与允诺一样长。在 OpenResty 中,这主要用于在 Lua 上下文中存储允诺的整个生命周期中的状态。技师们可以为组件做同样的事情,这样当 HTML 导出和重写在积极响应阶段运转时,在允诺阶段初始化的设置就在那
pub fn get_ctx(request: &ngx_http_request_t) -> Option { unsafe {match *request.ctx.add(ngx_http_rofl_module.ctx_index) { p if p.is_null() => None, p => Some(&mut *(p as *mut Ctx)), }}}这是生成 Nginx 组件所需的组件定义的一部分的类型结构。一旦有了这个,就可以将它指向包含想要的任何设置的结构。例如,下面是使用 LuaJIT 的 FFI 工具从 Lua 通过 FFI 到 Rust 组件启用电子邮件混淆机能的实际函数:
#[no_mangle]pub extern “C” fn rofl_module_email_obfuscation_new(request: &mut ngx_http_request_t, dry_run: bool, decode_script_url: *const u8, decode_script_url_len: usize,) {let ctx = context::get_or_init_ctx(request); let decode_script_url = unsafe {std::str::from_utf8(std::slice::from_raw_parts(decode_script_url, decode_script_url_len)).expect(“invalid utf-8 string for decode script”) }; ctx.register_module(EmailObfuscation::new(decode_script_url.to_owned()), dry_run);}如果结构不存在,它也会初始化结构。一旦在允诺过程中设置了所需的数据,就可以检查积极响应中需要运转哪些机能,而无需调用外部数据库,这可能会降低速度。
以此种方式存储状态以及与 Nginx 一起工作的好处之一是,它严重依赖内存池来存储允诺内容。这在很大程度上消除了程序员在使用后必须考虑释放内存的任何需求,内存池在允诺开始时将自动分配,并在允诺完成时自动释放。所需要的就是使用 Nginx 的内置函数来分配内存,将内存分配给内存池,然后注册两个回调,该回调将被调用以释放所有内容。在 Rust 中,它看起来类似于以下内容:
pub struct Pool(&a mut ngx_pool_t);impl Pool { /// Register a cleanup handler that will get called at the end of the request.fn add_cleanup(&mut self, value: *mut T) -> Result { unsafe { let cln = ngx_pool_cleanup_add(self.0, 0); if cln.is_null() {return Err(()); } (*cln).handler = Some(cleanup_handler::); (*cln).data = value as *mut c_void; Ok(()) } }/// Allocate memory for a given value. pub fn alloc(&mut self, value: T) -> Option { unsafe {let p = ngx_palloc(self.0, mem::size_of::()) as *mut _ as *mut T; ptr::write(p, value);if let Err(_) = self.add_cleanup(p) { ptr::drop_in_place(p); return None; }; Some(&mut *p) } }}unsafe extern “C” fn cleanup_handler(data: *mut c_void) { ptr::drop_in_place(data as *mut T);}这应该允许技师们为自己想要的任何东西分配内存,因为 Nginx 会为技师们处理。
遗憾的是,在 Rust 中处理 Nginx 的接口时,必须撰写大量的块。尽管已经做了大量的工作,尽可能地将其最小化,但不幸的是,撰写 Rust 标识符时经常会遇到此种情况,因为它必须通过 FFI 操作 C 结构。计划在未来做更多的工作,并删除尽可能多的行。
遇到的挑战
Nginx 组件系统在组件本身的工作方式方面允许大量的灵活性,这使得它非常适合某一的用例,但此种灵活性也会导致问题。遇到的两个问题是 Rust 和 FL 之间处理积极响应数据的方式。在 Nginx 中,积极响应体被分块,然后这些块被链接到两个列表中。此外,如果积极响应很大,每个积极响应可能有不止两个链接列表。
有效地处理这些块意味着处理它们并尽快传递它们。在撰写用于处理积极响应的 Rust 组件时,很容易在这些链接列表中实现如前所述 Rust 的视图。但是,如果这样做,则必须确保在改变它们的同时更新如前所述 Rust 的视图和底层 Nginx 数据结构,否则这可能会导致严重的错误,导致 Rust 与 Nginx 不同步。这是 ROFL 早期版本的两个小机能,它引起了大家的头痛:
fn handle_chunk(&mut self, chunk: &[u8]) {let mut free_chain = self.chains.free.borrow_mut(); let mut out_chain = self.chains.out.borrow_mut();let mut data = chunk; self.metrics.borrow_mut().bytes_out += data.len() as u64; while !data.is_empty() {let free_link = self .pool.get_free_chain_link(free_chain.head, self.tag, &mut self.metrics.borrow_mut()) .expect(“Could not get a free chain link.”);let mut link_buf = unsafe { TemporaryBuffer::from_ngx_buf(&mut *(*free_link).buf) }; data = link_buf.write_data(data).unwrap_or(b””);out_chain.append(free_link); }}有输出写入缓冲区。在这个方法论中,Nginx 应该负责将缓冲区从自由链中弹出,并将新的块附加到输出链中。然而,如果只考虑 Nginx 处理其链接列表视图的方式,可能不会注意到 Rust 从未更改其指向的缓冲区,导致方法论永远循环且 Nginx 工作进程完全锁定。此类问题需要很长时间才能找到,尤其是在了解它与积极响应体大小有关之前,我们无法在个人计算机上复制它。
分析也很困难,因为一旦注意到这一点,就已经太晚了,进程内存已经增长到服务项目器有崩溃的危险,而且消耗的内存太大,无法写入磁盘。幸运的是,这段标识符从未投入生产。与以往一样,虽然 Rust 的校对器可以帮助发现很多常见错误,但如果数据是通过 FFI 从另两个环境共享的,即使没有太多间接使用,也无济于事,因此在这些情况下必须格外小心,尤其是当 Nginx 允许某种灵活性可能导致整个机器停止运转时。
技师们面临的另两个主要挑战是来自传入积极响应体块的背压。本质上,如果 ROFL 必须向流中注入大量标识符(例如用 JavaScript 代替电子邮件地址)而增加了积极响应的大小,Nginx 可以将 ROFL 的输出提供给其他下游组件更快地推动它的速度,如果未处理来自下一组件的错误,则可能导致数据丢失和 HTTP 积极响应主体被截断。这是另两个问题很难测试的情况,因为大多数时候,积极响应会被快速冲洗,背压不会成为问题。为了处理这个问题,我们必须创建两个特殊的链来存储这些块,这需要两个附加到它的特殊方法。
#[derive(Debug)]pub struct Chains {/// This saves buffers from the `in` chain that were not processed for any reason (most likely/// backpressure for the next nginx module). saved_in: RefCell, pub free: RefCell, pub busy: RefCell, pub out: RefCell, […]}实际上,在短时间内对数据进行“排队”,这样就不会以超出其他组件处理能力的速度向其提供数据,从而压倒其他组件。《 Nginx 合作开发人员指南》中有很多很棒的信息,但其中的很多示例都微不足道,以至于不会出现类似的问题。像这样的事情是如前所述 Nginx 的复杂环境中工作的结果,需要独立发现。
没有 Nginx 的未来
很多人可能会问两个显而易见的问题:为什么我们仍然在使用 Nginx?如前所述,Cloudflare 已经开始很好地代替用于运转 Nginx/OpenResty 全权的组件,或者无需对本土平台进行大量投资的情况下就可以完成的组件。也就是说,一些组件比其他组件更容易代替,而 FL 是 Cloudflare 应用程序服务项目的绝大部分方法论运转的地方性,无疑是更具挑战性的一端。
做这项工作的另两个动机是,无论最终迁移到哪个平台,都需要运转组成 cf-html 的机能,为了做到这一点,希望拥有两个集成度较低且依赖 Nginx 的系统。ROFL 是专门在多个地方性运转它而设计的,因此很容易将它移动到另两个如前所述 Rust 的 Web 全权(或者实际上是我们的 Workers 平台),而不会有太多麻烦。也就是说,很难想象如果没有像 Rust 这样的词汇,会在同两个地方性,它在提供高安全性的同时提供速度,更不用说像 Bindgen 和 Serde 这样的高质量库。更广泛地说,FL 团队已经开始努力将平台的其他方面迁移到 Rust,尽管 cf-html 及其组成部分是我们基础建设中需要工作的关键部分,但还有很多其他方面。
编程词汇的安全性通常被视为有利于防止错误,但作为一家公司,技师们发现它还允许您做一些被认为非常困难或不可能安全完成的事情。无论是提供类似 Wireshark 的过滤词汇来撰写内网准则,还是允许数百万用户撰写任意 JavaScript 标识符并间接在我们的平台上运转,或是动态重写 HTML 积极响应,都有严格的界限允许我们提供我们无法提供的服务项目。尽管安全,但过去困扰行业的内存安全问题正日益成为过去。
Cloudflare 概述了她们如何在 Rust 中重写 Nginx 组件,且技师们也表示非常喜欢 Rust,并在她们的基础建设中使用它,以获得内存安全方面的好处、更多的现代机能和其他优势。
参考链接:
https://blog.cloudflare.com/rust-nginx-module/
https://www.phoronix.com/news/Cloudflare-Rewrite-Nginx-C-Rust