BPF 程式設計已成為 Linux 網路分析和控制的重要技術,允許開發者在核心層級高效處理封包。本文從 tcpdump 工具的應用切入,逐步解析 BPF 指令集,並深入探討不同型別的 BPF 程式設計,包含 socket filter、kprobe 和 tracepoint 等。同時,文章也涵蓋了 eBPF Exporter 與 Prometheus 的整合,以及如何利用 eBPF 檢測 Linux 封包排程子系統,提供讀者更全面的 BPF 程式設計實戰。理解 SKB 結構在 BPF 程式設計中的作用,以及如何操作 SKB 進行封包轉換,也是本文的重點之一。

在特定節點執行 BPF 程式

如果您想在特定節點上執行 BPF 程式,您需要提供節點的識別符,以便 Kubernetes 將工作排程到正確的位置。然後,執行程式的方式與之前看到的類別似。

# kubectl trace run node/node_identifier -e \
"kprobe:do_sys_open { @opens[str(arg1)] = count() }"

在特定容器執行 BPF 程式

在特定容器上執行程式需要更長的語法。以下是範例:

# kubectl trace run pod/pod_identifier -n application_name -e <<PROGRAM
uretprobe:/proc/$container_pid/exe:"main.main" {
    printf("exit: %d\n", retval)
}
PROGRAM

eBPF Exporter

eBPF Exporter 是一個工具,允許您將自定義 BPF 追蹤指標匯出到 Prometheus。Prometheus 是一個高可擴充套件性的監控和警示系統。eBPF Exporter 實作了 Prometheus 的 API,以從 BPF 程式中收集追蹤指標並匯入 Prometheus。

安裝 eBPF Exporter

雖然 eBPF Exporter 提供二進位制包,但建議從原始碼安裝,因為這樣可以獲得更新的功能。安裝前,您需要已經安裝 BCC 和 Go 的工具鏈。

組態 eBPF Exporter

eBPF Exporter 使用 YAML 檔案進行組態,您可以在其中指定要收集的指標、生成這些指標的 BPF 程式以及如何將其轉換為 Prometheus 可以理解的格式。當 Prometheus 傳送請求以提取指標時,eBPF Exporter 將 BPF 程式收集的資訊轉換為指標值。

programs:
  - name: timers
    metrics:
      counters:
        - name: timer_start_total
          help: Timers fired in the kernel
          table: counts
          labels:
            - name: function
              size: 8
              decoders:
                - name: ksym

Linux 網路與 BPF

BPF 程式在網路方面有兩個主要用途:封包捕捉和過濾。這意味著使用者空間程式可以將過濾器附加到任何 socket,並提取流經它的封包資訊,允許或不允許某些型別的封包,並在該層級重新導向它們。

本章的目標是解釋 BPF 程式如何與 Linux 核心網路堆積疊中的 Socket Buffer 結構在不同階段進行互動作用。我們將識別兩種常見的用途:

  • 與 socket 相關的程式型別
  • 為流量控制編寫的根據 BPF 的分類別器程式

Socket Buffer 結構,也稱為 SKB 或 sk_buff,是核心中為每個傳送或接收的封包建立和使用的結構。一些 BPF 程式允許您操縱 SKB,並轉換最終封包以重新導向它們或更改其基本結構。例如,在僅 IPv6 的系統上,您可能會編寫一個將所有從 IPv4 接收的封包轉換為 IPv6 的程式,這可以透過操縱 SKB 來完成。

理解不同型別的程式及其差異是瞭解 BPF 和 eBPF 在網路中的關鍵。在下一節中,我們將探討在 socket 級別進行過濾的第一種方法:使用 BPF 和封包過濾。

BPF 和封包過濾

如前所述,BPF 過濾器和 eBPF 程式是 BPF 程式在網路背景下的主要用途。然而,最初,BPF 程式與封包過濅是同義的。封包過濅仍然是最重要的用途之一,並且已從經典 BPF (cBPF) 擴充套件到 Linux 3.19 中的現代 eBPF,增加了與對映相關的函式到過濾器程式型別 BPF_PROG_TYPE_SOCKET_FILTER。

過濾器可以用於三種高階場景:

  • 直接封包丟棄(例如,只允許使用者資料報協定 (UDP) 交通並丟棄其他所有封包)
  • 直接觀察篩選後的封包集流入直播系統
  • 使用 pcap 格式等,對直播系統捕捉的網路流量進行回顧分析

術語 pcap 來自於封包和捕捉的結合。pcap 格式作為一個域特定 API 實作於一個名為 Packet Capture Library (libpcap) 的函式庫中。這種格式在除錯場景中很有用,您想要儲存一組在直播系統上捕捉的封包。

使用tcpdump和BPF進行封包過濾

在進行網路流量分析時,tcpdump是一個非常實用的工具。它可以讀取網路介面的封包,並將其輸出到標準輸出或檔案中。tcpdump使用pcap過濾語法來過濾封包,該語法是一種高階的DSL(Domain-Specific Language),使得使用者可以使用簡單的表示式來過濾封包。

tcpdump和BPF表示式

假設我們有一個網頁伺服器執行在port 8080,我們想要分析該伺服器接收到的請求。可以使用以下命令:

tcpdump -n 'ip and tcp port 8080'

這個命令會過濾出所有IPv4的TCP封包,且目的埠為8080。

封包過濾和BPF程式

tcpdump使用pcap過濾語法來過濾封包,但其實這些過濾語法會被編譯成BPF程式。因此,每次執行tcpdump時,都會載入一個BPF程式來過濾封包。

可以使用以下命令來 dump 出BPF指令:

tcpdump -d 'ip and tcp port 8080'

這個命令會輸出BPF指令集,而不是封包內容。

BPF指令集

以下是輸出的BPF指令集:

(000) ldh [12]
(001) jeq #0x800 jt 2 jf 12
(002) ldb [23]
(003) jeq #0x6 jt 4 jf 12
(004) ldh [20]
(005) jset #0x1fff jt 12 jf 6

這些指令是用於過濾封包的BPF程式。

讓我們一步一步分析這段程式碼。

(006) ldxb 4*([14]&0xf)

這行程式碼負責載入 IP Header 長度到暫存器 x 中。其中,[14] 代表的是 IP Header 中的「Header Length」欄位,這個欄位通常是 4 或 5(代表 IP Header 的長度分別為 24 或 32 bytes)。&0xf 是一個位元運算,負責取出「Header Length」欄位的最後 4 個位元。然後,4\* 將結果乘以 4,以得到 IP Header 的實際長度(因為每個欄位的長度是 4 bytes)。

(007) ldh [x + 14]

這行程式碼載入位於 x + 14 Offset 的半字(16 位元)資料到累加器中。這裡的 x 是前一行程式碼計算出來的 IP Header 長度,而 14 是 Ethernet Header 的長度。因此,x + 14 代表的是 IP Header 中的「Protocol」欄位的 Offset。

(008) jeq #0x1f90 jt 11 jf 9

這行程式碼檢查累加器中的資料是否等於 0x1f90。如果相等,則跳轉到第 11 行;否則,跳轉到第 9 行。

(009) ldh [x + 16]

這行程式碼載入位於 x + 16 Offset 的半字(16 位元)資料到累加器中。這裡的 x 是前面計算出來的 IP Header 長度,而 16 是 TCP Header 的 Offset。

(010) jeq #0x1f90 jt 11 jf 12

這行程式碼檢查累加器中的資料是否等於 0x1f90。如果相等,則跳轉到第 11 行;否則,跳轉到第 12 行。

(011) ret #262144

如果跳轉到這行程式碼,則傳回值為 262144

(012) ret #0

如果跳轉到這行程式碼,則傳回值為 0

根據這段程式碼,我們可以看到它是在進行網路封包過濾,根據 IP Header 和 TCP Header 中的資料進行跳轉和傳回。具體來說,它檢查了 IP Header 的「Protocol」欄位和 TCP Header 的某些資料,如果符合某些條件,則傳回特定的值。

網路封包過濾與BPF

在網路封包過濾中,BPF(Berkeley Packet Filter)是一種強大的工具,允許使用者定義自訂的過濾規則,以篩選出特定的網路封包。這些過濾規則是使用一組指令編寫的,這些指令會被BPF解譯器執行,以決定哪些封包應該被接受或拒絕。

BPF指令集

BPF指令集是一組低階別的指令,用於實作網路封包過濾邏輯。這些指令包括載入半字(load half-word)、載入位元組(load byte)、跳轉(jump)等。以下是幾個例子:

  • ldh [x + 14]:載入半字,從偏移量(x + 14)處載入一個半字(16位元)的值。
  • jeq #0x1f90 jt 11 jf 9:如果載入的值等於0x1f90(8080的十六進製表示),則跳轉到標籤11,否則跳轉到標籤9。
  • ret #262144:傳回一個值,表示匹配的快照長度。

範例:過濾目的地埠為8080的TCP封包

假設我們想要過濾所有目的地埠為8080的TCP封包,可以使用以下BPF過濾器:

tcpdump -d 'ip and tcp dst port 8080'

這個過濾器會生成一組BPF指令,用於篩選出目的地埠為8080的TCP封包。生成的指令集可能如下:

(000) ldh [12]
(001) jeq #0x800 jt 2 jf 10
(002) ldb [23]
(003) jeq #0x6 jt 4 jf 10
(004) ldh [20]
(005) jset #0x1fff jt 10 jf 6
(006) ldxb 4*([14]&0xf)
(007) ldh [x + 16]
(008) jeq #0x1f90 jt 9 jf 10

這組指令會檢查封包是否為IP封包、是否為TCP封包、以及目的地埠是否為8080。如果所有條件都滿足,則傳回一個值,表示匹配的快照長度。

網路封包過濾技術

網路封包過濾是指根據特定條件對網路封包進行過濾和處理的技術。這種技術可以用於安全、監控和最佳化網路流量等方面。

BPF 程式設計

BPF(Berkeley Packet Filter)是一種用於網路封包過濾的程式設計語言。它可以用於建立自定義的網路封包過濾器,以實作特定的功能。

BPF 程式型別

BPF 程式有多種型別,包括:

  • BPF_PROG_TYPE_SOCKET_FILTER:此型別的 BPF 程式可以附加到 socket 上,對接收到的封包進行過濾。
  • BPF_PROG_TYPE_KPROBE:此型別的 BPF 程式可以用於核心函式的探測。
  • BPF_PROG_TYPE_TRACEPOINT:此型別的 BPF 程式可以用於跟蹤點的處理。

BPF 程式設計步驟

設計 BPF 程式的步驟如下:

  1. 定義 BPF 程式型別:根據需要選擇適合的 BPF 程式型別。
  2. 編寫 BPF 程式碼:使用 C 語言或其他支援的語言編寫 BPF 程式碼。
  3. 編譯 BPF 程式碼:使用 Clang 編譯器將 BPF 程式碼編譯為 ELF 檔案。
  4. 載入 BPF 程式:使用 bpf_load 函式將編譯好的 BPF 程式載入到核心中。
  5. 附加 BPF 程式:將載入的 BPF 程式附加到相應的 socket 或其他物件上。

例項:封包計數器

以下是使用 BPF 程式設計的一個簡單封包計數器的例項:

#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/udp.h>
#include <linux/icmp.h>

struct bpf_map_def SEC("maps") counter = {
   .type = BPF_MAP_TYPE_ARRAY,
   .key_size = sizeof(u32),
   .value_size = sizeof(u32),
   .max_entries = 3,
};

SEC("socket")
int socket_filter(struct __sk_buff *skb) {
    u32 protocol = load_byte(skb, offsetof(struct ethhdr, h_proto));
    u32 *counter_ptr;

    if (protocol == ETH_P_IP) {
        struct iphdr *iph = (struct iphdr *)(skb->data + sizeof(struct ethhdr));
        u32 proto = iph->protocol;

        if (proto == IPPROTO_TCP) {
            counter_ptr = &counter[0];
        } else if (proto == IPPROTO_UDP) {
            counter_ptr = &counter[1];
        } else if (proto == IPPROTO_ICMP) {
            counter_ptr = &counter[2];
        } else {
            return TC_ACT_SHOT;
        }

        __u32 *value = (__u32 *)&counter_ptr;
        __sync_fetch_and_add(value, 1);
    }

    return TC_ACT_SHOT;
}

char _license[] SEC("license") = "GPL";

這個例項使用 BPF 程式設計了一個簡單的封包計數器,根據 TCP、UDP 和 ICMP 封包的數量進行統計。

BPF 程式設計:通訊端過濾器

簡介

在本章中,我們將探討如何設計一款 BPF 程式,該程式能夠過濾通訊端流量。BPF(Berkeley Packet Filter)是一種高效的封包過濾技術,允許開發人員在 Linux 核心中執行自定義程式。

程式型別

BPF 提供了多種程式型別,包括 BPF_PROG_TYPE_SOCKET_FILTERBPF_PROG_TYPE_XDPBPF_PROG_TYPE_CGROUP_SKB 等。每種型別都有其特定的用途和限制。在本例中,我們將使用 BPF_PROG_TYPE_SOCKET_FILTER 來過濾通訊端流量。

程式設計

要設計一個 BPF 程式,首先需要定義一個 ELF 標頭,並指定程式的型別和名稱。然後,需要定義一個 bpf_map_def 結構體來儲存封包計數資訊。

struct bpf_map_def SEC("maps") countmap = {
   .type = BPF_MAP_TYPE_ARRAY,
   .key_size = sizeof(int),
   .value_size = sizeof(int),
   .max_entries = 256,
};

接下來,需要定義一個 socket_prog 函式,這個函式將被附加到通訊端上,以過濾流量。

SEC("socket")
int socket_prog(struct __sk_buff *skb) {
    int proto = load_byte(skb, ETH_HLEN + offsetof(struct iphdr, protocol));
    int one = 1;
    int *el = bpf_map_lookup_elem(&countmap, &proto);

    if (el) {
        (*el)++;
    } else {
        el = &one;
    }
    bpf_map_update_elem(&countmap, &proto, el, BPF_ANY);
    return 0;
}

這個函式使用 load_byte 函式來提取封包的協定號碼,然後使用 bpf_map_lookup_elem 函式來查詢計數器的值。如果計數器不存在,則初始化為 1。最後,使用 bpf_map_update_elem 函式來更新計數器的值。

編譯和載入

要編譯 BPF 程式,需要使用 Clang 編譯器,並指定 -target bpf 選項。

clang -O2 -target bpf -c bpf_program.c -o bpf_program.o

然後,需要使用 BPF 載入器來載入編譯好的 ELF 檔案,並附加到通訊端上。

if (load_bpf_file(filename)) {
    printf("%s", bpf_log_buf);
    return 1;
}
sock = open_raw_sock("lo");
if (setsockopt(sock, SOL_SOCKET, SO_ATTACH_BPF, prog_fd, sizeof(prog_fd[0]))) {
    printf("setsockopt %s\n", strerror(errno));
}

這樣就完成了 BPF 程式的設計和載入。

Linux 網路和 BPF

Linux 網路和 BPF(Berkeley Packet Filter)是一個強大的工具,允許使用者空間程式碼直接與網路介面卡進行互動。以下是使用 BPF 進行網路封包過濾和計數的範例程式碼:

#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/udp.h>
#include <linux/icmp.h>

#define MAX_PROGS 10

int prog_fd[MAX_PROGS];

int main() {
    //...

    // 開啟 raw socket 並附加 BPF 程式
    int sock = open_raw_sock("lo");
    if (sock < 0) {
        perror("open_raw_sock");
        return -1;
    }

    // 將 BPF 程式附加到 socket
    if (bpf_prog_load("bpf_program.o", &prog_fd[0]) < 0) {
        perror("bpf_prog_load");
        return -1;
    }

    // 查詢 map 元素
    for (int i = 0; i < 10; i++) {
        uint32_t key = IPPROTO_TCP;
        uint32_t tcp_cnt;
        assert(bpf_map_lookup_elem(prog_fd[0], &key, &tcp_cnt) == 0);

        key = IPPROTO_UDP;
        uint32_t udp_cnt;
        assert(bpf_map_lookup_elem(prog_fd[0], &key, &udp_cnt) == 0);

        key = IPPROTO_ICMP;
        uint32_t icmp_cnt;
        assert(bpf_map_lookup_elem(prog_fd[0], &key, &icmp_cnt) == 0);

        printf("TCP %d UDP %d ICMP %d packets\n", tcp_cnt, udp_cnt, icmp_cnt);
        sleep(1);
    }

    return 0;
}

編譯 BPF 程式

要編譯 BPF 程式,需要使用 libbpf 函式庫。首先,需要編譯 libbpf 函式庫:

$ cd $KERNEL_SRCTREE/tools/lib/bpf
$ make

然後,使用以下指令編譯 BPF 程式:

$ clang -o loader-bin -I${KERNEL_SRCTREE}/tools/lib/bpf/ \
         -I${KERNEL_SRCTREE}/tools/lib -I${KERNEL_SRCTREE}/tools/include \
         -I${KERNEL_SRCTREE}/tools/perf -I${KERNEL_SRCTREE}/samples \
         ${KERNEL_SRCTREE}/samples/bpf/bpf_load.c \
         loader.c "${LIBBPF}" -lelf

執行 BPF 程式

執行 BPF 程式需要 root 許可權:

#./loader-bin bpf_program.o

這將載入 BPF 程式並開始執行。程式將每秒鐘輸出一次封包計數。

測試 BPF 程式

可以使用 ping 命令生成 ICMP 封包來測試 BPF 程式:

$ ping -c 100 127.0.0.1

這將生成 100 個 ICMP 封包,並且 BPF 程式將計數這些封包。

圖表翻譯:

  graph LR
    A[開啟 raw socket] --> B[附加 BPF 程式]
    B --> C[查詢 map 元素]
    C --> D[輸出封包計數]
    D --> E[等待 1 秒]
    E --> C

內容解密:

以上程式碼示範瞭如何使用 BPF 進行網路封包過濾和計數。首先,需要開啟一個 raw socket 並附加 BPF 程式。然後,需要查詢 map 元素來取得封包計數。最後,需要輸出封包計數並等待 1 秒後再次查詢 map 元素。

使用 eBPF 進行封包過濾

在上一節中,我們已經瞭解瞭如何使用 socket filter eBPF 程式來過濾封包。現在,我們將探討另一個重要的應用:使用 eBPF 來檢測 Linux 的封包排程子系統。

首先,我們需要了解 Linux 的封包排程子系統是如何運作的。當封包抵達網路介面卡 (NIC) 時,Linux 會將其傳遞給網路驅動程式,然後驅動程式會將封包放入一個佇列中。接著,封包排程子系統會從佇列中取出封包,並根據其優先順序和其他因素進行排程。

要使用 eBPF 來檢測封包排程子系統,我們需要建立一個 eBPF 程式,並將其附加到適當的 hook 點。eBPF 提供了多個 hook 點,包括 skb_getskb_putskb_clone,我們可以根據需要選擇適合的 hook 點。

以下是一個簡單的 eBPF 程式範例,示範如何使用 skb_get hook 點來檢測封包排程子系統:

#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>

SEC("skb_get")
int bpf_prog(struct __sk_buff *skb)
{
    // 取得封包的 IP 首部
    void *ip_header = (void *)(long)skb->data + sizeof(struct ethhdr);

    // 檢查是否為 ICMP 封包
    if (ip_header && ((struct iphdr *)ip_header)->protocol == IPPROTO_ICMP) {
        // 封包計數器
        __u32 *icmp_count = bpf_map_lookup_elem(&icmp_count_map, &skb->pkt_type);
        if (icmp_count) {
            (*icmp_count)++;
        }
    }

    return BPF_OK;
}

在這個範例中,我們定義了一個 eBPF 程式 bpf_prog,它會被附加到 skb_get hook 點。當封包抵達 skb_get hook 點時,eBPF 程式會被執行。程式會取得封包的 IP 首部,並檢查是否為 ICMP 封包。如果是,則會增加 ICMP 封包計數器。

要載入這個 eBPF 程式,我們可以使用 bpftool 命令:

# bpftool prog load bpf_program.o /sys/fs/bpf/bpf_program

接著,我們可以使用 tcpdump 命令來測試 eBPF 程式:

# tcpdump -i any -n -vv -s 0 -c 100 -W 100

這個命令會捕捉 100 個封包,並將其輸出到標準輸出。同時,eBPF 程式會被執行,並增加 ICMP 封包計數器。

最後,我們可以使用 bpftool 命令來檢視 ICMP 封包計數器:

# bpftool map dump /sys/fs/bpf/icmp_count_map

這個命令會輸出 ICMP 封包計數器的值,我們可以看到 eBPF 程式已經正確地增加了計數器。

Linux 網路與 BPF

深入剖析 Linux 網路與 BPF 的整合機制後,我們可以發現,BPF 技術在網路封包過濾、流量控制和效能分析等方面展現出強大的優勢。從 tcpdump 的應用到 eBPF 程式的設計和載入,BPF 提供了高度靈活且精確的網路控制能力。然而,BPF 程式設計的複雜性和對核心版本的依賴性也帶來了一定的挑戰。權衡效能提升與開發成本,對於需要精細化網路管理的場景,BPF 技術的應用價值顯著。玄貓認為,隨著 eBPF 技術的持續發展和社群的壯大,其應用門檻將逐步降低,並在雲原生網路、安全監控等領域扮演更關鍵的角色。未來,eBPF 與其他網路技術的融合將進一步釋放其潛力,值得密切關注。