From 7d64cab48ca187253fc5d09b778b6fa4fcba9580 Mon Sep 17 00:00:00 2001 From: Jorik Cronenberg Date: Mon, 27 Nov 2023 15:55:10 +0100 Subject: [PATCH] Add MAC assigning to base connection --- rust/Cargo.lock | 7 ++ rust/agama-dbus-server/Cargo.toml | 1 + .../src/network/dbus/interfaces.rs | 13 +++ rust/agama-dbus-server/src/network/model.rs | 67 ++++++++++++++++ rust/agama-dbus-server/src/network/nm/dbus.rs | 79 ++++++++++++++++++- rust/agama-dbus-server/tests/network.rs | 2 + rust/agama-lib/share/profile.schema.json | 4 + rust/agama-lib/src/network/client.rs | 12 +++ rust/agama-lib/src/network/proxies.rs | 4 + rust/agama-lib/src/network/settings.rs | 2 + 10 files changed, 188 insertions(+), 3 deletions(-) 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..9e101dbae9 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() + } + + #[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..513efcffac 100644 --- a/rust/agama-dbus-server/src/network/model.rs +++ b/rust/agama-dbus-server/src/network/model.rs @@ -306,12 +306,26 @@ 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: &str) -> Result<(), InvalidMacAddress> { + self.base_mut().mac_address = MacAddress::from_str(mac_address)?; + Ok(()) + } } #[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, @@ -324,6 +338,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] + None, +} + +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::None), + _ => 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::None => "".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 d601291e9f..0ae90bed0f 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.to_string()), + ), ]); let mut security: HashMap<&str, zvariant::Value> = @@ -291,6 +299,34 @@ 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 = + match MacAddress::from_str(mac_address.downcast_ref::()?) { + Ok(mac) => mac, + Err(e) => { + log::warn!("Couldn't parse MAC: {}", e); + MacAddress::None + } + }; + } else { + base_connection.mac_address = MacAddress::None; + } + } 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 = + match MacAddress::from_str(mac_address.downcast_ref::()?) { + Ok(mac) => mac, + Err(e) => { + log::warn!("Couldn't parse MAC: {}", e); + MacAddress::None + } + }; + } else { + base_connection.mac_address = MacAddress::None; + } + } + base_connection.ip_config = ip_config_from_dbus(&conn)?; Some(base_connection) @@ -585,6 +621,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 +678,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 +694,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])); @@ -679,6 +722,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 +853,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(""); + assert!(updated.set_mac_address("").is_ok()); 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 +903,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() } } @@ -859,6 +924,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 {