From 4830d7e80d6d34beeb5411c4172d172bb258be40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Aaron?= Date: Wed, 24 Jun 2026 12:15:09 +0200 Subject: [PATCH 1/2] feat: captive-portal onboarding via bumi/hub-os-config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flash → boot → join the "albyhub-setup" WiFi → pick your network in a captive portal → done. Removes the Raspberry Pi Imager customization dependency (the recurring "Imager 2.0 won't customize custom images" pain) for first-time WiFi setup. - build.yml: cross-compile the hub-os-config arm64 binary from source at a pinned commit and stage it into the overlay before the image build. - albyhub module: ship hub-os-config.service + config.toml, install NM / dnsmasq-base / rfkill / iw, bake a WiFi regulatory country (required for AP mode), enable the service (gracefully skipped if the binary isn't staged, e.g. local builds). - albyhub.service: gate startup on connectivity (albyhub-wait-online) so the hub doesn't claim :80 while the setup portal needs it on first boot. - README: rewrite onboarding around the captive-portal flow. --- .github/workflows/build.yml | 21 ++++++++ .gitignore | 2 + README.md | 50 ++++++++++++------- src/modules/albyhub/config | 17 +++++++ .../root/etc/hub-os-config/config.toml | 18 +++++++ .../root/etc/systemd/system/albyhub.service | 9 ++++ .../etc/systemd/system/hub-os-config.service | 26 ++++++++++ .../root/usr/local/bin/albyhub-wait-online | 21 ++++++++ src/modules/albyhub/start_chroot_script | 29 +++++++++++ 9 files changed, 176 insertions(+), 17 deletions(-) create mode 100644 src/modules/albyhub/filesystem/root/etc/hub-os-config/config.toml create mode 100644 src/modules/albyhub/filesystem/root/etc/systemd/system/hub-os-config.service create mode 100644 src/modules/albyhub/filesystem/root/usr/local/bin/albyhub-wait-online diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2dc3715..bc793f1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -36,6 +36,27 @@ jobs: "https://downloads.raspberrypi.org/raspios_lite_arm64/images/$BASEDIR/$FILE" ls -la src/image-raspberrypiarm64/ + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: stable + + - name: Build + stage hub-os-config (captive-portal onboarding) + run: | + set -euo pipefail + # Single source of truth for the repo + pinned commit. + source src/modules/albyhub/config + git clone --quiet "$HUBOSCONFIG_REPO" /tmp/hub-os-config + git -C /tmp/hub-os-config checkout --quiet "$HUBOSCONFIG_COMMIT" + ( cd /tmp/hub-os-config \ + && CGO_ENABLED=0 GOOS=linux GOARCH=arm64 \ + go build -trimpath -ldflags "-s -w -X main.version=$HUBOSCONFIG_COMMIT" \ + -o /tmp/hub-os-config-bin ./cmd/hub-os-config ) + DEST=src/modules/albyhub/filesystem/root/usr/local/bin + mkdir -p "$DEST" + install -m 0755 /tmp/hub-os-config-bin "$DEST/hub-os-config" + ls -l "$DEST/hub-os-config" + - name: Build image run: sudo bash ./build.sh diff --git a/.gitignore b/.gitignore index 6a84025..a29bee3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # build artifacts workspace/ +# hub-os-config binary staged into the overlay by CI (built from source, not committed) +src/modules/albyhub/filesystem/root/usr/local/bin/hub-os-config *.img *.img.xz *.img.zip diff --git a/README.md b/README.md index 28d4844..bff4ae9 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,15 @@ 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. +1. Flash the image with **any** flasher — no customization needed. 2. Insert the card, power on, wait ~2 min. -3. Open `http://albyhub.local`, set a password, save your seed, open a channel. +3. On your phone/laptop, join the **`albyhub-setup`** WiFi. A setup page opens + automatically (captive portal) — pick your home WiFi and confirm. +4. The Pi reboots onto your network. 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 Imager customization. Tested target: Raspberry Pi Zero 2 W +(512 MB) and up. ## What's baked in (`src/modules/albyhub`) @@ -25,13 +29,25 @@ No terminal. Tested target: Raspberry Pi Zero 2 W (512 MB) and up. - 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`. - -## 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. +- **Captive-portal onboarding** ([`bumi/hub-os-config`](https://github.com/bumi/hub-os-config), + pinned by commit): on first boot with no internet it raises an open + `albyhub-setup` AP serving a setup page where the user picks their WiFi and a + few hub options, then reboots online. The same UI stays reachable afterwards + on `:8090`. Built from source for arm64 and staged into the image by CI. + +## First-boot onboarding (captive portal) + +No Imager customization is required. On first boot the device checks for +internet; finding none, it broadcasts the open **`albyhub-setup`** WiFi and +serves a captive portal on `192.168.4.1`. The user joins from a phone, the setup +page opens automatically, they choose their home network, and the Pi reboots +onto it. WiFi credentials are stored by NetworkManager and reconnect on boot. + +The hub stays off port 80 until the device is online (`albyhub-wait-online`, an +`ExecStartPre` gate) so it never fights the setup portal for `:80`. A WiFi +regulatory country (`ALBYHUB_WIFI_COUNTRY`, default `US`) is baked in — AP mode +needs one set. SSH-based / Imager WiFi setup still works too: if WiFi is already +configured, the portal never appears and the device boots straight online. ## Build @@ -46,10 +62,10 @@ CI (`.github/workflows/build.yml`) builds on tag push and publishes ## Distribution Download the latest `HubOS--arm64.img.xz` from the -[latest GitHub release](https://github.com/getAlby/hub-os/releases/latest), then flash it with -**Raspberry Pi Imager** ("Use custom") — or any flasher (Etcher, `dd`). Set your -WiFi + hostname in Imager's customisation, write, boot, and open -`http://albyhub.local`. +[latest GitHub release](https://github.com/getAlby/hub-os/releases/latest), then +flash it with **any** tool (Raspberry Pi Imager, Etcher, `dd`) — no +customization step. Boot, join the `albyhub-setup` WiFi to configure your +network, then open `http://albyhub.local`. ## Configuration @@ -75,6 +91,6 @@ 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 (hub UI serves on `:80`). +Captive-portal onboarding (`bumi/hub-os-config`) is newly integrated and still +being validated on hardware. Not yet tagged as a release. diff --git a/src/modules/albyhub/config b/src/modules/albyhub/config index 9010bd5..0cdbb3f 100644 --- a/src/modules/albyhub/config +++ b/src/modules/albyhub/config @@ -13,3 +13,20 @@ export ALBYHUB_HOME="/opt/albyhub" export ALBYHUB_USER="albyhub" export ALBYHUB_SWAP_MB="1024" export ALBYHUB_HOSTNAME="albyhub" + +# --- captive-portal onboarding (bumi/hub-os-config) --- +# A small Go service that, on first boot with no internet, raises an open WiFi +# AP ("albyhub-setup") hosting a captive portal where the user picks their WiFi +# and sets a few hub options, then reboots online. Pinned to a commit because +# the upstream repo has no tagged releases yet. The arm64 binary is built on the +# CI host and staged into the overlay before the image build (see +# .github/workflows/build.yml); a local build without that step just ships +# without onboarding (the chroot script skips it gracefully). +export HUBOSCONFIG_REPO="https://github.com/bumi/hub-os-config" +export HUBOSCONFIG_COMMIT="1c7203303cf01ee1e5af309ba4861db5bf151332" + +# WiFi regulatory domain (ISO 3166-1 alpha-2). REQUIRED for the setup AP to come +# up — a fresh image leaves the radio in the restrictive "world" domain, which +# blocks AP mode. Channel 6 @ 2.4GHz is permitted in essentially every domain, +# so this just unblocks the radio; installers elsewhere can change it. +export ALBYHUB_WIFI_COUNTRY="US" diff --git a/src/modules/albyhub/filesystem/root/etc/hub-os-config/config.toml b/src/modules/albyhub/filesystem/root/etc/hub-os-config/config.toml new file mode 100644 index 0000000..2e389f4 --- /dev/null +++ b/src/modules/albyhub/filesystem/root/etc/hub-os-config/config.toml @@ -0,0 +1,18 @@ +# hub-os-config settings for the Alby Hub image. +# Most values are upstream defaults (see bumi/hub-os-config config.toml.example); +# only the entries that matter for this image are pinned here explicitly. + +[access_point] +# Open setup network the user joins to reach the captive portal. +ssid = "albyhub-setup" + +[web] +# Port the config UI listens on once online (Normal Mode). Alby Hub itself runs +# on :80 in this image, so the config UI sits alongside it on :8090 +# (http://albyhub.local:8090). It does NOT collide with the hub. +normal_port = 8090 + +[paths] +# The Advanced section of the portal edits these keys in the hub's env file, +# preserving everything else. +albyhub_env = "/opt/albyhub/.env" diff --git a/src/modules/albyhub/filesystem/root/etc/systemd/system/albyhub.service b/src/modules/albyhub/filesystem/root/etc/systemd/system/albyhub.service index 420dfcc..99bf734 100644 --- a/src/modules/albyhub/filesystem/root/etc/systemd/system/albyhub.service +++ b/src/modules/albyhub/filesystem/root/etc/systemd/system/albyhub.service @@ -10,6 +10,15 @@ RestartSec=1 User=albyhub # Grant port-80 binding at runtime (file setcap caps don't survive the image build) AmbientCapabilities=CAP_NET_BIND_SERVICE +# Don't claim :80 until the device is actually online. On a fresh image with no +# WiFi, hub-os-config serves its setup captive portal on :80 (via the AP gateway) +# — the hub must stay off that port until the user has connected. Once online the +# probe returns immediately and the hub starts normally. wait-online is a no-op +# if hub-os-config isn't installed (local builds without the onboarding binary). +ExecStartPre=/usr/local/bin/albyhub-wait-online +# wait-online may block indefinitely on a never-configured device; that's fine — +# the captive portal owns :80 meanwhile. Don't let the start timeout kill it. +TimeoutStartSec=infinity ExecStart=/opt/albyhub/bin/albyhub # Cap CPU so the wallet never starves the rest of a small Pi CPUQuota=90% diff --git a/src/modules/albyhub/filesystem/root/etc/systemd/system/hub-os-config.service b/src/modules/albyhub/filesystem/root/etc/systemd/system/hub-os-config.service new file mode 100644 index 0000000..d79f690 --- /dev/null +++ b/src/modules/albyhub/filesystem/root/etc/systemd/system/hub-os-config.service @@ -0,0 +1,26 @@ +[Unit] +Description=Alby Hub OS configuration service (captive-portal onboarding) +Documentation=https://github.com/bumi/hub-os-config +After=NetworkManager.service +Wants=NetworkManager.service + +[Service] +Type=simple +# Wait for NetworkManager to finish startup (devices managed), and make sure the +# WiFi radio is usable: a fresh Pi image often has wlan0 rfkill-soft-blocked, and +# AP mode needs the radio on. The leading "-" makes each step non-fatal. +# (The WiFi regulatory country that AP mode also requires is baked into the image +# by the albyhub module's start_chroot_script — see ALBYHUB_WIFI_COUNTRY.) +ExecStartPre=-/usr/sbin/rfkill unblock wifi +ExecStartPre=-/usr/bin/nm-online -s -q -t 30 +ExecStartPre=-/usr/bin/nmcli radio wifi on +ExecStart=/usr/local/bin/hub-os-config run +Restart=on-failure +RestartSec=5 +# Runs as root: needs nmcli, write access to /opt/albyhub/.env, and reboot. +User=root +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target diff --git a/src/modules/albyhub/filesystem/root/usr/local/bin/albyhub-wait-online b/src/modules/albyhub/filesystem/root/usr/local/bin/albyhub-wait-online new file mode 100644 index 0000000..58c388b --- /dev/null +++ b/src/modules/albyhub/filesystem/root/usr/local/bin/albyhub-wait-online @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Block until the device has working internet, so Alby Hub doesn't claim port 80 +# while hub-os-config is serving its setup captive portal there (also :80, via +# the AP gateway). Used as ExecStartPre of albyhub.service. +# +# - Fresh image, no WiFi: blocks here indefinitely. The hub never binds :80, so +# the captive portal owns it. Once the user connects (and the device reboots +# online), this returns immediately and the hub starts. +# - No onboarding installed (local build without the hub-os-config binary): +# nothing else wants :80, so skip the wait entirely and let the hub start now. +set -u + +# Only gate when the captive-portal service is actually present. +if [ ! -x /usr/local/bin/hub-os-config ]; then + exit 0 +fi + +URL="https://getalby.com/api/internal/info" +until curl -fsS --max-time 5 -o /dev/null "$URL"; do + sleep 5 +done diff --git a/src/modules/albyhub/start_chroot_script b/src/modules/albyhub/start_chroot_script index 6a4ee62..d7e3fc8 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 +# Captive-portal onboarding deps: NetworkManager (already default on RPi OS, but +# pin it), its dnsmasq backend for AP-mode DHCP/DNS, and the radio tools the +# hub-os-config systemd unit calls (rfkill / iw / nmcli). +apt-get install -y --no-install-recommends network-manager dnsmasq-base rfkill iw # --- dedicated, unprivileged service user --- if ! id -u "$ALBYHUB_USER" >/dev/null 2>&1; then @@ -83,6 +87,31 @@ 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" +# --- captive-portal onboarding (bumi/hub-os-config) --- +# The :80 gate helper always ships; it self-disables when the onboarding binary +# is absent (see the script), so local builds without staging still work. +chmod +x /usr/local/bin/albyhub-wait-online + +# WiFi regulatory country — REQUIRED for the setup AP. Use the RPi-blessed path +# when available (handles rfkill + persistence), else write the legacy regdb. +if command -v raspi-config >/dev/null 2>&1; then + raspi-config nonint do_wifi_country "$ALBYHUB_WIFI_COUNTRY" || true +else + echo "REGDOMAIN=$ALBYHUB_WIFI_COUNTRY" > /etc/default/crda +fi + +# The arm64 binary is staged into the overlay by CI (see build.yml). If present, +# finish wiring it up; otherwise skip onboarding so the build still succeeds. +if [ -x /usr/local/bin/hub-os-config ]; then + install -d -m 0755 /var/lib/hub-os-config # state.json lives here + systemctl_if_exists enable NetworkManager.service + systemctl_if_exists enable hub-os-config.service +else + echo "WARN: /usr/local/bin/hub-os-config not staged — image ships WITHOUT captive-portal onboarding" >&2 + rm -f /etc/systemd/system/hub-os-config.service + rm -rf /etc/hub-os-config +fi + # --- enable services (start happens on first boot, not in chroot) --- systemctl_if_exists enable albyhub.service systemctl_if_exists enable avahi-daemon.service From 521722b76ccc6dd9c5d5558e655bff5afdeda230 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Aaron?= Date: Wed, 1 Jul 2026 21:28:06 +0200 Subject: [PATCH 2/2] fix: set hostname to albyhub so albyhub.local resolves The CustomPiOS base module runs after the albyhub module and overwrites /etc/hostname with BASE_OVERRIDE_HOSTNAME, which defaults to the lowercased DIST_NAME (hubos). That broke avahi's albyhub.local advertisement. Pin the hostname explicitly at the distro level. --- src/config | 5 +++++ 1 file changed, 5 insertions(+) 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."