Suricata源码阅读笔记:数据包源

简介

Suricata支持多种数据包源:pcap(实时/文件)、nfq、ipfw、mpipe、af-packet、pfring、dag(实时/文件)、napatech。

每种数据包源的支持都对应于一个线程模块(Thread Module),得益于这种其模块化的架构,增加一个新的数据源支持只需要添加一个新的线程模块即可。

这里,我将主要记录最常见的pcap实时数据源的实现细节,包括相关数据结构、运行流程,以及与主框架和其他模块的交互等。

模块注册

TmModuleReceivePcapRegister函数用于实现pcap实时数据源的线程模块的注册,该函数在系统初始化阶段由RegisterAllModules函数所调用。函数内部唯一的工作就是填充TmModule类型的结构体变量:tmm_modules[TMM_RECEIVEPCAP]。下面是各字段的填充内容:

字段 填充值 含义及用处
name "ReceivePcap" 线程名字:目前没有看到代码中有对这个变量的使用。
ThreadInit ReceivePcapThreadInit 初始化函数:在_TmSlotSetFuncAppend中被传递给其所嵌入的slot的SlotThreadInit函数,而该函数将在线程执行函数(如TmThreadsSlotVar)中被调用。
Func NULL 模块执行函数:对于数据源模块,其执行函数为下面的PktAcqLoop。
PktAcqLoop ReceivePcapLoop 数据包获取函数:在TmThreadsSlotPktAcqLoop中被调用。
ThreadExitPrintStats ReceivePcapThreadExitStats 退出打印函数:用于打印模块统计信息,同样被赋给slot对应函数,然后在线程执行函数的退出阶段被调用。
ThreadDeinit NULL 清理函数:这里设成NULL可能是个BUG,因为存在一个正好用于这个目的却没有被引用过的函数:ReceivePcapThreadDeinit,其中调用pcap_close进行了清理。
RegisterTests NULL 注册测试函数:用来注册模块内部所编写的单元测试,在单元测试模式下,运行所有测试前将调用TmModuleRegisterTests函数先注册所有线程模块的单元测试函数。
cap_flags SC_CAP_NET_RAW 能力标志(capability flags):标志这个线程模块所需要的能力,以确定能让整个系统正常运行所需要的最小权限。SC_CAP_NET_RAW应该是表示需要获取原始数据包的能力。
flags TM_FLAG_RECEIVE_TM 其他标志:TM_FLAG_RECEIVE_TM表示这个模块的用途是收包(其他还有解码、检测等)。

模块初始化

如上表所示,模块初始化由ReceivePcapThreadInit函数完成,其函数原型为:

        TmEcode ReceivePcapThreadInit(ThreadVars *tv, void *initdata, void **data)

其中,tv参数对应该模块所嵌入的线程的ThreadVars,在使用某些与线程相关的函数时需要使用。

initdata参数是模块的初始化数据,在使用TmSlotSetFuncAppend向线程中嵌入模块时传入。对于这个模块,其初始数据是一个PcapIfaceConfig类型的结构体指针,而这个结构体是由运行模式(对应于这个模块的运行模式类型为RUNMODE_PCAP_DEV)初始化函数(例如如RunModeIdsPcapAutoFp)中的ParsePcapConfig通过查询配置节点树下的pcap节点信息所填充的。具体的一些重要配置信息包括:

字段 含义
iface 设备接口名字,如linux下的"eth0"。
threads 抓包线程数量,默认为1。
buffer_size 接收缓冲大小,通过pcap_set_buffer_size设置。
snaplen 抓取长度,通过pcap_set_snaplen设置。
promisc 是否开启混杂模式,通过pcap_set_promisc设置。注意:如果要打开混杂模式,那么网卡必须也要打开(ifconfig eth0 promisc)。
bpf_filter BPF过滤器表达式,通过pcap_compile编译成bpf_program后再通过pcap_setfilter设置。
checksum_mode 校验和验证模式,设置为auto表示使用统计方式确定当前是否有checksum off-loading,若有,则关闭后续的校验和验证。
ref, DerefFunc 实现对配置结构体的引用计数。ref初始为当前使用该接口的线程(threads),因为这些线程都会在ReceivePcapThreadInit中引用这个配置结构。当用完配置结构体后(或出错需要退出时),就调用DerefFunc(PcapDerefConfig)减少引用计数,并在引用计数值为0时调用SCFree回收内存。

最后一个data参数,是该初始化函数的结果输出函数内部会新建一个PcapThreadVars结构体,作为本模块的内部上下文,其中部分字段是直接copy的PcapIfaceConfig,另一些重要字段包括:

字段 含义
livedev 当前设备结构体(LiveDevice类型),记录设备统计信息(收包、丢包、校验和验证失败的包),通过iface调用LiveGetDevice查找得到。
pcap_handle pcap句柄,通过pcap_open_live获得,后续使用pcap库函数时都会用到。
pcap_state pcap状态,UP或DOWN,在使用PcapTryReopen重新打开pcap时用到。

在初始化函数返回时,填充好的PcapThreadVars结构体便传递给了包含该模块的slot的slot_data字段,该字段将做为参数传入后续的模块函数。

数据包获取

初始化完成后,TmThreadsSlotPktAcqLoop函数将进入一个while循环,调用slot的PktAcqLoop函数获取并处理数据包。对应的模块函数原型为:

        TmEcode ReceivePcapLoop(ThreadVars *tv, void *data, void *slot)

其中,data即为初始化阶段生成的PcapThreadVars结构体,而slot就是包含该模块的那个slot,传入进来的目的主要是获取线程中后续的slot(这里对应DecodePcap),以调用后续的数据包处理流程。

函数内部的核心是一个while(1)循环,流程如下:

  1. 检查suricata_ctl_flags是否包含STOP或KILL标志,即Suricata是否需要退出,若是的话则立即返回。这里让子线程去检查全局控制标志的处理方式感觉的不是很优雅,最好是只检查自己的线程标志。
  2. 等待packet pool中有空闲数据包结构(通过cond或sleep实现)。
  3. 调用pcap_dispatch获取并处理数据包。其中,第2个参数表示最多处理的数据包个数,传入的值packet_q_len为packet pool的当前大小;第3个参数为包处理回调函数PcapCallbackLoop;最后一个参数为传给前面的回调函数的用户数据,这里传入的是PcapThreadVars结构体。
  4. 检查返回值,若出错则调用pcap_geterr获取错误信息,然后尝试不断调用PcapTryReopen重新开启抓包。

此后,pcap库内部会对每一个原始数据包,都调用PcapCallbackLoop函数进行进一步处理。

数据包封装

在Suricata中,用来封装数据包的结构体为Packet,核心字段如下:

字段 含义
src/dst、sp/dp、proto 五元组信息:源/目的地址,源/目的端口号,传输层协议(TCP/UDP/…)。
flow 数据包所属的流指针(类型为Flow_ *)。
ip4h、ip6h 网络层数据指针。
tcph、udph、sctph、icmpv4/6h 传输层数据指针。
payload、payload_len 应用层负载指针及长度。
next、prev 前一个/后一个数据包指针,用于组成双向链表。

PcapCallbackLoop函数中第一步就是完成对数据包的封装。其函数原型如下:

        void PcapCallbackLoop(char *user, struct pcap_pkthdr *h, u_char *pkt)

其中,user为用户数据,即之前传入的PcapThreadVars,h为pcap包结构体头,pkt为包数据指针。函数流程如下:

  1. 调用PacketGetFromQueueOrAlloc获取一个Packet结构。该函数会首先尝试调用PacketPoolGetPacket从packet pool中直接获取,如果失败(已经用完了),那么就调用PacketGetFromAlloc新分配一个。

:与pool中取出的数据包最终可以回收不同,这种使用malloc动态分配的数据包最后需要free,因此为了区分会在其flags中打上标记PKT_ALLOC。

  1. 填充Packet结构体的部分字段:数据包源(PKT_SRC_WIRE)、时间戳、所属数据链路/设备。
  2. 调用PacketCopyData复制数据包内容到pkt字段中,并设置pktlen为相应的长度。

:为什么需要做复制这种开销大的操作呢?man pcap_dispatch给出了答案:The struct pcap_pkthdr… are not guaranteed to be valid after the callback routine returns; if the code needs them to be valid after the callback, it must make a copy of them. 而由于Suricata的多线程和异步特性,数据包在callback中会送入outq中等待后续线程继续处理,因此这里必须进行复制。

  1. 校验和相关的处理。若checksum_mode为DISABLE,将会给包的flags打上PKT_IGNORE_CHECKSUM标志,表示后续不再对其进行校验和验证。若checksum_mode为AUTO,则调用ChecksumAutoModeCheck进行统计分析,满足条件则后续该设备的数据包都会关闭校验和验证。目前是的关闭条件是:1000个包中若有超过10%的包校验和不正确,则认为网卡开启了checksum offloading,因而关闭检验和验证。
  2. 调用TmThreadsSlotProcessPkt让本线程中包含的其他slot中的模块对数据包进行后续处理。若处理返回失败,则调用pcap_breakloop中断抓包。
  3. 调用PcapDumpCounters打印抓包统计信息。这个打印保证每秒只触发一次,机制是:获取当前时间,只有当前秒数与上一次记录的秒数不同时才调用。

数据包处理

从上面可以看出,进一步的数据包处理是在TmThreadsSlotProcessPkt中完成,其原型为:

        static inline TmEcode TmThreadsSlotProcessPkt(ThreadVars *tv, TmSlot *s, Packet *p)

其中,s就是前面一路传下来的slot,而p为当前要处理的Packet。处理流程如下:

  1. 调用TmThreadsSlotVarRun,将数据包依次传入后续的各个slot进行处理。
  2. 若返回失败,则调用TmqhOutputPacketpool将数据包(以及各个slot的slot_post_pq中的数据包)进行回收或释放。然后设置线程标志为THV_FAILED,等待主线程处理。
  3. 若返回成功,则调用tmqh_out(线程创建时设置为与该线程绑定的outqh的处理函数OutHandler,在这里默认为TmqhOutputFlowActivePackets),将数据包送到后续队列中去。
  4. 此外,由于各模块在处理过程中可能会新生成数据包(如隧道数据包、重组数据包),这些数据包存储在与每个slot绑定的slot_pre_pq或slot_post_pq队列中,因此还需要类似上述流程,对这些数据包进行处理。这里只集中处理了slot_post_pq,slot_pre_pq将在处理每个slot后立即处理。

对数据包进行进一步处理的TmThreadsSlotVarRun函数原型如下:

        TmEcode TmThreadsSlotVarRun(ThreadVars *tv, Packet *p,TmSlot *slot)

按照函数头的注释说明,这个函数被从母函数中拉出来独立存在的原因是,为了能够对其进行递归调用。函数主流程是一个遍历所有slot的for循环,其执行过程如下:

  1. 调用slot的处理函数SlotFunc
  2. 若返回失败,处理流程与上面类似。
  3. 若返回成功,则继续处理slot_pre_pq:对其中每个数据包,都递归调用TmThreadsSlotVarRun,将其送入下一个slot进行处理。

: slot_pre_pq vs. slot_post_pq: 某个slot在处理某个母数据包时新产生的子数据包,若放入slot_pre_pq中,则这个数据包将在本个slot处理完母数据包后,在后续slot处理母数据包之前,先将这些子数据包放到后续的slot去处理;而如果是放如slot_post_pq,则需要等到母数据包被所有slot都处理完后,在下一个数据包处理之前,再去集中处理,如上面所述。

前一篇main()的笔记中已经记录了,抓包线程中只有两个slot,第一个slot在嵌入的即是前面所介绍的ReceivePcap模块,而下一个slot函数中嵌入的模块为DecodePcap,其对应的SlotFunc是DecodePcap,其任务是对从通过pcap抓取的数据包进行解码。

到次,这个pcap实时数据包源的任务已经完成了。其他数据包源模块的功能和流程也基本类似,最终都会递交给相应的解码模块。

你可能感兴趣的:(Suricata)