eBPF程序之间的协作-简单实现一个xdpdump

前不久,很多人问我有没有用过xdpdump,它是什么原理。

当然,当时我是没有用过的,也就没有多说,不过我答应大家一旦我了解了之后,肯定会第一时间给大家介绍。

最近在写一些测试小程序的时候,偶然间也有了XDP抓包的需求,也就顺便熟悉了一下xdpdump,最终,我自己写了一个简单的,主要是阐明它的原理。

当然了,经理没有看这篇文章的必要。

在XDP抓包不能使用tcpdump,因为tcpdump是基于PACKET套接字的,而PACKET套接字是运行在Linux内核协议栈的,XDP在内核协议栈之前,所以tcpdump够不到它,也就无法抓取它的数据包。因此,需要一个xdpdump。

xdpdump已经存在了,但是xdpdump并不是一个类似tcpdump的工具,它只是一个说法,并没有统一的实现,我Google了一下,发现xdpdump的实现有很多版本:

之所以会这样,在于XDP还没有实现一个统一类似处理PACKET套接字的 ptype_all框架 (至少在目前没有这种机制)。

这很容易理解,因为XDP本身就是网卡强相关的,不适合做generic操作。所以说,如果需要xdpdump这样的功能,除了掌握上面列的几家的现成工具外,最好的方式无外乎:

  • 自己动手写一个xdpdump。

现在让我们开始。

实现xdpdump之前,必须要解决的一个问题就是:

  • 如何让两个或多个独立的eBPF程序在XDP实现串联?

这是必须的,因为抓包只是一个旁路功能,它不能影响到既有的XDP上eBPF程序的运行,如果当前某网卡的XDP运行着一个eBPF程序,我们希望的是xdpdump和它一起工作,而不是替换它。

我们假设当前现有的eBPF程序是test_echo.c,如下所示:

#include <uapi/linux/bpf.h>
#include <linux/if_ether.h>
#include "bpf_helpers.h"

SEC("xdp_echo")
int xdp_echo_prog(struct xdp_md *ctx)
{
    void *data_end = (void *)(long)ctx->data_end;
    void *data = (void *)(long)ctx->data;
    int in_index = ctx->ingress_ifindex;
    char info_fmt[] = "echo to %d \n";

    if (data + sizeof(struct ethhdr) > data_end) {
        return XDP_DROP;
    }

    bpf_trace_printk(info_fmt, sizeof(info_fmt), in_index);
    return  bpf_redirect(in_index, 0);
}

char _license[] SEC("license") = "GPL";

非常简单的一个eBPF程序,它将一个数据包原路反射回去。

很显然,我们用tcpdump无法抓取到达对应网卡的数据包。我们现在的任务是实现一个xdpdump,它可以抓到到达对应网卡并发射回去的数据包。

不得不介绍一下eBPF的两类机制:

  • 尾调用机制。
    eBPF尾调用可以将控制权从一个eBPF程序转移到另一个eBPF程序,并且不再返回。
  • PIN map机制。
    一个PIN map可以在多个eBPF程序之间共享,它在sysfs中可见。

知道这些就够了。接下来我们就用尾调用和PIN map来将xdpdump的eBPF程序和原始test_echo这个eBPF程序串联起来,让它们一起工作。

首先,我们看xdpdump的eBPF程序,即test_dump.c,代码如下:

#include <uapi/linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/in.h>
#include <linux/ip.h>
#include <uapi/linux/tcp.h>
#include <uapi/linux/udp.h>
#include "bpf_helpers.h"
#include "bpf_endian.h"

struct bpf_elf_map {
    __u32 type;
    __u32 size_key;
    __u32 size_value;
    __u32 max_elem;
    __u32 flags;
    __u32 id;
    __u32 pinning;
};

#define PIN_GLOBAL_NS       2

// 保存下一个eBPF程序,即本文中的test_echo程序,提供给尾调用。
struct bpf_elf_map SEC("maps") next_prog_map = {
    .type = BPF_MAP_TYPE_PROG_ARRAY,
    .size_key = sizeof(u32),
    .size_value = sizeof(u32),
    .pinning        = PIN_GLOBAL_NS,
    .max_elem = 1,
};

// 保存抓取的数据包体,这仅仅用协议元组数据来模拟。
struct packet {
    unsigned int src;
    unsigned int dst;
    unsigned short l3proto;
    unsigned short l4proto;
    unsigned short sport;
    unsigned short dport;
};

// 保存抓取数据包事件信息
struct bpf_elf_map SEC("maps") event_map = {
    .type = BPF_MAP_TYPE_PERF_EVENT_ARRAY,
    .size_key = sizeof(u32),
    .size_value = sizeof(u32),
    .pinning        = PIN_GLOBAL_NS,
    .max_elem = 128,
};

SEC("xdp_dump")
int xdp_dump_prog(struct xdp_md *ctx)
{
    void *data_end = (void *)(long)ctx->data_end;
    void *data = (void *)(long)ctx->data;
    struct ethhdr *eth = data;
    struct packet p = {};

    if (data + sizeof(struct ethhdr) > data_end) {
        return XDP_DROP;
    }

    p.l3proto = bpf_htons(eth->h_proto);
    if (p.l3proto == ETH_P_IP) {
        struct iphdr *iph;

        iph = data + sizeof(struct ethhdr);
        if (iph + 1 > data_end)
            return XDP_DROP;

        p.src = iph->saddr;
        p.dst = iph->daddr;
        p.l4proto = iph->protocol;
        p.sport = p.dport = 0;
        if (iph->protocol == IPPROTO_TCP) {
            struct tcphdr *tcph;
            tcph = data + sizeof(struct ethhdr) + sizeof(struct iphdr);
            if (tcph + 1 > data_end)
                return XDP_DROP;

            p.sport = tcph->source;
            p.dport = tcph->dest;
        } else if (iph->protocol == IPPROTO_UDP) {
            struct udphdr *udph;
            udph = data + sizeof(struct ethhdr) + sizeof(struct iphdr);
            if (udph + 1 > data_end)
                return XDP_DROP;

            p.sport = udph->source;
            p.dport = udph->dest;
        }
        // 事件上报给xdpdump抓包进程
        bpf_perf_event_output(ctx, &event_map, BPF_F_CURRENT_CPU, &p, sizeof(p));
    }

    // 尾调用,调用正常的test_echo eBPF程序
    bpf_tail_call(ctx, &next_prog_map, 0);

    // 如果没有attach别的eBPF程序,则直接PASS
    return XDP_PASS;
}

char _license[] SEC("license") = "GPL";

依然是在源码树的samples/bpf目录下完成编译,得到两个.o文件备用:

-rw-r--r-- 1 root root  11864 12月 24 16:27 test_dump.o
-rw-r--r-- 1 root root   5648 12月 24 09:20 test_echo.o

OK,现在让我们加载test_dump.o到enp0s9网卡:

root@zhaoya-VirtualBox:~/bpf# ip -force link set dev enp0s9 xdp obj ./test_dump.o sec xdp_dump

此时,我们将在文件系统中看到两个PIN map:

root@zhaoya-VirtualBox:~/bpf# ll /sys/fs/bpf/xdp/globals/
total 0
drwx------ 2 root root 0 12月 24 15:25 ./
drwx------ 3 root root 0 12月 24 15:25 ../
-rw------- 1 root root 0 12月 24 15:25 event_map
-rw------- 1 root root 0 12月 24 15:25 next_prog_map
root@zhaoya-VirtualBox:~/bpf#

接下来要做的事情,任务很明确,即将test_echo.o这个eBPF程序,灌进next_prog_map的index=0的位置,这显然需要一个用户态程序来完成,即update_prog.c,代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <bpf/bpf.h>
#include <linux/bpf.h>
#include "bpf_util.h"

static int progmap_fd;

int main(int argc, char **argv)
{
    int idx = 0;
    int opt = 1;
    char *mapfile;
    struct bpf_object *obj;
    struct bpf_prog_load_attr prog_load_attr = {
        .prog_type   = BPF_PROG_TYPE_XDP,
    };
    int prog_fd;

    opt = atoi(argv[1]);
    mapfile = argv[2]; // 获取全局可见的PIN map文件位置
    prog_load_attr.file = argv[3];
    progmap_fd = bpf_obj_get(mapfile);
    if (opt == 0) {
        bpf_map_delete_elem(progmap_fd, &idx);
        return 0;
    }

    // 载入eBPF程序
    if (bpf_prog_load_xattr(&prog_load_attr, &obj, &prog_fd)) {
        return 1;
    }

    bpf_map_update_elem(progmap_fd, &idx, &prog_fd, 0);
    return 0;
}

我们将其编译成update_prog可执行程序,将test_echo.o灌入:

root@zhaoya-VirtualBox:~/bpf# ./update_prog 1 /sys/fs/bpf/xdp/globals/next_prog_map ./test_echo.o

原本能ping通enp0s9地址1.1.1.1,现在ping不通了,数据包被反射:

04:06:30.004904 IP 1.1.1.2 > 1.1.1.1: ICMP echo request, id 6242, seq 97, length 64
04:06:30.005273 IP 1.1.1.2 > 1.1.1.1: ICMP echo request, id 6242, seq 97, length 64
04:06:31.004684 IP 1.1.1.2 > 1.1.1.1: ICMP echo request, id 6242, seq 98, length 64
04:06:31.005394 IP 1.1.1.2 > 1.1.1.1: ICMP echo request, id 6242, seq 98, length 64

删除test_echo.o,重新可以ping通:

root@zhaoya-VirtualBox:~/bpf# ./update_prog 0 /sys/fs/bpf/xdp/globals/next_prog_map

可以在ping的机器上抓包确认:

04:09:47.234037 IP 1.1.1.1 > 1.1.1.2: ICMP echo reply, id 6242, seq 294, length 64
04:09:48.236846 IP 1.1.1.2 > 1.1.1.1: ICMP echo request, id 6242, seq 295, length 64
04:09:48.237430 IP 1.1.1.1 > 1.1.1.2: ICMP echo reply, id 6242, seq 295, length 64
04:09:49.238854 IP 1.1.1.2 > 1.1.1.1: ICMP echo request, id 6242, seq 296, length 64

这说明我们的串联两个eBPF程序成功了。

注意最初通过iproute2加载的test_dump.o这个eBPF程序,其中已经把数据包抓取并上报了,现在只需要最后一道工序,即实现用户态的xdpdump了。

这也不难,我们用perf event采集机制,xdpdump.c的代码如下:

#include <string.h>
#include <poll.h>
#include <perf-sys.h>
#include <linux/if_ether.h>
#include <arpa/inet.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <bpf/bpf.h>
#include <bpf/libbpf.h>

#define CPUS    4

struct packet {
    unsigned int src;
    unsigned int dst;
    unsigned short l3proto;
    unsigned short l4proto;
    unsigned short sport;
    unsigned short dport;
};

struct perf_event_data {
    struct perf_event_header header;
    unsigned long long ts;
    unsigned int size;
    struct packet p;
};

static enum bpf_perf_event_ret print_packet(struct perf_event_header *hdr, void *fn)
{
    struct perf_event_data *data = (struct perf_event_data *)hdr;
    struct packet p = data->p;
    unsigned long long ts = data->ts;
    char src[16], dst[16];
    char l3proto[8], l4proto[8];
    unsigned short sport = 0, dport = 0;

    // 直接打印数据包协议元数据,正常应该是利用libpcap接口来处理的。
    switch (p.l3proto) {
    case ETH_P_IP:
        strcpy(l3proto, "IP");
        inet_ntop(AF_INET, &p.src, src, 16);
        inet_ntop(AF_INET, &p.dst, dst, 16);
        break;
    default:
        sprintf(l3proto, "%04x", p.l3proto);
    }

    switch (p.l4proto) {
    case IPPROTO_TCP:
        strcpy(l4proto, "TCP");
        sport = ntohs(p.sport);
        dport = ntohs(p.dport);
        break;
    case IPPROTO_UDP:
        strcpy(l4proto, "UDP");
        sport = ntohs(p.sport);
        dport = ntohs(p.dport);
        break;
    case IPPROTO_ICMP:
        strcpy(l4proto, "ICMP");
        break;
    default:
        strcpy(l4proto, "Unknown");
    }

    printf("%lld.%06lld %s:%d > %s:%d > %s %s\n", ts/1000000000, (ts%1000000000)/1000, src, sport, dst, dport, l3proto, l4proto);
    return LIBBPF_PERF_EVENT_CONT;
}

int main(int argc, char **argv)
{
    static struct perf_event_mmap_page *buffer[CPUS];
    int eventmap_fd, he;
    int perf_fds[CPUS];
    void *tmp = NULL;
    unsigned long len = 0;
    int i;
    struct pollfd fds[CPUS];
    struct perf_event_attr attr = {
        .sample_type = PERF_SAMPLE_RAW | PERF_SAMPLE_TIME,
        .type        = PERF_TYPE_SOFTWARE,
        .config      = PERF_COUNT_SW_BPF_OUTPUT,
        .wakeup_events   = 1,
    };

    eventmap_fd = bpf_obj_get(argv[1]);

    for (i = 0; i < CPUS; i++) {
        he = sys_perf_event_open(&attr, -1, i, -1, 0);
        ioctl(he, PERF_EVENT_IOC_ENABLE, 0);
        buffer[i] = mmap(NULL, 8192, PROT_READ | PROT_WRITE, MAP_SHARED, he, 0);
        bpf_map_update_elem(eventmap_fd, &i, &he, BPF_ANY);
        perf_fds[i] = he;
    }

    for (i = 0; i < CPUS; i++) {
        fds[i].fd = perf_fds[i];
        fds[i].events = POLLIN;
    }

    while (1) {
        poll(fds, CPUS, 0);
        for (i = 0; i < CPUS; i++)
            bpf_perf_event_read_simple(buffer[i], 8192, 4096, &tmp, &len, print_packet, NULL);
    }
    return 0;
}

编译成xdpdump(同样在源码树的samples/bpf目录下)之后,我们看看效果:
在这里插入图片描述

OK,很像那么回事。

然而,缺失了很多东西,需要补充:

  • 和tcpdump的兼容性,即通过cBPF来设置filter,类似“tcp port 80 or icmp”这种。
  • 用libpcap接口保存以及解析pcap包。
  • 注入XDP的eBPF程序直接upload整个数据包的包体,而不仅仅是协议元数据。
  • 实现一个基于tail_call的eBPF程序协作框架。


再说下eBPF之好,eBPF可以实现一些内核空间才能的策略,且 不用再担心系统panic了。

此外,再说句题外话:

本文中我的代码均没有按照每行80字符的规矩,因为我觉得那是历史的遗毒。如今显示器的分辨率都这么高了,类似Linux社区这种还要以此为编码规范,我不能理解。其实,在很多方面,Linux内核社区这种都被过度无脑神话了,很多方面如果你仔细看,它就是垃圾!

  • 每行80字符规定
  • 邮件发送接收竟然如此麻烦

浙江温州皮鞋湿,下雨进水不会胖。

原文链接: https://blog.csdn.net/dog250/article/details/103686162

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    eBPF程序之间的协作-简单实现一个xdpdump

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/406149

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年4月26日 上午9:38
下一篇 2023年4月26日 上午9:39

相关推荐