Skip to main content

crabshell_services/
network.rs

1//! Network devices, Wi-Fi networks and saved connections from
2//! NetworkManager, on the system bus.
3
4mod saved;
5#[cfg(test)]
6mod tests;
7
8use std::cell::RefCell;
9use std::cmp::Ordering;
10use std::collections::btree_map::Entry;
11use std::collections::{BTreeMap, HashMap};
12use std::rc::Rc;
13use std::sync::Once;
14
15use crabshell_reactive::{ReadSignal, Signal};
16use crabshell_ui::Cx;
17use zbus::{Array, Connection, Message, ObjectPath, OwnedObjectPath, Variant};
18
19pub use saved::SavedConnection;
20use saved::{SAVED, SETTINGS, SETTINGS_PATH, SavedConnections};
21
22use crate::Shared;
23use crate::dbus::{self, CALL_TIMEOUT, Daemon, OBJECT_MANAGER, PROPERTIES, Props, set_changed};
24
25const NM: &str = "org.freedesktop.NetworkManager";
26const NM_PATH: &str = "/org/freedesktop/NetworkManager";
27/// Where NetworkManager's object manager is.
28const ROOT: &str = "/org/freedesktop";
29const DEVICE: &str = "org.freedesktop.NetworkManager.Device";
30const WIRELESS: &str = "org.freedesktop.NetworkManager.Device.Wireless";
31const WIRED: &str = "org.freedesktop.NetworkManager.Device.Wired";
32const ACCESS_POINT: &str = "org.freedesktop.NetworkManager.AccessPoint";
33const ACTIVE: &str = "org.freedesktop.NetworkManager.Connection.Active";
34
35/// The most devices kept, and the most active connections, about one a
36/// device. A host running containers has a virtual device for each.
37const MAX_DEVICES: usize = 64;
38/// The most access points kept: a scan in a city finds hundreds.
39const MAX_ACCESS_POINTS: usize = 512;
40/// The longest SSID 802.11 allows, in bytes.
41const MAX_SSID: usize = 32;
42
43/// NetworkManager's `DeviceType` for loopback, left out of the list.
44const LOOPBACK: u32 = 32;
45/// NetworkManager's `State` for a device it doesn't manage, left out.
46const UNMANAGED: u32 = 10;
47const ACTIVATED: u32 = 100;
48const FAILED: u32 = 120;
49/// The `InterfaceFlags` bit for a link, from NetworkManager 1.22, which
50/// takes over from `Device.Wired.Carrier`.
51const CARRIER: u32 = 0x10000;
52/// The `Flags` bit for an access point that asks for a key, as WEP does.
53const PRIVACY: u32 = 0x1;
54/// The key managements in an access point's `WpaFlags` and `RsnFlags`,
55/// from NetworkManager's `NM80211ApSecurityFlags`.
56const KEY_MGMT_PSK: u32 = 0x100;
57const KEY_MGMT_802_1X: u32 = 0x200;
58const KEY_MGMT_SAE: u32 = 0x400;
59/// Enhanced Open, and its transition mode.
60const KEY_MGMT_OWE: u32 = 0x800 | 0x1000;
61const KEY_MGMT_SUITE_B_192: u32 = 0x2000;
62/// The most objects kept, so a flood can't take the memory.
63const MAX_OBJECTS: usize = 4096;
64
65/// What a network device is, from NetworkManager's `DeviceType`.
66#[derive(Clone, Copy, Debug, PartialEq, Eq)]
67pub enum NetworkKind {
68  /// A cable.
69  Ethernet,
70  /// Wi-Fi.
71  Wifi,
72  /// Anything else, such as a bridge or a VPN's tunnel.
73  Other,
74}
75
76/// How a network device is doing, from NetworkManager's `State`.
77#[derive(Clone, Copy, Debug, PartialEq, Eq)]
78pub enum NetworkState {
79  /// Connected.
80  Connected,
81  /// On its way to connected: from preparing to waiting on secondary
82  /// connections.
83  Connecting,
84  /// Going down, as when told to disconnect or before connecting
85  /// elsewhere.
86  Disconnecting,
87  /// Ready but not connected, or failed to connect.
88  Disconnected,
89  /// Not ready, as with Wi-Fi off or no cable in, or a state not known.
90  Unavailable,
91}
92
93/// How far the host reaches, from NetworkManager's `Connectivity`.
94#[derive(Clone, Copy, Debug, PartialEq, Eq)]
95pub enum Connectivity {
96  /// Not known: NetworkManager isn't running, its checks are off, or
97  /// they haven't run yet.
98  Unknown,
99  /// On no network at all.
100  None,
101  /// Behind a captive portal, which wants a sign-in in a browser first.
102  Portal,
103  /// On a network, but not reaching the internet.
104  Limited,
105  /// Reaching the internet.
106  Full,
107}
108
109/// Why a device's last try to connect failed, from the reason
110/// NetworkManager gave with its `Failed` state.
111#[derive(Clone, Copy, Debug, PartialEq, Eq)]
112pub enum ConnectionFailReason {
113  /// It needed secrets, such as a Wi-Fi password, and got none.
114  NoSecrets,
115  /// The Wi-Fi supplicant failed, or its settings did.
116  WifiClientFailed,
117  /// The Wi-Fi supplicant disconnected.
118  WifiClientDisconnected,
119  /// The Wi-Fi network took too long to let it in, as with a wrong
120  /// password.
121  WifiAuthTimeout,
122  /// The Wi-Fi network wasn't found.
123  WifiNetworkLost,
124  /// Any other reason, such as DHCP getting no address.
125  Unknown,
126}
127
128/// What a Wi-Fi device does, from NetworkManager's `NM80211Mode`.
129#[derive(Clone, Copy, Debug, PartialEq, Eq)]
130pub enum WifiMode {
131  /// Not known, and for a device that isn't Wi-Fi.
132  Unknown,
133  /// Part of an ad-hoc network, with no access point.
134  AdHoc,
135  /// A station that joins networks, the usual mode.
136  Station,
137  /// An access point of its own, as a hotspot.
138  AccessPoint,
139  /// An 802.11s mesh point.
140  Mesh,
141}
142
143/// How a Wi-Fi network is secured, from its access point's flags.
144///
145/// Dynamic WEP and LEAP look the same as WEP from an access point, so
146/// all three show as [`Wep`](Self::Wep).
147#[derive(Clone, Copy, Debug, PartialEq, Eq)]
148pub enum WifiSecurity {
149  /// No encryption.
150  Open,
151  /// WEP.
152  Wep,
153  /// WPA with a password.
154  WpaPsk,
155  /// WPA with 802.1X, as an enterprise network.
156  WpaEap,
157  /// WPA2 with a password.
158  Wpa2Psk,
159  /// WPA2 with 802.1X.
160  Wpa2Eap,
161  /// WPA3 with a password, also for a network that takes WPA2's too.
162  Sae,
163  /// Enhanced Open: encrypted, with no password.
164  Owe,
165  /// WPA3 Enterprise's 192-bit mode.
166  Wpa3SuiteB192,
167  /// Flags this doesn't know.
168  Unknown,
169}
170
171/// A network device NetworkManager manages, other than loopback.
172#[derive(Clone, Debug, PartialEq)]
173pub struct NetworkDevice {
174  /// NetworkManager's object path for it, which names it while it exists.
175  pub path: String,
176  /// Its interface, such as `wlan0`.
177  pub interface: String,
178  /// What it is.
179  pub kind: NetworkKind,
180  /// How it is doing.
181  pub state: NetworkState,
182  /// The name of its active connection, or `""`.
183  pub connection: String,
184  /// Why its last try to connect failed, kept until it tries again.
185  /// `None` if it hasn't failed since the shell started following it.
186  pub fail_reason: Option<ConnectionFailReason>,
187  /// What it does, for a Wi-Fi device; `Unknown` for any other.
188  pub wifi_mode: WifiMode,
189  /// For a wired device, its link's speed in Mb/s; 0 when not known, and
190  /// for any other device.
191  pub link_speed: u32,
192  /// For a wired device, whether a cable is in, from the carrier bit of
193  /// `InterfaceFlags`, or `Carrier` before NetworkManager 1.22; `false`
194  /// for any other.
195  pub has_link: bool,
196}
197
198impl NetworkDevice {
199  fn from_props(path: String, object: &Object, objects: &BTreeMap<String, Object>) -> Option<Self> {
200    let props = object.device.as_ref()?;
201    let connection = dbus::get::<&ObjectPath>(props, "ActiveConnection")
202      .and_then(|at| objects.get(at.as_str())?.active.as_ref())
203      .map_or(String::new(), |active| dbus::text(active, "Id"));
204    let wifi_mode = object
205      .wireless
206      .as_ref()
207      .and_then(|w| dbus::get::<u32>(w, "Mode"));
208    let wired = object.wired.as_ref();
209    Some(NetworkDevice {
210      path,
211      interface: dbus::text(props, "Interface"),
212      kind: match dbus::get::<u32>(props, "DeviceType") {
213        Some(1) => NetworkKind::Ethernet,
214        Some(2) => NetworkKind::Wifi,
215        _ => NetworkKind::Other,
216      },
217      state: match dbus::get::<u32>(props, "State") {
218        Some(ACTIVATED) => NetworkState::Connected,
219        Some(40..=90) => NetworkState::Connecting,
220        Some(110) => NetworkState::Disconnecting,
221        Some(30 | FAILED) => NetworkState::Disconnected,
222        _ => NetworkState::Unavailable,
223      },
224      connection,
225      fail_reason: object.failure,
226      wifi_mode: match wifi_mode {
227        Some(1) => WifiMode::AdHoc,
228        Some(2) => WifiMode::Station,
229        Some(3) => WifiMode::AccessPoint,
230        Some(4) => WifiMode::Mesh,
231        _ => WifiMode::Unknown,
232      },
233      link_speed: wired.and_then(|w| dbus::get(w, "Speed")).unwrap_or(0),
234      has_link: wired
235        .and_then(|wired| {
236          let flags = dbus::get::<u32>(props, "InterfaceFlags");
237          let carrier = flags.map(|flags| flags & CARRIER != 0);
238          carrier.or_else(|| dbus::get(wired, "Carrier"))
239        })
240        .unwrap_or(false),
241    })
242  }
243}
244
245/// A Wi-Fi network a device sees.
246#[derive(Clone, Debug, PartialEq)]
247pub struct WifiNetwork {
248  /// Its name, at most 32 bytes, with bytes that aren't UTF-8 shown as
249  /// `�`.
250  pub ssid: String,
251  /// How strong its strongest access point is, from 0 to 100.
252  pub strength: u8,
253  /// Whether it asks for a key: any [`security`](Self::security) but
254  /// `Open`, `Owe` and `Unknown`.
255  pub secured: bool,
256  /// How its strongest access point is secured.
257  pub security: WifiSecurity,
258  /// Whether the device is connected to it, through any of its access
259  /// points.
260  pub connected: bool,
261  /// The path of the device that sees it.
262  pub device: String,
263  /// The path of its strongest access point.
264  pub access_point: String,
265}
266
267/// Networking from NetworkManager, from `cx.network()`. Cloning it is
268/// cheap.
269#[derive(Clone)]
270pub struct Network {
271  service: Option<Rc<NetworkService>>,
272  wifi_enabled: ReadSignal<bool>,
273  connectivity: ReadSignal<Connectivity>,
274  devices: ReadSignal<Vec<NetworkDevice>>,
275  wifi_networks: ReadSignal<Vec<WifiNetwork>>,
276  saved_connections: ReadSignal<Vec<SavedConnection>>,
277}
278
279impl Network {
280  /// Whether Wi-Fi is on. `false` while NetworkManager isn't running.
281  pub fn wifi_enabled(&self) -> ReadSignal<bool> {
282    self.wifi_enabled
283  }
284
285  /// How far the host reaches, from NetworkManager's own checks.
286  /// `Unknown` while it isn't running.
287  pub fn connectivity(&self) -> ReadSignal<Connectivity> {
288    self.connectivity
289  }
290
291  /// Every device but loopback and those NetworkManager doesn't manage,
292  /// in NetworkManager's numbering. Empty while it isn't running.
293  pub fn devices(&self) -> ReadSignal<Vec<NetworkDevice>> {
294    self.devices
295  }
296
297  /// The Wi-Fi networks each device sees, one per name and device,
298  /// strongest first. Hidden networks are left out. Empty while
299  /// NetworkManager isn't running.
300  pub fn wifi_networks(&self) -> ReadSignal<Vec<WifiNetwork>> {
301    self.wifi_networks
302  }
303
304  /// Turns Wi-Fi on or off.
305  pub fn set_wifi_enabled(&self, enabled: bool) {
306    let body = (NM, "WirelessEnabled", Variant::from(enabled));
307    self.call(NM_PATH, PROPERTIES, "Set", body);
308  }
309
310  /// Asks the Wi-Fi device at `device`, its path, to look for networks.
311  /// NetworkManager refuses a scan right after another, and that is
312  /// logged.
313  pub fn scan(&self, device: &str) {
314    let options = HashMap::<&str, Variant<'static>>::new();
315    self.call(device, WIRELESS, "RequestScan", options);
316  }
317
318  /// Connects `network`'s device to it. Without a password,
319  /// NetworkManager uses a connection saved for it, and refuses if it has
320  /// none. With one, it saves a new WPA connection and uses that. A
321  /// refusal is logged, never the password.
322  pub fn connect(&self, network: &WifiNetwork, password: Option<&str>) {
323    let paths = (
324      ObjectPath::try_from(network.device.clone()),
325      ObjectPath::try_from(network.access_point.clone()),
326    );
327    let (Ok(device), Ok(at)) = paths else {
328      return tracing::info!(network.ssid, "no such Wi-Fi network; nothing sent");
329    };
330    let Some(password) = password else {
331      return self.activate(network, device, at);
332    };
333    // The access point's own bytes: `ssid` may have lost some.
334    let ssid = self
335      .service
336      .as_ref()
337      .and_then(|service| {
338        let state = service.state.borrow();
339        let object = state.as_ref()?.objects.get(&network.access_point)?;
340        Some(ssid(object.access_point.as_ref()?))
341      })
342      .unwrap_or_else(|| network.ssid.clone().into_bytes());
343    let wireless = HashMap::from([("ssid", Variant::from(ssid))]);
344    // NetworkManager picks the key management, WPA2 or WPA3, from the
345    // access point.
346    let security = HashMap::from([("psk", Variant::from(password.to_owned()))]);
347    let settings = HashMap::from([
348      ("802-11-wireless", wireless),
349      ("802-11-wireless-security", security),
350    ]);
351    let body = (settings, device, at);
352    self.call(NM_PATH, NM, "AddAndActivateConnection", body);
353  }
354
355  /// Connects through a saved connection NetworkManager picks, and for
356  /// an open network with none, through a new one.
357  fn activate(&self, network: &WifiNetwork, device: ObjectPath<'static>, at: ObjectPath<'static>) {
358    let Some((shared, connection)) = self.bus("ActivateConnection") else {
359      return;
360    };
361    let (spawner, open) = (Rc::clone(&shared), !network.secured);
362    let ssid = network.ssid.clone().into_bytes();
363    let ssid = self
364      .service
365      .as_ref()
366      .and_then(|service| {
367        let state = service.state.borrow();
368        let object = state.as_ref()?.objects.get(&network.access_point)?;
369        Some(self::ssid(object.access_point.as_ref()?))
370      })
371      .unwrap_or(ssid);
372    spawner.spawn(async move {
373      let pick = ObjectPath::from_static_str_unchecked("/");
374      let body = (pick, &device, &at);
375      let call = connection.call_method(Some(NM), NM_PATH, Some(NM), "ActivateConnection", &body);
376      let Err(error) = shared.timeout(CALL_TIMEOUT, call).await else {
377        return;
378      };
379      if !open {
380        return tracing::warn!(%error, "no saved connection for that network");
381      }
382      let wireless = HashMap::from([("ssid", Variant::from(ssid))]);
383      let settings = HashMap::from([("802-11-wireless", wireless)]);
384      let body = (settings, &device, &at);
385      let method = "AddAndActivateConnection";
386      let call = connection.call_method(Some(NM), NM_PATH, Some(NM), method, &body);
387      if let Err(error) = shared.timeout(CALL_TIMEOUT, call).await {
388        tracing::warn!(%error, method, "NetworkManager refused it");
389      }
390    });
391  }
392
393  /// Disconnects the device at `device`, until it is connected again.
394  pub fn disconnect(&self, device: &str) {
395    self.call(device, DEVICE, "Disconnect", ());
396  }
397
398  /// The connections NetworkManager saved, in its numbering, at most
399  /// 256. NetworkManager is asked for them on the first call and
400  /// followed from then on, so a config that never lists them reads
401  /// none. Empty while it isn't running.
402  pub fn saved_connections(&self) -> ReadSignal<Vec<SavedConnection>> {
403    let saved = self.service.as_ref().and_then(|service| {
404      let state = service.state.borrow();
405      Some(Rc::clone(&state.as_ref()?.saved))
406    });
407    if let Some(saved) = saved {
408      saved.follow();
409    }
410    self.saved_connections
411  }
412
413  /// Deletes the saved connection `uuid` names, so NetworkManager
414  /// forgets that network. A refusal is logged.
415  pub fn forget(&self, uuid: &str) {
416    self.call_saved(uuid, "Delete");
417  }
418
419  /// Takes the secrets, such as a Wi-Fi password, out of the saved
420  /// connection `uuid` names, so NetworkManager asks for them next time.
421  /// A refusal is logged.
422  pub fn clear_secrets(&self, uuid: &str) {
423    self.call_saved(uuid, "ClearSecrets");
424  }
425
426  /// Calls `method` on the saved connection `uuid` names, and logs a
427  /// refusal.
428  fn call_saved(&self, uuid: &str, method: &'static str) {
429    let Some((shared, connection)) = self.bus(method) else {
430      return;
431    };
432    let (spawner, uuid) = (Rc::clone(&shared), uuid.to_owned());
433    spawner.spawn(async move {
434      let called = async {
435        let find = "GetConnectionByUuid";
436        let call = connection.call_method(Some(NM), SETTINGS_PATH, Some(SETTINGS), find, &uuid);
437        let reply = shared.timeout(CALL_TIMEOUT, call).await?;
438        let path = reply.body().deserialize::<OwnedObjectPath>()?;
439        let call = connection.call_method(Some(NM), path.as_str(), Some(SAVED), method, &());
440        shared.timeout(CALL_TIMEOUT, call).await.map(drop)
441      };
442      if let Err(error) = called.await {
443        tracing::warn!(%error, uuid, method, "NetworkManager refused it");
444      }
445    });
446  }
447
448  /// Calls `method` on NetworkManager's object at `path`, and logs a
449  /// refusal, without the body.
450  fn call<B>(&self, path: &str, interface: &'static str, method: &'static str, body: B)
451  where
452    B: serde::Serialize + zbus::DynamicType + 'static,
453  {
454    let Some((shared, connection)) = self.bus(method) else {
455      return;
456    };
457    let (spawner, path) = (Rc::clone(&shared), path.to_owned());
458    spawner.spawn(async move {
459      let call = connection.call_method(Some(NM), path.as_str(), Some(interface), method, &body);
460      if let Err(error) = shared.timeout(CALL_TIMEOUT, call).await {
461        tracing::warn!(%error, path, method, "NetworkManager refused it");
462      }
463    });
464  }
465
466  /// What a call to `method` goes through, or `None`, which is logged,
467  /// while NetworkManager isn't running.
468  fn bus(&self, method: &str) -> Option<(Rc<Shared>, Connection)> {
469    let service = self.service.as_ref()?;
470    let state = service.state.borrow();
471    let Some(connection) = state.as_ref().and_then(|state| state.connection.clone()) else {
472      tracing::info!(method, "NetworkManager isn't running; nothing sent");
473      return None;
474    };
475    Some((Rc::clone(&service.shared), connection))
476  }
477}
478
479/// `cx.network()`.
480pub(crate) fn network(cx: &Cx) -> Network {
481  let Some(service) = cx.service::<NetworkService>() else {
482    static LOGGED: Once = Once::new();
483    LOGGED.call_once(|| {
484      tracing::warn!("cx.network() needs the services the shell provides; it stays empty");
485    });
486    return Network {
487      service: None,
488      wifi_enabled: cx.signal(false).read_only(),
489      connectivity: cx.signal(Connectivity::Unknown).read_only(),
490      devices: cx.signal(Vec::new()).read_only(),
491      wifi_networks: cx.signal(Vec::new()).read_only(),
492      saved_connections: cx.signal(Vec::new()).read_only(),
493    };
494  };
495  service.start()
496}
497
498/// NetworkManager for one shell, followed from the first ask.
499pub(crate) struct NetworkService {
500  shared: Rc<Shared>,
501  state: RefCell<Option<State>>,
502}
503
504struct State {
505  wifi_enabled: Signal<bool>,
506  connectivity: Signal<Connectivity>,
507  devices: Signal<Vec<NetworkDevice>>,
508  wifi_networks: Signal<Vec<WifiNetwork>>,
509  /// The saved connections, followed apart once a config lists them.
510  saved: Rc<SavedConnections>,
511  /// NetworkManager's objects that this service reads, by path.
512  objects: BTreeMap<String, Object>,
513  /// The bus, while NetworkManager runs, for the methods.
514  connection: Option<Connection>,
515  generation: u64,
516}
517
518impl State {
519  /// The handle a config gets.
520  fn network(&self, service: &Rc<NetworkService>) -> Network {
521    Network {
522      service: Some(Rc::clone(service)),
523      wifi_enabled: self.wifi_enabled.read_only(),
524      connectivity: self.connectivity.read_only(),
525      devices: self.devices.read_only(),
526      wifi_networks: self.wifi_networks.read_only(),
527      saved_connections: self.saved.list.read_only(),
528    }
529  }
530}
531
532/// The interfaces this service reads on one of NetworkManager's objects.
533/// An object is kept while it has one other than `wireless` and `wired`.
534#[derive(Default)]
535struct Object {
536  manager: Option<Props>,
537  device: Option<Props>,
538  wireless: Option<Props>,
539  wired: Option<Props>,
540  access_point: Option<Props>,
541  active: Option<Props>,
542  /// A device's last failure, which its `StateReason` forgets as it moves
543  /// on.
544  failure: Option<ConnectionFailReason>,
545}
546
547impl Object {
548  /// Where `interface`'s properties go, or `None` for one this service
549  /// doesn't read.
550  fn slot(&mut self, interface: &str) -> Option<&mut Option<Props>> {
551    match interface {
552      NM => Some(&mut self.manager),
553      DEVICE => Some(&mut self.device),
554      WIRELESS => Some(&mut self.wireless),
555      WIRED => Some(&mut self.wired),
556      ACCESS_POINT => Some(&mut self.access_point),
557      ACTIVE => Some(&mut self.active),
558      _ => None,
559    }
560  }
561
562  fn kept(&self) -> bool {
563    self.manager.is_some()
564      || self.device.is_some()
565      || self.access_point.is_some()
566      || self.active.is_some()
567  }
568
569  /// Notes a device's failure from its `StateReason`, and forgets it once
570  /// the device tries again.
571  fn note_failure(&mut self) {
572    let Some(device) = &self.device else {
573      return;
574    };
575    // `(uu)`, the state and the reason the device went into it.
576    let reason = device.get("StateReason");
577    let reason = reason.and_then(|reason| <(u32, u32)>::try_from(&**reason).ok());
578    if let Some((FAILED, reason)) = reason {
579      self.failure = Some(fail_reason(reason));
580    }
581    if let Some(40..=ACTIVATED) = dbus::get::<u32>(device, "State") {
582      self.failure = None;
583    }
584  }
585}
586
587/// What NetworkManager's `NMDeviceStateReason` `reason` says about a
588/// failure.
589fn fail_reason(reason: u32) -> ConnectionFailReason {
590  match reason {
591    7 => ConnectionFailReason::NoSecrets,
592    8 => ConnectionFailReason::WifiClientDisconnected,
593    // The supplicant's settings failed, or the supplicant did.
594    9 | 10 => ConnectionFailReason::WifiClientFailed,
595    11 => ConnectionFailReason::WifiAuthTimeout,
596    53 => ConnectionFailReason::WifiNetworkLost,
597    _ => ConnectionFailReason::Unknown,
598  }
599}
600
601/// Orders NetworkManager's paths by the number that ends them, so
602/// `Devices/2` comes before `Devices/10`.
603fn by_number(a: &str, b: &str) -> Ordering {
604  (a.len(), a).cmp(&(b.len(), b))
605}
606
607/// An access point's SSID, cut to 802.11's 32 bytes. Empty for one that
608/// isn't an `ay`.
609fn ssid(props: &Props) -> Vec<u8> {
610  let bytes = dbus::get::<&Array>(props, "Ssid");
611  let bytes = bytes.iter().flat_map(|bytes| bytes.inner());
612  let bytes = bytes.map_while(|byte| match byte {
613    Variant::U8(byte) => Some(*byte),
614    _ => None,
615  });
616  bytes.take(MAX_SSID).collect()
617}
618
619/// The object paths in the `ao` property `name`.
620fn paths<'a>(props: &'a Props, name: &str) -> impl Iterator<Item = &'a str> {
621  let paths = dbus::get::<&Array>(props, name);
622  paths
623    .into_iter()
624    .flat_map(|paths| paths.inner())
625    .filter_map(|path| match path {
626      Variant::ObjectPath(path) => Some(path.as_str()),
627      _ => None,
628    })
629}
630
631/// Gives the object at `path` `interfaces`. Past [`MAX_OBJECTS`] a new
632/// one is left out; which are shown is decided as they are.
633fn add(objects: &mut BTreeMap<String, Object>, path: String, interfaces: HashMap<String, Props>) {
634  if objects.len() >= MAX_OBJECTS && !objects.contains_key(&path) {
635    return tracing::debug!(path, "too many network objects; leaving one out");
636  }
637  let mut object = objects.remove(&path).unwrap_or_default();
638  for (interface, props) in interfaces {
639    if let Some(slot) = object.slot(&interface) {
640      *slot = Some(props);
641    }
642  }
643  object.note_failure();
644  if object.kept() {
645    objects.insert(path, object);
646  }
647}
648
649/// How the access point `point` is secured, from the strongest key
650/// management it offers: RSN's before WPA's, a password before 802.1X,
651/// as one will do, and SAE before a WPA2 password. Enhanced Open comes
652/// after those, and then WEP, whose privacy bit is all that marks it.
653fn security(point: &Props) -> WifiSecurity {
654  let flags = |name| dbus::get::<u32>(point, name).unwrap_or(0);
655  let (wpa, rsn) = (flags("WpaFlags"), flags("RsnFlags"));
656  let strongest_first = [
657    (rsn, KEY_MGMT_SUITE_B_192, WifiSecurity::Wpa3SuiteB192),
658    (rsn, KEY_MGMT_SAE, WifiSecurity::Sae),
659    (rsn, KEY_MGMT_PSK, WifiSecurity::Wpa2Psk),
660    (rsn, KEY_MGMT_802_1X, WifiSecurity::Wpa2Eap),
661    (wpa, KEY_MGMT_PSK, WifiSecurity::WpaPsk),
662    (wpa, KEY_MGMT_802_1X, WifiSecurity::WpaEap),
663    (rsn, KEY_MGMT_OWE, WifiSecurity::Owe),
664  ];
665  let offered = strongest_first
666    .into_iter()
667    .find(|(flags, key, _)| flags & key != 0);
668  match offered {
669    Some((_, _, security)) => security,
670    None if wpa != 0 || rsn != 0 => WifiSecurity::Unknown,
671    None if flags("Flags") & PRIVACY != 0 => WifiSecurity::Wep,
672    None => WifiSecurity::Open,
673  }
674}
675
676/// Whether a network secured as `security` asks for a key. Enhanced
677/// Open encrypts without one.
678fn secured(security: WifiSecurity) -> bool {
679  !matches!(
680    security,
681    WifiSecurity::Open | WifiSecurity::Owe | WifiSecurity::Unknown
682  )
683}
684
685/// The properties this service reads, by interface: a change to any
686/// other, such as an access point's `LastSeen` on each scan, shows
687/// nothing new.
688fn read(interface: &str, name: &str) -> bool {
689  match interface {
690    NM => matches!(name, "WirelessEnabled" | "Connectivity"),
691    DEVICE => matches!(
692      name,
693      "Interface" | "DeviceType" | "State" | "StateReason" | "ActiveConnection" | "InterfaceFlags"
694    ),
695    WIRELESS => matches!(name, "AccessPoints" | "ActiveAccessPoint" | "Mode"),
696    WIRED => matches!(name, "Speed" | "Carrier"),
697    ACCESS_POINT => matches!(
698      name,
699      "Ssid" | "Strength" | "Flags" | "WpaFlags" | "RsnFlags"
700    ),
701    ACTIVE => name == "Id",
702    _ => false,
703  }
704}
705
706/// The networks each Wi-Fi device in `objects` sees: the strongest access
707/// point per SSID and device, strongest first.
708fn wifi_networks(objects: &BTreeMap<String, Object>) -> Vec<WifiNetwork> {
709  let mut best = BTreeMap::<(&str, Vec<u8>), WifiNetwork>::new();
710  for (path, object) in objects {
711    let (Some(device), Some(wireless)) = (&object.device, &object.wireless) else {
712      continue;
713    };
714    let on = dbus::get::<u32>(device, "State") == Some(ACTIVATED);
715    let active = dbus::get::<&ObjectPath>(wireless, "ActiveAccessPoint");
716    let active = active.map(|at| at.as_str());
717    for at in paths(wireless, "AccessPoints") {
718      let Some(point) = objects
719        .get(at)
720        .and_then(|object| object.access_point.as_ref())
721      else {
722        continue;
723      };
724      let ssid = ssid(point);
725      // Hidden: empty, or NUL bytes standing in for its length.
726      if ssid.iter().all(|byte| *byte == 0) {
727        continue;
728      }
729      let security = self::security(point);
730      let network = WifiNetwork {
731        ssid: String::from_utf8_lossy(&ssid).into_owned(),
732        strength: dbus::get::<u8>(point, "Strength").unwrap_or(0).min(100),
733        secured: secured(security),
734        security,
735        connected: on && active == Some(at),
736        device: path.clone(),
737        access_point: at.to_owned(),
738      };
739      match best.entry((path, ssid)) {
740        Entry::Vacant(entry) => {
741          entry.insert(network);
742        }
743        Entry::Occupied(mut entry) => {
744          let connected = entry.get().connected || network.connected;
745          if network.strength > entry.get().strength {
746            entry.insert(network);
747          }
748          entry.get_mut().connected = connected;
749        }
750      }
751    }
752  }
753  let mut networks = best.into_values().collect::<Vec<_>>();
754  networks.sort_by(|a, b| {
755    let ties = || (&a.ssid, &a.device).cmp(&(&b.ssid, &b.device));
756    b.strength.cmp(&a.strength).then_with(ties)
757  });
758  networks.truncate(MAX_ACCESS_POINTS);
759  networks
760}
761
762impl NetworkService {
763  pub(crate) fn new(shared: Rc<Shared>) -> NetworkService {
764    NetworkService {
765      shared,
766      state: RefCell::new(None),
767    }
768  }
769
770  fn start(self: &Rc<Self>) -> Network {
771    if let Some(state) = &*self.state.borrow() {
772      return state.network(self);
773    }
774    let root = &self.shared.root;
775    let state = State {
776      wifi_enabled: root.signal(false),
777      connectivity: root.signal(Connectivity::Unknown),
778      devices: root.signal(Vec::new()),
779      wifi_networks: root.signal(Vec::new()),
780      saved: SavedConnections::new(Rc::clone(&self.shared)),
781      objects: BTreeMap::new(),
782      connection: None,
783      generation: 0,
784    };
785    let network = state.network(self);
786    *self.state.borrow_mut() = Some(state);
787    match self.shared.config.system_bus.clone() {
788      Some(address) => {
789        let follow = dbus::follow(Rc::clone(&self.shared), Rc::clone(self), address);
790        self.shared.spawn(follow);
791      }
792      None => tracing::info!("no system bus, so there is no networking"),
793    }
794    network
795  }
796
797  /// Shows Wi-Fi, connectivity, the devices and the networks as they are
798  /// now. Each is set only if it changed, since a scan changes much that
799  /// none shows.
800  fn publish(&self) {
801    // Set apart from the borrow, since effects may ask for more.
802    let shown = self.state.borrow().as_ref().map(|state| {
803      let objects = &state.objects;
804      let manager = objects
805        .get(NM_PATH)
806        .and_then(|object| object.manager.as_ref());
807      let enabled = manager.and_then(|props| dbus::get(props, "WirelessEnabled"));
808      let connectivity = match manager.and_then(|props| dbus::get::<u32>(props, "Connectivity")) {
809        Some(1) => Connectivity::None,
810        Some(2) => Connectivity::Portal,
811        Some(3) => Connectivity::Limited,
812        Some(4) => Connectivity::Full,
813        _ => Connectivity::Unknown,
814      };
815      let mut devices = objects
816        .iter()
817        .filter_map(|(path, object)| {
818          let props = object.device.as_ref()?;
819          let kind = dbus::get::<u32>(props, "DeviceType");
820          let state = dbus::get::<u32>(props, "State");
821          if kind == Some(LOOPBACK) || state == Some(UNMANAGED) {
822            return None;
823          }
824          NetworkDevice::from_props(path.clone(), object, objects)
825        })
826        .collect::<Vec<_>>();
827      devices.sort_by(|a, b| by_number(&a.path, &b.path));
828      devices.truncate(MAX_DEVICES);
829      let shown = (
830        enabled.unwrap_or(false),
831        connectivity,
832        devices,
833        wifi_networks(objects),
834      );
835      let signals = (
836        state.wifi_enabled,
837        state.connectivity,
838        state.devices,
839        state.wifi_networks,
840      );
841      (signals, shown)
842    });
843    if let Some((signals, shown)) = shown {
844      set_changed(signals.0, shown.0);
845      set_changed(signals.1, shown.1);
846      set_changed(signals.2, shown.2);
847      set_changed(signals.3, shown.3);
848    }
849  }
850}
851
852impl Daemon for NetworkService {
853  const NAME: &'static str = NM;
854
855  /// Only what this service reads: NetworkManager also broadcasts IP and
856  /// DHCP settings, statistics and saved connections, which would each
857  /// wake the shell for nothing.
858  fn rules() -> Vec<String> {
859    let mut rules = vec![format!(
860      "type='signal',sender='{NM}',path='{ROOT}',interface='{OBJECT_MANAGER}'"
861    )];
862    rules.extend(
863      [NM, DEVICE, WIRELESS, WIRED, ACCESS_POINT, ACTIVE].map(|interface| {
864        format!(
865          "type='signal',sender='{NM}',interface='{PROPERTIES}',member='PropertiesChanged',\
866         arg0='{interface}'"
867        )
868      }),
869    );
870    rules
871  }
872
873  fn alive(&self) -> bool {
874    let state = self.state.borrow();
875    state
876      .as_ref()
877      .is_some_and(|state| state.devices.try_with(|_| ()).is_some())
878  }
879
880  fn clear(&self) -> u64 {
881    let generation = {
882      let mut state = self.state.borrow_mut();
883      let Some(state) = state.as_mut() else {
884        return 0;
885      };
886      state.objects.clear();
887      state.connection = None;
888      state.generation += 1;
889      state.generation
890    };
891    self.publish();
892    generation
893  }
894
895  async fn read(self: Rc<Self>, connection: Connection, generation: u64) {
896    let call = connection.call_method(
897      Some(NM),
898      ROOT,
899      Some(OBJECT_MANAGER),
900      "GetManagedObjects",
901      &(),
902    );
903    let read = async {
904      let reply = self.shared.timeout(CALL_TIMEOUT, call).await?;
905      reply
906        .body()
907        .deserialize::<HashMap<OwnedObjectPath, HashMap<String, Props>>>()
908    };
909    let managed = match read.await {
910      Ok(managed) => managed,
911      Err(error) => return tracing::warn!(%error, "couldn't read NetworkManager"),
912    };
913    // By number, so the caps keep the oldest objects, the same every time.
914    let mut managed = managed
915      .into_iter()
916      .map(|(path, interfaces)| (path.as_str().to_owned(), interfaces))
917      .collect::<Vec<_>>();
918    managed.sort_by(|(a, _), (b, _)| by_number(a, b));
919    {
920      let mut state = self.state.borrow_mut();
921      let Some(state) = state
922        .as_mut()
923        .filter(|state| state.generation == generation)
924      else {
925        return;
926      };
927      state.objects.clear();
928      for (path, interfaces) in managed {
929        add(&mut state.objects, path, interfaces);
930      }
931      state.connection = Some(connection);
932    }
933    self.publish();
934  }
935
936  fn signal(self: &Rc<Self>, _: &Connection, message: &Message) {
937    if let Some((path, interface, changed, dropped)) = dbus::properties_changed(message) {
938      {
939        let mut state = self.state.borrow_mut();
940        let object = state
941          .as_mut()
942          .and_then(|state| state.objects.get_mut(&path));
943        let Some(object) = object else {
944          return;
945        };
946        let Some(props) = object.slot(&interface).and_then(Option::as_mut) else {
947          return;
948        };
949        let shown = changed
950          .keys()
951          .chain(&dropped)
952          .any(|name| read(&interface, name));
953        dbus::apply(props, changed, dropped);
954        object.note_failure();
955        if !shown {
956          return;
957        }
958      }
959      return self.publish();
960    }
961    let header = message.header();
962    if header.interface().map(|interface| interface.as_str()) != Some(OBJECT_MANAGER) {
963      return;
964    }
965    let body = message.body();
966    {
967      let mut state = self.state.borrow_mut();
968      let Some(state) = state.as_mut() else {
969        return;
970      };
971      match header.member().map(|member| member.as_str()) {
972        Some("InterfacesAdded") => {
973          let Ok((path, interfaces)) =
974            body.deserialize::<(OwnedObjectPath, HashMap<String, Props>)>()
975          else {
976            return;
977          };
978          add(&mut state.objects, path.as_str().to_owned(), interfaces);
979        }
980        Some("InterfacesRemoved") => {
981          let Ok((path, interfaces)) = body.deserialize::<(OwnedObjectPath, Vec<String>)>() else {
982            return;
983          };
984          let Some(object) = state.objects.get_mut(path.as_str()) else {
985            return;
986          };
987          for interface in interfaces {
988            if let Some(slot) = object.slot(&interface) {
989              *slot = None;
990            }
991          }
992          if !object.kept() {
993            state.objects.remove(path.as_str());
994          }
995        }
996        _ => return,
997      }
998    }
999    self.publish();
1000  }
1001}