Kategorien
Themenabend

Themenabend: eBPF

Heute hat sich Markus daran versucht uns innerhalb von etwa 2 Stunden das Thema eBPF zu erklären. Das Thema ist durchaus komplex, aber versuchen wir, uns dem Ganzen langsam zu nähern.

Was ist eBPF?

Grob gesagt ermöglicht eBPF das Verhalten des Linux-Kernels zur Laufzeit zu beeinflussen, zu beobachten und zu erweitern, ohne eigene Kernelmodule zu schreiben oder den Kernel neu kompilieren zu müssen.

Ursprünglich stammt BPF aus dem Jahr 1997 und wurde als Packet Filter für BSD entwickelt, später dann in den Linux-Kernel übernommen.

Seit 2014 wurde BPF massiv erweitert – daraus entstand eBPF, das heute weit mehr kann als nur Netzwerkpakete filtern.

Heutzutage werden die Begriffe BPF und eBPF meist synonym verwendet.

Grundsätzlich ist eBPF eine virtuelle Maschine im Kernel, in der kleine Programme sicher ausgeführt werden können.

Diese Programme können sich z.B. an folgende Ereignisse hängen:

  • Systemaufrufe (syscalls)
  • Netzwerk-Events
  • Tracepoints
  • XDP (sehr frühe Paketverarbeitung, teils direkt auf der Netzwerkkarte)

Wichtig dabei: eBPF-Programme werden vor dem Laden verifiziert. Der Kernel stellt sicher, dass sie nicht abstürzen, keine Endlosschleifen enthalten und nur erlaubte Operationen ausführen.

„Compile once, run everywhere“ (CO-RE)

Ein zentrales Konzept moderner eBPF-Entwicklung ist CO-RE (Compile Once, Run Everywhere).

Das bedeutet:

  • eBPF-Programme müssen nicht für jeden einzelnen Kernel neu gebaut werden
  • sie laufen – einmal kompiliert – auf sehr vielen Kernel-Versionen
  • kein Reboot nötig
  • deutlich portabler als Kernel-Module
  • Änderungen müssen nicht upstream in den Kernel gebracht werden (was oft Jahre dauert bis die Änderungen in der Distribution der Wahl verfügbar sind)

Auf praktisch allen heutigen Linux-Kerneln ist eBPF bereits standardmäßig aktiviert.

Aufbau: Kernelspace & Userspace

Ein eBPF-Setup besteht fast immer aus zwei Teilen:

1. eBPF-Programm (Kernelspace)

  • läuft in der eBPF-VM im Kernel
  • meist: 1 Programm = 1 Funktion
    • denn Funktionsaufrufe sind nur sehr eingeschränkt möglich (oder müssen durch Inlining umgangen werden)
  • sollte schnell und leichtgewichtig sein
    • langsamer Code könnte sonst das ganze System ausbremsen

2. Userspace-Programm

  • lädt das eBPF-Programm in den Kernel
  • hängt es an Events (z.B. syscalls, Netzwerk-Events, Tracepoints)
  • kommuniziert über BPF-Maps (z.B. Ringbuffer) mit dem Kernel-Code
  • hier findet das „Heavy Lifting“ statt (Auswertung, Visualisierung, Logging, …)

Typischer Ablauf

  • Code zu eBPF-Bytecode kompilieren
  • eBPF-Programm in den Kernel laden
    • z.B. mit eigenem Userspace-Tool oder
    • bpftool prog load ...
  • und an Events hängen auf die es lauschen soll
    • auch wieder entweder mit eigenem Userspace-Programm, oder
    • bpftool prog attach ...
  • Kommunikation zwischen eBPF-Programm und Userspace-Programm über BPF-Maps

Welche BPF-Programme gerade auf dem eigenen System laufen kann man mit folgendem Befehl herausfinden:

bpftool prog

eBPF-Programme bleiben so lange geladen, wie noch etwas auf sie referenziert, z.B.:

  • das Userspace-Programm läuft noch
  • ein Netzwerk-Interface ist noch verbunden

Das Userspace-Programm kann man relativ einfach beenden, das hat man ja im Normalfall auch selbst gestartet. Netzwerk-Interfaces lassen sich aber z.B. wie folgt detachen:

bpftool net detach xdp dev eth0

Show me the Code!

Als erstes Beispiel haben wir dann folgenden Python-Code angeschaut. In diesem vereinfachten Beispiel wird der eBPF-Code als Text innerhalb einer Variablen gespeichert und über das Python-Modul bcc in den Kernel geladen und an den syscall execve gehängt.

Somit wird jedes Mal, wenn ein Programm gestartet wird „Hello World!“ in den Trace-Buffer geschrieben.

#!/usr/bin/python3
from bcc import BPF
import sys

program = r"""
int hello(void *ctx) {
    bpf_trace_printk("Hello World!");
    return 0;
}
"""

b = BPF(text=program)
syscall = b.get_syscall_fnname("execve")
b.attach_kprobe(event=syscall, fn_name="hello")

try:
    b.trace_print()
except KeyboardInterrupt:
    sys.exit(0)

Wenn ich das auf meinem System ausführe und in einer anderen Shell ein ls ausführe sieht die Ausgabe z.B. so aus:

$ sudo python3 helloworld.py 
b'           <...>-363432  [019] ...21 183556.882255: bpf_trace_printk: Hello World!'
b'           <...>-363434  [012] ...21 183556.894724: bpf_trace_printk: Hello World!'

In diesem Beispiel wird der globale Trace-Buffer genutzt. In der Praxis verwendet man meist eigene Ringbuffer, um sauber nur mit dem eigenen eBPF-Programm zu kommunizieren.

Weitere Anwendungsfälle

Wie schon angesprochen kann man damit auch Netzwerkpakete analysieren/filtern etc. Hierzu haben wir uns das Beispiel aus learning ebpf – chapter 8 angesehen. Das würde hier allerdings jetzt den Rahmen sprengen. Markus konnte das Buch dazu allerdings empfehlen, falls ihr euch näher damit beschäftigen wollt.

Auch nftables verwendet intern teilweise eBPF und wenn man richtig tief einsteigen will kann man mit dem Tool pwru (Packet, where are you?) den Weg von Paketen durch den Kernel nachvollziehen.

Weitere Infos zu eBPF gibt’s in deren Dokumentation und wie gesagt hat uns Markus auch die Beispiele aus https://github.com/lizrice/learning-ebpf und das dazugehörige Buch empfohlen.

Fazit

eBPF ist eines der mächtigsten Werkzeuge im Linux-Umfeld geworden, wenn es um:

  • Observability
  • Netzwerk
  • Security
  • Tracing
  • Performance-Analyse

geht. Und das alles, ohne den Kernel patchen oder neu starten zu müssen.

Next up: Themenabend Heimautomatisierung mit HomeAssistant

Bei der Themenwahl für den nächsten Themenabend waren wir uns zunächst unschlüssig. In der anschließenden Diskussion wurde jedoch schnell klar, dass aktuell viele von uns an Projekten rund um Heimautomatisierung arbeiten.

Um nur ein paar der Themen anzuteasern, die aufkamen:

  • Balkonkraftwerk
  • Auslesen von Stromzählern per IR-Diode und ESP
  • Heizungen automatisch beim Lüften abdrehen inkl. Benachrichtigungen aufs Handy, wenn die Fenster zu lange offen sind
  • und das Ganze dann natürlich idealerweise komplett lokal und ohne Cloud-Zwangsanbindung

Wir werden wir uns also beim nächsten Themenabend am 7. April zusammen das Thema Heimautomatisierung (vmtl. mit Fokus auf HomeAssistant) ansehen.

Wenn ihr Ideen für unsere nächsten Themenabende habt meldet euch gerne in unserem Matrix-Channel.