# 使用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