Технологията eXpress Data Path (XDP) позволява произволна обработка на трафик на интерфейсите на Linux преди пакетите да влязат в мрежовия стек на ядрото. Приложението на XDP включва защита от DDoS атаки (CloudFlare), сложни филтри и събиране на статистически данни (Netflix). Програмите на XDP се изпълняват от виртуалната машина eBPF, поради което имат ограничения както за своя код, така и за наличните функции на ядрото в зависимост от типа на филтъра.
Статията е предназначена да запълни пропуските в многобройните материали относно XDP. Първо, те предоставят готов код, който мигновено заобикаля специфичните особености на XDP: подготовен е за сертификация или е твърде прост, за да предизвика проблеми. При опит да напишете собствен код от нулата, не се разбира какво да се прави с характерните грешки. Второ, не се обсъждат методите за локално тестване на XDP без виртуални машини и
В тази част ще разгледаме в детайли как се изгражда XDP филтър и как да го тестваме, след което ще напишем прост вариант на известния механизъм SYN cookies на пакетовия слой. За момента няма да създаваме "бял списък"
на проверени клиенти, да поддържаме броячи и да управляваме филтъра — достатъчни са логовете.
Ще пишем на C — това не е модерно, но е практично. Целият код е наличен на GitHub по линка в края и е разделен на комити според етапите, описани в статията.
Отказване. В хода на статията ще разработим мини-решение за отблъскване от DDoS атаки, тъй като това е реалистична задача за XDP и моята област. Основната цел е да се запознаем с технологията, това не е ръководство за създаване на готова защита. Учебният код не е оптимизиран и пропуска някои нюанси.
Кратък преглед на XDP
Ще изложа само ключовите точки, за да не дублирам документацията и съществуващите статии.
Така, кодът на филтъра се зарежда в ядрото. Филтърът получава входящи пакети. В крайна сметка филтърът трябва да вземе решение: да пропусне пакета в ядрото (XDP_PASS), да отхвърли пакета (XDP_DROP) или да го изпрати обратно (XDP_TX). Филтърът може да променя пакета, което е особено актуално за XDP_TX. Също така, може аварийно да прекрати програмата (XDP_ABORTED) и да отхвърли пакета, но това е аналог на assert(0) — за отстраняване на грешки.
Виртуална машина eBPF (разширен Berkley Packet Filter) е специално проектирана да бъде проста, за да може ядрото да провери, че кодът не зацикля и не поврежда чужда памет. Общи ограничения и проверки:
- Циклите (обратни преминавания) са забранени.
- Има стек за данни, но няма функции (всички функции на C трябва да бъдат вградени).
- Забранени са достъпите до памет над пределите на стека и буфера на пакета.
- Размерът на кода е ограничен, но на практика това не е много съществено.
- Разрешени са повиквания само на специални функции на ядрото (eBPF помощни функции).
Разработката и инсталирането на филтъра изглеждат така:
- Изходният код (например,
kernel.c) се компилира в обектен (kernel.o) за архитектурата на виртуалната машина eBPF. Към октомври 2019 компилацията в eBPF се поддържа от Clang и е обещана за GCC 10.1. - Ако в този обектен код има достъпи до структури на ядрото (например, до таблици и счетчици), вместо техните ID стоят нули, тоест такъв код не може да бъде изпълнен. Преди зареждане в ядрото, трябва да замените тези нули с ID на конкретни обекти, създадени чрез повиквания на ядрото (линкване на кода). Може да се направи с външни утилити или да се напише програма, която ще линква и зарежда конкретния филтър.
- Ядрото проверява зарежданата програма. Проверява се отсъствието на цикли и излизане извън границите на пакета и стека. Ако проверяващият не може да докаже, че кодът е коректен, програмата се отхвърля – трябва да се умее да се угодни на него.
- След успешна проверка, ядрото компилира обектния код на архитектурата eBPF в машинен код на системната архитектура (just-in-time).
- Програмата се прикрепя към интерфейса и започва да обработва пакети.
Тъй като XDP работи в ядрото, отстраняването на грешки се извършва чрез логове на трасировката и по самите пакети, които програмата филтрира или генерира. Независимо от това, eBPF осигурява безопасност на заредения код за системата, така че можете да експериментирате с XDP направо на локален Linux.
Подготовка на околната среда
Сглобяване
Clang не може директно да генерира обектен код за архитектурата eBPF, така че процесът се състои от два етапа:
- Компилирайте кода на C в байт-код LLVM (
clang -emit-llvm). - Превърнете байт-кода в обектен код 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 BASEMakefile за 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 ./*.oKDIR съдържа пътя до заглавията на ядрото, 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";Команда направи сглобява 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, но ще се наложи да променят пакетите, което е неудобно по време на дебъгинг. По-добре е да се използват мрежови пространства от имена (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 е необходим, за да свържете нова програма, ако друга вече е свързана. „Без новини е добри новини“ не важи за тази команда, изходът във всеки случай е обемен. Указването на 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 продължава да работи, но работи ли програмата? Нека добавим логове. Функцията е подобна на 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: получен пакет: 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 нямат секция за данни, така че единственият начин да кодирате низ за форматиране е чрез незабавни аргументи на командите на ВМ:
$ python -c "import binascii; print(bytes(reversed(binascii.unhexlify('0a7025203a74656b63617020746f67'))))"
b'получен пакет: %pn'По тази причина отладъчният изход значително увеличава окончателния код.
Изпращане на пакети XDP
Нека променим филтъра: нека той да изпраща всички входящи пакети обратно. Това не е коректно от мрежова гледна точка, тъй като адресите в заглавките трябва да се променят, но сега е важна основната работа.
bpf_printk("получен пакет: %pn", ctx);
- return XDP_PASS;
+ return XDP_TX;
}Започваме tcpdump на xdp-remote. Той трябва да показва идентични изходящи и входящи ICMP Echo Request и да спре да показва ICMP Echo Reply. Но не показва. Оказва се, че за работа XDP_TX в програмата на xdp-local , за да е назначена програма на парния интерфейс, дори и да е празна, и да е активиран. xdp-remote Как разбрах това?
Проследяване на маршрута на пакета в ядрото
Трябва да направиш добро от злото, защото друго няма от какво да се направи.
$ 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 Няма такова устройство или адрес
veth_xdp_flush_bq()Функция получава кода на грешка от veth_xdp_xmit() , където търсим поENXIO и намираме коментара. Възстановим минималния филтър (
) в файлаXDP_PASSxdp_dummy.c , добавяме го в Makefile, свързваме сip netns exec remote ip link set dev int xdp object dummy.o xdp-remote:
показва това, което се очаква:Сега tcpdump 62:57:8e:70:44:64 > 26:0e:25:37:8f:96, ethertype IPv4 (0x0800), дължина 98: (tos 0x0, ttl 64, id 13762, offset 0, флагове [DF], протокол ICMP (1), дължина 84) 192.0.2.2 > 192.0.2.1: ICMP echo request, id 46966, seq 1, дължина 64 62:57:8e:70:44:64 > 26:0e:25:37:8f:96, ethertype IPv4 (0x0800), дължина 98: (tos 0x0, ttl 64, id 13762, offset 0, флагове [DF], протокол ICMP (1), дължина 84) 192.0.2.2 > 192.0.2.1: ICMP echo request, id 46966, seq 1, дължина 64
62:57:8e:70:44:64 > 26:0e:25:37:8f:96, ethertype IPv4 (0x0800), дължина 98: (tos 0x0, ttl 64, id 13762, offset 0, флагове [DF], протокол ICMP (1), дължина 84)
192.0.2.2 > 192.0.2.1: ICMP echo request, id 46966, seq 1, дължина 64
62:57:8e:70:44:64 > 26:0e:25:37:8f:96, ethertype IPv4 (0x0800), дължина 98: (tos 0x0, ttl 64, id 13762, offset 0, флагове [DF], протокол ICMP (1), дължина 84)
192.0.2.2 > 192.0.2.1: ICMP echo request, id 46966, seq 1, дължина 64Ако вместо това се показват само ARP, трябва да се премахнат филтрите (това прави sudo .\/stand detach), да се пусне ping, след което да се зададат филтрите и да се опита отново. Проблемът е, че филтърът XDP_TX също влияе на ARP, и ако стекът
наименованията xdp-test е успял да «запомни» MAC-адрес 192.0.2.1, той няма да може да разреши този IP.
Формулиране на задачата
Да преминем към заявената задача: да напишем механизъм за SYN cookies на XDP.
Досега популярната DDoS-атака остава SYN flood, чиято същност е следната. При установяване на връзка (TCP handshake) сървърът получава SYN, разпределя ресурси за бъдещата връзка, отговаря с SYNACK пакет и очаква ACK. Атакуващият просто изпраща SYN пакети от фалшиви адреси в количество хиляди в секунда от всеки хост от многотысячен ботнет. Сървърът е принуден да разпредели ресурси веднага след пристигането на пакета, а освобождава по голям таймаут, в резултат на което се изчерпват паметта или лимитите, новите връзки не се приемат, услугата е недостъпна.
Ако не се разпределят ресурси за SYN пакета, а само се отговаря със SYNACK пакет, как тогава сървърът да разбере, че ACK пакетът, дошъл по-късно, е свързан с SYN пакета, който не е запазен? В крайна сметка атакуващият може да генерира и фалшиви ACK. Същността на SYN cookie е да кодира в seqnum параметри на съединението като хеш от адреси, портове и променлива сол. Ако ACK е успял да дойде преди смяната на солта, можем отново да изчислим хеша и да го сравним с acknum. Подправяне acknum атакуващият не може, тъй като солта включва тайна, а да преглежда не може заради ограничен канал.
SYN cookie отдавна е реализиран в ядрото на Linux и дори може да се включва автоматично, ако SYN идват твърде бързо и масово.
Ликбез за TCP handshake
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 cookies не се използват винаги?
На първо място, ако загубим SYNACK или ACK, ще трябва да изчакаме повторното изпращане — това забавя установяването на връзката. На второ място, в SYN-пакета — и само в него! — се предават редица опции, които влияят на по-нататъшната работа на връзката. Не запомняйки входящите SYN-пакети, сървърът игнорира тези опции, а в следващите пакети клиентът вече няма да ги изпрати. TCP все пак може да работи, но поне в началния етап качеството на връзката ще намалее.
От гледна точка на пакетите, 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; /* неправилно форматиран пакет */
}
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, програмата ще компилира, но при зареждане ще има грешка при проверката:
Анализ на вернитора:
11: (7b) *(u64 *)(r10 -48) = r1
12: (71) r3 = *(u8 *)(r7 +13)
невалиден достъп до пакета, off=13 size=1, R7(id=0,off=0,r=0)
R7 offset е извън пакета
постепенни 11 инструкции (лимит 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0
Грешка при извличане на програма/карта!Ключова линия невалиден достъп до пакета, 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; /* невалиден пакет */
}
if (ip_len > MAX_CSUM_BYTES) {
return XDP_ABORTED; /* ограничение на реализацията */
}
const u32 tcp_len = tcp->doff * 4;
if ((void*)tcp + tcp_len > (void*)ctx->data_end) {
return XDP_DROP; /* невалиден пакет */
}
if (tcp_len > MAX_CSUM_BYTES) {
return XDP_ABORTED; /* ограничение на реализацията */
}Разгръщане на пакета
Запълваме 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 байта всеки. Може да се направи цикъл с фиксиран брой итерации, който може да завърши предсрочно.
Ще отбележа, че има относно това как да се пресмятат контролни суми частично, ако са променени само фиксирани думи на пакетите. Въпреки това, методът не е универсален, а реализирането му би било по-трудно за поддържане.
Функция за изчисление на контролна сума:
#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 <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 адресът на проверения клиент, за да се пропускат по-нататък пакети от него.
Проверка от легитимния клиент:
$ sudo ip 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 за клиент 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 утилита за управление на филтъра.
Линкове:
Източник: habr.com
