實現一個基於XDP_eBPF的學習型網橋

  • 2019 年 11 月 13 日
  • 筆記

eBPF技術風靡當下,eBPF位元組碼正以星火燎原之勢被HOOK在Linux內核中越來越多的位置,在這些HOOK點上,我們可以像編寫普通應用程式一樣編寫內核的HOOK程式,與以往為了實現一個功能動輒patch一整套邏輯框架程式碼(比如Netfilter)相比,eBPF的工作方式非常靈活。

我們先來看一下目前eBPF的一些重要HOOK點:

將來這個is_XXX序列肯定會不斷增加,布滿整個內核(有點密集恐懼症癥狀了…)。

本文將描述如何用eBPF實現一個學習型網橋的快速轉發,並將其部署在XDP。

在開始之前,為了讓所有人都能看懂本文,我們先來回顧一些前置知識,如果暫時還不懂這些前置知識,沒關係,先把程式run起來是一個很好的起點,如果到時候你覺得沒意思,再放棄也不遲。

前置知識

  • 什麼是BPF和eBPF

簡單來講,BPF是一套完整的 電腦體系結構 。和x86,ARM這些類似,BPF包含自己的指令集和運行時邏輯,同理,就像在x86平台編程,最終要落實到x86彙編指令一樣,BPF位元組碼也可以看成是彙編指令的序列。我們通過tcpdump的-d/-dd參數可見一斑:

[root@localhost ~]# tcpdump -i any tcp and host 1.1.1.1 -d(000) ldh      [14](001) jeq      #0x86dd          jt 10    jf 2(002) jeq      #0x800           jt 3    jf 10(003) ldb      [25](004) jeq      #0x6             jt 5    jf 10(005) ld       [28](006) jeq      #0x1010101       jt 9    jf 7(007) ld       [32](008) jeq      #0x1010101       jt 9    jf 10(009) ret      #262144(010) ret      #0[root@localhost ~]#

BPF的歷史非常古老,早在1992年就被構建出來了,其背後的思想是, 「與其把數據包複製到用戶空間執行用戶態程式過濾,不如把過濾程式灌進內核去。」

遺憾的是,BPF後來並沒有大行其道,只是被應用於非常有限的並不起眼的比如抓包層面。因此,由於它的語法並不複雜,人們直接手寫BPF彙編指令碼經簡單封裝即可生成最終的位元組碼。

當人們認識到BPF非常強壯的功能並準備將其大用時,指令系統以及作業系統內核均已經持續進化了好多年,這意味著簡單的BPF不能再滿足需要,它需要 「被複雜化」

於是就出現了eBPF,即extended BPF。總體而言,eBPF相比BPF有了以下改進:1. 更複雜的指令系統。2. 更多可調用的函數。3. … 詳情可參見下面的鏈接:https://lwn.net/Articles/740157/

就像彙編語言進化到C語言一樣,直接手寫eBPF位元組碼顯得即笨拙又低效,於是人們開始使用C語言直接編寫eBPF程式,然後用編譯器將其編譯成eBPF位元組碼。遺憾的是,目前eBPF體系結構還不被gcc支援,不過很快就會支援了。我們不得不使用 特定的編譯器 來編譯eBPF的C程式碼,比如clang。

  • 什麼是XDP

XDP,即eXpress Data Path,它其實是位於網卡驅動程式里的一個快速處理數據包的HOOK點,為什麼快?基於以下兩點:

  1. 數據包處理位置非常底層,避開了很多內核skb處理開銷。
  2. 可以將很多處理邏輯Offload到網卡硬體。

顯而易見,在XDP這個HOOK點灌進來一點eBPF位元組碼,將是一件令人愉快的事情。

  • 學習型網橋

Linux的Bridge模組就是一個學習型網橋,其實就是一個現代交換式乙太網交換機,它可以從埠學習到MAC地址,在內部生成MAC/埠映射表,以優化轉發效率。

本文我們將用eBPF實現的網橋就是一個學習型網橋,並且它的數據路徑和控制路徑相分離,用eBPF位元組碼實現的正是其數據路徑,它將被灌入XDP,而控制路徑則由一個用戶態程式實現。

  • 如何編譯eBPF程式

理論的學習自在平時,當打開電腦的時候,最快的速度run起來一些東西令人愉悅。我們不想花大量的時間在環境的搭建上。對於eBPF程式,內核源碼樹的samples/bpf目錄將是一個非常好的起點。

以我自己的環境為例,我使用的是Ubuntu 19.10發行版,5.3.0-19-generic內核,安裝源碼後,編譯之,最後編譯samples/bpf即可:

root@zhaoya-VirtualBox:/usr/src/linux-source-5.3.0/linux-source-5.3.0/samples/bpf# makemake -C ../../ /usr/src/linux-source-5.3.0/linux-source-5.3.0/samples/bpf/ BPF_SAMPLES_PATH=/usr/src/linux-source-5.3.0/linux-source-5.3.0/samples/bpfmake[1]: Entering directory '/usr/src/linux-source-5.3.0/linux-source-5.3.0'  CALL    scripts/checksyscalls.sh  CALL    scripts/atomic/check-atomics.sh  DESCEND  objtool  ...

samples/bpf目錄下的程式碼都是比較典型的範例,我們照貓畫虎就能實現我們想要的功能。

大體上,每一個範例均由兩個部分組成:

  1. XXX_kern.c文件:eBPF位元組碼本身。
  2. XXX_user.c文件:用戶態控制程式,控制eBPF位元組碼的注入,更新。

即然我們要實現一個網橋,那麼文件名我們可以確定為:

  1. xdpbridgekern.c
  2. xdpbridgeuser.c

同時我們修改Makefile文件,加入這兩個文件即可:

root@zhaoya-VirtualBox: samples/bpf# cat Makefile...hostprogs-y += xdp2hostprogs-y += xdp_bridgehostprogs-y += xdp_router_ipv4...xdp_bridge-objs := xdp_bridge_user.oxdp_router_ipv4-objs := xdp_router_ipv4_user.o...always += xdp2_kern.oalways += xdp_bridge_kern.oalways += xdp_router_ipv4_kern.o

網橋XDP快速轉發的實現

對上述前置知識有了充分的理解之後,程式碼就非常簡單了,我們剩下的工作就是填充xdpbridgekern.c和xdpbridgeuser.c兩個C文件,然後make它們。

我們先來看xdpbridgekern.c文件:

// xdp_bridge_kern.c#include <uapi/linux/bpf.h>#include <linux/if_ether.h>#include "bpf_helpers.h"  // mac_port_map保存該交換機的MAC/埠映射struct bpf_map_def SEC("maps") mac_port_map = {    .type = BPF_MAP_TYPE_HASH,    .key_size = sizeof(long),    .value_size = sizeof(int),    .max_entries = 100,};  // 以下函數是網橋轉發路徑的eBPF主函數實現SEC("xdp_br")int xdp_bridge_prog(struct xdp_md *ctx){    void *data_end = (void *)(long)ctx->data_end;    void *data = (void *)(long)ctx->data;    long dst_mac = 0;    int in_index = ctx->ingress_ifindex, *out_index;    // data即數據包開始位置    struct ethhdr *eth = (struct ethhdr *)data;    char info_fmt[] = "Destination Address: %lx   Redirect to:[%d]   From:[%d]n";      // 畸形包必須丟棄,否則無法通過內核的eBPF位元組碼合法性檢查    if (data + sizeof(struct ethhdr) > data_end) {        return XDP_DROP;    }      // 獲取目標MAC地址    __builtin_memcpy(&dst_mac, eth->h_dest, 6);      // 在MAC/埠映射表裡查找對應該MAC的埠    out_index = bpf_map_lookup_elem(&mac_port_map, &dst_mac);    if (out_index == NULL) {         // 如若找不到,則上傳到慢速路徑,必要時由控制路徑更新MAC/埠表項。        return XDP_PASS;    }      // 非Hairpin下生效    if (in_index == *out_index) { // Hairpin ?        return XDP_DROP;    }      // 簡單列印些調試資訊    bpf_trace_printk(info_fmt, sizeof(info_fmt), dst_mac, *out_index, in_index);      // 轉發到出埠    return  bpf_redirect(*out_index, 0);}  char _license[] SEC("license") = "GPL";

這裡有必要說一下內核對eBPF程式的合法性檢查,這個檢查一點都不多餘,它確保你的eBPF程式碼是安全的。這樣才不會造成內核數據結構被破壞掉,否則,如果任意eBPF程式都能注入內核,那結局顯然是細思極恐的。

現在繼續我們的用戶態C程式碼:

// xdp_bridge_user.c  #include <stdio.h>#include <signal.h>#include <sys/socket.h>#include <net/if.h>#include <bpf/bpf.h>#include <linux/bpf.h>#include <linux/rtnetlink.h>#include "bpf_util.h"  int flags = XDP_FLAGS_UPDATE_IF_NOEXIST;static int mac_port_map_fd;static int *ifindex_list;  // 退出時卸載掉XDP的eBPF位元組碼static void int_exit(int sig){    int i = 0;    for (i = 0; i < 2; i++) {        bpf_set_link_xdp_fd(ifindex_list[i], -1, 0);    }    exit(0);}  int main(int argc, char *argv[]){    int sock, i;    char buf[1024];    char filename[64];    static struct sockaddr_nl g_addr;    struct bpf_object *obj;    struct bpf_prog_load_attr prog_load_attr = {        // prog_type指明eBPF位元組碼注入的位置,我們網橋的例子中當然是XDP        .prog_type  = BPF_PROG_TYPE_XDP,    };    int prog_fd;      snprintf(filename, sizeof(filename), "xdp_bridge_kern.o");    prog_load_attr.file = filename;      // 載入eBPF位元組碼    if (bpf_prog_load_xattr(&prog_load_attr, &obj, &prog_fd)) {        return 1;    }      mac_port_map_fd = bpf_object__find_map_fd_by_name(obj, "mac_port_map");    ifindex_list = (int *)calloc(2, sizeof(int *));      // 我們的例子中僅僅支援兩個埠的網橋,事實上可以多個。    ifindex_list[0] = if_nametoindex(argv[1]);    ifindex_list[1] = if_nametoindex(argv[2]);      for (i = 0; i < 2/*total */; i++) {        // 將eBPF位元組碼注入到感興趣網卡的XDP        if (bpf_set_link_xdp_fd(ifindex_list[i], prog_fd, flags) < 0) {            printf("link set xdp fd failedn");            return 1;        }    }    signal(SIGINT, int_exit);      bzero(&g_addr, sizeof(g_addr));    g_addr.nl_family = AF_NETLINK;    g_addr.nl_groups = RTM_NEWNEIGH;      if ((sock = socket(AF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE)) < 0) {        int_exit(0);        return -1;    }      if (bind(sock, (struct sockaddr *) &g_addr, sizeof(g_addr)) < 0) {        int_exit(0);        return 1;    }      // 持續監聽socket,捕獲Linux網橋上傳的notify資訊,從而更新,刪除eBPF的map里特定的MAC/埠表項    while (1) {        int len;        struct nlmsghdr *nh;        struct ndmsg *ifimsg ;        int ifindex = 0;        unsigned char *cmac;        unsigned long lkey = 0;          len = recv(sock, buf, sizeof(buf), 0);        if (len <= 0) continue;          for (nh = (struct nlmsghdr *)buf; NLMSG_OK(nh, len); nh = NLMSG_NEXT(nh, len)) {            ifimsg = NLMSG_DATA(nh) ;            if (ifimsg->ndm_family != AF_BRIDGE) {                continue;            }              // 獲取notify資訊中的埠            ifindex = ifimsg->ndm_ifindex;            for (i = 0; i < 2; i++) {                if (ifindex == ifindex_list[i]) break;            }            if (i == 2) continue;              // 獲取notify資訊中的MAC地址            cmac = (unsigned char *)ifimsg + sizeof(struct ndmsg) + 4;              memcpy(&lkey, cmac, 6);            if (nh->nlmsg_type == RTM_DELNEIGH) {                bpf_map_delete_elem(mac_port_map_fd, (const void *)&lkey);                printf("Delete XDP bpf map-[HW Address:Port] item Key:[%lx]  Value:[%d]n", lkey, ifindex);            } else if (nh->nlmsg_type == RTM_NEWNEIGH) {                bpf_map_update_elem(mac_port_map_fd, (const void *)&lkey, (const void *)&ifindex, 0);                printf("Update XDP bpf map-[HW Address:Port]  item Key:[%lx]  Value:[%d]n", lkey, ifindex);            }        }    }}

用戶態程式同樣很容易理解。

數據面和控制面分離,這是網路設備的標準路數,幾十年前就這樣了,如今我們也能簡單實現一個了,很有趣不是嗎?

run起來

執行make之後,我們可以得到可執行文件xdpbridge以及eBPF位元組碼文件xdpbridge_kern.o,在當前目錄下直接執行即可:

root@zhaoya-VirtualBox:samples/bpf# ./xdp_bridge enp0s9 enp0s10

在另一個終端查看eBPF位元組碼里的map,即MAC/埠映射表:

root@zhaoya-VirtualBox:/home/zhaoya# bpftool p |tail -n 4166: xdp  name xdp_bridge_prog  tag 956a68e9ac54a0b3  gpl    loaded_at 2019-11-08T01:14:46+0800  uid 0    xlated 576B  jited 340B  memlock 4096B  map_ids 105    btf_id 114root@zhaoya-VirtualBox:/home/zhaoya# bpftool map dump id 105Found 0 elementsroot@zhaoya-VirtualBox:/home/zhaoya#

OK,一切順利。現在讓我們正式用它搭建一個網橋吧。

暫時X掉xdp_bridge程式的運行,讓我們一步一步來。

首先構建下面的拓撲:

中間的Linux Bridge主機(後面簡稱主機B)的enp0s9,enp0s10網卡將是我們注入eBPF位元組碼的位置。

現在讓我們在主機B上創建一個標準的Linux網橋:

brctl addbr br0;brctl addif br0 enp0s9;brctl addif br0 enp0s10;ifconfig br0 up;

在主機H1和主機H2的enp0s9上配置同網段的地址:

H1-enp0s9:40.40.40.201/24H2-enp0s9:40.40.40.100/24

互相ping確認是通的,並且主機B的enp0s9/enp0s10可以抓到雙向包,這說明主機B的Linux標準網橋工作是OK的。

接下來,停掉這一切,把br0也刪除掉。重新運行xdpbridge程式,確認OK後創建Linux標準網橋,從H1來ping H2,很暢通,同時我們會發現主機B的xdpbridge程式的輸出:

root@zhaoya-VirtualBox:/usr/src/linux-source-5.3.0/linux-source-5.3.0/samples/bpf# ./xdp_bridge enp0s9 enp0s10Update XDP bpf map-[HW Address:Port]  item Key:[683dbb270008]  Value:[4]Update XDP bpf map-[HW Address:Port]  item Key:[683dbb270008]  Value:[4]Update XDP bpf map-[HW Address:Port]  item Key:[e7f09f270008]  Value:[5]Update XDP bpf map-[HW Address:Port]  item Key:[e7f09f270008]  Value:[5]  Update XDP bpf map-[HW Address:Port]  item Key:[e6f09f270008]  Value:[4]

很顯然,eBPF的map學習到了新的MAC地址,我們可以用bpftool確認:

root@zhaoya-VirtualBox:~# bpftool p |tail -n 4170: xdp  name xdp_bridge_prog  tag 956a68e9ac54a0b3  gpl    loaded_at 2019-11-08T01:26:19+0800  uid 0    xlated 576B  jited 340B  memlock 4096B  map_ids 107    btf_id 117root@zhaoya-VirtualBox:~# bpftool map dump id 107key: 08 00 27 9f f0 e7 00 00  value: 05 00 00 00key: 08 00 27 9f f0 e6 00 00  value: 04 00 00 00key: 08 00 27 bb 3d 68 00 00  value: 04 00 00 00Found 3 elements

此時,主機B的enp0s9和enp0s10就抓不到任何H1和H2之間單播包了。廣播包仍然會被上傳到慢速路徑被標準Linux網橋處理。

我們看trace日誌:

root@zhaoya-VirtualBox:~# cat  /sys/kernel/debug/tracing/trace_pipe          <idle>-0     [003] ..s. 44274.198178: 0: Destination Address: e6f09f270008   Redirect to:[4]   From:[5]  ...

雖然主機B的網卡上沒有抓到包,但如何確保數據包真的就是從XDP的eBPF位元組碼轉發走的而不是直接飛過去的呢?

很好的問題,這作為下一個練習不是更好嗎?嗯,你應該試試加一個統計功能,而這個並不複雜。

資源與引用

本文只是拋磚引玉,如果覺得不過癮,是時候就著啤酒或咖啡可樂讀一下下面的資源了:

https://arthurchiao.github.io/blog/cilium-bpf-xdp-reference-guide-zh/

https://docs.cilium.io/en/v1.6/bpf/

https://github.com/tklauser/filter2xdp

https://klyr.github.io/posts/ebpf/https://linux.cn/article-9507-1.html

https://jvns.ca/blog/2017/06/28/notes-on-bpf—ebpf/

XDP

… 對了,如果你在使用VirtualBox搭建橋接環境遇到問題的時候,請參考這篇:https://blog.csdn.net/dog250/article/details/102972031