Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
7181662
Added an initial DevicesTable in the network overview
teclator Mar 5, 2026
6ddac52
Added connection column
teclator Mar 5, 2026
97b3675
Added network device connect/disconnect actions
teclator Mar 6, 2026
a73c40b
Do not use a postAction for connecting / disconnecting
teclator Mar 6, 2026
0b61ea0
Use Active Connection label
teclator Mar 6, 2026
d713c2e
Small fixes
teclator Mar 7, 2026
5bd53c5
Added connections table similar to the devices one
teclator Mar 9, 2026
95bd0a4
Remove WiFi connections tests
teclator Mar 9, 2026
539309e
Added progress to network UI
teclator Mar 10, 2026
fa70ef5
Some changes based on CR
teclator Mar 11, 2026
e12b6f1
Added more device types to the UI
teclator Mar 11, 2026
edd9ba9
Some changes for supporting the dnsSearchList in the HTTP API
teclator Mar 11, 2026
2ed3943
Added support for setting the dnsSearchList in the UI
teclator Mar 11, 2026
c01412d
feat(web): allow selecting the devices in connection form
dgdavid Mar 12, 2026
5d99521
Small fixed to the rust code
teclator Mar 12, 2026
1662279
Allow to remove the device active connection
teclator Mar 12, 2026
2f7adb8
feat(web): allow setting unbound iface on new connection
dgdavid Mar 12, 2026
50dcbad
refactor(web): allow connecting to Wi-Fi network
dgdavid Mar 12, 2026
8fbf7b3
refactor(web): use tags for network page
dgdavid Mar 12, 2026
2e92f5e
fix(web): stop complaining about missing Page.Section attrs.
dgdavid Mar 12, 2026
cbeb72e
web: add icons to newtork connections actions
dgdavid Mar 12, 2026
4b7187e
refactor(web): improve WifiConnectionForm
dgdavid Mar 12, 2026
6d612fa
Small UI fixes
teclator Mar 13, 2026
13dda2b
Allow to connect/disconnect a connection
teclator Mar 13, 2026
bb75047
Do not allow to modify the name of new connections
teclator Mar 13, 2026
9839eca
Do not show the option for adding a WiFi if not enabled
teclator Mar 13, 2026
c68f32c
Added Bind, Device and Status columns
teclator Mar 13, 2026
b9c289b
refactor(web): remove network page tabs
dgdavid Mar 13, 2026
96cacc8
refactor(web): remove network devices table
dgdavid Mar 13, 2026
5e483bb
fix(web): improve connection form field label
dgdavid Mar 13, 2026
f5e39ab
feat(web): make wired connection page compatible with wi-fi
dgdavid Mar 13, 2026
a677cc9
Some reorder of columns and small fixes
teclator Mar 13, 2026
747deb1
fix(web): drop dead component
dgdavid Mar 13, 2026
1c60d49
fix(web): drop dead network route
dgdavid Mar 13, 2026
a2fa527
fix(web): adapt explanation to latest interface state
dgdavid Mar 13, 2026
af2bf93
Listen to AccessPoints changes
teclator Mar 12, 2026
5f7fc30
Initialize the watcher connections registry
teclator Mar 12, 2026
765e2b4
Do not crash if not supported connection
teclator Mar 13, 2026
c2b42fd
Small fmt fixes
teclator Mar 13, 2026
aceb5e9
Network backend improvements (#3276)
teclator Mar 13, 2026
35c16ba
refactor(web): change information shown in ConnectionsTable
dgdavid Mar 13, 2026
8b3e1cc
fix(web): drop dead code
dgdavid Mar 13, 2026
cb3ce77
doc(web): update copyright date
dgdavid Mar 13, 2026
5b7ccd1
doc(web): update web changes file
dgdavid Mar 13, 2026
12eda98
Added rust changelog
teclator Mar 13, 2026
8e21c10
feat(web): add action for jump directly to edit binding
dgdavid Mar 13, 2026
15a2bc4
web: add missing tests and documentation
dgdavid Mar 13, 2026
3cba07e
refactor(web): adjust styling of binding hint
dgdavid Mar 13, 2026
3081e95
Apply suggestions from code review
dgdavid Mar 15, 2026
76140a6
Fixed ConnectionsTable unit test
teclator Mar 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion rust/agama-lib/share/profile.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -365,13 +365,21 @@
"type": "string"
}
},
"dnsSearchlist": {
"dnsSearchList": {
"type": "array",
"items": {
"description": "DNS search domains",
"type": "string"
}
},
"dnsSearchlist": {
"type": "array",
"items": {
"description": "DNS search domains",
"type": "string"
},
"deprecated": true
},
"ignoreAutoDns": {
"description": "Whether DNS options provided via DHCP are used or not",
"type": "boolean"
Expand Down
10 changes: 7 additions & 3 deletions rust/agama-network/src/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ pub enum Action {
GetConnections(Responder<Vec<Connection>>),
/// Gets all scanned access points
GetAccessPoints(Responder<Vec<AccessPoint>>),
/// Adds a new access point.
AddAccessPoint(Box<AccessPoint>),
/// Removes an access point by its hardware address.
RemoveAccessPoint(String),
/// Adds a new device.
AddDevice(Box<Device>),
/// Updates a device by its `name`.
Expand All @@ -66,7 +70,7 @@ pub enum Action {
GetDevices(Responder<Vec<Device>>),
GetGeneralState(Responder<GeneralState>),
/// Connection state changed
ChangeConnectionState(String, ConnectionState),
ChangeConnectionState(Uuid, ConnectionState),
/// Persists existing connections if none exist and the network copy is not disabled.
ProposeDefault(Responder<Result<(), NetworkStateError>>),
// Copies persistent connections to the target system
Expand All @@ -77,8 +81,8 @@ pub enum Action {
UpdateGeneralState(GeneralState),
/// Forces a wireless networks scan refresh
RefreshScan(Responder<Result<(), NetworkAdapterError>>),
/// Remove the connection with the given ID.
RemoveConnection(String),
/// Remove the connection with the given UUID.
RemoveConnection(Uuid),
/// Apply the current configuration.
Apply(Responder<Result<(), NetworkAdapterError>>),
}
2 changes: 2 additions & 0 deletions rust/agama-network/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ pub enum NetworkStateError {
CannotUpdateConnection(String),
#[error("Unknown device '{0}'")]
UnknownDevice(String),
#[error("Unknown access point '{0}'")]
UnknownAccessPoint(String),
#[error("Invalid connection UUID: '{0}'")]
InvalidUuid(String),
#[error("Invalid IP address: '{0}'")]
Expand Down
124 changes: 106 additions & 18 deletions rust/agama-network/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,12 +296,10 @@ impl NetworkState {

/// Updates a connection with a new one.
///
/// It uses the `id` to decide which connection to update.
///
/// Additionally, it registers the connection to be removed when the changes are applied.
/// It uses the `uuid` to decide which connection to update.
pub fn update_connection(&mut self, conn: Connection) -> Result<(), NetworkStateError> {
let Some(old_conn) = self.get_connection_mut(&conn.id) else {
return Err(NetworkStateError::UnknownConnection(conn.id.clone()));
let Some(old_conn) = self.get_connection_by_uuid_mut(conn.uuid) else {
return Err(NetworkStateError::UnknownConnection(conn.uuid.to_string()));
};
*old_conn = conn;

Expand All @@ -311,9 +309,9 @@ impl NetworkState {
/// Removes a connection from the state.
///
/// Additionally, it registers the connection to be removed when the changes are applied.
pub fn remove_connection(&mut self, id: &str) -> Result<(), NetworkStateError> {
let Some(position) = self.connections.iter().position(|d| d.id == id) else {
return Err(NetworkStateError::UnknownConnection(id.to_string()));
pub fn remove_connection(&mut self, uuid: Uuid) -> Result<(), NetworkStateError> {
let Some(position) = self.connections.iter().position(|d| d.uuid == uuid) else {
return Err(NetworkStateError::UnknownConnection(uuid.to_string()));
};

self.connections.remove(position);
Expand Down Expand Up @@ -343,6 +341,34 @@ impl NetworkState {
Ok(())
}

pub fn add_access_point(&mut self, ap: AccessPoint) -> Result<(), NetworkStateError> {
if let Some(position) = self
.access_points
.iter()
.position(|a| a.hw_address == ap.hw_address)
{
self.access_points.remove(position);
}
self.access_points.push(ap);

Ok(())
}

pub fn remove_access_point(&mut self, hw_address: &str) -> Result<(), NetworkStateError> {
let Some(position) = self
.access_points
.iter()
.position(|a| a.hw_address == hw_address)
else {
return Err(NetworkStateError::UnknownAccessPoint(
hw_address.to_string(),
));
};

self.access_points.remove(position);
Ok(())
}

/// Sets a controller's ports.
///
/// If the connection is not a controller, returns an error.
Expand Down Expand Up @@ -417,22 +443,23 @@ mod tests {
#[test]
fn test_update_connection() {
let mut state = NetworkState::default();
let uuid = Uuid::new_v4();
let conn0 = Connection {
id: "eth0".to_string(),
uuid: Uuid::new_v4(),
uuid,
..Default::default()
};
state.add_connection(conn0).unwrap();

let uuid = Uuid::new_v4();
let conn1 = Connection {
id: "eth0".to_string(),
uuid,
firewall_zone: Some("public".to_string()),
..Default::default()
};
state.update_connection(conn1).unwrap();
let found = state.get_connection("eth0").unwrap();
assert_eq!(found.uuid, uuid);
let found = state.get_connection_by_uuid(uuid).unwrap();
assert_eq!(found.firewall_zone, Some("public".to_string()));
}

#[test]
Expand All @@ -446,20 +473,77 @@ mod tests {
#[test]
fn test_remove_connection() {
let mut state = NetworkState::default();
let conn0 = Connection::new("eth0".to_string(), DeviceType::Ethernet);
let uuid = Uuid::new_v4();
let conn0 = Connection {
id: "eth0".to_string(),
uuid,
..Default::default()
};
state.add_connection(conn0).unwrap();
state.remove_connection("eth0".as_ref()).unwrap();
let found = state.get_connection("eth0");
state.remove_connection(uuid).unwrap();
let found = state.get_connection_by_uuid(uuid);
assert!(found.is_none());
}

#[test]
fn test_remove_unknown_connection() {
let mut state = NetworkState::default();
let error = state.remove_connection("unknown".as_ref()).unwrap_err();
let uuid = Uuid::new_v4();
let error = state.remove_connection(uuid).unwrap_err();
assert!(matches!(error, NetworkStateError::UnknownConnection(_)));
}

#[test]
fn test_remove_device() {
let mut state = NetworkState::default();
let device = Device {
name: "eth0".to_string(),
..Default::default()
};
state.add_device(device).unwrap();
state.remove_device("eth0").unwrap();
assert!(state.get_device("eth0").is_none());
}

#[test]
fn test_add_access_point() {
let mut state = NetworkState::default();
let ap = AccessPoint {
hw_address: "AA:BB:CC:DD:EE:FF".to_string(),
ssid: SSID(b"test".to_vec()),
..Default::default()
};
state.add_access_point(ap.clone()).unwrap();
assert_eq!(state.access_points.len(), 1);
assert_eq!(state.access_points[0].hw_address, "AA:BB:CC:DD:EE:FF");

// Adding same AP should replace it (in our implementation we remove and push)
let mut ap2 = ap.clone();
ap2.strength = 80;
state.add_access_point(ap2).unwrap();
assert_eq!(state.access_points.len(), 1);
assert_eq!(state.access_points[0].strength, 80);
}

#[test]
fn test_remove_access_point() {
let mut state = NetworkState::default();
let ap = AccessPoint {
hw_address: "AA:BB:CC:DD:EE:FF".to_string(),
..Default::default()
};
state.add_access_point(ap).unwrap();
state.remove_access_point("AA:BB:CC:DD:EE:FF").unwrap();
assert_eq!(state.access_points.len(), 0);
}

#[test]
fn test_remove_unknown_access_point() {
let mut state = NetworkState::default();
let error = state.remove_access_point("unknown").unwrap_err();
assert!(matches!(error, NetworkStateError::UnknownAccessPoint(_)));
}

#[test]
fn test_is_loopback() {
let conn = Connection::new("eth0".to_string(), DeviceType::Ethernet);
Expand Down Expand Up @@ -1726,7 +1810,7 @@ pub struct TunConfig {
#[serde(rename_all = "camelCase")]
pub enum NetworkChange {
ConnectionAdded(Connection),
ConnectionRemoved(String),
ConnectionRemoved(Uuid),
/// A new device has been added.
DeviceAdded(Device),
/// A device has been removed.
Expand All @@ -1737,9 +1821,13 @@ pub enum NetworkChange {
DeviceUpdated(String, Device),
/// A connection state has changed.
ConnectionStateChanged {
id: String,
uuid: Uuid,
state: ConnectionState,
},
/// A new access point has been added.
AccessPointAdded(AccessPoint),
/// An access point has been removed.
AccessPointRemoved(String),
}

#[derive(Default, Debug, PartialEq, Clone, Deserialize, Serialize, utoipa::ToSchema)]
Expand Down
47 changes: 45 additions & 2 deletions rust/agama-network/src/nm/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@ use crate::{
nm::{
dbus::connection_from_dbus,
model::NmDeviceType,
proxies::{ConnectionProxy, DeviceProxy, IP4ConfigProxy, IP6ConfigProxy},
proxies::{AccessPointProxy, ConnectionProxy, DeviceProxy, IP4ConfigProxy, IP6ConfigProxy},
},
types::{
AccessPoint, ConnectionFlags, Device, DeviceState, DeviceType, IpConfig, IpRoute,
MacAddress, SSID,
},
types::{ConnectionFlags, Device, DeviceState, DeviceType, IpConfig, IpRoute, MacAddress},
};
use cidr::IpInet;
use std::{collections::HashMap, net::IpAddr, str::FromStr};
Expand All @@ -44,6 +47,38 @@ pub struct DeviceFromProxyBuilder<'a> {
proxy: &'a DeviceProxy<'a>,
}

/// Builder to create an [AccessPoint] from its corresponding NetworkManager D-Bus representation.
pub struct AccessPointFromProxyBuilder<'a> {
device_name: String,
proxy: &'a AccessPointProxy<'a>,
}

impl<'a> AccessPointFromProxyBuilder<'a> {
pub fn new(device_name: String, proxy: &'a AccessPointProxy<'a>) -> Self {
Self { device_name, proxy }
}

/// Creates an [AccessPoint] starting on the [AccessPointProxy].
pub async fn build(&self) -> Result<AccessPoint, NmError> {
let ssid = SSID(self.proxy.ssid().await?);
let hw_address = self.proxy.hw_address().await?;
let strength = self.proxy.strength().await?;
let flags = self.proxy.flags().await?;
let rsn_flags = self.proxy.rsn_flags().await?;
let wpa_flags = self.proxy.wpa_flags().await?;

Ok(AccessPoint {
device: self.device_name.clone(),
ssid,
hw_address,
strength,
flags,
rsn_flags,
wpa_flags,
})
}
}

impl<'a> ConnectionFromProxyBuilder<'a> {
pub fn new(_connection: &zbus::Connection, proxy: &'a ConnectionProxy<'a>) -> Self {
Self { proxy }
Expand Down Expand Up @@ -139,6 +174,7 @@ impl<'a> DeviceFromProxyBuilder<'a> {
) -> Result<IpConfig, NmError> {
let address_data = ip4_proxy.address_data().await?;
let nameserver_data = ip4_proxy.nameserver_data().await?;
let mut dns_searchlist = ip4_proxy.searches().await?;
let mut addresses: Vec<IpInet> = vec![];
let mut nameservers: Vec<IpAddr> = vec![];

Expand All @@ -160,6 +196,12 @@ impl<'a> DeviceFromProxyBuilder<'a> {
nameservers.push(address)
}
}

for search in ip6_proxy.searches().await? {
if !dns_searchlist.contains(&search) {
dns_searchlist.push(search);
}
}
// FIXME: Convert from Vec<u8> to [u8; 16] and take into account big vs little endian order,
// in IP6Config there is no nameserver-data.
//
Expand Down Expand Up @@ -188,6 +230,7 @@ impl<'a> DeviceFromProxyBuilder<'a> {
let mut ip_config = IpConfig {
addresses,
nameservers,
dns_searchlist,
routes4,
routes6,
..Default::default()
Expand Down
16 changes: 16 additions & 0 deletions rust/agama-network/src/nm/streams/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ pub enum NmChange {
ActiveConnectionAdded(OwnedObjectPath),
ActiveConnectionUpdated(OwnedObjectPath),
ActiveConnectionRemoved(OwnedObjectPath),
AccessPointAdded(OwnedObjectPath, OwnedObjectPath),
AccessPointRemoved(OwnedObjectPath, OwnedObjectPath),
}

pub async fn build_added_and_removed_stream(
Expand Down Expand Up @@ -63,3 +65,17 @@ pub async fn build_properties_changed_stream(
let stream = MessageStream::for_match_rule(rule, connection, Some(1)).await?;
Ok(stream)
}

/// Returns a stream of wireless signals to be used by DeviceChangedStream.
///
/// It listens for AccessPointAdded and AccessPointRemoved signals.
pub async fn build_wireless_signals_stream(
connection: &zbus::Connection,
) -> Result<MessageStream, NmError> {
let rule = MatchRule::builder()
.msg_type(MessageType::Signal)
.interface("org.freedesktop.NetworkManager.Device.Wireless")?
.build();
let stream = MessageStream::for_match_rule(rule, connection, Some(1)).await?;
Ok(stream)
}
Loading
Loading