diff --git a/nmrs/src/api/models.rs b/nmrs/src/api/models.rs index 26ad0bcd..28cb842c 100644 --- a/nmrs/src/api/models.rs +++ b/nmrs/src/api/models.rs @@ -323,6 +323,10 @@ pub struct Network { pub is_psk: bool, /// Whether the network uses WPA-EAP (Enterprise) authentication pub is_eap: bool, + /// Assigned IPv4 address with CIDR notation (only present when connected) + pub ip4_address: Option, + /// Assigned IPv6 address with CIDR notation (only present when connected) + pub ip6_address: Option, } /// Detailed information about a Wi-Fi network. @@ -377,6 +381,10 @@ pub struct NetworkInfo { pub security: String, /// Connection status pub status: String, + /// Assigned IPv4 address with CIDR notation (only present when connected) + pub ip4_address: Option, + /// Assigned IPv6 address with CIDR notation (only present when connected) + pub ip6_address: Option, } /// Represents a network device managed by NetworkManager. @@ -430,6 +438,10 @@ pub struct Device { pub managed: Option, /// Kernel driver name pub driver: Option, + /// Assigned IPv4 address with CIDR notation (only present when connected) + pub ip4_address: Option, + /// Assigned IPv6 address with CIDR notation (only present when connected) + pub ip6_address: Option, // Link speed in Mb/s (wired devices) // pub speed: Option, } @@ -1604,11 +1616,6 @@ pub struct VpnConnection { /// Provides comprehensive information about an active VPN connection, /// including IP configuration and connection details. /// -/// # Limitations -/// -/// - `ip6_address`: IPv6 address parsing is not currently implemented and will -/// always return `None`. IPv4 addresses are fully supported. -/// /// # Example /// /// ```no_run @@ -1616,7 +1623,10 @@ pub struct VpnConnection { /// # // This struct is returned by the library, not constructed directly /// # let info: VpnConnectionInfo = todo!(); /// if let Some(ip) = &info.ip4_address { -/// println!("VPN IP: {}", ip); +/// println!("VPN IPv4: {}", ip); +/// } +/// if let Some(ip) = &info.ip6_address { +/// println!("VPN IPv6: {}", ip); /// } /// ``` #[non_exhaustive] @@ -1634,7 +1644,7 @@ pub struct VpnConnectionInfo { pub gateway: Option, /// Assigned IPv4 address with CIDR notation. pub ip4_address: Option, - /// IPv6 address (currently always `None` - IPv6 parsing not yet implemented). + /// Assigned IPv6 address with CIDR notation. pub ip6_address: Option, /// DNS servers configured for this VPN. pub dns_servers: Vec, @@ -2935,6 +2945,8 @@ mod tests { state: DeviceState::Activated, managed: Some(true), driver: Some("btusb".into()), + ip4_address: None, + ip6_address: None, }; assert!(bt_device.is_bluetooth()); diff --git a/nmrs/src/core/device.rs b/nmrs/src/core/device.rs index 98fa87a5..186ba179 100644 --- a/nmrs/src/core/device.rs +++ b/nmrs/src/core/device.rs @@ -12,6 +12,7 @@ use crate::core::bluetooth::populate_bluez_info; use crate::core::state_wait::wait_for_wifi_device_ready; use crate::dbus::{NMBluetoothProxy, NMDeviceProxy, NMProxy}; use crate::types::constants::device_type; +use crate::util::utils::get_ip_addresses_from_active_connection; use crate::Result; /// Lists all network devices managed by NetworkManager. @@ -74,6 +75,18 @@ pub(crate) async fn list_devices(conn: &Connection) -> Result> { } }; + // Get IP addresses from active connection + let (ip4_address, ip6_address) = + if let Ok(active_conn_path) = d_proxy.active_connection().await { + if active_conn_path.as_str() != "/" { + get_ip_addresses_from_active_connection(conn, &active_conn_path).await + } else { + (None, None) + } + } else { + (None, None) + }; + // Avoiding this breaking change for now // Get link speed for wired devices /* let speed = if raw_type == device_type::ETHERNET { @@ -94,6 +107,8 @@ pub(crate) async fn list_devices(conn: &Connection) -> Result> { state, managed, driver, + ip4_address, + ip6_address, // speed, }); } diff --git a/nmrs/src/core/scan.rs b/nmrs/src/core/scan.rs index b630dce3..0de2af4b 100644 --- a/nmrs/src/core/scan.rs +++ b/nmrs/src/core/scan.rs @@ -10,7 +10,10 @@ use crate::api::models::Network; use crate::dbus::{NMAccessPointProxy, NMDeviceProxy, NMProxy, NMWirelessProxy}; use crate::monitoring::info::current_ssid; use crate::types::constants::{device_type, security_flags}; -use crate::util::utils::{decode_ssid_or_empty, decode_ssid_or_hidden, for_each_access_point}; +use crate::util::utils::{ + decode_ssid_or_empty, decode_ssid_or_hidden, for_each_access_point, + get_ip_addresses_from_active_connection, +}; use crate::Result; /// Triggers a Wi-Fi scan on all wireless devices. @@ -78,6 +81,8 @@ pub(crate) async fn list_networks(conn: &Connection) -> Result> { secured, is_psk, is_eap, + ip4_address: None, + ip6_address: None, }; Ok(Some((ssid, frequency, network))) @@ -156,6 +161,18 @@ pub(crate) async fn current_network(conn: &Connection) -> Result let interface = dev.interface().await.unwrap_or_default(); + // Get IP addresses from active connection + let (ip4_address, ip6_address) = if let Ok(active_conn_path) = dev.active_connection().await + { + if active_conn_path.as_str() != "/" { + get_ip_addresses_from_active_connection(conn, &active_conn_path).await + } else { + (None, None) + } + } else { + (None, None) + }; + return Ok(Some(Network { device: interface, ssid: ssid.to_string(), @@ -165,6 +182,8 @@ pub(crate) async fn current_network(conn: &Connection) -> Result secured, is_psk, is_eap, + ip4_address, + ip6_address, })); } diff --git a/nmrs/src/core/vpn.rs b/nmrs/src/core/vpn.rs index 013937b7..a14661af 100644 --- a/nmrs/src/core/vpn.rs +++ b/nmrs/src/core/vpn.rs @@ -762,8 +762,33 @@ pub(crate) async fn get_vpn_info(conn: &Connection, name: &str) -> Result>>("AddressData") + .await + { + addr_array.first().and_then(|addr_map| { + let address = addr_map.get("address").and_then(|v| match v { + zvariant::Value::Str(s) => Some(s.as_str().to_string()), + _ => None, + })?; + let prefix = addr_map.get("prefix").and_then(|v| match v { + zvariant::Value::U32(p) => Some(p), + _ => None, + })?; + Some(format!("{}/{}", address, prefix)) + }) + } else { + None + } + } else { + None + }; return Ok(VpnConnectionInfo { name: id.to_string(), diff --git a/nmrs/src/dbus/active_connection.rs b/nmrs/src/dbus/active_connection.rs index 63eed9ac..d23f6f35 100644 --- a/nmrs/src/dbus/active_connection.rs +++ b/nmrs/src/dbus/active_connection.rs @@ -61,6 +61,16 @@ pub trait NMActiveConnection { #[zbus(property)] fn devices(&self) -> Result>; + /// Path to the IPv4 configuration object. + /// Returns "/" if no IPv4 configuration is available. + #[zbus(property)] + fn ip4_config(&self) -> Result; + + /// Path to the IPv6 configuration object. + /// Returns "/" if no IPv6 configuration is available. + #[zbus(property)] + fn ip6_config(&self) -> Result; + /// Signal emitted when the connection activation state changes. /// /// The method is named `activation_state_changed` to avoid conflicts with diff --git a/nmrs/src/dbus/device.rs b/nmrs/src/dbus/device.rs index 9608ae5a..08369464 100644 --- a/nmrs/src/dbus/device.rs +++ b/nmrs/src/dbus/device.rs @@ -1,6 +1,7 @@ //! NetworkManager Device proxy. use zbus::{proxy, Result}; +use zvariant::OwnedObjectPath; /// Proxy for NetworkManager device interface. /// @@ -58,6 +59,11 @@ pub trait NMDevice { #[zbus(property, name = "PermHwAddress")] fn perm_hw_address(&self) -> Result; + /// Path to the active connection object for this device. + /// Returns "/" if the device is not connected. + #[zbus(property)] + fn active_connection(&self) -> Result; + /// Signal emitted when device state changes. /// /// The method is named `device_state_changed` to avoid conflicts with the diff --git a/nmrs/src/monitoring/info.rs b/nmrs/src/monitoring/info.rs index 56bfc8f7..760a9080 100644 --- a/nmrs/src/monitoring/info.rs +++ b/nmrs/src/monitoring/info.rs @@ -12,7 +12,7 @@ use crate::try_log; use crate::types::constants::{device_type, rate, security_flags}; use crate::util::utils::{ bars_from_strength, channel_from_freq, decode_ssid_or_empty, for_each_access_point, - mode_to_string, strength_or_zero, + get_ip_addresses_from_active_connection, mode_to_string, strength_or_zero, }; use crate::Result; @@ -32,6 +32,43 @@ pub(crate) async fn show_details(conn: &Connection, net: &Network) -> Result d, + None => continue, + }; + + if dev.device_type().await.ok() == Some(device_type::WIFI) { + if let Ok(active_conn_path) = dev.active_connection().await { + if active_conn_path.as_str() != "/" { + ip_addrs = + get_ip_addresses_from_active_connection(conn, &active_conn_path).await; + break; + } + } + } + } + ip_addrs + } else { + (None, None) + }; + let results = for_each_access_point(conn, |ap| { let target_ssid = target_ssid_outer.clone(); let is_connected = is_connected_outer; @@ -122,12 +159,23 @@ pub(crate) async fn show_details(conn: &Connection, net: &Network) -> Result Option { + let proxy = nm_proxy(conn, config_path, interface).await.ok()?; + let addr_array: Vec> = + proxy.get_property("AddressData").await.ok()?; + + addr_array.first().and_then(|addr_map| { + let address = match addr_map.get("address")? { + zvariant::Value::Str(s) => s.as_str().to_string(), + _ => return None, + }; + let prefix = match addr_map.get("prefix")? { + zvariant::Value::U32(p) => *p, + _ => return None, + }; + Some(format!("{}/{}", address, prefix)) + }) +} + +/// Extracts IPv4 and IPv6 addresses from an active connection. +/// +/// Returns a tuple of (ipv4_address, ipv6_address) where each is an Option +/// in CIDR notation (e.g., "192.168.1.100/24" or "2001:db8::1/64"). +/// +/// Returns (None, None) if the connection has no IP configuration. +pub(crate) async fn get_ip_addresses_from_active_connection( + conn: &Connection, + active_conn_path: &OwnedObjectPath, +) -> (Option, Option) { + let ac_proxy = match async { + NMActiveConnectionProxy::builder(conn) + .path(active_conn_path.clone()) + .ok()? + .build() + .await + .ok() + } + .await + { + Some(proxy) => proxy, + None => return (None, None), + }; + + // Get IPv4 address + let ip4_address = match ac_proxy.ip4_config().await { + Ok(path) if path.as_str() != "/" => { + extract_ip_address(conn, path, "org.freedesktop.NetworkManager.IP4Config").await + } + _ => None, + }; + + // Get IPv6 address + let ip6_address = match ac_proxy.ip6_config().await { + Ok(path) if path.as_str() != "/" => { + extract_ip_address(conn, path, "org.freedesktop.NetworkManager.IP6Config").await + } + _ => None, + }; + + (ip4_address, ip6_address) +} + #[cfg(test)] mod tests { use super::*;