Several compounding bugs were blocking end-to-end TollGate provisioning
on OpenWrt 25.x (apk-native) routers:
- install_ipk's non-ar fallback assumed a flat tarball, but some .ipks are
a gzip tar of the three classic ipk members one level deep; it was
dumping debian-binary/data.tar.gz/control.tar.gz straight into / instead
of unpacking the real payload.
- Manually-extracted packages never ran their pending /etc/uci-defaults/*
scripts (that only happens through opkg/apk's own postinst bookkeeping),
so nothing ever created /etc/config/tollgate.
- uci_apply() never ensured the target config file existed first — `uci
set` fails outright on a config namespace nothing has created yet, which
is true for a package-defined one like "tollgate" (unlike wireless/
network/dhcp, which ship by default).
- The installed-check and restart_services looked for a binary/init script
named after the opkg package ("tollgate-module-basic-go"/"tollgate"),
but the real on-disk names are tollgate-wrt — so status always reported
"not installed" and service restarts silently no-op'd.
- provision_ssid used `uci add`, creating a new wifi-iface section (and
therefore a new duplicate broadcast SSID) on every provision call instead
of updating one in place.
Also adds a TollGateConfig.enabled field so the enable/disable state is
actually applied to the running service and the SSID's own broadcast
(stop + disable at boot, or start + enable), not just written to UCI.
On the frontend, the OpenWrt Gateway page's TollGate panel was read-only
once installed — add an edit form (price, step size, min steps, mint URL,
enabled toggle) that reuses the same idempotent provision-tollgate call.
110 lines
3.6 KiB
Rust
110 lines
3.6 KiB
Rust
use anyhow::{Context, Result};
|
|
use tracing::info;
|
|
|
|
use crate::tollgate::TollGateConfig;
|
|
use crate::Router;
|
|
|
|
/// Create (or update) the dedicated pay-as-you-go WiFi interface for TollGate.
|
|
///
|
|
/// Uses a fixed named section (`wireless.tollgate`) rather than `uci add`, so
|
|
/// re-provisioning (e.g. editing price/mint URL after install) updates the
|
|
/// same interface in place instead of piling up a new `wifi-iface` section —
|
|
/// and therefore a new duplicate broadcast SSID — on every call.
|
|
pub fn provision_ssid(router: &Router, cfg: &TollGateConfig) -> Result<()> {
|
|
let radio = detect_radio(router).context("detect WiFi radio")?;
|
|
info!("[{}] Using radio {} for TollGate SSID", router.host, radio);
|
|
|
|
router.uci_apply(
|
|
"wireless",
|
|
&[
|
|
("wireless.tollgate", "wifi-iface"),
|
|
("wireless.tollgate.device", &radio),
|
|
("wireless.tollgate.mode", "ap"),
|
|
("wireless.tollgate.ssid", &cfg.ssid),
|
|
("wireless.tollgate.encryption", "none"),
|
|
("wireless.tollgate.network", "tollgate"),
|
|
// Disable 802.11r/k/v — unnecessary for transient pay-as-you-go clients.
|
|
("wireless.tollgate.ieee80211r", "0"),
|
|
// Stop broadcasting entirely when disabled, rather than leaving an
|
|
// open SSID up that leads nowhere once the backend is stopped.
|
|
("wireless.tollgate.disabled", if cfg.enabled { "0" } else { "1" }),
|
|
],
|
|
)?;
|
|
|
|
provision_network(router)?;
|
|
provision_firewall(router)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Add a `tollgate` network interface (isolated LAN for TollGate clients).
|
|
fn provision_network(router: &Router) -> Result<()> {
|
|
router.uci_apply(
|
|
"network",
|
|
&[
|
|
("network.tollgate", "interface"),
|
|
("network.tollgate.proto", "static"),
|
|
("network.tollgate.ipaddr", "192.168.99.1"),
|
|
("network.tollgate.netmask", "255.255.255.0"),
|
|
],
|
|
)?;
|
|
|
|
// Enable DHCP for the tollgate interface.
|
|
router.uci_apply(
|
|
"dhcp",
|
|
&[
|
|
("dhcp.tollgate", "dhcp"),
|
|
("dhcp.tollgate.interface", "tollgate"),
|
|
("dhcp.tollgate.start", "100"),
|
|
("dhcp.tollgate.limit", "150"),
|
|
("dhcp.tollgate.leasetime", "5m"),
|
|
],
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Add firewall zone for the tollgate interface.
|
|
///
|
|
/// TollGate itself gates forwarding via iptables; the firewall zone isolates
|
|
/// tollgate clients from other LAN segments.
|
|
fn provision_firewall(router: &Router) -> Result<()> {
|
|
// Zone
|
|
router.uci_apply(
|
|
"firewall",
|
|
&[
|
|
("firewall.tollgate_zone", "zone"),
|
|
("firewall.tollgate_zone.name", "tollgate"),
|
|
("firewall.tollgate_zone.network", "tollgate"),
|
|
("firewall.tollgate_zone.input", "ACCEPT"),
|
|
("firewall.tollgate_zone.output", "ACCEPT"),
|
|
("firewall.tollgate_zone.forward", "REJECT"),
|
|
],
|
|
)?;
|
|
|
|
// Forwarding rule: tollgate → wan (TollGate manages which clients can forward)
|
|
router.uci_apply(
|
|
"firewall",
|
|
&[
|
|
("firewall.tollgate_fwd", "forwarding"),
|
|
("firewall.tollgate_fwd.src", "tollgate"),
|
|
("firewall.tollgate_fwd.dest", "wan"),
|
|
],
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Return the first available wireless radio device name (e.g. "radio0").
|
|
fn detect_radio(router: &Router) -> Result<String> {
|
|
let out = router.run_ok("uci show wireless | grep -o 'wireless\\.radio[0-9]*\\.type' | head -1")?;
|
|
// Extract "radioN" from "wireless.radioN.type"
|
|
let radio = out
|
|
.trim()
|
|
.split('.')
|
|
.nth(1)
|
|
.unwrap_or("radio0")
|
|
.to_string();
|
|
Ok(radio)
|
|
}
|