eBPF技術(shù)風(fēng)靡當(dāng)下,eBPF字節(jié)碼正以星火燎原之勢被HOOK在Linux內(nèi)核中越來越多的位置,在這些HOOK點(diǎn)上,我們可以像編寫普通應(yīng)用程序一樣編寫內(nèi)核的HOOK程序,與以往為了實(shí)現(xiàn)一個(gè)功能動輒patch一整套邏輯框架代碼(比如Netfilter)相比,eBPF的工作方式非常靈活。
我們先來看一下目前eBPF的一些重要HOOK點(diǎn):
將來這個(gè)is_XXX序列肯定會不斷增加,布滿整個(gè)內(nèi)核(有點(diǎn)密集恐懼癥癥狀了...)。
本文將描述如何用eBPF實(shí)現(xiàn)一個(gè)學(xué)習(xí)型網(wǎng)橋的快速轉(zhuǎn)發(fā),并將其部署在XDP。
在開始之前,為了讓所有人都能看懂本文,我們先來回顧一些前置知識,如果暫時(shí)還不懂這些前置知識,沒關(guān)系,先把程序run起來是一個(gè)很好的起點(diǎn),如果到時(shí)候你覺得沒意思,再放棄也不遲。
前置知識
什么是BPF和eBPF
簡單來講,BPF是一套完整的 計(jì)算機(jī)體系結(jié)構(gòu) 。和x86,ARM這些類似,BPF包含自己的指令集和運(yùn)行時(shí)邏輯,同理,就像在x86平臺編程,最終要落實(shí)到x86匯編指令一樣,BPF字節(jié)碼也可以看成是匯編指令的序列。我們通過tcpdump的-d/-dd參數(shù)可見一斑:
[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年就被構(gòu)建出來了,其背后的思想是, “與其把數(shù)據(jù)包復(fù)制到用戶空間執(zhí)行用戶態(tài)程序過濾,不如把過濾程序灌進(jìn)內(nèi)核去。”
遺憾的是,BPF后來并沒有大行其道,只是被應(yīng)用于非常有限的并不起眼的比如抓包層面。因此,由于它的語法并不復(fù)雜,人們直接手寫B(tài)PF匯編指令碼經(jīng)簡單封裝即可生成最終的字節(jié)碼。
當(dāng)人們認(rèn)識到BPF非常強(qiáng)壯的功能并準(zhǔn)備將其大用時(shí),指令系統(tǒng)以及操作系統(tǒng)內(nèi)核均已經(jīng)持續(xù)進(jìn)化了好多年,這意味著簡單的BPF不能再滿足需要,它需要 “被復(fù)雜化” 。
于是就出現(xiàn)了eBPF,即extended BPF。總體而言,eBPF相比BPF有了以下改進(jìn):1. 更復(fù)雜的指令系統(tǒng)。2. 更多可調(diào)用的函數(shù)。3. ...詳情可參見下面的鏈接:https://lwn.net/Articles/740157/
就像匯編語言進(jìn)化到C語言一樣,直接手寫eBPF字節(jié)碼顯得即笨拙又低效,于是人們開始使用C語言直接編寫eBPF程序,然后用編譯器將其編譯成eBPF字節(jié)碼。遺憾的是,目前eBPF體系結(jié)構(gòu)還不被gcc支持,不過很快就會支持了。我們不得不使用 特定的編譯器 來編譯eBPF的C代碼,比如clang。
什么是XDP
XDP,即eXpress Data Path,它其實(shí)是位于網(wǎng)卡驅(qū)動程序里的一個(gè)快速處理數(shù)據(jù)包的HOOK點(diǎn),為什么快?基于以下兩點(diǎn):
數(shù)據(jù)包處理位置非常底層,避開了很多內(nèi)核skb處理開銷。
顯而易見,在XDP這個(gè)HOOK點(diǎn)灌進(jìn)來一點(diǎn)eBPF字節(jié)碼,將是一件令人愉快的事情。
學(xué)習(xí)型網(wǎng)橋
Linux的Bridge模塊就是一個(gè)學(xué)習(xí)型網(wǎng)橋,其實(shí)就是一個(gè)現(xiàn)代交換式以太網(wǎng)交換機(jī),它可以從端口學(xué)習(xí)到MAC地址,在內(nèi)部生成MAC/端口映射表,以優(yōu)化轉(zhuǎn)發(fā)效率。
本文我們將用eBPF實(shí)現(xiàn)的網(wǎng)橋就是一個(gè)學(xué)習(xí)型網(wǎng)橋,并且它的數(shù)據(jù)路徑和控制路徑相分離,用eBPF字節(jié)碼實(shí)現(xiàn)的正是其數(shù)據(jù)路徑,它將被灌入XDP,而控制路徑則由一個(gè)用戶態(tài)程序?qū)崿F(xiàn)。
如何編譯eBPF程序
理論的學(xué)習(xí)自在平時(shí),當(dāng)打開電腦的時(shí)候,最快的速度run起來一些東西令人愉悅。我們不想花大量的時(shí)間在環(huán)境的搭建上。對于eBPF程序,內(nèi)核源碼樹的samples/bpf目錄將是一個(gè)非常好的起點(diǎn)。
以我自己的環(huán)境為例,我使用的是Ubuntu 19.10發(fā)行版,5.3.0-19-generic內(nèi)核,安裝源碼后,編譯之,最后編譯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目錄下的代碼都是比較典型的范例,我們照貓畫虎就能實(shí)現(xiàn)我們想要的功能。
大體上,每一個(gè)范例均由兩個(gè)部分組成:
XXX_kern.c文件:eBPF字節(jié)碼本身。
XXX_user.c文件:用戶態(tài)控制程序,控制eBPF字節(jié)碼的注入,更新。
即然我們要實(shí)現(xiàn)一個(gè)網(wǎng)橋,那么文件名我們可以確定為:
xdpbridgekern.c
xdpbridgeuser.c
同時(shí)我們修改Makefile文件,加入這兩個(gè)文件即可:
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
網(wǎng)橋XDP快速轉(zhuǎn)發(fā)的實(shí)現(xiàn)
對上述前置知識有了充分的理解之后,代碼就非常簡單了,我們剩下的工作就是填充xdpbridgekern.c和xdpbridgeuser.c兩個(gè)C文件,然后make它們。
我們先來看xdpbridgekern.c文件:
// xdp_bridge_kern.c
#include
#include
#include "bpf_helpers.h"
// mac_port_map保存該交換機(jī)的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,
};
// 以下函數(shù)是網(wǎng)橋轉(zhuǎn)發(fā)路徑的eBPF主函數(shù)實(shí)現(xiàn)
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即數(shù)據(jù)包開始位置
struct ethhdr *eth = (struct ethhdr *)data;
char info_fmt[] = "Destination Address: %lx Redirect to:[%d] From:[%d] ";
// 畸形包必須丟棄,否則無法通過內(nèi)核的eBPF字節(jié)碼合法性檢查
if (data + sizeof(struct ethhdr) > data_end) {
return XDP_DROP;
}
// 獲取目標(biāo)MAC地址
__builtin_memcpy(&dst_mac, eth->h_dest, 6);
// 在MAC/端口映射表里查找對應(yīng)該MAC的端口
out_index = bpf_map_lookup_elem(&mac_port_map, &dst_mac);
if (out_index == NULL) {
// 如若找不到,則上傳到慢速路徑,必要時(shí)由控制路徑更新MAC/端口表項(xiàng)。
return XDP_PASS;
}
// 非Hairpin下生效
if (in_index == *out_index) { // Hairpin ?
return XDP_DROP;
}
// 簡單打印些調(diào)試信息
bpf_trace_printk(info_fmt, sizeof(info_fmt), dst_mac, *out_index, in_index);
// 轉(zhuǎn)發(fā)到出端口
return bpf_redirect(*out_index, 0);
}
char _license[] SEC("license") = "GPL";
這里有必要說一下內(nèi)核對eBPF程序的合法性檢查,這個(gè)檢查一點(diǎn)都不多余,它確保你的eBPF代碼是安全的。這樣才不會造成內(nèi)核數(shù)據(jù)結(jié)構(gòu)被破壞掉,否則,如果任意eBPF程序都能注入內(nèi)核,那結(jié)局顯然是細(xì)思極恐的。
現(xiàn)在繼續(xù)我們的用戶態(tài)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;
// 退出時(shí)卸載掉XDP的eBPF字節(jié)碼
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字節(jié)碼注入的位置,我們網(wǎng)橋的例子中當(dāng)然是XDP
.prog_type = BPF_PROG_TYPE_XDP,
};
int prog_fd;
snprintf(filename, sizeof(filename), "xdp_bridge_kern.o");
prog_load_attr.file = filename;
// 載入eBPF字節(jié)碼
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 *));
// 我們的例子中僅僅支持兩個(gè)端口的網(wǎng)橋,事實(shí)上可以多個(gè)。
ifindex_list[0] = if_nametoindex(argv[1]);
ifindex_list[1] = if_nametoindex(argv[2]);
for (i = 0; i < 2/*total */; i++) {
// 將eBPF字節(jié)碼注入到感興趣網(wǎng)卡的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;
}
// 持續(xù)監(jiān)聽socket,捕獲Linux網(wǎng)橋上傳的notify信息,從而更新,刪除eBPF的map里特定的MAC/端口表項(xiàng)
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);
}
}
}
}
用戶態(tài)程序同樣很容易理解。
數(shù)據(jù)面和控制面分離,這是網(wǎng)絡(luò)設(shè)備的標(biāo)準(zhǔn)路數(shù),幾十年前就這樣了,如今我們也能簡單實(shí)現(xiàn)一個(gè)了,很有趣不是嗎?
run起來
執(zhí)行make之后,我們可以得到可執(zhí)行文件xdpbridge以及eBPF字節(jié)碼文件xdpbridge_kern.o,在當(dāng)前目錄下直接執(zhí)行即可:
root@zhaoya-VirtualBox:samples/bpf# ./xdp_bridge enp0s9 enp0s10
在另一個(gè)終端查看eBPF字節(jié)碼里的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,一切順利。現(xiàn)在讓我們正式用它搭建一個(gè)網(wǎng)橋吧。
暫時(shí)X掉xdp_bridge程序的運(yùn)行,讓我們一步一步來。
首先構(gòu)建下面的拓?fù)洌?/p>
中間的Linux Bridge主機(jī)(后面簡稱主機(jī)B)的enp0s9,enp0s10網(wǎng)卡將是我們注入eBPF字節(jié)碼的位置。
現(xiàn)在讓我們在主機(jī)B上創(chuàng)建一個(gè)標(biāo)準(zhǔn)的Linux網(wǎng)橋:
brctl addbr br0;
brctl addif br0 enp0s9;
brctl addif br0 enp0s10;
ifconfig br0 up;
在主機(jī)H1和主機(jī)H2的enp0s9上配置同網(wǎng)段的地址:
H1-enp0s9:40.40.40.201/24
H2-enp0s9:40.40.40.100/24
互相ping確認(rèn)是通的,并且主機(jī)B的enp0s9/enp0s10可以抓到雙向包,這說明主機(jī)B的Linux標(biāo)準(zhǔn)網(wǎng)橋工作是OK的。
接下來,停掉這一切,把br0也刪除掉。重新運(yùn)行xdpbridge程序,確認(rèn)OK后創(chuàng)建Linux標(biāo)準(zhǔn)網(wǎng)橋,從H1來ping H2,很暢通,同時(shí)我們會發(fā)現(xiàn)主機(jī)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學(xué)習(xí)到了新的MAC地址,我們可以用bpftool確認(rèn):
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
此時(shí),主機(jī)B的enp0s9和enp0s10就抓不到任何H1和H2之間單播包了。廣播包仍然會被上傳到慢速路徑被標(biāo)準(zhǔn)Linux網(wǎng)橋處理。
我們看trace日志:
root@zhaoya-VirtualBox:~# cat /sys/kernel/debug/tracing/trace_pipe
...
雖然主機(jī)B的網(wǎng)卡上沒有抓到包,但如何確保數(shù)據(jù)包真的就是從XDP的eBPF字節(jié)碼轉(zhuǎn)發(fā)走的而不是直接飛過去的呢?
很好的問題,這作為下一個(gè)練習(xí)不是更好嗎?嗯,你應(yīng)該試試加一個(gè)統(tǒng)計(jì)功能,而這個(gè)并不復(fù)雜。
-
Linux
+關(guān)注
關(guān)注
87文章
11232瀏覽量
208950 -
網(wǎng)橋
+關(guān)注
關(guān)注
0文章
129瀏覽量
16955 -
BPF
+關(guān)注
關(guān)注
0文章
24瀏覽量
3979
原文標(biāo)題:實(shí)現(xiàn)一個(gè)基于XDP_eBPF的學(xué)習(xí)型網(wǎng)橋
文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論