1#[cfg(test)]
5mod tests;
6
7use std::cell::RefCell;
8use std::cmp::Ordering;
9use std::collections::btree_map::Entry;
10use std::collections::{BTreeMap, HashMap};
11use std::rc::Rc;
12use std::sync::Once;
13
14use crabshell_reactive::{ReadSignal, Signal};
15use crabshell_ui::Cx;
16use zbus::{Array, Connection, Message, ObjectPath, OwnedObjectPath, Variant};
17
18use crate::Shared;
19use crate::dbus::{self, CALL_TIMEOUT, Daemon, OBJECT_MANAGER, PROPERTIES, Props, set_changed};
20
21const NM: &str = "org.freedesktop.NetworkManager";
22const NM_PATH: &str = "/org/freedesktop/NetworkManager";
23const ROOT: &str = "/org/freedesktop";
25const DEVICE: &str = "org.freedesktop.NetworkManager.Device";
26const WIRELESS: &str = "org.freedesktop.NetworkManager.Device.Wireless";
27const ACCESS_POINT: &str = "org.freedesktop.NetworkManager.AccessPoint";
28const ACTIVE: &str = "org.freedesktop.NetworkManager.Connection.Active";
29
30const MAX_DEVICES: usize = 64;
33const MAX_ACCESS_POINTS: usize = 512;
35const MAX_SSID: usize = 32;
37
38const LOOPBACK: u32 = 32;
40const UNMANAGED: u32 = 10;
42const ACTIVATED: u32 = 100;
43const PRIVACY: u32 = 0x1;
45const KEY_MGMT_KEYED: u32 = 0x100 | 0x200 | 0x400 | 0x2000;
48const MAX_OBJECTS: usize = 4096;
50
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
53pub enum NetworkKind {
54 Ethernet,
56 Wifi,
58 Other,
60}
61
62#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64pub enum NetworkState {
65 Connected,
67 Connecting,
70 Disconnected,
72 Unavailable,
74}
75
76#[derive(Clone, Debug, PartialEq)]
78pub struct NetworkDevice {
79 pub path: String,
81 pub interface: String,
83 pub kind: NetworkKind,
85 pub state: NetworkState,
87 pub connection: String,
89}
90
91impl NetworkDevice {
92 fn from_props(path: String, props: &Props, objects: &BTreeMap<String, Object>) -> NetworkDevice {
93 let connection = dbus::get::<&ObjectPath>(props, "ActiveConnection")
94 .and_then(|at| objects.get(at.as_str())?.active.as_ref())
95 .map_or(String::new(), |active| dbus::text(active, "Id"));
96 NetworkDevice {
97 path,
98 interface: dbus::text(props, "Interface"),
99 kind: match dbus::get::<u32>(props, "DeviceType") {
100 Some(1) => NetworkKind::Ethernet,
101 Some(2) => NetworkKind::Wifi,
102 _ => NetworkKind::Other,
103 },
104 state: match dbus::get::<u32>(props, "State") {
105 Some(ACTIVATED) => NetworkState::Connected,
106 Some(40..=90) => NetworkState::Connecting,
107 Some(30 | 110 | 120) => NetworkState::Disconnected,
108 _ => NetworkState::Unavailable,
109 },
110 connection,
111 }
112 }
113}
114
115#[derive(Clone, Debug, PartialEq)]
117pub struct WifiNetwork {
118 pub ssid: String,
121 pub strength: u8,
123 pub secured: bool,
125 pub connected: bool,
128 pub device: String,
130 pub access_point: String,
132}
133
134#[derive(Clone)]
137pub struct Network {
138 service: Option<Rc<NetworkService>>,
139 wifi_enabled: ReadSignal<bool>,
140 devices: ReadSignal<Vec<NetworkDevice>>,
141 wifi_networks: ReadSignal<Vec<WifiNetwork>>,
142}
143
144impl Network {
145 pub fn wifi_enabled(&self) -> ReadSignal<bool> {
147 self.wifi_enabled
148 }
149
150 pub fn devices(&self) -> ReadSignal<Vec<NetworkDevice>> {
153 self.devices
154 }
155
156 pub fn wifi_networks(&self) -> ReadSignal<Vec<WifiNetwork>> {
160 self.wifi_networks
161 }
162
163 pub fn set_wifi_enabled(&self, enabled: bool) {
165 let body = (NM, "WirelessEnabled", Variant::from(enabled));
166 self.call(NM_PATH, PROPERTIES, "Set", body);
167 }
168
169 pub fn scan(&self, device: &str) {
173 let options = HashMap::<&str, Variant<'static>>::new();
174 self.call(device, WIRELESS, "RequestScan", options);
175 }
176
177 pub fn connect(&self, network: &WifiNetwork, password: Option<&str>) {
182 let paths = (
183 ObjectPath::try_from(network.device.clone()),
184 ObjectPath::try_from(network.access_point.clone()),
185 );
186 let (Ok(device), Ok(at)) = paths else {
187 return tracing::info!(network.ssid, "no such Wi-Fi network; nothing sent");
188 };
189 let Some(password) = password else {
190 return self.activate(network, device, at);
191 };
192 let ssid = self
194 .service
195 .as_ref()
196 .and_then(|service| {
197 let state = service.state.borrow();
198 let object = state.as_ref()?.objects.get(&network.access_point)?;
199 Some(ssid(object.access_point.as_ref()?))
200 })
201 .unwrap_or_else(|| network.ssid.clone().into_bytes());
202 let wireless = HashMap::from([("ssid", Variant::from(ssid))]);
203 let security = HashMap::from([("psk", Variant::from(password.to_owned()))]);
206 let settings = HashMap::from([
207 ("802-11-wireless", wireless),
208 ("802-11-wireless-security", security),
209 ]);
210 let body = (settings, device, at);
211 self.call(NM_PATH, NM, "AddAndActivateConnection", body);
212 }
213
214 fn activate(&self, network: &WifiNetwork, device: ObjectPath<'static>, at: ObjectPath<'static>) {
217 let Some(service) = &self.service else {
218 return;
219 };
220 let connection = service
221 .state
222 .borrow()
223 .as_ref()
224 .and_then(|s| s.connection.clone());
225 let Some(connection) = connection else {
226 return tracing::info!("NetworkManager isn't running; nothing sent");
227 };
228 let (shared, open) = (Rc::clone(&service.shared), !network.secured);
229 let ssid = network.ssid.clone().into_bytes();
230 let ssid = self
231 .service
232 .as_ref()
233 .and_then(|service| {
234 let state = service.state.borrow();
235 let object = state.as_ref()?.objects.get(&network.access_point)?;
236 Some(self::ssid(object.access_point.as_ref()?))
237 })
238 .unwrap_or(ssid);
239 service.shared.spawn(async move {
240 let pick = ObjectPath::from_static_str_unchecked("/");
241 let body = (pick, &device, &at);
242 let call = connection.call_method(Some(NM), NM_PATH, Some(NM), "ActivateConnection", &body);
243 let Err(error) = shared.timeout(CALL_TIMEOUT, call).await else {
244 return;
245 };
246 if !open {
247 return tracing::warn!(%error, "no saved connection for that network");
248 }
249 let wireless = HashMap::from([("ssid", Variant::from(ssid))]);
250 let settings = HashMap::from([("802-11-wireless", wireless)]);
251 let body = (settings, &device, &at);
252 let method = "AddAndActivateConnection";
253 let call = connection.call_method(Some(NM), NM_PATH, Some(NM), method, &body);
254 if let Err(error) = shared.timeout(CALL_TIMEOUT, call).await {
255 tracing::warn!(%error, method, "NetworkManager refused it");
256 }
257 });
258 }
259
260 pub fn disconnect(&self, device: &str) {
262 self.call(device, DEVICE, "Disconnect", ());
263 }
264
265 fn call<B>(&self, path: &str, interface: &'static str, method: &'static str, body: B)
268 where
269 B: serde::Serialize + zbus::DynamicType + 'static,
270 {
271 let Some(service) = &self.service else {
272 return;
273 };
274 let Some(connection) = service
275 .state
276 .borrow()
277 .as_ref()
278 .and_then(|state| state.connection.clone())
279 else {
280 return tracing::info!(method, "NetworkManager isn't running; nothing sent");
281 };
282 let (shared, path) = (Rc::clone(&service.shared), path.to_owned());
283 service.shared.spawn(async move {
284 let call = connection.call_method(Some(NM), path.as_str(), Some(interface), method, &body);
285 if let Err(error) = shared.timeout(CALL_TIMEOUT, call).await {
286 tracing::warn!(%error, path, method, "NetworkManager refused it");
287 }
288 });
289 }
290}
291
292pub(crate) fn network(cx: &Cx) -> Network {
294 let Some(service) = cx.service::<NetworkService>() else {
295 static LOGGED: Once = Once::new();
296 LOGGED.call_once(|| {
297 tracing::warn!("cx.network() needs the services the shell provides; it stays empty");
298 });
299 return Network {
300 service: None,
301 wifi_enabled: cx.signal(false).read_only(),
302 devices: cx.signal(Vec::new()).read_only(),
303 wifi_networks: cx.signal(Vec::new()).read_only(),
304 };
305 };
306 service.start()
307}
308
309pub(crate) struct NetworkService {
311 shared: Rc<Shared>,
312 state: RefCell<Option<State>>,
313}
314
315struct State {
316 wifi_enabled: Signal<bool>,
317 devices: Signal<Vec<NetworkDevice>>,
318 wifi_networks: Signal<Vec<WifiNetwork>>,
319 objects: BTreeMap<String, Object>,
321 connection: Option<Connection>,
323 generation: u64,
324}
325
326#[derive(Default)]
329struct Object {
330 manager: Option<Props>,
331 device: Option<Props>,
332 wireless: Option<Props>,
333 access_point: Option<Props>,
334 active: Option<Props>,
335}
336
337impl Object {
338 fn slot(&mut self, interface: &str) -> Option<&mut Option<Props>> {
341 match interface {
342 NM => Some(&mut self.manager),
343 DEVICE => Some(&mut self.device),
344 WIRELESS => Some(&mut self.wireless),
345 ACCESS_POINT => Some(&mut self.access_point),
346 ACTIVE => Some(&mut self.active),
347 _ => None,
348 }
349 }
350
351 fn kept(&self) -> bool {
352 self.manager.is_some()
353 || self.device.is_some()
354 || self.access_point.is_some()
355 || self.active.is_some()
356 }
357}
358
359fn by_number(a: &str, b: &str) -> Ordering {
362 (a.len(), a).cmp(&(b.len(), b))
363}
364
365fn ssid(props: &Props) -> Vec<u8> {
368 let bytes = dbus::get::<&Array>(props, "Ssid");
369 let bytes = bytes.iter().flat_map(|bytes| bytes.inner());
370 let bytes = bytes.map_while(|byte| match byte {
371 Variant::U8(byte) => Some(*byte),
372 _ => None,
373 });
374 bytes.take(MAX_SSID).collect()
375}
376
377fn paths<'a>(props: &'a Props, name: &str) -> impl Iterator<Item = &'a str> {
379 let paths = dbus::get::<&Array>(props, name);
380 paths
381 .into_iter()
382 .flat_map(|paths| paths.inner())
383 .filter_map(|path| match path {
384 Variant::ObjectPath(path) => Some(path.as_str()),
385 _ => None,
386 })
387}
388
389fn add(objects: &mut BTreeMap<String, Object>, path: String, interfaces: HashMap<String, Props>) {
392 if objects.len() >= MAX_OBJECTS && !objects.contains_key(&path) {
393 return tracing::debug!(path, "too many network objects; leaving one out");
394 }
395 let mut object = objects.remove(&path).unwrap_or_default();
396 for (interface, props) in interfaces {
397 if let Some(slot) = object.slot(&interface) {
398 *slot = Some(props);
399 }
400 }
401 if object.kept() {
402 objects.insert(path, object);
403 }
404}
405
406fn secured(point: &Props) -> bool {
409 let flags = |name| dbus::get::<u32>(point, name).unwrap_or(0);
410 let keyed = (flags("WpaFlags") | flags("RsnFlags")) & KEY_MGMT_KEYED != 0;
411 let wep = flags("Flags") & PRIVACY != 0 && flags("WpaFlags") == 0 && flags("RsnFlags") == 0;
412 keyed || wep
413}
414
415fn read(interface: &str, name: &str) -> bool {
419 match interface {
420 NM => name == "WirelessEnabled",
421 DEVICE => matches!(
422 name,
423 "Interface" | "DeviceType" | "State" | "ActiveConnection"
424 ),
425 WIRELESS => matches!(name, "AccessPoints" | "ActiveAccessPoint"),
426 ACCESS_POINT => matches!(
427 name,
428 "Ssid" | "Strength" | "Flags" | "WpaFlags" | "RsnFlags"
429 ),
430 ACTIVE => name == "Id",
431 _ => false,
432 }
433}
434
435fn wifi_networks(objects: &BTreeMap<String, Object>) -> Vec<WifiNetwork> {
438 let mut best = BTreeMap::<(&str, Vec<u8>), WifiNetwork>::new();
439 for (path, object) in objects {
440 let (Some(device), Some(wireless)) = (&object.device, &object.wireless) else {
441 continue;
442 };
443 let on = dbus::get::<u32>(device, "State") == Some(ACTIVATED);
444 let active = dbus::get::<&ObjectPath>(wireless, "ActiveAccessPoint");
445 let active = active.map(|at| at.as_str());
446 for at in paths(wireless, "AccessPoints") {
447 let Some(point) = objects
448 .get(at)
449 .and_then(|object| object.access_point.as_ref())
450 else {
451 continue;
452 };
453 let ssid = ssid(point);
454 if ssid.iter().all(|byte| *byte == 0) {
456 continue;
457 }
458 let network = WifiNetwork {
459 ssid: String::from_utf8_lossy(&ssid).into_owned(),
460 strength: dbus::get::<u8>(point, "Strength").unwrap_or(0).min(100),
461 secured: secured(point),
462 connected: on && active == Some(at),
463 device: path.clone(),
464 access_point: at.to_owned(),
465 };
466 match best.entry((path, ssid)) {
467 Entry::Vacant(entry) => {
468 entry.insert(network);
469 }
470 Entry::Occupied(mut entry) => {
471 let connected = entry.get().connected || network.connected;
472 if network.strength > entry.get().strength {
473 entry.insert(network);
474 }
475 entry.get_mut().connected = connected;
476 }
477 }
478 }
479 }
480 let mut networks = best.into_values().collect::<Vec<_>>();
481 networks.sort_by(|a, b| {
482 let ties = || (&a.ssid, &a.device).cmp(&(&b.ssid, &b.device));
483 b.strength.cmp(&a.strength).then_with(ties)
484 });
485 networks.truncate(MAX_ACCESS_POINTS);
486 networks
487}
488
489impl NetworkService {
490 pub(crate) fn new(shared: Rc<Shared>) -> NetworkService {
491 NetworkService {
492 shared,
493 state: RefCell::new(None),
494 }
495 }
496
497 fn start(self: &Rc<Self>) -> Network {
498 if let Some(state) = &*self.state.borrow() {
499 return Network {
500 service: Some(Rc::clone(self)),
501 wifi_enabled: state.wifi_enabled.read_only(),
502 devices: state.devices.read_only(),
503 wifi_networks: state.wifi_networks.read_only(),
504 };
505 }
506 let root = &self.shared.root;
507 let wifi_enabled = root.signal(false);
508 let (devices, wifi_networks) = (root.signal(Vec::new()), root.signal(Vec::new()));
509 *self.state.borrow_mut() = Some(State {
510 wifi_enabled,
511 devices,
512 wifi_networks,
513 objects: BTreeMap::new(),
514 connection: None,
515 generation: 0,
516 });
517 match self.shared.config.system_bus.clone() {
518 Some(address) => {
519 let follow = dbus::follow(Rc::clone(&self.shared), Rc::clone(self), address);
520 self.shared.spawn(follow);
521 }
522 None => tracing::info!("no system bus, so there is no networking"),
523 }
524 Network {
525 service: Some(Rc::clone(self)),
526 wifi_enabled: wifi_enabled.read_only(),
527 devices: devices.read_only(),
528 wifi_networks: wifi_networks.read_only(),
529 }
530 }
531
532 fn publish(&self) {
535 let shown = self.state.borrow().as_ref().map(|state| {
537 let objects = &state.objects;
538 let enabled = objects
539 .get(NM_PATH)
540 .and_then(|object| dbus::get(object.manager.as_ref()?, "WirelessEnabled"))
541 .unwrap_or(false);
542 let mut devices = objects
543 .iter()
544 .filter_map(|(path, object)| {
545 let props = object.device.as_ref()?;
546 let kind = dbus::get::<u32>(props, "DeviceType");
547 let state = dbus::get::<u32>(props, "State");
548 if kind == Some(LOOPBACK) || state == Some(UNMANAGED) {
549 return None;
550 }
551 Some(NetworkDevice::from_props(path.clone(), props, objects))
552 })
553 .collect::<Vec<_>>();
554 devices.sort_by(|a, b| by_number(&a.path, &b.path));
555 devices.truncate(MAX_DEVICES);
556 let shown = (enabled, devices, wifi_networks(objects));
557 (
558 state.wifi_enabled,
559 state.devices,
560 state.wifi_networks,
561 shown,
562 )
563 });
564 if let Some((enabled_signal, devices_signal, networks_signal, shown)) = shown {
565 let (enabled, devices, networks) = shown;
566 set_changed(enabled_signal, enabled);
567 set_changed(devices_signal, devices);
568 set_changed(networks_signal, networks);
569 }
570 }
571}
572
573impl Daemon for NetworkService {
574 const NAME: &'static str = NM;
575
576 fn rules() -> Vec<String> {
580 let mut rules = vec![format!(
581 "type='signal',sender='{NM}',path='{ROOT}',interface='{OBJECT_MANAGER}'"
582 )];
583 rules.extend(
584 [NM, DEVICE, WIRELESS, ACCESS_POINT, ACTIVE].map(|interface| {
585 format!(
586 "type='signal',sender='{NM}',interface='{PROPERTIES}',member='PropertiesChanged',\
587 arg0='{interface}'"
588 )
589 }),
590 );
591 rules
592 }
593
594 fn alive(&self) -> bool {
595 let state = self.state.borrow();
596 state
597 .as_ref()
598 .is_some_and(|state| state.devices.try_with(|_| ()).is_some())
599 }
600
601 fn clear(&self) -> u64 {
602 let generation = {
603 let mut state = self.state.borrow_mut();
604 let Some(state) = state.as_mut() else {
605 return 0;
606 };
607 state.objects.clear();
608 state.connection = None;
609 state.generation += 1;
610 state.generation
611 };
612 self.publish();
613 generation
614 }
615
616 async fn read(self: Rc<Self>, connection: Connection, generation: u64) {
617 let call = connection.call_method(
618 Some(NM),
619 ROOT,
620 Some(OBJECT_MANAGER),
621 "GetManagedObjects",
622 &(),
623 );
624 let read = async {
625 let reply = self.shared.timeout(CALL_TIMEOUT, call).await?;
626 reply
627 .body()
628 .deserialize::<HashMap<OwnedObjectPath, HashMap<String, Props>>>()
629 };
630 let managed = match read.await {
631 Ok(managed) => managed,
632 Err(error) => return tracing::warn!(%error, "couldn't read NetworkManager"),
633 };
634 let mut managed = managed
636 .into_iter()
637 .map(|(path, interfaces)| (path.as_str().to_owned(), interfaces))
638 .collect::<Vec<_>>();
639 managed.sort_by(|(a, _), (b, _)| by_number(a, b));
640 {
641 let mut state = self.state.borrow_mut();
642 let Some(state) = state
643 .as_mut()
644 .filter(|state| state.generation == generation)
645 else {
646 return;
647 };
648 state.objects.clear();
649 for (path, interfaces) in managed {
650 add(&mut state.objects, path, interfaces);
651 }
652 state.connection = Some(connection);
653 }
654 self.publish();
655 }
656
657 fn signal(self: &Rc<Self>, _: &Connection, message: &Message) {
658 if let Some((path, interface, changed, dropped)) = dbus::properties_changed(message) {
659 {
660 let mut state = self.state.borrow_mut();
661 let props = state
662 .as_mut()
663 .and_then(|state| state.objects.get_mut(&path))
664 .and_then(|object| object.slot(&interface)?.as_mut());
665 let Some(props) = props else {
666 return;
667 };
668 let shown = changed
669 .keys()
670 .chain(&dropped)
671 .any(|name| read(&interface, name));
672 dbus::apply(props, changed, dropped);
673 if !shown {
674 return;
675 }
676 }
677 return self.publish();
678 }
679 let header = message.header();
680 if header.interface().map(|interface| interface.as_str()) != Some(OBJECT_MANAGER) {
681 return;
682 }
683 let body = message.body();
684 {
685 let mut state = self.state.borrow_mut();
686 let Some(state) = state.as_mut() else {
687 return;
688 };
689 match header.member().map(|member| member.as_str()) {
690 Some("InterfacesAdded") => {
691 let Ok((path, interfaces)) =
692 body.deserialize::<(OwnedObjectPath, HashMap<String, Props>)>()
693 else {
694 return;
695 };
696 add(&mut state.objects, path.as_str().to_owned(), interfaces);
697 }
698 Some("InterfacesRemoved") => {
699 let Ok((path, interfaces)) = body.deserialize::<(OwnedObjectPath, Vec<String>)>() else {
700 return;
701 };
702 let Some(object) = state.objects.get_mut(path.as_str()) else {
703 return;
704 };
705 for interface in interfaces {
706 if let Some(slot) = object.slot(&interface) {
707 *slot = None;
708 }
709 }
710 if !object.kept() {
711 state.objects.remove(path.as_str());
712 }
713 }
714 _ => return,
715 }
716 }
717 self.publish();
718 }
719}