* Hotspot captive portal: welcome page with the WROLPi's address
Devices that join the hotspot probe a fixed plain-HTTP URL to check for
Internet access. NetworkManager's shared-mode dnsmasq now resolves those
probe hostnames (and `wrolpi`) to the hotspot address via an include the
Controller writes when the hotspot starts, the probes land on the Controller
(port 80), and are redirected to /portal: "This is your WROLPi, its address is
10.42.0.1, open WROLPi / open the Controller".
Only the probe hostnames are hijacked so the hotspot keeps forwarding
everything else when the WROLPi has an uplink. The include is corrected and
the connection re-activated once if NetworkManager picked a non-default
subnet. Writing the include is best-effort; the hotspot starts regardless.
`hotspot.captive_portal` (controller.yaml, default true) toggles the probe
redirects and the include; like the other hotspot settings it applies on the
next hotspot start, so saving while the hotspot is up reports
restart_required and both Controller UIs say so. The status endpoint gains
the hotspot address and portal URL, shown in both UIs. repair.sh only
creates the include directory so a disabled setting survives upgrades.
Native only: Docker has no hotspot.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* captive portal: address review
* React: re-read the hotspot address whenever the HotspotToggle starts or
stops the hotspot, matching the fallback UI's refresh-on-status.
* A WPA3 start that reverts to WPA2 leaves the hotspot up, so it now gets
the captive portal address correction too.
* The corrective `nmcli connection up` logs a warning on a non-zero exit;
dnsmasq would otherwise keep the old address silently.
* HotspotStatusResponse.captive_portal defaults to true like everywhere else.
* Trim design narrative from the module docstring and React comments.
* The include-before-activate test now observes the include at the moment
of the `nmcli ... hotspot` call instead of inferring from call order.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* captive portal: never forward any record type for probe hosts
`address=` only synthesizes A/AAAA answers. iOS also asks for the HTTPS
record type; dnsmasq forwarded that upstream, Apple's answer aliased to
captive.g.aaplimg.com, and the phone fetched the genuine "Success" from
Apple instead of the portal. Each probe host now also gets `local=`, so
dnsmasq answers every type itself, and the CDN alias is a probe host too.
Observed with dnsmasq query logging on a hotspot with an eth0 uplink.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* captive portal: let acknowledged clients finish so the OS offers "Done"
A phone's sign-in sheet only shows "Done" once the OS re-probes and gets
the genuine success reply. The Controller now remembers each client (by
hotspot IP) that has loaded the portal page and answers its later probes
with the reply each OS expects (Apple "Success" HTML, Android 204, Windows
"Microsoft Connect Test", ...). The set is cleared when the hotspot
starts so a new session shows the page again.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* captive portal: acknowledge on Continue, not on view
iOS fetches the portal page in the background before opening the sheet,
then re-probes after the sheet's own navigation. Acknowledging on view
made that re-probe return "Success" as the sheet's content. Phones only
re-check the network (and offer "Done") after a full-page navigation, so
the welcome page now has a Continue button leading to /portal/continue,
which is the only place a client is acknowledged. The Continue page says
to tap Done and links to WROLPi and the Controller.
Also hijack clients3.google.com (older Android probe host).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* help: bump wrolpi-help to the captive portal and Speed Test docs
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* captive portal: comments describe the Continue design; clear acks on stop
Three comments still described acknowledging on view, which
|
||
|---|---|---|
| .. | ||
| api | ||
| app | ||
| archive | ||
| controller | ||
| help | ||
| samba | ||
| web | ||
| zim | ||
| README.md | ||
WROLPi Docker
Development and testing environment using Docker containers.
Quick Start
git clone https://github.com/lrnselfreliance/wrolpi.git
cd wrolpi
git submodule update --init
docker-compose build --parallel
docker volume create --name=openstreetmap-data
docker volume create --name=openstreetmap-rendered-tiles
docker-compose run --rm api db upgrade
docker-compose up
Browse to https://localhost:8443
Services
| Service | Port(s) | Description |
|---|---|---|
| web | 8443 (HTTPS), 8084-8086 | Caddy reverse proxy |
| api | 8081 | Sanic API server |
| app | (via web) | React frontend |
| controller | 8080 | System management (FastAPI) |
| archive | 8083 | SingleFile/Readability |
| zim | (via web:8085) | Kiwix |
| help | (via web:8086) | MkDocs documentation |
| map | (via web:8084) | OpenStreetMap tile server |
TLS Certificates
WROLPi uses a two-tier PKI for HTTPS. A root CA is generated once and persists in the media directory. A leaf certificate is regenerated on every container start to pick up hostname and IP changes.
How it works
- On first start, the
webcontainer generates a root CA at/opt/media/config/ssl/. - A leaf cert signed by the root CA is created at
/etc/ssl/caddy/inside the container. - Caddy serves HTTPS on ports 443, 8084, 8085, and 8086 using the leaf cert.
- The Controller serves on port 80 with a certificate trust banner when needed.
Trusting the certificate
- Browse to
http://<host-ip>to see the Controller. If the HTTPS certificate is not yet trusted, a banner will appear with the download link and platform-specific trust instructions. - Download and install the root CA on your device.
- All HTTPS ports will work without browser warnings.
The root CA only needs to be trusted once. Leaf certs can be regenerated (hostname change, container rebuild) without needing to re-trust.
Name Constraints
The root CA is restricted to local/private networks only via X.509 Name Constraints. Even if the CA key were compromised, it cannot sign certificates for public domains.
Permitted:
- DNS:
*.local,localhost - IPv4:
127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 - IPv6:
::1,fc00::/7(unique local),fe80::/10(link-local)
EXTRA_SANS
The Docker container cannot discover the host's real LAN IP. Set EXTRA_SANS in a
.env file so the leaf certificate includes your host's IP and hostname:
# .env
EXTRA_SANS=IP:10.0.0.5,DNS:myhost.local
Each entry must have a type prefix (IP: or DNS:). Multiple entries are
comma-separated. After changing EXTRA_SANS, restart the web container:
docker compose up -d --build web
Regenerating the root CA
If you need to regenerate the root CA (e.g. after updating the certificate generation script), delete the old CA files and rebuild:
rm test/config/ssl/ca.crt test/config/ssl/ca.key
docker compose up -d --build web
All devices will need to re-download and re-trust the new CA from
http://<host-ip>/ca.crt.
Volumes
The test/ directory is mounted to /opt/media inside containers, serving as the
media directory for development. Certificate files, configs, and downloaded content
are stored here.
Two external volumes are required for the map service:
docker volume create --name=openstreetmap-data
docker volume create --name=openstreetmap-rendered-tiles
Environment Variables
| Variable | Default | Description |
|---|---|---|
MEDIA_DIRECTORY |
/opt/media |
Media directory inside containers |
EXTRA_SANS |
(empty) | Additional SANs for the TLS leaf certificate |
WEB_HOST |
0.0.0.0 |
Host bind address for the web service |
WEB_HTTPS_PORT |
8443 |
HTTPS port |
REACT_APP_API |
0.0.0.0:8081 |
API bind address |
UID |
1000 |
User ID for api/app containers |
GID |
1000 |
Group ID for api/app containers |
Upgrading
See UPGRADE.md for upgrade instructions.