suricata源码之-流表管理

本篇文章将分析一下suricata中的流表管理,包括流表初始化,流的新建以及流的老化。

对于任何的网络分析工具和产品来说,流管理都是非常重要的一个方面。所谓的流就是由源目的IP,源目的端口以及传输层的协议构成的通信双方的虚拟链接,有了这条虚拟链接之后,数据包就可以在网络上中有目的的传输。通常来说网络分析工具和产品,通过五元组建立起流表之后,每处理一个报文,这个报文通常就会归属一个流,相当于对于报文进行了一次分类。同一条流上的报文就可以进行TCP的重组等一些重要的操作。

对于suricata来说流管理主要涉及到如下几个方面:

  • 1,流表的初始化。
  • 2,每一条流上下文的申请和建立。
  • 3,每一条流上下文的老化,释放等等。

流表的初始化

流表初始化对应的函数为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中有几个比较重要的模块,例如收包,解码以及检测处理,分别对应像,TmModuleReceivePcapFileRegisterTmModuleDecodePcapFileRegisterTmModuleFlowWorkerRegister这样几个注册模块中的功能。像流建立的处理就在TmModuleFlowWorkerRegister模块中 的FlowWorker函数中。

suricata的策略是当流的第一个包到来的时候就回去申请这条流的内存空间,虽然有PKT_WANTS_FLOW这样一个标志位的判断,但是对于像TCP,UDP,STCP,ICMP等需要建流的协议来说,每一个包都会在FlowSetupPacket函数中打上这个标志,因此基本上这列协议的包都会进入FlowHandlePacket->FlowGetFlowFromHash去寻找流表,根据五元组等值通过FlowGetHash计算的哈希值,直接去flow_hash表中索引对应的项。然后再去判断该项下面的哪一个哈希桶是目的流。

  • 1,如果找到目的流,则首先将该流的哈希桶放到,该索引下链表的头部,因为这是最新的活动流,下一个报文还有可能是该流的报文,主要是降低哈希桶匹配的查询次数。其次为了线程安全,防止其他线程对于该条流的读写,需要将该流加上读写锁,FLOWLOCK_WRLOCK(f);。也就是说在该报文的处理期间,本线程享有对于该流的读写操作,禁止其他线程的操作。什么时候去锁,就是在对该报文处理结束的时候去锁。可以发现在FlowWorker函数中return的地方,会有对于流的去锁操作FLOWLOCK_UNLOCK(p->flow);
  • 2,当然如果遍历该项所有的哈希桶没有找到目的流,这个时候就会调用FlowGetNew函数进行去新建流。创建新流最主要的动作就是申请流的内存空间。由于在流表初始化阶段,在flow_spare_q变量中提前申请了一定的流内存空间。因此首先去flow_spare_q对联中看看有没有内存空间使用,如果有,直接返回该流即可。如果没有,第一步先检查给流表分配的空间有没有被使用完,没有则调用FlowAlloc函数申请内存并返回即可。如果内存空间被使用完毕,这个时候就需要老化一部分流,也就是将存在时间较长的一部分流的内存回收供新流使用。共分为三个步奏:1,FlowTimeoutsEmergency启动紧急模式,缩短老化时长。2,FlowWakeupFlowManagerThread启动老化线程。3,当然在第二步的时候可能会由于老化时长没到,没有流老化释放出来,这个时候就会调用FlowGetUsedFlow函数。在该函数中,会遍历整个哈希表,找到某个索引项下面存在时长最长的那个流,复用其内存。当然哪些被上锁的索引项是不能够被回收的。如果遍历完整个哈希表,仍然没有可用的,则只能返回NULL。

流表的老化

在上述也提到了流表的老化,就是函数FlowManager的功能之一。FlowManager首先会调用FlowUpdateSpareFlows,该函数的目的就是更新备用流内存,也就是flow_spare_q队列的内容。前面提到在suricata初始化阶段,会设置预分配的流个数,并申请对应的内存交由flow_spare_q进行管理。FlowUpdateSpareFlows的目的就是检查当备用流个数小于预设值的时候,进行流内存申请,入队;当大于预设值的时候,进行流内存的释放,出队。实现流老化功能的函数为FlowTimeoutHash->FlowManagerHashRowTimeout

FlowManagerHashRowTimeout函数中:

  • 1,FlowManagerFlowTimeout判断该条流的老化时长有没有到;
  • 2,FlowManagerFlowTimedOut函数确定是都能够老化该流,因为虽然有的流老化时长到了,但是需要进行TCP重组,这个时候就不能够老化该流。
  • 3,将老化的流方式flow_recycle_q全局队列中。函数FlowRecycler会由一个单独的线程处理这种别老化的流,在改函数中,会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村中少年原创文章,转载记得加上原创出处,博主链接这里

猜你喜欢

转载自blog.csdn.net/javajiawei/article/details/106818360
今日推荐