diff --git a/src/app.rs b/src/app.rs index 95d7fb7..426dfc8 100644 --- a/src/app.rs +++ b/src/app.rs @@ -51,16 +51,16 @@ pub async fn prepare_ap_config( ) -> Result { let mut guard = config.lock().await; - if guard.wifi_pw.is_empty() { + if guard.wifi_ap_pw.is_empty() { let pw = generate_wifi_password()?; warn!("wifi_pw missing from config, generated new password"); - guard.wifi_pw = pw; + guard.wifi_ap_pw = pw; platform .save_config(&guard) .await .map_err(|_| sunset::error::BadUsage.build())?; } - info!("WIFI PSK: {}", guard.wifi_pw); + info!("WIFI PSK: {}", guard.wifi_ap_pw); let mac = guard .resolve_mac() @@ -73,8 +73,10 @@ pub async fn prepare_ap_config( print_hostkey_fingerprint(&guard.hostkey); Ok(WifiApConfigStatic { - ssid: guard.wifi_ssid.clone(), - password: guard.wifi_pw.clone(), + ap_ssid: guard.wifi_ap_ssid.clone(), + ap_password: guard.wifi_ap_pw.clone(), + sta_ssid: guard.wifi_sta_ssid.clone(), + sta_password: guard.wifi_sta_pw.clone(), channel: 1, mac, }) diff --git a/src/config.rs b/src/config.rs index 82e1b0e..7354204 100644 --- a/src/config.rs +++ b/src/config.rs @@ -35,8 +35,12 @@ pub struct SSHStampConfig { pub pubkeys: [Option; KEY_SLOTS], /// `WiFi` - pub wifi_ssid: String<32>, - pub wifi_pw: String<63>, + /// Access Point Mode + pub wifi_ap_ssid: String<32>, + pub wifi_ap_pw: String<63>, + /// Station Mode + pub wifi_sta_ssid: String<32>, + pub wifi_sta_pw: String<63>, /// Networking /// MAC address. Special values: @@ -103,9 +107,14 @@ impl SSHStampConfig { pub fn new(default_mac: [u8; 6]) -> Result { let hostkey = SignKey::generate(KeyType::Ed25519, None)?; - let wifi_ssid = Self::generate_wifi_ssid()?; + // Wifi Access Point Mode + let wifi_ap_ssid = Self::generate_wifi_ssid()?; + let wifi_ap_pw = Self::generate_wifi_password()?; + // Wifi Station Mode + let wifi_sta_ssid = Self::generate_wifi_ssid()?; + let wifi_sta_pw = Self::generate_wifi_password()?; + let mac = default_mac; - let wifi_pw = Self::generate_wifi_password()?; let uart_pins = UartPins::default(); debug!( @@ -116,8 +125,10 @@ impl SSHStampConfig { Ok(SSHStampConfig { hostkey, pubkeys: Default::default(), - wifi_ssid, - wifi_pw, + wifi_ap_ssid, + wifi_ap_pw, + wifi_sta_ssid, + wifi_sta_pw, mac, ipv4_static: None, #[cfg(feature = "ipv6")] @@ -310,8 +321,12 @@ impl SSHEncode for SSHStampConfig { enc_option(k.as_ref(), s)?; } - self.wifi_ssid.as_str().enc(s)?; - self.wifi_pw.as_str().enc(s)?; + // Wifi Access Point Mode + self.wifi_ap_ssid.as_str().enc(s)?; + self.wifi_ap_pw.as_str().enc(s)?; + // Wifi Station Mode + self.wifi_sta_ssid.as_str().enc(s)?; + self.wifi_sta_pw.as_str().enc(s)?; self.mac.enc(s)?; enc_ipv4_config(self.ipv4_static.as_ref(), s)?; @@ -341,8 +356,12 @@ impl<'de> SSHDecode<'de> for SSHStampConfig { *k = dec_option(s)?; } - let wifi_ssid = SSHDecode::dec(s)?; - let wifi_pw = SSHDecode::dec(s)?; + // Wifi Access Point Mode + let wifi_ap_ssid = SSHDecode::dec(s)?; + let wifi_ap_pw = SSHDecode::dec(s)?; + // Wifi Station Mode + let wifi_sta_ssid = SSHDecode::dec(s)?; + let wifi_sta_pw = SSHDecode::dec(s)?; let mac = SSHDecode::dec(s)?; @@ -361,8 +380,10 @@ impl<'de> SSHDecode<'de> for SSHStampConfig { Ok(Self { hostkey, pubkeys, - wifi_ssid, - wifi_pw, + wifi_ap_ssid, + wifi_ap_pw, + wifi_sta_ssid, + wifi_sta_pw, mac, ipv4_static, #[cfg(feature = "ipv6")] diff --git a/src/handle.rs b/src/handle.rs index 017b61c..76ee130 100644 --- a/src/handle.rs +++ b/src/handle.rs @@ -352,11 +352,17 @@ pub async fn session_env( "SSH_STAMP_PUBKEY" => { pubkey_env(a, config, ctx).await?; } - "SSH_STAMP_WIFI_SSID" => { - wifi_ssid_env(a, config, ctx).await?; + "SSH_STAMP_WIFI_AP_SSID" => { + wifi_ap_ssid_env(a, config, ctx).await?; } - "SSH_STAMP_WIFI_PSK" => { - wifi_psk_env(a, config, ctx).await?; + "SSH_STAMP_WIFI_AP_PSK" => { + wifi_ap_psk_env(a, config, ctx).await?; + } + "SSH_STAMP_WIFI_STA_SSID" => { + wifi_sta_ssid_env(a, config, ctx).await?; + } + "SSH_STAMP_WIFI_STA_PW" => { + wifi_sta_psk_env(a, config, ctx).await?; } "SSH_STAMP_WIFI_MAC_ADDRESS" => { wifi_mac_address_env(a, config, ctx).await?; @@ -414,11 +420,11 @@ pub async fn pubkey_env( Ok(()) } -/// Handles `SSH_STAMP_WIFI_SSID` environment variable requests. +/// Handles `SSH_STAMP_WIFI_AP_SSID` environment variable requests. /// /// # Errors /// Returns an error if SSH protocol operations fail or if the SSID is invalid. -pub async fn wifi_ssid_env( +pub async fn wifi_ap_ssid_env( a: sunset::event::ServEnvironmentRequest<'_, '_>, config: &SunsetMutex, ctx: &mut EventContext<'_>, @@ -426,27 +432,83 @@ pub async fn wifi_ssid_env( let mut config_guard = config.lock().await; if *ctx.auth_checked || config_guard.first_login { if let Some(s) = env_parser::parse_wifi_ssid(a.value()?) { - config_guard.wifi_ssid = s; - debug!("Set wifi SSID from ENV"); + config_guard.wifi_ap_ssid = s; + debug!("Set wifi Access Point SSID from ENV"); a.succeed()?; *ctx.config_changed = true; *ctx.needs_reset = true; } else { - warn!("SSH_STAMP_WIFI_SSID invalid and/or too long"); + warn!("SSH_STAMP_WIFI_AP_SSID invalid and/or too long"); a.fail()?; } } else { - warn!("SSH_STAMP_WIFI_SSID env received but not authenticated; rejecting"); + warn!("SSH_STAMP_WIFI_AP_SSID env received but not authenticated; rejecting"); a.fail()?; } Ok(()) } -/// Handles `SSH_STAMP_WIFI_PSK` environment variable requests. +/// Handles `SSH_STAMP_WIFI_AP_PSK` environment variable requests. /// /// # Errors /// Returns an error if SSH protocol operations fail or if the PSK is invalid. -pub async fn wifi_psk_env( +pub async fn wifi_ap_psk_env( + a: sunset::event::ServEnvironmentRequest<'_, '_>, + config: &SunsetMutex, + ctx: &mut EventContext<'_>, +) -> Result<(), sunset::Error> { + let mut config_guard = config.lock().await; + if *ctx.auth_checked || config_guard.first_login { + if let Some(s) = env_parser::parse_wifi_psk(a.value()?) { + config_guard.wifi_ap_pw = s; + debug!("Set WIFI AP PSK from ENV"); + a.succeed()?; + *ctx.config_changed = true; + *ctx.needs_reset = true; + } else { + warn!("SSH_STAMP_WIFI_AP_PSK invalid and/or not within 8-63 characters"); + a.fail()?; + } + } else { + warn!("SSH_STAMP_WIFI_AP_PSK env received but not authenticated; rejecting"); + a.fail()?; + } + Ok(()) +} + +/// Handles `SSH_STAMP_WIFI_STA_SSID` environment variable requests. +/// +/// # Errors +/// Returns an error if SSH protocol operations fail or if the SSID is invalid. +pub async fn wifi_sta_ssid_env( + a: sunset::event::ServEnvironmentRequest<'_, '_>, + config: &SunsetMutex, + ctx: &mut EventContext<'_>, +) -> Result<(), sunset::Error> { + let mut config_guard = config.lock().await; + if *ctx.auth_checked || config_guard.first_login { + if let Some(s) = env_parser::parse_wifi_ssid(a.value()?) { + config_guard.wifi_sta_ssid = s; + debug!("Set wifi STATION SSID from ENV"); + a.succeed()?; + *ctx.config_changed = true; + *ctx.needs_reset = true; + } else { + warn!("SSH_STAMP_WIFI_STA_SSID invalid and/or too long"); + a.fail()?; + } + } else { + warn!("SSH_STAMP_WIFI_STA_SSID env received but not authenticated; rejecting"); + a.fail()?; + } + Ok(()) +} + +/// Handles `SSH_STAMP_WIFI_STA_PSK` environment variable requests. +/// +/// # Errors +/// Returns an error if SSH protocol operations fail or if the SSID is invalid. +pub async fn wifi_sta_psk_env( a: sunset::event::ServEnvironmentRequest<'_, '_>, config: &SunsetMutex, ctx: &mut EventContext<'_>, @@ -454,17 +516,17 @@ pub async fn wifi_psk_env( let mut config_guard = config.lock().await; if *ctx.auth_checked || config_guard.first_login { if let Some(s) = env_parser::parse_wifi_psk(a.value()?) { - config_guard.wifi_pw = s; - debug!("Set WIFI PSK from ENV"); + config_guard.wifi_sta_pw = s; + debug!("Set wifi STATION PSK from ENV"); a.succeed()?; *ctx.config_changed = true; *ctx.needs_reset = true; } else { - warn!("SSH_STAMP_WIFI_PSK invalid and/or not within 8-63 characters"); + warn!("SSH_STAMP_WIFI_STA_PSK invalid and/or not within 8-63 characters"); a.fail()?; } } else { - warn!("SSH_STAMP_WIFI_PSK env received but not authenticated; rejecting"); + warn!("SSH_STAMP_WIFI_STA_PSK env received but not authenticated; rejecting"); a.fail()?; } Ok(()) diff --git a/src/store.rs b/src/store.rs index 32323df..168b55b 100644 --- a/src/store.rs +++ b/src/store.rs @@ -62,11 +62,11 @@ where match load(flash, buf) { Ok(mut c) => { debug!("Good existing config"); - if c.wifi_ssid.as_str() == "ssh-stamp" { - debug!("Migrating insecure default SSID, regenerating randomly"); - c.wifi_ssid = SSHStampConfig::generate_wifi_ssid()?; - if c.wifi_pw.is_empty() { - c.wifi_pw = SSHStampConfig::generate_wifi_password()?; + if c.wifi_ap_ssid.as_str() == "ssh-stamp" { + debug!("Migrating insecure default Access Point SSID, regenerating randomly"); + c.wifi_ap_ssid = SSHStampConfig::generate_wifi_ssid()?; + if c.wifi_ap_pw.is_empty() { + c.wifi_ap_pw = SSHStampConfig::generate_wifi_password()?; } save(flash, buf, &c)?; } diff --git a/ssh-stamp-esp32/src/bin/ssh-stamp-esp32.rs b/ssh-stamp-esp32/src/bin/ssh-stamp-esp32.rs index e96f7fb..574afd9 100644 --- a/ssh-stamp-esp32/src/bin/ssh-stamp-esp32.rs +++ b/ssh-stamp-esp32/src/bin/ssh-stamp-esp32.rs @@ -162,11 +162,19 @@ async fn main(spawner: Spawner) -> ! { let ap_config = app::prepare_ap_config(config, &platform) .await .expect("Failed to prepare AP config"); - info!( - "Connect to the AP `{}` as a DHCP client with IP: {}", - ap_config.ssid.as_str(), - DEFAULT_IP - ); + if ap_config.sta_ssid.as_str().is_empty() { + info!( + "Connect to the AP `{}` as a DHCP client with IP: {}", + ap_config.ap_ssid.as_str(), + DEFAULT_IP + ); + } else { + info!( + "SSH Stamp has connected to Access Point {}. Connect to the same Access Point as a DHCP client with IP: {}", + ap_config.sta_ssid.as_str(), + DEFAULT_IP + ); + } let mut wifi = EspWifi::new(spawner, peripherals.WIFI, rng, DEFAULT_IP); wifi.configure_ap(ap_config) diff --git a/ssh-stamp-esp32/src/network/wifi.rs b/ssh-stamp-esp32/src/network/wifi.rs index 15864ec..74fd9e4 100644 --- a/ssh-stamp-esp32/src/network/wifi.rs +++ b/ssh-stamp-esp32/src/network/wifi.rs @@ -30,7 +30,7 @@ use esp_hal::peripherals::WIFI; use esp_hal::rng::Rng; use esp_radio::wifi::{ AuthenticationMethod, Config as RadioConfig, ControllerConfig, Interface as WifiInterface, - WifiController, ap::AccessPointConfig, + WifiController, ap::AccessPointConfig, sta::StationConfig, }; use log::{debug, error, warn}; use ssh_stamp_hal::{HalError, NetworkProviderHal, WifiApConfigStatic, WifiError, WifiHal}; @@ -79,8 +79,10 @@ impl WifiHal for EspWifi { impl NetworkProviderHal for EspWifi { async fn bring_up(&mut self) -> Result, HalError> { static RESOURCES_CELL: StaticCell> = StaticCell::new(); - static SSID_CELL: StaticCell> = StaticCell::new(); - static PASSWORD_CELL: StaticCell> = StaticCell::new(); + static AP_SSID_CELL: StaticCell> = StaticCell::new(); + static AP_PASSWORD_CELL: StaticCell> = StaticCell::new(); + static STA_SSID_CELL: StaticCell> = StaticCell::new(); + static STA_PASSWORD_CELL: StaticCell> = StaticCell::new(); let ap_config = self .ap_config @@ -95,13 +97,38 @@ impl NetworkProviderHal for EspWifi { esp_hal::efuse::override_mac_address(esp_hal::efuse::MacAddress::new_eui48(ap_config.mac)) .map_err(|_| HalError::Wifi(WifiError::Initialization))?; - let password = AllocString::from(ap_config.password.as_str()); - let ap_radio_config = RadioConfig::AccessPoint( - AccessPointConfig::default() - .with_ssid(AllocString::from(ap_config.ssid.as_str())) - .with_auth_method(AuthenticationMethod::Wpa2Wpa3Personal) - .with_password(password.clone()), - ); + let ap_ssid_static: &'static str = AP_SSID_CELL.init(ap_config.ap_ssid.clone()).as_str(); + let ap_password_static: &'static str = AP_PASSWORD_CELL + .init(ap_config.ap_password.clone()) + .as_str(); + let sta_ssid_static: &'static str = STA_SSID_CELL.init(ap_config.sta_ssid.clone()).as_str(); + let sta_password_static: &'static str = STA_PASSWORD_CELL + .init(ap_config.sta_password.clone()) + .as_str(); + + let ap_radio_config; + let ap_password; + let sta_password; + + if sta_ssid_static.is_empty() { + // Default to Access Point mode + ap_password = AllocString::from(ap_config.ap_password.as_str()); + ap_radio_config = RadioConfig::AccessPoint( + AccessPointConfig::default() + .with_ssid(AllocString::from(ap_config.ap_ssid.as_str())) + .with_auth_method(AuthenticationMethod::Wpa2Wpa3Personal) + .with_password(ap_password), + ); + } else { + // Client/Station Mode + sta_password = AllocString::from(ap_config.sta_password.as_str()); + ap_radio_config = RadioConfig::Station( + StationConfig::default() + .with_ssid(AllocString::from(ap_config.sta_ssid.as_str())) + .with_auth_method(AuthenticationMethod::Wpa2Wpa3Personal) + .with_password(sta_password), + ); + } let controller_config = ControllerConfig::default().with_initial_config(ap_radio_config); let (wifi_controller, interfaces) = esp_radio::wifi::new(wifi_peri, controller_config) @@ -122,12 +149,15 @@ impl NetworkProviderHal for EspWifi { seed, ); - let ssid_static: &'static str = SSID_CELL.init(ap_config.ssid.clone()).as_str(); - let password_static: &'static str = PASSWORD_CELL.init(ap_config.password.clone()).as_str(); - self.spawner.spawn( - wifi_up(wifi_controller, ssid_static, password_static) - .map_err(|_| HalError::Wifi(WifiError::Initialization))?, + wifi_up( + wifi_controller, + ap_ssid_static, + ap_password_static, + sta_ssid_static, + sta_password_static, + ) + .map_err(|_| HalError::Wifi(WifiError::Initialization))?, ); self.spawner .spawn(net_up(runner).map_err(|_| HalError::Wifi(WifiError::Initialization))?); @@ -185,15 +215,28 @@ pub async fn accept_requests<'a>( #[embassy_executor::task] pub async fn wifi_up( mut wifi_controller: WifiController<'static>, - ssid: &'static str, - password: &'static str, + ap_ssid: &'static str, + ap_password: &'static str, + sta_ssid: &'static str, + sta_password: &'static str, ) { - let ap_config = RadioConfig::AccessPoint( - AccessPointConfig::default() - .with_ssid(AllocString::from(ssid)) - .with_auth_method(AuthenticationMethod::Wpa2Wpa3Personal) - .with_password(AllocString::from(password)), - ); + let ap_config = if sta_ssid.is_empty() { + // Default to Access Point mode + RadioConfig::AccessPoint( + AccessPointConfig::default() + .with_ssid(AllocString::from(ap_ssid)) + .with_auth_method(AuthenticationMethod::Wpa2Wpa3Personal) + .with_password(AllocString::from(ap_password)), + ) + } else { + // Client/Station Mode + RadioConfig::Station( + StationConfig::default() + .with_ssid(AllocString::from(sta_ssid)) + .with_auth_method(AuthenticationMethod::Wpa2Wpa3Personal) + .with_password(AllocString::from(sta_password)), + ) + }; loop { match wifi_controller.set_config(&ap_config) { diff --git a/ssh-stamp-hal/src/config.rs b/ssh-stamp-hal/src/config.rs index e0ebae5..6d90f7d 100644 --- a/ssh-stamp-hal/src/config.rs +++ b/ssh-stamp-hal/src/config.rs @@ -35,12 +35,15 @@ impl Default for UartConfig { /// Contains settings for running the device as a `WiFi` access point. #[derive(Clone, Debug)] pub struct WifiApConfigStatic { + /// Wifi Mode - Access Point (ap) or Station (sta) Mode. Access Point by default. /// Network name (SSID), max 32 characters. - pub ssid: String<32>, + pub ap_ssid: String<32>, + pub sta_ssid: String<32>, /// Mandatory `WiFi` password, max 63 characters. /// We don't want None here as it would present an open network, /// which is not something we want to support. - pub password: String<63>, + pub ap_password: String<63>, + pub sta_password: String<63>, /// `WiFi` channel (1-14 for 2.4GHz). pub channel: u8, /// MAC address for the access point interface. @@ -50,8 +53,10 @@ pub struct WifiApConfigStatic { impl Default for WifiApConfigStatic { fn default() -> Self { Self { - ssid: String::new(), - password: String::new(), + ap_ssid: String::new(), + ap_password: String::new(), + sta_ssid: String::new(), + sta_password: String::new(), channel: 1, mac: [0; 6], }