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.is_empty() {
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        // De mayor a menor señal.
650        networks.sort_by_key(|red| std::cmp::Reverse(red.signal_strength));
651        Ok(networks)
652    }
653
654    /// Collect paths of all wireless (type=2) devices from NetworkManager
655    fn wireless_device_paths(&self) -> Result<Vec<zbus::zvariant::OwnedObjectPath>> {
656        let devices_variant = self.proxy.get(
657            InterfaceName::from_static_str_unchecked("org.freedesktop.NetworkManager"),
658            "Devices",
659        )?;
660
661        let mut paths = Vec::new();
662        if let Ok(zbus::zvariant::Value::Array(devices)) = devices_variant.downcast_ref() {
663            for device in devices.iter() {
664                if let zbus::zvariant::Value::ObjectPath(device_path) = device {
665                    let device_props =
666                        zbus::blocking::fdo::PropertiesProxy::builder(&self.connection)
667                            .destination("org.freedesktop.NetworkManager")?
668                            .path(device_path.as_str())?
669                            .build()?;
670
671                    let device_type_variant = device_props.get(
672                        InterfaceName::from_static_str_unchecked(
673                            "org.freedesktop.NetworkManager.Device",
674                        ),
675                        "DeviceType",
676                    )?;
677
678                    if let Ok(zbus::zvariant::Value::U32(device_type)) =
679                        device_type_variant.downcast_ref()
680                    {
681                        if device_type == 2u32 {
682                            let owned = zbus::zvariant::OwnedObjectPath::try_from(
683                                device_path.as_str(),
684                            )
685                            .unwrap();
686                            paths.push(owned);
687                        }
688                    }
689                }
690            }
691        }
692        Ok(paths)
693    }
694
695    /// Request an explicit WiFi scan through NetworkManager and return a fresh list.
696    pub fn rescan_wifi(&self) -> Result<Vec<NetworkInfo>> {
697        let devices = self.wireless_device_paths()?;
698
699        if devices.is_empty() {
700            return Err(crate::error::NetworkError::OperationError(
701                "No wireless device available for scanning".to_string(),
702            ));
703        }
704
705        for device_path in &devices {
706            let wireless_proxy = zbus::blocking::Proxy::new(
707                &self.connection,
708                "org.freedesktop.NetworkManager",
709                device_path.as_str(),
710                "org.freedesktop.NetworkManager.Device.Wireless",
711            )?;
712
713            let options: HashMap<String, zbus::zvariant::OwnedValue> = HashMap::new();
714            if wireless_proxy.call::<_, _, ()>("RequestScan", &(options,)).is_ok() {
715                return self.list_wifi_networks();
716            }
717        }
718
719        Err(crate::error::NetworkError::OperationError(
720            "Failed to request WiFi scan on available wireless devices".to_string(),
721        ))
722    }
723
724    /// Connect to a WiFi network
725    pub fn connect_to_wifi(&self, config: WiFiConnectionConfig) -> Result<()> {
726        // Log connection attempt
727        log::debug!("connect_to_wifi called: ssid='{}' security={:?} username={:?}",
728                  config.ssid, config.security_type, config.username);
729
730        // Create connection settings
731        let mut connection_settings = HashMap::new();
732        let mut wifi_settings = HashMap::new();
733        let mut security_settings = HashMap::new();
734
735        // Set connection name and type
736        let mut connection = HashMap::new();
737        connection.insert("id".to_string(), Value::from(config.ssid.clone()));
738        connection.insert("type".to_string(), Value::from("802-11-wireless"));
739        connection_settings.insert("connection".to_string(), connection);
740
741        // Set WiFi settings
742        wifi_settings.insert("ssid".to_string(), Value::from(config.ssid.as_bytes().to_vec()));
743        wifi_settings.insert("mode".to_string(), Value::from("infrastructure"));
744
745        // Set security settings based on security type
746        match config.security_type {
747            WiFiSecurityType::None => {
748                // No security settings needed
749            }
750            WiFiSecurityType::Wep => {
751                security_settings.insert("key-mgmt".to_string(), Value::from("none"));
752                if let Some(password) = config.password.clone() {
753                    security_settings.insert("wep-key0".to_string(), Value::from(password));
754                }
755            }
756            WiFiSecurityType::WpaPsk => {
757                security_settings.insert("key-mgmt".to_string(), Value::from("wpa-psk"));
758                if let Some(password) = config.password.clone() {
759                    security_settings.insert("psk".to_string(), Value::from(password));
760                }
761            }
762            WiFiSecurityType::WpaEap => {
763                security_settings.insert("key-mgmt".to_string(), Value::from("wpa-eap"));
764                if let Some(password) = config.password.clone() {
765                    security_settings.insert("password".to_string(), Value::from(password));
766                }
767                if let Some(username) = config.username.clone() {
768                    security_settings.insert("identity".to_string(), Value::from(username));
769                }
770            }
771            WiFiSecurityType::Wpa2Psk => {
772                security_settings.insert("key-mgmt".to_string(), Value::from("wpa-psk"));
773                security_settings.insert("proto".to_string(), Value::from(vec!["rsn"]));
774                if let Some(password) = config.password.clone() {
775                    security_settings.insert("psk".to_string(), Value::from(password));
776                }
777            }
778            WiFiSecurityType::Wpa3Psk => {
779                security_settings.insert("key-mgmt".to_string(), Value::from("sae"));
780                if let Some(password) = config.password.clone() {
781                    security_settings.insert("psk".to_string(), Value::from(password));
782                }
783            }
784        }
785
786        connection_settings.insert("802-11-wireless".to_string(), wifi_settings);
787        if !security_settings.is_empty() {
788            connection_settings.insert("802-11-wireless-security".to_string(), security_settings);
789        }
790
791        // Log constructed settings for debugging
792        log::debug!("connection_settings: {:#?}", connection_settings);
793
794        // Crear un proxy para NetworkManager
795        let nm_proxy = zbus::blocking::Proxy::new(
796            &self.connection,
797            "org.freedesktop.NetworkManager",
798            "/org/freedesktop/NetworkManager",
799            "org.freedesktop.NetworkManager",
800        )?;
801
802        // Llamar al método AddAndActivateConnection con autoselección de NM
803        let any_path = zbus::zvariant::OwnedObjectPath::try_from("/").unwrap();
804        let call_result: zbus::Result<(zbus::zvariant::OwnedObjectPath, zbus::zvariant::OwnedObjectPath)> = nm_proxy.call("AddAndActivateConnection", &(connection_settings, &any_path, &any_path));
805
806        match call_result {
807            Ok((conn_path, active_path)) => {
808                log::info!(
809                    "AddAndActivateConnection succeeded for ssid='{}' conn='{}' active='{}'",
810                    config.ssid,
811                    conn_path.as_str(),
812                    active_path.as_str()
813                );
814            }
815            Err(e) => {
816                log::error!(
817                    "AddAndActivateConnection failed for ssid='{}': {:?}",
818                    config.ssid,
819                    e
820                );
821                return Err(e.into());
822            }
823        }
824
825        log::debug!("connect_to_wifi finished for ssid='{}'", config.ssid);
826
827        Ok(())
828    }
829
830    /// Toggle network state
831    pub fn toggle_network_state(&self, enabled: bool) -> Result<bool> {
832        let nm_proxy = zbus::blocking::Proxy::new(
833            &self.connection,
834            "org.freedesktop.NetworkManager",
835            "/org/freedesktop/NetworkManager",
836            "org.freedesktop.NetworkManager",
837        )?;
838
839        nm_proxy.set_property("NetworkingEnabled", enabled)?;
840
841        let current_state: bool = nm_proxy.get_property("NetworkingEnabled")?;
842        Ok(current_state)
843    }
844
845    /// Get wireless enabled state
846    pub fn get_wireless_enabled(&self) -> Result<bool> {
847        let nm_proxy = zbus::blocking::Proxy::new(
848            &self.connection,
849            "org.freedesktop.NetworkManager",
850            "/org/freedesktop/NetworkManager",
851            "org.freedesktop.NetworkManager",
852        )?;
853        Ok(nm_proxy.get_property("WirelessEnabled")?)
854    }
855
856    /// Set wireless enabled state
857    pub fn set_wireless_enabled(&self, enabled: bool) -> Result<()> {
858        let nm_proxy = zbus::blocking::Proxy::new(
859            &self.connection,
860            "org.freedesktop.NetworkManager",
861            "/org/freedesktop/NetworkManager",
862            "org.freedesktop.NetworkManager",
863        )?;
864        nm_proxy.set_property("WirelessEnabled", enabled)?;
865        Ok(())
866    }
867
868    /// Check if wireless device is available
869    pub fn is_wireless_available(&self) -> Result<bool> {
870        Ok(!self.wireless_device_paths()?.is_empty())
871    }
872
873    /// Listen for network changes
874    pub fn listen_network_changes(&self) -> Result<mpsc::Receiver<NetworkInfo>> {
875        let (tx, rx) = mpsc::channel();
876        let connection_clone = self.connection.clone();
877        let app_handle = self.app.clone();
878
879        std::thread::spawn(move || {
880            if let Ok(proxy) = zbus::blocking::Proxy::new(
881                &connection_clone,
882                "org.freedesktop.NetworkManager",
883                "/org/freedesktop/NetworkManager",
884                "org.freedesktop.NetworkManager",
885            ) {
886                if let Ok(signal) = proxy.receive_signal("StateChanged") {
887                    let proxy_props = zbus::blocking::fdo::PropertiesProxy::builder(
888                        &connection_clone,
889                    )
890                    .destination("org.freedesktop.NetworkManager")
891                    .unwrap()
892                    .path("/org/freedesktop/NetworkManager")
893                    .unwrap()
894                    .build()
895                    .unwrap();
896
897                    for _msg in signal {
898                        let network_manager = VSKNetworkManager {
899                            connection: connection_clone.clone(),
900                            proxy: proxy_props.clone(),
901                            app: app_handle.clone(),
902                        };
903
904                        if let Ok(network_info) =
905                            network_manager.get_current_network_state()
906                        {
907                            if tx.send(network_info).is_err() {
908                                break;
909                            }
910                        }
911                    }
912                }
913            }
914        });
915
916        Ok(rx)
917    }
918
919    /// Disconnect from the current WiFi network
920    pub fn disconnect_from_wifi(&self) -> Result<()> {
921        let nm_proxy = zbus::blocking::Proxy::new(
922            &self.connection,
923            "org.freedesktop.NetworkManager",
924            "/org/freedesktop/NetworkManager",
925            "org.freedesktop.NetworkManager",
926        )?;
927
928        // Obtener las conexiones activas
929        let active_connections_variant: zbus::zvariant::OwnedValue = self.proxy.get(
930            InterfaceName::from_static_str_unchecked("org.freedesktop.NetworkManager"),
931            "ActiveConnections",
932        )?;
933
934        // Convertir el valor a un vector de ObjectPath
935        let active_connections = match active_connections_variant.downcast_ref() {
936            Ok(zbus::zvariant::Value::Array(arr)) => arr
937                .iter()
938                .filter_map(|v| match v {
939                    zbus::zvariant::Value::ObjectPath(path) => {
940                        Some(zbus::zvariant::OwnedObjectPath::from(path.to_owned()))
941                    }
942                    _ => None,
943                })
944                .collect::<Vec<zbus::zvariant::OwnedObjectPath>>(),
945            _ => Vec::new(),
946        };
947
948        if !active_connections.is_empty() {
949            nm_proxy.call::<_, _, ()>("DeactivateConnection", &(&active_connections[0],))?;
950            Ok(())
951        } else {
952            Ok(())
953        }
954    }
955
956    /// Get the list of saved WiFi networks
957    pub fn get_saved_wifi_networks(&self) -> Result<Vec<NetworkInfo>> {
958        // Crear un proxy para el servicio de configuración de NetworkManager
959        let settings_proxy = zbus::blocking::Proxy::new(
960            &self.connection,
961            "org.freedesktop.NetworkManager",
962            "/org/freedesktop/NetworkManager/Settings",
963            "org.freedesktop.NetworkManager.Settings",
964        )?;
965
966        // Obtener todas las conexiones guardadas
967        let connections: Vec<zbus::zvariant::OwnedObjectPath> =
968            settings_proxy.call("ListConnections", &())?;
969        let mut saved_networks = Vec::new();
970
971        // Procesar cada conexión guardada
972        for conn_path in connections {
973            // Crear un proxy para cada conexión
974            let conn_proxy = zbus::blocking::Proxy::new(
975                &self.connection,
976                "org.freedesktop.NetworkManager",
977                conn_path.as_str(),
978                "org.freedesktop.NetworkManager.Settings.Connection",
979            )?;
980
981            // Obtener la configuración de la conexión como un HashMap
982            let settings: std::collections::HashMap<String, zbus::zvariant::OwnedValue> =
983                conn_proxy.call("GetSettings", &())?;
984
985            // Verificar si es una conexión WiFi
986            if let Some(connection) = settings.get("connection") {
987                let connection_dict = match connection.downcast_ref::<Value>()
988                    .and_then(|v| v.downcast::<std::collections::HashMap<String, zbus::zvariant::OwnedValue>>())
989                {
990                    Ok(dict) => dict,
991                    _ => continue,
992                };
993
994                // Verificar el tipo de conexión
995                if let Some(conn_type) = connection_dict.get("type") {
996                    let v_type: &zbus::zvariant::Value<'_> = conn_type;
997                    let conn_type_str = match v_type.downcast_ref::<String>() {
998                        Ok(s) => s,
999                        _ => continue,
1000                    };
1001
1002                    // Si es una conexión WiFi, extraer la información
1003                    if conn_type_str == "802-11-wireless" {
1004                        let mut network_info = NetworkInfo {
1005                            connection_type: "wifi".to_string(),
1006                            ..Default::default()
1007                        };
1008
1009                        // Obtener el nombre de la conexión
1010                        if let Some(id) = connection_dict.get("id") {
1011                            let v_id: &zbus::zvariant::Value<'_> = id;
1012                            if let Ok(name) = v_id.downcast_ref::<String>() {
1013                                network_info.name = name;
1014                            }
1015                        }
1016
1017                        // Obtener el SSID
1018                        if let Some(wireless) = settings.get("802-11-wireless") {
1019                            let wireless_dict = match wireless.downcast_ref::<Value>()
1020                                .and_then(|v| v.downcast::<std::collections::HashMap<String, zbus::zvariant::OwnedValue>>())
1021                            {
1022                                Ok(dict) => dict,
1023                                _ => continue,
1024                            };
1025
1026                            if let Some(ssid) = wireless_dict.get("ssid") {
1027                                let v_ssid: &zbus::zvariant::Value<'_> = ssid;
1028                                network_info.ssid = NetworkManagerHelpers::ssid_from_value(v_ssid);
1029                            }
1030                        }
1031
1032                        // Determinar el tipo de seguridad
1033                        if let Some(security) = settings.get("802-11-wireless-security") {
1034                            let security_dict = match security.downcast_ref::<Value>()
1035                                .and_then(|v| v.downcast::<std::collections::HashMap<String, zbus::zvariant::OwnedValue>>())
1036                            {
1037                                Ok(dict) => dict,
1038                                _ => {
1039                                    network_info.security_type = WiFiSecurityType::None;
1040                                    saved_networks.push(network_info);
1041                                    continue;
1042                                },
1043                            };
1044
1045                            if let Some(key_mgmt) = security_dict.get("key-mgmt") {
1046                                let v_km: &zbus::zvariant::Value<'_> = key_mgmt;
1047                                if let Ok(key_mgmt_str) = v_km.downcast_ref::<String>() {
1048                                    match key_mgmt_str.as_str() {
1049                                        "none" => {
1050                                            network_info.security_type = WiFiSecurityType::None
1051                                        }
1052                                        "wpa-psk" => {
1053                                            network_info.security_type = WiFiSecurityType::WpaPsk
1054                                        }
1055                                        "wpa-eap" => {
1056                                            network_info.security_type = WiFiSecurityType::WpaEap
1057                                        }
1058                                        _ => network_info.security_type = WiFiSecurityType::None,
1059                                    }
1060                                }
1061                            }
1062                        } else {
1063                            network_info.security_type = WiFiSecurityType::None;
1064                        }
1065
1066                        // Agregar a la lista de redes guardadas
1067                        saved_networks.push(network_info);
1068                    }
1069                }
1070            }
1071        }
1072
1073        Ok(saved_networks)
1074    }
1075
1076    /// Delete a saved WiFi connection by SSID
1077    pub fn delete_wifi_connection(&self, ssid: &str) -> Result<bool> {
1078        // Crear un proxy para el servicio de configuración de NetworkManager
1079        let settings_proxy = zbus::blocking::Proxy::new(
1080            &self.connection,
1081            "org.freedesktop.NetworkManager",
1082            "/org/freedesktop/NetworkManager/Settings",
1083            "org.freedesktop.NetworkManager.Settings",
1084        )?;
1085
1086        // Obtener todas las conexiones guardadas
1087        let connections: Vec<zbus::zvariant::OwnedObjectPath> =
1088            settings_proxy.call("ListConnections", &())?;
1089
1090        // Procesar cada conexión guardada
1091        for conn_path in connections {
1092            // Crear un proxy para cada conexión
1093            let conn_proxy = zbus::blocking::Proxy::new(
1094                &self.connection,
1095                "org.freedesktop.NetworkManager",
1096                conn_path.as_str(),
1097                "org.freedesktop.NetworkManager.Settings.Connection",
1098            )?;
1099
1100            // Obtener la configuración de la conexión como un HashMap
1101            let settings: std::collections::HashMap<String, zbus::zvariant::OwnedValue> =
1102                conn_proxy.call("GetSettings", &())?;
1103
1104            // Verificar si es una conexión WiFi
1105            if let Some(connection) = settings.get("connection") {
1106                let connection_dict = match connection.downcast_ref::<Value>()
1107                    .and_then(|v| v.downcast::<std::collections::HashMap<String, zbus::zvariant::OwnedValue>>())
1108                {
1109                    Ok(dict) => dict,
1110                    _ => continue,
1111                };
1112
1113                // Verificar el tipo de conexión
1114                if let Some(conn_type) = connection_dict.get("type") {
1115                    let v_type: &zbus::zvariant::Value<'_> = conn_type;
1116                    let conn_type_str = match v_type.downcast_ref::<String>() {
1117                        Ok(s) => s,
1118                        _ => continue,
1119                    };
1120
1121                    // Si es una conexión WiFi, verificar el SSID
1122                    if conn_type_str == "802-11-wireless" {
1123                        if let Some(wireless) = settings.get("802-11-wireless") {
1124                            let wireless_dict = match wireless.downcast_ref::<Value>()
1125                                .and_then(|v| v.downcast::<std::collections::HashMap<String, zbus::zvariant::OwnedValue>>())
1126                            {
1127                                Ok(dict) => dict,
1128                                _ => continue,
1129                            };
1130
1131                            if let Some(ssid_value) = wireless_dict.get("ssid") {
1132                                let v_ssid: &zbus::zvariant::Value<'_> = ssid_value;
1133                                let conn_ssid_str = NetworkManagerHelpers::ssid_from_value(v_ssid);
1134                                // Si el SSID coincide, eliminar la conexión
1135                                if conn_ssid_str == ssid {
1136                                    conn_proxy.call::<_, _, ()>("Delete", &())?;
1137                                    return Ok(true);
1138                                }
1139                            }
1140                        }
1141                    }
1142                }
1143            }
1144        }
1145
1146        // No se encontró ninguna conexión con el SSID especificado
1147        Ok(false)
1148    }
1149
1150    /// List saved VPN profiles from NetworkManager settings.
1151    pub fn list_vpn_profiles(&self) -> Result<Vec<VpnProfile>> {
1152        let connections = self.list_connection_paths()?;
1153        let mut profiles = Vec::new();
1154
1155        for conn_path in connections {
1156            let settings = self.get_connection_settings(&conn_path)?;
1157            if let Some(profile) = self.vpn_profile_from_settings(&settings) {
1158                profiles.push(profile);
1159            }
1160        }
1161
1162        profiles.sort_by(|a, b| a.id.cmp(&b.id));
1163        Ok(profiles)
1164    }
1165
1166    /// Get current VPN status from active connections.
1167    /// Whether an active connection is a tunnel, and how much of a VPN it is.
1168    ///
1169    /// NetworkManager only files as "vpn" what one of its own VPN plugins set
1170    /// up. A client that brings its own daemon — Twingate, Tailscale, ZeroTier,
1171    /// wg-quick — creates the interface itself, and NetworkManager sees a plain
1172    /// tun or wireguard device. It is still a VPN to whoever is using the
1173    /// machine: with Twingate connected, the control centre said there was no
1174    /// VPN while every packet was going through it.
1175    ///
1176    /// The number is how much of a VPN each kind is, for when there is more
1177    /// than one tunnel up: a profile NetworkManager manages says more than a
1178    /// tun device somebody else created.
1179    fn tunnel_priority(conn_type: &str) -> Option<u8> {
1180        match conn_type {
1181            "vpn" => Some(3),
1182            "wireguard" => Some(2),
1183            "tun" | "tap" | "ip-tunnel" => Some(1),
1184            _ => None,
1185        }
1186    }
1187
1188    /// The name to show for a tunnel NetworkManager did not create.
1189    ///
1190    /// Their connection id is the interface name, which tells nobody anything:
1191    /// "sdwan0" is Twingate, and there is nothing on the system that says so.
1192    /// The ones we know get their product name; the rest keep the id, which is
1193    /// still better than claiming there is no VPN.
1194    fn tunnel_display_name(id: &str, interface: Option<&str>) -> String {
1195        const CONOCIDAS: [(&str, &str); 7] = [
1196            ("sdwan", "Twingate"),
1197            ("tailscale", "Tailscale"),
1198            ("nordlynx", "NordVPN"),
1199            ("proton", "Proton VPN"),
1200            ("pvpn", "Proton VPN"),
1201            ("zt", "ZeroTier"),
1202            ("wg", "WireGuard"),
1203        ];
1204
1205        let nombre = interface.unwrap_or(id);
1206        for (prefijo, producto) in CONOCIDAS {
1207            if nombre.starts_with(prefijo) {
1208                return producto.to_string();
1209            }
1210        }
1211
1212        id.to_string()
1213    }
1214
1215    pub fn get_vpn_status(&self) -> Result<VpnStatus> {
1216        let active_connections_variant = self.proxy.get(
1217            InterfaceName::from_static_str_unchecked("org.freedesktop.NetworkManager"),
1218            "ActiveConnections",
1219        )?;
1220
1221        let mut mejor: Option<(u8, zbus::zvariant::OwnedObjectPath, String)> = None;
1222
1223        if let Ok(zbus::zvariant::Value::Array(arr)) = active_connections_variant.downcast_ref() {
1224            for value in arr.iter() {
1225                let active_path = match value {
1226                    zbus::zvariant::Value::ObjectPath(path) => path,
1227                    _ => continue,
1228                };
1229
1230                let active_props = zbus::blocking::fdo::PropertiesProxy::builder(&self.connection)
1231                    .destination("org.freedesktop.NetworkManager")?
1232                    .path(active_path)?
1233                    .build()?;
1234
1235                let conn_type_variant = active_props.get(
1236                    InterfaceName::from_static_str_unchecked(
1237                        "org.freedesktop.NetworkManager.Connection.Active",
1238                    ),
1239                    "Type",
1240                )?;
1241
1242                let conn_type = match conn_type_variant.downcast_ref() {
1243                    Ok(zbus::zvariant::Value::Str(v)) => v.to_string(),
1244                    _ => continue,
1245                };
1246
1247                let Some(prioridad) = Self::tunnel_priority(&conn_type) else {
1248                    continue;
1249                };
1250
1251                // `map_or` y no `is_none_or`: esta última es estable desde 1.82 y el
1252                // crate declara `rust-version = "1.77.2"`, así que con ella no
1253                // compilaba en el mínimo que promete.
1254                if mejor
1255                    .as_ref()
1256                    .map_or(true, |(previa, _, _)| prioridad > *previa)
1257                {
1258                    mejor = Some((
1259                        prioridad,
1260                        zbus::zvariant::OwnedObjectPath::from(active_path.to_owned()),
1261                        conn_type,
1262                    ));
1263                }
1264            }
1265        }
1266
1267        let Some((_, active_path, conn_type)) = mejor else {
1268            return Ok(VpnStatus::default());
1269        };
1270
1271        self.vpn_status_from_active(&active_path, &conn_type)
1272    }
1273
1274    /// Reads the status of one active tunnel.
1275    fn vpn_status_from_active(
1276        &self,
1277        active_path: &zbus::zvariant::OwnedObjectPath,
1278        conn_type: &str,
1279    ) -> Result<VpnStatus> {
1280        let mut status = VpnStatus::default();
1281
1282        let active_props = zbus::blocking::fdo::PropertiesProxy::builder(&self.connection)
1283            .destination("org.freedesktop.NetworkManager")?
1284            .path(active_path.as_ref())?
1285            .build()?;
1286
1287        let leer = |propiedad: &'static str| {
1288            active_props.get(
1289                InterfaceName::from_static_str_unchecked(
1290                    "org.freedesktop.NetworkManager.Connection.Active",
1291                ),
1292                propiedad,
1293            )
1294        };
1295
1296        let state = match leer("State")?.downcast_ref() {
1297            Ok(zbus::zvariant::Value::U32(v)) => v,
1298            _ => 0,
1299        };
1300        status.state = Self::vpn_state_from_active_state(state);
1301
1302        let id = match leer("Id")?.downcast_ref() {
1303            Ok(zbus::zvariant::Value::Str(v)) => v.to_string(),
1304            _ => String::new(),
1305        };
1306        status.active_profile_uuid = match leer("Uuid")?.downcast_ref() {
1307            Ok(zbus::zvariant::Value::Str(v)) => Some(v.to_string()),
1308            _ => None,
1309        };
1310
1311        // Una VPN de NetworkManager tiene nombre puesto por quien la creó; las
1312        // otras traen el nombre de la interfaz, que no le dice nada a nadie.
1313        status.managed_externally = conn_type != "vpn";
1314        status.interface = self.active_connection_interface(&active_props);
1315        status.active_profile_name = Some(if status.managed_externally {
1316            Self::tunnel_display_name(&id, status.interface.as_deref())
1317        } else {
1318            id.clone()
1319        });
1320        status.active_profile_id = Some(id);
1321
1322        if let Ok(zbus::zvariant::Value::ObjectPath(ip4_path)) =
1323            leer("Ip4Config")?.downcast_ref()
1324        {
1325            if ip4_path.as_str() != "/" {
1326                let ip4_props = zbus::blocking::fdo::PropertiesProxy::builder(&self.connection)
1327                    .destination("org.freedesktop.NetworkManager")?
1328                    .path(ip4_path)?
1329                    .build()?;
1330
1331                let leer_ip = |propiedad: &'static str| {
1332                    ip4_props.get(
1333                        InterfaceName::from_static_str_unchecked(
1334                            "org.freedesktop.NetworkManager.IP4Config",
1335                        ),
1336                        propiedad,
1337                    )
1338                };
1339
1340                if let Ok(gateway) = leer_ip("Gateway") {
1341                    status.gateway = match gateway.downcast_ref() {
1342                        Ok(zbus::zvariant::Value::Str(v)) if !v.is_empty() => Some(v.to_string()),
1343                        _ => None,
1344                    };
1345                }
1346
1347                // La dirección dentro del túnel: es lo que alguien mira para
1348                // saber con qué IP lo ve la red del otro lado.
1349                if let Ok(addresses) = leer_ip("Addresses") {
1350                    if let Ok(zbus::zvariant::Value::Array(arr)) = addresses.downcast_ref() {
1351                        if let Some(zbus::zvariant::Value::Array(tupla)) = arr.first() {
1352                            if let Some(zbus::zvariant::Value::U32(entero)) = tupla.first() {
1353                                use std::net::Ipv4Addr;
1354                                status.ip_address =
1355                                    Some(Ipv4Addr::from(entero.to_be()).to_string());
1356                            }
1357                        }
1358                    }
1359                }
1360            }
1361        }
1362
1363        Ok(status)
1364    }
1365
1366    /// The interface a tunnel came up on, asked to the device itself.
1367    fn active_connection_interface(
1368        &self,
1369        active_props: &zbus::blocking::fdo::PropertiesProxy,
1370    ) -> Option<String> {
1371        let devices = active_props
1372            .get(
1373                InterfaceName::from_static_str_unchecked(
1374                    "org.freedesktop.NetworkManager.Connection.Active",
1375                ),
1376                "Devices",
1377            )
1378            .ok()?;
1379
1380        let zbus::zvariant::Value::Array(arr) = devices.downcast_ref().ok()? else {
1381            return None;
1382        };
1383        let zbus::zvariant::Value::ObjectPath(device_path) = arr.first()? else {
1384            return None;
1385        };
1386
1387        let device_props = zbus::blocking::fdo::PropertiesProxy::builder(&self.connection)
1388            .destination("org.freedesktop.NetworkManager")
1389            .ok()?
1390            .path(device_path)
1391            .ok()?
1392            .build()
1393            .ok()?;
1394
1395        let interface = device_props
1396            .get(
1397                InterfaceName::from_static_str_unchecked("org.freedesktop.NetworkManager.Device"),
1398                "Interface",
1399            )
1400            .ok()?;
1401
1402        match interface.downcast_ref() {
1403            Ok(zbus::zvariant::Value::Str(v)) => Some(v.to_string()),
1404            _ => None,
1405        }
1406    }
1407
1408    /// Connect a VPN profile by UUID.
1409    pub fn connect_vpn(&self, uuid: String) -> Result<()> {
1410        let current_status = self.get_vpn_status()?;
1411        if current_status.state == VpnConnectionState::Connected
1412            && current_status.active_profile_uuid.as_deref() == Some(uuid.as_str())
1413        {
1414            return Err(crate::error::NetworkError::VpnAlreadyConnected(uuid));
1415        }
1416
1417        let conn_path = self.find_connection_path_by_uuid(&uuid)?;
1418
1419        let nm_proxy = zbus::blocking::Proxy::new(
1420            &self.connection,
1421            "org.freedesktop.NetworkManager",
1422            "/org/freedesktop/NetworkManager",
1423            "org.freedesktop.NetworkManager",
1424        )?;
1425
1426        let any_path = zbus::zvariant::OwnedObjectPath::try_from("/").unwrap();
1427        let activate_result: zbus::Result<zbus::zvariant::OwnedObjectPath> =
1428            nm_proxy.call("ActivateConnection", &(&conn_path, &any_path, &any_path));
1429
1430        match activate_result {
1431            Ok(_) => Ok(()),
1432            Err(e) => {
1433                let msg = e.to_string().to_lowercase();
1434                if msg.contains("secret") || msg.contains("authentication") {
1435                    Err(crate::error::NetworkError::VpnAuthFailed(e.to_string()))
1436                } else {
1437                    Err(crate::error::NetworkError::VpnActivationFailed(e.to_string()))
1438                }
1439            }
1440        }
1441    }
1442
1443    /// Disconnect VPN by UUID or disconnect active VPN if UUID is not provided.
1444    pub fn disconnect_vpn(&self, uuid: Option<String>) -> Result<()> {
1445        let active_connections_variant = self.proxy.get(
1446            InterfaceName::from_static_str_unchecked("org.freedesktop.NetworkManager"),
1447            "ActiveConnections",
1448        )?;
1449
1450        let mut target_active_connection: Option<zbus::zvariant::OwnedObjectPath> = None;
1451
1452        if let Ok(zbus::zvariant::Value::Array(arr)) = active_connections_variant.downcast_ref() {
1453            for value in arr.iter() {
1454                let active_path = match value {
1455                    zbus::zvariant::Value::ObjectPath(path) => {
1456                        zbus::zvariant::OwnedObjectPath::from(path.to_owned())
1457                    }
1458                    _ => continue,
1459                };
1460
1461                let active_props = zbus::blocking::fdo::PropertiesProxy::builder(&self.connection)
1462                    .destination("org.freedesktop.NetworkManager")?
1463                    .path(active_path.as_str())?
1464                    .build()?;
1465
1466                let conn_type_variant = active_props.get(
1467                    InterfaceName::from_static_str_unchecked(
1468                        "org.freedesktop.NetworkManager.Connection.Active",
1469                    ),
1470                    "Type",
1471                )?;
1472                let conn_type = match conn_type_variant.downcast_ref() {
1473                    Ok(zbus::zvariant::Value::Str(v)) => v.to_string(),
1474                    _ => continue,
1475                };
1476
1477                if conn_type != "vpn" {
1478                    continue;
1479                }
1480
1481                if let Some(target_uuid) = uuid.as_deref() {
1482                    let uuid_variant = active_props.get(
1483                        InterfaceName::from_static_str_unchecked(
1484                            "org.freedesktop.NetworkManager.Connection.Active",
1485                        ),
1486                        "Uuid",
1487                    )?;
1488                    let active_uuid = match uuid_variant.downcast_ref() {
1489                        Ok(zbus::zvariant::Value::Str(v)) => v.to_string(),
1490                        _ => continue,
1491                    };
1492
1493                    if active_uuid == target_uuid {
1494                        target_active_connection = Some(active_path.clone());
1495                        break;
1496                    }
1497                } else {
1498                    target_active_connection = Some(active_path.clone());
1499                    break;
1500                }
1501            }
1502        }
1503
1504        let target_active_connection = match target_active_connection {
1505            Some(path) => path,
1506            None => {
1507                if let Some(target_uuid) = uuid {
1508                    return Err(crate::error::NetworkError::VpnProfileNotFound(target_uuid));
1509                }
1510                return Err(crate::error::NetworkError::VpnNotActive);
1511            }
1512        };
1513
1514        let nm_proxy = zbus::blocking::Proxy::new(
1515            &self.connection,
1516            "org.freedesktop.NetworkManager",
1517            "/org/freedesktop/NetworkManager",
1518            "org.freedesktop.NetworkManager",
1519        )?;
1520
1521        nm_proxy.call::<_, _, ()>(
1522            "DeactivateConnection",
1523            &(target_active_connection.as_str(),),
1524        )?;
1525        Ok(())
1526    }
1527
1528    /// Create a new VPN profile in NetworkManager settings.
1529    pub fn create_vpn_profile(&self, config: VpnCreateConfig) -> Result<VpnProfile> {
1530        if config.id.trim().is_empty() {
1531            return Err(crate::error::NetworkError::VpnInvalidConfig(
1532                "id is required".to_string(),
1533            ));
1534        }
1535
1536        let uuid = Uuid::new_v4().to_string();
1537        let mut connection_section = HashMap::new();
1538        connection_section.insert("id".to_string(), Value::from(config.id.clone()));
1539        connection_section.insert("uuid".to_string(), Value::from(uuid.clone()));
1540        connection_section.insert("type".to_string(), Value::from("vpn"));
1541        connection_section.insert(
1542            "autoconnect".to_string(),
1543            Value::from(config.autoconnect.unwrap_or(false)),
1544        );
1545
1546        let mut vpn_section = HashMap::new();
1547        vpn_section.insert(
1548            "service-type".to_string(),
1549            Value::from(Self::service_type_from_vpn_type(&config.vpn_type)),
1550        );
1551
1552        if let Some(username) = config.username {
1553            vpn_section.insert("user-name".to_string(), Value::from(username));
1554        }
1555        if let Some(gateway) = config.gateway {
1556            vpn_section.insert("remote".to_string(), Value::from(gateway));
1557        }
1558        if let Some(ca_cert_path) = config.ca_cert_path {
1559            vpn_section.insert("ca".to_string(), Value::from(ca_cert_path));
1560        }
1561        if let Some(user_cert_path) = config.user_cert_path {
1562            vpn_section.insert("cert".to_string(), Value::from(user_cert_path));
1563        }
1564        if let Some(private_key_path) = config.private_key_path {
1565            vpn_section.insert("key".to_string(), Value::from(private_key_path));
1566        }
1567        if let Some(private_key_password) = config.private_key_password {
1568            vpn_section.insert("key-password".to_string(), Value::from(private_key_password));
1569        }
1570        if let Some(custom_settings) = config.settings {
1571            for (k, v) in custom_settings {
1572                vpn_section.insert(k, Value::from(v));
1573            }
1574        }
1575
1576        let mut vpn_secrets_section = HashMap::new();
1577        if let Some(password) = config.password {
1578            vpn_secrets_section.insert("password".to_string(), Value::from(password));
1579        }
1580        if let Some(custom_secrets) = config.secrets {
1581            for (k, v) in custom_secrets {
1582                vpn_secrets_section.insert(k, Value::from(v));
1583            }
1584        }
1585
1586        let mut settings: HashMap<String, HashMap<String, Value>> = HashMap::new();
1587        settings.insert("connection".to_string(), connection_section);
1588        settings.insert("vpn".to_string(), vpn_section);
1589        if !vpn_secrets_section.is_empty() {
1590            settings.insert("vpn-secrets".to_string(), vpn_secrets_section);
1591        }
1592
1593        let settings_proxy = zbus::blocking::Proxy::new(
1594            &self.connection,
1595            "org.freedesktop.NetworkManager",
1596            "/org/freedesktop/NetworkManager/Settings",
1597            "org.freedesktop.NetworkManager.Settings",
1598        )?;
1599
1600        let _created_path: zbus::zvariant::OwnedObjectPath =
1601            settings_proxy.call("AddConnection", &(settings,))?;
1602
1603        Ok(VpnProfile {
1604            id: config.id,
1605            uuid,
1606            vpn_type: config.vpn_type,
1607            interface_name: None,
1608            autoconnect: config.autoconnect.unwrap_or(false),
1609            editable: true,
1610            last_error: None,
1611        })
1612    }
1613
1614    /// Update an existing VPN profile by UUID.
1615    pub fn update_vpn_profile(&self, config: VpnUpdateConfig) -> Result<VpnProfile> {
1616        let conn_path = self.find_connection_path_by_uuid(&config.uuid)?;
1617        let existing_settings = self.get_connection_settings(&conn_path)?;
1618
1619        let existing_profile = self
1620            .vpn_profile_from_settings(&existing_settings)
1621            .ok_or_else(|| crate::error::NetworkError::VpnProfileNotFound(config.uuid.clone()))?;
1622
1623        let existing_vpn_settings = Self::string_map_from_section(&existing_settings, "vpn");
1624        let existing_vpn_secrets = Self::string_map_from_section(&existing_settings, "vpn-secrets");
1625
1626        // Start from the full current settings map to preserve unrelated sections
1627        // (IPv4/IPv6, routes, DNS, permissions, proxy, etc.).
1628        let mut settings: HashMap<String, HashMap<String, Value>> = HashMap::new();
1629        for (section_name, dict) in &existing_settings {
1630            let mut section_map: HashMap<String, Value> = HashMap::new();
1631            for (k, v) in dict {
1632                if let Ok(val) = v.downcast_ref::<Value>() {
1633                    section_map.insert(k.clone(), val);
1634                }
1635            }
1636            settings.insert(section_name.clone(), section_map);
1637        }
1638
1639        let connection_section = settings
1640            .entry("connection".to_string())
1641            .or_default();
1642        connection_section.insert(
1643            "id".to_string(),
1644            Value::from(config.id.clone().unwrap_or(existing_profile.id.clone())),
1645        );
1646        connection_section.insert("uuid".to_string(), Value::from(config.uuid.clone()));
1647        connection_section.insert("type".to_string(), Value::from("vpn"));
1648        connection_section.insert(
1649            "autoconnect".to_string(),
1650            Value::from(config.autoconnect.unwrap_or(existing_profile.autoconnect)),
1651        );
1652
1653        let service_type = existing_vpn_settings
1654            .get("service-type")
1655            .cloned()
1656            .unwrap_or_else(|| {
1657                Self::service_type_from_vpn_type(&existing_profile.vpn_type).to_string()
1658            });
1659
1660        let vpn_section = settings
1661            .entry("vpn".to_string())
1662            .or_default();
1663        vpn_section.insert("service-type".to_string(), Value::from(service_type));
1664
1665        let mut merged_settings = existing_vpn_settings;
1666        if let Some(username) = config.username {
1667            merged_settings.insert("user-name".to_string(), username);
1668        }
1669        if let Some(gateway) = config.gateway {
1670            merged_settings.insert("remote".to_string(), gateway);
1671        }
1672        if let Some(ca_cert_path) = config.ca_cert_path {
1673            merged_settings.insert("ca".to_string(), ca_cert_path);
1674        }
1675        if let Some(user_cert_path) = config.user_cert_path {
1676            merged_settings.insert("cert".to_string(), user_cert_path);
1677        }
1678        if let Some(private_key_path) = config.private_key_path {
1679            merged_settings.insert("key".to_string(), private_key_path);
1680        }
1681        if let Some(private_key_password) = config.private_key_password {
1682            merged_settings.insert("key-password".to_string(), private_key_password);
1683        }
1684        if let Some(custom_settings) = config.settings {
1685            for (k, v) in custom_settings {
1686                merged_settings.insert(k, v);
1687            }
1688        }
1689
1690        for (k, v) in merged_settings {
1691            vpn_section.insert(k, Value::from(v));
1692        }
1693
1694        let mut merged_secrets = existing_vpn_secrets;
1695        if let Some(password) = config.password {
1696            merged_secrets.insert("password".to_string(), password);
1697        }
1698        if let Some(custom_secrets) = config.secrets {
1699            for (k, v) in custom_secrets {
1700                merged_secrets.insert(k, v);
1701            }
1702        }
1703
1704        if merged_secrets.is_empty() {
1705            settings.remove("vpn-secrets");
1706        } else {
1707            let vpn_secrets_section = settings
1708                .entry("vpn-secrets".to_string())
1709                .or_default();
1710            vpn_secrets_section.clear();
1711            for (k, v) in merged_secrets {
1712                vpn_secrets_section.insert(k, Value::from(v));
1713            }
1714        }
1715
1716        let conn_proxy = zbus::blocking::Proxy::new(
1717            &self.connection,
1718            "org.freedesktop.NetworkManager",
1719            conn_path.as_str(),
1720            "org.freedesktop.NetworkManager.Settings.Connection",
1721        )?;
1722        conn_proxy.call::<_, _, ()>("Update", &(settings,))?;
1723
1724        let updated_settings = self.get_connection_settings(&conn_path)?;
1725        self.vpn_profile_from_settings(&updated_settings)
1726            .ok_or(crate::error::NetworkError::VpnProfileNotFound(config.uuid))
1727    }
1728
1729    /// Delete a VPN profile by UUID.
1730    pub fn delete_vpn_profile(&self, uuid: String) -> Result<()> {
1731        let conn_path = self.find_connection_path_by_uuid(&uuid)?;
1732
1733        let conn_proxy = zbus::blocking::Proxy::new(
1734            &self.connection,
1735            "org.freedesktop.NetworkManager",
1736            conn_path.as_str(),
1737            "org.freedesktop.NetworkManager.Settings.Connection",
1738        )?;
1739
1740        conn_proxy.call::<_, _, ()>("Delete", &())?;
1741        Ok(())
1742    }
1743}
1744
1745/// Initialize the network manager plugin
1746pub fn init(
1747    app: &AppHandle<tauri::Wry>,
1748    _api: PluginApi<tauri::Wry, ()>,
1749) -> Result<VSKNetworkManager<'static, tauri::Wry>> {
1750    VSKNetworkManager::new(app.clone())
1751}
1752
1753#[cfg(test)]
1754mod tests {
1755    use super::*;
1756
1757    #[test]
1758    fn vpn_state_deactivated_maps_to_disconnected() {
1759        assert_eq!(
1760            VSKNetworkManager::<tauri::Wry>::vpn_state_from_active_state(4),
1761            VpnConnectionState::Disconnected
1762        );
1763    }
1764
1765    /// El caso que motivó todo esto: con Twingate conectado, el centro de
1766    /// control decía que no había VPN. Twingate levanta su propio túnel, así
1767    /// que NetworkManager lo archiva como un `tun` cualquiera y no como una
1768    /// "vpn" suya.
1769    #[test]
1770    fn un_tun_ajeno_tambien_es_una_vpn() {
1771        type NM = VSKNetworkManager<'static, tauri::Wry>;
1772
1773        assert!(NM::tunnel_priority("tun").is_some(), "Twingate quedaba afuera");
1774        assert!(NM::tunnel_priority("wireguard").is_some());
1775        assert!(NM::tunnel_priority("vpn").is_some());
1776
1777        // Y lo que no es un túnel sigue sin serlo: si el wifi contara como VPN,
1778        // el indicador estaría siempre encendido.
1779        assert!(NM::tunnel_priority("802-11-wireless").is_none());
1780        assert!(NM::tunnel_priority("ethernet").is_none());
1781        assert!(NM::tunnel_priority("loopback").is_none());
1782        assert!(NM::tunnel_priority("bridge").is_none());
1783    }
1784
1785    /// Con varios túneles arriba manda el que NetworkManager administra: es el
1786    /// único que además se puede conectar y desconectar desde el escritorio.
1787    #[test]
1788    fn una_vpn_de_networkmanager_le_gana_a_un_tun() {
1789        type NM = VSKNetworkManager<'static, tauri::Wry>;
1790        assert!(NM::tunnel_priority("vpn") > NM::tunnel_priority("wireguard"));
1791        assert!(NM::tunnel_priority("wireguard") > NM::tunnel_priority("tun"));
1792    }
1793
1794    /// Contra el NetworkManager de verdad: que las propiedades que leemos
1795    /// existan y digan lo que creemos. Es donde se esconden los errores de una
1796    /// interfaz D-Bus, y ningún test puro los encuentra.
1797    ///
1798    /// Ignorado por defecto porque necesita un sistema con NetworkManager y una
1799    /// VPN levantada: `cargo test -- --ignored --nocapture`.
1800    #[test]
1801    #[ignore]
1802    fn el_tunel_de_esta_maquina_se_ve_como_vpn() {
1803        type NM = VSKNetworkManager<'static, tauri::Wry>;
1804        use zbus::names::InterfaceName;
1805        use zbus::zvariant::Value;
1806
1807        let conexion = zbus::blocking::Connection::system().expect("sin bus del sistema");
1808        let nm = zbus::blocking::fdo::PropertiesProxy::builder(&conexion)
1809            .destination("org.freedesktop.NetworkManager").unwrap()
1810            .path("/org/freedesktop/NetworkManager").unwrap()
1811            .build()
1812            .expect("NetworkManager no está");
1813
1814        let activas = nm
1815            .get(
1816                InterfaceName::from_static_str_unchecked("org.freedesktop.NetworkManager"),
1817                "ActiveConnections",
1818            )
1819            .unwrap();
1820
1821        let mut encontrados = Vec::new();
1822        if let Ok(Value::Array(arr)) = activas.downcast_ref() {
1823            for valor in arr.iter() {
1824                let Value::ObjectPath(ruta) = valor else { continue };
1825                let props = zbus::blocking::fdo::PropertiesProxy::builder(&conexion)
1826                    .destination("org.freedesktop.NetworkManager").unwrap()
1827                    .path(ruta).unwrap()
1828                    .build()
1829                    .unwrap();
1830                let leer = |p: &'static str| {
1831                    props.get(
1832                        InterfaceName::from_static_str_unchecked(
1833                            "org.freedesktop.NetworkManager.Connection.Active",
1834                        ),
1835                        p,
1836                    )
1837                };
1838                let tipo = match leer("Type").unwrap().downcast_ref() {
1839                    Ok(Value::Str(v)) => v.to_string(),
1840                    _ => continue,
1841                };
1842                let id = match leer("Id").unwrap().downcast_ref() {
1843                    Ok(Value::Str(v)) => v.to_string(),
1844                    _ => String::new(),
1845                };
1846                match NM::tunnel_priority(&tipo) {
1847                    Some(prioridad) => {
1848                        let nombre = NM::tunnel_display_name(&id, Some(&id));
1849                        println!("túnel: id={id} tipo={tipo} prioridad={prioridad} se muestra como «{nombre}»");
1850                        encontrados.push(nombre);
1851                    }
1852                    None => println!("no es túnel: id={id} tipo={tipo}"),
1853                }
1854            }
1855        }
1856
1857        assert!(
1858            !encontrados.is_empty(),
1859            "ningún túnel detectado; levantá la VPN antes de correr esto"
1860        );
1861    }
1862
1863    /// «sdwan0» no le dice nada a nadie; «Twingate» sí.
1864    #[test]
1865    fn los_tuneles_conocidos_se_muestran_con_su_nombre() {
1866        type NM = VSKNetworkManager<'static, tauri::Wry>;
1867
1868        assert_eq!(NM::tunnel_display_name("sdwan0", Some("sdwan0")), "Twingate");
1869        assert_eq!(NM::tunnel_display_name("tailscale0", Some("tailscale0")), "Tailscale");
1870        assert_eq!(NM::tunnel_display_name("wg0", Some("wg0")), "WireGuard");
1871
1872        // Uno desconocido conserva el nombre que le puso NetworkManager: es
1873        // menos de lo que querríamos, pero muchísimo más que decir que no hay
1874        // ninguna VPN.
1875        assert_eq!(NM::tunnel_display_name("Oficina", Some("tun0")), "Oficina");
1876    }
1877}