generated from ztimson/template
Update fallback-ap.sh
This commit is contained in:
+262
-45
@@ -4,8 +4,9 @@ set -e
|
||||
|
||||
# Configuration =======================================================================================================
|
||||
#
|
||||
# Connection settings (SSID, passphrase, subnet, DHCP range, interface) can
|
||||
# be supplied three ways, highest priority first:
|
||||
# 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.
|
||||
@@ -16,8 +17,27 @@ set -e
|
||||
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 interfaces (ethernet, wifi, gsm, cdma, bond, vlan).
|
||||
# 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// /}"
|
||||
|
||||
# What "online" means:
|
||||
# link - a monitored interface is connected (fast, never flaps)
|
||||
# internet - a monitored interface is connected AND can ping a target
|
||||
# out of that specific interface (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
|
||||
@@ -44,6 +64,13 @@ RECOVERY_WINDOW=20
|
||||
# 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"
|
||||
@@ -56,6 +83,14 @@ 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
|
||||
|
||||
# Resolve the actual location of this script.
|
||||
SCRIPT_PATH="$(readlink -f "$0")"
|
||||
|
||||
@@ -86,17 +121,150 @@ is_ap_on() {
|
||||
}
|
||||
|
||||
|
||||
is_wifi_connected() {
|
||||
nmcli -t -f DEVICE,TYPE,STATE,CONNECTION device status 2>/dev/null |
|
||||
awk -F: -v iface="$WLAN0_IFACE" -v ap="$CONNECTION_NAME" \
|
||||
'$1 == iface && $2 == "wifi" && $3 == "connected" && $4 != ap {found=1} END {exit !found}'
|
||||
# --- 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
|
||||
}
|
||||
|
||||
|
||||
# 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.
|
||||
# - 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 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
|
||||
}
|
||||
}'
|
||||
}
|
||||
|
||||
|
||||
# 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}'
|
||||
'$1 == iface && $2 == "wifi" && $3 ~ /^connected/ {print $4; exit}'
|
||||
}
|
||||
|
||||
|
||||
@@ -148,27 +316,14 @@ wait_for_ap_state() {
|
||||
}
|
||||
|
||||
|
||||
wait_for_wifi() {
|
||||
local timeout="$1"
|
||||
|
||||
for ((i=0; i<timeout*10; i++)); do
|
||||
if is_wifi_connected; 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"
|
||||
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"
|
||||
@@ -207,23 +362,32 @@ stop_ap() {
|
||||
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"
|
||||
|
||||
if is_wifi_connected; then
|
||||
update_online "$now"
|
||||
|
||||
if (( online )); then
|
||||
remember_wifi
|
||||
|
||||
if is_ap_on; then
|
||||
stop_ap
|
||||
log "Connectivity available, shutting down fallback AP"
|
||||
stop_ap || true
|
||||
fi
|
||||
|
||||
set_state "wifi"
|
||||
set_state "online"
|
||||
last_attempt="$now"
|
||||
return
|
||||
fi
|
||||
|
||||
if ! is_ap_on; then
|
||||
start_ap
|
||||
log "No connectivity on monitored interface(s), starting fallback AP"
|
||||
start_ap || true
|
||||
set_state "fallback"
|
||||
last_attempt="$now"
|
||||
return
|
||||
@@ -239,14 +403,20 @@ tick() {
|
||||
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
|
||||
attempt_recovery || true
|
||||
fi
|
||||
}
|
||||
|
||||
attempt_recovery() {
|
||||
log "No AP clients, checking for known Wi-Fi (AP down for up to ${RECOVERY_WINDOW}s)"
|
||||
log "No AP clients, checking for connectivity (AP down for up to ${RECOVERY_WINDOW}s)"
|
||||
|
||||
set_state "checking"
|
||||
|
||||
@@ -254,25 +424,27 @@ attempt_recovery() {
|
||||
|
||||
if is_ap_on; then
|
||||
log "AP did not go down, aborting recovery attempt"
|
||||
start_ap
|
||||
start_ap || true
|
||||
set_state "fallback"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if wait_for_wifi "$RECOVERY_WINDOW"; then
|
||||
if wait_for_online "$RECOVERY_WINDOW"; then
|
||||
sleep "$STATE_SETTLE_DELAY"
|
||||
|
||||
if is_wifi_connected; then
|
||||
log "Wi-Fi reconnected"
|
||||
if check_online; then
|
||||
log "Connectivity restored"
|
||||
remember_wifi
|
||||
set_state "wifi"
|
||||
online=1
|
||||
fail_count=0
|
||||
set_state "online"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
log "No Wi-Fi available, restoring AP"
|
||||
log "No connectivity available, restoring AP"
|
||||
|
||||
start_ap
|
||||
start_ap || true
|
||||
set_state "fallback"
|
||||
|
||||
return 1
|
||||
@@ -283,6 +455,9 @@ save_config() {
|
||||
mkdir -p "$(dirname "$CONFIG_FILE")"
|
||||
cat > "$CONFIG_FILE" <<EOF
|
||||
SAVED_IFACE="$WLAN0_IFACE"
|
||||
SAVED_MONITOR_IFACE="$MONITOR_IFACE"
|
||||
SAVED_CHECK_MODE="$CHECK_MODE"
|
||||
SAVED_CHECK_TARGETS="$CHECK_TARGETS"
|
||||
SAVED_SSID="$AP_NAME"
|
||||
SAVED_PASSWORD="$AP_PASSWORD"
|
||||
SAVED_ENCRYPTION="$AP_ENCRYPTION"
|
||||
@@ -303,16 +478,14 @@ install() {
|
||||
log "Installing NetworkManager fallback AP"
|
||||
|
||||
apt-get update
|
||||
apt-get install -y network-manager iw
|
||||
apt-get install -y network-manager iw iputils-ping
|
||||
|
||||
systemctl enable NetworkManager
|
||||
systemctl start NetworkManager
|
||||
|
||||
mkdir -p "$STATE_DIR"
|
||||
|
||||
if is_wifi_connected; then
|
||||
remember_wifi
|
||||
fi
|
||||
|
||||
create_connection
|
||||
save_config
|
||||
@@ -345,7 +518,12 @@ EOF
|
||||
log "Script: $SCRIPT_PATH"
|
||||
log "Config file: $CONFIG_FILE"
|
||||
log "SSID: $AP_NAME"
|
||||
log "Interface: $WLAN0_IFACE"
|
||||
log "AP interface: $WLAN0_IFACE"
|
||||
log "Monitored interfaces: ${MONITOR_IFACE:-all}"
|
||||
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"
|
||||
@@ -484,10 +662,40 @@ status() {
|
||||
echo
|
||||
echo "=== Config ($CONFIG_FILE) ==="
|
||||
echo "SSID: $AP_NAME"
|
||||
echo "Interface: $WLAN0_IFACE"
|
||||
echo "AP interface: $WLAN0_IFACE"
|
||||
echo "Monitored interfaces: ${MONITOR_IFACE:-all}"
|
||||
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 "=== Last Known Wi-Fi ==="
|
||||
get_last_wifi || echo "None"
|
||||
@@ -507,27 +715,36 @@ status() {
|
||||
|
||||
|
||||
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} | mode: $CHECK_MODE"
|
||||
exec 9>"$LOCK_FILE"
|
||||
|
||||
flock 9
|
||||
|
||||
if is_wifi_connected; then
|
||||
if check_online; then
|
||||
online=1
|
||||
remember_wifi
|
||||
set_state "wifi"
|
||||
log "Wi-Fi already connected"
|
||||
set_state "online"
|
||||
log "Connectivity already available"
|
||||
else
|
||||
log "No Wi-Fi connection, starting fallback AP"
|
||||
start_ap
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user