Пишуваме заштита од DDoS напади на XDP. Нуклеарен дел

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

Написот е наменет да ги пополни недостатоците на бројните материјали на XDP. Прво, тие обезбедуваат готов код кој веднаш ги заобиколува карактеристиките на XDP: тој е подготвен за верификација или е премногу едноставен за да предизвика проблеми. Кога потоа ќе се обидете да го напишете вашиот код од нула, немате идеја што да правите со типични грешки. Второ, начините за локално тестирање на XDP без VM и хардвер не се опфатени, и покрај фактот што тие имаат свои стапици. Текстот е наменет за програмери запознаени со мрежи и Linux кои се заинтересирани за XDP и eBPF.

Во овој дел детално ќе разбереме како се составува филтерот XDP и како да го тестираме, а потоа ќе напишеме едноставна верзија на добро познатиот механизам за колачиња SYN на ниво на обработка на пакети. Сè уште нема да креираме „бел список“.
проверени клиенти, чувајте бројачи и управувајте со филтерот - доволно дневници.

Ќе пишуваме во C - не е модерно, но е практично. Целиот код е достапен на GitHub преку врската на крајот и е поделен на обврски според фазите опишани во статијата.

Одрекување од одговорност. Во текот на овој напис, ќе развијам мини-решение за спречување на DDoS напади, бидејќи ова е реална задача за XDP и за мојата област на експертиза. Сепак, главната цел е да се разбере технологијата, ова не е водич за создавање готова заштита; Кодот за упатство не е оптимизиран и испушта некои нијанси.

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

Ќе ги наведам само клучните точки за да не се дуплираат документацијата и постоечките написи.

Значи, кодот на филтерот е вчитан во кернелот. Дојдовните пакети се пренесуваат на филтерот. Како резултат на тоа, филтерот мора да донесе одлука: да го пренесе пакетот во кернелот (XDP_PASS), фрли го пакетот (XDP_DROP) или испратете го назад (XDP_TX). Филтерот може да го промени пакетот, ова е особено точно за XDP_TX. Можете исто така да ја прекинете програмата (XDP_ABORTED) и ресетирајте го пакетот, но ова е аналогно assert(0) - за дебагирање.

Виртуелната машина eBPF (продолжен Берклиски филтер за пакети) е намерно едноставна за да може кернелот да провери дали кодот не се врти и не ја оштетува меморијата на другите луѓе. Кумулативни ограничувања и проверки:

  • Јамките (наназад) се забранети.
  • Има стек за податоци, но нема функции (сите функции C мора да бидат вметнати).
  • Пристапите до меморијата надвор од оџакот и баферот на пакетите се забранети.
  • Големината на кодот е ограничена, но во пракса тоа не е многу значајно.
  • Дозволени се само повици до специјални функции на кернелот (помошници на eBPF).

Дизајнирањето и инсталирањето на филтер изгледа вака:

  1. Изворниот код (на пр kernel.c) се компајлира во објект (kernel.o) за архитектурата на виртуелната машина eBPF. Од октомври 2019 година, компилацијата за eBPF е поддржана од Clang и ветена во GCC 10.1.
  2. Ако овој објектен код содржи повици до структури на јадрото (на пример, табели и бројачи), нивните ID се заменуваат со нули, што значи дека таквиот код не може да се изврши. Пред да ги вчитате во кернелот, треба да ги замените овие нули со ИД на одредени објекти создадени преку повици на јадрото (поврзете го кодот). Можете да го направите ова со надворешни комунални услуги, или можете да напишете програма што ќе поврзе и вчита одреден филтер.
  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

Макефајл за 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 (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 (виртуелен етернет) се погодни за нас: ова се пар виртуелни мрежни интерфејси „поврзани“ директно еден со друг. Можете да ги креирате вака (во овој дел сите команди 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 достапен за овој процес. Ова исто така функционира во спротивна насока.

Кога се движите помеѓу мрежи, интерфејсот се спушта и ја губи својата адреса. За да го конфигурирате интерфејсот во мрежите, треба да извршите 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.

Пинг сега треба да активира пораки како оваа:

<...>-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 Echo Reply. Но, тоа не се покажува. Излегува дека за работа 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

Ако наместо тоа се прикажани само ARPs, треба да ги отстраните филтрите (ова прави 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 одамна е имплементирано во кернелот на Линукс, па дури може и автоматски да се овозможи доколку SYN-овите пристигнуваат премногу брзо и масовно.

Едукативна програма за TCP ракување

TCP обезбедува пренос на податоци како поток од бајти, на пример, HTTP барањата се пренесуваат преку TCP. Потокот се пренесува на парчиња во пакети. Сите TCP пакети имаат логички знаменца и 32-битни секвентни броеви:

  • Комбинацијата на знаменца ја одредува улогата на одреден пакет. Знамето SYN покажува дека ова е првиот пакет на испраќачот на врската. Знамето ACK значи дека испраќачот ги примил сите податоци за поврзување до бајтот acknum. Еден пакет може да има неколку знаменца и се нарекува според нивната комбинација, на пример, пакет SYNACK.

  • Секвенцискиот број (секнум) го одредува поместувањето во протокот на податоци за првиот бајт што се пренесува во овој пакет. На пример, ако во првиот пакет со 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 колаче како секнум.

На лице место (**), додека немаме маса ќе го прескокнеме пакетот.

Имплементирање на TCP ракување

Парсирање на пакетот и потврдување на кодот

Ќе ни требаат структури за заглавија на мрежата: етернет (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);
}

Ви го обрнувам вниманието на проверките означени со А и Б. Ако коментирате А, програмата ќе се изгради, но ќе има грешка при верификацијата при вчитувањето:

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

Додадете коментар