文章目錄
- 預備知識
- TCP/IP 網絡模型(4層、7層)
- iptables/netfilter
- linux網絡為什么慢
- DPDK
- XDP
- BFP
- eBPF
- XDP
- XDP 程序典型執行流
- 通過網絡協議棧的入包
- XDP 組成
- 使用 GO 編寫 XDP 程序
- 明確流程
- 選擇eBPF庫
- 編寫eBPF代碼
- 編寫Go代碼
- 動態更新黑名單
預備知識
TCP/IP 網絡模型(4層、7層)
TCP/IP網絡模型
- 鏈路層:負責封裝和解封裝IP報文,發送和接受ARP/RARP報文等。
- 網絡層:負責路由以及把分組報文發送給目標網絡或主機。
- 傳輸層:負責對報文進行分組和重組,并以TCP或UDP協議格式封裝報文。
- 應用層:負責向用戶提供應用程序,比如HTTP、FTP、Telnet、DNS、SMTP等。
數據封裝
iptables/netfilter
iptables是一個配置Linux內核防火墻的命令行工具,它基于內核的netfilter機制。
新版本的內核(3.13+)也提供了nftables,用于取代iptables
iptables規則逐漸增加,遍歷iptables效率變得很低,一個表現就是kube-proxy,他是Kubernetes的一個組件,容器要使用iptables和-j DNAT規則為服務提供負載均衡。隨著服務增加,iptable的規則列表指數增長。隨著服務數量的增長,網絡延遲和性能嚴重下降。iptables的還有一個缺點,無法實現增量更新。每次添加新規則時,必須更新整個規則列表。
一個例子:裝配2萬個Kubernetes服務產生16萬條的iptables規則需要耗時5個小時。
在容器環境下還有一個問題:容器的生命周期可能很多,可能一個容器的生命周期只有幾秒,意味著iptables規則需要被快速更新,這也使得依靠使用IP地址進行安全過濾的系統受到壓力,因為集群中的所有節點都必須始終知道最新的IP到容器的映射。
一個解決方案是BPF,Cilium項目就利用了這種技術.
利用BPF構建的bpfilter性能遠高于iptables和nftables, linux內核社區的Florian Westphal提出了一個運行在bpfilter上框架,通過框架并將nftables轉換為BPF。框架允許保持特定領域nftables語句,而且還可以帶有JIT編譯器,硬件卸載和工具集等BPF運行時的所有優點。
linux網絡為什么慢
linux協議棧是在20世紀90年代作為一個通用操作系統實現的,想要支持現代的高速網絡,必須要做優化.
dog250 把linux協議棧重新"分層", 指出了其中的"門", 即那些會嚴重影響性能的門檻
目前有兩個比較火的方案:DPDK和XDP,兩種方案分別在用戶層和內核層直接處理數據包,避免了用戶、內核態切換k開銷。
DPDK
DPDK由intel支持,DPDK的加速方案原理是完全繞開內核實現的協議棧,把數據包直接從網卡拉到用戶態,依靠Intel自身處理器的一些專門優化,來高速處理數據包。
Intel DPDK全稱Intel Data Plane Development Kit,是intel提供的數據平面開發工具集,為Intel architecture(IA)處理器架構下用戶空間高效的數據包處理提供庫函數和驅動的支持,它不同于Linux系統以通用性設計為目的,而是專注于網絡應用中數據包的高性能處理。
DPDK應用程序是運行在用戶空間上利用自身提供的數據平面庫來收發數據包,繞過了Linux內核協議棧對數據包處理過程。Linux內核將DPDK應用程序看作是一個普通的用戶態進程,包括它的編譯、連接和加載方式和普通程序沒有什么兩樣。DPDK程序啟動后只能有一個主線程,然后創建一些子線程并綁定到指定CPU核心上運行。
在現有的所有框架中,內核旁路方式性能是最高的,這種方式在管理、維護和安全方面都存在不足。
XDP 采用了一種與內核旁路截然相反的方式:相比于將網絡硬件的控制權上移到用戶空間, XDP 將性能攸關的包處理操作直接放在內核中,在操作系統的網絡棧之前執行。
這種方式同樣避免了內核-用戶態切換開銷(所有操作都在內核);
但仍然由內核來管理硬件,因此保留了操作系統提供的管理接口和安全防護能力;
這里的主要創新是:使用了一個虛擬執行環境,它能對加載的 程序進行校驗,確保它們不會對內核造成破壞。
XDP
BFP
BPF (Berkeley Packet Filter) 是一個非常高效的網絡包過濾機制,它的目標是避免不必要的用戶空間申請。它直接在內核空間處理網絡數據包。 BPF 支持的最常見的應用就是 tcpdump 工具中使用的過濾器表達式。在 tcpdump 中,表達式被編譯轉換為 BPF 的字節碼。內核加載這些字節碼并且用在原始網絡包流中,以此來高效的把符合過濾條件的數據包發送到用戶空間。
eBPF
eBPF(extended Berkeley Packet Filter)起源于BPF,它提供了內核的數據包過濾機制。
- BPF is a highly flexible and efficient virtual machine-like construct in the Linux kernel allowing to execute bytecode at various hook points in a safe manner. It is used in a number of Linux kernel subsystems, most prominently networking, tracing and security (e.g. sandboxing).
- BPF的最原始版本為cBPF,曾用于tcpdump
- Berkeley Packet Filter 盡管名字的也表明最初是設計用于packet filtering,但是現在已經遠不止networking上面的用途了.
基于bpf 這個項目 開發了很多有用的小工具, 具體如下圖
eBPF 是對 Linux 觀測系統 BPF 的擴展和加強版本。可以把它看作是 BPF 的同類。有了 eBPF 就可以自定義沙盒中的字節碼,這個沙盒是 eBPF 在內核中提供的,可以在內核中安全的執行幾乎所有內核符號表拋出的函數,而不用擔心搞壞內核。實際上,eBPF 也是加強了在和用戶空間交互的安全性。在內核中的檢測器會拒絕加載引用了無效指針的字節碼或者是以達到最大棧大小限制。循環也是不允許的(除非在編譯時就知道是有常數上線的循環),字節碼只能夠調用一小部分指定的 eBPF 幫助函數。eBPF 程序保證能及時終止,避免耗盡系統資源,而這種情況出現在內核模塊執行中,內核模塊會造成內核的不穩定和可怕的內核奔潰。相反的,你可能會發現和內核模塊提供的自由度來比,eBPF有太多限制了,但是綜合考慮下來還是更傾向于 eBPF,而不是面向模塊的代碼,主要是基于授權后的 eBPF 不會對內核造成損害。然而這還不是它唯一的優勢。
XDP操作模式
XDP支持3種工作模式,默認使用native模式:
- Native XDP:在native模式下,XDP BPF程序運行在網絡驅動的早期接收路徑上(RX隊列),因此,使用該模式時需要網卡驅動程序支持。
- Offloaded XDP:在Offloaded模式下,XDP BFP程序直接在NIC(Network Interface Controller)中處理數據包,而不使用主機CPU,相比native模式,性能更高
- Generic XDP:Generic模式主要提供給開發人員測試使用,對于網卡或驅動無法支持native或offloaded模式的情況,內核提供了通用的generic模式,運行在協議棧中,不需要對驅動做任何修改。生產環境中建議使用native或offloaded模式
XDP
XDP的意思是eXpress Data Path,它能夠在網絡包進入用戶態直接對網絡包進行過濾或者處理。XDP依賴eBPF技術。
XDP 或 Express Data Path 的興起是因為 Linux 內核需要一個高性能的包處理能力。很多繞過內核的技術(DPDK是最突出的一個)目標都是通過把包處理遷移到用戶空間來加速網絡操作。
這就意味著要消除內核-用戶空間邊界之間的上下文切換、系統調用轉換或 IRQ 請求所引起的開銷。操作系統將網絡堆棧的控制權交給用戶空間進程,這些進程通過自己的驅動程序直接與 NIC 交互。
雖然這種做法的帶來了明顯的高性能,但是它也帶來了一系列的缺陷,包括在用戶空間要重新實現 TCP/IP 協議棧以及其它網絡功能,或者是放棄了內核中強大的資源抽象管理和安全管理。
XDP 的目的是在內核中也達到可編程的包處理,并且仍然保留基礎的網絡協議棧模塊。實際上,XDP 代表了 eBPF 指令的自然擴展能力。它使用 maps,可管理的幫助函數,沙箱字節運行器來做到可編程,這些字節碼會被檢測安全之后才會加載到內核中運行。
XDP 高速處理路徑的關鍵點在于這些編程字節碼被加載到網絡協議棧最早期的可能處理點上,就在網絡包接受隊列(RX)之后。在網絡協議棧的這一階段中,還沒有構建網絡包的任何內核屬性,所以非常有利于提升網絡處理速度。
相對于DPDK,XDP具有以下優點
- 無需第三方代碼庫和許可
- 同時支持輪詢式和中斷式網絡
- 無需分配大頁
- 無需專用的CPU
- 無需定義新的安全網絡模型
XDP的使用場景包括
- DDoS防御
- 防火墻
- 基于XDP_TX的負載均衡
- 網絡統計
- 復雜網絡采樣
- 高速交易平臺
為了強調 XDP 在網絡協議棧中的位置,讓我們來一起看看一個 TCP 包的生命過程,從它到達 NIC 知道它發送到用戶空間的目的 socket。始終要記住這是一個高級別的視圖。我們將只觸及這個復雜的核心網絡堆棧的表面層。
XDP 程序典型執行流
下圖是一個典型的 XDP 程序執行流:
網卡收到一個包時,XDP程序依次執行:
- 提取包頭中的信息(例如 IP、MAC、Port、Proto 等),
執行到程序時,系統會傳遞給它一個上下文對象(context object)作為參賽 (即 struct xdp_md *ctx,后面有例子),其中包括了指向原 始包數據的指針,以及描述這個包是從哪個網卡的哪個接口接收上來的等元數據字段。
- 讀取或更新一些資源的元信息(例如更新統計信息);
解析包數據之后,XDP 程序可以讀取 ctx 中的包元數據(packet metadata) 字段,例如從哪個網卡的哪個接口收上來的(ifindex)。除此之外,ctx 對象還允許 程序訪問與包數據毗鄰的一塊特殊內存區域(cb, control buffer), 在包穿越整個系統的過程中,可以將自定義的數據塞在這里。
除了 per-packet metadata,XDP 程序還可以通過 BPF map 定義和訪問自己的持久數據 ,以及通過各種 helper 函數訪問內核基礎設施。
BPF map 使 BPF 程序能與系統的其他部分之間通信;
Helpers 使 BPF 程序能利用到某些已有的內核功能(例如路由表), 而無需穿越整個內核網絡棧。
如果有需要,對這個包進行 rewrite header 操作,
程序能修改包數據的任何部分,包括添加或刪除包頭。這使得 XDP 程序能執行封裝/接封裝操作,以及重寫(rewrite)地址字段然后轉發等操作。
內核 helper 函數各有不同用途,例如修改一個包之后,計算新的校驗和(checksum)。
進行最后的判決(verdict),確定接下來對這個包執行什么操作;
程序還能通過尾調用(tail call),將控制權交給另一個 XDP 程序; 通過這種方式,可以將一個大程序拆分成幾個邏輯上的小程序(例如,根據 IPv4/IPv6)。
由于 XDP 程序可包含任意指令,因此前三步(讀取包數據、處理元數據、重寫包數據) 順序可以是任意的,而且支持多層嵌套。 但實際中為了獲得高性能,大部分情況下還是將執行結構組織成這順序的三步。
通過網絡協議棧的入包
網卡在收到一幀(所有校驗和正常檢查)時,網卡就會使用 DMA 來轉發數據包到對于的內存區域。這意味著數據包是由驅動做了映射后直接從網卡隊列拷貝到主內存區。當環形接受隊列有數據進入的時候,網卡會產生一個硬中斷,并且 CPU 會把處理事件下發到中斷向量表中,執行驅動代碼。
因為驅動的執行路徑必須非常短快,具體數據處理可以延遲到驅動中斷上下文之外,使用軟中斷來觸發處理(NET_RX_SOFTIRQ)。在中斷處理的時候中斷請求是被屏蔽的,內核更愿意把這種長時間處理的任務放在中斷上下文之外,以避免在中斷處理的時候丟失中斷事件。設備驅動開始使用 NAPI 循環和一個 CPU 一個內核線程(ksoftirqd)來從環形緩沖區中消費數據包。NAPI 循環的責任主要就是觸發軟中斷(NET_RX_SOFTIRQ),由軟中斷處理程序處理數據包并且發送數據到網絡協議棧。
設備驅動申請一個新的 socket 緩沖區(sk_buff)來存放入流量包。socket 緩沖區是內核中對數據包緩沖/處理抽象出來的一個最基礎的數據結構。在整個網絡協議棧中的上層中都在使用。
socket 緩沖區的結構體由多個字段,來標識不同的網絡層。
從 CPU 隊列上消費緩沖數據后,內核會填充這些元數據,復制 sk_buff 并且把它推到上游的網絡層的自隊列中做進一步處理。這是 IP 協議層在堆棧中注冊的位置。IP 層執行一些基本的完整型檢測,并且把包發送給 netfilter 的鉤子函數。如果包沒有被 netfilter 丟棄,IP 層會檢測高級協議,并且為之前提取的協議把處理交給響應的處理函數。
數據最終被拷貝到 socket 關聯的用戶空間緩沖區。進程通過阻塞系統調用(recv、read)函數或通過某種輪詢機制(epoll)主動接收數據。
在網卡把數據包拷貝到接受隊列之后就觸發了 XDP 的鉤子函數,在這一點上我們可以高效的阻止申請各種各樣的元數據結構,包括 sk_buffer。
如果我們看一下非常簡單的可能使用場景,比如在高流量網絡中的包過濾或者阻止 DDos 攻擊,傳統的網絡防火墻方案(iptables)由于網絡堆棧中的每個階段都會引入大量的工作負載,這將不可避免地給機器造成壓力。
在裸機速度下的 eBPF 和 XDP 包處理流程
在網絡協議棧中的 XDP 的鉤子
具體上來看在軟中斷任務中調度順序執行的 iptables 規則,會在 IP 協議層中去匹配指定的 IP 地址,以決定是否丟棄這個數據包。和 iptables 不一樣的是 XDP 會直接操作一個從 DMA 后端環形緩沖區中拿的原始的以太幀包,所以丟棄邏輯可以很早的執行,這樣就節省了內核時間,避免了會導致協議棧執行導致的延時。
XDP 組成
正如你已經知道的,eBPF 的字節碼可以掛載在各種策略執行點上,比如內核函數,socket,tracepoint,cgroup 層級或者用戶空間符號。這樣的話,每個 eBPF 程序操作特定的上下文- kprobes 場景下的 CPU 寄存器狀態,socket 程序的 socket 緩沖區等等。用 XDP 的說法,生成的 eBPF 字節碼的主干是圍繞 XDP 元數據上下文建模的(xdp_md)。XDP 上下文包含了所有需要在原始形式下訪問數據包的信息。
為了更好地理解 XDP 程序的關鍵模塊,讓我們剖析以下章節:
#include <linux/bpf.h>/** Comments from Linux Kernel:* Helper macro to place programs, maps, license in* different sections in elf_bpf file. Section names* are interpreted by elf_bpf loader.* End of comments* You can either use the helper header file below* so that you don't need to define it yourself:* #include <bpf/bpf_helpers.h> */
#define SEC(NAME) __attribute__((section(NAME), used))SEC("xdp")
int xdp_drop_the_world(struct xdp_md *ctx) {// drop everything// 意思是無論什么網絡數據包,都drop丟棄掉return XDP_DROP;
}char _license[] SEC("license") = "GPL";
這個小 XDP 程序一旦加載到網卡上就會丟棄所有數據包。
-
第一部分是第一行的頭文件
linux/bpf.h
,它包含了BPF程序使用到的所有結構和常量的定義(除了一些特定的子系統,如TC,它需要額外的頭文件)。理論上來說,所有的eBPF程序第一行都是這個頭文件。 -
第二部分是第二行的宏定義,它的作用是賦予了SEC(NAME)這一串字符具有意義,即可以被編譯通過。我截取了Linux內核代碼里的注釋,可以看出這段宏定義是為了ELF格式添加Section信息的。ELF全稱是Executable and Linkable Format,就是可執行文件的一種主流格式(詳細介紹點這里),廣泛用于Linux系統,我們的BPF程序一旦通過編譯后,也會是這種格式。下面代碼中的SEC(“xdp”)和SEC(“license”)都是基于這個宏定義。
-
第三部分,也就是我們的代碼主體,它是一個命名為xdp_drop_the_world函數,,返回值為int類型,接受一個參數,類型為xdp_md結構,上文已經介紹過,這個例子沒有使用到這個參數。函數內的就是一行返回語句,使用XDP_DROP,也就是1,意思就是丟棄所有收到的數據包。
-
第四部分是最后一行的許可證聲明。這行其實是給程序加載到內核時BPF驗證器看的,因為有些eBPF函數只能被具有GPL兼容許可證的程序調用。因此,驗證器會檢查程序所使用的函數的許可證和程序的許可證是否兼容,如果不兼容,則拒絕該程序。
還有一點,大家是否注意到整個程序是沒有main入口的,事實上,程序的執行入口可以由前面提到的ELF格式的對象文件中的Section來指定。入口也有默認值,它是ELF格式文件中.text這個標識的內容,程序編譯時會將能看到的函數放到.text里面。
現在來看我們 XDP 程序中處理數據包邏輯最相關的部分。XDP 做了預定義的一組判定可以決定內核處理數據包流。
例如,我們可以讓數據包通過,從而發送到常規的網絡協議棧中,或者丟棄它,或者重定向數據包到其它的網卡等。
在我們的例子中,XDP_DROP 是說超快速的丟棄數據包。同時注意,我們聲明了是在 prog 段中加載執行,eBPF 加載會檢測加載(如果段名稱沒有找到會加載失敗,但是我們可以根據 IP 來使用非標準段名稱 )。下面我們來編譯試運行一下上面的代碼。
$ clang -Wall -target bpf -c xdp-drop.c -o xdp-drop.o
我們可以使用不同的用戶空間工具把二進制目標代碼加載到內核中(iproute2 的部分工具就可以),tc 或者 ip 是是常用的。XDP支持虛擬網卡,所以要直接看出上面程序的作用,我們可以把代碼加載到一個已經存在的容器網卡上。我們會啟動一個 nginx 容器,并且在加載 XDP 程序之前和之后分別啟動一組 curl 請求。之前的 curl 請求會返回一個成功的 HTTP 狀態碼:
$ curl --write-out '%{http_code}' -s --output /dev/null 172.17.0.4:80
200
加載 XDP 字節碼可以使用下面的命令:
$ sudo ip link set dev veth74062a2 xdp obj xdp-drop.o
我們會看到虛擬網卡上有 xdp 被激活的標識:
veth74062a2@if16: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 xdp/id:37 qdisc noqueue master docker0 state UP group default
link/ether 0a:5e:36:21:9e:63 brd ff:ff:ff:ff:ff:ff link-netnsid 2
inet6 fe80::85e:36ff:fe21:9e63/64 scope link
valid_lft forever preferred_lft forever
curl 請求將會被阻塞一段時間直到返回如下的錯誤信息,這就說明 XDP 代碼生效了,也是我們預期的效果:
curl: (7) Failed to connect to 172.17.0.4 port 80: No route to host
我們在測試完整之后,可以使用下面的命令卸載 XDP 程序:
$ sudo ip link set dev veth74062a2 xdp off
使用 GO 編寫 XDP 程序
明確流程
明確整個流程的結構。用戶態用Go程序來管理黑名單,比如動態添加或刪除IP,然后將這些IP信息傳遞給內核中的eBPF程序。內核的XDP程序根據這些IP地址決定是否丟棄包。
那具體步驟:
-
eBPF程序(C語言),其中包含一個映射(map),用于存儲黑名單IP。當接收到數據包時,檢查源或目的IP是否在映射中,如果在,則丟棄包。
-
使用Go的ebpf庫將eBPF程序加載到內核,并管理這個映射,比如添加或刪除IP。
-
Go程序負責讀取用戶輸入的黑名單IP,更新到映射中。
那現在需要分兩部分:eBPF程序的C代碼,和Go的用戶態代碼。
上面的代碼片段演示了一些基本的概念,但是為了充分利用 XDP 的強大功能,我們將使用 Go 語言來制作稍微復雜點的軟件 - 圍繞某種規范用例構建的小工具:針對一些指定的黑名單 IP 地址進行包丟棄。
完整的代碼以及如何構建這個工具的文檔說明在這里。我們使用 gobpf
包,它提供了和 eBPF VM 交互的支持(加載程序到內核,訪問/操作 eBPF map 以及其它功能)。大量的 eBPF 程序都可以直接由 C 編寫,并且編譯為 ELF 目標文件。但是可惜的是,基于 ELF 的 XDP 程序還不行。另外一種方法就是,通過 BCC 模塊加載 XDP 程序仍然是可以的,但要是要依賴 libbcc。
不管怎么處理,BCC maps 有一個非常重要的限制:不能把他們掛到 bpffs 上面(事實上,你可以從用戶空間掛 maps,但是啟動 BCC 模塊的是,它就很容易忽略任何的掛載對象)。我們的工具需要侵入黑名單的 map,同時需要在 XDP 程序加載到網卡上之后仍然可以有能力從 map 中添加或者刪除元素。
我們就有足夠的動力來考慮使用 ELF 目標文件支持 XDP 程序,所以我們給上游倉庫提了這方面的 pr,并期望能合進去(目前這個 pr 已經被合并到 gobpf了)。我們認為這個功能對 XDP 程序的可移植性非常有價值,就像內核探測可以跨機器分布一樣,即使它們不附帶 clang、LLVM 和其他依賴項。
在網絡(XDP)應用程序場景中,使用 Go 編寫的用戶空間控制程序。
選擇eBPF庫
在大多數情況下,eBPF 庫主要協助實現兩個功能:
將 eBPF 程序和 Map 載入內核并執行重定位,通過其文件描述符將 eBPF 程序與正確的 Map 進行關聯。
與 eBPF Map 交互,允許對存儲在 Map 中的鍵/值對進行標準的 CRUD 操作。
部分庫也可以幫助你將 eBPF 程序附加到一個特定的鉤子,盡管對于網絡場景下,這可能很容易采用現有的 netlink API 庫完成。
當涉及到 eBPF 庫的選擇時,我并不是唯一感到困惑的人(見[1], [2])。事實是每個庫都有各自的范圍和限制。
- Calico 在用 bpftool 和 iproute2 實現的 CLI 命令基礎上實現了一個 Go 包裝器。
- Aqua 實現了對 libbpf C 庫的 Go 包裝器。
- Dropbox 支持一小部分程序,但有一個非常干凈和方便的用戶API。
- IO Visor 的 gobpf 是 BCC 框架的 Go 語言綁定,它更注重于跟蹤和性能分析。
- Cilium 和 Cloudflare 維護一個 純 Go 語言編寫的庫 (以下簡稱 “libbpf-go”),它將所有 eBPF 系統調用抽象在一個本地 Go 接口后面。
編寫eBPF代碼
不用多說了,讓我們從下面 XDP 代碼開始瀏覽最重要的代碼片段:
SEC("xdp/xdp_ip_filter")
int xdp_ip_filter(struct xdp_md *ctx) {void *end = (void *)(long)ctx->data_end;void *data = (void *)(long)ctx->data;u32 ip_src;u64 offset;u16 eth_type;struct ethhdr *eth = data;offset = sizeof(*eth);if (data + offset > end) {return XDP_ABORTED;}eth_type = eth->h_proto;/* handle VLAN tagged packet 處理 VLAN 標記的數據包*/if (eth_type == htons(ETH_P_8021Q) || eth_type ==
htons(ETH_P_8021AD)) {struct vlan_hdr *vlan_hdr;vlan_hdr = (void *)eth + offset;offset += sizeof(*vlan_hdr);if ((void *)eth + offset > end)return false;eth_type = vlan_hdr->h_vlan_encapsulated_proto;}/* let's only handle IPv4 addresses 只處理 IPv4 地址*/if (eth_type == ntohs(ETH_P_IPV6)) {return XDP_PASS;}struct iphdr *iph = data + offset;offset += sizeof(struct iphdr);/* make sure the bytes you want to read are within the packet's range before reading them * 在讀取之前,確保你要讀取的子節在數據包的長度范圍內*/if (iph + 1 > end) {return XDP_ABORTED;}ip_src = iph->saddr;if (bpf_map_lookup_elem(&blacklist, &ip_src)) {return XDP_DROP;}return XDP_PASS;
}
代碼看起來是稍微有點多,但是可以先忽略代碼中負責處理 VLAN 標簽的數據包的代碼。我們先從 XDP 元信息中訪問包數據開始,并且把這個指針轉換成 ethddr 的內核結構。你同時會注意到檢測包邊界的幾個條件。如果你忽略了他們,檢查器會拒絕加載 XDP 子節代碼。這個強制規則保證了 XDP 代碼在內核中的的正常運行,避免有無效指針或者違反安全策略的代碼被加載到內核。剩下的代碼從 IP 協議頭中提取了源 IP 地址,并且檢測是否在黑名單 map 中。如果從 map 中查找到了,就會丟棄這個包。
Hook 結構體是負責在網絡協議棧中加載或者卸載 XDP 程序。它實例化并且從對象文件中加載 XDP 模塊,最終調用 AttachXDP 或者 RemoveXDP 方法。
IP 地址黑名單是通過標準的 eBPF maps 來管理的。我們調用 UpdateElement 和 DeleteElement 來分別注冊或者刪除 IP 信息。黑名單管理者也包含了獲取 map 中可用的 IP 地址列表的方法。
其它的代碼把所有的代碼片段組合起來,以提供良好的 CLI 體驗,用戶可以利用這種體驗執行 XDP 程序附加/刪除和操作 IP 黑名單。要了解更多細節,請看源碼。
保存為 xdp_prog.c
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/in.h>struct {__uint(type, BPF_MAP_TYPE_HASH);__type(key, __u32);__type(value, __u8);__uint(max_entries, 1024);
} blacklist SEC(".maps");SEC("xdp")
int xdp_filter(struct xdp_md *ctx) {void *data_end = (void *)(long)ctx->data_end;void *data = (void *)(long)ctx->data;struct ethhdr *eth = data;if (eth + 1 > data_end)return XDP_PASS;if (eth->h_proto != htons(ETH_P_IP))return XDP_PASS;struct iphdr *ip = (struct iphdr *)(eth + 1);if (ip + 1 > data_end)return XDP_PASS;__u32 ip_src= ip->saddr;__u8 *val = bpf_map_lookup_elem(&blacklist, &ip_src);if (val)return XDP_DROP;return XDP_PASS;
}char _license[] SEC("license") = "GPL";
clang -O2 -Wall -target bpf -c xdp_prog.c -o xdp_prog.o
編寫Go代碼
package mainimport ("encoding/binary""log""net""os""os/signal""syscall""github.com/cilium/ebpf""github.com/cilium/ebpf/link""github.com/cilium/ebpf/rlimit"
)func main() {// 解除內存鎖定限制if err := rlimit.RemoveMemlock(); err != nil {log.Fatal(err)}// 加載eBPF ELF文件coll, err := ebpf.LoadCollectionSpec("xdp_prog.o")if err != nil {log.Fatalf("加載eBPF集合失敗: %v", err)}// 實例化eBPF對象var objs struct {XdpProg *ebpf.Program `ebpf:"xdp_filter"`Blacklist *ebpf.Map `ebpf:"blacklist"`}if err := coll.LoadAndAssign(&objs, nil); err != nil {log.Fatalf("加載eBPF對象失敗: %v", err)}defer objs.XdpProg.Close()defer objs.Blacklist.Close()// 獲取網絡接口iface, err := net.InterfaceByName("eth0") // 修改為你的接口名if err != nil {log.Fatalf("獲取網絡接口失敗: %v", err)}// 附加XDP程序l, err := link.AttachXDP(link.XDPOptions{Program: objs.XdpProg,Interface: iface.Index,Flags: link.XDPGenericMode,})if err != nil {log.Fatal(err)}defer l.Close()log.Printf("XDP程序已附加到 %q (索引 %d)", iface.Name, iface.Index)// 初始化黑名單blacklist := []string{"192.168.1.100","10.0.0.5",// 添加更多IP...}for _, ipStr := range blacklist {ip := net.ParseIP(ipStr).To4()if ip == nil {log.Printf("無效IPv4地址: %s", ipStr)continue}ipU32 := binary.BigEndian.Uint32(ip)if err := objs.Blacklist.Put(ipU32, uint8(1)); err != nil {log.Printf("添加IP %s 失敗: %v", ipStr, err)} else {log.Printf("已屏蔽IP: %s", ipStr)}}// 等待終止信號sigCh := make(chan os.Signal, 1)signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)<-sigChlog.Println("卸載程序...")
}
sudo go run main.go
動態更新黑名單
// 示例:運行時添加新IP
func addToBlacklist(ipStr string, blacklist *ebpf.Map) error {ip := net.ParseIP(ipStr).To4()if ip == nil {return fmt.Errorf("invalid IPv4 address")}ipU32 := binary.BigEndian.Uint32(ip)return blacklist.Put(ipU32, uint8(1))
}