引言
从上一篇 浅谈开源入侵检测引擎 Suricata 我们可以了解 Suricata 的安装部署大致框架、以及从配置方面谈及的性能优化,这场 Chat 则从代码角度带您剖析 Suricata。 通过本次 Chat 您将了解以下知识点:
- suricata运行模式
- suricata线程模块
- suricata数据流转
- suricata抓包&解包
- suricata数据流管理引擎
- suricata应用协议解析引擎
- 总结陈述
Suricata
提到Suricata源码分析,我们首先需要了解suricata整体框架设计,只有你了解了整体框架,他才能对阅读源码有更好的促进作用。而提到框架则需要了解它的运行模式,在上一篇文章中说,suricata相对于传统snort引擎,它是由很多单独的线程模块所组成,使用者可以自行配置组合所需要的线程模块既可进行优化、自定义需求。默认的运行模式主要分为workers模式、autofp模式以及单线程模式single。workers模式主要是采用串行流水线的模式,针对单独数据流采取唯一线程 从头至尾处理,而autofp则是将收包和解包放在一个单独线程,而剩下的工作线程则通过队列传递数据包进行处理。 显然workers模式更适用于高效率处理数据包。
其次suricata支持多种抓包引擎,如:pcap、netmap、pfring、socket等。本文就pfring抓包引擎来谈一下数据包的流转以及部分解析引擎。 ##运行模式 涉及文件:runmodes.c 、util-runmodes.c、tm-thread.c、tm-queue ……
1. 注册运行模式
void RunModeIdsPfringRegister(void)
{
default_mode_autofp = "autofp";
RunModeRegisterNewRunMode(RUNMODE_PFRING, "autofp",
"Multi threaded pfring mode. Packets from "
"each flow are assigned to a single detect "
"thread, unlike \"pfring_auto\" where packets "
"from the same flow can be processed by any "
"detect thread",
RunModeIdsPfringAutoFp);
RunModeRegisterNewRunMode(RUNMODE_PFRING, "single",
"Single threaded pfring mode",
RunModeIdsPfringSingle);
RunModeRegisterNewRunMode(RUNMODE_PFRING, "workers",
"Workers pfring mode, each thread does all"
" tasks from acquisition to logging",
RunModeIdsPfringWorkers);
return;
} ### 2. 主要数据结构
模块结构
typedef struct TmModule_ {
const char *name;
TmEcode (*ThreadInit)(ThreadVars *, const void *, void **);
......
} TmModule;
TmModule tmm_modules[TMM_SIZE]; 模块注册结构体,主要用于注册各个线程模块,提供全局变量**tmm_modules**, 便于组装回调。 如pfring模块注册:
void TmModuleReceivePfringRegister (void)
{
tmm_modules[TMM_RECEIVEPFRING].name = "ReceivePfring";
tmm_modules[TMM_RECEIVEPFRING].ThreadInit = ReceivePfringThreadInit;
tmm_modules[TMM_RECEIVEPFRING].Func = NULL;
tmm_modules[TMM_RECEIVEPFRING].PktAcqLoop = ReceivePfringLoop;
tmm_modules[TMM_RECEIVEPFRING].PktAcqBreakLoop = PfringBreakLoop;
tmm_modules[TMM_RECEIVEPFRING].RegisterTests = NULL;
tmm_modules[TMM_RECEIVEPFRING].flags = TM_FLAG_RECEIVE_TM;
} #### 线程结构
typedef struct ThreadVars_ {
pthread_t t;
char name[16];
char *printable_name;
char *thread_group_name;
SC_ATOMIC_DECLARE(unsigned int, flags);
uint8_t tmm_flags;
Tmq *inq;
Tmq *outq;
void *outctx;
const char *outqh_name;
struct Packet_ * (*tmqh_in)(struct ThreadVars_ *);
void (*InShutdownHandler)(struct ThreadVars_ *);
void (*tmqh_out)(struct ThreadVars_ *, struct Packet_ *);
void *(*tm_func)(void *);
struct TmSlot_ *tm_slots;
......
struct ThreadVars_ *next;
struct ThreadVars_ *prev;
} ThreadVars; 线程结构,主要用于自由组装模块之后,提供线程队列绑定、线程句柄等。
队列结构
typedef struct Tmqh_ {
const char *name;
Packet *(*InHandler)(ThreadVars *);
void (*InShutdownHandler)(ThreadVars *);
void (*OutHandler)(ThreadVars *, Packet *);
void *(*OutHandlerCtxSetup)(const char *);
void (*OutHandlerCtxFree)(void *);
void (*RegisterTests)(void);
} Tmqh;
Tmqh tmqh_table[TMQH_SIZE]; 队列数据结构,提供队列的注册回调、并提供全局句柄**tmqh_table**供线程绑定队列用。而队列主要包含 这几种: TMQH_SIMPLE, TMQH_NFQ, TMQH_PACKETPOOL, TMQH_FLOW, 这边我们主要关心**TMQH_PACKETPOOL、TMQH_FLOW**一个数据包队列、一个是流控队列。如:
//数据包尺队列注册
void TmqhPacketpoolRegister (void)
{
tmqh_table[TMQH_PACKETPOOL].name = "packetpool";
tmqh_table[TMQH_PACKETPOOL].InHandler = TmqhInputPacketpool;
tmqh_table[TMQH_PACKETPOOL].OutHandler = TmqhOutputPacketpool;
}
数据包输出队列trans_q句柄
PacketQueue *q = &trans_q[tv->inq->id];
//数据流队列注册
void TmqhFlowRegister(void)
{
tmqh_table[TMQH_FLOW].name = "flow";
tmqh_table[TMQH_FLOW].InHandler = TmqhInputFlow;
tmqh_table[TMQH_FLOW].OutHandlerCtxSetup = TmqhOutputFlowSetupCtx;
tmqh_table[TMQH_FLOW].OutHandlerCtxFree = TmqhOutputFlowFreeCtx;
tmqh_table[TMQH_FLOW].RegisterTests = TmqhFlowRegisterTests;
const char *scheduler = NULL;
//队列的负载均衡模式调度
if (ConfGet("autofp-scheduler", &scheduler) == 1) {
if (strcasecmp(scheduler, "round-robin") == 0) {
SCLogNotice("using flow hash instead of round robin");
tmqh_table[TMQH_FLOW].OutHandler = TmqhOutputFlowHash;
} else if (strcasecmp(scheduler, "active-packets") == 0) {
SCLogNotice("using flow hash instead of active packets");
tmqh_table[TMQH_FLOW].OutHandler = TmqhOutputFlowHash;
} else if (strcasecmp(scheduler, "hash") == 0) {
tmqh_table[TMQH_FLOW].OutHandler = TmqhOutputFlowHash;
} else if (strcasecmp(scheduler, "ippair") == 0) {
tmqh_table[TMQH_FLOW].OutHandler = TmqhOutputFlowIPPair;
} else {
SCLogError(SC_ERR_INVALID_YAML_CONF_ENTRY, "Invalid entry \"%s\" "
"for autofp-scheduler in conf. Killing engine.",
scheduler);
exit(EXIT_FAILURE);
}
} else {
tmqh_table[TMQH_FLOW].OutHandler = TmqhOutputFlowHash;
}
return;
} #### Slot链结构
typedef struct TmSlot_ {
ThreadVars *tv;
SC_ATOMIC_DECLARE(TmSlotFunc, SlotFunc);
TmEcode (*PktAcqLoop)(ThreadVars *, void *, void *);
TmEcode (*SlotThreadInit)(ThreadVars *, const void *, void **);
void (*SlotThreadExitPrintStats)(ThreadVars *, void *);
TmEcode (*SlotThreadDeinit)(ThreadVars *, void *);
......
struct TmSlot_ *slot_next;
TmEcode (*Management)(ThreadVars *, void *);
} TmSlot; suricata使用Slot链来讲线程模块中注册条件进行填充,并利用slot将线程模块串联起来。
3. autofp模式注册
这边我们就利用autofp来讲解一下,suricata如何利用这些数据结构进行串联起来的。
- RunModeSetLiveCaptureAutoFp 函数入口
- UtilCpuGetNumProcessorsOnline 计算cpu核心数
- LiveGetDeviceCount 指定网卡数量
- TmThreadGetNbThreads 指定最大线程数量,默认线程核心*threading_detect_ratio,最大1024,当然此处线程并非最终数量,如果配置文件配置了,那么还是取二者小的一个来创建进行抓包、解包线程,而flow-worker则使用之前的。
- RunmodeAutoFpCreatePickupQueuesString 根据线程数,初始化队列名字如:”pickup1,pickup2,pickup3\0”
- TmThreadCreatePacketHandler(tname,”packetpool”, “packetpool”, queues, “flow”, “pktacqloop”); 创建收包线程返回句柄ThreadVars *tv_receive 。
- tname –>线程名字 RX#01, RX#02…….
- 输入队列: tv->inq =NULL
- 输出队列: tv->outq =pickup1,pickup2…..
- packetpool–>第一个 输入队列名字,第二个 队列句柄的名字
- queues –> 输出句柄名字 “pickup1,pickup2,pickup3\0”
- flow –> 输出句柄名称
- 这边 收包线程队列 : 输入 === TmqhInputPacketpool 输出 === TmqhOutputFlowHash
- TmSlotSetFuncAppend 注册抓包 & decode解包线程 .
- TmThreadSpawn 接受线程启动
下面再来看一下流处理线程模块
for (int thread = 0; thread < thread_max; thread++) {
snprintf(tname, sizeof(tname), "%s#%02u", thread_name_workers, thread+1);
snprintf(qname, sizeof(qname), "pickup%u", thread+1);
SCLogDebug("tname %s, qname %s", tname, qname);
ThreadVars *tv_detect_ncpu =
TmThreadCreatePacketHandler(tname,
qname, "flow",
"packetpool", "packetpool",
"varslot");
if (tv_detect_ncpu == NULL) {
SCLogError(SC_ERR_RUNMODE, "TmThreadsCreate failed");
exit(EXIT_FAILURE);
}
TmModule *tm_module = TmModuleGetByName("FlowWorker");
if (tm_module == NULL) {
SCLogError(SC_ERR_RUNMODE, "TmModuleGetByName for FlowWorker failed");
exit(EXIT_FAILURE);
}
TmSlotSetFuncAppend(tv_detect_ncpu, tm_module, NULL);
TmThreadSetCPU(tv_detect_ncpu, WORKER_CPU_SET);
TmThreadSetGroupName(tv_detect_ncpu, "Detect");
tm_module = TmModuleGetByName("RespondReject");
if (tm_module == NULL) {
SCLogError(SC_ERR_RUNMODE, "TmModuleGetByName RespondReject failed");
exit(EXIT_FAILURE);
}
TmSlotSetFuncAppend(tv_detect_ncpu, tm_module, NULL);
if (TmThreadSpawn(tv_detect_ncpu) != TM_ECODE_OK) {
SCLogError(SC_ERR_RUNMODE, "TmThreadSpawn failed");
exit(EXIT_FAILURE);
}
}
- tname 线程名字:W#01 ,W#02…….
- qname 队列名字:pickup01,pickup02……
- TmThreadCreatePacketHandler(tname,qname, “flow”,”packetpool”, “packetpool”,”varslot”);
- 类似收包线程处理,创建线程并绑定队列
- 输入队列: tv->inq = pickup01……
- 输入处理句柄: tv->tmqh_in: TmqhInputFlow
- 输出处理句柄: tv->tmqh_out :TmqhOutputPacketpool
- 即 使用完的数据包扔回原始包池的意思。
- TmModule *tm_module = TmModuleGetByName(“FlowWorker”) 获取模块句柄
- TmSlotSetFuncAppend(tv_detect_ncpu, tm_module, NULL) 将模块注册进入slot链,
- 模式顺序: FlowWorker、Detect、RespondReject…
- TmThreadSetCPU 设置cpu亲和性绑定
- TmThreadSpawn worker线程启动
数据流转
** 让我们再回到 这个函数**:
ThreadVars *TmThreadCreate( const char *name, const char *inq_name,
const char *inqh_name,const char *outq_name,
const char *outqh_name, const char *slots,
void * (*fn_p)(void *), int mucond)
- 参数说明
- name 线程模块的名字
- inq_name 输入队列名称
- inqh_name 输入队列句柄名称,由TmqhSetup创建的
- outq_name 输出队列名称
- outqh_name 输出队列句柄名称,由TmqhSetup创建的
- slots 槽名称,后续使用
- fn_p 回调函数 Pointer to function when "slots" is of type "custom"
- mucond Flag to indicate whether to initialize the condition and the mutex variables for this newly created TV
- 线程变量初始化,返回句柄 ThreadVars *tv
- 设置线程名称、线程标志 THV_PAUSE&&THV_USE
- tv->inq,如果进来的inq_name不是“packetpool”,则是pickup0x,则创建新的输入队列。
- tv->tmqh_in 线程 输入队列句柄处理函数初始化
- tv->tmqh_out 线程 输出队列句柄处理函数初始化
- tv->outq 取决于outq_name 是否为”packetpool”,主要用于接受线程与work线程数据包传递包用
- tmqh->OutHandlerCtxSetup 调用TmqhOutputFlowSetupCtx 分离 outq_name “pickup1,pick2……”
接下来我们再来看一下 slot函数初始化过程:
static TmEcode TmThreadSetSlots(ThreadVars *tv, const char *name, void *(*fn_p)(void *))
- 参数说明
- tv 线程句柄
- name 即为上一个slots字符串名称
- fn_p 回调函数 Pointer to a custom slot function. Used only if slot variant “name” is “custom”
- tv->tm_func 线程函数注册 《这边是重点哦》
- varslot 对应 TmThreadsSlotVar , worker模式
- pktacqloop 对应 TmThreadsSlotPktAcqLoop, 接受线程模式
- management 对应 TmThreadsManagement ,管理线程模式
- command 对应 TmThreadsManagement
- custom 模式 如果 fn_p 不为NULL,则对应 fn_p函数
注册全部ok,那么让我们看一下 创建线程到底做了啥?
TmEcode TmThreadSpawn(ThreadVars *tv)
- 线程启动,tv线程句柄
- pthread_attr_init 线程属性初始化
- pthread_attr_setdetachstate 线程分离状态属性
- pthread_create(&tv->t, &attr, tv->tm_func, (void *)tv) 创建线程,调用tm_fun运行该线程主函数
-
TmThreadWaitForFlag(tv, THV_INIT_DONE THV_RUNNING_DONE) 等待线程结束标志,结束该线程 - TmThreadAppend 把线程类型 以及线程句柄扔回给 tv_root
下面到好戏了,让我们看看 主线程到底做什么
static void *TmThreadsSlotPktAcqLoop(void *td)
- 函数说明: 接受线程主函数
- UtilSignalBlock(SIGUSR2) 对USER2信号进行屏蔽,仅主线程有效
- SCSetThreadName 设置线程名称
- TmThreadSetupOptions 设置线程亲和性,进行线程绑定情况
- PacketPoolInit 数据池进行初始化 ,全局变量thread_pkt_pool ,根据yaml文件设定的max_pending_packets数量,对数据包池进行初始化
- 遍历slot链,进行数据包处理
- slot->SlotThreadInit 模块初始化
- StatsSetupPrivate 计数初始化
- TmThreadsSetFlag(tv, THV_INIT_DONE); 设定线程初始化完毕flag
- s->PktAcqLoop(tv, SC_ATOMIC_GET(s->slot_data), s); 启动线程循环,并调用主函数进行处理。抓包开始咯!
收包&解包
涉及文件:source-pfring.c
从上面 数据流转,我们看到接受线程,最终调用的是s->PktAcqLoop
即初始化:
tmm_modules[TMM_RECEIVEPFRING].PktAcqLoop = ReceivePfringLoop;
下面我们看一下 pfring关于 接受数据循环,到底做了哪些事情
TmEcode ReceivePfringLoop(ThreadVars *tv, void *data, void *slot)
- 参数说明
- tv 线程变量
- data 即为 SC_ATOMIC_GET(s->slot_data),即包含pfring 初始化过程中的一些信息
- slot 即为slot链句柄
- pfring_enable_ring 开启pfring
- suricata_ctl_flags & SURICATA_STOP 检查pfring运行状态
- ptv->slot = s->slot_next; 将slot函数转移到下一个可用节点函数。
- PacketPoolWait 数据包池是否为NULL等待。
- PacketGetFromQueueOrAlloc 获取数据包从包池
- PKT_SET_SRC(p, PKT_SRC_WIRE); 设置数据包状态
- pfring_recv pfring抓包,并填充进pkt_buffer,hdr
- PacketSetData && PfringProcessPacket 零拷贝将抓包信息,填充进预分配数据包中,而pfring包,会随着pfring环越来越大,最终释放。否则,拷贝数据包。
- TmThreadsSlotProcessPkt 将数据包做接下来处理,传递给decode线程
-
PfringDumpCounters 抓包计数
static inline TmEcode TmThreadsSlotProcessPkt(ThreadVars *tv, TmSlot *s, Packet *p)
- 检查s是否为NULL,若接下来的处理函数为NULL,则将数据包投递到下一个处理线程
- 否则调用 TmThreadsSlotVarRun
- SC_ATOMIC_GET(s->SlotFunc);
- SlotFunc(tv, p, SC_ATOMIC_GET(s->slot_data), &s->slot_pre_pq, &s->slot_post_pq);
- 即将数据包 投递给decode线程进行数据包 处理。
- tv->tmqh_out(tv, p); 将数据包投递给flow进行分发。
文章写道这边,相信读者已经清楚suricata,关于数据包流转的思想,主要是预先注册,然后使用slot链,将队列和线程串联起来。至于工作线程流转这边就略了,毕竟思想是类似的。数据流转清楚了,则梳理出了suricata的命脉,难道你还不能对suricata进行裁剪吗?
##流管理 数据流管理,讲到这边,首先我们需要先明确一下,数据流到底是什么?所谓数据流特指:具有相同特性五元组(源IP、源端口、目的IP、目的端口、传输协议)的一组集合。 下面我们通过结构体来认识一下suricata强大的数据流管理功能。
typedef struct Flow_
{
FlowAddress src, dst;
union {
Port sp; /**< tcp/udp source port */
uint8_t type; /**< icmp type */
};
union {
Port dp; /**< tcp/udp destination port */
uint8_t code; /**< icmp code */
};
uint8_t proto;
uint16_t vlan_id[2];
uint32_t flow_hash;
struct timeval lastts;
SC_ATOMIC_DECLARE(FlowStateType, flow_state);
SC_ATOMIC_DECLARE(FlowRefCount, use_cnt);
uint32_t tenant_id;
uint32_t probing_parser_toserver_alproto_masks;
uint32_t probing_parser_toclient_alproto_masks;
uint32_t flags; /**< generic flags */
uint16_t file_flags; /**< file tracking/extraction flags */
uint16_t protodetect_dp; /**< 0 if not used */
......
} Flow;
很显然,suricata是通过flow前面几个字段来确定五元组的。而其他一些字段则表明这一条数据流的特性,如flow_hash,则是表明这条数据流的五元组hash数值。
- FlowGetFlowFromHash(tv, dtv, p, &p->flow); 数据包寻找属于自身的数据流
-
p->flags = PKT_HAS_FLOW; 将数据包打上标志,表示已经找到归属
接下来我们会发现 所有应用层api、output输出层基本都会携带flow这个指针,flow作为一个线程全局数据结构,不仅携带了数据包基础属性,还携带了应用层数据解析结构指针。那么flow什么时候会释放呢?不然内存不是会不停上涨?那么我们必须依赖下面的线程来做这件事情。
void TmModuleFlowManagerRegister (void)
{
tmm_modules[TMM_FLOWMANAGER].name = "FlowManager";
tmm_modules[TMM_FLOWMANAGER].ThreadInit = FlowManagerThreadInit;
tmm_modules[TMM_FLOWMANAGER].ThreadDeinit = FlowManagerThreadDeinit;
tmm_modules[TMM_FLOWMANAGER].Management = FlowManager;
tmm_modules[TMM_FLOWMANAGER].cap_flags = 0;
tmm_modules[TMM_FLOWMANAGER].flags = TM_FLAG_MANAGEMENT_TM;
SC_ATOMIC_INIT(flowmgr_cnt);
SC_ATOMIC_INIT(flow_timeouts);
}
void TmModuleFlowRecyclerRegister (void)
{
tmm_modules[TMM_FLOWRECYCLER].name = "FlowRecycler";
tmm_modules[TMM_FLOWRECYCLER].ThreadInit = FlowRecyclerThreadInit;
tmm_modules[TMM_FLOWRECYCLER].ThreadDeinit = FlowRecyclerThreadDeinit;
tmm_modules[TMM_FLOWRECYCLER].Management = FlowRecycler;
tmm_modules[TMM_FLOWRECYCLER].cap_flags = 0;
tmm_modules[TMM_FLOWRECYCLER].flags = TM_FLAG_MANAGEMENT_TM;
SC_ATOMIC_INIT(flowrec_cnt);
} TmModuleFlowManagerRegister ---- 数据流管理线程主要是配合yaml关于数据流相关配置一起使用,在合适的时间、合适的内存点,进行数据流的老化管理。将老化的数据节点加到FlowEnqueue(&**flow_recycle_q**, f); TmModuleFlowRecyclerRegister ----- 取出老化后的数据节点FlowDequeue(&**flow_recycle_q**),进行重新清除数据,仍回到flow流表池**flow_spare_q**里面去。
应用层解析引擎
数据结构
typedef struct AppLayerParserProtoCtx_
{
/* 0 - to_server, 1 - to_client. */
int (*Parser[2])(Flow *f, void *protocol_state,
AppLayerParserState *pstate,
uint8_t *input, uint32_t input_len,
void *local_storage);
void *(*StateAlloc)(void);
void (*StateFree)(void *);
void (*StateTransactionFree)(void *, uint64_t);
void *(*LocalStorageAlloc)(void);
void (*LocalStorageFree)(void *);
......
} AppLayerParserProtoCtx;
typedef struct AppLayerParserCtx_ {
AppLayerParserProtoCtx ctxs[FLOW_PROTO_MAX][ALPROTO_MAX];
} AppLayerParserCtx;
/* Static global version of the parser context.
* Post 2.0 let's look at changing this to move it out to app-layer.c. */
static AppLayerParserCtx alp_ctx; 设定全局回调句柄用于应用层回调之用
应用协议注册
以SMTP为例:
void RegisterSMTPParsers(void)
- AppLayerProtoDetectConfProtoDetectionEnabled 匹配yaml关于协议的开关是否开启
- AppLayerProtoDetectRegisterProtocol 注册smtp协议号,交给全局变量alpd_ctx;
- SMTPRegisterPatternsForProtocolDetection 注册smtp协议识别特征 “EHLO”,”HELO”,”QUIT” 主要使用AppLayerProtoDetectPMRegisterPatternCI,确定传输协议 IPPROTO_TCP、协议号、协议特征、特征长度、特征相对偏移位置、以及特征所在报文的传输方向
- AppLayerParserRegisterStateFuncs 注册smtp协议相对当前线程的预分配 和 释放数据结构的句柄
- AppLayerParserRegisterParser 注册smtp 不同方向的数据包 进入不同的解析函数
###应用层数据回调
1、TCP数据报文应用层接口
int AppLayerHandleTCPData(ThreadVars *tv, TcpReassemblyThreadCtx *ra_ctx,
Packet *p, Flow *f,
TcpSession *ssn, TcpStream *stream,
uint8_t *data, uint32_t data_len,
uint8_t flags)
2、UDP 数据报文应用层接口
int AppLayerHandleUdp(ThreadVars *tv, AppLayerThreadCtx *tctx, Packet *p, Flow *f)
以UDP接口为例:
- p->flowflags 数据包的传输方向
- f->alproto 为ALPROTO_FAILED则返回
- f->alproto为ALPROTO_UNKNOWN 则进入协议识别引擎
- AppLayerProtoDetectGetProto 识别接口
- AppLayerProtoDetectPMGetProto 模式匹配算法进行payload识别,mpm_table中的算法根据配置文件确定,默认使用ac算法
- AppLayerProtoDetectPPGetProto 若PM未识别出来则进行端口识别
- AppLayerParserParse 识别出了协议进行数据申请回调解析
- AppLayerParserProtoCtx *p = &alp_ctx.ctxs[f->protomap][alproto]; 获取对应协议注册句柄
- p->StateAlloc 看有没有注册这个协议,没有则返回
- flags & STREAM_GAP 如果有gap则终止流程
- f->alparser = pstate = AppLayerParserStateAlloc(); 设置协议内部解析状态
- f->alstate = alstate = p->StateAlloc(); 数据结构申请内存,并将句柄指针挂载flow内部
- p->Parser(flags & STREAM_TOSERVER) ? 0 : 1 将数据传递给协议解析函数进行解析
#总结陈述 本文从suricata的运行模式,讲到数据包的流转,流管理、应用解析,从一个大的源码架构上,让读者先有一个整体认知,然后细化各个解析引擎。不过 细心的读者应该发现,本文并没有讲detect模块,因为detect引擎是suricata的核心规则检测模块,整体相对复杂,我们必须讲解snort规则之后,才能进行该模块的解析,这个之后会单独拎出来讲解。相信经过本篇学习,您已经学习到了如何利用suricata进行ids的一个处理过程,当然suricata的用途绝不仅仅于此,你可以替换suricata抓包引擎,可以裁剪suricata的流程等等方式,让suricata变得更加的高效。由于时间紧促,个人见解难免有所偏差,还希望读者提出来 一起参与讨论。文中若有理解错误之处,也请赐教。参与讨论方式:1、可以通过gitchat读者圈 2、可以加笔者QQ群524557245。让我们一起进步!