diff --git a/rust/agama-lib/src/network/settings.rs b/rust/agama-lib/src/network/settings.rs index 7ec3be4068..21031cdf88 100644 --- a/rust/agama-lib/src/network/settings.rs +++ b/rust/agama-lib/src/network/settings.rs @@ -63,6 +63,30 @@ impl Default for BondSettings { } } +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct IEEE8021XSettings { + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub eap: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub identity: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub password: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub ca_cert: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub ca_cert_password: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub client_cert: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub client_cert_password: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub private_key: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub private_key_password: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub anonymous_identity: Option, +} + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct NetworkDevice { pub id: String, @@ -105,6 +129,8 @@ pub struct NetworkConnection { pub status: Option, #[serde(skip_serializing_if = "is_zero", default)] pub mtu: u32, + #[serde(skip_serializing_if = "Option::is_none")] + pub ieee_8021x: Option, } fn is_zero(u: &u32) -> bool { diff --git a/rust/agama-server/src/network/error.rs b/rust/agama-server/src/network/error.rs index 348f0494f6..43018d0136 100644 --- a/rust/agama-server/src/network/error.rs +++ b/rust/agama-server/src/network/error.rs @@ -36,6 +36,8 @@ pub enum NetworkStateError { InvalidWEPAuthAlg(String), #[error("Invalid WEP key type: '{0}'")] InvalidWEPKeyType(u32), + #[error("Invalid EAP method: '{0}'")] + InvalidEAPMethod(String), } impl From for zbus::fdo::Error { diff --git a/rust/agama-server/src/network/model.rs b/rust/agama-server/src/network/model.rs index bbe2f8668d..2242e8f9a7 100644 --- a/rust/agama-server/src/network/model.rs +++ b/rust/agama-server/src/network/model.rs @@ -3,7 +3,9 @@ //! * This module contains the types that represent the network concepts. They are supposed to be //! agnostic from the real network service (e.g., NetworkManager). use crate::network::error::NetworkStateError; -use agama_lib::network::settings::{BondSettings, NetworkConnection, WirelessSettings}; +use agama_lib::network::settings::{ + BondSettings, IEEE8021XSettings, NetworkConnection, WirelessSettings, +}; use agama_lib::network::types::{BondMode, DeviceState, DeviceType, Status, SSID}; use cidr::IpInet; use serde::{Deserialize, Serialize}; @@ -490,6 +492,7 @@ pub struct Connection { pub port_config: PortConfig, pub match_config: MatchConfig, pub config: ConnectionConfig, + pub ieee_8021x_config: Option, } impl Connection { @@ -560,6 +563,7 @@ impl Default for Connection { port_config: Default::default(), match_config: Default::default(), config: Default::default(), + ieee_8021x_config: Default::default(), } } } @@ -599,6 +603,10 @@ impl TryFrom for Connection { connection.config = config.into(); } + if let Some(ieee_8021x_config) = conn.ieee_8021x { + connection.ieee_8021x_config = Some(IEEE8021XConfig::try_from(ieee_8021x_config)?); + } + connection.ip_config.addresses = conn.addresses; connection.ip_config.nameservers = conn.nameservers; connection.ip_config.dns_searchlist = conn.dns_searchlist; @@ -629,6 +637,9 @@ impl TryFrom for NetworkConnection { let interface = conn.interface; let status = Some(conn.status); let mtu = conn.mtu; + let ieee_8021x: Option = conn + .ieee_8021x_config + .and_then(|x| IEEE8021XSettings::try_from(x).ok()); let mut connection = NetworkConnection { id, @@ -644,6 +655,7 @@ impl TryFrom for NetworkConnection { interface, addresses, mtu, + ieee_8021x, ..Default::default() }; @@ -1353,3 +1365,117 @@ pub enum NetworkChange { /// device gets renamed. DeviceUpdated(String, Device), } + +#[derive(Default, Debug, PartialEq, Clone, Serialize)] +pub struct IEEE8021XConfig { + pub eap: Vec, + pub identity: Option, + pub password: Option, + pub ca_cert: Option, + pub ca_cert_password: Option, + pub client_cert: Option, + pub client_cert_password: Option, + pub private_key: Option, + pub private_key_password: Option, + pub anonymous_identity: Option, +} + +impl TryFrom for IEEE8021XConfig { + type Error = NetworkStateError; + + fn try_from(value: IEEE8021XSettings) -> Result { + let eap = value + .eap + .iter() + .map(|x| { + EAPMethod::from_str(x) + .map_err(|_| NetworkStateError::InvalidEAPMethod(x.to_string())) + }) + .collect::, NetworkStateError>>()?; + + Ok(IEEE8021XConfig { + eap, + identity: value.identity, + password: value.password, + ca_cert: value.ca_cert, + ca_cert_password: value.ca_cert_password, + client_cert: value.client_cert, + client_cert_password: value.client_cert_password, + private_key: value.private_key, + private_key_password: value.private_key_password, + anonymous_identity: value.anonymous_identity, + }) + } +} + +impl TryFrom for IEEE8021XSettings { + type Error = NetworkStateError; + + fn try_from(value: IEEE8021XConfig) -> Result { + let eap = value + .eap + .iter() + .map(|x| x.to_string()) + .collect::>(); + + Ok(IEEE8021XSettings { + eap, + identity: value.identity, + password: value.password, + ca_cert: value.ca_cert, + ca_cert_password: value.ca_cert_password, + client_cert: value.client_cert, + client_cert_password: value.client_cert_password, + private_key: value.private_key, + private_key_password: value.private_key_password, + anonymous_identity: value.anonymous_identity, + }) + } +} + +#[derive(Debug, Error)] +#[error("Invalid eap method: {0}")] +pub struct InvalidEAPMethod(String); + +#[derive(Debug, PartialEq, Clone, Serialize)] +pub enum EAPMethod { + LEAP, + MD5, + TLS, + PEAP, + TTLS, + PWD, + FAST, +} + +impl FromStr for EAPMethod { + type Err = InvalidEAPMethod; + + fn from_str(s: &str) -> Result { + match s { + "leap" => Ok(Self::LEAP), + "md5" => Ok(Self::MD5), + "tls" => Ok(Self::TLS), + "peap" => Ok(Self::PEAP), + "ttls" => Ok(Self::TTLS), + "pwd" => Ok(Self::PWD), + "fast" => Ok(Self::FAST), + _ => Err(InvalidEAPMethod(s.to_string())), + } + } +} + +impl fmt::Display for EAPMethod { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let value = match &self { + Self::LEAP => "leap", + Self::MD5 => "md5", + Self::TLS => "tls", + Self::PEAP => "peap", + Self::TTLS => "ttls", + Self::PWD => "pwd", + Self::FAST => "fast", + }; + write!(f, "{}", value) + } +} diff --git a/rust/agama-server/src/network/nm/dbus.rs b/rust/agama-server/src/network/nm/dbus.rs index b86853a8ad..06974e4dc3 100644 --- a/rust/agama-server/src/network/nm/dbus.rs +++ b/rust/agama-server/src/network/nm/dbus.rs @@ -25,6 +25,7 @@ const BRIDGE_KEY: &str = "bridge"; const BRIDGE_PORT_KEY: &str = "bridge-port"; const INFINIBAND_KEY: &str = "infiniband"; const TUN_KEY: &str = "tun"; +const IEEE_8021X_KEY: &str = "802-1x"; /// Converts a connection struct into a HashMap that can be sent over D-Bus. /// @@ -137,6 +138,10 @@ pub fn connection_to_dbus<'a>( PortConfig::None => {} } + if let Some(ieee_8021x_config) = &conn.ieee_8021x_config { + result.insert(IEEE_8021X_KEY, ieee_8021x_config_to_dbus(ieee_8021x_config)); + } + result.insert("connection", connection_dbus); result } @@ -151,6 +156,10 @@ pub fn connection_from_dbus(conn: OwnedNestedHash) -> Option { connection.port_config = PortConfig::Bridge(bridge_port_config); } + if let Some(ieee_8021x_config) = ieee_8021x_config_from_dbus(&conn) { + connection.ieee_8021x_config = Some(ieee_8021x_config); + } + if let Some(wireless_config) = wireless_config_from_dbus(&conn) { connection.config = ConnectionConfig::Wireless(wireless_config); return Some(connection); @@ -981,6 +990,138 @@ fn vlan_config_from_dbus(conn: &OwnedNestedHash) -> Option { }) } +fn ieee_8021x_config_to_dbus(config: &IEEE8021XConfig) -> HashMap<&str, zvariant::Value> { + let mut ieee_8021x_config: HashMap<&str, zvariant::Value> = HashMap::from([( + "eap", + config + .eap + .iter() + .map(|x| x.to_string()) + .collect::>() + .into(), + )]); + + if let Some(identity) = &config.identity { + ieee_8021x_config.insert("identity", identity.into()); + } + if let Some(password) = &config.password { + ieee_8021x_config.insert("password", password.into()); + } + if let Some(ca_cert) = &config.ca_cert { + ieee_8021x_config.insert("ca-cert", format_nm_path(ca_cert).into_bytes().into()); + } + if let Some(ca_cert_password) = &config.ca_cert_password { + ieee_8021x_config.insert("ca-cert-password", ca_cert_password.into()); + } + if let Some(client_cert) = &config.client_cert { + ieee_8021x_config.insert( + "client-cert", + format_nm_path(client_cert).into_bytes().into(), + ); + } + if let Some(client_cert_password) = &config.client_cert_password { + ieee_8021x_config.insert("client-cert-password", client_cert_password.into()); + } + if let Some(private_key) = &config.private_key { + ieee_8021x_config.insert( + "private-key", + format_nm_path(private_key).into_bytes().into(), + ); + } + if let Some(private_key_password) = &config.private_key_password { + ieee_8021x_config.insert("private-key-password", private_key_password.into()); + } + if let Some(anonymous_identity) = &config.anonymous_identity { + ieee_8021x_config.insert("anonymous-identity", anonymous_identity.into()); + } + + ieee_8021x_config +} + +fn format_nm_path(path: &String) -> String { + format!("file://{path}\0") +} + +fn ieee_8021x_config_from_dbus(conn: &OwnedNestedHash) -> Option { + let ieee_8021x = conn.get(IEEE_8021X_KEY)?; + + let mut ieee_8021x_config = IEEE8021XConfig::default(); + + if let Some(eap) = ieee_8021x.get("eap") { + let eap: &zvariant::Array = eap.downcast_ref()?; + let eap: Vec<&str> = eap + .iter() + .map(|x| x.downcast_ref::()) + .collect::>>()?; + let eap: Vec = eap + .iter() + .map(|x| EAPMethod::from_str(x)) + .collect::, InvalidEAPMethod>>() + .ok()?; + ieee_8021x_config.eap = eap; + } + + if let Some(identity) = ieee_8021x.get("identity") { + ieee_8021x_config.identity = Some(identity.downcast_ref::()?.to_string()); + } + if let Some(password) = ieee_8021x.get("password") { + ieee_8021x_config.password = Some(password.downcast_ref::()?.to_string()); + } + if let Some(ca_cert) = ieee_8021x.get("ca-cert") { + let ca_cert: &zvariant::Array = ca_cert.downcast_ref()?; + let ca_cert: String = ca_cert + .get() + .iter() + .map(|u| *u.downcast_ref::().unwrap() as char) + .collect(); + ieee_8021x_config.ca_cert = strip_nm_file_path(ca_cert); + } + if let Some(ca_cert_password) = ieee_8021x.get("ca-cert-password") { + ieee_8021x_config.ca_cert_password = + Some(ca_cert_password.downcast_ref::()?.to_string()); + } + if let Some(client_cert) = ieee_8021x.get("client-cert") { + let client_cert: &zvariant::Array = client_cert.downcast_ref()?; + let client_cert: String = client_cert + .get() + .iter() + .map(|u| *u.downcast_ref::().unwrap() as char) + .collect(); + ieee_8021x_config.client_cert = strip_nm_file_path(client_cert); + } + if let Some(client_cert_password) = ieee_8021x.get("client-cert-password") { + ieee_8021x_config.client_cert_password = + Some(client_cert_password.downcast_ref::()?.to_string()); + } + if let Some(private_key) = ieee_8021x.get("private-key") { + let private_key: &zvariant::Array = private_key.downcast_ref()?; + let private_key: String = private_key + .get() + .iter() + .map(|u| *u.downcast_ref::().unwrap() as char) + .collect(); + ieee_8021x_config.private_key = strip_nm_file_path(private_key); + } + if let Some(private_key_password) = ieee_8021x.get("private-key-password") { + ieee_8021x_config.private_key_password = + Some(private_key_password.downcast_ref::()?.to_string()); + } + if let Some(anonymous_identity) = ieee_8021x.get("anonymous-identity") { + ieee_8021x_config.anonymous_identity = + Some(anonymous_identity.downcast_ref::()?.to_string()); + } + + Some(ieee_8021x_config) +} + +// Strips NetworkManager path from "file://{path}\0" so only path remains. +fn strip_nm_file_path(path: String) -> Option { + let stripped_path = path + .strip_prefix("file://") + .and_then(|x| x.strip_suffix("\0"))?; + Some(stripped_path.to_string()) +} + /// Determines whether a value is empty. /// /// TODO: Generalize for other kind of values, like dicts or arrays. @@ -1002,7 +1143,7 @@ fn is_empty_value(value: &zvariant::Value) -> bool { mod test { use super::{ connection_from_dbus, connection_to_dbus, merge_dbus_connections, NestedHash, - OwnedNestedHash, + OwnedNestedHash, IEEE_8021X_KEY, }; use crate::network::{ model::*, @@ -1307,6 +1448,57 @@ mod test { ); } + #[test] + fn test_dbus_from_ieee_8021x() { + let ieee_8021x_config = IEEE8021XConfig { + eap: vec![ + EAPMethod::from_str("tls").unwrap(), + EAPMethod::from_str("peap").unwrap(), + ], + identity: Some("test_user".to_string()), + password: Some("test_pw".to_string()), + ca_cert: Some("/path/to/ca_cert.pem".to_string()), + ca_cert_password: Some("ca_cert_pw".to_string()), + client_cert: Some("/client_cert".to_string()), + client_cert_password: Some("client_cert_pw".to_string()), + private_key: Some("relative_path/private_key".to_string()), + private_key_password: Some("private_key_pw".to_string()), + anonymous_identity: Some("anon_identity".to_string()), + }; + let mut conn = build_base_connection(); + conn.ieee_8021x_config = Some(ieee_8021x_config); + let conn_dbus = connection_to_dbus(&conn, None); + + let config = conn_dbus.get(IEEE_8021X_KEY).unwrap(); + let eap: &Array = config.get("eap").unwrap().downcast_ref().unwrap(); + let eap: Vec<&str> = eap + .iter() + .map(|x| x.downcast_ref::().unwrap()) + .collect(); + assert_eq!(eap, ["tls".to_string(), "peap".to_string()]); + let identity: &str = config.get("identity").unwrap().downcast_ref().unwrap(); + assert_eq!(identity, "test_user"); + let password: &str = config.get("password").unwrap().downcast_ref().unwrap(); + assert_eq!(password, "test_pw"); + let ca_cert: &Array = config.get("ca-cert").unwrap().downcast_ref().unwrap(); + let ca_cert: String = ca_cert.iter().map(|x| *x.downcast_ref::().unwrap() as char).collect(); + assert_eq!(ca_cert, "file:///path/to/ca_cert.pem\0"); + let ca_cert_password: &str = config.get("ca-cert-password").unwrap().downcast_ref().unwrap(); + assert_eq!(ca_cert_password, "ca_cert_pw"); + let client_cert: &Array = config.get("client-cert").unwrap().downcast_ref().unwrap(); + let client_cert: String = client_cert.iter().map(|x| *x.downcast_ref::().unwrap() as char).collect(); + assert_eq!(client_cert, "file:///client_cert\0"); + let client_cert_password: &str = config.get("client-cert-password").unwrap().downcast_ref().unwrap(); + assert_eq!(client_cert_password, "client_cert_pw"); + let private_key: &Array = config.get("private-key").unwrap().downcast_ref().unwrap(); + let private_key: String = private_key.iter().map(|x| *x.downcast_ref::().unwrap() as char).collect(); + assert_eq!(private_key, "file://relative_path/private_key\0"); + let private_key_password: &str = config.get("private-key-password").unwrap().downcast_ref().unwrap(); + assert_eq!(private_key_password, "private_key_pw"); + let anonymous_identity: &str = config.get("anonymous-identity").unwrap().downcast_ref().unwrap(); + assert_eq!(anonymous_identity, "anon_identity"); + } + #[test] fn test_dbus_from_wireless_connection() { let config = WirelessConfig { diff --git a/rust/migrate-wicked/src/wireless.rs b/rust/migrate-wicked/src/wireless.rs index 64b07917ed..bd24f43b7f 100644 --- a/rust/migrate-wicked/src/wireless.rs +++ b/rust/migrate-wicked/src/wireless.rs @@ -38,6 +38,8 @@ pub struct Network { #[serde(rename = "access-point")] pub access_point: Option, pub wep: Option, + #[serde(rename = "wpa-eap")] + pub wpa_eap: Option, } #[derive(Default, Debug, PartialEq, SerializeDisplay, DeserializeFromStr, EnumString, Display)] @@ -73,6 +75,107 @@ pub struct Wep { pub key: Vec, } +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct WpaEap { + pub method: EapMethod, + #[serde(rename = "auth-proto")] + pub auth_proto: EapAuthProto, + #[serde(rename = "pairwise-cipher")] + pub pairwise_cipher: EapPairwiseCipher, + #[serde(rename = "group-cipher")] + pub group_cipher: EapGroupCipher, + pub identity: String, + pub tls: Option, +} + +#[derive(Default, Debug, PartialEq, SerializeDisplay, DeserializeFromStr, EnumString, Display)] +#[strum(serialize_all = "kebab-case")] +pub enum EapMethod { + #[default] + TLS, + PEAP, + TTLS, +} + +#[derive(Default, Debug, PartialEq, SerializeDisplay, DeserializeFromStr, EnumString, Display)] +// TODO i don't think this is correct +// but tbh this is probably overkill anyway +#[strum(serialize_all = "kebab-case")] +pub enum EapAuthProto { + #[default] + WPA, + NONE, + MD5, + TLS, + PAP, + CHAP, + MSCHAP, + MSCHAPV2, + PEAP, + TTLS, + GTC, + OTP, + LEAP, + PSK, + PAX, + SAKE, + GPSK, + WSC, + IKEV2, + TNC, + FAST, + AKA, + AkaPrime, + SIM, +} + +// TODO will have to look into wicked code into what options the "inner" and "outer" get translated +impl TryFrom for model::EAPMethod { + type Error = anyhow::Error; + + fn try_from(value: EapAuthProto) -> Result { + match value { + EapAuthProto::LEAP => Ok(model::EAPMethod::LEAP), + EapAuthProto::MD5 => Ok(model::EAPMethod::MD5), + EapAuthProto::TLS => Ok(model::EAPMethod::TLS), + EapAuthProto::PEAP => Ok(model::EAPMethod::PEAP), + EapAuthProto::TTLS => Ok(model::EAPMethod::TTLS), + EapAuthProto::FAST => Ok(model::EAPMethod::FAST), + _ => Err(anyhow!("EAP auth-proto isn't supported by NetworkManager")), + } + } +} + +#[derive(Default, Debug, PartialEq, SerializeDisplay, DeserializeFromStr, EnumString, Display)] +#[strum(serialize_all = "UPPERCASE")] +pub enum EapPairwiseCipher { + #[default] + TKIP, + CCMP, +} + +#[derive(Default, Debug, PartialEq, SerializeDisplay, DeserializeFromStr, EnumString, Display)] +#[strum(serialize_all = "UPPERCASE")] +pub enum EapGroupCipher { + #[default] + TKIP, + CCMP, + WEP104, + WEP40, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct WickedTLS { + #[serde(rename = "ca-cert")] + pub ca_cert: String, + #[serde(rename = "client-cert")] + pub client_cert: String, + #[serde(rename = "client-key")] + pub client_key: String, + #[serde(rename = "client-key-passwd")] + pub client_key_passwd: String, +} + fn unwrap_wireless_networks<'de, D>(deserializer: D) -> Result>, D::Error> where D: Deserializer<'de>, @@ -202,6 +305,7 @@ mod tests { key_management: vec!["wpa-psk".to_string()], access_point: None, wep: None, + wpa_eap: None, }]), ap_scan: 0, }), @@ -252,6 +356,7 @@ mod tests { default_key: 1, key: vec!["01020304ff".to_string(), "s:hello".to_string()], }), + wpa_eap: None, }]), ap_scan: 0, }), diff --git a/rust/migrate-wicked/tests/wireless/wicked_xml/wireless.xml b/rust/migrate-wicked/tests/wireless/wicked_xml/wireless.xml index 0edebaf1ce..e4ffc60cdc 100644 --- a/rust/migrate-wicked/tests/wireless/wicked_xml/wireless.xml +++ b/rust/migrate-wicked/tests/wireless/wicked_xml/wireless.xml @@ -75,3 +75,42 @@ false + + wlan2 + + manual + + + + 1 + + + test + true + ap + 12:34:56:78:9a:bc + wpa-eap + + tls + wpa + TKIP + TKIP + test + + /etc/sysconfig/network/./ca_cert + /etc/sysconfig/network/./client_cert + /etc/sysconfig/network/./client_key + testclientpw + + + + + + + + false + + + false + +