在前面的文章中,我们对ndpi中的example做了源码分析。这一次我们将尽可能深入的了解ndpi内部的结构和运作。我们将带着下面三个目的(问题)去阅读ndpi的源代码。
1、ndpi内部是怎么样注册和维护需要检测的协议呢?
2、ndpi在初始化的过程中,做了怎么样的工作?
3、ndpi在底层的实现中具体又是使用怎样的数据结构?
注:这里限于篇幅,本文章指针对使用中的初始化部分进行源码分析。主体的分析函数和具体的各个协议将在后面的文中陆续介绍。如果有不正确或者理解不到位的地方,欢迎大家一起讨论。
一、索引
在上文介绍的example(pcapReader)中给了我们一个非常有用的导航。就是setupDetection这个函数,他里面基本包含了我们初始化ndpi所用到的常用函数。在接下来的源码分析中,我们以这个函数为索引。逐步地窥探ndpi的内部。具体如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 |
static
void
setupDetection
(
void
)
{
NDPI_PROTOCOL_BITMASK
all
;
if
(
ndpi_struct
==
NULL
)
{
printf
(
"ERROR: global structure initialization failed
\n
"
);
exit
(
-
1
);
}
ndpi_struct
=
ndpi_init_detection_module
(
detection_tick_resolution
,
malloc_wrapper
,
free_wrapper
,
debug_printf
);
// enable all protocols
NDPI_BITMASK_SET_ALL
(
all
);
ndpi_set_protocol_detection_bitmask2
(
ndpi_struct
,
&
all
);
size_id_struct
=
ndpi_detection_get_sizeof_ndpi_id_struct
();
size_flow_struct
=
ndpi_detection_get_sizeof_ndpi_flow_struct
();
// clear memory for results
memset
(
protocol_counter
,
0
,
sizeof
(
protocol_counter
));
memset
(
protocol_counter_bytes
,
0
,
sizeof
(
protocol_counter_bytes
));
memset
(
protocol_flows
,
0
,
sizeof
(
protocol_flows
));
// array length :NDPI_MAX_SUPPORTED_PROTOCOLS + NDPI_MAX_NUM_CUSTOM_PROTOCOLS + 1
if
(
_protoFilePath
!=
NULL
)
//通过parseOptions函数中的命令行分析的-p选项获取文件路径
ndpi_load_protocols_file
(
ndpi_struct
,
_protoFilePath
);
//int ndpi_load_protocols_file(struct ndpi_detection_module_struct *ndpi_mod, char* path)
raw_packet_count
=
ip_packet_count
=
total_bytes
=
0
;
ndpi_flow_count
=
0
;
}
|
在上面的函数中,主要是前面的11行的操作。11行后面的操作主要是针对pcapReader本身的应用需要(其实上面我们已经标注了简单的注释)。接下来我们将分为两个阶段进行介绍。
注:二、三中标题的选取只是因为比重关系,上面源码中的宏等我们都将在下面进行介绍
二、ndpi_init_detection_module
这部分我们将介绍第8行中的初始化函数ndpi_init_detection_module,上方第3行中的定义等都将在第三部分系统地进行介绍。ndpi_init_detection_module的实现是在ndpi_main.c文件中。这个函数的工作并不是维护协议,而是更加底层的参数的初始化。函数结束的时候将返回ndpi_detection_module_struct类型指针。这个类型将贯穿整个初始化过程,提供存放参数的容器以及设置的数据。下面我们将一一进行介绍:
函数原型:
struct ndpi_detection_module_struct *ndpi_init_detection_module(u_int32_t ticks_per_second,
void* (*__ndpi_malloc)(unsigned long size),
void (*__ndpi_free)(void *ptr),
ndpi_debug_function_ptr ndpi_debug_printf)
1、内存管理函数的初始化
_ndpi_malloc = __ndpi_malloc;
_ndpi_free = __ndpi_free;
这里的__ndpi_malloc和__ndpi_free就是我们自己定义的函数,而这里等号左边的_ndpi_malloc和_ndpi_free并不是指变量。这里是一个ndi内部封装好的函数指针。在ndpi_main.c中定义,具体如下:
1
2
3
4
5
6 |
//实现的函数
void
*
ndpi_malloc
(
unsigned
long
size
)
{
return
(
_ndpi_malloc
(
size
));
}
void
ndpi_free
(
void
*
ptr
)
{
_ndpi_free
(
ptr
);
}
//传入的函数指针
static
void
*
(
*
_ndpi_malloc
)(
unsigned
long
size
);
static
void
(
*
_ndpi_free
)(
void
*
ptr
);
|
2、debug函数的初始化
这里使用ndpi_debug_printf函数的有两处地方,第一处就是楼下3步中申请内存时的错误处理。第二处就是在debug函数的传入,跟第1步类似。但是这里的debug信息输出函数并不是在ndpi_main.c中声明,而是在ndpi_detection_module_struct结构内部。结构内部声明了ndpi_debug_printf的函数指针。详细见源代码,这里不再列出。
3、ndpi_detection_module_struct指针的创建和初始化
这里其实就是ndpi_detection_module_struct指针的声明和通过malloc的内存申请。这里的ndpi_detection_module_struct将在函数最后return回去。
4、协议映射图的初始化
NDPI_BITMASK_RESET(ndpi_str->detection_bitmask);
这一部分主要是通过宏NDPI_BITMASK_RESET和结构体(ndpi_detection_module_struct)内部的detection_bitmask来共同实现。detection_bitmask其实就是一个u_int32_t的数组,NDPI_BITMASK_RESET则是在ndpi_macros.h头文件中定义用来维护注册协议的宏之一。在底层其实就是通过menset将detection_bitmask数组置0实现。这里正如我们大题目那样,是文章的核心内容。将在第三部分,系统地进行介绍。
5、Redis初始化
redis是一个key-value存储系统。和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型)。函数中通过结构体(ndpi_detection_module_struct)的redis变量进行了简单的初始化( ndpi_str->redis = NULL;)。这里不作详细介绍,这一部分定义在ndpi_credis.h和ndpi_credis.c中。
6、协议timeout值的初始化
在本函数中,用了比较大的篇幅进行timeout的初始化。这里其实没什么好解释的,各个应用层软件的timeout值。
7、AC算法的初始化
AC算法是指Aho-corasick自动机算法。包含在ndpi_main.h的头文件#include<ahocorasick.h>中,其实在ndpi中对ahocorasick在ahocorasick.c中进行了封装和实现。回归到这里,主要是通过ac_automata_init函数进行初始化。这里的初始化也不涉及算法本身,只是创建并初始化结构体AC_AUTIMATA_T并传递回结构体(ndpi_detection_module_struct)。
8、Lru内存管理的初始化
ndpi_init_lru_cache(&ndpi_str->skypeCache, 4096);
这里又是一个深深的坑,内存管理。但是单单ndpi_init_lru_cache实现的功能也不是很复杂,大家可以去看看。在ndpi_cache.c中进行实现。
9、多线程的初始化
1
2
3
4
5 |
#ifndef __KERNEL__
pthread_mutex_init
(
&
ndpi_str
->
skypeCacheLock
,
NULL
);
#else
spin_lock_init
(
&
ndpi_str
->
skypeCacheLock
);
#endif
|
这里是多线程的初始化,pthread_mutex_init()函数是以动态方式创建互斥锁的,参数attr指定了新建互斥锁的属性。如果参数attr为空,则使用默认的互斥锁属性,默认属性为快速互斥锁 。互斥锁的属性在创建锁的时候指定,在LinuxThreads实现中仅有一个锁类型属性,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同。
pthread_mutexattr_init()函数成功完成之后会返回零,其他任何返回值都表示出现了错误。
函数成功执行后,互斥锁被初始化为未锁住态。
10、默认端口的初始化
ndpi_init_protocol_defaults(ndpi_str);
这里ndpi_str是我们前几步初始化过后的结构体(ndpi_detection_module_struct)。ndpi_init_protocol_defaults函数的主要作用是维护一个二叉树型结构,用来记录各个协议的默认端口。这个函数里面的重复性比较高,我们粘一段比较有代表性的程序段来一起解释一下:
1
2
3 |
ndpi_set_proto_defaults
(
ndpi_mod
,
NDPI_PROTOCOL_FTP
,
"FTP"
,
ndpi_build_default_ports
(
ports_a
,
20
,
21
,
0
,
0
,
0
)
/* TCP */
,
ndpi_build_default_ports
(
ports_b
,
0
,
0
,
0
,
0
,
0
)
/* UDP */
);
|
这里牵涉了两个函数,就是ndpi_build_default_ports和ndpi_set_proto_defaults。ndpi_build_default_ports操作比较简单不作详细介绍,他这里只是把参数中的端口号传递进去ports_a/ports_b并返回。这里主要介绍ndpi_set_proto_defaults函数的实现流程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 |
void
ndpi_set_proto_defaults
(
struct
ndpi_detection_module_struct
*
ndpi_mod
,
u_int16_t
protoId
,
char
*
protoName
,
ndpi_port_range
*
tcpDefPorts
,
ndpi_port_range
*
udpDefPorts
)
{
//函数原型
char
*
name
=
ndpi_strdup
(
protoName
);
//ndpi_strdup主要是为参数中的字符串分配内存单元和赋值,并且返回char *
int
j
;
if
(
protoId
>=
NDPI_MAX_SUPPORTED_PROTOCOLS
+
NDPI_MAX_NUM_CUSTOM_PROTOCOLS
)
{
//这里主要事检查协议ID有没有出错
printf
(
"[NDPI] %s(protoId=%d): INTERNAL ERROR
\n
"
,
__FUNCTION__
,
protoId
);
return
;
}
//将协议的基本信息(名字和ID)存进ndpi_mod->proto_defaults。ndpi_mod是我们1-9步中进行初始化的结构提,proto_defaults类型定义见下方
ndpi_mod
->
proto_defaults
[
protoId
].
protoName
=
name
,
ndpi_mod
->
proto_defaults
[
protoId
].
protoId
=
protoId
;
/*udpDefPorts和tcpDefPorts是之前赋值的prots_a/ports_b。addDefaultPort函数中新增ndpi_default_ports_tree_node_t结构体作为二叉
*树的节点。这个结构包含了原来的ndpi_proto_defaults_t,再附加了默认端口号。然后按照传输层分类,分别挂到udpRoot和tcpRoot中。
*这里二叉查找插入通过ndpi_tsearch进行实现,在pcapReader源码分析一文中以将其列出有兴趣的朋友可以去看看。
*/
for
(
j
=
0
;
j
<
MAX_DEFAULT_PORTS
;
j
++
)
{
if
(
udpDefPorts
[
j
].
port_low
!=
0
)
addDefaultPort
(
&
udpDefPorts
[
j
],
&
ndpi_mod
->
proto_defaults
[
protoId
],
&
ndpi_mod
->
udpRoot
);
if
(
tcpDefPorts
[
j
].
port_low
!=
0
)
addDefaultPort
(
&
tcpDefPorts
[
j
],
&
ndpi_mod
->
proto_defaults
[
protoId
],
&
ndpi_mod
->
tcpRoot
);
}
}
//define in ndpi_structs.h
typedef
struct
ndpi_proto_defaults
{
char
*
protoName
;
u_int16_t
protoId
;
}
ndpi_proto_defaults_t
;
|
注:上面(10)所述函数均在ndpi_main.h中进行实现
三、ndpi_set_protocol_detection_bitmask2
在讲这个函数之前,我们先介绍一下这个函数所用到的参数是什么来历。也就是前面的宏定义和变量。
1、NDPI_PROTOCOL_BITMASKall
其实第一次看到这个语句的时候,第一反应还以是一个宏定义。但是其实这里NDPI_PROTOCOL_BITMASK代表的是一个变量类型,而all则是一个定义处理的实例(变量)。详细的定义在ndpi_macros.h中,如下:
1
2
3
4
5 |
typedef
u_int32_t
ndpi_ndpi_mask
;
typedef
struct
ndpi_protocol_bitmask_struct
{
ndpi_ndpi_mask
fds_bits
[
NDPI_NUM_FDS_BITS
];
}
ndpi_protocol_bitmask_struct_t
;
#define NDPI_PROTOCOL_BITMASK ndpi_protocol_bitmask_struct_t
|
看完上面的定义,不难发现
NDPI_PROTOCOL_BITMASK
类型说白了就是一个u_int32_t的数组。我们在继续看看数组的大小
NDPI_NUM_FDS_BITS又是怎么计算出来:
1
2
3
4 |
#define NDPI_NUM_BITS 256
//这个是ndpi现阶段支持的协议数
#define NDPI_BITS
/* 32 */
(sizeof(ndpi_ndpi_mask) * 8
/* number of bits in a byte */
)
/* bits per mask */
#define howmanybits(x, y) (((x)+((y)-1))/(y))
#define NDPI_NUM_FDS_BITS howmanybits(NDPI_NUM_BITS, NDPI_BITS)
|
这里提出了一系列疑问?为什么要
(((x)+((y)-1))/(y))
?还有这样定义的目的是什么?
根据我的理解,这里维护协议映射的数据结构是上面提到的ndpi_protocol_bitmask_struct(u_int32_t的数组)。对于数组的每一个位置比如fds_bits[1],这u_int32_t一共有4字节。也就事32位,每位代表这一个协议的映射。这一点不仅可以从上面的定义看出,在接下来的第2部分将更明显地可以看到这是一个类似hash的映射结构。然后回到为什么要(((x)+((y)-1))/(y))的问题,这里的y其实就是32,所以这里这样计算数组是为了得出一个恰好满足能存放协议映射的数组大小(当然数组的位数不是全部应用与映射,毕竟会有一点空间的浪费)
2、NDPI_BITMASK_SET_ALL(all)
这个宏的主要作用很显而易见,就是把映射中所以的应用都进行设置。但是这只是比较表面的理解。在ndpi_macros.h中,ndpi提供了非常健全的映射设置函数。如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 |
#define NDPI_SET(p, n) ((p)->fds_bits[(n)/NDPI_BITS] |= (1 << (((u_int32_t)n) % NDPI_BITS)))
//这里通过|=操作进行设置,原理和+=一样只是换成逻辑符。然后从后面的操作我们可以明显看到hash的身影
#define NDPI_CLR(p, n) ((p)->fds_bits[(n)/NDPI_BITS] &= ~(1 << (((u_int32_t)n) % NDPI_BITS)))
//首先通过n/NDPI_BITS在数组上进行定位,然后通过n%NDPI_BITS在4个字节的32位上进行定位。下面同理
#define NDPI_ISSET(p, n) ((p)->fds_bits[(n)/NDPI_BITS] & (1 << (((u_int32_t)n) % NDPI_BITS)))
#define NDPI_ZERO(p) memset((char *)(p), 0, sizeof(*(p)))
#define NDPI_ONE(p) memset((char *)(p), 0xFF, sizeof(*(p)))
//下面对原始宏进行了再封装
#define NDPI_BITMASK_ADD(a,b) NDPI_SET(&a,b)
#define NDPI_BITMASK_DEL(a,b) NDPI_CLR(&a,b)
#define NDPI_BITMASK_RESET(a) NDPI_ZERO(&a)
//在ndpi_init_detection_module的协议初始化中使用
#define NDPI_BITMASK_SET_ALL(a) NDPI_ONE(&a)
#define NDPI_BITMASK_SET(a, b) { memcpy(&a, &b, sizeof(NDPI_PROTOCOL_BITMASK)); }
//下面的定义也是根据第一部分的原始宏进行的封装,将在下面即将讲到的ndpi_set_protocol_detection_bitmask2函数中被大量使用
#define NDPI_ADD_PROTOCOL_TO_BITMASK(bmask,value) NDPI_SET(&bmask,value)
#define NDPI_DEL_PROTOCOL_FROM_BITMASK(bmask,value) NDPI_CLR(&bmask,value)
#define NDPI_COMPARE_PROTOCOL_TO_BITMASK(bmask,value) NDPI_ISSET(&bmask,value)
#define NDPI_SAVE_AS_BITMASK(bmask,value) { NDPI_ZERO(&bmask) ; NDPI_ADD_PROTOCOL_TO_BITMASK(bmask, value); }
|
注:那我们使用时如果只想注册个别应用的检测该怎么办?可以通过NDPI_BITMASK_ADD宏进行添加,协议对应的hash数在ndpi_protocols_osdpi.h中进行了定义。这里限于篇幅不再列出。
3、ndpi_set_protocol_detection_bitmask2(ndpi_struct,&all)
这个可以说是检测协议注册的核心函数。和第2部分中的10一样,这里的重复率也是比较高的。但是参杂着一些协议之间的依赖关系,所以我们下面列一个典型的代码段。一起看看它究竟完成的是什么工作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 |
#ifdef NDPI_PROTOCOL_SNMP
//SNMP是一个网络管理协议
if
(
NDPI_COMPARE_PROTOCOL_TO_BITMASK
(
*
detection_bitmask
,
NDPI_PROTOCOL_SNMP
)
!=
0
)
{
//我们在上面第2点中介绍了NDPI_COMPARE_PROTOCOL_TO_BITMASK的具体实现,如果我们有注册这个协议进入if语句里面
ndpi_struct
->
callback_buffer
[
a
].
func
=
ndpi_search_snmp
;
//这一步是非常核心的,它为SNMP协议注册了检测函数ndpi_search_snmp。这里的callback_buffer是在ndpi_detection_module_struct结构体中定义
ndpi_struct
->
callback_buffer
[
a
].
ndpi_selection_bitmask
=
NDPI_SELECTION_BITMASK_PROTOCOL_V4_V6_UDP_WITH_PAYLOAD
;
NDPI_SAVE_AS_BITMASK
(
ndpi_struct
->
callback_buffer
[
a
].
detection_bitmask
,
NDPI_PROTOCOL_UNKNOWN
);
//从第2点的实现不难知道,这里是把原来的detection_bitmask表清空。并注册NDPI_PROTOCOL_UNKNOWN。
//但是这里需要主要的是callback_buffer[a],所以这个清空的映射表是针对SNMP协议,而不是全部协议的映射记录表
NDPI_SAVE_AS_BITMASK
(
ndpi_struct
->
callback_buffer
[
a
].
excluded_protocol_bitmask
,
NDPI_PROTOCOL_SNMP
);
//同理,这里清空excluded_protocol_bitmask映射表,并注册NDPI_PROTOCOL_SNMP协议
a
++
;
//next
}
#endif
|