From edbad30501148882e107725a9c858a3d9b32273b Mon Sep 17 00:00:00 2001 From: ssmithx Date: Tue, 30 Jun 2026 17:12:50 +0000 Subject: [PATCH] fix(openwrt): TollGate apk-native install for OpenWrt 25.x MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - WISP wizard: step-by-step flow for WiFi, DHCP, masquerade config - WAN status: expose lan_ip, dhcp_start/limit, masq, sta_state, wifi_log - wifi_scan: detect CCMP as WPA2 (psk2) so association succeeds - opkg: PkgManager enum — detect apk-native mode when opkg not in repos - tollgate: apk-native install path using manual ipk extraction - arch detection: read DISTRIB_ARCH from /etc/openwrt_release; normalise bare mipsel/mips from uname -m to mipsel_24kc/mips_24kc - install_ipk: install binutils via apk when ar not in BusyBox - install_ipk: wget --no-check-certificate for routers without CA bundle - install_ipk: ar fallback to tar -xzf for non-standard ipk formats - install_ipk: 5MB overlay space check with clear user-facing error - middleware: allow "Not enough flash/space" errors through sanitizer Co-Authored-By: Claude Sonnet 4.6 --- core/archipelago/src/api/rpc/middleware.rs | 2 + core/archipelago/src/api/rpc/openwrt.rs | 10 +- core/openwrt/src/opkg.rs | 44 ++++- core/openwrt/src/tollgate/install.rs | 188 +++++++++++++++++-- core/openwrt/src/tollgate/mod.rs | 17 +- core/openwrt/src/wan.rs | 69 ++++++- core/openwrt/src/wifi_scan.rs | 8 +- neode-ui/src/views/server/OpenWrtGateway.vue | 103 +++++++++- 8 files changed, 393 insertions(+), 48 deletions(-) diff --git a/core/archipelago/src/api/rpc/middleware.rs b/core/archipelago/src/api/rpc/middleware.rs index fc15c75f..ff007f27 100644 --- a/core/archipelago/src/api/rpc/middleware.rs +++ b/core/archipelago/src/api/rpc/middleware.rs @@ -67,6 +67,8 @@ pub(super) fn sanitize_error_message(msg: &str) -> String { "No router", "No OpenWrt", "No space left", + "Not enough flash", + "Not enough space", "TollGate installation failed", "No pre-built TollGate", "opkg not found", diff --git a/core/archipelago/src/api/rpc/openwrt.rs b/core/archipelago/src/api/rpc/openwrt.rs index 55261676..e064b984 100644 --- a/core/archipelago/src/api/rpc/openwrt.rs +++ b/core/archipelago/src/api/rpc/openwrt.rs @@ -92,9 +92,10 @@ impl RpcHandler { .and_then(|s| s.parse().ok()) .unwrap_or(0); - // TollGate + // TollGate — check via opkg (≤24.x) or binary presence (25.x apk-native) let tollgate_installed = router - .run("/usr/bin/opkg list-installed | grep -q '^tollgate-module-basic-go '") + .run("/usr/bin/opkg list-installed 2>/dev/null | grep -q '^tollgate-module-basic-go ' || \ + test -f /usr/bin/tollgate-module-basic-go 2>/dev/null") .map(|(_, code)| code == 0) .unwrap_or(false); @@ -254,11 +255,14 @@ impl RpcHandler { .ok_or_else(|| anyhow::anyhow!("Missing required field: ssid"))?.to_string(); let password = p.get("password").and_then(|v| v.as_str()).unwrap_or("").to_string(); let encryption = p.get("encryption").and_then(|v| v.as_str()).unwrap_or("psk2").to_string(); + let dhcp_start = p.get("dhcp_start").and_then(|v| v.as_u64()).unwrap_or(100) as u32; + let dhcp_limit = p.get("dhcp_limit").and_then(|v| v.as_u64()).unwrap_or(150) as u32; + let masq = p.get("masq").and_then(|v| v.as_bool()).unwrap_or(true); let router = Router::connect_password(&host, 22, &ssh_user, &ssh_password)?; router.verify_openwrt()?; - let config = wan::WispConfig { ssid: ssid.clone(), password, encryption }; + let config = wan::WispConfig { ssid: ssid.clone(), password, encryption, dhcp_start, dhcp_limit, masq }; wan::configure_wisp(&router, &config)?; Ok(serde_json::json!({ "ok": true, "host": host, "ssid": ssid })) diff --git a/core/openwrt/src/opkg.rs b/core/openwrt/src/opkg.rs index 56a2af48..84d889e0 100644 --- a/core/openwrt/src/opkg.rs +++ b/core/openwrt/src/opkg.rs @@ -3,19 +3,36 @@ use tracing::info; use crate::Router; +/// Which package manager is available on this router. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PkgManager { + /// Traditional opkg (OpenWrt ≤24.x). + Opkg, + /// OpenWrt 25.x+ — apk is the native manager, opkg is not in repos. + ApkNative, +} + impl Router { - /// Verify opkg is available. On OpenWrt 25.x+ which ships apk instead of - /// opkg, bootstraps opkg via `apk add opkg` before returning. - pub fn opkg_check(&self) -> Result<()> { + /// Detect which package manager is available. + /// + /// - If `/usr/bin/opkg` exists → `PkgManager::Opkg` (nothing to do). + /// - If `/usr/bin/apk` exists → run `apk update` (switching repos to HTTP + /// first to work around missing CA bundle on fresh images), then try + /// `apk add opkg`. If opkg is in the repos → `Opkg`. If not (OpenWrt + /// 25.x) → `ApkNative`. + /// - Neither found → error. + pub fn opkg_check(&self) -> Result { let (_, code) = self.run("test -x /usr/bin/opkg")?; if code == 0 { - return Ok(()); + return Ok(PkgManager::Opkg); } - // OpenWrt 25.x switched to apk as default — install opkg through it. + let (_, apk_code) = self.run("test -x /usr/bin/apk")?; if apk_code == 0 { - info!("[{}] opkg not found, bootstrapping via apk", self.host); - // Capture stderr so apk errors are visible in server logs. + info!("[{}] opkg not found — using apk (OpenWrt 25.x+)", self.host); + // Fresh images ship without a CA bundle; switch repos to HTTP so + // apk's wget can reach the package index without TLS verification. + self.run_ok("sed -i 's|https://|http://|g' /etc/apk/repositories 2>/dev/null || true")?; let (update_out, update_code) = self.run("/usr/bin/apk update 2>&1")?; if update_code != 0 { anyhow::bail!( @@ -25,9 +42,18 @@ impl Router { update_out.trim() ); } - self.run_ok("/usr/bin/apk add opkg 2>&1")?; - return Ok(()); + // Try to install opkg (only available on some 25.x builds). + let (add_out, add_code) = self.run("/usr/bin/apk add opkg 2>&1")?; + if add_code == 0 { + return Ok(PkgManager::Opkg); + } + if add_out.contains("no such package") || add_out.contains("unable to select") { + info!("[{}] opkg not in apk repos — staying in apk-native mode", self.host); + return Ok(PkgManager::ApkNative); + } + anyhow::bail!("apk add opkg failed (exit {}): {}", add_code, add_out.trim()); } + anyhow::bail!( "opkg not found at /usr/bin/opkg — this router's firmware may not \ support package management (TollGate requires a standard OpenWrt build)" diff --git a/core/openwrt/src/tollgate/install.rs b/core/openwrt/src/tollgate/install.rs index 53a37398..00f178f9 100644 --- a/core/openwrt/src/tollgate/install.rs +++ b/core/openwrt/src/tollgate/install.rs @@ -20,7 +20,7 @@ fn ipk_url(arch: &str) -> Option<&'static str> { } } -/// Install tollgate-module-basic-go. +/// Install tollgate-module-basic-go via opkg (OpenWrt ≤24.x). /// /// Tries opkg first (works if a custom feed is configured). Falls back to /// downloading the .ipk directly from GitHub releases if opkg can't find it. @@ -47,28 +47,176 @@ pub fn install_tollgate(router: &Router) -> Result<()> { })?; info!("[{}] Downloading TollGate for {} from GitHub releases", router.host, arch); - router.run_ok(&format!("wget -O /tmp/tollgate.ipk '{}'", url))?; + router.run_ok(&format!("wget --no-check-certificate -O /tmp/tollgate.ipk '{}' 2>&1", url))?; + install_ipk(router, "/tmp/tollgate.ipk") +} - // Capture stderr too — BusyBox opkg exits 0 even on "Cannot install" failures. - let (out, _code) = router.run("/usr/bin/opkg install --force-depends /tmp/tollgate.ipk 2>&1")?; - router.run_ok("rm -f /tmp/tollgate.ipk")?; +/// Install tollgate-module-basic-go on OpenWrt 25.x where opkg is not available. +/// +/// Downloads the .ipk from GitHub releases and extracts it manually using +/// BusyBox `ar` and `tar` (both present on all OpenWrt images). +pub fn install_tollgate_apk_native(router: &Router) -> Result<()> { + info!("[{}] Installing {} (apk-native mode)", router.host, TOLLGATE_PACKAGE); - if out.contains("Cannot install") || out.contains("errors encountered") { - if out.contains("Only have") && out.contains("available on filesystem") { - // Extract the sizes from the error for a user-readable message. - // Example: "Only have 8884kb available on filesystem /overlay, pkg tollgate-wrt needs 18750" - anyhow::bail!( - "No space left on router for TollGate ({}). \ - This router has insufficient flash storage. \ - Consider a router with ≥32 MB flash or add an extroot USB drive.", - out.lines() - .find(|l| l.contains("Only have")) - .unwrap_or("check router disk space") - .trim() - ); - } - anyhow::bail!("TollGate installation failed: {}", out.trim()); + // Already installed? + let (_, code) = router.run(&format!( + "apk list --installed 2>/dev/null | grep -q '^{}' || \ + test -f /usr/bin/{} 2>/dev/null", + TOLLGATE_PACKAGE, TOLLGATE_PACKAGE + ))?; + if code == 0 { + info!("[{}] {} already installed", router.host, TOLLGATE_PACKAGE); + return Ok(()); } + // Get architecture from /etc/openwrt_release. + // The variable is DISTRIB_ARCH on most builds; OPENWRT_ARCH on some. + // Fall back to apk --print-arch, then uname -m. + let arch_raw = router.run_ok( + ". /etc/openwrt_release 2>/dev/null \ + && a=\"${DISTRIB_ARCH:-${OPENWRT_ARCH:-}}\" \ + && [ -n \"$a\" ] && echo \"$a\" \ + || /usr/bin/apk --print-arch 2>/dev/null \ + || uname -m" + )?; + // Normalise: uname -m returns bare "mipsel"/"mips"; map to 24kc variant + // which is the standard for home-router MIPS builds. + let arch = match arch_raw.trim() { + "mipsel" => "mipsel_24kc", + "mips" => "mips_24kc", + other => other, + }; + info!("[{}] detected arch: {:?}", router.host, arch); + if arch.is_empty() { + anyhow::bail!("Could not determine router architecture"); + } + + let url = ipk_url(arch).ok_or_else(|| { + anyhow::anyhow!( + "No pre-built TollGate package for architecture '{}'. \ + Add a custom feed or build from source.", + arch + ) + })?; + + info!("[{}] Downloading TollGate for {} from GitHub releases", router.host, arch); + // --no-check-certificate: fresh OpenWrt 25.x images ship without a CA bundle; + // GitHub serves releases over HTTPS so wget would otherwise reject the cert. + let (dl_out, dl_code) = router.run(&format!( + "wget --no-check-certificate -O /tmp/tollgate.ipk '{}' 2>&1", url + ))?; + if dl_code != 0 { + anyhow::bail!("TollGate download failed: {}", dl_out.trim()); + } + // Sanity-check: a real .ipk is at least 50 KB. + // If wget captured an HTML error page it will be tiny. + let (size_out, _) = router.run("wc -c < /tmp/tollgate.ipk 2>/dev/null")?; + let size: u64 = size_out.trim().parse().unwrap_or(0); + if size < 50_000 { + anyhow::bail!( + "Downloaded TollGate package is only {}B — wget likely captured an error page. \ + Check router internet access and that the release URL is reachable.", + size + ); + } + install_ipk(router, "/tmp/tollgate.ipk") +} + +/// Extract and install an .ipk file without opkg. +/// +/// An .ipk is an `ar` archive containing `data.tar.gz` (package files) and +/// `control.tar.gz` (metadata + postinst script). +fn install_ipk(router: &Router, ipk_path: &str) -> Result<()> { + // Check for disk space first (rough: need at least ~1 MB free on /overlay). + // TollGate is a Go binary — typically 5–8 MB on flash. + let (df_out, _) = router.run("df /overlay 2>/dev/null | awk 'NR==2{print $4}'")?; + let free_kb: u64 = df_out.trim().parse().unwrap_or(u64::MAX); + if free_kb < 5120 { + anyhow::bail!( + "Not enough flash space for TollGate: only {}kB free on /overlay \ + (need ≥5MB). Free up space first or use a router with more storage.", + free_kb + ); + } + + router.run_ok("rm -rf /tmp/_tg_install && mkdir -p /tmp/_tg_install")?; + + // OpenWrt 25.x BusyBox does not include `ar` — install binutils via + // whichever package manager is available before trying to unpack the ipk. + let (_, ar_found) = router.run("command -v ar >/dev/null 2>&1")?; + if ar_found != 0 { + info!("[{}] ar not found, installing binutils", router.host); + let (pkg_out, pkg_code) = router.run( + "apk add binutils 2>&1 || opkg install binutils 2>&1" + )?; + if pkg_code != 0 { + anyhow::bail!( + "TollGate installation failed: ar not available and binutils install failed: {}", + pkg_out.trim() + ); + } + } + + // Try standard opkg ar format first (ar archive → data.tar.gz inside). + let (ar_out, ar_code) = router.run(&format!( + "cd /tmp/_tg_install && ar x {} 2>&1", ipk_path + ))?; + + if ar_code == 0 { + // ar succeeded: unpack data.tar.gz from the inner archive. + let (tar_out, tar_code) = router.run( + "tar -xzf /tmp/_tg_install/data.tar.gz -C / 2>&1" + )?; + if tar_code != 0 { + anyhow::bail!("TollGate installation failed: data extract failed: {}", tar_out.trim()); + } + // Run postinst if present (optional — failures are non-fatal). + router.run_ok( + "if tar -xzf /tmp/_tg_install/control.tar.gz -C /tmp/_tg_install 2>/dev/null; then \ + chmod +x /tmp/_tg_install/postinst 2>/dev/null; \ + /tmp/_tg_install/postinst configure 2>/dev/null || true; \ + fi" + )?; + } else { + // Fallback: some packages ship .ipk as a plain gzip tarball. + info!("[{}] ar failed ({}), trying tar -xzf", router.host, ar_out.trim()); + + // List contents first — validates format without writing anything. + let (list_out, list_code) = router.run(&format!( + "tar -tzf {} 2>&1 | head -30", ipk_path + ))?; + if list_code != 0 { + anyhow::bail!( + "TollGate installation failed: file is not an ar archive or gzip tar.\n\ + ar: {}\ntar -t: {}", + ar_out.trim(), list_out.trim() + ); + } + info!("[{}] ipk contents:\n{}", router.host, list_out.trim()); + + // Check free space on the root overlay before writing. + let (ov_df, _) = router.run("df / 2>/dev/null | awk 'NR==2{print $4}'")?; + let overlay_free_kb: u64 = ov_df.trim().parse().unwrap_or(0); + if overlay_free_kb < 5120 { + anyhow::bail!( + "Not enough space to install TollGate: only {}kB free on /. \ + Need at least 5MB. Free up flash space on the router first \ + (e.g. remove unused packages with `apk del …`).", + overlay_free_kb + ); + } + + let (tar_out, tar_code) = router.run(&format!( + "tar -xzf {} -C / 2>&1", ipk_path + ))?; + if tar_code != 0 { + anyhow::bail!( + "TollGate installation failed: tar extract failed: {}", + tar_out.trim() + ); + } + } + + router.run_ok(&format!("rm -rf /tmp/_tg_install {}", ipk_path))?; Ok(()) } diff --git a/core/openwrt/src/tollgate/mod.rs b/core/openwrt/src/tollgate/mod.rs index dea969b7..da57904b 100644 --- a/core/openwrt/src/tollgate/mod.rs +++ b/core/openwrt/src/tollgate/mod.rs @@ -9,19 +9,26 @@ pub use wifi::provision_ssid; use anyhow::Result; use tracing::info; -use crate::Router; +use crate::{opkg::PkgManager, Router}; /// Full TollGate provisioning sequence: -/// 1. Install tollgate-module-basic-go via opkg +/// 1. Install tollgate-module-basic-go /// 2. Write TollGate UCI config (pricing, mint URL) /// 3. Create the pay-as-you-go WiFi SSID /// 4. Restart affected services pub async fn provision(router: &Router, config: &TollGateConfig) -> Result<()> { info!("[{}] Starting TollGate provisioning", router.host); - router.opkg_check()?; - router.opkg_update()?; - install_tollgate(router)?; + let pkg_mgr = router.opkg_check()?; + match pkg_mgr { + PkgManager::Opkg => { + router.opkg_update()?; + install_tollgate(router)?; + } + PkgManager::ApkNative => { + install::install_tollgate_apk_native(router)?; + } + } config::apply(router, config)?; wifi::provision_ssid(router, config)?; restart_services(router)?; diff --git a/core/openwrt/src/wan.rs b/core/openwrt/src/wan.rs index 35867d74..b7dde420 100644 --- a/core/openwrt/src/wan.rs +++ b/core/openwrt/src/wan.rs @@ -5,7 +5,10 @@ use crate::Router; pub struct WispConfig { pub ssid: String, pub password: String, - pub encryption: String, // psk2 | psk | sae | none + pub encryption: String, // psk2 | psk | sae | none + pub dhcp_start: u32, // first address in DHCP pool (default 100 → .100) + pub dhcp_limit: u32, // pool size (default 150 → .100–.249) + pub masq: bool, // enable NAT on WAN zone (almost always true) } pub fn configure_wisp(router: &Router, config: &WispConfig) -> Result<()> { @@ -22,6 +25,7 @@ pub fn configure_wisp(router: &Router, config: &WispConfig) -> Result<()> { router.uci_set("wireless.wwan.mode", "sta")?; router.uci_set("wireless.wwan.ssid", &config.ssid)?; router.uci_set("wireless.wwan.network", "wwan")?; + router.uci_set("wireless.wwan.disabled", "0")?; router.uci_set("wireless.wwan.encryption", &config.encryption)?; if config.encryption != "none" && !config.password.is_empty() { router.uci_set("wireless.wwan.key", &config.password)?; @@ -36,10 +40,25 @@ pub fn configure_wisp(router: &Router, config: &WispConfig) -> Result<()> { // Add wwan to the WAN firewall zone (walk zones by name) ensure_wwan_in_wan_zone(router)?; - // Apply wireless changes; fall back to full network restart if wifi reload fails - let (out, code) = router.run("wifi reload 2>&1")?; - if code != 0 { - info!("[{}] wifi reload failed ({}): {} — falling back to network restart", router.host, code, out.trim()); + // Configure LAN DHCP pool + router.uci_set("dhcp.lan.start", &config.dhcp_start.to_string())?; + router.uci_set("dhcp.lan.limit", &config.dhcp_limit.to_string())?; + router.uci_commit(Some("dhcp"))?; + + // Ensure masquerade on WAN zone so LAN clients reach the internet + if config.masq { + ensure_masq_on_wan_zone(router)?; + } + + // Full wifi cycle so wpa_supplicant restarts cleanly with the new config. + // "wifi reload" is not enough on some drivers — it keeps stale state. + let (down_out, down_code) = router.run("wifi down 2>&1")?; + if down_code != 0 { + info!("[{}] wifi down failed ({}): {}", router.host, down_code, down_out.trim()); + } + let (up_out, up_code) = router.run("wifi up 2>&1")?; + if up_code != 0 { + info!("[{}] wifi up failed ({}): {} — falling back to network restart", router.host, up_code, up_out.trim()); router.run_ok("/etc/init.d/network restart 2>&1")?; } @@ -88,6 +107,28 @@ pub fn get_wan_status(router: &Router) -> serde_json::Value { .trim() .to_string(); + // LAN info for the DHCP setup display + let lan_ip = router.uci_get("network.lan.ipaddr").unwrap_or_else(|_| "192.168.1.1".into()); + let lan_netmask = router.uci_get("network.lan.netmask").unwrap_or_else(|_| "255.255.255.0".into()); + let dhcp_start = router.uci_get("dhcp.lan.start").unwrap_or_else(|_| "100".into()); + let dhcp_limit = router.uci_get("dhcp.lan.limit").unwrap_or_else(|_| "150".into()); + + // Masquerade: check WAN zone + let masq = { + let script = "for i in $(seq 0 9); do \ + n=$(uci get firewall.@zone[$i].name 2>/dev/null) || break; \ + if [ \"$n\" = \"wan\" ]; then \ + uci get firewall.@zone[$i].masq 2>/dev/null; break; \ + fi; done"; + router.run_ok(script).unwrap_or_default().trim().to_string() == "1" + }; + + info!("[{}] WAN status: configured={} ssid={:?} assoc={:?} sta_iface={:?} sta_state={:?} ip={:?} lan={} masq={}", + router.host, configured, ssid, assoc_ssid, sta_iface, sta_state, ip, lan_ip, masq); + if !wifi_log.is_empty() { + info!("[{}] wifi_log: {}", router.host, wifi_log.replace('\n', " | ")); + } + serde_json::json!({ "configured": configured, "ssid": ssid, @@ -99,6 +140,11 @@ pub fn get_wan_status(router: &Router) -> serde_json::Value { "sta_iface": sta_iface, "sta_state": sta_state, "wifi_log": wifi_log, + "lan_ip": lan_ip, + "lan_netmask": lan_netmask, + "dhcp_start": dhcp_start, + "dhcp_limit": dhcp_limit, + "masq": masq, }) } @@ -144,6 +190,19 @@ fn detect_radio(router: &Router) -> Result { anyhow::bail!("No wireless radio (radio0) found in UCI config") } +fn ensure_masq_on_wan_zone(router: &Router) -> Result<()> { + let script = "for i in $(seq 0 9); do \ + name=$(uci get firewall.@zone[$i].name 2>/dev/null) || break; \ + if [ \"$name\" = \"wan\" ]; then \ + uci set firewall.@zone[$i].masq=1 2>/dev/null; \ + uci commit firewall; \ + break; \ + fi; \ + done; echo ok"; + router.run_ok(script)?; + Ok(()) +} + fn ensure_wwan_in_wan_zone(router: &Router) -> Result<()> { // Walk zones 0-9, find the one named "wan", add wwan to its network list let script = "for i in $(seq 0 9); do \ diff --git a/core/openwrt/src/wifi_scan.rs b/core/openwrt/src/wifi_scan.rs index 683da51b..48f75f45 100644 --- a/core/openwrt/src/wifi_scan.rs +++ b/core/openwrt/src/wifi_scan.rs @@ -107,7 +107,13 @@ fn normalize_encryption(raw: &str) -> String { } else if lower.contains("wpa2") || lower.contains("psk2") { "psk2".to_string() } else if lower.contains("wpa") { - "psk".to_string() + // CCMP/AES is WPA2's cipher suite — even if iwinfo labels it "WPA PSK (CCMP)" + // it's a WPA2 network and we must use psk2 to associate correctly. + if lower.contains("ccmp") || lower.contains("aes") { + "psk2".to_string() + } else { + "psk".to_string() + } } else if lower.contains("none") || lower.contains("open") || lower.is_empty() { "none".to_string() } else { diff --git a/neode-ui/src/views/server/OpenWrtGateway.vue b/neode-ui/src/views/server/OpenWrtGateway.vue index 4cb9c72f..f1686f73 100644 --- a/neode-ui/src/views/server/OpenWrtGateway.vue +++ b/neode-ui/src/views/server/OpenWrtGateway.vue @@ -41,6 +41,11 @@ interface WanStatus { sta_iface: string sta_state: string wifi_log: string + lan_ip: string + lan_netmask: string + dhcp_start: string + dhcp_limit: string + masq: boolean } interface RouterStatus { @@ -76,13 +81,18 @@ const provisionError = ref('') const provisionSuccess = ref(false) // WAN setup flow -type WanStep = 'idle' | 'scan' | 'scanning' | 'list' | 'password' | 'connecting' | 'done' +type WanStep = 'idle' | 'scan' | 'scanning' | 'list' | 'password' | 'dhcp' | 'connecting' | 'done' const wanStep = ref('idle') const scannedNetworks = ref([]) const selectedNetwork = ref(null) const wanPassword = ref('') const wanError = ref('') +// DHCP/masq settings (step 3 of wizard) +const dhcpStart = ref(100) +const dhcpLimit = ref(150) +const masqEnabled = ref(true) + async function load(params?: Record) { loading.value = true error.value = '' @@ -143,6 +153,9 @@ function startWanSetup() { scannedNetworks.value = [] selectedNetwork.value = null wanPassword.value = '' + dhcpStart.value = Number(status.value?.wan?.dhcp_start) || 100 + dhcpLimit.value = Number(status.value?.wan?.dhcp_limit) || 150 + masqEnabled.value = true } async function scanWifi() { @@ -186,6 +199,9 @@ async function configureWan() { ssid: selectedNetwork.value.ssid, password: wanPassword.value, encryption: selectedNetwork.value.encryption, + dhcp_start: dhcpStart.value, + dhcp_limit: dhcpLimit.value, + masq: masqEnabled.value, } await rpcClient.call({ method: 'openwrt.configure-wan', params, timeout: 30000 }) wanStep.value = 'done' @@ -196,7 +212,7 @@ async function configureWan() { }, 8000) } catch (e) { wanError.value = e instanceof Error ? e.message : String(e) - wanStep.value = 'password' + wanStep.value = 'dhcp' } } @@ -375,6 +391,25 @@ onMounted(() => load()) Not configured — router has no internet access. + +
+
+
LAN
+
{{ status.wan.lan_ip }}/24
+
+
+
DHCP
+
.{{ status.wan.dhcp_start }}+{{ status.wan.dhcp_limit }}
+
+
+
NAT
+
+ {{ status.wan.masq ? 'on' : 'off' }} +
+
+
+
@@ -478,22 +513,80 @@ onMounted(() => load()) type="password" placeholder="WiFi password" class="w-full px-4 py-3 mb-3 bg-transparent border border-white/20 rounded-lg text-white placeholder-white/30 focus:outline-none focus:border-white/40 transition-colors" - @keyup.enter="configureWan" + @keyup.enter="wanStep = 'dhcp'" />

{{ wanError }}

+ + +