From 0faaf4577f8629f0dca408a6f8f1cf74591a3eec Mon Sep 17 00:00:00 2001 From: ssmithx Date: Sat, 5 Sep 2026 14:07:05 +0000 Subject: [PATCH 1/9] docs: add OpenWrt Gateway setup guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Walks a node operator through pairing an OpenWrt router over SSH, running the WAN/WISP wizard, and provisioning TollGate pay-as-you-go WiFi — plus an RPC/architecture reference for developers. Distills the openwrt crate, RPC handlers, and Vue panel into user-facing steps that didn't exist anywhere in docs/ before. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0176RpCxFNS9ZaSJjL72W9Z5 --- docs/README.md | 1 + docs/openwrt-gateway-setup.md | 184 ++++++++++++++++++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 docs/openwrt-gateway-setup.md diff --git a/docs/README.md b/docs/README.md index 0ff22c46..4df75a51 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,6 +10,7 @@ disagree, the code wins and the doc is a bug. - [Talking to your node](COMMANDS.md) — the conversational command surface - [Seed Verification](SEED-VERIFICATION.md) — independently verify your 24-word backup - [Troubleshooting](troubleshooting.md) — common problems and how to resolve them +- [OpenWrt Gateway Setup](openwrt-gateway-setup.md) — pairing an OpenWrt router and provisioning TollGate pay-as-you-go WiFi - [Gamepad / Controller Navigation](GAMEPAD-NAV.md) — driving the UI from a controller - [Pine voice commands](pine-voice-commands.md) — the voice-satellite phrase surface diff --git a/docs/openwrt-gateway-setup.md b/docs/openwrt-gateway-setup.md new file mode 100644 index 00000000..f0e0392f --- /dev/null +++ b/docs/openwrt-gateway-setup.md @@ -0,0 +1,184 @@ +# OpenWrt Gateway Setup + +How to connect an OpenWrt router to an Archipelago node and, optionally, turn +it into a pay-as-you-go WiFi gateway with **TollGate**. Written for a node +operator following the UI; a developer-facing RPC/architecture reference is +at the bottom. + +This feature manages a **separate physical (or virtual) router** running +OpenWrt over SSH/UCI — it is not a containerized app. Archipelago itself does +not flash or install OpenWrt; you bring a router that already runs it. + +## What you get + +- **Status dashboard**: hostname, uptime, firmware release, WiFi interfaces, + WAN state — polled live from the router. +- **WAN/WISP wizard**: point the router's radio at an upstream WiFi network + (turns it into a wireless bridge/repeater) with DHCP + NAT configured for + you. +- **TollGate provisioning** (optional): installs the + [TollGate](https://tollgate.me) captive-portal package + (`tollgate-module-basic-go`) and stands up an `archipelago` SSID that + sells timed internet access for sats, settled against this node's local + Cashu mint. + +## Prerequisites + +1. **A router already flashed with OpenWrt.** Check the + [OpenWrt Table of Hardware](https://openwrt.org/toh/start) for your model + and follow OpenWrt's own install/flashing instructions — that part is + outside Archipelago's scope. +2. **SSH reachable.** Fresh OpenWrt images enable `dropbear` (SSH) on LAN by + default, listening as `root` with no password (or the password you set + during OpenWrt's first-boot wizard at `192.168.1.1`). Archipelago + connects with `ssh2` over a password (key-based auth is supported at the + library level but the UI only offers password so far). +3. **Same LAN as the Archipelago node**, at least for setup — plug the + router's LAN port into the same switch/network segment the node is on. +4. **For TollGate**: a running Cashu mint app (`nutshell`/`cashu-mint`) on + this node — provisioning defaults `mint_url` to + `http://:3338` and TollGate customers must be able to reach that + URL from outside the node's loopback. + +## Step 1: Open the OpenWrt Gateway panel + +1. In the Archipelago UI, go to **Server**. +2. Under the network status list, click **OpenWrt Gateway** + (`/dashboard/server/openwrt`). + +If no router has been connected before, you'll land on the connect form. + +## Step 2: Connect the router + +You have two options: + +- **Detect**: click **Detect** — this reads the node's own active wired + Ethernet interface, derives its subnet, and probes every host on it for + `TCP/22` + a valid `/etc/openwrt_release`. If it finds exactly one router + it fills in the host automatically; if it finds several you pick from the + list. A `/24` scan can take up to ~2 minutes (255 sequential probes at + 500 ms each on hosts that don't respond). +- **Manual**: type the router's LAN IP (commonly `192.168.1.1` on a router + freshly bridged in, or whatever address it has on your network) plus the + SSH username (default `root`) and password. + +Click **Connect**. On success the panel switches to the status dashboard and +the connection (host + credentials) is persisted server-side — you won't +need to re-enter them on future visits or from other views (e.g. the Home +dashboard's network tile also polls this without prompting again). + +> Credentials are stored in `router_config.json` under the node's data +> directory alongside other node config. There's no separate secrets +> vault entry for this yet — treat the router's SSH password like any other +> node-local config. + +## Step 3: (Optional) Configure WAN/WISP + +Use this to make the OpenWrt router pull its internet connection from an +upstream WiFi network instead of a wired uplink — useful for a +battery/off-grid TollGate node or extending coverage from an existing +network. + +1. From the status dashboard, start the **WAN setup** wizard. +2. **Scan** — the router's radio scans for visible networks (a few seconds + of SSH round-trips). +3. **Select network** — pick the upstream SSID from the list. +4. **Password** — enter the upstream network's WiFi password (encryption + defaults to `psk2`; leave blank only for open networks). +5. **DHCP / NAT** — review the LAN DHCP pool (default `.100`–`.249`) and + whether to enable NAT/masquerade on the WAN zone (leave this on unless + you have a specific reason not to). +6. **Connect** — this writes a `wwan` STA `wifi-iface` + `network` interface + over UCI, enables the radio if it was disabled (OpenWrt ships with + `radio0.disabled=1` on a fresh flash), and adds `wwan` to the WAN + firewall zone. + +The dashboard's WAN panel shows the resulting association state, assigned +IP, and whether the router currently has internet reachability. + +## Step 4: (Optional) Provision TollGate + +Once connected (and with a local Cashu mint app running), the dashboard +shows a **TollGate: not installed** state with a **Provision** action. + +1. Click **Provision TollGate**. +2. Set: + - **Price** (sats) + - **Step size** (minutes — billed in `step_size_ms` under the hood, + default 1 minute) + - **Minimum steps** a customer must buy at once + - **Mint URL** — leave as the auto-filled `http://:3338` unless + you're pointing at an external/different mint +3. Confirm. This can take a few minutes: the node downloads/installs + `tollgate-module-basic-go` on the router (via `opkg` on OpenWrt ≤24.x, or + a manual `.ipk` extract for 25.x images where `opkg` isn't available), + writes `/etc/tollgate/config.json`, and creates the `archipelago` SSID. +4. Once provisioned, the same panel lets you **edit** price/step/mint and + toggle TollGate enabled/disabled without reinstalling — changes are + pushed to `/etc/tollgate/config.json` and the daemon is restarted to + pick them up (it does not hot-reload). + +Anyone who joins the `archipelago` SSID sees TollGate's captive portal and +pays sats (via the configured Cashu mint) for timed access. + +## Reconfiguring or moving to a different router + +Use **Disconnect** on the status dashboard to return to the connect form — +this only clears the panel's client-side state, it doesn't delete the +persisted `router_config.json`, so reconnecting to the same router needs no +re-entry. To point at a *different* router, disconnect and connect with a +new host/credentials; the newly connected router becomes the persisted one. + +## Troubleshooting + +- **"No router configured"**: nothing has been connected yet, or the saved + config didn't include a host — go through Step 2 again. +- **Connect hangs or times out**: the router isn't reachable on `TCP/22` + from the node's network, or SSH auth failed. Confirm you can `ssh + root@` manually from the node (or a machine on the same LAN) + with the same credentials. +- **Router "moved networks" / stale saved host**: SSH/status calls are + bounded (5s TCP connect, 30s read/write) precisely so an unreachable + saved router can't stall other RPCs — but the dashboard will show a + connection error until you reconnect with the router's current address. +- **TollGate provision fails with "No pre-built TollGate package for + architecture..."**: your router's SoC isn't one of the prebuilt + `.ipk` targets (`mips_24kc`, `mipsel_24kc`, `aarch64_cortex-a53`, + `aarch64_cortex-a72`, `arm_cortex-a7`). You'll need a custom opkg feed or + to build `tollgate-module-basic-go` from source for your architecture. +- **TollGate download looks like it succeeded but provisioning still + fails**: the node sanity-checks the downloaded `.ipk` is at least 50 KB — + a smaller file usually means `wget` captured an HTML error page instead + (no internet access from the router, or a bad release URL). + +--- + +## Developer reference + +Backend crate: `core/openwrt` (`archipelago-openwrt`) — SSH/UCI plumbing, +WAN/WISP config, WiFi scanning, and TollGate install/config. See +[`architecture.md`](architecture.md) for where it sits in the workspace. + +RPC methods (`core/archipelago/src/api/rpc/openwrt.rs`, dispatched in +`core/archipelago/src/api/rpc/dispatcher.rs`): + +| Method | Purpose | +|---|---| +| `openwrt.scan` | Probe a subnet for OpenWrt routers (`subnet`, `prefix`, `ssh_user`, `ssh_password`) | +| `openwrt.get-status` | Full status: release, WiFi interfaces, WAN, TollGate state. No params → uses saved `router_config.json`; params with `host` also persist the connection | +| `openwrt.configure-wan` | Write WISP/WAN config (`ssid`, `password`, `encryption`, `dhcp_start`, `dhcp_limit`, `masq`) | +| `openwrt.scan-wifi` | Radio scan for visible upstream networks | +| `openwrt.provision-tollgate` | Install/reconfigure TollGate (`price_sats`, `step_size_ms`, `min_steps`, `mint_url`, `enabled`) | + +Note: these are distinct from the unrelated `router.*` methods +(`router.discover`, `router.configure`, `router.list-forwards`, ...), which +handle UPnP/NAT-PMP port forwarding on the node's own upstream home router — +not the OpenWrt gateway feature described here. + +Frontend: `neode-ui/src/views/server/OpenWrtGateway.vue`, routed at +`server/openwrt` (`neode-ui/src/router/index.ts`), linked from +`neode-ui/src/views/Server.vue`. + +Persisted connection state: `router_config.json` in the node's data +directory (`core/archipelago/src/network/router.rs`: +`load_router_config`/`save_router_config`). -- 2.54.0 From 0646bc4e8563894f9f352f834f2a4421b1743b08 Mon Sep 17 00:00:00 2001 From: ssmithx Date: Sat, 5 Sep 2026 14:26:21 +0000 Subject: [PATCH 2/9] =?UTF-8?q?docs(openwrt):=20add=20GL.iNet=20AX3000=20?= =?UTF-8?q?=E2=86=92=20stock=20OpenWrt=20flashing=20steps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Worked example for the Beryl AX (GL-MT3000, mediatek/filogic) verified against the OpenWrt wiki and firmware selector: exact sysupgrade image filename, GL.iNet UI / LuCI flash path, post-flash SSH state, and the U-Boot recovery procedure if the flash goes sideways. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0176RpCxFNS9ZaSJjL72W9Z5 --- docs/openwrt-gateway-setup.md | 37 ++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/docs/openwrt-gateway-setup.md b/docs/openwrt-gateway-setup.md index f0e0392f..35745717 100644 --- a/docs/openwrt-gateway-setup.md +++ b/docs/openwrt-gateway-setup.md @@ -27,7 +27,8 @@ not flash or install OpenWrt; you bring a router that already runs it. 1. **A router already flashed with OpenWrt.** Check the [OpenWrt Table of Hardware](https://openwrt.org/toh/start) for your model and follow OpenWrt's own install/flashing instructions — that part is - outside Archipelago's scope. + outside Archipelago's scope. See below for a worked example (GL.iNet + AX3000). 2. **SSH reachable.** Fresh OpenWrt images enable `dropbear` (SSH) on LAN by default, listening as `root` with no password (or the password you set during OpenWrt's first-boot wizard at `192.168.1.1`). Archipelago @@ -40,6 +41,40 @@ not flash or install OpenWrt; you bring a router that already runs it. `http://:3338` and TollGate customers must be able to reach that URL from outside the node's loopback. +## Worked example: flashing a GL.iNet AX3000 to stock OpenWrt + +GL.iNet's "AX3000" travel router is the **Beryl AX (GL-MT3000)** — +MediaTek MT7981B (Cortex-A53), OpenWrt target `mediatek/filogic`. It ships +running a GL.iNet fork of OpenWrt with its own web UI and LuCI already +enabled, but the steps below replace that with stock/vanilla OpenWrt so it +matches the prebuilt TollGate `.ipk` architectures exactly +(`aarch64_cortex-a53`). + +1. **Download the sysupgrade image** for the current stable release from + `https://downloads.openwrt.org/releases//targets/mediatek/filogic/` + — the file you want is + `openwrt--mediatek-filogic-glinet_gl-mt3000-squashfs-sysupgrade.bin`. +2. **Verify the checksum** against the `sha256sums` file in that same + directory before flashing anything. +3. **Flash from the GL.iNet UI**: on the router's default address + (`192.168.8.1`), go to **More Settings → Upgrade → Local Upgrade**, or + open **Advanced → LuCI** and use **System → Backup / Flash Firmware → + Flash new firmware image**. +4. Upload the `.bin` file. **Uncheck "Keep Settings"** — going from the + GL.iNet fork to stock OpenWrt needs a clean reset, not a config carry-over. +5. Confirm and wait ~3–5 minutes without power-cycling the router. +6. **After it reboots** you're on stock OpenWrt: LAN at `192.168.1.1`, DHCP + on, SSH (dropbear) open as `root` with **no password set yet** — set one + via LuCI at `192.168.1.1` or `passwd` over SSH before doing anything else. + From here, continue with the Prerequisites/Step 2 flow above to connect + it to the Archipelago node. + +**If the flash fails / the router doesn't come back**: filogic devices +don't use a reset-button recovery. Instead, connect to the router's LAN +port and, during boot, press a key within the first ~2 seconds to enter +U-Boot; per the OpenWrt wiki, typing `gl` then `httpd` at the U-Boot prompt +brings up a recovery web UI at `192.168.1.2` that accepts a firmware image. + ## Step 1: Open the OpenWrt Gateway panel 1. In the Archipelago UI, go to **Server**. -- 2.54.0 From a4ae375617af0b5d6dfcc3768a6486f57ded2a01 Mon Sep 17 00:00:00 2001 From: ssmithx Date: Sat, 5 Sep 2026 14:32:39 +0000 Subject: [PATCH 3/9] =?UTF-8?q?docs(openwrt):=20fix=20TollGate=20step=20?= =?UTF-8?q?=E2=80=94=20install=20is=20separate=20from=20configure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 4 described a single "Provision TollGate" action that prompts for price/step/mint upfront. The real UI (OpenWrtGateway.vue) doesn't work that way: "Install TollGate" is a one-click action with no config form that installs with defaults, and price/step/mint/enabled are only editable afterward via a separate "Edit" panel. Caught while walking through a live install. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0176RpCxFNS9ZaSJjL72W9Z5 --- docs/openwrt-gateway-setup.md | 49 ++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/docs/openwrt-gateway-setup.md b/docs/openwrt-gateway-setup.md index 35745717..2917e8b8 100644 --- a/docs/openwrt-gateway-setup.md +++ b/docs/openwrt-gateway-setup.md @@ -131,27 +131,40 @@ network. The dashboard's WAN panel shows the resulting association state, assigned IP, and whether the router currently has internet reachability. -## Step 4: (Optional) Provision TollGate +## Step 4: (Optional) Install TollGate Once connected (and with a local Cashu mint app running), the dashboard -shows a **TollGate: not installed** state with a **Provision** action. +shows a **TollGate: not installed** panel with a single **Install TollGate** +button — there's no config form at this stage, it installs with defaults. +The panel itself warns: *"Router needs internet access to install TollGate +— configure WAN above first"* (Step 3), since the router has to reach the +internet to download the package. -1. Click **Provision TollGate**. -2. Set: - - **Price** (sats) - - **Step size** (minutes — billed in `step_size_ms` under the hood, - default 1 minute) - - **Minimum steps** a customer must buy at once - - **Mint URL** — leave as the auto-filled `http://:3338` unless - you're pointing at an external/different mint -3. Confirm. This can take a few minutes: the node downloads/installs - `tollgate-module-basic-go` on the router (via `opkg` on OpenWrt ≤24.x, or - a manual `.ipk` extract for 25.x images where `opkg` isn't available), - writes `/etc/tollgate/config.json`, and creates the `archipelago` SSID. -4. Once provisioned, the same panel lets you **edit** price/step/mint and - toggle TollGate enabled/disabled without reinstalling — changes are - pushed to `/etc/tollgate/config.json` and the daemon is restarted to - pick them up (it does not hot-reload). +1. Click **Install TollGate**. The button relabels to *"Installing… this + may take a few minutes"* while it works. +2. Under the hood this installs `tollgate-module-basic-go` on the router + (via `opkg` on OpenWrt ≤24.x, or a manual `.ipk` extract on 25.x images + where `opkg` isn't available), writes `/etc/tollgate/config.json`, and + creates the `archipelago` SSID — all with default pricing (10 sats per + 1-minute step, minimum 1 step, `mint_url` auto-filled to + `http://:3338`, enabled). +3. On success you'll see *"TollGate provisioned successfully"* and the + panel switches to the installed view (Enabled/Disabled badge, current + price/step/mint). + +### Configuring price, step size, or mint (after install) + +The installed-state panel has an **Edit** button — this is the only place +you set price/step/mint, and it only appears once TollGate is already +installed: + +1. Click **Edit**. +2. Set **Price** (sats), **Step size** (minutes — billed as `step_size_ms` + under the hood), **Minimum steps** a customer must buy at once, **Mint + URL** (leave as the auto-filled node URL unless pointing at an external + mint), and the **Enable TollGate** toggle. +3. Click **Save**. Changes are pushed to `/etc/tollgate/config.json` and the + daemon is restarted to pick them up — it does not hot-reload. Anyone who joins the `archipelago` SSID sees TollGate's captive portal and pays sats (via the configured Cashu mint) for timed access. -- 2.54.0 From f3d96ae2ee79587d83af9fe866e4b3bfc02083db Mon Sep 17 00:00:00 2001 From: ssmithx Date: Sat, 5 Sep 2026 15:14:00 +0000 Subject: [PATCH 4/9] fix(openwrt): resolve opkg/apk via $PATH, not a hardcoded /usr/bin path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit opkg_check() and every opkg/apk invocation hardcoded /usr/bin/opkg and /usr/bin/apk. Official OpenWrt images don't all symlink /bin into /usr/bin — the glinet_gl-mt3000 24.10.2 build keeps them as separate real directories with opkg living in /bin — so the check silently missed a perfectly normal install and TollGate provisioning failed with "this router's firmware may not support package management". Switched every call to resolve through the router's own $PATH (command -v / bare opkg / apk) instead. Reproduced and fixed live against archy-x250-pa3, 2026-09-05. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0176RpCxFNS9ZaSJjL72W9Z5 --- core/archipelago/src/api/rpc/openwrt.rs | 2 +- core/openwrt/src/opkg.rs | 34 ++++++++++++++----------- core/openwrt/src/tollgate/install.rs | 4 +-- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/core/archipelago/src/api/rpc/openwrt.rs b/core/archipelago/src/api/rpc/openwrt.rs index 7ab27e30..32ac1ea6 100644 --- a/core/archipelago/src/api/rpc/openwrt.rs +++ b/core/archipelago/src/api/rpc/openwrt.rs @@ -135,7 +135,7 @@ impl RpcHandler { // not /usr/bin/tollgate-module-basic-go — that's only the opkg/apk // *package* name, never an on-disk filename. let tollgate_installed = router - .run("/usr/bin/opkg list-installed 2>/dev/null | grep -q '^tollgate-module-basic-go ' || \ + .run("opkg list-installed 2>/dev/null | grep -q '^tollgate-module-basic-go ' || \ test -f /usr/bin/tollgate-wrt 2>/dev/null") .map(|(_, code)| code == 0) .unwrap_or(false); diff --git a/core/openwrt/src/opkg.rs b/core/openwrt/src/opkg.rs index d62f36cf..332fee32 100644 --- a/core/openwrt/src/opkg.rs +++ b/core/openwrt/src/opkg.rs @@ -15,25 +15,32 @@ pub enum PkgManager { impl Router { /// Detect which package manager is available. /// - /// - If `/usr/bin/opkg` exists → `PkgManager::Opkg` (nothing to do). - /// - If `/usr/bin/apk` exists → run `apk update` (switching repos to HTTP + /// Looks up `opkg`/`apk` via the router's `$PATH` (`command -v`) rather + /// than a hardcoded `/usr/bin/` — official OpenWrt images don't all + /// symlink `/bin` into `/usr/bin` (e.g. the `glinet_gl-mt3000` 24.10.2 + /// build keeps them as separate real directories with `opkg` living in + /// `/bin`), so a fixed absolute path silently misses a perfectly normal + /// install and reports "no package management" (archy-x250-pa3, 2026-09-05). + /// + /// - If `opkg` is on PATH → `PkgManager::Opkg` (nothing to do). + /// - If `apk` is on PATH → run `apk update` (switching repos to HTTP /// first to work around missing CA bundle on fresh images), then try /// `apk add opkg`. If opkg is in the repos → `Opkg`. If not (OpenWrt /// 25.x) → `ApkNative`. /// - Neither found → error. pub fn opkg_check(&self) -> Result { - let (_, code) = self.run("test -x /usr/bin/opkg")?; + let (_, code) = self.run("command -v opkg >/dev/null 2>&1")?; if code == 0 { return Ok(PkgManager::Opkg); } - let (_, apk_code) = self.run("test -x /usr/bin/apk")?; + let (_, apk_code) = self.run("command -v apk >/dev/null 2>&1")?; if apk_code == 0 { info!("[{}] opkg not found — using apk (OpenWrt 25.x+)", self.host); // Fresh images ship without a CA bundle; switch repos to HTTP so // apk's wget can reach the package index without TLS verification. self.run_ok("sed -i 's|https://|http://|g' /etc/apk/repositories 2>/dev/null || true")?; - let (update_out, update_code) = self.run("/usr/bin/apk update 2>&1")?; + let (update_out, update_code) = self.run("apk update 2>&1")?; if update_code != 0 { anyhow::bail!( "apk update failed (exit {}) — router may have no internet access. \ @@ -43,7 +50,7 @@ impl Router { ); } // Try to install opkg (only available on some 25.x builds). - let (add_out, add_code) = self.run("/usr/bin/apk add opkg 2>&1")?; + let (add_out, add_code) = self.run("apk add opkg 2>&1")?; if add_code == 0 { return Ok(PkgManager::Opkg); } @@ -62,7 +69,7 @@ impl Router { } anyhow::bail!( - "opkg not found at /usr/bin/opkg — this router's firmware may not \ + "Neither opkg nor apk found on this router's $PATH — its firmware may not \ support package management (TollGate requires a standard OpenWrt build)" ); } @@ -70,31 +77,28 @@ impl Router { /// `opkg update` — refresh package lists. pub fn opkg_update(&self) -> Result<()> { info!("[{}] opkg update", self.host); - self.run_ok("/usr/bin/opkg update")?; + self.run_ok("opkg update")?; Ok(()) } /// Install a package, skipping if already installed. pub fn opkg_install(&self, package: &str) -> Result<()> { // Check if already installed to avoid unnecessary network traffic. - let (_, code) = self.run(&format!( - "/usr/bin/opkg list-installed | grep -q '^{} '", - package - ))?; + let (_, code) = self.run(&format!("opkg list-installed | grep -q '^{} '", package))?; if code == 0 { info!("[{}] {} already installed", self.host, package); return Ok(()); } info!("[{}] opkg install {}", self.host, package); - self.run_ok(&format!("/usr/bin/opkg install {}", package))?; + self.run_ok(&format!("opkg install {}", package))?; Ok(()) } /// Remove a package. pub fn opkg_remove(&self, package: &str) -> Result<()> { info!("[{}] opkg remove {}", self.host, package); - self.run_ok(&format!("/usr/bin/opkg remove {}", package))?; + self.run_ok(&format!("opkg remove {}", package))?; Ok(()) } @@ -121,7 +125,7 @@ impl Router { } info!("[{}] apk add {}", self.host, package); - self.run_ok(&format!("/usr/bin/apk add {}", package))?; + self.run_ok(&format!("apk add {}", package))?; Ok(()) } } diff --git a/core/openwrt/src/tollgate/install.rs b/core/openwrt/src/tollgate/install.rs index ff26a09e..e74db978 100644 --- a/core/openwrt/src/tollgate/install.rs +++ b/core/openwrt/src/tollgate/install.rs @@ -35,7 +35,7 @@ pub fn install_tollgate(router: &Router) -> Result<()> { // Package not in any feed — download the .ipk directly. let arch = router - .run_ok("/usr/bin/opkg print-architecture | grep -v all | grep -v noarch | tail -1 | awk '{print $2}'")?; + .run_ok("opkg print-architecture | grep -v all | grep -v noarch | tail -1 | awk '{print $2}'")?; let arch = arch.trim(); let url = ipk_url(arch).ok_or_else(|| { @@ -88,7 +88,7 @@ pub fn install_tollgate_apk_native(router: &Router) -> Result<()> { ". /etc/openwrt_release 2>/dev/null \ && a=\"${DISTRIB_ARCH:-${OPENWRT_ARCH:-}}\" \ && [ -n \"$a\" ] && echo \"$a\" \ - || /usr/bin/apk --print-arch 2>/dev/null \ + || apk --print-arch 2>/dev/null \ || uname -m", )?; // Normalise: uname -m returns bare "mipsel"/"mips"; map to 24kc variant -- 2.54.0 From be06b3ce2b4d3edf96997b174b484de0d253b720 Mon Sep 17 00:00:00 2001 From: ssmithx Date: Sat, 5 Sep 2026 15:14:07 +0000 Subject: [PATCH 5/9] fix(ui): stop sending an empty ssh_password over the saved router connection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit provisionTollgate/saveTollgateConfig/scanWifi/configureWan all fell back to the Connect form's local refs (host/sshUser/sshPassword) when connectedParams was null. Those refs only get populated if the form was actually submitted this session — on a normal page load the router reconnects via the server-persisted config instead, leaving sshPassword at its default ''. Sending that as an explicit (empty-but-present) ssh_password overrides the backend's saved-config fallback, so every action auths with a blank password instead of the real saved one. Added authParams(): omit host/ssh_user/ssh_password entirely unless connectedParams is actually set, same as the status poll already does. Caught live: dropbear on archy-x250-pa3's router logged a single bad password attempt at the exact moment "Install TollGate" was clicked, sandwiched between periodic status-poll connections succeeding with the real saved password. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0176RpCxFNS9ZaSJjL72W9Z5 --- neode-ui/src/views/server/OpenWrtGateway.vue | 38 +++++++++++--------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/neode-ui/src/views/server/OpenWrtGateway.vue b/neode-ui/src/views/server/OpenWrtGateway.vue index 5811074d..1905b02e 100644 --- a/neode-ui/src/views/server/OpenWrtGateway.vue +++ b/neode-ui/src/views/server/OpenWrtGateway.vue @@ -115,6 +115,24 @@ const showConnectForm = ref(false) const connecting = ref(false) const connectedParams = ref | null>(null) +// Every action below (install/edit TollGate, WiFi scan, WAN configure) needs +// host/ssh_user/ssh_password to reach the router. `connectedParams` only gets +// set when the Connect form was actually submitted this session (WR-03 above) +// — on a normal page load the router reconnects via the server-persisted +// config instead, so `sshPassword`/`sshUser`/`host` (the Connect form's own +// local refs) sit at their untouched defaults ('', 'root', ''). Falling back +// to those refs here used to send an explicit-but-empty ssh_password, which +// the backend treats as "the caller provided this" and never falls back to +// the real saved password — a real router password then fails auth on every +// action even though the status poll (which sends no params at all) keeps +// working fine (archy-x250-pa3, 2026-09-05: dropbear logged one bad-password +// attempt at the exact moment "Install TollGate" was clicked). Omitting the +// fields entirely when there's no explicit connectedParams lets the backend's +// own saved-config fallback do the right thing, same as the status poll. +function authParams(): Record { + return connectedParams.value ?? {} +} + const detecting = ref(false) const detectError = ref('') const detectedCandidates = ref([]) @@ -271,11 +289,7 @@ async function provisionTollgate() { provisionError.value = '' provisionSuccess.value = false try { - const params: Record = { - host: connectedParams.value?.host ?? status.value?.host, - ssh_user: connectedParams.value?.ssh_user ?? sshUser.value, - ssh_password: connectedParams.value?.ssh_password ?? sshPassword.value, - } + const params: Record = { ...authParams() } await rpcClient.call({ method: 'openwrt.provision-tollgate', params, timeout: 300000 }) provisionSuccess.value = true await load(connectedParams.value ?? undefined) @@ -302,9 +316,7 @@ async function saveTollgateConfig() { updateTollgateError.value = '' try { const params: Record = { - host: connectedParams.value?.host ?? status.value?.host, - ssh_user: connectedParams.value?.ssh_user ?? sshUser.value, - ssh_password: connectedParams.value?.ssh_password ?? sshPassword.value, + ...authParams(), price_sats: editPriceSats.value, step_size_ms: editStepSizeMin.value * 60_000, min_steps: editMinSteps.value, @@ -336,11 +348,7 @@ async function scanWifi() { wanStep.value = 'scanning' wanError.value = '' try { - const params: Record = { - host: connectedParams.value?.host ?? status.value?.host, - ssh_user: connectedParams.value?.ssh_user ?? sshUser.value, - ssh_password: connectedParams.value?.ssh_password ?? sshPassword.value, - } + const params: Record = { ...authParams() } const result = await rpcClient.call<{ networks: ScannedNetwork[] }>({ method: 'openwrt.scan-wifi', params, @@ -367,9 +375,7 @@ async function configureWan() { wanError.value = '' try { const params: Record = { - host: connectedParams.value?.host ?? status.value?.host, - ssh_user: connectedParams.value?.ssh_user ?? sshUser.value, - ssh_password: connectedParams.value?.ssh_password ?? sshPassword.value, + ...authParams(), ssid: selectedNetwork.value.ssid, password: wanPassword.value, encryption: selectedNetwork.value.encryption, -- 2.54.0 From 61b5d93b1102512a17b66791a8fc4bfb436b0aa7 Mon Sep 17 00:00:00 2001 From: ssmithx Date: Sat, 5 Sep 2026 16:36:53 +0000 Subject: [PATCH 6/9] docs(openwrt): document the transient post-reboot apk-update failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Observed live on archy-x250-pa3: right after WAN reconnects (fresh boot or WAN reconfigure), the first Install attempt can fail with "apk update failed ... router may have no internet access" purely because the WiFi-uplink STA association hasn't finished yet — it's not a real error, just retry a few seconds later. Also cross-referenced the now-fixed /usr/bin/opkg hardcoding bug for anyone hitting it on an older build. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0176RpCxFNS9ZaSJjL72W9Z5 --- docs/openwrt-gateway-setup.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/openwrt-gateway-setup.md b/docs/openwrt-gateway-setup.md index 2917e8b8..37e11ad6 100644 --- a/docs/openwrt-gateway-setup.md +++ b/docs/openwrt-gateway-setup.md @@ -198,6 +198,18 @@ new host/credentials; the newly connected router becomes the persisted one. fails**: the node sanity-checks the downloaded `.ipk` is at least 50 KB — a smaller file usually means `wget` captured an HTML error page instead (no internet access from the router, or a bad release URL). +- **Install fails right after a reboot or a fresh WAN setup** with `apk + update failed ... router may have no internet access` even though WAN + looks configured: this is usually just timing, not a real problem — the + router's WiFi-uplink association (`wwan`/`hakodosh`-style STA interface) + can take a few seconds longer to reconnect than the dashboard takes to + let you click Install. Wait ~10–15 seconds after WAN shows `sta_state: + up` and retry; it should succeed on the next attempt. +- **Install fails with `opkg not found at /usr/bin/opkg` (or similar) even + though the router clearly has `opkg`/`apk` installed**: fixed as of + 2026-09-05 — the backend used to hardcode `/usr/bin/opkg`/`/usr/bin/apk`, + which some official OpenWrt builds don't symlink into `/bin`. If you're + running an Archipelago build from before that fix, update first. --- -- 2.54.0 From 4fdf8e8c581bba693a1aec8cd1ac618349ebef68 Mon Sep 17 00:00:00 2001 From: ssmithx Date: Sat, 5 Sep 2026 17:28:20 +0000 Subject: [PATCH 7/9] fix(openwrt): bump pinned TollGate release v0.2.0 -> v0.5.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The install code was hardcoded to the Oct 2025 v0.2.0 release — nine releases behind. Its changelog covers exactly the failures hit live against archy-x250-pa3: a mint with an empty/broken keyset crash-looped tollgate-wrt forever (v0.5.0 adds "graceful degradation when Cashu mints fail"), and the bundled captive-portal JS had zero CBOR support, hard-rejecting the cashuB (NUT-00 V4) tokens modern wallets like Minibits generate by default. Also: v0.5.0 publishes native .apk packages for aarch64_cortex-a53 and x86_64. install_tollgate_apk_native now prefers those directly (apk add handles deps/postinst/uci-defaults itself) instead of always falling back to the manual ar/tar .ipk extraction dance, which only exists because earlier releases had no native apk build at all. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0176RpCxFNS9ZaSJjL72W9Z5 --- core/openwrt/src/tollgate/install.rs | 90 ++++++++++++++++++++++++---- 1 file changed, 79 insertions(+), 11 deletions(-) diff --git a/core/openwrt/src/tollgate/install.rs b/core/openwrt/src/tollgate/install.rs index e74db978..3dc21c28 100644 --- a/core/openwrt/src/tollgate/install.rs +++ b/core/openwrt/src/tollgate/install.rs @@ -6,18 +6,53 @@ use crate::Router; /// The OpenWrt package name for the TollGate reference implementation. const TOLLGATE_PACKAGE: &str = "tollgate-module-basic-go"; -/// Direct-download fallback URLs by opkg architecture string. +/// Pinned upstream release. Was stuck on v0.2.0 (Oct 2025) until 2026-09-05 — +/// nine releases behind. v0.5.0's changelog covers exactly the failure modes +/// hit live against archy-x250-pa3: a mint with an empty/broken keyset used +/// to crash-loop the daemon forever ("graceful degradation when Cashu mints +/// fail" in v0.5.0), and the bundled captive-portal build had no CBOR support +/// at all, so it could only decode legacy `cashuA` tokens — rejecting the +/// `cashuB` (NUT-00 V4) tokens modern wallets like Minibits generate by +/// default ("portal improvements" in v0.5.0 include a JS bundle update that +/// should carry a current cashu-ts with V4 support). Bump this string to move +/// both this crate's URLs and the version baked into the source comments. +const TOLLGATE_VERSION: &str = "v0.5.0"; + +/// Direct-download fallback URLs by opkg architecture string, for the +/// `.ipk` (ar-archive) package format. /// Used when the package is not in any configured feed. -/// Source: https://github.com/OpenTollGate/tollgate-module-basic-go/releases/tag/v0.2.0 -fn ipk_url(arch: &str) -> Option<&'static str> { - match arch { - "mips_24kc" => Some("https://github.com/OpenTollGate/tollgate-module-basic-go/releases/download/v0.2.0/mips_24kc.ipk"), - "mipsel_24kc" => Some("https://github.com/OpenTollGate/tollgate-module-basic-go/releases/download/v0.2.0/mipsel_24kc.ipk"), - "aarch64_cortex-a53" => Some("https://github.com/OpenTollGate/tollgate-module-basic-go/releases/download/v0.2.0/aarch64_cortex-a53.ipk"), - "aarch64_cortex-a72" => Some("https://github.com/OpenTollGate/tollgate-module-basic-go/releases/download/v0.2.0/aarch64_cortex-a72.ipk"), - "arm_cortex-a7" => Some("https://github.com/OpenTollGate/tollgate-module-basic-go/releases/download/v0.2.0/arm_cortex-a7.ipk"), - _ => None, - } +/// Source: https://github.com/OpenTollGate/tollgate-module-basic-go/releases/tag/v0.5.0 +fn ipk_url(arch: &str) -> Option { + let name = match arch { + "mips_24kc" => "mips_24kc", + "mipsel_24kc" => "mipsel_24kc", + "aarch64_cortex-a53" => "aarch64_cortex-a53", + "aarch64_cortex-a72" => "aarch64_cortex-a72", + "arm_cortex-a7" => "arm_cortex-a7", + "x86_64" => "x86_64", + _ => return None, + }; + Some(format!( + "https://github.com/OpenTollGate/tollgate-module-basic-go/releases/download/{TOLLGATE_VERSION}/tollgate-wrt_{TOLLGATE_VERSION}_{name}.ipk" + )) +} + +/// Direct-download URLs for the native Alpine-style `.apk` package format — +/// only published for a subset of architectures as of v0.5.0. Where +/// available this is strictly better than [`ipk_url`] on an apk-native +/// (OpenWrt 25.x+) router: `apk add` installs it directly (dependency +/// resolution, postinst, uci-defaults all handled by apk itself), instead of +/// the manual `ar`/`tar` extraction dance `install_ipk` has to do to unpack +/// an `.ipk` on a router with no `opkg`. +fn apk_url(arch: &str) -> Option { + let name = match arch { + "aarch64_cortex-a53" => "aarch64_cortex-a53", + "x86_64" => "x86_64", + _ => return None, + }; + Some(format!( + "https://github.com/OpenTollGate/tollgate-module-basic-go/releases/download/{TOLLGATE_VERSION}/tollgate-wrt_{TOLLGATE_VERSION}_{name}.apk" + )) } /// Install tollgate-module-basic-go via opkg (OpenWrt ≤24.x). @@ -103,6 +138,39 @@ pub fn install_tollgate_apk_native(router: &Router) -> Result<()> { anyhow::bail!("Could not determine router architecture"); } + // Prefer a native .apk when the release publishes one for this arch — + // `apk add` handles the install itself (deps, postinst, uci-defaults), + // skipping the manual ar/tar extraction the .ipk fallback below needs. + if let Some(url) = apk_url(arch) { + info!( + "[{}] Downloading native TollGate .apk for {} from GitHub releases", + router.host, arch + ); + let (dl_out, dl_code) = router.run(&format!( + "wget --no-check-certificate -O /tmp/tollgate.apk '{}' 2>&1", + url + ))?; + if dl_code != 0 { + anyhow::bail!("TollGate .apk download failed: {}", dl_out.trim()); + } + let (size_out, _) = router.run("wc -c < /tmp/tollgate.apk 2>/dev/null")?; + let size: u64 = size_out.trim().parse().unwrap_or(0); + if size < 50_000 { + anyhow::bail!( + "Downloaded TollGate .apk is only {}B — wget likely captured an error page. \ + Check router internet access and that the release URL is reachable.", + size + ); + } + let (add_out, add_code) = + router.run("apk add --allow-untrusted /tmp/tollgate.apk 2>&1")?; + router.run_ok("rm -f /tmp/tollgate.apk")?; + if add_code != 0 { + anyhow::bail!("TollGate .apk install failed: {}", add_out.trim()); + } + return Ok(()); + } + let url = ipk_url(arch).ok_or_else(|| { anyhow::anyhow!( "No pre-built TollGate package for architecture '{}'. \ -- 2.54.0 From da8c3ec1937b6a7c7b5124f7e99913be63fa04a0 Mon Sep 17 00:00:00 2001 From: ssmithx Date: Mon, 7 Sep 2026 02:53:21 +0000 Subject: [PATCH 8/9] docs(openwrt): note the Ctrl+T/LuCI workaround for setting the initial root password Archipelago's Connect form only authenticates with an existing password; it has no flow for setting one on a fresh, passwordless router. On the node's kiosk display there's no visible tab bar, so Ctrl+T to open a new tab to LuCI is the way to set it before Connect will work. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0176RpCxFNS9ZaSJjL72W9Z5 --- docs/openwrt-gateway-setup.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/openwrt-gateway-setup.md b/docs/openwrt-gateway-setup.md index 37e11ad6..40c1ce2e 100644 --- a/docs/openwrt-gateway-setup.md +++ b/docs/openwrt-gateway-setup.md @@ -69,6 +69,15 @@ matches the prebuilt TollGate `.ipk` architectures exactly From here, continue with the Prerequisites/Step 2 flow above to connect it to the Archipelago node. + > The Archipelago UI's Connect form (Step 2) authenticates *with* a + > password — it has no flow for setting the initial one on a fresh, + > passwordless router. You have to set it out-of-band first. If you're + > working from the node's own local kiosk display rather than a normal + > desktop browser, there's no visible tab bar/address bar to open a new + > tab from — press **Ctrl+T** to open one anyway, navigate to + > `192.168.1.1`, and use LuCI's first-boot prompt to set the root + > password. Then switch back to the Archipelago tab and Connect with it. + **If the flash fails / the router doesn't come back**: filogic devices don't use a reset-button recovery. Instead, connect to the router's LAN port and, during boot, press a key within the first ~2 seconds to enter -- 2.54.0 From 094f42312c41e731e7f196e6c7dba26d3c5b079e Mon Sep 17 00:00:00 2001 From: ssmithx Date: Mon, 7 Sep 2026 03:06:58 +0000 Subject: [PATCH 9/9] docs(openwrt): document the confirmed working end-to-end install flow Adds a verification checklist (service running, nodogsplash bound to br-tollgate not br-lan via the rendered config not just UCI, LAN/SSH untouched, mint probes succeeding) plus notes on the dev-build test-mint injection and the default-route race between a router's LAN interface and the node's other uplinks before the router's own WAN/WISP is live. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0176RpCxFNS9ZaSJjL72W9Z5 --- docs/openwrt-gateway-setup.md | 46 +++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/docs/openwrt-gateway-setup.md b/docs/openwrt-gateway-setup.md index 40c1ce2e..80f688de 100644 --- a/docs/openwrt-gateway-setup.md +++ b/docs/openwrt-gateway-setup.md @@ -178,6 +178,52 @@ installed: Anyone who joins the `archipelago` SSID sees TollGate's captive portal and pays sats (via the configured Cashu mint) for timed access. +## Verifying a successful install + +A clean install (flash → Connect → WAN/WISP → Install TollGate, all through +the UI as above) ends in this state — worth checking if you want to confirm +everything actually landed correctly rather than trusting the UI's success +toast alone: + +- `tollgate-wrt` is running (`/etc/init.d/tollgate-wrt status` → `running`). +- nodogsplash's **rendered** config — not just the UCI source — has + `GatewayInterface br-tollgate`. Check the actual file the daemon was + started with (typically `/tmp/etc/nodogsplash_main.conf`), since that's + what's actually enforced, not `uci show nodogsplash`. This matters because + provisioning must stop nodogsplash and reconfigure it to gate the + `br-tollgate` bridge *before* starting it — installing the package by hand + (bypassing the UI/RPC flow) leaves nodogsplash on its default + `br-lan`-gating behavior instead, which locks out the router's own + admin/SSH access. If you ever see a router become unreachable right after + a TollGate install, this is the first thing to check. +- The router's own LAN (the interface you manage it over — SSH, ping) is + still reachable and untouched by the portal. +- TollGate's own log (`logread | grep tollgate-wrt`) shows successful mint + probes for each configured mint. + +A `dev build detected (branch=unknown), injecting test mint: +https://nofee.testnut.cashu.space` line in that log means the installed +build considers itself a dev build and silently adds a test mint alongside +your configured one(s) — check the Edit panel's Mint URL afterward if you +don't want that test mint accepted. + +### A note on network topology during setup + +If the Archipelago node reaches the router over the same wired interface the +router uses as its LAN, expect the router to become the node's default +route on that interface once it has its own working WAN/WISP uplink — this +is normal and, once WAN is actually configured with internet access, works +fine end-to-end (the node's traffic routes out through the router's +uplink). It's only a problem *before* WAN is configured: a freshly flashed +or freshly factory-reset router has no upstream internet yet, so if it wins +the node's default-route race (lowest metric on its own interface) while +still offline, it creates a dead-end route and the node loses its own +connectivity (including anything tunneled, e.g. a VPN/mesh network the node +relies on) until that route is removed or the router gets its uplink +working. If you hit this, either wait until WAN/WISP is actually up before +letting the router's interface win the route race, or temporarily lower the +priority of that route until it is. + ## Reconfiguring or moving to a different router Use **Disconnect** on the status dashboard to return to the connect form — -- 2.54.0