diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 4c34f4a9b7..cb533f2e9b 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -50,6 +50,7 @@ dependencies = [ "cidr", "gettext-rs", "log", + "macaddr", "once_cell", "regex", "serde", @@ -1143,6 +1144,12 @@ dependencies = [ "value-bag", ] +[[package]] +name = "macaddr" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baee0bbc17ce759db233beb01648088061bf678383130602a298e6998eedb2d8" + [[package]] name = "malloc_buf" version = "0.0.6" diff --git a/rust/agama-dbus-server/Cargo.toml b/rust/agama-dbus-server/Cargo.toml index 368096cdfa..e6e95c67a0 100644 --- a/rust/agama-dbus-server/Cargo.toml +++ b/rust/agama-dbus-server/Cargo.toml @@ -24,3 +24,4 @@ tokio-stream = "0.1.14" gettext-rs = { version = "0.7.0", features = ["gettext-system"] } regex = "1.10.2" once_cell = "1.18.0" +macaddr = "1.0" diff --git a/rust/agama-dbus-server/src/network/dbus/interfaces.rs b/rust/agama-dbus-server/src/network/dbus/interfaces.rs index 682dbbfe10..23ebdbb784 100644 --- a/rust/agama-dbus-server/src/network/dbus/interfaces.rs +++ b/rust/agama-dbus-server/src/network/dbus/interfaces.rs @@ -6,11 +6,13 @@ use super::ObjectsRegistry; use crate::network::{ action::Action, error::NetworkStateError, - model::{Connection as NetworkConnection, Device as NetworkDevice, WirelessConnection}, + model::{ + Connection as NetworkConnection, Device as NetworkDevice, MacAddress, WirelessConnection, + }, }; use agama_lib::network::types::SSID; -use std::sync::Arc; +use std::{str::FromStr, sync::Arc}; use tokio::sync::mpsc::UnboundedSender; use tokio::sync::{MappedMutexGuard, Mutex, MutexGuard}; use zbus::{ @@ -238,6 +240,19 @@ impl Connection { connection.set_interface(name); self.update_connection(connection).await } + + /// Custom mac-address + #[dbus_interface(property)] + pub async fn mac_address(&self) -> String { + self.get_connection().await.mac_address() + } + + #[dbus_interface(property)] + pub async fn set_mac_address(&mut self, mac_address: &str) -> zbus::fdo::Result<()> { + let mut connection = self.get_connection().await; + connection.set_mac_address(MacAddress::from_str(mac_address)?); + self.update_connection(connection).await + } } /// D-Bus interface for Match settings diff --git a/rust/agama-dbus-server/src/network/model.rs b/rust/agama-dbus-server/src/network/model.rs index 178ddaa14b..b413284999 100644 --- a/rust/agama-dbus-server/src/network/model.rs +++ b/rust/agama-dbus-server/src/network/model.rs @@ -310,12 +310,25 @@ impl Connection { pub fn is_loopback(&self) -> bool { matches!(self, Connection::Loopback(_)) } + + pub fn is_ethernet(&self) -> bool { + matches!(self, Connection::Loopback(_)) || matches!(self, Connection::Ethernet(_)) + } + + pub fn mac_address(&self) -> String { + self.base().mac_address.to_string() + } + + pub fn set_mac_address(&mut self, mac_address: MacAddress) { + self.base_mut().mac_address = mac_address; + } } #[derive(Debug, Default, Clone)] pub struct BaseConnection { pub id: String, pub uuid: Uuid, + pub mac_address: MacAddress, pub ip_config: IpConfig, pub status: Status, pub interface: String, @@ -328,6 +341,59 @@ impl PartialEq for BaseConnection { } } +#[derive(Debug, Error)] +#[error("Invalid MAC address: {0}")] +pub struct InvalidMacAddress(String); + +#[derive(Debug, Default, Clone)] +pub enum MacAddress { + MacAddress(macaddr::MacAddr6), + Preserve, + Permanent, + Random, + Stable, + #[default] + Unset, +} + +impl FromStr for MacAddress { + type Err = InvalidMacAddress; + + fn from_str(s: &str) -> Result { + match s { + "preserve" => Ok(Self::Preserve), + "permanent" => Ok(Self::Permanent), + "random" => Ok(Self::Random), + "stable" => Ok(Self::Stable), + "" => Ok(Self::Unset), + _ => Ok(Self::MacAddress(match macaddr::MacAddr6::from_str(s) { + Ok(mac) => mac, + Err(e) => return Err(InvalidMacAddress(e.to_string())), + })), + } + } +} + +impl fmt::Display for MacAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let output = match &self { + Self::MacAddress(mac) => mac.to_string(), + Self::Preserve => "preserve".to_string(), + Self::Permanent => "permanent".to_string(), + Self::Random => "random".to_string(), + Self::Stable => "stable".to_string(), + Self::Unset => "".to_string(), + }; + write!(f, "{}", output) + } +} + +impl From for zbus::fdo::Error { + fn from(value: InvalidMacAddress) -> Self { + zbus::fdo::Error::Failed(value.to_string()) + } +} + #[derive(Debug, Default, Clone, Copy, PartialEq)] pub enum Status { #[default] diff --git a/rust/agama-dbus-server/src/network/nm/dbus.rs b/rust/agama-dbus-server/src/network/nm/dbus.rs index 7efeec357f..11cc97c19a 100644 --- a/rust/agama-dbus-server/src/network/nm/dbus.rs +++ b/rust/agama-dbus-server/src/network/nm/dbus.rs @@ -33,8 +33,12 @@ pub fn connection_to_dbus(conn: &Connection) -> NestedHash { result.insert("ipv6", ip_config_to_ipv6_dbus(conn.ip_config())); result.insert("match", match_config_to_dbus(conn.match_config())); - if let Connection::Wireless(wireless) = conn { - connection_dbus.insert("type", "802-11-wireless".into()); + if conn.is_ethernet() { + let ethernet_config = + HashMap::from([("assigned-mac-address", Value::new(conn.mac_address()))]); + result.insert(ETHERNET_KEY, ethernet_config); + } else if let Connection::Wireless(wireless) = conn { + connection_dbus.insert("type", WIRELESS_KEY.into()); let wireless_dbus = wireless_config_to_dbus(wireless); for (k, v) in wireless_dbus { result.insert(k, v); @@ -227,6 +231,10 @@ fn wireless_config_to_dbus(conn: &WirelessConnection) -> NestedHash { let wireless: HashMap<&str, zvariant::Value> = HashMap::from([ ("mode", Value::new(config.mode.to_string())), ("ssid", Value::new(config.ssid.to_vec())), + ( + "assigned-mac-address", + Value::new(conn.base.mac_address.to_string()), + ), ]); let mut security: HashMap<&str, zvariant::Value> = @@ -300,11 +308,31 @@ fn base_connection_from_dbus(conn: &OwnedNestedHash) -> Option { base_connection.match_config = match_config_from_dbus(match_config)?; } + if let Some(ethernet_config) = conn.get(ETHERNET_KEY) { + base_connection.mac_address = mac_address_from_dbus(ethernet_config)?; + } else if let Some(wireless_config) = conn.get(WIRELESS_KEY) { + base_connection.mac_address = mac_address_from_dbus(wireless_config)?; + } + base_connection.ip_config = ip_config_from_dbus(&conn)?; Some(base_connection) } +fn mac_address_from_dbus(config: &HashMap) -> Option { + if let Some(mac_address) = config.get("assigned-mac-address") { + match MacAddress::from_str(mac_address.downcast_ref::()?) { + Ok(mac) => Some(mac), + Err(e) => { + log::warn!("Couldn't parse MAC: {}", e); + None + } + } + } else { + Some(MacAddress::Unset) + } +} + fn match_config_from_dbus( match_config: &HashMap, ) -> Option { @@ -594,6 +622,8 @@ mod test { let match_config = connection.match_config(); assert_eq!(match_config.kernel, vec!["pci-0000:00:19.0"]); + assert_eq!(connection.mac_address(), "12:34:56:78:9A:BC"); + assert_eq!( ip_config.addresses, vec![ @@ -649,6 +679,10 @@ mod test { "ssid".to_string(), Value::new("agama".as_bytes()).to_owned(), ), + ( + "assigned-mac-address".to_string(), + Value::new("13:45:67:89:AB:CD").to_owned(), + ), ]); let security_section = @@ -661,6 +695,7 @@ mod test { ]); let connection = connection_from_dbus(dbus_conn).unwrap(); + assert_eq!(connection.mac_address(), "13:45:67:89:AB:CD".to_string()); assert!(matches!(connection, Connection::Wireless(_))); if let Connection::Wireless(connection) = connection { assert_eq!(connection.wireless.ssid, SSID(vec![97, 103, 97, 109, 97])); @@ -688,6 +723,12 @@ mod test { let wireless = wireless_dbus.get("802-11-wireless").unwrap(); let mode: &str = wireless.get("mode").unwrap().downcast_ref().unwrap(); assert_eq!(mode, "infrastructure"); + let mac_address: &str = wireless + .get("assigned-mac-address") + .unwrap() + .downcast_ref() + .unwrap(); + assert_eq!(mac_address, "FD:CB:A9:87:65:43"); let ssid: &zvariant::Array = wireless.get("ssid").unwrap().downcast_ref().unwrap(); let ssid: Vec = ssid @@ -813,19 +854,33 @@ mod test { Value::new("eth0".to_string()).to_owned(), ), ]); + let ethernet = HashMap::from([( + "assigned-mac-address".to_string(), + Value::new("12:34:56:78:9A:BC".to_string()).to_owned(), + )]); original.insert("connection".to_string(), connection); + original.insert(ETHERNET_KEY.to_string(), ethernet); let mut updated = Connection::Ethernet(EthernetConnection::default()); updated.set_interface(""); + updated.set_mac_address(MacAddress::Unset); let updated = connection_to_dbus(&updated); let merged = merge_dbus_connections(&original, &updated); let connection = merged.get("connection").unwrap(); assert_eq!(connection.get("interface-name"), None); + let ethernet = merged.get(ETHERNET_KEY).unwrap(); + assert_eq!(ethernet.get("assigned-mac-address"), Some(&Value::from(""))); } fn build_ethernet_section_from_dbus() -> HashMap { - HashMap::from([("auto-negotiate".to_string(), true.into())]) + HashMap::from([ + ("auto-negotiate".to_string(), true.into()), + ( + "assigned-mac-address".to_string(), + Value::new("12:34:56:78:9A:BC").to_owned(), + ), + ]) } fn build_base_connection() -> BaseConnection { @@ -849,9 +904,11 @@ mod test { }]), ..Default::default() }; + let mac_address = MacAddress::from_str("FD:CB:A9:87:65:43").unwrap(); BaseConnection { id: "agama".to_string(), ip_config, + mac_address, ..Default::default() } } @@ -868,6 +925,14 @@ mod test { let id: &str = connection_dbus.get("id").unwrap().downcast_ref().unwrap(); assert_eq!(id, "agama"); + let ethernet_connection = conn_dbus.get(ETHERNET_KEY).unwrap(); + let mac_address: &str = ethernet_connection + .get("assigned-mac-address") + .unwrap() + .downcast_ref() + .unwrap(); + assert_eq!(mac_address, "FD:CB:A9:87:65:43"); + let ipv4_dbus = conn_dbus.get("ipv4").unwrap(); let gateway4: &str = ipv4_dbus.get("gateway").unwrap().downcast_ref().unwrap(); assert_eq!(gateway4, "192.168.0.1"); diff --git a/rust/agama-dbus-server/tests/network.rs b/rust/agama-dbus-server/tests/network.rs index 37d404210a..e4f8d4862e 100644 --- a/rust/agama-dbus-server/tests/network.rs +++ b/rust/agama-dbus-server/tests/network.rs @@ -62,6 +62,7 @@ async fn test_add_connection() -> Result<(), Box> { let addresses: Vec = vec!["192.168.0.2/24".parse()?, "::ffff:c0a8:7ac7/64".parse()?]; let wlan0 = settings::NetworkConnection { id: "wlan0".to_string(), + mac_address: Some("FD:CB:A9:87:65:43".to_string()), method4: Some("auto".to_string()), method6: Some("disabled".to_string()), addresses: addresses.clone(), @@ -80,6 +81,7 @@ async fn test_add_connection() -> Result<(), Box> { let conn = conns.first().unwrap(); assert_eq!(conn.id, "wlan0"); + assert_eq!(conn.mac_address, Some("FD:CB:A9:87:65:43".to_string())); assert_eq!(conn.device_type(), DeviceType::Wireless); assert_eq!(&conn.addresses, &addresses); let method4 = conn.method4.as_ref().unwrap(); diff --git a/rust/agama-lib/share/profile.schema.json b/rust/agama-lib/share/profile.schema.json index 2d9cb33b0a..97e2a29293 100644 --- a/rust/agama-lib/share/profile.schema.json +++ b/rust/agama-lib/share/profile.schema.json @@ -36,6 +36,10 @@ "description": "The name of the network interface bound to this connection", "type": "string" }, + "mac-address": { + "description": "Custom mac-address (can also be 'preserve', 'permanent', 'random' or 'stable')", + "type": "string" + }, "method4": { "description": "IPv4 configuration method (e.g., 'auto')", "type": "string", diff --git a/rust/agama-lib/src/network/client.rs b/rust/agama-lib/src/network/client.rs index 485baee516..d8e3077216 100644 --- a/rust/agama-lib/src/network/client.rs +++ b/rust/agama-lib/src/network/client.rs @@ -67,6 +67,10 @@ impl<'a> NetworkClient<'a> { "" => None, value => Some(value.to_string()), }; + let mac_address = match connection_proxy.mac_address().await?.as_str() { + "" => None, + value => Some(value.to_string()), + }; let ip_proxy = IPProxy::builder(&self.connection) .path(path)? @@ -91,6 +95,7 @@ impl<'a> NetworkClient<'a> { addresses, nameservers, interface, + mac_address, ..Default::default() }) } @@ -189,6 +194,9 @@ impl<'a> NetworkClient<'a> { let interface = conn.interface.as_deref().unwrap_or(""); proxy.set_interface(interface).await?; + let mac_address = conn.mac_address.as_deref().unwrap_or(""); + proxy.set_mac_address(mac_address).await?; + self.update_ip_settings(path, conn).await?; if let Some(ref wireless) = conn.wireless { diff --git a/rust/agama-lib/src/network/proxies.rs b/rust/agama-lib/src/network/proxies.rs index 2b43124722..314e0c5b23 100644 --- a/rust/agama-lib/src/network/proxies.rs +++ b/rust/agama-lib/src/network/proxies.rs @@ -83,6 +83,10 @@ trait Connection { fn interface(&self) -> zbus::Result; #[dbus_proxy(property)] fn set_interface(&self, interface: &str) -> zbus::Result<()>; + #[dbus_proxy(property)] + fn mac_address(&self) -> zbus::Result; + #[dbus_proxy(property)] + fn set_mac_address(&self, mac_address: &str) -> zbus::Result<()>; } #[dbus_proxy( diff --git a/rust/agama-lib/src/network/settings.rs b/rust/agama-lib/src/network/settings.rs index cd344ef323..34be2f4090 100644 --- a/rust/agama-lib/src/network/settings.rs +++ b/rust/agama-lib/src/network/settings.rs @@ -69,6 +69,8 @@ pub struct NetworkConnection { pub interface: Option, #[serde(skip_serializing_if = "Option::is_none")] pub match_settings: Option, + #[serde(rename = "mac-address", skip_serializing_if = "Option::is_none")] + pub mac_address: Option, } impl NetworkConnection { diff --git a/rust/package/agama-cli.changes b/rust/package/agama-cli.changes index 1124dca5ac..0a6e8d573f 100644 --- a/rust/package/agama-cli.changes +++ b/rust/package/agama-cli.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Tue Dec 5 11:18:41 UTC 2023 - Jorik Cronenberg + +- Add ability to assign a custom MAC address for network + connections (gh#openSUSE/agama#893) + ------------------------------------------------------------------- Tue Dec 5 09:46:48 UTC 2023 - José Iván López González