From 25d6e19c11288a0ee40b0e2119b8c849e8cf0b22 Mon Sep 17 00:00:00 2001 From: Jorik Cronenberg <jcronenberg@suse.de> Date: Wed, 11 Sep 2024 17:06:10 +0200 Subject: [PATCH] Add additional wireless settings --- rust/agama-lib/share/profile.schema.json | 60 ++++++++ rust/agama-lib/src/network/settings.rs | 33 +++- rust/agama-server/src/network/error.rs | 10 ++ rust/agama-server/src/network/model.rs | 187 ++++++++++++++++++++++- rust/agama-server/src/network/nm/dbus.rs | 158 +++++++++++++++++-- 5 files changed, 435 insertions(+), 13 deletions(-) diff --git a/rust/agama-lib/share/profile.schema.json b/rust/agama-lib/share/profile.schema.json index c87728ab88..6a2c007fa0 100644 --- a/rust/agama-lib/share/profile.schema.json +++ b/rust/agama-lib/share/profile.schema.json @@ -164,6 +164,66 @@ "mesh", "ap" ] + }, + "hidden": { + "title": "Indicates that the wifi network is not broadcasting it's SSID", + "type": "boolean" + }, + "band": { + "title": "Frequency band of the wifi network", + "type": "string", + "enum": [ + "a", + "bg" + ] + }, + "channel": { + "title": "Wireless channel of the wifi network", + "type": "integer", + "minimum" : 0 + }, + "bssid": { + "title": "Only allow connection to this mac address", + "type": "string" + }, + "group_algorithms": { + "type": "array", + "items": { + "title": "A list of group/broadcast encryption algorithms", + "type": "string", + "enum": [ + "wep40", + "wep104", + "tkip", + "ccmp" + ] + }, + }, + "pairwise_algorithms": { + "type": "array", + "items": { + "title": "A list of pairwise encryption algorithms", + "type": "string", + "enum": [ + "tkip", + "ccmp" + ] + } + }, + "wpa_protocol_versions": { + "type": "array", + "items": { + "title": "A list of allowed WPA protocol versions", + "type": "string", + "enum": [ + "wpa", + "rsn" + ] + } + }, + "pmf": { + "title": "Indicates whether Protected Management Frames must be enabled for the connection", + "type": "integer" } } }, diff --git a/rust/agama-lib/src/network/settings.rs b/rust/agama-lib/src/network/settings.rs index 4c9ab13cc7..d55e25688c 100644 --- a/rust/agama-lib/src/network/settings.rs +++ b/rust/agama-lib/src/network/settings.rs @@ -35,13 +35,42 @@ impl MatchSettings { } } +/// Wireless configuration #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct WirelessSettings { + /// Password of the wifi network #[serde(skip_serializing_if = "Option::is_none")] pub password: Option<String>, + /// Security method/key management pub security: String, + /// SSID of the wifi network pub ssid: String, + /// Wifi network mode pub mode: String, + /// Frequency band of the wifi network + #[serde(skip_serializing_if = "Option::is_none")] + pub band: Option<String>, + /// Wireless channel of the wifi network + #[serde(skip_serializing_if = "is_zero", default)] + pub channel: u32, + /// Only allow connection to this mac address + #[serde(skip_serializing_if = "Option::is_none")] + pub bssid: Option<String>, + /// Indicates that the wifi network is not broadcasting it's SSID + #[serde(skip_serializing_if = "std::ops::Not::not", default)] + pub hidden: bool, + /// A list of group/broadcast encryption algorithms + #[serde(skip_serializing_if = "Vec::is_empty")] + pub group_algorithms: Vec<String>, + /// A list of pairwise encryption algorithms + #[serde(skip_serializing_if = "Vec::is_empty")] + pub pairwise_algorithms: Vec<String>, + /// A list of allowed WPA protocol versions + #[serde(skip_serializing_if = "Vec::is_empty")] + pub wpa_protocol_versions: Vec<String>, + /// Indicates whether Protected Management Frames must be enabled for the connection + #[serde(skip_serializing_if = "is_zero", default)] + pub pmf: i32, } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -154,8 +183,8 @@ pub struct NetworkConnection { pub ieee_8021x: Option<IEEE8021XSettings>, } -fn is_zero(u: &u32) -> bool { - *u == 0 +fn is_zero<T: PartialEq + From<u16>>(u: &T) -> bool { + *u == T::from(0) } impl NetworkConnection { diff --git a/rust/agama-server/src/network/error.rs b/rust/agama-server/src/network/error.rs index a3f349582d..701a734c53 100644 --- a/rust/agama-server/src/network/error.rs +++ b/rust/agama-server/src/network/error.rs @@ -40,6 +40,16 @@ pub enum NetworkStateError { InvalidEAPMethod(String), #[error("Invalid phase2 authentication method: '{0}'")] InvalidPhase2AuthMethod(String), + #[error("Invalid group algorithm: '{0}'")] + InvalidGroupAlgorithm(String), + #[error("Invalid pairwise algorithm: '{0}'")] + InvalidPairwiseAlgorithm(String), + #[error("Invalid WPA protocol version: '{0}'")] + InvalidWPAProtocolVersion(String), + #[error("Invalid wireless band: '{0}'")] + InvalidWirelessBand(String), + #[error("Invalid bssid: '{0}'")] + InvalidBssid(String), } impl From<NetworkStateError> for zbus::fdo::Error { diff --git a/rust/agama-server/src/network/model.rs b/rust/agama-server/src/network/model.rs index 4c3dd0b043..7ffcd2327c 100644 --- a/rust/agama-server/src/network/model.rs +++ b/rust/agama-server/src/network/model.rs @@ -971,13 +971,19 @@ pub struct WirelessConfig { pub security: SecurityProtocol, #[serde(skip_serializing_if = "Option::is_none")] pub band: Option<WirelessBand>, - #[serde(skip_serializing_if = "Option::is_none")] - pub channel: Option<u32>, + pub channel: u32, #[serde(skip_serializing_if = "Option::is_none")] pub bssid: Option<macaddr::MacAddr6>, #[serde(skip_serializing_if = "Option::is_none")] pub wep_security: Option<WEPSecurity>, pub hidden: bool, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub group_algorithms: Vec<GroupAlgorithm>, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub pairwise_algorithms: Vec<PairwiseAlgorithm>, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub wpa_protocol_versions: Vec<WPAProtocolVersion>, + pub pmf: i32, } impl TryFrom<ConnectionConfig> for WirelessConfig { @@ -998,11 +1004,60 @@ impl TryFrom<WirelessSettings> for WirelessConfig { let ssid = SSID(settings.ssid.as_bytes().into()); let mode = WirelessMode::try_from(settings.mode.as_str())?; let security = SecurityProtocol::try_from(settings.security.as_str())?; + let band = if let Some(band) = &settings.band { + Some( + WirelessBand::try_from(band.as_str()) + .map_err(|_| NetworkStateError::InvalidWirelessBand(band.to_string()))?, + ) + } else { + None + }; + let bssid = if let Some(bssid) = &settings.bssid { + Some( + macaddr::MacAddr6::from_str(bssid) + .map_err(|_| NetworkStateError::InvalidBssid(bssid.to_string()))?, + ) + } else { + None + }; + let group_algorithms = settings + .group_algorithms + .iter() + .map(|x| { + GroupAlgorithm::from_str(x) + .map_err(|_| NetworkStateError::InvalidGroupAlgorithm(x.to_string())) + }) + .collect::<Result<Vec<GroupAlgorithm>, NetworkStateError>>()?; + let pairwise_algorithms = settings + .pairwise_algorithms + .iter() + .map(|x| { + PairwiseAlgorithm::from_str(x) + .map_err(|_| NetworkStateError::InvalidGroupAlgorithm(x.to_string())) + }) + .collect::<Result<Vec<PairwiseAlgorithm>, NetworkStateError>>()?; + let wpa_protocol_versions = settings + .wpa_protocol_versions + .iter() + .map(|x| { + WPAProtocolVersion::from_str(x) + .map_err(|_| NetworkStateError::InvalidGroupAlgorithm(x.to_string())) + }) + .collect::<Result<Vec<WPAProtocolVersion>, NetworkStateError>>()?; + Ok(WirelessConfig { ssid, mode, security, password: settings.password, + band, + channel: settings.channel, + bssid, + hidden: settings.hidden, + group_algorithms, + pairwise_algorithms, + wpa_protocol_versions, + pmf: settings.pmf, ..Default::default() }) } @@ -1012,11 +1067,37 @@ impl TryFrom<WirelessConfig> for WirelessSettings { type Error = NetworkStateError; fn try_from(wireless: WirelessConfig) -> Result<Self, Self::Error> { + let band = wireless.band.map(|x| x.to_string()); + let bssid = wireless.bssid.map(|x| x.to_string()); + let group_algorithms = wireless + .group_algorithms + .iter() + .map(|x| x.to_string()) + .collect::<Vec<String>>(); + let pairwise_algorithms = wireless + .pairwise_algorithms + .iter() + .map(|x| x.to_string()) + .collect::<Vec<String>>(); + let wpa_protocol_versions = wireless + .wpa_protocol_versions + .iter() + .map(|x| x.to_string()) + .collect::<Vec<String>>(); + Ok(WirelessSettings { ssid: wireless.ssid.to_string(), mode: wireless.mode.to_string(), security: wireless.security.to_string(), password: wireless.password, + band, + channel: wireless.channel, + bssid, + hidden: wireless.hidden, + group_algorithms, + pairwise_algorithms, + wpa_protocol_versions, + pmf: wireless.pmf, }) } } @@ -1105,6 +1186,108 @@ impl TryFrom<&str> for SecurityProtocol { } } +#[derive(Debug, Clone, Copy, PartialEq, Serialize)] +pub enum GroupAlgorithm { + Wep40, + Wep104, + Tkip, + Ccmp, +} + +#[derive(Debug, Error)] +#[error("Invalid group algorithm: {0}")] +pub struct InvalidGroupAlgorithm(String); + +impl FromStr for GroupAlgorithm { + type Err = InvalidGroupAlgorithm; + + fn from_str(value: &str) -> Result<Self, Self::Err> { + match value { + "wep40" => Ok(GroupAlgorithm::Wep40), + "wep104" => Ok(GroupAlgorithm::Wep104), + "tkip" => Ok(GroupAlgorithm::Tkip), + "ccmp" => Ok(GroupAlgorithm::Ccmp), + _ => Err(InvalidGroupAlgorithm(value.to_string())), + } + } +} + +impl fmt::Display for GroupAlgorithm { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let name = match &self { + GroupAlgorithm::Wep40 => "wep40", + GroupAlgorithm::Wep104 => "wep104", + GroupAlgorithm::Tkip => "tkip", + GroupAlgorithm::Ccmp => "ccmp", + }; + write!(f, "{}", name) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Serialize)] +pub enum PairwiseAlgorithm { + Tkip, + Ccmp, +} + +#[derive(Debug, Error)] +#[error("Invalid pairwise algorithm: {0}")] +pub struct InvalidPairwiseAlgorithm(String); + +impl FromStr for PairwiseAlgorithm { + type Err = InvalidPairwiseAlgorithm; + + fn from_str(value: &str) -> Result<Self, Self::Err> { + match value { + "tkip" => Ok(PairwiseAlgorithm::Tkip), + "ccmp" => Ok(PairwiseAlgorithm::Ccmp), + _ => Err(InvalidPairwiseAlgorithm(value.to_string())), + } + } +} + +impl fmt::Display for PairwiseAlgorithm { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let name = match &self { + PairwiseAlgorithm::Tkip => "tkip", + PairwiseAlgorithm::Ccmp => "ccmp", + }; + write!(f, "{}", name) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Serialize)] +pub enum WPAProtocolVersion { + Wpa, + Rsn, +} + +#[derive(Debug, Error)] +#[error("Invalid WPA protocol version: {0}")] +pub struct InvalidWPAProtocolVersion(String); + +impl FromStr for WPAProtocolVersion { + type Err = InvalidWPAProtocolVersion; + + fn from_str(value: &str) -> Result<Self, Self::Err> { + match value { + "wpa" => Ok(WPAProtocolVersion::Wpa), + "rsn" => Ok(WPAProtocolVersion::Rsn), + _ => Err(InvalidWPAProtocolVersion(value.to_string())), + } + } +} + +impl fmt::Display for WPAProtocolVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let name = match &self { + WPAProtocolVersion::Wpa => "wpa", + WPAProtocolVersion::Rsn => "rsn", + }; + write!(f, "{}", name) + } +} + #[derive(Debug, Default, PartialEq, Clone, Serialize)] pub struct WEPSecurity { pub auth_alg: WEPAuthAlg, diff --git a/rust/agama-server/src/network/nm/dbus.rs b/rust/agama-server/src/network/nm/dbus.rs index 4d8a861e4e..68a3f5a095 100644 --- a/rust/agama-server/src/network/nm/dbus.rs +++ b/rust/agama-server/src/network/nm/dbus.rs @@ -384,16 +384,43 @@ fn wireless_config_to_dbus<'a>(config: &'a WirelessConfig) -> NestedHash<'a> { if let Some(band) = &config.band { wireless.insert("band", band.to_string().into()); - if let Some(channel) = config.channel { - wireless.insert("channel", channel.into()); - } + wireless.insert("channel", config.channel.into()); } if let Some(bssid) = &config.bssid { wireless.insert("bssid", bssid.as_bytes().into()); } - let mut security: HashMap<&str, zvariant::Value> = - HashMap::from([("key-mgmt", config.security.to_string().into())]); + let mut security: HashMap<&str, zvariant::Value> = HashMap::from([ + ("key-mgmt", config.security.to_string().into()), + ( + "group", + config + .group_algorithms + .iter() + .map(|x| x.to_string()) + .collect::<Vec<String>>() + .into(), + ), + ( + "pairwise", + config + .pairwise_algorithms + .iter() + .map(|x| x.to_string()) + .collect::<Vec<String>>() + .into(), + ), + ( + "proto", + config + .wpa_protocol_versions + .iter() + .map(|x| x.to_string()) + .collect::<Vec<String>>() + .into(), + ), + ("pmf", Value::new(config.pmf)), + ]); if let Some(password) = &config.password { security.insert("psk", password.to_string().into()); @@ -878,7 +905,7 @@ fn wireless_config_from_dbus(conn: &OwnedNestedHash) -> Option<WirelessConfig> { wireless_config.band = Some(band.downcast_ref::<str>()?.try_into().ok()?) } if let Some(channel) = wireless.get("channel") { - wireless_config.channel = Some(*channel.downcast_ref()?); + wireless_config.channel = *channel.downcast_ref()?; } if let Some(bssid) = wireless.get("bssid") { let bssid: &zvariant::Array = bssid.downcast_ref()?; @@ -896,7 +923,6 @@ fn wireless_config_from_dbus(conn: &OwnedNestedHash) -> Option<WirelessConfig> { *bssid.get(5)?, )); } - if let Some(hidden) = wireless.get("hidden") { wireless_config.hidden = *hidden.downcast_ref::<bool>()?; } @@ -931,6 +957,48 @@ fn wireless_config_from_dbus(conn: &OwnedNestedHash) -> Option<WirelessConfig> { } _ => wireless_config.wep_security = None, } + if let Some(group_algorithms) = security.get("group") { + let group_algorithms: &zvariant::Array = group_algorithms.downcast_ref()?; + let group_algorithms: Vec<&str> = group_algorithms + .iter() + .map(|x| x.downcast_ref::<str>()) + .collect::<Option<Vec<&str>>>()?; + let group_algorithms: Vec<GroupAlgorithm> = group_algorithms + .iter() + .map(|x| GroupAlgorithm::from_str(x)) + .collect::<Result<Vec<GroupAlgorithm>, InvalidGroupAlgorithm>>() + .ok()?; + wireless_config.group_algorithms = group_algorithms + } + if let Some(pairwise_algorithms) = security.get("pairwise") { + let pairwise_algorithms: &zvariant::Array = pairwise_algorithms.downcast_ref()?; + let pairwise_algorithms: Vec<&str> = pairwise_algorithms + .iter() + .map(|x| x.downcast_ref::<str>()) + .collect::<Option<Vec<&str>>>()?; + let pairwise_algorithms: Vec<PairwiseAlgorithm> = pairwise_algorithms + .iter() + .map(|x| PairwiseAlgorithm::from_str(x)) + .collect::<Result<Vec<PairwiseAlgorithm>, InvalidPairwiseAlgorithm>>() + .ok()?; + wireless_config.pairwise_algorithms = pairwise_algorithms + } + if let Some(wpa_protocol_versions) = security.get("proto") { + let wpa_protocol_versions: &zvariant::Array = wpa_protocol_versions.downcast_ref()?; + let wpa_protocol_versions: Vec<&str> = wpa_protocol_versions + .iter() + .map(|x| x.downcast_ref::<str>()) + .collect::<Option<Vec<&str>>>()?; + let wpa_protocol_versions: Vec<WPAProtocolVersion> = wpa_protocol_versions + .iter() + .map(|x| WPAProtocolVersion::from_str(x)) + .collect::<Result<Vec<WPAProtocolVersion>, InvalidWPAProtocolVersion>>() + .ok()?; + wireless_config.wpa_protocol_versions = wpa_protocol_versions + } + if let Some(pmf) = security.get("pmf") { + wireless_config.pmf = *pmf.downcast_ref::<i32>()?; + } } Some(wireless_config) @@ -1369,6 +1437,16 @@ mod test { ), ("auth-alg".to_string(), Value::new("open").to_owned()), ("wep-tx-keyidx".to_string(), Value::new(1_u32).to_owned()), + ( + "group".to_string(), + Value::new(vec!["wep40", "tkip"]).to_owned(), + ), + ( + "pairwise".to_string(), + Value::new(vec!["tkip", "ccmp"]).to_owned(), + ), + ("proto".to_string(), Value::new(vec!["rsn"]).to_owned()), + ("pmf".to_string(), Value::new(2_i32).to_owned()), ]); let dbus_conn = HashMap::from([ @@ -1385,13 +1463,26 @@ mod test { assert_eq!(wireless.mode, WirelessMode::Infra); assert_eq!(wireless.security, SecurityProtocol::WPA2); assert_eq!(wireless.band, Some(WirelessBand::A)); - assert_eq!(wireless.channel, Some(32_u32)); + assert_eq!(wireless.channel, 32_u32); assert_eq!( wireless.bssid, Some(macaddr::MacAddr6::from_str("12:34:56:78:9A:BC").unwrap()) ); assert!(!wireless.hidden); assert_eq!(wireless.wep_security, None); + assert_eq!( + wireless.group_algorithms, + vec![GroupAlgorithm::Wep40, GroupAlgorithm::Tkip] + ); + assert_eq!( + wireless.pairwise_algorithms, + vec![PairwiseAlgorithm::Tkip, PairwiseAlgorithm::Ccmp] + ); + assert_eq!( + wireless.wpa_protocol_versions, + vec![WPAProtocolVersion::Rsn] + ); + assert_eq!(wireless.pmf, 2_i32); } } @@ -1569,7 +1660,7 @@ mod test { password: Some("wpa-password".to_string()), ssid: SSID(vec![97, 103, 97, 109, 97]), band: Some(WirelessBand::BG), - channel: Some(10), + channel: 10, bssid: Some(macaddr::MacAddr6::from_str("12:34:56:78:9A:BC").unwrap()), wep_security: Some(WEPSecurity { auth_alg: WEPAuthAlg::Open, @@ -1581,6 +1672,10 @@ mod test { ], }), hidden: true, + group_algorithms: vec![GroupAlgorithm::Wep104, GroupAlgorithm::Tkip], + pairwise_algorithms: vec![PairwiseAlgorithm::Tkip, PairwiseAlgorithm::Ccmp], + wpa_protocol_versions: vec![WPAProtocolVersion::Wpa], + pmf: 1, ..Default::default() }; let mut wireless = build_base_connection(); @@ -1656,6 +1751,51 @@ mod test { assert_eq!(wep_key0, "5b73215e232f4c577c5073455d"); let wep_key1: &str = security.get("wep-key1").unwrap().downcast_ref().unwrap(); assert_eq!(wep_key1, "hello"); + + let group_algorithms: &zvariant::Array = + security.get("group").unwrap().downcast_ref().unwrap(); + let group_algorithms: Vec<GroupAlgorithm> = group_algorithms + .get() + .iter() + .map(|x| x.downcast_ref::<str>().unwrap()) + .collect::<Vec<&str>>() + .iter() + .map(|x| GroupAlgorithm::from_str(x).unwrap()) + .collect(); + assert_eq!( + group_algorithms, + vec![GroupAlgorithm::Wep104, GroupAlgorithm::Tkip] + ); + + let pairwise_algorithms: &zvariant::Array = + security.get("pairwise").unwrap().downcast_ref().unwrap(); + let pairwise_algorithms: Vec<PairwiseAlgorithm> = pairwise_algorithms + .get() + .iter() + .map(|x| x.downcast_ref::<str>().unwrap()) + .collect::<Vec<&str>>() + .iter() + .map(|x| PairwiseAlgorithm::from_str(x).unwrap()) + .collect(); + assert_eq!( + pairwise_algorithms, + vec![PairwiseAlgorithm::Tkip, PairwiseAlgorithm::Ccmp] + ); + + let wpa_protocol_versions: &zvariant::Array = + security.get("proto").unwrap().downcast_ref().unwrap(); + let wpa_protocol_versions: Vec<WPAProtocolVersion> = wpa_protocol_versions + .get() + .iter() + .map(|x| x.downcast_ref::<str>().unwrap()) + .collect::<Vec<&str>>() + .iter() + .map(|x| WPAProtocolVersion::from_str(x).unwrap()) + .collect(); + assert_eq!(wpa_protocol_versions, vec![WPAProtocolVersion::Wpa]); + + let pmf: i32 = *security.get("pmf").unwrap().downcast_ref().unwrap(); + assert_eq!(pmf, 1); } #[test]