Технология eXpress Data Path (XDP) позволява произволна обработка на трафика в интерфейсите на Linux преди пакетите да стигнат до мрежовия стек на ядрото. Приложения на XDP включват защита от DDoS атаки (CloudFlare), сложни филтри и събиране на статистика (Netflix). Програмите на XDP се изпълняват от виртуалната машина eBPF, поради което имат ограничения както на своя код, така и на достъпните функции на ядрото в зависимост от типа на филтъра.
Статията има за цел да запълни недостатъците на многобройните материали относно XDP. На първо място, в тях се предоставя готов код, който веднага заобикаля особеностите на XDP: той е подготвен за верификация или е твърде прост, за да предизвика проблеми. При опит да се напише собствен код от нулата, няма разбиране какво да се прави с типичните грешки. На второ място, не се обсъждат методите за локално тестване на XDP без виртуална машина и „железо“, въпреки че имат свои „подводни камъни“. Текстът е насочен към програмисти, запознати с мрежи и Linux, които се интересуват от XDP и eBPF.
В тази част ще разгледаме подробно как се сглобява 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";Екип , затова потребителите на Windows трябва да инсталират Cygwin или да се справят сами с Visual Studio и библиотеките. събира 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 е необходим, за да свържете нова програма, ако друга вече е свързана. „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 продължава да работи, но работи ли програмата? Нека добавим логовете. Функцията е подобна на 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 нямат секция за данни, следователно единственият начин да кодират форматирана низ е моментни аргументи на командите на ВМ:
$ 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 Echo Request и да спре да показва ICMP Echo Reply. Но не показва. Оказва се, че за работа XDP_TX в програмата на xdp-local , за да има и спарен интерфейс xdp-remote също назначена програма, макар и празна, и той е активиран.
Как разбрах това?
позволява механизмът perf events, между другото, използващ същата виртуална машина, тоест за разборите с 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 Няма такова устройство или адресФункция 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.
Формулиране на задачата
Да преминем към обявената задача: да напишем на 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 пристигат твърде бързо и масово.
Ликбез по 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; /* 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:
11: (7b) *(u64 *)(r10 -48) = r1
12: (71) r3 = *(u8 *)(r7 +13)
недопустимый доступ к пакету, off=13 size=1, R7(id=0,off=0,r=0)
Смещение R7 находится вне пакета
обработано 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;
}Въпреки че размер проверено от код, второто условие за изход е необходимо, за да може валидаторът да докаже завършването на цикъла.
За 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: Връзката беше нулирана от партньораВажно е да се проверява с истински приложения и да се наблюдава 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: Връзката беше нулирана от партньораВ логовете е регистрирано успешното преминаване на проверката (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 съвпада за клиент 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 несъвпадениеЗаключение
Понякога eBPF, и по-специално XDP, се представя по-скоро като инструмент за напреднали администратори, отколкото като платформа за разработка. Наистина, XDP е инструмент за намеса в обработката на пакети от ядрото, а не алтернатива на ядрения стек, какъвто е DPDK и други варианти за заобикаляне на ядрото. От друга страна, XDP позволява реализиране на доста сложна логика, която, освен това, лесно се обновява без прекъсване на обработката на трафика. Верификаторът не създава големи проблеми, лично аз не бих отказал такъв за части от userspace-кода.
Във втората част, ако темата е интересна, ще допълним таблицата с проверени клиенти и разриви в съединенията, ще внедрим броячи и ще напишем userspace-утилита за управление на филтъра.
Връзки:
Източник: habr.com
