Обзор
В предыдущей статье мы написали программу, использующую программу 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
.
- Укажите 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: 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 для упрощения.