Линус Торвальдс сказал о eBPF коротко: "BPF оказался действительно полезным, и настоящая сила его в том, что он позволяет людям писать специализированный код, который не включается, пока его не попросят." Брендан Грегг, один из самых известных специалистов по производительности Linux, назвал eBPF суперспособностями для Linux. За этими словами стоит конкретная идея: есть способ запустить произвольный код в привилегированной зоне ядра, не написав ни одной строки модуля ядра, не перезагрузив систему и не рискуя получить kernel panic от ошибки в коде. Способ этот называется eBPF, и за последние десять лет он изменил то, как строятся системы мониторинга, безопасности и сетевой обработки в Linux.
От фильтрации пакетов к виртуальной машине внутри ядра
История eBPF начинается в 1992 году с классического Berkeley Packet Filter. Тогда инженеры столкнулись с задачей: как позволить tcpdump перехватывать только нужные пакеты, не копируя весь трафик из ядра в пространство пользователя? Решением стал простой интерпретатор фильтров прямо в ядре: пользователь передаёт программу-фильтр, ядро её исполняет и отдаёт только подходящие пакеты.
В 2014 году Алексей Стапоровский и другие разработчики ядра сделали принципиальный шаг: расширили эту идею до полноценной виртуальной машины. Новая реализация, получившая название eBPF, имела 11 64-битных регистров (r0-r10), 512 байт стека, набор инструкций, близких к архитектуре x86-64, и главное, набор хуков в самых разных местах ядра. Фильтрация пакетов стала лишь одним из десятков возможных применений.
Загруженные программы, прошедшие верификатор, компилируются JIT-компилятором для нативной производительности. Модель исполнения событийная и, за редкими исключениями, run-to-completion: программы прикрепляются к различным хуковым точкам в ядре и запускаются при срабатывании события.
Убедиться, что JIT включён на вашей системе, можно так:
cat /proc/sys/net/core/bpf_jit_enable
# 1 означает включён
Если отключён, включить:
sysctl -w net.core.bpf_jit_enable=1
Верификатор как привратник который не даёт коду сломать ядро
Главный вопрос, который возникает при идее "запустить пользовательский код в ядре", звучит очевидно: что мешает этому коду сломать ядро? Ответ на него и есть основа безопасности eBPF. Загружаемая программа проходит два шага перед прикреплением к хуку. Шаг верификации проверяет, что программа безопасна: процесс, загружающий её, имеет необходимые привилегии, программа не крашит и не вредит системе, программа всегда завершается.
Верификатор работает методом статического анализа. Он строит граф всех возможных путей исполнения программы и проверяет каждый из них. Программы без гарантированного завершения отклоняются автоматически: до Linux 5.3 циклы были полностью запрещены, начиная с 5.3 разрешены ограниченные циклы с доказуемым завершением.
Попытаться загрузить программу с бесконечным циклом и посмотреть, что скажет верификатор, можно с помощью инструмента bpftool:
# установить bpftool
apt install linux-tools-$(uname -r)
# просмотр загруженных программ
bpftool prog list
# просмотр байткода конкретной программы
bpftool prog dump xlated id 42
# disassembly в виде машинного кода после JIT
bpftool prog dump jited id 42
Память ядра с eBPF-программой защищена и доступна только для чтения. Если по какой-либо причине, будь то баг ядра или вредоносная манипуляция, происходит попытка изменить eBPF-программу, ядро упадёт вместо того, чтобы продолжить исполнение скомпрометированного кода. Против атак типа Spectre eBPF-программы маскируют обращения к памяти, перенаправляя их в контролируемые области; верификатор также анализирует пути, доступные только при спекулятивном исполнении.
eBPF Maps как мост между ядром и пространством пользователя
eBPF-программы исполняются в ядре и не имеют прямого доступа к переменным пользовательского пространства. Для обмена данными между ядром и пользовательским процессом, а также между несколькими eBPF-программами существует механизм maps. eBPF maps являются эффективными хранилищами ключ-значение для совместного использования данных и хранения состояния между ядром и пространством пользователя.
Типов maps много: BPF_MAP_TYPE_HASH, BPF_MAP_TYPE_ARRAY, BPF_MAP_TYPE_RINGBUF, BPF_MAP_TYPE_PERF_EVENT_ARRAY, BPF_MAP_TYPE_LRU_HASH и другие. Каждый тип оптимизирован под свою задачу. Ring buffer, появившийся в ядре 5.8, стал предпочтительным способом передачи событий из ядра в пространство пользователя, заменив perf event array.
Создание и работа с map из пространства пользователя:
# создать map вручную через bpftool
bpftool map create /sys/fs/bpf/my_map type hash \
key 4 value 8 entries 1024 name my_map
# записать значение
bpftool map update id 5 key 0x01 0x00 0x00 0x00 \
value 0x0a 0x00 0x00 0x00 0x00 0x00 0x00 0x00
# прочитать все записи
bpftool map dump id 5
Хуки и типы программ: где именно живёт ваш код в ядре
eBPF-программы прикрепляются к хукам. Хуков много, и они покрывают практически всё, что происходит в ядре. Основные типы:
kprobes и kretprobes позволяют перехватывать вход и выход из любой функции ядра. Если нужно узнать, когда ядро вызывает tcp_connect, достаточно прикрепить kprobe к этой функции. Никакого патчинга ядра:
# отследить все вызовы sys_execve через bpftrace
bpftrace -e 'kprobe:sys_execve { printf("execve: %s\n", str(arg0)); }'
Tracepoints являются стабильными точками наблюдения, встроенными в ядро. В отличие от kprobes они не привязаны к конкретным именам функций и остаются стабильными между версиями ядра:
# список всех доступных tracepoints
bpftrace -l 'tracepoint:*' | head -20
# подписаться на создание новых процессов
bpftrace -e 'tracepoint:sched:sched_process_exec {
printf("new process: %s pid=%d\n", str(args->filename), pid);
}'
XDP (eXpress Data Path) является точкой прикрепления на уровне сетевого драйвера, до того как пакет попадает в сетевой стек ядра. Это самая быстрая точка обработки пакетов, позволяющая выполнять миллионы пакетов в секунду на одном ядре процессора:
# загрузить XDP-программу на интерфейс
ip link set dev eth0 xdp obj xdp_drop_icmp.o sec xdp
# посмотреть статистику
ip link show dev eth0
TC (Traffic Control) hooks работают на уровне сетевого стека, после XDP, с полным доступом к метаданным пакета:
# загрузить BPF-программу в tc
tc qdisc add dev eth0 clsact
tc filter add dev eth0 ingress bpf direct-action obj tc_prog.o sec ingress
Написание eBPF-программы на C с libbpf и CO-RE
Современный способ разработки eBPF-программ строится на связке Clang, libbpf и концепции CO-RE. CO-RE, Compile Once Run Everywhere, позволяет компилировать eBPF-модули только один раз. BTF захватывает информацию о типах ядра и структурах данных, а CO-RE фиксирует, какие части BPF-программы требуют перезаписи, чтобы программа была совместима с любой версией ядра, поддерживающей BTF.
Простейший счётчик системных вызовов на C (файл syscall_counter.bpf.c):
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 256);
__type(key, __u32); // syscall number
__type(value, __u64); // count
} syscall_count SEC(".maps");
SEC("tracepoint/raw_syscalls/sys_enter")
int count_syscalls(struct trace_event_raw_sys_enter *ctx)
{
__u32 syscall_id = ctx->id;
__u64 *count = bpf_map_lookup_elem(&syscall_count, &syscall_id);
if (count) {
__sync_fetch_and_add(count, 1);
} else {
__u64 init = 1;
bpf_map_update_elem(&syscall_count, &syscall_id, &init, BPF_ANY);
}
return 0;
}
char LICENSE[] SEC("license") = "GPL";
Компиляция и загрузка:
# компилировать в eBPF объектный файл
clang -O2 -g -target bpf \
-D__TARGET_ARCH_x86 \
-I/usr/include/x86_64-linux-gnu \
-c syscall_counter.bpf.c -o syscall_counter.bpf.o
# проверить что верификатор принимает программу
bpftool prog load syscall_counter.bpf.o /sys/fs/bpf/syscall_counter \
type tracepoint
# прикрепить к tracepoint
bpftool prog attach pinned /sys/fs/bpf/syscall_counter \
tracepoint raw_syscalls sys_enter
Прочитать накопленную статистику из map в реальном времени:
bpftool map dump pinned /sys/fs/bpf/syscall_count
bpftrace и готовые инструменты для быстрого анализа
Писать полноценные C-программы с libbpf оправдано для production-инструментов. Для оперативной диагностики существует bpftrace: высокоуровневый язык, позволяющий писать eBPF-программы в одну строку или короткий скрипт. Синтаксис напоминает awk и DTrace.
Отслеживать задержку чтения с диска в реальном времени:
bpftrace -e 'kprobe:blk_account_io_start { @start[arg0] = nsecs; }
kprobe:blk_account_io_done
/@start[arg0]/
{
@usecs = hist((nsecs - @start[arg0]) / 1000);
delete(@start[arg0]);
}'
Найти, какие процессы открывают файлы с определённым суффиксом:
bpftrace -e 'tracepoint:syscalls:sys_enter_openat {
if (str(args->filename) contains ".log") {
printf("%s opened %s\n", comm, str(args->filename));
}
}'
Показать latency всех системных вызовов в виде гистограммы:
bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @start[tid] = nsecs; }
tracepoint:raw_syscalls:sys_exit
/@start[tid]/
{
@ns = hist(nsecs - @start[tid]);
delete(@start[tid]);
}'
Готовые инструменты из набора BCC (BPF Compiler Collection) закрывают большинство повседневных задач диагностики:
# топ системных вызовов по процессам
execsnoop # отслеживать exec() в реальном времени
opensnoop # отслеживать open() в реальном времени
tcptracer # TCP-соединения с PID и именем процесса
biolatency # гистограмма задержки блочных I/O
profile # CPU profiling с flamegraph
Где eBPF используется в production прямо сейчас
Brendan Gregg назвал eBPF "суперспособностями для Linux", а Linus Torvalds отметил, что "BPF действительно оказался полезным, и настоящая сила его в том, как он позволяет людям писать специализированный код".
Cilium, сетевой плагин для Kubernetes, реализует весь сетевой стек, включая балансировку нагрузки, сетевые политики и шифрование, исключительно на eBPF. Это позволяет обходить iptables, который при тысячах правил становится узким местом, и получать производительность, сравнимую с аппаратными решениями.
Cloudflare использует XDP-программы для поглощения DDoS-атак прямо на уровне сетевого драйвера, до того как пакеты попадут в сетевой стек. Скорость обработки измеряется в миллионах пакетов в секунду на ядро процессора без заметного влияния на остальной трафик.
Falco от Sysdig строит систему обнаружения аномалий, прослушивая системные вызовы через eBPF и сигнализируя при подозрительных паттернах: попытках записи в /etc/passwd, запуске шеллов из контейнеров, неожиданных сетевых соединениях.
Meta применяет eBPF для балансировки нагрузки на L4 в собственных дата-центрах. Katran, открытый инструмент Meta, работает поверх XDP и обеспечивает горизонтальное масштабирование без единой точки отказа.
Overhead часто называют менее 0.1% CPU для ряда приложений. Это делает eBPF пригодным для production-наблюдаемости, которую нельзя отключить в спокойное время и включить только при инцидентах. Код живёт в ядре постоянно, потребляет ничтожно мало, и при появлении события реагирует немедленно, без переключений контекста в пространство пользователя за каждым наблюдением.