Skip to main content

tauri_plugin_network_manager/
desktop.rs

1use std::collections::HashMap;
2use std::sync::mpsc;
3use tauri::{plugin::PluginApi, AppHandle, Runtime};
4use uuid::Uuid;
5use zbus::names::InterfaceName;
6use zbus::zvariant::Value;
7
8use crate::error::Result;
9use crate::models::*;
10use crate::nm_helpers::NetworkManagerHelpers;
11
12impl<R: Runtime> VSKNetworkManager<'static, R> {
13    fn vpn_type_from_service_type(service_type: &str) -> VpnType {
14        match service_type {
15            "org.freedesktop.NetworkManager.openvpn" => VpnType::OpenVpn,
16            "org.freedesktop.NetworkManager.wireguard" => VpnType::WireGuard,
17            "org.freedesktop.NetworkManager.l2tp" => VpnType::L2tp,
18            "org.freedesktop.NetworkManager.pptp" => VpnType::Pptp,
19            "org.freedesktop.NetworkManager.sstp" => VpnType::Sstp,
20            "org.freedesktop.NetworkManager.strongswan" => VpnType::Ikev2,
21            "org.freedesktop.NetworkManager.fortisslvpn" => VpnType::Fortisslvpn,
22            "org.freedesktop.NetworkManager.openconnect" => VpnType::OpenConnect,
23            _ => VpnType::Generic,
24        }
25    }
26
27    fn service_type_from_vpn_type(vpn_type: &VpnType) -> &'static str {
28        match vpn_type {
29            VpnType::OpenVpn => "org.freedesktop.NetworkManager.openvpn",
30            VpnType::WireGuard => "org.freedesktop.NetworkManager.wireguard",
31            VpnType::L2tp => "org.freedesktop.NetworkManager.l2tp",
32            VpnType::Pptp => "org.freedesktop.NetworkManager.pptp",
33            VpnType::Sstp => "org.freedesktop.NetworkManager.sstp",
34            VpnType::Ikev2 => "org.freedesktop.NetworkManager.strongswan",
35            VpnType::Fortisslvpn => "org.freedesktop.NetworkManager.fortisslvpn",
36            VpnType::OpenConnect => "org.freedesktop.NetworkManager.openconnect",
37            VpnType::Generic => "org.freedesktop.NetworkManager.vpnc",
38        }
39    }
40
41    fn vpn_state_from_active_state(state: u32) -> VpnConnectionState {
42        match state {
43            1 => VpnConnectionState::Connecting,
44            2 => VpnConnectionState::Connected,
45            3 => VpnConnectionState::Disconnecting,
46            4 => VpnConnectionState::Disconnected,
47            _ => VpnConnectionState::Unknown,
48        }
49    }
50
51    fn extract_string_from_dict(
52        dict: &HashMap<String, zbus::zvariant::OwnedValue>,
53        key: &str,
54    ) -> Option<String> {
55        let value = dict.get(key)?;
56        let v: &zbus::zvariant::Value<'_> = value;
57        v.downcast_ref::<String>().ok()
58    }
59
60    fn extract_bool_from_dict(
61        dict: &HashMap<String, zbus::zvariant::OwnedValue>,
62        key: &str,
63    ) -> Option<bool> {
64        let value = dict.get(key)?;
65        let v: &zbus::zvariant::Value<'_> = value;
66        v.downcast_ref::<bool>().ok()
67    }
68
69    fn string_map_from_section(
70        settings: &HashMap<String, HashMap<String, zbus::zvariant::OwnedValue>>,
71        section_name: &str,
72    ) -> HashMap<String, String> {
73        let mut out = HashMap::new();
74        let section = match settings.get(section_name) {
75            Some(v) => v,
76            None => return out,
77        };
78
79        for (k, v) in section {
80            let val: &zbus::zvariant::Value<'_> = v;
81            if let Ok(s) = val.downcast_ref::<String>() {
82                out.insert(k.clone(), s);
83            }
84        }
85        out
86    }
87
88    fn list_connection_paths(&self) -> Result<Vec<zbus::zvariant::OwnedObjectPath>> {
89        let settings_proxy = zbus::blocking::Proxy::new(
90            &self.connection,
91            "org.freedesktop.NetworkManager",
92            "/org/freedesktop/NetworkManager/Settings",
93            "org.freedesktop.NetworkManager.Settings",
94        )?;
95
96        let connections: Vec<zbus::zvariant::OwnedObjectPath> =
97            settings_proxy.call("ListConnections", &())?;
98        Ok(connections)
99    }
100
101    fn get_connection_settings(
102        &self,
103        conn_path: &zbus::zvariant::OwnedObjectPath,
104    ) -> Result<HashMap<String, HashMap<String, zbus::zvariant::OwnedValue>>> {
105        let conn_proxy = zbus::blocking::Proxy::new(
106            &self.connection,
107            "org.freedesktop.NetworkManager",
108            conn_path.as_str(),
109            "org.freedesktop.NetworkManager.Settings.Connection",
110        )?;
111
112        let settings: HashMap<String, HashMap<String, zbus::zvariant::OwnedValue>> =
113            conn_proxy.call("GetSettings", &())?;
114        Ok(settings)
115    }
116
117    fn find_connection_path_by_uuid(
118        &self,
119        uuid: &str,
120    ) -> Result<zbus::zvariant::OwnedObjectPath> {
121        let connections = self.list_connection_paths()?;
122
123        for conn_path in connections {
124            let settings = self.get_connection_settings(&conn_path)?;
125            let dict = match settings.get("connection") {
126                Some(v) => v,
127                None => continue,
128            };
129
130            if let Some(conn_uuid) = Self::extract_string_from_dict(&dict, "uuid") {
131                if conn_uuid == uuid {
132                    return Ok(conn_path);
133                }
134            }
135        }
136
137        Err(crate::error::NetworkError::VpnProfileNotFound(uuid.to_string()))
138    }
139
140    fn vpn_profile_from_settings(
141        &self,
142        settings: &HashMap<String, HashMap<String, zbus::zvariant::OwnedValue>>,
143    ) -> Option<VpnProfile> {
144        let connection_dict = settings.get("connection")?;
145
146        let conn_type = Self::extract_string_from_dict(connection_dict, "type")?;
147        if conn_type != "vpn" {
148            return None;
149        }
150
151        let uuid = Self::extract_string_from_dict(connection_dict, "uuid")?;
152        let id = Self::extract_string_from_dict(connection_dict, "id")
153            .unwrap_or_else(|| uuid.clone());
154        let interface_name = Self::extract_string_from_dict(connection_dict, "interface-name");
155        let autoconnect = Self::extract_bool_from_dict(connection_dict, "autoconnect")
156            .unwrap_or(false);
157
158        let vpn_settings = Self::string_map_from_section(settings, "vpn");
159        let service_type = vpn_settings
160            .get("service-type")
161            .map(|s| s.as_str())
162            .unwrap_or("unknown");
163
164        Some(VpnProfile {
165            id,
166            uuid,
167            vpn_type: Self::vpn_type_from_service_type(service_type),
168            interface_name,
169            autoconnect,
170            editable: true,
171            last_error: None,
172        })
173    }
174
175    /// Get WiFi icon based on signal strength
176    fn get_wifi_icon(strength: u8) -> String {
177        match strength {
178            0..=25 => "network-wireless-signal-weak-symbolic".to_string(),
179            26..=50 => "network-wireless-signal-ok-symbolic".to_string(),
180            51..=75 => "network-wireless-signal-good-symbolic".to_string(),
181            76..=100 => "network-wireless-signal-excellent-symbolic".to_string(),
182            _ => "network-wireless-signal-none-symbolic".to_string(),
183        }
184    }
185
186    fn get_wired_icon(is_connected: bool) -> String {
187        if is_connected {
188            "network-wired-symbolic".to_string()
189        } else {
190            "network-offline-symbolic".to_string()
191        }
192    }
193
194    /// Create a new VSKNetworkManager instance
195    pub fn new(app: AppHandle<R>) -> Result<Self> {
196        let connection = zbus::blocking::Connection::system()?;
197        let proxy = zbus::blocking::fdo::PropertiesProxy::builder(&connection)
198            .destination("org.freedesktop.NetworkManager")?
199            .path("/org/freedesktop/NetworkManager")?
200            .build()?;
201
202        Ok(Self {
203            connection,
204            proxy,
205            app,
206        })
207    }
208
209    pub fn get_current_network_state(&self) -> Result<NetworkInfo> {
210        // Get active connections
211        let active_connections_variant = self.proxy.get(
212            InterfaceName::from_static_str_unchecked("org.freedesktop.NetworkManager"),
213            "ActiveConnections",
214        )?;
215
216        // If no active connections, return default
217        match active_connections_variant.downcast_ref() {
218            Ok(Value::Array(arr)) if !arr.is_empty() => {
219                // Get the first active connection path
220                match arr[0] {
221                    zbus::zvariant::Value::ObjectPath(ref path) => {
222                        // Get devices for this connection
223                        // Crear un proxy de propiedades para obtener las propiedades
224                        let properties_proxy =
225                            zbus::blocking::fdo::PropertiesProxy::builder(&self.connection)
226                                .destination("org.freedesktop.NetworkManager")?
227                                .path(path)?
228                                .build()?;
229
230                        let devices_variant = properties_proxy.get(
231                            InterfaceName::from_static_str_unchecked(
232                                "org.freedesktop.NetworkManager.Connection.Active",
233                            ),
234                            "Devices",
235                        )?;
236
237                        // Get the first device (if available)
238                        let device_path = match devices_variant.downcast_ref() {
239                            Ok(Value::Array(device_arr)) if !device_arr.is_empty() => {
240                                match device_arr[0] {
241                                    zbus::zvariant::Value::ObjectPath(ref dev_path) => {
242                                        dev_path.clone()
243                                    }
244                                    _ => return Ok(NetworkInfo::default()),
245                                }
246                            }
247                            _ => return Ok(NetworkInfo::default()),
248                        };
249
250                        // Retrieve connection details
251                        // Crear un proxy de propiedades para el dispositivo
252                        let device_properties_proxy =
253                            zbus::blocking::fdo::PropertiesProxy::builder(&self.connection)
254                                .destination("org.freedesktop.NetworkManager")?
255                                .path(&device_path)?
256                                .build()?;
257
258                        let connection_type = device_properties_proxy.get(
259                            InterfaceName::from_static_str_unchecked(
260                                "org.freedesktop.NetworkManager.Device",
261                            ),
262                            "DeviceType",
263                        )?;
264
265                        let state_variant = properties_proxy.get(
266                            InterfaceName::from_static_str_unchecked(
267                                "org.freedesktop.NetworkManager.Connection.Active",
268                            ),
269                            "State",
270                        )?;
271
272                        let is_connected = match state_variant.downcast_ref() {
273                            Ok(zbus::zvariant::Value::U32(state)) => state == 2, // 2 = ACTIVATED
274                            _ => false,
275                        };
276
277                        // Determine connection type
278                        let connection_type_str = match connection_type.downcast_ref() {
279                            Ok(zbus::zvariant::Value::U32(device_type)) => match device_type {
280                                1 => "Ethernet".to_string(),
281                                2 => "WiFi".to_string(),
282                                _ => "Unknown".to_string(),
283                            },
284                            _ => "Unknown".to_string(),
285                        };
286
287                        // Default network info
288                        let mut network_info = NetworkInfo {
289                            name: "Unknown".to_string(),
290                            ssid: "Unknown".to_string(),
291                            connection_type: connection_type_str.clone(),
292                            icon: "network-offline-symbolic".to_string(),
293                            ip_address: "0.0.0.0".to_string(),
294                            mac_address: "00:00:00:00:00:00".to_string(),
295                            signal_strength: 0,
296                            security_type: WiFiSecurityType::None,
297                            is_connected: is_connected && NetworkManagerHelpers::has_internet_connectivity(&self.proxy)?,
298                        };
299
300                        let hw_address_variant = device_properties_proxy.get(
301                            InterfaceName::from_static_str_unchecked(
302                                "org.freedesktop.NetworkManager.Device",
303                            ),
304                            "HwAddress",
305                        )?;
306
307                        network_info.mac_address = match hw_address_variant.downcast_ref() {
308                            Ok(zbus::zvariant::Value::Str(s)) => s.to_string(),
309                            _ => "00:00:00:00:00:00".to_string(),
310                        };
311
312                        // For WiFi networks, get additional details
313                        if connection_type_str == "WiFi" {
314                            // Get active access point
315                            // Crear un proxy de propiedades para el dispositivo inalámbrico
316                            let wireless_properties_proxy =
317                                zbus::blocking::fdo::PropertiesProxy::builder(&self.connection)
318                                    .destination("org.freedesktop.NetworkManager")?
319                                    .path(&device_path)?
320                                    .build()?;
321
322                            let active_ap_path = wireless_properties_proxy.get(
323                                InterfaceName::from_static_str_unchecked(
324                                    "org.freedesktop.NetworkManager.Device.Wireless",
325                                ),
326                                "ActiveAccessPoint",
327                            )?;
328
329                            if let Ok(zbus::zvariant::Value::ObjectPath(ap_path)) =
330                                active_ap_path.downcast_ref()
331                            {
332                                let _ap_proxy = zbus::blocking::Proxy::new(
333                                    &self.connection,
334                                    "org.freedesktop.NetworkManager",
335                                    &ap_path,
336                                    "org.freedesktop.NetworkManager.AccessPoint",
337                                )?;
338
339                                // Get SSID
340                                // Crear un proxy de propiedades para el punto de acceso
341                                let ap_properties_proxy =
342                                    zbus::blocking::fdo::PropertiesProxy::builder(&self.connection)
343                                        .destination("org.freedesktop.NetworkManager")?
344                                        .path(ap_path.as_str())?
345                                        .build()?;
346
347                                let ssid_variant = ap_properties_proxy.get(
348                                    InterfaceName::from_static_str_unchecked(
349                                        "org.freedesktop.NetworkManager.AccessPoint",
350                                    ),
351                                    "Ssid",
352                                )?;
353
354                                network_info.ssid = match ssid_variant.downcast_ref() {
355                                    Ok(v) => NetworkManagerHelpers::ssid_from_value(&v),
356                                    _ => "Unknown".to_string(),
357                                };
358                                network_info.name = network_info.ssid.clone();
359
360                                // Get signal strength
361                                let strength_variant = ap_properties_proxy.get(
362                                    InterfaceName::from_static_str_unchecked(
363                                        "org.freedesktop.NetworkManager.AccessPoint",
364                                    ),
365                                    "Strength",
366                                )?;
367
368                                network_info.signal_strength = match strength_variant.downcast_ref()
369                                {
370                                    Ok(zbus::zvariant::Value::U8(s)) => s,
371                                    _ => 0,
372                                };
373
374                                // Update icon based on signal strength
375                                network_info.icon =
376                                    Self::get_wifi_icon(network_info.signal_strength);
377
378                                // Determine security type using helper
379                                network_info.security_type = NetworkManagerHelpers::detect_security_type(&ap_properties_proxy)?;
380                            }
381                        } else {
382                            // This is a wired connection
383                            network_info.icon = Self::get_wired_icon(network_info.is_connected);
384                        }
385                        // Get IP configuration
386                        let ip4_config_path = device_properties_proxy.get(
387                            InterfaceName::from_static_str_unchecked(
388                                "org.freedesktop.NetworkManager.Device",
389                            ),
390                            "Ip4Config",
391                        )?;
392
393                        // Retrieve IP address if available
394                        if let Ok(zbus::zvariant::Value::ObjectPath(config_path)) =
395                            ip4_config_path.downcast_ref()
396                        {
397                            // Crear un proxy de propiedades para la configuración IP
398                            let ip_config_properties_proxy =
399                                zbus::blocking::fdo::PropertiesProxy::builder(&self.connection)
400                                    .destination("org.freedesktop.NetworkManager")?
401                                    .path(config_path)?
402                                    .build()?;
403
404                            let addresses_variant = ip_config_properties_proxy.get(
405                                InterfaceName::from_static_str_unchecked(
406                                    "org.freedesktop.NetworkManager.IP4Config",
407                                ),
408                                "Addresses",
409                            )?;
410
411                            if let Ok(Value::Array(addr_arr)) = addresses_variant.downcast_ref() {
412                                if let Some(Value::Array(ip_tuple)) = addr_arr.first() {
413                                    if ip_tuple.len() >= 1 {
414                                        if let Value::U32(ip_int) = &ip_tuple[0] {
415                                            use std::net::Ipv4Addr;
416                                            network_info.ip_address =
417                                                Ipv4Addr::from((*ip_int).to_be()).to_string();
418                                        }
419                                    }
420                                }
421                            }
422                        }
423
424                        Ok(network_info)
425                    }
426                    _ => Ok(NetworkInfo::default()),
427                }
428            }
429            _ => Ok(NetworkInfo::default()),
430        }
431    }
432
433    /// Lightweight: get the SSID of the currently connected access point (if any).
434    /// Avoids the ~10 D-Bus calls that `get_current_network_state()` makes.
435    fn connected_wifi_ssid(&self) -> Result<Option<String>> {
436        let devices = self.wireless_device_paths()?;
437        for device_path in &devices {
438            let wireless_props = zbus::blocking::fdo::PropertiesProxy::builder(&self.connection)
439                .destination("org.freedesktop.NetworkManager")?
440                .path(device_path.as_str())?
441                .build()?;
442
443            let active_ap = wireless_props.get(
444                InterfaceName::from_static_str_unchecked(
445                    "org.freedesktop.NetworkManager.Device.Wireless",
446                ),
447                "ActiveAccessPoint",
448            )?;
449
450            if let Ok(zbus::zvariant::Value::ObjectPath(ap_path)) = active_ap.downcast_ref() {
451                if ap_path.as_str() == "/" {
452                    continue;
453                }
454                let ap_props = zbus::blocking::fdo::PropertiesProxy::builder(&self.connection)
455                    .destination("org.freedesktop.NetworkManager")?
456                    .path(ap_path.as_str())?
457                    .build()?;
458
459                let ssid_variant = ap_props.get(
460                    InterfaceName::from_static_str_unchecked(
461                        "org.freedesktop.NetworkManager.AccessPoint",
462                    ),
463                    "Ssid",
464                )?;
465
466                if let Ok(v) = ssid_variant.downcast_ref() {
467                    let ssid = NetworkManagerHelpers::ssid_from_value(&v);
468                    if !ssid.is_empty() && ssid != "Unknown" {
469                        return Ok(Some(ssid));
470                    }
471                }
472            }
473        }
474        Ok(None)
475    }
476
477    /// List available WiFi networks
478    pub fn list_wifi_networks(&self) -> Result<Vec<NetworkInfo>> {
479        let devices_variant = self.proxy.get(
480            InterfaceName::from_static_str_unchecked("org.freedesktop.NetworkManager"),
481            "Devices",
482        )?;
483
484        let mut networks = Vec::new();
485        let connected_ssid = self.connected_wifi_ssid()?;
486
487        if let Ok(zbus::zvariant::Value::Array(devices)) = devices_variant.downcast_ref() {
488            for device in devices.iter() {
489                if let zbus::zvariant::Value::ObjectPath(ref device_path) = device {
490                    let device_props =
491                        zbus::blocking::fdo::PropertiesProxy::builder(&self.connection)
492                            .destination("org.freedesktop.NetworkManager")?
493                            .path(device_path)?
494                            .build()?;
495
496                    let device_type_variant = device_props.get(
497                        InterfaceName::from_static_str_unchecked(
498                            "org.freedesktop.NetworkManager.Device",
499                        ),
500                        "DeviceType",
501                    )?;
502
503                    if let Ok(zbus::zvariant::Value::U32(device_type)) =
504                        device_type_variant.downcast_ref()
505                    {
506                        if device_type == 2u32 {
507                            let mac_address = match device_props
508                                .get(
509                                    InterfaceName::from_static_str_unchecked(
510                                        "org.freedesktop.NetworkManager.Device",
511                                    ),
512                                    "HwAddress",
513                                )?
514                                .downcast_ref()
515                            {
516                                Ok(zbus::zvariant::Value::Str(s)) => s.to_string(),
517                                _ => "00:00:00:00:00:00".to_string(),
518                            };
519
520                            let wireless_props =
521                                zbus::blocking::fdo::PropertiesProxy::builder(&self.connection)
522                                    .destination("org.freedesktop.NetworkManager")?
523                                    .path(device_path)?
524                                    .build()?;
525
526                            let access_points_variant = wireless_props.get(
527                                InterfaceName::from_static_str_unchecked(
528                                    "org.freedesktop.NetworkManager.Device.Wireless",
529                                ),
530                                "AccessPoints",
531                            )?;
532
533                            if let Ok(zbus::zvariant::Value::Array(aps)) =
534                                access_points_variant.downcast_ref()
535                            {
536                                for ap in aps.iter() {
537                                    if let zbus::zvariant::Value::ObjectPath(ref ap_path) = ap {
538                                        let ap_props = zbus::blocking::fdo::PropertiesProxy::builder(
539                                            &self.connection,
540                                        )
541                                        .destination("org.freedesktop.NetworkManager")?
542                                        .path(ap_path)?
543                                        .build()?;
544
545                                        let ssid_variant = ap_props.get(
546                                            InterfaceName::from_static_str_unchecked(
547                                                "org.freedesktop.NetworkManager.AccessPoint",
548                                            ),
549                                            "Ssid",
550                                        )?;
551
552                                        let ssid = match ssid_variant.downcast_ref() {
553                                            Ok(v) => NetworkManagerHelpers::ssid_from_value(&v),
554                                            _ => "Unknown".to_string(),
555                                        };
556
557                                        let strength_variant = ap_props.get(
558                                            InterfaceName::from_static_str_unchecked(
559                                                "org.freedesktop.NetworkManager.AccessPoint",
560                                            ),
561                                            "Strength",
562                                        )?;
563
564                                        let strength = match strength_variant.downcast_ref() {
565                                            Ok(zbus::zvariant::Value::U8(s)) => s,
566                                            _ => 0,
567                                        };
568
569                                        let security_type = NetworkManagerHelpers::detect_security_type(&ap_props)?;
570
571                                        let is_connected = connected_ssid.as_deref() == Some(ssid.as_str());
572
573                                        let network_info = NetworkInfo {
574                                            name: ssid.clone(),
575                                            ssid,
576                                            connection_type: "wifi".to_string(),
577                                            icon: Self::get_wifi_icon(strength),
578                                            ip_address: "0.0.0.0".to_string(),
579                                            mac_address: mac_address.clone(),
580                                            signal_strength: strength,
581                                            security_type,
582                                            is_connected,
583                                        };
584
585                                        if !networks.iter().any(|n: &NetworkInfo| n.ssid == network_info.ssid) {
586                                            networks.push(network_info);
587                                        }
588                                    }
589                                }
590                            }
591                        }
592                    }
593                }
594            }
595        }
596
597        networks.sort_by(|a, b| b.signal_strength.cmp(&a.signal_strength));
598        Ok(networks)
599    }
600
601    /// Collect paths of all wireless (type=2) devices from NetworkManager
602    fn wireless_device_paths(&self) -> Result<Vec<zbus::zvariant::OwnedObjectPath>> {
603        let devices_variant = self.proxy.get(
604            InterfaceName::from_static_str_unchecked("org.freedesktop.NetworkManager"),
605            "Devices",
606        )?;
607
608        let mut paths = Vec::new();
609        if let Ok(zbus::zvariant::Value::Array(devices)) = devices_variant.downcast_ref() {
610            for device in devices.iter() {
611                if let zbus::zvariant::Value::ObjectPath(device_path) = device {
612                    let device_props =
613                        zbus::blocking::fdo::PropertiesProxy::builder(&self.connection)
614                            .destination("org.freedesktop.NetworkManager")?
615                            .path(device_path.as_str())?
616                            .build()?;
617
618                    let device_type_variant = device_props.get(
619                        InterfaceName::from_static_str_unchecked(
620                            "org.freedesktop.NetworkManager.Device",
621                        ),
622                        "DeviceType",
623                    )?;
624
625                    if let Ok(zbus::zvariant::Value::U32(device_type)) =
626                        device_type_variant.downcast_ref()
627                    {
628                        if device_type == 2u32 {
629                            let owned = zbus::zvariant::OwnedObjectPath::try_from(
630                                device_path.as_str(),
631                            )
632                            .unwrap();
633                            paths.push(owned);
634                        }
635                    }
636                }
637            }
638        }
639        Ok(paths)
640    }
641
642    /// Request an explicit WiFi scan through NetworkManager and return a fresh list.
643    pub fn rescan_wifi(&self) -> Result<Vec<NetworkInfo>> {
644        let devices = self.wireless_device_paths()?;
645
646        if devices.is_empty() {
647            return Err(crate::error::NetworkError::OperationError(
648                "No wireless device available for scanning".to_string(),
649            ));
650        }
651
652        for device_path in &devices {
653            let wireless_proxy = zbus::blocking::Proxy::new(
654                &self.connection,
655                "org.freedesktop.NetworkManager",
656                device_path.as_str(),
657                "org.freedesktop.NetworkManager.Device.Wireless",
658            )?;
659
660            let options: HashMap<String, zbus::zvariant::OwnedValue> = HashMap::new();
661            if wireless_proxy.call::<_, _, ()>("RequestScan", &(options,)).is_ok() {
662                return self.list_wifi_networks();
663            }
664        }
665
666        Err(crate::error::NetworkError::OperationError(
667            "Failed to request WiFi scan on available wireless devices".to_string(),
668        ))
669    }
670
671    /// Connect to a WiFi network
672    pub fn connect_to_wifi(&self, config: WiFiConnectionConfig) -> Result<()> {
673        // Log connection attempt
674        log::debug!("connect_to_wifi called: ssid='{}' security={:?} username={:?}",
675                  config.ssid, config.security_type, config.username);
676
677        // Create connection settings
678        let mut connection_settings = HashMap::new();
679        let mut wifi_settings = HashMap::new();
680        let mut security_settings = HashMap::new();
681
682        // Set connection name and type
683        let mut connection = HashMap::new();
684        connection.insert("id".to_string(), Value::from(config.ssid.clone()));
685        connection.insert("type".to_string(), Value::from("802-11-wireless"));
686        connection_settings.insert("connection".to_string(), connection);
687
688        // Set WiFi settings
689        wifi_settings.insert("ssid".to_string(), Value::from(config.ssid.as_bytes().to_vec()));
690        wifi_settings.insert("mode".to_string(), Value::from("infrastructure"));
691
692        // Set security settings based on security type
693        match config.security_type {
694            WiFiSecurityType::None => {
695                // No security settings needed
696            }
697            WiFiSecurityType::Wep => {
698                security_settings.insert("key-mgmt".to_string(), Value::from("none"));
699                if let Some(password) = config.password.clone() {
700                    security_settings.insert("wep-key0".to_string(), Value::from(password));
701                }
702            }
703            WiFiSecurityType::WpaPsk => {
704                security_settings.insert("key-mgmt".to_string(), Value::from("wpa-psk"));
705                if let Some(password) = config.password.clone() {
706                    security_settings.insert("psk".to_string(), Value::from(password));
707                }
708            }
709            WiFiSecurityType::WpaEap => {
710                security_settings.insert("key-mgmt".to_string(), Value::from("wpa-eap"));
711                if let Some(password) = config.password.clone() {
712                    security_settings.insert("password".to_string(), Value::from(password));
713                }
714                if let Some(username) = config.username.clone() {
715                    security_settings.insert("identity".to_string(), Value::from(username));
716                }
717            }
718            WiFiSecurityType::Wpa2Psk => {
719                security_settings.insert("key-mgmt".to_string(), Value::from("wpa-psk"));
720                security_settings.insert("proto".to_string(), Value::from(vec!["rsn"]));
721                if let Some(password) = config.password.clone() {
722                    security_settings.insert("psk".to_string(), Value::from(password));
723                }
724            }
725            WiFiSecurityType::Wpa3Psk => {
726                security_settings.insert("key-mgmt".to_string(), Value::from("sae"));
727                if let Some(password) = config.password.clone() {
728                    security_settings.insert("psk".to_string(), Value::from(password));
729                }
730            }
731        }
732
733        connection_settings.insert("802-11-wireless".to_string(), wifi_settings);
734        if !security_settings.is_empty() {
735            connection_settings.insert("802-11-wireless-security".to_string(), security_settings);
736        }
737
738        // Log constructed settings for debugging
739        log::debug!("connection_settings: {:#?}", connection_settings);
740
741        // Crear un proxy para NetworkManager
742        let nm_proxy = zbus::blocking::Proxy::new(
743            &self.connection,
744            "org.freedesktop.NetworkManager",
745            "/org/freedesktop/NetworkManager",
746            "org.freedesktop.NetworkManager",
747        )?;
748
749        // Llamar al método AddAndActivateConnection con autoselección de NM
750        let any_path = zbus::zvariant::OwnedObjectPath::try_from("/").unwrap();
751        let call_result: zbus::Result<(zbus::zvariant::OwnedObjectPath, zbus::zvariant::OwnedObjectPath)> = nm_proxy.call("AddAndActivateConnection", &(connection_settings, &any_path, &any_path));
752
753        match call_result {
754            Ok((conn_path, active_path)) => {
755                log::info!(
756                    "AddAndActivateConnection succeeded for ssid='{}' conn='{}' active='{}'",
757                    config.ssid,
758                    conn_path.as_str(),
759                    active_path.as_str()
760                );
761            }
762            Err(e) => {
763                log::error!(
764                    "AddAndActivateConnection failed for ssid='{}': {:?}",
765                    config.ssid,
766                    e
767                );
768                return Err(e.into());
769            }
770        }
771
772        log::debug!("connect_to_wifi finished for ssid='{}'", config.ssid);
773
774        Ok(())
775    }
776
777    /// Toggle network state
778    pub fn toggle_network_state(&self, enabled: bool) -> Result<bool> {
779        let nm_proxy = zbus::blocking::Proxy::new(
780            &self.connection,
781            "org.freedesktop.NetworkManager",
782            "/org/freedesktop/NetworkManager",
783            "org.freedesktop.NetworkManager",
784        )?;
785
786        nm_proxy.set_property("NetworkingEnabled", enabled)?;
787
788        let current_state: bool = nm_proxy.get_property("NetworkingEnabled")?;
789        Ok(current_state)
790    }
791
792    /// Get wireless enabled state
793    pub fn get_wireless_enabled(&self) -> Result<bool> {
794        let nm_proxy = zbus::blocking::Proxy::new(
795            &self.connection,
796            "org.freedesktop.NetworkManager",
797            "/org/freedesktop/NetworkManager",
798            "org.freedesktop.NetworkManager",
799        )?;
800        Ok(nm_proxy.get_property("WirelessEnabled")?)
801    }
802
803    /// Set wireless enabled state
804    pub fn set_wireless_enabled(&self, enabled: bool) -> Result<()> {
805        let nm_proxy = zbus::blocking::Proxy::new(
806            &self.connection,
807            "org.freedesktop.NetworkManager",
808            "/org/freedesktop/NetworkManager",
809            "org.freedesktop.NetworkManager",
810        )?;
811        nm_proxy.set_property("WirelessEnabled", enabled)?;
812        Ok(())
813    }
814
815    /// Check if wireless device is available
816    pub fn is_wireless_available(&self) -> Result<bool> {
817        Ok(!self.wireless_device_paths()?.is_empty())
818    }
819
820    /// Listen for network changes
821    pub fn listen_network_changes(&self) -> Result<mpsc::Receiver<NetworkInfo>> {
822        let (tx, rx) = mpsc::channel();
823        let connection_clone = self.connection.clone();
824        let app_handle = self.app.clone();
825
826        std::thread::spawn(move || {
827            if let Ok(proxy) = zbus::blocking::Proxy::new(
828                &connection_clone,
829                "org.freedesktop.NetworkManager",
830                "/org/freedesktop/NetworkManager",
831                "org.freedesktop.NetworkManager",
832            ) {
833                if let Ok(mut signal) = proxy.receive_signal("StateChanged") {
834                    let proxy_props = zbus::blocking::fdo::PropertiesProxy::builder(
835                        &connection_clone,
836                    )
837                    .destination("org.freedesktop.NetworkManager")
838                    .unwrap()
839                    .path("/org/freedesktop/NetworkManager")
840                    .unwrap()
841                    .build()
842                    .unwrap();
843
844                    while let Some(_msg) = signal.next() {
845                        let network_manager = VSKNetworkManager {
846                            connection: connection_clone.clone(),
847                            proxy: proxy_props.clone(),
848                            app: app_handle.clone(),
849                        };
850
851                        if let Ok(network_info) =
852                            network_manager.get_current_network_state()
853                        {
854                            if tx.send(network_info).is_err() {
855                                break;
856                            }
857                        }
858                    }
859                }
860            }
861        });
862
863        Ok(rx)
864    }
865
866    /// Disconnect from the current WiFi network
867    pub fn disconnect_from_wifi(&self) -> Result<()> {
868        let nm_proxy = zbus::blocking::Proxy::new(
869            &self.connection,
870            "org.freedesktop.NetworkManager",
871            "/org/freedesktop/NetworkManager",
872            "org.freedesktop.NetworkManager",
873        )?;
874
875        // Obtener las conexiones activas
876        let active_connections_variant: zbus::zvariant::OwnedValue = self.proxy.get(
877            InterfaceName::from_static_str_unchecked("org.freedesktop.NetworkManager"),
878            "ActiveConnections",
879        )?;
880
881        // Convertir el valor a un vector de ObjectPath
882        let active_connections = match active_connections_variant.downcast_ref() {
883            Ok(zbus::zvariant::Value::Array(arr)) => arr
884                .iter()
885                .filter_map(|v| match v {
886                    zbus::zvariant::Value::ObjectPath(path) => {
887                        Some(zbus::zvariant::OwnedObjectPath::from(path.to_owned()))
888                    }
889                    _ => None,
890                })
891                .collect::<Vec<zbus::zvariant::OwnedObjectPath>>(),
892            _ => Vec::new(),
893        };
894
895        if !active_connections.is_empty() {
896            nm_proxy.call::<_, _, ()>("DeactivateConnection", &(&active_connections[0],))?;
897            Ok(())
898        } else {
899            Ok(())
900        }
901    }
902
903    /// Get the list of saved WiFi networks
904    pub fn get_saved_wifi_networks(&self) -> Result<Vec<NetworkInfo>> {
905        // Crear un proxy para el servicio de configuración de NetworkManager
906        let settings_proxy = zbus::blocking::Proxy::new(
907            &self.connection,
908            "org.freedesktop.NetworkManager",
909            "/org/freedesktop/NetworkManager/Settings",
910            "org.freedesktop.NetworkManager.Settings",
911        )?;
912
913        // Obtener todas las conexiones guardadas
914        let connections: Vec<zbus::zvariant::OwnedObjectPath> =
915            settings_proxy.call("ListConnections", &())?;
916        let mut saved_networks = Vec::new();
917
918        // Procesar cada conexión guardada
919        for conn_path in connections {
920            // Crear un proxy para cada conexión
921            let conn_proxy = zbus::blocking::Proxy::new(
922                &self.connection,
923                "org.freedesktop.NetworkManager",
924                conn_path.as_str(),
925                "org.freedesktop.NetworkManager.Settings.Connection",
926            )?;
927
928            // Obtener la configuración de la conexión como un HashMap
929            let settings: std::collections::HashMap<String, zbus::zvariant::OwnedValue> =
930                conn_proxy.call("GetSettings", &())?;
931
932            // Verificar si es una conexión WiFi
933            if let Some(connection) = settings.get("connection") {
934                let connection_dict = match connection.downcast_ref::<Value>()
935                    .and_then(|v| v.downcast::<std::collections::HashMap<String, zbus::zvariant::OwnedValue>>())
936                {
937                    Ok(dict) => dict,
938                    _ => continue,
939                };
940
941                // Verificar el tipo de conexión
942                if let Some(conn_type) = connection_dict.get("type") {
943                    let v_type: &zbus::zvariant::Value<'_> = conn_type;
944                    let conn_type_str = match v_type.downcast_ref::<String>() {
945                        Ok(s) => s,
946                        _ => continue,
947                    };
948
949                    // Si es una conexión WiFi, extraer la información
950                    if conn_type_str == "802-11-wireless" {
951                        let mut network_info = NetworkInfo::default();
952                        network_info.connection_type = "wifi".to_string();
953
954                        // Obtener el nombre de la conexión
955                        if let Some(id) = connection_dict.get("id") {
956                            let v_id: &zbus::zvariant::Value<'_> = id;
957                            if let Ok(name) = v_id.downcast_ref::<String>() {
958                                network_info.name = name;
959                            }
960                        }
961
962                        // Obtener el SSID
963                        if let Some(wireless) = settings.get("802-11-wireless") {
964                            let wireless_dict = match wireless.downcast_ref::<Value>()
965                                .and_then(|v| v.downcast::<std::collections::HashMap<String, zbus::zvariant::OwnedValue>>())
966                            {
967                                Ok(dict) => dict,
968                                _ => continue,
969                            };
970
971                            if let Some(ssid) = wireless_dict.get("ssid") {
972                                let v_ssid: &zbus::zvariant::Value<'_> = ssid;
973                                network_info.ssid = NetworkManagerHelpers::ssid_from_value(v_ssid);
974                            }
975                        }
976
977                        // Determinar el tipo de seguridad
978                        if let Some(security) = settings.get("802-11-wireless-security") {
979                            let security_dict = match security.downcast_ref::<Value>()
980                                .and_then(|v| v.downcast::<std::collections::HashMap<String, zbus::zvariant::OwnedValue>>())
981                            {
982                                Ok(dict) => dict,
983                                _ => {
984                                    network_info.security_type = WiFiSecurityType::None;
985                                    saved_networks.push(network_info);
986                                    continue;
987                                },
988                            };
989
990                            if let Some(key_mgmt) = security_dict.get("key-mgmt") {
991                                let v_km: &zbus::zvariant::Value<'_> = key_mgmt;
992                                if let Ok(key_mgmt_str) = v_km.downcast_ref::<String>() {
993                                    match key_mgmt_str.as_str() {
994                                        "none" => {
995                                            network_info.security_type = WiFiSecurityType::None
996                                        }
997                                        "wpa-psk" => {
998                                            network_info.security_type = WiFiSecurityType::WpaPsk
999                                        }
1000                                        "wpa-eap" => {
1001                                            network_info.security_type = WiFiSecurityType::WpaEap
1002                                        }
1003                                        _ => network_info.security_type = WiFiSecurityType::None,
1004                                    }
1005                                }
1006                            }
1007                        } else {
1008                            network_info.security_type = WiFiSecurityType::None;
1009                        }
1010
1011                        // Agregar a la lista de redes guardadas
1012                        saved_networks.push(network_info);
1013                    }
1014                }
1015            }
1016        }
1017
1018        Ok(saved_networks)
1019    }
1020
1021    /// Delete a saved WiFi connection by SSID
1022    pub fn delete_wifi_connection(&self, ssid: &str) -> Result<bool> {
1023        // Crear un proxy para el servicio de configuración de NetworkManager
1024        let settings_proxy = zbus::blocking::Proxy::new(
1025            &self.connection,
1026            "org.freedesktop.NetworkManager",
1027            "/org/freedesktop/NetworkManager/Settings",
1028            "org.freedesktop.NetworkManager.Settings",
1029        )?;
1030
1031        // Obtener todas las conexiones guardadas
1032        let connections: Vec<zbus::zvariant::OwnedObjectPath> =
1033            settings_proxy.call("ListConnections", &())?;
1034
1035        // Procesar cada conexión guardada
1036        for conn_path in connections {
1037            // Crear un proxy para cada conexión
1038            let conn_proxy = zbus::blocking::Proxy::new(
1039                &self.connection,
1040                "org.freedesktop.NetworkManager",
1041                conn_path.as_str(),
1042                "org.freedesktop.NetworkManager.Settings.Connection",
1043            )?;
1044
1045            // Obtener la configuración de la conexión como un HashMap
1046            let settings: std::collections::HashMap<String, zbus::zvariant::OwnedValue> =
1047                conn_proxy.call("GetSettings", &())?;
1048
1049            // Verificar si es una conexión WiFi
1050            if let Some(connection) = settings.get("connection") {
1051                let connection_dict = match connection.downcast_ref::<Value>()
1052                    .and_then(|v| v.downcast::<std::collections::HashMap<String, zbus::zvariant::OwnedValue>>())
1053                {
1054                    Ok(dict) => dict,
1055                    _ => continue,
1056                };
1057
1058                // Verificar el tipo de conexión
1059                if let Some(conn_type) = connection_dict.get("type") {
1060                    let v_type: &zbus::zvariant::Value<'_> = conn_type;
1061                    let conn_type_str = match v_type.downcast_ref::<String>() {
1062                        Ok(s) => s,
1063                        _ => continue,
1064                    };
1065
1066                    // Si es una conexión WiFi, verificar el SSID
1067                    if conn_type_str == "802-11-wireless" {
1068                        if let Some(wireless) = settings.get("802-11-wireless") {
1069                            let wireless_dict = match wireless.downcast_ref::<Value>()
1070                                .and_then(|v| v.downcast::<std::collections::HashMap<String, zbus::zvariant::OwnedValue>>())
1071                            {
1072                                Ok(dict) => dict,
1073                                _ => continue,
1074                            };
1075
1076                            if let Some(ssid_value) = wireless_dict.get("ssid") {
1077                                let v_ssid: &zbus::zvariant::Value<'_> = ssid_value;
1078                                let conn_ssid_str = NetworkManagerHelpers::ssid_from_value(v_ssid);
1079                                // Si el SSID coincide, eliminar la conexión
1080                                if conn_ssid_str == ssid {
1081                                    conn_proxy.call::<_, _, ()>("Delete", &())?;
1082                                    return Ok(true);
1083                                }
1084                            }
1085                        }
1086                    }
1087                }
1088            }
1089        }
1090
1091        // No se encontró ninguna conexión con el SSID especificado
1092        Ok(false)
1093    }
1094
1095    /// List saved VPN profiles from NetworkManager settings.
1096    pub fn list_vpn_profiles(&self) -> Result<Vec<VpnProfile>> {
1097        let connections = self.list_connection_paths()?;
1098        let mut profiles = Vec::new();
1099
1100        for conn_path in connections {
1101            let settings = self.get_connection_settings(&conn_path)?;
1102            if let Some(profile) = self.vpn_profile_from_settings(&settings) {
1103                profiles.push(profile);
1104            }
1105        }
1106
1107        profiles.sort_by(|a, b| a.id.cmp(&b.id));
1108        Ok(profiles)
1109    }
1110
1111    /// Get current VPN status from active connections.
1112    pub fn get_vpn_status(&self) -> Result<VpnStatus> {
1113        let active_connections_variant = self.proxy.get(
1114            InterfaceName::from_static_str_unchecked("org.freedesktop.NetworkManager"),
1115            "ActiveConnections",
1116        )?;
1117
1118        let mut status = VpnStatus::default();
1119
1120        if let Ok(zbus::zvariant::Value::Array(arr)) = active_connections_variant.downcast_ref() {
1121            for value in arr.iter() {
1122                let active_path = match value {
1123                    zbus::zvariant::Value::ObjectPath(path) => path,
1124                    _ => continue,
1125                };
1126
1127                let active_props = zbus::blocking::fdo::PropertiesProxy::builder(&self.connection)
1128                    .destination("org.freedesktop.NetworkManager")?
1129                    .path(active_path)?
1130                    .build()?;
1131
1132                let conn_type_variant = active_props.get(
1133                    InterfaceName::from_static_str_unchecked(
1134                        "org.freedesktop.NetworkManager.Connection.Active",
1135                    ),
1136                    "Type",
1137                )?;
1138
1139                let conn_type = match conn_type_variant.downcast_ref() {
1140                    Ok(zbus::zvariant::Value::Str(v)) => v.to_string(),
1141                    _ => continue,
1142                };
1143
1144                if conn_type != "vpn" {
1145                    continue;
1146                }
1147
1148                let state_variant = active_props.get(
1149                    InterfaceName::from_static_str_unchecked(
1150                        "org.freedesktop.NetworkManager.Connection.Active",
1151                    ),
1152                    "State",
1153                )?;
1154                let state = match state_variant.downcast_ref() {
1155                    Ok(zbus::zvariant::Value::U32(v)) => v,
1156                    _ => 0,
1157                };
1158
1159                let id_variant = active_props.get(
1160                    InterfaceName::from_static_str_unchecked(
1161                        "org.freedesktop.NetworkManager.Connection.Active",
1162                    ),
1163                    "Id",
1164                )?;
1165                let uuid_variant = active_props.get(
1166                    InterfaceName::from_static_str_unchecked(
1167                        "org.freedesktop.NetworkManager.Connection.Active",
1168                    ),
1169                    "Uuid",
1170                )?;
1171
1172                status.state = Self::vpn_state_from_active_state(state);
1173                status.active_profile_name = match id_variant.downcast_ref() {
1174                    Ok(zbus::zvariant::Value::Str(v)) => Some(v.to_string()),
1175                    _ => None,
1176                };
1177                status.active_profile_id = status.active_profile_name.clone();
1178                status.active_profile_uuid = match uuid_variant.downcast_ref() {
1179                    Ok(zbus::zvariant::Value::Str(v)) => Some(v.to_string()),
1180                    _ => None,
1181                };
1182
1183                let ip4_config_variant = active_props.get(
1184                    InterfaceName::from_static_str_unchecked(
1185                        "org.freedesktop.NetworkManager.Connection.Active",
1186                    ),
1187                    "Ip4Config",
1188                )?;
1189
1190                if let Ok(zbus::zvariant::Value::ObjectPath(ip4_path)) =
1191                    ip4_config_variant.downcast_ref()
1192                {
1193                    if ip4_path.as_str() != "/" {
1194                        let ip4_props = zbus::blocking::fdo::PropertiesProxy::builder(&self.connection)
1195                            .destination("org.freedesktop.NetworkManager")?
1196                            .path(ip4_path)?
1197                            .build()?;
1198
1199                        if let Ok(gateway_variant) = ip4_props.get(
1200                            InterfaceName::from_static_str_unchecked(
1201                                "org.freedesktop.NetworkManager.IP4Config",
1202                            ),
1203                            "Gateway",
1204                        ) {
1205                            status.gateway = match gateway_variant.downcast_ref() {
1206                                Ok(zbus::zvariant::Value::Str(v)) => Some(v.to_string()),
1207                                _ => None,
1208                            };
1209                        }
1210                    }
1211                }
1212
1213                return Ok(status);
1214            }
1215        }
1216
1217        Ok(status)
1218    }
1219
1220    /// Connect a VPN profile by UUID.
1221    pub fn connect_vpn(&self, uuid: String) -> Result<()> {
1222        let current_status = self.get_vpn_status()?;
1223        if current_status.state == VpnConnectionState::Connected
1224            && current_status.active_profile_uuid.as_deref() == Some(uuid.as_str())
1225        {
1226            return Err(crate::error::NetworkError::VpnAlreadyConnected(uuid));
1227        }
1228
1229        let conn_path = self.find_connection_path_by_uuid(&uuid)?;
1230
1231        let nm_proxy = zbus::blocking::Proxy::new(
1232            &self.connection,
1233            "org.freedesktop.NetworkManager",
1234            "/org/freedesktop/NetworkManager",
1235            "org.freedesktop.NetworkManager",
1236        )?;
1237
1238        let any_path = zbus::zvariant::OwnedObjectPath::try_from("/").unwrap();
1239        let activate_result: zbus::Result<zbus::zvariant::OwnedObjectPath> =
1240            nm_proxy.call("ActivateConnection", &(&conn_path, &any_path, &any_path));
1241
1242        match activate_result {
1243            Ok(_) => Ok(()),
1244            Err(e) => {
1245                let msg = e.to_string().to_lowercase();
1246                if msg.contains("secret") || msg.contains("authentication") {
1247                    Err(crate::error::NetworkError::VpnAuthFailed(e.to_string()))
1248                } else {
1249                    Err(crate::error::NetworkError::VpnActivationFailed(e.to_string()))
1250                }
1251            }
1252        }
1253    }
1254
1255    /// Disconnect VPN by UUID or disconnect active VPN if UUID is not provided.
1256    pub fn disconnect_vpn(&self, uuid: Option<String>) -> Result<()> {
1257        let active_connections_variant = self.proxy.get(
1258            InterfaceName::from_static_str_unchecked("org.freedesktop.NetworkManager"),
1259            "ActiveConnections",
1260        )?;
1261
1262        let mut target_active_connection: Option<zbus::zvariant::OwnedObjectPath> = None;
1263
1264        if let Ok(zbus::zvariant::Value::Array(arr)) = active_connections_variant.downcast_ref() {
1265            for value in arr.iter() {
1266                let active_path = match value {
1267                    zbus::zvariant::Value::ObjectPath(path) => {
1268                        zbus::zvariant::OwnedObjectPath::from(path.to_owned())
1269                    }
1270                    _ => continue,
1271                };
1272
1273                let active_props = zbus::blocking::fdo::PropertiesProxy::builder(&self.connection)
1274                    .destination("org.freedesktop.NetworkManager")?
1275                    .path(active_path.as_str())?
1276                    .build()?;
1277
1278                let conn_type_variant = active_props.get(
1279                    InterfaceName::from_static_str_unchecked(
1280                        "org.freedesktop.NetworkManager.Connection.Active",
1281                    ),
1282                    "Type",
1283                )?;
1284                let conn_type = match conn_type_variant.downcast_ref() {
1285                    Ok(zbus::zvariant::Value::Str(v)) => v.to_string(),
1286                    _ => continue,
1287                };
1288
1289                if conn_type != "vpn" {
1290                    continue;
1291                }
1292
1293                if let Some(target_uuid) = uuid.as_deref() {
1294                    let uuid_variant = active_props.get(
1295                        InterfaceName::from_static_str_unchecked(
1296                            "org.freedesktop.NetworkManager.Connection.Active",
1297                        ),
1298                        "Uuid",
1299                    )?;
1300                    let active_uuid = match uuid_variant.downcast_ref() {
1301                        Ok(zbus::zvariant::Value::Str(v)) => v.to_string(),
1302                        _ => continue,
1303                    };
1304
1305                    if active_uuid == target_uuid {
1306                        target_active_connection = Some(active_path.clone());
1307                        break;
1308                    }
1309                } else {
1310                    target_active_connection = Some(active_path.clone());
1311                    break;
1312                }
1313            }
1314        }
1315
1316        let target_active_connection = match target_active_connection {
1317            Some(path) => path,
1318            None => {
1319                if let Some(target_uuid) = uuid {
1320                    return Err(crate::error::NetworkError::VpnProfileNotFound(target_uuid));
1321                }
1322                return Err(crate::error::NetworkError::VpnNotActive);
1323            }
1324        };
1325
1326        let nm_proxy = zbus::blocking::Proxy::new(
1327            &self.connection,
1328            "org.freedesktop.NetworkManager",
1329            "/org/freedesktop/NetworkManager",
1330            "org.freedesktop.NetworkManager",
1331        )?;
1332
1333        nm_proxy.call::<_, _, ()>(
1334            "DeactivateConnection",
1335            &(target_active_connection.as_str(),),
1336        )?;
1337        Ok(())
1338    }
1339
1340    /// Create a new VPN profile in NetworkManager settings.
1341    pub fn create_vpn_profile(&self, config: VpnCreateConfig) -> Result<VpnProfile> {
1342        if config.id.trim().is_empty() {
1343            return Err(crate::error::NetworkError::VpnInvalidConfig(
1344                "id is required".to_string(),
1345            ));
1346        }
1347
1348        let uuid = Uuid::new_v4().to_string();
1349        let mut connection_section = HashMap::new();
1350        connection_section.insert("id".to_string(), Value::from(config.id.clone()));
1351        connection_section.insert("uuid".to_string(), Value::from(uuid.clone()));
1352        connection_section.insert("type".to_string(), Value::from("vpn"));
1353        connection_section.insert(
1354            "autoconnect".to_string(),
1355            Value::from(config.autoconnect.unwrap_or(false)),
1356        );
1357
1358        let mut vpn_section = HashMap::new();
1359        vpn_section.insert(
1360            "service-type".to_string(),
1361            Value::from(Self::service_type_from_vpn_type(&config.vpn_type)),
1362        );
1363
1364        if let Some(username) = config.username {
1365            vpn_section.insert("user-name".to_string(), Value::from(username));
1366        }
1367        if let Some(gateway) = config.gateway {
1368            vpn_section.insert("remote".to_string(), Value::from(gateway));
1369        }
1370        if let Some(ca_cert_path) = config.ca_cert_path {
1371            vpn_section.insert("ca".to_string(), Value::from(ca_cert_path));
1372        }
1373        if let Some(user_cert_path) = config.user_cert_path {
1374            vpn_section.insert("cert".to_string(), Value::from(user_cert_path));
1375        }
1376        if let Some(private_key_path) = config.private_key_path {
1377            vpn_section.insert("key".to_string(), Value::from(private_key_path));
1378        }
1379        if let Some(private_key_password) = config.private_key_password {
1380            vpn_section.insert("key-password".to_string(), Value::from(private_key_password));
1381        }
1382        if let Some(custom_settings) = config.settings {
1383            for (k, v) in custom_settings {
1384                vpn_section.insert(k, Value::from(v));
1385            }
1386        }
1387
1388        let mut vpn_secrets_section = HashMap::new();
1389        if let Some(password) = config.password {
1390            vpn_secrets_section.insert("password".to_string(), Value::from(password));
1391        }
1392        if let Some(custom_secrets) = config.secrets {
1393            for (k, v) in custom_secrets {
1394                vpn_secrets_section.insert(k, Value::from(v));
1395            }
1396        }
1397
1398        let mut settings: HashMap<String, HashMap<String, Value>> = HashMap::new();
1399        settings.insert("connection".to_string(), connection_section);
1400        settings.insert("vpn".to_string(), vpn_section);
1401        if !vpn_secrets_section.is_empty() {
1402            settings.insert("vpn-secrets".to_string(), vpn_secrets_section);
1403        }
1404
1405        let settings_proxy = zbus::blocking::Proxy::new(
1406            &self.connection,
1407            "org.freedesktop.NetworkManager",
1408            "/org/freedesktop/NetworkManager/Settings",
1409            "org.freedesktop.NetworkManager.Settings",
1410        )?;
1411
1412        let _created_path: zbus::zvariant::OwnedObjectPath =
1413            settings_proxy.call("AddConnection", &(settings,))?;
1414
1415        Ok(VpnProfile {
1416            id: config.id,
1417            uuid,
1418            vpn_type: config.vpn_type,
1419            interface_name: None,
1420            autoconnect: config.autoconnect.unwrap_or(false),
1421            editable: true,
1422            last_error: None,
1423        })
1424    }
1425
1426    /// Update an existing VPN profile by UUID.
1427    pub fn update_vpn_profile(&self, config: VpnUpdateConfig) -> Result<VpnProfile> {
1428        let conn_path = self.find_connection_path_by_uuid(&config.uuid)?;
1429        let existing_settings = self.get_connection_settings(&conn_path)?;
1430
1431        let existing_profile = self
1432            .vpn_profile_from_settings(&existing_settings)
1433            .ok_or_else(|| crate::error::NetworkError::VpnProfileNotFound(config.uuid.clone()))?;
1434
1435        let existing_vpn_settings = Self::string_map_from_section(&existing_settings, "vpn");
1436        let existing_vpn_secrets = Self::string_map_from_section(&existing_settings, "vpn-secrets");
1437
1438        // Start from the full current settings map to preserve unrelated sections
1439        // (IPv4/IPv6, routes, DNS, permissions, proxy, etc.).
1440        let mut settings: HashMap<String, HashMap<String, Value>> = HashMap::new();
1441        for (section_name, dict) in &existing_settings {
1442            let mut section_map: HashMap<String, Value> = HashMap::new();
1443            for (k, v) in dict {
1444                if let Ok(val) = v.downcast_ref::<Value>() {
1445                    section_map.insert(k.clone(), val);
1446                }
1447            }
1448            settings.insert(section_name.clone(), section_map);
1449        }
1450
1451        let connection_section = settings
1452            .entry("connection".to_string())
1453            .or_insert_with(HashMap::new);
1454        connection_section.insert(
1455            "id".to_string(),
1456            Value::from(config.id.clone().unwrap_or(existing_profile.id.clone())),
1457        );
1458        connection_section.insert("uuid".to_string(), Value::from(config.uuid.clone()));
1459        connection_section.insert("type".to_string(), Value::from("vpn"));
1460        connection_section.insert(
1461            "autoconnect".to_string(),
1462            Value::from(config.autoconnect.unwrap_or(existing_profile.autoconnect)),
1463        );
1464
1465        let service_type = existing_vpn_settings
1466            .get("service-type")
1467            .cloned()
1468            .unwrap_or_else(|| {
1469                Self::service_type_from_vpn_type(&existing_profile.vpn_type).to_string()
1470            });
1471
1472        let vpn_section = settings
1473            .entry("vpn".to_string())
1474            .or_insert_with(HashMap::new);
1475        vpn_section.insert("service-type".to_string(), Value::from(service_type));
1476
1477        let mut merged_settings = existing_vpn_settings;
1478        if let Some(username) = config.username {
1479            merged_settings.insert("user-name".to_string(), username);
1480        }
1481        if let Some(gateway) = config.gateway {
1482            merged_settings.insert("remote".to_string(), gateway);
1483        }
1484        if let Some(ca_cert_path) = config.ca_cert_path {
1485            merged_settings.insert("ca".to_string(), ca_cert_path);
1486        }
1487        if let Some(user_cert_path) = config.user_cert_path {
1488            merged_settings.insert("cert".to_string(), user_cert_path);
1489        }
1490        if let Some(private_key_path) = config.private_key_path {
1491            merged_settings.insert("key".to_string(), private_key_path);
1492        }
1493        if let Some(private_key_password) = config.private_key_password {
1494            merged_settings.insert("key-password".to_string(), private_key_password);
1495        }
1496        if let Some(custom_settings) = config.settings {
1497            for (k, v) in custom_settings {
1498                merged_settings.insert(k, v);
1499            }
1500        }
1501
1502        for (k, v) in merged_settings {
1503            vpn_section.insert(k, Value::from(v));
1504        }
1505
1506        let mut merged_secrets = existing_vpn_secrets;
1507        if let Some(password) = config.password {
1508            merged_secrets.insert("password".to_string(), password);
1509        }
1510        if let Some(custom_secrets) = config.secrets {
1511            for (k, v) in custom_secrets {
1512                merged_secrets.insert(k, v);
1513            }
1514        }
1515
1516        if merged_secrets.is_empty() {
1517            settings.remove("vpn-secrets");
1518        } else {
1519            let vpn_secrets_section = settings
1520                .entry("vpn-secrets".to_string())
1521                .or_insert_with(HashMap::new);
1522            vpn_secrets_section.clear();
1523            for (k, v) in merged_secrets {
1524                vpn_secrets_section.insert(k, Value::from(v));
1525            }
1526        }
1527
1528        let conn_proxy = zbus::blocking::Proxy::new(
1529            &self.connection,
1530            "org.freedesktop.NetworkManager",
1531            conn_path.as_str(),
1532            "org.freedesktop.NetworkManager.Settings.Connection",
1533        )?;
1534        conn_proxy.call::<_, _, ()>("Update", &(settings,))?;
1535
1536        let updated_settings = self.get_connection_settings(&conn_path)?;
1537        self.vpn_profile_from_settings(&updated_settings)
1538            .ok_or_else(|| crate::error::NetworkError::VpnProfileNotFound(config.uuid))
1539    }
1540
1541    /// Delete a VPN profile by UUID.
1542    pub fn delete_vpn_profile(&self, uuid: String) -> Result<()> {
1543        let conn_path = self.find_connection_path_by_uuid(&uuid)?;
1544
1545        let conn_proxy = zbus::blocking::Proxy::new(
1546            &self.connection,
1547            "org.freedesktop.NetworkManager",
1548            conn_path.as_str(),
1549            "org.freedesktop.NetworkManager.Settings.Connection",
1550        )?;
1551
1552        conn_proxy.call::<_, _, ()>("Delete", &())?;
1553        Ok(())
1554    }
1555}
1556
1557#[cfg(test)]
1558mod tests {
1559    use super::*;
1560
1561    #[test]
1562    fn vpn_state_deactivated_maps_to_disconnected() {
1563        assert_eq!(
1564            VSKNetworkManager::<tauri::Wry>::vpn_state_from_active_state(4),
1565            VpnConnectionState::Disconnected
1566        );
1567    }
1568}
1569
1570/// Initialize the network manager plugin
1571pub fn init(
1572    app: &AppHandle<tauri::Wry>,
1573    _api: PluginApi<tauri::Wry, ()>,
1574) -> Result<VSKNetworkManager<'static, tauri::Wry>> {
1575    Ok(VSKNetworkManager::new(app.clone())?)
1576}