eBPF 介紹
eBPF 是一項革命性的技術,它能在操作系統內核中運行沙箱程序。被用於安全並有效地擴展內核的能力而無需修改內核代碼或者加載內核模塊。
從古至今,由於內核有監視和控制整個系統的特權,操作系統一直都是實現可觀察性、安全性和網絡功能的理想場所。同時,操作系統內核也很難進化,因爲它的核心角色以及對穩定和安全的高度要求。因此,操作系統級別的創新相比操作系統之外實現的功能較少。
eBPF 從根本上改變了這個定律。通過允許在操作系統內運行沙箱程序,應用開發者能夠運行 eBPF 程序在運行時爲操作系統增加額外的功能。然後操作系統保證安全和執行效率,就像藉助即時編譯器(JIT compiler)和驗證引擎在本地編譯那樣。這引發了一波基於 eBPF 的項目,涵蓋了一系列廣泛的使用案例,包括下一代網絡、可觀察性和安全功能。
現在,eBPF 被廣泛用於:在現代數據中心和雲原生環境中提供高性能網絡和負載均衡;以低開銷提取細粒度的安全可觀察性數據;幫助應用開發者追蹤應用程序;洞悉性能問題和加強容器運行時的安全性等等。一切皆有可能,而 eBPF 釋放的創新纔剛剛開始。
介紹
如果你想要深入瞭解 eBPF,查看 eBPF & XDP Reference Guide。無論你是一個想要構建 eBPF 程序的開發者還是對使用 eBPF 技術的解決方案感興趣,都有必要了解一下基本概念和架構。
鉤子(Hook)
eBPF 程序是事件驅動的,當內核或應用程序通過某個錨點時就會運行。預定義的鉤子包括系統調用、函數進入 / 退出、內核追蹤點、網絡事件等等。
如果預定義的鉤子不存在,可以創建一個內核探針(kprobe)或用戶探針(uprobe)來將 eBPF 程序附加至內核或用戶應用程序的任何地方。
怎麼寫 eBPF 程序?
在很多情況下,並不直接使用 eBPF,而是通過 Cilium、bcc 或 bpftrace 等項目間接使用,它們在 eBPF 之上提供了一層抽象,無需直接編寫程序而是提供了一些能力,由 eBPF 來實現。
要是沒有上層抽象的話,就要直接編寫程序了。Linux 內核期望 ePBF 程序以字節碼的形式加載。直接編寫字節碼不太可能,實際開發中更常見的是使用 LLVM 等編譯器套件將僞 C 代碼編譯成 eBPF 字節碼。
Loader & Verification 架構
當所需的鉤子被確定後,可以使用 bpf 系統調用 將 eBPF 程序加載至 Linux 內核。通常使用 eBPF 庫完成,下節將介紹可用的開發工具鏈。
當程序被加載至 Linux 內核時,在被連接到所請求的鉤子之前要經過兩個步驟:
驗證
驗證步驟確保 eBPF 程序可以安全運行,將檢查程序是否符合以下條件:
-
加載 eBPF 程序的進程的權限。除非啓用非特權 eBPF,否則只有特權(privileged)進程可以加載 eBPF 程序。
-
程序不會崩潰或損害系統。
-
程序是會運行完成的(不會處於循環狀態,這樣會耽誤進一步處理)。
JIT 編譯
JIT(Just-In-Time)編譯步驟將程序的通用字節碼翻譯成機器特定的指令集來優化程序的執行速度。這使得 eBPF 程序可以像本地編譯的內核代碼或作爲內核模塊加載的代碼一樣高效運行。
Maps
共享收集的信息和存儲狀態的能力對 eBPF 程序來說至關重要。爲此,eBPF 程序可以利用 eBPF maps 的概念來存儲和檢索數據。eBPF maps 可被 eBPF 程序以及用戶空間的應用程序通過系統調用來訪問。
map 類型支持多種數據結構:
-
哈希表、數組
-
LRU(Least Recently Used)
-
環形緩衝區(Ring Buffer)
-
棧
-
LPM(Longest Prefix match)
輔助調用
eBPF 程序不能隨意調用內核函數。這樣做的話需要使 eBPF 程序與特定的內核版本綁定,並使程序的兼容性變得複雜。相反,eBPF 程序調用輔助函數,這是內核提供的一個穩定的 API。
輔助調用的集合還在持續擴充中,以下是可用的輔助調用:
-
生成隨機數
-
獲取當前時間和日期
-
eBPF map 訪問
-
獲取進程 / cgroup 上下文
-
操控網絡包和轉發邏輯
尾部調用和函數調用
eBPF 程序可與尾部 & 函數調用的概念相結合。函數調用允許在 eBPF 程序中定義和調用函數。尾部調用能夠調用和執行另一個 eBPF 程序並替換執行上下文,類似於常規進程進行 execve() 系統調用。
eBPF 安全性
能力越大責任越大。
eBPF 是一項非常強大的技術,目前運行在許多關鍵軟件基礎設施組件的核心。在 eBPF 的開發中,當 eBPF 被考慮納入 Linux 內核時,eBPF 的安全性是最關鍵的方面。eBPF 的安全性通過幾個層面得到保證:
所需的權限
除非非特權的 eBPF 被啓用,否則所有打算將 eBPF 加載到 Linux 內核的進程必須以特權模式(root 權限)運行或需要 CAP_BPF 能力。這就意味着不受信任的程序無法加載 eBPF 程序。
如果啓用了非特權 eBPF,非特權進程可以加載特定的 eBPF 程序,但功能被閹割,對內核的訪問也受限制。
驗證器
如果一個進程被允許加載 eBPF 程序,所有程序仍會通過 eBPF 驗證器。eBPF 驗證器確保程序本身的安全性。例如:
-
eBPF 程序會被驗證它們總是可以執行完成,永不被阻塞或死循環。只有當驗證器能夠確保循環包含了退出條件並保證爲真時,程序纔會被接受。
-
程序不得使用任何未初始化的變量或越界訪問內存。
-
程序不能太大,不可能加載任意大的 eBPF 程序。
-
程序的複雜度必須有限。驗證器將評估所有可能的執行路徑,並且必須能夠在有限時間內完成分析。
加固
在驗證成功後,eBPF 程序會根據是從特權還是非特權進程加載的來運行一個 “加固進程”:
-
程序執行保護:持有 eBPF 程序的內核內存被保護且只讀。出於任何原因,不論是內核 bug 還是惡意操作,如果 eBPF 程序被試圖修改,內核將崩潰而不是允許它繼續執行被破壞 / 篡改的程序。
-
針對 Spectre 的緩解措施:CPU 可能會預測錯分支,並留下可觀察的副作用,這些副作用可以通過旁路被提取出來。舉幾個例子:eBPF 程序屏蔽了內存訪問來將瞬時指令下的訪問重定向到受控區域,驗證器也會遵循只有在投機執行下才能訪問的程序執行路徑,JIT 編譯器在尾調用不能轉換爲直接調用的情況下發出 Retpolines。
-
常量盲化:代碼中的所有常量都被屏蔽以防止 JIT 噴塗攻擊(JIT spraying)。這可以防止攻擊者將可執行代碼作爲常量注入,存在另一個內核 bug 的情況下,會允許攻擊者跳入 eBPF 程序的內存段執行代碼。
抽象的運行時上下文
eBPF 程序不能隨意直接訪問內核內存,要想訪問程序上下文之外的數據和數據結果必須通過 eBPF 輔助工具(eBPF helper)來完成,如此保證了數據訪問的一致性並限制了 eBPF 程序的訪問權限。舉個栗子,如果確保安全的情況下,一個正在運行的 eBPF 程序就被允許修改某些數據,但不能隨機修改內核中的數據。
開發工具鏈
開發者可以根據不同的需求選擇合適的工具來開發和管理 eBPF 項目:
bcc
BCC 是一個使用戶能夠編寫嵌入 eBPF 程序的 Python 腳本的框架,主要用於追蹤和剖析應用程序和系統,利用 eBPF 程序來在用戶空間收集統計數據或生成事件,並以對人類友好的形式展示。運行 Python 程序會生成 eBPF 字節碼並將其加載進內核。
bpftrace
bpftrace 是一種 Linux eBPF 的高級追蹤語言,在 4.x 版本的內核中可用。bpftrace 使用 LLVM 作爲後端來將腳本編譯爲 eBPF 字節碼,利用 BCC 和 Linux eBPF 子系統以及已有的 Linux 追蹤功能進行交互:內核動態追蹤(kprobes)、用戶級動態追蹤(uprobes)和追蹤點(tracepoint)。bpftrace 語言的靈感來自於 awk、C 和 Dtrace 還有 SystemTap 這樣的老一輩追蹤器。
eBPF Go 庫
gobpf 是一個通用的 eBPF Go 庫,將獲取 eBPF 字節碼的過程和加載 / 管理程序解耦。eBPF 程序通常由高級編程語言編寫,然後使用 clang/LLVM 編譯器來編譯成字節碼。
libbpf C/C++ 庫
libbpf 是一個基於 C/C++ 的通用 eBPF 庫,幫助將由 clang/LLVM 編譯器生成的 eBPF 對象文件和加載至內核解耦,提供易用的 API 來抽象與 BPF 系統調用的交互。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/tEdD8Dw2HfKvpAShcFLpQQ