fix(tollgate): install and configure NoDogSplash for real client gating
tollgate-wrt has no firewall/netfilter code of its own (confirmed via
strings on the binary and the upstream Go source) — it delegates all
MAC authorization and gate open/close to NoDogSplash via ndsctl.
Upstream's package declares +nodogsplash as a hard dependency, but
neither of our install paths pull it in: the opkg fast path only
resolves deps against a real feed, and the raw .ipk-extraction
fallback (used whenever the package isn't in a feed, and always on
ApkNative) skips dependency resolution entirely. Result: tollgate-wrt
ran, accepted payments, and tracked balances, but never actually
blocked unpaid clients — the static firewall zone just forwarded
everyone to WAN unconditionally.
Also fixes the config.json mismatch: tollgate-wrt reads
/etc/tollgate/config.json exclusively, never the tollgate.main.* UCI
keys we were writing — so mint/price changes through this project's
UI silently had no effect on what the daemon actually advertised.
- tollgate/nodogsplash.rs: install nodogsplash, configure it to gate
the dedicated br-tollgate bridge, open the pre-auth walled-garden
ports (2121 payment, 2050 portal).
- tollgate/wifi.rs: bind network.tollgate to a stable br-tollgate
bridge device (NoDogSplash needs a known interface name — the
driver-assigned name of a bare wifi vif isn't guaranteed); disable
IPv6 RA/DHCPv6 on it (NoDogSplash only manages IPv4 iptables, so
IPv6 would let clients bypass the portal entirely).
- tollgate/config.rs: apply_daemon_config() merges pricing/mint into
the real config.json instead of (only) UCI.
- opkg.rs: generic install_package() for standard feed packages under
either PkgManager mode.
- router.rs: upload_file() (SCP) for non-UCI config files.
2026-07-02 17:23:12 +00:00
|
|
|
use anyhow::{Context, Result};
|
|
|
|
|
|
|
|
|
|
use crate::opkg::PkgManager;
|
|
|
|
|
use crate::tollgate::TollGateConfig;
|
|
|
|
|
use crate::Router;
|
|
|
|
|
|
2026-07-02 19:28:48 +00:00
|
|
|
/// Install NoDogSplash and immediately stop it, before configuring anything.
|
fix(tollgate): install and configure NoDogSplash for real client gating
tollgate-wrt has no firewall/netfilter code of its own (confirmed via
strings on the binary and the upstream Go source) — it delegates all
MAC authorization and gate open/close to NoDogSplash via ndsctl.
Upstream's package declares +nodogsplash as a hard dependency, but
neither of our install paths pull it in: the opkg fast path only
resolves deps against a real feed, and the raw .ipk-extraction
fallback (used whenever the package isn't in a feed, and always on
ApkNative) skips dependency resolution entirely. Result: tollgate-wrt
ran, accepted payments, and tracked balances, but never actually
blocked unpaid clients — the static firewall zone just forwarded
everyone to WAN unconditionally.
Also fixes the config.json mismatch: tollgate-wrt reads
/etc/tollgate/config.json exclusively, never the tollgate.main.* UCI
keys we were writing — so mint/price changes through this project's
UI silently had no effect on what the daemon actually advertised.
- tollgate/nodogsplash.rs: install nodogsplash, configure it to gate
the dedicated br-tollgate bridge, open the pre-auth walled-garden
ports (2121 payment, 2050 portal).
- tollgate/wifi.rs: bind network.tollgate to a stable br-tollgate
bridge device (NoDogSplash needs a known interface name — the
driver-assigned name of a bare wifi vif isn't guaranteed); disable
IPv6 RA/DHCPv6 on it (NoDogSplash only manages IPv4 iptables, so
IPv6 would let clients bypass the portal entirely).
- tollgate/config.rs: apply_daemon_config() merges pricing/mint into
the real config.json instead of (only) UCI.
- opkg.rs: generic install_package() for standard feed packages under
either PkgManager mode.
- router.rs: upload_file() (SCP) for non-UCI config files.
2026-07-02 17:23:12 +00:00
|
|
|
///
|
2026-07-02 19:28:48 +00:00
|
|
|
/// The OpenWrt package's postinst auto-enables and starts nodogsplash on
|
|
|
|
|
/// install using its stock default config — critically, `gatewayinterface`
|
|
|
|
|
/// defaults to `br-lan`. On a fresh install that window is real: NoDogSplash
|
|
|
|
|
/// 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<()> {
|
fix(tollgate): install and configure NoDogSplash for real client gating
tollgate-wrt has no firewall/netfilter code of its own (confirmed via
strings on the binary and the upstream Go source) — it delegates all
MAC authorization and gate open/close to NoDogSplash via ndsctl.
Upstream's package declares +nodogsplash as a hard dependency, but
neither of our install paths pull it in: the opkg fast path only
resolves deps against a real feed, and the raw .ipk-extraction
fallback (used whenever the package isn't in a feed, and always on
ApkNative) skips dependency resolution entirely. Result: tollgate-wrt
ran, accepted payments, and tracked balances, but never actually
blocked unpaid clients — the static firewall zone just forwarded
everyone to WAN unconditionally.
Also fixes the config.json mismatch: tollgate-wrt reads
/etc/tollgate/config.json exclusively, never the tollgate.main.* UCI
keys we were writing — so mint/price changes through this project's
UI silently had no effect on what the daemon actually advertised.
- tollgate/nodogsplash.rs: install nodogsplash, configure it to gate
the dedicated br-tollgate bridge, open the pre-auth walled-garden
ports (2121 payment, 2050 portal).
- tollgate/wifi.rs: bind network.tollgate to a stable br-tollgate
bridge device (NoDogSplash needs a known interface name — the
driver-assigned name of a bare wifi vif isn't guaranteed); disable
IPv6 RA/DHCPv6 on it (NoDogSplash only manages IPv4 iptables, so
IPv6 would let clients bypass the portal entirely).
- tollgate/config.rs: apply_daemon_config() merges pricing/mint into
the real config.json instead of (only) UCI.
- opkg.rs: generic install_package() for standard feed packages under
either PkgManager mode.
- router.rs: upload_file() (SCP) for non-UCI config files.
2026-07-02 17:23:12 +00:00
|
|
|
router
|
|
|
|
|
.install_package(pkg_mgr, "nodogsplash")
|
|
|
|
|
.context("install nodogsplash — required by tollgate-wrt for client gating")?;
|
2026-07-02 19:28:48 +00:00
|
|
|
router.run_ok("/etc/init.d/nodogsplash stop || true")?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
fix(tollgate): install and configure NoDogSplash for real client gating
tollgate-wrt has no firewall/netfilter code of its own (confirmed via
strings on the binary and the upstream Go source) — it delegates all
MAC authorization and gate open/close to NoDogSplash via ndsctl.
Upstream's package declares +nodogsplash as a hard dependency, but
neither of our install paths pull it in: the opkg fast path only
resolves deps against a real feed, and the raw .ipk-extraction
fallback (used whenever the package isn't in a feed, and always on
ApkNative) skips dependency resolution entirely. Result: tollgate-wrt
ran, accepted payments, and tracked balances, but never actually
blocked unpaid clients — the static firewall zone just forwarded
everyone to WAN unconditionally.
Also fixes the config.json mismatch: tollgate-wrt reads
/etc/tollgate/config.json exclusively, never the tollgate.main.* UCI
keys we were writing — so mint/price changes through this project's
UI silently had no effect on what the daemon actually advertised.
- tollgate/nodogsplash.rs: install nodogsplash, configure it to gate
the dedicated br-tollgate bridge, open the pre-auth walled-garden
ports (2121 payment, 2050 portal).
- tollgate/wifi.rs: bind network.tollgate to a stable br-tollgate
bridge device (NoDogSplash needs a known interface name — the
driver-assigned name of a bare wifi vif isn't guaranteed); disable
IPv6 RA/DHCPv6 on it (NoDogSplash only manages IPv4 iptables, so
IPv6 would let clients bypass the portal entirely).
- tollgate/config.rs: apply_daemon_config() merges pricing/mint into
the real config.json instead of (only) UCI.
- opkg.rs: generic install_package() for standard feed packages under
either PkgManager mode.
- router.rs: upload_file() (SCP) for non-UCI config files.
2026-07-02 17:23:12 +00:00
|
|
|
|
2026-07-02 19:28:48 +00:00
|
|
|
/// 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<()> {
|
fix(tollgate): install and configure NoDogSplash for real client gating
tollgate-wrt has no firewall/netfilter code of its own (confirmed via
strings on the binary and the upstream Go source) — it delegates all
MAC authorization and gate open/close to NoDogSplash via ndsctl.
Upstream's package declares +nodogsplash as a hard dependency, but
neither of our install paths pull it in: the opkg fast path only
resolves deps against a real feed, and the raw .ipk-extraction
fallback (used whenever the package isn't in a feed, and always on
ApkNative) skips dependency resolution entirely. Result: tollgate-wrt
ran, accepted payments, and tracked balances, but never actually
blocked unpaid clients — the static firewall zone just forwarded
everyone to WAN unconditionally.
Also fixes the config.json mismatch: tollgate-wrt reads
/etc/tollgate/config.json exclusively, never the tollgate.main.* UCI
keys we were writing — so mint/price changes through this project's
UI silently had no effect on what the daemon actually advertised.
- tollgate/nodogsplash.rs: install nodogsplash, configure it to gate
the dedicated br-tollgate bridge, open the pre-auth walled-garden
ports (2121 payment, 2050 portal).
- tollgate/wifi.rs: bind network.tollgate to a stable br-tollgate
bridge device (NoDogSplash needs a known interface name — the
driver-assigned name of a bare wifi vif isn't guaranteed); disable
IPv6 RA/DHCPv6 on it (NoDogSplash only manages IPv4 iptables, so
IPv6 would let clients bypass the portal entirely).
- tollgate/config.rs: apply_daemon_config() merges pricing/mint into
the real config.json instead of (only) UCI.
- opkg.rs: generic install_package() for standard feed packages under
either PkgManager mode.
- router.rs: upload_file() (SCP) for non-UCI config files.
2026-07-02 17:23:12 +00:00
|
|
|
router.run_ok("touch /etc/config/nodogsplash")?;
|
2026-07-02 20:21:32 +00:00
|
|
|
|
|
|
|
|
// The nodogsplash package's own uci-defaults populate an anonymous
|
|
|
|
|
// `@nodogsplash[0]` section on first install, pointed at `br-lan` (its
|
|
|
|
|
// stock default — see `install_and_stop`). NoDogSplash supports multiple
|
|
|
|
|
// simultaneous gateway instances, one per config section, so leaving this
|
|
|
|
|
// in place alongside our own `nodogsplash.main` doesn't get overridden by
|
|
|
|
|
// it — it starts a *second* instance gating br-lan for real. Delete it;
|
|
|
|
|
// `main` is the only instance this project manages.
|
|
|
|
|
let _ = router.uci_delete("nodogsplash.@nodogsplash[0]");
|
|
|
|
|
|
fix(tollgate): install and configure NoDogSplash for real client gating
tollgate-wrt has no firewall/netfilter code of its own (confirmed via
strings on the binary and the upstream Go source) — it delegates all
MAC authorization and gate open/close to NoDogSplash via ndsctl.
Upstream's package declares +nodogsplash as a hard dependency, but
neither of our install paths pull it in: the opkg fast path only
resolves deps against a real feed, and the raw .ipk-extraction
fallback (used whenever the package isn't in a feed, and always on
ApkNative) skips dependency resolution entirely. Result: tollgate-wrt
ran, accepted payments, and tracked balances, but never actually
blocked unpaid clients — the static firewall zone just forwarded
everyone to WAN unconditionally.
Also fixes the config.json mismatch: tollgate-wrt reads
/etc/tollgate/config.json exclusively, never the tollgate.main.* UCI
keys we were writing — so mint/price changes through this project's
UI silently had no effect on what the daemon actually advertised.
- tollgate/nodogsplash.rs: install nodogsplash, configure it to gate
the dedicated br-tollgate bridge, open the pre-auth walled-garden
ports (2121 payment, 2050 portal).
- tollgate/wifi.rs: bind network.tollgate to a stable br-tollgate
bridge device (NoDogSplash needs a known interface name — the
driver-assigned name of a bare wifi vif isn't guaranteed); disable
IPv6 RA/DHCPv6 on it (NoDogSplash only manages IPv4 iptables, so
IPv6 would let clients bypass the portal entirely).
- tollgate/config.rs: apply_daemon_config() merges pricing/mint into
the real config.json instead of (only) UCI.
- opkg.rs: generic install_package() for standard feed packages under
either PkgManager mode.
- router.rs: upload_file() (SCP) for non-UCI config files.
2026-07-02 17:23:12 +00:00
|
|
|
router.uci_set("nodogsplash.main", "nodogsplash")?;
|
|
|
|
|
router.uci_set("nodogsplash.main.enabled", "1")?;
|
|
|
|
|
router.uci_set("nodogsplash.main.gatewayinterface", "br-tollgate")?;
|
|
|
|
|
router.uci_set("nodogsplash.main.gatewayname", &format!("{} Portal", cfg.ssid))?;
|
|
|
|
|
router.uci_set("nodogsplash.main.gatewaydomainname", "TollGate.lan")?;
|
|
|
|
|
router.uci_set("nodogsplash.main.gatewayport", "2050")?;
|
|
|
|
|
|
|
|
|
|
// Pre-auth "walled garden": unauthenticated clients must still be able to
|
|
|
|
|
// reach the TollGate payment endpoint (2121) and the splash portal itself
|
|
|
|
|
// (2050) — everything else is blocked by NoDogSplash until ndsctl
|
|
|
|
|
// authorizes their MAC. Rebuild the list each run rather than trying to
|
|
|
|
|
// dedupe, so repeated provisioning stays idempotent.
|
|
|
|
|
let _ = router.uci_delete("nodogsplash.main.users_to_router");
|
|
|
|
|
router.uci_add_list("nodogsplash.main.users_to_router", "allow tcp port 2121")?;
|
|
|
|
|
router.uci_add_list("nodogsplash.main.users_to_router", "allow tcp port 2050")?;
|
|
|
|
|
|
|
|
|
|
router.uci_commit(Some("nodogsplash"))?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// (Re)start the nodogsplash service so config changes and gate state take effect.
|
|
|
|
|
pub fn restart(router: &Router) -> Result<()> {
|
|
|
|
|
router.run_ok("/etc/init.d/nodogsplash enable")?;
|
|
|
|
|
router.run_ok("/etc/init.d/nodogsplash restart || /etc/init.d/nodogsplash start")?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|