diff --git a/README.md b/README.md index 8427b02..3a5dfb6 100644 --- a/README.md +++ b/README.md @@ -25,15 +25,18 @@ Extra files may be added next to `headless.apkovl.tar.gz` to customise boostrapp - `interfaces`[^3] (*optional*): define network interfaces at will, if defaults DCHP-based are not suitable. - `authorized_keys` (*optional*): provide client's public SSH key to secure `root` ssh login. - `ssh_host_*_key*` (*optional*): provide server's custom ssh keys to be injected (may be stored), instead of using bundled ones[^2] (not stored). Providing an empty key file will trigger new keys generation (ssh server may take longer to start). - - -**Goody:** seamless USB-serial & USB-ethernet gadget mode (PiZero for instance):\ -On supporting Pi devices, just add `dtoverlay=dwc2,dr_mode=peripheral` in `usercfg.txt` (or `config.txt`), and plug USB cable into host Computer port.\ -Serial terminal can then be connected-to from host Computer (e.g. `cu -l ttyACM0` on Linux. xon/xoff flow control).\ -Alternatively, with host Computer set-up to share networking with USB interface as 10.42.0.1 gateway, one can log into device from host with: `ssh root@10.42.0.2`. +- `auto-updt` (*optional*): allow apkovl file automatic update with latest from master branch: if contains *reboot* keyword all in one line, system will reboot after succesful update (unless ssh session is active or `unattended.sh` script is available). Main execution steps are logged: `cat /var/log/messages | grep headless`. +## Goody: +Seamless USB-serial & USB-ethernet gadget mode (*e.g. PiZero*): +- Make sure dwc2/dwc3 driver is loaded acordingly, and device configuration is set to `peripheral` mode: this may be hardware (including cable) and/or software driven.\ +(on supporting Pi devices, just add `dtoverlay=dwc2,dr_mode=peripheral` in `usercfg.txt` (or `config.txt`) to force by software) +- Plug USB cable into host Computer port before boot.\ +Serial terminal can then be connected-to from host Computer (e.g. `cu -l ttyACM0` on Linux. xon/xoff flow control).\ +Alternatively, with host Computer set-up to share networking with USB interface as 10.42.0.1 gateway, one can log into device from host with: `ssh root@10.42.0.2`. + [^1]: Initial boot fully preserves system's original state (config files & installed packages): a fresh system will therefore come-up as unconfigured. [^2]: About bundled ssh keys: this overlay is meant to **quickly bootstrap** system in order to then proceed with proper install; therefore it purposely embeds [some ssh keys](https://github.com/macmpi/alpine-linux-headless-bootstrap/tree/main/overlay/tmp/.trash) so that bootstrapping is as fast as possible. Those temporary keys are moved in RAM /tmp: they will **not be stored/reused** once actual system install is performed (whether or not ssh server is installed in final setup). diff --git a/headless.apkovl.tar.gz b/headless.apkovl.tar.gz index 8a57b83..7bbe38c 100644 Binary files a/headless.apkovl.tar.gz and b/headless.apkovl.tar.gz differ diff --git a/headless.apkovl.tar.gz.sha512 b/headless.apkovl.tar.gz.sha512 new file mode 100644 index 0000000..cd33774 --- /dev/null +++ b/headless.apkovl.tar.gz.sha512 @@ -0,0 +1 @@ +a8194eb225839a339f66610d68051516595bf30e943f67b7fd05527876cdbfbf7a1312da8d8abfec0a1ece04b8404ac5186c66a269491a5e6312f1e36b7f7694 headless.apkovl.tar.gz diff --git a/make.sh b/make.sh index ca10909..0297fb7 100755 --- a/make.sh +++ b/make.sh @@ -30,7 +30,8 @@ if [ -n "$build_path" ]; then doas tar cv -C "$build_path"/overlay --no-recursion \ $(doas find "$build_path"/overlay/ | sed "s|$build_path/overlay/||" | sort | xargs ) | \ gzip -c9n > headless.apkovl.tar.gz - TZ=UTC touch -cm -t "$t_stamp" headless.apkovl.tar.gz + sha512sum headless.apkovl.tar.gz > headless.apkovl.tar.gz.sha512 + TZ=UTC touch -cm -t "$t_stamp" headless.apkovl.tar.gz* doas rm -rf "$build_path" fi diff --git a/overlay/usr/local/bin/headless_bootstrap b/overlay/usr/local/bin/headless_bootstrap index 6ba5a96..9b7ccb5 100755 --- a/overlay/usr/local/bin/headless_bootstrap +++ b/overlay/usr/local/bin/headless_bootstrap @@ -3,7 +3,7 @@ # SPDX-FileCopyrightText: Copyright 2022-2023, macmpi # SPDX-License-Identifier: MIT -HDLSBSTRP_VERSION="1.1" +HDLSBSTRP_VERSION="1.2" _apk() { local cmd="$1" @@ -76,12 +76,13 @@ cat <<-EOF >> /tmp/.trash/headless_cleanup rm -f /etc/init.d/headless_* _logger "Clean-up done, enjoy !" cat /tmp/.trash/banner > /dev/console - if [ -c /dev/ttyGS0 ]; then - # Enabling terminal login into ttyGS0 serial for 60 sec - # no choice than making permanent change to pre 3.19 versions of /etc/securetty - grep -q "ttyGS0" /etc/securetty || echo "ttyGS0" >> /etc/securetty - /sbin/getty -L 115200 ttyGS0 vt100 & + if [ -n "${gdgt_term}" ]; then + # Enabling terminal login into valid serial port + # no choice than making permanent change to /etc/securetty (Alpine 3.19 already has ttyGS0) + grep -q "${gdgt_term}" /etc/securetty || echo "${gdgt_term}" >> /etc/securetty + /sbin/getty -L 115200 "${gdgt_term}" vt100 & fi + exit 0 EOF chmod +x /tmp/.trash/headless_cleanup } @@ -140,42 +141,74 @@ echo "$keygen_stance" >> /etc/conf.d/sshd rc-service sshd restart } +_updt_apkovl() { +## update apkovl overlay file & eventually reboot +# URL redirects to apkovl file on github master: is.gd shortener provides basic analytics. +# Analytics are public and can be checked at https://is.gd/stats.php?url=apkovl_master +# Privacy policy: https://is.gd/privacy.php + +local file_url="https://is.gd/apkovl_master" +local sha_url="https://github.com/macmpi/alpine-linux-headless-bootstrap/raw/main/headless.apkovl.tar.gz.sha512" +local updt_status="failed, keeping original version" +_logger "Updating overlay file..." + +# wget -q -O /tmp/sha -T 10 "$sha_url" > /dev/null 2>&1 && \ +if wget -q -O /tmp/apkovl -T 10 "$file_url" > /dev/null 2>&1 && \ +echo "36243ca58766232a1ff996de611724cee295109f981f3eb5d4b6df916c856036c002abd0cd58266602f3fcbf22777c1bfd1fb891888e864294b86dba366f6f2e toto" > /tmp/sha && \ + [ "$( sha512sum /tmp/apkovl | awk '{print $1}' )" = "$( awk '{print $1}' /tmp/sha )" ]; then + _is_ro && mount -o remount,rw "${ovlpath}" + cp /tmp/apkovl "${ovl}" + _is_ro && mount -o remount,ro "${ovlpath}" + ! [ "$( sha512sum "${ovl}" | awk '{print $1}' )" = "$( awk '{print $1}' /tmp/sha )" ] && \ + _logger "Bad update: original apkovl file may be altered, please check!..." && return 1 + updt_status="successful" +fi +rm -f /tmp/apkovl /tmp/sha +_logger "Update $updt_status" +[ "$updt_status" = "successful" ] || return 1 +# reboot if specified in auto-updt file (and no ssh session ongoing nor unattended.sh script available) +! pgrep -a -P "$( cat /run/sshd.pid 2>/dev/null )" 2>/dev/null | grep -q "sshd: root@pts" && \ + ! [ -f "${ovlpath}"/unattended.sh ] && \ + grep -q "^reboot$" "${ovlpath}"/auto-updt && \ + _logger "Will reboot in 3sec..." && sleep 3 && reboot +exit 0 +} + _tst_version() { -# Tested URL redirects to github project page: is.gd shortener provides basic analytics. +## Compare current version with latest online, notify & eventally calls for update +# URL redirects to github project page: is.gd shortener provides basic analytics. # Analytics are public and can be checked at https://is.gd/stats.php?url=apkovl_run # Privacy policy: https://is.gd/privacy.php local new_vers="" -local status="failed" local ref="/macmpi/alpine-linux-headless-bootstrap/releases/tag/v" -if wget -q -O /tmp/homepg -T 10 https://is.gd/apkovl_run > /dev/null 2>&1; then - status="success" +local url="https://is.gd/apkovl_run" + +if wget -q -O /tmp/homepg -T 10 "$url" > /dev/null 2>&1; then + _logger "Internet access: success" ver="$( grep -o "$ref.*\"" /tmp/homepg | grep -Eo '[0-9]+[\.[0-9]+]*' )" rm -f /tmp/homepg - [ -n "$ver" ] && ! [ "$ver" = "$HDLSBSTRP_VERSION" ] && \ - new_vers="!! Version $ver is available on Github project page !!" && \ - _logger "$new_vers" && \ + if [ -n "$ver" ] && ! [ "$ver" = "$HDLSBSTRP_VERSION" ]; then + new_vers="!! Version $ver is available on Github project page !!" + _logger "$new_vers" printf '%s\n\n' "$new_vers" >> /tmp/.trash/banner + # Optionally update apkovl if key-file allows it + [ -f "${ovlpath}"/auto-updt ] && _updt_apkovl & + fi +else + _logger "Internet access: failed" fi -_logger "Internet access: $status" + } _setup_networking() { ## Setup Network interfaces -local has_wifi +local has_wifi wlan_lst _has_wifi() { return "$has_wifi"; } - -find /sys/class/ieee80211/*/device/net/* -maxdepth 0 -type d -exec basename {} \; > /tmp/.wlan_list 2>/dev/null -[ -s /tmp/.wlan_list ] && [ -f "${ovlpath}"/wpa_supplicant.conf ] +wlan_lst="$( find /sys/class/net/*/phy80211 -exec \ + sh -c 'printf %s\| "$( basename "$( dirname "$0" )" )"' {} \; 2>/dev/null )" +wlan_lst="${wlan_lst%\|}" +[ -n "$wlan_lst" ] && [ -f "${ovlpath}"/wpa_supplicant.conf ] has_wifi=$? -if _has_wifi; then - _logger "Configuring wifi..." - _apk add wpa_supplicant - _preserve "/etc/wpa_supplicant/wpa_supplicant.conf" - install -m600 "${ovlpath}"/wpa_supplicant.conf /etc/wpa_supplicant/wpa_supplicant.conf - rc-service wpa_supplicant restart -else - _logger "No wifi interface or SSID/pass file supplied" -fi _preserve "/etc/network/interfaces" if ! install -m644 "${ovlpath}"/interfaces /etc/network/interfaces > /dev/null 2>&1; then @@ -202,24 +235,31 @@ if ! install -m644 "${ovlpath}"/interfaces /etc/network/interfaces > /dev/null 2 EOF ;; *) - _has_wifi && grep -q "$INTERFACE" /tmp/.wlan_list && \ + # According to below we could rely on DEVTYPE for wlan devices + # https://lists.freedesktop.org/archives/systemd-devel/2014-January/015999.html + # but...some wlan might still be ill-behaved: use wlan_lst + # shellcheck disable=SC2169 # ash does support string replacement + _has_wifi && ! [ "${wlan_lst/$INTERFACE/}" = "$wlan_lst" ] && \ cat <<-EOF >> /etc/network/interfaces auto $INTERFACE iface $INTERFACE inet dhcp EOF + # ensure considered gadget interface is actually connected over USB (may have several) [ "$DEVTYPE" = "gadget" ] && \ - cat <<-EOF >> /etc/network/interfaces && cat <<-EOF > /etc/resolv.conf - auto $INTERFACE - iface $INTERFACE inet static - address 10.42.0.2/24 - gateway 10.42.0.1 + ! [ "$( find -L /sys/class/udc/*/device/gadget*/net/"$INTERFACE" -maxdepth 0 -exec \ + sh -c 'cat "${0%/device*}"/current_speed' {} \; )" = "UNKNOWN" ] && \ + cat <<-EOF >> /etc/network/interfaces && cat <<-EOF > /etc/resolv.conf + auto $INTERFACE + iface $INTERFACE inet static + address 10.42.0.2/24 + gateway 10.42.0.1 - EOF - nameserver 208.67.222.222 - nameserver 208.67.220.220 + EOF + nameserver 208.67.222.222 + nameserver 208.67.220.220 - EOF + EOF ;; esac done @@ -230,35 +270,46 @@ echo "Using following network interfaces:" cat /etc/network/interfaces echo "###################################" +if _has_wifi && grep -qE "$wlan_lst" /etc/network/interfaces; then + _logger "Configuring wifi..." + _apk add wpa_supplicant + _preserve "/etc/wpa_supplicant/wpa_supplicant.conf" + install -m600 "${ovlpath}"/wpa_supplicant.conf /etc/wpa_supplicant/wpa_supplicant.conf + rc-service wpa_supplicant restart +else + _logger "No wifi interface or SSID/pass file supplied" +fi + _preserve "/etc/hostname" echo "alpine-headless" > /etc/hostname hostname -F /etc/hostname rc-service networking restart -rm -f /tmp/.wlan_list } _setup_gadget() { ## load composite USB Serial/USB Ethernel driver & setup terminal _logger "Enabling USB-gadget Serial and Ethernet ports" -lsmod | grep -q "dwc2" || modprobe -qs dwc2 # remove conflicting modules in case they were initially loaded (cmdline.txt) modprobe -rq g_serial g_ether g_cdc -modprobe -q g_cdc && sleep 1 -# once driver has settled check if cable is connected: unload if not -[ "$( cat "$udc_gadget"/current_speed )" = "UNKNOWN" ] && \ - _logger "USB cable not connected !!" && modprobe -rq g_cdc && return 1 +modprobe -q g_cdc -# default serial config: xon/xoff flow control -stty -g -F /dev/ttyGS0 >/dev/null 2>&1 -# notes to users willing to connect from Linux Ubuntu-based host terminal: -# - user on host needs to be part of dialout group (reboot required), and -# - disable spurious AT commands from ModemManager on host-side Gadget serial port -# you may create a /etc/udev/rules.d/99-ttyacms-gadget.rules as per: -# https://linux-tips.com/t/prevent-modem-manager-to-capture-usb-serial-devices/284/2 -# ATTRS{idVendor}=="0525" ATTRS{idProduct}=="a4aa", ENV{ID_MM_DEVICE_IGNORE}="1" +# look for valid serial port actually connected with USB cable +# (setting console to unconnect serial port would block boot) +gdgt_term="$( find /dev/ttyGS* -exec \ + sh -c 'timeout 1 echo "" > "$0" 2>/dev/null && echo "${0##*/}"' {} \; )" +if [ -n "$gdgt_term" ]; then + # default serial config: xon/xoff flow control + stty -g -F /dev/"$gdgt_term" >/dev/null 2>&1 + # notes to users willing to connect from Linux Ubuntu-based host terminal: + # - user on host needs to be part of dialout group (reboot required), and + # - disable spurious AT commands from ModemManager on host-side Gadget serial port + # you may create a /etc/udev/rules.d/99-ttyacms-gadget.rules as per: + # https://linux-tips.com/t/prevent-modem-manager-to-capture-usb-serial-devices/284/2 + # ATTRS{idVendor}=="0525" ATTRS{idProduct}=="a4aa", ENV{ID_MM_DEVICE_IGNORE}="1" -setconsole /dev/ttyGS0 + setconsole /dev/"$gdgt_term" +fi } @@ -272,10 +323,13 @@ _logger "Alpine Linux headless bootstrap v$HDLSBSTRP_VERSION by macmpi" # help randomness for wpa_supplicant and sshd (urandom until 3.16) rc-service seedrng restart || rc-service urandom restart -# setup USB gadget mode if such device mode is enabled -udc_gadget="$( dirname "$( find -L /sys/class/udc/* -maxdepth 2 -type f -name "is_a_peripheral" 2>/dev/null)" )" -[ "$( cat "$udc_gadget"/is_a_peripheral 2>/dev/null )" = "0" ] && \ - _setup_gadget +# setup USB gadget ports if some ports are enabled in peripheral mode +# Note: we assume dwc2/dwc3 is pre-loaded, we just check mode +gdgt_term="" +[ "$( find -L /sys/class/udc/*/is_a_peripheral -print0 2>/dev/null | \ + xargs -0 cat 2>/dev/null | \ + grep -c "0" )" -ge "1" ] && \ + _setup_gadget # Determine ovl file location # grab used ovl filename from dmesg @@ -291,8 +345,10 @@ fi # Create banner file warn="" -grep -q "${ovlpath}.*[[:space:]]ro[[:space:],]" /proc/mounts; RO=$? -[ "$RO" -eq "0" ] && warn="(remount partition rw!)" +grep -q "${ovlpath}.*[[:space:]]ro[[:space:],]" /proc/mounts; is_ro=$? +_is_ro() { return "$is_ro"; } + +_is_ro && warn="(remount partition rw!)" cat <<-EOF > /tmp/.trash/banner Alpine Linux headless bootstrap v$HDLSBSTRP_VERSION by macmpi diff --git a/sample_auto-updt b/sample_auto-updt new file mode 100644 index 0000000..5fbe9ba --- /dev/null +++ b/sample_auto-updt @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: Copyright 2022-2023, macmpi +# SPDX-License-Identifier: MIT + +# Automated reboot after update is cancelled if: +# - there is an opened ssh session +# - unattended.sh script is provided + +# Uncomment line below to enable reboot after update +#reboot + diff --git a/sample_unattended.sh b/sample_unattended.sh index 110e74f..c03ba8f 100644 --- a/sample_unattended.sh +++ b/sample_unattended.sh @@ -37,10 +37,11 @@ else fi # also works in case volume is mounted read-only -grep -q "${ovlpath}.*[[:space:]]ro[[:space:],]" /proc/mounts; RO=$? -[ "$RO" -eq "0" ] && mount -o remount,rw "${ovlpath}" +grep -q "${ovlpath}.*[[:space:]]ro[[:space:],]" /proc/mounts; is_ro=$? +_is_ro() { return "$is_ro"; } +_is_ro && mount -o remount,rw "${ovlpath}" rm -f "${ovl}" -[ "$RO" -eq "0" ] && mount -o remount,ro "${ovlpath}" +_is_ro && mount -o remount,ro "${ovlpath}" ########################################################