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
3 changes: 2 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,6 @@ fs2 = "0.4.3"
anyhow = "1.0.102"
clap = { version = "4.6.1", features = ["derive"] }
async-trait = "0.1.89"
bitflags = "2"
tokio = { version = "1.52.1", features = ["rt-multi-thread", "macros", "sync", "time"] }
tokio-util = { version = "0.7.18" }
7 changes: 4 additions & 3 deletions nmrs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
All notable changes to the `nmrs` crate will be documented in this file.

## [Unreleased]
### Changed
- Introduce `VpnConfig` trait and refactor `connect_vpn` signature ([#303](https://github.com/cachebag/nmrs/pull/303))

### Added
- `nmrs::agent` module: NetworkManager secret agent for credential prompting over D-Bus (`SecretAgent`, `SecretAgentBuilder`, `SecretAgentHandle`, `SecretRequest`, `SecretResponder`, `SecretSetting`, `SecretAgentFlags`, `SecretAgentCapabilities`, `CancelReason`, `SecretStoreEvent`)
- `VpnConfig` trait and `WireGuardConfig`; `NetworkManager::connect_vpn` accepts `VpnConfig` implementors; `VpnCredentials` deprecated with compatibility bridges ([#303](https://github.com/cachebag/nmrs/pull/303))

### Changed
- Introduce `VpnConfig` trait and refactor `connect_vpn` signature ([#303](https://github.com/cachebag/nmrs/pull/303))
- OpenVPN connection settings model expansion ([#309](https://github.com/cachebag/nmrs/pull/309))
- Multi-VPN plumbing: `detect_vpn_type()`, `VpnType::OpenVpn`, and shared detection across connect, disconnect, and list VPN flows ([#311](https://github.com/cachebag/nmrs/pull/311))
- `.ovpn` profile lexer and parser for translating OpenVPN configs toward NetworkManager ([#314](https://github.com/cachebag/nmrs/pull/314))
Expand Down
5 changes: 5 additions & 0 deletions nmrs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ futures-timer.workspace = true
base64.workspace = true
tokio.workspace = true
async-trait.workspace = true
bitflags.workspace = true

[package.metadata.docs.rs]
all-features = true
Expand All @@ -42,3 +43,7 @@ path = "examples/wifi_scan.rs"
[[example]]
name = "vpn_connect"
path = "examples/vpn_connect.rs"

[[example]]
name = "secret_agent"
path = "examples/secret_agent.rs"
31 changes: 31 additions & 0 deletions nmrs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Rust bindings for NetworkManager via D-Bus.
- **Network Discovery**: Scan and list available access points with signal strength
- **Profile Management**: Create, query, and delete saved connection profiles
- **Real-Time Monitoring**: Signal-based network and device state change notifications
- **Secret Agent**: Respond to NetworkManager credential prompts via an async stream API
- **Typed Errors**: Structured error types with specific failure reasons
- **Fully Async**: Built on `zbus` with async/await throughout

Expand Down Expand Up @@ -157,6 +158,36 @@ async fn main() -> nmrs::Result<()> {
}
```

### Secret Agent

Register as a NetworkManager secret agent to handle credential prompts
(Wi-Fi passwords, VPN tokens, 802.1X credentials):

```rust
use futures::StreamExt;
use nmrs::agent::{SecretAgent, SecretAgentFlags, SecretSetting};

#[tokio::main]
async fn main() -> nmrs::Result<()> {
let (handle, mut requests) = SecretAgent::builder()
.with_identifier("com.example.my_app")
.register()
.await?;

while let Some(req) = requests.next().await {
if let SecretSetting::WifiPsk { ref ssid } = req.setting {
println!("Password needed for {ssid}");
req.responder.wifi_psk("my-password").await?;
} else {
req.responder.cancel().await?;
}
}

handle.unregister().await?;
Ok(())
}
```

### Real-Time Monitoring

```rust
Expand Down
66 changes: 66 additions & 0 deletions nmrs/examples/secret_agent.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/// Registers a NetworkManager secret agent, prints incoming requests,
/// and responds to Wi-Fi PSK prompts by reading a password from stdin.
///
/// Run with:
///
/// ```sh
/// cargo run --example secret_agent
/// ```
///
/// Then trigger a password prompt (e.g. forget a saved Wi-Fi password and
/// reconnect). The agent will print the request and ask for input.
use std::io::{self, BufRead, Write};

use futures::StreamExt;
use nmrs::agent::{SecretAgent, SecretAgentFlags, SecretSetting};

#[tokio::main]
async fn main() -> nmrs::Result<()> {
let (handle, mut requests) = SecretAgent::builder()
.with_identifier("com.system76.nmrs.example.secret_agent")
.register()
.await?;

println!("Secret agent registered. Waiting for requests…");
println!("(The agent will exit after processing one request)\n");

if let Some(req) = requests.next().await {
println!("── Secret request ──");
println!(" UUID: {}", req.connection_uuid);
println!(" Name: {}", req.connection_id);
println!(" Type: {}", req.connection_type);
println!(" Setting: {:?}", req.setting);
println!(" Hints: {:?}", req.hints);
println!(" Flags: {:?}", req.flags);

if !req.flags.contains(SecretAgentFlags::ALLOW_INTERACTION) {
println!(" → interaction not allowed, cancelling");
req.responder.cancel().await?;
} else {
match req.setting {
SecretSetting::WifiPsk { ref ssid } => {
print!(" Enter password for \"{ssid}\": ");
io::stdout().flush().expect("flush stdout");
let mut line = String::new();
io::stdin().lock().read_line(&mut line).expect("read stdin");
let psk = line.trim();
if psk.is_empty() {
println!(" → empty input, cancelling");
req.responder.cancel().await?;
} else {
req.responder.wifi_psk(psk).await?;
println!(" → sent PSK");
}
}
_ => {
println!(" → unsupported setting type, cancelling");
req.responder.cancel().await?;
}
}
}
}

handle.unregister().await?;
println!("Agent unregistered.");
Ok(())
}
Loading