Технологии eBPF и XDP позволяют внедрять пользовательскую логику обработки сетевых пакетов прямо в ядре Linux, до попадания в стандартный сетевой стек. Это дает прирост производительности в десятки раз по сравнению с классическими iptables, что критично для защиты от DDoS-атак и высоконагруженной балансировки. В этом руководстве вы получите готовые программы на C, проверенные команды для компиляции и загрузки в ядро, а также четкий протокол безопасного тестирования в изолированном окружении.
Материал основан на практическом опыте и актуален для ядер Linux 5.15 и новее. Мы разберем, как написать простейший фильтр для блокировки IP, реализовать балансировку нагрузки L4 и корректно интегрировать решение в существующую инфраструктуру.
Зачем нужны eBPF и XDP, когда есть iptables? Сравнение производительности
Архитектурное отличие XDP от классического netfilter/iptables фундаментально. Программа XDP выполняется на самом раннем этапе обработки пакета, сразу после его поступления от сетевого драйвера, но до выделения памяти под структуры данных ядра (sk_buff). Это позволяет отбрасывать нежелательный трафик с минимальными накладными расходами. В то время как iptables работает в контексте сетевого стека ядра, обрабатывая уже сформированные sk_buff, что требует больше вычислительных ресурсов.
Производительность измеряется в пакетах в секунду (pps) и загрузке CPU. В сценарии простого отбрасывания пакетов по правилу DROP разрыв особенно заметен под нагрузкой, имитирующей DDoS-атаку с случайными IP-адресами источника. Iptables вынужден линейно проходить цепочки правил, в то время как eBPF-программа использует эффективные хэш-таблицы (BPF maps) для поиска, что сокращает время обработки до минимума.
Цифры не врут: тест обработки мелких пакетов под DDoS-нагрузкой
Результаты открытых бенчмарков, например, от проекта Cilium и тестов на Phoronix, наглядно демонстрируют преимущество XDP. Сравнение проводилось на сервере с ядром 5.15 и Ubuntu 22.04 LTS, под нагрузкой в 10 миллионов мелких пакетов в секунду.
| Технология / Действие | Пакетов в секунду (pps), млн | Загрузка CPU (1 ядро), % | Задержка (latency), мкс |
|---|---|---|---|
| iptables (одно правило DROP) | ~0.8 | ~95 | ~1200 |
| Базовая программа XDP (DROP по IP) | ~4.2 | ~45 | < 100 |
| XDP с LRU-хэшем (10k правил) | ~3.8 | ~50 | < 150 |
XDP показывает производительность выше в 4-5 раз при вдвое меньшей загрузке процессора. При увеличении количества правил до десятков тысяч разрыв становится еще более существенным, так как поиск в хэш-таблице eBPF происходит за время O(1), а iptables требует линейного прохода. Для задач маршрутизации и балансировки это означает возможность обрабатывать трафик линии 10 Гбит/с и выше на одном ядре CPU.
Первая программа на XDP: блокировка атакующего IP-адреса за 5 минут
Начнем с практического примера, который решает конкретную задачу – блокировку трафика с определенного IP-адреса. Этот сценарий является основой для построения систем защиты от DDoS или изоляции нежелательных источников.
Для работы потребуется система с ядром Linux 5.4 или новее и установленные инструменты для разработки eBPF. На Ubuntu 22.04 выполните установку зависимостей:
sudo apt update
sudo apt install clang llvm libbpf-dev linux-headers-$(uname -r) iproute2 bpftool
Полный листинг кода и команды для немедленного запуска
Создайте файл xdp_drop_simple.c со следующим содержимем. Программа проверяет исходный IP-адрес пакета и сравнивает его с заданным значением.
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
SEC("xdp")
int xdp_drop_by_ip(struct xdp_md *ctx) {
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
// Указатель на начало Ethernet-фрейма
struct ethhdr *eth = data;
if (data + sizeof(*eth) > data_end)
return XDP_PASS;
// Проверяем, что это IP-пакет (не ARP и т.д.)
if (eth->h_proto != __constant_htons(ETH_P_IP))
return XDP_PASS;
// Указатель на IP-заголовок
struct iphdr *iph = data + sizeof(*eth);
if (data + sizeof(*eth) + sizeof(*iph) > data_end)
return XDP_PASS;
// IP-адрес для блокировки (например, 192.0.2.1)
__u32 blocked_ip = 0x010200C0; // 192.0.2.1 в сетевом порядке байт
// Сравниваем source IP с адресом в черном списке
if (iph->saddr == blocked_ip) {
// Отбрасываем пакет
return XDP_DROP;
}
// Все остальные пакеты пропускаем дальше по стеку
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
Скомпилируйте программу в BPF-байткод:
clang -O2 -target bpf -c xdp_drop_simple.c -o xdp_drop_simple.o
Загрузите программу в ядро, прикрепив к сетевому интерфейсу (замените eth0 на имя вашего интерфейса):
sudo ip link set dev eth0 xdp obj xdp_drop_simple.o sec xdp
Для проверки отправьте ping с другого хоста, используя IP-адрес 192.0.2.1 в качестве источника (например, с помощью hping3). Пакеты должны теряться.
Как проверить, что программа работает, и что делать если она не загрузилась
Убедитесь, что программа загружена на интерфейс:
ip link show dev eth0
В выводе должна быть строка prog/xdp. Для просмотра всех загруженных в систему eBPF-программ используйте bpftool:
sudo bpftool prog list
Вы увидите список программ с их ID, именем и временем загрузки. Для детальной информации о конкретной программе:
sudo bpftool prog show id [PROG_ID] --pretty
Если программа не загрузилась, наиболее вероятная причина – ошибка верификатора (verifier) eBPF. Проверьте вывод команды dmesg | tail -20. Типичные ошибки включают выход за границы памяти (out of bounds access) или использование неподдерживаемых инструкций. Убедитесь, что все проверки границ указателей (как в коде выше: if (data + sizeof(*eth) > data_end)) присутствуют. Для постоянной загрузки программы используйте флаг -w с ip link, который «прикрепит» (pin) программу в файловой системе BPF FS.
От простого к сложному: балансировка нагрузки L4 на eBPF-мапах
Следующий шаг – реализация полезного бизнес-сценария: балансировка TCP/UDP трафика между несколькими бэкенд-серверами. В XDP мы можем принимать решение о перенаправлении пакета на ранней стадии, изменяя IP-адрес получателя прямо в заголовке.
Архитектура решения предполагает наличие BPF-карты (map) типа LRU hash, которая хранит пул бэкендов. Для каждого нового соединения (на основе хэша от 5-tuple: src ip, src port, dst ip, dst port, protocol) программа выбирает бэкенд и сохраняет это соответствие в другой карте для обеспечения session affinity.
Реализация хэш-таблицы бэкендов и алгоритма выбора
Объявим карту для хранения пула бэкендов. В userspace-загрузчике мы заполним ее IP-адресами и портами.
// Определение в программе eBPF (xdp_lb.c)
struct backend_info {
__be32 ip;
__be16 port;
};
struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__uint(max_entries, 32);
__type(key, __u32); // индекс бэкенда
__type(value, struct backend_info);
} backends SEC(".maps");
// Карта для сохранения выбора бэкенда для потока (session affinity)
struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__uint(max_entries, 65536);
__type(key, __u64); // хэш от 5-tuple
__type(value, __u32); // индекс выбранного бэкенда
} flow_to_backend SEC(".maps");
// Счетчик для round-robin выбора (хранится в отдельной карте-массиве)
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, 1);
__type(key, __u32);
__type(value, __u32);
} backend_counter SEC(".maps");
Логика выбора бэкенда в программе XDP может использовать алгоритм round-robin. Важно использовать атомарные операции для обновления счетчика, так как программа может выполняться одновременно в нескольких ядрах процессора:
__u32 key = 0;
__u32 *counter = bpf_map_lookup_elem(&backend_counter, &key);
if (!counter) return XDP_PASS;
__u32 backend_index = __sync_fetch_and_add(counter, 1) % TOTAL_BACKENDS;
// ... выбор бэкенда из карты 'backends' по backend_index
Userspace-загрузчик (loader.c) заполняет карту backends и периодически обновляет ее, например, при изменении состояния бэкендов.
Ограничения и подводные камни: почему не вся логика уместна в XDP
XDP работает на уровне сетевого драйвера, что накладывает ограничения. Программа не имеет доступа к данным полезной нагрузки (payload) после транспортного заголовка (L4). Для анализа HTTP-заголовков или модификации тела пакета потребуется переход на hook TC (Traffic Control), который работает выше по стеку.
Сложная логика обработки TCP-соединений (управление окнами, повторные передачи) также плохо ложится на XDP. Verifier eBPF ограничивает количество инструкций в программе (обычно до 1 млн для сложных программ) и запрещает циклы без гарантированного выхода. Для интенсивных вычислений лучше вынести логику в userspace, используя XDP только для классификации и перенаправления пакетов.
Рекомендуемая архитектура: быстрый дроп нежелательного трафика и балансировка на уровне L3/L4 в XDP, а более «умная» обработка (L7) – в программах eBPF, прикрепленных к TC, или в пользовательском пространстве с использованием AF_XDP для эффективной передачи пакетов.
Безопасное внедрение: тестирование в netns и откат на случай проблем
Внедрение низкоуровневого сетевого кода в production-среду требует осторожности. Ошибка может привести к потере сетевой связности. Методика безопасного тестирования включает изоляцию в сетевом пространстве имен (network namespace).
Создайте изолированное окружение для тестов:
# Создание network namespace
sudo ip netns add test_ns
# Создание виртуальной пары интерфейсов
sudo ip link add veth_a type veth peer name veth_b
# Помещение одного интерфейса в namespace
sudo ip link set veth_b netns test_ns
# Настройка адресов и поднятие интерфейсов
sudo ip addr add 10.0.0.1/24 dev veth_a
sudo ip link set veth_a up
sudo ip netns exec test_ns ip addr add 10.0.0.2/24 dev veth_b
sudo ip netns exec test_ns ip link set veth_b up
Загрузите XDP-программу на интерфейс veth_a в основном namespace. Генерируйте тестовый трафик из изолированного namespace с помощью ping, nc или инструмента scapy в Python. Мониторьте счетчики пакетов в BPF-картах с помощью bpftool map dump id [MAP_ID].
Процедура отката на случай проблем проста: удаление программы с интерфейса. Это можно сделать одной командой, которая вернет обработку пакетов стандартному стеку ядра.
sudo ip link set dev eth0 xdp off
Всегда тестируйте программу под нагрузкой, аналогичной production, используя такие инструменты как pktgen или trex.
Мониторинг работы eBPF-программ: bpftool, Grafana и Prometheus
После внедрения необходим оперативный контроль. Утилита bpftool – основной инструмент для мониторинга. Команда sudo bpftool prog show отображает ID, тип, время загрузки и количество выполненных инструкций (run_time_ns) для каждой программы. sudo bpftool map dump id [ID] позволяет инспектировать содержимое карт, например, счетчики обработанных пакетов.
Для интеграции в систему мониторинга данные из eBPF-программ можно экспортировать в Prometheus. Один из способов – использование perf buffer или ring buffer для передачи событий или метрик из ядра в userspace-демон, который преобразует их в метрики Prometheus. Готовые дашборды Grafana, например, из экосистемы Cilium, можно адаптировать для отображения кастомных метрик, таких как количество дропнутых пакетов в секунду или задержка обработки.
Простой userspace-загрузчик может периодически опрашивать значения из BPF-карт (например, счетчика дропнутых пакетов) и публиковать их через клиентскую библиотеку Prometheus. Это дает полную видимость работы ваших программ в production.
Когда писать код с нуля не нужно: обзор экосистемы (Cilium, BCC)
Разработка с нуля оправдана для специфичных задач высокопроизводительной обработки «в проводе». Однако для многих стандартных сценариев существуют готовые, отлаженные решения.
Cilium – это полноценная сетевая платформа для Kubernetes, которая использует eBPF на всех уровнях: от замены kube-proxy (балансировка нагрузки Service) до реализации сетевых политик (NetworkPolicy) и наблюдения. Если ваша инфраструктура работает в Kubernetes, Cilium – оптимальный выбор. Он избавляет от необходимости писать код, предоставляя декларативный API через Custom Resource Definitions (CRD).
BCC (BPF Compiler Collection) и bpftrace – это инструменты для динамической трассировки и отладки ядра и пользовательских приложений. Они идеальны для анализа производительности, поиска узких мест или отслеживания системных вызовов, но не предназначены для обработки сетевых пакетов в реальном времени.
AF_XDP – это сокетный тип, который позволяет передавать целые пакеты из XDP-программы прямо в userspace-приложение с минимальными накладными расходами (bypass-ядро). Это следующий уровень оптимизации, когда критична не только скорость фильтрации, но и скорость обработки данных приложениями в пользовательском пространстве.
Вывод: для реализации кастомной логики маршрутизации, балансировки или защиты от DDoS на уровне L3/L4 пишите программы XDP. Для управления сетевыми политиками в Kubernetes используйте Cilium. Для глубокой отладки и анализа производительности применяйте BCC/bpftrace.
Шпаргалка по ограничениям eBPF и типичным ошибкам новичка
Сборник наиболее частых проблем, которые возникают при разработке под eBPF/XDP.
- Ограничения верификатора (verifier). Верификатор гарантирует безопасность eBPF-программ. Он запрещает:
- Циклы с недетерминированным числом итераций. Используйте
#pragma unrollдля небольших циклов с известным на этапе компиляции пределом. - Выход за границы массивов или указателей. Всегда проверяйте границы:
if (ptr + size > data_end) return XDP_ABORTED;. - Использование неинициализированных переменных на стеке.
- Размер стека ограничен 512 байтами. Для больших структур используйте BPF-карты.
- Циклы с недетерминированным числом итераций. Используйте
- Версионные ловушки. API вспомогательных функций (helper functions) и структур данных может меняться между версиями ядра. Всегда проверяйте наличие функций через
#ifdefи ориентируйтесь на минимальную поддерживаемую версию ядра в вашем проекте (например, 5.10 LTS). - Ошибки компиляции.
- Отсутствие флага
-target bpfпри компиляции clang. - Неверный путь к заголовочным файлам. Убедитесь, что установлен пакет
linux-headers-$(uname -r). - Использование стандартных библиотек C (libc) – в eBPF они недоступны.
- Отсутствие флага
- Сетевые тонкости.
- Программа, возвращающая
XDP_DROPдля TCP-пакета, просто отбрасывает его без отправки RST. Это может привести к длительным таймаутам на стороне отправителя. Для корректного разрыва TCP-соединений может потребоваться дополнительная логика. - Изменение IP-адресов в заголовке пакета требует пересчета контрольной суммы (checksum). Используйте вспомогательную функцию
bpf_l3_csum_replace(). - Всегда начинайте отладку с UDP-трафика, его проще анализировать.
- Программа, возвращающая
Эта шпаргалка поможет избежать часов отладки и сосредоточиться на реализации бизнес-логики.