From d3e1ab52a9737c6c2b933f2ee1ba766d1022b806 Mon Sep 17 00:00:00 2001 From: Zakary Timson Date: Mon, 26 Oct 2026 15:38:30 -0400 Subject: [PATCH] Update fallback-ap.sh --- fallback-ap.sh | 257 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 227 insertions(+), 30 deletions(-) diff --git a/fallback-ap.sh b/fallback-ap.sh index c8ab983..05fcf61 100644 --- a/fallback-ap.sh +++ b/fallback-ap.sh @@ -23,16 +23,28 @@ WLAN0_IFACE="${FALLBACK_AP_IFACE:-${SAVED_IFACE:-wlan0}}" # Which interfaces to watch for connectivity. If any of them is online, the # AP stays off. Comma-separated list (e.g. "eth0,wlan1,wwan0"). -# Leave BLANK to monitor ALL interfaces (ethernet, wifi, gsm, cdma, bond, vlan). +# Leave BLANK to monitor all REAL interfaces: physical ethernet, wifi and +# cellular modems. Virtual interfaces (docker0, br-*, veth*, virbr*, tun/tap, +# wireguard, vlan, bridges, bonds, ...) are detected automatically and never +# count, so Docker/LXC/VM networking cannot keep the AP off. # Note the missing colon in the expansion: an explicitly empty # FALLBACK_AP_MONITOR_IFACE="" overrides a value saved in the config file. MONITOR_IFACE="${FALLBACK_AP_MONITOR_IFACE-${SAVED_MONITOR_IFACE-}}" MONITOR_IFACE="${MONITOR_IFACE// /}" +# Extra interfaces to ignore when monitoring all (comma-separated, globs +# allowed, e.g. "enx*,usb0"). Only needed for physical NICs you do not want +# to count; virtual interfaces are already excluded. Has no effect when +# MONITOR_IFACE names interfaces explicitly. +IGNORE_IFACE="${FALLBACK_AP_IGNORE_IFACE-${SAVED_IGNORE_IFACE-}}" +IGNORE_IFACE="${IGNORE_IFACE// /}" + # What "online" means: -# link - a monitored interface is connected (fast, never flaps) +# link - (default) a monitored interface is connected. An outage here is +# local and fixable; an upstream outage is not our problem, so +# the AP stays off. Fast and never flaps. # internet - a monitored interface is connected AND can ping a target -# out of that specific interface (detects dead uplinks) +# out of that specific interface (also detects dead uplinks) CHECK_MODE="${FALLBACK_AP_CHECK_MODE:-${SAVED_CHECK_MODE:-link}}" # Hosts pinged (space-separated, any one answering counts) in internet mode. @@ -46,11 +58,18 @@ FALLBACK_AP_CIDR="${FALLBACK_AP_CIDR:-${SAVED_CIDR:-10.10.10.1/24}}" FALLBACK_AP_DHCP_START_OCTET="${FALLBACK_AP_DHCP_START_OCTET:-${SAVED_DHCP_START_OCTET:-100}}" FALLBACK_AP_DHCP_END_OCTET="${FALLBACK_AP_DHCP_END_OCTET:-${SAVED_DHCP_END_OCTET:-250}}" -AP_CIDR="$FALLBACK_AP_CIDR" -AP_NETWORK_BASE="${AP_CIDR%%/*}" # e.g. "10.10.10.1" out of "10.10.10.1/24" -AP_NETWORK_BASE="${AP_NETWORK_BASE%.*}" # e.g. "10.10.10" -DHCP_START="${AP_NETWORK_BASE}.${FALLBACK_AP_DHCP_START_OCTET}" -DHCP_END="${AP_NETWORK_BASE}.${FALLBACK_AP_DHCP_END_OCTET}" +# Values derived from the CIDR / DHCP settings. A function, because the +# `install` command-line flags can change those settings and need to +# recompute these. +compute_derived() { + AP_CIDR="$FALLBACK_AP_CIDR" + AP_NETWORK_BASE="${AP_CIDR%%/*}" # e.g. "10.10.10.1" out of "10.10.10.1/24" + AP_NETWORK_BASE="${AP_NETWORK_BASE%.*}" # e.g. "10.10.10" + DHCP_START="${AP_NETWORK_BASE}.${FALLBACK_AP_DHCP_START_OCTET}" + DHCP_END="${AP_NETWORK_BASE}.${FALLBACK_AP_DHCP_END_OCTET}" +} + +compute_derived # How often, while the AP has no clients, to stop the AP and give # NetworkManager a chance to reconnect to a known Wi-Fi network. @@ -83,13 +102,17 @@ STATE_FILE="$STATE_DIR/state" # End of Configuration ================================================================================================ -case "$CHECK_MODE" in - link|internet) ;; - *) - echo "Invalid FALLBACK_AP_CHECK_MODE '$CHECK_MODE' (use: link | internet)" - exit 1 - ;; -esac +validate_check_mode() { + case "$CHECK_MODE" in + link|internet) ;; + *) + echo "Invalid check mode '$CHECK_MODE' (use: link | internet)" + exit 1 + ;; + esac +} + +validate_check_mode # Resolve the actual location of this script. SCRIPT_PATH="$(readlink -f "$0")" @@ -145,28 +168,88 @@ monitors_wlan0() { } +# A "real" interface is backed by hardware. Linux exposes this as a `device` +# link under /sys/class/net//, which physical NICs (PCI, USB, SDIO, +# virtio, ...) have and virtual ones (docker0, veth*, br-*, tun*, wg*, vlan, +# bridges, bonds, dummy) do not. Cellular modems are the exception: NetworkManager +# names them after their control port (e.g. cdc-wdm0), which is not in +# /sys/class/net, so gsm/cdma types are accepted as-is. +is_real_interface() { + local dev="$1" + local type="$2" + + case "$type" in + gsm|cdma) return 0 ;; + esac + + [ -e "/sys/class/net/$dev/device" ] +} + + +# True if the device matches an entry of IGNORE_IFACE (globs allowed). +is_ignored_interface() { + local dev="$1" + local pattern + local -a patterns=() + + IFS=',' read -r -a patterns <<< "$IGNORE_IFACE" || true + + for pattern in "${patterns[@]}"; do + if [ -n "$pattern" ] && [[ "$dev" == $pattern ]]; then + return 0 + fi + done + + return 1 +} + + # Prints (one per line) the monitored devices that NetworkManager reports as # connected, excluding the fallback AP connection itself. -# - Blank MONITOR_IFACE: every device of a real network type. +# - Blank MONITOR_IFACE: every real (hardware-backed) ethernet, wifi or +# cellular device that is not in IGNORE_IFACE. # - Otherwise: only the devices named in MONITOR_IFACE (any type). # The state is matched with a prefix because nmcli reports values such as # "connected (externally)" or "connected (site only)". get_monitored_devices() { + local dev type local all=0 if monitor_all; then all=1 fi - nmcli -t -f DEVICE,TYPE,STATE,CONNECTION device status 2>/dev/null | - awk -F: -v ap="$CONNECTION_NAME" -v list=",${MONITOR_IFACE}," -v all="$all" ' - $3 ~ /^connected/ && $4 != ap { - if (all) { - if ($2 ~ /^(ethernet|wifi|gsm|cdma|bond|vlan)$/) print $1 - } else if (index(list, "," $1 ",")) { - print $1 - } - }' + while IFS=: read -r dev type; do + if [ -z "$dev" ]; then + continue + fi + + if [ "$all" -eq 1 ]; then + case "$type" in + ethernet|wifi|gsm|cdma) ;; + *) continue ;; + esac + + if ! is_real_interface "$dev" "$type"; then + continue + fi + + if is_ignored_interface "$dev"; then + continue + fi + else + case ",${MONITOR_IFACE}," in + *",${dev},"*) ;; + *) continue ;; + esac + fi + + echo "$dev" + done < <( + nmcli -t -f DEVICE,TYPE,STATE,CONNECTION device status 2>/dev/null | + awk -F: -v ap="$CONNECTION_NAME" \ + '$3 ~ /^connected/ && $4 != ap {print $1 ":" $2}' + ) } @@ -456,6 +539,7 @@ save_config() { cat > "$CONFIG_FILE" </dev/null | + awk -F: -v ap="$CONNECTION_NAME" \ + '$2 ~ /^connected/ && $3 != ap {print $1}' | + while read -r dev; do + if ! grep -Fxq "$dev" <<< "$monitored"; then + echo "$dev" + fi + done + echo echo "=== Last Known Wi-Fi ===" get_last_wifi || echo "None" @@ -727,7 +922,7 @@ run() { log "Fallback AP service started" log "Script: $SCRIPT_PATH" - log "AP interface: $WLAN0_IFACE | monitoring: ${MONITOR_IFACE:-all} | mode: $CHECK_MODE" + log "AP interface: $WLAN0_IFACE | monitoring: ${MONITOR_IFACE:-all real interfaces} | mode: $CHECK_MODE" exec 9>"$LOCK_FILE" flock 9 @@ -768,7 +963,8 @@ fi case "${1:-}" in install) - install + shift + install "$@" ;; uninstall) @@ -792,7 +988,8 @@ case "${1:-}" in ;; *) - echo "Usage: $SCRIPT_PATH {install|uninstall|start|stop|status|run}" + echo "Usage: $SCRIPT_PATH {install [options]|uninstall|start|stop|status|run}" + echo " $SCRIPT_PATH install --help for install options" exit 1 ;; esac \ No newline at end of file