这里以ndpi的例程ndpiReader.c为例,讲述一下ndpi从抓包到最终分析出具体协议的流程。简单来讲ndpi是从下层开始逐层向上对数据包进行分析的。
先上一发自己画的流程图
这张图是我在最开始看ndpi源码的时候做的流程图,还不是非常的清楚和正确,就连函数的调用关系也只是按先后顺序画的,现在看起来真是有点low,不过也大致说明了一些问题,也就懒得修改了。我会在接下来的文章中说明。这里如果大家想真正了解其工作流程的话,最好自己通过gdb工具进行调试,进入example文件夹,运行gdb -tui ndpiReader命令,在test文件夹下找到.pcap文件或者自己上网抓个包都行,然后在gdb命令行中设置断点,运行 r -i *.pcap即可进入调试模式~~可以百度一下gdb调试相关知识。
第一步是程序的初始化,调用setupDetection()函数,这里所做的工作也比较多,打算新写一篇文章来专门讲述此函数作用。
接下来会开启线程调用libpcap库函数对通过电脑网卡的数据包进行抓取,或者读取传入的.pcap文件(具体的 如何运行等简单操作可以参考官方给出的文档,在doc文件夹下)
接下来对每一个数据包(这里需要明确两个概念,数据包(packet)和数据流(flow),一个数据流中可能会有很多个数据包,就像我们申请一个网页请求,由于页面信息很大,所以会分成很多个数据包来传输,但这些数据包同属于一个数据流),首先对其数据链路层和IP层进行拆包分析pcap_packet_callback()函数,判断是否为基于IP协议等,并获得其源目的IP、协议类型等。
在接下来调用packet_processing()函数,进行传输层分析。在进行传输层分析时调用了get_ndpi_flow()函数,该函数返回ndpi_flow这个结构体(这里需要注意ndpi_flow和ndpi_flow_struct两个结构体的区别)。在get_ndpi_flow()函数中获取传输层的信息如源目的端口等信息。然后根据(源目的IP、源目的端口、协议类型(tcp\udp))这五个元素计算出idx。
idx = (vlan_id + lower_ip + upper_ip + iph->protocol + lower_port + upper_port) % NUM_ROOTS;
ret = ndpi_tfind(&flow, &ndpi_thread_info[thread_id].ndpi_flows_root[idx], node_cmp);
这里就是我刚开始开源码时困扰我的地方,开始一直不知道idx的作用,后来发现程序维护了一个数组,用来记录所有的数据流,而idx是用来标识不同的数据流,根据前面解析出数据包的五元组计算idx,然后查询 ndpi_flows_root[]这个数组在索引为idx位置是否已经有了记录。一般,对于一个数据流而言,该流的第一个数据包查询时ndpi_flows_root[idx]为空,则建立一个新的ndpi_flow对象并保存到该位置处;等抓到该数据流的后续数据包时,因为属于同一个流(即idx相同),所以ndpi_flows_root[idx]不为空,则直接返回已经有的ndpi_flow即可。至此,我们得到了ndpi_flow这个结构体,这也是get_ndpi_flow()这个函数的意义。
接下来函数会调用ndpi_detection_process_packet()这个函数进行应用层分析。这也是应用协议分析的主体函数。注意这个函数传进的参数是ndpi_flow_struct(下面记为flow),函数首先会对flow->packet即对packet这个结构体进行初始化。因为对于同一个流flow而言,在该结构体中有些变量在第一个数据包时已经初始化了,这些变量可能在特定情况下才会发生改变,比如检测出了协议等;而对每一个数据包,flow中必须要变的就是flow->packet中的信息。接下来会调用ndpi_connection_tracking()函数,这个函数的主要作用是判断这个包的‘位置’,熟悉tcp协议的人都知道,一个tcp经过三次握手建立连接bababababa….这里自行脑补,主要要知道syn,ack,seq,ack_seq四个变量的作用和功能。这个函数在数据包重组等功能中会有很重要的作用。这里贴出部分代码
if(tcph->syn != 0 && tcph->ack == 0 && flow->l4.tcp.seen_syn == 0 && flow->l4.tcp.seen_syn_ack == 0
&& flow->l4.tcp.seen_ack == 0) {
flow->l4.tcp.seen_syn = 1;
}//第一次
if(tcph->syn != 0 && tcph->ack != 0 && flow->l4.tcp.seen_syn == 1 && flow->l4.tcp.seen_syn_ack == 0
&& flow->l4.tcp.seen_ack == 0) {
flow->l4.tcp.seen_syn_ack = 1;
}//第二次
if(tcph->syn == 0 && tcph->ack == 1 && flow->l4.tcp.seen_syn == 1 && flow->l4.tcp.seen_syn_ack == 1
&& flow->l4.tcp.seen_ack == 0) {
flow->l4.tcp.seen_ack = 1;
}//第三次
//上面三句是三次握手相应的判断语句
if((flow->next_tcp_seq_nr[0] == 0 && flow->next_tcp_seq_nr[1] == 0)
|| (proxy_enabled && (flow->next_tcp_seq_nr[0] == 0 || flow->next_tcp_seq_nr[1] == 0))) {
if(tcph->ack != 0) {
//packet_direction表示方向是从源IP到目的IP\从目的IP到源IP
flow->next_tcp_seq_nr[flow->packet.packet_direction] =
ntohl(tcph->seq) + (tcph->syn ? 1 : packet->payload_packet_len);
if(!proxy_enabled) {
flow->next_tcp_seq_nr[1 -flow->packet.packet_direction] = ntohl(tcph->ack_seq);
}
}
} else if(packet->payload_packet_len > 0) {
/* check tcp sequence counters */
if(((u_int32_t)
(ntohl(tcph->seq) -
flow->next_tcp_seq_nr[packet->packet_direction])) >
ndpi_struct->tcp_max_retransmission_window_size) {
packet->tcp_retransmission = 1;
}
接下来会对ndpi_selection_packet进行设置,这个变量主要记录每个数据包的下四层信息。该变量是NDPI_SELECTION_BITMASK_PROTOCOL_SIZE类型,大概就是一个10101011这样的东西,如下面代码所示,把这几个变量进行与或操作得到一个值,比如110111101中的两个0就表示不是IPV6和不是TCP。
#define NDPI_SELECTION_BITMASK_PROTOCOL_SIZE u_int32_t
#define NDPI_SELECTION_BITMASK_PROTOCOL_IP (1<<0)
#define NDPI_SELECTION_BITMASK_PROTOCOL_INT_TCP (1<<1)
#define NDPI_SELECTION_BITMASK_PROTOCOL_INT_UDP (1<<2)//移位操作
#define NDPI_SELECTION_BITMASK_PROTOCOL_INT_TCP_OR_UDP (1<<3)
#define NDPI_SELECTION_BITMASK_PROTOCOL_HAS_PAYLOAD (1<<4)
#define NDPI_SELECTION_BITMASK_PROTOCOL_NO_TCP_RETRANSMISSION (1<<5)
#define NDPI_SELECTION_BITMASK_PROTOCOL_IPV6 (1<<6)
#define NDPI_SELECTION_BITMASK_PROTOCOL_IPV4_OR_IPV6 (1<<7)
#define NDPI_SELECTION_BITMASK_PROTOCOL_COMPLETE_TRAFFIC (1<<8)
在接下来调用下面代码,这里的guessed_protocol_id 我还没有搞明白是做什么用的,之后用到再看吧
flow->guessed_protocol_id = (int16_t)ndpi_guess_protocol_id(ndpi_struct, protocol,
sport, dport);
flow->protocol_id_already_guessed = 1;
最后,调用check_ndpi_flow_func()函数进行具体应用协议的检测,这里会根据tcp\udp\二者都不进入不同的接口。这里的东西也比较多,暂时想着针对http协议类型再写一篇文章,所以就不详细叙述了。经过这个函数之后如果仍然没有检测出协议类型,那么就继续检测下一个数据包直到检测出该数据流的协议类型为止。