Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 19 additions & 7 deletions nmrs/src/api/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
/// Assigned IPv6 address with CIDR notation (only present when connected)
pub ip6_address: Option<String>,
}

/// Detailed information about a Wi-Fi network.
Expand Down Expand Up @@ -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<String>,
/// Assigned IPv6 address with CIDR notation (only present when connected)
pub ip6_address: Option<String>,
}

/// Represents a network device managed by NetworkManager.
Expand Down Expand Up @@ -430,6 +438,10 @@ pub struct Device {
pub managed: Option<bool>,
/// Kernel driver name
pub driver: Option<String>,
/// Assigned IPv4 address with CIDR notation (only present when connected)
pub ip4_address: Option<String>,
/// Assigned IPv6 address with CIDR notation (only present when connected)
pub ip6_address: Option<String>,
// Link speed in Mb/s (wired devices)
// pub speed: Option<u32>,
}
Expand Down Expand Up @@ -1604,19 +1616,17 @@ 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
/// # use nmrs::{VpnConnectionInfo, VpnType, DeviceState};
/// # // 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]
Expand All @@ -1634,7 +1644,7 @@ pub struct VpnConnectionInfo {
pub gateway: Option<String>,
/// Assigned IPv4 address with CIDR notation.
pub ip4_address: Option<String>,
/// IPv6 address (currently always `None` - IPv6 parsing not yet implemented).
/// Assigned IPv6 address with CIDR notation.
pub ip6_address: Option<String>,
/// DNS servers configured for this VPN.
pub dns_servers: Vec<String>,
Expand Down Expand Up @@ -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());
Expand Down
15 changes: 15 additions & 0 deletions nmrs/src/core/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -74,6 +75,18 @@ pub(crate) async fn list_devices(conn: &Connection) -> Result<Vec<Device>> {
}
};

// 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 {
Expand All @@ -94,6 +107,8 @@ pub(crate) async fn list_devices(conn: &Connection) -> Result<Vec<Device>> {
state,
managed,
driver,
ip4_address,
ip6_address,
// speed,
});
}
Expand Down
21 changes: 20 additions & 1 deletion nmrs/src/core/scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -78,6 +81,8 @@ pub(crate) async fn list_networks(conn: &Connection) -> Result<Vec<Network>> {
secured,
is_psk,
is_eap,
ip4_address: None,
ip6_address: None,
};

Ok(Some((ssid, frequency, network)))
Expand Down Expand Up @@ -156,6 +161,18 @@ pub(crate) async fn current_network(conn: &Connection) -> Result<Option<Network>

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(),
Expand All @@ -165,6 +182,8 @@ pub(crate) async fn current_network(conn: &Connection) -> Result<Option<Network>
secured,
is_psk,
is_eap,
ip4_address,
ip6_address,
}));
}

Expand Down
29 changes: 27 additions & 2 deletions nmrs/src/core/vpn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -762,8 +762,33 @@ pub(crate) async fn get_vpn_info(conn: &Connection, name: &str) -> Result<VpnCon
(None, vec![])
};

// IPv6 config parsing not implemented
let ip6_address = None;
// IPv6 config
let ip6_path: OwnedObjectPath = ac_proxy.get_property("Ip6Config").await?;
let ip6_address = if ip6_path.as_str() != "/" {
let ip6_proxy =
nm_proxy(conn, ip6_path, "org.freedesktop.NetworkManager.IP6Config").await?;

if let Ok(addr_array) = ip6_proxy
.get_property::<Vec<HashMap<String, zvariant::Value>>>("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(),
Expand Down
10 changes: 10 additions & 0 deletions nmrs/src/dbus/active_connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ pub trait NMActiveConnection {
#[zbus(property)]
fn devices(&self) -> Result<Vec<OwnedObjectPath>>;

/// Path to the IPv4 configuration object.
/// Returns "/" if no IPv4 configuration is available.
#[zbus(property)]
fn ip4_config(&self) -> Result<OwnedObjectPath>;

/// Path to the IPv6 configuration object.
/// Returns "/" if no IPv6 configuration is available.
#[zbus(property)]
fn ip6_config(&self) -> Result<OwnedObjectPath>;

/// Signal emitted when the connection activation state changes.
///
/// The method is named `activation_state_changed` to avoid conflicts with
Expand Down
6 changes: 6 additions & 0 deletions nmrs/src/dbus/device.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! NetworkManager Device proxy.

use zbus::{proxy, Result};
use zvariant::OwnedObjectPath;

/// Proxy for NetworkManager device interface.
///
Expand Down Expand Up @@ -58,6 +59,11 @@ pub trait NMDevice {
#[zbus(property, name = "PermHwAddress")]
fn perm_hw_address(&self) -> Result<String>;

/// Path to the active connection object for this device.
/// Returns "/" if the device is not connected.
#[zbus(property)]
fn active_connection(&self) -> Result<OwnedObjectPath>;

/// Signal emitted when device state changes.
///
/// The method is named `device_state_changed` to avoid conflicts with the
Expand Down
52 changes: 50 additions & 2 deletions nmrs/src/monitoring/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -32,6 +32,43 @@ pub(crate) async fn show_details(conn: &Connection, net: &Network) -> Result<Net
let target_ssid_outer = net.ssid.clone();
let target_strength = net.strength;

// Get IP addresses if connected
let (ip4_address, ip6_address) = if is_connected_outer {
// Find the WiFi device and get its active connection
let nm = NMProxy::new(conn).await?;
let devices = nm.get_devices().await?;

let mut ip_addrs = (None, None);
for dev_path in devices {
let dev = match async {
NMDeviceProxy::builder(conn)
.path(dev_path.clone())
.ok()?
.build()
.await
.ok()
}
.await
{
Some(d) => 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;
Expand Down Expand Up @@ -122,12 +159,23 @@ pub(crate) async fn show_details(conn: &Connection, net: &Network) -> Result<Net
bars,
security,
status,
ip4_address: None,
ip6_address: None,
}))
})
})
.await?;

results.into_iter().next().ok_or(ConnectionError::NotFound)
let mut info = results
.into_iter()
.next()
.ok_or(ConnectionError::NotFound)?;

// Set IP addresses
info.ip4_address = ip4_address;
info.ip6_address = ip6_address;

Ok(info)
}

/// Returns the SSID of the currently connected Wi-Fi network.
Expand Down
67 changes: 67 additions & 0 deletions nmrs/src/util/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

use log::{debug, warn};
use std::borrow::Cow;
use std::collections::HashMap;
use std::str;
use zbus::Connection;
use zvariant::OwnedObjectPath;
Expand Down Expand Up @@ -253,6 +254,72 @@ macro_rules! try_log {
};
}

/// Helper to extract IP address from AddressData property.
async fn extract_ip_address(
conn: &Connection,
config_path: OwnedObjectPath,
interface: &str,
) -> Option<String> {
let proxy = nm_proxy(conn, config_path, interface).await.ok()?;
let addr_array: Vec<HashMap<String, zvariant::Value>> =
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<String>
/// 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<String>, Option<String>) {
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::*;
Expand Down
Loading