1. 参考资料:
(1)suricata架构——数据结构和代码流程图解 https://blog.csdn.net/gengzhikui1992/article/details/103031874
(这里面几张图很不错,方便结构理解,另外解释了 行锁,全局的nf_conntrack_lock)
行锁:行级锁哈希表则每一行都有一把锁,内存开销大,实现复杂,但是在大并发,高效率的后台服务程序中使用非常广泛。suricata针对snort单线程处理数据包,无法很好利用多核cpu的劣势,开发了多线程架构方式并发处理数据包,而很多数据是线程间共享,所以在很多地方使用行级锁哈希表等其他高效数据结构。
全局的nf_conntrack_lock:用于保护全局会话表
(2)suricata 源码分析之行锁 https://blog.csdn.net/guoguangwu/article/details/63318277
连接管理的哈希表:FlowBucket *flow_hash。其为链式哈希表(数组每个元素存放一个链表首地址,一个链表为一个桶。输入一个数据,函数h将其转化为整型数据k,根据k处理得到数组元素指向的桶,在桶(链表)中执行插入或删除操作。)。
flow_hash 是一个数组,每个FlowBucket 元素由 head 、tail和flow的hnext、hprev 构成一个双向链表,也就是所谓的行。该哈希的行数由配置文件或者默认值决定(flow_config.hash_size)
flow_hash = SCCalloc(flow_config.hash_size, sizeof(FlowBucket));
使用接口如下,以下两个函数的返回成功的话,会对该节点加锁保护,也是用来并发访问;并且都会将当前的这个节点放在该行的第一个节点,作为缓存假定其为最活跃的、近期最可能被访问的内容。
FlowGetFlowFromHash:通过数据包的信息获取连接,如果哈希表中没有,则创建flow;
作用:先计算出该数据包对应的flow的哈希值,然后通过哈希值去定位到某一行,再对该行加锁,此时,flow_hash的其他行可以访问的,如果这一行不为空,再去遍历者这个链表(行),如果已经存在,对该链表节点加锁、解锁行锁返回,如果没有创建节点插入链表,对该链表节点加锁、解锁行锁返回,如果这一行为空,创建节点插入链表,对该链表节点加锁、解锁行锁返回。
FlowLookupFlowFromHash:通过数据包的信息查找连接,不会创建;先计算出该数据包对应的flow的哈希值,然后通过哈希值去定位到某一行,再对该行加锁,此时,flow_hash的其他行可以访问的,
(我下载的版本的surcicata里面没有找到这个函数)
(3)一个比较全面的suricata 的介绍博客:属乌鸦的 http://www.hyuuhit.com/categories/suricata/
缩略:
- 2019-11-16libpcap在libvirt虚拟化环境下捕获数据包不完整的一种情况分析
- 2018-06-20suricata 4.0.3 应用层协议检测
- 2018-05-28suricata 4.0.3 tcp reassembly
- 2018-05-28suricata 4.0.3 flow
- 2018-04-16suricata 4.0.3 收包解码
- 2018-03-22suricata 4.0.3 线程模型
- 2018-03-20suricata 4.0.3 runmode
- 2018-03-07suricata 4.0.3 配置模块
- 2018-02-11suricata命令行参数
(4)博客园博主:大数据和人工智能躺过的坑
* Suricata的规则解读(默认和自定义) https://www.cnblogs.com/zlslch/p/7382190.html
该文章主要讲解suricata的规则定义方式,涉及到具体规则细节,没有细看,后面在真正做的时候可以参考
* 使用 Suricata 进行入侵监控(一个简单小例子访问百度) https://www.cnblogs.com/zlslch/p/7327795.html
*基于CentOS6.5下Suricata(一款高性能的网络IDS、IPS和网络安全监控引擎)的搭建(图文详解)(博主推荐)https://www.cnblogs.com/zlslch/p/7326291.html
(5)Suricata配置文件说明: https://www.cnblogs.com/zlslch/p/7326291.html
该文章详细解读了 suricata.yaml 的各配置以及字段的作用和范围。
(6)Suricata 开源平台wiki: https://redmine.openinfosecfoundation.org/projects/suricata/wiki/Debugging
(7)Suricata 博客专栏: https://blog.csdn.net/vevenlcf/category_6085067.html
收录了11篇相关文章。作者: tiny丶
*Suricata源码阅读笔记:main() https://blog.csdn.net/vevenlcf/article/details/50600324
这个文章详细的写了SuricataMain函数中,每个步骤的作用。比较有用
* Suricata 3.2.1 源码阅读笔记:数据包队列: https://blog.csdn.net/vevenlcf/article/details/73123770
这篇文章解读了 缓存线程模块内部新产生数据包的线程内队列,以及线程之间用来传递数据包的线程间队列。下面相关的结构体和函数的笔记有部分来自该文章的借鉴。但是可能suricata版本不同,原文提到的一些函数和字段在当前版本的代码中已不存在。
(8)浅谈开源入侵检测引擎 Suricata https://gitbook.cn/gitchat/activity/5b14b66fdcaebb0977c7f94e?utm_source=csdn_blog
收费内容,暂时没学习到。后面有机会再看。
2. 几大结构体的定义和作用
(Flow_ / FlowBucket_ /PacketQueue_ /ThreadVars_ ):
(1)Flow_
(流。 用于处理包含多个包的消息流,流是为流的新数据包创建的全局数据结构,然后查找流的其他数据包)
流“头”(地址、端口、协议、递归级别)在初始化后是静态的,并且在流的整个活动期间保持只读。这就是为什么我们可以在没有锁保护的情况下访问它们。
流的锁:流由多个数据包同时更新/使用。这就是为什么会有流互斥。它是互斥锁,而不是自旋锁,因为流上的一些操作可能代价比较高,因此自旋锁代价更加高。
(2)FlowBucket_
(顾名思义:流的桶或者流的链表,每个存储桶包含一个或多个流,桶内的流具有相同的哈希键值(哈希是链式哈希)。进行修改时,整个桶都被锁定。也就是行锁)
全局变量 FlowBucket *flow_hash 用来存储流桶的哈希表,哈希表的大小通过配置设定。数组每个元素存放一个链表首地址,一个链表为一个桶。输入一个数据,函数h将其转化为整型数据k,根据k处理得到数组元素指向的桶,在桶(链表)中执行插入或删除操作。
flow_hash = SCMallocAligned(flow_config.hash_size * sizeof(FlowBucket), CLS);
/* 每个存储桶包含一个或多个流,桶内的流具有相同的哈希键值(哈希是链式哈希)。进行修改时,整个桶都被锁定。也就是行锁 */ typedef struct FlowBucket_ { /** head of the list of active flows for this row. */ Flow *head; /*可以形容为链表头*/ /** head of the list of evicted flows for this row. Waiting to be * collected by the Flow Manager. */ Flow *evicted; /*百度翻译为驱逐,结合现在的代码,可以理解为暂存在本桶中,但是不使用的第一个流的指针*/ /*行锁*/ #ifdef FBLOCK_MUTEX SCMutex m; #elif defined FBLOCK_SPIN SCSpinlock s; #else #error Enable FBLOCK_SPIN or FBLOCK_MUTEX #endif /** timestamp in seconds of the earliest possible moment a flow * will time out in this row. Set by the flow manager. Cleared * to 0 by workers, either when new flows are added or when a * flow state changes. The flow manager sets this to INT_MAX for * empty buckets. */ SC_ATOMIC_DECLARE(int32_t, next_ts); } __attribute__((aligned(CLS))) FlowBucket;
(3)数据包队列相关
* 数据包队列 PacketQueue_
* 线程内队列 TmSlot_
* 线程间队列: PacketQueue trans_q[256]; Suricata中使用了一个全局数组作为所有的线程间队列的存储,但是从我当前的代码来看,用的是动态生成的,而不是全局变量。
Tmq_ 用于管理线程间队列。
typedef struct Tmq_ { char *name;/* 队列名字 */ bool is_packet_pool; /*队列名为packetpool时为真*//*从数据包池中获取数据包,将数据包放回数据包池。(从代码实现看,此种情况不会动态分配队列)*/
uint16_t id; /* 对应的队列存储在trans_q中的索引 */ uint16_t reader_cnt; /* 向这个队列读数据的线程数 */ uint16_t writer_cnt; /* 向这个队列写数据的线程数 */ PacketQueue *pq; /*相比于之前的文档链接,多了一个这个指针,不再指向全局数组,而是动态分配*/ TAILQ_ENTRY(Tmq_) next; } Tmq; #define TAILQ_ENTRY(type) \ struct { \ struct type *tqe_next; /* next element */ \ struct type **tqe_prev; /* address of previous next element */ \ }
系统初始化时会调用TmqhSetup注册所有队列handler,类似包括:
类型 | 说明 |
simple | 简单地从inq获取数据包,线程处理完后将包送往唯一的outq。 |
packetpool* | 从数据包池中获取数据包,将数据包放回数据包池。见下面注释。(从代码实现看,此种情况不会动态分配队列) |
flow | 用于autofp模式的handler,实现流的绑定和负载均衡。 |
(4)线程变量 ThreadVars_
在代码中变量名称一般定义为 tv.
3. 一些函数:
(1)流比较:FlowCompare
static inline int FlowCompare(Flow *f, const Packet *p) { if (p->proto == IPPROTO_ICMP) { return FlowCompareICMPv4(f, p); } else if (p->proto == IPPROTO_ESP) { return FlowCompareESP(f, p); } else { return CmpFlowPacket(f, p); } }
(2)TcpSessionPacketSsnReuse 判断包是否可以重用?啥场景??
从代码看,FlowGetFlowFromHash 执行时,如果已经存在,则考虑复用该流,复用后替换原有的流。
(3)MoveToWorkQueue 从hash桶中删除或者放到 evicted 指针后
static inline void MoveToWorkQueue(ThreadVars *tv, FlowLookupStruct *fls, FlowBucket *fb, Flow *f, Flow *prev_f) { f->flow_end_flags |= FLOW_END_FLAG_TIMEOUT; /* remove from hash... */ if (prev_f) { prev_f->next = f->next; } if (f == fb->head) { fb->head = f->next; } if (f->proto != IPPROTO_TCP || FlowBelongsToUs(tv, f)) { // TODO thread_id[] direction f->fb = NULL; f->next = NULL; FlowQueuePrivateAppendFlow(&fls->work_queue, f); FLOWLOCK_UNLOCK(f); } else {
/*evicted 可以理解为暂存在本桶中,但是不使用的第一个流的指针,f 插入作为 evicted的第一个指针,原有的向后排列*/
/* implied: TCP but our thread does not own it. So set it * aside for the Flow Manager to pick it up. */ f->next = fb->evicted; fb->evicted = f; if (SC_ATOMIC_GET(f->fb->next_ts) != 0) { SC_ATOMIC_SET(f->fb->next_ts, 0); } FLOWLOCK_UNLOCK(f); } }
(4)TmThreadSetSlots 设置线程功能,在创建线程的时候使用:
static TmEcode TmThreadSetSlots(ThreadVars *tv, const char *name, void *(*fn_p)(void *)) { if (name == NULL) { if (fn_p == NULL) { printf("Both slot name and function pointer can't be NULL inside " "TmThreadSetSlots\n"); goto error; } else { name = "custom"; } } if (strcmp(name, "varslot") == 0) { tv->tm_func = TmThreadsSlotVar; } else if (strcmp(name, "pktacqloop") == 0) { tv->tm_func = TmThreadsSlotPktAcqLoop; } else if (strcmp(name, "management") == 0) { tv->tm_func = TmThreadsManagement; } else if (strcmp(name, "command") == 0) { tv->tm_func = TmThreadsManagement; } else if (strcmp(name, "custom") == 0) { if (fn_p == NULL) goto error; tv->tm_func = fn_p; } else { printf("Error: Slot \"%s\" not supported\n", name); goto error; } return TM_ECODE_OK; error: return TM_ECODE_FAILED; }
数组每个元素存放一个链表首地址,一个链表为一个桶。输入一个数据,函数h将其转化为整型数据k,根据k处理得到数组元素指向的桶,在桶(链表)中执行插入或删除操作。