快速上手 超XDP入门 Part2 (eBPF Map篇)
服务器 网络 编程
Published: 2024-09-17

概要

在上一篇文章中,我们编写了一个使用XDP程序丢弃所有数据包的程序。

这次我们将编写一个使用eBPF Map来统计丢弃的数据包的程序。

本次依然是一个简单的XDP+eBPF Map的实用话题,因此将省略eBPF等的详细解释和未使用的Map类型的说明。

环境构建

以Ubuntu 22.04为基础。
为了控制NIC的通信,建议在虚拟机等环境中进行测试。

有关设置的详细信息,请参见上一篇文章。

eBPF Map

eBPF Map是用于在内核和用户空间之间共享数据的数据结构。这使得XDP程序可以实时更新数据,而用户空间也可以读取这些数据。

这次我们将编写一个用于统计通过XDP丢弃的数据包数量的程序。

eBPF Map的类型

eBPF Map有多种类型,详细信息请查看Linux Kernel Documentation BPF Map
本次我们将使用以下类型:

  • BPF_MAP_TYPE_HASH
    • 表示哈希表的Map。
    • 可以存储哈希键和值对。

我们将使用数据包的源IP地址作为键,将丢弃的数据包数量作为值存储。

eBPF Map的创建

在eBPF程序中,可以如下定义eBPF Map。

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __type(key, uint32_t);
    __type(value, uint64_t);
    __uint(max_entries, 1024);
} icmp_count_map SEC(".maps");
  • type : 指定Map的类型。
  • key : 指定键的类型。此次指定为uint32_t,因为我们将使用IPv4地址作为键。
  • value : 指定值的类型。此次指定为uint64_t,因为我们要存储丢弃的数据包数量。
  • max_entries : 指定Map中可以存储的最大条目数。
    • 对于BPF_MAP_TYPE_HASH,超过条目数的添加将导致错误。
  • icmp_count_map : 指定Map的名称。
  • SEC(".maps") : 指定定义Map的节。

eBPF Map的操作

可以通过eBPF程序如下操作eBPF Map。

  • bpf_map_lookup_elem : 从Map中获取与键对应的值。
  • bpf_map_update_elem : 向Map添加键和值对。
  • bpf_map_delete_elem : 从Map中删除与键对应的值。
  • bpf_map_get_next_key : 获取Map中的下一个键。

实际编写XDP程序

使用eBPF Map的程序

以下是使用eBPF Map的程序示例。

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/icmp.h>
#include <arpa/inet.h>

// eBPF Map的定义
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __type(key, uint32_t);
    __type(value, uint64_t);
    __uint(max_entries, 1024);
} icmp_count_map SEC(".maps");

SEC("xdp")
int xdp_drop_icmp(struct xdp_md *ctx) {
    void *data_end = (void *)(unsigned long)ctx->data_end;
    void *data = (void *)(unsigned long)ctx->data;

    // 获取以太网头的指针
    struct ethhdr *eth = data;
    if ((void *)eth + sizeof(*eth) > data_end)
        return XDP_PASS;

    // 检查以太网帧是否包含IP数据包
    if (eth->h_proto != htons(ETH_P_IP))
        return XDP_PASS;

    // 获取IP头的指针
    struct iphdr *ip = data + sizeof(*eth);
    if ((void *)ip + sizeof(*ip) > data_end)
        return XDP_PASS;

    // 检查协议是否为ICMP
    if (ip->protocol != IPPROTO_ICMP)
        return XDP_PASS;

    // 获取ICMP头的指针
    struct icmphdr *icmp = (void *)ip + sizeof(*ip);
    if ((void *)icmp + sizeof(*icmp) > data_end)
        return XDP_PASS;

    // 提取源IP地址
    uint32_t src_ip = ip->saddr;

    // 根据源IP地址查找icmp_count_map
    uint64_t *count = bpf_map_lookup_elem(&icmp_count_map, &src_ip);

    // 如果u64类型的指针不为NULL,则增加计数
    if (count) {
        __sync_fetch_and_add(count, 1);
    } else {
        // 如果为NULL,则添加到icmp_count_map
        bpf_map_update_elem(&icmp_count_map, &src_ip, &(uint64_t){1}, BPF_ANY);
    }  

    // 丢弃ICMP数据包
    return XDP_DROP;
}

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

编译和加载

编译xdp.c,并加载XDP程序。

$ clang -O2 -target bpf -g -c xdp.c -o xdp.o
$ sudo ip link set dev eth0 xdp obj xdp.o sec xdp

从用户空间引用Map

设置pin以便可以引用map

为了从用户空间引用eBPF Map,我们需要对Map进行pin。
这样做能够使用户空间引用Map成为可能。

  • 获取BPF程序的map id

    $ sudo bpftool map
    16: hash  name icmp_count_map  flags 0x0
            key 4B  value 8B  max_entries 1024  memlock 86144B
            btf_id 25
    
    • 使用16作为Map的id。
  • 设置Map的pin

    $ sudo bpftool map pin id 16 /sys/fs/bpf/xdp/icmp_count_map
    

编写引用eBPF Map的用户空间程序

现在我们可以从用户空间引用存储在eBPF Map中的数据。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <linux/bpf.h>
#include <linux/if_link.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>

int main(int argc, char **argv) {
    int map_fd = bpf_obj_get("/sys/fs/bpf/xdp/icmp_count_map");
    if (map_fd < 0) {
        perror("bpf_obj_get");
        return 1;
    }

    struct bpf_map_info map_info = {};
    uint32_t info_len = sizeof(map_info);
    if (bpf_obj_get_info_by_fd(map_fd, &map_info, &info_len)) {
        perror("bpf_obj_get_info_by_fd");
        return 1;
    }

    // 显示Map的信息
    printf("Map name: %s\n", map_info.name);
    printf("Map type: %d\n", map_info.type);
    printf("Map key size: %d\n", map_info.key_size);
    printf("Map value size: %d\n", map_info.value_size);
    printf("Map max_entries: %d\n", map_info.max_entries);

    // 获取所有条目
    uint32_t key, next_key;
    uint64_t value;
    key = next_key = 0;
    while (bpf_map_get_next_key(map_fd, &key, &next_key) == 0) {
        key = next_key; // 使用更新后的键
        if (bpf_map_lookup_elem(map_fd, &key, &value) == 0) {
            // 将IP地址转换为字符串
            char ip_str[INET_ADDRSTRLEN];
            inet_ntop(AF_INET, &key, ip_str, sizeof(ip_str));
            printf("Key: %s, Value: %lu\n", ip_str, value);
        }
        key = next_key; // 为下一次循环做好准备
    }

    return 0;
}
  • bpf_obj_get : 获取pin的Map的文件描述符。

    • 指定/sys/fs/bpf/xdp/icmp_count_map的pin Map。
  • bpf_obj_get_info_by_fd : 获取Map的信息。

    • 获取Map的名称、类型、键的大小、值的大小和最大条目数。
  • bpf_map_get_next_key : 获取Map中的下一个键。

    • key : 当前键
    • next_key : 下一个键
  • bpf_map_lookup_elem : 从Map中获取与键对应的值。

编译和运行

$ gcc -o map_info map_info.c -lbpf
$ sudo ./map_info
Map name: icmp_count_map
Map type: 1
Map key size: 4
Map value size: 8
Map max_entries: 1024
Key: 192.168.XX.10, Value: 320
Key: 192.168.XX.11, Value: 2307

这样,我们就可以查看XDP程序丢弃的ICMP数据包的源IP地址和计数。

清理工作

  • 取消pin的Map

    $ sudo rm -iv /sys/fs/bpf/xdp/icmp_count_map
    
  • 卸载XDP程序

    $ sudo ip link set dev eth0 xdp off
    

XDP的加载和引用Map的程序

每次通过ip命令加载XDP并设置pin以访问Map是很麻烦的,如果是常驻程序,当加载xdp程序时可以获取到Map的文件描述符,
这样可以在不设置pin的情况下访问Map。此外,在程序结束时卸载XDP程序,避免了忘记卸载或解除pin的情况。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <linux/bpf.h>
#include <linux/if_link.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>
#include <unistd.h>
#include <net/if.h>

int main(int argc, char **argv) {
    struct bpf_object *obj;
    struct bpf_map *map;
    struct bpf_program *prog;
    struct bpf_link *link = NULL;
    int map_fd, prog_fd;
    int err;

    if (argc != 2) {
        fprintf(stderr, "Usage: %s <interface>\n", argv[0]);
        return 1;
    }

    // 加载xdp程序
    obj = bpf_object__open_file("./xdp.o", NULL);
    if (libbpf_get_error(obj)) {
        perror("Failed to open BPF object");
        return 1;
    }

    if (bpf_object__load(obj)) {
        perror("Failed to load BPF object");
        return 1;
    }

    // 获取程序
    prog = bpf_object__find_program_by_name(obj, "xdp_drop_icmp");
    if (libbpf_get_error(prog)) {
        perror("Failed to find BPF program by name");
        return 1;
    }

    // 获取Map
    map = bpf_object__find_map_by_name(obj, "icmp_count_map");
    if (!map) {
        perror("Failed to find BPF map by name");
        return 1;
    }

    map_fd = bpf_map__fd(map);
    if (map_fd < 0) {
        perror("Failed to get map fd");
        return 1;
    }

    // 附加XDP程序到eth0
    const char* if_name = argv[1];
    int ifindex = if_nametoindex(if_name);
    if (ifindex == 0) {
        perror("Failed to get interface index");
        return 1;
    }

    link = bpf_program__attach_xdp(prog, ifindex);
    if (libbpf_get_error(link)) {
        perror("Failed to attach XDP program");
        return 1;
    }

    // 无限循环,每秒显示一次Map的状态
    while (1) {
        // 分隔线
        printf("==============================\n");
        // 显示Map的信息
        struct bpf_map_info map_info = {};
        uint32_t info_len = sizeof(map_info);
        if (bpf_obj_get_info_by_fd(map_fd, &map_info, &info_len)) {
            perror("Failed to get map info");
            return 1;
        }

        printf("Map name: %s\n", map_info.name);
        printf("Map type: %d\n", map_info.type);
        printf("Map key size: %d\n", map_info.key_size);
        printf("Map value size: %d\n", map_info.value_size);
        printf("Map max_entries: %d\n", map_info.max_entries);

        // 获取所有条目
        uint32_t key, next_key;
        uint64_t value;
        key = next_key = 0;
        while (bpf_map_get_next_key(map_fd, &key, &next_key) == 0) {
            key = next_key; // 使用更新后的键
            if (bpf_map_lookup_elem(map_fd, &key, &value) == 0) {
                // 将IP地址转换为字符串
                char ip_str[INET_ADDRSTRLEN];
                inet_ntop(AF_INET, &key, ip_str, sizeof(ip_str));
                printf("Key: %s, Value: %lu\n", ip_str, value);
            }
            key = next_key; // 为下一次循环做好准备
        }
        sleep(1);  // 以1秒的间隔循环
    }

    return 0;
}
  • bpf_object__open_file : 从文件中打开BPF对象。
    • 指定已构建的XDP对象文件。
  • bpf_object__load : 加载BPF对象。
    • 加载BPF对象后,就可以获取程序和Map。
  • bpf_object__find_program_by_name : 根据名称获取BPF程序。
    • 指定XDP程序的名称,通常是sec(“xdp”)指定后紧随的函数名。
  • bpf_object__find_map_by_name : 根据名称获取BPF Map。
    • 指定Map的名称。
  • bpf_map__fd : 获取Map的文件描述符。
  • bpf_program__attach_xdp : 将XDP程序附加到指定的接口。

编译和运行

  • 编译

    $ gcc -o map_monitor map_monitor.c -lbpf
    
  • 运行

    ./map_monitor eth0
    Map name: icmp_count_map
    Map type: 1
    Map key size: 4
    Map value size: 8
    Map max_entries: 1024
    Key: 192.168.XX.2, Value: 4
    ==============================
    Map name: icmp_count_map
    Map type: 1
    Map key size: 4
    Map value size: 8
    Map max_entries: 1024
    Key: 192.168.XX.2, Value: 5
    ^C
    

    使用Ctrl+C退出。

退出时,XDP程序会被卸载。

总结

我们编写了一个用于统计XDP程序丢弃的ICMP数据包数量的程序。
通过使用Map,可以在eBPF程序和用户空间程序之间交换数据,
从而扩展XDP的应用范围。

最后补充

实际上,从性能考虑,应使用BPF_MAP_TYPE_PERCPU_HASH,而不是BPF_MAP_TYPE_HASH,
但为了简化,本次使用了BPF_MAP_TYPE_HASH。