From 96dcbb99205245abdba45553b2f52e8fa296edba Mon Sep 17 00:00:00 2001 From: stoutes <31317041+stoutes@users.noreply.github.com> Date: Thu, 26 Mar 2026 16:28:35 -0500 Subject: [PATCH 1/8] feat(models): add OpenVpn variant to VpnType enum --- nmrs/src/api/models/vpn.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/nmrs/src/api/models/vpn.rs b/nmrs/src/api/models/vpn.rs index 5f719e3c..9ba44504 100644 --- a/nmrs/src/api/models/vpn.rs +++ b/nmrs/src/api/models/vpn.rs @@ -13,6 +13,7 @@ use super::device::DeviceState; pub enum VpnType { /// WireGuard - modern, high-performance VPN protocol. WireGuard, + OpenVpn, } /// Common metadata shared by VPN connection configurations. From 8d0913a56baf35aa760b02e9571ae84f22c7807f Mon Sep 17 00:00:00 2001 From: stoutes <31317041+stoutes@users.noreply.github.com> Date: Thu, 26 Mar 2026 16:30:09 -0500 Subject: [PATCH 2/8] refactor(vpn): dispatch on vpn_type in connect_vpn, disconnect_vpn, forget_vpn --- nmrs/src/core/vpn.rs | 107 +++++++++++++++++++++++++++---------------- 1 file changed, 67 insertions(+), 40 deletions(-) diff --git a/nmrs/src/core/vpn.rs b/nmrs/src/core/vpn.rs index 95c36a29..fcea8b7a 100644 --- a/nmrs/src/core/vpn.rs +++ b/nmrs/src/core/vpn.rs @@ -27,6 +27,37 @@ use crate::dbus::{NMActiveConnectionProxy, NMProxy}; use crate::util::utils::{extract_connection_state_reason, nm_proxy, settings_proxy}; use crate::util::validation::{validate_connection_name, validate_vpn_credentials}; +/// Detects the VPN type from a raw NM connection settings map. +/// WireGuard: connection.type == "wireguard" +/// OpenVPN: connection.type == "vpn" + vpn.service-type == "org.freedesktop.NetworkManager.openvpn" +fn detect_vpn_type( + settings: &HashMap>>, +) -> Option { + let conn = settings.get("connection")?; + let conn_type = match conn.get("type")? { + zvariant::Value::Str(s) => s.as_str(), + _ => return None, + }; + + match conn_type { + "wireguard" => Some(VpnType::WireGuard), + "vpn" => { + let vpn = settings.get("vpn")?; + let service = match vpn.get("service-type")? { + zvariant::Value::Str(s) => s.as_str(), + _ => return None, + }; + if service == "org.freedesktop.NetworkManager.openvpn" { + Some(VpnType::OpenVpn) + } else { + None + } + } + _ => None, + } +} + + /// Connects to a WireGuard connection. /// /// This function checks for an existing saved connection by name. @@ -69,7 +100,10 @@ pub(crate) async fn connect_vpn( autoconnect_retries: None, }; - let settings = build_wireguard_connection(&creds, &opts)?; + let settings = match creds.vpn_type { + VpnType::WireGuard => build_wireguard_connection(&creds, &opts)?, + _ => return Err(ConnectionError::VpnFailed("unsupported VPN type".into())), + }; let settings_api = settings_proxy(conn).await?; @@ -237,16 +271,10 @@ pub(crate) async fn disconnect_vpn(conn: &Connection, name: &str) -> Result<()> }) .unwrap_or(false); - let is_wireguard = conn_sec - .get("type") - .and_then(|v| match v { - zvariant::Value::Str(s) => Some(s.as_str() == "wireguard"), - _ => None, - }) - .unwrap_or(false); + let is_vpn = detect_vpn_type(&settings_map).is_some(); - if id_match && is_wireguard { - debug!("Found active WireGuard connection, deactivating: {name}"); + if id_match && is_vpn { + debug!("Found active VPN connection, deactivating: {name}"); match nm.deactivate_connection(ac_path.clone()).await { Ok(_) => info!("Successfully disconnected VPN: {name}"), Err(e) => warn!("Failed to deactivate connection {}: {}", ac_path, e), @@ -503,7 +531,6 @@ pub(crate) async fn list_vpn_connections(conn: &Connection) -> Result Result<()> { - // Validate connection name validate_connection_name(name)?; debug!("Starting forget operation for VPN: {name}"); @@ -521,7 +548,7 @@ pub(crate) async fn forget_vpn(conn: &Connection, name: &str) -> Result<()> { "/org/freedesktop/NetworkManager/Settings", "org.freedesktop.NetworkManager.Settings", ) - .await?; + .await?; let list_reply = settings.call_method("ListConnections", &()).await?; let conns: Vec = list_reply.body().deserialize()?; @@ -532,7 +559,7 @@ pub(crate) async fn forget_vpn(conn: &Connection, name: &str) -> Result<()> { cpath.clone(), "org.freedesktop.NetworkManager.Settings.Connection", ) - .await + .await { Ok(p) => p, Err(e) => { @@ -550,36 +577,36 @@ pub(crate) async fn forget_vpn(conn: &Connection, name: &str) -> Result<()> { }; let body = msg.body(); - let settings_map: HashMap> = body.deserialize()?; + let settings_map: HashMap> = + match body.deserialize() { + Ok(map) => map, + Err(e) => { + warn!("Failed to deserialize settings for {}: {}", cpath, e); + continue; + } + }; - if let Some(conn_sec) = settings_map.get("connection") { - let id_ok = conn_sec - .get("id") - .and_then(|v| match v { - zvariant::Value::Str(s) => Some(s.as_str() == name), - _ => None, - }) - .unwrap_or(false); + let id_ok = settings_map + .get("connection") + .and_then(|c| c.get("id")) + .and_then(|v| match v { + zvariant::Value::Str(s) => Some(s.as_str() == name), + _ => None, + }) + .unwrap_or(false); - let type_ok = conn_sec - .get("type") - .and_then(|v| match v { - zvariant::Value::Str(s) => Some(s.as_str() == "wireguard"), - _ => None, - }) - .unwrap_or(false); + let is_vpn = detect_vpn_type(&settings_map).is_some(); - if id_ok && type_ok { - debug!("Found WireGuard connection, deleting: {name}"); - cproxy.call_method("Delete", &()).await.map_err(|e| { - ConnectionError::DbusOperation { - context: format!("failed to delete VPN connection '{}'", name), - source: e, - } - })?; - info!("Successfully deleted VPN connection: {name}"); - return Ok(()); - } + if id_ok && is_vpn { + debug!("Found VPN connection, deleting: {name}"); + cproxy.call_method("Delete", &()).await.map_err(|e| { + ConnectionError::DbusOperation { + context: format!("failed to delete VPN connection '{}'", name), + source: e, + } + })?; + info!("Successfully deleted VPN connection: {name}"); + return Ok(()); } } From ba22a9574135b88dd60450b52e16d9735451e1d0 Mon Sep 17 00:00:00 2001 From: stoutes <31317041+stoutes@users.noreply.github.com> Date: Thu, 26 Mar 2026 16:30:50 -0500 Subject: [PATCH 3/8] refactor(api): simplify connect_vpn to delegate fully to core::vpn --- nmrs/src/api/network_manager.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nmrs/src/api/network_manager.rs b/nmrs/src/api/network_manager.rs index caae9ac9..48a27812 100644 --- a/nmrs/src/api/network_manager.rs +++ b/nmrs/src/api/network_manager.rs @@ -3,7 +3,7 @@ use tokio::sync::watch; use zbus::Connection; -use crate::Result; +use crate::{Result}; use crate::api::models::{Device, Network, NetworkInfo, WifiSecurity}; use crate::core::bluetooth::connect_bluetooth; use crate::core::connection::{ From f4cac427cfb15268ec47eae14bc69c9a37c09324 Mon Sep 17 00:00:00 2001 From: stoutes <31317041+stoutes@users.noreply.github.com> Date: Thu, 26 Mar 2026 16:48:42 -0500 Subject: [PATCH 4/8] refactor(vpn): expand list_vpn_connections and get_vpn_info to detect openvpn connections --- nmrs/src/core/vpn.rs | 68 ++++++++++++++++++++------------------------ 1 file changed, 31 insertions(+), 37 deletions(-) diff --git a/nmrs/src/core/vpn.rs b/nmrs/src/core/vpn.rs index fcea8b7a..c2781574 100644 --- a/nmrs/src/core/vpn.rs +++ b/nmrs/src/core/vpn.rs @@ -396,12 +396,7 @@ pub(crate) async fn list_vpn_connections(conn: &Connection) -> Result continue, }; - let conn_type = match conn_sec.get("type") { - Some(zvariant::Value::Str(s)) => s.as_str(), - _ => continue, - }; - - if conn_type != "wireguard" { + if detect_vpn_type(&settings_map).is_none() { continue; } @@ -502,14 +497,9 @@ pub(crate) async fn list_vpn_connections(conn: &Connection) -> Result continue, }; - let conn_type = match conn_sec.get("type") { - Some(zvariant::Value::Str(s)) => s.as_str(), - _ => continue, - }; - - if conn_type != "wireguard" { + let Some(vpn_type) = detect_vpn_type(&settings_map) else { continue; - } + }; let (state, interface) = active_wg_map .get(&id) @@ -518,7 +508,7 @@ pub(crate) async fn list_vpn_connections(conn: &Connection) -> Result Result continue, }; - let conn_type = match conn_sec.get("type") { - Some(zvariant::Value::Str(s)) => s.as_str(), - _ => continue, + let Some(vpn_type) = detect_vpn_type(&settings_map) else { + continue; }; - if conn_type != "wireguard" || id != name { + if id != name { continue; } - // WireGuard type is known by connection.type - let vpn_type = VpnType::WireGuard; - // ActiveConnection state let state_val: u32 = ac_proxy.get_property("State").await?; let state = DeviceState::from(state_val); @@ -736,23 +722,31 @@ pub(crate) async fn get_vpn_info(conn: &Connection, name: &str) -> Result Some(s.as_str().to_string()), - _ => None, - }) - .and_then(|peers| { - // peers: "pubkey endpoint=host:port allowed-ips=... , pubkey2 ..." - let first = peers.split(',').next()?.trim().to_string(); - for tok in first.split_whitespace() { - if let Some(rest) = tok.strip_prefix("endpoint=") { - return Some(rest.to_string()); + let gateway = match vpn_type { + VpnType::WireGuard => settings_map + .get("wireguard") + .and_then(|wg_sec| wg_sec.get("peers")) + .and_then(|v| match v { + zvariant::Value::Str(s) => Some(s.as_str().to_string()), + _ => None, + }) + .and_then(|peers| { + let first = peers.split(',').next()?.trim().to_string(); + for tok in first.split_whitespace() { + if let Some(rest) = tok.strip_prefix("endpoint=") { + return Some(rest.to_string()); + } } - } - None - }); + None + }), + VpnType::OpenVpn => settings_map + .get("vpn") + .and_then(|vpn_sec| vpn_sec.get("data")) + .and_then(|v| match v { + zvariant::Value::Str(s) => Some(s.as_str().to_string()), + _ => None, + }), + }; // IPv4 config let ip4_path: OwnedObjectPath = ac_proxy.get_property("Ip4Config").await?; From 3849419c596e2e24afba62bf3e14641d0e177ed6 Mon Sep 17 00:00:00 2001 From: stoutes <31317041+stoutes@users.noreply.github.com> Date: Thu, 26 Mar 2026 16:56:28 -0500 Subject: [PATCH 5/8] style(vpn): apply cargo fmt --- nmrs/src/api/network_manager.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nmrs/src/api/network_manager.rs b/nmrs/src/api/network_manager.rs index 48a27812..caae9ac9 100644 --- a/nmrs/src/api/network_manager.rs +++ b/nmrs/src/api/network_manager.rs @@ -3,7 +3,7 @@ use tokio::sync::watch; use zbus::Connection; -use crate::{Result}; +use crate::Result; use crate::api::models::{Device, Network, NetworkInfo, WifiSecurity}; use crate::core::bluetooth::connect_bluetooth; use crate::core::connection::{ From 5585d45a68810a8c5f8efd201f86d27fffbf99a2 Mon Sep 17 00:00:00 2001 From: stoutes <31317041+stoutes@users.noreply.github.com> Date: Thu, 26 Mar 2026 17:03:26 -0500 Subject: [PATCH 6/8] chore(api): remove unused ConnectionOptions import --- nmrs/src/core/vpn.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/nmrs/src/core/vpn.rs b/nmrs/src/core/vpn.rs index c2781574..63c1a876 100644 --- a/nmrs/src/core/vpn.rs +++ b/nmrs/src/core/vpn.rs @@ -57,7 +57,6 @@ fn detect_vpn_type( } } - /// Connects to a WireGuard connection. /// /// This function checks for an existing saved connection by name. @@ -538,7 +537,7 @@ pub(crate) async fn forget_vpn(conn: &Connection, name: &str) -> Result<()> { "/org/freedesktop/NetworkManager/Settings", "org.freedesktop.NetworkManager.Settings", ) - .await?; + .await?; let list_reply = settings.call_method("ListConnections", &()).await?; let conns: Vec = list_reply.body().deserialize()?; @@ -549,7 +548,7 @@ pub(crate) async fn forget_vpn(conn: &Connection, name: &str) -> Result<()> { cpath.clone(), "org.freedesktop.NetworkManager.Settings.Connection", ) - .await + .await { Ok(p) => p, Err(e) => { From f33f01be48bb667cc86f8535801a37d16562f55d Mon Sep 17 00:00:00 2001 From: stoutes <31317041+stoutes@users.noreply.github.com> Date: Sat, 28 Mar 2026 16:11:37 -0500 Subject: [PATCH 7/8] refactor(vpn): regular comment on detect_vpn_type, explicit match in connect_vpn, FIXME in get_vpn_info, update wireguard peers comment, reverted back to body.deserialize()? in forget_vpn, --- nmrs/src/core/vpn.rs | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/nmrs/src/core/vpn.rs b/nmrs/src/core/vpn.rs index 63c1a876..2b9adb8c 100644 --- a/nmrs/src/core/vpn.rs +++ b/nmrs/src/core/vpn.rs @@ -27,9 +27,9 @@ use crate::dbus::{NMActiveConnectionProxy, NMProxy}; use crate::util::utils::{extract_connection_state_reason, nm_proxy, settings_proxy}; use crate::util::validation::{validate_connection_name, validate_vpn_credentials}; -/// Detects the VPN type from a raw NM connection settings map. -/// WireGuard: connection.type == "wireguard" -/// OpenVPN: connection.type == "vpn" + vpn.service-type == "org.freedesktop.NetworkManager.openvpn" +// Detects the VPN type from a raw NM connection settings map. +// WireGuard: connection.type == "wireguard" +// OpenVPN: connection.type == "vpn" + vpn.service-type == "org.freedesktop.NetworkManager.openvpn" fn detect_vpn_type( settings: &HashMap>>, ) -> Option { @@ -101,7 +101,9 @@ pub(crate) async fn connect_vpn( let settings = match creds.vpn_type { VpnType::WireGuard => build_wireguard_connection(&creds, &opts)?, - _ => return Err(ConnectionError::VpnFailed("unsupported VPN type".into())), + VpnType::OpenVpn => return Err(ConnectionError::VpnFailed( + "OpenVPN connect not yet implemented".into() + )), }; let settings_api = settings_proxy(conn).await?; @@ -567,13 +569,7 @@ pub(crate) async fn forget_vpn(conn: &Connection, name: &str) -> Result<()> { let body = msg.body(); let settings_map: HashMap> = - match body.deserialize() { - Ok(map) => map, - Err(e) => { - warn!("Failed to deserialize settings for {}: {}", cpath, e); - continue; - } - }; + body.deserialize()?; let id_ok = settings_map .get("connection") @@ -719,8 +715,10 @@ pub(crate) async fn get_vpn_info(conn: &Connection, name: &str) -> Result settings_map .get("wireguard") @@ -738,13 +736,13 @@ pub(crate) async fn get_vpn_info(conn: &Connection, name: &str) -> Result settings_map - .get("vpn") - .and_then(|vpn_sec| vpn_sec.get("data")) - .and_then(|v| match v { - zvariant::Value::Str(s) => Some(s.as_str().to_string()), - _ => None, - }), + VpnType::OpenVpn => { + // FIXME : vpn.data is a Dict in NM, not a plain string. + // Need to deserialize as HashMap and look up "remote". + // Cannot test without a real NM OpenVPN profile. + None + + } }; // IPv4 config From 26f09857cdc8ffd632088a777acc33c6b69bc3d7 Mon Sep 17 00:00:00 2001 From: stoutes <31317041+stoutes@users.noreply.github.com> Date: Sat, 28 Mar 2026 16:13:45 -0500 Subject: [PATCH 8/8] refactor(models): removed gateway method from the VpnConfig trait, VpnConfig for WireGuardConfig, and VpnConfig for VpnCredentials. Also removed OpenVpn from the VpnType enum. --- nmrs/src/api/models/vpn.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/nmrs/src/api/models/vpn.rs b/nmrs/src/api/models/vpn.rs index 9ba44504..5f719e3c 100644 --- a/nmrs/src/api/models/vpn.rs +++ b/nmrs/src/api/models/vpn.rs @@ -13,7 +13,6 @@ use super::device::DeviceState; pub enum VpnType { /// WireGuard - modern, high-performance VPN protocol. WireGuard, - OpenVpn, } /// Common metadata shared by VPN connection configurations.