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(&self, uuid: &str) -> Result<zbus::zvariant::OwnedObjectPath> {
118 let connections = self.list_connection_paths()?;
119
120 for conn_path in connections {
121 let settings = self.get_connection_settings(&conn_path)?;
122 let dict = match settings.get("connection") {
123 Some(v) => v,
124 None => continue,
125 };
126
127 if let Some(conn_uuid) = Self::extract_string_from_dict(dict, "uuid") {
128 if conn_uuid == uuid {
129 return Ok(conn_path);
130 }
131 }
132 }
133
134 Err(crate::error::NetworkError::VpnProfileNotFound(
135 uuid.to_string(),
136 ))
137 }
138
139 fn vpn_profile_from_settings(
140 &self,
141 settings: &HashMap<String, HashMap<String, zbus::zvariant::OwnedValue>>,
142 ) -> Option<VpnProfile> {
143 let connection_dict = settings.get("connection")?;
144
145 let conn_type = Self::extract_string_from_dict(connection_dict, "type")?;
146 if conn_type != "vpn" {
147 return None;
148 }
149
150 let uuid = Self::extract_string_from_dict(connection_dict, "uuid")?;
151 let id =
152 Self::extract_string_from_dict(connection_dict, "id").unwrap_or_else(|| uuid.clone());
153 let interface_name = Self::extract_string_from_dict(connection_dict, "interface-name");
154 let autoconnect =
155 Self::extract_bool_from_dict(connection_dict, "autoconnect").unwrap_or(false);
156
157 let vpn_settings = Self::string_map_from_section(settings, "vpn");
158 let service_type = vpn_settings
159 .get("service-type")
160 .map(|s| s.as_str())
161 .unwrap_or("unknown");
162
163 Some(VpnProfile {
164 id,
165 uuid,
166 vpn_type: Self::vpn_type_from_service_type(service_type),
167 interface_name,
168 autoconnect,
169 editable: true,
170 last_error: None,
171 })
172 }
173
174 fn get_wifi_icon(strength: u8) -> String {
176 match strength {
177 0..=25 => "network-wireless-signal-weak-symbolic".to_string(),
178 26..=50 => "network-wireless-signal-ok-symbolic".to_string(),
179 51..=75 => "network-wireless-signal-good-symbolic".to_string(),
180 76..=100 => "network-wireless-signal-excellent-symbolic".to_string(),
181 _ => "network-wireless-signal-none-symbolic".to_string(),
182 }
183 }
184
185 fn get_icon_for(connection_type: &str, is_connected: bool) -> String {
191 if !is_connected {
192 return "network-offline-symbolic".to_string();
193 }
194
195 match connection_type {
196 "Ethernet" => "network-wired-symbolic".to_string(),
197 "Modem" => "network-cellular-connected-symbolic".to_string(),
198 "VPN" | "WireGuard" => "network-vpn-symbolic".to_string(),
199 _ => "network-transmit-receive-symbolic".to_string(),
200 }
201 }
202
203 pub fn new(app: AppHandle<R>) -> Result<Self> {
205 let connection = zbus::blocking::Connection::system()?;
206 let proxy = zbus::blocking::fdo::PropertiesProxy::builder(&connection)
207 .destination("org.freedesktop.NetworkManager")?
208 .path("/org/freedesktop/NetworkManager")?
209 .build()?;
210
211 Ok(Self {
212 connection,
213 proxy,
214 app,
215 })
216 }
217
218 fn primary_connection_path(&self) -> Option<zbus::zvariant::OwnedObjectPath> {
228 let primary = self
229 .proxy
230 .get(
231 InterfaceName::from_static_str_unchecked("org.freedesktop.NetworkManager"),
232 "PrimaryConnection",
233 )
234 .ok()?;
235
236 match primary.downcast_ref() {
237 Ok(Value::ObjectPath(path)) if path.as_str() != "/" => Some(path.into()),
240 _ => None,
241 }
242 }
243
244 pub fn get_current_network_state(&self) -> Result<NetworkInfo> {
245 let active_connections_variant = self.proxy.get(
247 InterfaceName::from_static_str_unchecked("org.freedesktop.NetworkManager"),
248 "ActiveConnections",
249 )?;
250
251 let primary = self.primary_connection_path();
252
253 match active_connections_variant.downcast_ref() {
255 Ok(Value::Array(arr)) if !arr.is_empty() => {
256 let chosen = match (&primary, &arr[0]) {
257 (Some(path), _) => path.as_ref().to_owned(),
258 (None, Value::ObjectPath(path)) => path.to_owned(),
259 _ => return Ok(NetworkInfo::default()),
260 };
261 let chosen = Value::ObjectPath(chosen);
262
263 match chosen {
264 zbus::zvariant::Value::ObjectPath(ref path) => {
265 let properties_proxy =
268 zbus::blocking::fdo::PropertiesProxy::builder(&self.connection)
269 .destination("org.freedesktop.NetworkManager")?
270 .path(path)?
271 .build()?;
272
273 let devices_variant = properties_proxy.get(
274 InterfaceName::from_static_str_unchecked(
275 "org.freedesktop.NetworkManager.Connection.Active",
276 ),
277 "Devices",
278 )?;
279
280 let device_path = match devices_variant.downcast_ref() {
282 Ok(Value::Array(device_arr)) if !device_arr.is_empty() => {
283 match device_arr[0] {
284 zbus::zvariant::Value::ObjectPath(ref dev_path) => {
285 dev_path.clone()
286 }
287 _ => return Ok(NetworkInfo::default()),
288 }
289 }
290 _ => return Ok(NetworkInfo::default()),
291 };
292
293 let device_properties_proxy =
296 zbus::blocking::fdo::PropertiesProxy::builder(&self.connection)
297 .destination("org.freedesktop.NetworkManager")?
298 .path(&device_path)?
299 .build()?;
300
301 let connection_type = device_properties_proxy.get(
302 InterfaceName::from_static_str_unchecked(
303 "org.freedesktop.NetworkManager.Device",
304 ),
305 "DeviceType",
306 )?;
307
308 let state_variant = properties_proxy.get(
309 InterfaceName::from_static_str_unchecked(
310 "org.freedesktop.NetworkManager.Connection.Active",
311 ),
312 "State",
313 )?;
314
315 let is_connected = match state_variant.downcast_ref() {
316 Ok(zbus::zvariant::Value::U32(state)) => state == 2, _ => false,
318 };
319
320 let connection_type_str = match connection_type.downcast_ref() {
325 Ok(zbus::zvariant::Value::U32(device_type)) => match device_type {
326 1 => "Ethernet".to_string(),
327 2 => "WiFi".to_string(),
328 8 => "Modem".to_string(),
329 16 => "VPN".to_string(),
330 29 => "WireGuard".to_string(),
331 _ => "Unknown".to_string(),
332 },
333 _ => "Unknown".to_string(),
334 };
335
336 let mut network_info = NetworkInfo {
338 name: "Unknown".to_string(),
339 ssid: "Unknown".to_string(),
340 connection_type: connection_type_str.clone(),
341 icon: "network-offline-symbolic".to_string(),
342 ip_address: "0.0.0.0".to_string(),
343 mac_address: "00:00:00:00:00:00".to_string(),
344 signal_strength: 0,
345 security_type: WiFiSecurityType::None,
346 is_connected: is_connected
347 && 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 if connection_type_str == "WiFi" {
364 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 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 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 network_info.icon =
426 Self::get_wifi_icon(network_info.signal_strength);
427
428 network_info.security_type =
430 NetworkManagerHelpers::detect_security_type(
431 &ap_properties_proxy,
432 )?;
433 }
434 } else {
435 network_info.icon =
438 Self::get_icon_for(&connection_type_str, network_info.is_connected);
439 }
440 let ip4_config_path = device_properties_proxy.get(
442 InterfaceName::from_static_str_unchecked(
443 "org.freedesktop.NetworkManager.Device",
444 ),
445 "Ip4Config",
446 )?;
447
448 if let Ok(zbus::zvariant::Value::ObjectPath(config_path)) =
450 ip4_config_path.downcast_ref()
451 {
452 let ip_config_properties_proxy =
454 zbus::blocking::fdo::PropertiesProxy::builder(&self.connection)
455 .destination("org.freedesktop.NetworkManager")?
456 .path(config_path)?
457 .build()?;
458
459 let addresses_variant = ip_config_properties_proxy.get(
460 InterfaceName::from_static_str_unchecked(
461 "org.freedesktop.NetworkManager.IP4Config",
462 ),
463 "Addresses",
464 )?;
465
466 if let Ok(Value::Array(addr_arr)) = addresses_variant.downcast_ref() {
467 if let Some(Value::Array(ip_tuple)) = addr_arr.first() {
468 if !ip_tuple.is_empty() {
469 if let Value::U32(ip_int) = &ip_tuple[0] {
470 use std::net::Ipv4Addr;
471 network_info.ip_address =
472 Ipv4Addr::from((*ip_int).to_be()).to_string();
473 }
474 }
475 }
476 }
477 }
478
479 Ok(network_info)
480 }
481 _ => Ok(NetworkInfo::default()),
482 }
483 }
484 _ => Ok(NetworkInfo::default()),
485 }
486 }
487
488 fn connected_wifi_ssid(&self) -> Result<Option<String>> {
491 let devices = self.wireless_device_paths()?;
492 for device_path in &devices {
493 let wireless_props = zbus::blocking::fdo::PropertiesProxy::builder(&self.connection)
494 .destination("org.freedesktop.NetworkManager")?
495 .path(device_path.as_str())?
496 .build()?;
497
498 let active_ap = wireless_props.get(
499 InterfaceName::from_static_str_unchecked(
500 "org.freedesktop.NetworkManager.Device.Wireless",
501 ),
502 "ActiveAccessPoint",
503 )?;
504
505 if let Ok(zbus::zvariant::Value::ObjectPath(ap_path)) = active_ap.downcast_ref() {
506 if ap_path.as_str() == "/" {
507 continue;
508 }
509 let ap_props = zbus::blocking::fdo::PropertiesProxy::builder(&self.connection)
510 .destination("org.freedesktop.NetworkManager")?
511 .path(ap_path.as_str())?
512 .build()?;
513
514 let ssid_variant = ap_props.get(
515 InterfaceName::from_static_str_unchecked(
516 "org.freedesktop.NetworkManager.AccessPoint",
517 ),
518 "Ssid",
519 )?;
520
521 if let Ok(v) = ssid_variant.downcast_ref() {
522 let ssid = NetworkManagerHelpers::ssid_from_value(&v);
523 if !ssid.is_empty() && ssid != "Unknown" {
524 return Ok(Some(ssid));
525 }
526 }
527 }
528 }
529 Ok(None)
530 }
531
532 pub fn list_wifi_networks(&self) -> Result<Vec<NetworkInfo>> {
534 let devices_variant = self.proxy.get(
535 InterfaceName::from_static_str_unchecked("org.freedesktop.NetworkManager"),
536 "Devices",
537 )?;
538
539 let mut networks = Vec::new();
540 let connected_ssid = self.connected_wifi_ssid()?;
541
542 if let Ok(zbus::zvariant::Value::Array(devices)) = devices_variant.downcast_ref() {
543 for device in devices.iter() {
544 if let zbus::zvariant::Value::ObjectPath(ref device_path) = device {
545 let device_props =
546 zbus::blocking::fdo::PropertiesProxy::builder(&self.connection)
547 .destination("org.freedesktop.NetworkManager")?
548 .path(device_path)?
549 .build()?;
550
551 let device_type_variant = device_props.get(
552 InterfaceName::from_static_str_unchecked(
553 "org.freedesktop.NetworkManager.Device",
554 ),
555 "DeviceType",
556 )?;
557
558 if let Ok(zbus::zvariant::Value::U32(device_type)) =
559 device_type_variant.downcast_ref()
560 {
561 if device_type == 2u32 {
562 let mac_address = match device_props
563 .get(
564 InterfaceName::from_static_str_unchecked(
565 "org.freedesktop.NetworkManager.Device",
566 ),
567 "HwAddress",
568 )?
569 .downcast_ref()
570 {
571 Ok(zbus::zvariant::Value::Str(s)) => s.to_string(),
572 _ => "00:00:00:00:00:00".to_string(),
573 };
574
575 let wireless_props =
576 zbus::blocking::fdo::PropertiesProxy::builder(&self.connection)
577 .destination("org.freedesktop.NetworkManager")?
578 .path(device_path)?
579 .build()?;
580
581 let access_points_variant = wireless_props.get(
582 InterfaceName::from_static_str_unchecked(
583 "org.freedesktop.NetworkManager.Device.Wireless",
584 ),
585 "AccessPoints",
586 )?;
587
588 if let Ok(zbus::zvariant::Value::Array(aps)) =
589 access_points_variant.downcast_ref()
590 {
591 for ap in aps.iter() {
592 if let zbus::zvariant::Value::ObjectPath(ref ap_path) = ap {
593 let ap_props =
594 zbus::blocking::fdo::PropertiesProxy::builder(
595 &self.connection,
596 )
597 .destination("org.freedesktop.NetworkManager")?
598 .path(ap_path)?
599 .build()?;
600
601 let ssid_variant = ap_props.get(
602 InterfaceName::from_static_str_unchecked(
603 "org.freedesktop.NetworkManager.AccessPoint",
604 ),
605 "Ssid",
606 )?;
607
608 let ssid = match ssid_variant.downcast_ref() {
609 Ok(v) => NetworkManagerHelpers::ssid_from_value(&v),
610 _ => "Unknown".to_string(),
611 };
612
613 let strength_variant = ap_props.get(
614 InterfaceName::from_static_str_unchecked(
615 "org.freedesktop.NetworkManager.AccessPoint",
616 ),
617 "Strength",
618 )?;
619
620 let strength = match strength_variant.downcast_ref() {
621 Ok(zbus::zvariant::Value::U8(s)) => s,
622 _ => 0,
623 };
624
625 let security_type =
626 NetworkManagerHelpers::detect_security_type(&ap_props)?;
627
628 let is_connected =
629 connected_ssid.as_deref() == Some(ssid.as_str());
630
631 let network_info = NetworkInfo {
632 name: ssid.clone(),
633 ssid,
634 connection_type: "wifi".to_string(),
635 icon: Self::get_wifi_icon(strength),
636 ip_address: "0.0.0.0".to_string(),
637 mac_address: mac_address.clone(),
638 signal_strength: strength,
639 security_type,
640 is_connected,
641 };
642
643 if !networks
644 .iter()
645 .any(|n: &NetworkInfo| n.ssid == network_info.ssid)
646 {
647 networks.push(network_info);
648 }
649 }
650 }
651 }
652 }
653 }
654 }
655 }
656 }
657
658 networks.sort_by_key(|red| std::cmp::Reverse(red.signal_strength));
660 Ok(networks)
661 }
662
663 fn wireless_device_paths(&self) -> Result<Vec<zbus::zvariant::OwnedObjectPath>> {
665 let devices_variant = self.proxy.get(
666 InterfaceName::from_static_str_unchecked("org.freedesktop.NetworkManager"),
667 "Devices",
668 )?;
669
670 let mut paths = Vec::new();
671 if let Ok(zbus::zvariant::Value::Array(devices)) = devices_variant.downcast_ref() {
672 for device in devices.iter() {
673 if let zbus::zvariant::Value::ObjectPath(device_path) = device {
674 let device_props =
675 zbus::blocking::fdo::PropertiesProxy::builder(&self.connection)
676 .destination("org.freedesktop.NetworkManager")?
677 .path(device_path.as_str())?
678 .build()?;
679
680 let device_type_variant = device_props.get(
681 InterfaceName::from_static_str_unchecked(
682 "org.freedesktop.NetworkManager.Device",
683 ),
684 "DeviceType",
685 )?;
686
687 if let Ok(zbus::zvariant::Value::U32(device_type)) =
688 device_type_variant.downcast_ref()
689 {
690 if device_type == 2u32 {
691 let owned =
692 zbus::zvariant::OwnedObjectPath::try_from(device_path.as_str())
693 .unwrap();
694 paths.push(owned);
695 }
696 }
697 }
698 }
699 }
700 Ok(paths)
701 }
702
703 fn scan_settled(before: i64, current: i64) -> bool {
707 current != -1 && current != before
708 }
709
710 fn read_last_scan(props: &zbus::blocking::fdo::PropertiesProxy) -> i64 {
715 match props.get(
716 InterfaceName::from_static_str_unchecked(
717 "org.freedesktop.NetworkManager.Device.Wireless",
718 ),
719 "LastScan",
720 ) {
721 Ok(value) => match value.downcast_ref() {
722 Ok(zbus::zvariant::Value::I64(last)) => last,
723 _ => -1,
724 },
725 Err(_) => -1,
726 }
727 }
728
729 pub fn rescan_wifi(&self) -> Result<Vec<NetworkInfo>> {
737 const SCAN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
738 const SCAN_POLL: std::time::Duration = std::time::Duration::from_millis(300);
739
740 let devices = self.wireless_device_paths()?;
741
742 if devices.is_empty() {
743 return Err(crate::error::NetworkError::OperationError(
744 "No wireless device available for scanning".to_string(),
745 ));
746 }
747
748 let mut any_settled = false;
753
754 for device_path in &devices {
755 let wireless_props = zbus::blocking::fdo::PropertiesProxy::builder(&self.connection)
756 .destination("org.freedesktop.NetworkManager")?
757 .path(device_path.as_str())?
758 .build()?;
759
760 let last_scan_before = Self::read_last_scan(&wireless_props);
762
763 let wireless_proxy = zbus::blocking::Proxy::new(
764 &self.connection,
765 "org.freedesktop.NetworkManager",
766 device_path.as_str(),
767 "org.freedesktop.NetworkManager.Device.Wireless",
768 )?;
769
770 let options: HashMap<String, zbus::zvariant::OwnedValue> = HashMap::new();
775 let _ = wireless_proxy.call::<_, _, ()>("RequestScan", &(options,));
776
777 let deadline = std::time::Instant::now() + SCAN_TIMEOUT;
779 while std::time::Instant::now() < deadline {
780 if Self::scan_settled(last_scan_before, Self::read_last_scan(&wireless_props)) {
781 any_settled = true;
782 break;
783 }
784 std::thread::sleep(SCAN_POLL);
785 }
786 }
787
788 if !any_settled {
789 return Err(crate::error::NetworkError::OperationError(
790 "WiFi scan did not complete on any wireless device".to_string(),
791 ));
792 }
793
794 self.list_wifi_networks()
795 }
796
797 pub fn connect_to_wifi(&self, config: WiFiConnectionConfig) -> Result<()> {
799 log::debug!(
803 "connect_to_wifi called: ssid='{}' security={:?} has_username={}",
804 config.ssid,
805 config.security_type,
806 config.username.is_some()
807 );
808
809 let mut connection_settings = HashMap::new();
811 let mut wifi_settings = HashMap::new();
812 let mut security_settings = HashMap::new();
813
814 let mut connection = HashMap::new();
816 connection.insert("id".to_string(), Value::from(config.ssid.clone()));
817 connection.insert("type".to_string(), Value::from("802-11-wireless"));
818 connection_settings.insert("connection".to_string(), connection);
819
820 wifi_settings.insert(
822 "ssid".to_string(),
823 Value::from(config.ssid.as_bytes().to_vec()),
824 );
825 wifi_settings.insert("mode".to_string(), Value::from("infrastructure"));
826
827 match config.security_type {
829 WiFiSecurityType::None => {
830 }
832 WiFiSecurityType::Wep => {
833 security_settings.insert("key-mgmt".to_string(), Value::from("none"));
834 if let Some(password) = config.password.clone() {
835 security_settings.insert("wep-key0".to_string(), Value::from(password));
836 }
837 }
838 WiFiSecurityType::WpaPsk => {
839 security_settings.insert("key-mgmt".to_string(), Value::from("wpa-psk"));
840 if let Some(password) = config.password.clone() {
841 security_settings.insert("psk".to_string(), Value::from(password));
842 }
843 }
844 WiFiSecurityType::WpaEap => {
845 security_settings.insert("key-mgmt".to_string(), Value::from("wpa-eap"));
846 if let Some(password) = config.password.clone() {
847 security_settings.insert("password".to_string(), Value::from(password));
848 }
849 if let Some(username) = config.username.clone() {
850 security_settings.insert("identity".to_string(), Value::from(username));
851 }
852 }
853 WiFiSecurityType::Wpa2Psk => {
854 security_settings.insert("key-mgmt".to_string(), Value::from("wpa-psk"));
855 security_settings.insert("proto".to_string(), Value::from(vec!["rsn"]));
856 if let Some(password) = config.password.clone() {
857 security_settings.insert("psk".to_string(), Value::from(password));
858 }
859 }
860 WiFiSecurityType::Wpa3Psk => {
861 security_settings.insert("key-mgmt".to_string(), Value::from("sae"));
862 if let Some(password) = config.password.clone() {
863 security_settings.insert("psk".to_string(), Value::from(password));
864 }
865 }
866 }
867
868 connection_settings.insert("802-11-wireless".to_string(), wifi_settings);
869 if !security_settings.is_empty() {
870 connection_settings.insert("802-11-wireless-security".to_string(), security_settings);
871 }
872
873 log::debug!("connection_settings: {:#?}", connection_settings);
875
876 let nm_proxy = zbus::blocking::Proxy::new(
878 &self.connection,
879 "org.freedesktop.NetworkManager",
880 "/org/freedesktop/NetworkManager",
881 "org.freedesktop.NetworkManager",
882 )?;
883
884 let any_path = zbus::zvariant::OwnedObjectPath::try_from("/").unwrap();
886 let call_result: zbus::Result<(
887 zbus::zvariant::OwnedObjectPath,
888 zbus::zvariant::OwnedObjectPath,
889 )> = nm_proxy.call(
890 "AddAndActivateConnection",
891 &(connection_settings, &any_path, &any_path),
892 );
893
894 match call_result {
895 Ok((conn_path, active_path)) => {
896 log::info!(
897 "AddAndActivateConnection succeeded for ssid='{}' conn='{}' active='{}'",
898 config.ssid,
899 conn_path.as_str(),
900 active_path.as_str()
901 );
902 }
903 Err(e) => {
904 log::error!(
905 "AddAndActivateConnection failed for ssid='{}': {:?}",
906 config.ssid,
907 e
908 );
909 return Err(e.into());
910 }
911 }
912
913 log::debug!("connect_to_wifi finished for ssid='{}'", config.ssid);
914
915 Ok(())
916 }
917
918 pub fn toggle_network_state(&self, enabled: bool) -> Result<bool> {
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 nm_proxy.set_property("NetworkingEnabled", enabled)?;
928
929 let current_state: bool = nm_proxy.get_property("NetworkingEnabled")?;
930 Ok(current_state)
931 }
932
933 pub fn get_wireless_enabled(&self) -> Result<bool> {
935 let nm_proxy = zbus::blocking::Proxy::new(
936 &self.connection,
937 "org.freedesktop.NetworkManager",
938 "/org/freedesktop/NetworkManager",
939 "org.freedesktop.NetworkManager",
940 )?;
941 Ok(nm_proxy.get_property("WirelessEnabled")?)
942 }
943
944 pub fn set_wireless_enabled(&self, enabled: bool) -> Result<()> {
946 let nm_proxy = zbus::blocking::Proxy::new(
947 &self.connection,
948 "org.freedesktop.NetworkManager",
949 "/org/freedesktop/NetworkManager",
950 "org.freedesktop.NetworkManager",
951 )?;
952 nm_proxy.set_property("WirelessEnabled", enabled)?;
953 Ok(())
954 }
955
956 pub fn is_wireless_available(&self) -> Result<bool> {
958 Ok(!self.wireless_device_paths()?.is_empty())
959 }
960
961 pub fn listen_network_changes(&self) -> Result<mpsc::Receiver<NetworkInfo>> {
963 let (tx, rx) = mpsc::channel();
964 let connection_clone = self.connection.clone();
965 let app_handle = self.app.clone();
966
967 std::thread::spawn(move || {
968 if let Ok(proxy) = zbus::blocking::Proxy::new(
969 &connection_clone,
970 "org.freedesktop.NetworkManager",
971 "/org/freedesktop/NetworkManager",
972 "org.freedesktop.NetworkManager",
973 ) {
974 if let Ok(signal) = proxy.receive_signal("StateChanged") {
975 let proxy_props =
976 zbus::blocking::fdo::PropertiesProxy::builder(&connection_clone)
977 .destination("org.freedesktop.NetworkManager")
978 .unwrap()
979 .path("/org/freedesktop/NetworkManager")
980 .unwrap()
981 .build()
982 .unwrap();
983
984 for _msg in signal {
985 let network_manager = VSKNetworkManager {
986 connection: connection_clone.clone(),
987 proxy: proxy_props.clone(),
988 app: app_handle.clone(),
989 };
990
991 if let Ok(network_info) = network_manager.get_current_network_state() {
992 if tx.send(network_info).is_err() {
993 break;
994 }
995 }
996 }
997 }
998 }
999 });
1000
1001 Ok(rx)
1002 }
1003
1004 pub fn disconnect_from_wifi(&self) -> Result<()> {
1006 let nm_proxy = zbus::blocking::Proxy::new(
1007 &self.connection,
1008 "org.freedesktop.NetworkManager",
1009 "/org/freedesktop/NetworkManager",
1010 "org.freedesktop.NetworkManager",
1011 )?;
1012
1013 let active_connections_variant: zbus::zvariant::OwnedValue = self.proxy.get(
1015 InterfaceName::from_static_str_unchecked("org.freedesktop.NetworkManager"),
1016 "ActiveConnections",
1017 )?;
1018
1019 let active_connections = match active_connections_variant.downcast_ref() {
1021 Ok(zbus::zvariant::Value::Array(arr)) => arr
1022 .iter()
1023 .filter_map(|v| match v {
1024 zbus::zvariant::Value::ObjectPath(path) => {
1025 Some(zbus::zvariant::OwnedObjectPath::from(path.to_owned()))
1026 }
1027 _ => None,
1028 })
1029 .collect::<Vec<zbus::zvariant::OwnedObjectPath>>(),
1030 _ => Vec::new(),
1031 };
1032
1033 if !active_connections.is_empty() {
1034 nm_proxy.call::<_, _, ()>("DeactivateConnection", &(&active_connections[0],))?;
1035 Ok(())
1036 } else {
1037 Ok(())
1038 }
1039 }
1040
1041 pub fn get_saved_wifi_networks(&self) -> Result<Vec<NetworkInfo>> {
1043 let settings_proxy = zbus::blocking::Proxy::new(
1045 &self.connection,
1046 "org.freedesktop.NetworkManager",
1047 "/org/freedesktop/NetworkManager/Settings",
1048 "org.freedesktop.NetworkManager.Settings",
1049 )?;
1050
1051 let connections: Vec<zbus::zvariant::OwnedObjectPath> =
1053 settings_proxy.call("ListConnections", &())?;
1054 let mut saved_networks = Vec::new();
1055
1056 for conn_path in connections {
1058 let conn_proxy = zbus::blocking::Proxy::new(
1060 &self.connection,
1061 "org.freedesktop.NetworkManager",
1062 conn_path.as_str(),
1063 "org.freedesktop.NetworkManager.Settings.Connection",
1064 )?;
1065
1066 let settings: std::collections::HashMap<String, zbus::zvariant::OwnedValue> =
1068 conn_proxy.call("GetSettings", &())?;
1069
1070 if let Some(connection) = settings.get("connection") {
1072 let connection_dict = match connection.downcast_ref::<Value>().and_then(|v| {
1073 v.downcast::<std::collections::HashMap<String, zbus::zvariant::OwnedValue>>()
1074 }) {
1075 Ok(dict) => dict,
1076 _ => continue,
1077 };
1078
1079 if let Some(conn_type) = connection_dict.get("type") {
1081 let v_type: &zbus::zvariant::Value<'_> = conn_type;
1082 let conn_type_str = match v_type.downcast_ref::<String>() {
1083 Ok(s) => s,
1084 _ => continue,
1085 };
1086
1087 if conn_type_str == "802-11-wireless" {
1089 let mut network_info = NetworkInfo {
1090 connection_type: "wifi".to_string(),
1091 ..Default::default()
1092 };
1093
1094 if let Some(id) = connection_dict.get("id") {
1096 let v_id: &zbus::zvariant::Value<'_> = id;
1097 if let Ok(name) = v_id.downcast_ref::<String>() {
1098 network_info.name = name;
1099 }
1100 }
1101
1102 if let Some(wireless) = settings.get("802-11-wireless") {
1104 let wireless_dict = match wireless.downcast_ref::<Value>()
1105 .and_then(|v| v.downcast::<std::collections::HashMap<String, zbus::zvariant::OwnedValue>>())
1106 {
1107 Ok(dict) => dict,
1108 _ => continue,
1109 };
1110
1111 if let Some(ssid) = wireless_dict.get("ssid") {
1112 let v_ssid: &zbus::zvariant::Value<'_> = ssid;
1113 network_info.ssid = NetworkManagerHelpers::ssid_from_value(v_ssid);
1114 }
1115 }
1116
1117 if let Some(security) = settings.get("802-11-wireless-security") {
1119 let security_dict = match security.downcast_ref::<Value>()
1120 .and_then(|v| v.downcast::<std::collections::HashMap<String, zbus::zvariant::OwnedValue>>())
1121 {
1122 Ok(dict) => dict,
1123 _ => {
1124 network_info.security_type = WiFiSecurityType::None;
1125 saved_networks.push(network_info);
1126 continue;
1127 },
1128 };
1129
1130 if let Some(key_mgmt) = security_dict.get("key-mgmt") {
1131 let v_km: &zbus::zvariant::Value<'_> = key_mgmt;
1132 if let Ok(key_mgmt_str) = v_km.downcast_ref::<String>() {
1133 match key_mgmt_str.as_str() {
1134 "none" => {
1135 network_info.security_type = WiFiSecurityType::None
1136 }
1137 "wpa-psk" => {
1138 network_info.security_type = WiFiSecurityType::WpaPsk
1139 }
1140 "wpa-eap" => {
1141 network_info.security_type = WiFiSecurityType::WpaEap
1142 }
1143 _ => network_info.security_type = WiFiSecurityType::None,
1144 }
1145 }
1146 }
1147 } else {
1148 network_info.security_type = WiFiSecurityType::None;
1149 }
1150
1151 saved_networks.push(network_info);
1153 }
1154 }
1155 }
1156 }
1157
1158 Ok(saved_networks)
1159 }
1160
1161 pub fn delete_wifi_connection(&self, ssid: &str) -> Result<bool> {
1163 let settings_proxy = zbus::blocking::Proxy::new(
1165 &self.connection,
1166 "org.freedesktop.NetworkManager",
1167 "/org/freedesktop/NetworkManager/Settings",
1168 "org.freedesktop.NetworkManager.Settings",
1169 )?;
1170
1171 let connections: Vec<zbus::zvariant::OwnedObjectPath> =
1173 settings_proxy.call("ListConnections", &())?;
1174
1175 for conn_path in connections {
1177 let conn_proxy = zbus::blocking::Proxy::new(
1179 &self.connection,
1180 "org.freedesktop.NetworkManager",
1181 conn_path.as_str(),
1182 "org.freedesktop.NetworkManager.Settings.Connection",
1183 )?;
1184
1185 let settings: std::collections::HashMap<String, zbus::zvariant::OwnedValue> =
1187 conn_proxy.call("GetSettings", &())?;
1188
1189 if let Some(connection) = settings.get("connection") {
1191 let connection_dict = match connection.downcast_ref::<Value>().and_then(|v| {
1192 v.downcast::<std::collections::HashMap<String, zbus::zvariant::OwnedValue>>()
1193 }) {
1194 Ok(dict) => dict,
1195 _ => continue,
1196 };
1197
1198 if let Some(conn_type) = connection_dict.get("type") {
1200 let v_type: &zbus::zvariant::Value<'_> = conn_type;
1201 let conn_type_str = match v_type.downcast_ref::<String>() {
1202 Ok(s) => s,
1203 _ => continue,
1204 };
1205
1206 if conn_type_str == "802-11-wireless" {
1208 if let Some(wireless) = settings.get("802-11-wireless") {
1209 let wireless_dict = match wireless.downcast_ref::<Value>()
1210 .and_then(|v| v.downcast::<std::collections::HashMap<String, zbus::zvariant::OwnedValue>>())
1211 {
1212 Ok(dict) => dict,
1213 _ => continue,
1214 };
1215
1216 if let Some(ssid_value) = wireless_dict.get("ssid") {
1217 let v_ssid: &zbus::zvariant::Value<'_> = ssid_value;
1218 let conn_ssid_str = NetworkManagerHelpers::ssid_from_value(v_ssid);
1219 if conn_ssid_str == ssid {
1221 conn_proxy.call::<_, _, ()>("Delete", &())?;
1222 return Ok(true);
1223 }
1224 }
1225 }
1226 }
1227 }
1228 }
1229 }
1230
1231 Ok(false)
1233 }
1234
1235 pub fn list_vpn_profiles(&self) -> Result<Vec<VpnProfile>> {
1237 let connections = self.list_connection_paths()?;
1238 let mut profiles = Vec::new();
1239
1240 for conn_path in connections {
1241 let settings = self.get_connection_settings(&conn_path)?;
1242 if let Some(profile) = self.vpn_profile_from_settings(&settings) {
1243 profiles.push(profile);
1244 }
1245 }
1246
1247 profiles.sort_by(|a, b| a.id.cmp(&b.id));
1248 Ok(profiles)
1249 }
1250
1251 fn tunnel_priority(conn_type: &str) -> Option<u8> {
1265 match conn_type {
1266 "vpn" => Some(3),
1267 "wireguard" => Some(2),
1268 "tun" | "tap" | "ip-tunnel" => Some(1),
1269 _ => None,
1270 }
1271 }
1272
1273 fn tunnel_display_name(id: &str, interface: Option<&str>) -> String {
1280 const CONOCIDAS: [(&str, &str); 7] = [
1281 ("sdwan", "Twingate"),
1282 ("tailscale", "Tailscale"),
1283 ("nordlynx", "NordVPN"),
1284 ("proton", "Proton VPN"),
1285 ("pvpn", "Proton VPN"),
1286 ("zt", "ZeroTier"),
1287 ("wg", "WireGuard"),
1288 ];
1289
1290 let nombre = interface.unwrap_or(id);
1291 for (prefijo, producto) in CONOCIDAS {
1292 if nombre.starts_with(prefijo) {
1293 return producto.to_string();
1294 }
1295 }
1296
1297 id.to_string()
1298 }
1299
1300 pub fn get_vpn_status(&self) -> Result<VpnStatus> {
1301 let active_connections_variant = self.proxy.get(
1302 InterfaceName::from_static_str_unchecked("org.freedesktop.NetworkManager"),
1303 "ActiveConnections",
1304 )?;
1305
1306 let mut mejor: Option<(u8, zbus::zvariant::OwnedObjectPath, String)> = None;
1307
1308 if let Ok(zbus::zvariant::Value::Array(arr)) = active_connections_variant.downcast_ref() {
1309 for value in arr.iter() {
1310 let active_path = match value {
1311 zbus::zvariant::Value::ObjectPath(path) => path,
1312 _ => continue,
1313 };
1314
1315 let active_props = zbus::blocking::fdo::PropertiesProxy::builder(&self.connection)
1316 .destination("org.freedesktop.NetworkManager")?
1317 .path(active_path)?
1318 .build()?;
1319
1320 let conn_type_variant = active_props.get(
1321 InterfaceName::from_static_str_unchecked(
1322 "org.freedesktop.NetworkManager.Connection.Active",
1323 ),
1324 "Type",
1325 )?;
1326
1327 let conn_type = match conn_type_variant.downcast_ref() {
1328 Ok(zbus::zvariant::Value::Str(v)) => v.to_string(),
1329 _ => continue,
1330 };
1331
1332 let Some(prioridad) = Self::tunnel_priority(&conn_type) else {
1333 continue;
1334 };
1335
1336 if mejor
1340 .as_ref()
1341 .map_or(true, |(previa, _, _)| prioridad > *previa)
1342 {
1343 mejor = Some((
1344 prioridad,
1345 zbus::zvariant::OwnedObjectPath::from(active_path.to_owned()),
1346 conn_type,
1347 ));
1348 }
1349 }
1350 }
1351
1352 let Some((_, active_path, conn_type)) = mejor else {
1353 return Ok(VpnStatus::default());
1354 };
1355
1356 self.vpn_status_from_active(&active_path, &conn_type)
1357 }
1358
1359 fn vpn_status_from_active(
1361 &self,
1362 active_path: &zbus::zvariant::OwnedObjectPath,
1363 conn_type: &str,
1364 ) -> Result<VpnStatus> {
1365 let mut status = VpnStatus::default();
1366
1367 let active_props = zbus::blocking::fdo::PropertiesProxy::builder(&self.connection)
1368 .destination("org.freedesktop.NetworkManager")?
1369 .path(active_path.as_ref())?
1370 .build()?;
1371
1372 let leer = |propiedad: &'static str| {
1373 active_props.get(
1374 InterfaceName::from_static_str_unchecked(
1375 "org.freedesktop.NetworkManager.Connection.Active",
1376 ),
1377 propiedad,
1378 )
1379 };
1380
1381 let state = match leer("State")?.downcast_ref() {
1382 Ok(zbus::zvariant::Value::U32(v)) => v,
1383 _ => 0,
1384 };
1385 status.state = Self::vpn_state_from_active_state(state);
1386
1387 let id = match leer("Id")?.downcast_ref() {
1388 Ok(zbus::zvariant::Value::Str(v)) => v.to_string(),
1389 _ => String::new(),
1390 };
1391 status.active_profile_uuid = match leer("Uuid")?.downcast_ref() {
1392 Ok(zbus::zvariant::Value::Str(v)) => Some(v.to_string()),
1393 _ => None,
1394 };
1395
1396 status.managed_externally = conn_type != "vpn";
1399 status.interface = self.active_connection_interface(&active_props);
1400 status.active_profile_name = Some(if status.managed_externally {
1401 Self::tunnel_display_name(&id, status.interface.as_deref())
1402 } else {
1403 id.clone()
1404 });
1405 status.active_profile_id = Some(id);
1406
1407 if let Ok(zbus::zvariant::Value::ObjectPath(ip4_path)) = leer("Ip4Config")?.downcast_ref() {
1408 if ip4_path.as_str() != "/" {
1409 let ip4_props = zbus::blocking::fdo::PropertiesProxy::builder(&self.connection)
1410 .destination("org.freedesktop.NetworkManager")?
1411 .path(ip4_path)?
1412 .build()?;
1413
1414 let leer_ip = |propiedad: &'static str| {
1415 ip4_props.get(
1416 InterfaceName::from_static_str_unchecked(
1417 "org.freedesktop.NetworkManager.IP4Config",
1418 ),
1419 propiedad,
1420 )
1421 };
1422
1423 if let Ok(gateway) = leer_ip("Gateway") {
1424 status.gateway = match gateway.downcast_ref() {
1425 Ok(zbus::zvariant::Value::Str(v)) if !v.is_empty() => Some(v.to_string()),
1426 _ => None,
1427 };
1428 }
1429
1430 if let Ok(addresses) = leer_ip("Addresses") {
1433 if let Ok(zbus::zvariant::Value::Array(arr)) = addresses.downcast_ref() {
1434 if let Some(zbus::zvariant::Value::Array(tupla)) = arr.first() {
1435 if let Some(zbus::zvariant::Value::U32(entero)) = tupla.first() {
1436 use std::net::Ipv4Addr;
1437 status.ip_address =
1438 Some(Ipv4Addr::from(entero.to_be()).to_string());
1439 }
1440 }
1441 }
1442 }
1443 }
1444 }
1445
1446 Ok(status)
1447 }
1448
1449 fn active_connection_interface(
1451 &self,
1452 active_props: &zbus::blocking::fdo::PropertiesProxy,
1453 ) -> Option<String> {
1454 let devices = active_props
1455 .get(
1456 InterfaceName::from_static_str_unchecked(
1457 "org.freedesktop.NetworkManager.Connection.Active",
1458 ),
1459 "Devices",
1460 )
1461 .ok()?;
1462
1463 let zbus::zvariant::Value::Array(arr) = devices.downcast_ref().ok()? else {
1464 return None;
1465 };
1466 let zbus::zvariant::Value::ObjectPath(device_path) = arr.first()? else {
1467 return None;
1468 };
1469
1470 let device_props = zbus::blocking::fdo::PropertiesProxy::builder(&self.connection)
1471 .destination("org.freedesktop.NetworkManager")
1472 .ok()?
1473 .path(device_path)
1474 .ok()?
1475 .build()
1476 .ok()?;
1477
1478 let interface = device_props
1479 .get(
1480 InterfaceName::from_static_str_unchecked("org.freedesktop.NetworkManager.Device"),
1481 "Interface",
1482 )
1483 .ok()?;
1484
1485 match interface.downcast_ref() {
1486 Ok(zbus::zvariant::Value::Str(v)) => Some(v.to_string()),
1487 _ => None,
1488 }
1489 }
1490
1491 pub fn connect_vpn(&self, uuid: String) -> Result<()> {
1493 let current_status = self.get_vpn_status()?;
1494 if current_status.state == VpnConnectionState::Connected
1495 && current_status.active_profile_uuid.as_deref() == Some(uuid.as_str())
1496 {
1497 return Err(crate::error::NetworkError::VpnAlreadyConnected(uuid));
1498 }
1499
1500 let conn_path = self.find_connection_path_by_uuid(&uuid)?;
1501
1502 let nm_proxy = zbus::blocking::Proxy::new(
1503 &self.connection,
1504 "org.freedesktop.NetworkManager",
1505 "/org/freedesktop/NetworkManager",
1506 "org.freedesktop.NetworkManager",
1507 )?;
1508
1509 let any_path = zbus::zvariant::OwnedObjectPath::try_from("/").unwrap();
1510 let activate_result: zbus::Result<zbus::zvariant::OwnedObjectPath> =
1511 nm_proxy.call("ActivateConnection", &(&conn_path, &any_path, &any_path));
1512
1513 match activate_result {
1514 Ok(_) => Ok(()),
1515 Err(e) => {
1516 let msg = e.to_string().to_lowercase();
1517 if msg.contains("secret") || msg.contains("authentication") {
1518 Err(crate::error::NetworkError::VpnAuthFailed(e.to_string()))
1519 } else {
1520 Err(crate::error::NetworkError::VpnActivationFailed(
1521 e.to_string(),
1522 ))
1523 }
1524 }
1525 }
1526 }
1527
1528 pub fn disconnect_vpn(&self, uuid: Option<String>) -> Result<()> {
1530 let active_connections_variant = self.proxy.get(
1531 InterfaceName::from_static_str_unchecked("org.freedesktop.NetworkManager"),
1532 "ActiveConnections",
1533 )?;
1534
1535 let mut target_active_connection: Option<zbus::zvariant::OwnedObjectPath> = None;
1536
1537 if let Ok(zbus::zvariant::Value::Array(arr)) = active_connections_variant.downcast_ref() {
1538 for value in arr.iter() {
1539 let active_path = match value {
1540 zbus::zvariant::Value::ObjectPath(path) => {
1541 zbus::zvariant::OwnedObjectPath::from(path.to_owned())
1542 }
1543 _ => continue,
1544 };
1545
1546 let active_props = zbus::blocking::fdo::PropertiesProxy::builder(&self.connection)
1547 .destination("org.freedesktop.NetworkManager")?
1548 .path(active_path.as_str())?
1549 .build()?;
1550
1551 let conn_type_variant = active_props.get(
1552 InterfaceName::from_static_str_unchecked(
1553 "org.freedesktop.NetworkManager.Connection.Active",
1554 ),
1555 "Type",
1556 )?;
1557 let conn_type = match conn_type_variant.downcast_ref() {
1558 Ok(zbus::zvariant::Value::Str(v)) => v.to_string(),
1559 _ => continue,
1560 };
1561
1562 if conn_type != "vpn" {
1563 continue;
1564 }
1565
1566 if let Some(target_uuid) = uuid.as_deref() {
1567 let uuid_variant = active_props.get(
1568 InterfaceName::from_static_str_unchecked(
1569 "org.freedesktop.NetworkManager.Connection.Active",
1570 ),
1571 "Uuid",
1572 )?;
1573 let active_uuid = match uuid_variant.downcast_ref() {
1574 Ok(zbus::zvariant::Value::Str(v)) => v.to_string(),
1575 _ => continue,
1576 };
1577
1578 if active_uuid == target_uuid {
1579 target_active_connection = Some(active_path.clone());
1580 break;
1581 }
1582 } else {
1583 target_active_connection = Some(active_path.clone());
1584 break;
1585 }
1586 }
1587 }
1588
1589 let target_active_connection = match target_active_connection {
1590 Some(path) => path,
1591 None => {
1592 if let Some(target_uuid) = uuid {
1593 return Err(crate::error::NetworkError::VpnProfileNotFound(target_uuid));
1594 }
1595 return Err(crate::error::NetworkError::VpnNotActive);
1596 }
1597 };
1598
1599 let nm_proxy = zbus::blocking::Proxy::new(
1600 &self.connection,
1601 "org.freedesktop.NetworkManager",
1602 "/org/freedesktop/NetworkManager",
1603 "org.freedesktop.NetworkManager",
1604 )?;
1605
1606 nm_proxy.call::<_, _, ()>(
1607 "DeactivateConnection",
1608 &(target_active_connection.as_str(),),
1609 )?;
1610 Ok(())
1611 }
1612
1613 pub fn create_vpn_profile(&self, config: VpnCreateConfig) -> Result<VpnProfile> {
1615 if config.id.trim().is_empty() {
1616 return Err(crate::error::NetworkError::VpnInvalidConfig(
1617 "id is required".to_string(),
1618 ));
1619 }
1620
1621 let uuid = Uuid::new_v4().to_string();
1622 let mut connection_section = HashMap::new();
1623 connection_section.insert("id".to_string(), Value::from(config.id.clone()));
1624 connection_section.insert("uuid".to_string(), Value::from(uuid.clone()));
1625 connection_section.insert("type".to_string(), Value::from("vpn"));
1626 connection_section.insert(
1627 "autoconnect".to_string(),
1628 Value::from(config.autoconnect.unwrap_or(false)),
1629 );
1630
1631 let mut vpn_section = HashMap::new();
1632 vpn_section.insert(
1633 "service-type".to_string(),
1634 Value::from(Self::service_type_from_vpn_type(&config.vpn_type)),
1635 );
1636
1637 if let Some(username) = config.username {
1638 vpn_section.insert("user-name".to_string(), Value::from(username));
1639 }
1640 if let Some(gateway) = config.gateway {
1641 vpn_section.insert("remote".to_string(), Value::from(gateway));
1642 }
1643 if let Some(ca_cert_path) = config.ca_cert_path {
1644 vpn_section.insert("ca".to_string(), Value::from(ca_cert_path));
1645 }
1646 if let Some(user_cert_path) = config.user_cert_path {
1647 vpn_section.insert("cert".to_string(), Value::from(user_cert_path));
1648 }
1649 if let Some(private_key_path) = config.private_key_path {
1650 vpn_section.insert("key".to_string(), Value::from(private_key_path));
1651 }
1652 if let Some(private_key_password) = config.private_key_password {
1653 vpn_section.insert(
1654 "key-password".to_string(),
1655 Value::from(private_key_password),
1656 );
1657 }
1658 if let Some(custom_settings) = config.settings {
1659 for (k, v) in custom_settings {
1660 vpn_section.insert(k, Value::from(v));
1661 }
1662 }
1663
1664 let mut vpn_secrets_section = HashMap::new();
1665 if let Some(password) = config.password {
1666 vpn_secrets_section.insert("password".to_string(), Value::from(password));
1667 }
1668 if let Some(custom_secrets) = config.secrets {
1669 for (k, v) in custom_secrets {
1670 vpn_secrets_section.insert(k, Value::from(v));
1671 }
1672 }
1673
1674 let mut settings: HashMap<String, HashMap<String, Value>> = HashMap::new();
1675 settings.insert("connection".to_string(), connection_section);
1676 settings.insert("vpn".to_string(), vpn_section);
1677 if !vpn_secrets_section.is_empty() {
1678 settings.insert("vpn-secrets".to_string(), vpn_secrets_section);
1679 }
1680
1681 let settings_proxy = zbus::blocking::Proxy::new(
1682 &self.connection,
1683 "org.freedesktop.NetworkManager",
1684 "/org/freedesktop/NetworkManager/Settings",
1685 "org.freedesktop.NetworkManager.Settings",
1686 )?;
1687
1688 let _created_path: zbus::zvariant::OwnedObjectPath =
1689 settings_proxy.call("AddConnection", &(settings,))?;
1690
1691 Ok(VpnProfile {
1692 id: config.id,
1693 uuid,
1694 vpn_type: config.vpn_type,
1695 interface_name: None,
1696 autoconnect: config.autoconnect.unwrap_or(false),
1697 editable: true,
1698 last_error: None,
1699 })
1700 }
1701
1702 pub fn update_vpn_profile(&self, config: VpnUpdateConfig) -> Result<VpnProfile> {
1704 let conn_path = self.find_connection_path_by_uuid(&config.uuid)?;
1705 let existing_settings = self.get_connection_settings(&conn_path)?;
1706
1707 let existing_profile = self
1708 .vpn_profile_from_settings(&existing_settings)
1709 .ok_or_else(|| crate::error::NetworkError::VpnProfileNotFound(config.uuid.clone()))?;
1710
1711 let existing_vpn_settings = Self::string_map_from_section(&existing_settings, "vpn");
1712 let existing_vpn_secrets = Self::string_map_from_section(&existing_settings, "vpn-secrets");
1713
1714 let mut settings: HashMap<String, HashMap<String, Value>> = HashMap::new();
1717 for (section_name, dict) in &existing_settings {
1718 let mut section_map: HashMap<String, Value> = HashMap::new();
1719 for (k, v) in dict {
1720 if let Ok(val) = v.downcast_ref::<Value>() {
1721 section_map.insert(k.clone(), val);
1722 }
1723 }
1724 settings.insert(section_name.clone(), section_map);
1725 }
1726
1727 let connection_section = settings.entry("connection".to_string()).or_default();
1728 connection_section.insert(
1729 "id".to_string(),
1730 Value::from(config.id.clone().unwrap_or(existing_profile.id.clone())),
1731 );
1732 connection_section.insert("uuid".to_string(), Value::from(config.uuid.clone()));
1733 connection_section.insert("type".to_string(), Value::from("vpn"));
1734 connection_section.insert(
1735 "autoconnect".to_string(),
1736 Value::from(config.autoconnect.unwrap_or(existing_profile.autoconnect)),
1737 );
1738
1739 let service_type = existing_vpn_settings
1740 .get("service-type")
1741 .cloned()
1742 .unwrap_or_else(|| {
1743 Self::service_type_from_vpn_type(&existing_profile.vpn_type).to_string()
1744 });
1745
1746 let vpn_section = settings.entry("vpn".to_string()).or_default();
1747 vpn_section.insert("service-type".to_string(), Value::from(service_type));
1748
1749 let mut merged_settings = existing_vpn_settings;
1750 if let Some(username) = config.username {
1751 merged_settings.insert("user-name".to_string(), username);
1752 }
1753 if let Some(gateway) = config.gateway {
1754 merged_settings.insert("remote".to_string(), gateway);
1755 }
1756 if let Some(ca_cert_path) = config.ca_cert_path {
1757 merged_settings.insert("ca".to_string(), ca_cert_path);
1758 }
1759 if let Some(user_cert_path) = config.user_cert_path {
1760 merged_settings.insert("cert".to_string(), user_cert_path);
1761 }
1762 if let Some(private_key_path) = config.private_key_path {
1763 merged_settings.insert("key".to_string(), private_key_path);
1764 }
1765 if let Some(private_key_password) = config.private_key_password {
1766 merged_settings.insert("key-password".to_string(), private_key_password);
1767 }
1768 if let Some(custom_settings) = config.settings {
1769 for (k, v) in custom_settings {
1770 merged_settings.insert(k, v);
1771 }
1772 }
1773
1774 for (k, v) in merged_settings {
1775 vpn_section.insert(k, Value::from(v));
1776 }
1777
1778 let mut merged_secrets = existing_vpn_secrets;
1779 if let Some(password) = config.password {
1780 merged_secrets.insert("password".to_string(), password);
1781 }
1782 if let Some(custom_secrets) = config.secrets {
1783 for (k, v) in custom_secrets {
1784 merged_secrets.insert(k, v);
1785 }
1786 }
1787
1788 if merged_secrets.is_empty() {
1789 settings.remove("vpn-secrets");
1790 } else {
1791 let vpn_secrets_section = settings.entry("vpn-secrets".to_string()).or_default();
1792 vpn_secrets_section.clear();
1793 for (k, v) in merged_secrets {
1794 vpn_secrets_section.insert(k, Value::from(v));
1795 }
1796 }
1797
1798 let conn_proxy = zbus::blocking::Proxy::new(
1799 &self.connection,
1800 "org.freedesktop.NetworkManager",
1801 conn_path.as_str(),
1802 "org.freedesktop.NetworkManager.Settings.Connection",
1803 )?;
1804 conn_proxy.call::<_, _, ()>("Update", &(settings,))?;
1805
1806 let updated_settings = self.get_connection_settings(&conn_path)?;
1807 self.vpn_profile_from_settings(&updated_settings)
1808 .ok_or(crate::error::NetworkError::VpnProfileNotFound(config.uuid))
1809 }
1810
1811 pub fn delete_vpn_profile(&self, uuid: String) -> Result<()> {
1813 let conn_path = self.find_connection_path_by_uuid(&uuid)?;
1814
1815 let conn_proxy = zbus::blocking::Proxy::new(
1816 &self.connection,
1817 "org.freedesktop.NetworkManager",
1818 conn_path.as_str(),
1819 "org.freedesktop.NetworkManager.Settings.Connection",
1820 )?;
1821
1822 conn_proxy.call::<_, _, ()>("Delete", &())?;
1823 Ok(())
1824 }
1825}
1826
1827pub fn init(
1829 app: &AppHandle<tauri::Wry>,
1830 _api: PluginApi<tauri::Wry, ()>,
1831) -> Result<VSKNetworkManager<'static, tauri::Wry>> {
1832 VSKNetworkManager::new(app.clone())
1833}
1834
1835#[cfg(test)]
1836mod tests {
1837 use super::*;
1838
1839 #[test]
1840 fn vpn_state_deactivated_maps_to_disconnected() {
1841 assert_eq!(
1842 VSKNetworkManager::<tauri::Wry>::vpn_state_from_active_state(4),
1843 VpnConnectionState::Disconnected
1844 );
1845 }
1846
1847 #[test]
1852 fn un_tun_ajeno_tambien_es_una_vpn() {
1853 type NM = VSKNetworkManager<'static, tauri::Wry>;
1854
1855 assert!(
1856 NM::tunnel_priority("tun").is_some(),
1857 "Twingate quedaba afuera"
1858 );
1859 assert!(NM::tunnel_priority("wireguard").is_some());
1860 assert!(NM::tunnel_priority("vpn").is_some());
1861
1862 assert!(NM::tunnel_priority("802-11-wireless").is_none());
1865 assert!(NM::tunnel_priority("ethernet").is_none());
1866 assert!(NM::tunnel_priority("loopback").is_none());
1867 assert!(NM::tunnel_priority("bridge").is_none());
1868 }
1869
1870 #[test]
1873 fn una_vpn_de_networkmanager_le_gana_a_un_tun() {
1874 type NM = VSKNetworkManager<'static, tauri::Wry>;
1875 assert!(NM::tunnel_priority("vpn") > NM::tunnel_priority("wireguard"));
1876 assert!(NM::tunnel_priority("wireguard") > NM::tunnel_priority("tun"));
1877 }
1878
1879 #[test]
1886 #[ignore]
1887 fn el_tunel_de_esta_maquina_se_ve_como_vpn() {
1888 type NM = VSKNetworkManager<'static, tauri::Wry>;
1889 use zbus::names::InterfaceName;
1890 use zbus::zvariant::Value;
1891
1892 let conexion = zbus::blocking::Connection::system().expect("sin bus del sistema");
1893 let nm = zbus::blocking::fdo::PropertiesProxy::builder(&conexion)
1894 .destination("org.freedesktop.NetworkManager")
1895 .unwrap()
1896 .path("/org/freedesktop/NetworkManager")
1897 .unwrap()
1898 .build()
1899 .expect("NetworkManager no está");
1900
1901 let activas = nm
1902 .get(
1903 InterfaceName::from_static_str_unchecked("org.freedesktop.NetworkManager"),
1904 "ActiveConnections",
1905 )
1906 .unwrap();
1907
1908 let mut encontrados = Vec::new();
1909 if let Ok(Value::Array(arr)) = activas.downcast_ref() {
1910 for valor in arr.iter() {
1911 let Value::ObjectPath(ruta) = valor else {
1912 continue;
1913 };
1914 let props = zbus::blocking::fdo::PropertiesProxy::builder(&conexion)
1915 .destination("org.freedesktop.NetworkManager")
1916 .unwrap()
1917 .path(ruta)
1918 .unwrap()
1919 .build()
1920 .unwrap();
1921 let leer = |p: &'static str| {
1922 props.get(
1923 InterfaceName::from_static_str_unchecked(
1924 "org.freedesktop.NetworkManager.Connection.Active",
1925 ),
1926 p,
1927 )
1928 };
1929 let tipo = match leer("Type").unwrap().downcast_ref() {
1930 Ok(Value::Str(v)) => v.to_string(),
1931 _ => continue,
1932 };
1933 let id = match leer("Id").unwrap().downcast_ref() {
1934 Ok(Value::Str(v)) => v.to_string(),
1935 _ => String::new(),
1936 };
1937 match NM::tunnel_priority(&tipo) {
1938 Some(prioridad) => {
1939 let nombre = NM::tunnel_display_name(&id, Some(&id));
1940 println!("túnel: id={id} tipo={tipo} prioridad={prioridad} se muestra como «{nombre}»");
1941 encontrados.push(nombre);
1942 }
1943 None => println!("no es túnel: id={id} tipo={tipo}"),
1944 }
1945 }
1946 }
1947
1948 assert!(
1949 !encontrados.is_empty(),
1950 "ningún túnel detectado; levantá la VPN antes de correr esto"
1951 );
1952 }
1953
1954 #[test]
1956 fn los_tuneles_conocidos_se_muestran_con_su_nombre() {
1957 type NM = VSKNetworkManager<'static, tauri::Wry>;
1958
1959 assert_eq!(
1960 NM::tunnel_display_name("sdwan0", Some("sdwan0")),
1961 "Twingate"
1962 );
1963 assert_eq!(
1964 NM::tunnel_display_name("tailscale0", Some("tailscale0")),
1965 "Tailscale"
1966 );
1967 assert_eq!(NM::tunnel_display_name("wg0", Some("wg0")), "WireGuard");
1968
1969 assert_eq!(NM::tunnel_display_name("Oficina", Some("tun0")), "Oficina");
1973 }
1974
1975 #[test]
1987 fn scan_settled_solo_es_cierto_cuando_termino_un_barrido_nuevo() {
1988 type NM = VSKNetworkManager<'static, tauri::Wry>;
1989
1990 assert!(
1992 !NM::scan_settled(1000, 1000),
1993 "con el mismo LastScan no hay barrido nuevo: no se puede leer todavía"
1994 );
1995
1996 assert!(
1998 NM::scan_settled(1000, 1500),
1999 "LastScan avanzó: el barrido terminó y la lista ya es la fresca"
2000 );
2001
2002 assert!(
2005 !NM::scan_settled(-1, -1),
2006 "«nunca escaneó» no es un barrido terminado"
2007 );
2008 assert!(
2009 !NM::scan_settled(1000, -1),
2010 "volver a -1 (reset del contador) no es un barrido terminado: la regresión"
2011 );
2012 }
2013
2014 #[test]
2023 #[ignore]
2024 fn el_barrido_real_hace_avanzar_last_scan() {
2025 type NM = VSKNetworkManager<'static, tauri::Wry>;
2026 use zbus::names::InterfaceName;
2027 use zbus::zvariant::Value;
2028
2029 let conexion = zbus::blocking::Connection::system().expect("sin bus del sistema");
2030 let nm = zbus::blocking::fdo::PropertiesProxy::builder(&conexion)
2031 .destination("org.freedesktop.NetworkManager")
2032 .unwrap()
2033 .path("/org/freedesktop/NetworkManager")
2034 .unwrap()
2035 .build()
2036 .expect("NetworkManager no está");
2037
2038 let devices = nm
2039 .get(
2040 InterfaceName::from_static_str_unchecked("org.freedesktop.NetworkManager"),
2041 "Devices",
2042 )
2043 .unwrap();
2044
2045 let mut wireless_path = None;
2047 if let Ok(Value::Array(arr)) = devices.downcast_ref() {
2048 for valor in arr.iter() {
2049 let Value::ObjectPath(ruta) = valor else {
2050 continue;
2051 };
2052 let props = zbus::blocking::fdo::PropertiesProxy::builder(&conexion)
2053 .destination("org.freedesktop.NetworkManager")
2054 .unwrap()
2055 .path(ruta)
2056 .unwrap()
2057 .build()
2058 .unwrap();
2059 let tipo = props.get(
2060 InterfaceName::from_static_str_unchecked(
2061 "org.freedesktop.NetworkManager.Device",
2062 ),
2063 "DeviceType",
2064 );
2065 if let Ok(valor) = tipo {
2066 if let Ok(Value::U32(2)) = valor.downcast_ref() {
2067 wireless_path = Some(ruta.to_string());
2068 break;
2069 }
2070 }
2071 }
2072 }
2073
2074 let ruta = wireless_path.expect("no hay placa inalámbrica; este test la necesita");
2075
2076 let props = zbus::blocking::fdo::PropertiesProxy::builder(&conexion)
2077 .destination("org.freedesktop.NetworkManager")
2078 .unwrap()
2079 .path(ruta.as_str())
2080 .unwrap()
2081 .build()
2082 .unwrap();
2083
2084 let antes = NM::read_last_scan(&props);
2085 println!("LastScan antes del barrido: {antes}");
2086
2087 let wireless_proxy = zbus::blocking::Proxy::new(
2088 &conexion,
2089 "org.freedesktop.NetworkManager",
2090 ruta.as_str(),
2091 "org.freedesktop.NetworkManager.Device.Wireless",
2092 )
2093 .unwrap();
2094 let options: std::collections::HashMap<String, zbus::zvariant::OwnedValue> =
2095 std::collections::HashMap::new();
2096 let _ = wireless_proxy.call::<_, _, ()>("RequestScan", &(options,));
2098
2099 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
2100 let mut settled = false;
2101 while std::time::Instant::now() < deadline {
2102 let ahora = NM::read_last_scan(&props);
2103 if NM::scan_settled(antes, ahora) {
2104 println!("LastScan avanzó a {ahora}: el barrido terminó");
2105 settled = true;
2106 break;
2107 }
2108 std::thread::sleep(std::time::Duration::from_millis(300));
2109 }
2110
2111 assert!(
2112 settled,
2113 "LastScan no avanzó en 10 s: o la lógica de espera está rota o NM no barrió"
2114 );
2115 }
2116}