0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

如何用eBPF实现一个学习型网桥

Linux阅码场 来源:未知 2019-11-28 16:56 次阅读

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处理开销。

可以将很多处理逻辑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# 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

-0 [003] ..s. 44274.198178: 0: Destination Address: e6f09f270008 Redirect to:[4] From:[5]

...

虽然主机B的网卡上没有抓到包,但如何确保数据包真的就是从XDP的eBPF字节码转发走的而不是直接飞过去的呢?

很好的问题,这作为下一个练习不是更好吗?嗯,你应该试试加一个统计功能,而这个并不复杂。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • Linux
    +关注

    关注

    87

    文章

    11219

    浏览量

    208872
  • 网桥
    +关注

    关注

    0

    文章

    129

    浏览量

    16951
  • BPF
    BPF
    +关注

    关注

    0

    文章

    24

    浏览量

    3975

原文标题:实现一个基于XDP_eBPF的学习型网桥

文章出处:【微信号:LinuxDev,微信公众号:Linux阅码场】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    基于LPC1114的学习型红外遥控器程序设计

    本设计就是采用LPC1114+OLED+红外接收头构成学习型红外遥控器,
    发表于 12-14 16:44 6010次阅读

    分享下学习型红外遥控资料

    分享下学习型红外遥控资料
    发表于 07-30 19:47

    学习型遥控器

    学习型遥控器
    发表于 08-16 16:59

    智能学习型红外遥控器设计

    智能学习型红外遥控器设计
    发表于 08-16 19:26

    用89C52 做一个学习型遥控器

    现在有单片机89C52 红外线发射二级管 三级管 按钮 电阻 电容 0038红外接收 晶振 想做一个 学习型遥控器 遥控电视 和空调 带串口的更好 需要给我电路图和元器件型号 还有单片机文件 不要粘贴复制的那种 网上找了很多 都不行 需要懂行的朋友帮助 非常感谢你们 急
    发表于 02-13 09:04

    学习型红外遥控器设计

    有没有前辈知道怎么写基于MSP430F149的学习型红外遥控器设计程序的?麻烦联系我。O(∩_∩)O谢谢984300719
    发表于 05-01 15:22

    如何增加学习型遥控器的学习距离?

    如图,学习型遥控器,那部分是学习天线,如何增大信号?
    发表于 08-16 09:48

    请问单片机解码433MHZ EV1527学习型编码ic 学习功能如何用程序实现

    目前我在做一个51单片机解码433Mhz模块的EV1527学习型编码IC,不知道如何学习对码?
    发表于 09-09 15:04

    基于AT89C52的学习型遥控器的设计

    本文介绍了种基于 AT89C52 的学习型遥控器,并对其工作原理及软、硬件的设计和实现方法进行了详细的阐述。关键词: AT89C52; 学习型遥控器; 红外遥控编码Abstract:
    发表于 08-14 08:58 208次下载

    基于NiosⅡ的红外学习型遥控器设计

      本文设计了种基于NiosⅡ的红外学习型遥控器,把载波频率测量、红外信号解调、脉宽测量、调制发送IP核集中到FPG
    发表于 12-15 10:39 2588次阅读

    自主学习型网络课件的研究与开发

    随着网络信息的高速发展,传统网络课件以被动知识展示为主要手段已经不能满足学习者自主学习的需求。自主学习型网络课件立足学习者自身知识储备,可以实现
    发表于 10-24 15:19 0次下载
    自主<b class='flag-5'>学习型</b>网络课件的研究与开发

    学习型红外线遥控设计与制作解析

    本文主要介绍了学习型红外线遥控设计与制作解析.
    发表于 06-26 08:00 92次下载

    关于R8C/Lx学习型遥控器设计的介绍

    R8C/Lx学习型遥控器参考设计
    的头像 发表于 07-23 01:04 3202次阅读

    红外学习型遥控器方案说明

    红外学习型遥控器可通过学习操作学习其它遥控器上的部分按键,实现遥控器可遥控两种设备,方便用户操作。
    的头像 发表于 10-18 11:31 4330次阅读

    学习型遥控器的设计与实现

    电子发烧友网站提供《学习型遥控器的设计与实现.doc》资料免费下载
    发表于 10-24 09:33 1次下载
    <b class='flag-5'>学习型</b>遥控器的设计与<b class='flag-5'>实现</b>