2026-08-12 10:55:50 +00:00
|
|
|
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).
|
|
|
|
|
///
|
|
|
|
|
/// Binds to a named bridge device (`br-tollgate`) rather than leaving the
|
|
|
|
|
/// wifi-iface as the network's raw device — NoDogSplash's `gatewayinterface`
|
|
|
|
|
/// needs a stable, known interface name to gate (see `nodogsplash::provision`),
|
|
|
|
|
/// and the driver-assigned name of a bare wifi vif (e.g. `phy0-ap0`) isn't
|
|
|
|
|
/// guaranteed across hardware.
|
|
|
|
|
fn provision_network(router: &Router) -> Result<()> {
|
|
|
|
|
router.uci_apply(
|
|
|
|
|
"network",
|
|
|
|
|
&[
|
|
|
|
|
("network.tollgate_bridge", "device"),
|
|
|
|
|
("network.tollgate_bridge.type", "bridge"),
|
|
|
|
|
("network.tollgate_bridge.name", "br-tollgate"),
|
|
|
|
|
("network.tollgate", "interface"),
|
|
|
|
|
("network.tollgate.device", "br-tollgate"),
|
|
|
|
|
("network.tollgate.proto", "static"),
|
|
|
|
|
("network.tollgate.ipaddr", "192.168.99.1"),
|
|
|
|
|
("network.tollgate.netmask", "255.255.255.0"),
|
|
|
|
|
// NoDogSplash only manages IPv4 iptables rules. If IPv6 RA/DHCPv6
|
|
|
|
|
// stays enabled, clients get routable IPv6 addresses and their OS
|
|
|
|
|
// validates connectivity (and browses freely) over IPv6, bypassing
|
|
|
|
|
// the portal entirely. See OpenTollGate/tollgate-module-basic-go#148.
|
|
|
|
|
("network.tollgate.ip6assign", "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"),
|
|
|
|
|
("dhcp.tollgate.ra", "disabled"),
|
|
|
|
|
("dhcp.tollgate.dhcpv6", "disabled"),
|
|
|
|
|
],
|
|
|
|
|
)?;
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Add firewall zone for the tollgate interface.
|
|
|
|
|
///
|
|
|
|
|
/// This zone only isolates tollgate clients from other LAN segments and
|
|
|
|
|
/// opens the payment port to the router. Per-client forwarding to WAN is
|
|
|
|
|
/// actually gated by NoDogSplash's own iptables rules (via `ndsctl`), not by
|
|
|
|
|
/// anything in this static firewall config — `tollgate-wrt` has no netfilter
|
|
|
|
|
/// code of its own. See `nodogsplash::provision`.
|
|
|
|
|
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(())
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-07 14:37:12 +00:00
|
|
|
/// 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();
|
2026-09-08 21:01:20 -04:00
|
|
|
let ssid = router
|
|
|
|
|
.uci_get(&format!("{}.ssid", section))
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
// A failed/changed upstream first-boot script can leave a stock
|
|
|
|
|
// default_radioN section in place. Moving that interface merely
|
|
|
|
|
// because it is on LAN can seize the router's existing management AP.
|
|
|
|
|
// Only the public APs the TollGate installer demonstrably rebranded
|
|
|
|
|
// belong on the paid network.
|
|
|
|
|
if should_regate_upstream_ap(¤t, &ssid) {
|
2026-09-07 14:37:12 +00:00
|
|
|
info!(
|
2026-09-08 21:01:20 -04:00
|
|
|
"[{}] Re-gating upstream default AP {} ({}) onto the tollgate network",
|
|
|
|
|
router.host, section, ssid
|
2026-09-07 14:37:12 +00:00
|
|
|
);
|
|
|
|
|
router.uci_set(&network_key, "tollgate")?;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
router.uci_commit(Some("wireless"))?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-08 21:01:20 -04:00
|
|
|
fn should_regate_upstream_ap(network: &str, ssid: &str) -> bool {
|
|
|
|
|
network.trim() == "lan" && ssid.trim().starts_with("TollGate-")
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-12 10:55:50 +00:00
|
|
|
/// 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)
|
|
|
|
|
}
|
2026-09-08 21:01:20 -04:00
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::should_regate_upstream_ap;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn regates_only_confirmed_upstream_tollgate_aps() {
|
|
|
|
|
assert!(should_regate_upstream_ap("lan", "TollGate-3458"));
|
|
|
|
|
assert!(should_regate_upstream_ap(" lan\n", " TollGate-A1B2 "));
|
|
|
|
|
|
|
|
|
|
assert!(!should_regate_upstream_ap("lan", "OpenWrt"));
|
|
|
|
|
assert!(!should_regate_upstream_ap("lan", "Archipelago Admin"));
|
|
|
|
|
assert!(!should_regate_upstream_ap("tollgate", "TollGate-3458"));
|
|
|
|
|
assert!(!should_regate_upstream_ap("lan", "tollgate-3458"));
|
|
|
|
|
}
|
|
|
|
|
}
|