Kami menulis perlindungan terhadap serangan DDoS pada XDP. Bahagian nuklear

Teknologi Laluan Data eXpress (XDP) membenarkan pemprosesan trafik rawak dilakukan pada antara muka Linux sebelum paket memasuki timbunan rangkaian kernel. Aplikasi XDP - perlindungan terhadap serangan DDoS (CloudFlare), penapis kompleks, pengumpulan statistik (Netflix). Program XDP dilaksanakan oleh mesin maya eBPF, jadi ia mempunyai sekatan pada kedua-dua kod mereka dan fungsi kernel yang tersedia bergantung pada jenis penapis.

Artikel ini bertujuan untuk mengisi kekurangan banyak bahan di XDP. Pertama, mereka menyediakan kod siap pakai yang segera memintas ciri XDP: ia disediakan untuk pengesahan atau terlalu mudah untuk menimbulkan masalah. Apabila anda kemudian cuba menulis kod anda dari awal, anda tidak tahu apa yang perlu dilakukan dengan ralat biasa. Kedua, cara untuk menguji XDP secara tempatan tanpa VM dan perkakasan tidak dilindungi, walaupun pada hakikatnya mereka mempunyai perangkap mereka sendiri. Teks ini bertujuan untuk pengaturcara yang biasa dengan rangkaian dan Linux yang berminat dengan XDP dan eBPF.

Dalam bahagian ini, kami akan memahami secara terperinci bagaimana penapis XDP dipasang dan cara mengujinya, kemudian kami akan menulis versi ringkas mekanisme kuki SYN yang terkenal di peringkat pemprosesan paket. Kami tidak akan membuat "senarai putih" lagi
pelanggan yang disahkan, simpan kaunter dan uruskan penapis - log yang mencukupi.

Kami akan menulis dalam C - ia tidak bergaya, tetapi ia praktikal. Semua kod tersedia di GitHub melalui pautan di penghujung dan dibahagikan kepada komit mengikut peringkat yang diterangkan dalam artikel.

Penafian. Sepanjang artikel ini, saya akan membangunkan penyelesaian mini untuk menangkis serangan DDoS, kerana ini adalah tugas yang realistik untuk XDP dan bidang kepakaran saya. Walau bagaimanapun, matlamat utama adalah untuk memahami teknologi; ini bukan panduan untuk mencipta perlindungan sedia. Kod tutorial tidak dioptimumkan dan menghilangkan beberapa nuansa.

Gambaran Keseluruhan Ringkas XDP

Saya hanya akan menggariskan perkara-perkara penting sahaja supaya tidak menduplikasi dokumentasi dan artikel sedia ada.

Jadi, kod penapis dimuatkan ke dalam kernel. Paket masuk dihantar ke penapis. Akibatnya, penapis mesti membuat keputusan: hantar paket ke dalam kernel (XDP_PASS), drop paket (XDP_DROP) atau hantar semula (XDP_TX). Penapis boleh menukar pakej, ini terutama berlaku untuk XDP_TX. Anda juga boleh membatalkan program (XDP_ABORTED) dan tetapkan semula pakej, tetapi ini adalah analog assert(0) - untuk nyahpepijat.

Mesin maya eBPF (Extended Berkley Packet Filter) sengaja dibuat ringkas supaya kernel boleh menyemak sama ada kod tersebut tidak bergelung dan tidak merosakkan memori orang lain. Sekatan dan semakan kumulatif:

  • Gelung (ke belakang) adalah dilarang.
  • Terdapat timbunan untuk data, tetapi tiada fungsi (semua fungsi C mesti diselaraskan).
  • Akses memori di luar tindanan dan penimbal paket adalah dilarang.
  • Saiz kod adalah terhad, tetapi dalam amalan ini tidak begitu penting.
  • Hanya panggilan ke fungsi kernel khas (pembantu eBPF) dibenarkan.

Mereka bentuk dan memasang penapis kelihatan seperti ini:

  1. Kod sumber (cth kernel.c) disusun menjadi objek (kernel.o) untuk seni bina mesin maya eBPF. Sehingga Oktober 2019, kompilasi kepada eBPF disokong oleh Clang dan dijanjikan dalam GCC 10.1.
  2. Jika kod objek ini mengandungi panggilan ke struktur kernel (contohnya, jadual dan pembilang), ID mereka digantikan dengan sifar, yang bermaksud kod tersebut tidak boleh dilaksanakan. Sebelum memuatkan ke dalam kernel, anda perlu menggantikan sifar ini dengan ID objek tertentu yang dibuat melalui panggilan kernel (pautkan kod). Anda boleh melakukan ini dengan utiliti luaran, atau anda boleh menulis program yang akan memaut dan memuatkan penapis tertentu.
  3. Kernel mengesahkan program yang dimuatkan. Ketiadaan kitaran dan kegagalan untuk melebihi sempadan paket dan tindanan diperiksa. Jika pengesah tidak dapat membuktikan bahawa kod itu betul, program itu ditolak - anda perlu dapat menggembirakannya.
  4. Selepas pengesahan berjaya, kernel menyusun kod objek seni bina eBPF ke dalam kod mesin untuk seni bina sistem (tepat masa).
  5. Program ini melekat pada antara muka dan mula memproses paket.

Memandangkan XDP berjalan dalam kernel, penyahpepijatan dijalankan menggunakan log surih dan, sebenarnya, paket yang ditapis atau dihasilkan oleh program. Walau bagaimanapun, eBPF memastikan bahawa kod yang dimuat turun adalah selamat untuk sistem, jadi anda boleh mencuba XDP secara langsung pada Linux tempatan anda.

Menyediakan Alam Sekitar

Perhimpunan

Clang tidak boleh terus menghasilkan kod objek untuk seni bina eBPF, jadi prosesnya terdiri daripada dua langkah:

  1. Susun kod C kepada kod bait LLVM (clang -emit-llvm).
  2. Tukar bytecode kepada kod objek eBPF (llc -march=bpf -filetype=obj).

Apabila menulis penapis, beberapa fail dengan fungsi tambahan dan makro akan berguna daripada ujian kernel. Adalah penting bahawa ia sepadan dengan versi kernel (KVER). Muat turunnya ke 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 untuk Arch Linux (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 mengandungi laluan ke pengepala kernel, ARCH - seni bina sistem. Laluan dan alatan mungkin berbeza sedikit antara pengedaran.

Contoh perbezaan untuk Debian 10 (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 sambungkan direktori dengan pengepala tambahan dan beberapa direktori dengan pengepala kernel. Simbol __KERNEL__ bermakna pengepala UAPI (API ruang pengguna) ditakrifkan untuk kod kernel, kerana penapis dilaksanakan dalam kernel.

Perlindungan tindanan boleh dilumpuhkan (-fno-stack-protector), kerana pengesah kod eBPF masih menyemak pelanggaran timbunan di luar sempadan. Anda patut menghidupkan pengoptimuman dengan segera, kerana saiz kod bait eBPF adalah terhad.

Mari kita mulakan dengan penapis yang melepasi semua paket dan tidak melakukan apa-apa:

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

Pasukan make mengumpul xdp_filter.o. Di mana untuk mencubanya sekarang?

Tempat ujian

Pendirian mesti termasuk dua antara muka: yang akan ada penapis dan dari mana paket akan dihantar. Ini mestilah peranti Linux lengkap dengan IP mereka sendiri untuk menyemak cara aplikasi biasa berfungsi dengan penapis kami.

Peranti jenis veth (Ethernet maya) sesuai untuk kami: ini adalah sepasang antara muka rangkaian maya "disambungkan" terus antara satu sama lain. Anda boleh menciptanya seperti ini (dalam bahagian ini semua arahan ip dijalankan daripada root):

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

ia adalah xdp-remote и xdp-local — nama peranti. hidup xdp-local (192.0.2.1/24) penapis akan dilampirkan, dengan xdp-remote (192.0.2.2/24) trafik masuk akan dihantar. Walau bagaimanapun, terdapat masalah: antara muka berada pada mesin yang sama, dan Linux tidak akan menghantar trafik kepada salah satu daripada mereka melalui yang lain. Anda boleh menyelesaikannya dengan peraturan yang rumit iptables, tetapi mereka perlu menukar pakej, yang menyusahkan untuk penyahpepijatan. Adalah lebih baik untuk menggunakan ruang nama rangkaian (selepas ini netns).

Ruang nama rangkaian mengandungi satu set antara muka, jadual penghalaan dan peraturan NetFilter yang diasingkan daripada objek serupa dalam jaring lain. Setiap proses berjalan dalam ruang nama dan hanya mempunyai akses kepada objek jaring itu. Secara lalai, sistem mempunyai ruang nama rangkaian tunggal untuk semua objek, jadi anda boleh bekerja di Linux dan tidak tahu tentang jaring.

Mari buat ruang nama baharu xdp-test dan pindahkan ke sana xdp-remote.

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

Kemudian proses berjalan masuk xdp-test, tidak akan "melihat" xdp-local (ia akan kekal dalam netns secara lalai) dan apabila menghantar paket ke 192.0.2.1 ia akan melaluinya xdp-remotekerana ia adalah satu-satunya antara muka pada 192.0.2.0/24 yang boleh diakses untuk proses ini. Ini juga berfungsi dalam arah yang bertentangan.

Apabila bergerak antara jaring, antara muka turun dan kehilangan alamatnya. Untuk mengkonfigurasi antara muka dalam netns, anda perlu menjalankan ip ... dalam ruang nama arahan ini 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

Seperti yang anda lihat, ini tidak berbeza dengan tetapan xdp-local dalam ruang nama lalai:

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

Jika anda berlari tcpdump -tnevi xdp-local, anda boleh melihat bahawa paket dihantar dari xdp-test, dihantar ke antara muka ini:

ip netns exec xdp-test   ping 192.0.2.1

Ia adalah mudah untuk melancarkan shell masuk xdp-test. Repositori mempunyai skrip yang mengautomasikan kerja dengan pendirian; sebagai contoh, anda boleh mengkonfigurasi dirian dengan arahan sudo ./stand up dan padamkannya sudo ./stand down.

Menjejak

Penapis dikaitkan dengan peranti seperti ini:

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

Kunci -force diperlukan untuk memautkan program baharu jika program lain sudah dipautkan. "Tiada berita adalah berita baik" bukan mengenai arahan ini, kesimpulannya adalah besar dalam apa jua keadaan. menunjukkan verbose pilihan, tetapi dengannya laporan muncul pada kerja pengesah kod dengan penyenaraian pemasangan:

Verifier analysis:

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

Nyahpaut program daripada antara muka:

ip link set dev xdp-local xdp off

Dalam skrip ini adalah arahan sudo ./stand attach и sudo ./stand detach.

Dengan melampirkan penapis, anda boleh memastikannya ping terus berjalan, tetapi adakah program ini berfungsi? Mari tambah log. Fungsi bpf_trace_printk() sama seperti printf(), tetapi hanya menyokong sehingga tiga argumen selain daripada corak dan senarai penentu yang terhad. Makro bpf_printk() memudahkan panggilan.

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

Output pergi ke saluran surih kernel, yang perlu didayakan:

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

Lihat urutan mesej:

cat /sys/kernel/debug/tracing/trace_pipe

Kedua-dua arahan ini membuat panggilan sudo ./stand log.

Ping kini sepatutnya mencetuskan mesej seperti ini:

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

Jika anda melihat dengan teliti pada output pengesah, anda akan melihat pengiraan pelik:

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

Faktanya ialah program eBPF tidak mempunyai bahagian data, jadi satu-satunya cara untuk mengekod rentetan format ialah hujah segera arahan VM:

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

Atas sebab ini, keluaran nyahpepijat sangat membebankan kod yang terhasil.

Menghantar Paket XDP

Mari tukar penapis: biarkan ia menghantar semula semua paket masuk. Ini tidak betul dari sudut pandangan rangkaian, kerana ia adalah perlu untuk menukar alamat dalam pengepala, tetapi kini kerja pada dasarnya adalah penting.

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

Kami melancarkan tcpdump pada xdp-remote. Ia harus menunjukkan Permintaan Gema ICMP keluar dan masuk yang sama dan berhenti menunjukkan Balasan Gema ICMP. Tetapi ia tidak menunjukkan. Ternyata untuk kerja XDP_TX dalam program pada xdp-local perlukepada antara muka pasangan xdp-remote program juga telah ditetapkan, walaupun ia kosong, dan dia dibesarkan.

Bagaimana saya tahu ini?

Jejaki laluan pakej dalam kernel Mekanisme peristiwa perf membenarkan, dengan cara, menggunakan mesin maya yang sama, iaitu, eBPF digunakan untuk pembongkaran dengan eBPF.

Anda mesti membuat yang baik daripada yang jahat, kerana tidak ada apa-apa lagi untuk membuat daripadanya.

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

Apakah kod 6?

$ errno 6
ENXIO 6 No such device or address

Fungsi veth_xdp_flush_bq() menerima kod ralat daripada veth_xdp_xmit(), di mana carian oleh ENXIO dan cari komen.

Mari kita pulihkan penapis minimum (XDP_PASS) dalam fail xdp_dummy.c, tambahkannya pada Makefile, ikat padanya xdp-remote:

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

Sekarang tcpdump menunjukkan apa yang diharapkan:

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

Jika hanya ARP yang ditunjukkan, anda perlu mengalih keluar penapis (ini tidak sudo ./stand detach), lepaskan ping, kemudian tetapkan penapis dan cuba lagi. Masalahnya ialah penapis XDP_TX sah kedua-duanya pada ARP dan jika timbunan
ruang nama xdp-test berjaya "melupakan" alamat MAC 192.0.2.1, ia tidak akan dapat menyelesaikan IP ini.

Pernyataan masalah

Mari kita beralih kepada tugas yang dinyatakan: tulis mekanisme kuki SYN pada XDP.

Banjir SYN kekal sebagai serangan DDoS yang popular, intipatinya adalah seperti berikut. Apabila sambungan diwujudkan (jabat tangan TCP), pelayan menerima SYN, memperuntukkan sumber untuk sambungan masa hadapan, bertindak balas dengan paket SYNACK dan menunggu ACK. Penyerang hanya menghantar beribu-ribu paket SYN sesaat daripada alamat palsu daripada setiap hos dalam botnet yang mempunyai seribu-ribu kuat. Pelayan terpaksa memperuntukkan sumber serta-merta selepas ketibaan paket, tetapi melepaskannya selepas tamat masa yang besar; akibatnya, memori atau had kehabisan, sambungan baharu tidak diterima, dan perkhidmatan tidak tersedia.

Jika anda tidak memperuntukkan sumber berdasarkan paket SYN, tetapi hanya bertindak balas dengan paket SYNACK, bagaimanakah pelayan boleh memahami bahawa paket ACK yang tiba kemudian merujuk kepada paket SYN yang tidak disimpan? Lagipun, penyerang juga boleh menjana ACK palsu. Tujuan kuki SYN adalah untuk mengekodnya seqnum parameter sambungan sebagai cincang alamat, port dan garam yang berubah-ubah. Jika ACK berjaya tiba sebelum garam ditukar, anda boleh mengira cincang sekali lagi dan membandingkannya dengannya acknum. Menjalin acknum penyerang tidak boleh, kerana garam termasuk rahsia, dan tidak akan mempunyai masa untuk menyelesaikannya kerana saluran yang terhad.

Kuki SYN telah lama dilaksanakan dalam kernel Linux malah boleh didayakan secara automatik jika SYN tiba terlalu cepat dan beramai-ramai.

Program pendidikan mengenai jabat tangan TCP

TCP menyediakan penghantaran data sebagai aliran bait, contohnya, permintaan HTTP dihantar melalui TCP. Aliran dihantar dalam kepingan dalam paket. Semua paket TCP mempunyai bendera logik dan nombor urutan 32-bit:

  • Gabungan bendera menentukan peranan pakej tertentu. Bendera SYN menunjukkan bahawa ini adalah paket pertama penghantar pada sambungan. Bendera ACK bermakna penghantar telah menerima semua data sambungan sehingga bait acknum. Satu paket boleh mempunyai beberapa bendera dan dipanggil dengan gabungannya, sebagai contoh, paket SYNACK.

  • Nombor jujukan (seqnum) menentukan offset dalam aliran data untuk bait pertama yang dihantar dalam paket ini. Sebagai contoh, jika dalam paket pertama dengan X bait data nombor ini ialah N, dalam paket seterusnya dengan data baharu ia akan menjadi N+X. Pada permulaan sambungan, setiap pihak memilih nombor ini secara rawak.

  • Nombor pengakuan (acknum) - offset yang sama seperti seqnum, tetapi ia tidak menentukan bilangan bait yang dihantar, tetapi nombor bait pertama daripada penerima, yang tidak dilihat oleh pengirim.

Pada permulaan perhubungan, pihak-pihak mesti bersetuju seqnum и acknum. Pelanggan menghantar paket SYN dengannya seqnum = X. Pelayan bertindak balas dengan paket SYNACK, di mana ia merekodkannya seqnum = Y dan mendedahkan acknum = X + 1. Pelanggan bertindak balas kepada SYNACK dengan paket ACK, di mana seqnum = X + 1, acknum = Y + 1. Selepas ini, pemindahan data sebenar bermula.

Jika rakan sebaya tidak mengakui penerimaan paket, TCP menghantarnya semula selepas tamat masa.

Mengapa kuki SYN tidak selalu digunakan?

Pertama, jika SYNACK atau ACK hilang, anda perlu menunggu untuk dihantar semula - persediaan sambungan akan menjadi perlahan. Kedua, dalam pakej SYN - dan hanya di dalamnya! — beberapa pilihan dihantar yang menjejaskan operasi sambungan selanjutnya. Tanpa mengingati paket SYN yang masuk, pelayan itu mengabaikan pilihan ini; pelanggan tidak akan menghantarnya dalam paket seterusnya. TCP boleh berfungsi dalam kes ini, tetapi sekurang-kurangnya pada peringkat awal kualiti sambungan akan berkurangan.

Dari perspektif pakej, program XDP mesti melakukan perkara berikut:

  • balas SYN dengan SYNACK dengan kuki;
  • balas ACK dengan RST (putuskan sambungan);
  • buang bungkusan yang tinggal.

Pseudokod algoritma bersama-sama dengan parsing pakej:

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

satu (*) titik di mana anda perlu mengurus keadaan sistem ditandakan - pada peringkat pertama anda boleh melakukannya tanpanya dengan hanya melaksanakan jabat tangan TCP dengan penjanaan kuki SYN sebagai seqnum.

Segera (**), sementara kami tidak mempunyai meja, kami akan melangkau paket itu.

Melaksanakan jabat tangan TCP

Menghuraikan pakej dan mengesahkan kod

Kami memerlukan struktur pengepala rangkaian: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) dan TCP (uapi/linux/tcp.h). Saya tidak dapat menyambung yang terakhir kerana ralat yang berkaitan dengan atomic64_t, saya terpaksa menyalin definisi yang diperlukan ke dalam kod.

Semua fungsi yang diserlahkan dalam C untuk kebolehbacaan mesti diselaraskan pada titik panggilan, kerana pengesah eBPF dalam kernel melarang penjejakan ke belakang, iaitu, sebenarnya, gelung dan panggilan fungsi.

#define INTERNAL static __attribute__((always_inline))

Makro LOG() melumpuhkan pencetakan dalam binaan keluaran.

Program ini adalah penghantar fungsi. Setiap satu menerima paket di mana pengepala tahap yang sepadan diserlahkan, sebagai contoh, process_ether() mengharapkan ia akan diisi ether. Berdasarkan hasil analisis medan, fungsi boleh menghantar paket ke tahap yang lebih tinggi. Hasil daripada fungsi tersebut ialah tindakan XDP. Buat masa ini, pengendali SYN dan ACK melepasi semua paket.

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

Saya menarik perhatian anda kepada semakan bertanda A dan B. Jika anda mengulas A, program akan dibina, tetapi akan terdapat ralat pengesahan semasa memuatkan:

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!

Rentetan kunci invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0): Terdapat laluan pelaksanaan apabila bait ketiga belas dari permulaan penimbal berada di luar paket. Sukar untuk difahami dari penyenaraian baris mana yang kita bicarakan, tetapi terdapat nombor arahan (12) dan pembongkar yang menunjukkan baris kod sumber:

llvm-objdump -S xdp_filter.o | less

Dalam kes ini ia menunjuk ke garisan

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

yang menjelaskan bahawa masalahnya adalah ether. Ia akan sentiasa seperti ini.

Balas kepada SYN

Matlamat pada peringkat ini adalah untuk menjana paket SYNACK yang betul dengan tetap seqnum, yang akan digantikan pada masa hadapan dengan kuki SYN. Semua perubahan berlaku dalam process_tcp_syn() dan kawasan sekitarnya.

Pengesahan pakej

Anehnya, berikut adalah baris yang paling luar biasa, atau lebih tepatnya, ulasan kepadanya:

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

Semasa menulis versi pertama kod, kernel 5.1 telah digunakan, untuk pengesah yang terdapat perbezaan antara data_end и (const void*)ctx->data_end. Pada masa penulisan, kernel 5.3.1 tidak mempunyai masalah ini. Ada kemungkinan bahawa pengkompil mengakses pembolehubah tempatan secara berbeza daripada medan. Moral of the story: Memudahkan kod boleh membantu apabila terdapat banyak sarang.

Seterusnya ialah pemeriksaan panjang rutin untuk kemuliaan pengesah; O MAX_CSUM_BYTES di bawah.

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

Membuka bungkusan

Mengisi seqnum и acknum, tetapkan ACK (SYN sudah ditetapkan):

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

Tukar port TCP, alamat IP dan alamat MAC. Pustaka standard tidak boleh diakses daripada program XDP, jadi memcpy() — makro yang menyembunyikan intrinsik 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);

Pengiraan semula jumlah semak

Jumlah semak IPv4 dan TCP memerlukan penambahan semua perkataan 16-bit dalam pengepala, dan saiz pengepala ditulis ke dalamnya, iaitu, tidak diketahui pada masa penyusunan. Ini adalah masalah kerana pengesah tidak akan melangkau gelung biasa ke pembolehubah sempadan. Tetapi saiz pengepala adalah terhad: sehingga 64 bait setiap satu. Anda boleh membuat gelung dengan bilangan lelaran tetap, yang boleh berakhir lebih awal.

Saya perhatikan bahawa ada RFC 1624 tentang cara mengira semula sebahagian daripada jumlah semak jika hanya perkataan tetap pakej itu diubah. Walau bagaimanapun, kaedah ini tidak universal, dan pelaksanaannya akan menjadi lebih sukar untuk dikekalkan.

Fungsi pengiraan checksum:

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

Walaupun size disahkan oleh kod panggilan, syarat keluar kedua adalah perlu supaya pengesah boleh membuktikan penyiapan gelung.

Untuk perkataan 32-bit, versi yang lebih mudah dilaksanakan:

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

Sebenarnya mengira semula checksum dan menghantar semula paket:

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;

Fungsi carry() membuat checksum daripada jumlah 32-bit perkataan 16-bit, menurut RFC 791.

Pengesahan jabat tangan TCP

Penapis dengan betul mewujudkan sambungan dengan netcat, kehilangan ACK terakhir, yang Linux membalas dengan paket RST, kerana timbunan rangkaian tidak menerima SYN - ia telah ditukar kepada SYNACK dan dihantar semula - dan dari sudut pandangan OS, paket tiba yang tidak berkaitan dengan pembukaan sambungan.

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

Adalah penting untuk menyemak dengan aplikasi sepenuhnya dan memerhati tcpdump pada xdp-remote kerana, sebagai contoh, hping3 tidak bertindak balas kepada jumlah semak yang salah.

Dari sudut pandangan XDP, pengesahan itu sendiri adalah remeh. Algoritma pengiraan adalah primitif dan berkemungkinan terdedah kepada penyerang yang canggih. Kernel Linux, sebagai contoh, menggunakan SipHash kriptografi, tetapi pelaksanaannya untuk XDP jelas di luar skop artikel ini.

Diperkenalkan untuk TODO baharu yang berkaitan dengan komunikasi luaran:

  • Program XDP tidak boleh disimpan cookie_seed (bahagian rahsia garam) dalam pembolehubah global, anda memerlukan penyimpanan dalam kernel, yang nilainya akan dikemas kini secara berkala daripada penjana yang boleh dipercayai.

  • Jika kuki SYN sepadan dalam paket ACK, anda tidak perlu mencetak mesej, tetapi ingat IP klien yang disahkan untuk meneruskan penghantaran paket daripadanya.

Pengesahan pelanggan yang sah:

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

Log menunjukkan bahawa cek itu lulus (flags=0x2 - ini SYN, flags=0x10 ialah 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

Walaupun tiada senarai IP yang disahkan, tiada perlindungan daripada banjir SYN itu sendiri, tetapi berikut ialah reaksi terhadap banjir ACK yang dilancarkan oleh arahan berikut:

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

Catatan log:

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

Kesimpulan

Kadangkala eBPF secara umum dan XDP khususnya dipersembahkan lebih sebagai alat pentadbir lanjutan daripada sebagai platform pembangunan. Sesungguhnya, XDP ialah alat untuk mengganggu pemprosesan paket oleh kernel, dan bukan alternatif kepada tindanan kernel, seperti DPDK dan pilihan pintasan kernel lain. Sebaliknya, XDP membolehkan anda melaksanakan logik yang agak kompleks, yang, lebih-lebih lagi, mudah dikemas kini tanpa gangguan dalam pemprosesan lalu lintas. Pengesah tidak menimbulkan masalah besar; secara peribadi, saya tidak akan menolak ini untuk bahagian kod ruang pengguna.

Di bahagian kedua, jika topik itu menarik, kami akan melengkapkan jadual pelanggan yang disahkan dan pemutusan sambungan, melaksanakan kaunter dan menulis utiliti ruang pengguna untuk mengurus penapis.

Rujukan:

Sumber: www.habr.com

Tambah komen