Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be06b3ce2b | ||
|
|
f3d96ae2ee | ||
|
|
a4ae375617 | ||
|
|
0646bc4e85 | ||
|
|
0faaf4577f |
@@ -135,7 +135,7 @@ impl RpcHandler {
|
|||||||
// not /usr/bin/tollgate-module-basic-go — that's only the opkg/apk
|
// not /usr/bin/tollgate-module-basic-go — that's only the opkg/apk
|
||||||
// *package* name, never an on-disk filename.
|
// *package* name, never an on-disk filename.
|
||||||
let tollgate_installed = router
|
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")
|
test -f /usr/bin/tollgate-wrt 2>/dev/null")
|
||||||
.map(|(_, code)| code == 0)
|
.map(|(_, code)| code == 0)
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|||||||
+19
-15
@@ -15,25 +15,32 @@ pub enum PkgManager {
|
|||||||
impl Router {
|
impl Router {
|
||||||
/// Detect which package manager is available.
|
/// Detect which package manager is available.
|
||||||
///
|
///
|
||||||
/// - If `/usr/bin/opkg` exists → `PkgManager::Opkg` (nothing to do).
|
/// Looks up `opkg`/`apk` via the router's `$PATH` (`command -v`) rather
|
||||||
/// - If `/usr/bin/apk` exists → run `apk update` (switching repos to HTTP
|
/// 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
|
/// 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
|
/// `apk add opkg`. If opkg is in the repos → `Opkg`. If not (OpenWrt
|
||||||
/// 25.x) → `ApkNative`.
|
/// 25.x) → `ApkNative`.
|
||||||
/// - Neither found → error.
|
/// - Neither found → error.
|
||||||
pub fn opkg_check(&self) -> Result<PkgManager> {
|
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 {
|
if code == 0 {
|
||||||
return Ok(PkgManager::Opkg);
|
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 {
|
if apk_code == 0 {
|
||||||
info!("[{}] opkg not found — using apk (OpenWrt 25.x+)", self.host);
|
info!("[{}] opkg not found — using apk (OpenWrt 25.x+)", self.host);
|
||||||
// Fresh images ship without a CA bundle; switch repos to HTTP so
|
// Fresh images ship without a CA bundle; switch repos to HTTP so
|
||||||
// apk's wget can reach the package index without TLS verification.
|
// 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")?;
|
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 {
|
if update_code != 0 {
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
"apk update failed (exit {}) — router may have no internet access. \
|
"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).
|
// 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 {
|
if add_code == 0 {
|
||||||
return Ok(PkgManager::Opkg);
|
return Ok(PkgManager::Opkg);
|
||||||
}
|
}
|
||||||
@@ -62,7 +69,7 @@ impl Router {
|
|||||||
}
|
}
|
||||||
|
|
||||||
anyhow::bail!(
|
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)"
|
support package management (TollGate requires a standard OpenWrt build)"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -70,31 +77,28 @@ impl Router {
|
|||||||
/// `opkg update` — refresh package lists.
|
/// `opkg update` — refresh package lists.
|
||||||
pub fn opkg_update(&self) -> Result<()> {
|
pub fn opkg_update(&self) -> Result<()> {
|
||||||
info!("[{}] opkg update", self.host);
|
info!("[{}] opkg update", self.host);
|
||||||
self.run_ok("/usr/bin/opkg update")?;
|
self.run_ok("opkg update")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Install a package, skipping if already installed.
|
/// Install a package, skipping if already installed.
|
||||||
pub fn opkg_install(&self, package: &str) -> Result<()> {
|
pub fn opkg_install(&self, package: &str) -> Result<()> {
|
||||||
// Check if already installed to avoid unnecessary network traffic.
|
// Check if already installed to avoid unnecessary network traffic.
|
||||||
let (_, code) = self.run(&format!(
|
let (_, code) = self.run(&format!("opkg list-installed | grep -q '^{} '", package))?;
|
||||||
"/usr/bin/opkg list-installed | grep -q '^{} '",
|
|
||||||
package
|
|
||||||
))?;
|
|
||||||
if code == 0 {
|
if code == 0 {
|
||||||
info!("[{}] {} already installed", self.host, package);
|
info!("[{}] {} already installed", self.host, package);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("[{}] opkg install {}", self.host, package);
|
info!("[{}] opkg install {}", self.host, package);
|
||||||
self.run_ok(&format!("/usr/bin/opkg install {}", package))?;
|
self.run_ok(&format!("opkg install {}", package))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove a package.
|
/// Remove a package.
|
||||||
pub fn opkg_remove(&self, package: &str) -> Result<()> {
|
pub fn opkg_remove(&self, package: &str) -> Result<()> {
|
||||||
info!("[{}] opkg remove {}", self.host, package);
|
info!("[{}] opkg remove {}", self.host, package);
|
||||||
self.run_ok(&format!("/usr/bin/opkg remove {}", package))?;
|
self.run_ok(&format!("opkg remove {}", package))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +125,7 @@ impl Router {
|
|||||||
}
|
}
|
||||||
|
|
||||||
info!("[{}] apk add {}", self.host, package);
|
info!("[{}] apk add {}", self.host, package);
|
||||||
self.run_ok(&format!("/usr/bin/apk add {}", package))?;
|
self.run_ok(&format!("apk add {}", package))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ pub fn install_tollgate(router: &Router) -> Result<()> {
|
|||||||
|
|
||||||
// Package not in any feed — download the .ipk directly.
|
// Package not in any feed — download the .ipk directly.
|
||||||
let arch = router
|
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 arch = arch.trim();
|
||||||
|
|
||||||
let url = ipk_url(arch).ok_or_else(|| {
|
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 \
|
". /etc/openwrt_release 2>/dev/null \
|
||||||
&& a=\"${DISTRIB_ARCH:-${OPENWRT_ARCH:-}}\" \
|
&& a=\"${DISTRIB_ARCH:-${OPENWRT_ARCH:-}}\" \
|
||||||
&& [ -n \"$a\" ] && echo \"$a\" \
|
&& [ -n \"$a\" ] && echo \"$a\" \
|
||||||
|| /usr/bin/apk --print-arch 2>/dev/null \
|
|| apk --print-arch 2>/dev/null \
|
||||||
|| uname -m",
|
|| uname -m",
|
||||||
)?;
|
)?;
|
||||||
// Normalise: uname -m returns bare "mipsel"/"mips"; map to 24kc variant
|
// Normalise: uname -m returns bare "mipsel"/"mips"; map to 24kc variant
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ disagree, the code wins and the doc is a bug.
|
|||||||
- [Talking to your node](COMMANDS.md) — the conversational command surface
|
- [Talking to your node](COMMANDS.md) — the conversational command surface
|
||||||
- [Seed Verification](SEED-VERIFICATION.md) — independently verify your 24-word backup
|
- [Seed Verification](SEED-VERIFICATION.md) — independently verify your 24-word backup
|
||||||
- [Troubleshooting](troubleshooting.md) — common problems and how to resolve them
|
- [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
|
- [Gamepad / Controller Navigation](GAMEPAD-NAV.md) — driving the UI from a controller
|
||||||
- [Pine voice commands](pine-voice-commands.md) — the voice-satellite phrase surface
|
- [Pine voice commands](pine-voice-commands.md) — the voice-satellite phrase surface
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,232 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
## 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).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 connecting = ref(false)
|
||||||
const connectedParams = ref<Record<string, string> | null>(null)
|
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 detecting = ref(false)
|
||||||
const detectError = ref('')
|
const detectError = ref('')
|
||||||
const detectedCandidates = ref<string[]>([])
|
const detectedCandidates = ref<string[]>([])
|
||||||
@@ -271,11 +289,7 @@ async function provisionTollgate() {
|
|||||||
provisionError.value = ''
|
provisionError.value = ''
|
||||||
provisionSuccess.value = false
|
provisionSuccess.value = false
|
||||||
try {
|
try {
|
||||||
const params: Record<string, unknown> = {
|
const params: Record<string, unknown> = { ...authParams() }
|
||||||
host: connectedParams.value?.host ?? status.value?.host,
|
|
||||||
ssh_user: connectedParams.value?.ssh_user ?? sshUser.value,
|
|
||||||
ssh_password: connectedParams.value?.ssh_password ?? sshPassword.value,
|
|
||||||
}
|
|
||||||
await rpcClient.call({ method: 'openwrt.provision-tollgate', params, timeout: 300000 })
|
await rpcClient.call({ method: 'openwrt.provision-tollgate', params, timeout: 300000 })
|
||||||
provisionSuccess.value = true
|
provisionSuccess.value = true
|
||||||
await load(connectedParams.value ?? undefined)
|
await load(connectedParams.value ?? undefined)
|
||||||
@@ -302,9 +316,7 @@ async function saveTollgateConfig() {
|
|||||||
updateTollgateError.value = ''
|
updateTollgateError.value = ''
|
||||||
try {
|
try {
|
||||||
const params: Record<string, unknown> = {
|
const params: Record<string, unknown> = {
|
||||||
host: connectedParams.value?.host ?? status.value?.host,
|
...authParams(),
|
||||||
ssh_user: connectedParams.value?.ssh_user ?? sshUser.value,
|
|
||||||
ssh_password: connectedParams.value?.ssh_password ?? sshPassword.value,
|
|
||||||
price_sats: editPriceSats.value,
|
price_sats: editPriceSats.value,
|
||||||
step_size_ms: editStepSizeMin.value * 60_000,
|
step_size_ms: editStepSizeMin.value * 60_000,
|
||||||
min_steps: editMinSteps.value,
|
min_steps: editMinSteps.value,
|
||||||
@@ -336,11 +348,7 @@ async function scanWifi() {
|
|||||||
wanStep.value = 'scanning'
|
wanStep.value = 'scanning'
|
||||||
wanError.value = ''
|
wanError.value = ''
|
||||||
try {
|
try {
|
||||||
const params: Record<string, unknown> = {
|
const params: Record<string, unknown> = { ...authParams() }
|
||||||
host: connectedParams.value?.host ?? status.value?.host,
|
|
||||||
ssh_user: connectedParams.value?.ssh_user ?? sshUser.value,
|
|
||||||
ssh_password: connectedParams.value?.ssh_password ?? sshPassword.value,
|
|
||||||
}
|
|
||||||
const result = await rpcClient.call<{ networks: ScannedNetwork[] }>({
|
const result = await rpcClient.call<{ networks: ScannedNetwork[] }>({
|
||||||
method: 'openwrt.scan-wifi',
|
method: 'openwrt.scan-wifi',
|
||||||
params,
|
params,
|
||||||
@@ -367,9 +375,7 @@ async function configureWan() {
|
|||||||
wanError.value = ''
|
wanError.value = ''
|
||||||
try {
|
try {
|
||||||
const params: Record<string, unknown> = {
|
const params: Record<string, unknown> = {
|
||||||
host: connectedParams.value?.host ?? status.value?.host,
|
...authParams(),
|
||||||
ssh_user: connectedParams.value?.ssh_user ?? sshUser.value,
|
|
||||||
ssh_password: connectedParams.value?.ssh_password ?? sshPassword.value,
|
|
||||||
ssid: selectedNetwork.value.ssid,
|
ssid: selectedNetwork.value.ssid,
|
||||||
password: wanPassword.value,
|
password: wanPassword.value,
|
||||||
encryption: selectedNetwork.value.encryption,
|
encryption: selectedNetwork.value.encryption,
|
||||||
|
|||||||
Reference in New Issue
Block a user