Легкое руководство по XDP. Часть 2 (Раздел eBPF Map)

Обзор

В предыдущей статье мы написали программу, использующую программу XDP для DROPPING всех пакетов.

Теперь давайте напишем программу, использующую eBPF Map, чтобы подсчитать количество DROPPED пакетов.

На этот раз мы также поговорим о том, как быстро ознакомиться с XDP и eBPF Map.
Поэтому я опущу подробные объяснения по eBPF и описания видов Map, которые не используются.

Настройка окружения

Предполагается использование Ubuntu 22.04.
Рекомендуется попробовать реализовать это в виртуальной машине для управления сетевым интерфейсом с помощью XDP.

Пожалуйста, смотрите предыдущую статью для информации по настройке.

eBPF Map

eBPF Map — это структура данных для обмена данными между пространством ядра и пользовательским пространством. Благодаря этому
программа XDP может обновлять данные в реальном времени, а пользовательское пространство может их считывать.

На этот раз мы напишем программу, чтобы подсчитать количество DROPPED пакетов с помощью XDP.

Виды eBPF Map

Существует множество видов eBPF Map.
Подробности можно найти в Документации Linux Kernel по BPF Map.
На этот раз мы используем следующие виды.

  • BPF_MAP_TYPE_HASH
    • Это Map, представляющий хэш-таблицу.
    • Можете хранить пары ключ-значение.

На этот раз мы используем IP-адрес отправителя пакета в качестве ключа и количество DROPPED пакетов в качестве значения.

Создание eBPF Map

eBPF Map определяется в программе eBPF следующим образом.

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, так как значение будет количеством DROPPED пакетов.
  • max_entries : Указывает максимальное количество записей, которые можно хранить в Map.
    • Для BPF_MAP_TYPE_HASH добавление записи сверх этого количества приведет к ошибке.
  • icmp_count_map : Указывает имя Map.
  • SEC(".maps") : Указывает секцию для определения Map.

Операции над eBPF Map

eBPF Map можно управлять в программе eBPF следующим образом.

  • 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;

    // Получение указателя на заголовок Ethernet
    struct ethhdr *eth = data;
    if ((void *)eth + sizeof(*eth) > data_end)
        return XDP_PASS;

    // Проверка, содержит ли Ethernet-фрейм 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;

    // Поиск icmp_count_map по IP-адресу источника
    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);
    }  

    // DROPPING 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 из пользовательского пространства необходимо задать pin.
Это позволит получить доступ к Map из пользовательского пространства.

  • Получите id map BPF программы

    $ sudo bpftool map
    16: hash  name icmp_count_map  flags 0x0
            key 4B  value 8B  max_entries 1024  memlock 86144B
            btf_id 25
    
    • 16 — это id Map.
  • Установите pin для Map

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

Напишем программу для доступа к eBPF Map из пользовательского пространства

Теперь, когда мы сохранили данные в eBPF Map, давайте попробуем получить доступ к 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: %s\n", map_info.name);
    printf("Тип Map: %d\n", map_info.type);
    printf("Размер ключа Map: %d\n", map_info.key_size);
    printf("Размер значения Map: %d\n", map_info.value_size);
    printf("Максимальное количество записей Map: %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("Ключ: %s, Значение: %lu\n", ip_str, value);
        }
        key = next_key; // Подготовка к следующему циклу
    }

    return 0;
}
  • bpf_obj_get : Получите дескриптор файла для Map, которую вы зафиксировали.

    • Укажите pin для Map на /sys/fs/bpf/xdp/icmp_count_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: icmp_count_map
Тип Map: 1
Размер ключа: 4
Размер значения: 8
Максимальное количество записей: 1024
Ключ: 192.168.XX.10, Значение: 320
Ключ: 192.168.XX.11, Значение: 2307

Теперь вы можете увидеть IP-адреса источников и количество всех DROPPED ICMP пакетов.

Очистка

  • Unpin Map

    $ sudo rm -iv /sys/fs/bpf/xdp/icmp_count_map
    
  • Анлодируйте программу XDP

    $ sudo ip link set dev eth0 xdp off
    

Код для загрузки xdp и ссылки на map

Каждый раз загружать XDP с помощью IP-команды, а затем настраивать pin для доступа к map может быть неудобно.
Если у вас есть постоянно работающая программа, когда вы загружаете программу XDP, она может получить дескриптор файла для Map, что позволит вам получить доступ к Map без установки pin.

Кроме того, в момент завершения программы мы можем разгрести программу XDP, тем самым предотвращая забывание анлода или unpin.

#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, "Использование: %s <интерфейс>\n", argv[0]);
        return 1;
    }

    // Загрузка programma xdp
    obj = bpf_object__open_file("./xdp.o", NULL);
    if (libbpf_get_error(obj)) {
        perror("Не удалось открыть BPF объект");
        return 1;
    }

    if (bpf_object__load(obj)) {
        perror("Не удалось загрузить BPF объект");
        return 1;
    }

    // Получение программы
    prog = bpf_object__find_program_by_name(obj, "xdp_drop_icmp");
    if (libbpf_get_error(prog)) {
        perror("Не удалось найти BPF программу по имени");
        return 1;
    }

    // Получение Map
    map = bpf_object__find_map_by_name(obj, "icmp_count_map");
    if (!map) {
        perror("Не удалось найти BPF map по имени");
        return 1;
    }

    map_fd = bpf_map__fd(map);
    if (map_fd < 0) {
        perror("Не удалось получить дескриптор map");
        return 1;
    }

    // Присоединение программы XDP к eth0
    const char* if_name = argv[1];
    int ifindex = if_nametoindex(if_name);
    if (ifindex == 0) {
        perror("Не удалось получить индекс интерфейса");
        return 1;
    }

    link = bpf_program__attach_xdp(prog, ifindex);
    if (libbpf_get_error(link)) {
        perror("Не удалось прикрепить XDP программу");
        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("Не удалось получить информацию о map");
            return 1;
        }

        printf("Имя Map: %s\n", map_info.name);
        printf("Тип Map: %d\n", map_info.type);
        printf("Размер ключа Map: %d\n", map_info.key_size);
        printf("Размер значения Map: %d\n", map_info.value_size);
        printf("Максимальное количество записей Map: %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("Ключ: %s, Значение: %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: icmp_count_map
    Тип Map: 1
    Размер ключа: 4
    Размер значения: 8
    Максимальное количество записей: 1024
    Ключ: 192.168.XX.2, Значение: 4
    ==============================
    Имя Map: icmp_count_map
    Тип Map: 1
    Размер ключа: 4
    Размер значения: 8
    Максимальное количество записей: 1024
    Ключ: 192.168.XX.2, Значение: 5
    ^C
    

    Нажмите Ctrl+C для завершения.

При завершении программа XDP выгружается.

Заключение

Мы написали программу, чтобы подсчитать количество пакетов, DROPPED программой XDP.
Использование Map позволяет обмениваться данными между eBPF программами и пользовательскими программами, расширяя возможности применения XDP.

В заключение

На самом деле, с точки зрения производительности, лучше использовать не BPF_MAP_TYPE_HASH, а
BPF_MAP_TYPE_PERCPU_HASH, но в этот раз мы использовали BPF_MAP_TYPE_HASH для упрощения.