- PVSM.RU - https://www.pvsm.ru -

Реализация гео-блокировки на основе eBPF-XDP

Реализация гео-блокировки на основе eBPF-XDP - 1

В 2022 году финансовый сектор, в частности банки, столкнулся с волной продолжительных и достаточно мощных DDoS-атак разных векторов. Среди них были и банальные L7 HTTP-флуды, не представлявшие собой ничего сложного в техническом плане, но для организаций с несколькими сотнями пользовательских сервисов и защитой от L7-атак только критичных из них, это стало серьезным вызовом.

Типичная L4-защита не давала необходимой эффективности, а количество атакующих устройств было достаточно, чтобы забить каналы пропускной способностью в десятки гигабит. Тогда на помощь пришла фильтрация трафика по географическому признаку, поскольку основная часть атакующего трафика шла из-за границы. Применение гео-фильтрации оказалось достаточным, чтобы в критический момент восстановить доступность сервисов и выиграть время для настройки более точной защиты на основе детального анализа трафика.

Этот опыт наглядно показал, что иногда простые решения оказываются наиболее эффективными в критических ситуациях. Гео-фильтрация, будучи грубым инструментом, в условиях DDoS-атаки может стать тем самым «спасательным кругом», который позволяет локализовать проблему и выиграть время для более тонкой настройки.

Технология XDP (eXpress Data Path) идеально подходит для таких сценариев — она позволяет обрабатывать пакеты на самом раннем этапе, еще до того, как они попадут в сетевой стек ядра, что обеспечивает беспрецедентную производительность.

Данной статьей хочется продемонстрировать, как с помощью XDP можно достаточно легко реализовать собственный гео-фильтр. Эту защиту можно реализовать, например, модулями nginx, но в таком случае все нежелательные запросы будут проходить полный сетевой стек операционной системы и потреблять ресурсы веб-сервера, прежде чем быть отклоненными. В нашем же случае защита отрабатывает до того, как пакеты поступят в сетевой стек ядра.

В данной статье мы поэтапно реализуем XDP-фильтр на ограниченной версии языка C. Код сознательно упрощен для лучшего понимания основных концепций.


Для начала создадим заголовочный файл maps.h. В нем мы опишем все необходимы структуры и мапы. В eBPF мапы нужны для обмена данными между пользовательским пространством и XDP программой.

Так как работать мы будем с src IP-адресами, а гео-база может содержать префиксы любой длины, то в качестве типа мапы будем использовать BPF_MAP_TYPE_LPM_TRIE , который реализует Longest Prefix Match (LPM) на основе префиксного дерева (trie). Это означает, что для каждого IP-адреса будет найдена запись с самым длинным совпадающим префиксом.

struct {
    __uint(type, BPF_MAP_TYPE_LPM_TRIE);
    __type(key, struct geo_ip_key);
    __type(value, struct geo_value);
    __uint(max_entries, 600000);
    __uint(map_flags, BPF_F_NO_PREALLOC); // обязательно для LPM
} geo_m SEC(".maps");

Структура geo_ip_key должна иметь определенную структуру и включать в себя длину префикса и IP-адрес:

struct geo_ip_key {
    __u32 prefix_len; 	// Длина префикса в битах
    __u32 ip; 		    // IP-адрес в сетевом порядке байт
};

В geo_value будет содержаться гео-идентификатор страны:

struct geo_value {
    __u32 geoname_id;	// Идентификатор страны
};

Так же нам необходим блок с настройкой самой контрмеры:

struct cfg_geo {
    __u32 allowed_geos[MAX_GEO_ALLOWED]; 
    __u32 blocked_geos[MAX_GEO_BLOCKED]; 
    __u8 default_action; 
    __u8 enabled;					 // Включена ли фильтрация
    __u16 pad; 					     // Выравнивание
};

Где:

  • allowed_geos — разрешенные страны

  • blocked_geos — запрещенные страны

  • default_action — действие по умолчанию, если адрес принадлежит стране, которая не попала ни в список разрешенных, ни в список запрещенных

Здесь мы можем заметить __u16 pad. Это выравнивание (padding). Компилятор может добавить паддинг автоматически для оптимизации доступа к памяти и соблюдения требований процессора. Явное указание делает структуру данных предсказуемой и код более понятным.

Отлично! Мы описали заголовочные файлы, теперь можем приступить к реализации основной программы.

SEC("xdp")
int xdp_geo_filter(struct xdp_md *ctx) {

Программа начинается с макроса SEC("xdp"), который указывает компилятору, что эту функцию нужно прикрепить к XDP-хуку сетевого интерфейса. Функция xdp_geo_filter вызывается для каждого пакета, поступающего на сетевой интерфейс.

Разница в обработке пакетов:

  • Без XDP: Пакет → Драйвер сетевой карты → Ядро Linux → Приложение

  • С XDP: Пакет → XDP программа (наша функция) → Драйвер сетевой карты → Ядро Linux → Приложение

В главной функции мы выполняем стандартные проверки безопасности, необходимые для того, чтобы XDP-программа не была отклонена верификатором eBPF.

Определяем границы пакета:

void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;

Проверяем, достаточно ли места для Ethernet-заголовка. Если адрес выходит за конец пакета, значит пакет поврежден или слишком мал - в таком случае пропускаем его:

struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end) 
  return XDP_PASS; 

Проверяем, что это IP-пакет (нам нужны только IP-пакеты):

if (bpf_ntohs(eth->h_proto) != ETH_P_IP) return XDP_PASS;
    struct iphdr *iph = (void *)(eth + 1);
    if ((void *)(iph + 1) > data_end) {
        return XDP_PASS; 
    }

Извлекаем IP-адрес источника и вызываем функцию гео-фильтрации:

__u32 src_ip = iph->saddr;
if (check_geo(src_ip) == XDP_DROP) {
  return XDP_DROP;
}

Вообще XDP может возвращать несколько значений:

  • XDP_ABORTED           0  // Ошибка выполнения

  • XDP_DROP                  1  // Отбросить пакет

  • XDP_PASS                   2  // Передать пакет дальше в сетевой стек

  • XDP_TX                       3  // Отправить пакет обратно через тот же интерфейс

  • XDP_REDIRECT          4  // Перенаправить на другой интерфейс

Но в нашем случае функция возвращает только XDP_DROP (1) или XDP_PASS (2)

Реализация функции гео-фильтрации:

static __always_inline int check_geo(__u32 src_ip) {
    __u32 config_key = 0;
    struct cfg_geo *geo_cfg = bpf_map_lookup_elem(&cfg_geo_m, &config_key);
    
    if (!geo_cfg || !geo_cfg->enabled) 	// Проверяем, включена ли контрмера
        return XDP_PASS;

Создаем ключ для поиска в LPM-trie мапе geo_m используя IP-адрес источника:

struct geo_ip_key key = {
  .prefix_len = 32,
  .ip = src_ip,
};

Если адреса нет в гео-базе, применяем действие по умолчанию: пропустить или сбросить пакет:

struct geo_value *geo = bpf_map_lookup_elem(&geo_m, &key);
if (!geo)
  return geo_cfg->default_action ? XDP_DROP : XDP_PASS;

Если адрес найден в гео-базе, проверяем список разрешенных стран. Проходим по массиву allowed_geos и если идентификатор страны присутствует в нем, пакет пропускается:

#pragma unroll
for (int i = 0; i < MAX_GEO_ALLOWED; i++) {
  if (geo_cfg->allowed_geos[i] == 0) break;
  if (geo_cfg->allowed_geos[i] == geo->geoname_id) 
  return XDP_PASS;
}
Скрытый текст

Важно: Директива #pragma unroll указывает компилятору развернуть цикл, чтобы верификатор eBPF мог проверить его ограниченность. Это требование безопасности - все циклы в eBPF должны иметь предсказуемое максимальное количество итераций.

Проверяем список запрещенных стран. Аналогично проходим по массиву blocked_geos. Если идентификатор страны присутствует в черном списке, пакет сбрасывается:

#pragma unroll
for (int i = 0; i < MAX_GEO_BLOCKED; i++) {
  if (geo_cfg->blocked_geos[i] == 0) break;
  if (geo_cfg->blocked_geos[i] == geo->geoname_id)
  return XDP_DROP;
}

Действие по умолчанию применяется, если IP-адрес принадлежит стране, которая не находится ни в белом, ни в черном списке:

return geo_cfg->default_action ? XDP_DROP : XDP_PASS;

Это вся XDP-программа. Теперь ее можно скомпилировать с помощью Clang:

clang -O2 -g -Wall -target bpf -D__BPF_TRACING__ -I. -I./headers -c xdp_geo.c -o xdp_geo.o

Параметры компиляции:

  • -target bpf - компиляция для eBPF

  • -O2 - оптимизация для производительности

  • -D__BPF_TRACING__ - определение для трассировки

  • -I. -I./headers - пути к заголовочным файлам

Загружаем программу и привязываем ее к сетевому интерфейсу:

bpftool prog loadall xdp_geo.o /sys/fs/bpf/geo type xdp pinmaps /sys/fs/bpf/geo
ip link set dev ens160 xdpgeneric pinned /sys/fs/bpf/geo/xdp_geo_filter
Скрытый текст

Для тестирования мы используем xdpgeneric, для продакшена лучше xdp (драйверный режим)

Убедиться, что программа привязалась можно с помощью команды:

 ip link show

Теперь нам надо наполнить мапу geo_m, для этого необходим файл с базой данных, которая сопоставляет диапазоны IPv4-адресов с географической принадлежностью.

Базу можно поискать тут:

Пример такого файла в csv:

Реализация гео-блокировки на основе eBPF-XDP - 2

Здесь нам интересны столбцы network и geoname_id

Формат записи в мапе будет следующим:

        "key": {
            "prefix_len": 16,
            "ip": 30580
        },
        "value": {
            "geoname_id": 1269750
        },
Скрытый текст

Для удобства наполнения геоданными предусмотрен Go-скрипт (ссылка в конце статьи).

Использовать так:

go run main.go RU-GeoIP-Country-Blocks-IPv4.csv geo_m

Записей достаточно много, поэтому загрузка данных в мапу занимает какое-то время. Помните мы указывали __uint(max_entries, 600000) в структуре мапы geo_m? После выполнения go программы мы получим около 512k записей:

sudo bpftool map dump name geo_m | grep -c "key"
512171

Теперь у нас есть наполненная гео-база и мы можем протестировать нашу контрмеру

Скрытый текст

Кстати, вопрос знатокам, почему в гео-базах отсутствует адрес 1.1.1.1?

Реализация гео-блокировки на основе eBPF-XDP - 3

Давайте включим контрмеру, добавим в разрешенные RU, действие по умолчанию сброс. Для этого можем воспользоваться bpftool - основной утилитой для работы с BPF.

У RU geoname_id (уникальный идентификатор местоположения сети) является значение 2017370

eBPF карты хранят числа в little-endian формате. Это значит, что младший байт (наименее значащий) идёт первым, а старший — последним.

Возьмем число 2017370₁₀ = 0x001EC85A₁₆

В little-endian формате оно будет выглядеть как 5ac81e00

 В maps.h у нас есть определения

#define MAX_GEO_ALLOWED 32

#define MAX_GEO_BLOCKED 32

Это значит, что массивы allowed_geos и blocked_geos содержат по 32 элемента 4 байта каждый. Первый элемент будет 5a c8 1e 00, оставшиеся - нули. Не забываем про default_action, enabled и pad, это последние байты 01 01 00 00.

sudo bpftool map update pinned /sys/fs/bpf/geo/cfg_geo_m 
  key hex 00 00 00 00 
  value hex 
  5a c8 1e 00 00 00 00 00 00 00 00 00 00 00 00 00 
  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
  01 01 00 00

Данный способ внесения изменений не очень удобен, можно написать небольшой скрипт упрощающий этот процесс, но для наших целей этого достаточно.

Выведем содержимое карты cfg_geo_m:

bpftool map dump name cfg_geo_m
[{
        "key": 0,
        "value": {
            "allowed_geos": [2017370,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
            ],
            "blocked_geos": [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
            ],
            "default_action": 1,
            "enabled": 1,
            "pad": 0
        }
    }
]

Теперь у нас все готово для тестирования. Для проверки мы можем запустить на защищаемом ресурсе tcpdump в случае, если трафик будет попадать под сброс, мы не увидим его в захвате, т.к. наша xdp программа работает раньше.

Так же мы можем включить логирование самой xdp программы добавив непосредственно перед пропуском или сбросом пакета bpf_printk() – это встроенная (helper) функция в eBPF, которая используется для отладки, она позволяет выводить отладочные сообщения прямо из eBPF-кода в trace log ядра Linux.

Скрытый текст

Важно: В продакшене лучше не использовать bpf_printk(), т.к. отладка грузит ядро.

Сообщения, напечатанные через bpf_printk(), можно посмотреть в системных логах:

sudo cat /sys/kernel/debug/tracing/trace_pipe

Сгенерируем трафик используя в качестве src адрес из geoname_id  2017370

В логах увидим события такого типа:

<idle>-0       [005] ..s21 15327.319941: bpf_trace_printk: xdp_geo_filter: PASS - country 2017370 in allow list

При попытке использовать адрес, принадлежащий любой другой страны, например 8.8.8.8, лог покажет следующее:

<idle>-0       [005] ..s21 15509.623322: bpf_trace_printk: xdp_geo_filter: DROP - default action for country 6252001

В случае, если адреса нет в мапе, например используется серый ip, то будет применено действие по умолчанию.


Заключение: Реализация анти DDoS защиты на основе XDP/eBPF представляет собой мощное и эффективное решение для современных сетевых задач. А главное, достаточно несложное в реализации.

Ссылка на полный код:

https://github.com/mrOctaviusTru/test_geo.git [4]

Официальная документация:

https://www.kernel.org/doc/html/latest/bpf/index.html [5]

https://docs.cilium.io/en/stable/reference‑guides/bpf/index.html [6]

Книги:

«Learning eBPF» автор Liz Rice 

«BPF Performance Tools» автор Brendan Gregg

Автор: mrOctaviusTru

Источник [7]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/linux/436798

Ссылки в тексте:

[1] https://dev.maxmind.com/geoip/geolite2-free-geolocation-data: https://dev.maxmind.com/geoip/geolite2-free-geolocation-data

[2] https://lite.ip2location.com: https://lite.ip2location.com

[3] https://geoip.noc.gov.ru: https://geoip.noc.gov.ru

[4] https://github.com/mrOctaviusTru/test_geo.git: https://github.com/mrOctaviusTru/test_geo.git

[5] https://www.kernel.org/doc/html/latest/bpf/index.html: https://www.kernel.org/doc/html/latest/bpf/index.html

[6] https://docs.cilium.io/en/stable/reference‑guides/bpf/index.html: https://docs.cilium.io/en/stable/reference-guides/bpf/index.html

[7] Источник: https://habr.com/ru/articles/966938/?utm_source=habrahabr&utm_medium=rss&utm_campaign=966938