Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
226 changes: 37 additions & 189 deletions Cargo.lock

Large diffs are not rendered by default.

10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
![CI](https://github.com/cachebag/nmrs/actions/workflows/ci.yml/badge.svg)
# nmrs
#### Wayland-native frontend for NetworkManager. Provides a GTK4 UI and a D-Bus proxy core, built in Rust.
<img width="603" height="718" alt="image" src="https://github.com/user-attachments/assets/67f424b2-4c48-4d87-8ec2-7ae2db090048" />
# nmrs 🦀
<div align="center">
<h3>Wayland-native frontend for NetworkManager. Provides a GTK4 UI and a D-Bus proxy core, built in Rust.</h3>
</div>
<p align="center">
<img width="472" height="598" alt="image" src="https://github.com/user-attachments/assets/c2a46227-df88-4e9e-b3c9-f4c259399785" />
</p>

#

Expand Down
190 changes: 169 additions & 21 deletions nmrs-core/src/dbus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -249,19 +249,22 @@ impl NetworkManager {
);

let nm = NMProxy::new(&self.conn).await?;
let devices = nm.get_devices().await?;

let devices = nm.get_devices().await?;
let mut wifi_device: Option<OwnedObjectPath> = None;

for dp in devices {
let dev = NMDeviceProxy::builder(&self.conn)
.path(dp.clone())?
.build()
.await?;
if dev.device_type().await? == 2 {
wifi_device = Some(dp.clone());
eprintln!(" Found WiFi device: {dp}");
break;
}
}

let wifi_device =
wifi_device.ok_or(zbus::Error::Failure("no Wi-Fi device found".into()))?;

Expand All @@ -271,32 +274,50 @@ impl NetworkManager {
.await?;

if let Some(active) = self.current_ssid().await {
eprintln!("Currently connected to: {active}");
if active == ssid {
eprintln!("Already connected to {active}, skipping connect()");
return Ok(());
} else {
eprintln!("Currently connected to {active}, disconnecting before reconnecting...");
eprintln!("Disconnecting from {active}.");
if let Ok(conns) = nm.active_connections().await {
for conn_path in conns {
eprintln!("Deactivating connection: {conn_path}");
let _ = nm.deactivate_connection(conn_path).await;
}
}

for _ in 0..10 {
for i in 0..10 {
let d = NMDeviceProxy::builder(&self.conn)
.path(wifi_device.clone())?
.build()
.await?;
if DeviceState::from(d.state().await?) == DeviceState::Disconnected {
let state = DeviceState::from(d.state().await?);
eprintln!("Loop {i}: Device state = {state:?}");

if state == DeviceState::Disconnected
|| state == DeviceState::Unavailable
|| state == DeviceState::Deactivating
{
eprintln!("Device disconneced or deactivating");
break;
}
tokio::time::sleep(std::time::Duration::from_millis(300)).await;

Delay::new(Duration::from_millis(300)).await;
}

eprintln!("Disconnect complete");
}
} else {
eprintln!("Not currently connected to any network");
}

let _ = wifi.request_scan(HashMap::new()).await;
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
match wifi.request_scan(HashMap::new()).await {
Ok(_) => eprintln!("Scan requested successfully"),
Err(e) => eprintln!("Scan request FAILED: {e}"),
}
Delay::new(Duration::from_secs(3)).await;
eprintln!("Scan wait complete");

let mut ap_path: Option<OwnedObjectPath> = None;
for ap in wifi.get_all_access_points().await? {
Expand All @@ -306,30 +327,40 @@ impl NetworkManager {
.await?;
let ssid_bytes = apx.ssid().await?;
let ap_ssid = std::str::from_utf8(&ssid_bytes).unwrap_or("");
eprintln!("Found AP: '{ap_ssid}'");
if ap_ssid == ssid {
ap_path = Some(ap.clone());
eprintln!("Matched target SSID");
break;
}
}

if ap_path.is_none() {
eprintln!("Could not find AP for '{ssid}'");
}

let settings = build_wifi_connection(ssid, &creds);

if matches!(creds, crate::models::WifiSecurity::Open) {
println!("Connecting to open network '{ssid}'");
nm.add_and_activate_connection(
settings,
wifi_device.clone(),
ObjectPath::from_str_unchecked("/").into(),
)
.await?;
} else {
let specific_object =
ap_path.unwrap_or_else(|| ObjectPath::from_str_unchecked("/").into());
nm.add_and_activate_connection(settings, wifi_device.clone(), specific_object)
.await?;
let specific_object = ap_path.unwrap_or_else(|| ObjectPath::from_str_unchecked("/").into());

match nm
.add_and_activate_connection(settings, wifi_device.clone(), specific_object)
.await
{
Ok(_) => eprintln!("add_and_activate_connection() succeeded"),
Err(e) => {
eprintln!("add_and_activate_connection() failed: {e}");
return Err(e);
}
}

println!("Connection request for '{ssid}' submitted successfully");
let dev_proxy = NMDeviceProxy::builder(&self.conn)
.path(wifi_device.clone())?
.build()
.await?;
eprintln!("Dev state after connect(): {:?}", dev_proxy.state().await?);
eprintln!("---Connection request for '{ssid}' submitted successfully---");

Ok(())
}

Expand Down Expand Up @@ -547,4 +578,121 @@ impl NetworkManager {
_ => "Unknown",
}
}

pub async fn forget(&self, ssid: &str) -> zbus::Result<()> {
use std::collections::HashMap;
use zvariant::{OwnedObjectPath, Value};

let nm = NMProxy::new(&self.conn).await?;

let devices = nm.get_devices().await?;
for dev_path in &devices {
let dev = NMDeviceProxy::builder(&self.conn)
.path(dev_path.clone())?
.build()
.await?;
if dev.device_type().await? != 2 {
continue;
}

let wifi = NMWirelessProxy::builder(&self.conn)
.path(dev_path.clone())?
.build()
.await?;
if let Ok(ap_path) = wifi.active_access_point().await
&& ap_path.as_str() != "/"
{
let ap = NMAccessPointProxy::builder(&self.conn)
.path(ap_path.clone())?
.build()
.await?;
if let Ok(bytes) = ap.ssid().await
&& std::str::from_utf8(&bytes).ok() == Some(ssid)
{
let dev_proxy: zbus::Proxy<'_> = zbus::proxy::Builder::new(&self.conn)
.destination("org.freedesktop.NetworkManager")?
.path(dev_path.clone())?
.interface("org.freedesktop.NetworkManager.Device")?
.build()
.await?;
let _ = dev_proxy.call_method("Disconnect", &()).await;

for _ in 0..10 {
if dev.state().await? == 30 {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
}
}
}
}

let settings: zbus::Proxy<'_> = zbus::proxy::Builder::new(&self.conn)
.destination("org.freedesktop.NetworkManager")?
.path("/org/freedesktop/NetworkManager/Settings")?
.interface("org.freedesktop.NetworkManager.Settings")?
.build()
.await?;

let list_reply = settings.call_method("ListConnections", &()).await?;
let conns: Vec<OwnedObjectPath> = list_reply.body().deserialize()?;

let mut deleted_any = false;

for cpath in conns {
let cproxy: zbus::Proxy<'_> = zbus::proxy::Builder::new(&self.conn)
.destination("org.freedesktop.NetworkManager")?
.path(cpath.clone())?
.interface("org.freedesktop.NetworkManager.Settings.Connection")?
.build()
.await?;

if let Ok(id) = cproxy.get_property::<String>("Id").await
&& id == ssid
{
let _ = cproxy.call_method("Delete", &()).await;
deleted_any = true;
continue;
}

if let Ok(msg) = cproxy.call_method("GetSettings", &()).await {
let body = msg.body();
let settings_map: HashMap<String, HashMap<String, Value>> = body.deserialize()?;

if let Some(conn_sec) = settings_map.get("connection")
&& let Some(Value::Str(id)) = conn_sec.get("id")
&& id.as_str() == ssid
{
let _ = cproxy.call_method("Delete", &()).await;
deleted_any = true;
continue;
}

if let Some(wifi_sec) = settings_map.get("802-11-wireless")
&& let Some(Value::Array(arr)) = wifi_sec.get("ssid")
{
let mut raw = Vec::new();
for v in arr.iter() {
if let Ok(b) = u8::try_from(v.clone()) {
raw.push(b);
}
}
if std::str::from_utf8(&raw).ok() == Some(ssid) {
let _ = cproxy.call_method("Delete", &()).await;
deleted_any = true;
continue;
}
}
}
}

if deleted_any {
eprintln!("Forgot and disconnected '{ssid}'");
Ok(())
} else {
Err(zbus::Error::Failure(format!(
"No saved connection for {ssid}"
)))
}
}
}
7 changes: 7 additions & 0 deletions nmrs-core/src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub struct Network {
pub is_eap: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkInfo {
pub ssid: String,
pub bssid: String,
Expand Down Expand Up @@ -80,6 +81,8 @@ pub enum DeviceState {
Prepare,
Config,
Activated,
Deactivating,
Failed,
Other(u32),
}

Expand Down Expand Up @@ -114,6 +117,8 @@ impl From<u32> for DeviceState {
40 => DeviceState::Prepare,
50 => DeviceState::Config,
100 => DeviceState::Activated,
110 => DeviceState::Deactivating,
120 => DeviceState::Failed,
v => DeviceState::Other(v),
}
}
Expand All @@ -140,6 +145,8 @@ impl Display for DeviceState {
DeviceState::Prepare => write!(f, "Preparing"),
DeviceState::Config => write!(f, "Configuring"),
DeviceState::Activated => write!(f, "Activated"),
DeviceState::Deactivating => write!(f, "Deactivating"),
DeviceState::Failed => write!(f, "Failed"),
DeviceState::Other(v) => write!(f, "Other({v})"),
}
}
Expand Down
15 changes: 14 additions & 1 deletion nmrs-core/src/wifi_builders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,23 +117,36 @@ pub fn build_wifi_connection(
s_conn.insert("type", Value::from("802-11-wireless"));
s_conn.insert("id", Value::from(ssid.to_string()));
s_conn.insert("uuid", Value::from(uuid::Uuid::new_v4().to_string()));
s_conn.insert("autoconnect", Value::from(true));
s_conn.insert("interface-name", Value::from("wlan0"));
conn.insert("connection", s_conn);

let mut s_wifi = HashMap::new();
s_wifi.insert("ssid", Value::from(ssid.as_bytes().to_vec()));
s_wifi.insert("mode", Value::from("infrastructure"));
conn.insert("802-11-wireless", s_wifi);

match security {
models::WifiSecurity::Open => {}
models::WifiSecurity::WpaPsk { psk } => {
s_wifi.insert("security", Value::from("802-11-wireless-security"));
let mut s_sec = HashMap::new();
s_sec.insert("key-mgmt", Value::from("wpa-psk"));
s_sec.insert("auth-alg", Value::from("open"));
s_sec.insert("psk", Value::from(psk.to_string()));
conn.insert("802-11-wireless-security", s_sec);
}
_ => {}
}

conn.insert("802-11-wireless", s_wifi);

let mut ipv4 = HashMap::new();
ipv4.insert("method", Value::from("auto"));
conn.insert("ipv4", ipv4);

let mut ipv6 = HashMap::new();
ipv6.insert("method", Value::from("auto"));
conn.insert("ipv6", ipv6);

conn
}
16 changes: 16 additions & 0 deletions nmrs-ui/src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,19 @@ label.network-poor { color: #ef4444; }
.wifi-open {
color: white;
}

.loading-spinner {
margin-top: 12px;
margin-bottom: 12px;
opacity: 0.6;
}

.forget-button {
font-size: 0.85em;
opacity: 0.7;
padding: 2px 6px;
border-radius: 6px;
}
.forget-button:hover {
opacity: 1;
}
Loading