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

Технологията eXpress Data Path (XDP) позволява обработката на случаен трафик да се извършва на интерфейси на Linux, преди пакетите да влязат в мрежовия стек на ядрото. Приложение на XDP - защита срещу DDoS атаки (CloudFlare), комплексни филтри, събиране на статистика (Netflix). XDP програмите се изпълняват от виртуалната машина eBPF, така че те имат ограничения както върху своя код, така и върху наличните функции на ядрото в зависимост от типа филтър.

Статията има за цел да запълни недостатъците на много материали за XDP. Първо, те предоставят готов код, който незабавно заобикаля характеристиките на XDP: той е подготвен за проверка или е твърде прост, за да причини проблеми. Когато след това се опитате да напишете своя код от нулата, нямате представа какво да правите с типичните грешки. Второ, начините за локално тестване на XDP без виртуална машина и хардуер не са обхванати, въпреки факта, че имат свои собствени капани. Текстът е предназначен за програмисти, запознати с работата в мрежа и Linux, които се интересуват от XDP и eBPF.

В тази част ще разберем подробно как се сглобява XDP филтърът и как да го тестваме, след което ще напишем проста версия на добре познатия механизъм за бисквитки SYN на ниво обработка на пакети. Все още няма да създаваме „бял ​​списък“.
проверени клиенти, поддържайте броячи и управлявайте филтъра - достатъчно логове.

Ще пишем на C - не е модерно, но е практично. Целият код е достъпен в GitHub чрез връзката в края и е разделен на ангажименти според етапите, описани в статията.

Опровержение. В хода на тази статия ще разработя мини-решение за предотвратяване на DDoS атаки, защото това е реалистична задача за XDP и моята област на експертиза. Основната цел обаче е да се разбере технологията; това не е ръководство за създаване на готова защита. Кодът на урока не е оптимизиран и пропуска някои нюанси.

Кратък преглед на XDP

Ще очертая само ключовите точки, за да не дублирам документация и съществуващи статии.

И така, филтърният код се зарежда в ядрото. Входящите пакети се предават на филтъра. В резултат на това филтърът трябва да вземе решение: да прехвърли пакета в ядрото (XDP_PASS), пуснете пакет (XDP_DROP) или го изпратете обратно (XDP_TX). Филтърът може да промени опаковката, това важи особено за XDP_TX. Можете също така да прекратите програмата (XDP_ABORTED) и нулирайте пакета, но това е аналогично assert(0) - за отстраняване на грешки.

Виртуалната машина eBPF (extended Berkley Packet Filter) е умишлено опростена, така че ядрото да може да провери дали кодът не зацикля и не уврежда паметта на други хора. Кумулативни ограничения и проверки:

  • Примките (назад) са забранени.
  • Има стек за данни, но няма функции (всички C функции трябва да бъдат вградени).
  • Достъпите до паметта извън стека и пакетния буфер са забранени.
  • Размерът на кода е ограничен, но на практика това не е много важно.
  • Разрешени са само извиквания към специални функции на ядрото (eBPF помощници).

Проектирането и инсталирането на филтър изглежда така:

  1. Изходният код (напр kernel.c) се компилира в обект (kernel.o) за архитектурата на виртуална машина eBPF. От октомври 2019 г. компилирането в eBPF се поддържа от Clang и е обещано в GCC 10.1.
  2. Ако този обектен код съдържа извиквания към структури на ядрото (например таблици и броячи), техните идентификатори се заменят с нули, което означава, че такъв код не може да бъде изпълнен. Преди да заредите в ядрото, трябва да замените тези нули с идентификаторите на конкретни обекти, създадени чрез извиквания на ядрото (свържете кода). Можете да направите това с външни помощни програми или можете да напишете програма, която ще свърже и зареди конкретен филтър.
  3. Ядрото проверява заредената програма. Проверява се липсата на цикли и невъзможността за превишаване на границите на пакета и стека. Ако верификаторът не може да докаже, че кодът е правилен, програмата се отхвърля - трябва да можете да му угодите.
  4. След успешна проверка, ядрото компилира обектния код на eBPF архитектурата в машинен код за системната архитектура (точно навреме).
  5. Програмата се свързва с интерфейса и започва да обработва пакети.

Тъй като 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 (виртуален 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, но ще трябва да променят пакетите, което е неудобно за отстраняване на грешки. По-добре е да използвате мрежови пространства от имена (по-нататък netns).

Мрежовото пространство от имена съдържа набор от интерфейси, таблици за маршрутизиране и правила на NetFilter, които са изолирани от подобни обекти в други мрежи. Всеки процес се изпълнява в пространство от имена и има достъп само до обектите на тази мрежа. По подразбиране системата има едно мрежово пространство от имена за всички обекти, така че можете да работите в 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 необходими за свързване на нова програма, ако друга вече е свързана. „Няма новини е добра новина“ не е за тази команда, заключението е обемно във всеки случай. посочете 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: %pn", 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 програмите нямат раздел с данни, така че единственият начин за кодиране на форматиращ низ са непосредствените аргументи на VM командите:

$ python -c "import binascii; print(bytes(reversed(binascii.unhexlify('0a7025203a74656b63617020746f67'))))"
b'got packet: %pn'

Поради тази причина изходът за отстраняване на грешки значително раздува получения код.

Изпращане на XDP пакети

Нека променим филтъра: нека изпраща обратно всички входящи пакети. Това е неправилно от гледна точка на мрежата, тъй като би било необходимо да се променят адресите в заглавките, но сега работата по принцип е важна.

       bpf_printk("got packet: %pn", ctx);
-      return XDP_PASS;
+      return XDP_TX;
   }

Стартиране tcpdump на xdp-remote. Трябва да показва идентична изходяща и входяща ICMP ехо заявка и да спре да показва ICMP ехо отговор. Но не се вижда. Оказва се, че за работа XDP_TX в програмата на xdp-local необходимокъм интерфейса на двойката xdp-remote беше зададена и програма, дори и празна, и той беше вдигнат.

Как разбрах това?

Проследете пътя на пакет в ядрото Механизмът за събития на perf позволява между другото да се използва една и съща виртуална машина, тоест eBPF се използва за разглобяване с eBPF.

Трябва да направите добро от злото, защото няма от какво друго да го направите.

$ sudo perf trace --call-graph dwarf -e 'xdp:*'
   0.000 ping/123455 xdp:xdp_bulk_tx:ifindex=19 action=TX sent=0 drops=1 err=-6
                                     veth_xdp_flush_bq ([veth])
                                     veth_xdp_flush_bq ([veth])
                                     veth_poll ([veth])
                                     <...>

Какво е код 6?

$ errno 6
ENXIO 6 No such device or address

Функция veth_xdp_flush_bq() получава код за грешка от veth_xdp_xmit(), където търсене по ENXIO и намерете коментара.

Нека възстановим минималния филтър (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.

Проблем изявление

Нека да преминем към посочената задача: напишете механизъм за бисквитки SYN на XDP.

SYN flood остава популярна DDoS атака, чиято същност е следната. Когато се установи връзка (TCP ръкостискане), сървърът получава SYN, разпределя ресурси за бъдещата връзка, отговаря със SYNACK пакет и чака ACK. Нападателят просто изпраща хиляди SYN пакети в секунда от фалшиви адреси от всеки хост в многохилядна ботнет. Сървърът е принуден да разпредели ресурси веднага след пристигането на пакета, но ги освобождава след голямо изчакване; в резултат на това паметта или ограниченията са изчерпани, новите връзки не се приемат и услугата е недостъпна.

Ако не разпределяте ресурси въз основа на SYN пакета, а отговаряте само със SYNACK пакет, как тогава сървърът може да разбере, че ACK пакетът, който е пристигнал по-късно, се отнася до SYN пакет, който не е бил запазен? В края на краищата, атакуващият може също да генерира фалшиви ACK. Целта на бисквитката SYN е да я кодира в seqnum параметри на връзката като хеш на адреси, портове и променяща се сол. Ако ACK е успял да пристигне преди промяната на солта, можете да изчислите хеша отново и да го сравните с acknum. Ковачница acknum нападателят не може, тъй като солта включва тайната и няма да има време да я сортира поради ограничения канал.

Бисквитката SYN отдавна е внедрена в ядрото на Linux и дори може да бъде активирана автоматично, ако SYN пристигнат твърде бързо и масово.

Образователна програма за TCP ръкостискане

TCP осигурява предаване на данни като поток от байтове, например HTTP заявките се предават по TCP. Потокът се предава на части в пакети. Всички TCP пакети имат логически флагове и 32-битови поредни номера:

  • Комбинацията от флагове определя ролята на конкретен пакет. Флагът SYN показва, че това е първият пакет на подателя във връзката. Флагът ACK означава, че подателят е получил всички данни за връзка до байта acknum. Един пакет може да има няколко флага и се извиква чрез тяхната комбинация, например SYNACK пакет.

  • Поредният номер (seqnum) указва отместването в потока от данни за първия байт, който се предава в този пакет. Например, ако в първия пакет с X байта данни това число е N, в следващия пакет с нови данни ще бъде N+X. В началото на връзката всяка страна избира този номер на случаен принцип.

  • Номер на потвърждение (acknum) - същото отместване като seqnum, но не определя номера на предавания байт, а номера на първия байт от получателя, който подателят не е видял.

В началото на връзката страните трябва да се споразумеят seqnum и acknum. Клиентът изпраща SYN пакет със своя seqnum = X. Сървърът отговаря със SYNACK пакет, където записва своя seqnum = Y и излага acknum = X + 1. Клиентът отговаря на SYNACK с ACK пакет, където seqnum = X + 1, acknum = Y + 1. След това започва същинският трансфер на данни.

Ако партньорът не потвърди получаването на пакета, TCP го изпраща отново след изчакване.

Защо бисквитките SYN не се използват винаги?

Първо, ако SYNACK или ACK се загубят, ще трябва да изчакате да бъдат изпратени отново - настройката на връзката ще се забави. Второ, в пакета SYN - и само в него! — предават се редица опции, които засягат по-нататъшното функциониране на връзката. Без да помни входящите SYN пакети, сървърът игнорира тези опции; клиентът няма да ги изпрати в следващите пакети. TCP може да работи в този случай, но поне в началния етап качеството на връзката ще намалее.

По отношение на пакетите, XDP програмата трябва да прави следното:

  • отговорете на SYN със SYNACK с бисквитка;
  • отговори на ACK с RST (прекъсване на връзката);
  • изхвърлете останалите пакети.

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

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

един (*) точките, където трябва да управлявате състоянието на системата, са маркирани - на първия етап можете да се справите без тях, като просто внедрите TCP ръкостискане с генериране на SYN бисквитка като seqnum.

На мястото (**), докато нямаме маса, ще пропуснем пакета.

Внедряване на TCP ръкостискане

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

Ще ни трябват мрежови заглавни структури: 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. Всички промени настъпват в 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 не реагира на неправилни контролни суми.

От гледна точка на XDP самата проверка е тривиална. Алгоритъмът за изчисление е примитивен и вероятно е уязвим за сложен нападател. Ядрото на Linux, например, използва криптографския SipHash, но внедряването му за XDP очевидно е извън обхвата на тази статия.

Въведено за нови TODO, свързани с външна комуникация:

  • Програмата XDP не може да съхранява cookie_seed (тайната част на солта) в глобална променлива, имате нужда от хранилище в ядрото, чиято стойност ще се актуализира периодично от надежден генератор.

  • Ако бисквитката SYN съвпада в 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 наводнение, но ето реакцията на ACK наводнение, стартирано от следната команда:

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 и други опции за заобикаляне на ядрото. От друга страна, XDP ви позволява да внедрите доста сложна логика, която освен това е лесна за актуализиране без прекъсване на обработката на трафика. Верификаторът не създава големи проблеми, лично аз не бих го отказал за части от потребителския код.

Във втората част, ако темата е интересна, ще попълним таблицата с проверени клиенти и прекъсвания, ще внедрим броячи и ще напишем помощна програма за потребителско пространство за управление на филтъра.

за справка:

Източник: www.habr.com

Добавяне на нов коментар