Пішам абарону ад DDoS-нападаў на XDP. Ядзерная частка

Тэхналогія 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 (extended Berkley Packet Filter) спецыяльна зроблена простай, каб ядро ​​магло праверыць, што код не зацыкляецца і не пашкоджвае чужую памяць. Сукупныя абмежаванні і праверкі:

  • Забароненыя цыклы (пераходы назад).
  • Ёсць стэк для дадзеных, але няма функцый (усе функцыі C павінны ўбудоўвацца).
  • Забаронены звароты да памяці за межамі стэка і буфера пакета.
  • Памер кода абмежаваны, але на практыцы гэта не вельмі істотна.
  • Дазволены выклікі толькі спецыяльных функцый ядра (eBPF helpers).

Распрацоўка і ўстаноўка фільтра выглядаюць так:

  1. Зыходны код (напрыклад, kernel.c) кампілюецца ў аб'ектны (kernel.o) пад архітэктуру віртуальнай машыны eBPF. На кастрычнік 2019 года кампіляцыя ў eBPF падтрымліваецца Clang і абяцана ў GCC 10.1.
  2. Калі ў гэтым аб'ектным кодзе ёсць звароты да структур ядра (напрыклад, да табліц і лічыльнікам), замест іх ID стаяць нулі, гэта значыць выканаць такі код нельга. Перад загрузкай у ядро ​​трэба гэтыя нулі замяніць на ID пэўных аб'ектаў, створаных праз выклікі ядра (злінкаваць код). Можна зрабіць гэта вонкавымі ўтылітамі, а можна напісаць праграму, якая будзе лінкаваць і загружаць пэўны фільтр.
  3. Ядро верыфікуе загружаную праграму. Правяраецца адсутнасць цыклаў і нявыхад за межы пакета і стэка. Калі верыфікатар не можа даказаць, што код карэктны, праграма адпрэчваецца, - трэба ўмець лашчыць яго.
  4. Пасля паспяховай верыфікацыі ядро ​​кампілюе аб'ектны код архітэктуры eBPF у машынны код сістэмнай архітэктуры (just-in-time).
  5. Праграма прымацоўваецца да інтэрфейсу і пачынае апрацоўваць пакеты.

Паколькі XDP працуе ў ядры, адладка вядзецца па логах трасіроўкі і, уласна, па пакетах, якія праграма фільтруе ці генеруе. Тым не менш, eBPF забяспечвае бяспеку загружанага кода для сістэмы, таму эксперыментаваць з XDP можна прама на лакальным Linux.

Падрыхтоўка асяроддзя

зборка

Clang не можа наўпрост выдаваць аб'ектны код для архітэктуры eBPF, таму працэс складаецца з двух крокаў:

  1. Скампіляваць код на C у байт-код LLVM (clang -emit-llvm).
  2. Пераўтварыць байт-код у аб'ектны код eBPF (llc -march=bpf -filetype=obj).

Пры напісанні фільтра спатрэбіцца пара файлаў з дапаможнымі функцыямі і макрасамі. з тэстаў ядра. Важна, каб яны адпавядалі версіі ядра (KVER). Качаем іх у helpers/:

export KVER=v5.3.7
export BASE=https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/plain/tools/testing/selftests/bpf
wget -P helpers --content-disposition "${BASE}/bpf_helpers.h?h=${KVER}" "${BASE}/bpf_endian.h?h=${KVER}"
unset KVER BASE

Makefile для Arch Linux (ядро 5.3.7):

CLANG ?= clang
LLC ?= llc

KDIR ?= /lib/modules/$(shell uname -r)/build
ARCH ?= $(subst x86_64,x86,$(shell uname -m))

CFLAGS = 
    -Ihelpers 
    
    -I$(KDIR)/include 
    -I$(KDIR)/include/uapi 
    -I$(KDIR)/include/generated/uapi 
    -I$(KDIR)/arch/$(ARCH)/include 
    -I$(KDIR)/arch/$(ARCH)/include/generated 
    -I$(KDIR)/arch/$(ARCH)/include/uapi 
    -I$(KDIR)/arch/$(ARCH)/include/generated/uapi 
    -D__KERNEL__ 
    
    -fno-stack-protector -O2 -g

xdp_%.o: xdp_%.c Makefile
    $(CLANG) -c -emit-llvm $(CFLAGS) $< -o - | 
    $(LLC) -march=bpf -filetype=obj -o $@

.PHONY: all clean

all: xdp_filter.o

clean:
    rm -f ./*.o

KDIR змяшчае шлях да загалоўкаў ядра, ARCH - архітэктуру сістэмы. Шляхі і прылады могуць крыху адрознівацца паміж дыстрыбутывамі.

Прыклад адрозненняў для Debian 10 (ядро 4.19.67)

# другая команда
CLANG ?= clang
LLC ?= llc-7

# другой каталог
KDIR ?= /usr/src/linux-headers-$(shell uname -r)
ARCH ?= $(subst x86_64,x86,$(shell uname -m))

# два дополнительных каталога -I
CFLAGS = 
    -Ihelpers 
    
    -I/usr/src/linux-headers-4.19.0-6-common/include 
    -I/usr/src/linux-headers-4.19.0-6-common/arch/$(ARCH)/include 
    # далее без изменений

CFLAGS падлучаюць дырэкторыю з дапаможнымі загалоўкамі і некалькі дырэкторый з загалоўкамі ядра. Сімвал __KERNEL__ азначае, што загалоўкі UAPI (userspace API) вызначаюцца для кода ядра, бо фільтр выконваецца ў ядры.

Абарону стэка можна адключыць (-fno-stack-protector), таму што верыфікатар кода eBPF усё роўна правярае нявыхад за межы стэка. Адразу варта ўключыць аптымізацыі, бо памер байт-кода eBPF абмежаваны.

Пачнём з фільтра, які прапускае ўсе пакеты і нічога не робіць:

#include <uapi/linux/bpf.h>

#include <bpf_helpers.h>

SEC("prog")
int xdp_main(struct xdp_md* ctx) {
    return XDP_PASS;
}

char _license[] SEC("license") = "GPL";

Каманда make збірае xdp_filter.o. Дзе яго зараз выпрабаваць?

Выпрабавальны стэнд

Стэнд павінен уключаць два інтэрфейсы: на якім будзе фільтр і з якога будуць адпраўляцца пакеты. Гэта павінны быць паўнавартасныя прылады Linux са сваімі IP, каб правяраць, як звычайныя прыкладанні працуюць з нашым фільтрам.

Прылады тыпу veth (virtual 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 працягвае працаваць, але ці працуе праграма? Дададзім логі. Функцыя bpf_trace_printk() падобная на printf(), але падтрымлівае за ўсё да трох аргументаў, акрамя шаблону, і абмежаваны спіс спецыфікатараў. Макрос bpf_printk() спрашчае выклік.

   SEC("prog")
   int xdp_main(struct xdp_md* ctx) {
+      bpf_printk("got packet: %pn", ctx);
       return XDP_PASS;
   }

Выснова ідзе ў канал трасіроўкі ядра, які трэба ўключыць:

echo -n 1 | sudo tee /sys/kernel/debug/tracing/options/trace_printk

Прагляд патоку паведамленняў:

cat /sys/kernel/debug/tracing/trace_pipe

Абедзве гэтых каманды робіць выклік sudo ./stand log.

Ping зараз павінен выклікаць у ім такія паведамленні:

<...>-110930 [004] ..s1 78803.244967: 0: got packet: 00000000ac510377

Калі дагледзецца да высновы верыфікатара, можна заўважыць дзіўныя вылічэнні:

0: (bf) r3 = r1
1: (18) r1 = 0xa7025203a7465
3: (7b) *(u64 *)(r10 -8) = r1
4: (18) r1 = 0x6b63617020746f67
6: (7b) *(u64 *)(r10 -16) = r1
7: (bf) r1 = r10
8: (07) r1 += -16
9: (b7) r2 = 16
10: (85) call bpf_trace_printk#6
<...>

Справа ў тым, што ў праграм на eBPF няма секцыі дадзеных, таму адзіны спосаб закадзіраваць фарматны радок — immediate-аргументы каманд ВМ:

$ 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 No such device or address

Функцыя veth_xdp_flush_bq() атрымлівае код памылкі ад veth_xdp_xmit(), дзе пошукам па ENXIO і знаходзім каментар.

Адновім мінімальны фільтр (XDP_PASS) у файле xdp_dummy.c, дадамо яго ў Makefile, прывяжам да xdp-remote:

ip netns exec remote 
    ip link set dev int xdp object dummy.o

Цяпер tcpdump паказвае тое, што чакаецца:

62:57:8e:70:44:64 > 26:0e:25:37:8f:96, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 64, id 13762, offset 0, flags [DF], proto ICMP (1), length 84)
    192.0.2.2 > 192.0.2.1: ICMP echo request, id 46966, seq 1, length 64
62:57:8e:70:44:64 > 26:0e:25:37:8f:96, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 64, id 13762, offset 0, flags [DF], proto ICMP (1), length 84)
    192.0.2.2 > 192.0.2.1: ICMP echo request, id 46966, seq 1, length 64

Калі замест гэтага паказваюцца толькі ARP, трэба прыбраць фільтры (гэта робіць sudo ./stand detach), пусціць ping, Затым ўсталяваць фільтры і паспрабаваць зноў. Праблема ў тым, што фільтр XDP_TX дзейнічае і на ARP, і калі стэк
прасторы імёнаў xdp-test паспеў "забыцца" MAC-адрас 192.0.2.1, ён не зможа дазволіць гэты IP.

Пастаноўка задачы

Пяройдзем да заяўленай задачы: напісаць на 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 забяспечвае перадачу дадзеных як струменя байтаў, напрыклад, па-над TCP перадаюцца HTTP-запыты. Струмень перадаецца па кавалачках у пакетах. Ва ўсіх пакетаў TCP ёсць лагічныя сцягі і 32-бітныя нумары паслядоўнасцяў:

  • Камбінацыя сцягоў вызначае ролю канкрэтнага пакета. Сцяг SYN азначае, што гэта першы пакет адпраўшчыка ў злучэнні. Сцяг ACK азначае, што адпраўнік атрымаў усе дадзеныя злучэнні да байта. acknum. Пакет можа мець некалькі сцягоў і завецца па іх камбінацыі, напрыклад, SYNACK-пакет.

  • Sequence number (seqnum) вызначае зрушэнне ў струмені дадзеных для першага байта, які перадаецца ў гэтым пакеце. Напрыклад, калі ў першым пакеце з X байтамі дадзеных гэты нумар быў N, у наступным пакеце з новымі дадзенымі ён будзе N+X. У пачатку злучэння кожны бок выбірае гэты нумар адвольнай выявай.

  • Acknowledgement number (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 з кукі;
  • на 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 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 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; /* 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 cookie у 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 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

Дадаць каментар