generated from ztimson/template
Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8859375919 | ||
|
|
66b4cedd39 | ||
|
|
3243309bbb | ||
|
|
8b7702e90c | ||
|
|
ed20d616f5 | ||
|
|
0180f81420 | ||
|
|
a47fea04fb | ||
|
|
985c6b4ca7 | ||
|
|
6fca943d2c | ||
|
|
5c6862306d | ||
|
|
96c6b86d40 | ||
|
|
255385bf82 | ||
|
|
d0e2752cd8 | ||
|
|
8758a43088 | ||
|
|
4f79a652e5 | ||
|
|
6e201723d9 | ||
|
|
69372f0b94 | ||
|
|
63a3dec564 | ||
|
|
37f1a7e646 |
@@ -1,132 +1,86 @@
|
|||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div id="top" align="center">
|
<div id="top" align="center">
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<!-- Logo -->
|
<!-- Logo -->
|
||||||
<img src="https://git.zakscode.com/repo-avatars/2b4ee6ba1f2e2618bf7694e4a52fb56d1d0ea6abafa2dcbe496ab786b86d5a76" alt="Logo" width="200" height="200">
|
<img src="https://git.zakscode.com/repo-avatars/80775ae9cf0d2c4fb4aec035efb650e631d4d0046ab6fc04c6c041e91ba11d44" alt="Logo" width="200" height="200">
|
||||||
|
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
### Template
|
### Fallback AP
|
||||||
|
|
||||||
<!-- Description -->
|
<!-- Description -->
|
||||||
Simple repository template
|
Automatic fallback Wi-Fi access point for headless Linux devices
|
||||||
|
|
||||||
<!-- Repo badges -->
|
<!-- Repo badges -->
|
||||||
[](https://git.zakscode.com/ztimson/template/tags)
|
[](https://git.zakscode.com/ztimson/fallback-ap/tags)
|
||||||
[](https://git.zakscode.com/ztimson/template/pulls)
|
[](https://git.zakscode.com/ztimson/fallback-ap/pulls)
|
||||||
[](https://git.zakscode.com/ztimson/template/issues)
|
[](https://git.zakscode.com/ztimson/fallback-ap/issues)
|
||||||
|
|
||||||
<!-- Links -->
|
<!-- Links -->
|
||||||
|
|
||||||
---
|
---
|
||||||
<div>
|
<div>
|
||||||
<a href="https://git.zakscode.com/ztimson/template/wiki" target="_blank">Documentation</a>
|
<a href="https://git.zakscode.com/ztimson/fallback-ap/wiki" target="_blank">Documentation</a>
|
||||||
• <a href="https://git.zakscode.com/ztimson/template/releases" target="_blank">Release Notes</a>
|
• <a href="https://git.zakscode.com/ztimson/fallback-ap/releases" target="_blank">Release Notes</a>
|
||||||
• <a href="https://git.zakscode.com/ztimson/template/issues/new?template=.github%2fissue_template%2fbug.md" target="_blank">Report a Bug</a>
|
• <a href="https://git.zakscode.com/ztimson/fallback-ap/issues/new?template=.github%2fissue_template%2fbug.md" target="_blank">Report a Bug</a>
|
||||||
• <a href="https://git.zakscode.com/ztimson/template/issues/new?template=.github%2fissue_template%2fenhancement.md" target="_blank">Request a Feature</a>
|
• <a href="https://git.zakscode.com/ztimson/fallback-ap/issues/new?template=.github%2fissue_template%2fenhancement.md" target="_blank">Request a Feature</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
---
|
---
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
- [Template](#top)
|
- [Fallback AP](#top)
|
||||||
- [About](#about)
|
- [About](#about)
|
||||||
- [Demo](#demo)
|
|
||||||
- [Built With](#built-with)
|
- [Built With](#built-with)
|
||||||
- [Setup](#setup)
|
- [Setup](#setup)
|
||||||
- [Production](#production)
|
- [Installation](#installation)
|
||||||
- [Development](#development)
|
- [Usage](#usage)
|
||||||
- [License](#license)
|
- [License](#license)
|
||||||
|
|
||||||
## About
|
## About
|
||||||
|
|
||||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
A NetworkManager-based script for headless Linux devices (e.g. a Raspberry Pi) that keeps them reachable even when their configured Wi-Fi network isn't available. When the device loses its Wi-Fi connection, it automatically stands up a local access point so you can connect directly and reconfigure it. While that access point has no clients connected, it periodically steps aside for a short window to let NetworkManager try reconnecting to a known network in the background, then brings the access point back up if nothing was found.
|
||||||
|
|
||||||
### Demo
|
Runs as a `systemd` service and is managed entirely through `nmcli`/`iw` — no extra daemons (`hostapd`, `dnsmasq`, etc.) required.
|
||||||
|
|
||||||
Website: https://git.zakscode.com
|
|
||||||
|
|
||||||
### Built With
|
### Built With
|
||||||
[](https://angular.io/)
|
|
||||||
[](https://www.android.com/)
|
|
||||||
[](https://www.arduino.cc/)
|
|
||||||
[](https://getbootstrap.com)
|
|
||||||
[](https://en.cppreference.com/w/c/language)
|
|
||||||
[](https://cplusplus.com/)
|
|
||||||
[](https://dotnet.microsoft.com/)
|
|
||||||
[](https://www.w3.org/Style/CSS/Overview.en.html)
|
|
||||||
[](https://www.djangoproject.com/)
|
|
||||||
[](https://docker.com/)
|
|
||||||
[](https://www.electronjs.org/)
|
|
||||||
[](https://firebase.google.com/)
|
|
||||||
[](https://go.dev/)
|
|
||||||
[](https://graphql.org/)
|
|
||||||
[](https://developer.mozilla.org/en-US/docs/Glossary/HTML)
|
|
||||||
[](https://java.com/)
|
|
||||||
[](https://javascript.com/)
|
|
||||||
[](https://jquery.com )
|
|
||||||
[](https://laravel.com)
|
|
||||||
[](https://www.linux.org/)
|
|
||||||
[](https://git.zakscode.com/ztimson/momentum)
|
|
||||||
[](https://www.mongodb.com/)
|
|
||||||
[](https://www.mysql.com/)
|
|
||||||
[](https://nestjs.com/)
|
|
||||||
[](https://dotnet.microsoft.com/)
|
|
||||||
[](https://nextjs.org/)
|
|
||||||
[](https://www.nginx.com/)
|
|
||||||
[](https://nodejs.org/)
|
|
||||||
[](https://p5js.org/)
|
|
||||||
[](https://www.php.net/)
|
|
||||||
[](https://www.postgresql.org/)
|
|
||||||
[](https://www.python.org/)
|
|
||||||
[](https://reactjs.org/)
|
|
||||||
[](https://redis.com/)
|
|
||||||
[](https://sass-lang.com/)
|
|
||||||
[](https://en.wikipedia.org/wiki/Shell_script)
|
[](https://en.wikipedia.org/wiki/Shell_script)
|
||||||
[](https://www.microsoft.com/en-ca/sql-server)
|
[](https://www.linux.org/)
|
||||||
[](https://www.sqlite.org/index.html)
|
|
||||||
[](https://svelte.dev/)
|
|
||||||
[](https://typescriptlang.org/)
|
|
||||||
[](https://microsoft.com/windows)
|
|
||||||
[](https://vitejs.dev/)
|
|
||||||
[](https://vuejs.org/)
|
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
<details>
|
<details open>
|
||||||
<summary>
|
<summary>
|
||||||
<h3 id="production" style="display: inline">
|
<h3 id="installation" style="display: inline">
|
||||||
Production
|
Installation
|
||||||
</h3>
|
</h3>
|
||||||
</summary>
|
</summary>
|
||||||
|
|
||||||
#### Prerequisites
|
#### Prerequisites
|
||||||
- [Docker](https://docs.docker.com/install/)
|
- A Linux device with a Wi-Fi adapter (`wlan0` by default) — a Raspberry Pi or similar SBC
|
||||||
|
- `NetworkManager` and `iw` (installed automatically if missing)
|
||||||
|
- Root access
|
||||||
|
|
||||||
#### Instructions
|
#### Instructions
|
||||||
1. Run the docker image: `docker run -p 80:80 git.zakscode.com/ztimson/template:latest`
|
1. Download & Install: `curl -sL https://git.zakscode.com/ztimson/fallback-ap/raw/branch/master/install.sh | sudo bash`
|
||||||
2. Open [http://localhost](http://localhost)
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>
|
<summary>
|
||||||
<h3 id="development" style="display: inline">
|
<h3 id="usage" style="display: inline">
|
||||||
Development
|
Usage
|
||||||
</h3>
|
</h3>
|
||||||
</summary>
|
</summary>
|
||||||
|
|
||||||
#### Prerequisites
|
Once installed, the service runs automatically and requires no further interaction. A few commands are available for manual control:
|
||||||
- [Node.js](https://nodejs.org/en/download)
|
|
||||||
|
|
||||||
#### Instructions
|
- `sudo ./pi-fallback-ap.sh status` — show service, connection, and access point state
|
||||||
1. Install the dependencies: `npm install`
|
- `sudo ./pi-fallback-ap.sh start` / `stop` — manually bring the access point up or down
|
||||||
2. Start the Angular server: `npm run start`
|
- `sudo ./pi-fallback-ap.sh uninstall` — remove the service and access point profile
|
||||||
3. Open [http://localhost:4200](http://localhost:4200)
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
## License
|
## License
|
||||||
Copyright © 2023 Zakary Timson | All Rights Reserved | Available under MIT Licensing
|
Copyright © 2026 Zakary Timson | Available under MIT Licensing
|
||||||
|
|
||||||
See the [license](./LICENSE) for more information.
|
|
||||||
|
|||||||
+581
@@ -0,0 +1,581 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Configuration =======================================================================================================
|
||||||
|
#
|
||||||
|
# Connection settings (SSID, passphrase, subnet, DHCP range, interface) 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
|
||||||
|
|
||||||
|
WLAN0_IFACE="${FALLBACK_AP_IFACE:-${SAVED_IFACE:-wlan0}}"
|
||||||
|
|
||||||
|
AP_NAME="${FALLBACK_AP_SSID:-${SAVED_SSID:-$HOSTNAME}}"
|
||||||
|
AP_PASSWORD="${FALLBACK_AP_PASSWORD:-${SAVED_PASSWORD:-abc123}}"
|
||||||
|
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}}"
|
||||||
|
|
||||||
|
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}"
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
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 ================================================================================================
|
||||||
|
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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}'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
start_ap() {
|
||||||
|
if is_ap_on; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Enabling AP: $AP_NAME"
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
tick() {
|
||||||
|
local now="$1"
|
||||||
|
|
||||||
|
if is_wifi_connected; then
|
||||||
|
remember_wifi
|
||||||
|
|
||||||
|
if is_ap_on; then
|
||||||
|
stop_ap
|
||||||
|
fi
|
||||||
|
|
||||||
|
set_state "wifi"
|
||||||
|
last_attempt="$now"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! is_ap_on; then
|
||||||
|
start_ap
|
||||||
|
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
|
||||||
|
|
||||||
|
if (( now - last_attempt >= RECOVERY_CHECK_INTERVAL )); then
|
||||||
|
last_attempt="$now"
|
||||||
|
attempt_recovery
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
attempt_recovery() {
|
||||||
|
log "No AP clients, checking for known Wi-Fi (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
|
||||||
|
set_state "fallback"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if wait_for_wifi "$RECOVERY_WINDOW"; then
|
||||||
|
sleep "$STATE_SETTLE_DELAY"
|
||||||
|
|
||||||
|
if is_wifi_connected; then
|
||||||
|
log "Wi-Fi reconnected"
|
||||||
|
remember_wifi
|
||||||
|
set_state "wifi"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "No Wi-Fi available, restoring AP"
|
||||||
|
|
||||||
|
start_ap
|
||||||
|
set_state "fallback"
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
save_config() {
|
||||||
|
mkdir -p "$(dirname "$CONFIG_FILE")"
|
||||||
|
cat > "$CONFIG_FILE" <<EOF
|
||||||
|
SAVED_IFACE="$WLAN0_IFACE"
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
install() {
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
echo "Run this script as root."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Installing NetworkManager fallback AP"
|
||||||
|
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y network-manager iw
|
||||||
|
|
||||||
|
systemctl enable NetworkManager
|
||||||
|
systemctl start NetworkManager
|
||||||
|
|
||||||
|
mkdir -p "$STATE_DIR"
|
||||||
|
|
||||||
|
if is_wifi_connected; then
|
||||||
|
remember_wifi
|
||||||
|
fi
|
||||||
|
|
||||||
|
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 "Interface: $WLAN0_IFACE"
|
||||||
|
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 "Interface: $WLAN0_IFACE"
|
||||||
|
echo "Network: $AP_CIDR"
|
||||||
|
echo "DHCP: $DHCP_START - $DHCP_END"
|
||||||
|
|
||||||
|
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() {
|
||||||
|
local last_attempt=0
|
||||||
|
local now
|
||||||
|
|
||||||
|
mkdir -p "$STATE_DIR"
|
||||||
|
|
||||||
|
log "Fallback AP service started"
|
||||||
|
log "Script: $SCRIPT_PATH"
|
||||||
|
exec 9>"$LOCK_FILE"
|
||||||
|
|
||||||
|
flock 9
|
||||||
|
|
||||||
|
if is_wifi_connected; then
|
||||||
|
remember_wifi
|
||||||
|
set_state "wifi"
|
||||||
|
log "Wi-Fi already connected"
|
||||||
|
else
|
||||||
|
log "No Wi-Fi connection, starting fallback AP"
|
||||||
|
start_ap
|
||||||
|
set_state "fallback"
|
||||||
|
fi
|
||||||
|
|
||||||
|
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)
|
||||||
|
install
|
||||||
|
;;
|
||||||
|
|
||||||
|
uninstall)
|
||||||
|
uninstall
|
||||||
|
;;
|
||||||
|
|
||||||
|
start)
|
||||||
|
start
|
||||||
|
;;
|
||||||
|
|
||||||
|
stop)
|
||||||
|
stop
|
||||||
|
;;
|
||||||
|
|
||||||
|
status)
|
||||||
|
status
|
||||||
|
;;
|
||||||
|
|
||||||
|
run)
|
||||||
|
run
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
echo "Usage: $SCRIPT_PATH {install|uninstall|start|stop|status|run}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_URL="https://git.zakscode.com/ztimson/fallback-ap/raw/branch/master/fallback-ap.sh"
|
||||||
|
INSTALL_PATH="/usr/local/bin/fallback-ap"
|
||||||
|
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
echo "This installer needs to run as root, e.g.:"
|
||||||
|
echo " curl -sL https://git.zakscode.com/ztimson/fallback-ap/raw/branch/master/install.sh | sudo bash"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
prompt() {
|
||||||
|
local __var="$1" __question="$2" __default="$3" __answer=""
|
||||||
|
if [ -e /dev/tty ]; then
|
||||||
|
read -r -p "$__question [$__default]: " __answer < /dev/tty || __answer=""
|
||||||
|
fi
|
||||||
|
printf -v "$__var" '%s' "${__answer:-$__default}"
|
||||||
|
}
|
||||||
|
|
||||||
|
[ -z "${FALLBACK_AP_SSID:-}" ] && prompt FALLBACK_AP_SSID "Access point SSID" "$(hostname)"
|
||||||
|
[ -z "${FALLBACK_AP_PASSWORD:-}" ] && prompt FALLBACK_AP_PASSWORD "Access point passphrase" "abc123"
|
||||||
|
[ -z "${FALLBACK_AP_CIDR:-}" ] && prompt FALLBACK_AP_CIDR "Access point subnet (CIDR)" "10.10.10.1/24"
|
||||||
|
[ -z "${FALLBACK_AP_DHCP_START_OCTET:-}" ] && prompt FALLBACK_AP_DHCP_START_OCTET "DHCP range start (last octet)" "100"
|
||||||
|
[ -z "${FALLBACK_AP_DHCP_END_OCTET:-}" ] && prompt FALLBACK_AP_DHCP_END_OCTET "DHCP range end (last octet)" "250"
|
||||||
|
|
||||||
|
export FALLBACK_AP_SSID FALLBACK_AP_PASSWORD FALLBACK_AP_CIDR FALLBACK_AP_DHCP_START_OCTET FALLBACK_AP_DHCP_END_OCTET
|
||||||
|
|
||||||
|
echo "Downloading fallback-ap..."
|
||||||
|
curl -fsSL "$SCRIPT_URL" -o "$INSTALL_PATH"
|
||||||
|
chmod +x "$INSTALL_PATH"
|
||||||
|
echo "Running installer..."
|
||||||
|
"$INSTALL_PATH" install
|
||||||
Reference in New Issue
Block a user