diff --git a/nmrs/src/api/models/error.rs b/nmrs/src/api/models/error.rs index 86a788c5..751833e8 100644 --- a/nmrs/src/api/models/error.rs +++ b/nmrs/src/api/models/error.rs @@ -1,6 +1,7 @@ use thiserror::Error; use crate::core::ovpn_parser::error::OvpnParseError; +use crate::models::IpAddressParseError; use super::connection_state::ConnectionStateReason; use super::state_reason::StateReason; @@ -280,4 +281,7 @@ pub enum ConnectionError { /// No interface was found with the given name #[error("no interface named '{0}'")] InterfaceNotFound(String), + + #[error("invalid IP address: {0}")] + AddressParse(IpAddressParseError), } diff --git a/nmrs/src/api/models/ip_address.rs b/nmrs/src/api/models/ip_address.rs new file mode 100644 index 00000000..c45a2195 --- /dev/null +++ b/nmrs/src/api/models/ip_address.rs @@ -0,0 +1,92 @@ +use std::{ + fmt::{self, Display}, + net::{AddrParseError, Ipv4Addr, Ipv6Addr}, + num::ParseIntError, + str::FromStr, +}; + +use thiserror::Error; + +use crate::ConnectionError; + +/// An IP address with its prefix. +#[derive(Copy, Clone, Eq, PartialEq)] +pub struct IpAddress { + pub address: A, + pub prefix: u8, +} + +impl IpAddress { + /// Create the IP address from the address and prefix. + pub fn new(address: A, prefix: u8) -> Self { + Self { address, prefix } + } +} + +impl Display for IpAddress +where + A: Display, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}/{}", self.address, self.prefix) + } +} + +impl fmt::Debug for IpAddress +where + A: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}/{}", self.address, self.prefix) + } +} + +impl FromStr for IpAddress { + type Err = IpAddressParseError; + + fn from_str(s: &str) -> Result { + let (address, prefix) = s.rsplit_once('/').ok_or(IpAddressParseError::Split)?; + let address = address.parse()?; + let prefix = prefix.parse()?; + Ok(Self { address, prefix }) + } +} + +impl FromStr for IpAddress { + type Err = IpAddressParseError; + + fn from_str(s: &str) -> Result { + let (address, prefix) = s.rsplit_once('/').ok_or(IpAddressParseError::Split)?; + let address = address.parse()?; + let prefix = prefix.parse()?; + Ok(Self { address, prefix }) + } +} + +impl From> for Ipv4Addr { + fn from(value: IpAddress) -> Self { + value.address + } +} + +impl From> for Ipv6Addr { + fn from(value: IpAddress) -> Self { + value.address + } +} + +#[derive(Debug, Clone, Error)] +pub enum IpAddressParseError { + #[error("address parsing failed: {0}")] + Addr(#[from] AddrParseError), + #[error("prefix parsing failed: {0}")] + Prefix(#[from] ParseIntError), + #[error("could not split into address and prefix")] + Split, +} + +impl From for ConnectionError { + fn from(value: IpAddressParseError) -> Self { + Self::AddressParse(value) + } +} diff --git a/nmrs/src/api/models/mod.rs b/nmrs/src/api/models/mod.rs index a29dd252..5ec8e5f9 100644 --- a/nmrs/src/api/models/mod.rs +++ b/nmrs/src/api/models/mod.rs @@ -6,6 +6,7 @@ mod connection_state; mod connectivity; mod device; mod error; +mod ip_address; mod monitor; mod network_event; mod openvpn; @@ -44,6 +45,7 @@ pub use connection_state::*; pub use connectivity::*; pub use device::*; pub use error::*; +pub use ip_address::*; pub use monitor::*; pub use network_event::*; pub use openvpn::*; diff --git a/nmrs/src/api/models/saved_connection.rs b/nmrs/src/api/models/saved_connection.rs index 5580faac..1fde7b53 100644 --- a/nmrs/src/api/models/saved_connection.rs +++ b/nmrs/src/api/models/saved_connection.rs @@ -7,10 +7,15 @@ //! [`GetSecrets`](https://networkmanager.dev/docs/api/latest/gdbus-org.freedesktop.NetworkManager.Settings.Connection.html#gdbus-method-org-freedesktop-NetworkManager-Settings-Connection.GetSecrets) //! when a [secret agent](crate::agent) is registered. See feature `01-secret-agent`. -use std::collections::HashMap; +use std::{ + collections::HashMap, + net::{Ipv4Addr, Ipv6Addr}, +}; use zvariant::{OwnedObjectPath, OwnedValue}; +use crate::{builders::Route, models::IpAddress}; + /// Full saved profile with a structured [`SettingsSummary`]. #[non_exhaustive] #[derive(Debug, Clone)] @@ -39,6 +44,10 @@ pub struct SavedConnection { pub filename: Option, /// Decoded type-specific fields (no secrets). pub summary: SettingsSummary, + /// IPv4 specific settings. + pub ipv4: Option>, + /// IPv6 specific settings. + pub ipv6: Option>, } /// Cheap listing: path plus `connection` identity fields only (still one `GetSettings` per profile). @@ -214,3 +223,56 @@ pub enum SettingsSummary { sections: Vec, }, } + +/// Settings from the ipv4/6 section. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct IpSettings { + /// The method by which the connection obtains its IP address for this protocol. + pub method: IpMethod, + /// The list of IP addresses. + pub address_data: Vec>, + /// The gateway associated with this protocol configuration. + pub gateway: Option, + /// The list of DNS search domains. + pub dns_search: Vec, + /// The list of IP routes. + pub route_data: Vec, + /// If true, this connection will never be assigned the default route. + pub never_default: bool, + /// Ignore the automatically configured DNS servers, and only use the saved configuration. + pub ignore_auto_dns: bool, +} + +/// How the connection obtains its IP address. +#[derive(Debug, Clone)] +pub enum IpMethod { + /// IP configuration is automatically defined according to the hardware interface. + Auto, + /// IP configuration is manually specified in the connection settings. + Manual, + /// The connection does not use or require an IP address of this type. + Disabled, + /// The connection should only be configured for link-local operation. + LinkLocal, + /// Allows other devices to connect through this device to the default network. + Shared, + /// IP configuration is ignored for this protocol + Ignore, + /// Unknown method + Other(String), +} + +impl From for IpMethod { + fn from(value: String) -> Self { + match value.as_str() { + "auto" => Self::Auto, + "manual" => Self::Manual, + "disabled" => Self::Disabled, + "link-local" => Self::LinkLocal, + "shared" => Self::Shared, + "ignore" => Self::Ignore, + _ => Self::Other(value), + } + } +} diff --git a/nmrs/src/api/models/snapshot.rs b/nmrs/src/api/models/snapshot.rs index e4684e4e..0cb45c9a 100644 --- a/nmrs/src/api/models/snapshot.rs +++ b/nmrs/src/api/models/snapshot.rs @@ -369,6 +369,8 @@ mod tests { summary: SettingsSummary::Other { sections: vec!["connection".into()], }, + ipv4: None, + ipv6: None, } } @@ -463,6 +465,8 @@ mod tests { hidden: false, mac_randomization: None, }, + ipv4: None, + ipv6: None, } } @@ -486,6 +490,8 @@ mod tests { data_keys: Vec::new(), persistent: false, }, + ipv4: None, + ipv6: None, } } @@ -509,6 +515,8 @@ mod tests { peer_count: 1, first_peer_endpoint: None, }, + ipv4: None, + ipv6: None, } } diff --git a/nmrs/src/core/saved_connection.rs b/nmrs/src/core/saved_connection.rs index a49e5527..9b0be3fc 100644 --- a/nmrs/src/core/saved_connection.rs +++ b/nmrs/src/core/saved_connection.rs @@ -1,18 +1,21 @@ //! Decode and manage NetworkManager saved connection settings. use std::collections::HashMap; +use std::str::FromStr; use futures::stream::{self, StreamExt}; use log::warn; use zbus::Connection; -use zvariant::{OwnedObjectPath, OwnedValue, Str}; +use zvariant::{Array, OwnedObjectPath, OwnedValue, Str, Value}; use crate::Result; use crate::api::models::{ ConnectionError, SavedConnection, SavedConnectionBrief, SettingsPatch, SettingsSummary, VpnSecretFlags, WifiKeyMgmt, WifiSecuritySummary, }; +use crate::builders::Route; use crate::dbus::{NMSettingsConnectionProxy, NMSettingsProxy}; +use crate::models::{IpAddress, IpMethod, IpSettings}; use crate::util::utils::decode_ssid_or_empty; /// Builds the `a{sa{sv}}` delta for [`SettingsPatch`] (unit-tested). @@ -111,6 +114,10 @@ fn take_str(m: &HashMap, key: &str) -> Option { m.get(key).and_then(owned_to_str) } +fn take_str_ref<'a>(m: &'a HashMap, key: &str) -> Option<&'a str> { + m.get(key).and_then(|s| s.try_into().ok()) +} + fn take_bool(m: &HashMap, key: &str) -> Option { m.get(key).and_then(owned_to_bool) } @@ -172,6 +179,8 @@ pub(crate) fn decode_saved( let permissions = take_str_vec(conn, "permissions"); let summary = decode_summary(&connection_type, &settings); + let ipv4 = settings.get("ipv4").map(decode_ip); + let ipv6 = settings.get("ipv6").map(decode_ip); Ok(SavedConnection { path, @@ -186,6 +195,8 @@ pub(crate) fn decode_saved( unsaved, filename, summary, + ipv4, + ipv6, }) } @@ -427,6 +438,65 @@ fn decode_bluetooth(settings: &HashMap>) -> SettingsSummary::Bluetooth { bdaddr, bt_type } } +fn decode_ip(settings: &HashMap) -> IpSettings +where + A: FromStr, +{ + let method = take_str(settings, "method"); + let method = match method { + Some(method) => method.into(), + None => IpMethod::Auto, + }; + let mut address_data = Vec::new(); + if let Some(value) = settings.get("address-data") + && let Ok(array) = TryInto::<&Array>::try_into(value) + { + for entry in array.iter() { + if let Value::Dict(dict) = entry + && let Ok(Some(address)) = dict.get::<_, &str>(&"address") + && let Ok(Some(prefix)) = dict.get::<_, u32>(&"prefix") + { + if let Ok(address) = address.parse() { + address_data.push(IpAddress::new(address, prefix as u8)); + } + } + } + }; + let gateway = take_str_ref(settings, "gateway").and_then(|gateway| gateway.parse().ok()); + let dns_search = take_str_vec(settings, "dns-search"); + let mut route_data = Vec::new(); + if let Some(value) = settings.get("route-data") + && let Ok(array) = TryInto::<&Array>::try_into(value) + { + for entry in array.iter() { + if let Value::Dict(dict) = entry + && let Ok(Some(dest)) = dict.get::<_, String>(&"dest") + && let Ok(Some(prefix)) = dict.get(&"prefix") + { + let mut route = Route::new(dest, prefix); + if let Ok(Some(next_hop)) = dict.get::<_, String>(&"next_hop") { + route = route.next_hop(next_hop); + } + if let Ok(Some(metric)) = dict.get(&"metric") { + route = route.metric(metric) + } + route_data.push(route); + } + } + }; + let never_default = take_bool(settings, "never-default").unwrap_or(false); + let ignore_auto_dns = take_bool(settings, "ignore-auto-dns").unwrap_or(false); + IpSettings { + method, + address_data, + gateway, + dns_search, + route_data, + never_default, + ignore_auto_dns, + } +} + async fn fetch_one_full( conn: &Connection, path: OwnedObjectPath, diff --git a/nmrs/tests/integration_test.rs b/nmrs/tests/integration_test.rs index 6feb3542..d9d79b7f 100644 --- a/nmrs/tests/integration_test.rs +++ b/nmrs/tests/integration_test.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; use std::future::Future; +use std::net::Ipv4Addr; use std::panic::{AssertUnwindSafe, resume_unwind}; use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; @@ -402,6 +403,13 @@ async fn networkmanager_profile_crud_and_settings_events() { assert_eq!(profile.id, id); assert_eq!(profile.connection_type, "wireguard"); assert!(!profile.autoconnect); + match profile.ipv4 { + Some(ipv4) => { + assert_eq!(ipv4.address_data[0].address, Ipv4Addr::new(10, 203, 0, 2)); + assert_eq!(ipv4.address_data[0].prefix, 24); + } + None => panic!("expected an ipv4 section") + } match profile.summary { SettingsSummary::WireGuard { mtu,