diff --git a/rust/migrate-wicked/Cargo.lock b/rust/migrate-wicked/Cargo.lock index 2de197b277..5a2da36f8a 100644 --- a/rust/migrate-wicked/Cargo.lock +++ b/rust/migrate-wicked/Cargo.lock @@ -1267,6 +1267,7 @@ dependencies = [ "cidr", "clap", "log", + "macaddr", "quick-xml", "regex", "serde", diff --git a/rust/migrate-wicked/Cargo.toml b/rust/migrate-wicked/Cargo.toml index 3c383b65ca..ca04ac59bd 100644 --- a/rust/migrate-wicked/Cargo.toml +++ b/rust/migrate-wicked/Cargo.toml @@ -25,6 +25,7 @@ tokio = { version = "1.33.0", features = ["macros", "rt-multi-thread"] } serde_ignored = "0.1.9" uuid = { version = "1.3.4", features = ["v4"] } async-trait = "0.1.77" +macaddr = "1.0" [[bin]] name = "migrate-wicked" diff --git a/rust/migrate-wicked/src/bond.rs b/rust/migrate-wicked/src/bond.rs index 2e4089bc71..8f2931fd89 100644 --- a/rust/migrate-wicked/src/bond.rs +++ b/rust/migrate-wicked/src/bond.rs @@ -376,14 +376,15 @@ mod tests { ..Default::default() }; - let connection: model::Connection = bond_interface.to_connection().unwrap().connection; + let connection: &model::Connection = + &bond_interface.to_connection().unwrap().connections[0]; assert!(matches!( connection.config, model::ConnectionConfig::Bond(_) )); assert_eq!(connection.mac_address.to_string(), "02:11:22:33:44:55"); - if let model::ConnectionConfig::Bond(bond) = connection.config { + if let model::ConnectionConfig::Bond(bond) = &connection.config { assert_eq!(bond.mode, AgamaBondMode::LACP); let s = HashMap::from([ ("xmit_hash_policy", String::from("encap34")), diff --git a/rust/migrate-wicked/src/interface.rs b/rust/migrate-wicked/src/interface.rs index 6fdf865289..3c101278ac 100644 --- a/rust/migrate-wicked/src/interface.rs +++ b/rust/migrate-wicked/src/interface.rs @@ -1,6 +1,7 @@ use crate::bond::Bond; use crate::bridge::Bridge; use crate::vlan::Vlan; +use crate::wireless::Wireless; use crate::MIGRATION_SETTINGS; use agama_dbus_server::network::model::{ self, IpConfig, IpRoute, Ipv4Method, Ipv6Method, MacAddress, @@ -29,6 +30,8 @@ pub struct Interface { pub dummy: Option, pub ethernet: Option, pub bond: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub wireless: Option, #[serde(rename = "@origin")] pub origin: String, pub vlan: Option, @@ -118,7 +121,7 @@ pub struct Nexthop { } pub struct ConnectionResult { - pub connection: model::Connection, + pub connections: Vec, pub warnings: Vec, } @@ -129,6 +132,7 @@ pub struct IpConfigResult { impl Interface { pub fn to_connection(&self) -> Result { + let settings = MIGRATION_SETTINGS.get().unwrap(); let ip_config = self.to_ip_config()?; let mut connection = model::Connection { id: self.name.clone(), @@ -137,32 +141,52 @@ impl Interface { status: model::Status::Down, ..Default::default() }; + let mut connections: Vec = vec![]; - if let Some(settings) = MIGRATION_SETTINGS.get() { - if settings.activate_connections { - connection.status = model::Status::Up; - } + if settings.activate_connections { + connection.status = model::Status::Up; } if let Some(ethernet) = &self.ethernet { connection.mac_address = MacAddress::try_from(ðernet.address)?; - connection.config = model::ConnectionConfig::Ethernet + connection.config = model::ConnectionConfig::Ethernet; + connections.push(connection); } else if let Some(dummy) = &self.dummy { connection.mac_address = MacAddress::try_from(&dummy.address)?; - connection.config = model::ConnectionConfig::Dummy + connection.config = model::ConnectionConfig::Dummy; + connections.push(connection); } else if let Some(bond) = &self.bond { connection.mac_address = MacAddress::try_from(&bond.address)?; - connection.config = bond.into() + connection.config = bond.into(); + connections.push(connection); } else if let Some(vlan) = &self.vlan { connection.mac_address = MacAddress::try_from(&vlan.address)?; - connection.config = vlan.into() + connection.config = vlan.into(); + connections.push(connection); } else if let Some(bridge) = &self.bridge { connection.mac_address = MacAddress::try_from(&bridge.address)?; connection.config = bridge.into(); + connections.push(connection); + } else if let Some(wireless) = &self.wireless { + if let Some(networks) = &wireless.networks { + if networks.len() > 1 { + log::info!("{} has multiple networks defined, these will be split into different connections in NM", connection.id); + } + for (i, network) in networks.iter().enumerate() { + let mut wireless_connection = connection.clone(); + if networks.len() > 1 { + wireless_connection.id.push_str(&format!("-{}", i)); + } + wireless_connection.config = network.try_into()?; + connections.push(wireless_connection); + } + } + } else { + connections.push(connection); } Ok(ConnectionResult { - connection, + connections, warnings: ip_config.warnings, }) } @@ -337,7 +361,7 @@ mod tests { }; let static_connection: model::Connection = - static_interface.to_connection().unwrap().connection; + static_interface.to_connection().unwrap().connections[0].to_owned(); assert_eq!(static_connection.ip_config.method4, Ipv4Method::Manual); assert_eq!( static_connection.ip_config.addresses[0].to_string(), @@ -393,7 +417,7 @@ mod tests { }; let static_connection: model::Connection = - static_interface.to_connection().unwrap().connection; + static_interface.to_connection().unwrap().connections[0].to_owned(); assert_eq!(static_connection.ip_config.method4, Ipv4Method::Auto); assert_eq!(static_connection.ip_config.method6, Ipv6Method::Auto); assert_eq!(static_connection.ip_config.addresses.len(), 0); @@ -409,7 +433,8 @@ mod tests { ..Default::default() }; - let connection: model::Connection = dummy_interface.to_connection().unwrap().connection; + let connection: &model::Connection = + &dummy_interface.to_connection().unwrap().connections[0]; assert!(matches!(connection.config, model::ConnectionConfig::Dummy)); assert_eq!(connection.mac_address.to_string(), "12:34:56:78:9A:BC"); @@ -420,7 +445,8 @@ mod tests { ..Default::default() }; - let connection: model::Connection = dummy_interface.to_connection().unwrap().connection; + let connection: &model::Connection = + &dummy_interface.to_connection().unwrap().connections[0]; assert!(matches!(connection.config, model::ConnectionConfig::Dummy)); assert_eq!(dummy_interface.dummy.unwrap().address, None); assert!(matches!(connection.mac_address, MacAddress::Unset)); diff --git a/rust/migrate-wicked/src/main.rs b/rust/migrate-wicked/src/main.rs index 94974ee928..ce74c039ed 100644 --- a/rust/migrate-wicked/src/main.rs +++ b/rust/migrate-wicked/src/main.rs @@ -4,6 +4,7 @@ mod interface; mod migrate; mod reader; mod vlan; +mod wireless; use clap::builder::TypedValueParser; use clap::{Args, Parser, Subcommand}; diff --git a/rust/migrate-wicked/src/migrate.rs b/rust/migrate-wicked/src/migrate.rs index bc636e235f..8659f99c8c 100644 --- a/rust/migrate-wicked/src/migrate.rs +++ b/rust/migrate-wicked/src/migrate.rs @@ -96,21 +96,23 @@ impl Adapter for WickedAdapter { if !settings.continue_migration { return Err(anyhow::anyhow!( "Migration of {} failed", - connection_result.connection.id + connection_result.connections[0].id ) .into()); } } - if let Some(parent) = interface.link.master { - parents.insert(connection_result.connection.id.clone(), parent.clone()); - } - if let Some(bridge) = interface.bridge { - for port in bridge.ports { - bridge_ports.insert(port.device.clone(), port.clone()); + for connection in connection_result.connections { + if let Some(parent) = &interface.link.master { + parents.insert(connection.id.clone(), parent.clone()); + } + state.add_connection(connection)?; + if let Some(bridge) = &interface.bridge { + for port in &bridge.ports { + bridge_ports.insert(port.device.clone(), port.clone()); + } } } - state.add_connection(connection_result.connection)?; } update_parent_connection(&mut state, parents)?; diff --git a/rust/migrate-wicked/src/vlan.rs b/rust/migrate-wicked/src/vlan.rs index 6b458bd47c..0827f40ceb 100644 --- a/rust/migrate-wicked/src/vlan.rs +++ b/rust/migrate-wicked/src/vlan.rs @@ -84,9 +84,9 @@ mod tests { let ifc = vlan_interface.to_connection(); assert!(ifc.is_ok()); - let ifc = ifc.unwrap().connection; + let ifc = &ifc.unwrap().connections[0]; assert!(matches!(ifc.config, model::ConnectionConfig::Vlan(_))); - if let model::ConnectionConfig::Vlan(v) = ifc.config { + if let model::ConnectionConfig::Vlan(v) = &ifc.config { assert_eq!(v.id, 10); assert_eq!(v.protocol, model::VlanProtocol::IEEE802_1ad); assert_eq!(v.parent, "en0"); diff --git a/rust/migrate-wicked/src/wireless.rs b/rust/migrate-wicked/src/wireless.rs new file mode 100644 index 0000000000..0b0ac80699 --- /dev/null +++ b/rust/migrate-wicked/src/wireless.rs @@ -0,0 +1,286 @@ +use crate::MIGRATION_SETTINGS; +use agama_dbus_server::network::model::{self, WEPAuthAlg, WEPKeyType, WEPSecurity}; +use agama_lib::network::types::SSID; +use anyhow::anyhow; +use macaddr::MacAddr6; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_with::formats::CommaSeparator; +use serde_with::StringWithSeparator; +use serde_with::{serde_as, skip_serializing_none, DeserializeFromStr, SerializeDisplay}; +use std::str::FromStr; +use strum_macros::{Display, EnumString}; + +#[skip_serializing_none] +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct Wireless { + #[serde(rename = "ap-scan")] + pub ap_scan: u32, + #[serde(default)] + #[serde(deserialize_with = "unwrap_wireless_networks")] + pub networks: Option>, +} + +#[serde_as] +#[skip_serializing_none] +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct Network { + pub essid: String, + #[serde(rename = "scan-ssid")] + pub scan_ssid: bool, + pub mode: WickedWirelessMode, + #[serde(rename = "wpa-psk")] + pub wpa_psk: Option, + #[serde(default)] + #[serde(rename = "key-management")] + #[serde_as(as = "StringWithSeparator::")] + pub key_management: Vec, + pub channel: Option, + #[serde(rename = "access-point")] + pub access_point: Option, + pub wep: Option, +} + +#[derive(Default, Debug, PartialEq, SerializeDisplay, DeserializeFromStr, EnumString, Display)] +#[strum(serialize_all = "kebab-case")] +pub enum WickedWirelessMode { + AdHoc = 0, + #[default] + Infrastructure = 1, + AP = 2, +} + +impl From<&WickedWirelessMode> for model::WirelessMode { + fn from(value: &WickedWirelessMode) -> Self { + match value { + WickedWirelessMode::AdHoc => model::WirelessMode::AdHoc, + WickedWirelessMode::Infrastructure => model::WirelessMode::Infra, + WickedWirelessMode::AP => model::WirelessMode::AP, + } + } +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct WpaPsk { + pub passphrase: String, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct Wep { + #[serde(rename = "auth-algo")] + pub auth_algo: String, + #[serde(rename = "default-key")] + pub default_key: u32, + pub key: Vec, +} + +fn unwrap_wireless_networks<'de, D>(deserializer: D) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + #[derive(Debug, PartialEq, Default, Serialize, Deserialize)] + struct Networks { + network: Vec, + } + Ok(Some(Networks::deserialize(deserializer)?.network)) +} + +fn wireless_security_protocol( + wicked_value: &[String], +) -> Result { + if wicked_value.contains(&"wpa-psk".to_string()) + || wicked_value.contains(&"wpa-psk-sha256".to_string()) + { + Ok(model::SecurityProtocol::WPA2) + } else if wicked_value.contains(&"sae".to_string()) { + Ok(model::SecurityProtocol::WPA3Personal) + } else if wicked_value.contains(&"wpa-eap".to_string()) + || wicked_value.contains(&"wpa-eap-sha256".to_string()) + { + Ok(model::SecurityProtocol::WPA2Enterprise) + } else if wicked_value.contains(&"owe".to_string()) { + Ok(model::SecurityProtocol::OWE) + } else if wicked_value.contains(&"wpa-eap-suite-b-192".to_string()) { + Ok(model::SecurityProtocol::WPA3Only) + } else if wicked_value.contains(&"none".to_string()) { + Ok(model::SecurityProtocol::WEP) + } else { + Err(anyhow!("Unrecognized key-management protocol")) + } +} + +impl TryFrom<&Network> for model::ConnectionConfig { + type Error = anyhow::Error; + fn try_from(network: &Network) -> Result { + let settings = MIGRATION_SETTINGS.get().unwrap(); + let mut config = model::WirelessConfig { + ssid: SSID(network.essid.as_bytes().to_vec()), + hidden: network.scan_ssid, + ..Default::default() + }; + + if network.key_management.len() > 1 && settings.continue_migration { + log::warn!("Migration of multiple key-management algorithms isn't supported") + } else if network.key_management.len() > 1 { + return Err(anyhow!( + "Migration of multiple key-management algorithms isn't supported" + )); + } + config.security = wireless_security_protocol(&network.key_management)?; + + if let Some(wpa_psk) = &network.wpa_psk { + config.password = Some(wpa_psk.passphrase.clone()) + } + if let Some(channel) = network.channel { + config.channel = Some(channel); + if channel <= 14 { + config.band = Some("bg".try_into().unwrap()); + } else { + config.band = Some("a".try_into().unwrap()); + } + log::warn!( + "NetworkManager requires setting a band for wireless when a channel is set. The band has been set to \"{}\". This may in certain regions be incorrect.", + config.band.unwrap() + ); + } + if let Some(access_point) = &network.access_point { + config.bssid = Some(MacAddr6::from_str(access_point)?); + } + + if let Some(wep) = &network.wep { + // filter out `s:`, `h:`, `:`, and `-` of wep keys + let keys: Vec = wep + .key + .clone() + .into_iter() + .map(|mut x| { + x = x.replace("s:", ""); + x = x.replace("h:", ""); + x = x.replace(':', ""); + x.replace('-', "") + }) + .collect(); + let wep_security = WEPSecurity { + auth_alg: WEPAuthAlg::try_from(wep.auth_algo.as_str())?, + wep_key_type: WEPKeyType::Key, + keys, + wep_key_index: wep.default_key, + }; + config.wep_security = Some(wep_security); + } + + config.mode = (&network.mode).into(); + Ok(model::ConnectionConfig::Wireless(config)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::interface::*; + use crate::MIGRATION_SETTINGS; + + #[allow(dead_code)] + fn setup_default_migration_settings() { + let _ = MIGRATION_SETTINGS.set(crate::MigrationSettings { + continue_migration: false, + dry_run: false, + activate_connections: true, + }); + } + + #[test] + fn test_wireless_bands() { + setup_default_migration_settings(); + let mut wireless_interface = Interface { + wireless: Some(Wireless { + networks: Some(vec![Network { + channel: Some(0), + essid: "testssid".to_string(), + scan_ssid: false, + mode: WickedWirelessMode::AP, + wpa_psk: None, + key_management: vec!["wpa-psk".to_string()], + access_point: None, + wep: None, + }]), + ap_scan: 0, + }), + ..Default::default() + }; + let connections = wireless_interface.to_connection(); + assert!(connections.is_ok()); + let connection = &connections.unwrap().connections[0]; + let model::ConnectionConfig::Wireless(wireless) = &connection.config else { + panic!() + }; + assert_eq!(wireless.band, Some("bg".try_into().unwrap())); + + wireless_interface + .wireless + .as_mut() + .unwrap() + .networks + .as_mut() + .unwrap()[0] + .channel = Some(32); + let ifc = wireless_interface.to_connection(); + assert!(ifc.is_ok()); + let ifc = &ifc.unwrap().connections[0]; + let model::ConnectionConfig::Wireless(wireless) = &ifc.config else { + panic!() + }; + assert_eq!(wireless.band, Some("a".try_into().unwrap())); + } + + #[test] + fn test_wireless_migration() { + setup_default_migration_settings(); + let wireless_interface = Interface { + wireless: Some(Wireless { + networks: Some(vec![Network { + essid: "testssid".to_string(), + scan_ssid: true, + mode: WickedWirelessMode::Infrastructure, + wpa_psk: Some(WpaPsk { + passphrase: "testpassword".to_string(), + }), + key_management: vec!["wpa-psk".to_string()], + channel: Some(14), + access_point: Some("12:34:56:78:9A:BC".to_string()), + wep: Some(Wep { + auth_algo: "open".to_string(), + default_key: 1, + key: vec!["01020304ff".to_string(), "s:hello".to_string()], + }), + }]), + ap_scan: 0, + }), + ..Default::default() + }; + let connections = wireless_interface.to_connection(); + assert!(connections.is_ok()); + let connection = &connections.unwrap().connections[0]; + let model::ConnectionConfig::Wireless(wireless) = &connection.config else { + panic!() + }; + assert_eq!(wireless.ssid, SSID("testssid".as_bytes().to_vec())); + assert!(wireless.hidden); + assert_eq!(wireless.mode, model::WirelessMode::Infra); + assert_eq!(wireless.password, Some("testpassword".to_string())); + assert_eq!(wireless.security, model::SecurityProtocol::WPA2); + assert_eq!( + wireless.bssid, + Some(MacAddr6::from_str("12:34:56:78:9A:BC").unwrap()) + ); + assert_eq!( + wireless.wep_security, + Some(WEPSecurity { + auth_alg: WEPAuthAlg::Open, + wep_key_type: WEPKeyType::Key, + keys: vec!["01020304ff".to_string(), "hello".to_string()], + wep_key_index: 1, + }) + ); + assert_eq!(wireless.band, Some("bg".try_into().unwrap())); + } +} diff --git a/rust/migrate-wicked/tests/wireless/system-connections/wlan0-0.nmconnection b/rust/migrate-wicked/tests/wireless/system-connections/wlan0-0.nmconnection new file mode 100644 index 0000000000..26b6518325 --- /dev/null +++ b/rust/migrate-wicked/tests/wireless/system-connections/wlan0-0.nmconnection @@ -0,0 +1,28 @@ +[connection] +id=wlan0-0 +uuid=1089cc84-f99f-4bb8-8a0d-fafde6544e85 +type=wifi +interface-name=wlan0 + +[wifi] +band=a +bssid=12:34:56:78:9A:BC +channel=100 +hidden=true +mode=adhoc +ssid=example_ssid + +[wifi-security] +key-mgmt=wpa-psk +psk=example_passwd + +[match] + +[ipv4] +method=disabled + +[ipv6] +addr-gen-mode=default +method=disabled + +[proxy] diff --git a/rust/migrate-wicked/tests/wireless/system-connections/wlan0-1.nmconnection b/rust/migrate-wicked/tests/wireless/system-connections/wlan0-1.nmconnection new file mode 100644 index 0000000000..97c7cbbb54 --- /dev/null +++ b/rust/migrate-wicked/tests/wireless/system-connections/wlan0-1.nmconnection @@ -0,0 +1,25 @@ +[connection] +id=wlan0-1 +uuid=b0c56f00-cf58-4c6c-b6dd-edf97a612063 +type=wifi +interface-name=wlan0 + +[wifi] +hidden=true +mode=adhoc +ssid=example_ssid2 + +[wifi-security] +key-mgmt=wpa-psk +psk=example_passwd2 + +[match] + +[ipv4] +method=disabled + +[ipv6] +addr-gen-mode=default +method=disabled + +[proxy] diff --git a/rust/migrate-wicked/tests/wireless/system-connections/wlan1.nmconnection b/rust/migrate-wicked/tests/wireless/system-connections/wlan1.nmconnection new file mode 100644 index 0000000000..a42db83883 --- /dev/null +++ b/rust/migrate-wicked/tests/wireless/system-connections/wlan1.nmconnection @@ -0,0 +1,29 @@ +[connection] +id=wlan1 +uuid=3bc52d5e-4095-4e1a-9976-d46f766bb627 +type=wifi +interface-name=wlan1 + +[wifi] +bssid=12:34:56:78:9A:BC +mode=infrastructure +ssid=test + +[wifi-security] +auth-alg=shared +key-mgmt=none +wep-key-type=1 +wep-key0=hello +wep-key1=5b73215e232f4c577c5073455d +wep-tx-keyidx=1 + +[match] + +[ipv4] +method=disabled + +[ipv6] +addr-gen-mode=default +method=disabled + +[proxy] diff --git a/rust/migrate-wicked/tests/wireless/wicked_xml/wireless.xml b/rust/migrate-wicked/tests/wireless/wicked_xml/wireless.xml new file mode 100644 index 0000000000..0edebaf1ce --- /dev/null +++ b/rust/migrate-wicked/tests/wireless/wicked_xml/wireless.xml @@ -0,0 +1,77 @@ + + wlan0 + + manual + + + + 1 + + + example_ssid + 100 + 12:34:56:78:9A:BC + true + ad-hoc + wpa-psk,wpa-psk-sha256,sae + + example_passwd + CCMP + CCMP + optional + + + + example_ssid2 + true + ad-hoc + wpa-psk,wpa-psk-sha256,sae + + example_passwd2 + CCMP + CCMP + optional + + + + + + + false + + + false + + + + wlan1 + + manual + + + + 1 + + + test + false + infrastructure + 12:34:56:78:9a:bc + none + + shared + 1 + s:hello + 5b73215e232f4c577c5073455d + + + + + + + false + + + false + +