main()函数位于suricata.c文件,其主要流程如下:
1. 定义并初始化程序的全局实例变量。
2. 初始化sc_set_caps为FALSE –> 标识是否对主线程进行特权去除(drop privilege),主要是出于安全性考虑。
3. 初始化原子变量engine_stage –> 记录程序当前的运行阶段:SURICATA_INIT、SURICATA_RUNTIME、SURICATA_FINALIZE
4. 初始化日志模块,因为后续的执行流程中将使用日志输出,所以需要最先初始化该模块。
5. 设置当前主线程名字为“Suricata-Main”。线程名字还是挺重要的,至少在gdb调试时info threads可以看到各个线程名,从而可以精确地找到想要查看的线程。另外,在top -H时,也能够显示出线程名字(然而ps -efL时貌似还是只是显示进程名)。
6. 初始化ParserSize模块 –> 使用正则表达式来解析类似“10Mb”这种大小参数,其中正则引擎用的是pcre,因此初始化时就是调用pcre_compile、pcre_study对已经写好的正则表达式进行编译和预处理。
7. 注册各种运行模式。Suricata对“运行模式”这个概念也进行了封装。运行模式存储在runmodes数组中,定义为RunModes runmodes[RUNMODE_USER_MAX]。
8. 初始化引擎模式为IDS模式。引擎模式只有两种:IDS、IPS,初始默认为IDS,而在nfq或ipfw启用时,就会切换成IPS模式,该模式下能够执行“Drop”操作,即拦截数据包。
9. 初始化配置模块,为配置节点树建立root节点。
10. 解析命令行参数。其中,与包捕获相关的选项(如“-i”)都会调用LiveRegisterDevice,以注册一个数据包捕获设备接口(如eth0)。全局的所有已注册的设备接口存储在变量live_devices中,类型为LiveDevice。注意,用多设备同时捕获数据包这个特性在Suricata中目前还只是实验性的。“-v”选项可多次使用,每个v都能将当前日志等级提升一级。
11. 若运行模式为内部模式,则进入该模式执行,完毕后退出程序。
12. FinalizeRunMode,即为运行模式的处理划上句号。主要是设置offline标志、对unknown运行模式进行报错,以及设置全局的run_mode变量。
13. 若运行模式为单元测试模式,则跑(用户通过正则表达式指定的)单元测试,并输出测试结果。
14. 检查当前模式是否与daemon标志冲突。Pcap文件模式及单元测试模式都不能在daemon开启下进行。
15. 初始化全局变量。包括:数据包队列trans_q、数据队列data_queues(干嘛的?)、对应的mutex和cond、建立小写字母表。
16. 初始化时间。包括:获取当前时间所用的spin lock,以及设置时区(调用tzset()即可)。
17. 为快速模式匹配注册关键字。调用SupportFastPatternForSigMatchList函数,按照优先级大小插入到sm_fp_support_smlist_list链表中。
18. 若用户未在输入参数中指定配置文件,则使用默认配置文件(/etc/suricata/suricata.yaml)。
19. 调用LoadYamlConfig读取Yaml格式配置文件。Yaml格式解析是通过libyaml库来完成的,解析的结果存储在配置节点树(见conf.c)中。对include机制的支持:在第一遍调用ConfYamlLoadFile载入主配置文件后,将在当前配置节点树中搜寻“include”节点,并对其每个子节点的值(即通过include语句所指定的子配置文件路径),同样调用ConfYamlLoadFile进行载入。
20. 再次初始化日志模块。这次,程序将会根据配置文件中日志输出配置(logging.outputs)填充SCLogInitData类型的结构体,调用SCLogInitLogModule重新初始化日志模块。
21. 打印版本信息。这是Suricata启动开始后第一条打印信息。
22. 打印当前机器的CPU/核个数,这些信息是通过sysconf系统函数获取的。
23. 若运行模式为DUMP_CONFIG,则调用ConfDump打印出当前的所有配置信息。ConfDump通过递归调用ConfNodeDump函数实现对配置节点树的DFS(深度优先遍历)。
24. 执行PostConfLoadedSetup,即运行那些需要在配置载入完成后就立马执行的函数。这里面涉及的流程和函数非常多:
25. 检查是否进入Daemon模式。若需要进入Daemon模式,则会检测pidfile是否已经存在(daemon下只能有一个实例运行),然后进行Daemonize,最后创建一个pidfile。Daemonize的主要思路是:fork->子进程调用setsid创建一个新的session,关闭stdin、stdout、stderr,并告诉父进程 –> 父进程等待子进程通知,然后退出 –> 子进程继续执行。
26. 初始化信号handler。首先为SIGINT(ctrl-c触发)和SIGTERM(不带参数kill时触发)这两个常规退出信号分别注册handler,对SIGINT的处理是设置程序的状态标志为STOP,即让程序优雅地退出;而对SIGTERM是设置为KILL,即强杀。接着,程序会忽略SIGPIPE(这个信号通常是在Socket通信时向已关闭的连接另一端发送数据时收到)和SIGSYS(当进程尝试执行一个不存在的系统调用时收到)信号,以加强程序的容错性和健壮性。
27. 获取配置文件中指定的Suricata运行时的user和group,如果命令行中没有指定的话。然后,将指定的user和group通过getpwuid、getpwnam、getgrnam等函数转换为uid和gid,为后续的实际设置uid和gid做准备。注意,这段代码也是在InitSignalHandler中执行的,不知道为什么放这里,跟信号有关系么。。。
28. 初始化Packet pool,即预分配一些Packet结构体,分配的数目由之前配置的max_pending_packets确定,而数据包的数据大小由default_packet_size确定(一个包的总占用空间为default_packet_size+sizeof(Packet))。在调用PacketGetFromAlloc新建并初始化一个数据包后,再调用PacketPoolStorePacket将该数据包存入ringbuffer。Suricata中用于数据包池的Ring Buffer类型为RingBuffer16,即容量为2^16=65536(但为什么max_pending_packets的最大值被限定为65534呢?)。
29. 初始化Host engine。这货好像跟之前的host类型的storage有关系,具体怎么用后面再看看吧。
30. 初始化Flow engine。跟前面的host engine类似,不过这个的用处就很明显了,就是用来表示一条TCP/UDP/ICMP/SCTP流的,程序当前所记录的所有流便组成了流表,在flow引擎中,流表为flow_hash这个全局变量,其类型为FlowBucket *,而FlowBucket中则能够存储一个Flow链表,典型的一张chained hash Table。在初始化函数FlowInitConfig中,首先会使用配置文件信息填充flow_config,然后会按照配置中的hash_size为流表实际分配内存,接着按照prealloc进行流的预分配(FlowAlloc->FlowEnqueue,存储在flow_spare_q这个FlowQueue类型的队列中),最后调用FlowInitFlowProto为流表所用于的各种流协议进行配置,主要是设置timeout时间。
31. 初始化Decect engine。若配置文件中未指定mpm(多模式匹配器),则默认使用AC,即使用mpm_table中AC那一项。SRepInit函数(与前面的SCReputationInitCtx不同!)会初始化检测引擎中域reputaion相关信息,即从配置文件中指定的文件中读取声望数据。其余配置比较复杂,暂不关注。
32. 读取和解析classification.config和reference.config,这两个文件用于支持规则格式中的classification(规则分类)和refercence(规则参考资料)字段。
33. 设置规则的动作优先级顺序,默认为Pass->Drop->Reject->Alert。举例来说,若有一条Pass规则和Drop规则都匹配到了某个数据库,则会优先应用Pass规则。
34. 初始化Magic模块。Magic模块只是对libmagic库进行了一层封装,通过文件中的magic字段来检测文件的类型(如”PDF-1.3“对应PDF文件)。
35. 设置是否延迟检测。若delayed-detect为yes,则系统将在载入规则集之前就开始处理数据包,这样能够在IPS模式下将少系统的down time(宕机时间)。
36. 如果没有设置延迟检测,就调用LoadSignatures载入规则集。
37. 如果设置了live_reload,则重新注册用于规则重载的SIGUSR2信号处理函数(这次是设置为真正的重载处理函数)。放在这里是为了防止在初次载入规则集时就被触发重载。
38. 初始化ASN.1解码模块。Wikipedia:ASN.1(Abstract Syntax Notation One) 是一套标准,是描述数据的表示、编码、传输、解码的灵活的记法。应用层协议如X.400(email)、X.500和LDAP(目录服务)、H.323(VoIP)和SNMP使用 ASN.1 描述它们交互的协议数据单元。
39. 处理CoreDump相关配置。Linux下可用prctl函数获取和设置进程dumpable状态,设置corefile大小则是通过通用的setrlimit函数。
40. 调用gettimeofday保存当前时间,存储在suri->start_time中,作为系统的启动时间。
41. 去除主线程的权限。这个是通过libcap-ng实现的,首先调用capng_clear清空所有权限,然后根据运行模式添加一些必要权限(主要是为了抓包),最后调用capng_change_id设置新的uid和gid。主线程的权限应该会被新建的子线程继承,因此只需要在主线程设置即可。
42. 初始化所有Output模块。这些模块之前在线程模块注册函数里已经都注册了,这里会根据配置文件再进行配置和初始化,最后把当前配置下启用了的output模块放到RunModeOutputs链表中。
43. 若当前抓包模式下未指定设备接口(通过-i <dev>或--pcap=<dev>等方式),则解析配置文件中指定的Interface,并调用LiveRegisterDevice对其进行注册。
44. 若当前的模式为CONF_TEST,即测试配置文件是否有效,则现在就可以退出了。这也说明,程序运行到这里,配置工作已经基本完成了。
45. 初始化运行模式。首先,根据配置文件和程序中的默认值来配置运行模式(single、auto这些),而运行模式类型(PCAP_DEV、PCAPFILE这些)也在之前已经确定了,因此运行模式已经固定下来,可以从runmodes表中获取到特定的RunMode了,接着就调用RunMode中的RunModeFunc,进入当前运行模式的初始化函数。以PCAP_DEV类型下的autofp模式为例,该模式的初始化函数为:RunModeIdsPcapAutoFp。这个函数的执行流程为:
46. 若unix-command为enable状态,则创建Unix-socket命令线程,可与suricata客户端使用JSON格式信息进行通信。命令线程的创建是通过TmThreadCreateCmdThread函数,创建的线程类型为TVT_CMD。线程执行函数为UnixManagerThread。
47. 创建Flow管理线程,用于对流表进行超时删除处理。管理线程创建是通过TmThreadCreateMgmtThread函数,类型为TVT_MGMT,执行函数为FlowManagerThread。
48. 初始化Stream TCP模块。其中调用了StreamTcpReassembleInit函数进行重组模块初始化。
49. 创建性能计数相关线程,包括一个定期对各计数器进行同步的唤醒线程(SCPerfWakeupThread),和一个定期输出计数值的管理线程(SCPerfMgmtThread)。
50. 检查数据包队列的状态是否有效:每个数据包队列都应该至少有一个reader和一个writer。在前面线程绑定inq时会增加其reader_cnt,绑定outq时会增加其writer_cnt。
51. 等待子线程初始化完成。检查是否初始化完成的方式是遍历tv_root,调用TmThreadsCheckFlag检查子线程的状态标志。
52. 更新engine_stage为SURICATA_RUNTIME,即程序已经初始化完成,进入运转状态。这里的更新用的是原子CAS操作,防止并发更新导致状态不一致(但目前没在代码中只到到主线程有更新engine_stage操作,不存在并发更新)。
53. 让目前处于paused状态的线程继续执行。在TmThreadCreate中,线程的初始状态设置为了PAUSE,因此初始化完成后就会等待主线程调用TmThreadContinue让其继续。从这以后,各线程就开始正式执行其主流程了。
54. 若设置了delayed_detect,则现在开始调用LoadSignatures加载规则集,激活检测线程,并注册rule_reload信号处理函数。这里,激活检测线程是通过调用TmThreadActivateDummySlot函数,这个函数会将之前注册的slot中的slotFunc替换为实际操作函数,而不是原先在delayed_detect情况下设置的什么都不做的TmDummyFunc。
55. 进入死循环。若受到引擎退出信号(SURICATA_KILL或SURICATA_STOP),则退出循环,执行后续退出操作,否则就调用TmThreadCheckThreadState检查各线程的状态,决定是否进行结束线程、重启线程、终止程序等操作,然后usleep一会儿(1s),继续循环。
56. 接着,程序就进入到了退出阶段,首先会更新engine_stage为SURICATA_DEINIT,然后依次关闭Unix-socket线程、Flow管理线程。
57. 停止包含抓包或解码线程模块的线程。这个是通过TmThreadDisableThreadsWithTMS实现,里面会检查每个线程的slots里嵌入的线程模块的flags中是否包含指定的flag(这里是TM_FLAG_RECEIVE_TM或TM_FLAG_DECODE_TM),一个线程模块的flags在注册时就已经指定了。关闭是通过向线程发送KILL信号(设置线程变量的THV_KILL标志)实现,收到该信号的线程会进入RUNNING_DONE状态,然后等待主线程下一步发出DEINIT信号。
58. 强制对仍有未处理的分段的流进行重组。
59. 打印进程运行的总时间(elapsed time)。
60. 在rule_reload开启下,首先同样调用TmThreadDisableThreadsWithTMS停止检测线程。特别地,该函数对于inq不为"packetpool"的线程(即该线程从一个PakcetQueue中获取数据包),会等到inq中的数据包都处理完毕再关闭这个线程。然后,检测是否reload正在进行,如果是则等待其完成,即不去打断它。
61. 杀死所有子线程。杀死线程的函数为TmThreadKillThread,这个函数会同时向子线程发出KILL和DEINIT信号,然后等待子线程进入CLOSED状态,之后,再调用线程的清理函数(InShutdownHandler)以及其对应的ouqh的清理函数(OutHandlerCtxFree),最后调用pthread_join等待子线程退出。
62. 执行一大堆清理函数:清理性能计数模块、关闭Flow engine、清理StreamTCP、关闭Host engine、清理HTP模块并打印状态、移除PID文件、关闭检测引擎、清理应用层识别模块、清理Tag环境、关闭所有输出模块,etc…
63. 调用exit以engine_retval为退出状态终止程序。