執行人: jhin1228, D4nnyLee
專題講解影片 (這是私人影片看不了)
研究 XDP Firewall,解釋其運作原理,在其 GitHub Issue 找出可改進或提交內部實作的錯誤。
Reference Resource: A Beginners Guide to eBPF Programming for Networking: 透過 eBPF hook kernel space的網路模組 (如 TCP/IP stack),並寫成條件判斷,當封包進到 NIC 時直接在核心被分類並動作。
eXpress Data Path (XDP) 是 Extended Berkeley Packet Filter (eBPF) 的其中一種 BPF program type。不同的 program type 代表不同的 hook point 和 helper function,同時 ebpf program 的輸入和輸出格式也不同。
以下列舉 bpf_prog_type
種類 :
XDP 可以提早處理從網路裝置進來的封包,根據 hook point 分為以下三種 :
其中 Native/Offload XDP 需要網路裝置本身支援。
上圖描述整體 eBPF 架構,可分成 BPF program 撰寫及編譯、BPF bytecode 載入到核心並完成 hook point 設定、藉 BPF maps 在 kernel space 和 user space 間傳遞資料三大部分。
以 XDP Firewall 為例,會將封包過濾規則寫在 BPF program 後,以 Clang/LLVM 編譯成 BPF bytecode (BPF instruction) 並交由 Loader (BCC、libbpf…) 透過系統呼叫把 BPF ELF object file 載入到核心內。
進到核心後,會先建立 strcut bpf_prog
,這個結構體是 BPF bytecode 在核心的代表,之後將 BPF bytecode 從 user space 拷貝至 kernel space 並開始驗證此 bytecode 是否安全,最後分配一個 file descriptor 並傳回給 user space 的 process 作之後的處理。
- BPF bytecode: 就是一種可以被虛擬機執行的 machine code,之所以稱其為 bytecode 是因為 BPF 指令集的 opcode 都是一個 byte 長度。
- BPF instruction (BPF bytecode): BPF instruction 採用虛擬指令集,類似 assembly 在處理的指令集。
- BPF 虛擬機: 可理解成直譯器 (Interpreter),架構圖如下。
在 XDP Firewall 中以 xdpfw.c
作為 loader,透過以下流程將 BPF instruction 載入到核心:
關於 bpf skeleton 和 bpf app lifecycle 可參考這篇文章
完成 BPF program 載入核心動作後,接著關心 BPF machine code 的 hook point 設定及存取到此 hook point 時的後續行為,這裡以 XDP 這個 BPF program type 接續探討。
XDP Firewall 中當 xdpfw.c
將 xdpfw_kern.c
載入完成後便進入 attachment 階段,流程如下 :
在函式 do_setlink()
會根據先前設定好的 XDP flag 呼叫函示 dev_change_xdp_fd()
dev_change_xdp_fd()
是將 XDP program fd (struct bpf_prog
) 和指定的 NIC interface 作關聯。
在本次實驗 dev->netdev_ops
是 ixgbe_netdev_ops
,bpf_op
則是 ixgbe_xdp
。
從這裡可知 XDP program 是透過 netlink 中的 NETLINK_ROUTE 相關功能將其 hook 到指定的 interface 上。
在 dev_xdp_install
就是針對各家網路裝置的驅動程式進行 XDP program 安裝,並根據 XDP 預計 hook 到的地方執行進一步的設定。
本實驗由於 hook 在 driver,所以
xdp.command = XDP_SETUP_PROG;
ixgbe_xdp_setup
就是將 XDP program (xdp_prog) 記錄到 rx_ring
。
設定好 XDP program 的 hook 點後,接著思考何時觸發此 hook 及觸發後的後續工作。
一般網路裝置 (不考慮 SmartNIC) 本身沒有網路處理器,當封包從網路裝置進來時沒有進程處理就會被丟棄,而常見的收包方式有以下兩種 :
針對 IRQ 在高流量的情形下的改進方式就是 NAPI,它結合了 polling 和 interrupt :
poll()
時,會從 ring buffer 收取 batched 封包 (每次收取封包的量可以用 budget
決定),這段期間會接收所有到來的封包且不會觸發 IRQ。poll()
時,收到封包時會觸發 IRQ,核心會呼叫 poll()
收包。接著討論 DMA 將封包複製至 Rx ring buffer,產生一個 IRQ 後的流程 (以 ixgbe
驅動為例) :
__raise_softirq_irqoff()
會觸發 NR_SOFTIRQS
類型 soft-IRQ,最後會執行 net_rx_action()
。
ksoftirqd 會執行
net_rx_action()
可觀察到當 XDP 的 hook 點在 driver 時,會進到以下程式碼的第 14 行,此時 skb
只是大小為 8 bytes 的指標,尚未將封包內容拷貝給它。
在 ixgbe_run_xdp()
中,根據 bpf_prog_run_xdp()
取得的 XDP action 決定封包該如何處理。
XDP_PASS
: 正常處理封包,即封包交給 kernel networking stack 處理。
XDP_TX
: 封包從原 interface 出去,適用於 proxy、load balance。
XDP_REDIRECT
: 封包從其他 interface 出去、封包交由其他 CPU 處理、透過 AF_XDP
直接將封包導向 userspace 上的 process 處理。
XDP_ABORTED
: 類似 XDP_DROP
,只是 ebpf program 會在 tracepoint 上提供錯誤訊息的 log。
XDP_DROP
: 在 XDP hook 階段將滿足過濾規則的封包丟棄,適用於 DDoS mitigation。
bpf_prog_run_xdp()
正是觸發 XDP hook 後要執行的動作
以下是 loader 創建完 ebpf map 並回傳其 fd 給 userspace process 的過程 :
在 map_create()
中,首先會透過 find_and_alloc_map()
來為這個 map 分配空間,並且不同型態的 map 會使用不同的方法來分配,而最後統一都回傳 struct bpf_map *
,不同型態的 map 都可以用此結構來表示。
為不同型態的 map 配置好空間之後,接下來就可以透過 bpf_map_new_fd()
來將 struct bpf_map *
對應到一個 file descriptor。
有一個事先定義好的陣列 struct bpf_map_ops *bpf_map_types
,其中每個 struct bpf_map_ops
都對應到一種 map 型態的各種操作(例如分配空間、查找元素等),而 find_and_alloc_map()
就是透過 bpf_map_types[type]
來快速得到不同型態對應到的操作,而不是執行一連串的 if-else
來判斷。
bpf_map_new_fd()
做的事情其實很簡單,就只是在檔案系統上創建一個匿名檔案並且將前面分配好的 map
放到該檔案的 private_data
欄位,之後只要再將一個未使用的 fd 與此檔案關聯起來,我們就可以透過此 fd 存取到 map
。
函式開頭的 anon 代表的意思是 anonymous,也就是匿名的,而程式碼中的
"bpf-map"
則是此檔案的類別,而不是檔案名稱。
private_data
是一個可以讓開發者放自訂義結構的欄位。
要透過 fd 拿到對應的 map 的話,我們可以從 map_lookup_elem()
中看到其過程。
首先會透過 fdget()
從檔案系統中取得 fd 對應的檔案,之後 __bpf_map_get()
就會讀取檔案的 private_data
欄位,來得到前面 map_create()
中配置的 struct bpf_map
,也就是 fd 對應的 map。
之後函式就會根據不同的 map 型別來查找 key 對應的 value,並且將其結果複製到 value
(一個用 kmalloc()
得到的 buffer),最後再利用 copy_to_user()
把結果複製到 userspace 的指標(也就是 uvalue
)。
以下變數中前綴的
u
都是指 userspace。程式碼中的
map->ops
其實就是前面提到過的利用bpf_map_types[type]
得到的每個型別對應的操作。
xdpfw_kern.c
這個檔案定義要被載入核心執行的程式以及用來與 userspace 程式互相傳遞資料的 map,而為了方便與其他函式與變數做區分,XDP 程式與 map 都會用 SEC()
來表示他們要存放在不同的 section,如此一來透過解析 ELF 中的 section table 就可以快速找到要載入的程式以及 map。
xdp_prog_main()
ctx
這個參數讓我們有辦法存取到封包內容,封包的開頭與結尾分別是 ctx->data
和 ctx->data_end
。
此函式做的事情主要可以分為四個部分,檢查完整性、確認黑名單、紀錄 pps/bps 以及過濾封包。
檢查完整性
函式一開始會先檢查封包是否完整,從下面的程式可以看到程式還會順便確認封包的 protocol(目前只支援 TCP、UDP、ICMP)。
確認黑名單
若是有封包在後面過濾封包的階段被判斷成惡意封包(回傳 XDP_DROP
),並且過濾規則有設定 blocktime
時,接下來的 blocktime
秒內如果又收到同樣來源 IP 的封包的話就會在此階段直接丟棄封包。
時間紀錄的方式是將預計解除封鎖的時間點紀錄在 ip_blacklist_map
或 ip6_blacklist_map
中(依據 IP 為 IPv4 還是 IPv6 來決定),而每次這個階段則是檢查是否已經超過 IP 對應的時間點來決定是否繼續封鎖。
*blocked
和newTime
代表的都是解除封鎖的時間,單位為奈秒
紀錄 pps/bps
透過 ip_stats_map
和 ip6_stats_map
紀錄每個 IP 對應的 pps(packets per second)與 bps(bytes per second),計算的方法為每秒鐘都會將 pps/bps 歸零,並且紀錄接下來的一秒鐘處理的封包與 byte 數量。
注意到更新記錄在 map 的值的時候有使用到 __sync_fetch_and_add()
,這是因為 ip_stats
為所有 BPF 程式共享的 map,並且可能會有多個 CPU 都在執行 xdp_prog_main()
,因此需要使用 atomic operation 來避免可能的 race condition。
過濾封包
最後這個階段其實就只是根據過濾規則一一比對封包的內容,若是符合的話就會透過 goto matched
直接做後續的步驟,而不是繼續比對下一個規則。
而為了要讓 xdp_prog_main()
有辦法存取到使用者自訂的規則,loader(xdpfw.c
)在將程式載入到核心之後,就會呼叫 updateconfig()
來解析規則並且存到 cfg->filters
這個陣列裡面,接著就會呼叫 updatefilters()
來將 cfg->filters
裡的所有規則一個一個透過 bpf_map_update_elem()
儲存到核心的 map 中。
以下為 src/xdpfw_kern.c
中與比對封包 TCP Flag 相關的程式碼:
其中
tcph
是封包中的 TCP header,而filter->tcpopts
則是過濾規則中與 TCP 相關的部分
可以發現對於每種 flag 都會用一次 if
來判斷是否符合過濾規則。
因為實際上每種 flag 都只占用一個位元,透過將整數中的不同位元對應到不同的 flag,並利用 bit-wise 操作,我們可以將比對全部 flag 的過程簡化成只需要一次 if
就可比對完成。
在 <linux/tcp.h>
裡面有定義好的巨集讓我們可以以 32 位元整數的形式取出 struct tcphdr
中所有的 flag,並且也有定義每種 flag 對應的 mask,方便我們讀取特定的 flag。
tcp_flag_word()
以及TCP_FLAG_CWR
、TCP_FLAG_ECE
等
因此我們可以將 struct tcpopts
中的 flag 欄位改成兩個整數,其中 enabled_flags
是為了取代 do_*
欄位的 mask,而 expected_flags
則是取代各個 flag 的值。
如此一來,我們只需要一次 if
敘述就可以比對所有的 TCP flag。
TODO: 提交 pull request 到原專案
我們使用 iperf3
工具來測量防火牆對於封包吞吐量的影響。
實驗中我們將 192.168.1.100 做為 Server,而 192.168.2.200 則是 Client。
Server 會持續監聽 5201 port,而 Client 則透過傳送大量封包來測量吞吐量。
5.4.0-152-generic
目前因為 kernel 版本過舊導致最新版的 XDP-Firewall 無法 build,所以先使用較舊版本的 XDP-Firewall。
規則比對有以下特性:
if
來確認是否要比對因此為了盡量使得防火牆花費更多的時間比對,實驗中我們都只有比較 ICMP 的選項( ICMP 比對被寫在迴圈的最後)。
我們使用的規則為 90 個以下的 filter
首先我們先測量不開啟防火牆的狀態下的數據。
從下面數據可以看到開啟防火牆之後 Bandwidth 每秒下降了約 16.5 MB。
經過簡化 TCP Flag 的比對後,Bandwidth 提升了約 1.4 MB。