iT邦幫忙

2025 iThome 鐵人賽

DAY 7
0

如果覺得文章對你有所啟發,可以考慮用 🌟 支持 Gthulhu 專案,短期目標是集齊 300 個 🌟 藉此被 CNCF Landscape 採納 [ref]

前言

在前面的文章中,我們已經掌握了 eBPF 的基礎概念和 Maps 的使用方法。從本篇開始,我們將進入 eBPF 的實際應用領域。首先要介紹的是 XDP(eXpress Data Path),這是 eBPF 在網路程式設計中最重要的應用之一。

XDP 讓我們能夠在網路封包進入內核網路堆疊之前就進行處理,這帶來了前所未有的高效能網路處理能力。在本篇文章中,我們將深入理解 XDP 的工作原理,並實作一個簡單的封包過濾器。

XDP 技術概述

什麼是 XDP?

XDP(eXpress Data Path)是一種基於 eBPF 技術的高效能網路資料路徑。它的核心特點是:

  1. 極早期處理:在封包進入內核網路堆疊之前處理
  2. 零拷貝:直接在網卡驅動層面操作封包
  3. 高效能:接近 DPDK 的效能表現
  4. 靈活性:保持內核的完整網路功能

XDP 在網路堆疊中的位置

┌─────────────────┐
│   User Space    │ ← 應用程式
└─────────────────┘
┌─────────────────┐
│   Socket Layer  │ ← TCP/UDP Socket
└─────────────────┘
┌─────────────────┐
│  Network Stack  │ ← IP/TCP/UDP 處理
└─────────────────┘
┌─────────────────┐
│   XDP Hook      │ ← XDP 程式在這裡執行
└─────────────────┘
┌─────────────────┐
│  Network Driver │ ← 網卡驅動
└─────────────────┘
┌─────────────────┐
│  Network Card   │ ← 硬體網卡
└─────────────────┘

XDP 程式的返回值

XDP 程式必須返回一個 action code,告訴內核如何處理封包:

// XDP action code
enum xdp_action {
    XDP_ABORTED = 0,    // 異常終止,丟棄封包並記錄
    XDP_DROP,           // 丟棄封包
    XDP_PASS,           // 將封包傳遞給網路堆疊
    XDP_TX,             // 從同一網卡傳送封包
    XDP_REDIRECT,       // 重定向到其他網卡或 CPU
};

action code 詳解

  1. XDP_DROP:最常用的動作,用於 DDoS 防護或惡意流量過濾
  2. XDP_PASS:允許封包正常進入網路堆疊
  3. XDP_TX:用於實現反射攻擊防護或負載均衡
  4. XDP_REDIRECT:用於流量轉發或負載分散
  5. XDP_ABORTED:錯誤處理,很少使用

實作:簡單的封包過濾器

讓我們實作一個簡單的 XDP 程式,用來過濾特定的 IP 位址。

eBPF 程式實作

首先建立 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";

Go 語言載入器實作

建立 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

建立 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 模式比較

XDP 支援多種載入模式:

  1. Generic XDP (SKB 模式)

    • 相容性最好,但效能較低
    • 在網路堆疊中較晚執行
  2. Native XDP (驅動模式)

    • 效能最好,在驅動層面執行
    • 需要驅動支援
  3. Offloaded XDP (硬體模式)

    • 效能最高,在網卡硬體執行
    • 需要特殊硬體支援

實戰應用場景

DDoS 防護

// 簡單的速率限制
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 技術:

  1. 理解了 XDP 的工作原理:在網路堆疊早期進行高效能封包處理
  2. 掌握了 XDP 程式的基本結構:動作代碼、封包解析、邊界檢查
  3. 實作了完整的封包過濾器:包含統計功能和動態配置
  4. 學習了效能最佳化技巧:記憶體存取、統計方法、原子操作

XDP 是 eBPF 在網路領域最重要的應用之一,為高效能網路處理打開了新的可能性。在下一篇文章中,我們將進一步實作一個完整的負載均衡器,展示 XDP 在實際應用中的強大能力。
在下一篇文章中,我們將使用 XDP 技術實作一個完整的 Tiny Load Balancer,學習如何修改封包內容並實現負載均衡功能。


上一篇
eBPF Maps 詳解與使用
系列文
30 篇文帶你用 eBPF 與 Golang 打造 Linux Scheduler7
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言