ssmithx edbad30501 fix(openwrt): TollGate apk-native install for OpenWrt 25.x
- 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 <noreply@anthropic.com>
2026-06-30 17:12:57 +00:00

219 lines
8.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<String> {
// 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(())
}