- 网络安全
【免费下载链接】suricata
Suricata is a network Intrusion Detection System, Intrusion Prevention System and Network Security Monitoring engine developed by the OISF and the Suricata community.
导读
本文是 Suricata 开发者手册 "Working with the Codebase"(代码库协作)章节的系统性解读,覆盖六个核心主题:从 GIT 源码构建最新版、C 代码强制编码规范(clang-format 工作流)、C 与 Rust 两套单元测试体系、模糊测试(fuzz testing)以及测试输入的生成方法。读者将掌握一套从"拉取源码、编译安装"到"按规范提交补丁、编写并运行单元测试"的完整开发闭环,并了解每个环节背后对应的仓库文件与命令行用法,可直接用于 Suricata 的二次开发与贡献。
章节地图:Codebase 开发指南包含什么
doc/userguide/devguide/codebase/index.rst是该指南的索引页,它通过 Sphinx toctree 组织以下六篇文档:
- installation-from-git.rst:在 Ubuntu 上从 GIT 拉取并构建最新代码;
- code-style.rst:Suricata C/Rust 编码规范与 clang-format 使用流程;
- fuzz-testing.rst:启用并运行模糊测试目标;
- testing.rst:测试总览(单元测试、Suricata-Verify、静态/动态分析、CI)与测试输入生成;
- unittests-c.rst:C 单元测试的编写与运行;
- unittests-rust.rst:Rust 单元测试的编写与运行。
这六篇文档共同定义了 Suricata 社区"如何开发"的基线:代码必须符合严格风格、每个改动应配有对应语言的单元测试、复杂的协议行为用 Suricata-Verify 验证、持续集成与 OSS-Fuzz 持续兜底。
从 GIT 安装最新代码(Ubuntu 22.04)
installation-from-git.rst以 Ubuntu 22.04 为基准环境(文档注明"这些指令已在 Ubuntu 22.04 上测试通过")说明了完整流程;其他操作系统流程基本相同,只需把sudo、apt-get替换为对应发行版的命令。
预安装依赖
构建前先安装编译工具链、库与 Rust 工具链:
sudo apt-get -y install libpcre2-dev build-essential autoconf \ automake libtool libpcap-dev libnet1-dev libyaml-0-2 libyaml-dev \ pkg-config zlib1g zlib1g-dev libcap-ng-dev libcap-ng0 make \ libmagic-dev libjansson-dev rustc cargo jq git-core然后将 Cargo 的二进制目录加入 PATH 并安装 cbindgen(Rust FFI 绑定生成器,Suricata 构建脚本依赖它生成src/flow-bindgen.h、src/output-eve-bindgen.h等绑定头文件):
export PATH=$PATH:${HOME}/.cargo/bin cargo install --force cbindgen首次安装 cbindgen 可能需要较长时间。若需要以 IPS(入侵防御)模式运行,额外安装 netfilter 队列库:
sudo apt-get -y install libnetfilter-queue-dev libnetfilter-queue1 \ libnfnetlink-dev libnfnetlink0克隆仓库并生成构建系统
mkdir suricata # 目录名可自取,如 oisf cd suricata git clone https://github.com/OISF/suricata.git cd suricata注意:Suricata-update 并不随主仓库捆绑,需要单独获取:
./scripts/bundle.sh接着运行 autogen.sh(用 autoconf/automake/libtool 生成 configure 脚本),再依次 configure、编译、安装:
./autogen.sh ./configure make sudo make install sudo ldconfig一键自动配置:install-conf / install-rules / install-full
文档提供了三种 auto-setup 组合,免去手动建目录、写suricata.yaml、下载规则的繁琐步骤:
./configure && make && sudo make install-confmake install-conf会执行常规make install,然后自动创建运行所需的全部目录并生成suricata.yaml。
./configure && make && make install-rulesmake install-rules会执行常规安装并自动下载、配置来自 Emerging Threats(ET)的最新规则集。
./configure && make && make install-fullmake install-full是前两者的合体:安装 + 配置 + 规则一次完成,交付一个"开箱即跑"的 Suricata。安装完成后,请继续参考 doc/userguide 下的 Basic Setup 文档完成运行配置。
更新本地代码库
若已克隆过仓库,拉取最新代码后必须重新运行 autogen:
cd suricata/suricata git pull ./autogen.sh重新生成 configure 是为了把新增的 m4 宏、Makefile 规则等同步进构建系统。
C 编码规范:clang-format 驱动的严格风格
Suricata 采用相当严格的 C 编码风格(code-style.rst),并以仓库根目录的 .clang-format 配置(要求 clang 9 及以上;当前 CI 使用 clang-format-14 校验格式)强制落地。仓库同时提供了封装脚本 scripts/clang-format.sh,屏蔽不同版本 clang-format 的差异。
clang-format 工作流:格式化你的改动
打开 PR 前应先格式化自己的改动。git-clang-format只格式化你改动的代码,而非整个文件。
只格式化最近一次提交:
$ git clang-format HEAD^ # 或用封装脚本: $ scripts/clang-format.sh commit如果改动是琐碎的格式化修正,直接并入上一个提交:
$ git commit --amend -a较大的格式化调整应单独成 commit,不要与逻辑改动混在一起。
格式化已暂存(git add过)的代码:
$ git clang-format # 或用脚本: $ scripts/clang-format.sh cached连未暂存的改动一起处理:
$ git clang-format --force # 或用脚本: $ scripts/clang-format.sh cached --force批量修复分支上所有 commit 的格式(会按原有 commit 元数据重写历史,建议先复制分支再操作):
$ scripts/clang-format.sh rewrite-branch只格式化分支上各 commit 的改动(注意用first_commit_on_your_branch^而非main,避免把 main 上新提交也卷进来):
$ git clang-format first_commit_on_your_branch^ # 或用脚本: $ scripts/clang-format.sh branch检查分支改动格式是否合规:
$ scripts/clang-format.sh check-branch可加--diffstat查看需要格式化的文件列表,或加--diff查看格式化差异。
注意:不要默认对整个文件跑 clang-format。若确有必要(例如历史遗留的非规范代码),clang-format -i {file}产生的纯格式改动必须单独成 commit,严禁与功能改动混在一起。
某些场景(宏、多维数组、结构体初始化、手工精心排版处)可以局部关闭 clang-format:
/* clang-format off */ #define APP_LAYER_INCOMPLETE(c, n) (AppLayerResult){1, (c), (n)} /* clang-format on */clang-format 与 git-clang-format 的安装:Ubuntu 24.04 只需sudo apt-get install clang-format-14;Fedora 执行sudo dnf install clang git-clang-format。
格式化与排版规则
- 行宽:限制 100 字符。换行时从上一行缩进至少 8 个空格,并尽量只换行最少的内容(对应 clang-format:
ColumnLimit: 100、ContinuationIndentWidth: 8、ReflowComments: true)。 - 缩进:统一 4 空格。函数参数、循环、if 语句换行用 8 空格;变量定义换行用 4 空格(对应
IndentWidth: 4、UseTab: Never、AlignAfterOpenBracket: DontAlign)。 - 花括号:函数左花括号另起新行;控制/循环语句左花括号留在同一行;
else采用 "cuddled" 风格与右花括号同行;struct/union/enum 左花括号在同一行:
int SomeFunction(void) { DoSomething(); } if (unlikely(len < ETHERNET_HEADER_LEN)) { ENGINE_SET_INVALID_EVENT(p, ETHERNET_PKT_TOO_SMALL); return TM_ECODE_FAILED; } if (this) { DoThis(); } else { DoThat(); } struct { uint8_t type; uint8_t code; } icmp_s;- 控制流:禁止条件与语句写在同一行(
if (a) b = a;是反例);短函数、空函数、短 struct 不得压缩成一行;避免无谓分支,例如if (error) { goto error; } else { a = b; }应简写为if (error) { goto error; } a = b;。 - 指针对齐:指针符号右对齐(
void *ptr;、void f(int *a, const char *b);,对应PointerAlignment: Right)。 - 注释对齐:连续行的行尾注释应对齐(对应
AlignTrailingComments: true)。 - 宏:宏名
ALL_CAPS_WITH_UNDERSCORES,宏体内每次使用参数都要加括号,连续行的宏值对齐,多行宏的续行符(\)右对齐到列宽上限:
#define ACTION_ALERT 0x01 #define ACTION_DROP 0x02 #define ACTION_REJECT 0x04 #define MULTILINE_DEF(a, b) \ if ((a) > 2) { \ auto temp = (b) / 2; \ (b) += 10; \ someFunctionCall((a), (b)); \ }命名、注释与文件组织
- 函数命名:
SCNamedLikeThis(),所有非 static 函数必须以SC前缀开头;能声明为 static 的函数尽量 static;inline 仅用于关键路径(热路径)性能优化。 - 变量命名:全小写下划线(
named_like_this),如SCConfNode *parent_node = root;;循环变量i应为有符号 int。 - 宏与枚举:枚举值
ALL_CAPS_WITH_UNDERSCORES且使用公共前缀,每个值独占一行(给最后一项加尾逗号可强制 clang-format 保持"每行一个值");暴露在头文件中的枚举以SC_为前缀:
// 正确写法 enum { VALUE_ONE, VALUE_TWO, // <- 尾逗号强制每行一个 };- 结构体与 typedef:使用
TitleCase命名;暴露在头文件中时加SC前缀(如typedef struct SCPlugin_ { ... } SCPlugin;)。 - 函数注释:使用 Doxygen 记号,
\brief、\param、\retval标注齐全:
/** * \brief Helper function to get a node, creating it if it does not * exist. * * \param name The name of the configuration node to get. * \param final Flag to set created nodes as final or not. * * \retval The existing configuration node if it exists, or a newly * created node for the provided name. On error, NULL will be returned. */ static SCConfNode *SCConfGetNodeOrCreate(char *name, int final)- 普通注释:优先
/* foobar */风格,尽量避免//。 - 文件名:全小写,
.c/.h/.rs后缀,通常带子系统前缀(如detect-dsize.c、util-ip.c),多层前缀如util-mpm-ac.c。 - switch 语句:
case相对switch缩进;贯穿(fall through)的 case 用/* fall through */注释说明;case 标签不与语句同行;case 后如需声明变量,左花括号与 case 同行:
switch (ntohs(p->ethh->eth_type)) { case ETHERNET_TYPE_IP: DecodeIPV4(tv, dtv, p, pkt + ETHERNET_HEADER_LEN, len - ETHERNET_HEADER_LEN, pq); break; case 13: { int a = bla(); break; } }- goto:仅用于错误处理等场景,标签与花括号同级缩进:
static DetectFileextData *DetectFileextParse (char *str) { DetectFileextData *fileext = NULL; fileext = SCMalloc(sizeof(DetectFileextData)); if (unlikely(fileext == NULL)) goto error; memset(fileext, 0x00, sizeof(DetectFileextData)); if (DetectContentDataParse("fileext", str, &fileext->ext, &fileext->len, &fileext->flags) == -1) { goto error; } return fileext; error: if (fileext != NULL) DetectFileextFree(fileext); return NULL; }- includes:
.c文件应先包含自身同名头文件,或紧跟suricata-common.h之后包含。 - 单元测试数据注释:测试中若使用包含协议报文的字节数组,务必添加可读内容注释,例如
/* 220 mx.google.com ESMTP d15sm986283wfl.6<CR><LF> */,而不是只给一行十六进制。
禁用函数(Banned Functions)
为保证可移植性与安全性,以下函数被禁止使用并给出替代:
| 被禁函数 | 替代 | 原因 |
|---|---|---|
| strtok | strtok_r | 线程安全 |
| sprintf | snprintf | 不安全 |
| strcat | strlcat | 不安全 |
| strcpy | strlcpy | 不安全 |
| strncpy | strlcat | — |
| strncat | strlcpy | — |
| strndup | — | 操作系统相关 |
| strchrnul | — | — |
| rand / rand_r | — | — |
| index / rindex | — | — |
| bzero | memset | — |
编写新代码时还应对照既有实现,例如 src/decode-ethernet.c,如果风格"差别悬殊",多半是写错了。
Rust 代码风格
纯 Rust 代码遵循常规 Rust 风格(rustfmt/cargo fmt格式化;若重排既有文件,先单独提交格式化再改逻辑,此类改动在 PR 中可能被拒)。而暴露给 C 的 FFI 代码(所有#[no_mangle]函数)必须遵循 C 侧命名规范:
#[no_mangle] pub extern "C" SCJbNewArray() -> *mut JsonBuilder { }单元测试总览与测试输入生成
testing.rst将 Suricata 的测试手段划分为五个层次:
- 单元测试:独立验证某个函数或代码片段,C 与 Rust 各有独立的编写/运行方式(见下两节);
- Suricata-Verify(独立测试项目):验证更复杂的行为,例如给定输入(通常是多个报文组成的 pcap)时的日志输出或告警计数,适合验证协议日志、特征检测在重构后是否回归;
- 静态与动态分析工具:如 clang 的 scan-build(同时用于格式检查)、ASAN(内存问题检测);
- 模糊测试:善于暴露既有且往往不平凡(non-trivial)的 bug,详见 fuzz-testing 一节;
- CI 检查:每个提交到公共仓库的 PR 都会运行一系列 CI 工作流,覆盖格式与提交检查、模糊测试以及多种构建配置。
运行全部单元测试(C + Rust)只需在主目录执行:
make check单元测试代码示例
Rust 侧,以 DNS 解析器(rust/src/dns/parser.rs 中的dns_parse_name)为例:构造原始字节输入(注意注释标明每个字节段的含义),断言解析出的名字与未解析的剩余部分:
/// Parse a simple name with no pointers. #[test] fn test_dns_parse_name() { let buf: &[u8] = &[ 0x09, 0x63, /* .......c */ 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2d, 0x63, 0x66, /* lient-cf */ 0x07, 0x64, 0x72, 0x6f, 0x70, 0x62, 0x6f, 0x78, /* .dropbox */ 0x03, 0x63, 0x6f, 0x6d, 0x00, 0x00, 0x01, 0x00, /* .com.... */ ]; let expected_remainder: &[u8] = &[0x00, 0x01, 0x00]; let (remainder,name) = dns_parse_name(buf, buf).unwrap(); assert_eq!("client-cf.dropbox.com".as_bytes(), &name[..]); assert_eq!(remainder, expected_remainder); }C 侧,以 src/decode-ethernet.c 中的 DCE 以太网帧过小测试为例,用FAIL_IF_*断言引擎正确设置了解码事件:
/** * Test a DCE ethernet frame that is too small. */ static int DecodeEthernetTestDceTooSmall(void) { uint8_t raw_eth[] = { 0x00, 0x10, 0x94, 0x55, 0x00, 0x01, 0x00, 0x10, 0x94, 0x56, 0x00, 0x01, 0x89, 0x03, }; Packet *p = PacketGetFromAlloc(); FAIL_IF_NULL(p); ThreadVars tv; DecodeThreadVars dtv; memset(&dtv, 0, sizeof(DecodeThreadVars)); memset(&tv, 0, sizeof(ThreadVars)); DecodeEthernet(&tv, &dtv, p, raw_eth, sizeof(raw_eth)); FAIL_IF_NOT(ENGINE_ISSET_EVENT(p, DCE_PKT_TOO_SMALL)); PacketFree(p); PASS; }Suricata-Verify:验证端到端行为
单元测试难以覆盖"完整会话"级别的行为,Suricata-Verify 正是为此而生:无需模拟网络流量和引擎内部机制,直接以期望的 pcap 输入、配置和检查项运行 Suricata 即可。它特别适合保证代码重构不影响协议日志或特征检测——这类回归对用户与集成方影响巨大。简单测试只需提供 pcap;复杂场景还可附上规则,让 Suricata-Verify 匹配告警与特定事件。其测试仓库中的 app-layer-template 等样例是绝佳的起步参照。
生成测试输入
方法一:用真实流量 + Wireshark 提取
用 Wireshark 打开目标协议的抓包,选中作为测试输入的报文,使用Follow [TCP/UDP/HTTP/HTTP2/QUIC] Stream(或顶部菜单Analyze -> Follow -> TCP Stream)打开流视图,选择Show and save data as中的C Arrays,并可选择查看整个会话或仅 client / server 方向的报文。Wireshark 会以 C 数组风格的十六进制呈现报文数据,该格式同样易于适配 Rust 测试:
Wireshark 也常用来抓取样例流量并生成 pcap 文件。
方法二:用 Scapy 构造流量
Scapy 适合按需构造特定流量。Suricata-Verify 测试集中有大量由 Scapy 生成的 pcap 样例,例如 dcerpc-udp-scapy 测试中的dcerpc_udp_scapy.py脚本。此外其测试 readme 中还收录了 http2-range、http-range、smb2-delete、smtp-rset、http-auth-unrecognized 等一批带生成说明的样例。
方法三:公开数据集
若无法抓取或构造所需协议流量,可尝试在公开数据集中寻找,Suricata 官方论坛有"分享优质抓包来源"的讨论帖可供参考。
C 单元测试:编写、注册与运行
单元测试是检查解析器、结构体等内部状态的最佳手段(unittests-c.rst)。测试应满足:使用FAIL/PASS宏、确定性(deterministic)、PASS时不泄漏内存、不使用条件语句。
启用与运行
单元测试默认不随 Suricata 编译,需在 configure 阶段显式开启:
./configure --enable-unittests按模块运行(例如只跑 flowbits 相关测试):
suricata -u -U flowbit排查失败测试时可用调试构建辅助:
./configure --enable-debug SC_LOG_LEVEL=Debug suricata -uDebug 级别输出非常冗长,可用 grep 风格的SC_LOG_OP_FILTER过滤:
SC_LOG_LEVEL=Debug SC_LOG_OP_FILTER="(something|somethingelse)" suricata -u注意日志级别优先级:例如选了 Info 级别,就不会显示其他级别的消息。
编写 C 单元测试
C 单元测试是一个无参数、返回 0(失败)或 1(成功)的函数;实践中不必显式 return,而是使用FAIL_*与PASS宏:
void MyUnitTest(void) { int n = 1; void *p = NULL; FAIL_IF(n != 1); FAIL_IF_NOT(n == 1); FAIL_IF_NOT_NULL(p); FAIL_IF_NULL(p); PASS; }每个测试必须通过UtRegisterTest()注册,第一个参数是测试名,第二个是函数指针:
UtRegisterTest("MyUnitTest", MyUnitTest);已有模块通常自带注册函数,新模块可参考结构相近的既有模块来组织注册。
文档给出的两个实战范例:一是 src/conf-yaml-loader.c 中的ConfYamlOverrideTest,验证 YAML 配置中后出现的键覆盖前值(some-log-dir最终为/tmp),以及父节点被后续定义整体替换后parent.child0不再存在而parent.child1.key存在;二是detect-ike-chosen-sa.c中的解析测试,展示了#ifdef UNITTESTS包裹测试、DetectIkeChosenSaFree释放资源,以及集中注册的模式:
#ifdef UNITTESTS static int IKEChosenSaParserTest(void) { DetectIkeChosenSaData *de = NULL; de = DetectIkeChosenSaParse("alg_hash=2"); FAIL_IF_NULL(de); FAIL_IF(de->sa_value != 2); FAIL_IF(strcmp(de->sa_type, "alg_hash") != 0); DetectIkeChosenSaFree(NULL, de); PASS; } #endif /* UNITTESTS */ void IKEChosenSaRegisterTests(void) { #ifdef UNITTESTS UtRegisterTest("IKEChosenSaParserTest", IKEChosenSaParserTest); #endif /* UNITTESTS */ }Rust 单元测试:cargo test 与 tests 模块
Rust 侧测试走 Cargo 内置体系(unittests-rust.rst),基本命令为:
cargo test [options][testname][-- test-options]测试某个 Rust 模块(例如 http2),进入rust目录执行:
cargo test http2运行 Rust 代码库的全部单元测试:
cargo test在源码文件中添加测试
单元测试应放在被测代码同一文件末尾的mod tests中(没有就先建一个),测试函数加#[test]属性,并use被测模块及其他依赖模块。来自nfs > rpc_records.rs的范例:
mod tests { use crate::nfs::rpc_records::*; use nom::Err::Incomplete; use nom::Needed::Size; #[test] fn test_partial_input_ok() { let buf: &[u8] = &[ 0x80, 0x00, 0x00, 0x9c, // flags 0x8e, 0x28, 0x02, 0x7e, // xid 0x00, 0x00, 0x00, 0x01, // msgtype 0x00, 0x00, 0x00, 0x02, // rpcver 0x00, 0x00, 0x00, 0x03, // program 0x00, 0x00, 0x00, 0x04, // progver 0x00, 0x00, 0x00, 0x05, // procedure ]; let expected = RpcRequestPacketPartial { hdr: RpcPacketHeader { frag_is_last: true, frag_len: 156, xid: 2384986750, msgtype: 1 }, rpcver: 2, program: 3, progver: 4, procedure: 5 }; let r = parse_rpc_request_partial(buf); match r { Ok((rem, hdr)) => { assert_eq!(rem.len(), 0); assert_eq!(hdr, expected); }, _ => { panic!("failed {:?}",r); } } } }运行指定测试
单个测试按模块路径精确定位:
cargo test module::file_name::tests::test_name其中tests即mod tests。若测试名全局唯一,可直接:
cargo test test_name同样可只测某个模块或子模块:
cargo test nfs::rpc_records模糊测试:启用 fuzz targets
fuzz-testing.rst篇幅精炼但要点明确:在 configure 时加入--enable-fuzztargets即可编译 fuzz 目标。
./configure --enable-fuzztargets重要警告:启用该选项会改变 Suricata 的多个组成部分,使suricata二进制不再适合生产环境使用——fuzz 目标本质上是为暴露崩溃与畸形输入而生的测试替身。该配置项在 configure.ac 中定义(AC_ARG_ENABLE(fuzztargets, ...),并据此设置BUILD_FUZZTARGETS条件编译),configure 输出中也会打印Fuzz targets enabled:状态。
编译出的目标可用于 libFuzzer、AFL 及其他模糊测试平台。Suricata 还通过 OSS-Fuzz 项目接受持续的云端模糊测试。仓库中 qa/run-ossfuzz-corpus.sh 提供了运行 OSS-Fuzz 语料库的脚本,可作为本地复现与扩展覆盖的起点。文档中的"运行模糊器""复现问题""扩展覆盖""新增 fuzz 目标"等小节当前标注为 TODO,具体细节可查阅src/tests/fuzz目录下的 README。
结语:把六份文档串成开发闭环
从git clone与./autogen.sh的构建起步,到.clang-format与scripts/clang-format.sh强制风格一致,再到 C 侧UtRegisterTest、Rust 侧mod tests双轨单元测试,以及--enable-fuzztargets+ OSS-Fuzz 的持续兜底——doc/userguide/devguide/codebase章节实际给出了 Suricata 社区贡献代码的完整质量基线。无论你是想修一个解码器 bug、给某个协议解析器补测试,还是为引擎增加新配置项,都可以按本文顺序:先构建出可运行环境,再对照编码规范与既有实现(如 src/decode-ethernet.c、rust/src/dns/parser.rs)动手,最后用make check与定向的suricata -u/cargo test验证改动。
- 网络安全
【免费下载链接】suricata
Suricata is a network Intrusion Detection System, Intrusion Prevention System and Network Security Monitoring engine developed by the OISF and the Suricata community.
相关推荐
CVXPY 贡献指南:从源码构建、代码规范、单元测试到基准测试的完整开发流程
CVXPY 贡献指南:从源码构建、代码规范、单元测试到基准测试的完整开发流程 本文是 CVXPY(Python 凸优化建模语言)开发者的实战贡献指南,完整梳理了
科学计算INAV 开发者贡献指南:编码规范、单元测试、Git 分支工作流与发布流程全解析
INAV 开发者贡献指南:编码规范、单元测试、Git 分支工作流与发布流程全解析 本篇技术指南以 INAV 官方开发者文档( docs/development/
无人机嵌入式智能硬件Docker Compose 源码构建指南:CLI 编译、单元测试、E2E 测试与发布全流程
Docker Compose 源码构建指南:CLI 编译、单元测试、E2E 测试与发布全流程 本指南以仓库根目录 BUILDING.md https://lin
云原生容器编排DevOpsCLI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考