0%

00 篇说,这个系列适合”有 C 基础”的读者。可”C 基础”这个说法太宽了:会写 for 循环、懂指针和结构体,翻开 Suricata 的源码,还是会被一些写法卡住。这篇番外把读 Suricata 时会遇到的、超出入门教材的 C 语言知识挑出来,每一项都配上源码里的真实例子和位置(基于 8.0.7),并说明前面哪几篇用到了它。

先给一个总览。读 Suricata,最常卡住人的是这几类:

  1. **函数指针和”接口表”**:整个系列反复出现的骨架。
  2. 宏:预处理器在 Suricata 里几乎是一门小语言。
  3. 结构体的内存布局:柔性数组、union、位域、packed。
  4. 把字节当结构体看的指针技巧:解码层的日常。
  5. 侵入式数据结构:链表和红黑树的节点长在业务结构体里。
  6. 并发:锁、条件变量、线程局部变量、原子操作。
  7. 编译器提示、错误处理、条件编译:每个文件都有,但很少有人解释。
  8. 和 Rust 打交道:8.0 里越来越多的代码是 Rust 写的。

下面逐个看。

Read more »

这是系列第 10 篇,也是最后一篇。上一篇结尾,一条规则的所有条件都满足了,”产生一条告警”。可告警不是凭空出现在 eve.json 里的:它要先经过一轮收尾,再由日志模块拼成 JSON、写到文件里。00 篇开头那行 http 事件,走的也是这条路。这一篇就把最后这一段走完。

先给答案,输出这一段做了三件事:

  1. 告警先攒起来,再统一收尾。 规则命中时,告警先放进一个队列;整个包检测完,再统一排序、做抑制和阈值检查、执行 drop 这类动作,最后挂到 p->alerts 上。
  2. 日志按类型分发。 包处理的最后一步,OutputLoggerLog() 把包交给几类”根日志器”:包、事务、文件……每一类管着一批日志模块,各模块按自己的条件,或者按事务的进度,决定写不写。
  3. 拼成一行 JSON,写出去。 EVE 的每个日志模块用同一套工具拼 JSON:先拼所有事件共用的头,再加自己的字段,最后写到文件、syslog、unix socket 或 redis。

画出来是这样的:

从规则命中到一行 JSON

Read more »

这是系列第 9 篇。上一篇讲了一条规则怎么从文本变成结构。可真实的规则集动辄几万条,如果每个包都把几万条规则挨个试一遍,再快的机器也跟不上线速。这一篇看检测引擎怎么把”几万条”压到”少数几条”:规则是怎么分组的,多模式匹配(MPM)又是怎么做预过滤的。

先给答案,靠的是两层筛选:

  1. 分组。 启动时按”方向 × 协议 × 服务端端口”把规则分成若干组。一个发往 80 端口的 TCP 包,只需要看”发往服务器、TCP、80 端口”这一组规则,其他组的规则跟它无关。
  2. 预过滤。 每条规则挑出一段最有代表性的内容,叫”快速模式”(fast pattern)。每个组把组里所有规则的快速模式编进一个多模式匹配器,一个包来了,只搜一遍,就知道哪些规则的快速模式出现了。只有这些规则才需要逐条细查。

两层加起来,就是 00 篇第 ⑦ 站说的”先挑出一个规则组(SGH),再用 MPM 一次性搜出所有规则的关键字”:

几万条规则一起跑

Read more »

这是系列第 8 篇。前面几篇把一个包从网卡一路送到了应用层,HTTP 请求已经被解析成事务,URI、Host 这些字段都摆好了。接下来要轮到规则了。但在检查任何一个包之前,规则文件里那一行行文本,得先变成检测引擎能直接执行的结构。这一篇只看这一步:规则是怎么被读进来、拆开、存起来的。

先给答案,规则加载做了三件事:

  1. 规则头填进 Signature 的字段。 动作、协议、地址、端口、方向,每条规则对应一个 Signature 结构。
  2. 每个选项交给对应关键字的 Setup()。 检测引擎本身不认识 content、flow、http.uri 这些关键字,它只按名字去关键字表里查,找到对应的 Setup() 函数,让它自己解析参数、决定往哪里存。
  3. 先用链表攒,再转成检测用的结构。 解析时,条件先挂在一组临时链表上;所有规则读完后,再统一转成紧凑的数组和”检查引擎”,临时数据随即释放。

拿一条规则从头到尾走一遍:

1
alert http any any -> any any (msg:"Example"; flow:established,to_server; http.uri; content:"/admin"; nocase; sid:1000001; rev:1;)

一条规则从文本到结构

Read more »

这是系列第 7 篇。上一篇结尾,Stream 引擎把拼好、已被对方 ACK 的字节流交给了 AppLayerHandleTCPData()。但字节流本身不会说自己是 HTTP 还是 TLS,也不会自己分成”一个请求、一个响应”。这一篇看应用层框架怎么认出协议,怎么找到对应的解析器,以及解析出来的东西长什么样。

先给答案,应用层框架做了三件事:

  1. 认出协议。 三种手段依次尝试:在数据开头找特征串,按端口调用探测函数,查别的连接事先登记的”期望”。端口只是线索之一,不是判断的依据。
  2. 按协议查表,调用解析器。 每个协议在启动时注册一组回调:怎么建状态、两个方向的数据各交给哪个函数解析、怎么取出解析结果。C 写的解析器和 Rust 写的解析器,用的是同一套接口。
  3. 解析的产物是事务。 一问一答算一个事务,带着自己的进度。检测和日志都按事务进行,都处理完了才把它释放。

画出来是这样的:

应用层框架

Read more »

这个系列想把 Transformer 从头讲明白。我们不急着翻开论文,先把它依赖的前置知识一样样补齐:向量、矩阵、导数、softmax、神经网络……等读到《Attention Is All You Need》时,每个公式在算什么,就都能看懂了。

第一课是最基础的一块:向量与点积。Transformer 里几乎所有的计算,最后都落在这两样东西上。

Read more »

这是系列第 6 篇。上一篇讲了 Flow:同一条连接的包已经能归到一起了,Flow 上还挂着一个 protoctx 指针,指向这条连接的 TCP 会话。这一篇就看这个会话:TCP 的段可能乱序到达、可能重传、可能互相重叠,还可能有一部分根本没被抓到,Suricata 怎么把它们拼成一条连续、可信的字节流,交给应用层去解析?

先给答案,Stream 引擎做了三件事:

  1. 用状态机跟踪连接。 握手、挥手、序号、窗口,Suricata 都按 TCP 协议自己跟一遍。每个包先检查”它出现在这条连接里合不合理”,不合理的记事件。
  2. 按序号把数据拼起来。 乱序、重传、重叠的段,按序号放进一棵红黑树,数据拷进一个流式缓冲区,拼成连续的字节流。重叠的部分如果数据不同,还要决定信哪一份。
  3. 只交出对方确认收到的数据。 IDS 模式下,一个方向的数据要等另一方回了 ACK,才交给应用层;对方确认了、Suricata 却没抓到的部分,作为”缺口”通知应用层。

画出来是这样的:

TCP 重组:一个方向上的字节流

Read more »

这是系列第 5 篇。上一篇结尾,解码层把包的五元组填好,用 FlowSetupPacket() 打上 PKT_WANTS_FLOW 并算好了 flow_hash,交给 FlowWorker。到这一步为止,每个包都是孤立的:Suricata 处理完一个包就忘了它。从这一篇开始,引擎要”记住”事情了:同一条连接的包要归到一起,TCP 重组、应用层解析、很多规则,都建立在这份记忆之上。

先给答案,Flow 这一层做了三件事:

  1. 一张所有线程共用的哈希表。 每条连接对应一个 Flow,存在全局的流表里,按哈希桶加锁。哈希的算法让一条连接两个方向的包算出同一个值,落进同一个桶。
  2. 后面所有模块的挂钩。 TCP 会话、应用层解析状态、规则组缓存、流变量,都挂在 Flow 上。有了 Flow,后面的模块才有地方存跨包的状态。
  3. 有借有还。 Flow 从备用池里取,一条连接一段时间没有动静就算过期,写一条 flow 日志、清空内存,再还回备用池。内存不够时,还有一套紧急模式。

一条 Flow 从创建到回收的全过程,画出来是这样的:

一条 Flow 的一生

Read more »

这是系列第 4 篇。上一篇结尾,包已经装进了 Packet,交给了链上的第二道工位 DecodeAFP()。但这时的 Packet 里只有一串从以太网头开始的原始字节,还不知道它是 IPv4 还是 IPv6,是 TCP 还是 UDP,源地址和端口是多少。这一篇看解码层怎么把这串字节一层层剥开,又怎么对付畸形包、隧道和分片。

先给答案,解码层做了三件事:

  1. 一层层认出协议。 每一层都看头部里”下一层是什么”的字段(以太网的 eth_type、IP 的协议号、有时是 UDP 端口),决定下一层交给哪个解码函数。整个解码层就是一串层层嵌套的 switch。
  2. 只记位置,不拷贝。 解码不会把头部拷出来,只是把各层头部在原始数据里的位置记进 Packet,顺便填好地址、端口和载荷的位置。
  3. 坏包留痕,不丢掉。 遇到畸形包不崩溃、也不丢弃,而是记一个解码事件,照样往后送。遇到隧道和分片,则会生出新的 Packet。

整棵分发树画出来是这样的:

解码分发树

Read more »

这是系列第 3 篇。上一篇拆开了线程:workers 线程的链头是 ReceiveAFP,它用 PktAcqLoop 自己在循环里抓包,每抓到一个就交给后面的工位。这一篇钻进这个循环看:网卡上的字节是怎么到 Suricata 手里的?装它们的 Packet 长什么样?每秒几十万、上百万个包,Suricata 靠什么接得住?

先给答案,靠的是三件事:

  1. 和内核共用一块内存。 AF_PACKET 的环形缓冲区同时映射在内核和 Suricata 进程里,包不用从内核拷贝到用户态。
  2. Packet 提前备好。 每个线程启动时就预分配一批 Packet,放在自己的包池里反复复用,处理包的热路径上不做 malloc。
  3. 从环里取包也不拷贝。 Packet 不另存一份数据,只存一个指针,指向环里的那一帧;处理完,再把这一帧还给内核。

这三件事串起来,就是一个 Packet 从借到还的一生:

一个 Packet 的一生

下面先看 Packet 本身,再看包池,最后看 AF_PACKET 怎么和内核交接数据。

Read more »