Zajęcia 5: BPF¶
Data: 28.03.2023
Materiały dodatkowe¶
Wprowadzenie¶
BPF (Berkeley Packet Filter) to technologia pozwalająca na dostarczanie programu filtrującego przez proces użytkownika. W skrócie, BPF umożliwia napisanie krótkich programów (nie będących modułami), które są wykonane w trybie jądra. Najprostrzym przykładem programu BPF (dostępnym w “man 2 bpf”) jest filtr zliczający pakiety TCP i UDP otrzymanych przez system operacyjny.
BPF ma wiele praktycznych zastosowań, związanych między innymi z bezpieczeństwem, ślecedzeniem i profilowaniem procesów, obsługą interfejsów sieciowych oraz monitorowaniem systemu [1]. Technologia ta zyskuje na popularności np. w 2019 roku Netflix używał domyślnie 15, a Facebook 40 programów BPF w produkcyjnym środowisku [3], co z kolei przekłada się na intensywny rozwój technologii w ostatnich latach - zawartość katalogu linux/kernel/bpf modyfikowało w 2021 prawie 400 commitów.
Jedną z niewątpliwych zalet BPF jest jego wysoka wydajność pozwalająca na to, by wykonywać niezbyt skomplikowany program dla każdego pakietu przy 10Gb/s bez wyraźnych opóźnień [5]. Należy jednak pamiętać, że programy BPF nie będą specjalnie szybsze od ich odpowiedników zaimplementowanych w kodzie jądra [6], a najważniejszą cechą programów BPF jest to, że umożliwiają uruchomienie kodu w trybie jądra z procesu użytkownika. Jak łatwo się domyślić operacja taka wymaga szczególnych środków ostrożności, dlatego programy BPF są uruchamiane w piaskownicy (ang. sandbox) po wcześniejszej weryfikacji, o czym więcej za chwilę.
Jeśli chodzi o nazewnictwo to skrót BPF pochodzi z publikacji “The BSD Packet Filter” [7] napisanej w 1992. W Linuxie 3.18 dodano extended BPF (eBPF) wspierającego m.in. 64-bitowe rejestry, a wcześniejszą wersję zaczęto nazywać cBPF (classic BPF). W tym momencie najczęściej technologię po prostu nazywa się BPF, chociaż w niektórych miejscach ciągle można spotkać się z użyciem nazwy eBPF [2].
Typy programów BPF¶
Programy BPF mogą być różnych typów, które szczegółowo specyfikuje “enum bpf_prog_type” w pliku include/uapi/linux/bpf.h
.
Jądro w wersji 5.16.5 zawiera ponad 30 typów z których niektóre ważniejsze to:
BPF_PROG_TYPE_SOCKET_FILTER
pozwalający na porzucanie lub skracanie pakietówBPF_PROG_TYPE_KPROBE
pozwalający na intrumentację funkcjiBPF_PROG_TYPE_XDP
pozwala zadecydować o losie pakietu na wczesnym etapie jego obsługi(zanim kosztowne operacje zostaną wykonane), co jest przydatne do ochrony przed atakami DDoS.BPF_PROG_TYPE_CGROUP_*
pozwalające dodatkowo zarządzać uprawnieniami cgroup
Weryfikacja programów BPF¶
Ponieważ program BPF jest dostarczany z programu użytkownika, a wykonany w trybie jądra, potrzebna jest dodatkowa weryfikacja poprawności programu, aby zapobiec zarówno nieuprawnionym dostępom do pamięci jak i przypadkowym błędom mogącym zawiesić cały system. Weryfikacja przebiega w dwóch etapach.
Pierwszy etap sprawdza między innymi:
Rozmiar programu (maksymalnie dopuszcza się
BPF_MAXINSNS
instrukcji, w naszej wersji to 4096).Obecność pętli. Od jądra 5.3 dopuszczone są ograniczone pętle (ang. bounded loops) dla których łatwo dowieść własność stopu.
Wywołania funkcji. Generalnie nie można wołać funkcji które nie należą do grupy BPF helpers.
Osiągalność wszystkich instrukcji.
Drugi etap jest bardziej skomplikowany. Weryfikator zaczyna od pierwszej instrukcji programu i stara się zbadać wszystkie możliwe przebiegi programu,
jednocześnie weryfikując jego stan, zawartość rejestrów oraz operacje które są na nich wykonywane. Do weryfikacji stanu jest użyta struktura bpf_reg_state
dostępna w include/linux/bpf_verifier.h
przechowująca między innymi typ wartości (bpf_reg_type
w include/linux/bpf.h
). Wartość może mieć typ NOT_INIT
, SCALAR_VALUE
lub jeden z typów wskaźnika (np. PTR_TO_CTX
, PTR_TO_STACK
, PTR_TO_PACKET
). Operacje na wskaźnikach mogą zmienić ich typ, np. dodając dwa PTR_TO_CTX
otrzymujemy SCALAR_VALUE
i od tego momentu nie możemy już pamięci spod tej wartości (mogłoby to umożliwić nieuprawniony dostęp do pamięci).
Tworzenie programów BPF¶
Programy BPF przypominają ASM, ale mają własny zbiór rejestrów i instrukcji. Do dyspozycji programisty jest 11 rejestrów: R0-R9 umożliwiających odczyt i
zapis oraz R10 do odczytu adresu ramki (podobnie jak RBP w x86_64). Rejestry są modyfikowane przez liczne instrukcje [8], które umożliwiają między innymi
operacje arytmetyczne (np. BPF_ADD
, BPF_MUL
), skoki i wywołania funkcji (np. BPF_JEQ
, BPF_JLE
, BPF_CALL
), wczytywanie i zapisywanie wartości (np. BPF_LD
, BPF_ST
).
Jedną z możliwości na napisanie program BPF jest ręczne wykorzystanie struktury “struct bpf_insn” (tak jak w samples/bpf/bpf_insn.h
). Ma to jednak oczywiste wady
(tak samo jak pisanie programow w ASMie) dlatego istnieją narzędzia umożliwiające pisanie programów BPF w językach programowania takich jak C, C++, Python czy Go, między innymi bcc oraz libbpf.
Przygotowany program BPF jest po stronie kernela weryfikowany a następnie kompilowany metodą JIT (just in time compilation) do kodu maszynowego.
Kod odpowiedzialny za kompilację dla architektury x86 znajduje się w pliku arch/x86/net/bpf_jit_comp.c
. Po skompilowaniu program BPF może zostać uruchomiony.
Uruchamianie programów BPF¶
Podstawowa ścieżka uruchomienia programu BPF rozpoczyna się od użycia funkcji bpf_prog_load
, która otrzymuje typ programu BPF wraz z listą instrukcji.
Funkcja ta powoduje weryfikację oraz załadowanie programu a następnie zwraca numer deskryptora pliku powiązanego z programem.
Deskryptor ten można następnie wykorzystać np. przekazując go jako argument funkcji setsocketopt lub ioctl z requestem PERF_EVENT_IOC_SET_BPF.
Referencje¶
[1] https://ebpf.io/
[2] https://www.brendangregg.com/blog/2021-06-15/bpf-internals.html
[3] https://www.brendangregg.com/blog/2019-12-02/bpf-a-new-type-of-software.html
[4] https://www.brendangregg.com/bpf-performance-tools-book.html
[5] https://kinvolk.io/blog/2020/09/performance-benchmark-analysis-of-egress-filtering-on-linux/
[6] https://pchaigno.github.io/ebpf/2020/09/29/bpf-isnt-just-about-speed.html
[7] https://www.usenix.org/legacy/publications/library/proceedings/sd93/mccanne.pdf
[8] https://www.kernel.org/doc/html/latest/bpf/instruction-set.html