diff --git a/rust/Cargo.lock b/rust/Cargo.lock index bd2339e256..9e7f6d2134 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -38,6 +38,7 @@ dependencies = [ "agama-locale-data", "anyhow", "async-std", + "cidr", "futures", "log", "serde", @@ -66,6 +67,7 @@ dependencies = [ "agama-settings", "anyhow", "async-std", + "cidr", "curl", "futures", "futures-util", @@ -480,6 +482,15 @@ dependencies = [ "phf_codegen", ] +[[package]] +name = "cidr" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d18b093eba54c9aaa1e3784d4361eb2ba944cf7d0a932a830132238f483e8d8" +dependencies = [ + "serde", +] + [[package]] name = "clap" version = "4.3.0" diff --git a/rust/agama-dbus-server/Cargo.toml b/rust/agama-dbus-server/Cargo.toml index 97fca7575e..d1d0983ac9 100644 --- a/rust/agama-dbus-server/Cargo.toml +++ b/rust/agama-dbus-server/Cargo.toml @@ -20,3 +20,4 @@ thiserror = "1.0.40" serde = { version = "1.0.152", features = ["derive"] } serde_yaml = "0.9.24" futures = "0.3.28" +cidr = { version = "0.2.2", features = ["serde"] } diff --git a/rust/agama-dbus-server/src/network/dbus/interfaces.rs b/rust/agama-dbus-server/src/network/dbus/interfaces.rs index 4dbc4a9d54..f0194391ef 100644 --- a/rust/agama-dbus-server/src/network/dbus/interfaces.rs +++ b/rust/agama-dbus-server/src/network/dbus/interfaces.rs @@ -6,22 +6,21 @@ use super::ObjectsRegistry; use crate::network::{ action::Action, error::NetworkStateError, - model::{ - Connection as NetworkConnection, Device as NetworkDevice, IpAddress, WirelessConnection, - }, + model::{Connection as NetworkConnection, Device as NetworkDevice, WirelessConnection}, }; -use log; use agama_lib::network::types::SSID; use async_std::{channel::Sender, sync::Arc}; use futures::lock::{MappedMutexGuard, Mutex, MutexGuard}; -use std::net::{AddrParseError, Ipv4Addr}; use zbus::{ dbus_interface, zvariant::{ObjectPath, OwnedObjectPath}, SignalContext, }; +mod ip_config; +pub use ip_config::Ip; + /// D-Bus interface for the network devices collection /// /// It offers an API to query the devices collection. @@ -340,147 +339,6 @@ impl Match { } } -/// D-Bus interface for IPv4 settings -pub struct Ipv4 { - actions: Arc>>, - connection: Arc>, -} - -impl Ipv4 { - /// Creates an IPv4 interface object. - /// - /// * `actions`: sending-half of a channel to send actions. - /// * `connection`: connection to expose over D-Bus. - pub fn new(actions: Sender, connection: Arc>) -> Self { - Self { - actions: Arc::new(Mutex::new(actions)), - connection, - } - } - - /// Returns the underlying connection. - async fn get_connection(&self) -> MutexGuard { - self.connection.lock().await - } - - /// Updates the connection data in the NetworkSystem. - /// - /// * `connection`: Updated connection. - async fn update_connection<'a>( - &self, - connection: MutexGuard<'a, NetworkConnection>, - ) -> zbus::fdo::Result<()> { - let actions = self.actions.lock().await; - actions - .send(Action::UpdateConnection(connection.clone())) - .await - .unwrap(); - Ok(()) - } -} - -#[dbus_interface(name = "org.opensuse.Agama1.Network.Connection.IPv4")] -impl Ipv4 { - /// List of IP addresses. - /// - /// When the method is 'auto', these addresses are used as additional addresses. - #[dbus_interface(property)] - pub async fn addresses(&self) -> Vec { - let connection = self.get_connection().await; - connection - .ipv4() - .addresses - .iter() - .map(|ip| ip.to_string()) - .collect() - } - - #[dbus_interface(property)] - pub async fn set_addresses(&mut self, addresses: Vec) -> zbus::fdo::Result<()> { - let mut connection = self.get_connection().await; - let parsed: Vec = addresses - .into_iter() - .filter_map(|ip| match ip.parse::() { - Ok(address) => Some(address), - Err(error) => { - log::error!("Ignoring the invalid IPv4 address: {} ({})", ip, error); - None - } - }) - .collect(); - connection.ipv4_mut().addresses = parsed; - self.update_connection(connection).await - } - - /// IP configuration method. - /// - /// Possible values: "disabled", "auto", "manual" or "link-local". - /// - /// See [crate::network::model::IpMethod]. - #[dbus_interface(property)] - pub async fn method(&self) -> String { - let connection = self.get_connection().await; - connection.ipv4().method.to_string() - } - - #[dbus_interface(property)] - pub async fn set_method(&mut self, method: &str) -> zbus::fdo::Result<()> { - let mut connection = self.get_connection().await; - connection.ipv4_mut().method = method.parse()?; - self.update_connection(connection).await - } - - /// Name server addresses. - #[dbus_interface(property)] - pub async fn nameservers(&self) -> Vec { - let connection = self.get_connection().await; - connection - .ipv4() - .nameservers - .iter() - .map(|a| a.to_string()) - .collect() - } - - #[dbus_interface(property)] - pub async fn set_nameservers(&mut self, addresses: Vec) -> zbus::fdo::Result<()> { - let mut connection = self.get_connection().await; - let ipv4 = connection.ipv4_mut(); - addresses - .iter() - .map(|addr| addr.parse::()) - .collect::, AddrParseError>>() - .map(|parsed| ipv4.nameservers = parsed) - .map_err(NetworkStateError::from)?; - self.update_connection(connection).await - } - - /// Network gateway. - /// - /// An empty string removes the current value. It is not possible to set a gateway if the - /// Addresses property is empty. - #[dbus_interface(property)] - pub async fn gateway(&self) -> String { - let connection = self.get_connection().await; - match connection.ipv4().gateway { - Some(addr) => addr.to_string(), - None => "".to_string(), - } - } - - #[dbus_interface(property)] - pub async fn set_gateway(&mut self, gateway: String) -> zbus::fdo::Result<()> { - let mut connection = self.get_connection().await; - let ipv4 = connection.ipv4_mut(); - if gateway.is_empty() { - ipv4.gateway = None; - } else { - let parsed: Ipv4Addr = gateway.parse().map_err(NetworkStateError::from)?; - ipv4.gateway = Some(parsed); - } - self.update_connection(connection).await - } -} /// D-Bus interface for wireless settings pub struct Wireless { actions: Arc>>, diff --git a/rust/agama-dbus-server/src/network/dbus/interfaces/ip_config.rs b/rust/agama-dbus-server/src/network/dbus/interfaces/ip_config.rs new file mode 100644 index 0000000000..26ae3131c4 --- /dev/null +++ b/rust/agama-dbus-server/src/network/dbus/interfaces/ip_config.rs @@ -0,0 +1,229 @@ +//! Network D-Bus interfaces for IP configuration. +//! +//! This module contains the D-Bus interfaces to deal with IPv4 and IPv6 configuration. +//! The `dbus_interface` macro should be applied to structs, that's the reason there are +//! two different structs for IPv4 and IPv6 settings. The common code have been moved +//! to the `Ip` struct. +use crate::network::{ + action::Action, + model::{Connection as NetworkConnection, IpConfig, IpMethod}, +}; +use async_std::{channel::Sender, sync::Arc}; +use cidr::IpInet; +use futures::lock::{MappedMutexGuard, Mutex, MutexGuard}; +use std::net::IpAddr; +use zbus::dbus_interface; + +/// D-Bus interface for IPv4 and IPv6 settings +pub struct Ip { + actions: Arc>>, + connection: Arc>, +} + +impl Ip { + /// Creates an IP interface object. + /// + /// * `actions`: sending-half of a channel to send actions. + /// * `connection`: connection to expose over D-Bus. + pub fn new(actions: Sender, connection: Arc>) -> Self { + Self { + actions: Arc::new(Mutex::new(actions)), + connection, + } + } + + /// Returns the underlying connection. + async fn get_connection(&self) -> MutexGuard { + self.connection.lock().await + } + + /// Updates the connection data in the NetworkSystem. + /// + /// * `connection`: Updated connection. + async fn update_connection<'a>( + &self, + connection: MutexGuard<'a, NetworkConnection>, + ) -> zbus::fdo::Result<()> { + let actions = self.actions.lock().await; + actions + .send(Action::UpdateConnection(connection.clone())) + .await + .unwrap(); + Ok(()) + } +} + +impl Ip { + /// Returns the IpConfig struct. + async fn get_ip_config(&self) -> MappedMutexGuard { + MutexGuard::map(self.get_connection().await, |c| c.ip_config_mut()) + } + + /// Updates the IpConfig struct. + /// + /// * `func`: function to update the configuration. + async fn update_config(&self, func: F) -> zbus::fdo::Result<()> + where + F: Fn(&mut IpConfig), + { + let mut connection = self.get_connection().await; + func(connection.ip_config_mut()); + self.update_connection(connection).await?; + Ok(()) + } +} + +#[dbus_interface(name = "org.opensuse.Agama1.Network.Connection.IP")] +impl Ip { + /// List of IP addresses. + /// + /// When the method is 'auto', these addresses are used as additional addresses. + #[dbus_interface(property)] + pub async fn addresses(&self) -> Vec { + let ip_config = self.get_ip_config().await; + ip_config.addresses.iter().map(|a| a.to_string()).collect() + } + + #[dbus_interface(property)] + pub async fn set_addresses(&mut self, addresses: Vec) -> zbus::fdo::Result<()> { + let addresses = helpers::parse_addresses::(addresses); + self.update_config(|ip| ip.addresses = addresses.clone()) + .await + } + + /// IPv4 configuration method. + /// + /// Possible values: "disabled", "auto", "manual" or "link-local". + /// + /// See [crate::network::model::IpMethod]. + #[dbus_interface(property)] + pub async fn method4(&self) -> String { + let ip_config = self.get_ip_config().await; + ip_config.method4.to_string() + } + + #[dbus_interface(property)] + pub async fn set_method4(&mut self, method: &str) -> zbus::fdo::Result<()> { + let method: IpMethod = method.parse()?; + self.update_config(|ip| ip.method4 = method).await + } + + /// IPv6 configuration method. + /// + /// Possible values: "disabled", "auto", "manual" or "link-local". + /// + /// See [crate::network::model::IpMethod]. + #[dbus_interface(property)] + pub async fn method6(&self) -> String { + let ip_config = self.get_ip_config().await; + ip_config.method6.to_string() + } + + #[dbus_interface(property)] + pub async fn set_method6(&mut self, method: &str) -> zbus::fdo::Result<()> { + let method: IpMethod = method.parse()?; + self.update_config(|ip| ip.method6 = method).await + } + + /// Name server addresses. + #[dbus_interface(property)] + pub async fn nameservers(&self) -> Vec { + let ip_config = self.get_ip_config().await; + ip_config + .nameservers + .iter() + .map(IpAddr::to_string) + .collect() + } + + #[dbus_interface(property)] + pub async fn set_nameservers(&mut self, addresses: Vec) -> zbus::fdo::Result<()> { + let addresses = helpers::parse_addresses::(addresses); + self.update_config(|ip| ip.nameservers = addresses.clone()) + .await + } + + /// Network gateway for IPv4. + /// + /// An empty string removes the current value. + #[dbus_interface(property)] + pub async fn gateway4(&self) -> String { + let ip_config = self.get_ip_config().await; + match ip_config.gateway4 { + Some(ref address) => address.to_string(), + None => "".to_string(), + } + } + + #[dbus_interface(property)] + pub async fn set_gateway4(&mut self, gateway: String) -> zbus::fdo::Result<()> { + let gateway = helpers::parse_gateway(gateway)?; + self.update_config(|ip| ip.gateway4 = gateway).await + } + + /// Network gateway for IPv6. + /// + /// An empty string removes the current value. + #[dbus_interface(property)] + pub async fn gateway6(&self) -> String { + let ip_config = self.get_ip_config().await; + match ip_config.gateway6 { + Some(ref address) => address.to_string(), + None => "".to_string(), + } + } + + #[dbus_interface(property)] + pub async fn set_gateway6(&mut self, gateway: String) -> zbus::fdo::Result<()> { + let gateway = helpers::parse_gateway(gateway)?; + self.update_config(|ip| ip.gateway6 = gateway).await + } +} + +mod helpers { + use crate::network::error::NetworkStateError; + use log; + use std::{ + fmt::{Debug, Display}, + str::FromStr, + }; + + /// Parses a set of addresses in textual form into T. + /// + /// * `addresses`: addresses to parse. + pub fn parse_addresses(addresses: Vec) -> Vec + where + T: FromStr, + ::Err: Display, + { + addresses + .into_iter() + .filter_map(|ip| match ip.parse::() { + Ok(address) => Some(address), + Err(error) => { + log::error!("Ignoring the invalid IP address: {} ({})", ip, error); + None + } + }) + .collect() + } + + /// Sets the gateway for an IP configuration. + /// + /// * `ip`: IpConfig object. + /// * `gateway`: IP in textual form. + pub fn parse_gateway(gateway: String) -> Result, NetworkStateError> + where + T: FromStr, + ::Err: Debug + Display, + { + if gateway.is_empty() { + Ok(None) + } else { + let parsed = gateway + .parse() + .map_err(|_| NetworkStateError::InvalidIpAddr(gateway))?; + Ok(Some(parsed)) + } + } +} diff --git a/rust/agama-dbus-server/src/network/dbus/tree.rs b/rust/agama-dbus-server/src/network/dbus/tree.rs index bcf5f097b3..c21e87da91 100644 --- a/rust/agama-dbus-server/src/network/dbus/tree.rs +++ b/rust/agama-dbus-server/src/network/dbus/tree.rs @@ -103,7 +103,7 @@ impl Tree { self.add_interface( &path, - interfaces::Ipv4::new(self.actions.clone(), Arc::clone(&cloned)), + interfaces::Ip::new(self.actions.clone(), Arc::clone(&cloned)), ) .await?; @@ -186,7 +186,7 @@ impl Tree { async fn remove_connection_on(&self, path: &str) -> Result<(), ServiceError> { let object_server = self.connection.object_server(); _ = object_server.remove::(path).await; - object_server.remove::(path).await?; + object_server.remove::(path).await?; object_server .remove::(path) .await?; diff --git a/rust/agama-dbus-server/src/network/error.rs b/rust/agama-dbus-server/src/network/error.rs index 49a7c3cb5f..8c62c05f24 100644 --- a/rust/agama-dbus-server/src/network/error.rs +++ b/rust/agama-dbus-server/src/network/error.rs @@ -1,5 +1,4 @@ //! Error types. -use std::net::AddrParseError; use thiserror::Error; use uuid::Uuid; @@ -10,8 +9,8 @@ pub enum NetworkStateError { UnknownConnection(String), #[error("Invalid connection UUID: '{0}'")] InvalidUuid(String), - #[error("Invalid IP address")] - InvalidIpv4Addr(#[from] AddrParseError), + #[error("Invalid IP address: '{0}'")] + InvalidIpAddr(String), #[error("Invalid IP method: '{0}'")] InvalidIpMethod(u8), #[error("Invalid wireless mode: '{0}'")] diff --git a/rust/agama-dbus-server/src/network/model.rs b/rust/agama-dbus-server/src/network/model.rs index 7478e546de..1b6b49f76f 100644 --- a/rust/agama-dbus-server/src/network/model.rs +++ b/rust/agama-dbus-server/src/network/model.rs @@ -2,16 +2,17 @@ //! //! * 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 uuid::Uuid; - use crate::network::error::NetworkStateError; use agama_lib::network::types::{DeviceType, SSID}; +use cidr::IpInet; use std::{ + default::Default, fmt, - net::{AddrParseError, Ipv4Addr}, + net::IpAddr, str::{self, FromStr}, }; use thiserror::Error; +use uuid::Uuid; #[derive(Default, Clone)] pub struct NetworkState { @@ -274,12 +275,13 @@ impl Connection { self.base().uuid } - pub fn ipv4(&self) -> &Ipv4Config { - &self.base().ipv4 + /// FIXME: rename to ip_config + pub fn ip_config(&self) -> &IpConfig { + &self.base().ip_config } - pub fn ipv4_mut(&mut self) -> &mut Ipv4Config { - &mut self.base_mut().ipv4 + pub fn ip_config_mut(&mut self) -> &mut IpConfig { + &mut self.base_mut().ip_config } pub fn match_config(&self) -> &MatchConfig { @@ -308,7 +310,7 @@ impl Connection { pub struct BaseConnection { pub id: String, pub uuid: Uuid, - pub ipv4: Ipv4Config, + pub ip_config: IpConfig, pub status: Status, pub interface: String, pub match_config: MatchConfig, @@ -316,7 +318,7 @@ pub struct BaseConnection { impl PartialEq for BaseConnection { fn eq(&self, other: &Self) -> bool { - self.id == other.id && self.uuid == other.uuid && self.ipv4 == other.ipv4 + self.id == other.id && self.uuid == other.uuid && self.ip_config == other.ip_config } } @@ -327,12 +329,14 @@ pub enum Status { Removed, } -#[derive(Debug, Default, PartialEq, Clone)] -pub struct Ipv4Config { - pub method: IpMethod, - pub addresses: Vec, - pub nameservers: Vec, - pub gateway: Option, +#[derive(Default, Debug, PartialEq, Clone)] +pub struct IpConfig { + pub method4: IpMethod, + pub method6: IpMethod, + pub addresses: Vec, + pub nameservers: Vec, + pub gateway4: Option, + pub gateway6: Option, } #[derive(Debug, Default, PartialEq, Clone)] @@ -494,67 +498,3 @@ impl TryFrom<&str> for SecurityProtocol { } } } - -/// Represents an IPv4 address with a prefix. -#[derive(Debug, Clone, PartialEq)] -pub struct IpAddress(Ipv4Addr, u32); - -#[derive(Error, Debug)] -pub enum ParseIpAddressError { - #[error("Missing prefix")] - MissingPrefix, - #[error("Invalid prefix part '{0}'")] - InvalidPrefix(String), - #[error("Invalid address part: {0}")] - InvalidAddr(AddrParseError), -} - -impl IpAddress { - /// Returns an new IpAddress object - /// - /// * `addr`: IPv4 address. - /// * `prefix`: IPv4 address prefix. - pub fn new(addr: Ipv4Addr, prefix: u32) -> Self { - IpAddress(addr, prefix) - } - - /// Returns the IPv4 address. - pub fn addr(&self) -> &Ipv4Addr { - &self.0 - } - - /// Returns the prefix. - pub fn prefix(&self) -> u32 { - self.1 - } -} - -impl From for (String, u32) { - fn from(value: IpAddress) -> Self { - (value.0.to_string(), value.1) - } -} - -impl FromStr for IpAddress { - type Err = ParseIpAddressError; - - fn from_str(s: &str) -> Result { - let Some((address, prefix)) = s.split_once('/') else { - return Err(ParseIpAddressError::MissingPrefix); - }; - - let address: Ipv4Addr = address.parse().map_err(ParseIpAddressError::InvalidAddr)?; - - let prefix: u32 = prefix - .parse() - .map_err(|_| ParseIpAddressError::InvalidPrefix(prefix.to_string()))?; - - Ok(IpAddress(address, prefix)) - } -} - -impl fmt::Display for IpAddress { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}/{}", self.0, self.1) - } -} diff --git a/rust/agama-dbus-server/src/network/nm/dbus.rs b/rust/agama-dbus-server/src/network/nm/dbus.rs index 9e513c0898..cb873b2267 100644 --- a/rust/agama-dbus-server/src/network/nm/dbus.rs +++ b/rust/agama-dbus-server/src/network/nm/dbus.rs @@ -8,9 +8,10 @@ use agama_lib::{ dbus::{NestedHash, OwnedNestedHash}, network::types::SSID, }; -use std::collections::HashMap; +use cidr::IpInet; +use std::{collections::HashMap, net::IpAddr}; use uuid::Uuid; -use zbus::zvariant::{self, Value}; +use zbus::zvariant::{self, OwnedValue, Value}; const ETHERNET_KEY: &str = "802-3-ethernet"; const WIRELESS_KEY: &str = "802-11-wireless"; @@ -27,7 +28,8 @@ pub fn connection_to_dbus(conn: &Connection) -> NestedHash { ("type", ETHERNET_KEY.into()), ("interface-name", conn.interface().into()), ]); - result.insert("ipv4", ipv4_to_dbus(conn.ipv4())); + result.insert("ipv4", ip_config_to_ipv4_dbus(conn.ip_config())); + 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 { @@ -121,38 +123,74 @@ fn cleanup_dbus_connection(conn: &mut NestedHash) { } } -fn ipv4_to_dbus(ipv4: &Ipv4Config) -> HashMap<&str, zvariant::Value> { - let addresses: Vec> = ipv4 +fn ip_config_to_ipv4_dbus(ip_config: &IpConfig) -> HashMap<&str, zvariant::Value> { + let addresses: Vec> = ip_config .addresses .iter() + .filter(|ip| ip.is_ipv4()) .map(|ip| { HashMap::from([ - ("address", Value::new(ip.addr().to_string())), - ("prefix", Value::new(ip.prefix())), + ("address", Value::new(ip.address().to_string())), + ("prefix", Value::new(ip.network_length() as u32)), ]) }) .collect(); let address_data: Value = addresses.into(); - let dns_data: Value = ipv4 + let dns_data: Value = ip_config .nameservers .iter() + .filter(|ip| ip.is_ipv4()) .map(|ns| ns.to_string()) - .collect::>() + .collect::>() .into(); let mut ipv4_dbus = HashMap::from([ ("address-data", address_data), ("dns-data", dns_data), - ("method", ipv4.method.to_string().into()), + ("method", ip_config.method4.to_string().into()), ]); - if let Some(gateway) = ipv4.gateway { + if let Some(gateway) = &ip_config.gateway4 { ipv4_dbus.insert("gateway", gateway.to_string().into()); } ipv4_dbus } +fn ip_config_to_ipv6_dbus(ip_config: &IpConfig) -> HashMap<&str, zvariant::Value> { + let addresses: Vec> = ip_config + .addresses + .iter() + .filter(|ip| ip.is_ipv6()) + .map(|ip| { + HashMap::from([ + ("address", Value::new(ip.address().to_string())), + ("prefix", Value::new(ip.network_length() as u32)), + ]) + }) + .collect(); + let address_data: Value = addresses.into(); + + let dns_data: Value = ip_config + .nameservers + .iter() + .filter(|ip| ip.is_ipv6()) + .map(|ns| ns.to_string()) + .collect::>() + .into(); + + let mut ipv6_dbus = HashMap::from([ + ("address-data", address_data), + ("dns-data", dns_data), + ("method", ip_config.method6.to_string().into()), + ]); + + if let Some(gateway) = &ip_config.gateway6 { + ipv6_dbus.insert("gateway", gateway.to_string().into()); + } + ipv6_dbus +} + fn wireless_config_to_dbus(conn: &WirelessConnection) -> NestedHash { let config = &conn.wireless; let wireless: HashMap<&str, zvariant::Value> = HashMap::from([ @@ -231,9 +269,7 @@ fn base_connection_from_dbus(conn: &OwnedNestedHash) -> Option { base_connection.match_config = match_config_from_dbus(match_config)?; } - if let Some(ipv4) = conn.get("ipv4") { - base_connection.ipv4 = ipv4_config_from_dbus(ipv4)?; - } + base_connection.ip_config = ip_config_from_dbus(&conn)?; Some(base_connection) } @@ -278,38 +314,75 @@ fn match_config_from_dbus( Some(match_conf) } -fn ipv4_config_from_dbus(ipv4: &HashMap) -> Option { - let method: &str = ipv4.get("method")?.downcast_ref()?; - let address_data = ipv4.get("address-data")?; +fn ip_config_from_dbus(conn: &OwnedNestedHash) -> Option { + let mut ip_config = IpConfig::default(); + + if let Some(ipv4) = conn.get("ipv4") { + let method4: &str = ipv4.get("method")?.downcast_ref()?; + ip_config.method4 = NmMethod(method4.to_string()).try_into().ok()?; + + let address_data = ipv4.get("address-data")?; + let mut addresses = addresses_with_prefix_from_dbus(&address_data)?; + + ip_config.addresses.append(&mut addresses); + + if let Some(dns_data) = ipv4.get("dns-data") { + let mut servers = nameservers_from_dbus(dns_data)?; + ip_config.nameservers.append(&mut servers); + } + + if let Some(gateway) = ipv4.get("gateway") { + let gateway: &str = gateway.downcast_ref()?; + ip_config.gateway4 = Some(gateway.parse().unwrap()); + } + } + + if let Some(ipv6) = conn.get("ipv6") { + let method6: &str = ipv6.get("method")?.downcast_ref()?; + ip_config.method6 = NmMethod(method6.to_string()).try_into().ok()?; + + let address_data = ipv6.get("address-data")?; + let mut addresses = addresses_with_prefix_from_dbus(&address_data)?; + + ip_config.addresses.append(&mut addresses); + + if let Some(dns_data) = ipv6.get("dns-data") { + let mut servers = nameservers_from_dbus(dns_data)?; + ip_config.nameservers.append(&mut servers); + } + + if let Some(gateway) = ipv6.get("gateway") { + let gateway: &str = gateway.downcast_ref()?; + ip_config.gateway6 = Some(gateway.parse().unwrap()); + } + } + + Some(ip_config) +} + +fn addresses_with_prefix_from_dbus(address_data: &OwnedValue) -> Option> { let address_data = address_data.downcast_ref::()?; - let mut addresses: Vec = vec![]; + let mut addresses: Vec = vec![]; for addr in address_data.get() { let dict = addr.downcast_ref::()?; let map = >>::try_from(dict.clone()).unwrap(); let addr_str: &str = map.get("address")?.downcast_ref()?; let prefix: &u32 = map.get("prefix")?.downcast_ref()?; - addresses.push(IpAddress::new(addr_str.parse().unwrap(), *prefix)) - } - let mut ipv4_config = Ipv4Config { - method: NmMethod(method.to_string()).try_into().ok()?, - addresses, - ..Default::default() - }; - - if let Some(dns_data) = ipv4.get("dns-data") { - let dns_data = dns_data.downcast_ref::()?; - for server in dns_data.get() { - let server: &str = server.downcast_ref()?; - ipv4_config.nameservers.push(server.parse().unwrap()); - } + let prefix = *prefix as u8; + let address = IpInet::new(addr_str.parse().unwrap(), prefix).ok()?; + addresses.push(address) } + Some(addresses) +} - if let Some(gateway) = ipv4.get("gateway") { - let gateway: &str = gateway.downcast_ref()?; - ipv4_config.gateway = Some(gateway.parse().unwrap()); +fn nameservers_from_dbus(dns_data: &OwnedValue) -> Option> { + let dns_data = dns_data.downcast_ref::()?; + let mut servers: Vec = vec![]; + for server in dns_data.get() { + let server: &str = server.downcast_ref()?; + servers.push(server.parse().unwrap()); } - - Some(ipv4_config) + Some(servers) } fn wireless_config_from_dbus(conn: &OwnedNestedHash) -> Option { @@ -360,7 +433,7 @@ mod test { }; use crate::network::{model::*, nm::dbus::ETHERNET_KEY}; use agama_lib::network::types::SSID; - use std::{collections::HashMap, net::Ipv4Addr}; + use std::{collections::HashMap, net::IpAddr}; use uuid::Uuid; use zbus::zvariant::{self, OwnedValue, Value}; @@ -372,7 +445,7 @@ mod test { ("uuid".to_string(), Value::new(uuid).to_owned()), ]); - let address_data = vec![HashMap::from([ + let address_v4_data = vec![HashMap::from([ ("address".to_string(), Value::new("192.168.0.10")), ("prefix".to_string(), Value::new(24_u32)), ])]; @@ -381,7 +454,7 @@ mod test { ("method".to_string(), Value::new("auto").to_owned()), ( "address-data".to_string(), - Value::new(address_data).to_owned(), + Value::new(address_v4_data).to_owned(), ), ("gateway".to_string(), Value::new("192.168.0.1").to_owned()), ( @@ -390,6 +463,27 @@ mod test { ), ]); + let address_v6_data = vec![HashMap::from([ + ("address".to_string(), Value::new("::ffff:c0a8:10a")), + ("prefix".to_string(), Value::new(24_u32)), + ])]; + + let ipv6_section = HashMap::from([ + ("method".to_string(), Value::new("auto").to_owned()), + ( + "address-data".to_string(), + Value::new(address_v6_data).to_owned(), + ), + ( + "gateway".to_string(), + Value::new("::ffff:c0a8:101").to_owned(), + ), + ( + "dns-data".to_string(), + Value::new(vec!["::ffff:c0a8:102"]).to_owned(), + ), + ]); + let match_section = HashMap::from([( "kernel-command-line".to_string(), Value::new(vec!["pci-0000:00:19.0"]).to_owned(), @@ -398,6 +492,7 @@ mod test { let dbus_conn = HashMap::from([ ("connection".to_string(), connection_section), ("ipv4".to_string(), ipv4_section), + ("ipv6".to_string(), ipv6_section), ("match".to_string(), match_section), (ETHERNET_KEY.to_string(), build_ethernet_section_from_dbus()), ]); @@ -405,12 +500,26 @@ mod test { let connection = connection_from_dbus(dbus_conn).unwrap(); assert_eq!(connection.id(), "eth0"); - let ipv4 = connection.ipv4(); + let ip_config = connection.ip_config(); let match_config = connection.match_config(); assert_eq!(match_config.kernel, vec!["pci-0000:00:19.0"]); - assert_eq!(ipv4.addresses, vec!["192.168.0.10/24".parse().unwrap()]); - assert_eq!(ipv4.nameservers, vec![Ipv4Addr::new(192, 168, 0, 2)]); - assert_eq!(ipv4.method, IpMethod::Auto); + + assert_eq!( + ip_config.addresses, + vec![ + "192.168.0.10/24".parse().unwrap(), + "::ffff:c0a8:10a/24".parse().unwrap() + ] + ); + assert_eq!( + ip_config.nameservers, + vec![ + "192.168.0.2".parse::().unwrap(), + "::ffff:c0a8:102".parse::().unwrap() + ] + ); + assert_eq!(ip_config.method4, IpMethod::Auto); + assert_eq!(ip_config.method6, IpMethod::Auto); } #[test] @@ -518,8 +627,25 @@ mod test { Value::new(vec!["192.168.1.1"]).to_owned(), ), ]); + + let ipv6 = HashMap::from([ + ( + "method".to_string(), + Value::new("manual".to_string()).to_owned(), + ), + ( + "gateway".to_string(), + Value::new("::ffff:c0a8:101".to_string()).to_owned(), + ), + ( + "addresses".to_string(), + Value::new(vec!["::ffff:c0a8:102"]).to_owned(), + ), + ]); + original.insert("connection".to_string(), connection); original.insert("ipv4".to_string(), ipv4); + original.insert("ipv6".to_string(), ipv6); let base = BaseConnection { id: "agama".to_string(), @@ -555,6 +681,16 @@ mod test { Value::new("192.168.1.1".to_string()) ); assert!(ipv4.get("addresses").is_none()); + + let ipv6 = merged.get("ipv6").unwrap(); + assert_eq!( + *ipv6.get("method").unwrap(), + Value::new("disabled".to_string()) + ); + assert_eq!( + *ipv6.get("gateway").unwrap(), + Value::new("::ffff:c0a8:101".to_string()) + ); } #[test] @@ -587,15 +723,19 @@ mod test { } fn build_base_connection() -> BaseConnection { - let addresses = vec!["192.168.0.2/24".parse().unwrap()]; - let ipv4 = Ipv4Config { + let addresses = vec![ + "192.168.0.2/24".parse().unwrap(), + "::ffff:c0a8:2".parse().unwrap(), + ]; + let ip_config = IpConfig { addresses, - gateway: Some(Ipv4Addr::new(192, 168, 0, 1)), + gateway4: Some("192.168.0.1".parse().unwrap()), + gateway6: Some("::ffff:c0a8:1".parse().unwrap()), ..Default::default() }; BaseConnection { id: "agama".to_string(), - ipv4, + ip_config, ..Default::default() } } diff --git a/rust/agama-dbus-server/tests/network.rs b/rust/agama-dbus-server/tests/network.rs index e473fbfc7d..58044829b5 100644 --- a/rust/agama-dbus-server/tests/network.rs +++ b/rust/agama-dbus-server/tests/network.rs @@ -1,9 +1,14 @@ mod common; use self::common::DBusServer; -use agama_dbus_server::network::{self, model, Adapter, NetworkService, NetworkState}; +use agama_dbus_server::network::{ + self, + model::{self, IpMethod}, + Adapter, NetworkService, NetworkState, +}; use agama_lib::network::{settings, types::DeviceType, NetworkClient}; use async_std::test; +use cidr::IpInet; #[derive(Default)] pub struct NetworkTestAdapter(network::NetworkState); @@ -59,9 +64,15 @@ async fn test_add_connection() { .await .unwrap(); + let addresses: Vec = vec![ + "192.168.0.2/24".parse().unwrap(), + "::ffff:c0a8:7ac7/64".parse().unwrap(), + ]; let wlan0 = settings::NetworkConnection { id: "wlan0".to_string(), - method: Some("auto".to_string()), + method4: Some("auto".to_string()), + method6: Some("disabled".to_string()), + addresses: addresses.clone(), wireless: Some(settings::WirelessSettings { password: "123456".to_string(), security: "wpa-psk".to_string(), @@ -74,9 +85,15 @@ async fn test_add_connection() { let conns = client.connections().await.unwrap(); assert_eq!(conns.len(), 1); + let conn = conns.first().unwrap(); assert_eq!(conn.id, "wlan0"); assert_eq!(conn.device_type(), DeviceType::Wireless); + assert_eq!(&conn.addresses, &addresses); + let method4 = conn.method4.as_ref().unwrap(); + assert_eq!(method4, &IpMethod::Auto.to_string()); + let method6 = conn.method6.as_ref().unwrap(); + assert_eq!(method6, &IpMethod::Disabled.to_string()); } #[test] diff --git a/rust/agama-lib/Cargo.toml b/rust/agama-lib/Cargo.toml index 181a2cff50..ecff567b98 100644 --- a/rust/agama-lib/Cargo.toml +++ b/rust/agama-lib/Cargo.toml @@ -9,6 +9,7 @@ edition = "2021" agama-settings = { path="../agama-settings" } anyhow = "1.0" async-std = "1.12.0" +cidr = { version = "0.2.2", features = ["serde"] } curl = { version = "0.4.44", features = ["protocol-ftp"] } futures = "0.3.27" futures-util = "0.3.27" diff --git a/rust/agama-lib/share/examples/profile.json b/rust/agama-lib/share/examples/profile.json index 484108b9d4..0aae970862 100644 --- a/rust/agama-lib/share/examples/profile.json +++ b/rust/agama-lib/share/examples/profile.json @@ -4,7 +4,7 @@ "language": "en_US" }, "software": { - "product": "ALP" + "product": "ALP-Dolomite" }, "storage": { "bootDevice": "/dev/dm-1" @@ -22,14 +22,18 @@ "connections": [ { "id": "Ethernet network device 1", - "method": "manual", + "method4": "manual", + "method6": "manual", "interface": "eth0", "addresses": [ - "192.168.122.100/24" + "192.168.122.100/24", + "::ffff:c0a8:7ac7/64" ], - "gateway": "192.168.122.1", + "gateway4": "192.168.122.1", + "gateway6": "::ffff:c0a8:7a01", "nameservers": [ - "192.168.122.1" + "192.168.122.1", + "2001:4860:4860::8888" ] } ] diff --git a/rust/agama-lib/share/examples/profile.jsonnet b/rust/agama-lib/share/examples/profile.jsonnet index 1d04c5a0ec..cd6b63f5bf 100644 --- a/rust/agama-lib/share/examples/profile.jsonnet +++ b/rust/agama-lib/share/examples/profile.jsonnet @@ -52,8 +52,8 @@ local findBiggestDisk(disks) = }, { id: 'Etherned device 1', - method: 'manual', - gateway: '192.168.122.1', + method4: 'manual', + gateway4: '192.168.122.1', addresses: [ '192.168.122.100/24,' ], diff --git a/rust/agama-lib/share/profile.schema.json b/rust/agama-lib/share/profile.schema.json index 3244483a1d..2d9cb33b0a 100644 --- a/rust/agama-lib/share/profile.schema.json +++ b/rust/agama-lib/share/profile.schema.json @@ -36,7 +36,7 @@ "description": "The name of the network interface bound to this connection", "type": "string" }, - "method": { + "method4": { "description": "IPv4 configuration method (e.g., 'auto')", "type": "string", "enum": [ @@ -46,8 +46,22 @@ "disabled" ] }, - "gateway": { - "description": "Connection gateway address (e.g. 192.168.122.1)", + "method6": { + "description": "IPv6 configuration method (e.g., 'auto')", + "type": "string", + "enum": [ + "auto", + "manual", + "link-local", + "disabled" + ] + }, + "gateway4": { + "description": "Connection gateway address (e.g., '192.168.122.1')", + "type": "string" + }, + "gateway6": { + "description": "Connection gateway address (e.g., '::ffff:c0a8:7a01')", "type": "string" }, "addresses": { @@ -61,7 +75,7 @@ "nameservers": { "type": "array", "items": { - "description": "Connection nameservers", + "description": "Nameservers (IPv4 and/or IPv6 are allowed)", "type": "string", "additionalProperties": false } diff --git a/rust/agama-lib/src/network/client.rs b/rust/agama-lib/src/network/client.rs index 61be850280..1ac2d2510f 100644 --- a/rust/agama-lib/src/network/client.rs +++ b/rust/agama-lib/src/network/client.rs @@ -1,8 +1,7 @@ +use super::proxies::{ConnectionProxy, ConnectionsProxy, IPProxy, MatchProxy, WirelessProxy}; use super::settings::{MatchSettings, NetworkConnection, WirelessSettings}; use super::types::SSID; use crate::error::ServiceError; - -use super::proxies::{ConnectionProxy, ConnectionsProxy, IPv4Proxy, MatchProxy, WirelessProxy}; use async_std::stream::StreamExt; use zbus::zvariant::OwnedObjectPath; use zbus::Connection; @@ -69,23 +68,26 @@ impl<'a> NetworkClient<'a> { value => Some(value.to_string()), }; - let ipv4_proxy = IPv4Proxy::builder(&self.connection) + let ip_proxy = IPProxy::builder(&self.connection) .path(path)? .build() .await?; - let method = ipv4_proxy.method().await?; - let gateway = match ipv4_proxy.gateway().await?.as_str() { - "" => None, - value => Some(value.to_string()), - }; - let nameservers = ipv4_proxy.nameservers().await?; - let addresses = ipv4_proxy.addresses().await?; + let method4 = ip_proxy.method4().await?; + let gateway4 = ip_proxy.gateway4().await?.parse().ok(); + let method6 = ip_proxy.method6().await?; + let gateway6 = ip_proxy.gateway6().await?.parse().ok(); + let nameservers = ip_proxy.nameservers().await?; + let nameservers = nameservers.iter().filter_map(|a| a.parse().ok()).collect(); + let addresses = ip_proxy.addresses().await?; + let addresses = addresses.iter().filter_map(|a| a.parse().ok()).collect(); Ok(NetworkConnection { id, - method: Some(method.to_string()), - gateway, + method4: Some(method4.to_string()), + gateway4, + method6: Some(method6.to_string()), + gateway6, addresses, nameservers, interface, @@ -187,7 +189,7 @@ impl<'a> NetworkClient<'a> { let interface = conn.interface.as_deref().unwrap_or(""); proxy.set_interface(interface).await?; - self.update_ipv4_settings(path, conn).await?; + self.update_ip_settings(path, conn).await?; if let Some(ref wireless) = conn.wireless { self.update_wireless_settings(path, wireless).await?; @@ -204,28 +206,37 @@ impl<'a> NetworkClient<'a> { /// /// * `path`: connection D-Bus path. /// * `conn`: network connection. - async fn update_ipv4_settings( + async fn update_ip_settings( &self, path: &OwnedObjectPath, conn: &NetworkConnection, ) -> Result<(), ServiceError> { - let proxy = IPv4Proxy::builder(&self.connection) + let proxy = IPProxy::builder(&self.connection) .path(path)? .build() .await?; - if let Some(ref method) = conn.method { - proxy.set_method(method.as_str()).await?; + if let Some(ref method) = conn.method4 { + proxy.set_method4(method.as_str()).await?; } - let addresses: Vec<_> = conn.addresses.iter().map(String::as_ref).collect(); - proxy.set_addresses(addresses.as_slice()).await?; + if let Some(ref method) = conn.method6 { + proxy.set_method6(method.as_str()).await?; + } + + let addresses: Vec<_> = conn.addresses.iter().map(|a| a.to_string()).collect(); + let addresses: Vec<&str> = addresses.iter().map(|a| a.as_str()).collect(); + proxy.set_addresses(&addresses).await?; + + let nameservers: Vec<_> = conn.nameservers.iter().map(|a| a.to_string()).collect(); + let nameservers: Vec<_> = nameservers.iter().map(|a| a.as_str()).collect(); + proxy.set_nameservers(&nameservers).await?; - let nameservers: Vec<_> = conn.nameservers.iter().map(String::as_ref).collect(); - proxy.set_nameservers(nameservers.as_slice()).await?; + let gateway = conn.gateway4.map_or(String::from(""), |g| g.to_string()); + proxy.set_gateway4(&gateway).await?; - let gateway = conn.gateway.as_deref().unwrap_or(""); - proxy.set_gateway(gateway).await?; + let gateway = conn.gateway6.map_or(String::from(""), |g| g.to_string()); + proxy.set_gateway6(&gateway).await?; Ok(()) } diff --git a/rust/agama-lib/src/network/proxies.rs b/rust/agama-lib/src/network/proxies.rs index 4de94c5560..2b43124722 100644 --- a/rust/agama-lib/src/network/proxies.rs +++ b/rust/agama-lib/src/network/proxies.rs @@ -86,34 +86,42 @@ trait Connection { } #[dbus_proxy( - interface = "org.opensuse.Agama1.Network.Connection.IPv4", + interface = "org.opensuse.Agama1.Network.Connection.IP", default_service = "org.opensuse.Agama1", - default_path = "/org/opensuse/Agama1/Network" + default_path = "/org/opensuse/Agama1/Network/connections/0" )] -trait IPv4 { +trait IP { /// Addresses property - /// - /// By now just an array of IPv4 addresses in string format #[dbus_proxy(property)] fn addresses(&self) -> zbus::Result>; #[dbus_proxy(property)] fn set_addresses(&self, value: &[&str]) -> zbus::Result<()>; - /// Gateway property + /// Gateway4 property + #[dbus_proxy(property)] + fn gateway4(&self) -> zbus::Result; + #[dbus_proxy(property)] + fn set_gateway4(&self, value: &str) -> zbus::Result<()>; + + /// Gateway6 property #[dbus_proxy(property)] - fn gateway(&self) -> zbus::Result; + fn gateway6(&self) -> zbus::Result; #[dbus_proxy(property)] - fn set_gateway(&self, value: &str) -> zbus::Result<()>; + fn set_gateway6(&self, value: &str) -> zbus::Result<()>; - /// Method property + /// Method4 property #[dbus_proxy(property)] - fn method(&self) -> zbus::Result; + fn method4(&self) -> zbus::Result; #[dbus_proxy(property)] - fn set_method(&self, value: &str) -> zbus::Result<()>; + fn set_method4(&self, value: &str) -> zbus::Result<()>; + + /// Method6 property + #[dbus_proxy(property)] + fn method6(&self) -> zbus::Result; + #[dbus_proxy(property)] + fn set_method6(&self, value: &str) -> zbus::Result<()>; /// Nameservers property - /// - /// By now just an array of IPv4 addresses in string format #[dbus_proxy(property)] fn nameservers(&self) -> zbus::Result>; #[dbus_proxy(property)] diff --git a/rust/agama-lib/src/network/settings.rs b/rust/agama-lib/src/network/settings.rs index 0ff53aa7cb..cd344ef323 100644 --- a/rust/agama-lib/src/network/settings.rs +++ b/rust/agama-lib/src/network/settings.rs @@ -3,9 +3,11 @@ use super::types::DeviceType; use agama_settings::error::ConversionError; use agama_settings::{SettingObject, SettingValue, Settings}; +use cidr::IpInet; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; use std::default::Default; +use std::net::IpAddr; /// Network settings for installation #[derive(Debug, Default, Settings, Serialize, Deserialize)] @@ -50,13 +52,17 @@ pub struct WirelessSettings { pub struct NetworkConnection { pub id: String, #[serde(skip_serializing_if = "Option::is_none")] - pub method: Option, + pub method4: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub gateway: Option, + pub gateway4: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub method6: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub gateway6: Option, #[serde(skip_serializing_if = "Vec::is_empty", default)] - pub addresses: Vec, + pub addresses: Vec, #[serde(skip_serializing_if = "Vec::is_empty", default)] - pub nameservers: Vec, + pub nameservers: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub wireless: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -88,11 +94,13 @@ impl TryFrom for NetworkConnection { }; let default_method = SettingValue("disabled".to_string()); - let method = value.get("method").unwrap_or(&default_method); + let method4 = value.get("method4").unwrap_or(&default_method); + let method6 = value.get("method6").unwrap_or(&default_method); let conn = NetworkConnection { id: id.clone().try_into()?, - method: method.clone().try_into()?, + method4: method4.clone().try_into()?, + method6: method6.clone().try_into()?, ..Default::default() }; @@ -133,11 +141,17 @@ mod tests { #[test] fn test_setting_object_to_network_connection() { let name = SettingValue("Ethernet 1".to_string()); - let method = SettingValue("auto".to_string()); - let settings = HashMap::from([("id".to_string(), name), ("method".to_string(), method)]); + let method_auto = SettingValue("auto".to_string()); + let method_disabled = SettingValue("disabled".to_string()); + let settings = HashMap::from([ + ("id".to_string(), name), + ("method4".to_string(), method_auto), + ("method6".to_string(), method_disabled), + ]); let settings = SettingObject(settings); let conn: NetworkConnection = settings.try_into().unwrap(); assert_eq!(conn.id, "Ethernet 1"); - assert_eq!(conn.method, Some("auto".to_string())); + assert_eq!(conn.method4, Some("auto".to_string())); + assert_eq!(conn.method6, Some("disabled".to_string())); } } diff --git a/rust/package/agama-cli.changes b/rust/package/agama-cli.changes index e1e942c8cd..2d0a7d2554 100644 --- a/rust/package/agama-cli.changes +++ b/rust/package/agama-cli.changes @@ -1,3 +1,8 @@ +------------------------------------------------------------------- +Mon Sep 25 11:32:53 UTC 2023 - Imobach Gonzalez Sosa + +- Add support for IPv6 network settings (gh#openSUSE/agama#761). + ------------------------------------------------------------------- Mon Sep 25 10:46:53 UTC 2023 - Michal Filka