Za każdym razem, gdy chcę przetestować nową dystrybucję Linuksa na jakimś zapasowym sprzęcie, mam do wyboru albo znowu nagrać ją na pendrive, albo w końcu postawić coś, co bootuje się przez sieć. Pendrive formatowałem w tym celu chyba z dziesięć razy, zwykle zaraz po tym, jak sformatowałem go do czegoś innego. Więc tym razem zamiast tego zrobiłem PXE boot, konkretnie po to, żeby odpalić Omarchy, dystrybucję opartą na Arch Linuksie zbudowaną wokół kompozytora Hyprland, na starym testowym pudle bez dotykania pendrive’a.

Trudniejsza część nie była samym PXE. Była nią robota tego w domowej sieci, gdzie nie chcę, żeby jakiś drugi serwer DHCP walczył z routerem o to, kto rozdaje adresy.

Problem z większością tutoriali o PXE

Większość poradników o PXE zakłada, że masz pełną kontrolę nad jedynym serwerem DHCP w sieci, bo w labie albo w firmowym VLAN-ie zwykle tak jest. U mnie w domu router już rozdaje adresy telewizorowi, drukarce i czemuś tam, co akurat robi mój telefon. Postawienie obok niego drugiego pełnego serwera DHCP to prosta droga do sytuacji, w której dwa urządzenia kłócą się o to, kto jest właścicielem 192.168.1.50, a ta kłótnia zawsze wygrywa to, które odpowie pierwsze. Nie chcę zostawiać tego przypadkowi.

Rozwiązaniem jest tryb, który wspiera większość serwerów DHCP, nazywany proxy DHCP. Serwer nasłuchuje w sieci lokalnej, czeka na klienta, który podczas handshake’u DHCP zgłasza się jako klient PXE, i odpowiada mu wyłącznie informacją o bootowaniu: tu masz serwer TFTP, tu masz plik do bootowania. Nigdy nie przydziela adresu IP i nie rusza tabeli leasingów. Router robi swoje, kompletnie nieświadomy, że cokolwiek innego w ogóle działa. dnsmasq nazywa to dhcp-range=<sieć>,proxy i to jedna linijka sprawia, że całość można bezpiecznie postawić w sieci, nad którą nie masz pełnej kontroli.

Cztery ruchome części

Działający netboot dla instalatora opartego na archiso (Omarchy, Arch, Manjaro, wszystko zbudowane tak samo) potrzebuje:

  • dnsmasq w trybie proxy DHCP, obsługującego też TFTP
  • nginx serwującego wypakowaną zawartość ISO przez HTTP
  • obrazu netboot GRUB-a, który ogarnia UEFI
  • samego ISO, zamontowanego raz i skopiowanego w dwa różne miejsca

Ten ostatni punkt trochę mnie zaskoczył. Instalator nie siedzi w jednym miejscu, siedzi w dwóch, bo TFTP i HTTP są dobre w różnych rzeczach.

Dlaczego ISO kopiuje się dwa razy

TFTP jest stary, prosty i słaby w niezawodnym przesyłaniu dużych plików. Nadaje się do kernela i initramfs, razem to z dwadzieścia, trzydzieści megabajtów. Nie nadaje się do wielogigabajtowego systemu plików root w squashfs. Instalatory oparte na archiso rozwiązują to, bootując minimalny kernel i initrd przez TFTP, a potem przełączając się na HTTP do wszystkiego innego, przez parametr kernela: archiso_http_srv=http://<serwer>/omarchy/. Jak to minimalne środowisko już wstanie, resztę obrazu instalacyjnego ściąga bezpośrednio z nginx.

Skrypt montuje więc ISO raz i robi z nim dwie rzeczy: rsyncuje całe drzewo do web roota nginx, i kopiuje tylko vmlinuz-linux oraz initramfs-linux.img (plus mikrokod Intela lub AMD, jeśli jest) do TFTP roota. Kazałem skryptowi doinstalować też squashfs-tools, myśląc, że w pewnym momencie będę musiał ręcznie rozpakować albo zajrzeć do skompresowanego obrazu. Nie musiałem. Nadal jest zainstalowany. Nie odinstalowuję go, z czystego zabobonu.

Pisanie konfiguracji GRUB-a, która bootuje się przez sieć

grub-mknetdir --net-directory=/srv/tftp generuje binarkę netboot dla UEFI oraz strukturę katalogów, jakiej GRUB oczekuje, gdy ładuje się przez TFTP zamiast z lokalnego dysku. Potem piszesz normalnie wyglądający grub.cfg, tyle że linie linux i initrd wskazują na ścieżki TFTP, a parametry kernela niosą URL do HTTP:

1
2
3
4
menuentry "Install Omarchy OS 4.0.0 (UEFI PXE)" --class arch {
    linux /omarchy/vmlinuz-linux archisobasedir=arch archiso_http_srv=http://<serwer>/omarchy/ ip=dhcp cms_verify=n verify=n
    initrd /omarchy/initramfs-linux.img
}

ip=dhcp daje klientowi adres od prawdziwego serwera DHCP, czyli dalej od routera. cms_verify=n verify=n wyłącza sprawdzanie podpisu obrazu instalatora, bo przychodzi przez zwykłe HTTP z pudła, któremu ufam bezgranicznie i nikt inny poza mną. Wyłączanie weryfikacji podpisu instalatora systemu to dokładnie taki skrót, który bierzesz o jedenastej wieczorem, obiecując sobie, że to później dopracujesz. Nie dopracowałem tego później. To domowa sieć serwująca publiczne, niezmodyfikowane ISO, które sam ściągnąłem, więc jakoś się z tym pogodziłem, ale nie przeniósłbym tego nawyku nigdzie, gdzie model zagrożeń jest mniej wyrozumiały.

Konfiguracja dnsmasq, bez wchodzenia w drogę niczemu innemu

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
port=0
interface=eth0
bind-interfaces

enable-tftp
tftp-root=/srv/tftp

dhcp-range=192.168.1.0,proxy,255.255.255.0

dhcp-match=set:efi-x86_64,option:client-arch,7
dhcp-match=set:efi-x86_64,option:client-arch,9
dhcp-match=set:efi-x86,option:client-arch,6
dhcp-match=set:bios,option:client-arch,0

dhcp-boot=tag:efi-x86_64,boot/grub/x86_64-efi/core.efi,<serwer>,<serwer>
dhcp-boot=tag:bios,pxelinux.0,<serwer>,<serwer>

port=0 wyłącza własny resolver DNS dnsmasq. Mam już osobny resolver DNS dla całego domu, a jedyną rzeczą gorszą niż dwa kłócące się serwery DHCP są dwa serwery DNS nasłuchujące na porcie 53, z których żaden nie wygrywa czysto. Dzięki temu dnsmasq robi dokładnie dwie rzeczy, proxy DHCP i TFTP, i nic poza tym.

Odróżnianie BIOS-u od UEFI bez pytania użytkownika

Linie dhcp-match czytają kod architektury klienta, wysyłany jako opcja DHCP 93 podczas handshake’u PXE. W praktyce nowoczesny 64-bitowy firmware UEFI zgłasza się jako architektura 7, czasem 9 w zależności od dokładnej ścieżki bootowania, a naprawdę stary legacy BIOS zgłasza się jako 0. Konfiguracja taguje każdą architekturę i kieruje ją do innego pliku bootującego, binarki EFI GRUB-a dla klientów UEFI, pxelinux.0 dla BIOS-u. Dopiąłem fallback dla BIOS-u i jestem w miarę pewny, że ani razu się nie odpalił, bo każda maszyna w domu wystarczająco stara, żeby bootować się w trybie legacy, poszła na emeryturę, zanim zdążyłem to przetestować. To martwy kod, który i tak zostawiam, głównie na dzień, w którym jakaś maszyna udowodni, że się myliłem.

Co faktycznie poszło nie tak za pierwszym razem

Menu bootowania wstało bez problemu. GRUB załadował kernel i initramfs przez TFTP bez narzekania. Potem przez blisko minutę siedział na „Loading initial ramdisk", zanim nginx zaczął zwracać 403 w logu dostępu. Wypakowałem ISO jako root przez rsync z domyślnymi uprawnieniami, i proces roboczy nginx nie mógł odczytać połowy drzewa katalogów. Poprawka to jedna linijka, chmod -R 755 na katalogach HTTP i TFTP po wypakowaniu, czyli dokładnie taki nudny problem z uprawnieniami, który zjada dwadzieścia minut wieczoru i niczego nie uczy na przyszłość.

Gdzie to się faktycznie przydaje

Każde zapasowe pudło w sieci lokalnej może teraz zbootować instalator Omarchy bez dotykania pendrive’a, a przetestowanie nowszego ISO to kwestia wskazania skryptowi nowego pliku i ponownego uruchomienia, a nie nagrywania czegokolwiek od nowa. O to w tym wszystkim chodziło, to drobna wygoda, ale z takich drobnych wygód składają się projekty homelabowe.

Jest tu jeden koszt: dnsmasq i nginx są teraz ustawione tak, żeby działać na tym pudle non stop, czyli jedna para serwisów więcej do pilnowania, podobnie jak ostatnio zacząłem faktycznie czytać od deski do deski własny skrypt backupowy zamiast mu ślepo ufać. Serwer proxy PXE siedzący cicho w domowej sieci to niskie ryzyko, ale niskie ryzyko to nie to samo co brak ryzyka. Wrzucam to na listę rzeczy do sprawdzenia przy kolejnym przeglądzie tego, co faktycznie działa na domowym serwerze. Dołącza do rosnącej listy usług wpiętych w sieć domową obok kontrolera Omada, który już wcześniej sobie zaskryptowałem, który w tym momencie obsługuje więcej z tego domu, niż świadomie ogarniam.