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