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