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
# 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}}"
# 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 ================================================================================================
validate_check_mode() {
case "$CHECK_MODE" in
link|internet) ;;
*)
echo "Invalid FALLBACK_AP_CHECK_MODE '$CHECK_MODE' (use: 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/<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 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
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" -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
}
}'
awk -F: -v ap="$CONNECTION_NAME" \
'$3 ~ /^connected/ && $4 != ap {print $1 ":" $2}'
)
}
@@ -456,6 +539,7 @@ save_config() {
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"
@@ -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() {
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
@@ -519,7 +695,10 @@ EOF
log "Config file: $CONFIG_FILE"
log "SSID: $AP_NAME"
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"
if [ "$CHECK_MODE" = "internet" ]; then
log "Ping targets: $CHECK_TARGETS"
@@ -663,7 +842,10 @@ status() {
echo "=== Config ($CONFIG_FILE) ==="
echo "SSID: $AP_NAME"
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"
if [ "$CHECK_MODE" = "internet" ]; then
echo "Ping targets: $CHECK_TARGETS"
@@ -696,6 +878,19 @@ status() {
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"
@@ -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