Бид XDP дээр DDoS халдлагаас хамгаалах хамгаалалт бичиж байна. Цөмийн хэсэг

eXpress Data Path (XDP) технологи нь пакетууд цөмийн сүлжээний стек рүү орохоос өмнө Linux интерфейс дээр санамсаргүй траффик боловсруулах боломжийг олгодог. XDP програм - DDoS халдлагаас хамгаалах (CloudFlare), нарийн төвөгтэй шүүлтүүрүүд, статистик цуглуулга (Netflix). XDP программуудыг eBPF виртуал машин гүйцэтгэдэг тул шүүлтүүрийн төрлөөс хамааран тэдгээрийн код болон цөмийн функцүүдэд хязгаарлалт тавьдаг.

Энэхүү нийтлэл нь XDP-ийн талаархи олон тооны материалын дутагдлыг нөхөх зорилготой юм. Нэгдүгээрт, тэд XDP-ийн онцлог шинж чанаруудыг нэн даруй алгасах бэлэн кодыг өгдөг: энэ нь шалгахад бэлтгэгдсэн эсвэл асуудал үүсгэхэд хэтэрхий энгийн байдаг. Дараа нь та кодоо эхнээс нь бичих гэж оролдох үед ердийн алдаатай юу хийхээ мэдэхгүй байна. Хоёрдугаарт, XDP-ийг VM болон техник хангамжгүйгээр орон нутагт турших арга замууд нь өөрийн гэсэн алдаатай байдаг ч тусгагдаагүй болно. Текст нь XDP болон eBPF-ийг сонирхдог сүлжээ болон Линуксийг мэддэг програмистуудад зориулагдсан болно.

Энэ хэсэгт бид XDP шүүлтүүрийг хэрхэн угсарч, хэрхэн турших талаар нарийвчлан ойлгох болно, дараа нь пакет боловсруулах түвшинд алдартай SYN күүки механизмын энгийн хувилбарыг бичих болно. Бид "цагаан жагсаалт"-ыг хараахан гаргахгүй
баталгаажуулсан үйлчлүүлэгчид, тоолуур хөтлөх, шүүлтүүрийг удирдах - хангалттай бүртгэлүүд.

Бид C хэл дээр бичих болно - энэ нь загварлаг биш, гэхдээ энэ нь практик юм. Бүх кодыг GitHub дээр төгсгөлд байгаа холбоосоор авах боломжтой бөгөөд нийтлэлд тайлбарласан үе шатуудын дагуу амлалтуудад хуваагдана.

Disclaimer. Энэ нийтлэлийн туршид би DDoS халдлагаас сэргийлэх мини шийдлийг боловсруулах болно, учир нь энэ нь XDP болон миний мэргэшсэн салбарт бодитой ажил юм. Гэхдээ гол зорилго бол технологийг ойлгох явдал бөгөөд энэ нь бэлэн хамгаалалтыг бий болгох гарын авлага биш юм. Сургалтын код нь оновчтой биш бөгөөд зарим нарийн ширийн зүйлийг орхигдуулсан.

XDP товч тойм

Баримт бичиг болон одоо байгаа нийтлэлүүдийг давтахгүйн тулд би зөвхөн гол санааг тоймлох болно.

Тиймээс шүүлтүүрийн кодыг цөмд ачаална. Ирж буй пакетуудыг шүүлтүүрт дамжуулдаг. Үүний үр дүнд шүүлтүүр нь шийдвэр гаргах ёстой: пакетыг цөм рүү дамжуулах (XDP_PASS), буулгах пакет (XDP_DROP) эсвэл буцааж илгээх (XDP_TX). Шүүлтүүр нь багцыг өөрчлөх боломжтой бөгөөд энэ нь ялангуяа үнэн юм XDP_TX. Та мөн програмыг зогсоож болно (XDP_ABORTED) болон багцыг дахин тохируулах боловч энэ нь ижил төстэй юм assert(0) - дибаг хийх зориулалттай.

eBPF (өргөтгөсөн Berkley Packet Filter) виртуал машиныг зориудаар энгийн болгосон тул цөм нь код давтагдахгүй, бусад хүмүүсийн санах ойг гэмтээхгүй байхыг шалгах боломжтой. Хуримтлагдсан хязгаарлалт ба шалгалтууд:

  • Гогцоо (буцаж) хийхийг хориглоно.
  • Өгөгдлийн стек байгаа боловч ямар ч функц байхгүй (бүх C функцууд нь доторлогоотой байх ёстой).
  • Стек болон пакет буферээс гадна санах ойд хандахыг хориглоно.
  • Кодын хэмжээ хязгаарлагдмал боловч бодит байдал дээр энэ нь тийм ч чухал биш юм.
  • Зөвхөн цөмийн тусгай функцууд руу залгахыг зөвшөөрдөг (eBPF туслагч).

Шүүлтүүрийг зохион бүтээх, суурилуулах нь дараах байдалтай байна.

  1. Эх код (жишээ нь kernel.c) объект болгон эмхэтгэсэн (kernel.o) eBPF виртуал машины архитектурт зориулсан. 2019 оны 10.1-р сарын байдлаар eBPF-ийн эмхэтгэлийг Clang дэмждэг бөгөөд GCC XNUMX-д амласан.
  2. Хэрэв энэ объектын код нь цөмийн бүтцэд (жишээ нь, хүснэгт, тоолуур) дуудлагыг агуулж байвал тэдгээрийн ID-г тэгээр солих бөгөөд энэ нь ийм кодыг гүйцэтгэх боломжгүй гэсэн үг юм. Цөм рүү ачаалахын өмнө та эдгээр тэгүүдийг цөмийн дуудлагаар үүсгэсэн тодорхой объектуудын ID-аар солих хэрэгтэй (кодыг холбоно уу). Та үүнийг гадны хэрэгслүүдээр хийж болно, эсвэл тодорхой шүүлтүүрийг холбож, ачаалах програм бичиж болно.
  3. Цөм нь ачаалагдсан програмыг шалгадаг. Цикл байхгүй, пакет болон стекийн хил хязгаарыг давж чадаагүй эсэхийг шалгана. Хэрэв баталгаажуулагч код зөв болохыг баталж чадахгүй бол програм татгалзсан болно - та түүнд таалагдах чадвартай байх хэрэгтэй.
  4. Баталгаажуулалтыг амжилттай хийсний дараа цөм нь eBPF архитектурын объектын кодыг системийн архитектурт зориулсан машины код болгон хөрвүүлдэг (цагтаа).
  5. Програм нь интерфэйстэй холбогдож, пакетуудыг боловсруулж эхэлдэг.

XDP нь цөмд ажилладаг тул дибаг хийх нь ул мөрийн бүртгэлүүд ба үнэндээ програмын шүүдэг эсвэл үүсгэдэг пакетуудыг ашиглан хийгддэг. Гэсэн хэдий ч eBPF нь татаж авсан кодыг системд аюулгүй байлгахыг баталгаажуулдаг тул та XDP-г өөрийн локал Линукс дээр шууд туршиж үзэх боломжтой.

Байгаль орчныг бэлтгэх

Ассемблер

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-д зориулсан Makefile (цөм 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. Одоо хаана туршиж үзэх вэ?

Туршилтын тавиур

Стенд нь шүүлтүүр байх ба пакетуудыг илгээх гэсэн хоёр интерфейстэй байх ёстой. Эдгээр нь ердийн програмууд манай шүүлтүүртэй хэрхэн ажилладагийг шалгахын тулд өөрийн 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) ирж буй урсгалыг илгээнэ. Гэсэн хэдий ч, нэг асуудал байна: интерфэйсүүд нь нэг машин дээр байгаа бөгөөд Линукс нь тэдгээрийн аль нэг рүү нөгөө рүү нь траффик илгээхгүй. Та үүнийг төвөгтэй дүрмээр шийдэж чадна iptables, гэхдээ тэд багцаа өөрчлөх шаардлагатай бөгөөд энэ нь дибаг хийхэд тохиромжгүй юм. Сүлжээний нэрийн орон зай (цаашид netns) ашиглах нь дээр.

Сүлжээний нэрийн талбар нь бусад сүлжээн дэх ижил төстэй объектуудаас тусгаарлагдсан интерфэйсүүд, чиглүүлэлтийн хүснэгтүүд болон NetFilter дүрмийг агуулдаг. Процесс бүр нэрийн талбарт ажилладаг бөгөөд зөвхөн тухайн сүлжээний объектуудад хандах эрхтэй. Анхдагч байдлаар, систем нь бүх объектын нэг сүлжээний нэрийн орон зайтай тул та Linux дээр ажиллах боломжтой бөгөөд netns-ийн талаар мэдэхгүй.

Шинэ нэрийн орон зай үүсгэцгээе xdp-test тэгээд тийшээ шилжүүлээрэй xdp-remote.

ip netns add xdp-test
ip link set dev xdp-remote netns xdp-test

Дараа нь процесс явагдаж байна xdp-test, "харахгүй" xdp-local (энэ нь анхдагчаар netns-д үлдэх болно) бөгөөд 192.0.2.1 руу пакет илгээхдээ үүнийг дамжуулна. xdp-remoteУчир нь энэ нь 192.0.2.0/24 дээрх энэ процесст хандах боломжтой цорын ганц интерфейс юм. Энэ нь бас эсрэг чиглэлд ажилладаг.

Нэтнүүдийн хооронд шилжих үед интерфэйс буурч, хаягаа алддаг. Netns дахь интерфейсийг тохируулахын тулд та ажиллуулах хэрэгтэй 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.

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 программуудад өгөгдлийн хэсэг байдаггүй тул форматын мөрийг кодлох цорын ганц арга бол 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 Echo Request-ийг харуулах ба 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

Хэрэв оронд нь зөвхөн ARP-г харуулсан бол та шүүлтүүрийг арилгах хэрэгтэй (энэ нь sudo ./stand detach), орхи ping, дараа нь шүүлтүүрүүдийг тохируулаад дахин оролдоно уу. Асуудал нь шүүлтүүр юм XDP_TX нь ARP болон стекийн аль алинд нь хамаарна
нэрийн орон зай xdp-test 192.0.2.1 MAC хаягийг "мартаж" чадсан бол энэ IP-г шийдэж чадахгүй.

Асуудлын тодорхойлолт

Заасан даалгавар руу шилжье: XDP дээр SYN күүки механизмыг бичнэ үү.

SYN үер нь түгээмэл DDoS халдлага хэвээр байгаа бөгөөд түүний мөн чанар нь дараах байдалтай байна. Холболт (TCP гар барих) үед сервер SYN хүлээн авч, ирээдүйн холболтод нөөцийг хуваарилж, SYNACK пакетаар хариу өгч, ACK хүлээнэ. Халдагчид ердөө л олон мянган хүчтэй ботнетэд хост бүрээс хуурамч хаягуудаас секундэд хэдэн мянган SYN пакет илгээдэг. Сервер нь пакет ирсний дараа нөөцийг шууд хуваарилахаас өөр аргагүй болдог боловч их хэмжээний завсарлага авсны дараа тэдгээрийг гаргадаг; үүний үр дүнд санах ой эсвэл хязгаарлалт дуусч, шинэ холболтыг хүлээн авахгүй, үйлчилгээ боломжгүй болно.

Хэрэв та SYN пакет дээр тулгуурлан нөөцийг хуваарилдаггүй, харин зөвхөн SYNACK пакетаар хариу өгдөг бол дараа нь ирсэн ACK пакет нь хадгалагдаагүй SYN пакетийг хэлж байгааг сервер яаж ойлгох вэ? Эцсийн эцэст халдагчид хуурамч ACK үүсгэж болно. SYN күүкиний зорилго нь үүнийг кодлох явдал юм seqnum холболтын параметрүүдийг хаяг, порт, давсны хэш хэлбэрээр . Хэрэв ACK давсыг солихоос өмнө хүрч чадсан бол та хэшийг дахин тооцоолж, харьцуулж болно acknum. Forge acknum Давс нь нууцыг агуулдаг тул халдлага үйлдэгч чадахгүй бөгөөд суваг хязгаарлагдмал тул үүнийг цэгцлэх цаг гарахгүй.

SYN күүки нь Линуксийн цөмд удаан хугацаанд хэрэгжсэн бөгөөд SYN-ууд хэтэрхий хурдан бөгөөд бөөнөөр ирэх тохиолдолд автоматаар идэвхждэг.

TCP гар барих боловсролын хөтөлбөр

TCP нь өгөгдлийн дамжуулалтыг байт хэлбэрээр гүйцэтгэдэг, жишээлбэл, HTTP хүсэлтийг TCP-ээр дамжуулдаг. Урсгал нь багцаар хэсэг хэсгээр дамждаг. Бүх TCP пакетууд нь логик туг болон 32 бит дарааллын дугаартай:

  • Тугуудын хослол нь тодорхой багцын үүргийг тодорхойлдог. SYN туг нь энэ нь илгээгчийн холболт дээрх анхны пакет гэдгийг харуулж байна. ACK туг нь илгээгч нь байт хүртэлх бүх холболтын өгөгдлийг хүлээн авсан гэсэн үг юм acknum. Пакет нь хэд хэдэн тугтай байж болох ба тэдгээрийн хослолоор дуудагддаг, жишээлбэл, SYNACK пакет.

  • Дарааллын дугаар (seqnum) нь энэ багцад дамжуулагдсан эхний байтын өгөгдлийн урсгал дахь офсетийг тодорхойлдог. Жишээлбэл, X байт өгөгдөлтэй эхний пакетт энэ тоо N байсан бол шинэ өгөгдөлтэй дараагийн пакетад N+X байх болно. Холболтын эхэнд тал бүр энэ тоог санамсаргүй байдлаар сонгоно.

  • Баталгаажуулах дугаар (хүлээн зөвшөөрөх) - 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 програм нь дараахь зүйлийг хийх ёстой.

  • SYNA-д SYNACK-г күүки ашиглан хариулах;
  • ACK-д RST-ээр хариу өгөх (таслах);
  • үлдсэн пакетуудыг хая.

Багц задлан шинжлэхийн хамт алгоритмын псевдокод:

Если это не Ethernet,
    пропустить пакет.
Если это не IPv4,
    пропустить пакет.
Если адрес в таблице проверенных,               (*)
        уменьшить счетчик оставшихся проверок,
        пропустить пакет.
Если это не TCP,
    сбросить пакет.     (**)
Если это SYN,
    ответить SYN-ACK с cookie.
Если это ACK,
    если в acknum лежит не cookie,
        сбросить пакет.
    Занести в таблицу адрес с N оставшихся проверок.    (*)
    Ответить RST.   (**)
В остальных случаях сбросить пакет.

Нэг (*) Системийн төлөвийг удирдах шаардлагатай цэгүүдийг тэмдэглэсэн - эхний шатанд SYN күүки үүсгэх замаар TCP гар барилт хийх замаар тэдэнгүйгээр хийж болно.

Газар дээр нь (**), бидэнд ширээ байхгүй бол бид багцыг алгасах болно.

TCP гар барихыг хэрэгжүүлж байна

Багцыг задлан шинжилж, кодыг шалгаж байна

Бидэнд сүлжээний толгойн бүтэц хэрэгтэй болно: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) болон TCP (uapi/linux/tcp.h). Үүнтэй холбоотой алдааны улмаас би сүүлийнхтэй холбогдож чадсангүй atomic64_t, Би код руу шаардлагатай тодорхойлолтуудыг хуулах хэрэгтэй болсон.

Цөм дэх 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 тэмдэглэсэн чекүүдэд хандуулж байна. Хэрэв та А-г тайлбарлавал програмыг бүтээх боловч ачаалах үед баталгаажуулах алдаа гарна:

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. Бичиж байх үед kernel 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() RFC 32-ийн дагуу 16 бит үгийн 791 битийн нийлбэрээс хяналтын нийлбэрийг гаргадаг.

TCP гар барих баталгаажуулалт

Шүүлтүүр нь холболтыг зөв тогтоодог netcat, сүлжээний стек нь SYN хүлээн аваагүй - SYNACK болгон хувиргаж, буцааж илгээсэн тул Linux-аас RST пакетаар хариу өгсөн эцсийн ACK алга болсон бөгөөд OS-ийн үүднээс харахад нээхтэй холбоогүй пакет ирсэн. холболтууд.

$ 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-ийн үүднээс авч үзвэл баталгаажуулалт нь өөрөө өчүүхэн зүйл юм. Тооцооллын алгоритм нь энгийн бөгөөд боловсронгуй халдагчдад өртөмтгий байх магадлалтай. Жишээлбэл, Линуксийн цөм нь криптограф 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

сэтгэгдэл нэмэх