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點,為什么快?基于以下兩點:
數據包處理位置非常底層,避開了很多內核skb處理開銷。
顯而易見,在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# make
make -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/bpf
make[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目錄下的代碼都是比較典型的范例,我們照貓畫虎就能實現我們想要的功能。
大體上,每一個范例均由兩個部分組成:
XXX_kern.c文件:eBPF字節碼本身。
XXX_user.c文件:用戶態控制程序,控制eBPF字節碼的注入,更新。
即然我們要實現一個網橋,那么文件名我們可以確定為:
xdpbridgekern.c
xdpbridgeuser.c
同時我們修改Makefile文件,加入這兩個文件即可:
root@zhaoya-VirtualBox: samples/bpf# cat Makefile
...
hostprogs-y += xdp2
hostprogs-y += xdp_bridge
hostprogs-y += xdp_router_ipv4
...
xdp_bridge-objs := xdp_bridge_user.o
xdp_router_ipv4-objs := xdp_router_ipv4_user.o
...
always += xdp2_kern.o
always += xdp_bridge_kern.o
always += xdp_router_ipv4_kern.o
網橋XDP快速轉發的實現
對上述前置知識有了充分的理解之后,代碼就非常簡單了,我們剩下的工作就是填充xdpbridgekern.c和xdpbridgeuser.c兩個C文件,然后make它們。
我們先來看xdpbridgekern.c文件:
// xdp_bridge_kern.c
#include
#include
#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] ";
// 畸形包必須丟棄,否則無法通過內核的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
#include
#include
#include
#include
#include
#include
#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 failed ");
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] ", 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] ", 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 4
166: 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 114
root@zhaoya-VirtualBox:/home/zhaoya# bpftool map dump id 105
Found 0 elements
root@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/24
H2-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 enp0s10
Update 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 4
170: 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 117
root@zhaoya-VirtualBox:~# bpftool map dump id 107
key: 08 00 27 9f f0 e7 00 00 value: 05 00 00 00
key: 08 00 27 9f f0 e6 00 00 value: 04 00 00 00
key: 08 00 27 bb 3d 68 00 00 value: 04 00 00 00
Found 3 elements
此時,主機B的enp0s9和enp0s10就抓不到任何H1和H2之間單播包了。廣播包仍然會被上傳到慢速路徑被標準Linux網橋處理。
我們看trace日志:
root@zhaoya-VirtualBox:~# cat /sys/kernel/debug/tracing/trace_pipe
...
雖然主機B的網卡上沒有抓到包,但如何確保數據包真的就是從XDP的eBPF字節碼轉發走的而不是直接飛過去的呢?
很好的問題,這作為下一個練習不是更好嗎?嗯,你應該試試加一個統計功能,而這個并不復雜。
-
Linux
+關注
關注
87文章
11292瀏覽量
209329 -
網橋
+關注
關注
0文章
130瀏覽量
16969 -
BPF
+關注
關注
0文章
25瀏覽量
4002
原文標題:實現一個基于XDP_eBPF的學習型網橋
文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論