本篇文章将分析一下suricata中的流表管理,包括流表初始化,流的新建以及流的老化。
对于任何的网络分析工具和产品来说,流管理都是非常重要的一个方面。所谓的流就是由源目的IP,源目的端口以及传输层的协议构成的通信双方的虚拟链接,有了这条虚拟链接之后,数据包就可以在网络上中有目的的传输。通常来说网络分析工具和产品,通过五元组建立起流表之后,每处理一个报文,这个报文通常就会归属一个流,相当于对于报文进行了一次分类。同一条流上的报文就可以进行TCP的重组等一些重要的操作。
对于suricata来说流管理主要涉及到如下几个方面:
流表的初始化
流表初始化对应的函数为FlowInitConfig
,该函数在suricata初始化阶段被调用,具体调用函数为PreRunInit
。该函数的主要目的是初始化流的一些参数,包括流表占用的内存大小,流哈希表的长度,预分配的流表个数。当然初始化这些参数有两种方式,一种使用系统默认的方式,另外一种通过读取suricata.yaml配置文件中的配置。
流表占用内存指的是suricata给流表设置的内存大小,但是其实更常用的做法是设置流表的长度,也就是流表支持多少条流的存储。当然这个地方也是可以转换的,每一条流占用的内存空间在FlowAlloc
有所体现,为size_t size = sizeof(Flow) + FlowStorageSize();
,用总内存除以每条流的内存即可以得到流表的大小。系统默认的流内存大小为32M。
由于流表是使用哈希表这样的数据结构来组织的,流哈希表的长度即哈希表的长度,默认的值为65535。哈希的计算函数为FlowGetHash
,由于哈希存在冲突,因此每一项下面可能会下挂多个流,用哈希桶进行存储。
预分配流表个数指的是在初始化的时候预先申请流个数,当系统运行起来,动态的申请内存对于系统的处理性能有一定的影响,因此会预先申请一定数量的流内存,默认值为10000。这里就是配置的流个数。而不像前面配置的是内存。如下代码是FlowInitConfig
函数中预先申请流内存的代码片段:
/* pre allocate flows */
for (i = 0; i < flow_config.prealloc; i++) {
if (!(FLOW_CHECK_MEMCAP(sizeof(Flow) + FlowStorageSize()))) {
SCLogError(SC_ERR_FLOW_INIT, "preallocating flows failed: "
"max flow memcap reached. Memcap %"PRIu64", "
"Memuse %"PRIu64".", SC_ATOMIC_GET(flow_config.memcap),
((uint64_t)SC_ATOMIC_GET(flow_memuse) + (uint64_t)sizeof(Flow)));
exit(EXIT_FAILURE);
}
Flow *f = FlowAlloc();
if (f == NULL) {
SCLogError(SC_ERR_FLOW_INIT, "preallocating flow failed: %s", strerror(errno));
exit(EXIT_FAILURE);
}
FlowEnqueue(&flow_spare_q,f);
}
可以看到对于申请的流表内存,都会放入flow_spare_q这样一个全局队列进行管理。在系统运行起来之后,流建立的时候都会从flow_spare_q队列中欧取一条流的内存空间进行使用。
除了上述三个参数的初始化,在FlowInitFlowProto
函数中对于流老化时长也有进行初始化。suricata对于不同协议的流以及流的不同阶段都定义了不同的老化时长。可以明显的看到对于TCP流的初建阶段的老化时长默认值为FLOW_IPPROTO_TCP_NEW_TIMEOUT
30秒,而对于TCP握手完成之后的流老化时长默认为FLOW_IPPROTO_TCP_EST_TIMEOUT
300秒。
其实除此之外还有更为复杂的机制就是当流内存不够用的时候,会启用一个紧急状态机制,这个时候不同协议以及不同流状态的老化时间会大大的缩短,加速流表老化。以腾出更多的内存空间为新流所用,例如紧急状态下TCP流的初建阶段的老化时长默认值为FLOW_IPPROTO_TCP_EMERG_NEW_TIMEOUT
10秒,而对于TCP握手完成之后的流老化时长默认为FLOW_IPPROTO_TCP_EMERG_EST_TIMEOUT
100秒。
流的新建
suricata中有几个比较重要的模块,例如收包,解码以及检测处理,分别对应像,TmModuleReceivePcapFileRegister
,TmModuleDecodePcapFileRegister
,TmModuleFlowWorkerRegister
这样几个注册模块中的功能。像流建立的处理就在TmModuleFlowWorkerRegister
模块中 的FlowWorker函数中。
suricata的策略是当流的第一个包到来的时候就回去申请这条流的内存空间,虽然有PKT_WANTS_FLOW这样一个标志位的判断,但是对于像TCP,UDP,STCP,ICMP等需要建流的协议来说,每一个包都会在FlowSetupPacket函数中打上这个标志,因此基本上这列协议的包都会进入FlowHandlePacket->FlowGetFlowFromHash
去寻找流表,根据五元组等值通过FlowGetHash
计算的哈希值,直接去flow_hash表中索引对应的项。然后再去判断该项下面的哪一个哈希桶是目的流。
,FLOWLOCK_WRLOCK(f);
。也就是说在该报文的处理期间,本线程享有对于该流的读写操作,禁止其他线程的操作。什么时候去锁,就是在对该报文处理结束的时候去锁。可以发现在FlowWorker函数中return的地方,会有对于流的去锁操作FLOWLOCK_UNLOCK(p->flow);
。FlowTimeoutsEmergency
启动紧急模式,缩短老化时长。2,FlowWakeupFlowManagerThread
启动老化线程。3,当然在第二步的时候可能会由于老化时长没到,没有流老化释放出来,这个时候就会调用FlowGetUsedFlow函数。在该函数中,会遍历整个哈希表,找到某个索引项下面存在时长最长的那个流,复用其内存。当然哪些被上锁的索引项是不能够被回收的。如果遍历完整个哈希表,仍然没有可用的,则只能返回NULL。流表的老化
在上述也提到了流表的老化,就是函数FlowManager
的功能之一。FlowManager首先会调用FlowUpdateSpareFlows
,该函数的目的就是更新备用流内存,也就是flow_spare_q队列的内容。前面提到在suricata初始化阶段,会设置预分配的流个数,并申请对应的内存交由flow_spare_q进行管理。FlowUpdateSpareFlows
的目的就是检查当备用流个数小于预设值的时候,进行流内存申请,入队;当大于预设值的时候,进行流内存的释放,出队。实现流老化功能的函数为FlowTimeoutHash->FlowManagerHashRowTimeout
。
在FlowManagerHashRowTimeout
函数中:
FlowClearMemory
清空该流的内存,清空之后通过FlowMoveToSpare
函数方式备用流flow_spare_q中,当然flow_spare_q的管理就是刚刚提到的。总的原则就是流内存的申请是一个比较耗时的动作,并不会轻易的将其释放,最好用于重用。在FlowManager
函数之中除了FlowTimeoutHash
用于老化流,还有DefragTimeoutHash,HostTimeoutHash,IPPairTimeoutHash这三个老化的函数,那么这些函数的作用是什么?同时在FlowAlloc函数中,可以看到申请的内存空间为size_t size = sizeof(Flow) + FlowStorageSize();
,即流结构体的大小加上flow storage的大小,这个flow storage又是什么呢?这些方面的内容将在下一篇进行介绍。
本文为CSDN村中少年原创文章,转载记得加上原创出处,博主链接这里。