#!/bin/sh # SPDX-FileCopyrightText: Copyright 2022-2023, macmpi # SPDX-License-Identifier: MIT HDLSBSTRP_VERSION="1.2" _apk() { local cmd="$1" local pkg="$2" case $cmd in add) # install only if not already present if ! apk info | grep -wq "${pkg}"; then apk add "$pkg" && printf '%s ' "${pkg}" >>/tmp/.trash/installed fi ;; del) # delete only if previously installed if grep -wq "$pkg" /tmp/.trash/installed >/dev/null 2>&1; then apk del "$pkg" && sed -i 's/\b'"${pkg}"'\b//' /tmp/.trash/installed fi ;; *) echo "only add/del: wrong usage"; exit ;; esac } _preserve() { # Create a back-up of element (file, folder, symlink). [ -z "${1}" ] && return 1 [ -e "${1}" ] && cp -a "${1}" "${1}".orig } _restore() { # Remove element (file, folder, symlink) and replace by # previous back-up if available. [ -z "${1}" ] && return 1 rm -rf "${1}" [ -e "${1}".orig ] && mv -f "${1}".orig "${1}" } # shellcheck disable=SC2142 # known special case alias _logger='logger -st "${0##*/}"' ##### End of part to be duplicated into headless_cleanup (do not alter!) _prep_cleanup() { ## Prep for final headless_cleanup: # clears any installed packages and settings. # Copy begininng of this file to keep functions. sed -n '/^#* End .*alter!)$/q;p' /usr/local/bin/headless_bootstrap >/tmp/.trash/headless_cleanup cat <<-EOF >>/tmp/.trash/headless_cleanup # Redirect stdout and errors to console as service won't show messages exec 1>/dev/console 2>&1 _logger "Cleaning-up..." _restore "/etc/ssh/sshd_config" _restore "/etc/conf.d/sshd" _apk del openssh-server _restore "/etc/wpa_supplicant/wpa_supplicant.conf" _apk del wpa_supplicant _restore "/etc/network/interfaces" _restore "/etc/hostname" rm -f /etc/modprobe.d/headless_gadget.conf # Remove from boot service to avoid spurious openrc recalls from unattended script. rm -f /etc/runlevels/default/headless_bootstrap rm -f /usr/local/bin/headless_bootstrap # Run unattended script if available. install -m755 ${ovlpath}/unattended.sh /tmp/headless_unattended >/dev/null 2>&1 && \ _logger "Starting headless_unattended service" && \ rc-service headless_unattended start rm -f /etc/init.d/headless_* _logger "Clean-up done, enjoy !" cat /tmp/.trash/banner >/dev/console if [ -c /dev/ttyGS${gdgt_id} ]; 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 "ttyGS${gdgt_id}" /etc/securetty || echo "ttyGS${gdgt_id}" >>/etc/securetty /sbin/getty -L 115200 /dev/ttyGS${gdgt_id} vt100 & fi exit 0 EOF chmod +x /tmp/.trash/headless_cleanup } _setup_sshd() { ## Setup temporary SSH server (root login, no password): # we use some bundled (or optionaly provided) keys to avoid generation at startup and save time. _apk add openssh-server _preserve "/etc/ssh/sshd_config" _preserve "/etc/conf.d/sshd" cat <<-EOF >/etc/ssh/sshd_config PermitRootLogin yes Banner /tmp/.trash/banner EOF # Client authorized_keys or no authentication. if install -m600 "${ovlpath}"/authorized_keys /tmp/.trash/authorized_keys >/dev/null 2>&1; then _logger "Enabling public key SSH authentication..." cat <<-EOF >>/etc/ssh/sshd_config AuthenticationMethods publickey AuthorizedKeysFile /tmp/.trash/authorized_keys # relax strict mode as authorized_keys are inside /tmp StrictModes no EOF else _logger "No SSH authentication." cat <<-EOF >>/etc/ssh/sshd_config AuthenticationMethods none PermitEmptyPasswords yes EOF fi # Server keys: inject optional custom keys, or generate new (might be stored), # or use bundeled ones (not stored) local keygen_stance="sshd_disable_keygen=yes" if install -m600 "${ovlpath}"/ssh_host_*_key* /etc/ssh/ >/dev/null 2>&1; then # Check for empty key within injected ones: if found, generate new keys. if find /etc/ssh/ -maxdepth 1 -type f -name 'ssh_host_*_key*' -empty | grep -q .; then rm /etc/ssh/ssh_host_*_key* keygen_stance="" _logger "Will generate new SSH keys..." else chmod 644 /etc/ssh/ssh_host_*_key.pub _logger "Using injected SSH keys..." fi else _logger "Using bundled ssh keys from RAM..." cat <<-EOF >>/etc/ssh/sshd_config HostKey /tmp/.trash/ssh_host_ed25519_key HostKey /tmp/.trash/ssh_host_rsa_key EOF fi 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" if wget -q -O /tmp/apkovl -T 10 "$file_url" >/dev/null 2>&1 && \ wget -q -O /tmp/sha -T 10 "$sha_url" >/dev/null 2>&1 && \ [ "$( 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" if [ "$updt_status" = "successful" ]; then printf '%s\n\n' "Updated" >>/tmp/.trash/banner else printf '\n' >>/tmp/.trash/banner return 1 fi # 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() { ## 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 vers="" local ref="/macmpi/alpine-linux-headless-bootstrap/releases/tag/v" 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" vers="$( grep -o "$ref.*\"" /tmp/homepg | grep -Eo '[0-9]+[\.[0-9]+]*' )" rm -f /tmp/homepg if [ -n "$vers" ] && ! [ "$vers" = "$HDLSBSTRP_VERSION" ]; then vers="!! Version $vers is available on Github project page !!" _logger "$vers" printf '%s\n' "$vers" >>/tmp/.trash/banner # Optionally update apkovl if key-file allows it. if [ -f "${ovlpath}"/auto-updt ]; then _logger "Updating overlay file..." _updt_apkovl & else _logger "(check doc to enable auto-update)" printf '%s\n\n' "(check doc to enable auto-update)" >>/tmp/.trash/banner fi fi else _logger "Internet access: failed" fi } _setup_networking() { ## Setup network interfaces. local has_wifi wlan_lst _has_wifi() { return "$has_wifi"; } 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=$? _preserve "/etc/network/interfaces" if ! install -m644 "${ovlpath}"/interfaces /etc/network/interfaces >/dev/null 2>&1; then _logger "No interfaces file supplied, building defaults..." cat <<-EOF >/etc/network/interfaces auto lo iface lo inet loopback EOF for dev in /sys/class/net/*; do # shellcheck disable=SC2034 # Unused IFINDEX while still sourced from uevent. local DEVTYPE INTERFACE IFINDEX DEVTYPE="" # shellcheck source=/dev/null . "$dev"/uevent case ${INTERFACE%%[0-9]*} in lo) ;; eth) cat <<-EOF >>/etc/network/interfaces auto $INTERFACE iface $INTERFACE inet dhcp EOF ;; *) # 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 the connected one (may have several). [ "$DEVTYPE" = "gadget" ] && \ find /sys/class/udc/*/device/gadget."${gdgt_id}"/net/"$INTERFACE" -maxdepth 0 >/dev/null 2>&1 && \ 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 ;; esac done fi echo "###################################" 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 } _setup_gadget() { ## Load composite USB Serial/USB Ethernel driver & setup terminal. _logger "Enabling USB-gadget Serial and Ethernet ports" # Remove conflicting modules in case they were initially loaded (cmdline.txt). modprobe -r g_serial g_ether g_cdc modprobe g_cdc # Wait for g_cdc to settle and serial ports become available timeout 1 sh <<-EOF while ! grep -q "ttyGS" /proc/devices; do sleep 0.2; done EOF # Determine which gadget ID is connected with USB cable (assume just one max). # (setting console to unconnected serial port would block boot) gdgt_id="$( find /sys/class/udc/*/current_speed -exec \ sh -c 'grep -vq "UNKNOWN" "$0" && find ${0/current_speed/}device/gadget.* -maxdepth 0' {} \; \ | sed 's/\/.*gadget\.//' )" if [ -c /dev/ttyGS"${gdgt_id}" ]; then # Default serial config: xon/xoff flow control. stty -F /dev/ttyGS"${gdgt_id}" setconsole /dev/ttyGS"${gdgt_id}" # 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 # one 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" else _logger "USB-gadget port not connected !" modprobe -r g_cdc fi } ############################################################################# ## Main # Redirect stdout and errors to console as service won't show messages exec 1>/dev/console 2>&1 _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 ports if some ports are enabled in peripheral mode. # Note: we assume dwc2/dwc3 is pre-loaded, we just check mode. gdgt_id="" find /sys/class/udc/*/is_a_peripheral -print0 2>/dev/null | \ xargs -0 cat 2>/dev/null | grep -q "0" && \ _setup_gadget # Determine ovl file location. # Grab used ovl filename from dmesg. ovl="$( dmesg | grep -o 'Loading user settings from .*:' | awk '{print $5}' | sed 's/:.*$//' )" if [ -f "${ovl}" ]; then ovlpath="$( dirname "$ovl" )" else # Search path again as mountpoint have been changed later in the boot process... ovl="$( basename "${ovl}" )" ovlpath=$( find /media -maxdepth 2 -type d -path '*/.*' -prune -o -type f -name "${ovl}" -exec dirname {} \; | head -1 ) ovl="${ovlpath}/${ovl}" fi # Create banner file. warn="" 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 You may want to delete/rename .apkovl file before reboot ${warn}: ${ovl} (can be done automatically with unattended script - see sample snippet) EOF _setup_networking # Test latest available version online. # Can be skipped by creating a 'opt-out'-named dummy file aside apkovl file. [ -f "${ovlpath}"/opt-out ] || _tst_version & # Setup sshd unless unattended.sh script prevents it. grep -q "^#NO_SSH$" "${ovlpath}"/unattended.sh >/dev/null 2>&1 \ || _setup_sshd _prep_cleanup _logger "Initial setup done, handing-over to clean-up" rc-service headless_cleanup start exit 0