Skip to main content

wayle_network/core/settings_connection/
mod.rs

1mod controls;
2mod monitoring;
3mod types;
4
5use std::{collections::HashMap, sync::Arc};
6
7use controls::ConnectionSettingsControls;
8use derive_more::Debug;
9use futures::{Stream, StreamExt};
10use tokio_util::sync::CancellationToken;
11pub(crate) use types::{ConnectionSettingsParams, LiveConnectionSettingsParams};
12use wayle_core::{Property, unwrap_dbus};
13use wayle_traits::{ModelMonitoring, Reactive};
14use zbus::{
15    Connection,
16    zvariant::{self, OwnedObjectPath, OwnedValue},
17};
18
19use super::access_point::types::Ssid;
20use crate::{
21    error::Error,
22    proxy::settings::connection::SettingsConnectionProxy,
23    types::{connectivity::ConnectionType, flags::NMConnectionSettingsFlags},
24};
25
26/// Connection Settings Profile.
27///
28/// Represents a single network connection configuration.
29#[derive(Debug, Clone)]
30pub struct ConnectionSettings {
31    #[debug(skip)]
32    pub(crate) connection: Connection,
33    #[debug(skip)]
34    pub(crate) cancellation_token: Option<CancellationToken>,
35    /// D-Bus object path for this settings connection.
36    pub object_path: OwnedObjectPath,
37
38    /// Human-readable connection name (e.g. "Home WiFi", "Wired connection 1").
39    pub id: Property<String>,
40
41    /// Stable unique identifier for this connection profile.
42    pub uuid: Property<String>,
43
44    /// Network connection type.
45    pub connection_type: Property<ConnectionType>,
46
47    /// WiFi SSID, if this is a wireless connection.
48    pub wifi_ssid: Property<Option<Ssid>>,
49
50    /// Whether the in-memory state differs from the on-disk state.
51    pub unsaved: Property<bool>,
52
53    /// Additional flags of the connection profile.
54    pub flags: Property<NMConnectionSettingsFlags>,
55
56    /// File that stores the connection in case the connection is file-backed.
57    pub filename: Property<String>,
58}
59
60impl Reactive for ConnectionSettings {
61    type Context<'a> = ConnectionSettingsParams<'a>;
62    type LiveContext<'a> = LiveConnectionSettingsParams<'a>;
63    type Error = Error;
64
65    async fn get(params: Self::Context<'_>) -> Result<Self, Self::Error> {
66        Self::from_path(params.connection, params.path, None).await
67    }
68
69    async fn get_live(params: Self::LiveContext<'_>) -> Result<Arc<Self>, Self::Error> {
70        let properties = Self::fetch_properties(params.connection, &params.path).await?;
71        let settings = Arc::new(Self::from_props(
72            params.path.clone(),
73            properties,
74            params.connection,
75            Some(params.cancellation_token.child_token()),
76        ));
77
78        settings.clone().start_monitoring().await?;
79
80        Ok(settings)
81    }
82}
83
84impl PartialEq for ConnectionSettings {
85    fn eq(&self, other: &Self) -> bool {
86        self.object_path == other.object_path
87    }
88}
89
90impl ConnectionSettings {
91    /// Update the connection with new settings and properties (replacing all
92    /// previous settings and properties) and save the connection to disk.
93    /// Secrets may be part of the update request, and will be either stored
94    /// in persistent storage or sent to a Secret Agent for storage, depending
95    /// on the flags associated with each secret.
96    ///
97    /// # Errors
98    ///
99    /// Returns `NetworkError::OperationFailed` if the update operation fails.
100    pub async fn update(
101        &self,
102        properties: HashMap<String, HashMap<String, OwnedValue>>,
103    ) -> Result<(), Error> {
104        ConnectionSettingsControls::update(&self.connection, &self.object_path, properties).await
105    }
106
107    /// Update the connection without immediately saving to disk.
108    ///
109    /// Update the connection with new settings and properties (replacing all
110    /// previous settings and properties) but do not immediately save the
111    /// connection to disk. Secrets may be part of the update request and may
112    /// be sent to a Secret Agent for storage, depending on the flags associated
113    /// with each secret. Use the 'Save' method to save these changes to disk.
114    /// Note that unsaved changes will be lost if the connection is reloaded
115    /// from disk (either automatically on file change or due to an explicit
116    /// ReloadConnections call).
117    ///
118    /// # Errors
119    ///
120    /// Returns `NetworkError::OperationFailed` if the update operation fails.
121    pub async fn update_unsaved(
122        &self,
123        properties: HashMap<String, HashMap<String, OwnedValue>>,
124    ) -> Result<(), Error> {
125        ConnectionSettingsControls::update_unsaved(&self.connection, &self.object_path, properties)
126            .await
127    }
128
129    /// Delete the connection.
130    ///
131    /// # Errors
132    ///
133    /// Returns `NetworkError::OperationFailed` if the delete operation fails.
134    pub async fn delete(&self) -> Result<(), Error> {
135        ConnectionSettingsControls::delete(&self.connection, &self.object_path).await
136    }
137
138    /// Get the settings maps describing this network configuration.
139    ///
140    /// This will never include any secrets required for connection to the
141    /// network, as those are often protected. Secrets must be requested
142    /// separately using the GetSecrets() call.
143    ///
144    /// # Errors
145    ///
146    /// Returns `NetworkError::OperationFailed` if retrieving settings fails.
147    pub async fn get_settings(
148        &self,
149    ) -> Result<HashMap<String, HashMap<String, OwnedValue>>, Error> {
150        ConnectionSettingsControls::get_settings(&self.connection, &self.object_path).await
151    }
152
153    /// Get the secrets belonging to this network configuration.
154    ///
155    /// Only secrets from persistent storage or a Secret Agent running in the
156    /// requestor's session will be returned. The user will never be prompted
157    /// for secrets as a result of this request.
158    ///
159    /// # Arguments
160    ///
161    /// * `setting_name` - Name of the setting to return secrets for. If empty,
162    ///   all secrets will be returned.
163    ///
164    /// # Errors
165    ///
166    /// Returns `NetworkError::OperationFailed` if retrieving secrets fails.
167    pub async fn get_secrets(
168        &self,
169        setting_name: &str,
170    ) -> Result<HashMap<String, HashMap<String, OwnedValue>>, Error> {
171        ConnectionSettingsControls::get_secrets(&self.connection, &self.object_path, setting_name)
172            .await
173    }
174
175    /// Clear the secrets belonging to this network connection profile.
176    ///
177    /// # Errors
178    ///
179    /// Returns `NetworkError::OperationFailed` if clearing secrets fails.
180    pub async fn clear_secrets(&self) -> Result<(), Error> {
181        ConnectionSettingsControls::clear_secrets(&self.connection, &self.object_path).await
182    }
183
184    /// Saves a "dirty" connection to persistent storage.
185    ///
186    /// Saves a connection (that had previously been updated with UpdateUnsaved)
187    /// to persistent storage.
188    ///
189    /// # Errors
190    ///
191    /// Returns `NetworkError::OperationFailed` if saving fails.
192    pub async fn save(&self) -> Result<(), Error> {
193        ConnectionSettingsControls::save(&self.connection, &self.object_path).await
194    }
195
196    /// Update the connection with new settings and properties.
197    ///
198    /// Update2 is an alternative to Update, UpdateUnsaved and Save extensible
199    /// with extra flags and args arguments.
200    ///
201    /// # Arguments
202    ///
203    /// * `settings` - New connection settings, properties, and (optionally) secrets.
204    ///   Provide an empty HashMap to use the current settings.
205    /// * `flags` - Optional flags. Unknown flags cause the call to fail.
206    ///   - 0x1 (to-disk): The connection is persisted to disk.
207    ///   - 0x2 (in-memory): The change is only made in memory.
208    ///   - 0x4 (in-memory-detached): Like "in-memory", but behaves slightly different when migrating.
209    ///   - 0x8 (in-memory-only): Like "in-memory", but behaves slightly different when migrating.
210    ///   - 0x10 (volatile): Connection is volatile.
211    ///   - 0x20 (block-autoconnect): Blocks auto-connect on the updated profile.
212    ///   - 0x40 (no-reapply): Prevents "connection.zone" and "connection.metered" from taking effect on active devices.
213    /// * `args` - Optional arguments dictionary. Accepts "plugin" and "version-id" keys.
214    ///
215    /// # Errors
216    ///
217    /// Returns `NetworkError::OperationFailed` if the update operation fails.
218    pub async fn update2(
219        &self,
220        settings: HashMap<String, HashMap<String, OwnedValue>>,
221        flags: u32,
222        args: HashMap<String, OwnedValue>,
223    ) -> Result<HashMap<String, OwnedValue>, Error> {
224        ConnectionSettingsControls::update2(
225            &self.connection,
226            &self.object_path,
227            settings,
228            flags,
229            args,
230        )
231        .await
232    }
233
234    /// Whether this is a wireless connection with the given SSID.
235    pub(crate) fn matches_ssid(&self, ssid: &Ssid) -> bool {
236        self.wifi_ssid
237            .get()
238            .as_ref()
239            .is_some_and(|stored| stored == ssid)
240    }
241
242    async fn from_path(
243        connection: &Connection,
244        path: OwnedObjectPath,
245        cancellation_token: Option<CancellationToken>,
246    ) -> Result<Self, Error> {
247        let properties = Self::fetch_properties(connection, &path).await?;
248        Ok(Self::from_props(
249            path,
250            properties,
251            connection,
252            cancellation_token,
253        ))
254    }
255
256    async fn fetch_properties(
257        connection: &Connection,
258        path: &OwnedObjectPath,
259    ) -> Result<SettingsConnectionProperties, Error> {
260        let proxy = SettingsConnectionProxy::new(connection, path)
261            .await
262            .map_err(Error::DbusError)?;
263
264        let (unsaved, flags, filename, settings) = tokio::join!(
265            proxy.unsaved(),
266            proxy.flags(),
267            proxy.filename(),
268            proxy.get_settings()
269        );
270
271        let (id, uuid, connection_type, wifi_ssid) = match settings {
272            Ok(ref settings_map) => extract_identity(settings_map),
273            Err(err) => {
274                tracing::debug!("cannot fetch GetSettings for {:?}: {}", path, err);
275                (String::new(), String::new(), ConnectionType::None, None)
276            }
277        };
278
279        Ok(SettingsConnectionProperties {
280            unsaved: unwrap_dbus!(unsaved, path),
281            flags: unwrap_dbus!(flags, path),
282            filename: unwrap_dbus!(filename, path),
283            id,
284            uuid,
285            connection_type,
286            wifi_ssid,
287        })
288    }
289
290    fn from_props(
291        path: OwnedObjectPath,
292        props: SettingsConnectionProperties,
293        connection: &Connection,
294        cancellation_token: Option<CancellationToken>,
295    ) -> Self {
296        Self {
297            connection: connection.clone(),
298            cancellation_token,
299            object_path: path,
300            id: Property::new(props.id),
301            uuid: Property::new(props.uuid),
302            connection_type: Property::new(props.connection_type),
303            wifi_ssid: Property::new(props.wifi_ssid),
304            unsaved: Property::new(props.unsaved),
305            flags: Property::new(NMConnectionSettingsFlags::from_bits_truncate(props.flags)),
306            filename: Property::new(props.filename),
307        }
308    }
309
310    /// Emitted when any property of any settings object within this Connection has changed.
311    ///
312    /// # Errors
313    /// Returns error if D-Bus proxy creation fails.
314    pub async fn properties_changed_signal(
315        &self,
316    ) -> Result<impl Stream<Item = HashMap<String, OwnedValue>>, Error> {
317        let proxy = SettingsConnectionProxy::new(&self.connection, &self.object_path).await?;
318        let stream = proxy.receive_properties_changed().await?;
319
320        Ok(stream
321            .filter_map(|signal| async move { signal.args().ok().map(|args| args.properties) }))
322    }
323
324    /// Emitted when the connection is updated.
325    ///
326    /// # Errors
327    /// Returns error if D-Bus proxy creation fails.
328    pub async fn updated_signal(&self) -> Result<impl Stream<Item = ()>, Error> {
329        let proxy = SettingsConnectionProxy::new(&self.connection, &self.object_path).await?;
330        let stream = proxy.receive_updated().await?;
331
332        Ok(stream.filter_map(|_signal| async move { Some(()) }))
333    }
334
335    /// Emitted when the connection is removed.
336    ///
337    /// # Errors
338    /// Returns error if D-Bus proxy creation fails.
339    pub async fn removed_signal(&self) -> Result<impl Stream<Item = ()>, Error> {
340        let proxy = SettingsConnectionProxy::new(&self.connection, &self.object_path).await?;
341        let stream = proxy.receive_removed().await?;
342
343        Ok(stream.filter_map(|_signal| async move { Some(()) }))
344    }
345}
346
347fn extract_identity(
348    settings_map: &HashMap<String, HashMap<String, OwnedValue>>,
349) -> (String, String, ConnectionType, Option<Ssid>) {
350    let connection_group = settings_map.get("connection");
351
352    let id = connection_group
353        .and_then(|conn| conn.get("id"))
354        .and_then(|val| String::try_from(val.clone()).ok())
355        .unwrap_or_default();
356
357    let uuid = connection_group
358        .and_then(|conn| conn.get("uuid"))
359        .and_then(|val| String::try_from(val.clone()).ok())
360        .unwrap_or_default();
361
362    let connection_type = connection_group
363        .and_then(|conn| conn.get("type"))
364        .and_then(|val| String::try_from(val.clone()).ok())
365        .map(|type_str| ConnectionType::from_nm_type(&type_str))
366        .unwrap_or(ConnectionType::None);
367
368    let wifi_ssid = settings_map
369        .get("802-11-wireless")
370        .and_then(|wireless| wireless.get("ssid"))
371        .and_then(|val| val.downcast_ref::<zvariant::Array>().ok())
372        .and_then(|arr| <Vec<u8>>::try_from(arr).ok())
373        .map(Ssid::new);
374
375    (id, uuid, connection_type, wifi_ssid)
376}
377
378struct SettingsConnectionProperties {
379    unsaved: bool,
380    flags: u32,
381    filename: String,
382    id: String,
383    uuid: String,
384    connection_type: ConnectionType,
385    wifi_ssid: Option<Ssid>,
386}