diff --git a/menus/wifi.sh b/menus/wifi.sh index 5212991..8ce9efe 100644 --- a/menus/wifi.sh +++ b/menus/wifi.sh @@ -20,13 +20,89 @@ # is exercised by hand against real hardware only. # # Unlike the other migrated menus, this one has no sub-options - it's a -# single linear wizard, same as the legacy configure_wifi - so wifi_menu -# IS the action, not a run_menu wrapper. +# single linear wizard, same as the legacy configure_wifi. wifi_menu runs +# it (wifi_configure) and then pauses: run_menu clears the screen as soon +# as a handler returns, which used to wipe every error message here +# before it could be read - the menu just seemed to bounce back. +# +# The WiFi config goes in its own file, $NETPLAN_DIR/60-kiosk-wifi.yaml, +# which netplan merges with the rest - not over the first *.yaml it finds, +# which on a fresh install is the installer's wired config. # # Depends on: lib/menu.sh, lib/config.sh being sourced first. ################################################################################ +WIFI_NETPLAN_FILE="$NETPLAN_DIR/60-kiosk-wifi.yaml" + wifi_menu() { + local rc=0 + wifi_configure || rc=$? + echo + pause + return "$rc" +} + +# The first wireless interface, by what it is (it has a wireless/ or +# phy80211 entry in sysfs) rather than by name - adapters aren't always +# named wl*. Falls back to the wl* naming convention. +wifi_detect_iface() { + local d + for d in /sys/class/net/*; do + if [[ -d "$d/wireless" || -e "$d/phy80211" ]]; then + basename "$d" + return 0 + fi + done + local name + for d in /sys/class/net/wl*; do + [[ -e "$d" ]] || continue + name=$(basename "$d") + echo "$name" + return 0 + done + return 1 +} + +# Clear a software WiFi block (the rfkill "soft" switch) - with it set, +# `ip link set up` fails with "RF-kill". The rfkill tool isn't installed +# on a server, so via sysfs. A hard block (physical switch / BIOS) can't +# be cleared from here and is reported instead. +wifi_unblock() { + local r type + for r in /sys/class/rfkill/rfkill*; do + [[ -e "$r/type" ]] || continue + type=$(cat "$r/type" 2>/dev/null) + [[ "$type" == "wlan" ]] || continue + if [[ "$(cat "$r/soft" 2>/dev/null)" == "1" ]]; then + echo "WiFi is switched off in software (rfkill) - switching it on..." + echo 0 | sudo tee "$r/soft" >/dev/null || true + fi + if [[ "$(cat "$r/hard" 2>/dev/null)" == "1" ]]; then + log_warning "WiFi is switched off by a hardware switch or the BIOS/UEFI - turn it on there" + fi + done +} + +# YAML single-quoted scalar: the only special character inside is the +# quote itself, doubled. Safe for any SSID/password (", \, #, : ...) - +# double quotes broke on a password containing " or \. +wifi_yaml_quote() { + local v="$1" + printf "'%s'" "${v//\'/\'\'}" +} + +# IFACE's first IPv4 address (without the /prefix), or nothing. +wifi_iface_ip() { + ip -4 -o addr show dev "$1" 2>/dev/null \ + | awk '{for (i = 1; i < NF; i++) if ($i == "inet") { sub("/.*", "", $(i+1)); print $(i+1); exit }}' +} + +# True if IFACE has an IPv4 address. +wifi_iface_has_ip() { + ip -4 addr show dev "$1" 2>/dev/null | grep -q "inet " +} + +wifi_configure() { echo echo " ═══ WIFI CONFIGURATION ═══" echo @@ -55,11 +131,11 @@ wifi_menu() { fi local wifi_iface - wifi_iface=$(ls /sys/class/net 2>/dev/null | grep -E "^wl" | head -1) - - if [[ -z "$wifi_iface" ]]; then + if ! wifi_iface=$(wifi_detect_iface); then log_warning "No WiFi hardware detected" - echo "If you have a USB WiFi adapter, ensure it's plugged in." + echo "Network interfaces found: $(ls /sys/class/net 2>/dev/null | tr '\n' ' ')" + echo "If you have a USB WiFi adapter, make sure it's plugged in. A built-in" + echo "adapter with no driver shows up in 'lspci -k' / 'lsusb' but not above." return 1 fi @@ -74,9 +150,11 @@ wifi_menu() { ask_yes_no "Configure WiFi?" "n" || return 0 + wifi_unblock echo "Bringing up interface..." - if ! sudo ip link set "$wifi_iface" up 2>/dev/null; then - log_error "Failed to bring up interface" + local link_err + if ! link_err=$(sudo ip link set "$wifi_iface" up 2>&1); then + log_error "Failed to bring up $wifi_iface: ${link_err:-unknown error}" return 1 fi sleep 3 @@ -161,15 +239,9 @@ apply_wifi_config() { local ssid="$2" local password="$3" - # `|| true`: under set -e + pipefail (this whole tool runs under both), - # `ls` matching nothing exits non-zero even with stderr silenced, which - # would abort this function outright instead of falling through to the - # default filename below. Same class of bug as the run_menu fix in - # lib/menu.sh - masked here in practice because cloud-init almost - # always leaves a *.yaml file behind, but not guaranteed. - local netplan_file - netplan_file=$(ls "$NETPLAN_DIR"/*.yaml 2>/dev/null | head -1) || true - [[ -z "$netplan_file" ]] && netplan_file="$NETPLAN_DIR/50-cloud-init.yaml" + # Our own file, merged by netplan with the others (wired config etc). + local netplan_file="$WIFI_NETPLAN_FILE" + sudo mkdir -p "$NETPLAN_DIR" local backup="" if [[ -f "$netplan_file" ]]; then @@ -190,29 +262,31 @@ network: dhcp6: false optional: true access-points: - "$ssid": - password: "$password" + $(wifi_yaml_quote "$ssid"): + password: $(wifi_yaml_quote "$password") EOF - if [[ -n "${SSH_CONNECTION:-}" ]] && [[ -n "$backup" ]]; then + if [[ -n "${SSH_CONNECTION:-}" ]]; then + # Reverts if the WiFi interface still has no address after 60s - + # not "can't ping 8.8.8.8", which on a network without internet + # would undo a perfectly good connection every time. No backup + # means there was no WiFi file before: revert = remove ours. local watchdog watchdog=$(mktemp --suffix=.sh) cat > "$watchdog" <<'WATCHEOF' #!/bin/bash +# $1 = backup ("" if none), $2 = netplan file, $3 = WiFi interface sleep 60 -if [[ -f "$1" && -f "$2" ]]; then - ip=$(hostname -I | awk '{print $1}') - if [[ -z "$ip" ]] || ! ping -c 2 8.8.8.8 >/dev/null 2>&1; then - cp "$1" "$2" - netplan apply 2>/dev/null - echo "WiFi config reverted - connection failed" | wall - fi +if ! ip -4 addr show dev "$3" 2>/dev/null | grep -q "inet "; then + if [[ -n "$1" && -f "$1" ]]; then cp "$1" "$2"; else rm -f "$2"; fi + netplan apply 2>/dev/null + echo "WiFi config reverted - $3 got no address" | wall fi rm -f "$0" WATCHEOF chmod +x "$watchdog" - nohup sudo bash "$watchdog" "$backup" "$netplan_file" >/dev/null 2>&1 & - echo "Watchdog started - will revert in 60s if the connection fails" + nohup sudo bash "$watchdog" "$backup" "$netplan_file" "$wifi_iface" >/dev/null 2>&1 & + echo "Watchdog started - will revert in 60s if $wifi_iface gets no address" fi sudo cp "$temp_plan" "$netplan_file" @@ -224,22 +298,30 @@ WATCHEOF netplan_log=$(mktemp) if sudo netplan apply > "$netplan_log" 2>&1; then cat "$netplan_log" - sleep 10 - local new_ip - new_ip=$(get_ip_address) - if [[ -n "$new_ip" && "$new_ip" != "No IP" ]]; then - log_success "Connected: $ssid ($new_ip)" + echo "Waiting for $wifi_iface to connect (up to 30 seconds)..." + local _ + for _ in {1..30}; do + wifi_iface_has_ip "$wifi_iface" && break + sleep 1 + done + if wifi_iface_has_ip "$wifi_iface"; then + log_success "Connected to $ssid: $(wifi_iface_ip "$wifi_iface")" [[ -n "${SSH_CONNECTION:-}" ]] && echo "Connection successful - watchdog will not revert" else - log_warning "Config applied but no IP yet" - echo "Check: sudo journalctl -u systemd-networkd -f" + log_warning "Config applied but $wifi_iface has no address yet" + echo "Wrong password or network name is the usual cause. Details:" + echo " sudo journalctl -b -u 'netplan-wpa-*' -u systemd-networkd -n 30 --no-pager" fi else log_error "netplan apply failed" echo "Error log:" cat "$netplan_log" - if [[ -n "$backup" ]] && ask_yes_no "Restore backup?" "y"; then - sudo cp "$backup" "$netplan_file" + if ask_yes_no "Undo this WiFi change?" "y"; then + if [[ -n "$backup" ]]; then + sudo cp "$backup" "$netplan_file" + else + sudo rm -f "$netplan_file" + fi # Last resort after everything else failed: report, don't crash # the session if even the restore-and-reapply doesn't work. if sudo netplan apply; then