Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
094f42312c | ||
|
|
da8c3ec193 | ||
|
|
4fdf8e8c58 | ||
|
|
61b5d93b11 | ||
|
|
be06b3ce2b | ||
|
|
f3d96ae2ee | ||
|
|
a4ae375617 | ||
|
|
0646bc4e85 | ||
|
|
0faaf4577f |
@@ -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);
|
||||
|
||||
+19
-15
@@ -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/<tool>` — 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<PkgManager> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> {
|
||||
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<String> {
|
||||
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).
|
||||
@@ -35,7 +70,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 +123,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
|
||||
@@ -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 '{}'. \
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
# 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. 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
|
||||
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://<node-ip>: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/<version>/targets/mediatek/filogic/`
|
||||
— the file you want is
|
||||
`openwrt-<version>-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.
|
||||
|
||||
> 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
|
||||
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. 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) Install TollGate
|
||||
|
||||
Once connected (and with a local Cashu mint app running), the dashboard
|
||||
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 **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://<node-ip>: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.
|
||||
|
||||
## 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 —
|
||||
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@<router-ip>` 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).
|
||||
- **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.
|
||||
|
||||
---
|
||||
|
||||
## 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`).
|
||||
@@ -115,6 +115,24 @@ const showConnectForm = ref(false)
|
||||
const connecting = ref(false)
|
||||
const connectedParams = ref<Record<string, string> | 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<string, string> {
|
||||
return connectedParams.value ?? {}
|
||||
}
|
||||
|
||||
const detecting = ref(false)
|
||||
const detectError = ref('')
|
||||
const detectedCandidates = ref<string[]>([])
|
||||
@@ -271,11 +289,7 @@ async function provisionTollgate() {
|
||||
provisionError.value = ''
|
||||
provisionSuccess.value = false
|
||||
try {
|
||||
const params: Record<string, unknown> = {
|
||||
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<string, unknown> = { ...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<string, unknown> = {
|
||||
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<string, unknown> = {
|
||||
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<string, unknown> = { ...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<string, unknown> = {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user