Author SHA1 Message Date
ztimson 4b2769aa98 Update install.sh
Code review / review (pull_request) Successful in 1m2s
2026-10-26 15:41:34 -04:00
ztimson d3e1ab52a9 Update fallback-ap.sh 2026-10-26 15:38:30 -04:00
ztimson 14b08ed70b Update fallback-ap.sh 2026-10-26 15:31:26 -04:00
ztimson 09387088df Merge pull request 'Update fallback-ap.sh' (#15) from ztimson-patch-15 into master
Reviewed-on: #15
2026-09-12 19:58:08 -04:00
ztimson 14b8eefb77 Update fallback-ap.sh
Code review / review (pull_request) Successful in 20s
2026-09-12 19:58:03 -04:00
ztimson d1e1842052 Merge pull request 'Update install.sh' (#14) from ztimson-patch-13 into master
Reviewed-on: #14
2026-09-12 19:57:49 -04:00
ztimson ca3f2d15a8 Update install.sh
Code review / review (pull_request) Successful in 20s
2026-09-12 19:56:47 -04:00
ztimson 1107712e75 Merge pull request 'Update install.sh' (#12) from ztimson-patch-12 into master
Reviewed-on: #12
2026-09-12 19:55:16 -04:00
ztimson bd19cc87e3 Update install.sh
Code review / review (pull_request) Successful in 33s
2026-09-12 19:55:10 -04:00
ztimson 1e4b9394db Merge pull request 'Update install.sh' (#11) from ztimson-patch-11 into master
Reviewed-on: #11
2026-09-12 19:54:06 -04:00
ztimson b83210d0eb Update install.sh
Code review / review (pull_request) Successful in 46s
2026-09-12 19:53:58 -04:00
ztimson 9d2987323e Merge pull request 'Update install.sh' (#10) from ztimson-patch-10 into master
Reviewed-on: #10
2026-09-12 19:51:05 -04:00
ztimson b319455ef3 Update install.sh
Code review / review (pull_request) Successful in 32s
2026-09-12 19:51:00 -04:00
ztimson 825fe96dee Merge pull request 'ztimson-patch-9' (#9) from ztimson-patch-9 into master
Reviewed-on: #9
2026-09-12 19:47:51 -04:00
ztimson 8859375919 Update fallback-ap.sh
Code review / review (pull_request) Successful in 30s
2026-09-12 19:47:42 -04:00
ztimson 66b4cedd39 Update install.sh 2026-09-12 19:47:26 -04:00
ztimson 3243309bbb Merge pull request 'Update fallback-ap.sh' (#8) from ztimson-patch-8 into master
Reviewed-on: #8
2026-09-12 19:46:03 -04:00
ztimson 8b7702e90c Update fallback-ap.sh
Code review / review (pull_request) Successful in 35s
2026-09-12 19:45:56 -04:00
ztimson ed20d616f5 Merge pull request 'Update install.sh' (#7) from ztimson-patch-7 into master
Reviewed-on: #7
2026-09-12 19:40:29 -04:00
ztimson 0180f81420 Update install.sh
Code review / review (pull_request) Successful in 32s
2026-09-12 19:40:23 -04:00
ztimson a47fea04fb Merge pull request 'ztimson-patch-6' (#6) from ztimson-patch-6 into master
Reviewed-on: #6
2026-09-12 19:31:08 -04:00
ztimson 985c6b4ca7 Update fallback-ap.sh
Code review / review (pull_request) Successful in 32s
2026-09-12 19:30:45 -04:00
ztimson 6fca943d2c Update install.sh 2026-09-12 19:30:06 -04:00
ztimson 5c6862306d Merge pull request 'Update install.sh' (#5) from ztimson-patch-5 into master
Reviewed-on: #5
2026-09-12 19:22:30 -04:00
ztimson 96c6b86d40 Update install.sh
Code review / review (pull_request) Successful in 21s
2026-09-12 19:22:25 -04:00
ztimson 255385bf82 Merge pull request 'Update README.md' (#4) from ztimson-patch-4 into master
Reviewed-on: #4
2026-09-12 19:20:50 -04:00
ztimson d0e2752cd8 Update README.md
Code review / review (pull_request) Successful in 27s
2026-09-12 19:20:45 -04:00
ztimson 8758a43088 Merge pull request 'Add install.sh' (#3) from ztimson-patch-3 into master
Reviewed-on: #3
2026-09-12 19:18:55 -04:00
ztimson 4f79a652e5 Add install.sh
Code review / review (pull_request) Successful in 23s
2026-09-12 19:18:48 -04:00
ztimson 6e201723d9 Merge pull request 'Update README.md' (#2) from ztimson-patch-2 into master
Reviewed-on: #2
2026-09-12 19:16:11 -04:00
ztimson 69372f0b94 Update README.md
Code review / review (pull_request) Successful in 28s
2026-09-12 19:16:05 -04:00
ztimson 63a3dec564 Merge pull request 'Add fallback-ap.sh' (#1) from ztimson-patch-1 into master
Reviewed-on: #1
2026-09-12 19:12:27 -04:00
3 changed files with 692 additions and 154 deletions
+33 -79
View File
@@ -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 -->
[![Version](https://img.shields.io/badge/dynamic/json.svg?label=Version&style=for-the-badge&url=https://git.zakscode.com/api/v1/repos/ztimson/template/tags&query=$[0].name)](https://git.zakscode.com/ztimson/template/tags) [![Version](https://img.shields.io/badge/dynamic/json.svg?label=Version&style=for-the-badge&url=https://git.zakscode.com/api/v1/repos/ztimson/fallback-ap/tags&query=$[0].name)](https://git.zakscode.com/ztimson/fallback-ap/tags)
[![Pull Requests](https://img.shields.io/badge/dynamic/json.svg?label=Pull%20Requests&style=for-the-badge&url=https://git.zakscode.com/api/v1/repos/ztimson/template&query=open_pr_counter)](https://git.zakscode.com/ztimson/template/pulls) [![Pull Requests](https://img.shields.io/badge/dynamic/json.svg?label=Pull%20Requests&style=for-the-badge&url=https://git.zakscode.com/api/v1/repos/ztimson/fallback-ap&query=open_pr_counter)](https://git.zakscode.com/ztimson/fallback-ap/pulls)
[![Issues](https://img.shields.io/badge/dynamic/json.svg?label=Issues&style=for-the-badge&url=https://git.zakscode.com/api/v1/repos/ztimson/template&query=open_issues_count)](https://git.zakscode.com/ztimson/template/issues) [![Issues](https://img.shields.io/badge/dynamic/json.svg?label=Issues&style=for-the-badge&url=https://git.zakscode.com/api/v1/repos/ztimson/fallback-ap&query=open_issues_count)](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
[![Angular](https://img.shields.io/badge/Angular-DD0031?style=for-the-badge&logo=angular)](https://angular.io/)
[![Android](https://img.shields.io/badge/android-34A853?style=for-the-badge&logo=android&logoColor=ffffff)](https://www.android.com/)
[![Arduino](https://img.shields.io/badge/Arduino-00878F?style=for-the-badge&logo=arduino&logoColor=white)](https://www.arduino.cc/)
[![Bootstrap](https://img.shields.io/badge/Bootstrap-563D7C?style=for-the-badge&logo=bootstrap&logoColor=white)](https://getbootstrap.com)
[![C](https://img.shields.io/badge/C-A8B9CC?style=for-the-badge&logo=c&logoColor=ffffff)](https://en.cppreference.com/w/c/language)
[![C++](https://img.shields.io/badge/C%2B%2B-00599C?style=for-the-badge&logo=cplusplus)](https://cplusplus.com/)
[![C#](https://img.shields.io/badge/C%23-239120?style=for-the-badge&logo=csharp)](https://dotnet.microsoft.com/)
[![CSS](https://img.shields.io/badge/CSS-1572B6?style=for-the-badge&logo=css3)](https://www.w3.org/Style/CSS/Overview.en.html)
[![Django](https://img.shields.io/badge/django-0C4B33?style=for-the-badge&logo=django)](https://www.djangoproject.com/)
[![Docker](https://img.shields.io/badge/Docker-384d54?style=for-the-badge&logo=docker)](https://docker.com/)
[![Electron](https://img.shields.io/badge/Electron-47848F?style=for-the-badge&logo=electron&logoColor=white)](https://www.electronjs.org/)
[![Firebase](https://img.shields.io/badge/Firebase-FFFFFF?style=for-the-badge&logo=firebase)](https://firebase.google.com/)
[![Go](https://img.shields.io/badge/Go-00ADD8?style=for-the-badge&logo=go&logoColor=ffffff)](https://go.dev/)
[![GraphQL](https://img.shields.io/badge/GraphQL-E10098?style=for-the-badge&logo=graphql)](https://graphql.org/)
[![HTML](https://img.shields.io/badge/HTML-FFFFFF?style=for-the-badge&logo=html5)](https://developer.mozilla.org/en-US/docs/Glossary/HTML)
[![Java](https://img.shields.io/badge/Java-5382A1?style=for-the-badge&logo=coffeescript&logoColor=F8981D)](https://java.com/)
[![JavaScript](https://img.shields.io/badge/JavaScript-000000?style=for-the-badge&logo=javascript)](https://javascript.com/)
[![JQuery](https://img.shields.io/badge/jQuery-0769AD?style=for-the-badge&logo=jquery)](https://jquery.com )
[![Laravel](https://img.shields.io/badge/Laravel-6C6C6C?style=for-the-badge&logo=laravel)](https://laravel.com)
[![Linux](https://img.shields.io/badge/Linux-eeeeee?style=for-the-badge&logo=linux&logoColor=000000)](https://www.linux.org/)
[![Momentum](https://img.shields.io/badge/Momentum-000000?style=for-the-badge&logo=data:image/svg%2bxml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pgo8IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDIwMDEwOTA0Ly9FTiIgImh0dHA6Ly93d3cudzMub3JnL1RSLzIwMDEvUkVDLVNWRy0yMDAxMDkwNC9EVEQvc3ZnMTAuZHRkIj4KPHN2ZyB2ZXJzaW9uPSIxLjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI1MHB0IiBoZWlnaHQ9IjI1MHB0IiB2aWV3Qm94PSIwIDAgMjUwIDI1MCIgIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaWRZTWlkIG1lZXQiPgoJPHN0eWxlPgoJCUBtZWRpYSAocHJlZmVycy1jb2xvci1zY2hlbWU6IGxpZ2h0KSB7CgkJZyB7IGZpbGw6ICMwMDAwMDA7IH0KCQl9CgkJQG1lZGlhIChwcmVmZXJzLWNvbG9yLXNjaGVtZTogZGFyaykgewoJCWcgeyBmaWxsOiAjZmZmZmZmOyB9CgkJfQoJPC9zdHlsZT4KCTxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsMjUwKSBzY2FsZSgwLjEsLTAuMSkiPgoJCTxwYXRoIGQ9Ik0xMTY1IDIyOTkgYy0yNDUgLTE4IC00NzAgLTEyMyAtNjUxIC0zMDMgLTExNyAtMTE4IC0yMDAgLTI1MCAtMjUyIC00MDEgLTcxIC0yMDkgLTcxIC00NjggLTEgLTY3OSAzNyAtMTA4IDExOCAtMjU0IDE3OCAtMzE5IGwyNSAtMjggLTEzMiAtMTMyIC0xMzIgLTEzMiA1MiAtNTMgNTMgLTUyIDEzMiAxMzIgMTMyIDEzMiAyOCAtMjUgYzQxIC0zOCAxMDkgLTgyIDE4OCAtMTIxIDE1MyAtNzcgMjkyIC0xMDkgNDcwIC0xMDkgMjkxIDAgNTM2IDEwMSA3NDEgMzA1IDE1MyAxNTQgMjQ0IDMyNCAyODkgNTQwIDIxIDEwMyAyMSAyOTkgLTEgNDAyIC0zMiAxNTkgLTEwNSAzMjQgLTE4NyA0MjYgbC00OSA2MSAxMjIgMTIyIDEyMyAxMjMgLTIyIDMxIGMtMTIgMTYgLTM2IDQwIC01MiA1MiBsLTMxIDIyIC0xMjEgLTEyMSAtMTIyIC0xMjEgLTYwIDQ2IGMtODEgNjEgLTI2MSAxNDcgLTM2NSAxNzMgLTEwNCAyNyAtMjM1IDM3IC0zNTUgMjl6IG0yNzQgLTE2NCBjMTAyIC0yMSAyMTcgLTY4IDI5NyAtMTIxIDEwOSAtNzIgMTA2IC02NiA0OSAtMTI0IGwtNTAgLTUwIC01MCA5IGMtODMgMTYgLTE2OSAtNSAtMzA1IC03NCBsLTc1IC0zOCA0NSAtMzggYzI1IC0yMiA1MyAtNDIgNjMgLTQ1IDExIC0zIDQ1IDYgNzcgMjAgNjggMzEgMTM1IDQ1IDE3MCAzNiAyMiAtNSAyNSAtMTIgMjggLTU2IDEyIC0yMDQgLTM2MCAtNjQ1IC02NzQgLTgwMCAtMTA3IC01MyAtMTgwIC02NiAtMjA4IC0zOCAtMjcgMjcgLTE4IDExMyAyMiAxOTggMTggMzcgMjkgNzMgMjUgODAgLTE0IDI0IC03NiA5NiAtODIgOTYgLTEyIDAgLTc4IC0xMzIgLTEwMSAtMTk5IC0xNiAtNDkgLTIxIC04NCAtMTkgLTE0OCBsMyAtODQgLTQxIC00MSBjLTUwIC01MCAtNTIgLTQ5IC0xMTkgNTggLTU1IDg4IC05OCAxOTYgLTExOSAyOTQgLTE5IDkxIC0xOSAyNzkgMCAzNjkgNzIgMzQzIDM0NyA2MjEgNjg4IDY5NSA4NSAxOSAyODkgMTkgMzc2IDF6IG01NjkgLTM5MyBjNjMgLTk1IDEwNSAtMTk1IDEyOCAtMzA1IDE4IC04OSAxOCAtMjc3IC0xIC0zNjcgLTczIC0zNDYgLTM0OSAtNjIyIC02OTUgLTY5NSAtOTEgLTE5IC0yNzggLTE5IC0zNjggMCAtMTMwIDI3IC0yNzggOTUgLTM2NyAxNjkgbC0zMCAyNSA0NSA0NSBjNDQgNDUgNDUgNDYgMTEwIDQ2IDE3MyAwIDM5MiAxMjQgNjI1IDM1NSAxMzAgMTI5IDIwNCAyMjIgMjc4IDM1MCA2MyAxMDkgODkgMTg1IDk0IDI4MCBsNSA4MCA1MSA1MyBjMjggMjggNTUgNTIgNTkgNTIgNSAwIDM1IC00MCA2NiAtODh6Ii8+CgkJPHBhdGggZD0iTTc1MiAxODM5IGMtMTk1IC05NyAtOTMgLTQzMyAyMzggLTc5MSA3MSAtNzcgNzIgLTc4IDEwMCAtNjMgMTUgOCA0MiAyNiA2MCA0MSBsMzEgMjcgLTY5IDcxIGMtMTc2IDE4MSAtMjk3IDM3NCAtMzE3IDUwOCAtMTQgOTIgMzIgMTEwIDE1OCA2NSAxMjAgLTQ0IDI2OCAtMTQ3IDQwOSAtMjg3IGw4OCAtODcgMTkgMjEgYzEwIDEyIDI4IDM2IDM5IDU1IGwyMSAzNCAtMjYgMzEgYy00NyA1NyAtMTk3IDE4NiAtMjg1IDI0NyAtMTk2IDEzNCAtMzYxIDE4MCAtNDY2IDEyOHoiLz4KCQk8cGF0aCBkPSJNMTAxMiAxNDk3IGMtMjkgLTI5IC01MiAtNTggLTUyIC02NiAwIC0xMyA1NCAtOTYgNjggLTEwNSA1IC0zIDQ0IDMwIDg2IDczIGw3OCA3OSAtNTMgMzYgYy0zMCAyMCAtNTggMzYgLTY0IDM2IC02IDAgLTM0IC0yNCAtNjMgLTUzeiIvPgoJCTxwYXRoIGQ9Ik0xNjcyIDExMzkgYy00NiAtNTYgLTQ5IC02OSAtMjggLTEwMiAyMyAtMzUgNTYgLTE0MyA1NiAtMTgxIDAgLTc4IC01OSAtODMgLTIwNiAtMjAgbC04MyAzNiAtNTEgLTQ0IGMtMjcgLTI0IC01MCAtNDYgLTUwIC01MCAwIC0xNCAxODYgLTk5IDI1MiAtMTE0IDIwMCAtNDkgMzE3IDc4IDI2MyAyODYgLTE2IDYzIC03MCAxODYgLTk3IDIyMyAtMTMgMTcgLTE3IDE0IC01NiAtMzR6Ii8+Cgk8L2c+Cjwvc3ZnPgo=)](https://git.zakscode.com/ztimson/momentum)
[![MongoDB](https://img.shields.io/badge/mongodb-000000?style=for-the-badge&logo=mongodb)](https://www.mongodb.com/)
[![MySQL](https://img.shields.io/badge/MySQL-4479A1?style=for-the-badge&logo=mysql&logoColor=ffffff)](https://www.mysql.com/)
[![Nest](https://img.shields.io/badge/nestjs-E0234E?style=for-the-badge&logo=nestjs)](https://nestjs.com/)
[![.NET](https://img.shields.io/badge/.NET-512BD4?style=for-the-badge&logo=dotnet)](https://dotnet.microsoft.com/)
[![Next](https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs)](https://nextjs.org/)
[![NGINX](https://img.shields.io/badge/NGINX-009639?style=for-the-badge&logo=nginx)](https://www.nginx.com/)
[![Node](https://img.shields.io/badge/Node.js-000000?style=for-the-badge&logo=nodedotjs)](https://nodejs.org/)
[![p5.js](https://img.shields.io/badge/p5.js-ed225d?style=for-the-badge&logo=p5dotjs&logoColor=white)](https://p5js.org/)
[![PHP](https://img.shields.io/badge/PHP-474A8A?style=for-the-badge&logo=php&logoColor=white)](https://www.php.net/)
[![PostgreSQL](https://img.shields.io/badge/PostgreSQl-212121?style=for-the-badge&logo=postgresql)](https://www.postgresql.org/)
[![Python](https://img.shields.io/badge/Python-FFD43B?style=for-the-badge&logo=python)](https://www.python.org/)
[![React](https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react)](https://reactjs.org/)
[![Redis](https://img.shields.io/badge/Redis-ffffff?style=for-the-badge&logo=redis)](https://redis.com/)
[![SASS](https://img.shields.io/badge/SASS-CC6699?style=for-the-badge&logo=sass&logoColor=ffffff)](https://sass-lang.com/)
[![Shell](https://img.shields.io/badge/Shell-000000?style=for-the-badge&logo=windowsterminal&logoColor=00ff00)](https://en.wikipedia.org/wiki/Shell_script) [![Shell](https://img.shields.io/badge/Shell-000000?style=for-the-badge&logo=windowsterminal&logoColor=00ff00)](https://en.wikipedia.org/wiki/Shell_script)
[![SQL Server](https://img.shields.io/badge/SQL%20Server-CC2927?style=for-the-badge&logo=microsoftsqlserver)](https://www.microsoft.com/en-ca/sql-server) [![Linux](https://img.shields.io/badge/Linux-eeeeee?style=for-the-badge&logo=linux&logoColor=000000)](https://www.linux.org/)
[![SQLite](https://img.shields.io/badge/SQLITE-003B57?style=for-the-badge&logo=sqlite)](https://www.sqlite.org/index.html)
[![Svelte](https://img.shields.io/badge/Svelte-4A4A55?style=for-the-badge&logo=svelte)](https://svelte.dev/)
[![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=for-the-badge&logo=typescript&logoColor=white)](https://typescriptlang.org/)
[![Windows](https://img.shields.io/badge/Windows-0078D4?style=for-the-badge&logo=windows)](https://microsoft.com/windows)
[![Vite](https://img.shields.io/badge/vite-1b1b1b?style=for-the-badge&logo=vite)](https://vitejs.dev/)
[![Vue](https://img.shields.io/badge/Vue.js-35495E?style=for-the-badge&logo=vuedotjs)](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.
+510 -75
View File
@@ -3,16 +3,73 @@
set -e set -e
# Configuration ======================================================================================================= # Configuration =======================================================================================================
#
# 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.
#
# Everything else (timers, paths) is not meant to be reconfigured per-install
# and stays as plain constants.
WLAN0_IFACE="wlan0" CONFIG_FILE="/etc/fallback-ap.conf"
if [ -f "$CONFIG_FILE" ]; then source "$CONFIG_FILE"; fi
AP_NAME="${HOSTNAME}" # The interface the AP is created on. The AP is NEVER created on any other
AP_PASSWORD='guesswhatitis' # interface, regardless of what is being monitored.
AP_ENCRYPTION="wpa-psk" # none | wpa-psk | sae WLAN0_IFACE="${FALLBACK_AP_IFACE:-${SAVED_IFACE:-wlan0}}"
AP_CIDR="10.10.10.1/24" # Which interfaces to watch for connectivity. If any of them is online, the
DHCP_START="10.10.10.100" # AP stays off. Comma-separated list (e.g. "eth0,wlan1,wwan0").
DHCP_END="10.10.10.250" # 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 - (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 (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.
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
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 # 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.
@@ -26,11 +83,18 @@ 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="pi-fallback-ap" SERVICE_NAME="fallback-ap"
SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME.service" SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME.service"
STATE_DIR="/var/lib/pi-fallback-ap" STATE_DIR="/var/lib/fallback-ap"
LAST_WIFI_FILE="$STATE_DIR/last-wifi" LAST_WIFI_FILE="$STATE_DIR/last-wifi"
LOCK_FILE="$STATE_DIR/lock" LOCK_FILE="$STATE_DIR/lock"
STATE_FILE="$STATE_DIR/state" STATE_FILE="$STATE_DIR/state"
@@ -38,6 +102,18 @@ STATE_FILE="$STATE_DIR/state"
# End of Configuration ================================================================================================ # End of Configuration ================================================================================================
validate_check_mode() {
case "$CHECK_MODE" in
link|internet) ;;
*)
echo "Invalid check mode '$CHECK_MODE' (use: link | internet)"
exit 1
;;
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")"
@@ -68,17 +144,210 @@ 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
}
# 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 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" \
'$3 ~ /^connected/ && $4 != ap {print $1 ":" $2}'
)
}
# 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}'
} }
@@ -130,27 +399,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"
@@ -190,32 +446,31 @@ stop_ap() {
} }
# ========================================================================== # --- Main loop logic -------------------------------------------------------------------------------------------------
# One tick of the control loop. Called once per second, always while
# holding the wlan0 lock, so everything here runs to completion before # Called once per second from run(). Relies on run()'s local variables
# the next tick starts - there is no interleaving to race against. # (last_attempt, online, fail_count, last_online_check) via dynamic scoping.
#
# `last_attempt` is intentionally not declared `local` here: it lives in
# run()'s scope and this function updates it directly (bash resolves it
# via the caller's scope as long as nothing else shadows it).
# ==========================================================================
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
@@ -227,23 +482,24 @@ tick() {
clients="$(get_client_count)" clients="$(get_client_count)"
if [ "$clients" -gt 0 ]; then if [ "$clients" -gt 0 ]; then
# Someone is using the AP right now. Leave it alone and push the
# next recovery attempt back so we don't kick them off.
last_attempt="$now" last_attempt="$now"
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
} }
# Stop the AP, give NetworkManager a window to autoconnect to any known
# network it can see, and bring the AP back if that doesn't happen.
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"
@@ -251,52 +507,164 @@ 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
} }
save_config() {
mkdir -p "$(dirname "$CONFIG_FILE")"
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"
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"
}
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
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
log "Creating recovery service" log "Creating recovery service"
@@ -324,8 +692,17 @@ EOF
log "Installation complete" log "Installation complete"
log "Script: $SCRIPT_PATH" log "Script: $SCRIPT_PATH"
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 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"
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"
@@ -411,6 +788,7 @@ uninstall() {
nmcli connection delete "$CONNECTION_NAME" >/dev/null 2>&1 || true nmcli connection delete "$CONNECTION_NAME" >/dev/null 2>&1 || true
rm -f "$SERVICE_FILE" rm -f "$SERVICE_FILE"
rm -f "$CONFIG_FILE"
rm -rf "$STATE_DIR" rm -rf "$STATE_DIR"
systemctl daemon-reload systemctl daemon-reload
@@ -460,6 +838,59 @@ status() {
echo "=== Fallback AP ===" echo "=== Fallback AP ==="
nmcli connection show "$CONNECTION_NAME" 2>/dev/null || echo "Not installed" nmcli connection show "$CONNECTION_NAME" 2>/dev/null || echo "Not installed"
echo
echo "=== Config ($CONFIG_FILE) ==="
echo "SSID: $AP_NAME"
echo "AP interface: $WLAN0_IFACE"
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"
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 "=== 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"
@@ -479,34 +910,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 real interfaces} | mode: $CHECK_MODE"
# Open the lock fd once for the life of the process. Using `flock`
# directly against this fd (instead of wrapping each tick in its own
# `( ... ) 9>"$LOCK_FILE"` subshell) matters: any variable a tick
# updates - like last_attempt below - has to survive into the next
# loop iteration, and a subshell would silently throw those updates
# away when it exits.
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
@@ -530,7 +963,8 @@ fi
case "${1:-}" in case "${1:-}" in
install) install)
install shift
install "$@"
;; ;;
uninstall) uninstall)
@@ -554,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
+149
View File
@@ -0,0 +1,149 @@
#!/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
# Ask a question; Enter accepts the default.
prompt() {
local __var="$1" __question="$2" __default="$3" __answer=""
read -r -p "$__question [$__default]: " __answer < /dev/tty || __answer=""
printf -v "$__var" '%s' "${__answer:-$__default}"
}
# Like prompt, but "blank" is a legitimate value. Enter accepts the default;
# typing the clear word (e.g. "all" or "none") sets the value to empty.
prompt_blankable() {
local __var="$1" __question="$2" __default="$3" __clear="$4" __answer=""
read -r -p "$__question [${__default:-$__clear}]: " __answer < /dev/tty || __answer=""
case "$__answer" in
"") __answer="$__default" ;;
"$__clear") __answer="" ;;
esac
printf -v "$__var" '%s' "${__answer// /}"
}
# Ask until the answer is one of the allowed choices. Enter accepts the default.
prompt_choice() {
local __var="$1" __question="$2" __default="$3" __answer="" __opt
shift 3
while true; do
read -r -p "$__question [$__default]: " __answer < /dev/tty || __answer=""
__answer="${__answer:-$__default}"
for __opt in "$@"; do
if [ "$__answer" = "$__opt" ]; then
printf -v "$__var" '%s' "$__answer"
return 0
fi
done
echo " Please enter one of: $*"
done
}
# Lists wireless interfaces (those with a /sys/class/net/<iface>/wireless dir).
detect_wireless() {
local dir
for dir in /sys/class/net/*; do
if [ -d "$dir/wireless" ]; then
basename "$dir"
fi
done
}
EXISTING_CONFIG="/etc/fallback-ap.conf"
if [ -f "$EXISTING_CONFIG" ]; then
# shellcheck disable=SC1090
source "$EXISTING_CONFIG"
fi
DETECTED_WIFI="$(detect_wireless | tr '\n' ' ')"
DETECTED_WIFI="${DETECTED_WIFI% }"
DEFAULT_SSID="${SAVED_SSID:-$(hostname)}"
DEFAULT_PASSWORD="${SAVED_PASSWORD:-abcd1234}"
DEFAULT_IFACE="${SAVED_IFACE:-${DETECTED_WIFI%% *}}"
DEFAULT_IFACE="${DEFAULT_IFACE:-wlan0}"
DEFAULT_CIDR="${SAVED_CIDR:-10.10.10.1/24}"
DEFAULT_DHCP_START_OCTET="${SAVED_DHCP_START_OCTET:-100}"
DEFAULT_DHCP_END_OCTET="${SAVED_DHCP_END_OCTET:-250}"
DEFAULT_MONITOR_IFACE="${SAVED_MONITOR_IFACE-}"
DEFAULT_IGNORE_IFACE="${SAVED_IGNORE_IFACE-}"
DEFAULT_CHECK_MODE="${SAVED_CHECK_MODE:-link}"
DEFAULT_CHECK_TARGETS="${SAVED_CHECK_TARGETS:-1.1.1.1 8.8.8.8}"
# --- Access point ----------------------------------------------------------------------------------------------------
[ -z "${FALLBACK_AP_SSID:-}" ] && prompt FALLBACK_AP_SSID "Access point SSID" "$DEFAULT_SSID"
[ -z "${FALLBACK_AP_PASSWORD:-}" ] && prompt FALLBACK_AP_PASSWORD "Access point passphrase" "$DEFAULT_PASSWORD"
if [ -z "${FALLBACK_AP_IFACE:-}" ]; then
if [ -n "$DETECTED_WIFI" ]; then
echo "Detected Wi-Fi interfaces: $DETECTED_WIFI"
fi
prompt FALLBACK_AP_IFACE "Interface to create the access point on" "$DEFAULT_IFACE"
fi
[ -z "${FALLBACK_AP_CIDR:-}" ] && prompt FALLBACK_AP_CIDR "Access point subnet (CIDR)" "$DEFAULT_CIDR"
[ -z "${FALLBACK_AP_DHCP_START_OCTET:-}" ] && prompt FALLBACK_AP_DHCP_START_OCTET "DHCP range start (last octet)" "$DEFAULT_DHCP_START_OCTET"
[ -z "${FALLBACK_AP_DHCP_END_OCTET:-}" ] && prompt FALLBACK_AP_DHCP_END_OCTET "DHCP range end (last octet)" "$DEFAULT_DHCP_END_OCTET"
# --- Connectivity monitoring -----------------------------------------------------------------------------------------
#
# The AP only starts when none of the monitored interfaces are online, and it
# is only ever created on the interface chosen above.
#
# These use "unset" (not "empty") to decide whether to ask, because an empty
# FALLBACK_AP_MONITOR_IFACE / FALLBACK_AP_IGNORE_IFACE is a valid answer
# meaning "all real interfaces" / "ignore nothing".
if [ -z "${FALLBACK_AP_MONITOR_IFACE+x}" ]; then
echo
echo "Which interfaces should count as 'online'? Enter a comma-separated list"
echo "(e.g. eth0,wlan0), or 'all' for every real interface. Virtual interfaces"
echo "(docker, veth, bridges, VPNs) are skipped automatically."
prompt_blankable FALLBACK_AP_MONITOR_IFACE "Interfaces to monitor" "$DEFAULT_MONITOR_IFACE" "all"
fi
if [ -z "$FALLBACK_AP_MONITOR_IFACE" ] && [ -z "${FALLBACK_AP_IGNORE_IFACE+x}" ]; then
echo
echo "Any physical interfaces to ignore (globs allowed, e.g. enx*,usb0)?"
prompt_blankable FALLBACK_AP_IGNORE_IFACE "Interfaces to ignore" "$DEFAULT_IGNORE_IFACE" "none"
fi
if [ -z "${FALLBACK_AP_CHECK_MODE:-}" ]; then
echo
echo "How should 'online' be determined?"
echo " link - the interface is connected (an upstream outage keeps the AP off)"
echo " internet - the interface is connected AND can ping out"
prompt_choice FALLBACK_AP_CHECK_MODE "Check mode" "$DEFAULT_CHECK_MODE" link internet
fi
if [ "$FALLBACK_AP_CHECK_MODE" = "internet" ] && [ -z "${FALLBACK_AP_CHECK_TARGETS:-}" ]; then
prompt FALLBACK_AP_CHECK_TARGETS "Hosts to ping (space-separated)" "$DEFAULT_CHECK_TARGETS"
fi
export FALLBACK_AP_SSID FALLBACK_AP_PASSWORD FALLBACK_AP_IFACE FALLBACK_AP_CIDR FALLBACK_AP_DHCP_START_OCTET FALLBACK_AP_DHCP_END_OCTET
export FALLBACK_AP_CHECK_MODE
[ -n "${FALLBACK_AP_MONITOR_IFACE+x}" ] && export FALLBACK_AP_MONITOR_IFACE
[ -n "${FALLBACK_AP_IGNORE_IFACE+x}" ] && export FALLBACK_AP_IGNORE_IFACE
[ -n "${FALLBACK_AP_CHECK_TARGETS+x}" ] && export FALLBACK_AP_CHECK_TARGETS
echo
echo "Downloading fallback-ap..."
curl -fsSL "$SCRIPT_URL" -o "$INSTALL_PATH"
chmod +x "$INSTALL_PATH"
echo "Running installer..."
"$INSTALL_PATH" install