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    /// Whether an active connection is a tunnel, and how much of a VPN it is.
1165    ///
1166    /// NetworkManager only files as "vpn" what one of its own VPN plugins set
1167    /// up. A client that brings its own daemon — Twingate, Tailscale, ZeroTier,
1168    /// wg-quick — creates the interface itself, and NetworkManager sees a plain
1169    /// tun or wireguard device. It is still a VPN to whoever is using the
1170    /// machine: with Twingate connected, the control centre said there was no
1171    /// VPN while every packet was going through it.
1172    ///
1173    /// The number is how much of a VPN each kind is, for when there is more
1174    /// than one tunnel up: a profile NetworkManager manages says more than a
1175    /// tun device somebody else created.
1176    fn tunnel_priority(conn_type: &str) -> Option<u8> {
1177        match conn_type {
1178            "vpn" => Some(3),
1179            "wireguard" => Some(2),
1180            "tun" | "tap" | "ip-tunnel" => Some(1),
1181            _ => None,
1182        }
1183    }
1184
1185    /// The name to show for a tunnel NetworkManager did not create.
1186    ///
1187    /// Their connection id is the interface name, which tells nobody anything:
1188    /// "sdwan0" is Twingate, and there is nothing on the system that says so.
1189    /// The ones we know get their product name; the rest keep the id, which is
1190    /// still better than claiming there is no VPN.
1191    fn tunnel_display_name(id: &str, interface: Option<&str>) -> String {
1192        const CONOCIDAS: [(&str, &str); 7] = [
1193            ("sdwan", "Twingate"),
1194            ("tailscale", "Tailscale"),
1195            ("nordlynx", "NordVPN"),
1196            ("proton", "Proton VPN"),
1197            ("pvpn", "Proton VPN"),
1198            ("zt", "ZeroTier"),
1199            ("wg", "WireGuard"),
1200        ];
1201
1202        let nombre = interface.unwrap_or(id);
1203        for (prefijo, producto) in CONOCIDAS {
1204            if nombre.starts_with(prefijo) {
1205                return producto.to_string();
1206            }
1207        }
1208
1209        id.to_string()
1210    }
1211
1212    pub fn get_vpn_status(&self) -> Result<VpnStatus> {
1213        let active_connections_variant = self.proxy.get(
1214            InterfaceName::from_static_str_unchecked("org.freedesktop.NetworkManager"),
1215            "ActiveConnections",
1216        )?;
1217
1218        let mut mejor: Option<(u8, zbus::zvariant::OwnedObjectPath, String)> = None;
1219
1220        if let Ok(zbus::zvariant::Value::Array(arr)) = active_connections_variant.downcast_ref() {
1221            for value in arr.iter() {
1222                let active_path = match value {
1223                    zbus::zvariant::Value::ObjectPath(path) => path,
1224                    _ => continue,
1225                };
1226
1227                let active_props = zbus::blocking::fdo::PropertiesProxy::builder(&self.connection)
1228                    .destination("org.freedesktop.NetworkManager")?
1229                    .path(active_path)?
1230                    .build()?;
1231
1232                let conn_type_variant = active_props.get(
1233                    InterfaceName::from_static_str_unchecked(
1234                        "org.freedesktop.NetworkManager.Connection.Active",
1235                    ),
1236                    "Type",
1237                )?;
1238
1239                let conn_type = match conn_type_variant.downcast_ref() {
1240                    Ok(zbus::zvariant::Value::Str(v)) => v.to_string(),
1241                    _ => continue,
1242                };
1243
1244                let Some(prioridad) = Self::tunnel_priority(&conn_type) else {
1245                    continue;
1246                };
1247
1248                if mejor.as_ref().is_none_or(|(previa, _, _)| prioridad > *previa) {
1249                    mejor = Some((
1250                        prioridad,
1251                        zbus::zvariant::OwnedObjectPath::from(active_path.to_owned()),
1252                        conn_type,
1253                    ));
1254                }
1255            }
1256        }
1257
1258        let Some((_, active_path, conn_type)) = mejor else {
1259            return Ok(VpnStatus::default());
1260        };
1261
1262        self.vpn_status_from_active(&active_path, &conn_type)
1263    }
1264
1265    /// Reads the status of one active tunnel.
1266    fn vpn_status_from_active(
1267        &self,
1268        active_path: &zbus::zvariant::OwnedObjectPath,
1269        conn_type: &str,
1270    ) -> Result<VpnStatus> {
1271        let mut status = VpnStatus::default();
1272
1273        let active_props = zbus::blocking::fdo::PropertiesProxy::builder(&self.connection)
1274            .destination("org.freedesktop.NetworkManager")?
1275            .path(active_path.as_ref())?
1276            .build()?;
1277
1278        let leer = |propiedad: &'static str| {
1279            active_props.get(
1280                InterfaceName::from_static_str_unchecked(
1281                    "org.freedesktop.NetworkManager.Connection.Active",
1282                ),
1283                propiedad,
1284            )
1285        };
1286
1287        let state = match leer("State")?.downcast_ref() {
1288            Ok(zbus::zvariant::Value::U32(v)) => v,
1289            _ => 0,
1290        };
1291        status.state = Self::vpn_state_from_active_state(state);
1292
1293        let id = match leer("Id")?.downcast_ref() {
1294            Ok(zbus::zvariant::Value::Str(v)) => v.to_string(),
1295            _ => String::new(),
1296        };
1297        status.active_profile_uuid = match leer("Uuid")?.downcast_ref() {
1298            Ok(zbus::zvariant::Value::Str(v)) => Some(v.to_string()),
1299            _ => None,
1300        };
1301
1302        // Una VPN de NetworkManager tiene nombre puesto por quien la creó; las
1303        // otras traen el nombre de la interfaz, que no le dice nada a nadie.
1304        status.managed_externally = conn_type != "vpn";
1305        status.interface = self.active_connection_interface(&active_props);
1306        status.active_profile_name = Some(if status.managed_externally {
1307            Self::tunnel_display_name(&id, status.interface.as_deref())
1308        } else {
1309            id.clone()
1310        });
1311        status.active_profile_id = Some(id);
1312
1313        if let Ok(zbus::zvariant::Value::ObjectPath(ip4_path)) =
1314            leer("Ip4Config")?.downcast_ref()
1315        {
1316            if ip4_path.as_str() != "/" {
1317                let ip4_props = zbus::blocking::fdo::PropertiesProxy::builder(&self.connection)
1318                    .destination("org.freedesktop.NetworkManager")?
1319                    .path(ip4_path)?
1320                    .build()?;
1321
1322                let leer_ip = |propiedad: &'static str| {
1323                    ip4_props.get(
1324                        InterfaceName::from_static_str_unchecked(
1325                            "org.freedesktop.NetworkManager.IP4Config",
1326                        ),
1327                        propiedad,
1328                    )
1329                };
1330
1331                if let Ok(gateway) = leer_ip("Gateway") {
1332                    status.gateway = match gateway.downcast_ref() {
1333                        Ok(zbus::zvariant::Value::Str(v)) if !v.is_empty() => Some(v.to_string()),
1334                        _ => None,
1335                    };
1336                }
1337
1338                // La dirección dentro del túnel: es lo que alguien mira para
1339                // saber con qué IP lo ve la red del otro lado.
1340                if let Ok(addresses) = leer_ip("Addresses") {
1341                    if let Ok(zbus::zvariant::Value::Array(arr)) = addresses.downcast_ref() {
1342                        if let Some(zbus::zvariant::Value::Array(tupla)) = arr.first() {
1343                            if let Some(zbus::zvariant::Value::U32(entero)) = tupla.first() {
1344                                use std::net::Ipv4Addr;
1345                                status.ip_address =
1346                                    Some(Ipv4Addr::from(entero.to_be()).to_string());
1347                            }
1348                        }
1349                    }
1350                }
1351            }
1352        }
1353
1354        Ok(status)
1355    }
1356
1357    /// The interface a tunnel came up on, asked to the device itself.
1358    fn active_connection_interface(
1359        &self,
1360        active_props: &zbus::blocking::fdo::PropertiesProxy,
1361    ) -> Option<String> {
1362        let devices = active_props
1363            .get(
1364                InterfaceName::from_static_str_unchecked(
1365                    "org.freedesktop.NetworkManager.Connection.Active",
1366                ),
1367                "Devices",
1368            )
1369            .ok()?;
1370
1371        let zbus::zvariant::Value::Array(arr) = devices.downcast_ref().ok()? else {
1372            return None;
1373        };
1374        let zbus::zvariant::Value::ObjectPath(device_path) = arr.first()? else {
1375            return None;
1376        };
1377
1378        let device_props = zbus::blocking::fdo::PropertiesProxy::builder(&self.connection)
1379            .destination("org.freedesktop.NetworkManager")
1380            .ok()?
1381            .path(device_path)
1382            .ok()?
1383            .build()
1384            .ok()?;
1385
1386        let interface = device_props
1387            .get(
1388                InterfaceName::from_static_str_unchecked("org.freedesktop.NetworkManager.Device"),
1389                "Interface",
1390            )
1391            .ok()?;
1392
1393        match interface.downcast_ref() {
1394            Ok(zbus::zvariant::Value::Str(v)) => Some(v.to_string()),
1395            _ => None,
1396        }
1397    }
1398
1399    /// Connect a VPN profile by UUID.
1400    pub fn connect_vpn(&self, uuid: String) -> Result<()> {
1401        let current_status = self.get_vpn_status()?;
1402        if current_status.state == VpnConnectionState::Connected
1403            && current_status.active_profile_uuid.as_deref() == Some(uuid.as_str())
1404        {
1405            return Err(crate::error::NetworkError::VpnAlreadyConnected(uuid));
1406        }
1407
1408        let conn_path = self.find_connection_path_by_uuid(&uuid)?;
1409
1410        let nm_proxy = zbus::blocking::Proxy::new(
1411            &self.connection,
1412            "org.freedesktop.NetworkManager",
1413            "/org/freedesktop/NetworkManager",
1414            "org.freedesktop.NetworkManager",
1415        )?;
1416
1417        let any_path = zbus::zvariant::OwnedObjectPath::try_from("/").unwrap();
1418        let activate_result: zbus::Result<zbus::zvariant::OwnedObjectPath> =
1419            nm_proxy.call("ActivateConnection", &(&conn_path, &any_path, &any_path));
1420
1421        match activate_result {
1422            Ok(_) => Ok(()),
1423            Err(e) => {
1424                let msg = e.to_string().to_lowercase();
1425                if msg.contains("secret") || msg.contains("authentication") {
1426                    Err(crate::error::NetworkError::VpnAuthFailed(e.to_string()))
1427                } else {
1428                    Err(crate::error::NetworkError::VpnActivationFailed(e.to_string()))
1429                }
1430            }
1431        }
1432    }
1433
1434    /// Disconnect VPN by UUID or disconnect active VPN if UUID is not provided.
1435    pub fn disconnect_vpn(&self, uuid: Option<String>) -> Result<()> {
1436        let active_connections_variant = self.proxy.get(
1437            InterfaceName::from_static_str_unchecked("org.freedesktop.NetworkManager"),
1438            "ActiveConnections",
1439        )?;
1440
1441        let mut target_active_connection: Option<zbus::zvariant::OwnedObjectPath> = None;
1442
1443        if let Ok(zbus::zvariant::Value::Array(arr)) = active_connections_variant.downcast_ref() {
1444            for value in arr.iter() {
1445                let active_path = match value {
1446                    zbus::zvariant::Value::ObjectPath(path) => {
1447                        zbus::zvariant::OwnedObjectPath::from(path.to_owned())
1448                    }
1449                    _ => continue,
1450                };
1451
1452                let active_props = zbus::blocking::fdo::PropertiesProxy::builder(&self.connection)
1453                    .destination("org.freedesktop.NetworkManager")?
1454                    .path(active_path.as_str())?
1455                    .build()?;
1456
1457                let conn_type_variant = active_props.get(
1458                    InterfaceName::from_static_str_unchecked(
1459                        "org.freedesktop.NetworkManager.Connection.Active",
1460                    ),
1461                    "Type",
1462                )?;
1463                let conn_type = match conn_type_variant.downcast_ref() {
1464                    Ok(zbus::zvariant::Value::Str(v)) => v.to_string(),
1465                    _ => continue,
1466                };
1467
1468                if conn_type != "vpn" {
1469                    continue;
1470                }
1471
1472                if let Some(target_uuid) = uuid.as_deref() {
1473                    let uuid_variant = active_props.get(
1474                        InterfaceName::from_static_str_unchecked(
1475                            "org.freedesktop.NetworkManager.Connection.Active",
1476                        ),
1477                        "Uuid",
1478                    )?;
1479                    let active_uuid = match uuid_variant.downcast_ref() {
1480                        Ok(zbus::zvariant::Value::Str(v)) => v.to_string(),
1481                        _ => continue,
1482                    };
1483
1484                    if active_uuid == target_uuid {
1485                        target_active_connection = Some(active_path.clone());
1486                        break;
1487                    }
1488                } else {
1489                    target_active_connection = Some(active_path.clone());
1490                    break;
1491                }
1492            }
1493        }
1494
1495        let target_active_connection = match target_active_connection {
1496            Some(path) => path,
1497            None => {
1498                if let Some(target_uuid) = uuid {
1499                    return Err(crate::error::NetworkError::VpnProfileNotFound(target_uuid));
1500                }
1501                return Err(crate::error::NetworkError::VpnNotActive);
1502            }
1503        };
1504
1505        let nm_proxy = zbus::blocking::Proxy::new(
1506            &self.connection,
1507            "org.freedesktop.NetworkManager",
1508            "/org/freedesktop/NetworkManager",
1509            "org.freedesktop.NetworkManager",
1510        )?;
1511
1512        nm_proxy.call::<_, _, ()>(
1513            "DeactivateConnection",
1514            &(target_active_connection.as_str(),),
1515        )?;
1516        Ok(())
1517    }
1518
1519    /// Create a new VPN profile in NetworkManager settings.
1520    pub fn create_vpn_profile(&self, config: VpnCreateConfig) -> Result<VpnProfile> {
1521        if config.id.trim().is_empty() {
1522            return Err(crate::error::NetworkError::VpnInvalidConfig(
1523                "id is required".to_string(),
1524            ));
1525        }
1526
1527        let uuid = Uuid::new_v4().to_string();
1528        let mut connection_section = HashMap::new();
1529        connection_section.insert("id".to_string(), Value::from(config.id.clone()));
1530        connection_section.insert("uuid".to_string(), Value::from(uuid.clone()));
1531        connection_section.insert("type".to_string(), Value::from("vpn"));
1532        connection_section.insert(
1533            "autoconnect".to_string(),
1534            Value::from(config.autoconnect.unwrap_or(false)),
1535        );
1536
1537        let mut vpn_section = HashMap::new();
1538        vpn_section.insert(
1539            "service-type".to_string(),
1540            Value::from(Self::service_type_from_vpn_type(&config.vpn_type)),
1541        );
1542
1543        if let Some(username) = config.username {
1544            vpn_section.insert("user-name".to_string(), Value::from(username));
1545        }
1546        if let Some(gateway) = config.gateway {
1547            vpn_section.insert("remote".to_string(), Value::from(gateway));
1548        }
1549        if let Some(ca_cert_path) = config.ca_cert_path {
1550            vpn_section.insert("ca".to_string(), Value::from(ca_cert_path));
1551        }
1552        if let Some(user_cert_path) = config.user_cert_path {
1553            vpn_section.insert("cert".to_string(), Value::from(user_cert_path));
1554        }
1555        if let Some(private_key_path) = config.private_key_path {
1556            vpn_section.insert("key".to_string(), Value::from(private_key_path));
1557        }
1558        if let Some(private_key_password) = config.private_key_password {
1559            vpn_section.insert("key-password".to_string(), Value::from(private_key_password));
1560        }
1561        if let Some(custom_settings) = config.settings {
1562            for (k, v) in custom_settings {
1563                vpn_section.insert(k, Value::from(v));
1564            }
1565        }
1566
1567        let mut vpn_secrets_section = HashMap::new();
1568        if let Some(password) = config.password {
1569            vpn_secrets_section.insert("password".to_string(), Value::from(password));
1570        }
1571        if let Some(custom_secrets) = config.secrets {
1572            for (k, v) in custom_secrets {
1573                vpn_secrets_section.insert(k, Value::from(v));
1574            }
1575        }
1576
1577        let mut settings: HashMap<String, HashMap<String, Value>> = HashMap::new();
1578        settings.insert("connection".to_string(), connection_section);
1579        settings.insert("vpn".to_string(), vpn_section);
1580        if !vpn_secrets_section.is_empty() {
1581            settings.insert("vpn-secrets".to_string(), vpn_secrets_section);
1582        }
1583
1584        let settings_proxy = zbus::blocking::Proxy::new(
1585            &self.connection,
1586            "org.freedesktop.NetworkManager",
1587            "/org/freedesktop/NetworkManager/Settings",
1588            "org.freedesktop.NetworkManager.Settings",
1589        )?;
1590
1591        let _created_path: zbus::zvariant::OwnedObjectPath =
1592            settings_proxy.call("AddConnection", &(settings,))?;
1593
1594        Ok(VpnProfile {
1595            id: config.id,
1596            uuid,
1597            vpn_type: config.vpn_type,
1598            interface_name: None,
1599            autoconnect: config.autoconnect.unwrap_or(false),
1600            editable: true,
1601            last_error: None,
1602        })
1603    }
1604
1605    /// Update an existing VPN profile by UUID.
1606    pub fn update_vpn_profile(&self, config: VpnUpdateConfig) -> Result<VpnProfile> {
1607        let conn_path = self.find_connection_path_by_uuid(&config.uuid)?;
1608        let existing_settings = self.get_connection_settings(&conn_path)?;
1609
1610        let existing_profile = self
1611            .vpn_profile_from_settings(&existing_settings)
1612            .ok_or_else(|| crate::error::NetworkError::VpnProfileNotFound(config.uuid.clone()))?;
1613
1614        let existing_vpn_settings = Self::string_map_from_section(&existing_settings, "vpn");
1615        let existing_vpn_secrets = Self::string_map_from_section(&existing_settings, "vpn-secrets");
1616
1617        // Start from the full current settings map to preserve unrelated sections
1618        // (IPv4/IPv6, routes, DNS, permissions, proxy, etc.).
1619        let mut settings: HashMap<String, HashMap<String, Value>> = HashMap::new();
1620        for (section_name, dict) in &existing_settings {
1621            let mut section_map: HashMap<String, Value> = HashMap::new();
1622            for (k, v) in dict {
1623                if let Ok(val) = v.downcast_ref::<Value>() {
1624                    section_map.insert(k.clone(), val);
1625                }
1626            }
1627            settings.insert(section_name.clone(), section_map);
1628        }
1629
1630        let connection_section = settings
1631            .entry("connection".to_string())
1632            .or_insert_with(HashMap::new);
1633        connection_section.insert(
1634            "id".to_string(),
1635            Value::from(config.id.clone().unwrap_or(existing_profile.id.clone())),
1636        );
1637        connection_section.insert("uuid".to_string(), Value::from(config.uuid.clone()));
1638        connection_section.insert("type".to_string(), Value::from("vpn"));
1639        connection_section.insert(
1640            "autoconnect".to_string(),
1641            Value::from(config.autoconnect.unwrap_or(existing_profile.autoconnect)),
1642        );
1643
1644        let service_type = existing_vpn_settings
1645            .get("service-type")
1646            .cloned()
1647            .unwrap_or_else(|| {
1648                Self::service_type_from_vpn_type(&existing_profile.vpn_type).to_string()
1649            });
1650
1651        let vpn_section = settings
1652            .entry("vpn".to_string())
1653            .or_insert_with(HashMap::new);
1654        vpn_section.insert("service-type".to_string(), Value::from(service_type));
1655
1656        let mut merged_settings = existing_vpn_settings;
1657        if let Some(username) = config.username {
1658            merged_settings.insert("user-name".to_string(), username);
1659        }
1660        if let Some(gateway) = config.gateway {
1661            merged_settings.insert("remote".to_string(), gateway);
1662        }
1663        if let Some(ca_cert_path) = config.ca_cert_path {
1664            merged_settings.insert("ca".to_string(), ca_cert_path);
1665        }
1666        if let Some(user_cert_path) = config.user_cert_path {
1667            merged_settings.insert("cert".to_string(), user_cert_path);
1668        }
1669        if let Some(private_key_path) = config.private_key_path {
1670            merged_settings.insert("key".to_string(), private_key_path);
1671        }
1672        if let Some(private_key_password) = config.private_key_password {
1673            merged_settings.insert("key-password".to_string(), private_key_password);
1674        }
1675        if let Some(custom_settings) = config.settings {
1676            for (k, v) in custom_settings {
1677                merged_settings.insert(k, v);
1678            }
1679        }
1680
1681        for (k, v) in merged_settings {
1682            vpn_section.insert(k, Value::from(v));
1683        }
1684
1685        let mut merged_secrets = existing_vpn_secrets;
1686        if let Some(password) = config.password {
1687            merged_secrets.insert("password".to_string(), password);
1688        }
1689        if let Some(custom_secrets) = config.secrets {
1690            for (k, v) in custom_secrets {
1691                merged_secrets.insert(k, v);
1692            }
1693        }
1694
1695        if merged_secrets.is_empty() {
1696            settings.remove("vpn-secrets");
1697        } else {
1698            let vpn_secrets_section = settings
1699                .entry("vpn-secrets".to_string())
1700                .or_insert_with(HashMap::new);
1701            vpn_secrets_section.clear();
1702            for (k, v) in merged_secrets {
1703                vpn_secrets_section.insert(k, Value::from(v));
1704            }
1705        }
1706
1707        let conn_proxy = zbus::blocking::Proxy::new(
1708            &self.connection,
1709            "org.freedesktop.NetworkManager",
1710            conn_path.as_str(),
1711            "org.freedesktop.NetworkManager.Settings.Connection",
1712        )?;
1713        conn_proxy.call::<_, _, ()>("Update", &(settings,))?;
1714
1715        let updated_settings = self.get_connection_settings(&conn_path)?;
1716        self.vpn_profile_from_settings(&updated_settings)
1717            .ok_or_else(|| crate::error::NetworkError::VpnProfileNotFound(config.uuid))
1718    }
1719
1720    /// Delete a VPN profile by UUID.
1721    pub fn delete_vpn_profile(&self, uuid: String) -> Result<()> {
1722        let conn_path = self.find_connection_path_by_uuid(&uuid)?;
1723
1724        let conn_proxy = zbus::blocking::Proxy::new(
1725            &self.connection,
1726            "org.freedesktop.NetworkManager",
1727            conn_path.as_str(),
1728            "org.freedesktop.NetworkManager.Settings.Connection",
1729        )?;
1730
1731        conn_proxy.call::<_, _, ()>("Delete", &())?;
1732        Ok(())
1733    }
1734}
1735
1736#[cfg(test)]
1737mod tests {
1738    use super::*;
1739
1740    #[test]
1741    fn vpn_state_deactivated_maps_to_disconnected() {
1742        assert_eq!(
1743            VSKNetworkManager::<tauri::Wry>::vpn_state_from_active_state(4),
1744            VpnConnectionState::Disconnected
1745        );
1746    }
1747
1748    /// El caso que motivó todo esto: con Twingate conectado, el centro de
1749    /// control decía que no había VPN. Twingate levanta su propio túnel, así
1750    /// que NetworkManager lo archiva como un `tun` cualquiera y no como una
1751    /// "vpn" suya.
1752    #[test]
1753    fn un_tun_ajeno_tambien_es_una_vpn() {
1754        type NM = VSKNetworkManager<'static, tauri::Wry>;
1755
1756        assert!(NM::tunnel_priority("tun").is_some(), "Twingate quedaba afuera");
1757        assert!(NM::tunnel_priority("wireguard").is_some());
1758        assert!(NM::tunnel_priority("vpn").is_some());
1759
1760        // Y lo que no es un túnel sigue sin serlo: si el wifi contara como VPN,
1761        // el indicador estaría siempre encendido.
1762        assert!(NM::tunnel_priority("802-11-wireless").is_none());
1763        assert!(NM::tunnel_priority("ethernet").is_none());
1764        assert!(NM::tunnel_priority("loopback").is_none());
1765        assert!(NM::tunnel_priority("bridge").is_none());
1766    }
1767
1768    /// Con varios túneles arriba manda el que NetworkManager administra: es el
1769    /// único que además se puede conectar y desconectar desde el escritorio.
1770    #[test]
1771    fn una_vpn_de_networkmanager_le_gana_a_un_tun() {
1772        type NM = VSKNetworkManager<'static, tauri::Wry>;
1773        assert!(NM::tunnel_priority("vpn") > NM::tunnel_priority("wireguard"));
1774        assert!(NM::tunnel_priority("wireguard") > NM::tunnel_priority("tun"));
1775    }
1776
1777    /// Contra el NetworkManager de verdad: que las propiedades que leemos
1778    /// existan y digan lo que creemos. Es donde se esconden los errores de una
1779    /// interfaz D-Bus, y ningún test puro los encuentra.
1780    ///
1781    /// Ignorado por defecto porque necesita un sistema con NetworkManager y una
1782    /// VPN levantada: `cargo test -- --ignored --nocapture`.
1783    #[test]
1784    #[ignore]
1785    fn el_tunel_de_esta_maquina_se_ve_como_vpn() {
1786        type NM = VSKNetworkManager<'static, tauri::Wry>;
1787        use zbus::names::InterfaceName;
1788        use zbus::zvariant::Value;
1789
1790        let conexion = zbus::blocking::Connection::system().expect("sin bus del sistema");
1791        let nm = zbus::blocking::fdo::PropertiesProxy::builder(&conexion)
1792            .destination("org.freedesktop.NetworkManager").unwrap()
1793            .path("/org/freedesktop/NetworkManager").unwrap()
1794            .build()
1795            .expect("NetworkManager no está");
1796
1797        let activas = nm
1798            .get(
1799                InterfaceName::from_static_str_unchecked("org.freedesktop.NetworkManager"),
1800                "ActiveConnections",
1801            )
1802            .unwrap();
1803
1804        let mut encontrados = Vec::new();
1805        if let Ok(Value::Array(arr)) = activas.downcast_ref() {
1806            for valor in arr.iter() {
1807                let Value::ObjectPath(ruta) = valor else { continue };
1808                let props = zbus::blocking::fdo::PropertiesProxy::builder(&conexion)
1809                    .destination("org.freedesktop.NetworkManager").unwrap()
1810                    .path(ruta).unwrap()
1811                    .build()
1812                    .unwrap();
1813                let leer = |p: &'static str| {
1814                    props.get(
1815                        InterfaceName::from_static_str_unchecked(
1816                            "org.freedesktop.NetworkManager.Connection.Active",
1817                        ),
1818                        p,
1819                    )
1820                };
1821                let tipo = match leer("Type").unwrap().downcast_ref() {
1822                    Ok(Value::Str(v)) => v.to_string(),
1823                    _ => continue,
1824                };
1825                let id = match leer("Id").unwrap().downcast_ref() {
1826                    Ok(Value::Str(v)) => v.to_string(),
1827                    _ => String::new(),
1828                };
1829                match NM::tunnel_priority(&tipo) {
1830                    Some(prioridad) => {
1831                        let nombre = NM::tunnel_display_name(&id, Some(&id));
1832                        println!("túnel: id={id} tipo={tipo} prioridad={prioridad} se muestra como «{nombre}»");
1833                        encontrados.push(nombre);
1834                    }
1835                    None => println!("no es túnel: id={id} tipo={tipo}"),
1836                }
1837            }
1838        }
1839
1840        assert!(
1841            !encontrados.is_empty(),
1842            "ningún túnel detectado; levantá la VPN antes de correr esto"
1843        );
1844    }
1845
1846    /// «sdwan0» no le dice nada a nadie; «Twingate» sí.
1847    #[test]
1848    fn los_tuneles_conocidos_se_muestran_con_su_nombre() {
1849        type NM = VSKNetworkManager<'static, tauri::Wry>;
1850
1851        assert_eq!(NM::tunnel_display_name("sdwan0", Some("sdwan0")), "Twingate");
1852        assert_eq!(NM::tunnel_display_name("tailscale0", Some("tailscale0")), "Tailscale");
1853        assert_eq!(NM::tunnel_display_name("wg0", Some("wg0")), "WireGuard");
1854
1855        // Uno desconocido conserva el nombre que le puso NetworkManager: es
1856        // menos de lo que querríamos, pero muchísimo más que decir que no hay
1857        // ninguna VPN.
1858        assert_eq!(NM::tunnel_display_name("Oficina", Some("tun0")), "Oficina");
1859    }
1860}
1861
1862/// Initialize the network manager plugin
1863pub fn init(
1864    app: &AppHandle<tauri::Wry>,
1865    _api: PluginApi<tauri::Wry, ()>,
1866) -> Result<VSKNetworkManager<'static, tauri::Wry>> {
1867    Ok(VSKNetworkManager::new(app.clone())?)
1868}