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 d3e1ab52a9 - Show all commits
+215 -18
View File
@@ -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 # Which interfaces to watch for connectivity. If any of them is online, the
# AP stays off. Comma-separated list (e.g. "eth0,wlan1,wwan0"). # 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 # Note the missing colon in the expansion: an explicitly empty
# FALLBACK_AP_MONITOR_IFACE="" overrides a value saved in the config file. # FALLBACK_AP_MONITOR_IFACE="" overrides a value saved in the config file.
MONITOR_IFACE="${FALLBACK_AP_MONITOR_IFACE-${SAVED_MONITOR_IFACE-}}" MONITOR_IFACE="${FALLBACK_AP_MONITOR_IFACE-${SAVED_MONITOR_IFACE-}}"
MONITOR_IFACE="${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: # 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 # 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}}" CHECK_MODE="${FALLBACK_AP_CHECK_MODE:-${SAVED_CHECK_MODE:-link}}"
# Hosts pinged (space-separated, any one answering counts) in internet mode. # 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_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}}" 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_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_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" AP_NETWORK_BASE="${AP_NETWORK_BASE%.*}" # e.g. "10.10.10"
DHCP_START="${AP_NETWORK_BASE}.${FALLBACK_AP_DHCP_START_OCTET}" DHCP_START="${AP_NETWORK_BASE}.${FALLBACK_AP_DHCP_START_OCTET}"
DHCP_END="${AP_NETWORK_BASE}.${FALLBACK_AP_DHCP_END_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 # 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. # NetworkManager a chance to reconnect to a known Wi-Fi network.
@@ -83,13 +102,17 @@ STATE_FILE="$STATE_DIR/state"
# End of Configuration ================================================================================================ # End of Configuration ================================================================================================
validate_check_mode() {
case "$CHECK_MODE" in case "$CHECK_MODE" in
link|internet) ;; link|internet) ;;
*) *)
echo "Invalid FALLBACK_AP_CHECK_MODE '$CHECK_MODE' (use: link | internet)" echo "Invalid check mode '$CHECK_MODE' (use: link | internet)"
exit 1 exit 1
;; ;;
esac esac
}
validate_check_mode
# Resolve the actual location of this script. # Resolve the actual location of this script.
SCRIPT_PATH="$(readlink -f "$0")" 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/<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 # Prints (one per line) the monitored devices that NetworkManager reports as
# connected, excluding the fallback AP connection itself. # 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). # - Otherwise: only the devices named in MONITOR_IFACE (any type).
# The state is matched with a prefix because nmcli reports values such as # The state is matched with a prefix because nmcli reports values such as
# "connected (externally)" or "connected (site only)". # "connected (externally)" or "connected (site only)".
get_monitored_devices() { get_monitored_devices() {
local dev type
local all=0 local all=0
if monitor_all; then if monitor_all; then
all=1 all=1
fi 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 | 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" ' awk -F: -v ap="$CONNECTION_NAME" \
$3 ~ /^connected/ && $4 != ap { '$3 ~ /^connected/ && $4 != ap {print $1 ":" $2}'
if (all) { )
if ($2 ~ /^(ethernet|wifi|gsm|cdma|bond|vlan)$/) print $1
} else if (index(list, "," $1 ",")) {
print $1
}
}'
} }
@@ -456,6 +539,7 @@ save_config() {
cat > "$CONFIG_FILE" <<EOF cat > "$CONFIG_FILE" <<EOF
SAVED_IFACE="$WLAN0_IFACE" SAVED_IFACE="$WLAN0_IFACE"
SAVED_MONITOR_IFACE="$MONITOR_IFACE" SAVED_MONITOR_IFACE="$MONITOR_IFACE"
SAVED_IGNORE_IFACE="$IGNORE_IFACE"
SAVED_CHECK_MODE="$CHECK_MODE" SAVED_CHECK_MODE="$CHECK_MODE"
SAVED_CHECK_TARGETS="$CHECK_TARGETS" SAVED_CHECK_TARGETS="$CHECK_TARGETS"
SAVED_SSID="$AP_NAME" SAVED_SSID="$AP_NAME"
@@ -469,12 +553,104 @@ EOF
} }
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() { install() {
if [ "$EUID" -ne 0 ]; then if [ "$EUID" -ne 0 ]; then
echo "Run this script as root." echo "Run this script as root."
exit 1 exit 1
fi fi
parse_install_args "$@"
log "Installing NetworkManager fallback AP" log "Installing NetworkManager fallback AP"
apt-get update apt-get update
@@ -519,7 +695,10 @@ EOF
log "Config file: $CONFIG_FILE" log "Config file: $CONFIG_FILE"
log "SSID: $AP_NAME" log "SSID: $AP_NAME"
log "AP interface: $WLAN0_IFACE" log "AP interface: $WLAN0_IFACE"
log "Monitored interfaces: ${MONITOR_IFACE:-all}" 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" log "Check mode: $CHECK_MODE"
if [ "$CHECK_MODE" = "internet" ]; then if [ "$CHECK_MODE" = "internet" ]; then
log "Ping targets: $CHECK_TARGETS" log "Ping targets: $CHECK_TARGETS"
@@ -663,7 +842,10 @@ status() {
echo "=== Config ($CONFIG_FILE) ===" echo "=== Config ($CONFIG_FILE) ==="
echo "SSID: $AP_NAME" echo "SSID: $AP_NAME"
echo "AP interface: $WLAN0_IFACE" echo "AP interface: $WLAN0_IFACE"
echo "Monitored interfaces: ${MONITOR_IFACE:-all}" 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" echo "Check mode: $CHECK_MODE"
if [ "$CHECK_MODE" = "internet" ]; then if [ "$CHECK_MODE" = "internet" ]; then
echo "Ping targets: $CHECK_TARGETS" echo "Ping targets: $CHECK_TARGETS"
@@ -696,6 +878,19 @@ status() {
echo "No monitored interface is connected" echo "No monitored interface is connected"
fi 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
echo "=== Last Known Wi-Fi ===" echo "=== Last Known Wi-Fi ==="
get_last_wifi || echo "None" get_last_wifi || echo "None"
@@ -727,7 +922,7 @@ run() {
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" log "AP interface: $WLAN0_IFACE | monitoring: ${MONITOR_IFACE:-all real interfaces} | mode: $CHECK_MODE"
exec 9>"$LOCK_FILE" exec 9>"$LOCK_FILE"
flock 9 flock 9
@@ -768,7 +963,8 @@ fi
case "${1:-}" in case "${1:-}" in
install) install)
install shift
install "$@"
;; ;;
uninstall) 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 exit 1
;; ;;
esac esac