scadastrangelove/russcan-lite

GitHub: scadastrangelove/russcan-lite

Vectorscan/Hyperscan 字面量匹配引擎的 Rust 安全重写,专为 WAF/IDS 数据平面消除 C 依赖。

Stars: 1 | Forks: 0

# russcan-lite *一个研究型正则表达式引擎的 C++ 移植版的 Rust 移植版,精简为防火墙在每个请求中实际运行的部分。移植无止境。* **russcan-lite 是 russcan 移植版中可部署的字面量匹配引擎** —— 这是一个从零开始的 Rust 重新实现,用于 Vectorscan/Hyperscan 运行时,专为**安全匹配**优化:WAF 或 IDS 的关键热路径。Block 模式,floating literals,FDR + Teddy + confirm + Rose literal 解释器,并且**数据平面中完全没有 C。** ## 为什么移植一个完美的 C++ 引擎? 因为它是一个完美的 C++ 引擎。 WAF 的存在是为了防止恶意输入到达内存不安全的解析器。因此,如果内存不安全的解析器在传统上就是 WAF,那就有些尴尬了:一个 C/C++ 模式匹配器,位于热路径中,处理每个请求的每一个字节,决定*下一个*内容是否被允许存在危险。 反对的理由不是速度。C/C++ 并不慢,而且——参见底部的表格——我们也不慢;在现实的流量中,内存安全的引擎反而是更快的那个。反对的理由是在“快速且*可能*正确”的基础上运行安全控制。russcan-lite 将扫描器移至一种语言中,在这种语言中,我们最担心的那类 bug 通常会作为编译错误而不是 CVE 出现。 ## 范围 —— 防火墙实际使用的引擎 我们没有移植所有的 Hyperscan。我们对真实的规则集实际编译成的内容进行了普查 —— **OWASP CRS v4** (`@rx`)、**Suricata ET Open 7.0.3**(约 7k 个 PCRE 以及约 50k 个 `content` 字面量)以及一个生产环境的 WAAP 特征库 —— 并发布了承载*典型*特征的引擎,而不是那些奇异的边缘情况。事实证明,生产环境特征库几乎编译成了纯字面量的 Rose 程序(`CHECK_MED_LIT_NOCASE` + `REPORT` + `END`/`FINAL_REPORT`/`DEDUPE` 占了约 99% 的指令)。因此,这就是这个代码库的内容。 **包含 (IN) —— 字面量快速路径(block 模式,floating):** - `russcan-simd` —— `V128` SIMD 抽象(SSSE3 / NEON / 标量后端)。 - `russcan-bytecode` —— 序列化数据库读取器 + `RoseEngine` 访问器(经 CRC 校验)。 - `russcan-hwlm` —— FDR + Teddy + noodle 多字面量匹配器 + confirm 路径(带有解析时的 confirm 区域验证 —— 参见 *安全*)。 - `russcan-rose` —— 纯**字面量** `roseRunProgram_l` 解释器(`CHECK_BYTE` / `CHECK_MED_LIT` / `REPORT` / `DEDUPE` / `INCLUDED_JUMP` / `PUSH_DELAYED`),带有可失败的 operand 读取和指令预算。 - `russcan` —— `Database::load` + `scan_block` 外观(FDR/Teddy 调度,延迟字面量重放)。 **排除 (OUT) —— 仅存在于完整的 russcan 移植版中:** - **Ф3 regex/NFA 轨道** —— LimEx / McClellan / Sheng / Castle / LBR。它们是真实的,但这是重型 CRS `@rx` 模式所需要的,而不是这个引擎所面向的以字面量为主导的快速路径。在这个代码库中,`russcan-nfa` 是一个约 40 行的**存根**:字面量解释器逐字节地编译且保持不变,而非字面量数据库会被干净的 `Unsupported` 错误拒绝,而不是拖入我们不提供的引擎。 - **Gough, Tamarama, smallwrite** —— 在 CRS + Suricata + 生产特征库中一次都没有被触发过,因此从未被移植(并在读取器中通过断言排除)。 - 流式处理、anchored 匹配器、`libhs` FFI diff-oracle、`tools/`、`census/`、`fuzz/` —— 开发和研究脚手架。 完整的移植版是事实来源;russcan-lite 是一个精心挑选、依赖精简的子集。更改通过复制从完整版流向精简版,这正是为什么 NFA 存根保持 `russcan-rose` 在文本上完全一致而不是分叉的原因。 ## 使用它 —— 嵌入引擎,为其提供数据库 russcan-lite **不**编译模式。它*加载*序列化的数据库并进行扫描。分为两步,而这种分离正是全部的意义所在:编译是 C 语言,离线的,在构建时进行;扫描是 Rust,在每个请求中运行。 **1 —— 离线编译一次你的模式。** 这是唯一运行 C 的地方,它在构建时运行,而不是在流量上运行。使用锁定的上游 `vectorscan`/Hyperscan(`a1c107e`, 5.4.12)—— 与 diff-oracle 使用的相同的 `hs_compile_multi` + `hs_serialize_database`,因此字节数据逐字节加载: ``` // build-db.c — link against the pinned vectorscan. Run once; ship the output. #include const char *pats[] = { "union select", "/etc/passwd", "foo|bar|baz" }; unsigned ids[] = { 101, 102, 103 }; unsigned flg[] = { 0, 0, 0 }; hs_database_t *db; hs_compile_error_t *e; hs_compile_multi(pats, flg, ids, 3, HS_MODE_BLOCK, NULL, &db, &e); char *bytes; size_t len; hs_serialize_database(db, &bytes, &len); // → write `bytes` to patterns.db ``` **2 —— 嵌入引擎。** 添加外观 crate 并加载序列化字节: ``` [dependencies] russcan = { git = "https://github.com/scadastrangelove/russcan-lite" } russcan-hwlm = { git = "https://github.com/scadastrangelove/russcan-lite" } ``` ``` use russcan::Database; use russcan_hwlm::ScanCtl; let blob = std::fs::read("patterns.db")?; // the serialized DB from step 1 let db = Database::load(&blob)?; // parse + validate (CRC + bounds) db.scan_block(request_body, &mut |id: u32, to: u64| { // id = your pattern id (101 / 102 / …); to = end offset (last byte + 1) println!("rule {id} matched, ending at byte {to}"); ScanCtl::Continue // return Terminate to stop early })?; ``` 这就是整个集成界面:`Database::load` + `scan_block` 搭配一个闭包。没有全局状态,没有 C 运行时,扫描路径上没有内存分配。 ### “……并为我提供自己的正则表达式?” 是的 —— 只有一个诚实的限制。你的模式通过*完整的* Hyperscan 编译器,因此 regex **语法**是被接受的;但 russcan-lite 只*运行*那些能归约为**字面量**的模式。当 regex 实际上并不是真正的 regex 时,它是可以使用的。 | 模式 | 编译为 | russcan-lite | |---|---|---| | `union select`, `/etc/passwd`, `content:` 字符串 | 字面量 (FDR/Teddy) | ✅ 运行 | | `foo\|bar\|baz`,固定交替 | 多字面量 | ✅ 运行 | | `a.*b`, `\d{3,}`,字符类,反向引用 | NFA (LimEx/McClellan/Sheng) | ❌ 拒绝 | 非字面量数据库会被**拒绝并抛出一个干净的 `Err`,绝不进行错误扫描** —— NFA 存根返回 `Unsupported` 而不是进行猜测。如果你的规则集以字面量为主(大多数 WAF/IDS 的 `content` 特征都是如此),那就是全部工作了。如果你需要在引擎内部进行真正的自动机评估,那需要完整的 russcan 移植版,而不是这个。 ## 正确性 逐字节一致,否则不计入。 - `cargo build`(开发 + 发布)—— 干净,无警告,稳定的 Rust 1.86,零 C 依赖;`cargo test` 全绿。 - `scan_db` diff-harness 在所有 8 个测试夹具(`basic`, `fdr400`, `fdrlit`, `realpack`, `t3_len7`, `t4_long`, `u1_len1`, `u2_len12`)上**逐字节复现**了 C oracle 的黄金输出 —— 这是验收关口。 ``` cargo build --release cargo test target/release/scan_db # == the C oracle, exactly ``` ## 安全性 —— 重写的引力 用 Rust 重写安全扫描器并不能消除它的漏洞。它将一个*已知*的 bug 类别——他们的,已经被记录了二十年——换成了一个*未知*的类别:我们的,上周才写的。每个新的代码库都有自己的引力,每次重写都是另一个黑洞,有它自己充满全新 CVE 的视界——这是一个《新希望》(New Hop),而你曾以为会被承诺一个《新希望》(A New Hope)。 因此,russcan-lite 由其兄弟项目 **[rust-in-peace](https://github.com/scadastrangelove/rust-in-peace)** 进行审计——这是 Anthropic 防御性代码参考工具(Miri UB / sanitizer / panic / 挂起检测器)的一个 Rust 安全分支。字面量引擎经历了它的完整周期:内存安全集群已关闭,并且**通过 CRC 校验但充满恶意的数据库会在解析时被拒绝**,而不是在热路径中被越界读取(`crates/russcan/tests/hostile_db.rs` 锁定了这一点)。完整性验证不是边界验证——签名证明了字节是完整的,但绝不代表它们内部的偏移量指向它们所声称的位置。保护你的解析器的扫描器,自身也需要一个保护者。 ## 性能 特定于检测的工作负载——真实的 WAF body 扫描,而不是微基准测试。比率是**字面量引擎 ÷ 发布版 Vectorscan**(数值越高 = Rust 引擎越快),在共享机器上中位数为 3 倍。 | body | 比率 | 测试压力 | |---|---:|---| | clean (FP 饱和) | 0.88× | confirm 密集型最坏情况 —— 大量 FDR 候选,零字面量匹配 | | random | 1.01× | 扫描密集型,持平 | | body256k (真实环境) | ~1.10× | 真实的 WAF 请求 body | | dense (匹配饱和) | ~1.09× | 对抗性,匹配密集 | 它唯一输掉的工作负载是 `clean`——一个被设计为必输的人造最坏情况(全是预过滤,没有匹配;对抗基于 LLVM 构建的 C 时差距小于 2%)。在两个类似于生产环境的 body 上,内存安全的引擎是更快的那个。这让人们感到惊讶。在经历了大约第四个优化阶段后,移植的作者们不再感到惊讶——从 0.53× 到持平并获胜的完整历程记录在完整移植版的 `PERF_METHODOLOGY.md` 中。 ## 联系方式 **Sergey Gordeychik** - 邮箱: [scadastrangelove@gmail.com](mailto:scadastrangelove@gmail.com) - X/Twitter: [@scadasl](https://x.com/scadasl) - 博客: [scadastrangelove.blogspot.com](https://scadastrangelove.blogspot.com/) ## 许可证 / 来源 衍生自 russcan 移植版,该版本移植了上游的 [`vectorscan`](https://github.com/VectorCamp/vectorscan)(锁定 `a1c107e`, 5.4.12; Apache-2.0 / BSD-3-Clause)。有关锁定的上游来源,请参见完整的移植版。
标签:AppImage, Rust, Web应用防火墙, 入侵检测系统, 内存安全, 可视化界面, 安全数据湖, 模式匹配, 正则表达式引擎, 网络流量审计, 自动化资产收集, 通知系统