use log::{debug, info, warn};
use std::collections::HashMap;
use zbus::Connection;
use zvariant::OwnedObjectPath;
use crate::Result;
use crate::api::models::{
ConnectionError, ConnectionOptions, DeviceState, TimeoutConfig, VpnConnection,
VpnConnectionInfo, VpnCredentials, VpnType,
};
use crate::builders::build_wireguard_connection;
use crate::core::state_wait::wait_for_connection_activation;
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};
pub(crate) async fn connect_vpn(
conn: &Connection,
creds: VpnCredentials,
timeout_config: Option<TimeoutConfig>,
) -> Result<()> {
validate_vpn_credentials(&creds)?;
debug!("Connecting to VPN: {}", creds.name);
let nm = NMProxy::new(conn).await?;
let saved =
crate::core::connection_settings::get_saved_connection_path(conn, &creds.name).await?;
let vpn_device_path = OwnedObjectPath::default();
let specific_object = OwnedObjectPath::default();
let active_conn = if let Some(saved_path) = saved {
debug!("Activating existent VPN connection");
nm.activate_connection(saved_path, vpn_device_path.clone(), specific_object.clone())
.await?
} else {
debug!("Creating new VPN connection");
let opts = ConnectionOptions {
autoconnect: false,
autoconnect_priority: None,
autoconnect_retries: None,
};
let settings = build_wireguard_connection(&creds, &opts)?;
let settings_api = settings_proxy(conn).await?;
debug!("Adding connection via Settings API");
let add_reply = settings_api
.call_method("AddConnection", &(settings,))
.await?;
let conn_path: OwnedObjectPath = add_reply.body().deserialize()?;
debug!("Connection added, activating VPN connection");
nm.activate_connection(conn_path, vpn_device_path, specific_object)
.await?
};
let timeout = timeout_config.map(|c| c.connection_timeout);
wait_for_connection_activation(conn, &active_conn, timeout).await?;
debug!("Connection reached Activated state, waiting briefly...");
match NMActiveConnectionProxy::builder(conn).path(active_conn.clone()) {
Ok(builder) => match builder.build().await {
Ok(active_conn_check) => {
let final_state = active_conn_check.state().await?;
let state = crate::api::models::ActiveConnectionState::from(final_state);
debug!("Connection state after delay: {:?}", state);
match state {
crate::api::models::ActiveConnectionState::Activated => {
info!("Successfully connected to VPN: {}", creds.name);
Ok(())
}
crate::api::models::ActiveConnectionState::Deactivated => {
warn!("Connection deactivated immediately after activation");
let reason = extract_connection_state_reason(conn, &active_conn).await;
Err(crate::api::models::ConnectionError::ActivationFailed(
reason,
))
}
_ => {
warn!("Connection in unexpected state: {:?}", state);
Err(crate::api::models::ConnectionError::Stuck(format!(
"connection in state {:?}",
state
)))
}
}
}
Err(e) => {
warn!("Failed to build active connection proxy after delay: {}", e);
let reason = extract_connection_state_reason(conn, &active_conn).await;
Err(crate::api::models::ConnectionError::ActivationFailed(
reason,
))
}
},
Err(e) => {
warn!(
"Failed to create active connection proxy builder after delay: {}",
e
);
let reason = extract_connection_state_reason(conn, &active_conn).await;
Err(crate::api::models::ConnectionError::ActivationFailed(
reason,
))
}
}
}
pub(crate) async fn disconnect_vpn(conn: &Connection, name: &str) -> Result<()> {
validate_connection_name(name)?;
debug!("Disconnecting VPN: {name}");
let nm = NMProxy::new(conn).await?;
let active_conns = match nm.active_connections().await {
Ok(conns) => conns,
Err(e) => {
debug!("Failed to get active connections: {}", e);
info!("Disconnected VPN: {name} (could not verify active state)");
return Ok(());
}
};
for ac_path in active_conns {
let ac_proxy = match nm_proxy(
conn,
ac_path.clone(),
"org.freedesktop.NetworkManager.Connection.Active",
)
.await
{
Ok(p) => p,
Err(e) => {
warn!(
"Failed to create proxy for active connection {}: {}",
ac_path, e
);
continue;
}
};
let conn_path: OwnedObjectPath = match ac_proxy.call_method("Connection", &()).await {
Ok(msg) => match msg.body().deserialize::<OwnedObjectPath>() {
Ok(path) => path,
Err(e) => {
warn!(
"Failed to deserialize connection path for {}: {}",
ac_path, e
);
continue;
}
},
Err(e) => {
warn!("Failed to get Connection property from {}: {}", ac_path, e);
continue;
}
};
let cproxy = match nm_proxy(
conn,
conn_path.clone(),
"org.freedesktop.NetworkManager.Settings.Connection",
)
.await
{
Ok(p) => p,
Err(e) => {
warn!(
"Failed to create proxy for connection settings {}: {}",
conn_path, e
);
continue;
}
};
let msg = match cproxy.call_method("GetSettings", &()).await {
Ok(msg) => msg,
Err(e) => {
warn!("Failed to get settings for connection {}: {}", conn_path, e);
continue;
}
};
let body = msg.body();
let settings_map: HashMap<String, HashMap<String, zvariant::Value>> =
match body.deserialize() {
Ok(map) => map,
Err(e) => {
warn!("Failed to deserialize settings for {}: {}", conn_path, e);
continue;
}
};
if let Some(conn_sec) = settings_map.get("connection") {
let id_match = conn_sec
.get("id")
.and_then(|v| match v {
zvariant::Value::Str(s) => Some(s.as_str() == name),
_ => None,
})
.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);
if id_match && is_wireguard {
debug!("Found active WireGuard 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),
}
return Ok(());
}
}
}
info!("Disconnected VPN: {name} (not active)");
Ok(())
}
pub(crate) async fn list_vpn_connections(conn: &Connection) -> Result<Vec<VpnConnection>> {
let nm = NMProxy::new(conn).await?;
let settings = nm_proxy(
conn,
"/org/freedesktop/NetworkManager/Settings",
"org.freedesktop.NetworkManager.Settings",
)
.await?;
let list_reply = settings
.call_method("ListConnections", &())
.await
.map_err(|e| ConnectionError::DbusOperation {
context: "failed to list saved connections".to_string(),
source: e,
})?;
let saved_conns: Vec<OwnedObjectPath> = list_reply.body().deserialize()?;
let active_conns = nm.active_connections().await?;
let mut active_wg_map: HashMap<String, (DeviceState, Option<String>)> = HashMap::new();
for ac_path in active_conns {
let ac_proxy = match nm_proxy(
conn,
ac_path.clone(),
"org.freedesktop.NetworkManager.Connection.Active",
)
.await
{
Ok(p) => p,
Err(e) => {
warn!(
"Failed to create proxy for active connection {}: {}",
ac_path, e
);
continue;
}
};
let conn_path: OwnedObjectPath = match ac_proxy.call_method("Connection", &()).await {
Ok(msg) => match msg.body().deserialize::<OwnedObjectPath>() {
Ok(p) => p,
Err(e) => {
warn!(
"Failed to deserialize connection path for {}: {}",
ac_path, e
);
continue;
}
},
Err(e) => {
warn!("Failed to get Connection property from {}: {}", ac_path, e);
continue;
}
};
let cproxy = match nm_proxy(
conn,
conn_path.clone(),
"org.freedesktop.NetworkManager.Settings.Connection",
)
.await
{
Ok(p) => p,
Err(e) => {
warn!(
"Failed to create proxy for connection settings {}: {}",
conn_path, e
);
continue;
}
};
let msg = match cproxy.call_method("GetSettings", &()).await {
Ok(m) => m,
Err(e) => {
warn!("Failed to get settings for connection {}: {}", conn_path, e);
continue;
}
};
let body = msg.body();
let settings_map: HashMap<String, HashMap<String, zvariant::Value>> =
match body.deserialize() {
Ok(m) => m,
Err(e) => {
warn!("Failed to deserialize settings for {}: {}", conn_path, e);
continue;
}
};
let conn_sec = match settings_map.get("connection") {
Some(s) => s,
None => continue,
};
let id = match conn_sec.get("id") {
Some(zvariant::Value::Str(s)) => s.as_str().to_string(),
_ => continue,
};
let conn_type = match conn_sec.get("type") {
Some(zvariant::Value::Str(s)) => s.as_str(),
_ => continue,
};
if conn_type != "wireguard" {
continue;
}
let state = if let Ok(state_val) = ac_proxy.get_property::<u32>("State").await {
DeviceState::from(state_val)
} else {
DeviceState::Other(0)
};
let interface = if let Ok(dev_paths) = ac_proxy
.get_property::<Vec<OwnedObjectPath>>("Devices")
.await
{
if let Some(dev_path) = dev_paths.first() {
match nm_proxy(
conn,
dev_path.clone(),
"org.freedesktop.NetworkManager.Device",
)
.await
{
Ok(dev_proxy) => match dev_proxy.get_property::<String>("Interface").await {
Ok(iface) => Some(iface),
Err(e) => {
debug!(
"Failed to get interface name for VPN device {}: {}",
dev_path, e
);
None
}
},
Err(e) => {
debug!("Failed to create device proxy for {}: {}", dev_path, e);
None
}
}
} else {
None
}
} else {
None
};
active_wg_map.insert(id, (state, interface));
}
let mut wg_conns = Vec::new();
for cpath in saved_conns {
let cproxy = match nm_proxy(
conn,
cpath.clone(),
"org.freedesktop.NetworkManager.Settings.Connection",
)
.await
{
Ok(p) => p,
Err(e) => {
warn!(
"Failed to create proxy for saved connection {}: {}",
cpath, e
);
continue;
}
};
let msg = match cproxy.call_method("GetSettings", &()).await {
Ok(m) => m,
Err(e) => {
warn!(
"Failed to get settings for saved connection {}: {}",
cpath, e
);
continue;
}
};
let body = msg.body();
let settings_map: HashMap<String, HashMap<String, zvariant::Value>> =
match body.deserialize() {
Ok(m) => m,
Err(e) => {
warn!(
"Failed to deserialize settings for saved connection {}: {}",
cpath, e
);
continue;
}
};
let conn_sec = match settings_map.get("connection") {
Some(s) => s,
None => continue,
};
let id = match conn_sec.get("id") {
Some(zvariant::Value::Str(s)) => s.as_str().to_string(),
_ => continue,
};
let conn_type = match conn_sec.get("type") {
Some(zvariant::Value::Str(s)) => s.as_str(),
_ => continue,
};
if conn_type != "wireguard" {
continue;
}
let (state, interface) = active_wg_map
.get(&id)
.cloned()
.unwrap_or((DeviceState::Other(0), None));
wg_conns.push(VpnConnection {
name: id,
vpn_type: VpnType::WireGuard,
interface,
state,
});
}
Ok(wg_conns)
}
pub(crate) async fn forget_vpn(conn: &Connection, name: &str) -> Result<()> {
validate_connection_name(name)?;
debug!("Starting forget operation for VPN: {name}");
match disconnect_vpn(conn, name).await {
Ok(_) => debug!("VPN disconnected before deletion"),
Err(e) => warn!(
"Failed to disconnect VPN before deletion (may already be disconnected): {}",
e
),
}
let settings = nm_proxy(
conn,
"/org/freedesktop/NetworkManager/Settings",
"org.freedesktop.NetworkManager.Settings",
)
.await?;
let list_reply = settings.call_method("ListConnections", &()).await?;
let conns: Vec<OwnedObjectPath> = list_reply.body().deserialize()?;
for cpath in conns {
let cproxy = match nm_proxy(
conn,
cpath.clone(),
"org.freedesktop.NetworkManager.Settings.Connection",
)
.await
{
Ok(p) => p,
Err(e) => {
warn!("Failed to create proxy for connection {}: {}", cpath, e);
continue;
}
};
let msg = match cproxy.call_method("GetSettings", &()).await {
Ok(msg) => msg,
Err(e) => {
warn!("Failed to get settings for connection {}: {}", cpath, e);
continue;
}
};
let body = msg.body();
let settings_map: HashMap<String, HashMap<String, zvariant::Value>> = body.deserialize()?;
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 type_ok = conn_sec
.get("type")
.and_then(|v| match v {
zvariant::Value::Str(s) => Some(s.as_str() == "wireguard"),
_ => None,
})
.unwrap_or(false);
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(());
}
}
}
debug!("No saved VPN connection found for '{name}'");
Ok(())
}
pub(crate) async fn get_vpn_info(conn: &Connection, name: &str) -> Result<VpnConnectionInfo> {
validate_connection_name(name)?;
let nm = NMProxy::new(conn).await?;
let active_conns = nm.active_connections().await?;
for ac_path in active_conns {
let ac_proxy = match nm_proxy(
conn,
ac_path.clone(),
"org.freedesktop.NetworkManager.Connection.Active",
)
.await
{
Ok(p) => p,
Err(e) => {
warn!(
"Failed to create proxy for active connection {}: {}",
ac_path, e
);
continue;
}
};
let conn_path: OwnedObjectPath = match ac_proxy.call_method("Connection", &()).await {
Ok(msg) => match msg.body().deserialize::<OwnedObjectPath>() {
Ok(p) => p,
Err(e) => {
warn!(
"Failed to deserialize connection path for {}: {}",
ac_path, e
);
continue;
}
},
Err(e) => {
warn!("Failed to get Connection property from {}: {}", ac_path, e);
continue;
}
};
let cproxy = match nm_proxy(
conn,
conn_path.clone(),
"org.freedesktop.NetworkManager.Settings.Connection",
)
.await
{
Ok(p) => p,
Err(e) => {
warn!(
"Failed to create proxy for connection settings {}: {}",
conn_path, e
);
continue;
}
};
let msg = match cproxy.call_method("GetSettings", &()).await {
Ok(m) => m,
Err(e) => {
warn!("Failed to get settings for connection {}: {}", conn_path, e);
continue;
}
};
let body = msg.body();
let settings_map: HashMap<String, HashMap<String, zvariant::Value>> =
match body.deserialize() {
Ok(m) => m,
Err(e) => {
warn!("Failed to deserialize settings for {}: {}", conn_path, e);
continue;
}
};
let conn_sec = match settings_map.get("connection") {
Some(s) => s,
None => continue,
};
let id = match conn_sec.get("id") {
Some(zvariant::Value::Str(s)) => s.as_str(),
_ => continue,
};
let conn_type = match conn_sec.get("type") {
Some(zvariant::Value::Str(s)) => s.as_str(),
_ => continue,
};
if conn_type != "wireguard" || id != name {
continue;
}
let vpn_type = VpnType::WireGuard;
let state_val: u32 = ac_proxy.get_property("State").await?;
let state = DeviceState::from(state_val);
let dev_paths: Vec<OwnedObjectPath> = ac_proxy.get_property("Devices").await?;
let interface = if let Some(dev_path) = dev_paths.first() {
let dev_proxy = nm_proxy(
conn,
dev_path.clone(),
"org.freedesktop.NetworkManager.Device",
)
.await?;
Some(dev_proxy.get_property::<String>("Interface").await?)
} else {
None
};
let gateway = 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
});
let ip4_path: OwnedObjectPath = ac_proxy.get_property("Ip4Config").await?;
let (ip4_address, dns_servers) = if ip4_path.as_str() != "/" {
let ip4_proxy =
nm_proxy(conn, ip4_path, "org.freedesktop.NetworkManager.IP4Config").await?;
let ip4_address = if let Ok(addr_array) = ip4_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
};
let dns_servers =
if let Ok(dns_array) = ip4_proxy.get_property::<Vec<u32>>("Nameservers").await {
dns_array
.iter()
.map(|ip| {
format!(
"{}.{}.{}.{}",
ip & 0xFF,
(ip >> 8) & 0xFF,
(ip >> 16) & 0xFF,
(ip >> 24) & 0xFF
)
})
.collect()
} else {
vec![]
};
(ip4_address, dns_servers)
} else {
(None, vec![])
};
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(),
vpn_type,
state,
interface,
gateway,
ip4_address,
ip6_address,
dns_servers,
});
}
Err(crate::api::models::ConnectionError::NoVpnConnection)
}