Skip to main content

crabshell_services/network/
saved.rs

1//! The connections NetworkManager saved, read on the first ask over a
2//! bus connection of their own, so a config that never lists them reads
3//! none and never wakes for their changes.
4
5use std::cell::Cell;
6use std::collections::HashMap;
7use std::rc::Rc;
8
9use crabshell_reactive::Signal;
10use serde_json::Value as Json;
11use zbus::{Connection, Message, OwnedObjectPath, Variant};
12
13use super::{NM, by_number};
14use crate::Shared;
15use crate::dbus::{self, CALL_TIMEOUT, Daemon, Props, set_changed};
16
17pub(super) const SETTINGS: &str = "org.freedesktop.NetworkManager.Settings";
18pub(super) const SETTINGS_PATH: &str = "/org/freedesktop/NetworkManager/Settings";
19pub(super) const SAVED: &str = "org.freedesktop.NetworkManager.Settings.Connection";
20
21/// The most saved connections read: a laptop keeps one for each Wi-Fi
22/// network it joined.
23const MAX_SAVED: usize = 256;
24/// The biggest reply a saved connection may come in; one with 802.1X
25/// certificates inside is about 10 KiB.
26const MAX_REPLY: usize = 64 * 1024;
27/// About the most memory the list keeps: every connection's own strings,
28/// and the settings that fit. A byte is one byte in a reply and a whole
29/// JSON value once read, so this counts what is kept, not what came.
30const MAX_KEPT: usize = 1024 * 1024;
31
32/// A connection NetworkManager saved, as for a Wi-Fi network joined
33/// before.
34#[derive(Clone, Debug, PartialEq)]
35pub struct SavedConnection {
36  /// What names it for good, as [`forget`](super::Network::forget) and
37  /// [`clear_secrets`](super::Network::clear_secrets) take.
38  pub uuid: String,
39  /// Its name, often the Wi-Fi network's.
40  pub id: String,
41  /// NetworkManager's type for it, such as `802-11-wireless`,
42  /// `802-3-ethernet`, `vpn` or `wireguard`. Its settings for that type
43  /// are under the same name.
44  pub kind: String,
45  /// Its settings by group and name, such as `settings["ipv4"]["method"]`,
46  /// without secrets, which NetworkManager never hands out with them. A
47  /// byte array, such as an SSID, is an array of numbers. `null` for one
48  /// whose settings didn't fit in the 1 MiB kept for the whole list.
49  pub settings: Json,
50}
51
52impl SavedConnection {
53  /// One named from NetworkManager's `settings`, without them yet.
54  fn named(settings: &HashMap<String, Props>) -> SavedConnection {
55    let about = settings.get("connection");
56    let text = |name| about.map_or(String::new(), |props| dbus::text(props, name));
57    SavedConnection {
58      uuid: text("uuid"),
59      id: text("id"),
60      kind: text("type"),
61      settings: Json::Null,
62    }
63  }
64
65  /// About the memory it takes without its settings.
66  fn weight(&self) -> usize {
67    size_of::<SavedConnection>() + self.uuid.len() + self.id.len() + self.kind.len()
68  }
69}
70
71/// NetworkManager's `settings` as a JSON object of groups, adding about
72/// the memory it takes to `cost`.
73fn settings_json(settings: &HashMap<String, Props>, cost: &mut usize) -> Json {
74  let mut groups = serde_json::Map::new();
75  for (group, props) in settings {
76    let mut entries = serde_json::Map::new();
77    for (name, value) in props {
78      *cost += size_of::<String>() + name.len();
79      entries.insert(name.clone(), json(value, cost));
80    }
81    *cost += size_of::<String>() + group.len() + size_of::<Json>();
82    groups.insert(group.clone(), Json::Object(entries));
83  }
84  *cost += size_of::<Json>();
85  Json::Object(groups)
86}
87
88/// `value` as JSON: a dictionary as an object, an array or a structure
89/// as an array. A file descriptor, or a number JSON can't hold, is `null`.
90/// Adds about the memory the JSON takes to `cost`: each value, and each
91/// string's and key's bytes.
92pub(super) fn json(value: &Variant<'_>, cost: &mut usize) -> Json {
93  let node = match value {
94    // A variant inside a variant is the value it holds.
95    Variant::Variant(inner) => return json(inner, cost),
96    Variant::U8(n) => Json::from(*n),
97    Variant::Bool(b) => Json::from(*b),
98    Variant::I16(n) => Json::from(*n),
99    Variant::U16(n) => Json::from(*n),
100    Variant::I32(n) => Json::from(*n),
101    Variant::U32(n) => Json::from(*n),
102    Variant::I64(n) => Json::from(*n),
103    Variant::U64(n) => Json::from(*n),
104    Variant::F64(n) => Json::from(*n),
105    Variant::Str(text) => text_json(text.as_str().to_owned(), cost),
106    Variant::Signature(signature) => text_json(signature.to_string(), cost),
107    Variant::ObjectPath(path) => text_json(path.as_str().to_owned(), cost),
108    Variant::Array(array) => array.inner().iter().map(|item| json(item, cost)).collect(),
109    Variant::Structure(structure) => {
110      let fields = structure.fields().iter();
111      fields.map(|field| json(field, cost)).collect()
112    }
113    Variant::Dict(dict) => {
114      let mut entries = serde_json::Map::new();
115      for (key, value) in dict.iter() {
116        let key = match key {
117          Variant::Str(text) => text.as_str().to_owned(),
118          key => json(key, &mut 0).to_string(),
119        };
120        *cost += size_of::<String>() + key.len();
121        entries.insert(key, json(value, cost));
122      }
123      Json::Object(entries)
124    }
125    Variant::Fd(_) => Json::Null,
126  };
127  *cost += size_of::<Json>();
128  node
129}
130
131/// `text` as JSON, adding its bytes to `cost`.
132fn text_json(text: String, cost: &mut usize) -> Json {
133  *cost += text.len();
134  Json::String(text)
135}
136
137/// The saved connections for one shell, followed from the first ask.
138pub(super) struct SavedConnections {
139  shared: Rc<Shared>,
140  pub(super) list: Signal<Vec<SavedConnection>>,
141  followed: Cell<bool>,
142  generation: Cell<u64>,
143}
144
145impl SavedConnections {
146  pub(super) fn new(shared: Rc<Shared>) -> Rc<SavedConnections> {
147    let list = shared.root.signal(Vec::new());
148    Rc::new(SavedConnections {
149      shared,
150      list,
151      followed: Cell::new(false),
152      generation: Cell::new(0),
153    })
154  }
155
156  /// Starts following them, the first time.
157  pub(super) fn follow(self: &Rc<Self>) {
158    if self.followed.replace(true) {
159      return;
160    }
161    // Without a system bus there is nothing to follow, which the network
162    // service has logged.
163    if let Some(address) = self.shared.config.system_bus.clone() {
164      let follow = dbus::follow(Rc::clone(&self.shared), Rc::clone(self), address);
165      self.shared.spawn(follow);
166    }
167  }
168}
169
170impl Daemon for SavedConnections {
171  const NAME: &'static str = NM;
172
173  /// A saved connection added, removed or changed: nothing else.
174  fn rules() -> Vec<String> {
175    vec![
176      format!("type='signal',sender='{NM}',path='{SETTINGS_PATH}',interface='{SETTINGS}'"),
177      format!("type='signal',sender='{NM}',interface='{SAVED}',member='Updated'"),
178    ]
179  }
180
181  fn alive(&self) -> bool {
182    self.list.try_with(|_| ()).is_some()
183  }
184
185  fn clear(&self) -> u64 {
186    set_changed(self.list, Vec::new());
187    self.generation.set(self.generation.get() + 1);
188    self.generation.get()
189  }
190
191  async fn read(self: Rc<Self>, connection: Connection, generation: u64) {
192    let shared = &self.shared;
193    let call = connection.call_method(
194      Some(NM),
195      SETTINGS_PATH,
196      Some(SETTINGS),
197      "ListConnections",
198      &(),
199    );
200    let listed = async {
201      let reply = shared.timeout(CALL_TIMEOUT, call).await?;
202      reply.body().deserialize::<Vec<OwnedObjectPath>>()
203    };
204    let mut paths = match listed.await {
205      Ok(paths) => paths,
206      Err(error) => {
207        return tracing::warn!(%error, "couldn't list NetworkManager's saved connections");
208      }
209    };
210    // By number, so the cap keeps the oldest, the same every time.
211    paths.sort_by(|a, b| by_number(a.as_str(), b.as_str()));
212    let (mut list, mut room) = (Vec::new(), MAX_KEPT);
213    let (mut without_settings, mut left_out) = (0, 0);
214    for path in paths.iter().take(MAX_SAVED).map(|path| path.as_str()) {
215      let call = connection.call_method(Some(NM), path, Some(SAVED), "GetSettings", &());
216      let reply = match shared.timeout(CALL_TIMEOUT, call).await {
217        Ok(reply) => reply,
218        // Gone since the list, or hidden from this user.
219        Err(error) => {
220          tracing::info!(%error, path, "couldn't read a saved connection");
221          continue;
222        }
223      };
224      let body = reply.body();
225      if body.len() > MAX_REPLY {
226        tracing::warn!(
227          path,
228          bytes = body.len(),
229          "a saved connection too big; left out"
230        );
231        continue;
232      }
233      let Ok(settings) = body.deserialize::<HashMap<String, Props>>() else {
234        tracing::warn!(
235          path,
236          "a saved connection's settings that don't parse; left out"
237        );
238        continue;
239      };
240      let mut saved = SavedConnection::named(&settings);
241      let Some(left) = room.checked_sub(saved.weight()) else {
242        left_out += 1;
243        continue;
244      };
245      room = left;
246      let mut cost = 0;
247      let json = settings_json(&settings, &mut cost);
248      match room.checked_sub(cost) {
249        Some(left) => (room, saved.settings) = (left, json),
250        None => without_settings += 1,
251      }
252      list.push(saved);
253    }
254    if without_settings + left_out > 0 {
255      let what = "saved connections past 1 MiB listed without settings, or left out";
256      tracing::warn!(without_settings, left_out, "{what}");
257    }
258    if self.generation.get() == generation {
259      set_changed(self.list, list);
260    }
261  }
262
263  fn signal(self: &Rc<Self>, _: &Connection, _: &Message) {}
264
265  /// Each signal names a saved connection without its settings, so all
266  /// are read again.
267  fn reads_again(&self, _: &Message) -> bool {
268    true
269  }
270}