XDP-də DDoS hücumlarından qorunma yazırıq. Nüvə hissəsi

eXpress Data Path (XDP) texnologiyası paketlərin nüvə şəbəkə yığınına daxil olmasından əvvəl Linux interfeyslərində təsadüfi trafikin işlənməsini həyata keçirməyə imkan verir. XDP tətbiqi - DDoS hücumlarından qorunma (CloudFlare), mürəkkəb filtrlər, statistika toplusu (Netflix). XDP proqramları eBPF virtual maşını tərəfindən icra edilir, ona görə də onların həm kodları, həm də filtr növündən asılı olaraq mövcud nüvə funksiyaları üzrə məhdudiyyətlər var.

Məqalə XDP ilə bağlı çoxsaylı materialların çatışmazlıqlarını doldurmaq üçün nəzərdə tutulub. Birincisi, onlar XDP-nin xüsusiyyətlərindən dərhal yan keçən hazır kod təqdim edirlər: o, yoxlama üçün hazırlanmışdır və ya problem yaratmaq üçün çox sadədir. Daha sonra kodunuzu sıfırdan yazmağa çalışdığınız zaman tipik səhvlərlə nə edəcəyinizi bilmirsiniz. İkincisi, XDP-ni VM və aparat olmadan yerli sınaqdan keçirməyin yolları, onların öz tələləri olmasına baxmayaraq, əhatə olunmur. Mətn şəbəkə və Linux ilə tanış olan, XDP və eBPF ilə maraqlanan proqramçılar üçün nəzərdə tutulub.

Bu hissədə biz XDP filtrinin necə yığıldığını və necə sınaqdan keçiriləcəyini ətraflı anlayacağıq, sonra paket emal səviyyəsində tanınmış SYN kukilər mexanizminin sadə versiyasını yazacağıq. Biz hələ “ağ siyahı” yaratmayacağıq
təsdiqlənmiş müştərilər, sayğacları saxlayın və filtri idarə edin - kifayət qədər qeydlər.

C-də yazacağıq - bu dəb deyil, amma praktikdir. Bütün kodlar GitHub-da sondakı link vasitəsilə mövcuddur və məqalədə təsvir olunan mərhələlərə uyğun olaraq öhdəliklərə bölünür.

Disclaimer. Bu məqalə ərzində mən DDoS hücumlarının qarşısını almaq üçün mini-həll hazırlayacağam, çünki bu, XDP və mənim təcrübə sahəm üçün real vəzifədir. Bununla belə, əsas məqsəd texnologiyanı başa düşməkdir, bu, hazır qoruma yaratmaq üçün bələdçi deyil. Dərslik kodu optimallaşdırılmayıb və bəzi nüansları buraxır.

XDP Qısa Baxışı

Sənədləri və mövcud məqalələri təkrarlamamaq üçün yalnız əsas məqamları qeyd edəcəyəm.

Beləliklə, filtr kodu nüvəyə yüklənir. Gələn paketlər filtrə ötürülür. Nəticədə filtr qərar verməlidir: paketi nüvəyə keçir (XDP_PASS), paketi buraxın (XDP_DROP) və ya geri göndərin (XDP_TX). Süzgəc paketi dəyişə bilər, bu, xüsusilə də bu üçün doğrudur XDP_TX. Siz həmçinin proqramı dayandıra bilərsiniz (XDP_ABORTED) və paketi sıfırlayın, lakin bu analojidir assert(0) - ayıklama üçün.

eBPF (genişləndirilmiş Berkley Packet Filter) virtual maşını bilərəkdən sadələşdirilmişdir ki, nüvə kodun dövrə vurmadığını və digər insanların yaddaşına zərər vermədiyini yoxlaya bilsin. Kumulyativ məhdudiyyətlər və yoxlamalar:

  • Döngələr (geri) qadağandır.
  • Məlumatlar üçün bir yığın var, lakin heç bir funksiya yoxdur (bütün C funksiyaları sətirə daxil edilməlidir).
  • Yığın və paket buferindən kənar yaddaşa giriş qadağandır.
  • Kodun ölçüsü məhduddur, lakin praktikada bu çox əhəmiyyətli deyil.
  • Yalnız xüsusi nüvə funksiyalarına (eBPF köməkçiləri) zənglərə icazə verilir.

Bir filtrin dizaynı və quraşdırılması belə görünür:

  1. Mənbə kodu (məs kernel.c) obyektə yığılır (kernel.o) eBPF virtual maşın arxitekturası üçün. 2019-cu ilin oktyabr ayından etibarən eBPF-yə tərtib Clang tərəfindən dəstəklənir və GCC 10.1-də vəd edilir.
  2. Əgər bu obyekt kodu nüvə strukturlarına (məsələn, cədvəllər və sayğaclar) çağırışları ehtiva edirsə, onların identifikatorları sıfırlarla əvəz olunur, yəni belə kodun icrası mümkün deyil. Kernelə yükləməzdən əvvəl bu sıfırları kernel zəngləri vasitəsilə yaradılmış xüsusi obyektlərin identifikatorları ilə əvəz etməlisiniz (kodu əlaqələndirin). Bunu xarici yardım proqramları ilə edə bilərsiniz və ya müəyyən bir filtri bağlayacaq və yükləyəcək bir proqram yaza bilərsiniz.
  3. Kernel yüklənmiş proqramı yoxlayır. Dövrlərin olmaması və paket və yığın sərhədlərini aşmamaq yoxlanılır. Doğrulayıcı kodun düzgün olduğunu sübut edə bilmirsə, proqram rədd edilir - onu razı sala bilməlisiniz.
  4. Uğurlu yoxlamadan sonra kernel eBPF arxitekturası obyekt kodunu sistem arxitekturası üçün maşın koduna (tam vaxtında) tərtib edir.
  5. Proqram interfeysə qoşulur və paketləri emal etməyə başlayır.

XDP nüvədə işlədiyindən, sazlama iz jurnallarından və əslində proqramın süzdüyü və ya yaratdığı paketlərdən istifadə etməklə həyata keçirilir. Bununla belə, eBPF endirilmiş kodun sistem üçün təhlükəsiz olmasını təmin edir, beləliklə, siz XDP ilə birbaşa yerli Linux-da sınaq keçirə bilərsiniz.

Ətraf Mühitin Hazırlanması

Məclis

Clang birbaşa eBPF arxitekturası üçün obyekt kodu istehsal edə bilməz, ona görə də proses iki addımdan ibarətdir:

  1. C kodunu LLVM bayt koduna tərtib edin (clang -emit-llvm).
  2. Bayt kodunu eBPF obyekt koduna çevirin (llc -march=bpf -filetype=obj).

Filtr yazarkən köməkçi funksiyaları və makroları olan bir neçə fayl faydalı olacaq kernel testlərindən. Onların nüvə versiyasına uyğun olması vacibdir (KVER). Onları yükləyin 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 üçün Makefile (kernel 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 nüvə başlıqlarına gedən yolu ehtiva edir, ARCH - sistem arxitekturası. Yollar və alətlər paylamalar arasında bir qədər fərqli ola bilər.

Debian 10 üçün fərq nümunəsi (kernel 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 köməkçi başlıqları olan bir kataloqu və nüvə başlıqları olan bir neçə kataloqu birləşdirin. Simvol __KERNEL__ o deməkdir ki, UAPI (userspace API) başlıqları kernel kodu üçün müəyyən edilmişdir, çünki filtr nüvədə icra olunur.

Yığın qorunması deaktiv edilə bilər (-fno-stack-protector), çünki eBPF kodu yoxlayıcısı hələ də yığından kənar pozuntuları yoxlayır. Dərhal optimallaşdırmaları işə salmağa dəyər, çünki eBPF bayt kodunun ölçüsü məhduddur.

Bütün paketləri keçən və heç nə etməyən filtrlə başlayaq:

#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";

Komanda make toplayır xdp_filter.o. İndi harada sınamaq lazımdır?

Test stendi

Stenddə iki interfeys olmalıdır: hansı filtr olacaq və hansı paketlərdən göndəriləcək. Müntəzəm tətbiqlərin filtrimizlə necə işlədiyini yoxlamaq üçün bunlar öz IP-ləri olan tam hüquqlu Linux cihazları olmalıdır.

Veth (virtual Ethernet) tipli cihazlar bizim üçün uyğundur: bunlar birbaşa bir-birinə "qoşulmuş" bir cüt virtual şəbəkə interfeysidir. Onları belə yarada bilərsiniz (bu bölmədə bütün əmrlər ip -dən həyata keçirilir root):

ip link add xdp-remote type veth peer name xdp-local

Burada xdp-remote и xdp-local - cihaz adları. Aktiv xdp-local (192.0.2.1/24) ilə bir filtr əlavə olunacaq xdp-remote (192.0.2.2/24) daxil olan trafik göndəriləcək. Bununla belə, bir problem var: interfeyslər eyni maşındadır və Linux onlardan birinə digəri vasitəsilə trafik göndərməyəcək. Bunu çətin qaydalarla həll edə bilərsiniz iptables, lakin onlar paketləri dəyişdirməli olacaqlar ki, bu da ayıklama üçün əlverişsizdir. Şəbəkə ad boşluqlarından (bundan sonra netns) istifadə etmək daha yaxşıdır.

Şəbəkə ad məkanı digər şəbəkələrdəki oxşar obyektlərdən təcrid olunmuş bir sıra interfeyslər, marşrutlaşdırma cədvəlləri və NetFilter qaydaları ehtiva edir. Hər bir proses ad məkanında işləyir və yalnız həmin şəbəkələrin obyektlərinə çıxışı var. Varsayılan olaraq, sistem bütün obyektlər üçün vahid şəbəkə ad sahəsinə malikdir, belə ki, siz Linux-da işləyə bilərsiniz və netns haqqında bilmirsiniz.

Gəlin yeni ad sahəsi yaradaq xdp-test və ora köçürün xdp-remote.

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

Sonra proses gedir xdp-test, "görməyəcək" xdp-local (defolt olaraq netns-də qalacaq) və 192.0.2.1-ə paket göndərilərkən ondan keçir. xdp-remoteçünki 192.0.2.0/24-də bu proses üçün əlçatan olan yeganə interfeysdir. Bu da əks istiqamətdə işləyir.

Netns arasında hərəkət edərkən interfeys aşağı düşür və ünvanını itirir. Netns-də interfeysi konfiqurasiya etmək üçün işə başlamaq lazımdır ip ... bu əmr ad məkanında 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

Gördüyünüz kimi, bu parametrdən heç bir fərqi yoxdur xdp-local standart ad sahəsində:

    ip address add 192.0.2.1/24 dev xdp-local
    ip link set xdp-local up

Əgər qaçsan tcpdump -tnevi xdp-local, siz paketlərin göndərildiyini görə bilərsiniz xdp-test, bu interfeysə çatdırılır:

ip netns exec xdp-test   ping 192.0.2.1

Bir qabığı işə salmaq rahatdır xdp-test. Repozitoriyada stendlə işi avtomatlaşdıran skript var, məsələn, stendi əmrlə konfiqurasiya edə bilərsiniz. sudo ./stand up və silin sudo ./stand down.

İzləmə

Filtr cihazla belə əlaqələndirilir:

ip -force link set dev xdp-local xdp object xdp_filter.o verbose

Açar -force başqa bir proqram artıq bağlıdırsa, yeni proqramı əlaqələndirmək lazımdır. “Heç bir xəbər yaxşı xəbər deyil” bu əmrlə bağlı deyil, nəticə hər halda həcmlidir. göstərir verbose isteğe bağlıdır, lakin bununla birlikdə montaj siyahısı ilə kod yoxlayıcısının işi haqqında hesabat görünür:

Verifier analysis:

0: (b7) r0 = 2
1: (95) exit

Proqramı interfeysdən ayırın:

ip link set dev xdp-local xdp off

Skriptdə bunlar əmrlərdir sudo ./stand attach и sudo ./stand detach.

Bir filtr əlavə edərək, buna əmin ola bilərsiniz ping işləməyə davam edir, lakin proqram işləyirmi? Günlükləri əlavə edək. Funksiya bpf_trace_printk() oxşar printf(), lakin nümunədən başqa yalnız üç arqumenti və müəyyənedicilərin məhdud siyahısını dəstəkləyir. Makro bpf_printk() zəngi asanlaşdırır.

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

Çıxış, aktivləşdirilməli olan kernel izləmə kanalına gedir:

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

Mesaj mövzusuna baxın:

cat /sys/kernel/debug/tracing/trace_pipe

Bu əmrlərin hər ikisi zəng edir sudo ./stand log.

Ping indi bu kimi mesajları işə salmalıdır:

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

Doğrulayıcının çıxışına diqqətlə baxsanız, qəribə hesablamalar görəcəksiniz:

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
<...>

Fakt budur ki, eBPF proqramlarında məlumat bölməsi yoxdur, buna görə də format sətirini kodlaşdırmağın yeganə yolu VM əmrlərinin dərhal arqumentləridir:

$ python -c "import binascii; print(bytes(reversed(binascii.unhexlify('0a7025203a74656b63617020746f67'))))"
b'got packet: %pn'

Bu səbəbdən, debug çıxışı nəticədə yaranan kodu çox şişirdir.

XDP paketləri göndərilir

Gəlin filtri dəyişdirək: bütün gələn paketləri geri göndərsin. Şəbəkə nöqteyi-nəzərindən bu düzgün deyil, çünki başlıqlardakı ünvanları dəyişdirmək lazım olacaq, amma indi prinsipcə iş vacibdir.

       bpf_printk("got packet: %pn", ctx);
-      return XDP_PASS;
+      return XDP_TX;
   }

Başlat tcpdump haqqında xdp-remote. O, eyni gedən və gələn ICMP Echo Sorğunu göstərməli və ICMP Echo Cavabını göstərməyi dayandırmalıdır. Amma göstərmir. Belə çıxır ki, iş üçün XDP_TX üzrə proqramda xdp-local gərəkcüt interfeysinə xdp-remote bir proqram da boş olsa belə təyin olundu və o, böyüdü.

Mən bunu hardan bildim?

Kerneldə paketin yolunu izləyin Perf hadisələr mexanizmi, yeri gəlmişkən, eyni virtual maşından istifadə etməyə imkan verir, yəni eBPF eBPF ilə sökülmə üçün istifadə olunur.

Pislikdən yaxşılıq çıxarmalısan, çünki onu çıxaracaq başqa bir şey yoxdur.

$ 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])
                                     <...>

kod 6 nədir?

$ errno 6
ENXIO 6 No such device or address

Function veth_xdp_flush_bq() -dən səhv kodu alır veth_xdp_xmit(), harada axtarış ENXIO və şərhi tapın.

Minimum filtri bərpa edək (XDP_PASS) faylda xdp_dummy.c, onu Makefile-ə əlavə edin, bağlayın xdp-remote:

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

indi tcpdump gözlənilənləri göstərir:

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

Əvəzində yalnız ARP-lər göstərilibsə, siz filtrləri silməlisiniz (bu, sudo ./stand detach), buraxın ping, sonra filtrləri təyin edin və yenidən cəhd edin. Problem filtrdədir XDP_TX həm ARP-də, həm də yığında etibarlıdır
ad boşluqları xdp-test 192.0.2.1 MAC ünvanını "unuda" bilsəniz, bu IP-ni həll edə bilməyəcək.

Problem problemi

Gəlin qeyd olunan tapşırığa keçək: XDP-də SYN kuki mexanizmini yazın.

SYN daşqını populyar DDoS hücumu olaraq qalır, mahiyyəti aşağıdakı kimidir. Bağlantı qurulduqda (TCP əl sıxma), server SYN alır, gələcək əlaqə üçün resursları ayırır, SYNACK paketi ilə cavab verir və ACK-nı gözləyir. Təcavüzkar sadəcə olaraq çoxminlik güclü botnetdə hər hostdan saxta ünvanlardan saniyədə minlərlə SYN paketi göndərir. Server paketin gəlişi ilə dərhal resursları ayırmağa məcbur olur, lakin böyük bir fasilədən sonra onları buraxır, nəticədə yaddaş və ya limitlər tükənir, yeni bağlantılar qəbul edilmir və xidmət mövcud deyil.

Əgər siz SYN paketi əsasında resurslar ayırmırsınızsa, ancaq SYNACK paketi ilə cavab verirsinizsə, server sonradan gələn ACK paketinin saxlanmamış SYN paketinə aid olduğunu necə başa düşə bilər? Axı, təcavüzkar saxta ACK-lar da yarada bilər. SYN kukisinin məqsədi onu kodlaşdırmaqdır seqnum ünvanların, portların və dəyişən duzların hash kimi əlaqə parametrləri. Əgər ACK duz dəyişdirilmədən əvvəl gəlməyi bacarıbsa, siz hashı yenidən hesablaya və onunla müqayisə edə bilərsiniz acknum. Forge acknum təcavüzkar edə bilməz, çünki duz sirri ehtiva edir və məhdud kanal səbəbindən onu çeşidləməyə vaxtı olmayacaq.

SYN kukisi çoxdan Linux nüvəsində tətbiq edilib və hətta SYN-lər çox tez və kütləvi şəkildə daxil olduqda avtomatik olaraq aktivləşdirilə bilər.

TCP handshake üzrə təhsil proqramı

TCP bayt axını kimi məlumat ötürülməsini təmin edir, məsələn, HTTP sorğuları TCP üzərindən ötürülür. Axın paketlərdə hissə-hissə ötürülür. Bütün TCP paketlərində məntiqi bayraqlar və 32 bitlik ardıcıllıq nömrələri var:

  • Bayraqların birləşməsi müəyyən bir paketin rolunu müəyyənləşdirir. SYN bayrağı göstərir ki, bu, göndəricinin əlaqədəki ilk paketidir. ACK bayrağı göndəricinin bayta qədər bütün əlaqə məlumatlarını qəbul etdiyini bildirir acknum. Paketdə bir neçə bayraq ola bilər və onların birləşməsi ilə çağırılır, məsələn, SYNACK paketi.

  • Sıra nömrəsi (seqnum) bu paketdə ötürülən ilk bayt üçün məlumat axınındakı ofseti təyin edir. Məsələn, əgər X bayt verilənlərdən ibarət ilk paketdə bu rəqəm N idisə, yeni verilənlərdən ibarət növbəti paketdə N+X olacaqdır. Bağlantının əvvəlində hər tərəf bu nömrəni təsadüfi seçir.

  • Təsdiq nömrəsi (aknum) - seqnum ilə eyni ofsetdir, lakin o, ötürülən baytın sayını deyil, alıcıdan ilk baytın nömrəsini təyin edir, göndərənin görmədiyi.

Əlaqənin əvvəlində tərəflər razılaşmalıdırlar seqnum и acknum. Müştəri özü ilə SYN paketi göndərir seqnum = X. Server SYNACK paketi ilə cavab verir, burada özünü qeyd edir seqnum = Y və ifşa edir acknum = X + 1. Müştəri SYNACK-a ACK paketi ilə cavab verir, burada seqnum = X + 1, acknum = Y + 1. Bundan sonra faktiki məlumat ötürülməsi başlayır.

Əgər həmyaşıd paketin alınmasını qəbul etmirsə, TCP onu bir müddət sonra yenidən göndərir.

SYN kukiləri niyə həmişə istifadə edilmir?

Birincisi, SYNACK və ya ACK itirilərsə, onun yenidən göndərilməsini gözləməli olacaqsınız - əlaqənin qurulması yavaşlayacaq. İkincisi, SYN paketində - və yalnız içində! — əlaqənin sonrakı işinə təsir edən bir sıra seçimlər ötürülür. Daxil olan SYN paketlərini xatırlamadan server bu seçimlərə məhəl qoymur, müştəri onları növbəti paketlərdə göndərməyəcək. TCP bu halda işləyə bilər, lakin heç olmasa ilkin mərhələdə əlaqənin keyfiyyəti aşağı düşəcək.

Paketlər baxımından XDP proqramı aşağıdakıları etməlidir:

  • SYNACK ilə SYN-ə kuki ilə cavab verin;
  • ACK-a RST ilə cavab verin (bağlantıyı kəsin);
  • qalan paketləri atın.

Paketin təhlili ilə birlikdə alqoritmin psevdokodu:

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

bir (*) sistemin vəziyyətini idarə etməyiniz lazım olan nöqtələr qeyd olunur - birinci mərhələdə siz sadəcə olaraq SYN kukisini seqnum kimi yaratmaqla TCP əl sıxışmasını həyata keçirməklə onlarsız edə bilərsiniz.

Yerində (**), masamız olmadığı halda, paketi atlayacağıq.

TCP əl sıxma həyata keçirilir

Paketin təhlili və kodun yoxlanması

Bizə şəbəkə başlıq strukturlarına ehtiyacımız olacaq: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) və TCP (uapi/linux/tcp.h). ilə əlaqəli səhvlərə görə sonuncuya qoşula bilmədim atomic64_t, lazım olan tərifləri koda köçürməli oldum.

C-də oxunaqlılıq üçün vurğulanan bütün funksiyalar çağırış nöqtəsində daxil edilməlidir, çünki nüvədəki eBPF yoxlayıcısı geri izləməyi, yəni əslində döngələri və funksiya çağırışlarını qadağan edir.

#define INTERNAL static __attribute__((always_inline))

Makro LOG() buraxılış quruluşunda çapı söndürür.

Proqram funksiyaların daşıyıcısıdır. Hər biri müvafiq səviyyə başlığının vurğulandığı bir paket alır, məsələn, process_ether() doldurulmasını gözləyir ether. Sahə analizinin nəticələrinə əsasən, funksiya paketi daha yüksək səviyyəyə ötürə bilər. Funksiyanın nəticəsi XDP hərəkətidir. Hələlik SYN və ACK işləyiciləri bütün paketləri keçir.

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);
}

Diqqətinizi A və B ilə işarələnmiş çeklərə cəlb edirəm. Əgər A şərhini versəniz, proqram qurulacaq, lakin yükləmə zamanı yoxlama xətası olacaq:

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!

Açar sətri invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0): Buferin əvvəlindən on üçüncü bayt paketdən kənarda olduqda icra yolları var. Siyahıdan hansı sətirdən bəhs etdiyimizi başa düşmək çətindir, lakin mənbə kodunun sətirlərini göstərən bir təlimat nömrəsi (12) və sökücü var:

llvm-objdump -S xdp_filter.o | less

Bu vəziyyətdə o, xətti göstərir

LOG("Ether(proto=0x%x)", bpf_ntohs(ether->h_proto));

bu da problemin olduğunu aydın göstərir ether. Həmişə belə olacaqdı.

SYN-ə cavab verin

Bu mərhələdə məqsəd sabit olan düzgün SYNACK paketi yaratmaqdır seqnum, gələcəkdə SYN kuki ilə əvəz olunacaq. Bütün dəyişikliklər baş verir process_tcp_syn() və ətraf ərazilər.

Paketin yoxlanılması

Qəribədir ki, ən diqqətçəkən sətir, daha doğrusu, onun şərhi budur:

/* Required to verify checksum calculation */
const void* data_end = (const void*)ctx->data_end;

Kodun ilk versiyasını yazarkən 5.1 ləpəsindən istifadə edilmişdir, onun yoxlanışı üçün arasında fərq var idi. data_end и (const void*)ctx->data_end. Yazı zamanı kernel 5.3.1-də bu problem yox idi. Mümkündür ki, kompilyator bir sahədən fərqli olaraq yerli dəyişənə daxil olub. Hekayənin əxlaqı: Çoxlu yuvalar olduqda kodun sadələşdirilməsi kömək edə bilər.

Sonrakı, yoxlayıcının şöhrəti üçün müntəzəm uzunluq yoxlamalarıdır; O MAX_CSUM_BYTES aşağıda.

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 */
}

Paketin açılması

doldururuq seqnum и acknum, ACK təyin edin (SYN artıq qurulub):

const u32 cookie = 42;
tcp->ack_seq = bpf_htonl(bpf_ntohl(tcp->seq) + 1);
tcp->seq = bpf_htonl(cookie);
tcp->ack = 1;

TCP portlarını, IP ünvanlarını və MAC ünvanlarını dəyişdirin. Standart kitabxanaya XDP proqramından daxil olmaq mümkün deyil memcpy() — Clang intrinsiklərini gizlədən makro.

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);

Yoxlama məbləğlərinin yenidən hesablanması

IPv4 və TCP yoxlama məbləğləri başlıqlara bütün 16 bitlik sözlərin əlavə edilməsini tələb edir və başlıqların ölçüsü onlara yazılır, yəni tərtib zamanı məlum deyil. Bu problemdir, çünki yoxlayıcı normal dövranı sərhəd dəyişəninə atlamayacaq. Lakin başlıqların ölçüsü məhduddur: hər biri 64 bayta qədər. Sabit sayda iterasiya ilə bir döngə edə bilərsiniz, bu da erkən bitə bilər.

qeyd edim ki, var RFC 1624 yalnız paketlərin sabit sözləri dəyişdirilərsə, yoxlama məbləğinin qismən yenidən hesablanması haqqında. Bununla belə, metod universal deyil və həyata keçirilməsini saxlamaq daha çətin olacaq.

Yoxlama məbləğinin hesablanması funksiyası:

#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;
}

Baxmayaraq ki size zəng kodu ilə yoxlanılırsa, ikinci çıxış şərti lazımdır ki, yoxlayıcı dövrənin tamamlandığını sübut edə bilsin.

32 bitlik sözlər üçün daha sadə versiya tətbiq olunur:

INTERNAL u32
sum16_32(u32 v) {
    return (v >> 16) + (v & 0xffff);
}

Əslində yoxlama məbləğlərinin yenidən hesablanması və paketin geri göndərilməsi:

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;

Function carry() RFC 32-ə uyğun olaraq 16 bitlik sözlərin 791 bitlik cəmindən yoxlama məbləği yaradır.

TCP əl sıxma yoxlanışı

Filtr ilə əlaqəni düzgün qurur netcat, Linux-un RST paketi ilə cavab verdiyi son ACK-dan məhrum oldu, çünki şəbəkə yığını SYN almadı - o, SYNACK-a çevrildi və geri göndərildi - və OS nöqteyi-nəzərindən, açmaqla əlaqəli olmayan bir paket gəldi. əlaqələri.

$ sudo ip netns exec xdp-test   nc -nv 192.0.2.1 6666
192.0.2.1 6666: Connection reset by peer

Tam hüquqlu tətbiqlərlə yoxlamaq və müşahidə etmək vacibdir tcpdump haqqında xdp-remote çünki, məsələn, hping3 səhv yoxlama məbləğlərinə cavab vermir.

XDP nöqteyi-nəzərindən yoxlamanın özü əhəmiyyətsizdir. Hesablama alqoritmi primitivdir və çox güman ki, mürəkkəb hücumçuya qarşı həssasdır. Məsələn, Linux nüvəsi kriptoqrafik SipHash-dan istifadə edir, lakin onun XDP üçün tətbiqi açıq şəkildə bu məqalənin əhatə dairəsindən kənardadır.

Xarici ünsiyyətlə bağlı yeni TODO-lar üçün təqdim edilmişdir:

  • XDP proqramı saxlaya bilmir cookie_seed (duzun gizli hissəsi) qlobal dəyişəndə, dəyəri etibarlı bir generatordan vaxtaşırı yenilənəcək nüvədə saxlama lazımdır.

  • Əgər SYN kukisi ACK paketinə uyğun gəlirsə, mesajı çap etməyə ehtiyac yoxdur, lakin ondan paketləri ötürməyə davam etmək üçün təsdiqlənmiş müştərinin İP-ni yadda saxlayın.

Qanuni müştəri yoxlaması:

$ sudoip netns exec xdp-test   nc -nv 192.0.2.1 6666
192.0.2.1 6666: Connection reset by peer

Qeydlər çekin keçdiyini göstərir (flags=0x2 - bu 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

Təsdiqlənmiş İP-lərin siyahısı olmasa da, SYN daşqınından heç bir qorunma olmayacaq, lakin aşağıdakı əmrlə işə salınan ACK daşqına reaksiya budur:

sudo ip netns exec xdp-test   hping3 --flood -A -s 1111 -p 2222 192.0.2.1

Giriş qeydləri:

Ether(proto=0x800)
  IP(src=0x15bd11a dst=0x15bd11e proto=6)
    TCP(sport=3236 dport=2222 flags=0x10)
      cookie mismatch

Nəticə

Bəzən ümumiyyətlə eBPF və xüsusilə XDP inkişaf platforması kimi deyil, daha çox inkişaf etmiş idarəçi aləti kimi təqdim olunur. Həqiqətən, XDP DPDK və digər kernel bypass variantları kimi kernel yığınına alternativ deyil, kernel tərəfindən paketlərin işlənməsinə müdaxilə etmək üçün bir vasitədir. Digər tərəfdən, XDP kifayət qədər mürəkkəb məntiqi həyata keçirməyə imkan verir ki, bu da üstəlik, trafikin emalında fasiləsiz yeniləmək asandır. Doğrulayıcı böyük problemlər yaratmır; şəxsən mən istifadəçi sahəsi kodunun hissələri üçün bundan imtina etməzdim.

İkinci hissədə, əgər mövzu maraqlıdırsa, biz təsdiqlənmiş müştərilər və kəsilmələr cədvəlini tamamlayacağıq, sayğacları həyata keçirəcəyik və filtri idarə etmək üçün istifadəçi sahəsi yardım proqramını yazacağıq.

Referanslar:

Mənbə: www.habr.com

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