BPF(2) | Руководство программиста Linux | BPF(2) |
bpf - выполняет команду с расширенной картой BPF или программу
#include <linux/bpf.h> int bpf(int cmd, union bpf_attr *attr, unsigned int size);
Системный вызов bpf() выполняет набор операций, связанных с расширенными пакетными фильтрами Беркли (Berkeley Packet Filters). Расширенные BPF (или eBPF) подобны первоначальным («классическим») BPF (cBPF), которые используются для фильтрации сетевых пакетов. Перед загрузкой программы cBPF и eBPF анализируются ядром на предмет их безвредности для работающей системы.
Набор eBPF расширяет cBPF в разных направлениях, включая способность вызова фиксированного набора вспомогательных функций ядра (через расширенный код операции BPF_CALL, предоставляемый eBPF) и доступа к общим структурам данных, таким как карты eBPF.
Карты eBPF — это обобщённая структура данных, которая позволяет хранить данных различных типов. Типы данных, в общем случае, считаются двоичными объектами (binary blobs), поэтому пользователь просто указывает размер ключа и размер значения при создании карты. Другими словами, ключ/значение задаваемой карты могут иметь произвольную структуру.
Пользовательский процесс может создать несколько карт (с парами ключ/значение нераспознаваемых байт данных (opaque bytes of data)) и работать с ними через файловые дескрипторы. Несколько программ eBPF могут получать доступ к одним и тем же картам параллельно. Решение, что хранить в картах, полностью отдано пользовательскому процессу и программе eBPF.
Существует специальный карточный тип, называемый программным массивом (program array). В данном типе карты хранятся файловые дескрипторы, указывающие на другие программы eBPF. Когда выполняется поиск в карте программный поток в этом месте перенаправляется в начало другой программы eBPF и не возвращается в вызывающую программу. Уровень вложенности ограничен 32, поэтому бесконечные циклы невозможны. Во время выполнения программные файловые дескрипторы, хранящиеся в карте, не могут быть изменены, поэтому функциональность программы можно изменить только на основе специальных требований. Все программы, на которые есть ссылки из карты программного массива, должны заранее загружаться в ядро с помощью bpf(). Если поиск по карте завершился с ошибкой, то текущая программа продолжает выполняться. Подробней смотрите далее в описании BPF_MAP_TYPE_PROG_ARRAY.
Обычно, программы eBPF загружаются пользовательским процессом и выгружаются при его завершении. В некоторых случаях, например, tc-bpf(8), программа продолжает работать внутри ядра даже после того, как процесс загрузивший программу, закончил работать. В этом случае ссылку на программу eBPF после того, как файловый дескриптор был закрыт программой из пользовательского пространства, содержит подсистема tc. То есть, будет ли специальная программа продолжать работать внутри ядра, зависит от того, будет ли она присоединена к указанной подсистеме ядра после загрузки через bpf().
Программа eBPF представляет собой набор инструкций, безопасно выполняющаяся от начала и до конца. Ядерный механизм проверки статически определяет, что программа eBPF завершится и её безопасно запускать. Во время проверки ядро увеличивает счётчик ссылок для каждой карты, которая используется программой eBPF, поэтому присоединённые карты невозможно удалить пока не будет выгружена программа.
Программы eBPF могут быть присоединены к различным событиям. Эти события могут возникать при поступлении сетевых пакетов, это могут быть события трассировки, события распределения по сетевым очередям (для программ eBPF, присоединённых к классификатору tc(8)) и другие типы событий, которые могут быть добавлены в будущем. Новое событие активирует выполнение программы eBPF, которое может сохранить информацию о событии в картах eBPF. Кроме сохранения данных, программы eBPF могут вызывать фиксированный набор вспомогательных функций ядра.
Программа eBPF может быть присоединена к нескольким событиям, а различные программы eBPF могут иметь доступ к одной карте:
tracing tracing tracing packet packet packet event A event B event C on eth0 on eth1 on eth2 | | | | | ^ | | | | v | --> tracing <-- tracing socket tc ingress tc egress prog_1 prog_2 prog_3 classifier action | | | | prog_4 prog_5 |--- -----| |------| map_3 | | map_1 map_2 --| map_4 |--
В аргументе cmd указывается операция, которая будет выполнена системным вызовом bpf(). Для каждой операции в attr задаётся соответствующий аргумент, который является объединением типа bpf_attr (смотрите далее). В аргументе size указывается размер объединения, на который ссылается attr.
Значением cmd может быть одно из:
union bpf_attr { struct { /* используется в BPF_MAP_CREATE */ __u32 map_type; __u32 key_size; /* размер ключа в байтах */ __u32 value_size; /* размер значения в байтах */ __u32 max_entries; /* максимальное количество элементов в карте */ }; struct { /* используется в командах BPF_MAP_*_ELEM и BPF_MAP_GET_NEXT_KEY */ __u32 map_fd; __aligned_u64 key; union { __aligned_u64 value; __aligned_u64 next_key; }; __u64 flags; }; struct { /* используется в BPF_PROG_LOAD */ __u32 prog_type; __u32 insn_cnt; __aligned_u64 insns; /* 'const struct bpf_insn *' */ __aligned_u64 license; /* 'const char *' */ __u32 log_level; /* уровень детализации при проверке */ __u32 log_size; /* размер пользовательского буфера */ __aligned_u64 log_buf; /* буфер 'char *' выделяемый пользователем */ __u32 kern_version; /* проверяется, если prog_type=kprobe (начиная с Linux 4.1) */ }; } __attribute__((aligned(8)));
Карты представляют собой обобщённую структуру данных, которая позволяет хранить данных различных типов. Карты позволяют использовать данные нескольким ядерным программам eBPF одновременно, а также ядру и приложениям пользовательского пространства.
Каждый тип карты имеет следующие атрибуты:
Следующие обёрточные функции показывают как для доступа к картам можно использовать различные команды bpf(). Для указания вызываемой операции служит параметр cmd.
int bpf_create_map(enum bpf_map_type map_type, unsigned int key_size, unsigned int value_size, unsigned int max_entries) { union bpf_attr attr = { .map_type = map_type, .key_size = key_size, .value_size = value_size, .max_entries = max_entries }; return bpf(BPF_MAP_CREATE, &attr, sizeof(attr)); }
bpf_map_lookup_elem(map_fd, fp - 4)
bpf_map_lookup_elem(map_fd, void *key)
value = bpf_map_lookup_elem(...); *(u32 *) value = 1;
enum bpf_map_type { BPF_MAP_TYPE_UNSPEC, /* 0 зарезервирован для карты неправильного типа */ BPF_MAP_TYPE_HASH, BPF_MAP_TYPE_ARRAY, BPF_MAP_TYPE_PROG_ARRAY, BPF_MAP_TYPE_PERF_EVENT_ARRAY, BPF_MAP_TYPE_PERCPU_HASH, BPF_MAP_TYPE_PERCPU_ARRAY, BPF_MAP_TYPE_STACK_TRACE, BPF_MAP_TYPE_CGROUP_ARRAY, BPF_MAP_TYPE_LRU_HASH, BPF_MAP_TYPE_LRU_PERCPU_HASH, BPF_MAP_TYPE_LPM_TRIE, BPF_MAP_TYPE_ARRAY_OF_MAPS, BPF_MAP_TYPE_HASH_OF_MAPS, BPF_MAP_TYPE_DEVMAP, BPF_MAP_TYPE_SOCKMAP, BPF_MAP_TYPE_CPUMAP, };
int bpf_lookup_elem(int fd, const void *key, void *value) { union bpf_attr attr = { .map_fd = fd, .key = ptr_to_u64(key), .value = ptr_to_u64(value), }; return bpf(BPF_MAP_LOOKUP_ELEM, &attr, sizeof(attr)); }
int bpf_update_elem(int fd, const void *key, const void *value, uint64_t flags) { union bpf_attr attr = { .map_fd = fd, .key = ptr_to_u64(key), .value = ptr_to_u64(value), .flags = flags, }; return bpf(BPF_MAP_UPDATE_ELEM, &attr, sizeof(attr)); }
int bpf_delete_elem(int fd, const void *key) { union bpf_attr attr = { .map_fd = fd, .key = ptr_to_u64(key), }; return bpf(BPF_MAP_DELETE_ELEM, &attr, sizeof(attr)); }
int bpf_get_next_key(int fd, const void *key, void *next_key) { union bpf_attr attr = { .map_fd = fd, .key = ptr_to_u64(key), .next_key = ptr_to_u64(next_key), }; return bpf(BPF_MAP_GET_NEXT_KEY, &attr, sizeof(attr)); }
Поддерживаются следующие типы карт:
void bpf_tail_call(void *context, void *prog_map, unsigned int index);
Команда BPF_PROG_LOAD используется для загрузки программы eBPF в ядро. Возвращаемым значением является новый файловый дескриптор, связанный с этой программой eBPF.
char bpf_log_buf[LOG_BUF_SIZE]; int bpf_prog_load(enum bpf_prog_type type, const struct bpf_insn *insns, int insn_cnt, const char *license) { union bpf_attr attr = { .prog_type = type, .insns = ptr_to_u64(insns), .insn_cnt = insn_cnt, .license = ptr_to_u64(license), .log_buf = ptr_to_u64(bpf_log_buf), .log_size = LOG_BUF_SIZE, .log_level = 1, }; return bpf(BPF_PROG_LOAD, &attr, sizeof(attr)); }
Значением prog_type может быть один из типов программ:
enum bpf_prog_type { BPF_PROG_TYPE_UNSPEC, /* 0 используется как некорректное значение типа программы */ BPF_PROG_TYPE_SOCKET_FILTER, BPF_PROG_TYPE_KPROBE, BPF_PROG_TYPE_SCHED_CLS, BPF_PROG_TYPE_SCHED_ACT, };
Дополнительную информацию о типах программ eBPF смотрите далее.
Остальные поля bpf_attr заполняются следующим образом:
При применении close(2) к файловому дескриптору, полученному от BPF_PROG_LOAD, происходит выгрузка программы eBPF (но смотрите ЗАМЕЧАНИЯ).
Карты доступны из программ eBPF и используются для обмена данными между программами eBPF, а также между программами eBPF и приложениями пользовательского пространства. Например, программы eBPF могут обрабатывать различные события (kprobe, пакеты) и сохранять свои данные в карте, а затем программы пользовательского пространства могут выбирать данные из карты. И наоборот, программы пользовательского пространства могут использовать карту в качестве механизма настройки, заполняя карту значениями, читаемыми программой eBPF, которая затем, согласно этим значениям, изменяет своё поведение на лету.
Типом программы eBPF (prog_type) определяется поднабор вспомогательных функций ядра, который программа может вызывать. Тип программы также определяет входящие данные программы (контекст) — в виде формата struct bpf_context (который представляет собой двоичный объект данных, передаваемый в программу eBPF первым параметром).
Например, программа трассировки не имеет доступа к тому же поднабору вспомогательных функций, как у программы фильтрации сокетов (хотя они могут обращаться к некоторым одинаковым функциям). Также, входные данные (контекст) программы трассировки — это набор значений регистров, а у фильтра сокетов — сетевой пакет.
Набор функций, доступных программам eBPF определённого типа, может увеличиться в будущем.
Поддерживаются следующие типы программ:
bpf_map_lookup_elem(map_fd, void *key) /* поиск ключа в map_fd */ bpf_map_update_elem(map_fd, void *key, void *value) /* обновление ключа/значения */ bpf_map_delete_elem(map_fd, void *key) /* удаление ключа из map_fd */
После того как программа загружена, к ней можно присоединить событие. Различные подсистемы ядра делают это по-разному.
Начиная с Linux 3.19, следующий вызов присоединяет программу prog_fd к сокету sockfd, который был создан вызовом socket(2) ранее:
setsockopt(sockfd, SOL_SOCKET, SO_ATTACH_BPF, &prog_fd, sizeof(prog_fd));
Начиная с Linux 4.1, следующий вызов можно использовать для присоединения программы eBPF, на которую ссылается файловый дескриптор prog_fd, к файловому дескриптору событий perf event_fd, созданному вызовом perf_event_open(2) ранее:
ioctl(event_fd, PERF_EVENT_IOC_SET_BPF, prog_fd);
/* пример bpf+sockets: * 1. создать карту в виде массива из 256 элементов * 2. загрузить программу, подсчитывающую количество принятых пакетов * r0 = skb->data[ETH_HLEN + offsetof(struct iphdr, protocol)] * map[r0]++ * 3. присоединить prog_fd к неструктурированному сокету от setsockopt() * 4. напечатать количество пакетов TCP/UDP, принимаемых каждую секунду */ int main(int argc, char **argv) { int sock, map_fd, prog_fd, key; long long value = 0, tcp_cnt, udp_cnt; map_fd = bpf_create_map(BPF_MAP_TYPE_ARRAY, sizeof(key), sizeof(value), 256); if (map_fd < 0) { printf("ошибка при создании карты '%s'\n", strerror(errno)); /* вероятно, запущена без прав root */ return 1; } struct bpf_insn prog[] = { BPF_MOV64_REG(BPF_REG_6, BPF_REG_1), /* r6 = r1 */ BPF_LD_ABS(BPF_B, ETH_HLEN + offsetof(struct iphdr, protocol)), /* r0 = ip->proto */ BPF_STX_MEM(BPF_W, BPF_REG_10, BPF_REG_0, -4), /* *(u32 *)(fp - 4) = r0 */ BPF_MOV64_REG(BPF_REG_2, BPF_REG_10), /* r2 = fp */ BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4), /* r2 = r2 - 4 */ BPF_LD_MAP_FD(BPF_REG_1, map_fd), /* r1 = map_fd */ BPF_CALL_FUNC(BPF_FUNC_map_lookup_elem), /* r0 = map_lookup(r1, r2) */ BPF_JMP_IMM(BPF_JEQ, BPF_REG_0, 0, 2), /* if (r0 == 0) goto pc+2 */ BPF_MOV64_IMM(BPF_REG_1, 1), /* r1 = 1 */ BPF_XADD(BPF_DW, BPF_REG_0, BPF_REG_1, 0, 0), /* lock *(u64 *) r0 += r1 */ BPF_MOV64_IMM(BPF_REG_0, 0), /* r0 = 0 */ BPF_EXIT_INSN(), /* вернуть r0 */ }; prog_fd = bpf_prog_load(BPF_PROG_TYPE_SOCKET_FILTER, prog, sizeof(prog) / sizeof(prog[0]), "GPL"); sock = open_raw_sock("lo"); assert(setsockopt(sock, SOL_SOCKET, SO_ATTACH_BPF, &prog_fd, sizeof(prog_fd)) == 0); пакетов for (;;) { key = IPPROTO_TCP; assert(bpf_lookup_elem(map_fd, &key, &tcp_cnt) == 0); key = IPPROTO_UDP; assert(bpf_lookup_elem(map_fd, &key, &udp_cnt) == 0); printf("TCP %lld UDP %lld пакетов\n", tcp_cnt, udp_cnt); sleep(1); } return 0; }
Другой рабочий код можно найти в каталоге samples/bpf дерева исходного кода ядра.
При успешном выполнении возвращаемое значение зависит от используемой команды:
В случае ошибки возвращается -1 и значение errno устанавливается соответствующим образом.
Системный вызов bpf() впервые появился в Linux 3.18.
Системный вызов bpf() есть только в Linux.
В текущей реализации для всех команд bpf() требуется, чтобы у вызывающего был мандат CAP_SYS_ADMIN.
Объекты eBPF (карты и программы) могут использоваться несколькими процессами одновременно. Например, после fork(2) потомок наследует файловые дескрипторы, ссылающиеся на одинаковые объекты eBPF. Также, файловые дескрипторы, ссылающиеся на объекты eBPF, можно передавать через доменные сокеты UNIX. Файловые дескрипторы, ссылающиеся на объекты eBPF, можно дублировать обычным образом с помощью dup(2) и подобных вызовов. Объекты eBPF уничтожаются только после закрытия всех файловых дескрипторов, ссылающихся на объект.
Программы eBPF можно писать на специализированной версии языка C, которая компилируется (с помощью компилятора clang) в байт-код eBPF. В этой версии C отсутствуют различные свойства, например, глобальные переменные, функции с переменным числом аргументов, числа с плавающей запятой и нельзя передавать структуры в качестве аргументов. Примеры можно найти в файлах samples/bpf/*_kern.c из дерева исходного кода ядра.
В ядре имеется оперативный компилятор (JIT), который с целью производительности транслирует байт-код eBPF в машинный код. В ядрах Linux до версии 4.15 по умолчанию компилятор JIT отключён, но эта возможность контролируется записью следующих строк целых чисел в файл /proc/sys/net/core/bpf_jit_enable:
Начиная с Linux 4.15, ядро можно настраивать через параметр CONFIG_BPF_JIT_ALWAYS_ON. В этом случае компилятор JIT всегда включён и bpf_jit_enable устанавливается в 1 и это нельзя изменить (данный параметр ядра был добавлен для предотвращения одной из атак «Спектр», направленной на интерпретатор BPF).
В настоящее время компилятор JIT для eBPF доступен на следующих архитектурах:
seccomp(2), bpf-helpers(7), socket(7), tc(8), tc-bpf(8)
Классический и расширенный BPF описаны в файле исходного кода ядра Documentation/networking/filter.txt.
2019-03-06 | Linux |