# 使用BPF(Berkeley Packet Filter)過濾封包 ###### tags: `Linux` `Socket` [TOC] ## 簡介 BPF是一套在內核作用的封包過濾機制,可以在內核將封包複製到使用者空間之前篩檢封包,避免額外的記憶體複製,提昇應用程式的效能,並降低編程的複雜度。 BPF的擴展eBPF(Extended BPF)可以用來剖析內核的行為以作為性能優化的參考,在SECCOMP(Secure Computation)中能夠限制行程的系統呼叫,盡可能降低惡意程式的破壞性。 為了與eBPF做出區隔,用來過濾封包的BPF也被稱作為Classical BPF,本文紀錄一個在raw socket(`AF_INET, SOCK_RAW, IPPROTO_ICMP`)上套用Classical BPF過濾器的例子。 如果沒有特別提及,以下的BPF均指Classical BPF。 ## 建立BPF code BPF過濾器由BPF虛擬機執行,BPF有自己一組指令集,使用者可以自行撰寫供虛擬機執行的程式碼或是透過編譯器(LLVM)的支援產生BPF bytecode; BPF bytecode執行的最後會返還一個數值,該數值代表會有多少byte的封包通過,因此我們可以透過該數值決定是否讓封包通過、截斷或者完全捨棄。 本例使用的過濾器要容許三種ICMPv4封包通過,其IP協定號分別為: 類型|代碼|16進位 -|-|- Echo Reply|0|0x0 Unreachable|3|0x3 Time Exceeded|11|0xb 寫成類C的偽碼就是: ```= if (type == echo_reply): goto pass if (type == unreachable): goto pass if (type == time_exceeded): goto pass return 0 pass: return -1 /* 0xffffffff */ ``` 要寫成BPF assembly,必須參考BPF虛擬機的模型,在Linux Kernel的[這份文件](https://www.kernel.org/doc/Documentation/networking/filter.txt)中有cBPF和eBPF的說明。 必須注意到,該文件提供的例子是將過濾器加掛在**接收所有類型raw ethernet packet**(`socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)`)上的socket,文件雖然說明了BPF可以加掛在其他種類的socket上,卻並沒有描述加掛後的行為。 根據stackoverflow上的兩篇文章(見參考2、3)和投影片(參考4),似乎是會作用在本來應該收到的封包上,也就是說會將filter加掛在呼叫者最終見到的"那個層次"的socket上, 以`socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)`來說,使用者的BPF見到的封包會是以IPv4標頭開始、包含ICMPv4訊息的封包。 在這篇文件中的記憶體數值術語和x86使用的不一樣,如`word`並不是代表`2 bytes`而是`4 bytes`,故指令裡的`ldh`(`load half word`)實際上是載入`2 bytes`。 考慮到了以上的因素,我們可以寫出以下的BPF code: ```= ldxb 4*([0] & 0xf) ldb [x + 0] jeq #0x0, pass jeq #0x3, pass jeq #0xb, pass ret #0 pass: ret #-1 ; 0xffffffff ``` ## 轉換為C語言可用形式 接下來要將BPF assembly轉換成虛擬機用的二進位碼,由於`tcpdump`預設的層次和我們使用的socket不一樣,故無法使用`tcpdump -dd <condition>`轉換程式,我轉而使用`netsniff-ng`套件提供的`bpfc`工具編譯程式,該工具可以將BPF assembly轉換成可以供C語言使用的形式,形如: ```= { 0xb1, 0, 0, 0x00000000 }, { 0x50, 0, 0, 0x00000000 }, { 0x15, 3, 0, 0x00000000 }, { 0x15, 2, 0, 0x00000003 }, { 0x15, 1, 0, 0x0000000b }, { 0x6, 0, 0, 0x00000000 }, { 0x6, 0, 0, 0xffffffff }, ``` ## 將filter加掛在socket上 得到了可在C語言中使用的形式,我們可以開始為socket進行加掛過濾器的作業,以下是該引入的標頭和使用的結構: ``` #include <linux/filter.h> #define ARRAY_SZ(arr) (sizeof((arr))/sizeof((arr)[0])) struct sock_filter code[] = { /* bpf codes ... */ }; struct sock_fprog filter = { .len = ARRAY_SZ(code), .filter = code, }; ``` Linux中將過濾器加掛在socket上的界面為: ```2 setsockopt(sockfd, SOL_SOCKET, SO_ATTACH_FILTER, &bpf, sizeof(struct sock_fprog)); ``` > `SOL`=`socket option level`? 如此應可成功地在指定的socket上加掛過濾器。 ## 注意事項 一個socket只能加掛一個過濾器,而且只有在加掛成功時才會覆寫掉先前的filter。 ## 參考 - https://www.kernel.org/doc/Documentation/networking/filter.txt - https://stackoverflow.com/questions/49577061/reading-bpf-assembly - https://stackoverflow.com/questions/39540291/classic-bpf-on-linux-filter-does-not-work - [errzey - Know Thy BPF](https://gist.github.com/errzey/1111503/bbcda355e8ffbf5141dc10e0e551eb6edf666e36) - https://www.slideshare.net/kerneltlv/berkeley-packet-filters