Od počátku roku 2022 pracujeme na přechodu na cgroups v2. Vyžaduje to úpravy v linuxovém jádře, ale také integraci v uživatelském prostoru. Proto vznikla utilita devcgprog.
V předchozím zápisku jsem psal o plánovaném přechodu na cgroups v2. Pomalu na tom pracujeme už od počátku roku 2022. Vyžaduje to úpravy jednak v našem kernelu, to je stále v řešení, a taky integraci v user space: nutnost delegace controllerů, rozdílný způsob konfigurace a čtení parametrů, až nakonec propojení s vpsAdminem. Pro mě asi největší oříšek zatím byl devices controller. Pro zajímavost popíšu k čemu to je a jak to funguje.
devices controller je pro nás stěžejní komponenta, bez které nemůžeme fungovat. Řídí totiž přístup ke všem zařízením – blokové jako disky /dev/sda
, atd. a znakové jako /dev/tty, /dev/null
, /dev/zero
, apod. Protože ve VPS můžete udělat mknod
libovolného zařízení, devices cgroup je nezbytná pro řízení přístupu.
Ve výchozím stavu umožnujeme přístup k /dev/{console, full, kmsg, null, ptmx, random, urandom, tty*}
. Podle VPS features pak i /dev/{net/tun, kvm, ppp}
. Pro nás je nejdůležitější neumožnit přístup k diskovým zařízením, nad kterými běží ZFS.
devices controller u cgroups v1 je krásně jednoduchý. Povolené zařízení vidíme v devices.list:
# cat /sys/fs/cgroup/devices/devices.list c 1:3 rwm c 1:5 rwm c 1:7 rwm c 1:8 rwm c 1:9 rwm c 1:11 rwm c 5:0 rwm c 5:1 rwm c 5:2 rwm c 136:* rwm b *:* m c *:* m
Tedy read-write-mknod (rwm) k vybraným zařízením a mknod (m) pro všechno ostatní. Na Linuxu v user namespace normálně mknod není možný, ale na vpsAdminOS ano. Hodí se to třeba, když rozbalujete nějaký archiv, který obsahuje soubory zařízení.
Tohoto nastavení devices.list
docílíme tak, že nejprve zakážeme přístup ke všemu:
# echo a > devices.deny
A potom povolíme vybrané zařízení:
echo c 1:3 rwm > devices.allow echo c 1:5 rwm > devices.allow [...]
Je to krásně jednoduché jednak na přehled a taky na konfiguraci. Jediný zádrhel je zde v tom, že echo a > devices.deny
funguje jen pokud daná cgroup nemá potomky. Protože se s cgroups pracuje z různých míst, řešil jsem to tak, že se devices cgroup nastavovala jako první při spuštění systému nebo vytvoření VPS.
cgroups v2 dlouho devices controller neměly vůbec… a když přišel, byl implementován přes BPF. Funguje to tak, že napíšete BPF program, zkompilujete, nahrajete do kernelu a potom ho připojíte k dané cgroup. Při přístupu k zařízení se daný BPF program vykoná a buď přístup umožní, nebo zakáže. Ukázkový program je součástí kernelu.
Můj největší problém byl, že jsem to ani za nic nebyl schopen zkompilovat. Detaily už samozřejmě nevím, ale byl jsem z toho absolutně zoufalý. Kompilovat programy tímto způsobem by sice nebylo ideální, protože k tomu potřebujete LLVM, což nafukuje velikost rootfs… ale aspoň by se tím dalo začít.
Když se podíváme na projekty jako systemd nebo LXC, tam s devices cgroup pracují taky. Nekompilují programy přes LLVM, ale vytvoří program rovnou z BPF instrukcí. Pěkně je to vidět v LXC.
Tahle cesta se mi líbila mnohem víc, ale taky jsem nikam nepokročil. Použít přímo tuto funkci LXC nemůžeme, protože LXC spouštíme pod neprivilegovaným uživatelem a devices cgroup může nastavovat jen root. Ani vykopírovat ten kód není jednoduché, potřebuje to spousty vaty okolo a zřejmě jsem na to neměl nervy. Zkoušel jsem použít i libbpf, taky bezúspěšně.
Vzdal jsem to a vrátil se k tomu zhruba o rok později… situace byla úplně stejná. Našel jsem ale knihovnu v Golangu, pomocí které si můžu poskládat program z instrukcí, nahrát ho do kernelu a dokonce připojit k cgroup! Nejlepší je, že k tomu není potřeba mít LLVM, zdrojové soubory kernelu, libbpf, nic – stačí Golang.
Další nutný článek je virtuální souborový systém bpffs. BPF program totiž zůstane v kernelu jen po dobu, kdy běží proces, který ho tam nahrál. My takový proces prakticky nemáme, všechno se může při aktualizaci restartovat… to by znamenalo, že se najednou ztratí všechny programy hlídající přístup k zařízením.
Pokud na daný BPF program ale uděláme referenci v bpffs, program zůstane v kernelu i po ukončení procesu, který ho vytvořil. Program pak uvolníme smazáním souboru (jeho reference) v bpffs. Podobně můžeme dělat reference na připojení programu (attach/link) k cgroup.
Celý náš stack je v Ruby, takže jsem na nastavovaní devices cgroup udělal oddělený program v Golangu — vznikl devcgprog. Ten z Ruby voláme s cestou k cgroup a seznamem zařízení, které mají být povolené. devcgprog vytvoří BPF program, nahraje ho do kernelu, uloží do bpffs a připojí k cgroup. Kdyby se to někomu náhodou taky hodilo, devcgprog najdete na GitHubu.
Použití je jednoduché:
devcgprog set /sys/fs/bpf/my-program \ /sys/fs/cgroup/my-cgroup \ /sys/fs/bpf/my-program-on-my-cgroup \ allow \ c:1:3:rwm c:1:5:rwm [...]
Parametry jsou: vytvořená reference v bpffs, cesta k cgroup, reference k linku na cgroup, typ seznamu (allow pro allowlist a deny pro denylist) a seznam zařízení.
Jeden BPF program můžete nahrát jednou a použít u vícero cgroup, k tomu slouží devcgprog new/attach. U nás je kombinace možných programů omezená podle VPS features. Jako název programu používáme hash všech zařízení, existují tak programy pro různé kombinace TUN/TAP, KVM a PPP. Pokud už program s daným hash existuje, uděláme jen attach na další VPS cgroup.
Oproti cgroups v1 je to stále méně přehledné, BPF program je v porovnání s devices.list neprůhledný. Může taky dojít k tomu, že cgroup se smaže a vytvoří znovu se stejným názvem, link soubor v bpffs zůstane, ale přitom program tam už připojen není. Existence reference v bpffs tedy ještě neznamená, že je vše v pořádku. Pravidelně tak voláme bpftool a kontrolujeme, zda jsou programy správně nastaveny. To je jen pro jistotu, nastavované cgroupy má pod kontrolou pouze root na nodu, tedy by tato situace neměla nastat.
Neříkám, že BPF není super, naopak na trasování, flamegraphy, apod. je to paráda. Akorát někdy není úplně snadné to použít.