=============================================== Zajęcia 5: BPF =============================================== Data: 29.03.2021 .. contents:: .. toctree:: :hidden: zadanie Materiały dodatkowe =================== - :ref:`05-zadanie` 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ów - ``BPF_PROG_TYPE_KPROBE`` pozwalający na intrumentację funkcji - ``BPF_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 - [9] https://github.com/iovisor/bcc/