From 9ed6c1263caaadaf3385f173c5235c89c80d5ac2 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Wed, 18 Mar 2026 15:59:58 -0600 Subject: [PATCH] fix: derive wifi interface dynamically from gateway info instead of detecting at startup Remove static wifi_interface/ethernet_interface fields from RpcContextSeed. Instead, look up the wifi interface from the DB (populated by gateway sync) and check ethernet connectivity by querying gateway entries. This ensures the wifi manager always uses the correct interface even if network devices change after boot. --- core/src/context/rpc.rs | 11 +- core/src/db/model/public.rs | 4 +- core/src/init.rs | 26 ++--- core/src/net/gateway.rs | 35 ++++-- core/src/net/wifi.rs | 227 +++++++++++++++++++----------------- 5 files changed, 164 insertions(+), 139 deletions(-) diff --git a/core/src/context/rpc.rs b/core/src/context/rpc.rs index 204b000b5..dbb886f3c 100644 --- a/core/src/context/rpc.rs +++ b/core/src/context/rpc.rs @@ -35,7 +35,6 @@ use crate::lxc::LxcManager; use crate::net::gateway::WildcardListener; use crate::net::net_controller::{NetController, NetService}; use crate::net::socks::DEFAULT_SOCKS_LISTEN; -use crate::net::utils::{find_eth_iface, find_wifi_iface}; use crate::net::web_server::WebServerAcceptorSetter; use crate::net::wifi::WpaCli; use crate::prelude::*; @@ -55,8 +54,6 @@ use crate::{DATA_DIR, PLATFORM, PackageId}; pub struct RpcContextSeed { is_closed: AtomicBool, pub os_partitions: OsPartitionInfo, - pub wifi_interface: Option, - pub ethernet_interface: String, pub disk_guid: InternedString, pub ephemeral_sessions: SyncMutex, pub db: TypedPatchDb, @@ -73,7 +70,7 @@ pub struct RpcContextSeed { pub open_authed_continuations: OpenAuthedContinuations>, pub rpc_continuations: RpcContinuations, pub callbacks: Arc, - pub wifi_manager: Arc>>, + pub wifi_manager: RwLock>, pub current_secret: Arc, pub client: Client, pub start_time: Instant, @@ -323,13 +320,9 @@ impl RpcContext { }); } - let wifi_interface = find_wifi_iface().await?; - let seed = Arc::new(RpcContextSeed { is_closed: AtomicBool::new(false), os_partitions: OsPartitionInfo::from_fstab().await?, - wifi_interface: wifi_interface.clone(), - ethernet_interface: find_eth_iface().await?, disk_guid, ephemeral_sessions: SyncMutex::new(Sessions::new()), sync_db: watch::Sender::new(db.sequence().await), @@ -350,7 +343,7 @@ impl RpcContext { shutdown, lxc_manager: Arc::new(LxcManager::new()), open_authed_continuations: OpenAuthedContinuations::new(), - wifi_manager: Arc::new(RwLock::new(wifi_interface.clone().map(|i| WpaCli::init(i)))), + wifi_manager: RwLock::new(None), current_secret: Arc::new( Jwk::generate_ec_key(josekit::jwk::alg::ec::EcCurve::P256).map_err(|e| { tracing::debug!("{:?}", e); diff --git a/core/src/db/model/public.rs b/core/src/db/model/public.rs index 9477dac82..11b68c226 100644 --- a/core/src/db/model/public.rs +++ b/core/src/db/model/public.rs @@ -98,7 +98,7 @@ impl Public { port_forwards: BTreeSet::new(), }, wifi: WifiInfo { - enabled: true, + enabled: false, ..Default::default() }, gateways: OrdMap::new(), @@ -378,7 +378,7 @@ pub struct ServerStatus { #[ts(export)] pub struct WifiInfo { pub enabled: bool, - pub interface: Option, + pub interface: Option, pub ssids: BTreeSet, pub selected: Option, #[ts(type = "string | null")] diff --git a/core/src/init.rs b/core/src/init.rs index ce489c0e9..eb74d6a19 100644 --- a/core/src/init.rs +++ b/core/src/init.rs @@ -23,7 +23,6 @@ use crate::middleware::auth::local::LocalAuthContext; use crate::net::gateway::WildcardListener; use crate::net::net_controller::{NetController, NetService}; use crate::net::socks::DEFAULT_SOCKS_LISTEN; -use crate::net::utils::find_wifi_iface; use crate::net::web_server::WebServerAcceptorSetter; use crate::prelude::*; use crate::progress::{ @@ -280,20 +279,17 @@ pub async fn init( load_ca_cert.complete(); load_wifi.start(); - let wifi_interface = find_wifi_iface().await?; - let wifi = db - .mutate(|db| { - let wifi = db - .as_public_mut() - .as_server_info_mut() - .as_network_mut() - .as_wifi_mut(); - wifi.as_interface_mut().ser(&wifi_interface)?; - wifi.de() - }) - .await - .result?; - crate::net::wifi::synchronize_network_manager(MAIN_DATA, &wifi).await?; + crate::net::wifi::synchronize_network_manager( + MAIN_DATA, + &peek + .as_public() + .as_server_info() + .as_network() + .as_wifi() + .de() + .unwrap_or_default(), + ) + .await?; load_wifi.complete(); init_tmp.start(); diff --git a/core/src/net/gateway.rs b/core/src/net/gateway.rs index 91a012df1..a4959b796 100644 --- a/core/src/net/gateway.rs +++ b/core/src/net/gateway.rs @@ -34,6 +34,7 @@ use crate::db::model::public::{IpInfo, NetworkInterfaceInfo, NetworkInterfaceTyp use crate::net::forward::START9_BRIDGE_IFACE; use crate::net::gateway::device::DeviceProxy; use crate::net::host::all_hosts; +use crate::net::utils::find_wifi_iface; use crate::net::web_server::{Accept, AcceptStream, MetadataVisitor, TcpMetadata}; use crate::prelude::*; use crate::util::Invoke; @@ -775,7 +776,9 @@ async fn watcher( async fn get_wan_ipv4(iface: &str, base_url: &Url) -> Result, Error> { let client = reqwest::Client::builder(); #[cfg(target_os = "linux")] - let client = client.interface(iface); + let client = client + .interface(iface) + .local_address(IpAddr::V4(Ipv4Addr::UNSPECIFIED)); let url = base_url.join("/ip").with_kind(ErrorKind::ParseUrl)?; let text = client .build()? @@ -1289,14 +1292,15 @@ async fn poll_ip_info( }; let mut wan_ip = None; for echoip_url in echoip_urls { - let wan_ip = if echoip_ratelimit_state + if echoip_ratelimit_state .get(&echoip_url) .map_or(true, |i| i.elapsed() > Duration::from_secs(300)) && !subnets.is_empty() && !matches!( device_type, Some(NetworkInterfaceType::Bridge | NetworkInterfaceType::Loopback) - ) { + ) + { match get_wan_ipv4(iface.as_str(), &echoip_url).await { Ok(a) => { wan_ip = a; @@ -1458,12 +1462,27 @@ impl NetworkInterfaceController { ) -> Result<(), Error> { tracing::debug!("syncronizing {info:?} to db"); + let mut wifi_iface = info + .iter() + .find(|(_, info)| { + info.ip_info.as_ref().map_or(false, |i| { + i.device_type == Some(NetworkInterfaceType::Wireless) + }) + }) + .map(|(id, _)| id.clone()); + if wifi_iface.is_none() { + wifi_iface = find_wifi_iface() + .await + .ok() + .and_then(|a| a) + .map(InternedString::from) + .map(GatewayId::from); + } + db.mutate(|db| { - db.as_public_mut() - .as_server_info_mut() - .as_network_mut() - .as_gateways_mut() - .ser(info)?; + let network = db.as_public_mut().as_server_info_mut().as_network_mut(); + network.as_gateways_mut().ser(info)?; + network.as_wifi_mut().as_interface_mut().ser(&wifi_iface)?; let hostname = crate::hostname::ServerHostname::load(db.as_public().as_server_info())?; let ports = db.as_private().as_available_ports().de()?; for host in all_hosts(db) { diff --git a/core/src/net/wifi.rs b/core/src/net/wifi.rs index 57ccbd107..cead621bb 100644 --- a/core/src/net/wifi.rs +++ b/core/src/net/wifi.rs @@ -1,6 +1,5 @@ use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::path::Path; -use std::sync::Arc; use std::time::Duration; use clap::Parser; @@ -11,30 +10,100 @@ use regex::Regex; use rpc_toolkit::{Context, Empty, HandlerExt, ParentHandler, from_fn_async}; use serde::{Deserialize, Serialize}; use tokio::process::Command; -use tokio::sync::RwLock; +use tokio::sync::{RwLockMappedWriteGuard, RwLockReadGuard, RwLockWriteGuard}; use tracing::instrument; use ts_rs::TS; use crate::context::{CliContext, RpcContext}; use crate::db::model::Database; -use crate::db::model::public::WifiInfo; +use crate::db::model::public::{NetworkInterfaceType, WifiInfo}; use crate::prelude::*; use crate::util::Invoke; use crate::util::serde::{HandlerExtSerde, WithIoFormat, display_serializable}; -use crate::{Error, ErrorKind}; +use crate::{Error, ErrorKind, GatewayId}; -type WifiManager = Arc>>; +impl RpcContext { + async fn read_wifi_manager(&self) -> Result, Error> { + let err = || { + Error::new( + color_eyre::eyre::eyre!("{}", t!("net.wifi.no-interface-available")), + ErrorKind::Wifi, + ) + }; + let Some(interface) = self + .db + .peek() + .await + .as_public() + .as_server_info() + .as_network() + .as_wifi() + .as_interface() + .de()? + else { + return Err(err()); + }; + let mut cli = RwLockReadGuard::try_map(self.wifi_manager.read().await, |c| c.as_ref()).ok(); + while cli.as_ref().map_or(true, |c| c.interface != interface) { + drop(cli.take()); + let mut guard = self.wifi_manager.write().await; + *guard = Some(WpaCli::new(interface.clone())); + drop(guard); + cli = RwLockReadGuard::try_map(self.wifi_manager.read().await, |c| c.as_ref()).ok(); + } + cli.ok_or_else(err) + } -// pub fn wifi_manager(ctx: &RpcContext) -> Result<&WifiManager, Error> { -// if let Some(wifi_manager) = ctx.wifi_manager.as_ref() { -// Ok(wifi_manager) -// } else { -// Err(Error::new( -// color_eyre::eyre::eyre!("{}", t!("net.wifi.no-interface-available")), -// ErrorKind::Wifi, -// )) -// } -// } + async fn write_wifi_manager(&self) -> Result, Error> { + let err = || { + Error::new( + color_eyre::eyre::eyre!("{}", t!("net.wifi.no-interface-available")), + ErrorKind::Wifi, + ) + }; + let Some(interface) = self + .db + .peek() + .await + .as_public() + .as_server_info() + .as_network() + .as_wifi() + .as_interface() + .de()? + else { + return Err(err()); + }; + let mut cli = self.wifi_manager.write().await; + if cli.as_ref().map_or(true, |c| c.interface != interface) { + *cli = Some(WpaCli::new(interface)); + } + RwLockWriteGuard::try_map(cli, |c| c.as_mut()).map_err(|_| err()) + } + + async fn ethernet_interface_connected(&self) -> Result { + for (iface, info) in self + .db + .peek() + .await + .as_public() + .as_server_info() + .as_network() + .as_gateways() + .as_entries()? + { + let Some(info) = info.as_ip_info().transpose_ref() else { + continue; + }; + if info.as_deref().as_device_type().de()? == Some(NetworkInterfaceType::Ethernet) { + if interface_connected(iface.as_str()).await? { + return Ok(true); + } + } + } + Ok(false) + } +} pub fn wifi() -> ParentHandler { ParentHandler::new() @@ -165,55 +234,35 @@ pub async fn add( ctx: RpcContext, WifiAddParams { ssid, password }: WifiAddParams, ) -> Result<(), Error> { - let wifi_manager = ctx.wifi_manager.clone(); + let mut wpa_supplicant = ctx.write_wifi_manager().await?; if !ssid.is_ascii() { return Err(Error::new( color_eyre::eyre::eyre!("{}", t!("net.wifi.ssid-no-special-characters")), ErrorKind::Wifi, )); } + let ssid = Ssid(ssid); if !password.is_ascii() { return Err(Error::new( color_eyre::eyre::eyre!("{}", t!("net.wifi.password-no-special-characters")), ErrorKind::Wifi, )); } - async fn add_procedure( - db: TypedPatchDb, - wifi_manager: WifiManager, - ssid: &Ssid, - password: &Psk, - ) -> Result<(), Error> { - tracing::info!("{}", t!("net.wifi.adding-network", ssid = &ssid.0)); - let mut wpa_supplicant = wifi_manager.write_owned().await; - let wpa_supplicant = wpa_supplicant.as_mut().ok_or_else(|| { - Error::new( - color_eyre::eyre::eyre!("{}", t!("net.wifi.no-interface-available")), - ErrorKind::Wifi, - ) - })?; - wpa_supplicant.add_network(db, ssid, password).await?; - Ok(()) - } - if let Err(err) = add_procedure( - ctx.db.clone(), - wifi_manager.clone(), - &Ssid(ssid.clone()), - &Psk(password.clone()), - ) - .await + if let Err(err) = wpa_supplicant + .add_network(ctx.db.clone(), &ssid, &Psk(password)) + .await { tracing::error!( "{}", t!( "net.wifi.failed-to-add-network", - ssid = &ssid, + ssid = &ssid.0, error = err.to_string() ) ); tracing::debug!("{:?}", err); return Err(Error::new( - color_eyre::eyre::eyre!("{}", t!("net.wifi.failed-adding", ssid = &ssid)), + color_eyre::eyre::eyre!("{}", t!("net.wifi.failed-adding", ssid = &ssid.0)), ErrorKind::Wifi, )); } @@ -225,7 +274,7 @@ pub async fn add( .as_wifi_mut() .as_ssids_mut() .mutate(|s| { - s.insert(ssid); + s.insert(ssid.0); Ok(()) }) }) @@ -247,45 +296,36 @@ pub async fn connect( ctx: RpcContext, WifiSsidParams { ssid }: WifiSsidParams, ) -> Result<(), Error> { - let wifi_manager = ctx.wifi_manager.clone(); + let mut wpa_supplicant = ctx.write_wifi_manager().await?; if !ssid.is_ascii() { return Err(Error::new( color_eyre::eyre::eyre!("{}", t!("net.wifi.ssid-no-special-characters")), ErrorKind::Wifi, )); } - async fn connect_procedure( - db: TypedPatchDb, - wifi_manager: WifiManager, - ssid: &Ssid, - ) -> Result<(), Error> { - let mut wpa_supplicant = wifi_manager.write_owned().await; - let wpa_supplicant = wpa_supplicant.as_mut().ok_or_else(|| { - Error::new( - color_eyre::eyre::eyre!("{}", t!("net.wifi.no-interface-available")), - ErrorKind::Wifi, - ) - })?; + if let Err(err) = async { let current = wpa_supplicant.get_current_network().await?; - let connected = wpa_supplicant.select_network(db.clone(), ssid).await?; + let connected = wpa_supplicant + .select_network(ctx.db.clone(), &Ssid(ssid.clone())) + .await?; if connected { - tracing::info!("{}", t!("net.wifi.connected-successfully", ssid = &ssid.0)); + tracing::info!("{}", t!("net.wifi.connected-successfully", ssid = &ssid)); } else { - tracing::info!("{}", t!("net.wifi.connection-failed", ssid = &ssid.0)); + tracing::info!("{}", t!("net.wifi.connection-failed", ssid = &ssid)); match current { None => { tracing::info!("{}", t!("net.wifi.no-wifi-to-revert")); } Some(current) => { - wpa_supplicant.select_network(db, ¤t).await?; + wpa_supplicant + .select_network(ctx.db.clone(), ¤t) + .await?; } } } - Ok(()) + Ok::<_, Error>(()) } - - if let Err(err) = - connect_procedure(ctx.db.clone(), wifi_manager.clone(), &Ssid(ssid.clone())).await + .await { tracing::error!( "{}", @@ -321,26 +361,18 @@ pub async fn connect( #[instrument(skip_all)] pub async fn remove(ctx: RpcContext, WifiSsidParams { ssid }: WifiSsidParams) -> Result<(), Error> { - let wifi_manager = ctx.wifi_manager.clone(); + let mut wpa_supplicant = ctx.write_wifi_manager().await?; if !ssid.is_ascii() { return Err(Error::new( color_eyre::eyre::eyre!("{}", t!("net.wifi.ssid-no-special-characters")), ErrorKind::Wifi, )); } - - let mut wpa_supplicant = wifi_manager.write_owned().await; - let wpa_supplicant = wpa_supplicant.as_mut().ok_or_else(|| { - Error::new( - color_eyre::eyre::eyre!("{}", t!("net.wifi.no-interface-available")), - ErrorKind::Wifi, - ) - })?; let current = wpa_supplicant.get_current_network().await?; let ssid = Ssid(ssid); let is_current_being_removed = matches!(current, Some(current) if current == ssid); let is_current_removed_and_no_hardwire = - is_current_being_removed && !interface_connected(&ctx.ethernet_interface).await?; + is_current_being_removed && !ctx.ethernet_interface_connected().await?; if is_current_removed_and_no_hardwire { return Err(Error::new( color_eyre::eyre::eyre!("{}", t!("net.wifi.forbidden-delete-would-disconnect")), @@ -487,19 +519,12 @@ fn display_wifi_list(params: WithIoFormat, info: Vec) -> Res // #[command(display(display_wifi_info))] #[instrument(skip_all)] pub async fn get(ctx: RpcContext, _: Empty) -> Result { - let wifi_manager = ctx.wifi_manager.clone(); - let wpa_supplicant = wifi_manager.read_owned().await; - let wpa_supplicant = wpa_supplicant.as_ref().ok_or_else(|| { - Error::new( - color_eyre::eyre::eyre!("{}", t!("net.wifi.no-interface-available")), - ErrorKind::Wifi, - ) - })?; + let wpa_supplicant = ctx.read_wifi_manager().await?; let (list_networks, current_res, country_res, ethernet_res, signal_strengths) = tokio::join!( wpa_supplicant.list_networks_low(), wpa_supplicant.get_current_network(), wpa_supplicant.get_country_low(), - interface_connected(&ctx.ethernet_interface), + ctx.ethernet_interface_connected(), wpa_supplicant.list_wifi_low() ); let signal_strengths = signal_strengths?; @@ -541,14 +566,7 @@ pub async fn get(ctx: RpcContext, _: Empty) -> Result { #[instrument(skip_all)] pub async fn get_available(ctx: RpcContext, _: Empty) -> Result, Error> { - let wifi_manager = ctx.wifi_manager.clone(); - let wpa_supplicant = wifi_manager.read_owned().await; - let wpa_supplicant = wpa_supplicant.as_ref().ok_or_else(|| { - Error::new( - color_eyre::eyre::eyre!("{}", t!("net.wifi.no-interface-available")), - ErrorKind::Wifi, - ) - })?; + let wpa_supplicant = ctx.read_wifi_manager().await?; let (wifi_list, network_list) = tokio::join!( wpa_supplicant.list_wifi_low(), wpa_supplicant.list_networks_low() @@ -584,20 +602,13 @@ pub async fn set_country( ctx: RpcContext, SetCountryParams { country }: SetCountryParams, ) -> Result<(), Error> { - let wifi_manager = ctx.wifi_manager.clone(); - if !interface_connected(&ctx.ethernet_interface).await? { + if !ctx.ethernet_interface_connected().await? { return Err(Error::new( color_eyre::eyre::eyre!("{}", t!("net.wifi.wont-change-country-without-ethernet")), crate::ErrorKind::Wifi, )); } - let mut wpa_supplicant = wifi_manager.write_owned().await; - let wpa_supplicant = wpa_supplicant.as_mut().ok_or_else(|| { - Error::new( - color_eyre::eyre::eyre!("{}", t!("net.wifi.no-interface-available")), - ErrorKind::Wifi, - ) - })?; + let mut wpa_supplicant = ctx.write_wifi_manager().await?; wpa_supplicant.set_country_low(country.alpha2()).await?; for (network_id, _wifi_info) in wpa_supplicant.list_networks_low().await? { wpa_supplicant.remove_network_low(network_id).await?; @@ -611,7 +622,7 @@ pub async fn set_country( #[derive(Debug)] pub struct WpaCli { - interface: String, + interface: GatewayId, } #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct NetworkId(String); @@ -661,7 +672,7 @@ pub struct WifiInfoLow { #[derive(Clone, Debug)] pub struct Psk(String); impl WpaCli { - pub fn init(interface: String) -> Self { + pub fn new(interface: GatewayId) -> Self { WpaCli { interface } } @@ -709,7 +720,7 @@ impl WpaCli { .arg("modify") .arg(&ssid.0) .arg("ifname") - .arg(&self.interface) + .arg(self.interface.as_str()) .invoke(ErrorKind::Wifi) .await .map(|_| ()) @@ -951,7 +962,7 @@ impl WpaCli { #[instrument(skip_all)] pub async fn get_current_network(&self) -> Result, Error> { let r = Command::new("iwgetid") - .arg(&self.interface) + .arg(self.interface.as_str()) .arg("--raw") .invoke(ErrorKind::Wifi) .await?; @@ -1055,6 +1066,12 @@ pub async fn synchronize_network_manager>( .arg("all") .invoke(ErrorKind::Wifi) .await?; + } else { + Command::new("rfkill") + .arg("unblock") + .arg("all") + .invoke(ErrorKind::Wifi) + .await?; } Command::new("ip") @@ -1093,7 +1110,7 @@ pub async fn synchronize_network_manager>( }; Command::new("ifconfig") - .arg(wifi_iface) + .arg(wifi_iface.as_str()) .arg("up") .invoke(ErrorKind::Wifi) .await?;