http://blog.donews.com/quickmouse/archive/2004/11/17/173266.aspx
第一次聽說socket BPF的東西是CTO說sniffer要注意效率問題,需要針對規則設定一定的過濾規則,這樣可以減少程序在用戶空間和內核空間的切換。於是就去google那個東西了。不過結果並不是很理想的,似乎研究這個的人不多。從方方面面的情況看,似乎用libpcap庫設置BPF的過濾器是比較容易的,但是我的機器並沒有裝libpcap,man了半天就是沒有東西,呵呵。不過折騰了一下也是弄出來了,那都是大半年前的事情了。今天寫程序又用到BPF了,突然想到應用過程當中有一個邏輯問題,所以就想順便寫點什么吧。如果你不想裝libpcap庫,又想折騰BPF,看這篇文章就對了。不過,如果你是打算空手套白狼,不會用tcpdump,或者想從頭學怎么寫BPF規則,那我還沒有鑽研得那么深,咱們可以以后討論討論,呵呵。
設置BPF過濾器是通過setsockopt調用來完成的,格式如下:
setsockopt(sd, SOL_SOCKET, SO_ATTACH_FILTER, &Filter, sizeof(Filter));
這個調用的格式大家都很熟悉了,不清楚的在參數Filter的設置上。Filter的定義是struct sock_fprog Filter; 此結構在linux/filter.h當中有定義:
struct sock_fprog /* Required for SO_ATTACH_FILTER. */ { unsigned short len; /* Number of filter blocks */ struct sock_filter *filter; };
其中的filter指針指向結構為struct sock_filter的BPF過濾代碼。結構同樣也在同一個文件當中定義:
struct sock_filter /* Filter block */ { __u16 code; /* Actual filter code */ __u8 jt; /* Jump true */ __u8 jf; /* Jump false */ __u32 k; /* Generic multiuse field */ };
其實我們並不關心如何具體的編寫struct sock_filter內的東西,因為tcpdump已經內置了這樣的功能。例如,想要對所接受的數據包過濾,只想接收udp數據包,那么在tcpdump當中的命令就是tcpdump udp。如果你想讓tcpdump幫你編譯這樣的過濾器,則用tcpdump udp -d,可以得到輸出:
[root@Kernel26 root]# tcpdump udp -d (000) ldh [12] (001) jeq #0×86dd jt 2 jf 4 (002) ldb [20] (003) jeq #0×11 jt 7 jf 8 (004) jeq #0×800 jt 5 jf 8 (005) ldb [23] (006) jeq #0×11 jt 7 jf 8 (007) ret #96 (008) ret #0
瞧,這就是BPF的代碼了,看不懂吧@_@,其實挺像匯編的,琢磨一下就會了,ld開頭的表示加載某地址數據,jeq是比較啦,jt就是jump when true,jf呢就是jump when false,后面表示行號。不過這樣的東西用在程序里還是不習慣,再用tcpdump udp -dd,可以得到:
[root@Kernel26 root]# tcpdump udp -dd { 0×28, 0, 0, 0×0000000c }, { 0×15, 0, 2, 0×000086dd }, { 0×30, 0, 0, 0×00000014 }, { 0×15, 3, 4, 0×00000011 }, { 0×15, 0, 3, 0×00000800 }, { 0×30, 0, 0, 0×00000017 }, { 0×15, 0, 1, 0×00000011 }, { 0×6, 0, 0, 0×00000060 }, { 0×6, 0, 0, 0×00000000 },
哈哈,這個像什么?像c當中的數組的定義吧。不錯,這個就是過濾udp包的struct sock_filter的數組代碼。把這部分復制到程序當中,將Filter.filter指向這個數組,Filter.len設置長度,就可以用setsockopt設置過濾器了。
不過使用這樣的過濾器還是有一些需要注意的問題的,例如,設置一個過濾器,只允許兩個源MAC地址的數據包進入,我們先用:
[root@Kernel26 root]# tcpdump ether src 01:02:03:04:05:06 or ether src 04:05:06:07:08:09 -dd { 0×20, 0, 0, 0×00000008 }, { 0×15, 0, 2, 0×03040506 }, { 0×28, 0, 0, 0×00000006 }, { 0×15, 3, 4, 0×00000102 }, { 0×15, 0, 3, 0×06070809 }, { 0×28, 0, 0, 0×00000006 }, { 0×15, 0, 1, 0×00000405 }, { 0×6, 0, 0, 0×00000060 }, { 0×6, 0, 0, 0×00000000 },
生成模板,我們注意到第2、4行比較了第一個MAC地址,第5、7行比較了第二個MAC地址,所以我們只需要在我們的程序當中動態的改變這四行當中的數值就可以了,例如:
SetFilter(char *mac1, char *mac2) { struct sock_filter code[]={ { 0×20, 0, 0, 0×00000008 }, { 0×15, 0, 2, ntohl(*(unsigned int *)(mac1 + 2)) }, { 0×28, 0, 0, 0×00000006 }, { 0×15, 3, 4, ntohs(*(unsigned short *)mac1) }, { 0×15, 0, 3, ntohl(*(unsigned int *)(mac2 + 2)) }, { 0×28, 0, 0, 0×00000006 }, { 0×15, 0, 1, ntohs(*(unsigned short *)mac2) }, { 0×6, 0, 0, 0×00000060 }, { 0×6, 0, 0, 0×00000000 } }; … }
這里,需要用ntohl/ntohs等函數將網絡字節序轉換為主機字節序。但是這段代碼是有邏輯問題的。它首先比較第一個mac地址的后4個字節,如果不正確轉入比較第二個mac地址,如果正確轉入比較第一個mac地址的高2個字節。因此,如果打算將這個代碼用作通用的mac比較,那么在輸入的兩個mac地址后4字節都相同的情況下就會出現邏輯覆蓋錯誤,即無法對滿足第二個mac地址的條件進行判斷。因此在這種情況下必須要准備兩段比較代碼,根據情況進行設置。具體不再累述。
此外,這段BPF代碼還存在的一個問題是,一般情況下tcpdump只返回所捕獲包的頭96字節,也就是0×60字節,可見代碼的倒數第二行是ret #96。對於需要完整的包處理還是不行的,因此你需要將其設置為0×0000ffff,或者在用tcpdump生成的時候用tcpdump -s 65535 -dd … 來生成。
最后,用tcpdump生成的BPF代碼只能用於SOCK_RAW的socket,這類socket是可以直接操作數據鏈路層的,如果你打算將BPF用於ip層等較高層次的socket,那么你需要手工修改部分行的code.k,也就是修改如ldh [12]當中的[12]這個數值,因為這個數值的偏移量是按照從鏈路層開始計算得到的,在沒有鏈路層之后,這個值就發生了變化,這個是需要注意的。