fix(tollgate): fix nodogsplash provisioning order (found live: gated br-lan)

Deploying the previous commit's fix live exposed a real bug: nodogsplash's
OpenWrt package postinst auto-enables and starts the service immediately
on install, using its stock default config — gatewayinterface=br-lan.
Since NoDogSplash only touches IPv4 iptables, this silently cut IPv4
(not IPv6) connectivity for anything on br-lan, including the admin
management box plugged into this router's LAN port, for the window
between install and our own configure step.

Split nodogsplash provisioning into install_and_stop() (runs first,
closes that window immediately) and configure() (runs after
wifi::provision_ssid has created br-tollgate, so gatewayinterface is
pointed at the isolated tollgate bridge instead of br-lan).
This commit is contained in:
ssmithx 2026-07-02 19:28:48 +00:00
parent a252eb12fd
commit 7439a1251c
2 changed files with 44 additions and 18 deletions

View File

@ -14,12 +14,14 @@ use crate::{opkg::PkgManager, Router};
/// Full TollGate provisioning sequence: /// Full TollGate provisioning sequence:
/// 1. Install tollgate-module-basic-go /// 1. Install tollgate-module-basic-go
/// 2. Install and configure NoDogSplash (client gating — tollgate-wrt has no /// 2. Install NoDogSplash and immediately stop it (its postinst auto-starts
/// firewall/enforcement code of its own; see `nodogsplash::provision`) /// it against `br-lan` by default — see `nodogsplash::install_and_stop`)
/// 3. Write TollGate config: UCI (status/detection only) + the JSON file the /// 3. Write TollGate config: UCI (status/detection only) + the JSON file the
/// daemon actually reads /// daemon actually reads
/// 4. Create the pay-as-you-go WiFi SSID and its dedicated bridge/network /// 4. Create the pay-as-you-go WiFi SSID and its dedicated bridge/network
/// 5. Restart affected services /// 5. Configure NoDogSplash to gate that bridge (now that it exists) —
/// client gating; tollgate-wrt has no enforcement code of its own
/// 6. Restart affected services
pub async fn provision(router: &Router, config: &TollGateConfig) -> Result<()> { pub async fn provision(router: &Router, config: &TollGateConfig) -> Result<()> {
info!("[{}] Starting TollGate provisioning", router.host); info!("[{}] Starting TollGate provisioning", router.host);
@ -40,9 +42,11 @@ pub async fn provision(router: &Router, config: &TollGateConfig) -> Result<()> {
// the raw .ipk-extraction fallback (used whenever the package isn't in a // the raw .ipk-extraction fallback (used whenever the package isn't in a
// feed, and always on ApkNative) skips dependency resolution entirely. // feed, and always on ApkNative) skips dependency resolution entirely.
// Without it, tollgate-wrt runs and accepts payments but never actually // Without it, tollgate-wrt runs and accepts payments but never actually
// blocks unpaid clients. // blocks unpaid clients. Install + stop happens before anything else so
nodogsplash::provision(router, pkg_mgr, config) // its auto-started default config (gating br-lan) is live for as little
.context("provision nodogsplash — tollgate-wrt cannot gate clients without it")?; // time as possible.
nodogsplash::install_and_stop(router, pkg_mgr)
.context("install nodogsplash — tollgate-wrt cannot gate clients without it")?;
config::apply(router, config)?; config::apply(router, config)?;
wifi::provision_ssid(router, config)?; wifi::provision_ssid(router, config)?;
@ -50,6 +54,11 @@ pub async fn provision(router: &Router, config: &TollGateConfig) -> Result<()> {
// 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)
.context("write /etc/tollgate/config.json — tollgate-wrt reads this, not UCI")?; .context("write /etc/tollgate/config.json — tollgate-wrt reads this, not UCI")?;
// Also must come after provision_ssid: points gatewayinterface at
// br-tollgate, which provision_ssid is what creates.
nodogsplash::configure(router, config)
.context("configure nodogsplash — tollgate-wrt cannot gate clients without it")?;
restart_services(router, config.enabled)?; restart_services(router, config.enabled)?;
nodogsplash::restart(router)?; nodogsplash::restart(router)?;

View File

@ -4,23 +4,40 @@ use crate::opkg::PkgManager;
use crate::tollgate::TollGateConfig; use crate::tollgate::TollGateConfig;
use crate::Router; use crate::Router;
/// Install and configure NoDogSplash — the captive-portal engine that /// Install NoDogSplash and immediately stop it, before configuring anything.
/// `tollgate-wrt` delegates all MAC authorization and gate open/close to via
/// `ndsctl`. `tollgate-wrt` contains no firewall/netfilter code of its own
/// (confirmed: its binary has no `nft`/`ipset`/`iptables` calls at all); the
/// upstream package therefore hard-depends on `+nodogsplash` and its postinst
/// restarts it. Without NoDogSplash actually running, nothing blocks
/// unauthenticated clients from reaching the internet, regardless of what
/// TollGate itself is configured to charge.
/// ///
/// Gates the dedicated `br-tollgate` bridge (see `wifi::provision_network`), /// The OpenWrt package's postinst auto-enables and starts nodogsplash on
/// not `br-lan` — the paid SSID here lives on its own isolated network/subnet /// install using its stock default config — critically, `gatewayinterface`
/// rather than the canonical upstream layout where it's bridged into `lan`. /// defaults to `br-lan`. On a fresh install that window is real: NoDogSplash
pub fn provision(router: &Router, pkg_mgr: PkgManager, cfg: &TollGateConfig) -> Result<()> { /// only manages IPv4 iptables, so anything plugged into `br-lan` (e.g. an
/// admin's own management box) silently loses IPv4 connectivity (DHCP still
/// listens, but the gate blocks the client until ndsctl authorizes its MAC)
/// until we get a chance to repoint it — a full network re-scan can take
/// long enough for that to matter. Stopping it right after install, before
/// `configure()` ever runs, closes that window as early as possible.
///
/// `tollgate-wrt` delegates all MAC authorization and gate open/close to
/// NoDogSplash via `ndsctl` — it has no firewall/netfilter code of its own
/// (confirmed: its binary has no `nft`/`ipset`/`iptables` calls at all).
/// Upstream's package therefore hard-depends on `+nodogsplash`, but neither
/// of our install paths (see `tollgate::install`) pull it in automatically.
pub fn install_and_stop(router: &Router, pkg_mgr: PkgManager) -> Result<()> {
router router
.install_package(pkg_mgr, "nodogsplash") .install_package(pkg_mgr, "nodogsplash")
.context("install nodogsplash — required by tollgate-wrt for client gating")?; .context("install nodogsplash — required by tollgate-wrt for client gating")?;
router.run_ok("/etc/init.d/nodogsplash stop || true")?;
Ok(())
}
/// Configure NoDogSplash to gate the dedicated `br-tollgate` bridge (see
/// `wifi::provision_network`), not `br-lan` — the paid SSID here lives on its
/// own isolated network/subnet rather than the canonical upstream layout
/// where it's bridged into `lan`.
///
/// Must run after `wifi::provision_ssid` has created `br-tollgate` — pointing
/// `gatewayinterface` at a bridge that doesn't exist yet is at best a no-op
/// and at worst leaves NoDogSplash in a confused state.
pub fn configure(router: &Router, cfg: &TollGateConfig) -> Result<()> {
router.run_ok("touch /etc/config/nodogsplash")?; router.run_ok("touch /etc/config/nodogsplash")?;
router.uci_set("nodogsplash.main", "nodogsplash")?; router.uci_set("nodogsplash.main", "nodogsplash")?;
router.uci_set("nodogsplash.main.enabled", "1")?; router.uci_set("nodogsplash.main.enabled", "1")?;