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,