Linux eBPF和XDP高速处理数据包;使用EBPF编写XDP网络过滤器;高性能ACL

目录

eBPF和XDP以裸机速度处理数据包

通过网络堆栈的入口数据包流

XDP构造

在Go中编程XDP

结论

使用EBPF编写XDP网络过滤器

01.简介

02.什么是XDP

03.示例问题

04. XDP加载程序

05. XDP应用

06.放在一起

07.还有什么?

08.代码样本

eBPF技术实践:高性能ACL

iptables / nftables 性能瓶颈

O(N)匹配

协议栈丢包

XDP

eBPF

整体架构

匹配算法

规则预处理

举例说明:

交集

通配

类 O(1)匹配

举例说明:

总结

参考


 

关键字

  1. BPF(Berkeley Packet Filter)
  2. XDP(eXpress Data Path)
  3. ACL(Access control list)

相关阅读

  1. iptables详解(1):iptables概念https://rtoax.blog.csdn.net/article/details/108897178
  2. iptables详解(2):路由表https://rtoax.blog.csdn.net/article/details/108897396
  3. eBPF.io eBPF文档:扩展的数据包过滤器(BPF)https://rtoax.blog.csdn.net/article/details/108990364
  4. 介绍Calico eBPF数据平面:Linux内核网络、安全性和跟踪(Kubernetes、kube-proxy)https://rtoax.blog.csdn.net/article/details/108993500

 

eBPF和XDP以裸机速度处理数据包

https://dev.to/sematext/ebpf-and-xdp-for-processing-packets-at-bare-metal-speed-m5b


XDP或 Express Data Path的出现是由于Linux内核对高性能数据包处理的迫切需求。几种内核旁路技术(DPDK是最主要的一种)旨在通过将数据包处理移至用户空间来加速网络操作。

这意味着放弃内核用户空间边界之间的上下文切换,系统调用转换或IRQ请求所引起的开销。操作系统将对网络堆栈的控制权移交给用户空间进程,这些进程通过它们自己的驱动程序直接与NIC交互。

即使这种方法可以显着提高性能,但它也带来了一系列缺点,包括在用户空间中重新发明TCP / IP堆栈以及其他网络功能,或者放弃经过考验的内核功能,这些功能为程序提供了强大的资源抽象和安全性原语。

XDP的任务是在内核中实现可编程的数据包处理,同时仍保留网络堆栈的基本构建块。实际上,XDP代表了eBPF仪器功能的自然扩展。它采用围绕地图构建的编程模型,监督的辅助函数和沙盒字节码,这些码以安全的方式检查并加载到内核中。

XDP快速处理路径的关键点是,在数据包到达网络适配器接收(RX)队列之后,字节码就附加在了网络堆栈的最早位置。在网络堆栈的这一阶段,还没有建立内核数据包特征,这有助于在数据包处理路径中获得巨大的速度。

如果您错过了我以前有关eBPF要点的博客文章,建议您先阅读一下。为了突出XDP在网络堆栈中的位置,让我们看一看TCP数据包的寿命,因为它到达NIC直到到达用户空间中的目标套接字为止。请记住,这将是一个高级概述。我们只是简单介绍了这个复杂的野兽的表面,它是内核网络堆栈。

 

通过网络堆栈的入口数据包流


一旦网卡收到一帧(应用所有校验和和健康检查之后),它将使用DMA将数据包传输到相应的存储区。这意味着数据包直接从NIC队列复制到驱动程序映射的主内存区域。当环形缓冲区接收队列的阈值到来时,NIC会发出硬IRQ,CPU将处理分派给IRQ向量表中的例程以运行驱动程序的代码。

由于必须非常快地执行驱动程序执行路径,因此通过软IRQ(NET_RX_SOFTIRQ)将处理推迟到驱动程序IRQ上下文之外。鉴于在中断处理程序执行期间禁用了IRQ,内核更喜欢在IRQ上下文之外安排长时间运行的任务,以避免丢失在中断例程繁忙时可能发生的任何事件。设备的驱动程序启动 NAPI循环,每个cpu内核线程(ksoftirqd)使用来自环形缓冲区的数据包。NAPI循环的责任主要与触发要由softirq处理程序处理的软IRQ(NET_RX_SOFTIRQ)有关,该处理程序又将数据发送到网络堆栈。

网络设备驱动程序分配了一个新的套接字缓冲区(sk_buff),以容纳入站数据包流。套接字缓冲区代表用于在内核中抽象化数据包缓冲/操作的基本数据结构。它还支撑了网络堆栈中的所有高层。

套接字缓冲区的结构具有几个标识不同网络层的字段。从CPU队列使用缓冲区套接字后,内核将填充元数据,克隆sk_buff并将其推入上游,进入后续的网络层,以进行进一步处理。这是IP协议层在堆栈中注册的位置。IP层执行一些基本的完整性检查,并将数据包移交给netfilter挂钩。如果网络过滤器未丢弃该数据包,则IP层将检查高级协议,并将处理过程交给先前提取的协议的处理程序功能。

数据最终被复制到连接套接字的用户空间缓冲区。进程通过阻塞的系统调用(recv,读取)系列或通过某种轮询机制(epoll)主动接收数据。

在NIC将数据包数据复制到RX队列后立即触发XDP挂钩,这时我们可以有效地防止分配各种元数据结构,包括sk_buffers。如果我们考虑最简单的可能用例,例如高速网络或受DDoS攻击的节点中的数据包过滤,则传统的网络防火墙(iptables)解决方案将不可避免地淹没计算机,这是由于每个阶段引入的工作量所致。网络堆栈。

网络堆栈中的XDP挂钩

具体来说,在自己的softirq任务中调度但也要顺序评估的iptables规则将在IP协议层匹配,以决定是否将丢弃来自特定IP地址的数据包。相反,XDP将直接在从DMA支持的环形缓冲区获得的原始以太网帧上运行,因此丢弃逻辑可以过早发生,从而避免了内核的大量处理,从而导致网络堆栈延迟并最终导致资源匮乏。

 

XDP构造


您可能已经知道,eBPF字节码可以附加在各种策略点上,例如内核函数,套接字,跟踪点,cgroup层次结构或用户空间符号。因此,每种类型的eBPF程序都在特定的上下文中运行-在kprobes情况下,CPU寄存器的状态,用于套接字程序的套接字缓冲区等。用XDP的话来说,生成的eBPF字节码的主干是围绕XDP元数据上下文(xdp_md)建模的.XDP上下文包含所有以原始格式访问数据包所需的数据。

为了更好地理解XDP程序的关键块,让我们剖析以下节:

#include <linux/bpf.h>
#define  SEC(NAME) __attribute__((section(NAME), used))
SEC("prog")

int xdp_drop(struct xdp_md *ctx) {
   return XDP_DROP;
}
char __license[] SEC("license") = "GPL";

这是最小的XDP程序,一旦附加到网络接口,它就会丢弃每个数据包。我们首先导入bpf标头,该标头引入各种结构的定义,包括xdp_md结构。接下来,我们声明SEC宏,以将映射,函数,许可证元数据和其他元素放置在eBPF加载程序自检的ELF部分中。

现在是我们XDP程序中最相关的部分,它处理数据包的处理逻辑。XDP附带了一组预定义的判决,这些判决确定内核如何转移数据包流。例如,我们可以将数据包传递到常规网络堆栈,将其丢弃,将数据包重定向到另一个NIC等。在我们的情况下,XDP_DROP产生超快速的数据包丢弃。还要注意,我们已经将prog节固定在eBPF加载程序期望遇到的函数中(如果找到不同的节名称,则程序将无法加载,但是我们可以指示ip使用非标准的节名称)。让我们编译上面的程序并尝试一下。

$ clang -Wall -target bpf -c xdp-drop.c -o xdp-drop.o

可以使用不同的用户空间工具(iproute2套件的一部分)将二进制对象加载到内核中,其中tc或ip的使用最为广泛。XDP支持veth(虚拟以太网)接口,因此,即时查看我们的程序的实际方法是将其卸载到现有的容器接口上。在接口上附加XDP程序之前和之后,我们将旋转一个nginx容器并启动几次curl。第一次尝试卷曲nginx根上下文会导致成功的HTTP状态代码:

$ curl --write-out '%{http_code}' -s --output /dev/null 172.17.0.4:80

200

可以使用以下命令完成XDP字节码的加载:

$ sudo ip link set dev veth74062a2 xdp obj xdp-drop.o

我们应该看到在veth界面中激活了xdp标志:

veth74062a2@if16: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 xdp/id:37 qdisc noqueue master docker0 state UP group default  

  link/ether 0a:5e:36:21:9e:63 brd ff:ff:ff:ff:ff:ff link-netnsid 2

  inet6 fe80::85e:36ff:fe21:9e63/64 scope link  

     valid_lft forever preferred_lft forever

随后的curl请求将挂起一段时间,然后失败,并显示如下错误消息,该消息有效地确认XDP挂钩正在按预期工作:

curl: (7) Failed to connect to 172.17.0.4 port 80: No route to host

当我们完成实验后,可以通过以下方式卸载XDP程序:

$ sudo ip link set dev veth74062a2 xdp off

 

在Go中编程XDP


先前的代码片段展示了一些基本概念,但是为了利用XDP的超强功能,我们将使用Go语言制作一款稍微复杂一些的软件-一种围绕某种规范用例构建的小工具:丢弃特定黑名单中的数据包IP地址。完整的源代码以及有关如何构建该工具的说明可在此处的存储库中找到。与上一篇博客文章一样,我们利用gobpf软件包,提供与eBPF VM交互的基础(将程序加载到内核,访问/操纵eBPF映射等)。许多eBPF程序类型可以直接用C编写并编译为ELF目标文件。不幸的是,基于XDP ELF的程序尚未涵盖。或者,仍然可以通过BCC模块附加XDP程序,但要以处理libbcc依赖项为代价。

但是,BCC映射中有一个重要限制,可防止将它们固定在bpff上(实际上,您可以从用户空间固定映射,但是在BCC模块的引导期间,它很乐意忽略任何固定的对象)。我们的工具需要检查黑名单映射,但是还具有在将XDP程序附加到网络接口上并退出主进程之后能够从中添加/删除元素的能力。

这足以考虑在ELF对象中支持XDP程序,因此我们提交了pull请求,希望将其合并到上游仓库中。我们认为,这是支持XDP程序可移植性的宝贵补充,类似于内核探针如何在计算机之间分布,即使它们不附带clang,LLVM和其他依赖项也是如此。

事不宜迟,让我们通过XDP代码开始浏览最重要的代码段

SEC("xdp/xdp_ip_filter")

int xdp_ip_filter(struct xdp_md *ctx) {

    void *end = (void *)(long)ctx->data_end;
    void *data = (void *)(long)ctx->data;

    u32 ip_src;
    u64 offset;
    u16 eth_type;

    struct ethhdr *eth = data;

    offset = sizeof(*eth);

    if (data + offset > end) {
        return XDP_ABORTED;
    }

    eth_type = eth->h_proto;

    /* handle VLAN tagged packet */
    if (eth_type == htons(ETH_P_8021Q) || eth_type == htons(ETH_P_8021AD)) {

        struct vlan_hdr *vlan_hdr;
        vlan_hdr = (void *)eth + offset;
        offset += sizeof(*vlan_hdr);

        if ((void *)eth + offset > end)
            return false;

        eth_type = vlan_hdr->h_vlan_encapsulated_proto;

    }

    /* let's only handle IPv4 addresses */
    if (eth_type == ntohs(ETH_P_IPV6)) {
        return XDP_PASS;
    }

    struct iphdr *iph = data + offset;
    offset += sizeof(struct iphdr);

    /* make sure the bytes you want to read are within the packet's range before reading them */
    if (iph + 1 > end) {
        return XDP_ABORTED;
    }

    ip_src = iph->saddr;

    if (bpf_map_lookup_elem(&blacklist, &ip_src)) {
        return XDP_DROP;
    }

    return XDP_PASS;
}

看起来有些吓人,但例如让我们忽略负责处理VLAN标记数据包的代码块。我们首先从XDP元数据上下文访问数据包数据,然后将指针转换为ethddr内核结构。您可能还会注意到检查包中字节边界的几种情况。如果您省略它们,验证程序将拒绝加载XDP字节码。如果代码引用了无效的指针或违反了安全策略,这将强制执行确保运行XDP程序而不会在内核中引起混乱的规则。该代码的其余部分从IP标头中提取源IP地址,并检查其在黑名单图中的存在。如果查找成功,则丢弃数据包。

结构负责附接/在网络堆栈分离XDP程序。它实例化并从目标文件中加载XDP模块,并调用AttachXDP或RemoveXDP方法。

黑名单的IP地址是通过标准eBPF地图管理。我们分别调用UpdateElement和DeleteElement来注册或删除条目。黑名单管理器还包含一种在地图中列出可用IP地址的方法。

其余代码将所有部分粘合在一起,以提供良好的CLI体验,用户可以利用该体验执行XDP程序附加/删除和IP黑名单的操作。有关更多详细信息,请前往来源

 

结论


XDP逐渐成为Linux内核中用于快速数据包处理的标准。在整个博客文章中,我已经解释了构成数据包处理生态系统的基本构建块。尽管网络堆栈是一个复杂的主题,但是由于eBPF / XDP的可编程特性,因此创建XDP程序相对容易。

 

 

使用EBPF编写XDP网络过滤器

https://duo.com/labs/tech-notes/writing-an-xdp-network-filter-with-ebpf


01.简介

在Kubecon 2019上,有许多精彩的演讲都提到eBPF是一种非常强大的工具,用于监视,创建审计跟踪甚至是高性能网络。Cilium的一篇演讲描述了eBPF和XDP如何从iptables中释放Kubernetes确实很有趣,因为我们当时正在研究KubernetesIstio。仅仅是因为它听起来像是一种非常整洁的技术,所以我们花了一些时间来提高对eBPF的工作原理和使用范围的了解。

在本文中,我不会像XDP那样专注于通用eBPF。与可以监视系统的eBPF的大多数使用相比,使用XDP,实际上您可以在数据包进入系统的某些最早时刻修改原始网络流量,甚至在内核没有机会处理它们之前。支持“卸载” XDP的NIC甚至可以运行XDP应用程序并在NIC硬件本身上处理数据包,而CPU甚至看不到它!太酷了!

 

02.什么是XDP


XDP代表eXpress数据路径,并在Linux内核中提供了高性能的数据路径,用于在网络数据包到达NIC时对其进行处理。本质上,您可以将XDP程序附加到网络接口,然后每次在该接口上看到新的数据包时,这些程序就会获得回调。而已。真的很简单。

将XDP程序附加到接口时,可以用以下三种模式之一附加它:

  1. 本机XDP-XDP程序由NIC驱动程序加载到其早期接收路径中。这需要NIC驱动程序的支持。
  2. 卸载的XDP-XDP程序被加载到NIC本身,完全在CPU之外执行。这需要NIC设备本身的支持。
  3. 通用XDP-XDP程序作为常规网络路径的一部分加载到内核中。此模式不能带来与本机或分载XDP相同的性能优势,但是从4.12开始,该模式通常在内核上运行,非常适合测试XDP程序或在通用硬件上运行它们。

将数据包移交给XDP程序后,您可以对它进行任何操作,包括在适当位置进行修改。完成后,您的返回值会向XDP数据包处理器指示下一步如何处理数据包。

  1. XDP_DROP指示应该丢弃该数据包,并且不对其进行进一步处理。这对于早期丢弃DOS攻击数据包很有用,用户空间应用程序可以分析流量模式并实时更新XDP应用程序有关应使用哪些过滤器以尽快丢弃数据包的信息。
  2. XDP_PASS指示应将数据包向上传递到常规网络堆栈以进行进一步处理。可以在此之前修改数据包,或将其保留。
  3. XDP_TXXDP_REDIRECT的相似之处在于,它们都告诉数据包处理器立即重新传输数据包。XDP_TX告诉它直接将(可能已修改的)数据包转发回其进入的同一网络接口。XDP_REDIRECT告诉它通过另一个NIC或可能通过BPF cpumap将其转发到用户空间进程,从而绕过正常的网络堆栈。
  4. XDP_ABORTED是用于错误的,永远不要显式使用。
 

03.示例问题


从小处开始总是好的。对于这个实验,我想从一个可以改变某些东西的程序开始,但是它会很小并且易于测试和可视化。为此,我选择了一个简单的问题,即将UDP数据包上的dest端口从7999更改为7998

这很容易可视化并且易于测试。打开三个终端,并在其中两个中运行以下两个命令:

nc -kul 127.0.0.1 7999
nc -kul 127.0.0.1 7998

PS

NAME
       ncat - Concatenate and redirect sockets

SYNOPSIS
       ncat [OPTIONS...] [hostname] [port]

DESCRIPTION
       Ncat is a feature-packed networking utility which reads and writes data across networks from the command
       line. Ncat was written for the Nmap Project and is the culmination of the currently splintered family of
       Netcat incarnations. It is designed to be a reliable back-end tool to instantly provide network
       connectivity to other applications and users. Ncat will not only work with IPv4 and IPv6 but provides the
       user with a virtually limitless number of potential uses.

       Among Ncat's vast number of features there is the ability to chain Ncats together; redirection of TCP,
       UDP, and SCTP ports to other sites; SSL support; and proxy connections via SOCKS4 or HTTP proxies (with
       optional proxy authentication as well). Some general principles apply to most applications and thus give
       you the capability of instantly adding networking support to software that would normally never support
       it.

这些终端是我们的收听过程。我们使用ncnetcat来开辟一个插座istening到ü DP那进来的端口7999和7998.在127.0.0.1地址的报文-k的说法只是告诉它已经收到一个数据包后netcat来继续听,它可以接收更多的数据包来自其他客户。

在我们的第三个终端中,运行:

nc -u 127.0.0.1 7999

然后在下一行上键入一些文本,然后按<Enter>键。您应该看到在第一个终端上侦听的文本,侦听端口7999。将我们的XDP应用程序放置在适当位置并连接到lo回送设备后,该数据包将在途中被修改并转移到侦听端口7998的另一个终端。

 

04. XDP加载程序


执行我们的XDP程序(尚未显示)的第一步是编写一个加载器,将其加载到数据路径中。为此,我们将使用令人惊叹的密件抄送工具包,使一切变得简单。

% main.py

#!/usr/bin/env python3

from bcc import BPF

device = "lo"                            % (1)
b = BPF(src_file="filter.c")             % (2)
fn = b.load_func("udpfilter", BPF.XDP)   % (3)
b.attach_xdp(device, fn, 0)              % (4)

我们的加载器首先导入bcc库,尤其是BPF加载器。

  • 在(1)中,我们指定要连接的网络接口。我们选择回送设备,因为我们计划修改将遍历该设备的数据包。
  • 在(2)中,我们基于源文件filter.c(稍后介绍)创建BPF程序。我相信这会调用BPF编译器和验证程序,以确保BPF程序有效且可以安全运行。
  • 在(3)中,我们从BPF程序中指定要用作回调处理传入数据包的函数,并将其指定为XDP程序类型。
  • 在(4)中,我们将XDP函数附加到在(1)中指定的设备上,并为标志传递0。如果我们想指定本机或卸载的XDP,则可以使用flags参数来完成。

将我们的XDP功能附加到网络接口后,它将开始处理数据包。但是,我们仅需添加一些额外的行即可包装装入程序应用程序。

% main.py

try:
  b.trace_print()                        % (5)
except KeyboardInterrupt:
  pass

b.remove_xdp(device, 0)                  % (6)
  • 在(5)中,我们将加载程序置于等待循环中,该循环监视来自BPF应用程序的所有打印消息并将其打印到屏幕上。这将无限期运行,因此我们将其包装在try / except块中以捕获Ctrl-C并允许程序继续进行。
  • 在(6)中,在用户指示程序应该退出的情况下,我们从网络接口中删除了XDP应用程序。
 

05. XDP应用


现在我们有了一个可以将我们的XDP程序安装到位的加载器,我们需要一个XDP程序来实际解析和修改我们想要的数据包。

// filter.c

#define KBUILD_MODNAME "filter"
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/in.h>
#include <linux/udp.h>

我们将从所需的所有库开始。在这种情况下,我们需要主bpf.h库以及可用来解析以太网,IPv4和UDP标头结构的库,因为XDP在内核有机会为我们做任何事情之前就开始运行。

// filter.c

int udpfilter(struct xdp_md *ctx) {

在这里,我们首先定义一个称为的函数udpfilter,该函数将在每次新包进入网络接口时被调用。请注意,它udpfilter与我们在加载程序中的(3)中指定的功能匹配。该数据包包含在xdp_md传递给我们的函数的结构中。

// filter.c

  bpf_trace_printk("got a packet\n");                   // (7)
  void *data = (void *)(long)ctx->data;                 // (8)
  void *data_end = (void *)(long)ctx->data_end;         // (8)
  struct ethhdr *eth = data;                            // (9)
  • 在(7)中,我们首先非常简单地确认一切正常。每当我们收到一个数据包时,无论其内容如何,​​我们都希望打印收到的数据包。来自的消息bpf_trace_printk()将传递到b.trace_print()我们的加载程序中。
  • 在(8)中,我们从结构中拉出指向数据包开始和结束的指针xdp_md。至此,我们所拥有的只是字节。
  • 在(9)中,我们创建一个以太网头结构指针,并将其指向数据包数据的开头。这使我们可以使用以太网报头结构中提供的偏移量来引用数据包字段。这是底层网络代码中使用的一种常见技术。
// filter.c

  if ((void*)eth + sizeof(*eth) <= data_end) {

但是,在对以太网标头执行任何操作之前,我们需要验证实际上是否存在足够的数据来填充标头。

// filter.c

    struct iphdr *ip = data + sizeof(*eth);
    if ((void*)ip + sizeof(*ip) <= data_end) {

IPv4标头也是如此。

注意:在此示例中,我假设我们收到的数据包将是IPv4数据包,而不是IPv6数据包。如果任何IPv6数据包通过网络接口,则以后的代码将中断。由于这只是一个简单的示例,因此我们进行了简化的假设。

// filter.c

      if (ip->protocol == IPPROTO_UDP) {
        struct udphdr *udp = (void*)ip + sizeof(*ip);
        if ((void*)udp + sizeof(*udp) <= data_end) {
          if (udp->dest == htons(7999)) {               // (10)
            bpf_trace_printk("udp port 7999\n");
            udp->dest = htons(7998);                    // (11)
          }
        }
      }
    }
  }
  return XDP_PASS;                                      // (12)
}

在进行了额外的边界检查并将udphdr结构映射到数据之后,在(10)中,我们检查数据包的目标端口是否为7999。因为文字7999以主机字节顺序表示(little-endian,0x3f1f),而端口号为以网络字节顺序(big-endian,0x1f3f)表示,我们使用htons函数(“主机到网络短”)正确比较它们。

如果数据包的UDP目标端口实际上是7999,则在(11)中,我们将目标端口值修改为7998。请注意,由于udpstruct指针仍然指向原始data指针的偏移量,因此我们正在修改数据包本身的原始字节。 ,而不是副本。

在(12)中,无论是否修改了数据包,我们XDP_PASS都将其返回以将数据包传递到常规网络堆栈以进行进一步处理。

 

06.放在一起


现在,让我们回到原始示例。

nc -kul 127.0.0.1 7999
nc -kul 127.0.0.1 7998

在我们的第三个终端中,运行:

nc -u 127.0.0.1 7999

在发送任何数据之前,我们现在还运行main.py将新的XDP应用程序(filter.c)加载到回送接口上。

$ sudo ./main.py
b'     ksoftirqd/2-21    [002] ..s. 367485.247738: 0: got a packet'
b'     ksoftirqd/2-21    [002] ..s. 367485.247802: 0: got a packet'
b'           <...>-728756 [001] ..s1 367485.980134: 0: got a packet'
b'           <...>-728756 [001] ..s1 367485.980157: 0: udp port 7999'
b'           <...>-728756 [001] ..s1 367485.980200: 0: got a packet'

您应该在回送接口上看到每个新数据包的“获取数据包”消息。现在,如果您在第三终端nc实例中键入一些数据,则应该也看到“ udp port 7999”消息。

您还应该看到已收到消息,不是nc像以前一样在端口7999上侦听实例,而是nc现在在端口7998上侦听了实例。

 

07.还有什么?


这篇文章的确只是您使用eBPF和XDP可以做的事情的铺垫,但希望能使您大致了解主要组件如何协同工作以做出有关数据包筛选或数据包重写的潜在复杂决策。

本文未涉及的一件事是eBPF / XDP程序和用户空间加载器程序如何在使用map的程序操作期间相互通信。映射本质上是可以由eBPF程序和用户空间组件访问的共享内存位置,并且通常可用于来回共享数据。使用地图,用户空间组件可以访问广泛的库以提供丰富的查询和决策功能,可以确定eBPF程序应执行的操作,并可以实时配置eBPF程序来执行此操作。

如今,使用eBPF和XDP进行的一些最酷的工作来自Cloudflare和Cilium等公司。Cloudflare在其DDoS缓解策略中广泛使用XDP,他们在博客中详细介绍了这些内容。Cilium使用XDP为Kubernetes和Docker提供高性能的网络平台。天空真的是极限。eBPF和XDP使您可以在网络领域执行几乎所有您想做的事情。

 

08.代码样本

main.py

#!/usr/bin/env python3

from bcc import BPF
import time

device = "lo"
b = BPF(src_file="filter.c")
fn = b.load_func("udpfilter", BPF.XDP)
b.attach_xdp(device, fn, 0)

try:
  b.trace_print()
except KeyboardInterrupt:
  pass

b.remove_xdp(device, 0)

filter.c

#define KBUILD_MODNAME "filter"
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/in.h>
#include <linux/udp.h>

int udpfilter(struct xdp_md *ctx) {
  bpf_trace_printk("got a packet\n");
  void *data = (void *)(long)ctx->data;
  void *data_end = (void *)(long)ctx->data_end;
  struct ethhdr *eth = data;
  if ((void*)eth + sizeof(*eth) <= data_end) {
    struct iphdr *ip = data + sizeof(*eth);
    if ((void*)ip + sizeof(*ip) <= data_end) {
      if (ip->protocol == IPPROTO_UDP) {
        struct udphdr *udp = (void*)ip + sizeof(*ip);
        if ((void*)udp + sizeof(*udp) <= data_end) {
          if (udp->dest == ntohs(7999)) {
            bpf_trace_printk("udp port 7999\n");
            udp->dest = ntohs(7998);
          }
        }
      }
    }
  }
  return XDP_PASS;
}

 

eBPF技术实践:高性能ACL

https://mp.weixin.qq.com/s/ttQDXvpppoBpWOUsyxsyrA


本文是由字节跳动系统部 STE 团队出品的文章。对于 Linux 而言,iptables / nftables 是主流的网络 ACL(Access Control List)解决方案。近些年随着 eBPF 技术的快速发展,bpfilter 也被提上了日程,有望取代 iptables/nftables,成为下一代网络 ACL 的解决方案。本文追随 bpfilter 的脚步,利用 XDP+eBPF 技术解决 iptables / nftables 性能瓶颈,提供一种高性能网络 ACL 的技术解决方案。

 

iptables / nftables 性能瓶颈


  1. iptables详解(1):iptables概念https://rtoax.blog.csdn.net/article/details/108897178
  2. iptables详解(2):路由表https://rtoax.blog.csdn.net/article/details/108897396
  • filter表:负责过滤功能,防火墙;内核模块:iptables_filter
  • nat表:network address translation,网络地址转换功能;内核模块:iptable_nat
  • mangle表:拆解报文,做出修改,并重新封装 的功能;iptable_mangle
  • raw表:关闭nat表上启用的连接追踪机制;iptable_raw

由于 iptables 和 nftables 的技术相似,但 iptables 相较简单,所以我们用 iptables 举例分析:

O(N)匹配

iptables 的规则样式:

# iptables -A INPUT -m set --set black_list src -j DROP

# ......省略N条规则

# iptables -A INPUT -p tcp -m multiport --dports 53,80 -j ACCEPT

# ......省略N条规则

# iptables -A INPUT -p udp --dport 53 -j ACCEPT

如上所示,若匹配 DNS 的请求报文(目的端口 53 的 udp 报文),需要依次遍历所有规则,才能匹配中。其中,ipset/multiport 等 match 项,只能减少规则数量,无法改变 O(N)的匹配方式。

 

协议栈丢包


iptables 常常和 ipset 结合使用,设置一些 IP 地址黑名单,防御 DDOS(distributed denial-of-service)网络攻击。对于 DDOS 这样的网络攻击,更早地丢包,就能更好地缓解 CPU 的损耗。但是用 iptables 作为防 DDOS 攻击的手段,效果往往很差。是因为 iptables 基于 netfilter 框架实现,即便是攻击报文在 netfilter 框架 PREROUTING 的 hook 点(收包路径的最早 hook 点)丢弃,也已经走过了很多 Linux 网络协议栈的处理流程。网上有比较数据,利用 XDP 技术的丢包速率要比 iptables 高 4 倍左右:

  • 预置条件:单条 udp 流、单个 CPU 处理
  • CPU: i7-6700K CPU @ 4.00GHz
  • NIC: 50Gbit/s Mellanox-CX4
  • iptables 规则:iptables -t raw -I PREROUTING -m set --set black_list src -j DROP
  • iptables 丢包速率:4,748,646 pps
  • XDP:PERCPU_HASH 类型的 eBPF map,存储 IP 黑名单
  • XDP 丢包速率:16,939,941 pps

 

XDP


XDP(eXpress Data Path)是基于 eBPF 实现的高性能、可编程的数据平面技术。基本的软件架构如下图所示:

 

XDP 位于网卡驱动层,当数据包经过 DMA 存放到 ring buffer 之后,分配 skb 之前,即可被 XDP 处理。数据包经过 XDP 之后,会有 4 种去向:

  • XDP_DROP:丢包
  • XDP_PASS:上送协议栈
  • XDP_TX:从当前网卡发送出去
  • XDP_REDIRECT:从其他网卡发送出去

由于 XDP 位于整个 Linux 内核网络软件栈的底部,能够非常早地识别并丢弃攻击报文,具有很高的性能。这为我们改善 iptables/nftables 协议栈丢包的性能瓶颈,提供了非常棒的解决方案。

 

eBPF


BPF(Berkeley Packet Filter)是 Linux 内核提供的基于 BPF 字节码的动态注入技术(常应用于 tcpdump、raw socket 过滤等)。eBPF(extended Berkeley Packet Filter)是针对于 BPF 的扩展增强,丰富了 BPF 指令集,提供了 Map 的 KV 存储结构。我们可以利用 bpf()系统调用,初始化 eBPF 的 Program 和 Map,利用 netlink 消息或者 setsockopt()系统调用,将 eBPF 字节码注入到特定的内核处理流程中(如 XDP、socket filter 等)。如下图所示:

至此,我们高性能 ACL 的技术方向已经明确,即利用 XDP 技术在软件栈的最底层做报文的过滤。

 

整体架构


如下图所示,ACL 控制平面负责创建 eBPF 的 Program、Map,注入 XDP 处理流程中。其中 eBPF 的 Program 存放报文匹配、丢包等处理逻辑,eBPF 的 Map 存放 ACL 规则。

 

匹配算法


为了提升匹配效率,我们将所有的 ACL 规则做了预处理,将链式的规则拆分存储。规则匹配时,我们参考内核的 O(1)调度算法,在多个匹配的规则中,快速选取高优先级的规则。

 

规则预处理


我们以之前的 iptables 规则举例,看如何将其拆分存储。首先,将所有规则根据优先级编号。比如,例子中的规则分别编号为:1(0x1)、16(0x10)、256(0x100)。其次,将所有规则的匹配项归类拆分。比如,例子中的匹配项可以归类为:源地址、目的端口、协议。最后,将规则编号、具体匹配项分类存储到 eBPF 的 Map 中。

# ipset create black_list hash:net
# ipset add black_list 192.168.3.0/24

# iptables -A INPUT -m set --set black_list src -j DROP

# ...... 省略15条规则

# iptables -A INPUT -p tcp -m multiport --dports 53,80 -j ACCEPT

# ...... 省略240条规则

# iptables -A INPUT -p udp --dport 53 -j ACCEPT
  1. iptables详解(1):iptables概念https://rtoax.blog.csdn.net/article/details/108897178
  2. iptables详解(2):路由表https://rtoax.blog.csdn.net/article/details/108897396

 

举例说明:


  • 规则 1 只有源地址匹配项,我们用源地址 192.168.3.0/24 作为 key,规则编号 0x1 作为 value,存储到 src Map 中。
  • 规则 16 有目的端口、协议 2 个匹配项,我们依次将 53、80 作为 key,规则编号 0x10 作为 value,存储到 dport Map 中;将 tcp 协议号 6 作为 key,规则编号 0x10 作为 value,存储到 proto Map 中。
  • 规则 256 有目的端口、协议 2 个匹配项,我们将 53 作为 key,规则编号 0x100 作为 value,存储到 dport Map 中;将 udp 协议号 17 作为 key,规则编号 0x100 作为 value,存储到 proto Map 中。
  • 我们依次将规则 1、16、256 的规则编号作为 key,动作作为 value,存储到 action Map 中。

 

交集


需要注意的是,规则 16、256 均有目的端口为 53 的匹配项,我们应该将 16、256 的规则编号进行按位或操作,然后进行存储,即 0x10 | 0x100 = 0x110。

 

通配


另外,规则 1 的目的端口、协议均为通配项,我们应该将规则 1 的编号按位或追加到现有的匹配项中(图中下划线的 value 值:0x111、0x11 等)。同理,将规则 16、256 的规则编号按位或追加到现有的源地址匹配项中(图中下划线的 value 值:0x111、0x110)。至此,我们的规则预处理完成,将所有规则的匹配项、动作拆分存储到 6 个 eBPF Map 中,如上图所示。

 

类 O(1)匹配


报文匹配时,我们报文的 5 元组(源、目的地址,源、目的端口、协议)依次作为 key,分别查找对应的 eBPF Map,得到 5 个 value。我们将这 5 个 value 进行按位与操作,得到一个 bitmap。这个 bitmap 的每个 bit,就表示了对应的一条规则;被置位为 1 的 bit,表示对应的规则匹配成功。

 

举例说明:


  • 当用报文(192.168.4.1:10000 -> 192.168.4.100:53, udp)的 5 元组作为 key,查找 eBPF Map 后,得到的 value 分别为:src_value = 0x110、dst_value = NULL、sport_value = NULL、dport_value = 0x111、proto_value = 0x101。将非 NULL 的 value 进行按位与操作,得到 bitmap = 0x100(0x110 & 0x111 & 0x101)。由于 bitmap 只有一位被置位 1(只有一条规则匹配成功,即规则 256),利用该 bitmap 作为 key,查找 action Map,得到 value 为 ACCEPT。与 iptables 的规则匹配结果一致。
  • 同理,当报文(192.168.4.1:1000 -> 192.168.4.100:53, tcp)的 5 元组作为 key,查找 eBPF Map 后,处理的流程和上面的一致,最终匹配到规则 16。
  • 同样,当报文(192.168.3.1:1000 -> 192.168.4.100:53, udp)的 5 元组作为 key,查找 eBPF Map 后,处理的流程和上面的一致。不同的是,得到的 bitmap = 0x101,由于 bitmap 有两位被置位 1(规则 1、256),我们应该取优先级最高的规则编号作为 key,查找 action Map。这里借鉴了内核 O(1)调度算法的思想,做如下操作:
bitmap &= -bitmap

即可取到优先级最高的 bit,如 0x101 &= -(0x101)最终等于 0x1。我们用 0x1 作为 key,查找 action Map,得到 value 为 DROP。与 iptables 的规则匹配结果一致。

 

总结


本文基于 Linux 内核的 XDP 机制,提出了一种改善 iptables / nftables 性能的 ACL 方案。目前 eBPF 的技术在开源社区非常流行,特性非常丰富,我们可以利用这项技术做很多有意思的事情。感兴趣的朋友可以加入我们,一起讨论交流。

 

参考


 

字节跳动系统部 STE 团队


字节跳动系统部 STE 团队一直致力于操作系统内核与虚拟化、系统基础软件与基础库的构建和性能优化、超大规模数据中心的稳定性和可靠性建设、新硬件与软件的协同设计等基础技术领域的研发与工程化落地,具备全面的基础软件工程能力,为字节上层业务保驾护航。同时,团队积极关注社区技术动向,拥抱开源和标准。欢迎更多有志之士加入,如有意向可发送简历至  sysrecruitment@bytedance.com 

 

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 酷酷鲨 设计师:CSDN官方博客 返回首页