在 Linux 核心開發與監控領域,追蹤技術是我們理解系統行為的關鍵工具。當我們需要深入瞭解核心如何執行特定操作時,可以選擇多種追蹤機制,每種機制都有其獨特的優勢和適用場景。
Kprobe 與 BPF_KPROBE 的細微差別
在實作核心追蹤時,我發現 BPF_KPROBE 和 BPF_KPROBE_SYSCALL 這兩個巨集雖然目的相似,但有重要區別。當我選擇使用 BPF_KPROBE 而非 BPF_KPROBE_SYSCALL 時,是考慮到引數型別的不同:
BPF_KPROBE_SYSCALL專門處理系統呼叫的引數BPF_KPROBE則用於一般核心函式的引數
這兩者的差異在處理路徑名稱引數時尤為明顯。以 execve 相關函式為例,系統呼叫版本的引數是一個指向字串的指標 (char *),而核心內部函式 do_execve() 使用的是 struct filename * 型別的引數。
核心函式引數的正確使用
要正確追蹤核心函式,必須瞭解其確切的函式簽名。以 do_execve() 為例,其函式簽名如下:
int do_execve(struct filename *filename,
const char __user *const __user *__argv,
const char __user *const __user *__envp)
在我的追蹤程式中,我只關注 filename 引數,而忽略了 __argv 和 __envp。這是一個重要的技巧:在引數列表中,你可以忽略後面的引數,但不能跳過前面的引數後使用後續引數,因為引數在記憶體中是連續排列的。
在核心程式設計中,引數的記憶體佈局是連續的,這意味著如果你要存取第三個引數,你必須先正確宣告前兩個引數。這是因為程式會根據引數型別計算偏移量來找到正確的記憶體位置。這也是為什麼在 eBPF 程式中正確宣告引數型別如此重要。
存取核心內部結構
struct filename 是核心內部使用的資料結構,這正體現了 eBPF 程式設計實際上就是核心程式設計的一部分。我必須查詢 do_execve() 的定義及 struct filename 的結構才能正確存取所需資料。
在範例程式中,我透過以下程式碼存取可執行檔的名稱:
const char *name = BPF_CORE_READ(filename, name);
bpf_probe_read_kernel(&data.command, sizeof(data.command), name);
BPF_CORE_READ 是一個方便的巨集,它處理了指標的安全存取,並確保即使核心版本間結構有所變化,程式碼仍能正常工作。
BPF_CORE_READ 巨集使用 CO-RE (Compile Once, Run Everywhere) 技術,它在編譯時建立重定位訊息,使程式能在不同核心版本上執行。當存取 filename->name 時,如果結構在不同核心版本中位置有變,CO-RE 會自動調整偏移量。這大提高了 eBPF 程式的可移植性。
Kprobe 與 Kretprobe 的差異
總結一下 kprobe 的上下文引數:
- 系統呼叫的 kprobe:上下文是代表使用者空間傳入系統呼叫的值的結構
- 一般核心函式的 kprobe:上下文是代表呼叫函式時傳入引數的結構,其結構取決於函式定義
而 kretprobe 與 kprobe 類別似,但它們在函式回傳時觸發,能夠存取回傳值而非輸入引數。
更高效的追蹤選擇:Fentry/Fexit
雖然 kprobe 和 kretprobe 是追蹤核心函式的合理方式,但在較新的核心版本中,有更高效的選擇。
Fentry/Fexit 的優勢
在核心版本 5.5 (x86 處理器) 引入了一種更高效的機制來追蹤核心函式的進入和結束,這就是 fentry/fexit 機制,它與 BPF trampoline 概念一同推出。在 ARM 處理器上,這一功能直到 Linux 6.0 才可用。
如果你使用的是足夠新的核心,fentry/fexit 現在是追蹤核心函式進入或結束的首選方法。你可以在 kprobe 或 fentry 型別的程式中編寫相同的程式碼。
實作 Fentry 程式
以下是一個使用 fentry 機制的範例:
SEC("fentry/do_execve")
int BPF_PROG(fentry_execve, struct filename *filename)
{
// 程式碼實作
}
這裡的 section 名稱告訴 libbpf 將此 eBPF 程式附加到 do_execve() 核心函式開始處的 fentry 掛鉤點。與 kprobe 範例類別似,上下文引數反映了你想要附加此 eBPF 程式的核心函式所接收的引數。
BPF_PROG 巨集是 libbpf 提供的便捷包裝器,它允許使用型別化引數而不是通用上下文指標。這大提高了程式碼的可讀性和型別安全性。第一個引數是函式名稱,後續引數是函式接收的引數型別和名稱,這與核心函式定義必須比對。
Fexit 的獨特優勢
Fentry 和 fexit 附加點不僅比 kprobe 更高效,而與 fexit 還有一個 kretprobe 所沒有的優勢:fexit 掛鉤可以同時存取函式的輸入引數和回傳值。
比較 kretprobe 和 fexit 的差異:
// kretprobe 版本
SEC("kretprobe/do_unlinkat")
int BPF_KRETPROBE(do_unlinkat_exit, long ret)
{
// 只能存取回傳值
}
// fexit 版本
SEC("fexit/do_unlinkat")
int BPF_PROG(do_unlinkat_exit, int dfd, struct filename *name, long ret)
{
// 可以同時存取輸入引數和回傳值
}
在 kretprobe 版本中,eBPF 程式只能接收 ret 引數,它儲存了 do_unlinkat() 的回傳值。而在 fexit 版本中,程式不僅能存取回傳值 ret,還能存取 do_unlinkat() 的輸入引數 dfd 和 name。
這一差異在實際應用中非常重要。例如,當你需要根據函式的輸入和輸出進行決策時,fexit 可以讓你在一個處理程式中完成,而使用 kretprobe 則需要額外的工作來儲存和關聯輸入引數。這使得 fexit 在需要上下文完整性的場景中特別有價值。
Tracepoints:穩定的核心追蹤點
Tracepoints 是核心程式碼中的標記位置,它們不僅限於 eBPF 使用,長期以來一直被用於生成核心追蹤輸出和被 SystemTap 等工具使用。
Tracepoints 的優勢
與使用 kprobe 附加到任意指令不同,tracepoints 在核心發布版本之間是穩定的(雖然較舊的核心可能沒有新核心中增加的全部 tracepoints)。
你可以透過檢視 /sys/kernel/tracing/available_events 來檢視核心上可用的追蹤子系統:
$ cat /sys/kernel/tracing/available_events
tls:tls_device_offload_set
tls:tls_device_decrypted
...
syscalls:sys_exit_execveat
syscalls:sys_enter_execveat
syscalls:sys_exit_execve
syscalls:sys_enter_execve
...
在我的 5.15 版本核心中,這個列表中定義了超過 1,400 個 tracepoints。
定義 Tracepoint 程式
Tracepoint eBPF 程式的 section 定義應該比對這些專案之一,以便 libbpf 可以自動將其附加到 tracepoint。定義的格式為 SEC("tp/tracing subsystem/tracepoint name")。
例如,要附加到 syscalls:sys_enter_execve tracepoint(當核心開始處理 execve() 呼叫時觸發),section 定義如下:
SEC("tp/syscalls/sys_enter_execve")
section 名稱在 eBPF 程式中非常重要,它告訴載入器(如 libbpf)該如何處理和附加這個程式。格式 tp/subsystem/name 指定了這是一個 tracepoint 程式,以及它應該附加到哪個具體的 tracepoint。libbpf 會解析這個字串並自動處理附加細節。
Tracepoint 上下文結構
那麼 tracepoint 的上下文引數是什麼?每個 tracepoint 都有一個格式,描述從中追蹤出的欄位。例如,以下是 execve() 系統呼叫入口處 tracepoint 的格式:
$ cat /sys/kernel/tracing/events/syscalls/sys_enter_execve/format
name: sys_enter_execve
ID: 622
format:
field:unsigned short common_type; offset:0; size:2; signed:0;
field:unsigned char common_flags; offset:2; size:1; signed:0;
field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
field:int common_pid; offset:4; size:4; signed:1;
field:int __syscall_nr; offset:8; size:4; signed:1;
field:const char * filename; offset:16; size:8; signed:0;
field:const char *const * argv; offset:24; size:8; signed:0;
field:const char *const * envp; offset:32; size:8; signed:0;
print fmt: "filename: 0x%08lx, argv: 0x%08lx, envp: 0x%08lx",
((unsigned long)(REC->filename)), ((unsigned long)(REC->argv)),
((unsigned long)(REC->envp))
根據這些訊息,我們可以定義一個比對的結構:
struct my_syscalls_enter_execve {
unsigned short common_type;
unsigned char common_flags;
unsigned char common_preempt_count;
int common_pid;
long syscall_nr;
long filename_ptr;
long argv_ptr;
long envp_ptr;
};
需要注意的是,eBPF 程式不允許存取前四個欄位。如果嘗試存取它們,程式將無法透過驗證,並顯示 “invalid bpf_context access” 錯誤。
前四個欄位(以 common_ 開頭)是所有 tracepoint 共用的通用欄位,包含了追蹤系統的內部訊息。eBPF 程式被禁止存取這些欄位是出於安全考慮,因為它們可能包含敏感訊息或被用於繞過安全機制。實際上,我們通常只關心特定 tracepoint 的獨特欄位,例如這裡的 filename_ptr、argv_ptr 和 envp_ptr。
使用 Tracepoint 程式
附加到這個 tracepoint 的 eBPF 程式可以使用指向這個型別的指標作為其上下文引數:
int tp_sys_enter_execve(struct my_syscalls_enter_execve *ctx) {
// 程式碼實作
}
然後可以存取這個結構的內容。例如,可以取得檔案名指標:
bpf_probe_read_user_str(&data.command, sizeof(data.command), ctx->filename_ptr);
bpf_probe_read_user_str() 函式安全地從使用者空間讀取字串到 eBPF 程式的記憶體中。這是必要的,因為 eBPF 程式不能直接解參照使用者空間指標。
bpf_probe_read_user_str() 是專門設計用來從使用者空間安全讀取字串的輔助函式。它處理了所有必要的檢查和邊界條件,確保即使用者空間記憶體無效或不可讀,eBPF 程式也不會當機。它還會自動處理字串的結束符,確保讀取的資料是有效的 C 字串。
Raw Tracepoint
當你使用 tracepoint 程式型別時,傳遞給 eBPF 程式的結構已經從一組原始引數對映而來。為了獲得更好的效能,你可以使用 raw tracepoint eBPF 程式型別直接存取這些原始引數。
section 定義應該以 raw_tp(或 raw_tracepoint)而不是 tp 開頭。你需要將引數從 __u64 轉換為 tracepoint 結構使用的任何型別(當 tracepoint 是系統呼叫的入口時,這些引數取決於系統呼叫的定義)。
Raw tracepoint 提供了更高的效能,因為它跳過了引數對映層,直接提供原始引數。這減少了核心中的處理開銷,但需要在 eBPF 程式中進行更多的手動轉換。對於高效能需求或需要存取未在標準 tracepoint 格式中公開的資料的場景,raw tracepoint 是理想選擇。
深入理解核心追蹤技術的選擇
在實際應用中,選擇合適的追蹤技術取決於多種因素:
- 核心版本相容性:較新的技術如 fentry/fexit 需要較新的核心版本
- 穩定性需求:tracepoints 在核心版本間更穩定,而 kprobes 可能因核心程式碼變化而失效
- 效能考量:fentry/fexit 和 raw tracepoints 通常比 kprobes 和標準 tracepoints 有更好的效能
- 功能需求:fexit 能同時存取輸入引數和回傳值,這是 kretprobe 所不具備的
在我的實踐中,當目標環境使用較新核心時,我傾向於選擇 fentry/fexit,因為它們提供了更好的效能和更豐富的上下文訊息。對於需要在多種核心版本間保持相容的應用,tracepoints 通常是更安全的選擇。
核心追蹤是理解系統行為和診斷問題的強大工具。透過掌握這些不同的追蹤技術及其適用場景,你可以更有效地監控和分析 Linux 系統的行為,無論是用於開發、除錯還是效能最佳化。理解這些技術的細微差別和適用場景,是成為高效核心開發者或系統工程師的關鍵一步。
啟用 BTF 的追蹤點:簡化 eBPF 程式開發
在開發 eBPF 程式時,一個常見的挑戰是確保我們定義的結構體能與執行環境的 kernel 相比對。過去我們需要手動定義結構體,例如像 my_syscalls_enter_execve 這樣的結構來定義 eBPF 程式的上下文引數。這種方法存在風險,因為我們的程式碼可能與執行的 kernel 不比對。
BTF(BPF Type Format)技術為這個問題提供了優雅的解決方案。有了 BTF 支援,我們可以利用 vmlinux.h 中定義的結構體,確保它與傳遞給追蹤點 eBPF 程式的上下文結構完全比對。
BTF 追蹤點的使用方法
要使用 BTF 追蹤點,我們必須使用特定格式的 section 定義:
SEC("tp_btf/tracepoint_name")
這裡的 tracepoint_name 是 /sys/kernel/tracing/available_events 中列出的可用事件之一。
以下是一個實際的例子:
SEC("tp_btf/sched_process_exec")
int handle_exec(struct trace_event_raw_sched_process_exec *ctx)
{
// 程式邏輯實作
return 0;
}
這段程式碼定義了一個 eBPF 程式,它會在系統執行新程式時被觸發(sched_process_exec 事件)。注意結構體名稱的模式:它與追蹤點名稱比對,但字首為 trace_event_raw_。這種命名慣例使得我們能夠輕鬆找到正確的結構體定義。
使用 BTF 的主要優勢在於,我們不需要擔心結構體定義是否正確,因為它們直接來自於正在執行的 kernel。這大降低了 eBPF 程式在不同 kernel 版本間的相容性問題。
使用者空間連線:擴充套件 eBPF 的應用範圍
eBPF 不僅限於 kernel 空間,它還可以連線到使用者空間的事件。這為應用程式的監控和分析提供了強大的功能。主要有三種使用者空間連線方式:
- uprobes:連線到使用者空間函式的入口點
- uretprobes:連線到使用者空間函式的結束點
- 使用者靜態定義的追蹤點(USDTs):連線到應用程式碼或使用者空間程式函式庫中指定的追蹤點
這些連線點都使用 BPF_PROG_TYPE_KPROBE 程式型別。
使用者空間連線例項
如果使用 libbpf,可以利用 SEC() 巨集定義這些使用者空間探針的自動連線點。例如,要連線到 OpenSSL 中 SSL_write() 函式的開始,可以這樣定義 eBPF 程式的 section:
SEC("uprobe/usr/lib/aarch64-linux-gnu/libssl.so.3/SSL_write")
int trace_ssl_write(struct pt_regs *ctx)
{
// 追蹤 SSL_write 函式的實作
return 0;
}
這段程式碼定義了一個 uprobe,它會在 OpenSSL 函式庫的 SSL_write() 函式被呼叫時觸發。struct pt_regs *ctx 引數包含了呼叫時的暫存器狀態,可以從中提取函式引數和其他相關訊息。section 名稱指定了要連線的確切函式路徑,包括分享函式庫的完整路徑。
使用者空間連線的挑戰
在使用者空間進行程式碼檢測時,有幾個需要注意的問題:
-
路徑依賴性:上例中分享函式庫的路徑是特定於架構的(aarch64-linux-gnu),在不同架構上可能需要不同的定義。
-
環境不確定性:除非你控制執行程式碼的機器,否則無法確定哪些使用者空間程式函式庫和應用程式會被安裝。
-
獨立二進位檔:應用程式可能被構建為獨立的二進位檔,不會使用你可能連線的分享程式函式庫中的探針。
-
容器環境:容器通常有自己的檔案系統副本,其中安裝了自己的依賴項。容器中分享程式函式庫的路徑與主機上的路徑不同。
-
程式語言差異:eBPF 程式可能需要了解應用程式的編寫語言。例如,C 語言通常使用暫存器傳遞函式引數,而 Go 語言則使用堆積積疊,因此包含暫存器訊息的
pt_args結構可能用處有限。
儘管存在這些挑戰,使用 eBPF 檢測使用者空間應用程式仍有許多實用工具。例如,可以鉤入 SSL 程式函式庫來追蹤加密訊息的解密版本,或者使用 Parca 等工具對應用程式進行持續剖析。
LSM:利用 eBPF 增強系統安全性
BPF_PROG_TYPE_LSM 程式型別連線到 Linux 安全模組(LSM)API,這是 kernel 內的一個穩定介面,最初設計用於讓 kernel 模組實施安全策略。現在,eBPF 安全工具也可以使用這個介面。
LSM 程式的特性
BPF_PROG_TYPE_LSM 程式使用 bpf(BPF_RAW_TRACEPOINT_OPEN) 進行連線,在許多方面它們被視為追蹤程式。但與追蹤程式有一個重要區別:LSM 程式的回傳值會影響 kernel 的行為。
SEC("lsm/file_open")
int BPF_PROG(restrict_file_open, struct file *file)
{
// 實作安全檢查邏輯
if (should_block_access(file)) {
return -EPERM; // 非零回傳碼表示安全檢查未透過
}
return 0; // 允許操作繼續
}
這個 LSM eBPF 程式會在檔案開啟操作時被呼叫。它檢查是否應該阻止存取該檔案,如果應該阻止,則回傳非零錯誤碼(這裡是 -EPERM,表示操作不被允許)。非零回傳碼表示安全檢查未透過,kernel 不會繼續執行被請求的操作。這與效能相關的程式型別不同,後者的回傳碼會被忽略。
LSM 程式型別並非唯一在安全方面發揮作用的型別。許多網路相關的程式型別也可以用於網路安全,用於允許或拒絕網路流量或網路相關操作。
網路功能:eBPF 的網路處理能力
eBPF 提供了多種程式型別,用於在網路堆積積疊的不同點處理網路訊息。這些程式型別都需要 CAP_NET_ADMIN 和 CAP_BPF,或 CAP_SYS_ADMIN 許可權才能被允許執行。
傳遞給這些型別程式的上下文是相關的網路訊息,但結構型別取決於 kernel 在網路堆積積疊相關點擁有的資料。在堆積積疊底部,資料以第 2 層網路封包的形式儲存,這基本上是一系列已經或準備在「線路上」傳輸的位元組。在堆積積疊頂部,應用程式使用通訊端,kernel 建立通訊端緩衝區來處理從這些通訊端傳送和接收的資料。
網路程式型別的特點
與之前討論的追蹤相關型別相比,網路程式型別的一個重要區別是它們通常用於自定義網路行為。這涉及兩個主要特性:
-
回傳碼控制行為:使用 eBPF 程式的回傳碼告訴 kernel 如何處理網路封包——可能是正常處理、丟棄或重定向到不同的目的地。
-
修改能力:允許 eBPF 程式修改網路封包、通訊端設定引數等。
通訊端相關程式型別
在堆積積疊頂部,有一部分網路相關的程式型別與通訊端和通訊端操作相關:
-
BPF_PROG_TYPE_SOCKET_FILTER: 這是最早增加到 kernel 的程式型別。它用於通訊端過濾,但並不是過濾傳送到應用程式或從應用程式傳送的資料。它用於過濾傳送到觀測工具(如 tcpdump)的通訊端資料副本。通訊端特定於第 4 層(TCP)連線。
-
BPF_PROG_TYPE_SOCK_OPS: 允許 eBPF 程式攔截通訊端上發生的各種操作和動作,並為該通訊端設定引數,如 TCP 超時值。通訊端只存在於連線的端點,而不存在於它們可能經過的任何中間裝置。
-
BPF_PROG_TYPE_SK_SKB: 與一種特殊的對映型別一起使用,該對映型別包含對通訊端的一組參照,提供所謂的 sockmap 操作:將流量重定向到不同的目的地。
以下是一個簡單的通訊端過濾器範例:
SEC("socket")
int socket_filter(struct __sk_buff *skb)
{
// 只允許 TCP 封包透過
if (skb->protocol == htons(ETH_P_IP)) {
struct iphdr *iph = (struct iphdr *)(skb->data + skb->mac_header);
if (iph->protocol == IPPROTO_TCP) {
return 0; // 允許 TCP 封包
}
}
return -1; // 丟棄非 TCP 封包
}
這段程式碼實作了一個簡單的通訊端過濾器,它只允許 TCP 封包透過。程式首先檢查封包是否是 IPv4 封包(ETH_P_IP),然後從封包資料中提取 IP 標頭,檢查協定是否是 TCP。如果是 TCP 封包,則回傳 0 表示允許透過;否則回傳 -1 表示丟棄封包。
這種能力使 eBPF 成為實作自定義網路策略、負載平衡和安全控制的強大工具。透過 eBPF,我們可以在不修改 kernel 程式碼的情況下,定製網路堆積積疊的行為。
eBPF 程式型別的實際應用
理解不同 eBPF 程式型別的附加點和功能對於選擇合適的工具解決特定問題至關重要。以下是一些實際應用場景:
-
效能監控與分析: 使用追蹤點和 kprobes/uprobes 監控系統和應用程式的效能,識別瓶頸和異常。
-
安全監控與強制: 利用 LSM 程式型別實作自定義安全策略,監控和控制系統呼叫、檔案存取和網路通訊。
-
網路最佳化與控制: 使用網路相關程式型別實作自定義負載平衡、流量控制和網路安全策略。
-
應用程式級可觀測性: 透過 uprobes 和 USDTs 深入瞭解應用程式的行為,包括函式呼叫、延遲和資源使用。
eBPF 的強大之處在於它能夠在不修改 kernel 或應用程式碼的情況下,提供這些功能。這使得 eBPF 成為現代 Linux 系統中不可或缺的工具。
當開發 eBPF 程式時,選擇正確的程式型別和附加點是成功的關鍵。理解每種程式型別的能力和限制,可以幫助我們設計出高效與功能強大的 eBPF 解決方案。
在實際應用中,玄貓發現 eBPF 程式的靈活性和效能使其成為解決各種系統和網路問題的理想工具。隨著 Linux kernel 的不斷發展,eBPF 的功能也在不斷擴充套件,為系統和應用程式的可觀測性、安全性和效能最佳化提供了越來越多的可能性。
eBPF 技術的廣泛應用正在改變我們監控、保護和最佳化 Linux 系統的方式,使我們能夠以前所未有的靈活性和精確度瞭解系統的內部工作。無論是開發人員、系統管理員還是安全工作者,掌握 eBPF 技術都將為我們提供強大的工具來應對現代計算環境的挑戰。
Linux 網路控制機制的進階應用
在現代網路基礎設施中,對網路流量的精確控制變得越來越重要。無論是提高效能、實施安全策略,還是最佳化資源分配,Linux 核心提供了一系列強大的網路控制機制。本文將探討 Traffic Control、XDP 以及 eBPF 如何協同工作,為網路管理提供前所未有的靈活性。
深入理解 Traffic Control
Traffic Control(簡稱 TC)是 Linux 核心中一個功能強大的子系統,用於管理網路封包的處理方式。若你曾經檢視過 tc 命令的手冊頁,就會發現這個系統的複雜性以及它對網路計算的重要性。TC 提供了對網路封包處理方式的深度控制能力,這對於現代網路環境至關重要。
在實務應用中,TC 允許我們透過 eBPF 程式附加自定義過濾器和分類別器,處理進入(ingress)和外出(egress)的網路流量。這是 Cilium 專案的基礎構建塊之一。這種功能可以透過程式化方式實作,也可以直接使用 tc 命令來操作 eBPF 程式。
在我的網路最佳化實踐中,發現 TC 結合 eBPF 特別適合以下場景:
- 流量整形與限速
- 封包優先順序處理
- 自定義封包過濾規則
- 網路監控與分析
實作自定義封包處理
使用 eBPF 與 TC 結合,可以實作高度自定義的封包處理邏輯。例如,以下是一個簡單的範例,展示如何使用 tc 命令附加 eBPF 程式到網路介面:
# 編譯 eBPF 程式
clang -O2 -target bpf -c tc_filter.c -o tc_filter.o
# 附加到網路介面的入口點
tc qdisc add dev eth0 clsact
tc filter add dev eth0 ingress bpf direct-action obj tc_filter.o sec classifier
這種方式允許我們在網路堆積積疊的較低層次上操作封包,提供極大的靈活性。
XDP:極速資料路徑
XDP(eXpress Data Path)代表了 Linux 網路處理的一個重大進步。它允許在網路封包到達系統最早的階段進行處理,甚至在進入主要網路堆積積疊之前。這提供了前所未有的效能和靈活性。
XDP 程式的附加與管理
XDP 程式需要附加到特定的網路介面,不同介面可以附加不同的 XDP 程式。以下是使用 bpftool 載入和附加 XDP 程式的方式:
bpftool prog load hello.bpf.o /sys/fs/bpf/hello
bpftool net attach xdp id 540 dev eth0
另外,也可以使用 ip 命令實作相同功能:
ip link set dev eth0 xdp obj hello.bpf.o sec xdp
這個命令從 hello.bpf.o 物件中讀取標記為 xdp 區段的 eBPF 程式,並將其附加到 eth0 網路介面。使用 ip link show 命令可以檢視附加到介面的 XDP 程式訊息:
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500
xdpgeneric qdisc fq_codel
state UP mode DEFAULT group default qlen 1000
link/ether 52:55:55:3a:1b:a2 brd ff:ff:ff:ff:ff:ff
prog/xdp id 1255 tag 9d0e949f89f1a82c jited
移除 XDP 程式可以使用以下命令:
ip link set dev eth0 xdp off
XDP 的技術優勢
XDP 之所以能提供極高效能,主要是因為它在網路處理的最早階段執行 - 在封包剛從網路卡到達時。這意味著:
- 可以在封包進入主要網路堆積積疊前就做出決策(丟棄、修改或透過)
- 避免了不必要的記憶體複製和上下文切換
- 某些情況下可以解除安裝到支援的網路卡上執行,進一步提高效能
這使得 XDP 特別適合需要高吞吐量、低延遲處理的場景,如 DDoS 防禦、負載平衡和封包過濾。
流量解析器(Flow Dissector)
在網路堆積積疊的不同位置,系統需要從封包頭中提取詳細訊息,這就是流量解析器的工作。eBPF 程式型別 BPF_PROG_TYPE_FLOW_DISSECTOR 可以實作自定義封包解析。
自定義流量解析器特別有用的場景包括:
- 支援新的封包格式或協定
- 最佳化特定流量型別的處理
- 實作自定義負載平衡邏輯
- 提取特定應用層協定的訊息
輕量級隧道(Lightweight Tunnels)
BPF_PROG_TYPE_LWT_* 系列程式型別可用於在 eBPF 程式中實作網路封裝。這些程式型別也可以使用 ip 命令進行操作,但這次是使用 route 子命令。在實際應用中,這些程式型別使用頻率較低,但在特定場景下非常有價值,如自定義隧道協定實作或網路虛擬化解決方案。
Cgroups 與 eBPF 的結合
Cgroups 的網路控制能力
Cgroups(控制群組)是 Linux 核心中的一個概念,用於限制一組程式可以存取的資源集。Cgroups 是隔離一個容器(或 Kubernetes Pod)與另一個的機制之一。將 eBPF 程式附加到 cgroup 可以實作僅適用於該 cgroup 程式的自定義行為。
值得注意的是,所有程式都與某個 cgroup 相關聯,包括那些不在容器內執行的程式。這為系統範圍內的資源控制提供了強大的機制。
Cgroup 相關的 eBPF 程式型別
目前,cgroup 相關的程式型別幾乎都與網路有關,包括:
BPF_PROG_TYPE_CGROUP_SOCK:處理 socket 操作BPF_PROG_TYPE_CGROUP_SKB:處理網路資料傳輸BPF_CGROUP_SYSCTL:處理影響特定 cgroup 的 sysctl 命令
這些程式可以決定是否允許特定 cgroup 執行請求的 socket 操作或資料傳輸,非常適合實作網路安全策略。Socket 程式甚至可以欺騙呼叫程式,使其以為正在連線到特定目標地址。
紅外線控制器
程式型別 BPF_PROG_TYPE_LIRC_MODE2 可以附加到紅外線控制器裝置的檔案描述符,為紅外線協定提供解碼功能。雖然目前這種程式型別需要 CAP_NET_ADMIN 許可權,但它展示了 eBPF 的應用範圍遠超出追蹤和網路領域。
eBPF 附加型別詳解
附加型別(Attachment Type)提供了更細粒度的控制,決定程式可以附加到系統中的哪些位置。對於某些程式型別,它們與可以附加的鉤子型別有一對應關係,因此附加型別由程式型別隱式定義。例如,XDP 程式附加到網路堆積積疊中的 XDP 鉤子。
附加型別參與決定哪些輔助函式是有效的,並且在某些情況下還限制對上下文訊息的存取。這就是為什麼有時候驗證器會給出 “invalid bpf_context access” 錯誤。
附加型別與程式型別的關係
可以透過檢視核心函式 bpf_prog_load_check_attach(定義在 bpf/syscall.c 中)瞭解哪些程式型別需要指定附加型別,以及哪些附加型別是有效的。
例如,對於 CGROUP_SOCK 型別的程式,有效的附加型別包括:
case BPF_PROG_TYPE_CGROUP_SOCK:
switch (expected_attach_type) {
case BPF_CGROUP_INET_SOCK_CREATE:
case BPF_CGROUP_INET_SOCK_RELEASE:
case BPF_CGROUP_INET4_POST_BIND:
case BPF_CGROUP_INET6_POST_BIND:
return 0;
default:
return -EINVAL;
}
這表明該程式型別可以附加到多個位置:socket 建立時、socket 釋放時,或者在 IPv4 或 IPv6 中完成 bind 操作後。
附加型別的實際意義
附加型別實際上定義了 eBPF 程式在系統中的精確執行點。這種細粒度的控制使得開發者可以:
- 精確選擇程式執行的時機(例如,在 socket 建立前還是建立後)
- 存取特定於該執行點的上下文資料
- 使用僅在特定附加點有效的輔助函式
- 實作針對特定系統操作的精確控制邏輯
libbpf 檔案中也列出了每種程式和附加型別的有效區段名稱,這對於使用 libbpf 開發 eBPF 應用非常有用。
實用應用場景與最佳實踐
根據以上討論的各種 eBPF 程式型別和附加點,以下是一些實用的應用場景和最佳實踐:
網路安全增強
使用 XDP 和 TC 程式實作高效的防火牆和 DDoS 防禦措施:
SEC("xdp")
int ddos_defense(struct xdp_md *ctx) {
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
struct ethhdr *eth = data;
// 確保封包至少包含乙太網頭部
if (data + sizeof(*eth) > data_end)
return XDP_PASS;
// 分析封包並決定是否丟棄
// ...
// 對於可疑流量,直接丟棄
return XDP_DROP;
}
容器網路策略實施
使用 Cgroup eBPF 程式實作細粒度的容器網路策略:
SEC("cgroup/skb")
int container_policy(struct __sk_buff *skb) {
// 提取源和目標 IP 地址
// ...
// 檢查是否符合策略
if (is_allowed(src_ip, dst_ip, dst_port))
return 1; // 允許
return 0; // 拒絕
}
自定義負載平衡
使用 XDP 或 TC 程式實作高效的負載平衡器:
SEC("xdp")
int load_balancer(struct xdp_md *ctx) {
// 提取封包訊息
// ...
// 選擇後端伺服器
int backend_index = select_backend(hash);
// 修改目標 IP 和 MAC
update_packet_headers(ctx, backend_index);
return XDP_TX; // 修改後從同一介面傳送
}
網路監控與分析
使用 TC 和 kprobe 程式收集深入的網路指標:
SEC("tc")
int network_monitor(struct __sk_buff *skb) {
// 提取封包訊息
// ...
// 更新統計資料
update_stats(src_ip, dst_ip, len);
return TC_ACT_OK; // 繼續正常處理
}
深入理解 eBPF 程式型別選擇
選擇正確的 eBPF 程式型別對於實作預期功能至關重要。如果想要回應特定事件,需要確定適合該事件的程式型別。傳遞給程式的上下文取決於程式型別,核心對程式回傳碼的回應也可能因型別而異。
在實際開發中,我發現以下考量因素非常重要:
-
執行時機:程式需要在網路堆積積疊的哪個階段執行?越早執行效能越高,但可用訊息和操作可能有限。
-
可用上下文:不同程式型別可存取的上下文資料不同。例如,XDP 程式只能存取基本的封包資料,而 TC 程式可以存取更多網路堆積積疊訊息。
-
輔助函式可用性:不同程式型別可使用的輔助函式集合不同。
-
效能需求:XDP 提供最高效能但功能有限,TC 次之但功能更豐富,通訊端層程式功能最完整但效能較低。
-
操作範圍:是針對單個網路介面、特定 cgroup 還是全系統範圍?
透過仔細評估這些因素,可以選擇最適合特定使用案例的 eBPF 程式型別。
實用練習與深入探索
為了更好地理解 eBPF 程式型別,可以嘗試以下練習:
-
使用
strace捕捉bpf()系統呼叫,觀察不同程式型別的載入過程:strace -e bpf -o outfile ./hello這將記錄每個
bpf()系統呼叫的訊息到名為outfile的檔案中。查詢檔案中的BPF_PROG_LOAD指令,觀察不同程式的prog_type欄位如何變化。 -
修改範例使用者空間程式碼,只載入和附加一個 eBPF 程式,而不從原始碼中移除其他程式。
-
編寫 kprobe 或 fentry 程式,在呼叫某些其他核心函式時觸發。
這些練習將幫助更深入理解 eBPF 程式型別和附加機制,為開發更複雜的 eBPF 應用奠定基礎。
技術發展與未來趨勢
eBPF 技術正在快速發展,未來可能出現更多程式型別和附加點。值得關注的趨勢包括:
-
硬體解除安裝能力增強:更多網路卡將支援 XDP 解除安裝,提供更高效能。
-
更廣泛的應用領域:eBPF 將擴充套件到更多非網路和非追蹤領域。
-
更豐富的輔助函式:新的輔助函式將使 eBPF 程式能夠執行更複雜的操作。
-
更好的開發工具:更完善的開發、除錯和分析工具將使 eBPF 開發更加容易。
-
與其他技術的整合:eBPF 將與容器、服務網格和雲原生技術更深入整合。
透過掌握 eBPF 程式型別和附加機制的基礎知識,開發者將能夠更好地利用這一強大技術,構建高效、靈活的網路和系統解決方案。
網路和系統程式設計領域的發展日新月異,eBPF 作為一項革命性技術,正在改變我們構建和最佳化系統的方式。透過深入理解 eBPF 程式型別和附加點,我們能夠更精確地控制系統行為,實作前所未有的效能和安全性。無論是網路最佳化、安全增強還是系統監控,eBPF 都提供了強大而靈活的工具。
隨著技術的不斷發展,持續學習和實踐將是掌握這一領域的關鍵。希望本文能為讀者提供有價值的見解,幫助更好地理解和應用 eBPF 技術。