BPF、eBPF 與 XDP 簡介與使用

大雜燴,基本翻譯自

A brief introduction to XDP and eBPF

The eXpress Data Path

xdp-ebpf 簡介

Kernel Bypass

在過去幾年中,我們看到了編程工具包和技術的升級,以克服 Linux kernel 的限制,來進行高性能數據包處理。最流行的技術之一是 kernel bypass(內核旁路),這意味着跳過內核的網絡層,在用戶態 (user-sapce) 做全部的包處理。kernel bypass 涉及從 user-space 管理 NIC(network interface controller,也就是常說的網卡),也就是說需要用戶態的驅動程序 (user space driver) 來處理 NIC

用戶態程序完全控制 NIC,有什麼好處呢?減少了內核開銷;等

壞處呢?用戶程序需要直接管理硬件;kernel 被完全跳過,所以內核提供的所有網絡功能也被跳過,用戶程序可能需要實現一些原來內核提供的功能;

本質上 kernel bypass 實現高性能包處理是通過將數據包從 kernel 移動到 user-space

XDP(後面會講) 實際上正好相反,XDP 允許我們在數據包到達 NIC 時,在它移動到 kernel’s networking subsystem 之前,執行我們定義的處理函數,從而顯著提高數據包處理速度。但是用戶態定義的程序如何在內核中執行呢?

這就用到了 BPF,BPF 就是一種在內核中運行用戶指定的程序的設計

BPF

Berkeley packet filter,用於過濾網絡報文 (packet)

是 tcpdump(linux)和 wireshark(windows)乃至整個網絡監控 (network monitoring) 的基石

BPF 實際上並不只是包處理,而更像一個 VM(virtual machine)

BPF 虛擬機及其字節碼由 Steve McCanne 和 Van Jacobson 於 1992 年底在其論文《The BSD Packet Filter: A New Architecture for User-level Packet Capture》中介紹,並首次在 1993 年冬季 Usenix 會議上提出。

由於 BPF 是一個 VM,它定義了一個程序執行的環境。除了字節碼,它還定義了基於數據包的內存模型 (packet-based memory model)、寄存器 (A and X; Accumulator ans Index register)、暫存內存 (scratch memory)、隱式程序計數器 (implicit pc)。有趣的是,BPF 的字節碼是模仿摩托羅拉 6502ISA 的。Steve McCanne 在他的 Sharkfest ‘11 keynote 主題演講中回憶道,他在初中時就熟悉 6502 assembly 在 Apple II 上的編程,這在他設計 BPF 字節碼時對他產生了影響

Linux 內核從 v2.5 開始就支持 BPF,主要由 Jay Schullist 添加。直到 2011 年,BPF 代碼才發生重大變化,Eric Dumazet 將 BPF 解釋器轉換爲 JIT(來源:A JIT for packet filters)。現在內核不再解釋 BPF 字節碼,而是能夠將 BPF 程序直接轉換爲目標體系結構:x86、ARM、MIPS 等。

隨後,在 2014 年,Alexei Starovoitov 引入了新的 BPF JIT。這種新的 JIT 實際上是一種基於 BPF 的新體系結構,稱爲 eBPF。我認爲這兩個虛擬機共存了一段時間,但現在包過濾是在 eBPF 之上實現的。事實上,許多文檔現在將 eBPF 稱爲 BPF,而經典的 BPF 稱爲 cBPF。

eBPF

eBPF 在以下幾個方面擴展了傳統的 BPF 虛擬機:

eBPF 怎麼使用呢?

看一個例子,也是 Linux kernel 自帶的樣例。它們可在 samples/bpf/ 上獲得。要編譯這些示例,可參考我前面的一篇文章。

我們選擇 tracex4 程序分析,eBPF 編程通常包括兩個程序:eBPF 程序和 user-sapce 程序

首先我們需要將tracex4_kern.c編程成 eBPF bytecode,gcc 缺乏 BPF 後端,幸運的是,Clang 支持,自帶的 Makefile 利用 Clang 將trace4_kern.c編譯成一個目標文件 (object file)

閱讀以下tracex4_kern.c源碼:

Maps are key/value stores that allow to exchange data between user-space and kernel-space programs. tracex4_kern defines one map:

struct pair {
    u64 val;
    u64 ip;
};  

struct bpf_map_def SEC("maps") my_map = {
    .type = BPF_MAP_TYPE_HASH,
    .key_size = sizeof(long),
    .value_size = sizeof(struct pair),
    .max_entries = 1000000,
};

BPF_MAP_TYPE_HASH 是 eBPF 提供的多個 Map 中的一個,你還能看到 SEC("map"),SEC 是一個宏用來在二進制文件 (目標文件,.o 文件) 中生成一個新的 section

tracex4_kern.c還定義了另外兩個 section:

SEC("kprobe/kmem_cache_free")
int bpf_prog1(struct pt_regs *ctx)
{   
    long ptr = PT_REGS_PARM2(ctx);

    bpf_map_delete_elem(&my_map, &ptr); 
    return 0;
}
    
SEC("kretprobe/kmem_cache_alloc_node") 
int bpf_prog2(struct pt_regs *ctx)
{
    long ptr = PT_REGS_RC(ctx);
    long ip = 0;

    // get ip address of kmem_cache_alloc_node() caller
    BPF_KRETPROBE_READ_RET_IP(ip, ctx);

    struct pair v = {
        .val = bpf_ktime_get_ns(),
        .ip = ip,
    };
    
    bpf_map_update_elem(&my_map, &ptr, &v, BPF_ANY);
    return 0;
}

這兩個函數允許我們在 map 中增加一個entry(kprobe/kmem_cache_free) 和添加一條entry(kretprobe/kmem_cache_alloc_node)

所有大寫字母的函數實際上都是宏,定義在 bpf_helpers.h

如果我們反彙編目標文件,我們可以看見新的 section 被定義:

$ objdump -h tracex4_kern.o

tracex4_kern.o:     file format elf64-little

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000000  0000000000000000  0000000000000000  00000040  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 kprobe/kmem_cache_free 00000048  0000000000000000  0000000000000000  00000040  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  2 kretprobe/kmem_cache_alloc_node 000000c0  0000000000000000  0000000000000000  00000088  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  3 maps          0000001c  0000000000000000  0000000000000000  00000148  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  4 license       00000004  0000000000000000  0000000000000000  00000164  2**0
                  CONTENTS, ALLOC, LOAD, DATA
  5 version       00000004  0000000000000000  0000000000000000  00000168  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  6 .eh_frame     00000050  0000000000000000  0000000000000000  00000170  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA

main program 是 tracex4_user.c,大體上,這個程序的作用就是監聽 kmem_cache_alloc_node 上的事件,當事件發生時,對應的 eBPF code 會被執行,且把 ip 信息保存到 Map,main program 從 map 中讀取並打印出來

$ sudo ./tracex4
obj 0xffff8d6430f60a00 is  2sec old was allocated at ip ffffffff9891ad90
obj 0xffff8d6062ca5e00 is 23sec old was allocated at ip ffffffff98090e8f
obj 0xffff8d5f80161780 is  6sec old was allocated at ip ffffffff98090e8f

這 user-sapce program 和 eBPF program 是怎麼連接在一起的?在初始化的時候,tracex4_user.c 使用load_bpf_file 加載 tracex4_kern.o

int main(int ac, char **argv)
{
    struct rlimit r = {RLIM_INFINITY, RLIM_INFINITY};
    char filename[256];
    int i;

    snprintf(filename, sizeof(filename), "%s_kern.o", argv[0]);

    if (setrlimit(RLIMIT_MEMLOCK, &r)) {
        perror("setrlimit(RLIMIT_MEMLOCK, RLIM_INFINITY)");
        return 1;
    }

    if (load_bpf_file(filename)) {
        printf("%s", bpf_log_buf);
        return 1;
    }

    for (i = 0; ; i++) {
        print_old_objects(map_fd[1]);
        sleep(1);
    }

    return 0;
}

執行 load_bpf_file 時,eBPF 文件中定義的探測 (kprobe) 將添加到 /sys/kernel/debug/tracing/kprobe_events 中。我們現在正在監聽這些事件,當它們發生時,我們的程序可以做一些事情。

$ sudo cat /sys/kernel/debug/tracing/kprobe_events
p:kprobes/kmem_cache_free kmem_cache_free
r:kprobes/kmem_cache_alloc_node kmem_cache_alloc_node

XDP

XDP 的設計源於 Cloudflare 在 Netdev 1.1 上提出的 DDoS 攻擊緩解解決方案

因爲 Cloudflare 希望保持使用 iptables(以及內核網絡堆棧的其餘部分)的便利性,所以他們無法使用完全控制硬件的解決方案(即前面的 kernel bypass),例如 DPDK

Cloudflare 的解決方案使用 Netmap 工具包實現其部分內核旁路 (partial kernel bypass)(來源:Single Rx queue kernel bypass with Netmap)。這個想法可以通過在 Linux 內核網絡堆棧中添加一個檢查點 (checkpoint),最好是在 NIC 中接收到數據包之後。該 checkpoint 應將數據包傳遞給 eBPF 程序,該程序將決定如何處理該數據包:丟棄該數據包(drop) 或讓其繼續通過正常路徑(pass). 就像這幅圖一樣:

Example: An IPv6 packet filter

介紹 XDP 的典型例子是 DDos 過濾器,它的作用是:果數據包來自可疑來源,就丟棄它們。在我的例子中,我將使用更簡單的功能:一個過濾除 IPv6 之外的所有流量的功能。

爲了簡單處理,我們不需要管理可疑地址列表。我們只簡單地檢查數據包的 ethertype 值,並讓它繼續通過網絡堆棧 (network stack),或者根據是否是 IPv6 數據包丟棄它。

SEC("prog")
int xdp_ipv6_filter_program(struct xdp_md *ctx)
{
    void *data_end = (void *)(long)ctx->data_end;
    void *data     = (void *)(long)ctx->data;
    struct ethhdr *eth = data;
    u16 eth_type = 0;

    if (!(parse_eth(eth, data_end, eth_type))) {
        bpf_debug("Debug: Cannot parse L2\n");
        return XDP_PASS;
    }

    bpf_debug("Debug: eth_type:0x%x\n", ntohs(eth_type));
    if (eth_type == ntohs(0x86dd)) {
        return XDP_PASS;
    } else {
        return XDP_DROP;
    }
}

函數 xdp_ipv6_filter_程序是我們的主程序。我們在二進制文件中定義了一個稱爲 prog 的新部分。這是我們的程序和 XDP 之間的掛鉤。每當 XDP 收到一個數據包,我們的代碼就會被執行。

CTX 表示一個上下文,一個包含訪問數據包所需的所有數據的結構。我們的程序調用 parse_eth 來獲取 ethertype。然後檢查其值是否爲 0x86dd(IPv6 以太網類型),如果是,數據包將通過。否則,數據包將被丟棄。此外,出於調試目的,所有 ethertype 值都會打印出來。

bpf_debug 實際上是一個宏,定義如下:

#define bpf_debug(fmt, ...)                          \
    ({                                               \
        char ____fmt[] = fmt;                        \
        bpf_trace_printk(____fmt, sizeof(____fmt),   \
            ##__VA_ARGS__);                          \
    })

內部其實也是調用了 bpf_trace_printk,這個函數會打印在 _/sys/kernel/debug/tracing/trace_pipe_中的信息

函數 parse_eth 獲取數據包的開頭和結尾,並解析其內容:

static __always_inline
bool parse_eth(struct ethhdr *eth, void *data_end, u16 *eth_type)
{
    u64 offset;

    offset = sizeof(*eth);
    if ((void *)eth + offset > data_end)
        return false;
    *eth_type = eth->h_proto;
    return true;
}

在內核中運行外部代碼涉及某些風險。例如,無限循環可能會凍結內核,或者程序可能會訪問不受限制的內存區域。爲避免這些潛在危險,在加載 eBPF 代碼時運行驗證器。驗證器遍歷所有可能的代碼路徑,檢查我們的程序沒有訪問超出範圍的內存,也沒有越界跳轉;驗證器還確保程序在有限時間內終止。
我們的 eBPF 程序符合這些要求。現在我們只需要編譯它(完整的源代碼可以在:xdp_ipv6_filter 上找到)。

$ make

這會生成 xdp_ipv6_filter.o,一個 eBPF object file

現在我們需要把這個 object file 加載到 network interface,這有兩種方式可以做到這一點:

在這個例子中,我們將使用後一種方法

目前,支持 XDP 的網絡接口數量有限(ixgbe、i40e、mlx5、veth、tap、tun、virtio_net 和其他),儘管數量在不斷增加。其中一些網絡接口在驅動程序級別支持 XDP(言下之意有些還不能在驅動級別)。這意味着,XDP 鉤子是在網絡層的最低點實現的,就在 NIC 在 Rx ring 中接收到數據包的時候。在其他情況下,XDP 鉤子在網絡堆棧中的較高點實現。前者提供了更好的性能結果,儘管後者使 XDP 可用於任何網絡接口。

幸運的是,XDP 支持 veth interfaces,我將創建一個 veth 對,並將 eBPF 程序連接到它的一端。記住 veth 總是成對的,它就像一根虛擬電纜連接兩個端口,任何在一端傳送的東西都會到達另一端,反之亦然。

$ sudo ip link add dev veth0 type veth peer name veth1
$ sudo ip link set up dev veth0
$ sudo ip link set up dev veth1

現在我們將 eBPF program attach 到 veth1 上:

$ sudo ip link set dev veth1 xdp object xdp_ipv6_filter.o

您可能已經注意到,我將 eBPF 程序的部分稱爲 “prog”。這是iproute2希望查找的節的名稱,使用其他名稱命名該節將導致錯誤。
如果程序成功加載,我將在 veth1 接口中看到一個 xdp 標誌:

$ sudo ip link sh veth1
8: veth1@veth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 xdp qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether 32:05:fc:9a:d8:75 brd ff:ff:ff:ff:ff:ff
    prog/xdp id 32 tag bdb81fb6a5cf3154 jited

爲了驗證我的程序是否按預期工作,我將把 IPv4 和 IPv6 數據包的混合推送到 veth0(IPv4-and-IPv6-data.pcap)。我的示例總共有 20 個數據包(10 個 IPv4 和 10 個 IPv6)。但在這樣做之前,我將在 veth1 上啓動一個 tcpdump 程序,它只准備捕獲 10 個 IPv6 數據包。

$ sudo tcpdump "ip6" -i veth1 -w captured.pcap -c 10
tcpdump: listening on veth1, link-type EN10MB (Ethernet), capture size 262144 bytes

送 packets 到 veth0:

$ sudo tcpreplay -i veth0 ipv4-and-ipv6-data.pcap

過濾後的數據包到達另一端。由於收到了所有預期的數據包,tcpdump 程序終止。

10 packets captured
10 packets received by filter
0 packets dropped by kernel

我們也可以打印出 /sys/kernel/debug/tracing/trace_pipe,來檢查 ethertype value.

$ sudo cat /sys/kernel/debug/tracing/trace_pipe
tcpreplay-4496  [003] ..s1 15472.046835: 0: Debug: eth_type:0x86dd
tcpreplay-4496  [003] ..s1 15472.046847: 0: Debug: eth_type:0x86dd
tcpreplay-4496  [003] ..s1 15472.046855: 0: Debug: eth_type:0x86dd
tcpreplay-4496  [003] ..s1 15472.046862: 0: Debug: eth_type:0x86dd
tcpreplay-4496  [003] ..s1 15472.046869: 0: Debug: eth_type:0x86dd
tcpreplay-4496  [003] ..s1 15472.046878: 0: Debug: eth_type:0x800
tcpreplay-4496  [003] ..s1 15472.046885: 0: Debug: eth_type:0x800
tcpreplay-4496  [003] ..s1 15472.046892: 0: Debug: eth_type:0x800
tcpreplay-4496  [003] ..s1 15472.046903: 0: Debug: eth_type:0x800
tcpreplay-4496  [003] ..s1 15472.046911: 0: Debug: eth_type:0x800
...

非常建議大家親自動手做這個實驗的!!

最後 mark 一些沒詳細看完的資料:

狄衛華_E B P F 技術簡介

LINUX.CONG.AU_BPF: Tracing and More

Taiwan Linux Kernel Hackers_主題分享:Introduction to eBPF and XDP

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://www.cnblogs.com/lfri/p/15411668.html