Bu makale, Go Konf Istanbul '24 konferansında verdiğim konuşmadan derlenmiştir.
Yazılım geliştirirken ya da kullanırken çoğu zaman işletim sisteminin güvenli sınırları içinde oynarız. Ağ arayüzünden gelen bir IP paketinin nasıl karşılandığını ya da bir dosyayı kaydettiğimizde inode'ların dosya sistemi tarafından nasıl ele alındığını çoğu zaman bilmeyiz bile.
İşte bu sınıra user space denir; uygulamalarımızı, kütüphanelerimizi ve araçlarımızı yazdığımız yer burasıdır. Ama bir de kernel space var. Bu, işletim sistemi kernel'inin yaşadığı ve bellek, CPU, I/O cihazları gibi sistem kaynaklarının yönetiminden sorumlu olduğu yerdir.

Genellikle soketlerin veya dosya tanımlayıcılarının altına inmemize gerek olmaz, ama bazen gerekir. Diyelim ki bir uygulamanın ne kadar kaynak tükettiğini görmek için profilini çıkarmak istiyorsunuz.
Eğer uygulamayı user space'ten profillerseniz, sadece çok sayıda faydalı detayı kaçırmakla kalmaz, aynı zamanda profillemenin kendisi için ciddi miktarda kaynak harcarsınız; çünkü CPU veya belleğin üzerindeki her katman bir miktar overhead ekler.
Daha Derine İnme İhtiyacı
Diyelim ki yığında aşağıya inmek ve bir şekilde kendi kodunuzu kernel'in içine sokmak istiyorsunuz; ister uygulamayı profillemek, ister sistem çağrılarını izlemek, ister ağ paketlerini gözlemlemek için olsun. Bunu nasıl yaparsınız?
Geleneksel olarak iki seçeneğiniz var.
1. Seçenek: Kernel Kaynak Kodunu Düzenlemek
Eğer Linux kernel kaynak kodunu değiştirmek ve aynı kernel'i müşterinizin makinesine göndermek isterseniz, önce Linux kernel topluluğunu bu değişikliğin gerekli olduğuna ikna etmeniz gerekir. Sonra da yeni kernel versiyonunun Linux dağıtımları tarafından benimsenmesi için birkaç yıl beklemeniz gerekir.
Bu, çoğu durum için pratik bir yaklaşım değildir; üstelik sadece bir uygulamayı profillemek ya da ağ paketlerini izlemek için bayağı abartılı kalır.
2. Seçenek: Kernel Modülü Yazmak
Kernel'e yüklenip çalıştırılabilen bir kod parçası olan kernel modülü yazabilirsiniz. Bu daha pratik bir yaklaşımdır ama kendine has riskleri ve dezavantajları vardır.
Birincisi, kernel modülü yazmanız gerekir ki bu kolay bir iş değildir. Ardından düzenli olarak bakımını yapmanız gerekir; çünkü kernel canlı bir varlıktır ve zamanla değişir. Kernel modülünüzün bakımını yapmazsanız, modülünüz güncelliğini yitirir ve yeni kernel versiyonlarıyla çalışmaz.
İkincisi, Linux kernel'inizi bozma riski taşırsınız; çünkü kernel modüllerinin güvenlik sınırları yoktur. Eğer hatalı bir kernel modülü yazarsanız, tüm sistemi çökertebilir.
eBPF Sahneye Çıkıyor
eBPF (Extended Berkeley Packet Filter), sistemi yeniden başlatmaya bile gerek kalmadan Linux kernel'ini dakikalar içinde yeniden programlamanıza olanak tanıyan devrim niteliğinde bir teknolojidir.
eBPF; sistem çağrılarını, userspace fonksiyonlarını, kütüphane fonksiyonlarını, ağ paketlerini ve çok daha fazlasını izlemenizi sağlar. Sistem performansı, observability, güvenlik ve çok daha fazlası için güçlü bir araçtır.
Peki nasıl?
eBPF, birkaç bileşenden oluşan bir sistemdir:
- eBPF programları
- eBPF hook'ları
- BPF map'leri
- eBPF verifier
- eBPF sanal makinesi
"BPF" ve "eBPF" terimlerini birbirinin yerine kullandığımı belirteyim. eBPF, "Extended Berkeley Packet Filter" anlamına gelir. BPF, Linux'a aslen ağ paketlerini filtrelemek için eklendi; eBPF ise orijinal BPF'i başka amaçlar için de kullanılabilecek şekilde genişletir. Bugün artık Berkeley ile bir ilgisi yok ve sadece paket filtrelemeye yaramıyor.
Aşağıda eBPF'in hem user space'te hem de kaputun altında nasıl çalıştığını gösteren bir illüstrasyon var. eBPF programları C gibi yüksek seviyeli bir dilde yazılır ve ardından eBPF bytecode'una derlenir. Bu eBPF bytecode'u kernel'e yüklenir ve eBPF sanal makinesi tarafından yürütülür.
Bir eBPF programı, kernel'deki belirli bir kod yoluna (örneğin bir sistem çağrısına) bağlanır. Bu kod yollarına "hook" denir. Hook tetiklendiğinde eBPF programı çalıştırılır ve yazdığınız özel mantığı yerine getirir. Bu sayede kendi kodumuzu kernel uzayında çalıştırabiliriz.

eBPF ile Hello World
Detaylara geçmeden önce, execve sistem çağrısını izleyen basit bir eBPF programı yazalım. Programı C'de, user space programını ise Go'da yazacağız. Sonra user space programını çalıştırıp eBPF programını kernel'e yükleyecek ve gerçek execve sistem çağrısı yürütülmeden hemen önce eBPF programımızdan göndereceğimiz özel olayları dinleyeceğiz.
eBPF Programını Yazmak
hello_ebpf.c 1#include "vmlinux.h"
2#include <bpf/bpf_helpers.h>
3
4struct event {
5 u32 pid;
6 u8 comm[100];
7};
8
9struct {
10 __uint(type, BPF_MAP_TYPE_RINGBUF);
11 __uint(max_entries, 1000);
12} events SEC(".maps");
Burada önce kernel'in veri yapılarını ve fonksiyon prototiplerini içeren vmlinux.h başlık dosyasını dahil ediyoruz. Ardından eBPF programları için yardımcı fonksiyonlar barındıran bpf_helpers.h başlık dosyasını ekliyoruz.
Sonra olay verisini tutacak bir struct tanımlıyor ve ardından olayları saklayacağımız bir BPF map tanımlıyoruz. Bu map'i, kernel uzayında çalışacak eBPF programıyla user space programı arasında olayları iletmek için kullanacağız.
BPF map'lerinin detaylarına ileride gireceğiz, o yüzden neden
BPF_MAP_TYPE_RINGBUFkullandığımızı veyaSEC(".maps")'in ne işe yaradığını şimdi anlamadıysanız endişelenmeyin.
Artık ilk programımızı yazmaya ve onu bağlayacağımız hook'u tanımlamaya hazırız:
hello_ebpf.c 1SEC("kprobe/sys_execve")
2int hello_execve(struct pt_regs *ctx) {
3 u64 id = bpf_get_current_pid_tgid();
4 pid_t pid = id >> 32;
5 pid_t tid = (u32)id;
6
7 if (pid != tid)
8 return 0;
9
10 struct event *e;
11
12 e = bpf_ringbuf_reserve(&events, sizeof(struct event), 0);
13 if (!e) {
14 return 0;
15 }
16
17 e->pid = pid;
18 bpf_get_current_comm(&e->comm, 100);
19
20 bpf_ringbuf_submit(e, 0);
21
22 return 0;
23}
Burada hello_execve adında bir fonksiyon tanımlıyor ve kprobe hook'unu kullanarak bunu sys_execve sistem çağrısına bağlıyoruz. kprobe, eBPF'in sunduğu pek çok hook'tan biridir ve kernel fonksiyonlarını izlemek için kullanılır. Bu hook, sys_execve sistem çağrısı yürütülmeden hemen önce hello_execve fonksiyonumuzu tetikleyecek.
hello_execve fonksiyonunun içinde önce process ID ve thread ID'yi alıyoruz, sonra ikisinin aynı olup olmadığını kontrol ediyoruz. Aynı değillerse bir thread içindeyiz demektir ve thread'leri izlemek istemediğimiz için sıfır döndürerek eBPF programından çıkıyoruz.
Ardından olay verisini saklamak için events map'inde yer ayırıyoruz, olay verisini process ID ve süreç adıyla dolduruyoruz ve olayı events map'ine gönderiyoruz.
Buraya kadar oldukça basit, değil mi?
User Space Programını Yazmak
User space programını yazmaya başlamadan önce, programın user space'te ne yapması gerektiğini kısaca açıklayayım. eBPF programını kernel'e yüklemek, BPF map'i oluşturmak, bu map'e bağlanmak ve oradan olayları okumak için bir user space programına ihtiyacımız var.
Bu işlemleri gerçekleştirmek için belirli bir sistem çağrısını kullanmamız gerekiyor. Bu sistem çağrısının adı bpf() ve BPF map'inin içeriğini okumak gibi eBPF'le ilgili birçok işlemi yapmak için kullanılıyor.
bpf() sistem çağrısına yüksek seviye bir arayüz sunan kütüphaneler var. Bunlardan biri Cilium'un ebpf-go paketi; bu örnekte onu kullanacağız.Hadi biraz Go koduna dalalım.
main.go 1//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -type event ebpf hello_ebpf.c
2
3func main() {
4 stopper := make(chan os.Signal, 1)
5 signal.Notify(stopper, os.Interrupt, syscall.SIGTERM)
6
7 // Allow the current process to lock memory for eBPF resources.
8 if err := rlimit.RemoveMemlock(); err != nil {
9 log.Fatal(err)
10 }
11
12 objs := ebpfObjects{}
13 if err := loadEbpfObjects(&objs, nil); err != nil {
14 log.Fatalf("loading objects: %v", err)
15 }
16 defer objs.Close()
17
18 kp, err := link.Kprobe(kprobeFunc, objs.HelloExecve, nil)
19 if err != nil {
20 log.Fatalf("opening kprobe: %s", err)
21 }
22 defer kp.Close()
23
24 rd, err := ringbuf.NewReader(objs.Events)
25 if err != nil {
26 log.Fatalf("opening ringbuf reader: %s", err)
27 }
28 defer rd.Close()
29
30 ...
İlk satır bir Go derleyici direktifi: go:generate. Burada Go derleyicisine, github.com/cilium/ebpf/cmd/bpf2go paketinden bpf2go aracını çalıştırmasını ve hello_ebpf.c dosyasından bir Go dosyası üretmesini söylüyoruz.
Üretilen Go dosyaları; eBPF programının Go karşılığını, eBPF programında tanımladığımız tipleri ve struct'ları vs. içerecek. Bu karşılıkları Go kodumuzun içinde eBPF programını kernel'e yüklemek ve BPF map'iyle etkileşmek için kullanacağız.
Ardından üretilen tipleri kullanarak eBPF programını yüklüyor (loadEbpfObjects), kprobe hook'una bağlanıyor (link.Kprobe) ve BPF map'inden olayları okuyoruz (ringbuf.NewReader). Bu fonksiyonların hepsi üretilen tipleri kullanıyor.
Sıra kernel tarafıyla etkileşime gelmek için:
main.go 1 ...
2
3 go func() {
4 <-stopper
5
6 if err := rd.Close(); err != nil {
7 log.Fatalf("closing ringbuf reader: %s", err)
8 }
9 }()
10
11 log.Println("Waiting for events..")
12
13 var event ebpfEvent
14 for {
15 record, err := rd.Read()
16 if err != nil {
17 if errors.Is(err, ringbuf.ErrClosed) {
18 log.Println("Received signal, exiting..")
19 return
20 }
21 log.Printf("reading from reader: %s", err)
22 continue
23 }
24
25 if err := binary.Read(bytes.NewBuffer(record.RawSample), binary.LittleEndian, &event); err != nil {
26 log.Printf("parsing ringbuf event: %s", err)
27 continue
28 }
29
30 procName := unix.ByteSliceToString(event.Comm[:])
31 log.Printf("pid: %d\tcomm: %s\n", event.Pid, procName)
32 }
33}
Önceki Go parçasında tanımladığımız stopper kanalını dinlemek için bir goroutine başlatıyoruz. Bu kanal, bir interrupt sinyali aldığımızda programı temiz şekilde sonlandırmak için kullanılacak.
Sonra BPF map'inden olayları okumak için bir döngü başlatıyoruz. Olayları okumak için ringbuf.Reader tipini kullanıyor, ardından olay verisini binary.Read fonksiyonuyla, eBPF programından üretilmiş olan ebpfEvent tipine parse ediyoruz.
Son olarak süreç ID'sini ve süreç adını standart çıktıya yazdırıyoruz.
Programı Çalıştırmak
Artık programı çalıştırmaya hazırız. Önce eBPF programını derlememiz, sonra user space programını çalıştırmamız gerekiyor.
1$ go generate
2Compiled /Users/sazak/workspace/gocode/src/github.com/ozansz/intro-ebpf-with-go/0x01-helloworld/ebpf_bpfel.o
3Stripped /Users/sazak/workspace/gocode/src/github.com/ozansz/intro-ebpf-with-go/0x01-helloworld/ebpf_bpfel.o
4Wrote /Users/sazak/workspace/gocode/src/github.com/ozansz/intro-ebpf-with-go/0x01-helloworld/ebpf_bpfel.go
5Compiled /Users/sazak/workspace/gocode/src/github.com/ozansz/intro-ebpf-with-go/0x01-helloworld/ebpf_bpfeb.o
6Stripped /Users/sazak/workspace/gocode/src/github.com/ozansz/intro-ebpf-with-go/0x01-helloworld/ebpf_bpfeb.o
7Wrote /Users/sazak/workspace/gocode/src/github.com/ozansz/intro-ebpf-with-go/0x01-helloworld/ebpf_bpfeb.go
8
9$ go build -o hello_ebpf
Önce eBPF programını derlemek için go generate komutunu, sonra user space programını derlemek için go build komutunu çalıştırıyoruz.
Ardından user space programını çalıştırıyoruz:
1sudo ./hello_ebpf
2hello_ebpf: 01:20:54 Waiting for events..
1limactl shell intro-ebpf
2
3$
Bu sırada ilk kabukta:
1hello_ebpf: 01:22:22 pid: 3360 comm: sshd
2hello_ebpf: 01:22:22 pid: 3360 comm: bash
3hello_ebpf: 01:22:22 pid: 3361 comm: bash
4hello_ebpf: 01:22:22 pid: 3362 comm: bash
5hello_ebpf: 01:22:22 pid: 3363 comm: bash
6hello_ebpf: 01:22:22 pid: 3366 comm: bash
7hello_ebpf: 01:22:22 pid: 3367 comm: lesspipe
8hello_ebpf: 01:22:22 pid: 3369 comm: lesspipe
9hello_ebpf: 01:22:22 pid: 3370 comm: bash
Beklendiği gibi önce sshd sürecinin, sonra bash sürecinin, ardından lesspipe sürecinin başladığını görüyoruz vs.
Bu, execve sistem çağrısını izlemek ve sonra olayları user space'ten BPF map üzerinden okumak için eBPF'i nasıl kullanabileceğimize dair basit bir örnek. Oldukça basit ama güçlü bir program yazdık; kernel kaynak kodunu değiştirmeden ya da sistemi yeniden başlatmadan execve sistem çağrısını yakaladık.
eBPF Hook'ları ve Map'leri
Peki önceki örnekte aslında ne oldu? kprobe hook'unu kullanarak eBPF programını sys_execve sistem çağrısına bağladık, böylece sys_execve her çağrıldığında orijinal sistem çağrısı kodu yürütülmeden hemen önce hello_execve fonksiyonu çalıştı.

eBPF olay-tabanlıdır, yani eBPF programını kernel'deki belirli bir kod yoluna bağlamamızı bekler. Bu kod yollarına "hook" denir ve eBPF'in sunduğu birkaç çeşit hook vardır. En yaygın olanları şunlar:
kprobe,kretprobe: Kernel fonksiyonlarını izleruprobe,uretprobe: User space fonksiyonlarını izlertracepoint: Kernel'de önceden tanımlı tracepoint'leri izlerxdp: eXpress Data Path; ağ paketlerini filtrelemek ve yönlendirmek için kullanılırusdt: User Statically Defined Tracing; user space fonksiyonlarını daha verimli izlemek için kullanılır
kprobe ve uprobe hook'ları bağlı oldukları eBPF programlarını fonksiyon/syscall yürütülmeden önce, kretprobe ve uretprobe ise yürütüldükten sonra çağırır.
Ayrıca olayları saklamak için bir BPF map kullandık. BPF map'leri, farklı türde verileri saklayıp iletmek için kullanılan veri yapılarıdır. Bunları aynı zamanda durum yönetimi için de kullanırız. Pek çok BPF map türü desteklenir ve farklı amaçlar için farklı türde map'ler kullanırız. En yaygın BPF map türlerinden bazıları şunlar:
BPF_MAP_TYPE_HASH: Bir hash mapBPF_MAP_TYPE_ARRAY: Bir diziBPF_MAP_TYPE_RINGBUF: Bir ring bufferBPF_MAP_TYPE_STACK: Bir stackBPF_MAP_TYPE_QUEUE: Bir kuyrukBPF_MAP_TYPE_LRU_HASH: Least recently used (LRU) bir hash map
Bu map türlerinin bazılarının BPF_MAP_TYPE_PERCPU_HASH gibi per-CPU varyantları da vardır; bu, her CPU çekirdeği için ayrı bir hash tablosu tutan bir hash map'tir.
Bir Adım Daha Öteye: Gelen IP Paketlerini İzlemek
Bir adım daha öteye geçelim ve daha karmaşık bir eBPF programı yazalım. Bu kez XDP hook'unu kullanarak eBPF programını ağ arayüzü bir paketi kernel'e gönderdikten hemen sonra, hatta kernel paketi işlemeden önce çalıştıracağız.

eBPF Programını Yazmak
Gelen IP paketlerini kaynak IP adresi ve port numarasına göre sayan bir eBPF programı yazacağız ve sonra bu sayıları user space'te BPF map'ten okuyacağız. Her paketin ethernet, IP ve TCP/UDP başlıklarını parse edeceğiz ve geçerli TCP/UDP paketlerinin sayılarını BPF map'te saklayacağız.
Önce eBPF programı:
hello_ebpf.c 1#include "vmlinux.h"
2#include <bpf/bpf_helpers.h>
3#include <bpf/bpf_endian.h>
4
5#define MAX_MAP_ENTRIES 100
6
7/* Define an LRU hash map for storing packet count by source IP and port */
8struct {
9 __uint(type, BPF_MAP_TYPE_LRU_HASH);
10 __uint(max_entries, MAX_MAP_ENTRIES);
11 __type(key, u64); // source IPv4 addresses and port tuple
12 __type(value, u32); // packet count
13} xdp_stats_map SEC(".maps");
İlk örnekteki gibi vmlinux.h ve BPF yardımcı başlıklarını dahil ediyoruz. Ayrıca IP:port bilgisi ile paket sayısını saklamak için xdp_stats_map adında bir map tanımlıyoruz. Bu map'i hook fonksiyonunun içinde dolduracak ve içeriğini user space programında okuyacağız.
IP:ports derken kastettiğim şey, kaynak IP, kaynak port ve hedef portla paketlenmiş bir u64 değer. IP adresi (özellikle IPv4) 32 bit, her port numarası ise 16 bit; yani üçünü saklamak için tam 64 bite ihtiyacımız var; bu yüzden burada u64 kullanıyoruz. Sadece ingress (gelen paketler) işlediğimiz için hedef IP adresini saklamamıza gerek yok.
Önceki örnekten farklı olarak burada map türü olarak BPF_MAP_TYPE_LRU_HASH kullandık. Bu map türü, bir (key, value) çiftini LRU varyantlı bir hashmap olarak saklamamıza izin verir.
Map'i nasıl tanımladığımıza dikkat edin; maksimum giriş sayısını ve map anahtar ile değerlerinin türlerini açıkça belirtiyoruz. Anahtar için 64-bit unsigned integer, değer için 32-bit unsigned integer kullanıyoruz.
u32'nin maksimum değeri2^32 - 1'dir ki bu, bu örnek için fazlasıyla yeterli sayıda pakettir.
IP adresini ve port numarasını öğrenmek için önce paketi parse edip ethernet, IP ve ardından TCP/UDP başlıklarını okumamız gerekiyor.
XDP, ağ arayüz kartının hemen ardına yerleştirildiği için bize ham paket verisi bayt olarak verilecek; dolayısıyla bayt dizisi üzerinde manuel olarak yürüyerek ethernet, IP ve TCP/UDP başlıklarını çözmemiz gerekecek.
Neyse ki tüm başlık tanımları (struct ethhdr, struct iphdr, struct tcphdr ve struct udphdr) vmlinux.h başlık dosyasının içinde mevcut. Bu struct'ları kullanarak IP adresini ve port numarasını ayrı bir parse_ip_packet fonksiyonunda çıkaracağız:
hello_ebpf.c 1#define ETH_P_IP 0x0800 /* Internet Protocol packet */
2
3#define PARSE_SKIP 0
4#define PARSED_TCP_PACKET 1
5#define PARSED_UDP_PACKET 2
6
7static __always_inline int parse_ip_packet(struct xdp_md *ctx, u64 *ip_metadata) {
8 void *data_end = (void *)(long)ctx->data_end;
9 void *data = (void *)(long)ctx->data;
10
11 // First, parse the ethernet header.
12 struct ethhdr *eth = data;
13 if ((void *)(eth + 1) > data_end) {
14 return PARSE_SKIP;
15 }
16
17 if (eth->h_proto != bpf_htons(ETH_P_IP)) {
18 // The protocol is not IPv4, so we can't parse an IPv4 source address.
19 return PARSE_SKIP;
20 }
21
22 // Then parse the IP header.
23 struct iphdr *ip = (void *)(eth + 1);
24 if ((void *)(ip + 1) > data_end) {
25 return PARSE_SKIP;
26 }
27
28 u16 src_port, dest_port;
29 int retval;
30
31 if (ip->protocol == IPPROTO_TCP) {
32 struct tcphdr *tcp = (void*)ip + sizeof(*ip);
33 if ((void*)(tcp+1) > data_end) {
34 return PARSE_SKIP;
35 }
36 src_port = bpf_ntohs(tcp->source);
37 dest_port = bpf_ntohs(tcp->dest);
38 retval = PARSED_TCP_PACKET;
39 } else if (ip->protocol == IPPROTO_UDP) {
40 struct udphdr *udp = (void*)ip + sizeof(*ip);
41 if ((void*)(udp+1) > data_end) {
42 return PARSE_SKIP;
43 }
44 src_port = bpf_ntohs(udp->source);
45 dest_port = bpf_ntohs(udp->dest);
46 retval = PARSED_UDP_PACKET;
47 } else {
48 // The protocol is not TCP or UDP, so we can't parse a source port.
49 return PARSE_SKIP;
50 }
51
52 // Return the (source IP, destination IP) tuple in network byte order.
53 // |<-- Source IP: 32 bits -->|<-- Source Port: 16 bits --><-- Dest Port: 16 bits -->|
54 *ip_metadata = ((u64)(ip->saddr) << 32) | ((u64)src_port << 16) | (u64)dest_port;
55 return retval;
56}
Bu fonksiyon:
- Paketin geçerli bir ethernet başlığı, IP başlığı ve TCP veya UDP başlığı olup olmadığını kontrol eder. Bu kontroller
struct ethhdr'ninh_protovestruct iphdr'ninprotocolalanları kullanılarak yapılır. Her başlık, sardığı içteki paketin protokolünü saklar. - IP başlığından IP adresini, TCP/UDP başlıklarından port numarasını çıkarır ve bunlardan 64-bit unsigned integer (
u64) içinde birIP:portstuple'ı oluşturur. - Çağırana paketin TCP mi, UDP mi, yoksa başka bir şey mi olduğunu belirten bir kod döndürür (
PARSE_SKIP).
Fonksiyon imzasının başındaki __always_inline'a dikkat edin. Bu, derleyiciye bu fonksiyonu daima statik kod olarak inline etmesini söyler; bu da bir fonksiyon çağrısı maliyetinden bizi kurtarır.
Şimdi hook fonksiyonunu yazıp parse_ip_packet'ı kullanma zamanı:
hello_ebpf.c 1SEC("xdp")
2int xdp_prog_func(struct xdp_md *ctx) {
3 u64 ip_meta;
4 int retval = parse_ip_packet(ctx, &ip_meta);
5
6 if (retval != PARSED_TCP_PACKET) {
7 return XDP_PASS;
8 }
9
10 u32 *pkt_count = bpf_map_lookup_elem(&xdp_stats_map, &ip_meta);
11 if (!pkt_count) {
12 // No entry in the map for this IP tuple yet, so set the initial value to 1.
13 u32 init_pkt_count = 1;
14 bpf_map_update_elem(&xdp_stats_map, &ip_meta, &init_pkt_count, BPF_ANY);
15 } else {
16 // Entry already exists for this IP tuple,
17 // so increment it atomically.
18 __sync_fetch_and_add(pkt_count, 1);
19 }
20
21 return XDP_PASS;
22}
xdp_prog_func oldukça basit, çünkü program mantığının büyük kısmını zaten parse_ip_packet içinde yazmıştık. Burada yaptığımız şu:
parse_ip_packetile paketi parse etmek- TCP veya UDP paketi değilse
XDP_PASSdöndürerek saymayı atlamak bpf_map_lookup_elemyardımcı fonksiyonuylaIP:portstuple'ını BPF map anahtarlarında aramakIP:portstuple'ı ilk kez görülüyorsa değeri bire ayarlamak, aksi halde bir artırmak. Buradaki__sync_fetch_and_addbir LLVM yerleşik fonksiyonudur.
Son olarak bu fonksiyonu SEC("xdp") makrosuyla XDP alt sistemine bağlıyoruz.
User Space Programını Yazmak
Tekrar Go koduna dalma zamanı.
main.go 1//go:generate go run github.com/cilium/ebpf/cmd/bpf2go ebpf xdp.c
2
3var (
4 ifaceName = flag.String("iface", "", "network interface to attach XDP program to")
5)
6
7func main() {
8 log.SetPrefix("packet_count: ")
9 log.SetFlags(log.Ltime | log.Lshortfile)
10 flag.Parse()
11
12 // Subscribe to signals for terminating the program.
13 stop := make(chan os.Signal, 1)
14 signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
15
16 iface, err := net.InterfaceByName(*ifaceName)
17 if err != nil {
18 log.Fatalf("network iface lookup for %q: %s", *ifaceName, err)
19 }
20
21 // Load pre-compiled programs and maps into the kernel.
22 objs := ebpfObjects{}
23 if err := loadEbpfObjects(&objs, nil); err != nil {
24 log.Fatalf("loading objects: %v", err)
25 }
26 defer objs.Close()
27
28 // Attach the program.
29 l, err := link.AttachXDP(link.XDPOptions{
30 Program: objs.XdpProgFunc,
31 Interface: iface.Index,
32 })
33 if err != nil {
34 log.Fatalf("could not attach XDP program: %s", err)
35 }
36 defer l.Close()
37
38 log.Printf("Attached XDP program to iface %q (index %d)", iface.Name, iface.Index)
39
40 ...
Burada önce loadEbpfObjects fonksiyonuyla üretilen eBPF programını ve map'i yüklüyoruz. Ardından link.AttachXDP fonksiyonuyla programı belirtilen ağ arayüzüne bağlıyoruz. Önceki örnekteki gibi interrupt sinyalini dinlemek ve programı temizce kapatmak için bir kanal kullandık.
Sırada her saniye map içeriğini okumak ve paket sayılarını standart çıktıya yazdırmak var:
main.go 1 ...
2
3 ticker := time.NewTicker(time.Second)
4 defer ticker.Stop()
5 for {
6 select {
7 case <-stop:
8 if err := objs.XdpStatsMap.Close(); err != nil {
9 log.Fatalf("closing map reader: %s", err)
10 }
11 return
12 case <-ticker.C:
13 m, err := parsePacketCounts(objs.XdpStatsMap, excludeIPs)
14 if err != nil {
15 log.Printf("Error reading map: %s", err)
16 continue
17 }
18 log.Printf("Map contents:\n%s", m)
19 srv.Submit(m)
20 }
21 }
22}
Map içeriğini okuyup paket sayılarını parse etmek için parsePacketCounts adında bir yardımcı fonksiyon kullanacağız. Bu fonksiyon map içeriğini bir döngüde okuyacak.
Bize map'ten ham baytlar verileceği için, bu baytları parse edip okunabilir bir formata dönüştürmemiz gerekecek. Parse edilmiş map içeriğini saklamak için PacketCounts adında yeni bir tip tanımlayacağız.
main.go 1type IPMetadata struct {
2 SrcIP netip.Addr
3 SrcPort uint16
4 DstPort uint16
5}
6
7func (t *IPMetadata) UnmarshalBinary(data []byte) (err error) {
8 if len(data) != 8 {
9 return fmt.Errorf("invalid data length: %d", len(data))
10 }
11 if err = t.SrcIP.UnmarshalBinary(data[4:8]); err != nil {
12 return
13 }
14 t.SrcPort = uint16(data[3])<<8 | uint16(data[2])
15 t.DstPort = uint16(data[1])<<8 | uint16(data[0])
16 return nil
17}
18
19func (t IPMetadata) String() string {
20 return fmt.Sprintf("%s:%d => :%d", t.SrcIP, t.SrcPort, t.DstPort)
21}
22
23type PacketCounts map[string]int
24
25func (i PacketCounts) String() string {
26 var keys []string
27 for k := range i {
28 keys = append(keys, k)
29 }
30 sort.Strings(keys)
31
32 var sb strings.Builder
33 for _, k := range keys {
34 sb.WriteString(fmt.Sprintf("%s\t| %d\n", k, i[k]))
35 }
36
37 return sb.String()
38}
IP:ports tuple'ını saklamak için IPMetadata adında yeni bir tip tanımladık. Ham baytları parse edip okunabilir bir formata çevirmek için bir UnmarshalBinary metodu da tanımladık. IP:ports tuple'ını okunabilir formatta yazdırmak için bir String metodu da ekledik.
Sonra parse edilmiş map içeriğini saklamak için PacketCounts adında yeni bir tip tanımladık. Map içeriğini okunabilir formatta yazdırmak için bunun da bir String metodunu tanımladık.
Son olarak PacketCounts tipini kullanarak map içeriğini parse edip paket sayılarını yazdıracağız:
main.go 1func parsePacketCounts(m *ebpf.Map, excludeIPs map[string]bool) (PacketCounts, error) {
2 var (
3 key IPMetadata
4 val uint32
5 counts = make(PacketCounts)
6 )
7 iter := m.Iterate()
8 for iter.Next(&key, &val) {
9 if _, ok := excludeIPs[key.SrcIP.String()]; ok {
10 continue
11 }
12 counts[key.String()] = int(val)
13 }
14 return counts, iter.Err()
15}
Programı Çalıştırmak
Önce eBPF programını derlememiz, sonra user space programını çalıştırmamız gerekiyor.
1$ go generate
2Compiled /Users/sazak/workspace/gocode/src/github.com/ozansz/intro-ebpf-with-go/0x03-packet-count/ebpf_bpfel.o
3Stripped /Users/sazak/workspace/gocode/src/github.com/ozansz/intro-ebpf-with-go/0x03-packet-count/ebpf_bpfel.o
4Wrote /Users/sazak/workspace/gocode/src/github.com/ozansz/intro-ebpf-with-go/0x03-packet-count/ebpf_bpfel.go
5Compiled /Users/sazak/workspace/gocode/src/github.com/ozansz/intro-ebpf-with-go/0x03-packet-count/ebpf_bpfeb.o
6Stripped /Users/sazak/workspace/gocode/src/github.com/ozansz/intro-ebpf-with-go/0x03-packet-count/ebpf_bpfeb.o
7Wrote /Users/sazak/workspace/gocode/src/github.com/ozansz/intro-ebpf-with-go/0x03-packet-count/ebpf_bpfeb.go
8
9$ go build -o packet_count
Şimdi çalıştırabiliriz:
1$ sudo ./packet_count --iface eth0
2packet_count: 22:11:10 main.go:107: Attached XDP program to iface "eth0" (index 2)
3packet_count: 22:11:10 main.go:132: Map contents:
4192.168.5.2:58597 => :22 | 51
5packet_count: 22:11:11 main.go:132: Map contents:
6192.168.5.2:58597 => :22 | 52
7packet_count: 22:11:11 main.go:132: Map contents:
8192.168.5.2:58597 => :22 | 53
192.168.5.2 IP adresinden 22 portuna gelen paketler SSH paketleri; çünkü bu programı bir VM içinde çalıştırıyorum ve VM'e SSH ile bağlanıyorum.
VM içinde başka bir terminalde curl çalıştıralım da ne olacağına bakalım:
1$ curl https://www.google.com/
Bu sırada ilk terminalde:
1packet_count: 22:14:07 main.go:132: Map contents:
2172.217.22.36:443 => :38324 | 12
3192.168.5.2:58597 => :22 | 551
4packet_count: 22:14:08 main.go:132: Map contents:
5172.217.22.36:443 => :38324 | 12
6192.168.5.2:58597 => :22 | 552
7packet_count: 22:14:08 main.go:132: Map contents:
8172.217.22.36:443 => :38324 | 30
9192.168.5.2:58597 => :22 | 570
10packet_count: 22:14:09 main.go:132: Map contents:
11172.217.22.36:443 => :38324 | 30
12192.168.5.2:58597 => :22 | 571
172.217.22.36 IP adresinden 38324 portuna gelen paketleri görüyoruz; bunlar curl komutundan gelen paketler.
Sonuç
eBPF birçok açıdan güçlü bir teknoloji ve özellikle sistem programlama, observability veya güvenlikle ilgileniyorsanız zaman ayırmaya değer bir teknoloji olduğunu düşünüyorum. Bu makalede eBPF'in ne olduğunu, nasıl çalıştığını ve Go ile nasıl kullanmaya başlayabileceğimizi gördük.
Umarım bu makaleyi beğenmişsinizdir ve yeni bir şeyler öğrenmişsinizdir. Sorularınız olursa bana yazmaktan çekinmeyin.
Kaynaklar
- Systems Performance, Brendan Gregg
- Learning eBPF, Liz Rice
- docs.kernel.org
- ebpf.io
- cilium.io
- iovisor.org
- brendangregg.com
