在前面的文章中,我們對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; }
|
來自CODE的代碼片
注:二、三中標題的選取只是因爲比重關係,上面源碼中的宏等我們都將在下面進行介紹
二、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中定義,具體如下:
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、多線程的初始化
這裏是多線程的初始化,pthread_mutex_init()函數是以動態方式創建互斥鎖的,參數attr指定了新建互斥鎖的屬性。如果參數attr爲空,則使用默認的互斥鎖屬性,默認屬性爲快速互斥鎖 。互斥鎖的屬性在創建鎖的時候指定,在LinuxThreads實現中僅有一個鎖類型屬性,不同的鎖類型在試圖對一個已經被鎖定的互斥鎖加鎖時表現不同。
10、默認端口的初始化
ndpi_init_protocol_defaults(ndpi_str);
這裏ndpi_str是我們前幾步初始化過後的結構體(ndpi_detection_module_struct)。ndpi_init_protocol_defaults函數的主要作用是維護一個二叉樹型結構,用來記錄各個協議的默認端口。這個函數裏面的重複性比較高,我們粘一段比較有代表性的程序段來一起解釋一下:
這裏牽涉了兩個函數,就是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;
|
來自CODE的代碼片
注:上面(10)所述函數均在ndpi_main.h中進行實現
三、ndpi_set_protocol_detection_bitmask2
在講這個函數之前,我們先介紹一下這個函數所用到的參數是什麼來歷。也就是前面的宏定義和變量。
1、NDPI_PROTOCOL_BITMASKall
其實第一次看到這個語句的時候,第一反應還以是一個宏定義。但是其實這裏NDPI_PROTOCOL_BITMASK代表的是一個變量類型,而all則是一個定義處理的實例(變量)。詳細的定義在ndpi_macros.h中,如下:
根據我的理解,這裏維護協議映射的數據結構是上面提到的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); }
|
來自CODE的代碼片
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
|