diff --git a/rust/agama-dbus-server/src/network/dbus/interfaces.rs b/rust/agama-dbus-server/src/network/dbus/interfaces.rs index 682dbbfe10..9b0d9763e4 100644 --- a/rust/agama-dbus-server/src/network/dbus/interfaces.rs +++ b/rust/agama-dbus-server/src/network/dbus/interfaces.rs @@ -238,6 +238,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().to_string() + } + + #[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(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 88a5416180..11fe49601c 100644 --- a/rust/agama-dbus-server/src/network/model.rs +++ b/rust/agama-dbus-server/src/network/model.rs @@ -306,12 +306,25 @@ impl Connection { pub fn is_loopback(&self) -> bool { matches!(self, Connection::Loopback(_)) } + + pub fn mac_address(&self) -> &str { + &self.base().mac_address + } + + pub fn set_mac_address(&mut self, mac_address: &str) { + self.base_mut().mac_address = mac_address.to_string(); + } + + pub fn is_ethernet(&self) -> bool { + matches!(self, Connection::Loopback(_)) || matches!(self, Connection::Ethernet(_)) + } } #[derive(Debug, Default, Clone)] pub struct BaseConnection { pub id: String, pub uuid: Uuid, + pub mac_address: String, pub ip_config: IpConfig, pub status: Status, pub interface: String, diff --git a/rust/agama-dbus-server/src/network/nm/dbus.rs b/rust/agama-dbus-server/src/network/nm/dbus.rs index d601291e9f..3785332430 100644 --- a/rust/agama-dbus-server/src/network/nm/dbus.rs +++ b/rust/agama-dbus-server/src/network/nm/dbus.rs @@ -32,8 +32,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); @@ -218,6 +222,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.clone()), + ), ]); let mut security: HashMap<&str, zvariant::Value> = @@ -291,6 +299,20 @@ 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) { + if let Some(mac_address) = ethernet_config.get("assigned-mac-address") { + base_connection.mac_address = mac_address.downcast_ref::()?.to_string(); + } else { + base_connection.mac_address = "".to_string(); + } + } else if let Some(wireless_config) = conn.get(WIRELESS_KEY) { + if let Some(mac_address) = wireless_config.get("assigned-mac-address") { + base_connection.mac_address = mac_address.downcast_ref::()?.to_string(); + } else { + base_connection.mac_address = "".to_string(); + } + } + base_connection.ip_config = ip_config_from_dbus(&conn)?; Some(base_connection) @@ -585,6 +607,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![ @@ -640,6 +664,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 = @@ -652,6 +680,10 @@ mod test { ]); let connection = connection_from_dbus(dbus_conn).unwrap(); + assert_eq!( + connection.base().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])); @@ -679,6 +711,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 @@ -804,19 +842,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(""); 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 { @@ -840,9 +892,11 @@ mod test { }]), ..Default::default() }; + let mac_address = "FD:CB:A9:87:65:43".to_string(); BaseConnection { id: "agama".to_string(), ip_config, + mac_address, ..Default::default() } } @@ -859,6 +913,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..aecd0b481a 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,13 @@ impl<'a> NetworkClient<'a> { let interface = conn.interface.as_deref().unwrap_or(""); proxy.set_interface(interface).await?; + let mac_address = if let Some(mac_address) = &conn.mac_address { + mac_address + } else { + "" + }; + 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 {