一、简介
在本文中,我们将看到ndpi提供的的分析函数ndpi_detection_process_packet的主要实现。以及ndpi内部是如何对FTP协议实现深度包检测技术。对于前期的初始化和API的使用,请参考附录中的文章。欢迎大家一起讨论学习。然后我们这次会带着下面几个问题阅读源代码:
1、ndpi的分析函数进行了怎么样的工作?
2、ndpi内部是如何实现深度包检测技术的?
3、ndpi里面,对于多种协议的检测是基于怎样的准则进行选择?
注意:因为限于篇幅需要下面进行分析默认针对ipv4,如果有不正确的地方欢迎大家一起来讨论学习
二、分析函数API的实现
ndpi内部提供了ndpi_detection_process_packet(在ndpi_main.c中进行了定义)函数作为协议检测的API,函数原型及解释如下:
unsigned int ndpi_detection_process_packet(
struct ndpi_detection_module_struct *ndpi_struct,
//ndpi_detection_module_struct在附录2中进行了详细的叙述,在ndpi_init_detection_module函数进行初始化
struct ndpi_flow_struct *flow,
//用来维护当前会话的检测协议栈,以及检测的参数等
const unsigned char *packet,
//ip层数据报文
const unsigned short packetlen,
//报文长度
const u_int32_t current_tick,
//包的tick值
struct ndpi_id_struct *src,struct ndpi_id_struct *dst)
//ndpi_id_struct里面包含各个协议的源目的端信息
1、检测包长度
这里它通过检测包长度(packetlen),对包的可用性进行了测试。如果包长度没有20字节(ip数据报文至少20字节),则利用ndpi_int_reset_packet_protocol把flow内部的协议栈顶类型置为UNKNOW。并且清0协议栈信息字段,最后返回UNKNOW类型。这里关于flow协议栈的结构和使用我们将在第三章的FTP协议检测中详细进行介绍。
2、flow->packet的初始化
这里通过捕获的ip报文(packet参数)和用户设置的心跳值(current_tick参数),对flow->packet.iph和flow->packet.tick_timestamp进行初始化。
3、传输层检测及flow的初始化
这里主要通过函数ndpi_init_packet_header(在ndpi_main.c中进行了定义)进行了实现,他完成比较多的工作。
1)首先一个就是根据ndpi_packet_struct中的协议栈内容和描述信息,对flow的协议栈内容和描述信息进行了初始化。这部分通过内部的ndpi_apply_flow_protocol_to_packet函数进行了实现。
2)根据ipv4和ipv6对flow中的packet分别进行初始化(flow->packet.iph和flow->packet.iphv6)
3)通过ndpi_detection_get_l4_internal对报文的ipv4(ipv6)header进行检测,并且获取传输层协议信息。通过l4protocol变量进行传递,记录传输层协议号。
4)根据l4protocol字段进行传输层的判别,如果是tcp(协议号是6)则对包内部的syn和ack等字段进行初步的检测。如果是udp(协议号是17),则计算出包的长度。初始化flow->packet当中的字段
4、flow传输层信息的初始化
这里主要通过报文获取传输层信息,比如在tcp协议中我们捕获到的报文是握手中的什么角色,是ack包还是其他的。这些信息将对检测提供一些数据。
1)首先通过src和dst参数初始化flow->src和flow->dst字段
2)通过ndpi_connection_tracking函数进行我们上述的工作。这里它判断的tcp握手的状态,并且通过flow->next_tcp_seq_nr数组对tcp序列进行了描述。
我们这里拿一段代码来一起进行分析。看看具体是怎样操作的。
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;}
上面这段代码中,我们不难看出ndpi中通过flow->l4.tcp中的seen_syn、seen_syn_ack和seen_ack记录tcp的握手状态。然后根据分析报文中的syn和ack字段进行归类,为后期的检测提供数据基础。
5、建立传输层的描述
这一部分通过ndpi_selection_packet进行记录,主要是提供给下面第6部分一个传输层上分类的检测。这里的ndpi_selection_packet将记录传输层的信息。包括是tcp还是udp,里面还检测不出来时的灰色选项(tcp or udp)。数据结构这里通过一个整形进行记录,大家还有不明白可以参考附录中的ndpi协议的注册及维护的文章。里面第三章的第一点详细讲述了该方法。
6、调用对应的检测函数
这里ndpi根据第5点提供的参考,对捕获的数据包进行了传输层的分类。分成tcp、udp和none tcp&&udp(既不是tcp也不是udp)。然后对于每个类别,调用其对用的注册函数。
注:关于检测协议函数的注册,我们在协议的注册与维护一文中进行了详细的介绍。具体请参考附录2
我们这里列一段代码来一起看看他的具体操作是怎么样的:
if (flow != NULL && flow->packet.tcp != NULL) {
//....tcp类别
} else if (flow != NULL && flow->packet.udp != NULL) {
//....udp类别
for (a = 0; a < ndpi_struct->callback_buffer_size_udp; a++) {
//对每个udp协议,ndpi内部根据先前的检测结果,判断是否应该继续进行检测。ndpi内部很多运用移位和与或运算,有兴趣的朋友可以先去看看这些
if ((ndpi_struct->callback_buffer_udp[a].ndpi_selection_bitmask & ndpi_selection_packet) ==
ndpi_struct->callback_buffer_udp[a].ndpi_selection_bitmask
&& NDPI_BITMASK_COMPARE(flow->excluded_protocol_bitmask,
ndpi_struct->callback_buffer_udp[a].excluded_protocol_bitmask) == 0
&& NDPI_BITMASK_COMPARE(ndpi_struct->callback_buffer_udp[a].detection_bitmask,
detection_bitmask) != 0) {
//这里调用在初始化阶段注册的检测函数,func字段是一个函数指针。我们接下来的FTP协议也是通过这种方式进行调用。不过FTP是tcp类别的
ndpi_struct->callback_buffer_udp[a].func(ndpi_struct, flow);
if(flow->detected_protocol_stack[0] != NDPI_PROTOCOL_UNKNOWN)
break; /* Stop after detecting the first protocol */
}
}
} else {
//....非tcp&&udp类别
}
三、FTP深度包检测的实现
这里我们将介绍一下,ndpi里面FTP的深度深度包检测是怎么实现。通过上述的第二章第6点的func调用,我们在ndpi_detection_process_packet函数内部的tcp传输层类别调用FTP的检测函数ndpi_search_ftp_tcp。具体定义在<ndpi home>/src/lib/protocol/中,里面囊括了ndpi支持的所有协议的源代码。我们马上一起来看看ndpi是怎么样完成FTP的深度包检测的。这一部分我们分为两part进行介绍,具体如下:
1、源和目的ip及端口检测
这里首先是两个分别针对源端口和目的端口的if语句检查,这里应该是考虑到FTP请求发起方的问题。检查内容包括:
1)src非空
2)ip报文的源ip地址和src中的FTP地址是否匹配
3)协议栈顶不等于UNKNOW类型
4)比较src中的检测协议映射是否包含FTP协议
5)src中ftp_timer的设置非0
然后完成上面的工作之后,如果会话的目的端口号大于1024,并且源端口号大于1024或等于20。则判定这个会话为FTP会话,通过ndpi_int_ftp_add_connection(这个函数在ftp.c中定义和实现,具体实现的函数在ndpi_main.c中抽象出来和其他检测协议共用)修改协议栈。这里因为调用关系比较多,我们不列代码了。我们将在下面第2部分,针对栈的实现和操作进行介绍。
2、检测协议栈的操作
ndpi内部的ndpi_flow_struct中通过数组和自定义的栈描述描述信息,实现了深度检测的栈结构。其实属性Linux内核的人会发现,这个结果和进程内核栈的实现是非常相似的。只是比较简单,没有了想thread_info那样的索引。栈本身是通过detected_protocol_stack数组进行实现,然后在protocol_stack_info字段中定义了两个信息来描述这个栈。一个是entry_is_real_protocol,它描述的是栈中是否检测出了正确的协议。第二个是current_stack_size_minus_one,它描述的是栈的大小。detected_protocol_stack栈中的是从高地址开始的,也就是detected_protocol_stack[0]代表的是栈顶。current_stack_size_minus_one表示的栈底部。具体定义如下:
typedef struct ndpi_flow_struct {
u_int16_t detected_protocol_stack[NDPI_PROTOCOL_HISTORY_SIZE]; //define in ndpi_struct.h中
#if NDPI_PROTOCOL_HISTORY_SIZE > 1
# if NDPI_PROTOCOL_HISTORY_SIZE > 5
# error protocol stack size not supported
# endif
struct {
u_int8_t entry_is_real_protocol:5;
u_int8_t current_stack_size_minus_one:3;
}//栈描述结构
#if !defined(WIN32)
__attribute__ ((__packed__))
#endif
protocol_stack_info;
#endif
.......
} ndpi_flow_struct_t;
对栈的操作,并没有进行封装。直接在各自的功能函数中进行实现,以上面的添加协议为例,将最终在ndpi_int_change_flow_protocol(ndpi_main.c中实现)进行操作。这里的函数是在FTP的检测函数ndpi_search_ftp_tcp内部通过ndpi_int_ftp_add_connection协议操作函数,最终在ndpi_main.c中得到调用和实现。具体的调用过程这里省略,有兴趣的朋友可以联系我。我们这里列一段ndpi_int_change_flow_protocol的栈操作代码,其实并没有我们相信中那么难。
/* now shift and insert */
for (a = stack_size - 1; a > 0; a--) {
flow->detected_protocol_stack[a] = flow->detected_protocol_stack[a - 1];
}
flow->protocol_stack_info.entry_is_real_protocol <<= 1;
/* now set the new protocol */
flow->detected_protocol_stack[0] = detected_protocol;
上面这几行代码,实现了栈的滑动和插入过程。没有想象中那么难吧。
3、FTP主动模式和被动模式的检测
这里我们需要补充一点背景知识:
FTP主动模式:由服务器端发FTP请求
FTP被动模式:由客户端发送FTP请求
在ndpi_search_ftp_tcp中,ndpi通过search_passive_ftp_mode和search_active_ftp_mode调用并进行实现。这里因这两个函数原理差不多,我们针对search_passive_ftp_mode进行介绍。这里主要采用的方法是对捕获的FTP数据报文进行搜索,包搜索的实现是通过memcmp函数进行实现,这个是公共库中的函数。在#include <string.h>或#include<memory.h>中,我们可以获得这个函数。函数原型如下:
int memcmp(const void *buf1, const void *buf2, unsigned int count);
当buf1<buf2时,返回值<0
当buf1=buf2时,返回值=0
当buf1>buf2时,返回值>0
然后工具有了,接下来就是利用这个函数对捕获的包进行比较。看看有没有被动模式包含的字段,这里我们再列一段代码一起看看:
if (packet->payload_packet_len > 34 && ndpi_mem_cmp(packet->payload, "229 Entering Extended Passive Mode", 34) == 0) {
if (dst != NULL) {
ndpi_packet_src_ip_get(packet, &dst->ftp_ip);
dst->ftp_timer = packet->tick_timestamp;
dst->ftp_timer_set = 1;
NDPI_LOG(NDPI_PROTOCOL_FTP, ndpi_struct, NDPI_LOG_DEBUG, "saved ftp_ip, ftp_timer, ftp_timer_set to dst");
NDPI_LOG(NDPI_PROTOCOL_FTP, ndpi_struct, NDPI_LOG_DEBUG,
"FTP Extended PASSIVE MODE FOUND: use Server %s\n", ndpi_get_ip_string(ndpi_struct, &dst->ftp_ip));
}
if (src != NULL) {
ndpi_packet_dst_ip_get(packet, &src->ftp_ip);
src->ftp_timer = packet->tick_timestamp;
src->ftp_timer_set = 1;
NDPI_LOG(NDPI_PROTOCOL_FTP, ndpi_struct, NDPI_LOG_DEBUG, "saved ftp_ip, ftp_timer, ftp_timer_set to src");
NDPI_LOG(NDPI_PROTOCOL_FTP, ndpi_struct, NDPI_LOG_DEBUG,
"FTP Extended PASSIVE MODE FOUND: use Server %s\n", ndpi_get_ip_string(ndpi_struct, &src->ftp_ip));
}
return;
}
上述代码中,通过if中的判断如果包长度大于34,并且能匹配到229 Entering Extended Passive Mode。则进入判断分别对src和dst进行非空的测试,最后通过NDPI_LOG记录进去日志中。注:这里的src和dst并不是指源端口,而是上文中描述的ndpi_id_struct结构。
四、附录
附录一:ndpi网站(源代码用svn详细见网站介绍):
附录二:协议的注册与维护——ndpi源码分析
附录三:pcapreader——源码分析
最后谢谢在这个过程中,williamy给予我的帮助。