BRONSON
DOBERMANN
Гай Ричи
MrLoot
Frank Vinci
atmservice
kiprijanov
probiv
atmservice

Интересно Пишем защиту от DDoS-атак на XDP. Ядерная часть

DOMINUS

«EDEM CORP»
Старший модератор
Модератор
Проверенный продавец
Legend user

DOMINUS

«EDEM CORP»
Старший модератор
Модератор
Проверенный продавец
Legend user
Статус
Offline
Регистрация
20 Дек 2015
Сообщения
3,980
Реакции
391
Депозит
3.000р
Покупки через Гарант
0
Продажи через Гарант
12
Технология eXpress Data Path (XDP) позволяет выполнить произвольную обработку трафика на интерфейсах Linux до того, как пакеты поступят в сетевой стек ядра. Применение XDP — защита от DDoS-атак (CloudFlare), сложные фильтры, сбор статистики (Netflix). Программы XDP исполняются виртуальной машиной eBPF, поэтому имеют ограничения как на свой код, так и на доступные функции ядра в зависимости от типа фильтра.

Статья призвана восполнить недостатки многочисленных материалов по XDP. Во-первых, в них дается готовый код, который сразу обходит особенности XDP: подготовлен для верификации или слишком прост, чтобы вызвать проблемы. При попытке потом написать свой код с нуля нет понимания, что делать с характерными ошибками. Во-вторых, не освещаются способы локально тестировать XDP без ВМ и «железа», при том, что у них свои «подводные камни». Текст рассчитан на программистов, знакомых с сетями и Linux, которым интересен XDP и eBPF.

В этой части детально разберемся, как собирается XDP-фильтр и как его тестировать, затем напишем простой вариант известного механизма SYN cookies на уровне обработки пакетов. Пока не будем формировать «белый список»
проверенных клиентов, вести счетчики и управлять фильтром — хватит логов.

Писать будем на C — это не модно, зато практично. Весь код доступен на GitHub по ссылке в конце и разбит на коммиты по этапам, описанным в статье.

Disclaimer. В ходе статьи будет разрабатываться мини-решение для отражения от DDoS-атак, потому что это реалистичная задача для XDP и моя область. Однако главная цель — разобраться с технологией, это не руководство по созданию готовой защиты. Учебный код не оптимизирован и опускает некоторые нюансы.

Краткий обзор XDP

Изложу только ключевые моменты, чтобы не дублировать документацию и существующие статьи.

Итак, в ядро загружается код фильтра. Фильтру передаются входящие пакеты. В итоге фильтр должен принять решение: пропустить пакет в ядро (XDP_PASS), сбросить пакет (XDP_DROP) или отправить его обратно (XDP_TX). Фильтр может изменить пакет, это особенно актуально для XDP_TX. Также можно аварийно прервать программу (XDP_ABORTED) и сбросить пакет, но это аналог assert(0) — для отладки.

Виртуальная машина eBPF (extended Berkley Packet Filter) специально сделана простой, дабы ядро могло проверить, что код не зацикливается и не повреждает чужую память. Совокупные ограничения и проверки:

  1. Запрещены циклы (переходы назад).
  2. Есть стек для данных, но нет функций (все функции C должны встраиваться).
  3. Запрещены обращения к памяти за пределами стека и буфера пакета.
  4. Размер кода ограничен, но на практике это не очень существенно.
  5. Разрешены вызовы только специальных функций ядра (eBPF helpers).

Разработка и установка фильтра выглядят так:

  • Исходный код (например, kernel.c) компилируется в объектный (kernel.o) под архитектуру виртуальной машины eBPF. На октябрь 2019 компиляция в eBPF поддерживается Clang и обещана в GCC 10.1.
  • Если в этом объектном коде есть обращения к структурам ядра (например, к таблицам и счетчикам), вместо их ID стоят нули, то есть выполнить такой код нельзя. Перед загрузкой в ядро нужно эти нули заменить на ID конкретных объектов, созданных через вызовы ядра (слинковать код). Можно сделать это внешними утилитами, а можно написать программу, которая будет линковать и загружать конкретный фильтр.
  • Ядро верифицирует загружаемую программу. Проверяется отсутствие циклов и невыход за границы пакета и стека. Если верификатор не может доказать, что код корректен, программа отвергается, — надо уметь ублажать его.
  • После успешной верификации ядро компилирует объектный код архитектуры eBPF в машинный код системной архитектуры (just-in-time).
  • Программа прикрепляется к интерфейсу и начинает обрабатывать пакеты.

Поскольку XDP работает в ядре, отладка ведется по логам трассировки и, собственно, по пакетам, которые программа фильтрует или генерирует. Тем не менее, eBPF обеспечивает безопасность загруженного кода для системы, поэтому экспериментировать с XDP можно прямо на локальном Linux.

Подготовка окружения

Сборка

Clang не может напрямую выдавать объектный код для архитектуры eBPF, поэтому процесс состоит из двух шагов:

  1. Скомпилировать код на C в байт-код LLVM (clang -emit-llvm).
  2. Преобразовать байт-код в объектный код eBPF (llc -march=bpf -filetype=obj).

При написании фильтра пригодится пара файлов со вспомогательными функциями и макросами из тестов ядра. Важно, чтобы они соответствовали версии ядра (KVER). Качаем их в helpers/:

Код:
export KVER=v5.3.7
export BASE=https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/plain/tools/testing/selftests/bpf
wget -P helpers --content-disposition "${BASE}/bpf_helpers.h?h=${KVER}" "${BASE}/bpf_endian.h?h=${KVER}"
unset KVER BASE

Makefile для Arch Linux (ядро 5.3.7):

Код:
CLANG ?= clang
LLC ?= llc

KDIR ?= /lib/modules/$(shell uname -r)/build
ARCH ?= $(subst x86_64,x86,$(shell uname -m))

CFLAGS = \
    -Ihelpers \
    \
    -I$(KDIR)/include \
    -I$(KDIR)/include/uapi \
    -I$(KDIR)/include/generated/uapi \
    -I$(KDIR)/arch/$(ARCH)/include \
    -I$(KDIR)/arch/$(ARCH)/include/generated \
    -I$(KDIR)/arch/$(ARCH)/include/uapi \
    -I$(KDIR)/arch/$(ARCH)/include/generated/uapi \
    -D__KERNEL__ \
    \
    -fno-stack-protector -O2 -g

xdp_%.o: xdp_%.c Makefile
    $(CLANG) -c -emit-llvm $(CFLAGS) $< -o - | \
    $(LLC) -march=bpf -filetype=obj -o $@

.PHONY: all clean

all: xdp_filter.o

clean:
    rm -f ./*.o
KDIR содержит путь к заголовкам ядра, ARCH — архитектуру системы. Пути и инструменты могут немного отличаться между дистрибутивами.

Пример отличий для Debian 10 (ядро 4.19.67)

Код:
# другая команда
CLANG ?= clang
LLC ?= llc-7

# другой каталог
KDIR ?= /usr/src/linux-headers-$(shell uname -r)
ARCH ?= $(subst x86_64,x86,$(shell uname -m))

# два дополнительных каталога -I
CFLAGS = \
    -Ihelpers \
    \
    -I/usr/src/linux-headers-4.19.0-6-common/include \
    -I/usr/src/linux-headers-4.19.0-6-common/arch/$(ARCH)/include \
    # далее без изменений
CFLAGS подключают директорию со вспомогательными заголовками и несколько директорий с заголовками ядра. Символ __KERNEL__ означает, что заголовки UAPI (userspace API) определяются для кода ядра, так как фильтр выполняется в ядре.

Защиту стека можно отключить (-fno-stack-protector), потому что верификатор кода eBPF все равно проверяет невыход за границы стека. Сразу стоит включить оптимизации, потому что размер байт-кода eBPF ограничен.

Начнем с фильтра, который пропускает все пакеты и ничего не делает:

Код:
#include <uapi/linux/bpf.h>

#include <bpf_helpers.h>

SEC("prog")
int xdp_main(struct xdp_md* ctx) {
    return XDP_PASS;
}

char _license[] SEC("license") = "GPL";
Команда make собирает xdp_filter.o. Где его теперь испытать?

Тестовый стенд

Стенд должен включать два интерфейса: на котором будет фильтр и с которого будут отправляться пакеты. Это должны быть полноценные устройства Linux со своими IP, чтобы проверять, как обычные приложения работают с нашим фильтром.

Устройства типа veth (virtual Ethernet) нам подходят: это пара виртуальных сетевых интерфейсов, «соединенных» между собой напрямую. Создать их можно так (в этом разделе все команды ip выполняются от root):

Код:
ip link add xdp-remote type veth peer name xdp-local
Здесь xdp-remote и xdp-local — имена устройств. На xdp-local (192.0.2.1/24) будет присоединен фильтр, с xdp-remote (192.0.2.2/24) будет отправляться входящий трафик. Однако есть проблема: интерфейсы находятся на одной машине, и Linux не будет слать трафик на один из них через другой. Можно решать это хитрыми правилами iptables, но им придется менять пакеты, что неудобно при отладке. Лучше использовать сетевые пространства имен (network namespaces, далее netns).

Сетевое пространство имен содержит набор интерфейсов, таблиц маршрутизации и правил NetFilter, изолированные от аналогичных объектов в других netns. Каждый процесс работает в каком-то пространстве имен, и ему доступны только объекты этого netns. По умолчанию в системе единственное сетевое пространство имен для всех объектов, поэтому можно работать в Linux и не знать про netns.

Создадим новое пространство имен xdp-test и переместим туда xdp-remote.

Код:
ip netns add xdp-test
ip link set dev xdp-remote netns xdp-test
Тогда процесс, выполняющийся в xdp-test, не будет «видеть» xdp-local (он останется в netns по умолчанию) и при отправке пакета на 192.0.2.1 передаст его через xdp-remote, потому что это единственный интерфейс в 192.0.2.0/24, доступный этому процессу. Это действует и в обратную стррону.

При перемещении между netns интерфейс опускается и теряет адрес. Чтобы настроить интерфейс в netns, нужно запустить ip ... в этом пространстве имен командной ip netns exec:

Код:
ip netns exec xdp-test \
    ip address add 192.0.2.2/24 dev xdp-remote
ip netns exec xdp-test \
    ip link set xdp-remote up
Как можно видеть, это не отличается от настройки xdp-local в пространстве имен по умолчанию:

Код:
ip address add 192.0.2.1/24 dev xdp-local
ip link set xdp-local up
Если запустить tcpdump -tnevi xdp-local, можно увидеть, что пакеты, отправленные из xdp-test, доставляются на этот интерфейс:

Код:
ip netns exec xdp-test   ping 192.0.2.1
Удобно запустить шелл в xdp-test. В репозитарии есть скрипт, автоматизирующий работу со стендом, например, можно настроить стенд командой sudo ./stand up и удалить его командой sudo ./stand down.

Трассировка

Фильтр привязывается к устройству так:

Код:
ip -force link set dev xdp-local xdp object xdp_filter.o verbose
Ключ -force нужен, чтобы привязать новую программу, если другая уже привязана. «No news is good news» не про эту команду, вывод в любом случае объемный. Указывать verbose необязательно, но с ним появляется отчет о работе верификатора кода с листингом ассемблера:

Код:
Verifier analysis:

0: (b7) r0 = 2
1: (95) exit
Отвязать программу от интерфейса:

ip link set dev xdp-local xdp off

В скрипте это команды sudo ./stand attach и sudo ./stand detach.

Привязав фильтр, можно убедиться, что ping продолжает работать, но работает ли программа? Добавим логи. Функция bpf_trace_printk() похожа на printf(), но поддерживает всего до трех аргументов, кроме шаблона, и ограниченный список спецификаторов. Макрос bpf_printk() упрощает вызов.

Код:
   SEC("prog")
   int xdp_main(struct xdp_md* ctx) {
+      bpf_printk("got packet: %p\n", ctx);
       return XDP_PASS;
   }
Вывод идет в канал трассировки ядра, который нужно включить:

Код:
echo -n 1 | sudo tee /sys/kernel/debug/tracing/options/trace_printk
Просмотр потока сообщений:

Код:
cat /sys/kernel/debug/tracing/trace_pipe
Обе этих команды делает вызов sudo ./stand log.

Ping теперь должен вызывать в нем такие сообщения:

Код:
<...>-110930 [004] ..s1 78803.244967: 0: got packet: 00000000ac510377
Если присмотреться к выводу верификатора, можно заметить странные вычисления:

Код:
0: (bf) r3 = r1
1: (18) r1 = 0xa7025203a7465
3: (7b) *(u64 *)(r10 -8) = r1
4: (18) r1 = 0x6b63617020746f67
6: (7b) *(u64 *)(r10 -16) = r1
7: (bf) r1 = r10
8: (07) r1 += -16
9: (b7) r2 = 16
10: (85) call bpf_trace_printk#6
<...>
Дело в том, что у программ на eBPF нет секции данных, поэтому единственный способ закодировать форматную строку — immediate-аргументы команд ВМ:

Код:
$ python -c "import binascii; print(bytes(reversed(binascii.unhexlify('0a7025203a74656b63617020746f67'))))"
b'got packet: %p\n'
По этой причине отладочный вывод сильно раздувает итоговый код.

Отправка пакетов XDP

Изменим фильтр: пусть он все входящие пакеты отправляет обратно. Это некорректно с сетевой точки зрения, так как нужно было бы менять адреса в заголовках, но сейчас важна работа в принципе.

Код:
       bpf_printk("got packet: %p\n", ctx);
-      return XDP_PASS;
+      return XDP_TX;
   }
Запускаем tcpdump на xdp-remote. Он должен показать идентичные исходящие и входящие ICMP Echo Request и перестать показывать ICMP Echo Reply. Но не показывает. Оказывается, для работы XDP_TX в программе на xdp-local необходимо, чтобы парному интерфейсу xdp-remote тоже была назначена программа, хотя бы пустая, и он был поднят.

Восстановим минимальный фильтр (XDP_PASS) в файле xdp_dummy.c, добавим его в Makefile, привяжем к xdp-remote:

Код:
ip netns exec remote \
    ip link set dev int xdp object dummy.o

Теперь tcpdump показывает то, что ожидается:

Код:
62:57:8e:70:44:64 > 26:0e:25:37:8f:96, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 64, id 13762, offset 0, flags [DF], proto ICMP (1), length 84)
    192.0.2.2 > 192.0.2.1: ICMP echo request, id 46966, seq 1, length 64
62:57:8e:70:44:64 > 26:0e:25:37:8f:96, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 64, id 13762, offset 0, flags [DF], proto ICMP (1), length 84)
    192.0.2.2 > 192.0.2.1: ICMP echo request, id 46966, seq 1, length 64
Если вместо этого показываются только ARP, нужно убрать фильтры (это делает sudo ./stand detach), пустить ping, затем установить фильтры и попробовать снова. Проблема в том, что фильтр XDP_TX действует и на ARP, и если стек
пространства имен xdp-test успел «забыть» MAC-адрес 192.0.2.1, он не сможет разрешить этот IP.

Постановка задачи

Перейдем к заявленной задачи: написать на XDP механизм SYN cookies.

До сих пор популярной DDoS-атакой остается SYN flood, суть которой в следующем. При установке соединения (TCP handshake) сервер получает SYN, выделяет ресурсы под будущее соединение, отвечает SYNACK-пакетом и ожидает ACK. Атакующий просто отправляет SYN-пакеты с поддельных адресов в количестве тысяч в секунду с каждого хоста из многотысячного ботнета. Сервер вынужден выделять ресурсы сразу по прибытии пакета, а освобождает по большому таймауту, в результате исчерпывается память или лимиты, новые соединения не принимаются, сервис недоступен.

Если не выделять по SYN-пакету ресурсы, а только отвечать SYNACK-пакетом, как тогда серверу понять, что ACK-пакет, пришедший позже, относится к SYN-пакету, который не сохраняли? Ведь атакующий может генерировать и фальшивые ACK. Суть SYN cookie в том, чтобы кодировать в seqnum параметры соединения как хэш от адресов, портов и меняющейся соли. Если ACK успел прийти до смены соли, можно еще раз посчитать хэш и сравнить с acknum. Подделать acknum атакующий не может, так как соль включает секрет, а перебрать не успеет из-за ограниченного канала.

SYN cookie давно реализован в ядре Linux и даже может автоматически включаться, если SYN приходят слишком быстро и массово.

С точки зрения пакетов, XDP-программа должна делать следующее:

  • на SYN отвечать SYNACK с cookie;
  • на ACK отвечать RST (разрывать соединение);
  • остальные пакеты сбрасывать.

Псевдокод алгоритма вместе с разбором пакета:

Код:
Если это не Ethernet,
    пропустить пакет.
Если это не IPv4,
    пропустить пакет.
Если адрес в таблице проверенных,               (*)
        уменьшить счетчик оставшихся проверок,
        пропустить пакет.
Если это не TCP,
    сбросить пакет.     (**)
Если это SYN,
    ответить SYN-ACK с cookie.
Если это ACK,
    если в acknum лежит не cookie,
        сбросить пакет.
    Занести в таблицу адрес с N оставшихся проверок.    (*)
    Ответить RST.   (**)
В остальных случаях сбросить пакет.

Одной (*) отмечены пункты, в которых нужно управлять состоянием системы — на первом этапе можно обойтись без них, просто реализовав TCP handshake с генерацией SYN cookie в качестве seqnum.

На месте (**), пока у нас нет таблицы, будем пропускать пакет.

Реализация TCP handshake

Разбор пакета и верификация кода

Нам понадобятся структуры сетевых заголовков: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) и TCP (uapi/linux/tcp.h). Последний у меня так и не получилось подключить из-за ошибок, связанных с atomic64_t, пришлось скопировать нужные определения в код.

Все функции, которые в C выделяются для удобства чтения, должны быть встроены по месту вызова, так как верификатор eBPF в ядре запрещает переходы назад, то есть, фактически, циклы и вызовы функций.

Код:
#define INTERNAL static __attribute__((always_inline))
Макрос LOG() отключает печать в релизной сборке.

Программа представляет собой конвейер из функций. Каждая принимает пакет, в котором выделен заголовок соответствующего уровня, например, process_ether() ожидает, что заполнено ether. По результатам анализа полей функция может передать пакет на уровень выше. Результат работы функции — действие XDP. Пока обработчики SYN и ACK пропускают все пакеты.

Код:
struct Packet {
    struct xdp_md* ctx;

    struct ethhdr* ether;
    struct iphdr* ip;
    struct tcphdr* tcp;
};

INTERNAL int process_tcp_syn(struct Packet* packet) { return XDP_PASS; }
INTERNAL int process_tcp_ack(struct Packet* packet) { return XDP_PASS; }
INTERNAL int process_tcp(struct Packet* packet) { ... }
INTERNAL int process_ip(struct Packet* packet) { ... }

INTERNAL int
process_ether(struct Packet* packet) {
    struct ethhdr* ether = packet->ether;

    LOG("Ether(proto=0x%x)", bpf_ntohs(ether->h_proto));

    if (ether->h_proto != bpf_ntohs(ETH_P_IP)) {
        return XDP_PASS;
    }

    // B
    struct iphdr* ip = (struct iphdr*)(ether + 1);
    if ((void*)(ip + 1) > (void*)packet->ctx->data_end) {
        return XDP_DROP; /* malformed packet */
    }

    packet->ip = ip;
    return process_ip(packet);
}

SEC("prog")
int xdp_main(struct xdp_md* ctx) {
    struct Packet packet;
    packet.ctx = ctx;

    // A
    struct ethhdr* ether = (struct ethhdr*)(void*)ctx->data;
    if ((void*)(ether + 1) > (void*)ctx->data_end) {
        return XDP_PASS;
    }

    packet.ether = ether;
    return process_ether(&packet);
}
Обращаю внимание на проверки, отмеченные A и B. Если закомментировать A, программа соберется, но при загрузке будет ошибка верификации:

Код:
Verifier analysis:

<...>
11: (7b) *(u64 *)(r10 -48) = r1
12: (71) r3 = *(u8 *)(r7 +13)
invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0)
R7 offset is outside of the packet
processed 11 insns (limit 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0

Error fetching program/map!
Ключевая строка invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0): есть пути выполнения, когда тринадцатый байт от начала буфера находится вне пакета. По листингу сложновато понять, о какой строке идет речь, зато есть номер инструкции (12) и дизассемблер, показывающий строки исходного кода:

Код:
llvm-objdump -S xdp_filter.o | less
В данном случае он указывает на строку

Код:
LOG("Ether(proto=0x%x)", bpf_ntohs(ether->h_proto));
по которой понятно, что проблема в ether. Всегда бы так.

Ответ на SYN

Цель на этом этапе — формировать корректный SYNACK-пакет с фиксированным seqnum, который в будущем заменится на SYN cookie. Все изменения происходят в process_tcp_syn() и окрестностях.

Проверка пакета

Как ни странно, вот самая примечательная строка, точнее, комментарий к ней:

Код:
/* Required to verify checksum calculation */
const void* data_end = (const void*)ctx->data_end;
При написании первой версии кода использовалось ядро 5.1, для верификатора которого была разница между data_end и (const void*)ctx->data_end. При написании статьи ядро 5.3.1 не имело такой проблемы. Возможно, компилятор обращался к локальной переменной иначе, чем к полю. Мораль — на большой вложенности упрощение кода может помочь.

Далее рутинные проверки длин во славу верификатора; о MAX_CSUM_BYTES ниже.

Код:
const u32 ip_len = ip->ihl * 4;
if ((void*)ip + ip_len > data_end) {
    return XDP_DROP; /* malformed packet */
}
if (ip_len > MAX_CSUM_BYTES) {
    return XDP_ABORTED; /* implementation limitation */
}

const u32 tcp_len = tcp->doff * 4;
if ((void*)tcp + tcp_len > (void*)ctx->data_end) {
    return XDP_DROP; /* malformed packet */
}
if (tcp_len > MAX_CSUM_BYTES) {
    return XDP_ABORTED; /* implementation limitation */
}
Разворот пакета

Заполняем seqnum и acknum, выставляем ACK (SYN уже выставлен):
Код:
const u32 cookie = 42;
tcp->ack_seq = bpf_htonl(bpf_ntohl(tcp->seq) + 1);
tcp->seq = bpf_htonl(cookie);
tcp->ack = 1;
Меняем местами порты TCP, адрес IP и MAC-адреса. Стандартная библиотека недоступна из XDP-программы, поэтому memcpy() — макрос, скрывающий интринсик Clang.

Код:
const u16 temp_port = tcp->source;
tcp->source = tcp->dest;
tcp->dest = temp_port;

const u32 temp_ip = ip->saddr;
ip->saddr = ip->daddr;
ip->daddr = temp_ip;

struct ethhdr temp_ether = *ether;
memcpy(ether->h_dest, temp_ether.h_source, ETH_ALEN);
memcpy(ether->h_source, temp_ether.h_dest, ETH_ALEN);
Пересчет контрольных сумм

Контрольные суммы IPv4 и TCP требуют сложения всех 16-битных слов в заголовках, а размер заголовков записан в них, то есть на момент компиляции неизвестен. Это проблема, потому что верификатор не пропустит обычный цикл до переменной границы. Зато размер заголовков ограничен: до 64 байтов каждый. Можно сделать цикл с фиксированным количеством итераций, который может закончиться досрочно.

Замечу, что есть RFC 1624 про то, как пересчитывать контрольную сумму частично, если изменены только фиксированные слова пакетов. Однако способ не универсальный, а реализацию было бы сложнее поддерживать.

Функция расчета контрольной суммы:

Код:
#define MAX_CSUM_WORDS 32
#define MAX_CSUM_BYTES (MAX_CSUM_WORDS * 2)

INTERNAL u32
sum16(const void* data, u32 size, const void* data_end) {
    u32 s = 0;
#pragma unroll
    for (u32 i = 0; i < MAX_CSUM_WORDS; i++) {
        if (2*i >= size) {
            return s; /* normal exit */
        }
        if (data + 2*i + 1 + 1 > data_end) {
            return 0; /* should be unreachable */
        }
        s += ((const u16*)data)[i];
    }
    return s;
}
Несмотря на то, что size проверено вызывающим кодом, второе условие выхода необходимо, чтобы верификатор мог доказать завершение цикла.

Для 32-битных слов реализована более простая версия:

Код:
INTERNAL u32
sum16_32(u32 v) {
    return (v >> 16) + (v & 0xffff);
}
Собственно пересчет контрольных сумм и отправка пакета обратно:

Код:
ip->check = 0;
ip->check = carry(sum16(ip, ip_len, data_end));

u32 tcp_csum = 0;
tcp_csum += sum16_32(ip->saddr);
tcp_csum += sum16_32(ip->daddr);
tcp_csum += 0x0600;
tcp_csum += tcp_len << 8;
tcp->check = 0;
tcp_csum += sum16(tcp, tcp_len, data_end);
tcp->check = carry(tcp_csum);

return XDP_TX;
Функция carry() делает из 32-битной суммы 16-битных слов контрольную сумму, согласно RFC 791.

Проверка рукопожатия TCP

Фильтр корректно устанавливает соединение с netcat, пропуская финальный ACK, на который Linux отвечал RST-пакетом, так как сетевой стек не получал SYN — он был переделан в SYNACK и отправлен обратно - и с точки зрения ОС прибыл пакет, не относящийся к открытым соединениям.

Код:
$ sudo ip netns exec xdp-test   nc -nv 192.0.2.1 6666
192.0.2.1 6666: Connection reset by peer
Важно проверять именно полноценными приложениями и наблюдать tcpdump на xdp-remote потому что, например, hping3 не реагирует на некорректные контрольные суммы.

SYN cookie

С точки зрения XDP сама проверка тривиальна. Алгоритм расчета примитивный и, вероятно, уязвимый для изощренного злоумышленника. Ядро Linux, например, использует криптографический SipHash, но его реализация для XDP явно выходит за рамки статьи.

Появилось для новых TODO, связанных со внешним взаимодействием:

  • XDP-программа не может хранить cookie_seed (секретную часть соли) в глобальной переменной, нужно хранилище в ядре, значение в котором будет периодически обновляться из надежного генератора.
  • При совпадении SYN cookie в ACK-пакете нужно не печатать сообщение, а запоминать IP проверенного клиента, чтобы далее пропускать пакетыот него.

Проверка легитимным клиентом:

Код:
$ sudoip netns exec xdp-test   nc -nv 192.0.2.1 6666
192.0.2.1 6666: Connection reset by peer
В логах зафиксировано прохождение проверки (flags=0x2 — это SYN, flags=0x10 — это ACK):

Код:
Ether(proto=0x800)
  IP(src=0x20e6e11a dst=0x20e6e11e proto=6)
    TCP(sport=50836 dport=6666 flags=0x2)
Ether(proto=0x800)
  IP(src=0xfe2cb11a dst=0xfe2cb11e proto=6)
    TCP(sport=50836 dport=6666 flags=0x10)
      cookie matches for client 20200c0
Пока нет списка проверенных IP, защиты от собственно SYN flood не будет, но вот реакция на ACK flood, запускаемый такой командой:

Код:
sudo ip netns exec xdp-test   hping3 --flood -A -s 1111 -p 2222 192.0.2.1
Записи в логе:

Код:
Ether(proto=0x800)
  IP(src=0x15bd11a dst=0x15bd11e proto=6)
    TCP(sport=3236 dport=2222 flags=0x10)
      cookie mismatch
Заключение

Иногда eBPF вообще и XDP в частности представляется скорее как инструмент продвинутого администратора, нежели как платформа для разработки. Действительно, XDP — инструмент вмешательства в обработку пакетов ядром, а не альтернатива ядерному стеку, как DPDK и прочие варианты kernel bypass. С другой стороны, XDP позволяет реализовать довольно сложную логику, которую, к тому же, легко обновлять без паузы в обработке трафика. Верификатор не создает больших проблем, лично я не отказался бы от такого для частей userspace-кода.

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