eBPF'e Giriş
Giriş
İşletim sistemlerinin içinde bir tür "çekirdek" olduğunu duymuşsunuzdur, hepimiz (farkında olarak veya olmadan) çok büyük bir sıklıkla bu çekirdek ile etkileşime geçeriz.
Bu lab'de kaputu biraz aralayıp, altında çalışan mekanizmalara göz attıktan sonra, nispeten yeni (fakat etkili) bir teknoloji olan eBPF'ten ve eBPF'in çekirdek ekosistemine kattıklarından bahsedeceğiz.
Lab ortamına özel hazırlanmış VM'lere "Start" butonuna tıklayarak erişebilirsiniz. VM'ler için IDE ve Terminal'ler de bu şekilde açılacaktır.
VM'lerde ihtiyacımız olan header dosyalarını barındıran bir eBPF klasörü mevcut, eBPF kodlarımızı bu klasörün içinde, header dosyalarını dahil ederek yazacağız.
Çekirdek (Kernel) nedir?

(Kaynak: [1])
İşletim Sistemi çekirdeği, İşletim Sisteminin merkezinde olup sistemin tüm noktalarına tam ve doğrudan erişim hakkına sahip olan ve sistemin içindeki diğer programları koordine eden bir bilgisayar programıdır.
Bununla birlikte, çekirdek donanım ile yazılım arasında bir köprü görevi görür. Donanım kaynaklarını yönetir ve uygulamaların bu kaynaklara erişimini sağlar.
Çekirdeğin kendisi hariç bilgisayarda çalışan tüm programlara kullanıcı alanı (userspace) programları denir. Günlük hayatta somut olarak etkileşime geçtiğimiz çoğu uygulama (web tarayıcıları, ofis programları, oyunlar vb.) kullanıcı alanı (userspace) programlarıdır.

(Kaynak: [5])
Bu tür programlar donanım kaynaklarına (CPU, RAM, disk, network kartları vb.) erişmek istediğinde, bunu doğrudan yapamaz. Bunun yerine, çekirdek aracılığıyla bu kaynaklara erişir. Çekirdek, donanım kaynaklarını yönetir ve uygulamaların bu kaynaklara erişimini düzenler.
Sistem Çağrıları (Syscalls)

Basitleştirilmiş sistem çağrısı örneği
Yukarıda belirttiğimiz gibi, kullanıcı alanı programları donanım kaynaklarına doğrudan erişemezler. Bunun yerine, çekirdek aracılığıyla bu kaynaklara erişirler. Bu erişim işlemi, sistem çağrıları (syscalls) adı verilen özel işlevler aracılığıyla gerçekleştirilir.
Sistem çağrıları, kullanıcı alanı programlarının çekirdek ile iletişim kurmasını sağlar. Örneğin, bir dosya açmak, bir ağ bağlantısı kurmak veya uygulamayı belleğe yüklemek gibi işlemler için sistem çağrıları kullanılır.
Alıştırma 1 -> `strace` kullanarak sistem çağrılarını sayma
strace bir programın yaptığı sistem çağrılarını izlemek için kullandığımız bir araçtır.
Sizce ekrana "Hello, World!" yazdıran basit bir programın kaç tane sistem çağrısı yapması gerekir?
İlk önce aşağıdaki C kodunu kullanarak basit bir "Hello, World!" programı yazın ve derleyin:
#include <stdio.h>
int main() {
printf("Hello, World!\n");
return 0;
}
Programı clang kullanarak derlemek için:
clang hello_world.c -o hello_world
Programı çalıştırmak için:
./hello_world
Programınızı derledikten, çalıştığına emin olduktan ve tahmininizi yaptıktan sonra, aşağıdaki komutu kullanarak bu programın ekrana "Hello, World!" yazdırmak için yaptığı sistem çağrılarını sayabilirsiniz:
strace -c ./hello_world
Çıktıyı gördükten sonra buraya tıklayın
Tahmininiz ne kadar doğru çıktı? Bu kadar basit bir işlem için bile kernel ile tam 34 kere ping pong oynuyoruz, ama bunun farkında bile değiliz!
Çekirdeği Değiştirmek
Çekirdeğin sistem için olan önemini beraber gördük. Başta belirttiğimiz gibi, çekirdek de aslında yukarıda yazdığımız hello world programı gibi kaynak kodu olan ve derlenen bir programdır.
Peki ya çekirdeğin işleyişini değiştirmek, ona yeni özellikler eklemek veya bir güvenlik açığını kapatmak istersek bunu nasıl yapabiliriz?
Yöntem 1 -> Çekirdeği Yeniden Derlemek

Linux çekirdek kaynak kodu [3]
Değiştirmek istediğimiz çekirdeğin Linux olduğunu varsayarsak, çekirdeğin kaynak kodunu indirip, istediğimiz değişiklikleri yaptıktan sonra çekirdeği yeniden derleyebiliriz.
Fakat bu yöntem için yaptığımız değişikliklerin çekirdeğin geri kalanıyla uyumlu olduğuna ve sistemin kararlı bir şekilde çalışmaya devam ettiğine emin olmamız gerekir, zira çekirdekteki herhangi bir hatanın kernel panic'e sebep olma ihtimali vardır.
Çekirdeğin paniklediği bir senaryoda tüm sistem de çekirdek ile beraber çöker.
Bütün bunlara ilaveten, her yeni iterasyon için çekirdeği yeniden derlemek ve sistemi yeniden başlatmak gerektiği için bu yöntem oldukça zahmetlidir.
Yöntem 2 -> Çekirdek Modülleri

Çekirdek modülü örneği [4]
Çekirdek modülleri (kernel modules), çekirdeğin işleyişini değiştirmek veya yeni özellikler eklemek için kullanılan, çekirdekten bağımsız olarak derlenebilen ve yüklenebilen programlardır.
Çekirdek modüllerinin avantajı, çekirdeği yeniden derlemek zorunda kalmadan, istediğimiz değişiklikleri yapabilmemizdir. Ayrıca, çekirdek modülleri gerektiğinde yükleyip gerektiğinde kaldırabiliriz, bu da sistemin esnekliğini artırır.
Fakat çekirdek modüllerinin de bazı dezavantajları vardır. Bu dezavantajlardan en büyüğü, modüllerin farklı çekirdek sürümleriyle uyumlu olmama ihtimalidir. Modüller ilk yazıldıkları çekirdek sürümünde çalışmalarına rağmen, çekirdek güncellendiğinde modüller uyumsuz hale gelebilir, bu durumda yine bir kernel panic ile karşılaşabiliriz.
eBPF

eBPF mimarisi (Kaynak: [5])
eBPF, çekirdeğin direkt olarak içerisinde bulunan ve çekirdeği yeniden derlemeye gerek kalmadan, çekirdeğin işleyişini değiştirmemize olanak sağlayan bir çekirdek içi sanal makinedir (in-kernel virtual machine).
eBPF ayrıca içindeki Verifier (doğrulayıcı) sayesinde, yüklenen eBPF programlarının güvenli olduğunu ve çekirdeği çökertmeyeceğini garanti eder.
Bu özellikleri sayesinde eBPF, bize çekirdeği değiştirme konusunda geleneksel yöntemelere kıyasla daha hızlı, esnek ve güvenli bir yol sunar.
eBPF Programları

eBPF hook noktaları (Kaynak: [5])
eBPF programları, eBPF sanal makinesi üzerinde çalışan küçük, verimli ve güvenli programlardır. Genelde programlar çekirdekte üzerinde çalışan katmana göre (TC, cgroups, uprobes vb.) isimlendirilirler. Bu programlar, sistem çağrıları dahil çekirdeğin belirli noktalarına takılabilir ve bu noktalarda çalıştırılabilirler.
eBPF programları direkt çekirdeğin içine gömülü oldukları için, klasik userspace programlarına kıyasla çok daha yüksek performans sunar ve sistem kaynaklarını daha verimli bir şekilde kullanırlar.
Alıştırma 2 -> eBPF ile execve() Çağrılarını İzlemek
Bu alıştırmada, eBPF kullanarak execve() sistem çağrısını izleyen bir program yazacağız. Bu program, hangi komutların çalıştırıldığını ve bu komutları çalıştıran programın ne olduğunu görmemizi sağlayacak.
Aşağıdaki eBPF kodunu execve_monitor.c olarak kaydedip derleyin:
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
char LICENSE[] SEC("license") = "GPL";
SEC("tp/syscalls/sys_enter_execve")
int handle_execve(struct trace_event_raw_sys_enter *ctx) {
__u64 pid_tgid = bpf_get_current_pid_tgid();
__u32 pid = pid_tgid >> 32;
char comm[16];
char filename[256];
// Process adını al (execve'yi çağıran program)
bpf_get_current_comm(&comm, sizeof(comm));
// Filename parametresini oku (çalıştırılacak program)
bpf_probe_read_user_str(&filename, sizeof(filename), (void *)ctx->args[0]);
// Trace pipe'a yazdır - hem çağıran hem de çalıştırılacak programı göster
bpf_printk("\n'%s' programi \n '%s' programini calistirdi. \n PID: %d", comm, filename, pid );
return 0;
}
Programı derlemek için:
# eBPF programını derleyin
clang -O2 -target bpf -c execve_monitor.c -o execve_monitor.o
Lab ortamındaki VM'lerde bpftool halihazırda mevcut olduğu için bpf işlemlerimizi bpftool ile yapacağız.
bpftool, eBPF programlarını ve objelerini (map'ler, program'lar, link'ler vb.) yönetmek için kullanılan bir komut satırı aracıdır. eBPF programlarını yükleme, listeleme, denetleme ve hata ayıklama için kullanılır.
Temel kullanım alanları:
- Yüklenmiş eBPF programlarını listeleme ve inceleme
- eBPF objelerini dosya sistemine pin'leme
- eBPF programlarını yükleme ve kaldırma
- eBPF programlarını çekirdeğin çeşitli hook noktalarına takma (attach) ve çıkarma (detach)
bpftool sayesinde eBPF programlarını manuel olarak yönetebilir ve sistemdeki eBPF aktivitesini izleyebiliriz.
Programı yüklemek ve otomatik olarak yerine takmak için:
# eBPF programını yükleyin (load) ve autoattach kullanarak otomatik olarak yerine takın
sudo bpftool prog load execve_monitor.o /sys/fs/bpf/execve_monitor autoattach
Program çalışırken başka bir terminalde komutlar çalıştırarak execve çağrılarını gözlemleyebilirsiniz.
# Trace pipe çıktısını izleyin
sudo cat /sys/kernel/debug/tracing/trace_pipe
Programı silmek/durdurmak için:
sudo rm /sys/fs/bpf/execve_monitor
Burada ne oluyor?

eBPF programımızın çekirdek içindeki lokasyonu [5]
Bu program çalıştırıldığında, onu çalıştıran process'i ve çalıştırılmaya çalışılan programı PID'si ile beraber görebileceksiniz. Örneğin, bir terminal açtığınızda veya bir komut çalıştırdığınızda, bu eBPF programı bu olayı yakalayacak ve olay ile ilgili detayları konsolda görüntüleyecektir.
Bu basit örnek, bize eBPF'in temel işlevini canlı bir şekilde gösteriyor, çekirdeği yeniden derlemeden, sistemde yapılan işlemleri sistem çağrısı seviyesinde, direkt olarak çekirdeğin içinden izleyebiliyoruz!
XDP
eBPF'in en yaygın ve efektif kullanım alanlarından biri de XDP (eXpress Data Path) programları ile ağ paketlerini işleme yeteneğidir. XDP, eBPF sanal makinesinde çalışan program türlerinden sadece birisidir. XDP, ağ paketlerini çekirdek seviyesinde işleyerek, iptables gibi araçlara kıyasla çok daha yüksek performans sunar.
Alıştırma 3 -> iptables ile Paket Filtreleme
Bu alıştırmada, geleneksel bir ağ filtreleme aracı olan iptables kullanarak belirli bir IP adresinden gelen paketleri nasıl engelleyeceğimizi göreceğiz. İlerleyen bölümlerde bunu eBPF/XDP ile karşılaştıracağız.
172.16.0.102 IP adresinden gelen paketleri engellemek için:
# Gelen paketleri engellemek için iptables'ın INPUT chain'ine gerekli kuralı ekleyin
sudo iptables -A INPUT -s 172.16.0.102 -j DROP
Test etmek için:
# box-02'den (172.16.0.102)
sudo ping 172.16.0.101
Kural aktif olduğunda ping paketleri hiçbir yanıt almayacaktır.
Kuralı kaldırmak için:
sudo iptables -D INPUT 1

Koyduğumuz iptables kuralının çekirdek içindeki konumu
iptables, çekirdeğin Netfilter adlı modülünü kullanarak ağ trafiğini kontrol eder, iptables aslında çekirdeğin içindeki bu modülün userspace arayüzüdür. Asıl paket filtreleme işlemi netfilter katmanında, netfilter hook'ları aracılığıyla gerçekleşir.

iptables ile ilgili daha detaylı bilgi ve yukarıdaki gibi iyi çizilmiş diyagramlar için bu platformun da yaratıcısı olan Ivan'ın Layman's iptables [6] bloguna göz atmanızı öneririm.
Alıştırma 4 -> XDP ile Paket Filtreleme
Bu alıştırmada, bu sefer 172.16.0.102 IP adresinden gelen paketleri iptables yerine eBPF/XDP kullanarak engelleyeceğiz.
Aşağıdaki XDP kodunu xdp_drop.c olarak kaydedin:
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
#include "xdp_helpers.h"
char LICENSE[] SEC("license") = "GPL";
// Engellenecek IP adresi: 172.16.0.102
#define BLOCKED_IP 0xAC100066 // 172.16.0.102'nin hexadecimal karşılığı
SEC("xdp")
int xdp_drop_ip(struct xdp_md *ctx) {
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
// Ethernet header
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return XDP_ABORTED; // Paket geçersizse direkt olarak paketi düşür ve bunu belirt
// Sadece IP paketlerini kontrol et
if (bpf_ntohs(eth->h_proto) != ETH_P_IP)
return XDP_PASS; // IP paketi değilse geçir
// IP header
struct iphdr *ip = (void *)(eth + 1);
if ((void *)(ip + 1) > data_end)
return XDP_ABORTED; // Paket geçersizse direkt olarak paketi düşür ve bunu belirt
// Kaynak IP adresini kontrol et
if (bpf_ntohl(ip->saddr) == BLOCKED_IP) {
bpf_printk("\n XDP: Paket engellendi! \n Kaynak IP: 172.16.0.102");
return XDP_DROP; // Paketi düşür
}
return XDP_PASS; // Diğer paketleri geçir
}
Programı derlemek ve yüklemek için:
# XDP programını derleyin (compile)
clang -O2 -target bpf -c xdp_drop.c -o xdp_drop.o
# Programı yükleyin (load)
sudo bpftool prog load xdp_drop.o /sys/fs/bpf/xdp_drop
# Programı network interface'e takın (attach)
sudo bpftool net attach xdpgeneric pinned /sys/fs/bpf/xdp_drop dev eth0
# Programın hazır olup olmadığını kontrol edin
sudo bpftool net list
Test etmek için:
# box-02'den (172.16.0.102) ping atın
sudo ping 172.16.0.101
# XDP loglarını izlemek için
sudo cat /sys/kernel/debug/tracing/trace_pipe
Programı kaldırmak için:
# XDP programını interface'den ayırın
sudo bpftool net detach xdpgeneric dev eth0
# Programı kaldırın
sudo rm /sys/fs/bpf/xdp_drop
Burada ne oluyor?

IPv4 paket header yapısı (Kaynak: [9])
XDP programları gelen paketin herhangi bir bölümüne erişebilir ve onu değiştirebilir. Bizim örneğimizde, (eğer paket bir IP paketi ise) program gelen paketin IP header'ında bulunan kaynak IP adresini okuyarak bu adresin düşürülmek istenen IP adresi olup olmadığını kontrol ediyor.
Eğer paketin header'ında bulunan kaynak IP adresi düşürülmek istenen IP adresi ile eşleşiyorsa, program paketi düşürüyor ( XDP_DROP ).
Eşleşme olmaması durumunda ise paket, XDP programı orada hiç yokmuş gibi, çekirdeğin network stack'indeki yolcuğuna devam ediyor ( XDP_PASS ).

XDP'nin çalışabileceği yerler
Yukardaki diyagramda da görebileceğiniz gibi, XDP programları modlarına göre (skb/generic, Native veya Offload) çekirdeğin farklı noktalarında (veya direkt olarak Ağ kartının üzerinde) çalışabilirler, bir XDP programı ne kadar erken çalışırsa, o kadar yüksek performans sunar.
Lab VM'lerinin sanal NIC'leri diğer modları desteklemediği için XDP programımız bu modlar arasında en yavaş mod olan skb/generic modunda çalışıyor.
XDP ve iptables Performans Karşılaştırması
Her iki yöntemi de kullanarak belirli bir IP adresinden gelen paketleri engelledik. İki yöntem de aynı işlevi yerine getiriyor gibi görünse de, işler performans açısından oldukça farklı.
Yukarıda, dünyadaki bütün web sitelerinin yaklaşık 20.9%'unun [7] DDoS saldırılarına karşı korunmak için kullandığı Cloudflare'in 2018 yılında yayınladığı bir çalışmanın [8] sonuçlarını görüyoruz.
Çalışmada tek bir CPU çekirdeğine saniyede 14 milyon UDP paketi gönderiliyor (14 Mpps) ve çekirdeğin farklı paket filtreleme yöntemleri ile bu paketleri ne kadar hızlı işleyebildiği karşılaştırılmak isteniyor.
İlk görseldeki sonuçlar, iptables ve nftables kullanılarak yapılan paket filtreleme işlemlerinin performansını gösteriyor.
iptables, kendi paket filtreleme sınırlarını PREROUTING zincirinde zorlamasına rağmen ortalama 1.7 milyon paket/saniye (1.7 Mpps) seviyelerinde kalıyor.
İkinci görselde ise XDP kullanılarak yapılan paket filtreleme işlemlerinin performansını görüyoruz. XDP, Native modunda iken aynı donanım üzerinde ortalama 10 milyon paket/saniye (10 Mpps) seviyelerine kadar çıkabiliyor.
Bu sonuçlar, XDP'nin (native modunda) iptables'a kıyasla yaklaşık 6 kat daha yüksek performans sunduğunu gösteriyor. Bu durumda da, yüksek trafikli ağlarda paket filtreleme işlemleri için XDP'nin iptables'a kıyasla ideal bir çözüm olduğunu görmek çok zor değil.
Cloudflare, Meta veya Netflix gibi hyperscaler şirketlerin yanı sıra Kubernetes ekosisteminde de Cilium gibi eBPF/XDP tabanlı çözümler giderek daha popüler hale geliyor, gelecek ne gösterecek hep birlikte göreceğiz.
Kaynakça
Level up your Server Side game — Join 20,000 engineers who receive insightful learning materials straight to their inbox

