diff --git a/README.md b/README.md index 28d4844..6bd66f4 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,19 @@ Lightning) with **zero manual install** — flash, boot, open Manually installing Alby Hub on a Pi means SSH + `curl | bash` + a signature prompt + systemd setup. This image bakes all of that in, so onboarding is just: -1. Flash the image with **Raspberry Pi Imager 2.0+** (“Use custom”), set WiFi + password. -2. Insert the card, power on, wait ~2 min. +1. Flash the image with **Raspberry Pi Imager 2.0+** (“Use custom”) and set a + user + password in the customization dialog (required — Raspberry Pi OS won't + finish first boot without one). Setting WiFi there is optional — you can also + configure it on first boot (step 2). Leave the hostname at its default so + `albyhub.local` works. +2. Insert the card, power on, wait ~2 min. If the Pi has no internet yet, it + raises an open WiFi network **`albyhub-setup`** — join it from your phone, + pick your home WiFi, and enter the password. (Powered by + [balena wifi-connect](https://github.com/balena-os/wifi-connect).) 3. Open `http://albyhub.local`, set a password, save your seed, open a channel. -No terminal. Tested target: Raspberry Pi Zero 2 W (512 MB) and up. +No terminal, no computer needed for WiFi. Tested target: Raspberry Pi Zero 2 W +(512 MB) and up. ## What's baked in (`src/modules/albyhub`) @@ -24,14 +32,21 @@ No terminal. Tested target: Raspberry Pi Zero 2 W (512 MB) and up. with a low-resource default configuration). - Port-80 binding via systemd `AmbientCapabilities`; `ld.so.conf.d` entry for the bundled libraries. - `gpu_mem=16` (frees ~48 MB) and `vm.swappiness=10` + 1 GB swap. -- `avahi-daemon` for `albyhub.local`; default hostname `albyhub`. +- `avahi-daemon` for `albyhub.local`; default hostname `albyhub` (Imager's + hostname customization can still override it — leave it default). +- **WiFi onboarding** via `wifi-connect` (pinned release, checksum-verified): + `wifi-connect.service` raises the `albyhub-setup` portal on first boot only if + the device is offline, then exits so the hub takes `:80`. NetworkManager + + `dnsmasq-base` back it; a WiFi regulatory domain (`ALBYHUB_WIFI_COUNTRY`) is + baked in because AP mode needs one. ## What Imager handles at flash time (not baked) -WiFi credentials, SSH enable/key, user/password, locale/timezone, hostname — -applied on first boot. **Use Raspberry Pi Imager 2.0 or newer** — older versions -don't reliably apply customization to current Raspberry Pi OS (cloud-init), which -silently drops the SSH/WiFi settings. +SSH enable/key, user/password (required), locale/timezone, hostname — applied on +first boot. WiFi is **optional** here: set it in Imager to skip the portal, or +leave it blank and use the `albyhub-setup` portal on first boot. **Use Raspberry Pi Imager 2.0 or newer** +— older versions don't reliably apply customization to current Raspberry Pi OS +(cloud-init), which silently drops the SSH/WiFi settings. ## Build @@ -75,6 +90,7 @@ app auto-updater off entirely. ## Status -Builds green in CI and boots on a Raspberry Pi Zero 2 W (setup screen serves on -`:80`). Flash with **Raspberry Pi Imager 2.0+** so first-boot customization -applies. Not yet tagged as a release. +Builds green in CI and boots on a Raspberry Pi Zero 2 W. On first boot with no +internet, the `albyhub-setup` WiFi portal comes up; once connected, the hub +serves on `:80` at `albyhub.local`. Flash with **Raspberry Pi Imager 2.0+** so +first-boot customization applies. Not yet tagged as a release. diff --git a/src/config b/src/config index bff8ca1..4daa24e 100644 --- a/src/config +++ b/src/config @@ -1,6 +1,11 @@ export DIST_NAME=HubOS export DIST_VERSION=0.1.0 +# Hostname the device advertises (avahi -> albyhub.local). Without this, the base +# module defaults the hostname to the lowercased DIST_NAME ("hubos"), which would +# break the albyhub.local onboarding story. +export BASE_OVERRIDE_HOSTNAME=albyhub + # rpi-imager metadata export RPI_IMAGER_NAME="Alby Hub" export RPI_IMAGER_DESCRIPTION="Self-custodial Lightning wallet on your Pi. No full node, no manual install." diff --git a/src/modules/albyhub/config b/src/modules/albyhub/config index 9010bd5..cd41f3e 100644 --- a/src/modules/albyhub/config +++ b/src/modules/albyhub/config @@ -13,3 +13,24 @@ export ALBYHUB_HOME="/opt/albyhub" export ALBYHUB_USER="albyhub" export ALBYHUB_SWAP_MB="1024" export ALBYHUB_HOSTNAME="albyhub" + +# --- WiFi onboarding: balena wifi-connect --- +# The de-facto standard first-boot WiFi captive portal for headless Linux devices. +# On first boot with no internet it raises an open AP ("albyhub-setup") hosting a +# portal where the user picks their WiFi; then it writes a NetworkManager profile +# and exits, and Alby Hub comes up at albyhub.local. WiFi-only by design — every +# hub setting is configured in Alby Hub's own onboarding UI once it is online. +# +# Pinned release, downloaded + checksum-verified in the chroot (same reproducible +# pattern as the Alby Hub binary above). balena publishes no checksum manifest, so +# the SHA-256s are pinned here directly. +export WIFICONNECT_VERSION="v4.11.84" +export WIFICONNECT_SHA256_BIN="413d70e6d1c1366cbe2b32555e8476f3e92878178ed1b9c82205985f055f1936" +export WIFICONNECT_SHA256_UI="e57a3cec559729516decf892beb1e7f191b23e71b2e13bcd43d36b980034ffbe" + +# WiFi regulatory domain (ISO 3166-1 alpha-2). REQUIRED for the setup AP to come +# up — a fresh image sits in the restrictive "world" (00) domain, which flags +# 2.4GHz channels NO-IR and blocks AP beaconing. wifi-connect does not set a +# regdom itself, so one must be baked in. Channel 6 @ 2.4GHz is permitted in +# essentially every domain; installers elsewhere can change it. +export ALBYHUB_WIFI_COUNTRY="US" diff --git a/src/modules/albyhub/filesystem/root/etc/systemd/system/wifi-connect.service b/src/modules/albyhub/filesystem/root/etc/systemd/system/wifi-connect.service new file mode 100644 index 0000000..319d12b --- /dev/null +++ b/src/modules/albyhub/filesystem/root/etc/systemd/system/wifi-connect.service @@ -0,0 +1,29 @@ +[Unit] +Description=WiFi onboarding captive portal (balena wifi-connect) +Documentation=https://github.com/balena-os/wifi-connect +After=NetworkManager.service +Wants=NetworkManager.service +# Run to completion before the hub starts. wifi-connect serves its portal on :80 +# (same port as the hub) and exits once WiFi is configured, so ordering it before +# albyhub.service means the hub only claims :80 after onboarding is done — no port +# race, no separate wait gate needed. +Before=albyhub.service + +[Service] +Type=oneshot +RemainAfterExit=yes +# AP mode needs the radio usable: a fresh Pi image often has wlan0 rfkill-soft- +# blocked. (The regulatory country AP mode also requires is baked into the image +# by the albyhub module — see ALBYHUB_WIFI_COUNTRY.) Leading "-" = non-fatal. +# No nm-online wait here: the wrapper itself waits for NM when a connection is +# configured, so already-onboarded devices don't pay a serialized boot delay. +ExecStartPre=-/usr/sbin/rfkill unblock wifi +ExecStartPre=-/usr/bin/nmcli radio wifi on +ExecStart=/usr/local/bin/albyhub-wifi-connect +# The portal blocks until the user picks a network; never time it out. +TimeoutStartSec=infinity +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target diff --git a/src/modules/albyhub/filesystem/root/usr/local/bin/albyhub-wifi-connect b/src/modules/albyhub/filesystem/root/usr/local/bin/albyhub-wifi-connect new file mode 100755 index 0000000..5d46ad0 --- /dev/null +++ b/src/modules/albyhub/filesystem/root/usr/local/bin/albyhub-wifi-connect @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Raise balena wifi-connect's captive portal ("albyhub-setup") when the device +# cannot join any network, so the user configures WiFi with no computer. Once +# they pick a network, wifi-connect writes a NetworkManager profile and exits; +# NM brings the link up and Alby Hub (also :80) starts. +# +# The gate is "did NetworkManager connect a device?", NOT "is the internet +# reachable": a configured hub must never be flipped back into setup mode by an +# ISP outage, DNS hiccup, or an offline LAN — the portal is only for devices +# that have nothing to join (fresh image, wrong password, moved house). +set -u + +nm_connected() { + # Any device (wlan0, eth0, …) holding an active connection counts — the hub + # can serve albyhub.local on a LAN regardless of internet reachability. + nmcli -t -f DEVICE,STATE device status 2>/dev/null | grep -q ':connected$' +} + +has_config() { + # A saved WiFi profile or a plugged-in ethernet device means NM has something + # to bring up — give it time before concluding onboarding is needed. + nmcli -t -f TYPE connection show 2>/dev/null | grep -q '802-11-wireless' \ + || nmcli -t -f TYPE,STATE device status 2>/dev/null | grep -q '^ethernet:connecting' +} + +nm_connected && exit 0 + +# Fresh image with nothing configured: no point waiting, go straight to setup. +# Otherwise (profile exists — e.g. Imager-provisioned WiFi still associating on +# first boot, or a normal reboot) give NM up to 90s to bring the link up. +if has_config; then + for _ in $(seq 1 18); do + sleep 5 + nm_connected && exit 0 + done +fi + +exec /usr/local/sbin/wifi-connect \ + --portal-ssid albyhub-setup \ + --ui-directory /usr/local/share/wifi-connect/ui diff --git a/src/modules/albyhub/start_chroot_script b/src/modules/albyhub/start_chroot_script index 6a4ee62..15240c5 100755 --- a/src/modules/albyhub/start_chroot_script +++ b/src/modules/albyhub/start_chroot_script @@ -17,6 +17,10 @@ unpack /filesystem/root / # --- dependencies --- apt-get update apt-get install -y --no-install-recommends bzip2 ca-certificates gnupg avahi-daemon libcap2-bin unattended-upgrades +# WiFi onboarding deps: NetworkManager (already default on RPi OS, but pin it), +# its dnsmasq backend which wifi-connect drives for the AP's DHCP/DNS, and +# rfkill, which the wifi-connect unit calls to unblock the radio. +apt-get install -y --no-install-recommends network-manager dnsmasq-base rfkill # --- dedicated, unprivileged service user --- if ! id -u "$ALBYHUB_USER" >/dev/null 2>&1; then @@ -83,7 +87,40 @@ sed -i "s/127.0.1.1.*/127.0.1.1\t${ALBYHUB_HOSTNAME}/" /etc/hosts || true # --- auto-update: make the weekly updater executable --- chmod +x "$ALBYHUB_HOME/autoupdate.sh" +# --- WiFi onboarding: balena wifi-connect --- +# Download the pinned release (binary + web UI) and verify checksums, same +# reproducible pattern as the Alby Hub binary above. The arm64 asset matches the +# image arch, so it installs straight into the (qemu) chroot. WiFi-only: all hub +# settings live in Alby Hub's own onboarding UI once the device is online. +WC_TMP="$(mktemp -d)" +WC_BASE="https://github.com/balena-os/wifi-connect/releases/download/${WIFICONNECT_VERSION}" +wget -q -O "$WC_TMP/wifi-connect.tar.gz" "$WC_BASE/wifi-connect-aarch64-unknown-linux-gnu.tar.gz" +wget -q -O "$WC_TMP/wifi-connect-ui.tar.gz" "$WC_BASE/wifi-connect-ui.tar.gz" +echo "$WIFICONNECT_SHA256_BIN $WC_TMP/wifi-connect.tar.gz" | sha256sum -c - +echo "$WIFICONNECT_SHA256_UI $WC_TMP/wifi-connect-ui.tar.gz" | sha256sum -c - +tar -xzf "$WC_TMP/wifi-connect.tar.gz" -C "$WC_TMP" +install -m 0755 "$WC_TMP/wifi-connect" /usr/local/sbin/wifi-connect +install -d /usr/local/share/wifi-connect/ui +tar -xzf "$WC_TMP/wifi-connect-ui.tar.gz" -C /usr/local/share/wifi-connect/ui +rm -rf "$WC_TMP" +chmod +x /usr/local/bin/albyhub-wifi-connect + +# WiFi regulatory country — REQUIRED for the setup AP (world/00 domain flags +# 2.4GHz NO-IR and blocks AP mode). Bake it as a cfg80211 module option, which +# the kernel applies deterministically at module load — no live radio needed, so +# it works in the build chroot. Additionally try the RPi-blessed raspi-config +# path when available (persists via cmdline.txt too); its failure is non-fatal +# because the modprobe.d file above is the guarantee. +cat > /etc/modprobe.d/cfg80211.conf </dev/null 2>&1; then + raspi-config nonint do_wifi_country "$ALBYHUB_WIFI_COUNTRY" || true +fi + # --- enable services (start happens on first boot, not in chroot) --- +systemctl_if_exists enable NetworkManager.service +systemctl_if_exists enable wifi-connect.service systemctl_if_exists enable albyhub.service systemctl_if_exists enable avahi-daemon.service systemctl_if_exists enable albyhub-update.timer