fix(openwrt): close TollGate free-access gap and mint URL mismatch

Two bugs found live against archy-x250-pa3: TollGate-3458 (the upstream
tollgate-module-basic-go installer's own default AP, rebranded from
OpenWrt's factory default wireless.default_radioN sections) was left
bound to `network=lan` — wide open, unmetered, and sharing the router's
admin LAN — because install_ipk() runs the upstream package's own
uci-defaults scripts but nothing reconciled the AP they create with the
separate `tollgate` network/bridge/firewall this project's own
provision_ssid() sets up for the "archipelago" SSID. Fixed by folding any
default_radioN section left on `lan` onto the `tollgate` network right
after it's created.

Separately, a caller-supplied mint_url with a trailing slash
(https://mint.minibits.cash/Bitcoin/) got written byte-for-byte into
accepted_mints[0].url, which tollgate-wrt string-compares exactly against
a token's embedded (slash-less) mint URL — rejecting every otherwise-valid
token as an "untrusted mint". Fixed by trimming trailing slashes before
the value is used anywhere.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KTdMfVJChCwCCYF1ZTRQLc
This commit is contained in:
2026-09-08 21:06:36 -04:00
committed by archipelago
co-authored by Claude Sonnet 5
parent db52c06a72
commit 2947277205
3 changed files with 56 additions and 0 deletions
+8
View File
@@ -240,10 +240,18 @@ impl RpcHandler {
.unwrap_or_default(); .unwrap_or_default();
let default_mint_url = format!("http://{}:{}", self.config.host_ip, LOCAL_MINT_PORT); let default_mint_url = format!("http://{}:{}", self.config.host_ip, LOCAL_MINT_PORT);
// Trim trailing slash(es): tollgate-wrt matches a token's embedded
// mint URL against this value with an exact string compare, and
// Cashu wallets (Minibits included) encode mint URLs without a
// trailing slash. A stray slash here means every otherwise-valid
// token gets rejected as "untrusted mint" — confirmed live against
// archy-x250-pa3 2026-09-07 with a manually-entered
// "https://mint.minibits.cash/Bitcoin/".
let mint_url = p let mint_url = p
.get("mint_url") .get("mint_url")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.unwrap_or(&default_mint_url) .unwrap_or(&default_mint_url)
.trim_end_matches('/')
.to_string(); .to_string();
let config = TollGateConfig { let config = TollGateConfig {
+5
View File
@@ -59,6 +59,11 @@ pub async fn provision(router: &Router, config: &TollGateConfig) -> Result<()> {
config::apply(router, config)?; config::apply(router, config)?;
wifi::provision_ssid(router, config)?; wifi::provision_ssid(router, config)?;
// Must come after provision_ssid (creates the `tollgate` network this
// folds the upstream installer's own default AP onto) — see
// regate_upstream_default_aps for why this is needed at all.
wifi::regate_upstream_default_aps(router)
.context("re-gate upstream tollgate-module-basic-go default AP(s)")?;
// Must come after provision_ssid (which creates br-tollgate) and before // Must come after provision_ssid (which creates br-tollgate) and before
// the daemon restart below — config.json is only read at startup. // the daemon restart below — config.json is only read at startup.
config::apply_daemon_config(router, config) config::apply_daemon_config(router, config)
+43
View File
@@ -118,6 +118,49 @@ fn provision_firewall(router: &Router) -> Result<()> {
Ok(()) Ok(())
} }
/// Fold the upstream `tollgate-module-basic-go` installer's own default
/// AP(s) onto the gated `tollgate` network.
///
/// `install::install_ipk` runs the package's `/etc/uci-defaults/*` first-boot
/// scripts itself (no real package manager to trigger them on OpenWrt 25.x —
/// see its doc comment). Those upstream scripts rebrand OpenWrt's
/// factory-default wifi sections (`wireless.default_radioN`, present on
/// every fresh install) to a `TollGate-<serial>` SSID, but only ever touch
/// the SSID — they leave `network` at its original `lan` binding. Nothing
/// else in this project's own provisioning (`provision_ssid` above) ever
/// looks at those sections; it only manages the separate `wireless.tollgate`
/// SSID it creates itself. Left alone, the result is two open SSIDs
/// broadcasting side by side: ours (gated by NoDogSplash) and upstream's
/// (wide open on `lan`, with a direct route to whatever's plugged into the
/// wired LAN port).
///
/// Confirmed live against archy-x250-pa3 2026-09-07: a client joining
/// "TollGate-3458" landed on `br-lan` with unrestricted WAN forwarding and
/// zero NoDogSplash involvement — free, unmetered internet, no captive
/// portal, on the router's own admin network.
///
/// Must run after `provision_network` (needs the `tollgate` network/bridge
/// to already exist) and before the network/wifi restart in
/// `restart_services` picks the new binding up.
pub fn regate_upstream_default_aps(router: &Router) -> Result<()> {
let sections = router.run_ok(
"uci show wireless 2>/dev/null | grep -o '^wireless\\.default_radio[0-9]*' | sort -u",
)?;
for section in sections.lines().map(str::trim).filter(|s| !s.is_empty()) {
let network_key = format!("{}.network", section);
let current = router.uci_get(&network_key).unwrap_or_default();
if current == "lan" {
info!(
"[{}] Re-gating upstream default AP {} (was network=lan) onto the tollgate network",
router.host, section
);
router.uci_set(&network_key, "tollgate")?;
}
}
router.uci_commit(Some("wireless"))?;
Ok(())
}
/// Return the first available wireless radio device name (e.g. "radio0"). /// Return the first available wireless radio device name (e.g. "radio0").
fn detect_radio(router: &Router) -> Result<String> { fn detect_radio(router: &Router) -> Result<String> {
let out = let out =