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 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 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 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 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 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 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 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 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 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 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, _ => false,
319 };
320
321 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 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 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 = NetworkManagerHelpers::detect_security_type(&ap_properties_proxy)?;
430 }
431 } else {
432 network_info.icon =
435 Self::get_icon_for(&connection_type_str, network_info.is_connected);
436 }
437 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 if let Ok(zbus::zvariant::Value::ObjectPath(config_path)) =
447 ip4_config_path.downcast_ref()
448 {
449 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 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 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 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 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 pub fn connect_to_wifi(&self, config: WiFiConnectionConfig) -> Result<()> {
725 log::debug!("connect_to_wifi called: ssid='{}' security={:?} username={:?}",
727 config.ssid, config.security_type, config.username);
728
729 let mut connection_settings = HashMap::new();
731 let mut wifi_settings = HashMap::new();
732 let mut security_settings = HashMap::new();
733
734 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 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 match config.security_type {
746 WiFiSecurityType::None => {
747 }
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::debug!("connection_settings: {:#?}", connection_settings);
792
793 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 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 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 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 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 pub fn is_wireless_available(&self) -> Result<bool> {
869 Ok(!self.wireless_device_paths()?.is_empty())
870 }
871
872 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 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 let active_connections_variant: zbus::zvariant::OwnedValue = self.proxy.get(
929 InterfaceName::from_static_str_unchecked("org.freedesktop.NetworkManager"),
930 "ActiveConnections",
931 )?;
932
933 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 pub fn get_saved_wifi_networks(&self) -> Result<Vec<NetworkInfo>> {
957 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 let connections: Vec<zbus::zvariant::OwnedObjectPath> =
967 settings_proxy.call("ListConnections", &())?;
968 let mut saved_networks = Vec::new();
969
970 for conn_path in connections {
972 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 let settings: std::collections::HashMap<String, zbus::zvariant::OwnedValue> =
982 conn_proxy.call("GetSettings", &())?;
983
984 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 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 if conn_type_str == "802-11-wireless" {
1003 let mut network_info = NetworkInfo::default();
1004 network_info.connection_type = "wifi".to_string();
1005
1006 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 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 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 saved_networks.push(network_info);
1065 }
1066 }
1067 }
1068 }
1069
1070 Ok(saved_networks)
1071 }
1072
1073 pub fn delete_wifi_connection(&self, ssid: &str) -> Result<bool> {
1075 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 let connections: Vec<zbus::zvariant::OwnedObjectPath> =
1085 settings_proxy.call("ListConnections", &())?;
1086
1087 for conn_path in connections {
1089 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 let settings: std::collections::HashMap<String, zbus::zvariant::OwnedValue> =
1099 conn_proxy.call("GetSettings", &())?;
1100
1101 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 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 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 if conn_ssid_str == ssid {
1133 conn_proxy.call::<_, _, ()>("Delete", &())?;
1134 return Ok(true);
1135 }
1136 }
1137 }
1138 }
1139 }
1140 }
1141 }
1142
1143 Ok(false)
1145 }
1146
1147 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 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 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 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 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 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 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 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 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 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 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 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 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 #[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 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 #[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 #[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 #[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 assert_eq!(NM::tunnel_display_name("Oficina", Some("tun0")), "Oficina");
1859 }
1860}
1861
1862pub 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}