如果覺得文章對你有所啟發,可以考慮用 🌟 支持 Gthulhu 專案,短期目標是集齊 300 個 🌟 藉此被 CNCF Landscape 採納 [ref]。
在前面的文章中,我們已經掌握了 eBPF 的基礎概念和 Maps 的使用方法。從本篇開始,我們將進入 eBPF 的實際應用領域。首先要介紹的是 XDP(eXpress Data Path),這是 eBPF 在網路程式設計中最重要的應用之一。
XDP 讓我們能夠在網路封包進入內核網路堆疊之前就進行處理,這帶來了前所未有的高效能網路處理能力。在本篇文章中,我們將深入理解 XDP 的工作原理,並實作一個簡單的封包過濾器。
XDP(eXpress Data Path)是一種基於 eBPF 技術的高效能網路資料路徑。它的核心特點是:
┌─────────────────┐
│ User Space │ ← 應用程式
└─────────────────┘
┌─────────────────┐
│ Socket Layer │ ← TCP/UDP Socket
└─────────────────┘
┌─────────────────┐
│ Network Stack │ ← IP/TCP/UDP 處理
└─────────────────┘
┌─────────────────┐
│ XDP Hook │ ← XDP 程式在這裡執行
└─────────────────┘
┌─────────────────┐
│ Network Driver │ ← 網卡驅動
└─────────────────┘
┌─────────────────┐
│ Network Card │ ← 硬體網卡
└─────────────────┘
XDP 程式必須返回一個 action code,告訴內核如何處理封包:
// XDP action code
enum xdp_action {
XDP_ABORTED = 0, // 異常終止,丟棄封包並記錄
XDP_DROP, // 丟棄封包
XDP_PASS, // 將封包傳遞給網路堆疊
XDP_TX, // 從同一網卡傳送封包
XDP_REDIRECT, // 重定向到其他網卡或 CPU
};
讓我們實作一個簡單的 XDP 程式,用來過濾特定的 IP 位址。
首先建立 packet_filter.bpf.c
:
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/in.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
// 統計 Map
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
__uint(max_entries, 256);
__type(key, __u32);
__type(value, __u64);
} stats_map SEC(".maps");
// 被阻擋的 IP 列表
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 1024);
__type(key, __u32);
__type(value, __u8);
} blocked_ips SEC(".maps");
// 統計類型
enum {
STAT_RX_PACKETS = 0,
STAT_RX_BYTES,
STAT_DROPPED_PACKETS,
STAT_DROPPED_BYTES,
STAT_PASSED_PACKETS,
STAT_PASSED_BYTES,
};
static __always_inline void update_stats(__u32 key, __u64 bytes)
{
__u64 *value = bpf_map_lookup_elem(&stats_map, &key);
if (value) {
__sync_fetch_and_add(value, bytes);
}
}
SEC("xdp")
int packet_filter(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_ABORTED;
}
// 只處理 IPv4 封包
if (bpf_ntohs(eth->h_proto) != ETH_P_IP) {
return XDP_PASS;
}
// 檢查 IP 標頭
struct iphdr *ip = data + sizeof(*eth);
if (data + sizeof(*eth) + sizeof(*ip) > data_end) {
return XDP_ABORTED;
}
__u32 packet_size = data_end - data;
__u32 src_ip = ip->saddr;
// 更新接收統計
update_stats(STAT_RX_PACKETS, 1);
update_stats(STAT_RX_BYTES, packet_size);
// 檢查是否為被阻擋的 IP
__u8 *blocked = bpf_map_lookup_elem(&blocked_ips, &src_ip);
if (blocked) {
// 更新丟棄統計
update_stats(STAT_DROPPED_PACKETS, 1);
update_stats(STAT_DROPPED_BYTES, packet_size);
bpf_printk("Blocked packet from IP: %pI4", &src_ip);
return XDP_DROP;
}
// 更新通過統計
update_stats(STAT_PASSED_PACKETS, 1);
update_stats(STAT_PASSED_BYTES, packet_size);
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
建立 main.go
:
package main
import (
"fmt"
"log"
"net"
"os"
"os/signal"
"syscall"
"time"
"unsafe"
"github.com/aquasecurity/libbpfgo"
)
const (
STAT_RX_PACKETS = iota
STAT_RX_BYTES
STAT_DROPPED_PACKETS
STAT_DROPPED_BYTES
STAT_PASSED_PACKETS
STAT_PASSED_BYTES
)
type PacketFilter struct {
module *libbpfgo.Module
link *libbpfgo.BPFLink
}
func NewPacketFilter(objPath, ifaceName string) (*PacketFilter, error) {
// 載入 eBPF 物件
module, err := libbpfgo.NewModuleFromFile(objPath)
if err != nil {
return nil, fmt.Errorf("failed to load BPF object: %v", err)
}
if err := module.BPFLoadObject(); err != nil {
return nil, fmt.Errorf("failed to load BPF object: %v", err)
}
// 取得程式
prog, err := module.GetProgram("packet_filter")
if err != nil {
return nil, fmt.Errorf("failed to get BPF program: %v", err)
}
// 附加到網路介面
link, err := prog.AttachXDP(ifaceName)
if err != nil {
return nil, fmt.Errorf("failed to attach XDP program: %v", err)
}
return &PacketFilter{
module: module,
link: link,
}, nil
}
func (pf *PacketFilter) AddBlockedIP(ip string) error {
ipAddr := net.ParseIP(ip)
if ipAddr == nil {
return fmt.Errorf("invalid IP address: %s", ip)
}
// 轉換為 uint32 (little endian)
ipv4 := ipAddr.To4()
if ipv4 == nil {
return fmt.Errorf("not an IPv4 address: %s", ip)
}
ipUint32 := *(*uint32)(unsafe.Pointer(&ipv4[0]))
blockedMap, err := pf.module.GetMap("blocked_ips")
if err != nil {
return fmt.Errorf("failed to get blocked_ips map: %v", err)
}
key := ipUint32
value := uint8(1)
if err := blockedMap.Update(unsafe.Pointer(&key), unsafe.Pointer(&value)); err != nil {
return fmt.Errorf("failed to update blocked_ips map: %v", err)
}
fmt.Printf("Added blocked IP: %s\n", ip)
return nil
}
func (pf *PacketFilter) GetStats() (map[string]uint64, error) {
statsMap, err := pf.module.GetMap("stats_map")
if err != nil {
return nil, fmt.Errorf("failed to get stats_map: %v", err)
}
stats := make(map[string]uint64)
statNames := []string{
"rx_packets", "rx_bytes", "dropped_packets",
"dropped_bytes", "passed_packets", "passed_bytes",
}
for i, name := range statNames {
key := uint32(i)
value, err := statsMap.GetValue(unsafe.Pointer(&key))
if err != nil {
continue
}
// 對於 PERCPU_ARRAY,需要計算所有 CPU 的總和
cpuCount := len(value) / 8 // 每個 uint64 是 8 bytes
total := uint64(0)
for j := 0; j < cpuCount; j++ {
cpuValue := *(*uint64)(unsafe.Pointer(&value[j*8]))
total += cpuValue
}
stats[name] = total
}
return stats, nil
}
func (pf *PacketFilter) PrintStats() {
stats, err := pf.GetStats()
if err != nil {
log.Printf("Failed to get stats: %v", err)
return
}
fmt.Println("\n=== Packet Filter Statistics ===")
fmt.Printf("RX Packets: %d\n", stats["rx_packets"])
fmt.Printf("RX Bytes: %d\n", stats["rx_bytes"])
fmt.Printf("Dropped Packets: %d\n", stats["dropped_packets"])
fmt.Printf("Dropped Bytes: %d\n", stats["dropped_bytes"])
fmt.Printf("Passed Packets: %d\n", stats["passed_packets"])
fmt.Printf("Passed Bytes: %d\n", stats["passed_bytes"])
if stats["rx_packets"] > 0 {
dropRate := float64(stats["dropped_packets"]) / float64(stats["rx_packets"]) * 100
fmt.Printf("Drop Rate: %.2f%%\n", dropRate)
}
}
func (pf *PacketFilter) Close() {
if pf.link != nil {
pf.link.Destroy()
}
if pf.module != nil {
pf.module.Close()
}
}
func main() {
if len(os.Args) != 3 {
fmt.Printf("Usage: %s <interface> <blocked_ip>\n", os.Args[0])
fmt.Printf("Example: %s eth0 192.168.1.100\n", os.Args[0])
os.Exit(1)
}
ifaceName := os.Args[1]
blockedIP := os.Args[2]
// 建立封包過濾器
filter, err := NewPacketFilter("packet_filter.bpf.o", ifaceName)
if err != nil {
log.Fatalf("Failed to create packet filter: %v", err)
}
defer filter.Close()
// 新增被阻擋的 IP
if err := filter.AddBlockedIP(blockedIP); err != nil {
log.Fatalf("Failed to add blocked IP: %v", err)
}
fmt.Printf("XDP packet filter loaded on interface %s\n", ifaceName)
fmt.Printf("Blocking traffic from IP: %s\n", blockedIP)
fmt.Println("Press Ctrl+C to stop...")
// 設定信號處理
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
// 定期顯示統計資訊
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
filter.PrintStats()
case <-sigChan:
fmt.Println("\nShutting down...")
filter.PrintStats()
return
}
}
}
建立 Makefile
:
# XDP Packet Filter Makefile
CLANG ?= clang
LLVM_STRIP ?= llvm-strip
ARCH := x86_64
# 輸出檔案
BPF_OBJ = packet_filter.bpf.o
TARGET = packet_filter
# 編譯標誌
CFLAGS := -O2 -g -Wall -Werror
BPF_CFLAGS := -target bpf -D__TARGET_ARCH_$(ARCH)
# 包含路徑
INCLUDES := -I/usr/include/$(shell uname -m)-linux-gnu
.PHONY: all clean test
all: $(BPF_OBJ) $(TARGET)
# 編譯 eBPF 程式
$(BPF_OBJ): packet_filter.bpf.c
$(CLANG) $(BPF_CFLAGS) $(INCLUDES) $(CFLAGS) -c $< -o $@
$(LLVM_STRIP) -g $@
# 編譯 Go 程式
$(TARGET): main.go $(BPF_OBJ)
go build -o $(TARGET) main.go
# 測試 (需要 root 權限)
test: $(BPF_OBJ) $(TARGET)
@echo "Testing packet filter..."
@echo "Note: This requires root privileges and a valid network interface"
@echo "Run: sudo ./$(TARGET) <interface> <blocked_ip>"
# 安裝依賴
deps:
go mod init packet-filter
go get github.com/aquasecurity/libbpfgo
# 檢查程式
verify: $(BPF_OBJ)
bpftool prog show
bpftool map show
clean:
rm -f $(BPF_OBJ) $(TARGET)
rm -f go.mod go.sum
help:
@echo "Available targets:"
@echo " all - Build eBPF object and Go binary"
@echo " deps - Install Go dependencies"
@echo " test - Run test (requires root)"
@echo " verify - Verify loaded BPF programs"
@echo " clean - Clean build artifacts"
@echo " help - Show this help message"
# 安裝依賴
make deps
# 編譯
make all
# 需要 root 權限
sudo ./packet_filter eth0 192.168.1.100
# 或者使用 loopback 介面測試
sudo ./packet_filter lo 127.0.0.1
在另一個終端機中:
# 測試正常流量 (應該通過)
ping 8.8.8.8
# 從被阻擋的 IP 發送流量 (如果可能的話)
# 或者使用 hping3 模擬
sudo hping3 -S -p 80 -a 192.168.1.100 target_ip
XDP 支援多種載入模式:
Generic XDP (SKB 模式):
Native XDP (驅動模式):
Offloaded XDP (硬體模式):
// 簡單的速率限制
struct rate_limit_key {
__u32 src_ip;
__u32 time_window;
};
// 實現每秒封包數限制
if (packet_count > MAX_PPS) {
return XDP_DROP;
}
// 基於 hash 的負載均衡
__u32 hash = jhash_2words(src_ip, dst_port, 0);
__u32 backend = hash % num_backends;
// 修改目標 IP
ip->daddr = backends[backend];
// 重新計算 checksum
return XDP_TX;
// 記錄流量模式
struct flow_key {
__u32 src_ip;
__u32 dst_ip;
__u16 src_port;
__u16 dst_port;
__u8 protocol;
};
// 更新流量統計
update_flow_stats(&flow_key, packet_size);
在本篇文章中,我們深入學習了 XDP 技術:
XDP 是 eBPF 在網路領域最重要的應用之一,為高效能網路處理打開了新的可能性。在下一篇文章中,我們將進一步實作一個完整的負載均衡器,展示 XDP 在實際應用中的強大能力。
在下一篇文章中,我們將使用 XDP 技術實作一個完整的 Tiny Load Balancer,學習如何修改封包內容並實現負載均衡功能。