什么是eBPF和tc?
eBPF簡介
eBPF是一種革命性的內核技術,它允許用戶在不修改內核源代碼或load內核模塊的情況下,安全高效地擴展內核功能。eBPF程序運行在內核的沙盒環境中,通過驗證器確保其安全性。
tc簡介
tc(Traffic Control)是Linux內核提供的網絡流量控制工具,用于實現QoS(服務質量)功能,包括流量整形、調度和過濾等。tc提供了豐富的分類器和動作,而eBPF可以作為一種強大的分類器。
為什么選擇eBPF+tc?
-
高性能:eBPF程序在內核空間運行,avoid了用戶空間和內核空間之間的上下文切換
-
安全性:所有eBPF程序都必須通過驗證器的嚴格檢查
-
靈活性:可以動態load和unload,無需重啟系統
-
可編程性:使用C等高級語言編寫,比傳統tc命令更強大
開發環境準備
在開始編寫eBPF tc程序前,我們需要準備以下環境:
# 安裝必要的工具 sudo apt-get update sudo apt-get install -y clang llvm libbpf-dev libelf-dev build-essential linux-tools-common linux-tools-generic # 檢查內核eBPF支持 sudo grep -i ebpf /boot/config-$(uname -r)
第一個eBPF tc程序
讓我們從一個簡單的例子開始:統計通過某個網絡接口的TCP SYN包數量。
1. 編寫eBPF程序
創建文件syn_counter.c:
#include <linux/bpf.h> #include <linux/if_ether.h> #include <linux/ip.h> #include <linux/tcp.h> #include <bpf/bpf_helpers.h> struct { __uint(type, BPF_MAP_TYPE_ARRAY); __type(key, __u32); __type(value, __u64); __uint(max_entries, 1); } syn_counter SEC(".maps"); SEC("classifier") int count_syn(struct __sk_buff *skb) { void *data_end = (void *)(long)skb->data_end; void *data = (void *)(long)skb->data; struct ethhdr *eth = data; if ((void *)eth + sizeof(*eth) > data_end) return TC_ACT_OK; if (eth->h_proto != htons(ETH_P_IP)) return TC_ACT_OK; struct iphdr *ip = data + sizeof(*eth); if ((void *)ip + sizeof(*ip) > data_end) return TC_ACT_OK; if (ip->protocol != IPPROTO_TCP) return TC_ACT_OK; struct tcphdr *tcp = (void *)ip + sizeof(*ip); if ((void *)tcp + sizeof(*tcp) > data_end) return TC_ACT_OK; if (tcp->syn && !tcp->ack) { __u32 key = 0; __u64 *count = bpf_map_lookup_elem(&syn_counter, &key); if (count) { (*count)++; } } return TC_ACT_OK; } char __license[] SEC("license") = "GPL";
2. 編譯eBPF程序
使用以下命令編譯:
clang -O2 -target bpf -c syn_counter.c -o syn_counter.o
3. load eBPF程序到tc
假設我們要監控eth0接口:
# 創建clsact qdisc(如果不存在) sudo tc qdisc add dev eth0 clsact # load eBPF程序到ingress方向 sudo tc filter add dev eth0 ingress bpf obj syn_counter.o sec classifier direct-action # 查看load的filter sudo tc filter show dev eth0 ingress
4. 查看統計結果
我們可以使用bpftool查看統計結果:
# 首先找到map的id sudo bpftool map list # 然后查看map內容(替換上面的map id) sudo bpftool map dump id <map_id>
eBPF tc編程踩坑點
一般來講tc編程所需要用的helper函數都能在bpf.h里找到相應的注釋說明,但實際代碼過程中筆者依然遇到幾個較坑的點
1、解析數據包時,skb->data_end-skb->data要小于skb->len,這是由于數據包skb是非線性的,也就是data_end-data其實是線性區的大小,而skb->len是線性區加非線性區的大小,前者其實是skb->data_len,但是BPF程序中無法從__sk_buff中直接拿到這個值,我們可以使用bpf_skb_pull_data(skb,len)直接訪問非線性數據,這個helper函數的官方解釋如下
- 如果 skb 是非線性的并且len 由線性和非線性部分組成,則pull入非線性數據,使得 skb 中的 len 字節可讀可寫,如果為 len 傳遞了一個零值,則拉取整個 skb 長度。
- 此 helper 僅用于通過 direct packet access 進行讀取和寫入。
- 對于direct packet access,如果偏移量無效,或者如果請求的數據在 skb 的非線性部分中,則訪問偏移量在數據包邊界內的測試(在 skb->data_end 上測試)很容易失敗。失敗時,程序可以退出,或者在非線性緩沖區的情況下,使用helper程序使數據可用。 bpf_skb_load_bytes() helper是訪問數據的第一個解決方案。另一種方法是使用 bpf_skb_pull_data 拉入一次非線性部分,然后重新測試并最終訪問數據。
- 同時,這也保證了skb是未克隆的,這是direct write的必要條件。由于這僅需要是寫入部分的不變量,因此驗證程序檢測寫入并添加一個調用 bpf_skb_pull_data 的prologue??,以從一開始就有效地取消克隆 skb,以防它確實被克隆。
- 對這個helper的調用很容易改變底層的數據包緩沖區。因此,在load時,如果helper與direct packet access結合使用,則verifier先前對指針所做的所有檢查都將無效并且必須再次執行。
2、tc程序對數據包頭部進行剪裁,并load到網卡的ingress方向,使用tcpdump抓包發現報文并未進行相應的修改,原因是入方向tcpdump抓包點要早于tc程序,在tc程序中使用重定向將報文發到另一張網卡再使用tcp dump抓包,可看到報文符合代碼處理邏輯。