Kami menulis perlindungan terhadap serangan DDoS di XDP. Bagian nuklir

Teknologi eXpress Data Path (XDP) memungkinkan pemrosesan lalu lintas sewenang-wenang pada antarmuka Linux sebelum paket memasuki tumpukan jaringan kernel. Penerapan XDP - perlindungan terhadap serangan DDoS (CloudFlare), filter kompleks, pengumpulan statistik (Netflix). Program XDP dijalankan oleh mesin virtual eBPF, dan karenanya memiliki batasan pada kodenya dan fungsi kernel yang tersedia, bergantung pada jenis filter.

Artikel ini dimaksudkan untuk menutupi kekurangan berbagai materi di XDP. Pertama, mereka menyediakan kode siap pakai yang langsung melewati fitur XDP: disiapkan untuk verifikasi atau terlalu sederhana untuk menimbulkan masalah. Saat Anda mencoba menulis kode Anda sendiri dari awal nanti, tidak ada pemahaman tentang apa yang harus dilakukan dengan kesalahan tipikal. Kedua, ini tidak mencakup cara untuk menguji XDP secara lokal tanpa VM dan perangkat keras, meskipun faktanya mereka memiliki jebakannya sendiri. Teks ini ditujukan untuk pemrogram yang akrab dengan jaringan dan Linux yang tertarik dengan XDP dan eBPF.

Pada bagian ini, kami akan memahami secara detail bagaimana filter XDP dirakit dan cara mengujinya, kemudian kami akan menulis versi sederhana dari mekanisme cookie SYN yang terkenal di tingkat pemrosesan paket. Sampai kita membentuk "daftar putih"
klien terverifikasi, simpan penghitung dan kelola filter - log yang cukup.

Kami akan menulis dalam C - ini tidak modis, tetapi praktis. Semua kode tersedia di GitHub di tautan di bagian akhir dan dibagi menjadi komit sesuai dengan langkah-langkah yang dijelaskan dalam artikel.

Penolakan. Dalam artikel ini, solusi mini untuk menangkis serangan DDoS akan dikembangkan, karena ini adalah tugas realistis untuk XDP dan area saya. Namun, tujuan utamanya adalah untuk memahami teknologinya, ini bukan panduan untuk menciptakan perlindungan yang siap pakai. Kode tutorial tidak dioptimalkan dan menghilangkan beberapa nuansa.

Tinjauan Singkat XDP

Saya hanya akan menyebutkan poin-poin penting agar tidak menduplikasi dokumentasi dan artikel yang ada.

Jadi, kode filter dimuat ke dalam kernel. Filter melewati paket yang masuk. Akibatnya, filter harus membuat keputusan: meneruskan paket ke kernel (XDP_PASS), jatuhkan paket (XDP_DROP) atau mengirimkannya kembali (XDP_TX). Filter dapat mengubah paket, terutama untuk XDP_TX. Anda juga dapat menghentikan program (XDP_ABORTED) dan jatuhkan paketnya, tetapi ini analog assert(0) - untuk men-debug.

Mesin virtual eBPF (extended Berkley Packet Filter) sengaja dibuat sederhana agar kernel dapat memeriksa bahwa kode tidak berulang dan tidak merusak memori orang lain. Pembatasan dan pemeriksaan kumulatif:

  • Loop (melompat mundur) dilarang.
  • Ada tumpukan untuk data, tetapi tidak ada fungsi (semua fungsi C harus sebaris).
  • Akses ke memori di luar stack dan buffer paket dilarang.
  • Ukuran kodenya terbatas, tetapi dalam praktiknya tidak terlalu signifikan.
  • Hanya fungsi kernel khusus (pembantu eBPF) yang diizinkan.

Mengembangkan dan memasang filter terlihat seperti ini:

  1. kode sumber (mis. kernel.c) mengkompilasi ke objek (kernel.o) untuk arsitektur mesin virtual eBPF. Sejak Oktober 2019, kompilasi ke eBPF didukung oleh Clang dan dijanjikan di GCC 10.1.
  2. Jika dalam kode objek ini ada panggilan ke struktur kernel (misalnya, ke tabel dan penghitung), alih-alih ID mereka ada nol, artinya kode tersebut tidak dapat dieksekusi. Sebelum memuat ke kernel, angka nol ini harus diganti dengan ID objek tertentu yang dibuat melalui panggilan kernel (tautkan kode). Anda dapat melakukannya dengan utilitas eksternal, atau Anda dapat menulis program yang akan menautkan dan memuat filter tertentu.
  3. Kernel memverifikasi program yang dimuat. Ini memeriksa tidak adanya siklus dan non-keluar dari paket dan batas tumpukan. Jika pemverifikasi tidak dapat membuktikan bahwa kodenya benar, program ditolak - seseorang harus dapat menyenangkannya.
  4. Setelah verifikasi berhasil, kernel mengkompilasi kode objek arsitektur eBPF ke dalam kode mesin arsitektur sistem (tepat waktu).
  5. Program dilampirkan ke antarmuka dan mulai memproses paket.

Karena XDP berjalan di kernel, debugging dilakukan oleh log pelacakan dan, pada kenyataannya, oleh paket yang difilter atau dihasilkan oleh program. Namun, eBPF menjaga keamanan kode yang diunduh untuk sistem, sehingga Anda dapat bereksperimen dengan XDP langsung di Linux lokal Anda.

Mempersiapkan Lingkungan

Majelis

Dentang tidak dapat langsung mengeluarkan kode objek untuk arsitektur eBPF, jadi prosesnya terdiri dari dua langkah:

  1. Kompilasi kode C ke bytecode LLVM (clang -emit-llvm).
  2. Konversi bytecode ke kode objek eBPF (llc -march=bpf -filetype=obj).

Saat menulis filter, beberapa file dengan fungsi tambahan dan makro akan berguna dari pengujian kernel. Adalah penting bahwa mereka cocok dengan versi kernel (KVER). Unduh mereka 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 berisi path ke header kernel, ARCH - sistem arsitektur. Jalur dan alat mungkin sedikit berbeda di antara distribusi.

Contoh perbedaan 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 sertakan direktori dengan header tambahan dan beberapa direktori dengan header kernel. Simbol __KERNEL__ berarti header UAPI (userspace API) ditentukan untuk kode kernel, karena filter dijalankan di kernel.

Perlindungan tumpukan dapat dinonaktifkan (-fno-stack-protector) karena pemverifikasi kode eBPF tetap memeriksa batas tumpukan yang tidak keluar. Anda harus segera mengaktifkan pengoptimalan, karena ukuran bytecode eBPF terbatas.

Mari kita mulai dengan filter yang melewatkan 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";

Tim make mengumpulkan xdp_filter.o. Di mana Anda dapat mengujinya sekarang?

Tempat uji coba

Dudukan harus menyertakan dua antarmuka: di mana akan ada filter dan dari mana paket akan dikirim. Ini harus perangkat Linux lengkap dengan IP mereka sendiri untuk memeriksa bagaimana aplikasi biasa bekerja dengan filter kami.

Perangkat seperti veth (virtual Ethernet) cocok untuk kita: mereka adalah sepasang antarmuka jaringan virtual yang "terhubung" langsung satu sama lain. Anda dapat membuatnya seperti ini (di bagian ini, semua perintah ip dilakukan dari root):

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

Di sini xdp-remote и xdp-local — nama perangkat. Pada xdp-local (192.0.2.1/24) filter akan dilampirkan, dengan xdp-remote (192.0.2.2/24) lalu lintas masuk akan dikirim. Namun, ada masalah: antarmuka berada di mesin yang sama, dan Linux tidak akan mengirimkan lalu lintas ke salah satunya melalui yang lain. Anda bisa menyelesaikannya dengan aturan rumit iptables, tetapi mereka harus mengubah paket, yang merepotkan saat melakukan debug. Lebih baik menggunakan ruang nama jaringan (ruang nama jaringan, jaringan lebih lanjut).

Ruang nama jaringan berisi sekumpulan antarmuka, tabel perutean, dan aturan NetFilter yang diisolasi dari objek serupa di netn lain. Setiap proses berjalan di beberapa ruang nama, dan hanya objek dari jaring ini yang tersedia untuknya. Secara default, sistem memiliki satu namespace jaringan untuk semua objek, sehingga Anda dapat bekerja di Linux dan tidak tahu tentang netns.

Mari buat namespace baru xdp-test dan pindah 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 (itu akan tetap di netns secara default) dan ketika mengirim paket ke 192.0.2.1 akan melewatinya xdp-remote, karena hanya itu antarmuka di 192.0.2.0/24 yang tersedia untuk proses ini. Ini juga bekerja secara terbalik.

Saat berpindah antar jaringan, antarmuka turun dan kehilangan alamat. Untuk menyiapkan antarmuka di netns, Anda perlu menjalankan ip ... di namespace perintah 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 berbeda dengan pengaturan xdp-local di ruang nama default:

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

Jika lari tcpdump -tnevi xdp-local, Anda dapat melihat bahwa paket dikirim dari xdp-test, dikirim ke antarmuka ini:

ip netns exec xdp-test   ping 192.0.2.1

Lebih mudah untuk menjalankan shell xdp-test. Repositori memiliki skrip yang mengotomatiskan pekerjaan dengan dudukan, misalnya, Anda dapat mengatur dudukan dengan perintah sudo ./stand up dan menghapusnya sudo ./stand down.

pelacakan

Filter terpasang ke perangkat seperti ini:

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

Ключ -force diperlukan untuk menautkan program baru jika yang lain sudah ditautkan. "Tidak ada berita adalah kabar baik" bukan tentang perintah ini, hasilnya tetap banyak. menunjukkan verbose opsional, tetapi dengan itu muncul laporan tentang pekerjaan pemverifikasi kode dengan daftar assembler:

Verifier analysis:

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

Lepaskan program dari antarmuka:

ip link set dev xdp-local xdp off

Dalam skrip, ini adalah perintah sudo ./stand attach и sudo ./stand detach.

Dengan mengikat filter, Anda dapat memastikannya ping terus bekerja, tetapi apakah programnya berhasil? Mari tambahkan logo. Fungsi bpf_trace_printk() mirip dengan printf(), tetapi hanya mendukung hingga tiga argumen selain pola, dan daftar penentu yang terbatas. Makro bpf_printk() menyederhanakan panggilan.

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

Outputnya menuju ke saluran jejak kernel, yang perlu diaktifkan:

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

Lihat aliran pesan:

cat /sys/kernel/debug/tracing/trace_pipe

Kedua tim ini melakukan panggilan sudo ./stand log.

Ping sekarang harus menghasilkan pesan seperti ini di dalamnya:

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

Jika Anda melihat lebih dekat pada keluaran pemverifikasi, Anda dapat melihat perhitungan yang aneh:

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 adalah bahwa program eBPF tidak memiliki bagian data, jadi satu-satunya cara untuk menyandikan string format adalah argumen langsung dari perintah VM:

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

Karena alasan ini, keluaran debug sangat membengkakkan kode yang dihasilkan.

Mengirim Paket XDP

Mari kita ubah filter: biarkan mengirim kembali semua paket yang masuk. Ini salah dari sudut pandang jaringan, karena perlu mengubah alamat di header, tetapi sekarang pekerjaan pada prinsipnya penting.

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

Meluncurkan tcpdump pada xdp-remote. Itu harus menunjukkan Permintaan Gema ICMP keluar dan masuk yang identik dan berhenti menampilkan Balas Gema ICMP. Tapi itu tidak terlihat. Ternyata bekerja XDP_TX dalam program untuk xdp-local perluuntuk memasangkan antarmuka xdp-remote sebuah program juga ditugaskan, meskipun kosong, dan dinaikkan.

Bagaimana saya tahu?

Menelusuri jalur paket di kernel mekanisme acara perf memungkinkan, menggunakan mesin virtual yang sama, yaitu, eBPF digunakan untuk membongkar dengan eBPF.

Anda harus membuat yang baik dari yang jahat, karena tidak ada lagi yang bisa dibuat darinya.

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

Apa itu kode 6?

$ errno 6
ENXIO 6 No such device or address

Fungsi veth_xdp_flush_bq() mendapat kode kesalahan dari veth_xdp_xmit(), di mana cari berdasarkan ENXIO dan menemukan komentar.

Kembalikan filter minimum (XDP_PASS) dalam file xdp_dummy.c, tambahkan ke Makefile, ikat ke 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 ditampilkan, Anda perlu menghapus filter (ini membuat sudo ./stand detach), membiarkan ping, lalu instal filter dan coba lagi. Masalahnya adalah bahwa filter XDP_TX mempengaruhi ARP juga, dan jika stack
ruang nama xdp-test berhasil "melupakan" alamat MAC 192.0.2.1, dia tidak akan dapat menyelesaikan IP ini.

Pernyataan masalah

Mari beralih ke tugas yang disebutkan: menulis mekanisme cookie SYN di XDP.

Hingga saat ini, SYN Flood tetap menjadi serangan DDoS yang populer, yang intinya adalah sebagai berikut. Saat koneksi dibuat (jabat tangan TCP), server menerima SYN, mengalokasikan sumber daya untuk koneksi di masa mendatang, merespons dengan paket SYNACK, dan menunggu ACK. Penyerang hanya mengirimkan paket SYN dari alamat palsu dalam jumlah ribuan per detik dari setiap host dalam ribuan botnet. Server dipaksa untuk mengalokasikan sumber daya segera setelah kedatangan paket, tetapi melepaskannya setelah waktu tunggu yang lama, akibatnya, memori atau batas habis, koneksi baru tidak diterima, layanan tidak tersedia.

Jika Anda tidak mengalokasikan sumber daya pada paket SYN, tetapi hanya merespons dengan paket SYNACK, lalu bagaimana server dapat memahami bahwa paket ACK yang datang kemudian adalah milik paket SYN yang tidak disimpan? Lagi pula, penyerang juga bisa menghasilkan ACK palsu. Inti dari cookie SYN adalah untuk menyandikan seqnum parameter koneksi sebagai hash alamat, port, dan garam yang berubah. Jika ACK berhasil sampai sebelum salt berubah, Anda dapat menghitung hash lagi dan membandingkannya acknum. palsu acknum penyerang tidak bisa, karena garam menyertakan rahasianya, dan tidak akan punya waktu untuk memilahnya karena saluran yang terbatas.

Cookie SYN telah diterapkan di kernel Linux sejak lama dan bahkan dapat diaktifkan secara otomatis jika SYN datang terlalu cepat dan dalam jumlah besar.

Program pendidikan tentang jabat tangan TCP

TCP menyediakan transfer data sebagai aliran byte, misalnya, permintaan HTTP dikirim melalui TCP. Aliran ditransmisikan sepotong demi sepotong dalam paket. Semua paket TCP memiliki flag logis dan nomor urut 32-bit:

  • Kombinasi flag menentukan peran paket tertentu. Bendera SYN berarti bahwa ini adalah paket pertama pengirim pada koneksi. Bendera ACK berarti pengirim telah menerima semua data koneksi hingga satu byte. acknum. Sebuah paket mungkin memiliki beberapa flag dan dinamai berdasarkan kombinasinya, misalnya paket SYNACK.

  • Nomor urut (seqnum) menentukan offset dalam aliran data untuk byte pertama yang dikirim dalam paket ini. Misalnya, jika di paket pertama dengan data X byte, nomor ini adalah N, di paket berikutnya dengan data baru akan menjadi N+X. Di awal koneksi, masing-masing pihak memilih nomor ini secara acak.

  • Nomor pengakuan (acknum) - offset yang sama dengan seqnum, tetapi tidak menentukan jumlah byte yang dikirimkan, tetapi jumlah byte pertama dari penerima, yang tidak dilihat oleh pengirim.

Di awal hubungan, para pihak harus setuju seqnum и acknum. Klien mengirimkan paket SYN dengan miliknya seqnum = X. Server merespons dengan paket SYNACK, di mana ia menulis sendiri seqnum = Y dan mengekspos acknum = X + 1. Klien menanggapi SYNACK dengan paket ACK, di mana seqnum = X + 1, acknum = Y + 1. Setelah itu, transfer data sebenarnya dimulai.

Jika lawan bicara tidak mengakui penerimaan paket, TCP mengirimkannya kembali dengan batas waktu.

Mengapa cookie SYN tidak selalu digunakan?

Pertama, jika SYNACK atau ACK hilang, Anda harus menunggu pengiriman ulang - koneksi melambat. Kedua, di paket SYN - dan hanya di dalamnya! - sejumlah opsi ditransmisikan yang memengaruhi pengoperasian koneksi lebih lanjut. Karena tidak mengingat paket SYN yang masuk, server mengabaikan opsi ini, dalam paket berikut klien tidak akan lagi mengirimnya. TCP dapat berfungsi dalam kasus ini, tetapi setidaknya pada tahap awal kualitas koneksi akan menurun.

Dalam hal paket, program XDP harus melakukan hal berikut:

  • menanggapi SYN dengan SYNACK dengan cookie;
  • jawab ACK dengan RST (putuskan koneksi);
  • jatuhkan paket lain.

Pseudocode dari algoritma bersama dengan penguraian paket:

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

Satu (*) titik-titik di mana Anda perlu mengelola status sistem ditandai - pada tahap pertama, Anda dapat melakukannya tanpanya hanya dengan menerapkan jabat tangan TCP dengan membuat cookie SYN sebagai seqnum.

Di tempat (**), sementara kami tidak memiliki tabel, kami akan melewatkan paketnya.

Implementasi jabat tangan TCP

Parsing paket dan verifikasi kode

Kami membutuhkan struktur tajuk jaringan: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) dan TCP (uapi/linux/tcp.h). Yang terakhir saya tidak dapat terhubung karena kesalahan terkait atomic64_t, saya harus menyalin definisi yang diperlukan ke dalam kode.

Semua fungsi yang dibedakan dalam C untuk keterbacaan harus digariskan di situs panggilan, karena pemverifikasi eBPF di kernel melarang lompatan balik, yaitu, sebenarnya, loop dan panggilan fungsi.

#define INTERNAL static __attribute__((always_inline))

Makro LOG() menonaktifkan pencetakan dalam versi rilis.

Program ini adalah saluran fungsi. Masing-masing menerima paket di mana header dari level yang sesuai disorot, misalnya, process_ether() menunggu untuk diisi ether. Berdasarkan hasil analisis lapangan, fungsi tersebut dapat mentransfer paket ke level yang lebih tinggi. Hasil dari fungsi tersebut adalah aksi XDP. Sementara penangan SYN dan ACK membiarkan semua paket lewat.

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 memperhatikan tanda centang A dan B. Jika Anda berkomentar A, program akan dibangun, tetapi akan ada kesalahan verifikasi saat memuat:

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!

Tali kunci invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0): ada jalur eksekusi ketika byte ketiga belas dari awal buffer berada di luar paket. Sulit untuk mengetahui dari daftar baris mana yang sedang kita bicarakan, tetapi ada nomor instruksi (12) dan disassembler yang menunjukkan baris kode sumber:

llvm-objdump -S xdp_filter.o | less

Dalam hal ini, itu menunjuk ke garis

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

yang menjelaskan bahwa masalahnya adalah ether. Akan selalu seperti itu.

Balas ke SYN

Tujuan pada tahap ini adalah menghasilkan paket SYNACK yang benar dengan fixed seqnum, yang akan digantikan oleh cookie SYN di masa mendatang. Semua perubahan terjadi di process_tcp_syn() dan sekitarnya.

Memeriksa paket

Anehnya, inilah baris yang paling luar biasa, atau lebih tepatnya, komentar untuk itu:

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

Saat menulis kode versi pertama, kernel 5.1 digunakan, untuk pemverifikasi yang ada perbedaan antara data_end и (const void*)ctx->data_end. Pada saat penulisan, kernel 5.3.1 tidak mengalami masalah ini. Mungkin kompiler mengakses variabel lokal berbeda dari bidang. Moral - pada sarang yang besar, menyederhanakan kode dapat membantu.

Pemeriksaan panjang rutin lebih lanjut untuk kemuliaan pemverifikasi; HAI 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 */
}

Penyebaran paket

Mengisi seqnum и acknum, setel ACK (SYN sudah disetel):

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 MAC. Pustaka standar tidak tersedia dari program XDP, jadi memcpy() — sebuah 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);

Perhitungan ulang checksum

Checksum IPv4 dan TCP memerlukan penambahan semua kata 16-bit di header, dan ukuran header ditulis di dalamnya, yaitu, pada saat kompilasi tidak diketahui. Ini menjadi masalah karena pemverifikasi tidak akan melewatkan loop normal hingga variabel batas. Tetapi ukuran header terbatas: masing-masing hingga 64 byte. Anda dapat membuat perulangan dengan jumlah iterasi tetap, yang dapat berakhir lebih awal.

Saya perhatikan bahwa ada RFC 1624 tentang cara menghitung ulang sebagian checksum jika hanya kata-kata tetap dari paket yang diubah. Namun, metodenya tidak universal, dan penerapannya akan lebih sulit dipertahankan.

Fungsi perhitungan 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;
}

Meskipun size diperiksa oleh kode pemanggil, kondisi keluar kedua diperlukan agar pemverifikasi dapat membuktikan akhir dari perulangan.

Untuk kata-kata 32-bit, versi yang lebih sederhana diimplementasikan:

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

Sebenarnya menghitung ulang checksum dan mengirim paket kembali:

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 dari jumlah 32-bit kata-kata 16-bit, menurut RFC 791.

Pemeriksaan jabat tangan TCP

Filter dengan benar membuat koneksi dengan netcat, melewatkan ACK terakhir, yang ditanggapi Linux dengan paket RST, karena tumpukan jaringan tidak menerima SYN - itu diubah menjadi SYNACK dan dikirim kembali - dan dari sudut pandang OS, sebuah paket tiba yang tidak berhubungan dengan koneksi terbuka.

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

Penting untuk memeriksa dengan aplikasi lengkap dan mengamati tcpdump pada xdp-remote karena, misalnya, hping3 tidak menanggapi checksum yang salah.

Dari sudut pandang XDP, pemeriksaan itu sendiri sepele. Algoritme perhitungannya primitif dan mungkin rentan terhadap penyerang yang canggih. Kernel Linux, misalnya, menggunakan kriptografi SipHash, tetapi penerapannya untuk XDP jelas berada di luar cakupan artikel ini.

Muncul untuk TODO baru terkait interaksi eksternal:

  • Program XDP tidak dapat menyimpan cookie_seed (bagian rahasia dari garam) dalam variabel global, Anda memerlukan penyimpanan kernel yang nilainya akan diperbarui secara berkala dari generator yang andal.

  • Jika cookie SYN dalam paket ACK cocok, Anda tidak perlu mencetak pesan, tetapi ingat IP klien terverifikasi untuk melewati paket lebih lanjut darinya.

Validasi oleh klien yang sah:

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

Log mencatat bagian dari cek (flags=0x2 adalah SYN, flags=0x10 adalah 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

Selama tidak ada daftar IP terverifikasi, tidak akan ada perlindungan terhadap banjir SYN itu sendiri, tetapi berikut adalah reaksi terhadap banjir ACK yang diluncurkan oleh perintah ini:

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

Entri log:

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

Kesimpulan

Terkadang eBPF secara umum dan XDP secara khusus ditampilkan lebih sebagai alat administrator tingkat lanjut daripada platform pengembangan. Memang, XDP adalah alat untuk mengganggu pemrosesan paket kernel, dan bukan alternatif tumpukan kernel, seperti DPDK dan opsi bypass kernel lainnya. Di sisi lain, XDP memungkinkan Anda menerapkan logika yang agak rumit, yang, terlebih lagi, mudah diperbarui tanpa jeda dalam pemrosesan lalu lintas. Pemverifikasi tidak menimbulkan masalah besar, secara pribadi saya tidak akan menolak untuk bagian dari kode ruang pengguna.

Di bagian kedua, jika topiknya menarik, kami akan melengkapi tabel klien terverifikasi dan memutus koneksi, menerapkan penghitung, dan menulis utilitas ruang pengguna untuk mengelola filter.

Ссылки:

Sumber: www.habr.com

Tambah komentar