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