Rust · 34150 bytes Raw Blame History
1 //! Network module - WiFi control via NetworkManager D-Bus
2 //!
3 //! Uses org.freedesktop.NetworkManager interface on system bus.
4
5 use anyhow::{Context, Result};
6 use tracing::{info, warn, debug};
7 use zbus::blocking::Connection;
8 use zbus::zvariant::Value;
9
10 use super::eap::EapFormState;
11
12 /// WiFi access point info
13 #[derive(Debug, Clone)]
14 pub struct AccessPoint {
15 /// SSID (network name)
16 pub ssid: String,
17 /// Signal strength (0-100)
18 pub strength: u8,
19 /// Whether currently connected
20 pub connected: bool,
21 /// Security type (none, wpa, wpa2, etc.)
22 pub security: String,
23 /// Whether this is an enterprise (802.1X) network
24 pub is_enterprise: bool,
25 /// D-Bus object path for this AP
26 pub path: String,
27 }
28
29 /// Network state
30 #[derive(Debug, Clone, Default)]
31 pub struct NetworkState {
32 pub wifi_enabled: bool,
33 pub wifi_available: bool,
34 /// Currently connected network SSID
35 pub connected_ssid: Option<String>,
36 /// Available access points
37 pub access_points: Vec<AccessPoint>,
38 /// Whether a scan is in progress
39 pub scanning: bool,
40 }
41
42 /// Network module via NetworkManager D-Bus
43 pub struct NetworkModule {
44 conn: Option<Connection>,
45 state: NetworkState,
46 }
47
48 impl NetworkModule {
49 /// Create a new network module
50 pub fn new() -> Self {
51 Self {
52 conn: None,
53 state: NetworkState::default(),
54 }
55 }
56
57 /// Connect to D-Bus and query initial state
58 pub fn connect(&mut self) -> Result<()> {
59 let conn = Connection::system().context("Failed to connect to system D-Bus")?;
60
61 // Check if NetworkManager is available
62 let nm_running = self.check_nm_available(&conn);
63 if !nm_running {
64 warn!("NetworkManager not available");
65 self.state.wifi_available = false;
66 self.conn = Some(conn);
67 return Ok(());
68 }
69
70 self.state.wifi_available = true;
71 self.conn = Some(conn);
72
73 // Query initial WiFi state
74 self.update_state()?;
75
76 info!("Network module connected, WiFi enabled: {}", self.state.wifi_enabled);
77 Ok(())
78 }
79
80 /// Check if NetworkManager is running
81 fn check_nm_available(&self, conn: &Connection) -> bool {
82 let result: Result<String, zbus::Error> = conn.call_method(
83 Some("org.freedesktop.DBus"),
84 "/org/freedesktop/DBus",
85 Some("org.freedesktop.DBus"),
86 "GetNameOwner",
87 &("org.freedesktop.NetworkManager",),
88 ).and_then(|m: zbus::Message| m.body().deserialize());
89
90 result.is_ok()
91 }
92
93 /// Update state from NetworkManager
94 pub fn update_state(&mut self) -> Result<()> {
95 if let Some(ref conn) = self.conn {
96 if !self.state.wifi_available {
97 return Ok(());
98 }
99
100 // Get WirelessEnabled property
101 match self.get_bool_property(conn, "WirelessEnabled") {
102 Ok(enabled) => {
103 self.state.wifi_enabled = enabled;
104 }
105 Err(e) => {
106 debug!("Failed to get WirelessEnabled: {}", e);
107 }
108 }
109 }
110 Ok(())
111 }
112
113 /// Get a boolean property from NetworkManager
114 fn get_bool_property(&self, conn: &Connection, property: &str) -> Result<bool> {
115 let reply: zbus::Message = conn.call_method(
116 Some("org.freedesktop.NetworkManager"),
117 "/org/freedesktop/NetworkManager",
118 Some("org.freedesktop.DBus.Properties"),
119 "Get",
120 &("org.freedesktop.NetworkManager", property),
121 )?;
122
123 // Properties.Get returns a Variant<T>
124 let body = reply.body();
125 let value: Value = body.deserialize()?;
126 match value {
127 Value::Value(inner) => {
128 if let Value::Bool(b) = *inner {
129 Ok(b)
130 } else {
131 anyhow::bail!("Expected bool, got {:?}", inner)
132 }
133 }
134 Value::Bool(b) => Ok(b),
135 _ => anyhow::bail!("Expected bool variant, got {:?}", value),
136 }
137 }
138
139 /// Set a property on NetworkManager
140 fn set_property(&self, conn: &Connection, property: &str, value: bool) -> Result<()> {
141 conn.call_method(
142 Some("org.freedesktop.NetworkManager"),
143 "/org/freedesktop/NetworkManager",
144 Some("org.freedesktop.DBus.Properties"),
145 "Set",
146 &("org.freedesktop.NetworkManager", property, Value::Bool(value)),
147 )?;
148 Ok(())
149 }
150
151 /// Toggle WiFi on/off
152 pub fn toggle_wifi(&mut self) -> Result<()> {
153 if let Some(ref conn) = self.conn {
154 if !self.state.wifi_available {
155 warn!("WiFi not available");
156 return Ok(());
157 }
158
159 let new_state = !self.state.wifi_enabled;
160 info!("Toggling WiFi: {} -> {}", self.state.wifi_enabled, new_state);
161
162 self.set_property(conn, "WirelessEnabled", new_state)?;
163 self.state.wifi_enabled = new_state;
164 }
165 Ok(())
166 }
167
168 /// Enable WiFi
169 pub fn enable_wifi(&mut self) -> Result<()> {
170 if let Some(ref conn) = self.conn {
171 if !self.state.wifi_available {
172 return Ok(());
173 }
174 self.set_property(conn, "WirelessEnabled", true)?;
175 self.state.wifi_enabled = true;
176 info!("WiFi enabled");
177 }
178 Ok(())
179 }
180
181 /// Disable WiFi
182 pub fn disable_wifi(&mut self) -> Result<()> {
183 if let Some(ref conn) = self.conn {
184 if !self.state.wifi_available {
185 return Ok(());
186 }
187 self.set_property(conn, "WirelessEnabled", false)?;
188 self.state.wifi_enabled = false;
189 info!("WiFi disabled");
190 }
191 Ok(())
192 }
193
194 /// Get current state
195 pub fn state(&self) -> &NetworkState {
196 &self.state
197 }
198
199 /// Check if WiFi is enabled
200 pub fn is_wifi_enabled(&self) -> bool {
201 self.state.wifi_enabled
202 }
203
204 /// Check if WiFi hardware is available
205 pub fn is_wifi_available(&self) -> bool {
206 self.state.wifi_available
207 }
208
209 /// Get connected network SSID
210 pub fn connected_ssid(&self) -> Option<&str> {
211 self.state.connected_ssid.as_deref()
212 }
213
214 /// Get available access points
215 pub fn access_points(&self) -> &[AccessPoint] {
216 &self.state.access_points
217 }
218
219 /// Check if scan is in progress
220 pub fn is_scanning(&self) -> bool {
221 self.state.scanning
222 }
223
224 /// Mark scan as starting (for UI feedback before blocking scan)
225 pub fn set_scanning(&mut self, scanning: bool) {
226 self.state.scanning = scanning;
227 }
228
229 /// Scan for WiFi networks
230 pub fn scan_networks(&mut self) -> Result<()> {
231 // Refresh wifi_enabled state before scanning (may have changed since startup)
232 self.update_state()?;
233
234 let conn = match &self.conn {
235 Some(c) => c,
236 None => {
237 debug!("No D-Bus connection for WiFi scan");
238 return Ok(());
239 }
240 };
241
242 if !self.state.wifi_available || !self.state.wifi_enabled {
243 debug!("WiFi not available or not enabled, skipping scan");
244 self.state.access_points.clear();
245 self.state.scanning = false;
246 return Ok(());
247 }
248
249 info!("Scanning for WiFi networks...");
250 self.state.scanning = true;
251
252 // Get wireless device
253 let wifi_device = match self.get_wifi_device(conn) {
254 Some(d) => {
255 info!("Found WiFi device: {}", d);
256 d
257 }
258 None => {
259 warn!("No WiFi device found");
260 self.state.scanning = false;
261 return Ok(());
262 }
263 };
264
265 // Request scan and wait briefly for adapter to discover networks
266 if self.request_scan(conn, &wifi_device).is_ok() {
267 // Give the adapter time to scan (especially after WiFi was just enabled)
268 std::thread::sleep(std::time::Duration::from_millis(1500));
269 }
270
271 // Get access points
272 let aps = match self.get_access_points(conn, &wifi_device) {
273 Ok(a) => a,
274 Err(e) => {
275 warn!("Failed to get access points: {}", e);
276 return Err(e);
277 }
278 };
279
280 // Get active connection to find connected SSID
281 self.state.connected_ssid = self.get_active_ssid(conn, &wifi_device);
282
283 // Mark connected AP
284 let connected_ssid = self.state.connected_ssid.clone();
285 self.state.access_points = aps.into_iter()
286 .map(|mut ap| {
287 ap.connected = Some(&ap.ssid) == connected_ssid.as_ref();
288 ap
289 })
290 .collect();
291
292 // Sort by signal strength (strongest first), connected first
293 self.state.access_points.sort_by(|a, b| {
294 match (a.connected, b.connected) {
295 (true, false) => std::cmp::Ordering::Less,
296 (false, true) => std::cmp::Ordering::Greater,
297 _ => b.strength.cmp(&a.strength),
298 }
299 });
300
301 self.state.scanning = false;
302 info!("Found {} access points, connected: {:?}",
303 self.state.access_points.len(),
304 self.state.connected_ssid);
305 Ok(())
306 }
307
308 /// Get the first WiFi device path
309 fn get_wifi_device(&self, conn: &Connection) -> Option<String> {
310 // GetDevices returns array of object paths
311 let reply: zbus::Message = conn.call_method(
312 Some("org.freedesktop.NetworkManager"),
313 "/org/freedesktop/NetworkManager",
314 Some("org.freedesktop.NetworkManager"),
315 "GetDevices",
316 &(),
317 ).ok()?;
318
319 let devices: Vec<zbus::zvariant::OwnedObjectPath> = reply.body().deserialize().ok()?;
320
321 for device in devices {
322 let device_path = device.to_string();
323 // Check device type (2 = WiFi)
324 if let Ok(device_type) = self.get_device_type(conn, &device_path) {
325 if device_type == 2 {
326 return Some(device_path);
327 }
328 }
329 }
330 None
331 }
332
333 /// Get device type
334 fn get_device_type(&self, conn: &Connection, device_path: &str) -> Result<u32> {
335 let reply: zbus::Message = conn.call_method(
336 Some("org.freedesktop.NetworkManager"),
337 device_path,
338 Some("org.freedesktop.DBus.Properties"),
339 "Get",
340 &("org.freedesktop.NetworkManager.Device", "DeviceType"),
341 )?;
342
343 let body = reply.body();
344 let value: Value = body.deserialize()?;
345 match value {
346 Value::Value(inner) => {
347 if let Value::U32(t) = *inner {
348 Ok(t)
349 } else {
350 anyhow::bail!("Expected u32")
351 }
352 }
353 Value::U32(t) => Ok(t),
354 _ => anyhow::bail!("Expected u32 variant"),
355 }
356 }
357
358 /// Request WiFi scan (non-blocking)
359 fn request_scan(&self, conn: &Connection, device_path: &str) -> Result<()> {
360 let options: std::collections::HashMap<&str, Value> = std::collections::HashMap::new();
361 conn.call_method(
362 Some("org.freedesktop.NetworkManager"),
363 device_path,
364 Some("org.freedesktop.NetworkManager.Device.Wireless"),
365 "RequestScan",
366 &(options,),
367 )?;
368 Ok(())
369 }
370
371 /// Get access points from wireless device
372 fn get_access_points(&self, conn: &Connection, device_path: &str) -> Result<Vec<AccessPoint>> {
373 let reply: zbus::Message = conn.call_method(
374 Some("org.freedesktop.NetworkManager"),
375 device_path,
376 Some("org.freedesktop.NetworkManager.Device.Wireless"),
377 "GetAccessPoints",
378 &(),
379 )?;
380
381 let body = reply.body();
382 let ap_paths: Vec<zbus::zvariant::OwnedObjectPath> = body.deserialize()?;
383 debug!("Got {} AP paths from D-Bus", ap_paths.len());
384 let mut aps = Vec::new();
385
386 for ap_path in ap_paths {
387 let path_str = ap_path.to_string();
388 debug!("Getting info for AP: {}", path_str);
389 match self.get_ap_info(conn, &path_str) {
390 Ok(ap) => {
391 debug!(" SSID: '{}', strength: {}", ap.ssid, ap.strength);
392 // Skip hidden networks (empty SSID)
393 if !ap.ssid.is_empty() {
394 aps.push(ap);
395 }
396 }
397 Err(e) => {
398 debug!(" Failed to get AP info: {}", e);
399 }
400 }
401 }
402
403 // Deduplicate by SSID (keep strongest signal)
404 let mut seen: std::collections::HashMap<String, AccessPoint> = std::collections::HashMap::new();
405 for ap in aps {
406 seen.entry(ap.ssid.clone())
407 .and_modify(|existing| {
408 if ap.strength > existing.strength {
409 *existing = ap.clone();
410 }
411 })
412 .or_insert(ap);
413 }
414
415 Ok(seen.into_values().collect())
416 }
417
418 /// Get access point info
419 fn get_ap_info(&self, conn: &Connection, ap_path: &str) -> Result<AccessPoint> {
420 // Get SSID (byte array)
421 let ssid = self.get_ap_ssid(conn, ap_path)?;
422
423 // Get Strength (u8, 0-100)
424 let strength = self.get_ap_strength(conn, ap_path).unwrap_or(0);
425
426 // Get security flags
427 let (security, is_enterprise) = self.get_ap_security(conn, ap_path)
428 .unwrap_or_else(|| ("Open".to_string(), false));
429
430 Ok(AccessPoint {
431 ssid,
432 strength,
433 connected: false, // Will be set later
434 security,
435 is_enterprise,
436 path: ap_path.to_string(),
437 })
438 }
439
440 /// Get AP SSID
441 fn get_ap_ssid(&self, conn: &Connection, ap_path: &str) -> Result<String> {
442 let reply: zbus::Message = conn.call_method(
443 Some("org.freedesktop.NetworkManager"),
444 ap_path,
445 Some("org.freedesktop.DBus.Properties"),
446 "Get",
447 &("org.freedesktop.NetworkManager.AccessPoint", "Ssid"),
448 )?;
449
450 let body = reply.body();
451 // Properties.Get returns variant<ay> - try to deserialize directly
452 let variant: zbus::zvariant::OwnedValue = body.deserialize()?;
453
454 // Debug: print the variant structure
455 let json = serde_json::to_value(&variant).unwrap_or(serde_json::Value::Null);
456 debug!("SSID json: {}", json);
457
458 // The variant should contain an array of bytes
459 // Try different parsing strategies
460 if let serde_json::Value::Array(arr) = &json {
461 let bytes: Vec<u8> = arr.iter()
462 .filter_map(|v| v.as_u64().map(|n| n as u8))
463 .collect();
464 if !bytes.is_empty() {
465 return Ok(String::from_utf8_lossy(&bytes).to_string());
466 }
467 }
468
469 // Try as zvariant object with "zvariant::Value::Value" key
470 if let serde_json::Value::Object(obj) = &json {
471 if let Some(serde_json::Value::Array(arr)) = obj.get("zvariant::Value::Value") {
472 let bytes: Vec<u8> = arr.iter()
473 .filter_map(|v| v.as_u64().map(|n| n as u8))
474 .collect();
475 if !bytes.is_empty() {
476 return Ok(String::from_utf8_lossy(&bytes).to_string());
477 }
478 }
479 }
480
481 Err(anyhow::anyhow!("Could not parse SSID from: {}", json))
482 }
483
484 /// Get AP signal strength
485 fn get_ap_strength(&self, conn: &Connection, ap_path: &str) -> Option<u8> {
486 let reply: zbus::Message = conn.call_method(
487 Some("org.freedesktop.NetworkManager"),
488 ap_path,
489 Some("org.freedesktop.DBus.Properties"),
490 "Get",
491 &("org.freedesktop.NetworkManager.AccessPoint", "Strength"),
492 ).ok()?;
493
494 let body = reply.body();
495 let value: Value = body.deserialize().ok()?;
496 match value {
497 Value::Value(inner) => {
498 if let Value::U8(s) = *inner { Some(s) } else { None }
499 }
500 Value::U8(s) => Some(s),
501 _ => None,
502 }
503 }
504
505 /// Get AP security type and whether it's an enterprise network
506 fn get_ap_security(&self, conn: &Connection, ap_path: &str) -> Option<(String, bool)> {
507 const KEY_MGMT_802_1X: u32 = 0x200;
508
509 // WpaFlags property
510 let reply: zbus::Message = conn.call_method(
511 Some("org.freedesktop.NetworkManager"),
512 ap_path,
513 Some("org.freedesktop.DBus.Properties"),
514 "Get",
515 &("org.freedesktop.NetworkManager.AccessPoint", "WpaFlags"),
516 ).ok()?;
517
518 let body = reply.body();
519 let value: Value = body.deserialize().ok()?;
520 let wpa_flags = match value {
521 Value::Value(inner) => {
522 if let Value::U32(f) = *inner { f } else { 0 }
523 }
524 Value::U32(f) => f,
525 _ => 0,
526 };
527
528 // RsnFlags (WPA2)
529 let rsn_flags = self.get_ap_rsn_flags(conn, ap_path).unwrap_or(0);
530
531 let is_enterprise = (wpa_flags & KEY_MGMT_802_1X) != 0 || (rsn_flags & KEY_MGMT_802_1X) != 0;
532
533 let security = if rsn_flags > 0 {
534 if is_enterprise { "WPA2-Ent" } else { "WPA2" }
535 } else if wpa_flags > 0 {
536 if is_enterprise { "WPA-Ent" } else { "WPA" }
537 } else {
538 "Open"
539 };
540
541 Some((security.to_string(), is_enterprise))
542 }
543
544 /// Get AP RSN (WPA2) flags
545 fn get_ap_rsn_flags(&self, conn: &Connection, ap_path: &str) -> Option<u32> {
546 let reply: zbus::Message = conn.call_method(
547 Some("org.freedesktop.NetworkManager"),
548 ap_path,
549 Some("org.freedesktop.DBus.Properties"),
550 "Get",
551 &("org.freedesktop.NetworkManager.AccessPoint", "RsnFlags"),
552 ).ok()?;
553
554 let body = reply.body();
555 let value: Value = body.deserialize().ok()?;
556 match value {
557 Value::Value(inner) => {
558 if let Value::U32(f) = *inner { Some(f) } else { None }
559 }
560 Value::U32(f) => Some(f),
561 _ => None,
562 }
563 }
564
565 /// Connect to a WiFi network
566 /// If password is None and network is secured, this will try to use saved credentials
567 pub fn connect_to_network(&mut self, ssid: &str) -> Result<()> {
568 let conn = match &self.conn {
569 Some(c) => c,
570 None => anyhow::bail!("No D-Bus connection"),
571 };
572
573 if !self.state.wifi_available || !self.state.wifi_enabled {
574 anyhow::bail!("WiFi not available or not enabled");
575 }
576
577 info!("Connecting to WiFi network: {}", ssid);
578
579 // Get the WiFi device
580 let wifi_device = match self.get_wifi_device(conn) {
581 Some(d) => d,
582 None => anyhow::bail!("No WiFi device found"),
583 };
584
585 // Find the access point path for this SSID
586 let ap_path = self.state.access_points.iter()
587 .find(|ap| ap.ssid == ssid)
588 .map(|ap| ap.path.clone())
589 .ok_or_else(|| anyhow::anyhow!("Access point not found: {}", ssid))?;
590
591 // Try to find existing connection settings for this SSID
592 if let Some(connection_path) = self.find_connection_for_ssid(conn, ssid) {
593 info!("Found existing connection profile, activating: {}", connection_path);
594 return self.activate_connection(conn, &connection_path, &wifi_device, &ap_path);
595 }
596
597 // No existing connection - try to connect (NM will create one for open networks)
598 // For secured networks without saved credentials, this will fail
599 info!("No saved connection, attempting AddAndActivateConnection");
600 self.add_and_activate_connection(conn, ssid, &wifi_device, &ap_path)
601 }
602
603 /// Connect to a WiFi network with a password
604 pub fn connect_with_password(&mut self, ssid: &str, password: &str) -> Result<()> {
605 if !self.state.wifi_available || !self.state.wifi_enabled {
606 anyhow::bail!("WiFi not available or not enabled");
607 }
608
609 info!("Connecting to WiFi network with password: {}", ssid);
610
611 // Use nmcli which handles secrets properly
612 // nmcli device wifi connect <SSID> password <password>
613 let output = std::process::Command::new("nmcli")
614 .args(["device", "wifi", "connect", ssid, "password", password])
615 .output()
616 .context("Failed to run nmcli")?;
617
618 if output.status.success() {
619 info!("Successfully connected to {}", ssid);
620 Ok(())
621 } else {
622 let stderr = String::from_utf8_lossy(&output.stderr);
623 warn!("nmcli failed: {}", stderr);
624 anyhow::bail!("Failed to connect: {}", stderr.trim())
625 }
626 }
627
628 /// Check if a network requires a password (is secured)
629 pub fn network_needs_password(&self, ssid: &str) -> bool {
630 // Check if we have saved credentials
631 if let Some(ref conn) = self.conn {
632 if self.find_connection_for_ssid(conn, ssid).is_some() {
633 return false; // Has saved connection
634 }
635 }
636
637 // Check if network is secured
638 self.state.access_points.iter()
639 .find(|ap| ap.ssid == ssid)
640 .map(|ap| ap.security != "Open")
641 .unwrap_or(false)
642 }
643
644 /// Check if a network requires EAP (enterprise) authentication
645 pub fn network_needs_eap(&self, ssid: &str) -> bool {
646 if let Some(ref conn) = self.conn {
647 if self.find_connection_for_ssid(conn, ssid).is_some() {
648 return false;
649 }
650 }
651 self.state.access_points.iter()
652 .find(|ap| ap.ssid == ssid)
653 .map(|ap| ap.is_enterprise)
654 .unwrap_or(false)
655 }
656
657 /// Connect to an enterprise WiFi network with EAP credentials
658 pub fn connect_with_eap(&mut self, form: &EapFormState) -> Result<()> {
659 if !self.state.wifi_available || !self.state.wifi_enabled {
660 anyhow::bail!("WiFi not available or not enabled");
661 }
662
663 info!("Connecting to enterprise WiFi: {} ({:?})", form.ssid, form.eap_method);
664
665 // Use nmcli for enterprise WiFi - it handles the complex settings correctly
666 let mut cmd = std::process::Command::new("nmcli");
667 cmd.args([
668 "connection", "add",
669 "type", "wifi",
670 "con-name", &form.ssid,
671 "ssid", &form.ssid,
672 "wifi-sec.key-mgmt", "wpa-eap",
673 "802-1x.eap", form.eap_method.as_str(),
674 "802-1x.identity", &form.identity,
675 "802-1x.password", &form.password,
676 "802-1x.phase2-auth", form.phase2_auth.as_str(),
677 ]);
678
679 // Add optional anonymous identity
680 if !form.anonymous_identity.is_empty() {
681 cmd.args(["802-1x.anonymous-identity", &form.anonymous_identity]);
682 }
683
684 // Add optional CA certificate
685 if !form.ca_cert_path.is_empty() {
686 cmd.args(["802-1x.ca-cert", &form.ca_cert_path]);
687 }
688
689 let output = cmd.output().context("Failed to run nmcli")?;
690
691 if !output.status.success() {
692 let stderr = String::from_utf8_lossy(&output.stderr);
693 warn!("nmcli connection add failed: {}", stderr);
694 anyhow::bail!("Failed to create connection: {}", stderr.trim());
695 }
696
697 info!("Created enterprise WiFi connection profile");
698
699 // Now activate the connection
700 let output = std::process::Command::new("nmcli")
701 .args(["connection", "up", &form.ssid])
702 .output()
703 .context("Failed to run nmcli")?;
704
705 if output.status.success() {
706 info!("Successfully connected to {}", form.ssid);
707 Ok(())
708 } else {
709 let stderr = String::from_utf8_lossy(&output.stderr);
710 warn!("nmcli connection up failed: {}", stderr);
711 anyhow::bail!("Failed to connect: {}", stderr.trim())
712 }
713 }
714
715 /// Disconnect from current WiFi network
716 pub fn disconnect(&mut self) -> Result<()> {
717 let conn = match &self.conn {
718 Some(c) => c,
719 None => anyhow::bail!("No D-Bus connection"),
720 };
721
722 let wifi_device = match self.get_wifi_device(conn) {
723 Some(d) => d,
724 None => anyhow::bail!("No WiFi device found"),
725 };
726
727 info!("Disconnecting WiFi device");
728 conn.call_method(
729 Some("org.freedesktop.NetworkManager"),
730 wifi_device.as_str(),
731 Some("org.freedesktop.NetworkManager.Device"),
732 "Disconnect",
733 &(),
734 )?;
735
736 self.state.connected_ssid = None;
737 Ok(())
738 }
739
740 /// Find existing connection settings for an SSID
741 fn find_connection_for_ssid(&self, conn: &Connection, ssid: &str) -> Option<String> {
742 // Get all connection settings from Settings service
743 let reply: zbus::Message = conn.call_method(
744 Some("org.freedesktop.NetworkManager"),
745 "/org/freedesktop/NetworkManager/Settings",
746 Some("org.freedesktop.NetworkManager.Settings"),
747 "ListConnections",
748 &(),
749 ).ok()?;
750
751 let body = reply.body();
752 let connections: Vec<zbus::zvariant::OwnedObjectPath> = body.deserialize().ok()?;
753
754 for conn_path in connections {
755 let path_str = conn_path.to_string();
756 if let Some(conn_ssid) = self.get_connection_ssid(conn, &path_str) {
757 if conn_ssid == ssid {
758 return Some(path_str);
759 }
760 }
761 }
762 None
763 }
764
765 /// Get SSID from a connection settings object
766 fn get_connection_ssid(&self, conn: &Connection, conn_path: &str) -> Option<String> {
767 // GetSettings returns a{sa{sv}} - dict of setting names to dict of properties
768 let reply: zbus::Message = conn.call_method(
769 Some("org.freedesktop.NetworkManager"),
770 conn_path,
771 Some("org.freedesktop.NetworkManager.Settings.Connection"),
772 "GetSettings",
773 &(),
774 ).ok()?;
775
776 let body = reply.body();
777 let settings: std::collections::HashMap<String, std::collections::HashMap<String, Value>> =
778 body.deserialize().ok()?;
779
780 // Check connection type is 802-11-wireless
781 if let Some(connection) = settings.get("connection") {
782 if let Some(Value::Str(conn_type)) = connection.get("type") {
783 if conn_type.as_str() != "802-11-wireless" {
784 return None;
785 }
786 }
787 }
788
789 // Get SSID from 802-11-wireless settings
790 let wifi_settings = settings.get("802-11-wireless")?;
791 let ssid_value = wifi_settings.get("ssid")?;
792
793 // SSID is stored as ay (array of bytes)
794 match ssid_value {
795 Value::Array(arr) => {
796 let bytes: Vec<u8> = arr.iter()
797 .filter_map(|v| {
798 if let Value::U8(b) = v { Some(*b) } else { None }
799 })
800 .collect();
801 if !bytes.is_empty() {
802 Some(String::from_utf8_lossy(&bytes).to_string())
803 } else {
804 None
805 }
806 }
807 _ => None,
808 }
809 }
810
811 /// Activate an existing connection
812 fn activate_connection(
813 &self,
814 conn: &Connection,
815 connection_path: &str,
816 device_path: &str,
817 ap_path: &str,
818 ) -> Result<()> {
819 use zbus::zvariant::ObjectPath;
820 conn.call_method(
821 Some("org.freedesktop.NetworkManager"),
822 "/org/freedesktop/NetworkManager",
823 Some("org.freedesktop.NetworkManager"),
824 "ActivateConnection",
825 &(
826 ObjectPath::from_str_unchecked(connection_path),
827 ObjectPath::from_str_unchecked(device_path),
828 ObjectPath::from_str_unchecked(ap_path),
829 ),
830 )?;
831 info!("Connection activation requested");
832 Ok(())
833 }
834
835 /// Add and activate a new connection (for open networks or when NM has secrets)
836 fn add_and_activate_connection(
837 &self,
838 conn: &Connection,
839 ssid: &str,
840 device_path: &str,
841 ap_path: &str,
842 ) -> Result<()> {
843 use std::collections::HashMap;
844 use zbus::zvariant::ObjectPath;
845
846 // Build minimal connection settings
847 let mut connection: HashMap<&str, Value> = HashMap::new();
848 connection.insert("type", Value::Str("802-11-wireless".into()));
849 connection.insert("id", Value::Str(ssid.into()));
850
851 let mut wireless: HashMap<&str, Value> = HashMap::new();
852 // SSID as byte array
853 let ssid_bytes: Vec<Value> = ssid.bytes().map(Value::U8).collect();
854 wireless.insert("ssid", Value::Array(ssid_bytes.into()));
855 wireless.insert("mode", Value::Str("infrastructure".into()));
856
857 let mut settings: HashMap<&str, HashMap<&str, Value>> = HashMap::new();
858 settings.insert("connection", connection);
859 settings.insert("802-11-wireless", wireless);
860
861 conn.call_method(
862 Some("org.freedesktop.NetworkManager"),
863 "/org/freedesktop/NetworkManager",
864 Some("org.freedesktop.NetworkManager"),
865 "AddAndActivateConnection",
866 &(
867 settings,
868 ObjectPath::from_str_unchecked(device_path),
869 ObjectPath::from_str_unchecked(ap_path),
870 ),
871 )?;
872
873 info!("AddAndActivateConnection requested for: {}", ssid);
874 Ok(())
875 }
876
877 /// Add and activate a new connection with password (for WPA/WPA2 networks)
878 fn add_and_activate_connection_with_password(
879 &self,
880 conn: &Connection,
881 ssid: &str,
882 password: &str,
883 security: &str,
884 device_path: &str,
885 ap_path: &str,
886 ) -> Result<()> {
887 use std::collections::HashMap;
888 use zbus::zvariant::ObjectPath;
889
890 // Build connection settings
891 let mut connection: HashMap<&str, Value> = HashMap::new();
892 connection.insert("type", Value::Str("802-11-wireless".into()));
893 connection.insert("id", Value::Str(ssid.into()));
894 // Set as user-owned connection so secrets can be saved without root
895 let user = std::env::var("USER").unwrap_or_else(|_| "user".to_string());
896 let permissions = vec![Value::Str(format!("user:{}:", user).into())];
897 connection.insert("permissions", Value::Array(permissions.into()));
898
899 let mut wireless: HashMap<&str, Value> = HashMap::new();
900 // SSID as byte array
901 let ssid_bytes: Vec<Value> = ssid.bytes().map(Value::U8).collect();
902 wireless.insert("ssid", Value::Array(ssid_bytes.into()));
903 wireless.insert("mode", Value::Str("infrastructure".into()));
904
905 // Security settings
906 let mut wireless_security: HashMap<&str, Value> = HashMap::new();
907
908 // Determine key management based on security type
909 let key_mgmt = if security == "WPA2" || security == "WPA" {
910 "wpa-psk"
911 } else {
912 "none"
913 };
914 wireless_security.insert("key-mgmt", Value::Str(key_mgmt.into()));
915 wireless_security.insert("psk", Value::Str(password.into()));
916 // Set psk-flags to 0 (NM_SETTING_SECRET_FLAG_NONE) so NM uses the embedded password
917 // instead of asking a secret agent
918 wireless_security.insert("psk-flags", Value::U32(0));
919
920 // Also set security reference in wireless settings
921 wireless.insert("security", Value::Str("802-11-wireless-security".into()));
922
923 let mut settings: HashMap<&str, HashMap<&str, Value>> = HashMap::new();
924 settings.insert("connection", connection);
925 settings.insert("802-11-wireless", wireless);
926 settings.insert("802-11-wireless-security", wireless_security);
927
928 conn.call_method(
929 Some("org.freedesktop.NetworkManager"),
930 "/org/freedesktop/NetworkManager",
931 Some("org.freedesktop.NetworkManager"),
932 "AddAndActivateConnection",
933 &(
934 settings,
935 ObjectPath::from_str_unchecked(device_path),
936 ObjectPath::from_str_unchecked(ap_path),
937 ),
938 )?;
939
940 info!("AddAndActivateConnection with password requested for: {}", ssid);
941 Ok(())
942 }
943
944 /// Get active connection SSID for a device
945 fn get_active_ssid(&self, conn: &Connection, device_path: &str) -> Option<String> {
946 // Get ActiveAccessPoint from the wireless device
947 let reply: zbus::Message = conn.call_method(
948 Some("org.freedesktop.NetworkManager"),
949 device_path,
950 Some("org.freedesktop.DBus.Properties"),
951 "Get",
952 &("org.freedesktop.NetworkManager.Device.Wireless", "ActiveAccessPoint"),
953 ).ok()?;
954
955 let body = reply.body();
956 let value: Value = body.deserialize().ok()?;
957 let ap_path = match value {
958 Value::Value(inner) => {
959 if let Value::ObjectPath(p) = &*inner {
960 p.to_string()
961 } else {
962 return None;
963 }
964 }
965 Value::ObjectPath(p) => p.to_string(),
966 _ => return None,
967 };
968
969 // "/" means no active connection
970 if ap_path == "/" {
971 return None;
972 }
973
974 self.get_ap_ssid(conn, &ap_path).ok()
975 }
976 }
977
978 impl Default for NetworkModule {
979 fn default() -> Self {
980 Self::new()
981 }
982 }
983