use anyhow::Result; use tracing::info; use crate::Router; pub struct WispConfig { pub ssid: String, pub password: String, 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<()> { info!("[{}] Configuring WISP → ssid={}", router.host, config.ssid); let radio = detect_radio(router)?; // Ensure the radio is enabled (disabled=1 by default on fresh flash) router.uci_set("wireless.radio0.disabled", "0")?; // Create/update named sta wifi-iface "wwan" (idempotent: uci set creates if absent) router.uci_set("wireless.wwan", "wifi-iface")?; router.uci_set("wireless.wwan.device", &radio)?; 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)?; } router.uci_commit(Some("wireless"))?; // Create/update wwan network interface (DHCP) router.uci_set("network.wwan", "interface")?; router.uci_set("network.wwan.proto", "dhcp")?; router.uci_commit(Some("network"))?; // Add wwan to the WAN firewall zone (walk zones by name) ensure_wwan_in_wan_zone(router)?; // 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")?; } Ok(()) } pub fn get_wan_status(router: &Router) -> serde_json::Value { let configured = router .uci_get("network.wwan.proto") .map(|v| v == "dhcp") .unwrap_or(false); let ssid = router.uci_get("wireless.wwan.ssid").unwrap_or_default(); let encryption = router.uci_get("wireless.wwan.encryption").unwrap_or_default(); let radio0_disabled = router .uci_get("wireless.radio0.disabled") .map(|v| v == "1") .unwrap_or(false); // Find the active sta-mode interface and its association state let iw_out = router.run_ok("iw dev 2>/dev/null").unwrap_or_default(); let (sta_iface, assoc_ssid) = parse_sta_iface(&iw_out); // Interface operstate (up / down / absent) let sta_state = if !sta_iface.is_empty() { router .run_ok(&format!("cat /sys/class/net/{}/operstate 2>/dev/null", sta_iface)) .unwrap_or_else(|_| "unknown".into()) .trim() .to_string() } else { "absent".to_string() }; // Source IP for reaching 8.8.8.8 — empty if no default route yet let ip = router .run_ok("ip -4 route get 8.8.8.8 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i==\"src\"){print $(i+1); exit}}'") .unwrap_or_default() .trim() .to_string(); // Recent wifi-related kernel/syslog lines for quick diagnosis let wifi_log = router .run_ok("logread 2>/dev/null | grep -iE 'wlan|wwan|wifi|assoc|deauth|auth fail|CTRL-EVENT|wpa_supplicant' | tail -8 2>/dev/null") .unwrap_or_default() .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, "assoc_ssid": assoc_ssid, "encryption": encryption, "ip": ip, "internet": !ip.is_empty(), "radio0_disabled": radio0_disabled, "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, }) } fn parse_sta_iface(iw_out: &str) -> (String, String) { let mut result_iface = String::new(); let mut result_ssid = String::new(); let mut current_iface = String::new(); let mut current_type = String::new(); let mut current_ssid = String::new(); for line in iw_out.lines() { let line = line.trim(); if let Some(name) = line.strip_prefix("Interface ") { // Save previous interface if it was a sta if current_type == "managed" && result_iface.is_empty() { result_iface = current_iface.clone(); result_ssid = current_ssid.clone(); } current_iface = name.trim().to_string(); current_type.clear(); current_ssid.clear(); } else if let Some(t) = line.strip_prefix("type ") { current_type = t.trim().to_string(); } else if let Some(s) = line.strip_prefix("ssid ") { current_ssid = s.trim().to_string(); } } // Handle last block if current_type == "managed" && result_iface.is_empty() { result_iface = current_iface; result_ssid = current_ssid; } (result_iface, result_ssid) } fn detect_radio(router: &Router) -> Result { // radio0 is universal; verify it exists let out = router.uci_get("wireless.radio0").unwrap_or_default(); if !out.is_empty() { return Ok("radio0".to_string()); } 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 \ name=$(uci get firewall.@zone[$i].name 2>/dev/null) || break; \ if [ \"$name\" = \"wan\" ]; then \ uci add_list firewall.@zone[$i].network=wwan 2>/dev/null; \ uci commit firewall; \ break; \ fi; \ done; echo ok"; router.run_ok(script)?; Ok(()) }