Wsparcie dla WiFi w initramfs/initrd by odszyfrować LUKS przez SSH bezprzewodowo

Spis treści

W poprzednim artykule, który traktował o odszyfrowaniu kontenera LUKS przez SSH z poziomu initramfs/initrd na Raspberry Pi, została poruszona kwestia adresacji IP, która w opisanym tam rozwiązaniu miała pewne ograniczenia. Chodziło o to, że połączenie SSH do RPI mogło być realizowane jedynie przez przewodowy interfejs sieciowy eth0 . Trzeba było zatem się zastanowić nad rozwiązaniem, które umożliwiłoby korzystanie również z bezprzewodowego interfejsu WiFi, tj. wlan0 . Celem niniejszego wpisu jest pokazanie w jaki sposób można dorobić wsparcie dla połączeń WiFi w naszej malinie, tak by szło odszyfrować kontener LUKS przez SSH, w sytuacji gdy z jakiegoś powodu nie chcemy lub też nie możemy korzystać z przewodowego interfejsu sieciowego tego minikomputera z zainstalowanym system RasPiOS/Raspbian.

Problematyczne wsparcie dla WiFi w initramfs/initrd

Te nieco bardziej spostrzegawcze osobniki zapewne zdążyły już zauważyć, że w przypadku przewodowego interfejsu sieciowego nie trzeba było za bardzo zmieniać konfiguracji obrazu initramfs/initrd. No i faktycznie połączenie przewodowe w tym stadium rozruchu systemu działa w zasadzie OOTB. W przypadku interfejsów bezprzewodowych trzeba się będzie trochę wysilić, a to ze względu na fakt, że w obrazie initramfs/initrd brakuje oprogramowania odpowiedzialnego za zestawienie połączenia WiFi.

W dystrybucjach bazujących na Debianie, w tym też RasPiOS/Raspbian, oprogramowaniem odpowiedzialnym za konfigurację połączenia WiFi jest pakiet wpasupplicant , który jest domyślnie zainstalowany w Raspberry Pi. Niemniej jednak, binarki pokroju wpa_supplicant czy wpa_cli to nie wszystko, by komunikacja bezprzewodowa mogła zostać ustanowiona. Potrzebne są sterowniki i firmware od tego modelu karty WiFi, który siedzi w naszym RPI. Dodatkowo trzeba też określić parametry dla połączenia WiFi, takie jak ESSID czy hasło, które zwykle trzymane są w pliku /etc/wpa_supplicant.conf .

O ile te wszystkie wyżej wymienione rzeczy są standardowo dostępne w działającym systemie RasPiOS/Raspbian, o tyle żadna z nich nie jest dostępna w obrazie initramfs/initrd, przez co połączenie WiFi na tym etapie startu systemu nam zwyczajnie działać nie może. Dobre wieści są jednak takie, że skoro WiFi działa nam po uruchomieniu systemu, to możne też działa w fazie initramfs/initrd. Trzeba tylko te wyżej wypisane komponenty zapakować do tego obrazu i odpowiednio skonfigurować.

Chciałbym tutaj wyraźnie zaznaczyć, że informacje na temat tego jak skonfigurować usługę SSH, tak by działała ona w fazie initramfs/initrd na Raspberry Pi znajdują się w osobnym artykule i nie będę tutaj tego zagadnienia opisywał ponownie. Dalsza część artykułu zakłada, że jesteśmy w stanie podłączyć się po SSH z wykorzystaniem przewodowego interfejsu sieciowego w naszej malinie.

Konfiguracja obrazu initramfs/initrd na potrzeby WiFi

Szukając informacji na temat tego jak włączyć wsparcie dla WiFi w obrazie initramfs/initrd natrafiłem na ten oto dość przyzwoicie napisany post. Problem z przedstawionym w tym poście rozwiązaniem jest jednak taki, że po uruchomieniu się systemu (do momentu, w którym trzeba odszyfrować kontener LUKS) wcina nam całkowicie interfejs WiFi. Jest to efektem braku dołączenia do obrazu initramfs/initrd plików z firmware, których wymaga karta WiFi do pracy. Postanowiłem zatem w oparciu o tego podlinkowanego wyżej posta naskrobać nieco bardziej aktualną wersję opisanego tam rozwiązania, które będzie działać na Raspberry Pi 4B i być może też i innych malinach.

Plik /etc/initramfs-tools/scripts/init-premount/a_enable_wireless

W katalogu /etc/initramfs-tools/scripts/init-premount/ tworzymy plik a_enable_wireless . Nadajemy mu również prawa wykonywania:

root@raspberrypi:~# touch /etc/initramfs-tools/scripts/init-premount/a_enable_wireless
root@raspberrypi:~# chmod +x /etc/initramfs-tools/scripts/init-premount/a_enable_wireless

Następnie wrzucamy do tego pliku poniższą zawartość, odpowiednio zmieniając wartości w zmiennych AUTH_LIMIT= oraz INTERFACE= :

#!/bin/sh
# this goes into /etc/initramfs-tools/scripts/init-premount/a_enable_wireless
PREREQ=""
prereqs()
{
    echo "$PREREQ"
}

case $1 in
prereqs)
    prereqs
    exit 0
    ;;
esac

. /scripts/functions

AUTH_LIMIT=30
INTERFACE="wlan0"
alias WPACLI="/sbin/wpa_cli -p/tmp/wpa_supplicant -i$INTERFACE "

log_begin_msg "Setting WLAN regdomain"
# Set this to your region
/sbin/iw reg set PL

log_begin_msg "Starting WLAN connection"
sleep 1 && /sbin/wpa_supplicant  -i$INTERFACE -c/etc/wpa_supplicant.conf -P/run/initram-wpa_supplicant.pid -B -f /tmp/wpa_supplicant.log

# Wait for AUTH_LIMIT seconds, then check the status
limit=${AUTH_LIMIT}

echo -n "Waiting for connection (max ${AUTH_LIMIT} seconds)"
while [ $limit -ge 0 -a `WPACLI status | grep wpa_state` != "wpa_state=COMPLETED" ]
do
    sleep 1
    echo -n "."
    limit=`expr $limit - 1`
done
echo ""

if [ `WPACLI status | grep wpa_state` != "wpa_state=COMPLETED" ]; then
  ONLINE=0
  log_failure_msg "WLAN offline after timeout"
  panic
else
  ONLINE=1
  log_success_msg "WLAN online"
fi

configure_networking

Celem tego skrypt jest podłączenia nas do określonej sieci WiFi. W moim przypadku przy wykorzystaniu oryginalnego skryptu pojawił się błąd: Failed to connect to non-global ctrl_ifname error: No such file or directory , co można zaobserwować na poniższej fotce:

raspberry-pi-initramfs-initrd-ssh-wifi-luks-error

Efektem naturalnie był brak połączenia WiFi w Raspberry Pi, przez co nie szło się zalogować do systemu po SSH. Natomiast rozwiązaniem tego problemu okazało się opóźnienie wywołania polecenia /sbin/wpa_supplicant przy pomocy sleep 1 && .

Plik /etc/initramfs-tools/hooks/enable-wireless

W katalogu /etc/initramfs-tools/hooks/ tworzymy plik enable-wireless i nadajemy mu prawa wykonywania:

root@raspberrypi:~# touch /etc/initramfs-tools/hooks/enable-wireless
root@raspberrypi:~# chmod +x /etc/initramfs-tools/hooks/enable-wireless

Następnie wrzucamy do tego pliku poniższą zawartość, odpowiednio dostosowując moduły, których wymaga karta WiFi (w tym przypadku brcmfmac ):

# !/bin/sh
# This goes into /etc/initramfs-tools/hooks/enable-wireless
set -e
PREREQ=""
prereqs()
{
    echo "${PREREQ}"
}
case "${1}" in
    prereqs)
        prereqs
        exit 0
        ;;
esac

. /usr/share/initramfs-tools/hook-functions

# CHANGE HERE for your correct modules.
manual_add_modules brcmfmac

copy_exec /sbin/wpa_supplicant
copy_exec /sbin/wpa_cli
copy_exec /sbin/iw

copy_file config /etc/initramfs-tools/wpa_supplicant.conf /etc/wpa_supplicant.conf

#copy_file firmware /lib/firmware/brcm/brcmfmac43455-sdio.bin
copy_file firmware /lib/firmware/brcm/brcmfmac43455-sdio.clm_blob
copy_file firmware /lib/firmware/brcm/brcmfmac43455-sdio.txt

copy_file config /lib/crda/regulatory.bin
copy_file config /lib/firmware/regulatory.db
copy_file config /lib/firmware/regulatory.db.p7s

Ten plik różni się trochę od oryginału. Wszystko dlatego, że gdy próbowałem uruchomić system na oryginalnej wersji tego pliku, to w systemie nie został wykryty interfejs wlan0 , a w logu można było zaobserwować poniższy komunikat:

raspberrypi kernel: brcmfmac: F1 signature read @0x18000000=0x15264345
raspberrypi kernel: brcmfmac: brcmf_fw_alloc_request: using brcm/brcmfmac43455-sdio for chip BCM4345/6
raspberrypi kernel: brcmfmac mmc1:0001:1: Direct firmware load for brcm/brcmfmac43455-sdio.txt failed with error -2
raspberrypi kernel: Console: switching to colour frame buffer device 160x64
raspberrypi kernel: vc4-drm gpu: [drm] fb0: vc4drmfb frame buffer device
raspberrypi kernel: brcmfmac: brcmf_sdio_htclk: HT Avail timeout (1000000): clkctl 0x50
raspberrypi kernel: usbcore: registered new interface driver brcmfmac

W przypadku mojego Raspberry Pi 4B, mamy do czynienia z kartą Broadcom, która wymaga do pracy modułu kernela brcmfmac . Gdy się zajrzy w informacje o tym module, to zobaczymy listę plików firmware, które ten moduł może chcieć zażądać. Na tej liście są uwzględnione firmware do różnych kart WiFi ale nas w tym momencie interesuje jedynie brakujący plik brcm/brcmfmac43455-sdio.txt :

root@raspberrypi:/tmp# modinfo -F firmware /lib/modules/5.10.63-v7l+/kernel/drivers/net/wireless/broadcom/brcm80211/brcmfmac/brcmfmac.ko
...
brcm/brcmfmac43455-sdio.bin
...

Jak widać, jedyny plik mający w nazwie brcm/brcmfmac43455-sdio , to brcm/brcmfmac43455-sdio.bin , a potrzebnego nam pliku brcm/brcmfmac43455-sdio.txt nie ma już na tej liście. Dlatego też polecenie manual_add_modules , z którego korzystaliśmy w powyższym skrypcie, nie było w stanie skopiować wszystkich potrzebnych plików do obrazu initramfs/initrd.

Jeśli zajrzymy do katalogu /lib/firmware/ , to zastaniemy tam te trzy poniższe pliki zamiast jednego:

root@raspberrypi:~# ls -al /lib/firmware/brcm/brcmfmac43455*
-rw-r--r-- 1 root root 631467 Nov  1 23:07 /lib/firmware/brcm/brcmfmac43455-sdio.bin
-rw-r--r-- 1 root root   7163 Nov  1 23:07 /lib/firmware/brcm/brcmfmac43455-sdio.clm_blob
-rw-r--r-- 1 root root   2074 Nov  1 23:07 /lib/firmware/brcm/brcmfmac43455-sdio.txt

Wszystkie te pliki firmware trzeba skopiować do obrazu initramfs/initrd, by karta WiFi w Raspberry Pi działała prawidłowo. Możemy naturalnie te wszystkie pliki skopiować do obrazu via copy_file firmware ale lepszym rozwiązaniem jest pozostawić manual_add_modules i skopiować jedynie brakujące pliki. Chodzi generalnie o to, że manual_add_modules bierze pod uwagę zależności modułów, czego efektem będzie skopiowanie wszystkich niezbędnych modułów do obrazu initramfs/initrd. Jeśli jednak chcielibyśmy ręcznie kopiować wszystkie rzeczy, to zależności modułów możemy wyciągnąć w poniższy sposób:

root@raspberrypi:~# modprobe --show-depends --ignore-install brcmfmac
insmod /lib/modules/5.10.63-v7l+/kernel/net/rfkill/rfkill.ko
insmod /lib/modules/5.10.63-v7l+/kernel/net/wireless/cfg80211.ko
insmod /lib/modules/5.10.63-v7l+/kernel/drivers/net/wireless/broadcom/brcm80211/brcmutil/brcmutil.ko
insmod /lib/modules/5.10.63-v7l+/kernel/drivers/net/wireless/broadcom/brcm80211/brcmfmac/brcmfmac.ko

Ewentualne pliki z firmware wymagane przez te moduły również trzeba uwzględnić w obrazie initramfs/initrd.

Ja jeszcze dodatkowo skopiowałem sobie bazę danych regulacji WiFi, tj. pliki regulatory.db* , które kernel zwykle żąda przy konfiguracji połączeń bezprzewodowych.

Plik /etc/initramfs-tools/scripts/local-bottom/kill_wireless

W katalogu /etc/initramfs-tools/scripts/local-bottom/ tworzymy skrypt kill_wireless i nadajemy mu prawa wykonywania:

root@raspberrypi:~# touch /etc/initramfs-tools/scripts/local-bottom/kill_wireless
root@raspberrypi:~# chmod +x /etc/initramfs-tools/scripts/local-bottom/kill_wireless

Następnie wrzucamy do tego pliku poniższą zawartość:

#!/bin/sh
# this goes into /etc/initramfs-tools/scripts/local-bottom/kill_wireless
PREREQ=""
prereqs()
{
    echo "$PREREQ"
}

case $1 in
prereqs)
    prereqs
    exit 0
    ;;
esac

echo "Killing wpa_supplicant so the system takes over later."
kill `cat /run/initram-wpa_supplicant.pid`

Zadaniem tego skryptu jest zerwanie połączenia WiFi po pomyślnym odszyfrowaniu kontenera LUKS. W późniejszym czasie startu systemu, Raspberry Pi ponownie się podłączy do WiFi, gdy już system się w pełni zainicjuje.

Plik /etc/initramfs-tools/wpa_supplicant.conf

W katalogu /etc/initramfs-tools/ tworzymy plik wpa_supplicant.conf , w którym będzie przechowywana konfiguracja sieci WiFi jedynie na potrzeby fazy initramfs/initrd:

root@raspberrypi:~# touch /etc/initramfs-tools/wpa_supplicant.conf

Następnie wrzucamy do tego pliku poniższą zawartość:

# sample /etc/initramfs-tools/wpa_supplicant.conf
# note that this is independent of the system /etc/wpa_supplicant.conf (if any)
# only add the network you need at boot time. **And keep the ctrl_interface** !!
ctrl_interface=/tmp/wpa_supplicant
country=PL

network={
    ssid="Ever_Vigilant"
    scan_ssid=1
    psk="DontLookAtMe"
    key_mgmt=WPA-PSK
}

Plik /etc/initramfs-tools/initramfs.conf

W pliku /etc/initramfs-tools/initramfs.conf musimy jedynie zmienić interfejs z przewodowego na bezprzewodowy, tak by system skonfigurował na nim adresację IP:

Dla statycznej adresacji IP:

IP="192.168.1.239::192.168.1.1:255.255.255.0:rpi:wlan0:none"

Dla dynamicznej adresacji IP via DHCP:

IP="::::rpi:wlan0:dhcp"

Regeneracja obrazu initramfs/initrd

Po dostosowaniu tych wszystkich powyższych plików musimy wygenerować nowy obraz initramfs/initrd przy pomocy polecenia update-initramfs :

root@raspberrypi:~# update-initramfs -u -k $(uname -r)
ln: failed to create hard link '/boot/initrd.img-5.10.63-v7l+.dpkg-bak' => '/boot/initrd.img-5.10.63-v7l+': Operation not permitted
update-initramfs: Generating /boot/initrd.img-5.10.63-v7l+
Building v7l+ image, updating top initrd

Test połączenia SSH przez WiFi z poziomu initramfs/initrd

Pozostało nam już tylko uruchomić Raspberry Pi ponownie i sprawdzić czy w fazie initramfs/initrd adresacja IP na interfejsie wlan0 jest przydzielana oraz czy jesteśmy w stanie bez problemu nawiązać połączenie SSH z RPI:

$ ssh 192.168.1.239
Please unlock disk rpi_crypt:

Połączenie zostało ustanowione i pojawił się prompt z prośbą o hasło by odszyfrować kontener LUKS.

Patrząc na monitor podłączony do Raspberry Pi, mamy taki oto obrazek chwilę przed wpisaniem hasła do kontenera:

raspberry-pi-initramfs-initrd-ssh-wifi-luks-ip-config

Komunikat Success: "WLAN online" świadczy o fakcie podłączenia do zdefiniowanej sieci WiFi. Adresacja na interfejsie wlan0 również została skonfigurowana. Po wpisaniu prawidłowego hasła przez SSH, kontener LUKS powinien zostać odszyfrowany, a system powinien kontynuować procedurę startu w tradycyjny sposób:

raspberry-pi-initramfs-initrd-ssh-wifi-luks-system-boot

Jako, że mam na swoim domowym routerze WiFi wgrany firmware OpenWRT, to mogę podejrzeć o czym te dwa urządzenia ze sobą rozmawiają. Chwilę po uruchomieniu Raspberry Pi ale przed otworzeniem kontenera w logu routera można zarejestrować poniższe komunikaty:

hostapd: wifi2g: STA dc:a6:32:af:5b:03 IEEE 802.11: authenticated
hostapd: wifi2g: STA dc:a6:32:af:5b:03 IEEE 802.11: associated (aid 2)
hostapd: wifi2g: AP-STA-CONNECTED dc:a6:32:af:5b:03
hostapd: wifi2g: STA dc:a6:32:af:5b:03 RADIUS: starting accounting session 49BEFF3343C481BD
hostapd: wifi2g: STA dc:a6:32:af:5b:03 WPA: pairwise key handshake completed (RSN)

Zatem Raspberry Pi uwierzytelnia się i podłącza bez problemu do routera po WiFi.

Po wpisaniu hasła do kontenera LUKS via SSH, po chwili w logu na routerze pojawiają się takie oto komunikaty:

hostapd: wifi2g: AP-STA-DISCONNECTED dc:a6:32:af:5b:03
hostapd: wifi2g: STA dc:a6:32:af:5b:03 IEEE 802.11: disassociated
hostapd: wifi2g: STA dc:a6:32:af:5b:03 IEEE 802.11: deauthenticated due to inactivity (timer DEAUTH/REMOVE)

Mamy tutaj informację, żę Raspberry Pi rozłącza połączenie WiFi po odszyfrowaniu kontenera LUKS, czyli dokładnie takie zachowanie, o jakie nam chodziło.

Po chwili w logu pojawiają się też te poniższe komunikaty:

hostapd: wifi2g: STA dc:a6:32:af:5b:03 IEEE 802.11: authenticated
hostapd: wifi2g: STA dc:a6:32:af:5b:03 IEEE 802.11: associated (aid 2)
hostapd: wifi2g: AP-STA-CONNECTED dc:a6:32:af:5b:03
hostapd: wifi2g: STA dc:a6:32:af:5b:03 RADIUS: starting accounting session FA7A4354549318AB
hostapd: wifi2g: STA dc:a6:32:af:5b:03 WPA: pairwise key handshake completed (RSN)
dnsmasq-dhcp[3510]: DHCPREQUEST(br-lan) 192.168.1.239 dc:a6:32:af:5b:03
dnsmasq-dhcp[3510]: DHCPACK(br-lan) 192.168.1.239 dc:a6:32:af:5b:03 RaspberryPi

Czego efektem naturalnie jest ponowne uzyskanie przez Raspberry Pi połączenia drogą bezprzewodową z domowym routerem WiFi i tym razem to router przydziela RPI adresację IP przy wykorzystaniu protokołu DHCP.

Podsumowanie

Implementując wsparcie dla połączeń WiFi w fazie initramfs/initrd usunęliśmy kluczowy problem, z jakim mogą zmagać się osoby próbujące otworzyć kontener LUKS w tej fazie korzystając z połączenia SSH. Możemy teraz sobie wybrać czy połączenia do Raspberry Pi mają być realizowane przewodowo, czy też bezprzewodowo. Opisane tutaj rozwiązanie nie jest specyficzne dla Raspberry Pi i można je wykorzystać również na innych dystrybucjach linux'a.

Mikhail Morfikov avatar
Mikhail Morfikov
Po ponad 10 latach spędzonych z różnej maści linux'ami (Debian/Ubuntu, OpenWRT, Android) mogę śmiało powiedzieć, że nie ma rzeczy niemożliwych i problemów, których nie da się rozwiązać. Jedną umiejętność, którą ludzki umysł musi posiąść, by wybrnąć nawet z tej najbardziej nieprzyjemniej sytuacji, to zdolność logicznego rozumowania.