Ztimson patch 16 #16

Merged
ztimson merged 3 commits from ztimson-patch-16 into master 2026-10-26 15:43:18 -04:00
Showing only changes of commit 14b08ed70b - Show all commits
+263 -46
View File
@@ -4,8 +4,9 @@ set -e
# Configuration ======================================================================================================= # Configuration =======================================================================================================
# #
# Connection settings (SSID, passphrase, subnet, DHCP range, interface) can # Connection settings (SSID, passphrase, subnet, DHCP range, interface,
# be supplied three ways, highest priority first: # monitor interfaces, check mode) can be supplied three ways, highest
# priority first:
# 1. Environment variables (FALLBACK_AP_*) - used for headless installs. # 1. Environment variables (FALLBACK_AP_*) - used for headless installs.
# 2. The persisted config file below, written by `install`. # 2. The persisted config file below, written by `install`.
# 3. The hard-coded defaults in this block. # 3. The hard-coded defaults in this block.
@@ -16,8 +17,27 @@ set -e
CONFIG_FILE="/etc/fallback-ap.conf" CONFIG_FILE="/etc/fallback-ap.conf"
if [ -f "$CONFIG_FILE" ]; then source "$CONFIG_FILE"; fi 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}}" 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_NAME="${FALLBACK_AP_SSID:-${SAVED_SSID:-$HOSTNAME}}"
AP_PASSWORD="${FALLBACK_AP_PASSWORD:-${SAVED_PASSWORD:-abcd1234}}" AP_PASSWORD="${FALLBACK_AP_PASSWORD:-${SAVED_PASSWORD:-abcd1234}}"
AP_ENCRYPTION="${FALLBACK_AP_ENCRYPTION:-${SAVED_ENCRYPTION:-wpa-psk}}" # none | wpa-psk | sae AP_ENCRYPTION="${FALLBACK_AP_ENCRYPTION:-${SAVED_ENCRYPTION:-wpa-psk}}" # none | wpa-psk | sae
@@ -44,6 +64,13 @@ RECOVERY_WINDOW=20
# we query it again. # we query it again.
STATE_SETTLE_DELAY=1 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" CONNECTION_NAME="Fallback AP"
SERVICE_NAME="fallback-ap" SERVICE_NAME="fallback-ap"
SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME.service" SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME.service"
@@ -56,6 +83,14 @@ STATE_FILE="$STATE_DIR/state"
# End of Configuration ================================================================================================ # 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. # Resolve the actual location of this script.
SCRIPT_PATH="$(readlink -f "$0")" SCRIPT_PATH="$(readlink -f "$0")"
@@ -86,17 +121,150 @@ is_ap_on() {
} }
is_wifi_connected() { # --- Connectivity monitoring -----------------------------------------------------------------------------------------
nmcli -t -f DEVICE,TYPE,STATE,CONNECTION device status 2>/dev/null |
awk -F: -v iface="$WLAN0_IFACE" -v ap="$CONNECTION_NAME" \ # True if no monitor interface is configured (= monitor everything).
'$1 == iface && $2 == "wifi" && $3 == "connected" && $4 != ap {found=1} END {exit !found}' 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() { get_wifi_connection() {
nmcli -t -f DEVICE,TYPE,STATE,CONNECTION device status 2>/dev/null | nmcli -t -f DEVICE,TYPE,STATE,CONNECTION device status 2>/dev/null |
awk -F: -v iface="$WLAN0_IFACE" \ 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() { # --- AP control (always on $WLAN0_IFACE only) ------------------------------------------------------------------------
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
}
start_ap() { start_ap() {
if is_ap_on; then if is_ap_on; then
return 0 return 0
fi 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 || { nmcli connection up "$CONNECTION_NAME" ifname "$WLAN0_IFACE" >/dev/null 2>&1 || {
log "Failed to start AP" log "Failed to start AP"
@@ -207,23 +362,32 @@ stop_ap() {
return 0 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() { tick() {
local now="$1" local now="$1"
if is_wifi_connected; then update_online "$now"
if (( online )); then
remember_wifi remember_wifi
if is_ap_on; then if is_ap_on; then
stop_ap log "Connectivity available, shutting down fallback AP"
stop_ap || true
fi fi
set_state "wifi" set_state "online"
last_attempt="$now" last_attempt="$now"
return return
fi fi
if ! is_ap_on; then if ! is_ap_on; then
start_ap log "No connectivity on monitored interface(s), starting fallback AP"
start_ap || true
set_state "fallback" set_state "fallback"
last_attempt="$now" last_attempt="$now"
return return
@@ -239,14 +403,20 @@ tick() {
return return
fi 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 if (( now - last_attempt >= RECOVERY_CHECK_INTERVAL )); then
last_attempt="$now" last_attempt="$now"
attempt_recovery attempt_recovery || true
fi fi
} }
attempt_recovery() { 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" set_state "checking"
@@ -254,25 +424,27 @@ attempt_recovery() {
if is_ap_on; then if is_ap_on; then
log "AP did not go down, aborting recovery attempt" log "AP did not go down, aborting recovery attempt"
start_ap start_ap || true
set_state "fallback" set_state "fallback"
return 1 return 1
fi fi
if wait_for_wifi "$RECOVERY_WINDOW"; then if wait_for_online "$RECOVERY_WINDOW"; then
sleep "$STATE_SETTLE_DELAY" sleep "$STATE_SETTLE_DELAY"
if is_wifi_connected; then if check_online; then
log "Wi-Fi reconnected" log "Connectivity restored"
remember_wifi remember_wifi
set_state "wifi" online=1
fail_count=0
set_state "online"
return 0 return 0
fi fi
fi fi
log "No Wi-Fi available, restoring AP" log "No connectivity available, restoring AP"
start_ap start_ap || true
set_state "fallback" set_state "fallback"
return 1 return 1
@@ -283,6 +455,9 @@ save_config() {
mkdir -p "$(dirname "$CONFIG_FILE")" mkdir -p "$(dirname "$CONFIG_FILE")"
cat > "$CONFIG_FILE" <<EOF cat > "$CONFIG_FILE" <<EOF
SAVED_IFACE="$WLAN0_IFACE" SAVED_IFACE="$WLAN0_IFACE"
SAVED_MONITOR_IFACE="$MONITOR_IFACE"
SAVED_CHECK_MODE="$CHECK_MODE"
SAVED_CHECK_TARGETS="$CHECK_TARGETS"
SAVED_SSID="$AP_NAME" SAVED_SSID="$AP_NAME"
SAVED_PASSWORD="$AP_PASSWORD" SAVED_PASSWORD="$AP_PASSWORD"
SAVED_ENCRYPTION="$AP_ENCRYPTION" SAVED_ENCRYPTION="$AP_ENCRYPTION"
@@ -303,16 +478,14 @@ install() {
log "Installing NetworkManager fallback AP" log "Installing NetworkManager fallback AP"
apt-get update apt-get update
apt-get install -y network-manager iw apt-get install -y network-manager iw iputils-ping
systemctl enable NetworkManager systemctl enable NetworkManager
systemctl start NetworkManager systemctl start NetworkManager
mkdir -p "$STATE_DIR" mkdir -p "$STATE_DIR"
if is_wifi_connected; then remember_wifi
remember_wifi
fi
create_connection create_connection
save_config save_config
@@ -345,7 +518,12 @@ EOF
log "Script: $SCRIPT_PATH" log "Script: $SCRIPT_PATH"
log "Config file: $CONFIG_FILE" log "Config file: $CONFIG_FILE"
log "SSID: $AP_NAME" 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 "Network: $AP_CIDR"
log "DHCP: $DHCP_START - $DHCP_END" log "DHCP: $DHCP_START - $DHCP_END"
log "Encryption: $AP_ENCRYPTION" log "Encryption: $AP_ENCRYPTION"
@@ -484,10 +662,40 @@ status() {
echo echo
echo "=== Config ($CONFIG_FILE) ===" echo "=== Config ($CONFIG_FILE) ==="
echo "SSID: $AP_NAME" 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 "Network: $AP_CIDR"
echo "DHCP: $DHCP_START - $DHCP_END" 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
echo "=== Last Known Wi-Fi ===" echo "=== Last Known Wi-Fi ==="
get_last_wifi || echo "None" get_last_wifi || echo "None"
@@ -507,27 +715,36 @@ status() {
run() { run() {
# These are shared with tick/update_online/attempt_recovery through
# dynamic scoping.
local last_attempt=0 local last_attempt=0
local last_online_check=0
local fail_count=0
local online=0
local now local now
mkdir -p "$STATE_DIR" mkdir -p "$STATE_DIR"
log "Fallback AP service started" log "Fallback AP service started"
log "Script: $SCRIPT_PATH" log "Script: $SCRIPT_PATH"
log "AP interface: $WLAN0_IFACE | monitoring: ${MONITOR_IFACE:-all} | mode: $CHECK_MODE"
exec 9>"$LOCK_FILE" exec 9>"$LOCK_FILE"
flock 9 flock 9
if is_wifi_connected; then if check_online; then
online=1
remember_wifi remember_wifi
set_state "wifi" set_state "online"
log "Wi-Fi already connected" log "Connectivity already available"
else else
log "No Wi-Fi connection, starting fallback AP" log "No connectivity on monitored interface(s), starting fallback AP"
start_ap start_ap || true
set_state "fallback" set_state "fallback"
fi fi
last_online_check="$(date +%s)"
flock -u 9 flock -u 9
while true; do while true; do