Технологията 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 помощници).
Проектирането и инсталирането на филтър изглежда така:
Изходният код (напр kernel.c) се компилира в обект (kernel.o) за архитектурата на виртуална машина eBPF. От октомври 2019 г. компилирането в eBPF се поддържа от Clang и е обещано в GCC 10.1.
Ако този обектен код съдържа извиквания към структури на ядрото (например таблици и броячи), техните идентификатори се заменят с нули, което означава, че такъв код не може да бъде изпълнен. Преди да заредите в ядрото, трябва да замените тези нули с идентификаторите на конкретни обекти, създадени чрез извиквания на ядрото (свържете кода). Можете да направите това с външни помощни програми или можете да напишете програма, която ще свърже и зареди конкретен филтър.
Ядрото проверява заредената програма. Проверява се липсата на цикли и невъзможността за превишаване на границите на пакета и стека. Ако верификаторът не може да докаже, че кодът е правилен, програмата се отхвърля - трябва да можете да му угодите.
След успешна проверка, ядрото компилира обектния код на eBPF архитектурата в машинен код за системната архитектура (точно навреме).
Програмата се свързва с интерфейса и започва да обработва пакети.
Тъй като XDP работи в ядрото, отстраняването на грешки се извършва с помощта на журнали за проследяване и всъщност пакети, които програмата филтрира или генерира. eBPF обаче гарантира, че изтегленият код е защитен за системата, така че можете да експериментирате с XDP директно на вашия локален Linux.
Подготовка на околната среда
монтаж
Clang не може директно да произведе обектен код за eBPF архитектурата, така че процесът се състои от две стъпки:
Компилирайте C код в байт код на LLVM (clang -emit-llvm).
Конвертиране на байт код в eBPF обектен код (llc -march=bpf -filetype=obj).
Когато пишете филтър, няколко файла със спомагателни функции и макроси ще бъдат полезни от тестовете на ядрото. Важно е те да съответстват на версията на ядрото (KVER). Изтеглете ги на helpers/:
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 е ограничен.
Нека започнем с филтър, който пропуска всички пакети и не прави нищо:
Отбор 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() опростява разговора.
Факт е, че eBPF програмите нямат раздел с данни, така че единственият начин за кодиране на форматиращ низ са непосредствените аргументи на VM командите:
Поради тази причина изходът за отстраняване на грешки значително раздува получения код.
Изпращане на XDP пакети
Нека променим филтъра: нека изпраща обратно всички входящи пакети. Това е неправилно от гледна точка на мрежата, тъй като би било необходимо да се променят адресите в заглавките, но сега работата по принцип е важна.
Стартиране tcpdump на xdp-remote. Трябва да показва идентична изходяща и входяща ICMP ехо заявка и да спре да показва ICMP ехо отговор. Но не се вижда. Оказва се, че за работа XDP_TX в програмата на xdp-localнеобходимокъм интерфейса на двойката xdp-remote беше зададена и програма, дори и празна, и той беше вдигнат.
Как разбрах това?
Проследете пътя на пакет в ядрото Механизмът за събития на perf позволява между другото да се използва една и съща виртуална машина, тоест eBPF се използва за разглобяване с eBPF.
Трябва да направите добро от злото, защото няма от какво друго да го направите.
Ако вместо това се показват само 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 верификаторът в ядрото забранява обратно проследяване, което всъщност е цикли и извиквания на функции.
макрос LOG() забранява печатането в компилацията на изданието.
Програмата е конвейер от функции. Всеки получава пакет, в който съответното заглавие на ниво е маркирано, например, process_ether() очаква да бъде запълнена ether. Въз основа на резултатите от анализа на полето, функцията може да прехвърли пакета на по-високо ниво. Резултатът от функцията е XDP действието. Засега манипулаторите SYN и ACK пропускат всички пакети.
Ключов низ invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0): Има пътеки за изпълнение, когато тринадесетият байт от началото на буфера е извън пакета. Трудно е да се разбере от списъка за кой ред говорим, но има номер на инструкция (12) и дизасемблер, показващ редовете на изходния код:
от което става ясно, че проблемът е ether. Винаги щеше да е така.
Отговор на SYN
Целта на този етап е да се генерира правилен SYNACK пакет с фиксирана seqnum, която в бъдеще ще бъде заменена от бисквитката SYN. Всички промени настъпват в process_tcp_syn() и околните райони.
Проверка на пакета
Колкото и да е странно, ето най-забележителният ред или по-скоро коментарът към него:
При писането на първата версия на кода е използвано ядрото 5.1, за чийто верификатор имаше разлика между data_end и (const void*)ctx->data_end. Към момента на писане, ядрото 5.3.1 не е имало този проблем. Възможно е компилаторът да е осъществявал достъп до локална променлива по различен начин от поле. Морал на историята: Опростяването на кода може да помогне, когато има много влагане.
Следват рутинни проверки на дължината за славата на верификатора; О MAX_CSUM_BYTES по-долу.
Разменете TCP портове, IP адрес и MAC адреси. Стандартната библиотека не е достъпна от програмата XDP, така че memcpy() — макрос, който скрива вътрешността на Clang.
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);
}
Всъщност преизчисляване на контролните суми и изпращане на пакета обратно:
Функция 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 бисквитка
От гледна точка на XDP самата проверка е тривиална. Алгоритъмът за изчисление е примитивен и вероятно е уязвим за сложен нападател. Ядрото на Linux, например, използва криптографския SipHash, но внедряването му за XDP очевидно е извън обхвата на тази статия.
Въведено за нови TODO, свързани с външна комуникация:
Програмата XDP не може да съхранява cookie_seed (тайната част на солта) в глобална променлива, имате нужда от хранилище в ядрото, чиято стойност ще се актуализира периодично от надежден генератор.
Ако бисквитката SYN съвпада в ACK пакета, не е необходимо да отпечатвате съобщение, но запомните IP адреса на верифицирания клиент, за да продължите да предавате пакети от него.
Въпреки че няма списък с проверени IP адреси, няма да има защита от самото SYN наводнение, но ето реакцията на ACK наводнение, стартирано от следната команда:
sudo ip netns exec xdp-test hping3 --flood -A -s 1111 -p 2222 192.0.2.1
Понякога eBPF като цяло и XDP в частност се представят повече като усъвършенстван администраторски инструмент, отколкото като платформа за разработка. Всъщност XDP е инструмент за намеса в обработката на пакети от ядрото, а не алтернатива на стека на ядрото, като DPDK и други опции за заобикаляне на ядрото. От друга страна, XDP ви позволява да внедрите доста сложна логика, която освен това е лесна за актуализиране без прекъсване на обработката на трафика. Верификаторът не създава големи проблеми, лично аз не бих го отказал за части от потребителския код.
Във втората част, ако темата е интересна, ще попълним таблицата с проверени клиенти и прекъсвания, ще внедрим броячи и ще напишем помощна програма за потребителско пространство за управление на филтъра.