Մենք գրում ենք պաշտպանություն DDoS հարձակումներից XDP-ի վրա: Միջուկային մաս

eXpress Data Path (XDP) տեխնոլոգիան թույլ է տալիս կամայական մշակել տրաֆիկը Linux ինտերֆեյսներում, նախքան փաթեթները միջուկի ցանցի կույտ մտնելը: XDP-ի կիրառում - պաշտպանություն DDoS հարձակումներից (CloudFlare), բարդ զտիչներ, վիճակագրության հավաքագրում (Netflix): XDP ծրագրերն իրականացվում են eBPF վիրտուալ մեքենայի կողմից և, հետևաբար, սահմանափակումներ ունեն ինչպես իրենց կոդի, այնպես էլ միջուկի հասանելի գործառույթների վրա՝ կախված ֆիլտրի տեսակից:

Հոդվածը նախատեսված է լրացնել XDP-ի բազմաթիվ նյութերի թերությունները: Նախ, նրանք տրամադրում են պատրաստի կոդ, որն անմիջապես շրջանցում է XDP-ի առանձնահատկությունները՝ պատրաստված ստուգման համար կամ չափազանց պարզ՝ խնդիրներ առաջացնելու համար: Երբ ավելի ուշ փորձում եք զրոյից գրել ձեր սեփական կոդը, հասկանալի չէ, թե ինչ անել բնորոշ սխալների հետ: Երկրորդ, այն չի ներառում XDP-ն առանց VM-ի և սարքաշարի տեղական փորձարկման ուղիները, չնայած այն հանգամանքին, որ նրանք ունեն իրենց սեփական թակարդները: Տեքստը նախատեսված է ցանցերին և Linux-ին ծանոթ ծրագրավորողների համար, ովքեր հետաքրքրված են 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 թվականի հոկտեմբերի դրությամբ eBPF-ին կազմելը աջակցվում է Clang-ի կողմից և խոստացված է GCC 10.1-ում:
  2. Եթե ​​այս օբյեկտի կոդում կան զանգեր դեպի միջուկային կառույցներ (օրինակ՝ աղյուսակներ և հաշվիչներ), ապա դրանց ID-ների փոխարեն կան զրոներ, այսինքն՝ նման կոդը հնարավոր չէ կատարել։ Նախքան միջուկը բեռնելը, այս զրոները պետք է փոխարինվեն միջուկի կանչերի միջոցով ստեղծված հատուկ օբյեկտների 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

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-ի (վիրտուալ 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, բայց նրանք ստիպված կլինեն փոխել փաթեթները, ինչը անհարմար է վրիպազերծման ժամանակ։ Ավելի լավ է օգտագործել ցանցի անվանատարածքները (ցանցային անվանատարածքներ, հետագա ցանցեր):

Ցանցի անվանատարածքը պարունակում է մի շարք միջերեսներ, երթուղային աղյուսակներ և NetFilter կանոններ, որոնք մեկուսացված են այլ ցանցերի նմանատիպ օբյեկտներից: Յուրաքանչյուր գործընթաց աշխատում է ինչ-որ անվանատարածքում, և միայն այս ցանցերի օբյեկտները հասանելի են նրան: Լռելյայնորեն, համակարգը ունի մեկ ցանցի անվանատարածք բոլոր օբյեկտների համար, այնպես որ կարող եք աշխատել Linux-ով և չիմանալ ցանցերի մասին:

Եկեք ստեղծենք նոր անվանատարածք xdp-test և տեղափոխվել այնտեղ xdp-remote.

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

Այնուհետև գործընթացը սկսվում է xdp-test, չեմ «տեսնի» xdp-local (այն լռելյայն կմնա ցանցերում) և 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.

Այժմ 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-ին, և եթե stack
անունների տարածքներ xdp-test հաջողվել է «մոռանալ» MAC հասցեն 192.0.2.1, նա չի կարողանա լուծել այս IP-ն։

Խնդրի ձևակերպում

Անցնենք նշված առաջադրանքին՝ գրել SYN թխուկների մեխանիզմ XDP-ում:

Մինչ այժմ SYN ջրհեղեղը մնում է հանրաճանաչ DDoS գրոհ, որի էությունը հետևյալն է. Երբ կապ է հաստատվում (TCP ձեռքսեղմում), սերվերը ստանում է SYN, հատկացնում է ռեսուրսներ ապագա կապի համար, պատասխանում է SYNACK փաթեթով և սպասում ACK-ին: Հարձակվողը պարզապես ուղարկում է SYN փաթեթներ կեղծ հասցեներից՝ հազարավոր վայրկյանում յուրաքանչյուր հոսթից բազմահազար բոտնետում: Սերվերը ստիպված է լինում ռեսուրսներ բաշխել անմիջապես փաթեթը ժամանելուն պես, սակայն այն թողարկում է երկար ժամանակի ավարտից հետո, արդյունքում հիշողությունը կամ սահմանափակումները սպառվում են, նոր կապեր չեն ընդունվում, ծառայությունն անհասանելի է:

Եթե ​​դուք ռեսուրսներ չեք հատկացնում SYN փաթեթի վրա, այլ պատասխանում եք միայն SYNACK փաթեթով, ապա ինչպե՞ս կարող է սերվերը հասկանալ, որ ավելի ուշ եկած ACK փաթեթը պատկանում է SYN փաթեթին, որը չի պահպանվել: Ի վերջո, հարձակվողը կարող է նաև կեղծ ACK-ներ ստեղծել: SYN թխուկի էությունը կոդավորումն է seqnum կապի պարամետրերը որպես հասցեների, նավահանգիստների և փոփոխվող աղի հաշ: Եթե ​​ACK-ին հաջողվել է հասնել մինչև աղի փոփոխությունը, կարող եք նորից հաշվել հեշը և համեմատել դրա հետ acknum. կեղծ acknum հարձակվողը չի կարող, քանի որ աղն իր մեջ ներառում է գաղտնիքը և չի հասցնի տեսակավորել այն սահմանափակ ալիքի պատճառով:

SYN թխուկները երկար ժամանակ ներդրվել են Linux միջուկում և կարող են նույնիսկ ավտոմատ կերպով միացվել, եթե SYN-ները շատ արագ և մեծ քանակությամբ հայտնվեն:

Կրթական ծրագիր TCP ձեռքսեղմման վերաբերյալ

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 թխուկները միշտ չէ, որ օգտագործվում են:

Նախ, եթե 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 ձեռքսեղմման իրականացում

Փաթեթի վերլուծություն և կոդի ստուգում

Մեզ անհրաժեշտ են ցանցի վերնագրի կառուցվածքներ՝ 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 թխուկով: Բոլոր փոփոխությունները տեղի են ունենում 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 (աղի գաղտնի մասը) գլոբալ փոփոխականում ձեզ անհրաժեշտ է միջուկի պահեստ, որի արժեքը պարբերաբար կթարմացվի հուսալի գեներատորից:

  • Եթե ​​ACK փաթեթում SYN թխուկը համընկնում է, ապա ձեզ հարկավոր չէ հաղորդագրություն տպել, այլ հիշել ստուգված հաճախորդի 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-ն թույլ է տալիս իրականացնել բավականին բարդ տրամաբանություն, որը, ավելին, հեշտ է թարմացնել առանց երթևեկության մշակման դադարի։ Ստուգիչը մեծ խնդիրներ չի ստեղծում, անձամբ ես այդպիսիք չէի հրաժարվի օգտվողների տարածքի կոդի մասերի համար:

Երկրորդ մասում, եթե թեման հետաքրքիր է, մենք կլրացնենք ստուգված հաճախորդների աղյուսակը և կխզենք կապերը, կիրականացնենք հաշվիչներ և կգրենք օգտվողների տարածքի օգտակար ծրագիր՝ ֆիլտրը կառավարելու համար։

Հղումներ.

Source: www.habr.com

Добавить комментарий