這裡以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協議類型再寫一篇文章,所以就不詳細敘述了。經過這個函數之後如果仍然沒有檢測出協議類型,那麼就繼續檢測下一個資料包直到檢測出該資料流的協議類型為止。