Libpcap BPF(BSD Packet Filter)包過濾機制

http://hi.baidu.com/ahtaria/blog/item/969ae4447eaa59076a63e57d.html


Libpcap 重點使用 BPF(BSD Packet Filter)包過濾機制,BPF 於 1992 年被設計出來,其設計目的主要是解決當時已存在的過濾機制效率低下的問題。BPF的工作步驟如下:當一個數據包到達網絡接口時,數據鏈路層的驅動會把它向系統的協議棧傳送。但如果 BPF 監聽接口,驅動首先調用 BPF。BPF 首先進行過濾操作,然後把數據包存放在過濾器相關的緩衝區中,最後設備驅動再次獲得控制。注意到BPF是先對數據包過濾再緩衝,避免了類似 sun 的 NIT 過濾機制先緩衝每個數據包直到用戶讀數據時再過濾所造成的效率問題。參考資料D是關於 BPF 設計思想最重要的文獻。

BPF 的設計思想和當時的計算機硬件的發展有很大聯繫,相對老式的過濾方式CSPF(CMU/Stanford Packet Filter)它有兩大特點。1:基於寄存器的過濾機制,而不是早期內存堆棧過濾機制,2:直接使用獨立的、非共享的內存緩衝區。同時,BPF 在過濾算法是也有很大進步,它使用無環控制流圖(CFG control flow graph),而不是老式的布爾表達式樹(boolean expression tree)。布爾表達式樹理解上比較直觀,它的每一個葉子節點即是一個謂詞判斷,而非葉子節點則爲 AND 操作或 OR操作。CSPF 有三個主要的缺點。1:過濾操作使用的棧在內存中被模擬,維護棧指針需要使用若干的加/減等操作,而內存操作是現代計算機架構的主要瓶頸。2:布爾表達式樹造成了不需要的重複計算。3:不能分析數據包的變長頭部。BPF 使用的CFG 算法實際上是一種特殊的狀態機,每一節點代表了一個謂詞判斷,而左右邊分別對應了判斷失敗和成功後的跳轉,跳轉後又是謂詞判斷,這樣反覆操作,直到到達成功或失敗的終點。CFG 算法的優點在於把對數據包的分析信息直接建立在圖中,從而不需要重複計算。直觀的看,CFG 是一種"快速的、一直向前"的算法。

過濾代碼的編譯

BPF 對 CFG 算法的代碼實現非常複雜,它使用僞機器方式。BPF 僞機器是一個輕量級的,高效的狀態機,對 BPF 過濾代碼進行解釋處理。BPF 過濾代碼形式爲"opcode jt jf k",分別代表了操作碼和尋址方式、判斷正確的跳轉、判斷失敗的跳轉、操作使用的通用數據域。BPF 過濾代碼從邏輯上看很類似於彙編語言,但它實際上是機器語言,注意到上述 4 個域的數據類型都是 int 和 char 型。顯然,由用戶來寫過濾代碼太過複雜,因此 libpcap 允許用戶書寫高層的、容易理解的過濾字符串,然後將其編譯爲BPF代碼。

Libpcap 使用了 4 個源程序 gencode.c、optimize.c、grammar.c、scanner.c完成編譯操作,其中前兩個實現了對過濾字符串的編譯和優化,後兩個主要是爲編譯提供從協議相關過濾條件到協議無關(的字符數組)位置信息的映射,並且它們由詞彙分析器生成器 flex 和 bison 生成。參考資料 C 有對此兩個工具的講解。

flex -Ppcap_ -t scanner.l > $$.scanner.c; mv $$.scanner.c scanner.c

bison -y -p pcap_ -d grammar.y

mv y.tab.c grammar.c

mv y.tab.h tokdefs.h

編譯過濾字符串調用了函數 pcap_compile()[getcode.c],形式爲:

int pcap_compile(pcap_t *p, struct bpf_program *program,

         char *buf, int optimize, bpf_u_int32 mask)

其中 buf 指向用戶過濾字符串,編譯後的 BPF 代碼存在在結構 bpf_program中,標誌 optimize 指示是否對 BPF 代碼進行優化。

/* [pcap-bpf.h] */

struct bpf_program {

u_int bf_len; /* BPF 代碼中謂詞判斷指令的數目 */

struct bpf_insn *bf_insns; /* 第一個謂詞判斷指令 */

};

     

/* 謂詞判斷指令結構,含意在前面已描述 [pcap-bpf.h] */

struct bpf_insn {

u_short       code;

u_char       jt;

u_char       jf;

bpf_int32 k;

};

過濾代碼的安裝

前面我們曾經提到,在內核空間過濾數據包對整個捕獲機制的效率是至關重要的。早期使用 SOCK_PACKET 方式的 Linux 不支持內核過濾,因此過濾操作只能在用戶空間執行(請參閱函數 pcap_read_packet() 代碼),在《UNIX 網絡編程(第一卷)》(參考資料 B)的第 26 章中對此有明確的描述。不過現在看起來情況已經發生改變,linux 在 PF_PACKET 類型的 socket 上支持內核過濾。Linux 內核允許我們把一個名爲 LPF(Linux Packet Filter) 的過濾器直接放到 PF_PACKET 類型 socket 的處理過程中,過濾器在網卡接收中斷執行後立即執行。LSF 基於 BPF 機制,但兩者在實現上有略微的不同。實際代碼如下:

/* 在包捕獲設備上附加 BPF 代碼 [pcap-linux.c]*/

static int

pcap_setfilter_linux(pcap_t *handle, struct bpf_program *filter)

{

#ifdef SO_ATTACH_FILTER

struct sock_fprog       fcode;

int can_filter_in_kernel;

int err = 0;

#endif

/* 檢查句柄和過濾器結構的正確性 */

if (!handle)

return -1;

if (!filter) {

strncpy(handle->errbuf, "setfilter: No filter specified",

sizeof(handle->errbuf));

return -1;

}

/* 具體描述如下 */

if (install_bpf_program(handle, filter) < 0)

return -1;

/* 缺省情況下在用戶空間運行過濾器,但如果在內核安裝成功,則值爲 1 */

handle->md.use_bpf = 0;

     

/* 嘗試在內核安裝過濾器 */

#ifdef SO_ATTACH_FILTER

#ifdef USHRT_MAX

if (handle->fcode.bf_len > USHRT_MAX) {

/*過濾器代碼太長,內核不支持 */

fprintf(stderr, "Warning: Filter too complex for kernel\n");

fcode.filter = NULL;

can_filter_in_kernel = 0;

} else

#endif /* USHRT_MAX */

{

/* linux 內核設置過濾器時使用的數據結構是 sock_fprog,而不是 BPF 的結構 bpf_program ,因此應做結構之間的轉換 */

switch (fix_program(handle, &fcode)) {

                             

/* 嚴重錯誤,直接退出 */

case -1:

default:

return -1;

                             

/* 通過檢查,但不能工作在內核中 */

case 0:

can_filter_in_kernel = 0;

break;

/* BPF 可以在內核中工作 */

case 1:

can_filter_in_kernel = 1;

break;

}

}

/* 如果可以在內核中過濾,則安裝過濾器到內核中 */

if (can_filter_in_kernel) {

if ((err = set_kernel_filter(handle, &fcode)) == 0)

{

/* 安裝成功 !!! */

handle->md.use_bpf = 1;

}

else if (err == -1)       /* 出現非致命性錯誤 */

{

if (errno != ENOPROTOOPT && errno != EOPNOTSUPP) {

fprintf(stderr, "Warning: Kernel filter failed:

%s\n",pcap_strerror(errno));

}

}

}

/* 如果不能在內核中使用過濾器,則去掉曾經可能在此 socket

上安裝的內核過濾器。主要目的是爲了避免存在的過濾器對數據包過濾的干擾 */

if (!handle->md.use_bpf)

reset_kernel_filter(handle);[pcap-linux.c]

#endif

}

/* 把 BPF 代碼拷貝到 pcap_t 數據結構的 fcode 上 */

int install_bpf_program(pcap_t *p, struct bpf_program *fp)

{

size_t prog_size;

/* 首先釋放可能已存在的 BPF 代碼 */

pcap_freecode(&p->fcode);

/* 計算過濾代碼的長度,分配內存空間 */

prog_size = sizeof(*fp->bf_insns) * fp->bf_len;

p->fcode.bf_len = fp->bf_len;

p->fcode.bf_insns = (struct bpf_insn *)malloc(prog_size);

if (p->fcode.bf_insns == NULL) {

snprintf(p->errbuf, sizeof(p->errbuf),

"malloc: %s", pcap_strerror(errno));

return (-1);

}

/* 把過濾代碼保存在捕獲句柄中 */

memcpy(p->fcode.bf_insns, fp->bf_insns, prog_size);

                 

return (0);

}

/* 在內核中安裝過濾器 */

static int set_kernel_filter(pcap_t *handle, struct sock_fprog *fcode)

{

int total_filter_on = 0;

int save_mode;

int ret;

int save_errno;

/*在設置過濾器前,socket 的數據包接收隊列中可能已存在若干數據包。當設置過濾器後,

這些數據包極有可能不滿足過濾條件,但它們不被過濾器丟棄。

這意味着,傳遞到用戶空間的頭幾個數據包不滿足過濾條件。

注意到在用戶空間過濾這不是問題,因爲用戶空間的過濾器是在包進入隊列後執行的。

Libpcap 解決這個問題的方法是在設置過濾器之前,

首先讀完接收隊列中所有的數據包。具體步驟如下。*/

      

/*爲了避免無限循環的情況發生(反覆的讀數據包並丟棄,但新的數據包不停的到達),首先設置一個過濾器,阻止所有的包進入 */

      

setsockopt(handle->fd, SOL_SOCKET, SO_ATTACH_FILTER,

&total_fcode, sizeof(total_fcode);

/* 保存 socket 當前的屬性 */

save_mode = fcntl(handle->fd, F_GETFL, 0);

/* 設置 socket 它爲非阻塞模式 */

fcntl(handle->fd, F_SETFL, save_mode | O_NONBLOCK);

/* 反覆讀隊列中的數據包,直到沒有數據包可讀。這意味着接收隊列已被清空 */

while (recv(handle->fd, &drain, sizeof drain, MSG_TRUNC) >= 0);

                       

/* 恢復曾保存的 socket 屬性 */

fcntl(handle->fd, F_SETFL, save_mode);

                 

/* 現在安裝新的過濾器 */

setsockopt(handle->fd, SOL_SOCKET, SO_ATTACH_FILTER,

fcode, sizeof(*fcode));

}

/* 釋放 socket 上可能有的內核過濾器 */

static int reset_kernel_filter(pcap_t *handle)

{

int dummy;

return setsockopt(handle->fd, SOL_SOCKET, SO_DETACH_FILTER,

&dummy, sizeof(dummy));

}


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章