Files
fallback-ap/fallback-ap.sh
2026-10-26 15:38:30 -04:00

995 lines
25 KiB
Bash

#!/bin/bash
set -e
# Configuration =======================================================================================================
#
# Connection settings (SSID, passphrase, subnet, DHCP range, interface,
# monitor interfaces, check mode) can be supplied three ways, highest
# priority first:
# 1. Environment variables (FALLBACK_AP_*) - used for headless installs.
# 2. The persisted config file below, written by `install`.
# 3. The hard-coded defaults in this block.
#
# Everything else (timers, paths) is not meant to be reconfigured per-install
# and stays as plain constants.
CONFIG_FILE="/etc/fallback-ap.conf"
if [ -f "$CONFIG_FILE" ]; then source "$CONFIG_FILE"; fi
# The interface the AP is created on. The AP is NEVER created on any other
# interface, regardless of what is being monitored.
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 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 - (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 (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.
CHECK_TARGETS="${FALLBACK_AP_CHECK_TARGETS:-${SAVED_CHECK_TARGETS:-1.1.1.1 8.8.8.8}}"
AP_NAME="${FALLBACK_AP_SSID:-${SAVED_SSID:-$HOSTNAME}}"
AP_PASSWORD="${FALLBACK_AP_PASSWORD:-${SAVED_PASSWORD:-abcd1234}}"
AP_ENCRYPTION="${FALLBACK_AP_ENCRYPTION:-${SAVED_ENCRYPTION:-wpa-psk}}" # none | wpa-psk | sae
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}}"
# 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.
RECOVERY_CHECK_INTERVAL=60
# How long to leave the AP down during a recovery attempt before giving
# up and bringing the AP back.
RECOVERY_WINDOW=20
# Small delay after changing wlan0 state, to let things settle before
# we query it again.
STATE_SETTLE_DELAY=1
# Internet mode only: how often to run the ping checks, how many consecutive
# failed checks are needed before declaring "offline" (prevents the AP from
# flapping on a single dropped ping), and the per-ping timeout in seconds.
ONLINE_CHECK_INTERVAL=5
FAIL_THRESHOLD=3
PING_TIMEOUT=2
CONNECTION_NAME="Fallback AP"
SERVICE_NAME="fallback-ap"
SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME.service"
STATE_DIR="/var/lib/fallback-ap"
LAST_WIFI_FILE="$STATE_DIR/last-wifi"
LOCK_FILE="$STATE_DIR/lock"
STATE_FILE="$STATE_DIR/state"
# End of Configuration ================================================================================================
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")"
if [ -z "$SCRIPT_PATH" ] || [ ! -f "$SCRIPT_PATH" ]; then
echo "Unable to determine script path."
exit 1
fi
log() {
echo "[$(date '+%H:%M:%S')] $*"
}
set_state() {
printf '%s\n' "$1" > "$STATE_FILE"
}
get_state() {
cat "$STATE_FILE" 2>/dev/null || echo "unknown"
}
is_ap_on() {
nmcli -t -f NAME connection show --active 2>/dev/null |
grep -Fxq "$CONNECTION_NAME"
}
# --- Connectivity monitoring -----------------------------------------------------------------------------------------
# True if no monitor interface is configured (= monitor everything).
monitor_all() {
[ -z "$MONITOR_IFACE" ]
}
# True if the AP interface is among the monitored interfaces. If it is not,
# a "recovery attempt" (taking the AP down to let wlan0 reconnect to Wi-Fi)
# could never change the result, so it is skipped.
monitors_wlan0() {
if monitor_all; then
return 0
fi
case ",${MONITOR_IFACE}," in
*",${WLAN0_IFACE},"*) return 0 ;;
esac
return 1
}
# A "real" interface is backed by hardware. Linux exposes this as a `device`
# link under /sys/class/net/<iface>/, 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 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
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}'
)
}
# Can this specific interface reach the internet? Pings are forced out of the
# given interface (-I) so another interface cannot answer on its behalf.
iface_has_internet() {
local dev="$1"
local target
for target in $CHECK_TARGETS; do
if ping -I "$dev" -c 1 -W "$PING_TIMEOUT" "$target" >/dev/null 2>&1; then
return 0
fi
done
return 1
}
# Is ANY monitored interface online right now (according to CHECK_MODE)?
check_online() {
local dev
while read -r dev; do
if [ -z "$dev" ]; then
continue
fi
if [ "$CHECK_MODE" = "link" ]; then
return 0
fi
if iface_has_internet "$dev"; then
return 0
fi
done < <(get_monitored_devices)
return 1
}
# Debounced wrapper around check_online, used by the main loop. Updates the
# `online`, `fail_count` and `last_online_check` variables of run().
# link mode: checked every tick, no debounce (cheap).
# internet mode: checked every ONLINE_CHECK_INTERVAL seconds, and only
# declared offline after FAIL_THRESHOLD consecutive failures.
# Coming back online is detected on the first success.
update_online() {
local now="$1"
local interval=0
local threshold=1
if [ "$CHECK_MODE" = "internet" ]; then
interval="$ONLINE_CHECK_INTERVAL"
threshold="$FAIL_THRESHOLD"
fi
if (( now - last_online_check < interval )); then
return 0
fi
last_online_check="$now"
if check_online; then
fail_count=0
online=1
else
fail_count=$((fail_count + 1))
if (( fail_count >= threshold )); then
online=0
fi
fi
}
wait_for_online() {
local timeout="$1"
local i
for ((i=0; i<timeout; i++)); do
if check_online; then
return 0
fi
sleep 1
done
return 1
}
# --- Wi-Fi bookkeeping -----------------------------------------------------------------------------------------------
get_wifi_connection() {
nmcli -t -f DEVICE,TYPE,STATE,CONNECTION device status 2>/dev/null |
awk -F: -v iface="$WLAN0_IFACE" \
'$1 == iface && $2 == "wifi" && $3 ~ /^connected/ {print $4; exit}'
}
remember_wifi() {
local connection
connection="$(get_wifi_connection)"
[ -z "$connection" ] && return
[ "$connection" = "$CONNECTION_NAME" ] && return
mkdir -p "$STATE_DIR"
if [ "$(cat "$LAST_WIFI_FILE" 2>/dev/null || true)" != "$connection" ]; then
log "Remembering Wi-Fi connection: $connection"
printf '%s\n' "$connection" > "$LAST_WIFI_FILE"
fi
}
get_last_wifi() {
[ -f "$LAST_WIFI_FILE" ] && cat "$LAST_WIFI_FILE"
}
get_client_count() {
iw dev "$WLAN0_IFACE" station dump 2>/dev/null |
grep -c '^Station ' || true
}
wait_for_ap_state() {
local wanted="$1"
local timeout="${2:-5}"
for ((i=0; i<timeout*10; i++)); do
if [ "$wanted" = "on" ] && is_ap_on; then
return 0
fi
if [ "$wanted" = "off" ] && ! is_ap_on; then
return 0
fi
sleep 0.1
done
return 1
}
# --- AP control (always on $WLAN0_IFACE only) ------------------------------------------------------------------------
start_ap() {
if is_ap_on; then
return 0
fi
log "Enabling AP: $AP_NAME (on $WLAN0_IFACE)"
nmcli connection up "$CONNECTION_NAME" ifname "$WLAN0_IFACE" >/dev/null 2>&1 || {
log "Failed to start AP"
return 1
}
if ! wait_for_ap_state on 5; then
log "AP failed to become active"
return 1
fi
sleep "$STATE_SETTLE_DELAY"
log "AP enabled"
return 0
}
stop_ap() {
if ! is_ap_on; then
return 0
fi
log "Disabling AP"
nmcli connection down "$CONNECTION_NAME" >/dev/null 2>&1 || true
if ! wait_for_ap_state off 5; then
log "AP failed to stop cleanly"
return 1
fi
sleep "$STATE_SETTLE_DELAY"
return 0
}
# --- Main loop logic -------------------------------------------------------------------------------------------------
# Called once per second from run(). Relies on run()'s local variables
# (last_attempt, online, fail_count, last_online_check) via dynamic scoping.
tick() {
local now="$1"
update_online "$now"
if (( online )); then
remember_wifi
if is_ap_on; then
log "Connectivity available, shutting down fallback AP"
stop_ap || true
fi
set_state "online"
last_attempt="$now"
return
fi
if ! is_ap_on; then
log "No connectivity on monitored interface(s), starting fallback AP"
start_ap || true
set_state "fallback"
last_attempt="$now"
return
fi
set_state "fallback"
local clients
clients="$(get_client_count)"
if [ "$clients" -gt 0 ]; then
last_attempt="$now"
return
fi
# Recovery only makes sense if wlan0 is one of the monitored interfaces:
# it works by briefly freeing wlan0 so it can reconnect to known Wi-Fi.
if ! monitors_wlan0; then
return
fi
if (( now - last_attempt >= RECOVERY_CHECK_INTERVAL )); then
last_attempt="$now"
attempt_recovery || true
fi
}
attempt_recovery() {
log "No AP clients, checking for connectivity (AP down for up to ${RECOVERY_WINDOW}s)"
set_state "checking"
stop_ap || true
if is_ap_on; then
log "AP did not go down, aborting recovery attempt"
start_ap || true
set_state "fallback"
return 1
fi
if wait_for_online "$RECOVERY_WINDOW"; then
sleep "$STATE_SETTLE_DELAY"
if check_online; then
log "Connectivity restored"
remember_wifi
online=1
fail_count=0
set_state "online"
return 0
fi
fi
log "No connectivity available, restoring AP"
start_ap || true
set_state "fallback"
return 1
}
save_config() {
mkdir -p "$(dirname "$CONFIG_FILE")"
cat > "$CONFIG_FILE" <<EOF
SAVED_IFACE="$WLAN0_IFACE"
SAVED_MONITOR_IFACE="$MONITOR_IFACE"
SAVED_IGNORE_IFACE="$IGNORE_IFACE"
SAVED_CHECK_MODE="$CHECK_MODE"
SAVED_CHECK_TARGETS="$CHECK_TARGETS"
SAVED_SSID="$AP_NAME"
SAVED_PASSWORD="$AP_PASSWORD"
SAVED_ENCRYPTION="$AP_ENCRYPTION"
SAVED_CIDR="$FALLBACK_AP_CIDR"
SAVED_DHCP_START_OCTET="$FALLBACK_AP_DHCP_START_OCTET"
SAVED_DHCP_END_OCTET="$FALLBACK_AP_DHCP_END_OCTET"
EOF
chmod 600 "$CONFIG_FILE"
}
usage_install() {
cat <<EOF
Usage: $SCRIPT_PATH install [options]
Options (each overrides the matching FALLBACK_AP_* env var and any saved value):
--iface NAME Interface the AP is created on (default: wlan0)
--monitor-iface LIST Comma-separated interfaces to watch for connectivity.
Blank ("") monitors all REAL interfaces; virtual ones
(docker, veth, bridges, ...) are skipped (default: blank)
--ignore-iface LIST Comma-separated physical interfaces to ignore when
monitoring all; globs allowed, e.g. "enx*"
(default: none)
--check-mode MODE link | internet (default: link)
--check-targets "H.." Space-separated hosts to ping in internet mode
(default: "1.1.1.1 8.8.8.8")
--ssid NAME AP name (default: hostname)
--password PASS AP passphrase (default: abcd1234)
--encryption TYPE none | wpa-psk | sae (default: wpa-psk)
--cidr ADDR/MASK AP address and subnet (default: 10.10.10.1/24)
--dhcp-start OCTET First DHCP address, last octet (default: 100)
--dhcp-end OCTET Last DHCP address, last octet (default: 250)
-h, --help Show this help
Both "--opt value" and "--opt=value" are accepted. Precedence, highest first:
flags, FALLBACK_AP_* env vars, saved config file, built-in defaults.
Examples:
$SCRIPT_PATH install
$SCRIPT_PATH install --monitor-iface eth0,wwan0
$SCRIPT_PATH install --iface wlan1 --monitor-iface "" --check-mode internet
EOF
}
parse_install_args() {
local opt val
while [ $# -gt 0 ]; do
opt="$1"
case "$opt" in
-h|--help)
usage_install
exit 0
;;
--*=*)
val="${opt#*=}"
opt="${opt%%=*}"
shift
;;
--*)
if [ $# -lt 2 ]; then
echo "Option $opt requires a value."
exit 1
fi
val="$2"
shift 2
;;
*)
echo "Unknown argument: $opt"
usage_install
exit 1
;;
esac
case "$opt" in
--iface) WLAN0_IFACE="$val" ;;
--monitor-iface) MONITOR_IFACE="${val// /}" ;;
--ignore-iface) IGNORE_IFACE="${val// /}" ;;
--check-mode) CHECK_MODE="$val" ;;
--check-targets) CHECK_TARGETS="$val" ;;
--ssid) AP_NAME="$val" ;;
--password) AP_PASSWORD="$val" ;;
--encryption) AP_ENCRYPTION="$val" ;;
--cidr) FALLBACK_AP_CIDR="$val" ;;
--dhcp-start) FALLBACK_AP_DHCP_START_OCTET="$val" ;;
--dhcp-end) FALLBACK_AP_DHCP_END_OCTET="$val" ;;
*)
echo "Unknown option: $opt"
usage_install
exit 1
;;
esac
done
validate_check_mode
compute_derived
}
install() {
if [ "$EUID" -ne 0 ]; then
echo "Run this script as root."
exit 1
fi
parse_install_args "$@"
log "Installing NetworkManager fallback AP"
apt-get update
apt-get install -y network-manager iw iputils-ping
systemctl enable NetworkManager
systemctl start NetworkManager
mkdir -p "$STATE_DIR"
remember_wifi
create_connection
save_config
log "Creating recovery service"
cat > "$SERVICE_FILE" <<EOF
[Unit]
Description=NetworkManager Fallback AP
After=NetworkManager.service
Wants=NetworkManager.service
[Service]
Type=simple
ExecStart=$SCRIPT_PATH run
Restart=always
RestartSec=2
[Install]
WantedBy=multi-user.target
EOF
chmod 644 "$SERVICE_FILE"
systemctl daemon-reload
systemctl enable "$SERVICE_NAME"
systemctl restart "$SERVICE_NAME"
log "Installation complete"
log "Script: $SCRIPT_PATH"
log "Config file: $CONFIG_FILE"
log "SSID: $AP_NAME"
log "AP interface: $WLAN0_IFACE"
log "Monitored interfaces: ${MONITOR_IFACE:-all real interfaces}"
if monitor_all && [ -n "$IGNORE_IFACE" ]; then
log "Ignored interfaces: $IGNORE_IFACE"
fi
log "Check mode: $CHECK_MODE"
if [ "$CHECK_MODE" = "internet" ]; then
log "Ping targets: $CHECK_TARGETS"
fi
log "Network: $AP_CIDR"
log "DHCP: $DHCP_START - $DHCP_END"
log "Encryption: $AP_ENCRYPTION"
log "Recovery check interval: ${RECOVERY_CHECK_INTERVAL}s"
log "Recovery window: ${RECOVERY_WINDOW}s"
log "Last Wi-Fi: $(get_last_wifi || echo none)"
echo
nmcli device status
}
create_connection() {
log "Creating NetworkManager AP connection..."
nmcli connection delete "$CONNECTION_NAME" >/dev/null 2>&1 || true
nmcli connection add \
type wifi \
ifname "$WLAN0_IFACE" \
con-name "$CONNECTION_NAME" \
autoconnect no \
ssid "$AP_NAME"
nmcli connection modify "$CONNECTION_NAME" \
connection.autoconnect no \
connection.autoconnect-priority -999 \
connection.autoconnect-retries 0 \
802-11-wireless.mode ap \
802-11-wireless.powersave 2 \
ipv4.method shared \
ipv4.addresses "$AP_CIDR" \
ipv4.shared-dhcp-range "$DHCP_START,$DHCP_END" \
ipv6.method disabled
case "$AP_ENCRYPTION" in
none)
nmcli connection modify "$CONNECTION_NAME" \
wifi-sec.key-mgmt none
;;
wpa-psk)
if [ "${#AP_PASSWORD}" -lt 8 ] || [ "${#AP_PASSWORD}" -gt 63 ]; then
echo "WPA-PSK password must be between 8 and 63 characters."
exit 1
fi
nmcli connection modify "$CONNECTION_NAME" \
wifi-sec.key-mgmt wpa-psk \
wifi-sec.psk "$AP_PASSWORD"
;;
sae)
if [ "${#AP_PASSWORD}" -lt 8 ]; then
echo "WPA3-SAE password must be at least 8 characters."
exit 1
fi
nmcli connection modify "$CONNECTION_NAME" \
wifi-sec.key-mgmt sae \
wifi-sec.psk "$AP_PASSWORD"
;;
*)
echo "Unsupported encryption: $AP_ENCRYPTION"
exit 1
;;
esac
}
uninstall() {
if [ "$EUID" -ne 0 ]; then
echo "Run this script as root."
exit 1
fi
log "Uninstalling fallback AP"
systemctl disable --now "$SERVICE_NAME" >/dev/null 2>&1 || true
nmcli connection down "$CONNECTION_NAME" >/dev/null 2>&1 || true
nmcli connection delete "$CONNECTION_NAME" >/dev/null 2>&1 || true
rm -f "$SERVICE_FILE"
rm -f "$CONFIG_FILE"
rm -rf "$STATE_DIR"
systemctl daemon-reload
log "Fallback AP removed"
}
start() {
mkdir -p "$STATE_DIR"
(
flock -n 9 || exit 1
start_ap
) 9>"$LOCK_FILE"
}
stop() {
mkdir -p "$STATE_DIR"
(
flock -n 9 || exit 1
stop_ap
) 9>"$LOCK_FILE"
}
status() {
echo
echo "=== Service ==="
systemctl status "$SERVICE_NAME" --no-pager || true
echo
echo "=== Devices ==="
nmcli device status
echo
echo "=== Active Connections ==="
nmcli connection show --active
echo
echo "=== Saved Wi-Fi Profiles ==="
nmcli -f NAME,TYPE,AUTOCONNECT,AUTOCONNECT-PRIORITY connection show
echo
echo "=== Fallback AP ==="
nmcli connection show "$CONNECTION_NAME" 2>/dev/null || echo "Not installed"
echo
echo "=== Config ($CONFIG_FILE) ==="
echo "SSID: $AP_NAME"
echo "AP interface: $WLAN0_IFACE"
echo "Monitored interfaces: ${MONITOR_IFACE:-all real interfaces}"
if monitor_all && [ -n "$IGNORE_IFACE" ]; then
echo "Ignored interfaces: $IGNORE_IFACE"
fi
echo "Check mode: $CHECK_MODE"
if [ "$CHECK_MODE" = "internet" ]; then
echo "Ping targets: $CHECK_TARGETS"
fi
echo "Network: $AP_CIDR"
echo "DHCP: $DHCP_START - $DHCP_END"
echo
echo "=== Connectivity ($CHECK_MODE) ==="
local dev found=0
while read -r dev; do
if [ -z "$dev" ]; then
continue
fi
found=1
if [ "$CHECK_MODE" = "internet" ]; then
if iface_has_internet "$dev"; then
echo "$dev: internet OK"
else
echo "$dev: connected, NO internet"
fi
else
echo "$dev: connected"
fi
done < <(get_monitored_devices)
if [ "$found" -eq 0 ]; then
echo "No monitored interface is connected"
fi
echo
echo "=== Connected but NOT counted (virtual / ignored / unmonitored) ==="
local monitored
monitored="$(get_monitored_devices)"
nmcli -t -f DEVICE,STATE,CONNECTION device status 2>/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"
echo
echo "=== State ==="
get_state
echo
echo "=== AP Clients ==="
get_client_count
echo
echo "=== Script ==="
echo "$SCRIPT_PATH"
}
run() {
# These are shared with tick/update_online/attempt_recovery through
# dynamic scoping.
local last_attempt=0
local last_online_check=0
local fail_count=0
local online=0
local now
mkdir -p "$STATE_DIR"
log "Fallback AP service started"
log "Script: $SCRIPT_PATH"
log "AP interface: $WLAN0_IFACE | monitoring: ${MONITOR_IFACE:-all real interfaces} | mode: $CHECK_MODE"
exec 9>"$LOCK_FILE"
flock 9
if check_online; then
online=1
remember_wifi
set_state "online"
log "Connectivity already available"
else
log "No connectivity on monitored interface(s), starting fallback AP"
start_ap || true
set_state "fallback"
fi
last_online_check="$(date +%s)"
flock -u 9
while true; do
now="$(date +%s)"
if flock -n 9; then
tick "$now"
flock -u 9
fi
sleep 1
done
}
if [ "$EUID" -ne 0 ]; then
echo "Run this script as root."
exit 1
fi
case "${1:-}" in
install)
shift
install "$@"
;;
uninstall)
uninstall
;;
start)
start
;;
stop)
stop
;;
status)
status
;;
run)
run
;;
*)
echo "Usage: $SCRIPT_PATH {install [options]|uninstall|start|stop|status|run}"
echo " $SCRIPT_PATH install --help for install options"
exit 1
;;
esac