1
0

autohotspot 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. #!/bin/bash
  2. #
  3. # Dune Weaver Autohotspot
  4. #
  5. # Two modes:
  6. # Boot mode (no args): Scans for known WiFi with 30s retry timeout.
  7. # Check mode (--check): Quick single-pass check, runs every 60s via timer.
  8. #
  9. # Logic:
  10. # - If connected to known WiFi → do nothing
  11. # - If in hotspot mode and known WiFi appears → switch to client, reboot
  12. # - If disconnected → scan once → connect or activate hotspot
  13. #
  14. # DNS redirect for captive portal is handled by NetworkManager's built-in
  15. # dnsmasq via config in /etc/NetworkManager/dnsmasq-shared.d/.
  16. #
  17. # Requires: NetworkManager (Pi Trixie)
  18. #
  19. set -euo pipefail
  20. HOTSPOT_CON_NAME="DuneWeaver-Hotspot"
  21. MODE_FILE="/tmp/dw-wifi-mode"
  22. SCAN_TIMEOUT=30
  23. SCAN_INTERVAL=5
  24. IFACE="wlan0"
  25. log() {
  26. echo "[autohotspot] $*"
  27. logger -t autohotspot "$*"
  28. }
  29. # Wait for NetworkManager to be ready
  30. wait_for_nm() {
  31. local waited=0
  32. while [ $waited -lt 10 ]; do
  33. if nmcli general status &>/dev/null; then
  34. return 0
  35. fi
  36. sleep 1
  37. waited=$((waited + 1))
  38. done
  39. log "ERROR: NetworkManager not ready after 10s"
  40. return 1
  41. }
  42. # Get list of saved WiFi connection names
  43. get_saved_wifi() {
  44. nmcli -t -f NAME,TYPE con show | grep ':.*wireless' | cut -d: -f1 | grep -v "^${HOTSPOT_CON_NAME}$" || true
  45. }
  46. # Get current wlan0 connection state: "client", "hotspot", or "disconnected"
  47. get_current_state() {
  48. local active
  49. active=$(nmcli -t -f NAME,TYPE,DEVICE con show --active 2>/dev/null || true)
  50. while IFS= read -r line; do
  51. local name device
  52. name=$(echo "$line" | cut -d: -f1)
  53. device=$(echo "$line" | rev | cut -d: -f1 | rev)
  54. if [ "$device" = "$IFACE" ]; then
  55. if [ "$name" = "$HOTSPOT_CON_NAME" ]; then
  56. echo "hotspot"
  57. return
  58. fi
  59. echo "client"
  60. return
  61. fi
  62. done <<< "$active"
  63. echo "disconnected"
  64. }
  65. # Scan for available networks and check if any match saved connections
  66. check_for_known_wifi() {
  67. # Trigger a fresh scan
  68. nmcli dev wifi rescan ifname "$IFACE" 2>/dev/null || true
  69. sleep 2
  70. # Get available SSIDs
  71. local available
  72. available=$(nmcli -t -f SSID dev wifi list ifname "$IFACE" 2>/dev/null | sort -u | grep -v '^$' || true)
  73. if [ -z "$available" ]; then
  74. return 1
  75. fi
  76. # Get saved connections
  77. local saved
  78. saved=$(get_saved_wifi)
  79. if [ -z "$saved" ]; then
  80. return 1
  81. fi
  82. # Check if any saved connection matches an available network
  83. while IFS= read -r saved_name; do
  84. # Get the SSID associated with this saved connection
  85. local saved_ssid
  86. 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)
  87. # If we couldn't get the SSID from connection details, use the connection name as SSID
  88. if [ -z "$saved_ssid" ]; then
  89. saved_ssid="$saved_name"
  90. fi
  91. if echo "$available" | grep -qxF "$saved_ssid"; then
  92. echo "$saved_name"
  93. return 0
  94. fi
  95. done <<< "$saved"
  96. return 1
  97. }
  98. # Activate client mode with a known connection
  99. activate_client_mode() {
  100. local con_name="$1"
  101. log "Found known network, activating connection: $con_name"
  102. # Deactivate hotspot if it's active
  103. nmcli con down "$HOTSPOT_CON_NAME" 2>/dev/null || true
  104. # Connect
  105. if nmcli con up "$con_name" 2>/dev/null; then
  106. local ip
  107. ip=$(nmcli -t -f IP4.ADDRESS dev show "$IFACE" 2>/dev/null | head -1 | cut -d: -f2 | cut -d/ -f1 || echo "unknown")
  108. log "Client mode active. Connected to '$con_name', IP: $ip"
  109. echo "client" > "$MODE_FILE"
  110. return 0
  111. else
  112. log "Failed to connect to '$con_name'"
  113. return 1
  114. fi
  115. }
  116. # Activate hotspot mode
  117. activate_hotspot_mode() {
  118. log "No known WiFi found. Activating hotspot mode..."
  119. # Make sure we're not connected to anything
  120. nmcli dev disconnect "$IFACE" 2>/dev/null || true
  121. # Activate the hotspot profile
  122. # NM's shared mode starts built-in dnsmasq for DHCP+DNS.
  123. # Our config in /etc/NetworkManager/dnsmasq-shared.d/ makes it
  124. # redirect all DNS queries to 10.42.0.1 for captive portal detection.
  125. if nmcli con up "$HOTSPOT_CON_NAME" 2>/dev/null; then
  126. log "Hotspot active. SSID: $(nmcli -t -f 802-11-wireless.ssid con show "$HOTSPOT_CON_NAME" 2>/dev/null | cut -d: -f2)"
  127. log "DNS redirect active via NM dnsmasq (captive portal mode)"
  128. echo "hotspot" > "$MODE_FILE"
  129. return 0
  130. else
  131. log "ERROR: Failed to activate hotspot"
  132. return 1
  133. fi
  134. }
  135. # Boot mode: scan with retry loop, then fallback to hotspot
  136. boot_mode() {
  137. log "Boot mode: scanning for known WiFi (${SCAN_TIMEOUT}s timeout)..."
  138. # Wait for NetworkManager
  139. if ! wait_for_nm; then
  140. log "NetworkManager not available, exiting"
  141. exit 1
  142. fi
  143. # Check if hotspot profile exists
  144. if ! nmcli con show "$HOTSPOT_CON_NAME" &>/dev/null; then
  145. log "Hotspot profile '$HOTSPOT_CON_NAME' not found. Run 'dw wifi setup' first."
  146. echo "client" > "$MODE_FILE"
  147. exit 0
  148. fi
  149. # Scan for known WiFi with timeout
  150. local elapsed=0
  151. while [ $elapsed -lt $SCAN_TIMEOUT ]; do
  152. local match
  153. if match=$(check_for_known_wifi); then
  154. activate_client_mode "$match"
  155. exit 0
  156. fi
  157. log "No known WiFi found yet, retrying... (${elapsed}s/${SCAN_TIMEOUT}s)"
  158. sleep $SCAN_INTERVAL
  159. elapsed=$((elapsed + SCAN_INTERVAL))
  160. done
  161. # No known WiFi found after timeout — go to hotspot mode
  162. activate_hotspot_mode
  163. }
  164. # Check mode: quick single-pass check for periodic timer
  165. check_mode() {
  166. # Check if hotspot profile exists
  167. if ! nmcli con show "$HOTSPOT_CON_NAME" &>/dev/null; then
  168. exit 0
  169. fi
  170. local state
  171. state=$(get_current_state)
  172. case "$state" in
  173. client)
  174. # Already connected — nothing to do
  175. ;;
  176. hotspot)
  177. # In hotspot mode — check if a known network has appeared
  178. local match
  179. if match=$(check_for_known_wifi); then
  180. log "Known WiFi found while in hotspot mode, switching to client..."
  181. activate_client_mode "$match"
  182. fi
  183. ;;
  184. disconnected)
  185. # WiFi dropped — try to reconnect or fall back to hotspot
  186. log "WiFi disconnected, attempting recovery..."
  187. local match
  188. if match=$(check_for_known_wifi); then
  189. activate_client_mode "$match"
  190. else
  191. activate_hotspot_mode
  192. fi
  193. ;;
  194. esac
  195. }
  196. # Main
  197. case "${1:-}" in
  198. --check)
  199. check_mode
  200. ;;
  201. *)
  202. boot_mode
  203. ;;
  204. esac