| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237 |
- #!/bin/bash
- #
- # Dune Weaver Autohotspot
- #
- # Two modes:
- # Boot mode (no args): Scans for known WiFi with 30s retry timeout.
- # Check mode (--check): Quick single-pass check, runs every 60s via timer.
- #
- # Logic:
- # - If connected to known WiFi → do nothing
- # - If in hotspot mode and known WiFi appears → switch to client, reboot
- # - If disconnected → scan once → connect or activate hotspot
- #
- # DNS redirect for captive portal is handled by NetworkManager's built-in
- # dnsmasq via config in /etc/NetworkManager/dnsmasq-shared.d/.
- #
- # Requires: NetworkManager (Pi Trixie)
- #
- set -euo pipefail
- HOTSPOT_CON_NAME="DuneWeaver-Hotspot"
- MODE_FILE="/tmp/dw-wifi-mode"
- SCAN_TIMEOUT=30
- SCAN_INTERVAL=5
- IFACE="wlan0"
- log() {
- echo "[autohotspot] $*"
- logger -t autohotspot "$*"
- }
- # Wait for NetworkManager to be ready
- wait_for_nm() {
- local waited=0
- while [ $waited -lt 10 ]; do
- if nmcli general status &>/dev/null; then
- return 0
- fi
- sleep 1
- waited=$((waited + 1))
- done
- log "ERROR: NetworkManager not ready after 10s"
- return 1
- }
- # Get list of saved WiFi connection names
- get_saved_wifi() {
- nmcli -t -f NAME,TYPE con show | grep ':.*wireless' | cut -d: -f1 | grep -v "^${HOTSPOT_CON_NAME}$" || true
- }
- # Get current wlan0 connection state: "client", "hotspot", or "disconnected"
- get_current_state() {
- local active
- active=$(nmcli -t -f NAME,TYPE,DEVICE con show --active 2>/dev/null || true)
- while IFS= read -r line; do
- local name device
- name=$(echo "$line" | cut -d: -f1)
- device=$(echo "$line" | rev | cut -d: -f1 | rev)
- if [ "$device" = "$IFACE" ]; then
- if [ "$name" = "$HOTSPOT_CON_NAME" ]; then
- echo "hotspot"
- return
- fi
- echo "client"
- return
- fi
- done <<< "$active"
- echo "disconnected"
- }
- # Scan for available networks and check if any match saved connections
- check_for_known_wifi() {
- # Trigger a fresh scan
- nmcli dev wifi rescan ifname "$IFACE" 2>/dev/null || true
- sleep 2
- # Get available SSIDs
- local available
- available=$(nmcli -t -f SSID dev wifi list ifname "$IFACE" 2>/dev/null | sort -u | grep -v '^$' || true)
- if [ -z "$available" ]; then
- return 1
- fi
- # Get saved connections
- local saved
- saved=$(get_saved_wifi)
- if [ -z "$saved" ]; then
- return 1
- fi
- # Check if any saved connection matches an available network
- while IFS= read -r saved_name; do
- # Get the SSID associated with this saved connection
- local saved_ssid
- saved_ssid=$(nmcli -t -f connection.id,802-11-wireless.ssid con show "$saved_name" 2>/dev/null | grep '802-11-wireless.ssid' | cut -d: -f2 || true)
- # If we couldn't get the SSID from connection details, use the connection name as SSID
- if [ -z "$saved_ssid" ]; then
- saved_ssid="$saved_name"
- fi
- if echo "$available" | grep -qxF "$saved_ssid"; then
- echo "$saved_name"
- return 0
- fi
- done <<< "$saved"
- return 1
- }
- # Activate client mode with a known connection
- activate_client_mode() {
- local con_name="$1"
- log "Found known network, activating connection: $con_name"
- # Deactivate hotspot if it's active
- nmcli con down "$HOTSPOT_CON_NAME" 2>/dev/null || true
- # Connect
- if nmcli con up "$con_name" 2>/dev/null; then
- local ip
- ip=$(nmcli -t -f IP4.ADDRESS dev show "$IFACE" 2>/dev/null | head -1 | cut -d: -f2 | cut -d/ -f1 || echo "unknown")
- log "Client mode active. Connected to '$con_name', IP: $ip"
- echo "client" > "$MODE_FILE"
- return 0
- else
- log "Failed to connect to '$con_name'"
- return 1
- fi
- }
- # Activate hotspot mode
- activate_hotspot_mode() {
- log "No known WiFi found. Activating hotspot mode..."
- # Make sure we're not connected to anything
- nmcli dev disconnect "$IFACE" 2>/dev/null || true
- # Activate the hotspot profile
- # NM's shared mode starts built-in dnsmasq for DHCP+DNS.
- # Our config in /etc/NetworkManager/dnsmasq-shared.d/ makes it
- # redirect all DNS queries to 10.42.0.1 for captive portal detection.
- if nmcli con up "$HOTSPOT_CON_NAME" 2>/dev/null; then
- log "Hotspot active. SSID: $(nmcli -t -f 802-11-wireless.ssid con show "$HOTSPOT_CON_NAME" 2>/dev/null | cut -d: -f2)"
- log "DNS redirect active via NM dnsmasq (captive portal mode)"
- echo "hotspot" > "$MODE_FILE"
- return 0
- else
- log "ERROR: Failed to activate hotspot"
- return 1
- fi
- }
- # Boot mode: scan with retry loop, then fallback to hotspot
- boot_mode() {
- log "Boot mode: scanning for known WiFi (${SCAN_TIMEOUT}s timeout)..."
- # Wait for NetworkManager
- if ! wait_for_nm; then
- log "NetworkManager not available, exiting"
- exit 1
- fi
- # Check if hotspot profile exists
- if ! nmcli con show "$HOTSPOT_CON_NAME" &>/dev/null; then
- log "Hotspot profile '$HOTSPOT_CON_NAME' not found. Run 'dw wifi setup' first."
- echo "client" > "$MODE_FILE"
- exit 0
- fi
- # Scan for known WiFi with timeout
- local elapsed=0
- while [ $elapsed -lt $SCAN_TIMEOUT ]; do
- local match
- if match=$(check_for_known_wifi); then
- activate_client_mode "$match"
- exit 0
- fi
- log "No known WiFi found yet, retrying... (${elapsed}s/${SCAN_TIMEOUT}s)"
- sleep $SCAN_INTERVAL
- elapsed=$((elapsed + SCAN_INTERVAL))
- done
- # No known WiFi found after timeout — go to hotspot mode
- activate_hotspot_mode
- }
- # Check mode: quick single-pass check for periodic timer
- check_mode() {
- # Check if hotspot profile exists
- if ! nmcli con show "$HOTSPOT_CON_NAME" &>/dev/null; then
- exit 0
- fi
- local state
- state=$(get_current_state)
- case "$state" in
- client)
- # Already connected — nothing to do
- ;;
- hotspot)
- # In hotspot mode — check if a known network has appeared
- local match
- if match=$(check_for_known_wifi); then
- log "Known WiFi found while in hotspot mode, switching to client..."
- activate_client_mode "$match"
- fi
- ;;
- disconnected)
- # WiFi dropped — try to reconnect or fall back to hotspot
- log "WiFi disconnected, attempting recovery..."
- local match
- if match=$(check_for_known_wifi); then
- activate_client_mode "$match"
- else
- activate_hotspot_mode
- fi
- ;;
- esac
- }
- # Main
- case "${1:-}" in
- --check)
- check_mode
- ;;
- *)
- boot_mode
- ;;
- esac
|