Introduction
Welcome to the nmrs documentation! This guide will help you understand and use nmrs, a powerful Rust library for managing network connections on Linux via NetworkManager.
What is nmrs?
nmrs is a high-level, async Rust API for NetworkManager over D-Bus. It provides:
- Simple WiFi Management - Scan, connect, and manage wireless networks
- VPN Support - WireGuard and OpenVPN VPN support
- Ethernet Control - Manage wired network connections
- Bluetooth - Connect to Bluetooth network devices
- Real-Time Monitoring - Event-driven network state updates
- Type Safety - Comprehensive error handling with specific failure reasons
- Async/Await - Built on modern async Rust with runtime flexibility
Why nmrs?
- Safe Abstractions - No unsafe code, leveraging Rust's type system
- Async-First - Built for modern async Rust applications
- Signal-Based - Efficient D-Bus signal monitoring instead of polling
- Well-Documented - Comprehensive docs with examples for every feature
- Runtime Agnostic - Works with Tokio, async-std, smol, and more
Quick Example
Here's a taste of what nmrs can do:
use nmrs::{NetworkManager, WifiSecurity}; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; // Scan for networks let networks = nm.list_networks(None).await?; for net in networks { println!("{} - {}%", net.ssid, net.strength.unwrap_or(0)); } // Connect to a network nm.connect("MyWiFi", None, WifiSecurity::WpaPsk { psk: "password123".into() }).await?; Ok(()) }
Community
- Discord: Join our Discord server to chat with developers and users
- GitHub: Report issues, contribute, or browse the code at github.com/cachebag/nmrs
- Crates.io: Install from crates.io/crates/nmrs
- API Docs: Full API reference at docs.rs/nmrs
License
nmrs is dual-licensed under MIT and Apache 2.0, giving you flexibility in how you use it.
Ready to get started? Head to the Installation guide!
Installation
This guide covers installation for the nmrs library.
Using Cargo
The easiest way to add nmrs to your project:
cargo add nmrs
Or manually add to your Cargo.toml:
[dependencies]
nmrs = "2.0.0"
From Source
Clone and build from source:
git clone https://github.com/cachebag/nmrs.git
cd nmrs/nmrs
cargo build --release
Verify Installation
Create a simple test to verify nmrs is working:
use nmrs::NetworkManager; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; println!("nmrs is working!"); Ok(()) }
System Requirements
- Operating System: Linux (any modern distribution)
- Rust: 1.90.0 or later
- NetworkManager: Version 1.0 or later, running and accessible via D-Bus
- D-Bus: System bus must be available
Permissions
nmrs requires permission to manage network connections. On most systems, this is handled by PolicyKit. Ensure your user is in the appropriate groups:
# Check if you're in the network group
groups
# Add yourself to the network group if needed (requires logout/login)
sudo usermod -aG network $USER
Verify NetworkManager
Ensure NetworkManager is running:
systemctl status NetworkManager
If it's not running:
sudo systemctl start NetworkManager
sudo systemctl enable NetworkManager # Start on boot
Next Steps
- Continue to the Quick Start guide
- Having issues? Check Troubleshooting
Quick Start
This guide will get you up and running with nmrs in minutes.
Prerequisites
Make sure you have:
- Rust installed (1.78.0+)
- NetworkManager running on your Linux system
- Basic familiarity with async Rust
Create a New Project
cargo new nmrs-demo
cd nmrs-demo
cargo add nmrs tokio --features tokio/full
Your First nmrs Program
Let's create a simple program that lists available WiFi networks:
use nmrs::NetworkManager; #[tokio::main] async fn main() -> nmrs::Result<()> { // Initialize NetworkManager connection let nm = NetworkManager::new().await?; // List all available networks let networks = nm.list_networks(None).await?; // Print network information for network in networks { println!( "SSID: {:<20} Signal: {:>3}% Security: {:?}", network.ssid, network.strength.unwrap_or(0), network.security ); } Ok(()) }
Run it:
cargo run
You should see output like:
SSID: MyHomeNetwork Signal: 85% Security: WpaPsk
SSID: CoffeeShopWiFi Signal: 62% Security: Open
SSID: Neighbor5G Signal: 45% Security: WpaEap
Connecting to a Network
Now let's connect to a WiFi network:
use nmrs::{NetworkManager, WifiSecurity}; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; // Connect to a WPA-PSK protected network nm.connect("MyHomeNetwork", None, WifiSecurity::WpaPsk { psk: "your_password_here".into() }).await?; println!("Connected successfully!"); // Verify the connection if let Some(ssid) = nm.current_ssid().await { println!("Current network: {}", ssid); } Ok(()) }
Error Handling
nmrs provides detailed error types for better error handling:
use nmrs::{NetworkManager, WifiSecurity, ConnectionError}; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; match nm.connect("MyNetwork", None, WifiSecurity::WpaPsk { psk: "password123".into() }).await { Ok(_) => println!("✓ Connected successfully"), Err(ConnectionError::AuthFailed) => { eprintln!("✗ Authentication failed - wrong password?"); } Err(ConnectionError::NotFound) => { eprintln!("✗ Network not found or out of range"); } Err(ConnectionError::Timeout) => { eprintln!("✗ Connection timed out"); } Err(ConnectionError::DhcpFailed) => { eprintln!("✗ Failed to obtain IP address"); } Err(e) => eprintln!("✗ Error: {}", e), } Ok(()) }
Device Management
List all network devices:
use nmrs::NetworkManager; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; let devices = nm.list_devices().await?; for device in devices { println!( "Interface: {:<10} Type: {:<10} State: {:?}", device.interface, device.device_type, device.state ); } Ok(()) }
Working with Connection Profiles
List saved connection profiles:
use nmrs::NetworkManager; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; let profiles = nm.list_saved_connection_ids().await?; println!("Saved connections:"); for profile in profiles { println!(" - {}", profile); } Ok(()) }
Real-Time Monitoring
Monitor network changes:
use nmrs::NetworkManager; use std::sync::Arc; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = Arc::new(NetworkManager::new().await?); let nm_clone = nm.clone(); // Monitor network changes nm.monitor_network_changes(move || { println!("Networks changed! Scanning..."); // In a real app, you'd update your UI here }).await?; // Keep the program running tokio::signal::ctrl_c().await.ok(); Ok(()) }
Complete Example: Network Scanner
Here's a complete example that puts it all together:
use nmrs::{NetworkManager, WifiSecurity}; use std::io::{self, Write}; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; println!("Scanning for networks...\n"); let networks = nm.list_networks(None).await?; // Display networks with numbering for (i, net) in networks.iter().enumerate() { println!( "{:2}. {:<25} Signal: {:>3}% {:?}", i + 1, net.ssid, net.strength.unwrap_or(0), net.security ); } // Get user input print!("\nEnter network number to connect (or 0 to exit): "); io::stdout().flush().unwrap(); let mut input = String::new(); io::stdin().read_line(&mut input).unwrap(); let choice: usize = input.trim().parse().unwrap_or(0); if choice == 0 || choice > networks.len() { println!("Exiting..."); return Ok(()); } let selected = &networks[choice - 1]; // Ask for password if needed let security = match selected.security { nmrs::models::WifiSecurity::Open => WifiSecurity::Open, _ => { print!("Enter password: "); io::stdout().flush().unwrap(); let mut password = String::new(); io::stdin().read_line(&mut password).unwrap(); WifiSecurity::WpaPsk { psk: password.trim().to_string() } } }; // Connect println!("Connecting to {}...", selected.ssid); nm.connect(&selected.ssid, None, security).await?; println!("✓ Connected successfully!"); Ok(()) }
Next Steps
Now that you've got the basics, explore more features:
- WiFi Management - Advanced WiFi features
- VPN Connections - Set up WireGuard VPNs
- Device Management - Control network devices
- Error Handling - Comprehensive error handling
- Examples - More complete examples
Using Different Async Runtimes
nmrs works with any async runtime. Here are examples with popular runtimes:
async-std
[dependencies]
nmrs = "2.0.0"
async-std = { version = "1.12", features = ["attributes"] }
#[async_std::main] async fn main() -> nmrs::Result<()> { let nm = nmrs::NetworkManager::new().await?; // ... your code Ok(()) }
smol
[dependencies]
nmrs = "2.0.0"
smol = "2.0"
fn main() -> nmrs::Result<()> { smol::block_on(async { let nm = nmrs::NetworkManager::new().await?; // ... your code Ok(()) }) }
Requirements
This page details all the requirements needed to use nmrs effectively.
System Requirements
Operating System
nmrs is Linux-only and requires:
- Any modern Linux distribution (kernel 3.10+)
- NetworkManager 1.0 or later
- D-Bus system bus
Tested on:
- Arch Linux
- Ubuntu 20.04+
- Fedora 35+
- Debian 11+
- NixOS
NetworkManager
NetworkManager must be:
- Installed on your system
- Running and accessible via D-Bus
- Version 1.0 or later (1.46+ recommended for latest features)
Check your NetworkManager version:
NetworkManager --version
Ensure it's running:
systemctl status NetworkManager
D-Bus
The D-Bus system bus must be available and running. This is standard on all modern Linux distributions.
Verify D-Bus is working:
dbus-send --system --print-reply \
--dest=org.freedesktop.NetworkManager \
/org/freedesktop/NetworkManager \
org.freedesktop.DBus.Properties.Get \
string:'org.freedesktop.NetworkManager' \
string:'Version'
Rust Requirements
- Rust: 1.90.0 or later
- Edition: 2024
The library uses stable Rust features only.
Dependencies
nmrs Library Dependencies
The library depends on:
zbus5.x - D-Bus communicationtokioor another async runtimeserde- Serializationthiserror- Error handlingfutures- Async utilities
All dependencies are automatically handled by Cargo.
Permissions
PolicyKit
nmrs needs permission to manage network connections. This is typically handled by PolicyKit on modern Linux systems.
User Groups
Your user should be in the appropriate groups. On most systems:
# Check current groups
groups
# Add to network group (may vary by distribution)
sudo usermod -aG network $USER
On some distributions, no special group is needed if PolicyKit is properly configured.
Running as Root
While nmrs can run as root, it's not recommended for security reasons. Use PolicyKit instead.
Hardware Requirements
WiFi
- A WiFi adapter supported by NetworkManager
- WiFi hardware must be recognized by the Linux kernel
Check your WiFi adapter:
nmcli device status
Ethernet
- Network interface card (NIC)
- Recognized by the Linux kernel
Bluetooth
For Bluetooth network features:
- Bluetooth adapter
- BlueZ stack installed and running
VPN
For WireGuard VPN:
- WireGuard kernel module or userspace implementation
- WireGuard tools (usually bundled with NetworkManager)
Check WireGuard support:
modprobe wireguard
lsmod | grep wireguard
Or use userspace implementation (automatic with NetworkManager 1.16+).
Development Requirements
Additional requirements for developing nmrs:
Testing
dockeranddocker-compose(for containerized testing)- WiFi hardware or
mac80211_hwsimkernel module
Building Documentation
mdbookfor this documentationcargo-docfor API documentation
IDE/Editor
Recommended:
- rust-analyzer
- clippy
- rustfmt
Optional Dependencies
Logging
For detailed logging, use any logger that implements the log facade:
[dependencies]
env_logger = "0.11"
TLS/Certificates
For WPA-EAP with certificate validation:
- CA certificates installed in system certificate store
- OpenSSL or rustls (handled by NetworkManager)
Troubleshooting
NetworkManager Not Found
If you get "Failed to connect to D-Bus":
# Check if NetworkManager is running
systemctl status NetworkManager
# Start it if needed
sudo systemctl start NetworkManager
# Enable it to start on boot
sudo systemctl enable NetworkManager
Permission Denied
If you get permission errors:
- Check PolicyKit rules:
/usr/share/polkit-1/actions/org.freedesktop.NetworkManager.policy - Ensure D-Bus is accessible:
ls -l /var/run/dbus/system_bus_socket - Try with PolicyKit agent running
Dependency Issues
For build issues:
# Update Rust
rustup update stable
# Clear Cargo cache
cargo clean
# Update dependencies
cargo update
Version Compatibility
| nmrs Version | Minimum Rust | NetworkManager | Notable Features |
|---|---|---|---|
| 3.0.0 | 1.90.0 | 1.0+ | Edition 2024 |
| 2.0.0 | 1.78.0 | 1.0+ | Full API rewrite |
| 1.x | 1.70.0 | 1.0+ | Initial release |
Next Steps
Once you have all requirements met:
- Install nmrs
- Follow the Quick Start guide
- Start building with WiFi Management
If you encounter issues, see Troubleshooting.
WiFi Management
nmrs provides comprehensive WiFi management capabilities through the NetworkManager API. This chapter covers all WiFi-related operations.
Overview
WiFi management in nmrs includes:
- Network Discovery - Scan for available access points
- Connection Management - Connect, disconnect, and monitor connections
- Security Support - Open, WPA-PSK, WPA-EAP/Enterprise
- Signal Monitoring - Real-time signal strength updates
- Profile Management - Save and manage connection profiles
- Advanced Features - Hidden networks, custom DNS, static IP
Quick Reference
use nmrs::{NetworkManager, WifiSecurity}; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; // Scan for networks let networks = nm.list_networks(None).await?; // Connect to WPA-PSK network nm.connect("MyWiFi", None, WifiSecurity::WpaPsk { psk: "password".into() }).await?; // Get current connection if let Some(ssid) = nm.current_ssid().await { println!("Connected to: {}", ssid); } // Disconnect nm.disconnect(None).await?; Ok(()) }
Security Types
nmrs supports all major WiFi security protocols:
Open Networks
No authentication required:
#![allow(unused)] fn main() { nm.connect("FreeWiFi", None, WifiSecurity::Open).await?; }
WPA-PSK (Personal)
Password-based authentication:
#![allow(unused)] fn main() { nm.connect("HomeWiFi", None, WifiSecurity::WpaPsk { psk: "your_password".into() }).await?; }
WPA-EAP (Enterprise)
802.1X authentication with various methods:
#![allow(unused)] fn main() { use nmrs::{WifiSecurity, EapOptions, EapMethod, Phase2}; let eap_opts = EapOptions::new("user@company.com", "password") .with_method(EapMethod::Peap) .with_phase2(Phase2::Mschapv2) .with_domain_suffix_match("company.com"); nm.connect("CorpWiFi", None, WifiSecurity::WpaEap { opts: eap_opts }).await?; }
Network Information
The Network struct contains detailed information about discovered networks:
#![allow(unused)] fn main() { pub struct Network { pub ssid: String, // Network name pub strength: Option<u8>, // Signal strength (0-100) pub security: WifiSecurity, // Security type pub frequency: Option<u32>, // Frequency in MHz pub hwaddress: Option<String>, // BSSID/MAC address } }
Example usage:
#![allow(unused)] fn main() { let networks = nm.list_networks(None).await?; for net in networks { println!("SSID: {}", net.ssid); if let Some(strength) = net.strength { println!(" Signal: {}%", strength); if strength > 70 { println!(" Quality: Excellent"); } else if strength > 50 { println!(" Quality: Good"); } else { println!(" Quality: Weak"); } } if let Some(freq) = net.frequency { let band = if freq > 5000 { "5GHz" } else { "2.4GHz" }; println!(" Band: {}", band); } } }
Connection Options
Customize connection behavior with ConnectionOptions:
#![allow(unused)] fn main() { use nmrs::{NetworkManager, WifiSecurity, ConnectionOptions}; let opts = ConnectionOptions::new(true) // autoconnect .with_priority(10) // higher = preferred .with_ipv4_method("auto") // DHCP .with_dns(vec!["1.1.1.1".into(), "8.8.8.8".into()]); // Note: Advanced connection options require using builders directly // See the Advanced Topics section for details }
WiFi Radio Control
Enable or disable WiFi hardware:
#![allow(unused)] fn main() { // Disable WiFi (airplane mode) nm.set_wireless_enabled(false).await?; // Enable WiFi nm.set_wireless_enabled(true).await?; // Check WiFi status let state = nm.wifi_state().await?; println!("WiFi is {}", if state.enabled { "enabled" } else { "disabled" }); println!("Hardware switch is {}", if state.hardware_enabled { "on" } else { "off" }); }
Network Scanning
Trigger a fresh scan:
#![allow(unused)] fn main() { // Request a scan (may take a few seconds) nm.scan_networks(None).await?; // Wait a moment for scan to complete tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; // Get updated results let networks = nm.list_networks(None).await?; }
Detecting Connection State
Check your current WiFi status:
#![allow(unused)] fn main() { // Get current SSID if let Some(ssid) = nm.current_ssid().await { println!("Connected to: {}", ssid); } else { println!("Not connected"); } // Get detailed network info if let Some(network) = nm.current_network().await? { println!("SSID: {}", network.ssid); println!("Signal: {}%", network.strength.unwrap_or(0)); } }
Error Handling
WiFi operations can fail for various reasons. Handle them gracefully:
#![allow(unused)] fn main() { use nmrs::ConnectionError; match nm.connect("Network", None, WifiSecurity::WpaPsk { psk: "pass".into() }).await { Ok(_) => println!("Connected!"), Err(ConnectionError::AuthFailed) => { eprintln!("Wrong password"); } Err(ConnectionError::NotFound) => { eprintln!("Network not found - out of range?"); } Err(ConnectionError::Timeout) => { eprintln!("Connection timed out"); } Err(ConnectionError::DhcpFailed) => { eprintln!("Failed to get IP address"); } Err(ConnectionError::NoSecrets) => { eprintln!("Missing password or credentials"); } Err(e) => eprintln!("Error: {}", e), } }
Real-Time Updates
Monitor WiFi networks in real-time:
#![allow(unused)] fn main() { use std::sync::Arc; let nm = Arc::new(NetworkManager::new().await?); let nm_clone = nm.clone(); nm.monitor_network_changes(move || { println!("Network list changed!"); // In a GUI app, you'd trigger a UI refresh here }).await?; // Monitor device state (connection/disconnection) nm.monitor_device_changes(|| { println!("Device state changed!"); }).await?; }
Related Guides
- Scanning Networks - Detailed scanning guide
- Connecting to Networks - Connection details
- WPA-PSK Networks - Password-protected WiFi
- WPA-EAP (Enterprise) - Enterprise WiFi
- Hidden Networks - Connecting to hidden SSIDs
- Error Handling - Comprehensive error guide
- Per-Device Scoping - Multi-radio, per-interface operations
Best Practices
1. Cache the NetworkManager Instance
#![allow(unused)] fn main() { // Good - reuse the same instance let nm = NetworkManager::new().await?; nm.list_networks(None).await?; nm.connect("WiFi", None, WifiSecurity::Open).await?; // Avoid - creating multiple instances let nm1 = NetworkManager::new().await?; nm1.list_networks(None).await?; let nm2 = NetworkManager::new().await?; // Unnecessary nm2.connect("WiFi", None, WifiSecurity::Open).await?; }
2. Handle Signal Strength
#![allow(unused)] fn main() { // Always check for None if let Some(strength) = network.strength { println!("Signal: {}%", strength); } else { println!("Signal: Unknown"); } }
3. Use Timeouts
#![allow(unused)] fn main() { use tokio::time::{timeout, Duration}; // Wrap operations in timeouts match timeout(Duration::from_secs(30), nm.connect("WiFi", None, security)).await { Ok(Ok(_)) => println!("Connected"), Ok(Err(e)) => eprintln!("Connection failed: {}", e), Err(_) => eprintln!("Operation timed out"), } }
4. Monitor for Disconnections
#![allow(unused)] fn main() { // Keep monitoring in the background tokio::spawn(async move { loop { if nm.current_ssid().await.is_none() { eprintln!("Disconnected!"); // Attempt reconnection logic } tokio::time::sleep(Duration::from_secs(5)).await; } }); }
Next Steps
- Learn about VPN Connections
- Explore Device Management
- See complete Examples
Scanning Networks
nmrs provides two approaches to discovering Wi-Fi networks: triggering an active scan and listing cached results.
Triggering a Scan
scan_networks() instructs all wireless devices to perform an active 802.11 probe scan. This sends probe requests on each channel and waits for responses from nearby access points.
use nmrs::NetworkManager; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; // Trigger an active scan on all wireless devices nm.scan_networks(None).await?; Ok(()) }
Note: Scanning is asynchronous at the hardware level. After
scan_networks()returns, NetworkManager continues to receive beacon frames and probe responses for a short period. You may want to add a brief delay before listing networks if you need the freshest results.
Listing Networks
list_networks() returns all Wi-Fi networks currently known to NetworkManager. This includes results from the most recent scan as well as networks that NetworkManager has cached from prior scans.
use nmrs::NetworkManager; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; let networks = nm.list_networks(None).await?; for net in &networks { println!("{:30} {}%", net.ssid, net.strength.unwrap_or(0)); } Ok(()) }
The Network Struct
Each discovered network is represented by the Network struct:
| Field | Type | Description |
|---|---|---|
device | String | Interface name (e.g., "wlan0") |
ssid | String | Network name |
bssid | Option<String> | Access point MAC address |
strength | Option<u8> | Signal strength (0–100) |
frequency | Option<u32> | Frequency in MHz |
secured | bool | Whether the network requires authentication |
is_psk | bool | WPA-PSK (password) authentication |
is_eap | bool | WPA-EAP (enterprise) authentication |
ip4_address | Option<String> | IPv4 address if connected |
ip6_address | Option<String> | IPv6 address if connected |
Getting Detailed Information
For richer details about a specific network, use show_details():
use nmrs::NetworkManager; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; let networks = nm.list_networks(None).await?; if let Some(network) = networks.first() { let info = nm.show_details(network).await?; println!("SSID: {}", info.ssid); println!("BSSID: {}", info.bssid); println!("Signal: {} {}", info.strength, info.bars); println!("Frequency: {:?} MHz", info.freq); println!("Channel: {:?}", info.channel); println!("Mode: {}", info.mode); println!("Speed: {:?} Mbps", info.rate_mbps); println!("Security: {}", info.security); println!("Status: {}", info.status); } Ok(()) }
The NetworkInfo struct returned by show_details() includes:
- bars – a visual signal-strength indicator (e.g.,
"▂▄▆█") - channel – the Wi-Fi channel number derived from frequency
- rate_mbps – link speed when connected
- security – human-readable security description
- status – connection status string
Scan + List Pattern
The most common pattern is to trigger a scan, then list the results:
use nmrs::NetworkManager; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; nm.scan_networks(None).await?; let networks = nm.list_networks(None).await?; for net in &networks { let security = if net.is_eap { "EAP" } else if net.is_psk { "PSK" } else { "Open" }; let band = match net.frequency { Some(f) if f > 5900 => "6 GHz", Some(f) if f > 5000 => "5 GHz", Some(_) => "2.4 GHz", None => "?", }; println!( "{:30} {:>3}% {:>7} {}", net.ssid, net.strength.unwrap_or(0), band, security, ); } Ok(()) }
Network Deduplication
When multiple access points broadcast the same SSID (common in mesh or enterprise deployments), nmrs merges them into a single Network entry. The entry retains the strongest signal, while security flags are combined with a logical OR. This means a single SSID entry might show both is_psk and is_eap as true if different APs advertise different capabilities.
Next Steps
- Connecting to Networks – use scan results to connect
- WPA-PSK Networks – password-based authentication
- WPA-EAP (Enterprise) – 802.1X authentication
Connecting to Networks
This page covers the general flow for connecting to Wi-Fi networks with nmrs. For security-specific details, see the dedicated pages on WPA-PSK, WPA-EAP, and Hidden Networks.
Basic Connection Flow
Connecting to a Wi-Fi network requires two things: the SSID and the security credentials.
use nmrs::{NetworkManager, WifiSecurity}; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; // Open network (no password) nm.connect("CafeWiFi", None, WifiSecurity::Open).await?; // WPA-PSK network (password) nm.connect("HomeWiFi", None, WifiSecurity::WpaPsk { psk: "my_password".into(), }).await?; Ok(()) }
What Happens During Connect
When you call connect(), nmrs performs the following steps:
- Validates the SSID and credentials
- Searches for the network among visible access points
- Checks for a saved connection profile matching the SSID
- Creates a new connection profile if none exists, or reuses the saved one
- Activates the connection via NetworkManager
- Waits for the device to reach the
Activatedstate - Returns
Ok(())on success, or a specific error on failure
The entire process respects the configured timeout. The default connection timeout is 30 seconds.
Checking Connection State
Current Network
#![allow(unused)] fn main() { let nm = NetworkManager::new().await?; // Get the full Network object if let Some(network) = nm.current_network().await? { println!("Connected to: {} ({}%)", network.ssid, network.strength.unwrap_or(0), ); } // Or just the SSID if let Some(ssid) = nm.current_ssid().await { println!("SSID: {}", ssid); } // SSID + frequency if let Some((ssid, freq)) = nm.current_connection_info().await { println!("Connected to {} at {:?} MHz", ssid, freq); } }
Check If Connected to a Specific Network
#![allow(unused)] fn main() { if nm.is_connected("HomeWiFi").await? { println!("Already connected to HomeWiFi"); } }
Check If a Connection Is In Progress
Before starting a new connection, check if one is already underway. Concurrent connection attempts are not supported and may cause undefined behavior.
#![allow(unused)] fn main() { if nm.is_connecting().await? { eprintln!("A connection is already in progress"); return Ok(()); } nm.connect("HomeWiFi", None, WifiSecurity::WpaPsk { psk: "password".into(), }).await?; }
Disconnecting
#![allow(unused)] fn main() { let nm = NetworkManager::new().await?; // Disconnect from the current Wi-Fi network nm.disconnect(None).await?; }
disconnect() deactivates the current wireless connection and waits for the device to reach the Disconnected state. If no connection is active, it returns Ok(()).
Saved Connections
When nmrs connects to a network, NetworkManager saves a connection profile. On subsequent connections to the same SSID, the saved profile is reused automatically.
#![allow(unused)] fn main() { let nm = NetworkManager::new().await?; // Check if a profile exists if nm.has_saved_connection("HomeWiFi").await? { println!("Profile exists — will reconnect without needing credentials"); } // Connect using saved profile (WifiSecurity value is ignored if profile exists) nm.connect("HomeWiFi", None, WifiSecurity::Open).await?; }
See Connection Profiles for more on managing saved connections.
Error Handling
connect() returns specific error variants for different failure modes:
#![allow(unused)] fn main() { use nmrs::{NetworkManager, WifiSecurity, ConnectionError}; let nm = NetworkManager::new().await?; match nm.connect("MyNetwork", None, WifiSecurity::WpaPsk { psk: "password".into(), }).await { Ok(_) => println!("Connected!"), Err(ConnectionError::NotFound) => { eprintln!("Network not visible — is it in range?"); } Err(ConnectionError::AuthFailed) => { eprintln!("Wrong password"); } Err(ConnectionError::Timeout) => { eprintln!("Connection timed out — try increasing the timeout"); } Err(ConnectionError::DhcpFailed) => { eprintln!("Failed to get an IP address"); } Err(e) => eprintln!("Connection failed: {}", e), } }
See Error Handling for a full reference of error types.
Next Steps
- WPA-PSK Networks – password-protected home/office networks
- WPA-EAP (Enterprise) – corporate/university 802.1X networks
- Hidden Networks – connecting to non-broadcast SSIDs
- Error Handling – comprehensive error handling guide
WPA-PSK Networks
WPA-PSK (Wi-Fi Protected Access with Pre-Shared Key) is the most common security type for home and small-office Wi-Fi networks. You provide a password, and nmrs handles the WPA handshake.
Connecting with a Password
use nmrs::{NetworkManager, WifiSecurity}; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; nm.connect("HomeWiFi", None, WifiSecurity::WpaPsk { psk: "my_secure_password".into(), }).await?; println!("Connected!"); Ok(()) }
The WifiSecurity::WpaPsk variant works with WPA, WPA2, and WPA3 Personal networks. NetworkManager negotiates the strongest supported protocol automatically.
Password Requirements
- Must not be empty —
ConnectionError::MissingPasswordis returned for empty strings - WPA-PSK passwords are typically 8–63 characters (ASCII passphrase) or exactly 64 hex characters (raw PSK)
- nmrs passes the password directly to NetworkManager, which handles validation
Reading the Password at Runtime
Avoid hardcoding passwords. Read them from environment variables, user input, or a secrets manager:
use nmrs::{NetworkManager, WifiSecurity}; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; let password = std::env::var("WIFI_PASSWORD") .expect("Set WIFI_PASSWORD environment variable"); nm.connect("HomeWiFi", None, WifiSecurity::WpaPsk { psk: password, }).await?; Ok(()) }
Reconnecting to Saved Networks
After the first successful connection, NetworkManager saves the credentials in a connection profile. Subsequent connections to the same SSID will reuse the saved profile automatically — you don't need to provide the password again:
#![allow(unused)] fn main() { let nm = NetworkManager::new().await?; if nm.has_saved_connection("HomeWiFi").await? { // Saved profile exists; password is stored in it. // The WifiSecurity value is ignored when a saved profile exists. nm.connect("HomeWiFi", None, WifiSecurity::Open).await?; } }
Error Handling
The most common errors for WPA-PSK connections:
| Error | Meaning |
|---|---|
ConnectionError::AuthFailed | Wrong password |
ConnectionError::MissingPassword | Empty password string |
ConnectionError::NotFound | Network not in range |
ConnectionError::Timeout | Connection took too long |
ConnectionError::DhcpFailed | Connected to AP but DHCP failed |
#![allow(unused)] fn main() { use nmrs::{NetworkManager, WifiSecurity, ConnectionError}; let nm = NetworkManager::new().await?; match nm.connect("HomeWiFi", None, WifiSecurity::WpaPsk { psk: "password".into(), }).await { Ok(_) => println!("Connected!"), Err(ConnectionError::AuthFailed) => { eprintln!("Wrong password — check and try again"); } Err(ConnectionError::MissingPassword) => { eprintln!("Password cannot be empty"); } Err(e) => eprintln!("Error: {}", e), } }
Next Steps
- WPA-EAP (Enterprise) – for corporate/university networks
- Hidden Networks – connecting to non-broadcast SSIDs
- Connection Profiles – managing saved connections
WPA-EAP (Enterprise)
WPA-EAP (802.1X) is used by corporate and university networks that require individual user credentials rather than a shared password. nmrs supports PEAP and EAP-TTLS with configurable inner authentication methods.
Quick Start
use nmrs::{NetworkManager, WifiSecurity, EapOptions, EapMethod, Phase2}; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; let eap = EapOptions::new("user@company.com", "my_password") .with_method(EapMethod::Peap) .with_phase2(Phase2::Mschapv2); nm.connect("CorpWiFi", None, WifiSecurity::WpaEap { opts: eap }).await?; println!("Connected to enterprise WiFi!"); Ok(()) }
EAP Methods
nmrs supports two outer EAP methods:
| Method | Description | Common Use |
|---|---|---|
EapMethod::Peap | Protected EAP — tunnels inner auth in TLS | Corporate networks |
EapMethod::Ttls | Tunneled TLS — flexible inner auth | Universities, ISPs |
Phase 2 (Inner Authentication)
The inner authentication runs inside the TLS tunnel established by the outer method:
| Method | Description | Typical Pairing |
|---|---|---|
Phase2::Mschapv2 | MS-CHAPv2 — challenge-response | PEAP |
Phase2::Pap | PAP — plaintext (protected by TLS tunnel) | TTLS |
Building EAP Options
Direct Construction
#![allow(unused)] fn main() { use nmrs::{EapOptions, EapMethod, Phase2}; let eap = EapOptions::new("user@company.com", "password") .with_method(EapMethod::Peap) .with_phase2(Phase2::Mschapv2) .with_anonymous_identity("anonymous@company.com") .with_domain_suffix_match("company.com") .with_system_ca_certs(true); }
Builder Pattern
For complex configurations, the builder pattern makes each option explicit:
#![allow(unused)] fn main() { use nmrs::{EapOptions, EapMethod, Phase2}; let eap = EapOptions::builder() .identity("user@company.com") .password("my_password") .method(EapMethod::Peap) .phase2(Phase2::Mschapv2) .anonymous_identity("anonymous@company.com") .domain_suffix_match("company.com") .system_ca_certs(true) .build(); }
Configuration Reference
| Option | Required | Description |
|---|---|---|
identity | Yes | Username (usually email) |
password | Yes | User password |
method | Yes | Outer EAP method (PEAP or TTLS) |
phase2 | Yes | Inner authentication (MSCHAPv2 or PAP) |
anonymous_identity | No | Outer identity for privacy (sent in the clear) |
domain_suffix_match | No | Verify server certificate domain |
ca_cert_path | No | Path to CA certificate (file:// URL) |
system_ca_certs | No | Use system CA store (default: false) |
Certificate Validation
For security, you should validate the authentication server's certificate. There are two approaches:
System CA Certificates
Use the operating system's trusted certificate store:
#![allow(unused)] fn main() { let eap = EapOptions::new("user@company.com", "password") .with_system_ca_certs(true) .with_domain_suffix_match("company.com") .with_method(EapMethod::Peap) .with_phase2(Phase2::Mschapv2); }
Custom CA Certificate
Point to a specific CA certificate file:
#![allow(unused)] fn main() { let eap = EapOptions::new("user@company.com", "password") .with_ca_cert_path("file:///etc/ssl/certs/company-ca.pem") .with_domain_suffix_match("company.com") .with_method(EapMethod::Peap) .with_phase2(Phase2::Mschapv2); }
Security: Without certificate validation, your connection is vulnerable to evil-twin attacks. Always configure either
system_ca_certsorca_cert_pathin production.
Common Configurations
Corporate PEAP/MSCHAPv2
The most common enterprise setup:
#![allow(unused)] fn main() { let eap = EapOptions::new("employee@corp.com", "password") .with_method(EapMethod::Peap) .with_phase2(Phase2::Mschapv2) .with_anonymous_identity("anonymous@corp.com") .with_domain_suffix_match("corp.com") .with_system_ca_certs(true); }
University EAP-TTLS/PAP
Common at educational institutions using eduroam:
#![allow(unused)] fn main() { let eap = EapOptions::new("student@university.edu", "password") .with_method(EapMethod::Ttls) .with_phase2(Phase2::Pap) .with_ca_cert_path("file:///etc/ssl/certs/university-ca.pem") .with_domain_suffix_match("university.edu"); }
Full Example
use nmrs::{NetworkManager, WifiSecurity, EapOptions, EapMethod, Phase2}; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; let eap = EapOptions::builder() .identity("user@company.com") .password( std::env::var("WIFI_PASSWORD") .expect("Set WIFI_PASSWORD env var"), ) .method(EapMethod::Peap) .phase2(Phase2::Mschapv2) .anonymous_identity("anonymous@company.com") .domain_suffix_match("company.com") .system_ca_certs(true) .build(); nm.connect("CorpNetwork", None, WifiSecurity::WpaEap { opts: eap, }).await?; if let Some(ssid) = nm.current_ssid().await { println!("Connected to: {}", ssid); } Ok(()) }
Troubleshooting
| Symptom | Likely Cause |
|---|---|
AuthFailed | Wrong username/password, or server rejected credentials |
SupplicantConfigFailed | Misconfigured EAP method or phase2 |
SupplicantTimeout | Server not responding — check CA cert and domain |
Timeout | Authentication taking too long — try increasing timeout |
For enterprise networks, the authentication process can take longer than standard WPA-PSK connections. Consider using custom timeouts:
#![allow(unused)] fn main() { use nmrs::{NetworkManager, TimeoutConfig}; use std::time::Duration; let config = TimeoutConfig::new() .with_connection_timeout(Duration::from_secs(60)); let nm = NetworkManager::with_config(config).await?; }
Next Steps
- Hidden Networks – enterprise networks are often hidden
- Custom Timeouts – increase timeout for slow auth servers
- Error Handling – handle enterprise auth errors
Hidden Networks
Hidden networks do not broadcast their SSID in beacon frames. To connect, you must know the exact SSID and provide the correct credentials. nmrs handles hidden network connections the same way as visible networks — if the SSID is not found during the scan, NetworkManager will attempt a directed probe.
Connecting to a Hidden Network
The API for connecting to hidden networks is identical to visible networks. Simply provide the SSID and security credentials:
use nmrs::{NetworkManager, WifiSecurity}; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; // Hidden open network nm.connect("HiddenCafe", None, WifiSecurity::Open).await?; // Hidden WPA-PSK network nm.connect("SecretLab", None, WifiSecurity::WpaPsk { psk: "lab_password".into(), }).await?; Ok(()) }
How It Works
When you call connect() with an SSID:
- nmrs first checks if there is a saved connection profile for that SSID — if so, it activates the saved profile directly
- If no saved profile exists, it searches the visible access point list
- If the network is not visible (hidden), NetworkManager creates a connection profile with the hidden flag set and performs a directed probe request for the specific SSID
This means hidden networks work transparently. The first connection may take slightly longer as NetworkManager performs the directed scan.
Hidden Enterprise Networks
Hidden networks can also use WPA-EAP authentication:
#![allow(unused)] fn main() { use nmrs::{NetworkManager, WifiSecurity, EapOptions, EapMethod, Phase2}; let nm = NetworkManager::new().await?; let eap = EapOptions::new("user@company.com", "password") .with_method(EapMethod::Peap) .with_phase2(Phase2::Mschapv2) .with_system_ca_certs(true); nm.connect("HiddenCorpNet", None, WifiSecurity::WpaEap { opts: eap, }).await?; }
Reconnecting
After the first successful connection, NetworkManager saves the profile with the hidden flag. Subsequent connections to the same SSID will reconnect automatically using the saved profile, even though the network doesn't appear in scan results.
Considerations
- Privacy: Hidden networks are not truly hidden — the SSID is transmitted during the association process. They provide obscurity, not security.
- Battery impact: Devices probing for hidden networks transmit more frequently, which can reduce battery life on mobile devices.
- First connection: The initial connection may be slower than visible networks because NetworkManager must perform a directed probe.
Next Steps
- WPA-PSK Networks – password-protected networks
- WPA-EAP (Enterprise) – corporate authentication
- Connection Profiles – managing saved hidden network profiles
Per-Device Wi-Fi Scoping
Many machines have more than one Wi-Fi radio — a built-in card plus a USB dongle, a laptop in a dock with a secondary adapter, or an IoT gateway with dual radios on different bands. By default, nmrs routes every Wi-Fi operation through whichever device NetworkManager returns first. That works on single-radio systems, but on multi-radio setups you need to control which adapter scans, connects, or gets disabled.
nmrs 3.0 introduces per-device scoping so you can target a specific interface by name.
Listing Wi-Fi Devices
Start by discovering the available radios:
use nmrs::NetworkManager; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; let devices = nm.list_wifi_devices().await?; for dev in &devices { println!("{} ({})", dev.interface, dev.mac); println!(" State: {:?}", dev.state); if let Some(ssid) = &dev.active_ssid { println!(" Connected to: {}", ssid); } } Ok(()) }
Each WifiDevice contains:
| Field | Type | Description |
|---|---|---|
interface | String | Interface name (wlan0, wlp2s0, …) |
mac | String | Hardware MAC address |
state | DeviceState | Current operational state |
active_ssid | Option<String> | SSID of the active connection, if any |
You can also look up a single device directly:
#![allow(unused)] fn main() { let dev = nm.wifi_device_by_interface("wlan1").await?; println!("{} is {:?}", dev.interface, dev.state); }
The WifiScope Pattern
The most ergonomic way to work with a specific radio is WifiScope. Call nm.wifi("wlan1") to get a scope pinned to that interface, then chain operations without repeating the interface name:
use nmrs::{NetworkManager, WifiSecurity}; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; let scope = nm.wifi("wlan1"); scope.scan().await?; let networks = scope.list_networks().await?; for net in &networks { println!("{} ({}%)", net.ssid, net.strength.unwrap_or(0)); } scope.connect("HomeWiFi", WifiSecurity::WpaPsk { psk: "hunter2".into(), }).await?; Ok(()) }
WifiScope delegates to NetworkManager under the hood but locks every call to a single interface. The available methods are:
| Method | Description |
|---|---|
scope.interface() | Returns the interface name this scope is pinned to |
scope.scan() | Trigger a scan on this device |
scope.list_networks() | List networks visible to this device |
scope.list_access_points() | List raw access points (including duplicates per BSSID) |
scope.connect(ssid, creds) | Connect through this device |
scope.connect_to_bssid(ssid, bssid, creds) | Connect to a specific BSSID through this device |
scope.disconnect() | Disconnect this device |
scope.set_enabled(bool) | Enable or disable this device |
scope.forget(ssid) | Remove a saved connection from this device |
Because the interface is already captured, none of these methods take an interface parameter.
BSSID targeting
When the same SSID is broadcast by multiple access points, use connect_to_bssid to force a specific one:
#![allow(unused)] fn main() { let scope = nm.wifi("wlan0"); let aps = scope.list_access_points().await?; if let Some(best) = aps.iter().max_by_key(|ap| ap.strength.unwrap_or(0)) { scope.connect_to_bssid( &best.ssid, &best.hwaddress.as_deref().unwrap_or_default(), WifiSecurity::WpaPsk { psk: "password".into() }, ).await?; } }
Per-Interface vs Global Operations
nmrs distinguishes between operations that target one device and operations that affect the entire Wi-Fi subsystem.
| Operation | Per-device | Global |
|---|---|---|
| Enable/disable radio | nm.set_wifi_enabled("wlan1", true) | nm.set_wireless_enabled(false) |
| Scan | nm.scan_networks(Some("wlan1")) | nm.scan_networks(None) (scans all) |
| List networks | nm.list_networks(Some("wlan1")) | nm.list_networks(None) (merges all) |
| Connect | nm.connect("ssid", Some("wlan1"), creds) | nm.connect("ssid", None, creds) |
| Disconnect | nm.disconnect(Some("wlan1")) | nm.disconnect(None) (all devices) |
When you pass None, nmrs falls back to the original behavior: pick the first Wi-Fi device for single-device operations, or aggregate across all devices for scans and listings.
Per-Device Enable/Disable
There are two distinct toggles:
set_wireless_enabled(bool)flips NetworkManager's globalWirelessEnabledproperty. This affects every Wi-Fi radio on the system — equivalent to airplane-mode for Wi-Fi.set_wifi_enabled(interface, bool)targets a single radio. It setsAutoconnect = falseand disconnects the device (to disable) or re-enables autoconnect (to enable). The rest of the system's Wi-Fi radios are unaffected.
#![allow(unused)] fn main() { let nm = NetworkManager::new().await?; // Disable only the USB dongle nm.set_wifi_enabled("wlan1", false).await?; // The built-in radio stays online let dev = nm.wifi_device_by_interface("wlan0").await?; assert_ne!(dev.state, nmrs::DeviceState::Unavailable); }
Using WifiScope:
#![allow(unused)] fn main() { let scope = nm.wifi("wlan1"); scope.set_enabled(false).await?; }
Note:
set_wifi_enabledis not the same as the globalset_wireless_enabled. The global toggle controls the NM-levelWirelessEnabledproperty (equivalent tonmcli radio wifi off), while per-device disable works through the device's autoconnect and disconnect mechanism.
Direct Method Approach
If you don't want a WifiScope, every Wi-Fi method on NetworkManager accepts an optional interface name. Pass None for single-radio behavior or Some("wlan1") to target a device:
use nmrs::{NetworkManager, WifiSecurity}; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; // Scan on a specific interface nm.scan_networks(Some("wlan1")).await?; // List networks from a specific interface let networks = nm.list_networks(Some("wlan1")).await?; // Connect through a specific interface nm.connect("OfficeWiFi", Some("wlan1"), WifiSecurity::WpaPsk { psk: "secret".into(), }).await?; // Disconnect a specific interface nm.disconnect(Some("wlan1")).await?; // Or use None to get the default (first device) behavior nm.scan_networks(None).await?; nm.connect("HomeWiFi", None, WifiSecurity::Open).await?; Ok(()) }
Error Handling
Two error variants are specific to per-device scoping:
#![allow(unused)] fn main() { use nmrs::ConnectionError; let nm = NetworkManager::new().await?; match nm.wifi_device_by_interface("wlan99").await { Ok(dev) => println!("Found: {}", dev.interface), Err(ConnectionError::WifiInterfaceNotFound { interface }) => { eprintln!("No Wi-Fi device named '{}'", interface); } Err(e) => eprintln!("Unexpected error: {}", e), } }
| Variant | Meaning |
|---|---|
WifiInterfaceNotFound { interface } | No network device with that name exists |
NotAWifiDevice { interface } | The interface exists but is not a Wi-Fi device (e.g., eth0) |
NotAWifiDevice fires when you pass a valid interface name that belongs to an Ethernet or Bluetooth adapter:
#![allow(unused)] fn main() { match nm.wifi_device_by_interface("eth0").await { Err(ConnectionError::NotAWifiDevice { interface }) => { eprintln!("'{}' is not a Wi-Fi interface", interface); } _ => {} } }
Next Steps
- WiFi Management — general Wi-Fi operations (scanning, security types, connection options)
- Device Management — listing and inspecting all device types
- Real-Time Monitoring — subscribe to device state changes
- Error Handling — full error variant reference
VPN Connections
nmrs provides full support for WireGuard and OpenVPN connections through NetworkManager. This guide covers everything you need to know about managing VPNs with nmrs.
Overview
VPN support includes:
- WireGuard — Modern, fast, secure VPN protocol (native NetworkManager integration)
- OpenVPN — Widely deployed VPN protocol (via NetworkManager OpenVPN plugin)
.ovpnImport — Import existing OpenVPN configuration files- Profile Management — Save and reuse VPN configurations
- Connection Control — Connect, disconnect, monitor VPN status
- Multiple Peers — Support for multiple WireGuard peers
- Custom DNS — Override DNS servers for VPN connections
- MTU Configuration — Optimize packet sizes
WireGuard Quick Start
use nmrs::{NetworkManager, WireGuardConfig, WireGuardPeer}; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; let peer = WireGuardPeer::new( "server_public_key", "vpn.example.com:51820", vec!["0.0.0.0/0".into()], ).with_persistent_keepalive(25); let config = WireGuardConfig::new( "MyVPN", "vpn.example.com:51820", "your_private_key", "10.0.0.2/24", vec![peer], ).with_dns(vec!["1.1.1.1".into()]); nm.connect_vpn(config).await?; println!("Connected to WireGuard VPN!"); Ok(()) }
OpenVPN Quick Start
use nmrs::{NetworkManager, OpenVpnConfig, OpenVpnAuthType}; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; let config = OpenVpnConfig::new("CorpVPN", "vpn.example.com", 1194, false) .with_auth_type(OpenVpnAuthType::PasswordTls) .with_username("user") .with_password("secret") .with_ca_cert("/etc/openvpn/ca.crt") .with_client_cert("/etc/openvpn/client.crt") .with_client_key("/etc/openvpn/client.key"); nm.connect_vpn(config).await?; println!("Connected to OpenVPN!"); Ok(()) }
.ovpn File Import
Import an existing OpenVPN configuration file directly:
#![allow(unused)] fn main() { nm.import_ovpn("client.ovpn", Some("user"), Some("secret")).await?; }
For certificate-only configs that don't require credentials:
#![allow(unused)] fn main() { nm.import_ovpn("client.ovpn", None, None).await?; }
See the .ovpn Import Example for builder-based import and inline certificate handling.
VPN Operations
Connect
#![allow(unused)] fn main() { // WireGuard nm.connect_vpn(wireguard_config).await?; // OpenVPN nm.connect_vpn(openvpn_config).await?; }
Connect by Name or UUID
Reconnect to a saved VPN profile without rebuilding the config:
#![allow(unused)] fn main() { nm.connect_vpn_by_id("MyVPN").await?; nm.connect_vpn_by_uuid("a1b2c3d4-e5f6-...").await?; }
Disconnect
#![allow(unused)] fn main() { nm.disconnect_vpn("MyVPN").await?; nm.disconnect_vpn_by_uuid("a1b2c3d4-e5f6-...").await?; }
List VPN Connections
#![allow(unused)] fn main() { let vpns = nm.list_vpn_connections().await?; for vpn in &vpns { println!("{} ({:?}) — active: {}", vpn.name, vpn.vpn_type, vpn.active); if let Some(iface) = &vpn.interface { println!(" Interface: {iface}"); } } }
list_vpn_connections() returns Vec<VpnConnection> with fields:
| Field | Type | Description |
|---|---|---|
uuid | String | Connection UUID |
id | String | Connection name (alias for name) |
name | String | Connection profile name |
vpn_type | VpnType | VPN protocol (WireGuard, OpenVpn, etc.) |
state | DeviceState | Current state (Activated, Disconnected, etc.) |
interface | Option<String> | Network interface when active |
active | bool | Whether the connection is currently active |
kind | VpnKind | VpnKind::Plugin (OpenVPN) or VpnKind::WireGuard |
Active VPN Connections
Get only currently active VPN connections:
#![allow(unused)] fn main() { let active = nm.active_vpn_connections().await?; for vpn in &active { println!("Active: {} ({:?})", vpn.name, vpn.vpn_type); } }
Get VPN Information
#![allow(unused)] fn main() { let info = nm.get_vpn_info("MyVPN").await?; println!("Name: {}", info.name); println!("Kind: {:?}", info.vpn_kind); println!("State: {:?}", info.state); println!("Interface: {:?}", info.interface); println!("Gateway: {:?}", info.gateway); println!("IPv4: {:?}", info.ip4_address); println!("IPv6: {:?}", info.ip6_address); println!("DNS: {:?}", info.dns_servers); if let Some(details) = &info.details { println!("Details: {:?}", details); } }
Remove a VPN Profile
#![allow(unused)] fn main() { nm.forget_vpn("MyVPN").await?; }
Routing Configuration
Route All Traffic (WireGuard)
#![allow(unused)] fn main() { let peer = WireGuardPeer::new( "public_key", "vpn.example.com:51820", vec!["0.0.0.0/0".into()], ); }
Split Tunnel (WireGuard)
Route only specific networks through VPN:
#![allow(unused)] fn main() { let peer = WireGuardPeer::new( "public_key", "vpn.example.com:51820", vec![ "10.0.0.0/8".into(), "192.168.0.0/16".into(), ], ); }
IPv6 Support
#![allow(unused)] fn main() { let peer = WireGuardPeer::new( "public_key", "vpn.example.com:51820", vec![ "0.0.0.0/0".into(), "::/0".into(), ], ); }
Error Handling
#![allow(unused)] fn main() { use nmrs::ConnectionError; match nm.connect_vpn(config).await { Ok(_) => println!("VPN connected"), Err(ConnectionError::AuthFailed) => { eprintln!("Authentication failed — check keys or credentials"); } Err(ConnectionError::Timeout) => { eprintln!("Connection timed out — check gateway address"); } Err(ConnectionError::VpnFailed) => { eprintln!("VPN activation failed — check plugin or config"); } Err(ConnectionError::NotFound) => { eprintln!("VPN gateway not reachable"); } Err(e) => eprintln!("VPN error: {e}"), } }
Complete Example
use nmrs::{NetworkManager, WireGuardConfig, WireGuardPeer}; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; // Check if already connected let active = nm.active_vpn_connections().await?; if let Some(vpn) = active.first() { println!("Already connected to: {}", vpn.name); return Ok(()); } // Create WireGuard configuration let peer = WireGuardPeer::new( std::env::var("WG_PUBLIC_KEY")?, std::env::var("WG_ENDPOINT")?, vec!["0.0.0.0/0".into()], ).with_persistent_keepalive(25); let config = WireGuardConfig::new( "AutoVPN", &std::env::var("WG_ENDPOINT")?, &std::env::var("WG_PRIVATE_KEY")?, &std::env::var("WG_ADDRESS")?, vec![peer], ).with_dns(vec!["1.1.1.1".into(), "8.8.8.8".into()]); // Connect println!("Connecting to VPN..."); nm.connect_vpn(config).await?; // Verify connection let info = nm.get_vpn_info("AutoVPN").await?; println!("Connected! VPN IP: {:?}", info.ip4_address); // Keep connection alive println!("Press Ctrl+C to disconnect..."); tokio::signal::ctrl_c().await?; // Disconnect nm.disconnect_vpn("AutoVPN").await?; println!("Disconnected from VPN"); Ok(()) }
Advanced Topics
For more advanced VPN usage, see:
- WireGuard Setup — Detailed WireGuard configuration
- OpenVPN Setup — Detailed OpenVPN configuration
- VPN Management — Managing VPN profiles
- WireGuard Client Example — Complete WireGuard example
- OpenVPN Client Example — Complete OpenVPN example
.ovpnImport Example — Import.ovpnfiles
Security Best Practices
- Never hardcode keys or passwords — Use environment variables or secure storage
- Rotate keys regularly — Update WireGuard keys periodically
- Use preshared keys — Add extra layer of security with PSK (WireGuard)
- Protect certificates — Store OpenVPN certs with restrictive file permissions (
chmod 600) - Use TLS authentication — Prefer
PasswordTlsorTlsoverPasswordalone for OpenVPN - Verify endpoints — Ensure gateway addresses are correct
- Monitor connection — Check VPN status regularly
Troubleshooting
VPN Won't Connect
#![allow(unused)] fn main() { let vpns = nm.list_vpn_connections().await?; for vpn in &vpns { println!("{}: {:?} (active: {})", vpn.name, vpn.state, vpn.active); } }
Connection Drops (WireGuard)
Use persistent keepalive:
#![allow(unused)] fn main() { let peer = peer.with_persistent_keepalive(25); }
DNS Not Working
Explicitly set DNS servers:
#![allow(unused)] fn main() { let config = config.with_dns(vec![ "1.1.1.1".into(), "8.8.8.8".into(), ]); }
OpenVPN Plugin Not Found
Ensure the NetworkManager OpenVPN plugin is installed:
# Arch Linux
sudo pacman -S networkmanager-openvpn
# Debian/Ubuntu
sudo apt install network-manager-openvpn
# Fedora
sudo dnf install NetworkManager-openvpn
Next Steps
- WireGuard Setup Guide
- OpenVPN Setup Guide
- VPN Management
- OpenVPN Client Example
- WireGuard Client Example
WireGuard Setup
WireGuard is a modern, high-performance VPN protocol. nmrs provides full WireGuard support through NetworkManager's native WireGuard integration — no additional plugins required.
Prerequisites
- NetworkManager 1.16+ (WireGuard support was added in 1.16)
- The
wireguardkernel module must be loaded (built into Linux 5.6+, available as a module on older kernels) - A WireGuard configuration from your VPN provider or server administrator
Quick Start
use nmrs::{NetworkManager, WireGuardConfig, WireGuardPeer}; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; let peer = WireGuardPeer::new( "SERVER_PUBLIC_KEY_BASE64", "vpn.example.com:51820", vec!["0.0.0.0/0".into()], ).with_persistent_keepalive(25); let config = WireGuardConfig::new( "MyVPN", "vpn.example.com:51820", "CLIENT_PRIVATE_KEY_BASE64", "10.0.0.2/24", vec![peer], ).with_dns(vec!["1.1.1.1".into()]); nm.connect_vpn(config).await?; println!("VPN connected!"); Ok(()) }
Understanding WireGuard Concepts
| Concept | Description |
|---|---|
| Private Key | Your client's secret key (base64, 44 chars). Never share this. |
| Public Key | The server's public key (base64, 44 chars). Provided by server admin. |
| Endpoint | Server address in host:port format (e.g., vpn.example.com:51820) |
| Address | Your client's IP within the VPN tunnel (e.g., 10.0.0.2/24) |
| Allowed IPs | IP ranges to route through the tunnel. 0.0.0.0/0 routes everything. |
| DNS | DNS servers to use while the VPN is active |
| Persistent Keepalive | Seconds between keepalive packets (helps with NAT traversal) |
WireGuardConfig Fields
| Field | Required | Description |
|---|---|---|
name | Yes | Connection profile name |
gateway | Yes | Server endpoint (host:port) |
private_key | Yes | Client private key (base64) |
address | Yes | Client IP with CIDR (10.0.0.2/24) |
peers | Yes | At least one WireGuardPeer |
dns | No | DNS servers for the VPN |
mtu | No | MTU size (typical: 1420) |
uuid | No | Custom UUID (auto-generated if omitted) |
Building Configuration
#![allow(unused)] fn main() { use nmrs::{WireGuardConfig, WireGuardPeer}; let peer = WireGuardPeer::new( "HIgo9xNzJMWLKAShlKl6/bUT1VI9Q0SDBXGtLXkPFXc=", "vpn.example.com:51820", vec!["0.0.0.0/0".into(), "::/0".into()], ).with_persistent_keepalive(25) .with_preshared_key("OPTIONAL_PSK_BASE64"); let config = WireGuardConfig::new( "HomeVPN", "vpn.example.com:51820", "YBk6X3pP8KjKz7+HFWzVHNqL3qTZq8hX9VxFQJ4zVmM=", "10.0.0.2/24", vec![peer], ).with_dns(vec!["1.1.1.1".into(), "8.8.8.8".into()]) .with_mtu(1420); }
WireGuardPeer Configuration
| Field | Required | Description |
|---|---|---|
public_key | Yes | Peer's WireGuard public key (base64) |
gateway | Yes | Peer endpoint (host:port) |
allowed_ips | Yes | IP ranges to route through this peer |
preshared_key | No | Additional shared secret for post-quantum security |
persistent_keepalive | No | Keepalive interval in seconds |
Multiple Peers
WireGuard supports multiple peers with different routing rules:
#![allow(unused)] fn main() { use nmrs::WireGuardPeer; let full_tunnel = WireGuardPeer::new( "peer1_public_key", "vpn.example.com:51820", vec!["0.0.0.0/0".into()], ).with_persistent_keepalive(25); let split_tunnel = WireGuardPeer::new( "peer2_public_key", "office.example.com:51820", vec!["10.0.0.0/8".into(), "192.168.0.0/16".into()], ); }
Routing with Allowed IPs
| Configuration | Effect |
|---|---|
["0.0.0.0/0"] | Full tunnel — all traffic goes through VPN |
["0.0.0.0/0", "::/0"] | Full tunnel with IPv6 |
["10.0.0.0/8"] | Split tunnel — only 10.x.x.x traffic |
["192.168.1.0/24"] | Split tunnel — only one subnet |
Validation
nmrs validates all WireGuard parameters before sending them to NetworkManager:
- Private/public keys: Must be valid base64, approximately 44 characters
- Address: Must include CIDR notation (e.g.,
10.0.0.2/24) - Gateway: Must be in
host:portformat with a valid port - Peers: At least one peer is required, each with a valid public key and non-empty allowed IPs
Invalid parameters produce specific error variants:
| Error | Cause |
|---|---|
InvalidPrivateKey | Key missing, wrong length, or invalid base64 |
InvalidPublicKey | Peer key invalid |
InvalidAddress | Missing CIDR prefix or invalid IP |
InvalidGateway | Missing port or invalid format |
InvalidPeers | No peers, or peer has no allowed IPs |
Security Best Practices
- Never hardcode private keys — use environment variables or a secrets manager
- Use preshared keys when available for additional post-quantum security
- Set persistent keepalive to 25 seconds if behind NAT
- Use split tunneling when you only need to reach specific networks
Next Steps
- VPN Management – list, disconnect, and remove VPN connections
- OpenVPN Setup – set up OpenVPN connections
- Custom Timeouts – adjust VPN connection timeouts
- Error Handling – handle VPN-specific errors
OpenVPN Setup
OpenVPN is a widely-deployed, battle-tested VPN protocol that uses TLS for key exchange and supports a variety of authentication methods. nmrs provides full OpenVPN support through the NetworkManager OpenVPN plugin, letting you create, import, and manage OpenVPN connections programmatically.
Prerequisites
- NetworkManager 1.2+
- The OpenVPN plugin for NetworkManager:
- Fedora / RHEL:
NetworkManager-openvpn - Debian / Ubuntu:
network-manager-openvpn - Arch Linux:
networkmanager-openvpn
- Fedora / RHEL:
- OpenVPN certificates and/or credentials from your VPN provider
Quick Start
Connect to an OpenVPN server using password + TLS certificate authentication:
use nmrs::{NetworkManager, OpenVpnConfig, OpenVpnAuthType}; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; let config = OpenVpnConfig::new("CorpVPN", "vpn.example.com", 1194, false) .with_auth_type(OpenVpnAuthType::PasswordTls) .with_ca_cert("/etc/openvpn/ca.crt") .with_client_cert("/etc/openvpn/client.crt") .with_client_key("/etc/openvpn/client.key") .with_username("alice") .with_password("hunter2") .with_dns(vec!["1.1.1.1".into()]); nm.connect_vpn(config).await?; println!("VPN connected!"); Ok(()) }
Authentication Types
OpenVPN supports four authentication modes, selected with OpenVpnAuthType:
| Variant | Description | Required Fields |
|---|---|---|
Password | Username/password only | username |
Tls | TLS certificate only | ca_cert, client_cert, client_key |
PasswordTls | Password + TLS certificates | username, ca_cert, client_cert, client_key |
StaticKey | Pre-shared static key | (static key file via TLS auth) |
Password Authentication
#![allow(unused)] fn main() { use nmrs::{OpenVpnConfig, OpenVpnAuthType}; let config = OpenVpnConfig::new("SimpleVPN", "vpn.example.com", 1194, false) .with_auth_type(OpenVpnAuthType::Password) .with_username("alice") .with_password("secret") .with_ca_cert("/etc/openvpn/ca.crt"); }
TLS Certificate Authentication
#![allow(unused)] fn main() { use nmrs::{OpenVpnConfig, OpenVpnAuthType}; let config = OpenVpnConfig::new("CertVPN", "vpn.example.com", 1194, false) .with_auth_type(OpenVpnAuthType::Tls) .with_ca_cert("/etc/openvpn/ca.crt") .with_client_cert("/etc/openvpn/client.crt") .with_client_key("/etc/openvpn/client.key"); }
If the client key is encrypted:
#![allow(unused)] fn main() { let config = config.with_key_password("keyfile-passphrase"); }
Password + TLS Authentication
The most common configuration for corporate VPNs — the server verifies both your certificate and your credentials:
#![allow(unused)] fn main() { use nmrs::{OpenVpnConfig, OpenVpnAuthType}; let config = OpenVpnConfig::new("CorpVPN", "vpn.example.com", 443, true) .with_auth_type(OpenVpnAuthType::PasswordTls) .with_ca_cert("/etc/openvpn/ca.crt") .with_client_cert("/etc/openvpn/client.crt") .with_client_key("/etc/openvpn/client.key") .with_username("alice") .with_password("secret"); }
Configuration Reference
| Field | Builder Method | Required | Description |
|---|---|---|---|
name | constructor | Yes | Connection profile name |
remote | constructor | Yes | Server hostname or IP |
port | constructor | Yes | Server port (typically 1194 or 443) |
tcp | constructor | Yes | true for TCP, false for UDP |
auth_type | with_auth_type | No | Authentication mode (see above) |
ca_cert | with_ca_cert | No* | Path to CA certificate |
client_cert | with_client_cert | No* | Path to client certificate |
client_key | with_client_key | No* | Path to client private key |
key_password | with_key_password | No | Password for encrypted key file |
username | with_username | No* | Username for password auth |
password | with_password | No | Password for password auth |
cipher | with_cipher | No | Data channel cipher (e.g. "AES-256-GCM") |
auth | with_auth | No | HMAC digest algorithm (e.g. "SHA256") |
dns | with_dns | No | DNS servers while connected |
mtu | with_mtu | No | MTU size |
uuid | with_uuid | No | Custom UUID (auto-generated if omitted) |
compression | with_compression | No | Compression mode (see below) |
proxy | with_proxy | No | Proxy configuration |
redirect_gateway | with_redirect_gateway | No | Full tunnel (false by default) |
routes | with_routes | No | Split tunnel routes |
* Required depending on the chosen auth_type.
Importing .ovpn Files
If you already have an .ovpn configuration file, you can import it directly.
High-Level Import
The simplest approach — import and connect in one call:
use nmrs::NetworkManager; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; nm.import_ovpn("corp.ovpn", Some("alice"), Some("secret")).await?; println!("Connected via imported .ovpn profile"); Ok(()) }
import_ovpn parses the file, creates a NetworkManager connection profile, and activates it. The connection name defaults to the filename stem (e.g., corp from corp.ovpn).
Builder-Based Import
For more control, use OpenVpnBuilder::from_ovpn_file to parse the file into a builder, then customise before connecting:
use nmrs::builders::OpenVpnBuilder; use nmrs::NetworkManager; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; let config = OpenVpnBuilder::from_ovpn_file("corp.ovpn")? .username("alice") .dns(vec!["1.1.1.1".into()]) .mtu(1400) .remote_cert_tls("server") .build()?; nm.connect_vpn(config).await?; Ok(()) }
You can also parse from a string with OpenVpnBuilder::from_ovpn_str(content, name) if the configuration is fetched from a remote source.
TLS Hardening
OpenVPN's TLS layer can be hardened with several options. These are independent of the authentication type and can be combined.
TLS Auth
Adds an HMAC firewall to the control channel, providing DoS protection. Both sides must share the same static key and agree on direction:
#![allow(unused)] fn main() { let config = config .with_tls_auth("/etc/openvpn/ta.key", Some(1)); }
TLS-Crypt
Encrypts and authenticates the entire control channel with a pre-shared key — stronger than tls-auth because the TLS handshake itself is hidden:
#![allow(unused)] fn main() { let config = config .with_tls_crypt("/etc/openvpn/tls-crypt.key"); }
TLS-Crypt-v2
Per-client key wrapping, allowing the server to issue unique keys to each client while retaining the benefits of TLS-Crypt:
#![allow(unused)] fn main() { let config = config .with_tls_crypt_v2("/etc/openvpn/client-tls-crypt-v2.key"); }
Note:
tls-auth,tls-crypt, andtls-crypt-v2are mutually exclusive. Use only one.
Certificate Verification
Verify the server's certificate identity to prevent man-in-the-middle attacks:
#![allow(unused)] fn main() { use nmrs::OpenVpnConfig; let config = OpenVpnConfig::new("SecureVPN", "vpn.example.com", 1194, false) .with_auth_type(nmrs::OpenVpnAuthType::Tls) .with_ca_cert("/etc/openvpn/ca.crt") .with_client_cert("/etc/openvpn/client.crt") .with_client_key("/etc/openvpn/client.key") .with_remote_cert_tls("server") .with_verify_x509_name("vpn.example.com", "name"); }
| Method | Purpose |
|---|---|
with_remote_cert_tls("server") | Require the remote cert to have server (TLS Web Server) usage |
with_verify_x509_name(name, type) | Verify the CN or subject of the server certificate |
with_tls_version_min("1.2") | Enforce minimum TLS version |
with_tls_version_max("1.3") | Cap the maximum TLS version |
with_tls_cipher(suite) | Restrict control-channel cipher suites |
Split Tunneling
By default, redirect_gateway is false — only traffic matching explicit routes goes through the VPN.
Full Tunnel
Route all traffic through the VPN:
#![allow(unused)] fn main() { let config = config.with_redirect_gateway(true); }
Split Tunnel with Routes
Route only specific networks through the VPN using VpnRoute:
#![allow(unused)] fn main() { use nmrs::{OpenVpnConfig, OpenVpnAuthType, VpnRoute}; let config = OpenVpnConfig::new("OfficeVPN", "vpn.example.com", 1194, false) .with_auth_type(OpenVpnAuthType::Tls) .with_ca_cert("/etc/openvpn/ca.crt") .with_client_cert("/etc/openvpn/client.crt") .with_client_key("/etc/openvpn/client.key") .with_routes(vec![ VpnRoute::new("10.0.0.0", 8), VpnRoute::new("192.168.1.0", 24).next_hop("10.0.0.1").metric(100), ]); }
VpnRoute Method | Description |
|---|---|
VpnRoute::new(dest, prefix) | Destination network and CIDR prefix length |
.next_hop(gateway) | Optional gateway for the route |
.metric(m) | Optional route metric (lower = higher priority) |
Compression
OpenVPN supports several compression algorithms, but compression is disabled by default for security reasons.
#![allow(unused)] fn main() { use nmrs::OpenVpnCompression; let config = config.with_compression(OpenVpnCompression::No); }
| Variant | Description |
|---|---|
No | Disabled (recommended default) |
Lzo | LZO compression (deprecated) |
Lz4 | LZ4 compression |
Lz4V2 | LZ4 v2 compression |
Yes | Adaptive compression |
Security Warning: Enabling compression on an OpenVPN tunnel that carries TLS traffic (HTTPS, etc.) exposes the connection to the VORACLE attack. An attacker who can observe encrypted VPN traffic and induce the victim to visit attacker-controlled content can recover plaintext via compression oracle side-channels. Leave compression disabled unless you have a specific need and understand the risk.
Proxy Support
Route OpenVPN traffic through an HTTP or SOCKS proxy:
HTTP Proxy
#![allow(unused)] fn main() { use nmrs::OpenVpnProxy; let config = config.with_proxy(OpenVpnProxy::Http { server: "proxy.example.com".into(), port: 8080, username: Some("proxyuser".into()), password: Some("proxypass".into()), retry: true, }); }
SOCKS Proxy
#![allow(unused)] fn main() { use nmrs::OpenVpnProxy; let config = config.with_proxy(OpenVpnProxy::Socks { server: "socks.example.com".into(), port: 1080, retry: false, }); }
When using a proxy, TCP mode (tcp: true in the constructor) is typically required.
Error Handling
Handle OpenVPN-specific errors:
use nmrs::{ConnectionError, NetworkManager, OpenVpnConfig, OpenVpnAuthType}; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; let config = OpenVpnConfig::new("CorpVPN", "vpn.example.com", 1194, false) .with_auth_type(OpenVpnAuthType::PasswordTls) .with_ca_cert("/etc/openvpn/ca.crt") .with_client_cert("/etc/openvpn/client.crt") .with_client_key("/etc/openvpn/client.key") .with_username("alice") .with_password("secret"); match nm.connect_vpn(config).await { Ok(()) => println!("VPN connected"), Err(ConnectionError::VpnFailed(msg)) => { eprintln!("OpenVPN activation failed: {msg}"); } Err(ConnectionError::AuthFailed) => { eprintln!("Authentication failed — check username/password and certificates"); } Err(ConnectionError::Timeout) => { eprintln!("Connection timed out — verify server address and port"); } Err(ConnectionError::InvalidGateway(msg)) => { eprintln!("Bad server address: {msg}"); } Err(e) => eprintln!("Unexpected error: {e}"), } Ok(()) }
| Error | Cause |
|---|---|
VpnFailed | Plugin missing, config rejected, or activation failed |
AuthFailed | Bad username/password or certificate rejected |
Timeout | Server unreachable or handshake timed out |
InvalidGateway | Empty or invalid remote address |
Next Steps
- VPN Connections – VPN overview and general operations
- VPN Management – list, disconnect, and remove VPN profiles
- Error Handling – comprehensive error reference
VPN Management
Once you've set up a WireGuard or OpenVPN connection, nmrs provides methods to list, inspect, connect, disconnect, and remove VPN profiles.
Listing VPN Connections
use nmrs::NetworkManager; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; let vpns = nm.list_vpn_connections().await?; for vpn in &vpns { println!("{}: {:?} [{:?}] (active: {})", vpn.name, vpn.vpn_type, vpn.state, vpn.active, ); if let Some(iface) = &vpn.interface { println!(" Interface: {iface}"); } } Ok(()) }
list_vpn_connections() returns all saved VPN profiles with their current state. The VpnConnection struct contains:
| Field | Type | Description |
|---|---|---|
uuid | String | Connection UUID |
id | String | Connection name (alias for name) |
name | String | Connection profile name |
vpn_type | VpnType | VPN protocol — a data-carrying enum with WireGuard, OpenVpn, and other variants |
state | DeviceState | Current state (Activated, Disconnected, etc.) |
interface | Option<String> | Network interface when active (e.g., wg0, tun0) |
active | bool | Whether the connection is currently active |
kind | VpnKind | VpnKind::Plugin (OpenVPN) or VpnKind::WireGuard |
Active VPN Connections
Get only currently active VPN connections:
#![allow(unused)] fn main() { let active = nm.active_vpn_connections().await?; for vpn in &active { println!("Active: {} ({:?}) on {:?}", vpn.name, vpn.vpn_type, vpn.interface); } }
Getting VPN Details
For an active VPN connection, retrieve detailed information including IP configuration:
#![allow(unused)] fn main() { let nm = NetworkManager::new().await?; let info = nm.get_vpn_info("MyVPN").await?; println!("Name: {}", info.name); println!("Kind: {:?}", info.vpn_kind); println!("State: {:?}", info.state); println!("Interface: {:?}", info.interface); println!("Gateway: {:?}", info.gateway); println!("IPv4: {:?}", info.ip4_address); println!("IPv6: {:?}", info.ip6_address); println!("DNS: {:?}", info.dns_servers); if let Some(details) = &info.details { println!("Details: {:?}", details); } }
The VpnConnectionInfo struct provides:
| Field | Type | Description |
|---|---|---|
name | String | Connection name |
vpn_kind | VpnKind | VpnKind::Plugin or VpnKind::WireGuard |
state | DeviceState | Current state |
interface | Option<String> | Interface name |
gateway | Option<String> | VPN gateway address |
ip4_address | Option<String> | Assigned IPv4 address |
ip6_address | Option<String> | Assigned IPv6 address |
dns_servers | Vec<String> | Active DNS servers |
details | Option<VpnDetails> | Additional VPN-specific details |
Note:
get_vpn_info()returnsConnectionError::NoVpnConnectionif the VPN is not currently active.
Connecting to a Saved VPN
Reconnect to an existing VPN profile by name or UUID without rebuilding the config:
#![allow(unused)] fn main() { let nm = NetworkManager::new().await?; // By profile name nm.connect_vpn_by_id("MyVPN").await?; // By UUID nm.connect_vpn_by_uuid("a1b2c3d4-e5f6-7890-abcd-ef1234567890").await?; }
Disconnecting a VPN
#![allow(unused)] fn main() { let nm = NetworkManager::new().await?; // By name nm.disconnect_vpn("MyVPN").await?; // By UUID nm.disconnect_vpn_by_uuid("a1b2c3d4-e5f6-7890-abcd-ef1234567890").await?; println!("VPN disconnected"); }
disconnect_vpn() deactivates the VPN connection by name. If the VPN is not currently active or doesn't exist, it returns Ok(()) — the operation is idempotent.
Removing a VPN Profile
To permanently delete a saved VPN connection:
#![allow(unused)] fn main() { let nm = NetworkManager::new().await?; nm.forget_vpn("MyVPN").await?; println!("VPN profile deleted"); }
forget_vpn() searches for a saved VPN profile with the given name. If the VPN is currently connected, it disconnects first, then deletes the profile. Returns Ok(()) if no matching profile is found.
Complete Example
use nmrs::{NetworkManager, WireGuardConfig, WireGuardPeer}; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; // List existing VPNs println!("Saved VPN connections:"); for vpn in nm.list_vpn_connections().await? { println!(" {} ({:?}) - {:?} [active: {}]", vpn.name, vpn.vpn_type, vpn.state, vpn.active); } // Connect a new WireGuard VPN let peer = WireGuardPeer::new( "SERVER_PUBLIC_KEY", "vpn.example.com:51820", vec!["0.0.0.0/0".into()], ).with_persistent_keepalive(25); let config = WireGuardConfig::new( "ExampleVPN", "vpn.example.com:51820", "CLIENT_PRIVATE_KEY", "10.0.0.2/24", vec![peer], ).with_dns(vec!["1.1.1.1".into()]); nm.connect_vpn(config).await?; // Show details let info = nm.get_vpn_info("ExampleVPN").await?; println!("\nConnected: IP = {:?}", info.ip4_address); // Disconnect nm.disconnect_vpn("ExampleVPN").await?; println!("Disconnected"); // Clean up nm.forget_vpn("ExampleVPN").await?; println!("Profile removed"); Ok(()) }
Error Handling
| Error | Method | Meaning |
|---|---|---|
NoVpnConnection | get_vpn_info | VPN not active |
VpnFailed | connect_vpn | Connection activation failed |
InvalidPrivateKey | connect_vpn | Bad WireGuard key |
InvalidAddress | connect_vpn | Bad IP/CIDR |
InvalidGateway | connect_vpn | Bad endpoint format |
AuthFailed | connect_vpn | OpenVPN authentication failed |
InvalidConfig | connect_vpn | OpenVPN configuration error (missing certs, bad options) |
Next Steps
- WireGuard Setup — credential configuration details
- OpenVPN Setup — OpenVPN configuration details
- Error Handling — comprehensive error reference
- Real-Time Monitoring — monitor VPN state changes
Ethernet Management
nmrs supports wired (Ethernet) connections through NetworkManager. Ethernet connections are simpler than Wi-Fi since they don't require authentication in most cases.
Connecting
use nmrs::NetworkManager; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; nm.connect_wired().await?; println!("Ethernet connected!"); Ok(()) }
connect_wired() finds the first available wired device and either activates an existing saved connection or creates a new one with DHCP. The connection will activate when a cable is plugged in.
Listing Wired Devices
use nmrs::NetworkManager; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; let wired = nm.list_wired_devices().await?; for device in &wired { println!("{}: {} [{:?}]", device.interface, device.device_type, device.state, ); println!(" MAC: {}", device.identity.current_mac); if let Some(ip) = &device.ip4_address { println!(" IPv4: {}", ip); } if let Some(driver) = &device.driver { println!(" Driver: {}", driver); } } Ok(()) }
Errors
| Error | Meaning |
|---|---|
ConnectionError::NoWiredDevice | No Ethernet adapter found |
ConnectionError::Timeout | DHCP or activation took too long |
ConnectionError::DhcpFailed | Failed to obtain an IP address |
#![allow(unused)] fn main() { use nmrs::{NetworkManager, ConnectionError}; let nm = NetworkManager::new().await?; match nm.connect_wired().await { Ok(_) => println!("Connected"), Err(ConnectionError::NoWiredDevice) => { eprintln!("No Ethernet adapter found"); } Err(e) => eprintln!("Error: {}", e), } }
How It Works
When you call connect_wired():
- nmrs finds the first managed wired device
- Checks for an existing saved connection for that device
- If a saved connection exists, activates it
- If no saved connection exists, creates a new profile with DHCP and activates it
- Waits for the connection to reach
Activatedstate
The connection profile is saved for future use, so the device will auto-connect when a cable is plugged in.
Next Steps
- Device Management – list all network devices
- Connection Profiles – manage saved Ethernet profiles
- Error Handling – handle connection errors
Bluetooth
nmrs supports Bluetooth network connections through NetworkManager's Bluetooth integration with BlueZ. This covers Bluetooth PAN (Personal Area Network) and DUN (Dial-Up Networking) profiles.
Prerequisites
- BlueZ must be running (the Linux Bluetooth stack)
- The Bluetooth device must be paired using
bluetoothctlor another pairing tool before nmrs can connect - The Bluetooth adapter must be powered on
Listing Bluetooth Devices
use nmrs::NetworkManager; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; let devices = nm.list_bluetooth_devices().await?; for device in &devices { println!("{}", device); } Ok(()) }
The BluetoothDevice struct provides:
| Field | Type | Description |
|---|---|---|
bdaddr | String | Bluetooth MAC address |
name | Option<String> | Device name from BlueZ |
alias | Option<String> | User-friendly alias |
bt_caps | u32 | Bluetooth capability flags |
state | DeviceState | Current connection state |
The Display implementation shows devices as alias (role) [MAC].
Connecting
Connecting requires a device name and a BluetoothIdentity:
use nmrs::{NetworkManager, models::{BluetoothIdentity, BluetoothNetworkRole}}; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; let identity = BluetoothIdentity::new( "C8:1F:E8:F0:51:57".into(), BluetoothNetworkRole::PanU, )?; nm.connect_bluetooth("My Phone", &identity).await?; println!("Bluetooth connected!"); Ok(()) }
Network Roles
| Role | Description |
|---|---|
BluetoothNetworkRole::PanU | Personal Area Network User — most common for phone tethering |
BluetoothNetworkRole::Dun | Dial-Up Networking — for modem-style connections |
BluetoothIdentity Validation
BluetoothIdentity::new() validates the Bluetooth MAC address format. It returns a ConnectionError if the address is invalid.
Connecting to the First Available Device
A practical pattern is to list devices and connect to the first one:
use nmrs::{NetworkManager, models::BluetoothIdentity}; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; let devices = nm.list_bluetooth_devices().await?; if devices.is_empty() { println!("No Bluetooth devices found"); println!("Make sure the device is paired with bluetoothctl"); return Ok(()); } let device = &devices[0]; println!("Connecting to: {}", device); let identity = BluetoothIdentity::new( device.bdaddr.clone(), device.bt_caps.into(), )?; let name = device.alias.as_deref() .or(device.name.as_deref()) .unwrap_or("Bluetooth Device"); nm.connect_bluetooth(name, &identity).await?; println!("Connected!"); Ok(()) }
Forgetting a Bluetooth Connection
Remove a saved Bluetooth connection profile:
#![allow(unused)] fn main() { let nm = NetworkManager::new().await?; nm.forget_bluetooth("My Phone").await?; }
If the device is currently connected, it will be disconnected first before the profile is deleted.
Errors
| Error | Meaning |
|---|---|
ConnectionError::NoBluetoothDevice | No Bluetooth adapter found |
ConnectionError::InvalidAddress | Invalid Bluetooth MAC address format |
ConnectionError::Timeout | Connection took too long |
ConnectionError::NoSavedConnection | No matching profile found (for forget) |
Pairing Devices
nmrs does not handle Bluetooth pairing — that's the responsibility of BlueZ. Use bluetoothctl to pair devices before connecting with nmrs:
# Start bluetoothctl
bluetoothctl
# Scan for devices
scan on
# Pair with a device
pair C8:1F:E8:F0:51:57
# Trust the device (allows auto-reconnection)
trust C8:1F:E8:F0:51:57
# Exit
exit
After pairing, the device will appear in list_bluetooth_devices().
Next Steps
- Device Management – list all network devices including Bluetooth
- Connection Profiles – manage saved connections
- Error Handling – handle Bluetooth-specific errors
Device Management
nmrs provides methods to list, inspect, and control network devices managed by NetworkManager.
Listing All Devices
use nmrs::NetworkManager; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; let devices = nm.list_devices().await?; for device in &devices { println!("{}", device); // "wlan0 (Wi-Fi) [Activated]" } Ok(()) }
The Device Struct
Each device provides the following information:
| Field | Type | Description |
|---|---|---|
path | String | D-Bus object path |
interface | String | Interface name (e.g., wlan0, eth0) |
identity | DeviceIdentity | MAC addresses (permanent and current) |
device_type | DeviceType | Type of device |
state | DeviceState | Current operational state |
managed | Option<bool> | Whether NetworkManager manages this device |
driver | Option<String> | Kernel driver name |
ip4_address | Option<String> | IPv4 address with CIDR (when connected) |
ip6_address | Option<String> | IPv6 address with CIDR (when connected) |
Device Types
#![allow(unused)] fn main() { use nmrs::DeviceType; }
| Variant | Description |
|---|---|
DeviceType::Wifi | Wi-Fi (802.11) wireless adapter |
DeviceType::Ethernet | Wired Ethernet interface |
DeviceType::Bluetooth | Bluetooth network device |
DeviceType::WifiP2P | Wi-Fi Direct (peer-to-peer) |
DeviceType::Loopback | Loopback interface (localhost) |
DeviceType::Other(u32) | Unknown type with raw code |
Type Helper Methods
#![allow(unused)] fn main() { let device = &devices[0]; if device.is_wireless() { println!("{} is a Wi-Fi adapter", device.interface); } if device.is_wired() { println!("{} is an Ethernet interface", device.interface); } if device.is_bluetooth() { println!("{} is a Bluetooth device", device.interface); } }
DeviceType also provides capability queries:
#![allow(unused)] fn main() { let dt = &device.device_type; dt.supports_scanning(); // true for Wifi, WifiP2P dt.requires_specific_object(); // true for Wifi, WifiP2P dt.has_global_enabled_state(); // true for Wifi dt.connection_type_str(); // "802-11-wireless", "802-3-ethernet", etc. dt.to_code(); // raw NM type code (2 for Wifi, 1 for Ethernet) }
Device States
#![allow(unused)] fn main() { use nmrs::DeviceState; }
| State | Description |
|---|---|
Unmanaged | Not managed by NetworkManager |
Unavailable | Managed but not ready (e.g., Wi-Fi disabled) |
Disconnected | Available but not connected |
Prepare | Preparing to connect |
Config | Being configured |
NeedAuth | Waiting for credentials |
IpConfig | Requesting IP configuration |
IpCheck | Verifying IP connectivity |
Secondaries | Waiting for secondary connections |
Activated | Fully connected and operational |
Deactivating | Disconnecting |
Failed | Connection failed |
Other(u32) | Unknown state with raw code |
Transitional States
Use is_transitional() to check if a device is in a connecting or disconnecting state:
#![allow(unused)] fn main() { if device.state.is_transitional() { println!("{} is in a transitional state: {}", device.interface, device.state); } }
Transitional states include: Prepare, Config, NeedAuth, IpConfig, IpCheck, Secondaries, and Deactivating.
Filtered Device Lists
#![allow(unused)] fn main() { let nm = NetworkManager::new().await?; // Only wireless devices let wireless = nm.list_wireless_devices().await?; // Only wired devices let wired = nm.list_wired_devices().await?; // Only Bluetooth devices (returns BluetoothDevice, not Device) let bluetooth = nm.list_bluetooth_devices().await?; }
Wi-Fi Radio Control
Check and control the Wi-Fi radio globally:
#![allow(unused)] fn main() { let nm = NetworkManager::new().await?; // Check current state (software + hardware) let state = nm.wifi_state().await?; println!("Wi-Fi enabled: {}", state.enabled); println!("Wi-Fi hardware enabled: {}", state.hardware_enabled); // Global toggle nm.set_wireless_enabled(false).await?; // Disable nm.set_wireless_enabled(true).await?; // Enable }
Note:
wifi_state().hardware_enabledreflects the rfkill state. If the hardware switch is off, enabling Wi-Fi via software will have no effect.
For per-device Wi-Fi enable/disable, see Per-Device Scoping.
Waiting for Wi-Fi Ready
After enabling Wi-Fi, the device may take a moment to become ready:
#![allow(unused)] fn main() { let nm = NetworkManager::new().await?; nm.set_wireless_enabled(true).await?; nm.wait_for_wifi_ready().await?; // Now safe to scan and connect nm.scan_networks(None).await?; }
Finding a Device by Interface Name
#![allow(unused)] fn main() { let nm = NetworkManager::new().await?; let device_path = nm.get_device_by_interface("wlan0").await?; println!("D-Bus path: {}", device_path.as_str()); }
Device Identity
Each device has both a permanent (factory) and current MAC address:
#![allow(unused)] fn main() { for device in nm.list_devices().await? { println!("{}: permanent={}, current={}", device.interface, device.identity.permanent_mac, device.identity.current_mac, ); } }
If MAC randomization is enabled, the current MAC will differ from the permanent one.
Checking Connection Progress
Before starting a new connection, check if any device is currently connecting:
#![allow(unused)] fn main() { if nm.is_connecting().await? { println!("A connection operation is in progress"); } }
Next Steps
- WiFi Management – Wi-Fi-specific operations
- Bluetooth – Bluetooth device management
- Ethernet Management – wired connections
- Real-Time Monitoring – subscribe to device state changes
Connection Profiles
NetworkManager stores connection profiles for every network you've connected to. These profiles contain the configuration needed to reconnect — SSID, credentials, IP settings, and more. nmrs provides methods to list, query, and remove these profiles.
Listing Saved Connections
use nmrs::NetworkManager; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; let connections = nm.list_saved_connections().await?; for conn in &connections { println!(" {} ({})", conn.id, conn.connection_type); } Ok(()) }
list_saved_connections() returns full SavedConnection objects for all saved connection profiles across all connection types — Wi-Fi, Ethernet, VPN, and Bluetooth. Each SavedConnection includes the profile id (name), connection_type, and other metadata.
Checking for a Saved Connection
#![allow(unused)] fn main() { let nm = NetworkManager::new().await?; if nm.has_saved_connection("HomeWiFi").await? { println!("Profile exists for HomeWiFi"); } else { println!("No saved profile — credentials will be needed"); } }
How Saved Profiles Affect Connection
When you call connect() with an SSID that has a saved profile, nmrs activates the saved profile directly. This means:
- Credentials are already stored — the
WifiSecurityvalue you pass is ignored - Connection is faster — no need to create a new profile
- Settings are preserved — autoconnect, priority, and IP configuration are retained
#![allow(unused)] fn main() { let nm = NetworkManager::new().await?; // First connection — credentials are required and saved nm.connect("HomeWiFi", None, WifiSecurity::WpaPsk { psk: "password".into(), }).await?; // Later reconnection — saved profile is used, security parameter is ignored nm.connect("HomeWiFi", None, WifiSecurity::Open).await?; }
Forgetting (Deleting) Connections
Wi-Fi Connections
#![allow(unused)] fn main() { let nm = NetworkManager::new().await?; nm.forget("HomeWiFi").await?; println!("Wi-Fi profile deleted"); }
If currently connected to that network, forget() disconnects first, then deletes all saved profiles matching the SSID.
VPN Connections
#![allow(unused)] fn main() { nm.forget_vpn("MyVPN").await?; }
Bluetooth Connections
#![allow(unused)] fn main() { nm.forget_bluetooth("My Phone").await?; }
Getting the D-Bus Path
For advanced use cases, you can retrieve the D-Bus object path of a saved connection:
#![allow(unused)] fn main() { let nm = NetworkManager::new().await?; if let Some(path) = nm.get_saved_connection_path("HomeWiFi").await? { println!("D-Bus path: {}", path.as_str()); } }
Profile Lifecycle
- Created — when you first connect to a network, NetworkManager creates a profile
- Persisted — profiles are saved to
/etc/NetworkManager/system-connections/ - Reused — subsequent connections to the same SSID use the saved profile
- Updated — if you connect with different credentials, the profile may be updated
- Deleted — calling
forget(),forget_vpn(), orforget_bluetooth()removes it
Next Steps
- WiFi Management – scan and connect to Wi-Fi networks
- VPN Management – manage VPN profiles
- Bluetooth – Bluetooth connection profiles
Real-Time Monitoring
nmrs uses D-Bus signals to provide real-time notifications when network state changes. This is more efficient than polling — your callback fires only when something actually changes.
Network Change Monitoring
Subscribe to network changes (access points appearing or disappearing, or signal strength changing):
use nmrs::NetworkManager; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; // This runs indefinitely — spawn it as a background task nm.monitor_network_changes(|| { println!("Network list changed!"); }).await?; Ok(()) }
monitor_network_changes() subscribes to D-Bus signals for access point additions, removals, and signal strength updates on all Wi-Fi devices. The callback fires whenever the visible network list or signal data changes.
Device State Monitoring
Subscribe to device state changes (connected, disconnected, cable plugged in, etc.):
use nmrs::NetworkManager; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; nm.monitor_device_changes(|| { println!("Device state changed!"); }).await?; Ok(()) }
monitor_device_changes() subscribes to state change signals on all network devices — both wired and wireless.
Running Monitors as Background Tasks
Both monitoring functions run indefinitely. In a real application, spawn them as background tasks:
With Tokio
use nmrs::NetworkManager; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; // Spawn network monitor let nm_clone = nm.clone(); tokio::spawn(async move { if let Err(e) = nm_clone.monitor_network_changes(|| { println!("Networks changed"); }).await { eprintln!("Network monitor error: {}", e); } }); // Spawn device monitor let nm_clone = nm.clone(); tokio::spawn(async move { if let Err(e) = nm_clone.monitor_device_changes(|| { println!("Device state changed"); }).await { eprintln!("Device monitor error: {}", e); } }); // Your main application logic here loop { tokio::time::sleep(std::time::Duration::from_secs(60)).await; } }
With GTK/GLib (for GUI applications)
#![allow(unused)] fn main() { use nmrs::NetworkManager; // Inside a GTK application let nm = NetworkManager::new().await?; glib::MainContext::default().spawn_local({ let nm = nm.clone(); async move { let _ = nm.monitor_network_changes(|| { println!("Networks changed — refresh the UI!"); }).await; } }); glib::MainContext::default().spawn_local({ let nm = nm.clone(); async move { let _ = nm.monitor_device_changes(|| { println!("Device changed — update status!"); }).await; } }); }
Thread Safety
NetworkManager is Clone and can be safely shared across async tasks. Each clone shares the same underlying D-Bus connection, making it lightweight to pass into multiple monitoring tasks.
Practical Pattern: Refresh on Change
A common pattern is to refresh your application state whenever a change is detected:
use nmrs::NetworkManager; use std::sync::Arc; use tokio::sync::Notify; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; let notify = Arc::new(Notify::new()); // Monitor for changes let notify_clone = notify.clone(); let nm_clone = nm.clone(); tokio::spawn(async move { let _ = nm_clone.monitor_network_changes(move || { notify_clone.notify_one(); }).await; }); // React to changes loop { notify.notified().await; let networks = nm.list_networks(None).await?; println!("Updated: {} networks visible", networks.len()); } }
What Triggers Each Monitor
| Monitor | Triggers |
|---|---|
monitor_network_changes | Access point added, access point removed, signal strength change |
monitor_device_changes | Device state change (connected, disconnected, etc.), cable plug/unplug |
Next Steps
- Device Management – understand device states
- WiFi Management – scan and connect to networks
- Error Handling – handle monitoring errors
Error Handling
nmrs uses a single error type, ConnectionError, for all operations. Each variant describes a specific failure mode, making it straightforward to handle errors precisely.
The Result Type
nmrs re-exports a Result type alias:
#![allow(unused)] fn main() { pub type Result<T> = std::result::Result<T, ConnectionError>; }
All public API methods return nmrs::Result<T>.
ConnectionError Variants
Network & Wi-Fi Errors
| Variant | Description |
|---|---|
NotFound | Network not visible during scan |
AuthFailed | Wrong password or rejected credentials |
MissingPassword | Empty password provided |
NoWifiDevice | No Wi-Fi adapter found |
WifiNotReady | Wi-Fi device not ready in time |
WifiInterfaceNotFound | Specified Wi-Fi interface doesn't exist |
NotAWifiDevice | Interface exists but isn't Wi-Fi |
HardwareRadioKilled | Hardware kill switch is on |
NoWiredDevice | No Ethernet adapter found |
DhcpFailed | Failed to obtain an IP address via DHCP |
Timeout | Operation timed out waiting for activation |
Stuck(String) | Connection stuck in an unexpected state |
Authentication Errors
| Variant | Description |
|---|---|
SupplicantConfigFailed | wpa_supplicant configuration error |
SupplicantTimeout | wpa_supplicant timed out during auth |
VPN Errors
| Variant | Description |
|---|---|
NoVpnConnection | VPN not found or not active |
VpnFailed(String) | VPN connection failed with details |
VpnIdAmbiguous | Multiple VPNs share the same name |
IncompleteBuilder | VPN builder missing required fields |
InvalidPrivateKey(String) | Bad WireGuard private key |
InvalidPublicKey(String) | Bad WireGuard public key |
InvalidAddress(String) | Bad IP address or CIDR notation |
InvalidGateway(String) | Bad gateway format (host:port) |
InvalidPeers(String) | Invalid peer configuration |
Bluetooth Errors
| Variant | Description |
|---|---|
NoBluetoothDevice | No Bluetooth adapter found |
Profile Errors
| Variant | Description |
|---|---|
NoSavedConnection | No saved profile for the requested network |
Low-Level Errors
| Variant | Description |
|---|---|
Dbus(zbus::Error) | D-Bus communication error |
DbusOperation { context, source } | D-Bus error with context |
DeviceFailed(StateReason) | Device failure with NM reason code |
ActivationFailed(ConnectionStateReason) | Activation failure with reason |
InvalidUtf8(Utf8Error) | Invalid UTF-8 in SSID |
Basic Error Handling
Use the ? operator for simple propagation:
use nmrs::{NetworkManager, WifiSecurity}; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; nm.connect("MyWiFi", None, WifiSecurity::Open).await?; Ok(()) }
Pattern Matching
Handle specific errors differently:
#![allow(unused)] fn main() { use nmrs::{NetworkManager, WifiSecurity, ConnectionError}; let nm = NetworkManager::new().await?; match nm.connect("MyWiFi", None, WifiSecurity::WpaPsk { psk: "password".into(), }).await { Ok(_) => println!("Connected!"), Err(ConnectionError::NotFound) => { eprintln!("Network not in range"); } Err(ConnectionError::AuthFailed) => { eprintln!("Wrong password"); } Err(ConnectionError::Timeout) => { eprintln!("Connection timed out"); } Err(ConnectionError::DhcpFailed) => { eprintln!("Connected to AP but DHCP failed"); } Err(ConnectionError::NoWifiDevice) => { eprintln!("No Wi-Fi adapter found"); } Err(e) => eprintln!("Unexpected error: {}", e), } }
Retry Logic
Implement retries for transient failures:
#![allow(unused)] fn main() { use nmrs::{NetworkManager, WifiSecurity, ConnectionError}; let nm = NetworkManager::new().await?; for attempt in 1..=3 { match nm.connect("MyWiFi", None, WifiSecurity::WpaPsk { psk: "password".into(), }).await { Ok(_) => { println!("Connected on attempt {}", attempt); break; } Err(ConnectionError::Timeout) if attempt < 3 => { eprintln!("Attempt {} timed out, retrying...", attempt); continue; } Err(e) => return Err(e), } } }
VPN Error Handling
#![allow(unused)] fn main() { use nmrs::{NetworkManager, ConnectionError}; let nm = NetworkManager::new().await?; match nm.get_vpn_info("MyVPN").await { Ok(info) => println!("VPN IP: {:?}", info.ip4_address), Err(ConnectionError::NoVpnConnection) => { eprintln!("VPN is not active"); } Err(e) => eprintln!("Error: {}", e), } }
Converting to Other Error Types
ConnectionError implements std::error::Error and Display, so it works with error handling crates like anyhow:
#![allow(unused)] fn main() { use anyhow::Result; use nmrs::NetworkManager; async fn connect() -> Result<()> { let nm = NetworkManager::new().await?; nm.connect("MyWiFi", None, nmrs::WifiSecurity::Open).await?; Ok(()) } }
Radio / Airplane-mode Errors
| Variant | Description |
|---|---|
HardwareRadioKilled | Hardware kill switch is on; Wi-Fi cannot be enabled until the switch is toggled |
BluezUnavailable | Bluetooth D-Bus service (BlueZ) is not running or unreachable |
Non-Exhaustive
ConnectionError is marked #[non_exhaustive], which means new variants may be added in future versions without a breaking change. Always include a wildcard arm in match expressions:
#![allow(unused)] fn main() { match result { Err(ConnectionError::AuthFailed) => { /* ... */ } Err(ConnectionError::NotFound) => { /* ... */ } Err(e) => { /* catch-all for current and future variants */ } Ok(_) => {} } }
Next Steps
- WiFi Management – Wi-Fi-specific operations
- VPN Management – VPN-specific errors
- Custom Timeouts – prevent timeout errors
Async Runtime Support
nmrs is built on async Rust and uses zbus for D-Bus communication. While the examples in this book use Tokio, nmrs works with any async runtime.
Tokio (Recommended)
Tokio is the most commonly used runtime and the one used in all examples:
use nmrs::NetworkManager; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; let networks = nm.list_networks(None).await?; println!("{} networks found", networks.len()); Ok(()) }
Add to your Cargo.toml:
[dependencies]
nmrs = "2.2"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
Single-Threaded Tokio
For lightweight applications, you can use the current-thread runtime:
#[tokio::main(flavor = "current_thread")] async fn main() -> nmrs::Result<()> { let nm = nmrs::NetworkManager::new().await?; // ... Ok(()) }
async-std
use nmrs::NetworkManager; #[async_std::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; let networks = nm.list_networks(None).await?; println!("{} networks found", networks.len()); Ok(()) }
[dependencies]
nmrs = "2.2"
async-std = { version = "1", features = ["attributes"] }
smol
use nmrs::NetworkManager; fn main() -> nmrs::Result<()> { smol::block_on(async { let nm = NetworkManager::new().await?; let networks = nm.list_networks(None).await?; println!("{} networks found", networks.len()); Ok(()) }) }
[dependencies]
nmrs = "2.2"
smol = "2"
GLib/GTK (for GUI applications)
When building GTK4 applications, use the GLib main context:
#![allow(unused)] fn main() { use nmrs::NetworkManager; // Inside a GTK application's async context glib::MainContext::default().spawn_local(async { let nm = NetworkManager::new().await.unwrap(); let networks = nm.list_networks(None).await.unwrap(); for net in &networks { println!("{}: {}%", net.ssid, net.strength.unwrap_or(0)); } }); }
How It Works
nmrs uses zbus for D-Bus communication, which itself uses an internal async runtime. When you call NetworkManager::new().await, zbus establishes a connection to the system D-Bus. This connection is runtime-agnostic — zbus handles the async I/O internally.
The NetworkManager struct is Clone and Send, so you can share it across tasks regardless of which runtime you use.
Thread Safety
NetworkManager is:
Clone— clones share the same D-Bus connection (cheap)Send— can be moved across threadsSync— can be shared viaArc(thoughCloneis usually simpler)
However, concurrent connection operations are not supported. Don't call connect() from multiple tasks simultaneously. Use is_connecting() to check if a connection is in progress.
Next Steps
- Custom Timeouts – configure operation timeouts
- D-Bus Architecture – understand the D-Bus layer
Custom Timeouts
nmrs uses timeouts to prevent operations from hanging indefinitely. You can customize these timeouts for different network environments.
Default Timeouts
| Timeout | Default | Purpose |
|---|---|---|
connection_timeout | 30 seconds | How long to wait for a connection to activate |
disconnect_timeout | 10 seconds | How long to wait for a device to disconnect |
Creating Custom Timeouts
Use TimeoutConfig with the builder pattern:
use nmrs::{NetworkManager, TimeoutConfig}; use std::time::Duration; #[tokio::main] async fn main() -> nmrs::Result<()> { let config = TimeoutConfig::new() .with_connection_timeout(Duration::from_secs(60)) .with_disconnect_timeout(Duration::from_secs(20)); let nm = NetworkManager::with_config(config).await?; println!("Connection timeout: {:?}", nm.timeout_config().connection_timeout); println!("Disconnect timeout: {:?}", nm.timeout_config().disconnect_timeout); Ok(()) }
When to Increase Timeouts
Enterprise Wi-Fi (WPA-EAP)
802.1X authentication involves multiple round trips to a RADIUS server and can take significantly longer than WPA-PSK:
#![allow(unused)] fn main() { let config = TimeoutConfig::new() .with_connection_timeout(Duration::from_secs(60)); let nm = NetworkManager::with_config(config).await?; }
Slow DHCP Servers
Some networks have slow or overloaded DHCP servers:
#![allow(unused)] fn main() { let config = TimeoutConfig::new() .with_connection_timeout(Duration::from_secs(45)); }
VPN Connections
VPN connections through distant servers may need extra time:
#![allow(unused)] fn main() { let config = TimeoutConfig::new() .with_connection_timeout(Duration::from_secs(45)); }
When to Decrease Timeouts
For fast-fail scenarios where you want quick feedback:
#![allow(unused)] fn main() { let config = TimeoutConfig::new() .with_connection_timeout(Duration::from_secs(10)) .with_disconnect_timeout(Duration::from_secs(5)); let nm = NetworkManager::with_config(config).await?; }
Reading Current Configuration
#![allow(unused)] fn main() { let nm = NetworkManager::new().await?; let config = nm.timeout_config(); println!("Connection timeout: {:?}", config.connection_timeout); println!("Disconnect timeout: {:?}", config.disconnect_timeout); }
Timeout Errors
When a timeout is exceeded, nmrs returns ConnectionError::Timeout:
#![allow(unused)] fn main() { use nmrs::{NetworkManager, WifiSecurity, ConnectionError, TimeoutConfig}; use std::time::Duration; let config = TimeoutConfig::new() .with_connection_timeout(Duration::from_secs(10)); let nm = NetworkManager::with_config(config).await?; match nm.connect("SlowNetwork", None, WifiSecurity::Open).await { Ok(_) => println!("Connected!"), Err(ConnectionError::Timeout) => { eprintln!("Connection timed out — try a longer timeout"); } Err(e) => eprintln!("Error: {}", e), } }
What Timeouts Affect
Timeouts apply to all operations that wait for NetworkManager state transitions:
connect()— Wi-Fi connection activationconnect_wired()— Ethernet connection activationconnect_bluetooth()— Bluetooth connection activationconnect_vpn()— VPN connection activationdisconnect()— Wi-Fi disconnection
The disconnect_timeout applies to the waiting period after requesting disconnection.
Next Steps
- Connection Options – configure autoconnect behavior
- Error Handling – handle timeout errors
Connection Options
ConnectionOptions controls how NetworkManager handles saved connection profiles — specifically, automatic connection behavior, priority, and retry limits.
Default Options
#![allow(unused)] fn main() { use nmrs::ConnectionOptions; let opts = ConnectionOptions::default(); // autoconnect: true // autoconnect_priority: None (NM default = 0) // autoconnect_retries: None (unlimited) }
Configuration Fields
| Field | Type | Default | Description |
|---|---|---|---|
autoconnect | bool | true | Connect automatically when available |
autoconnect_priority | Option<i32> | None (0) | Higher values are preferred when multiple networks are available |
autoconnect_retries | Option<i32> | None (unlimited) | Maximum retry attempts before giving up |
Creating Options
Enable Autoconnect (Default)
#![allow(unused)] fn main() { use nmrs::ConnectionOptions; let opts = ConnectionOptions::new(true); }
Disable Autoconnect
#![allow(unused)] fn main() { let opts = ConnectionOptions::new(false); }
High-Priority Connection
#![allow(unused)] fn main() { let opts = ConnectionOptions::new(true) .with_priority(10) .with_retries(3); }
Higher priority values make NetworkManager prefer this connection over others when multiple are available.
How Priority Works
When multiple saved connections are available (e.g., you're in range of both "HomeWiFi" and "CafeWiFi"), NetworkManager connects to the one with the highest autoconnect_priority. If priorities are equal, NetworkManager uses its own heuristics (most recently used, signal strength, etc.).
| Priority | Use Case |
|---|---|
| 0 (default) | Normal connections |
| 1–10 | Preferred connections |
| -1 to -10 | Fallback connections |
How Retries Work
autoconnect_retries limits how many times NetworkManager will try to auto-connect a failing connection:
None(default) — unlimited retriesSome(0)— never auto-retrySome(3)— try up to 3 times, then stop
This is useful for connections that might intermittently fail (e.g., a network at the edge of range).
Using with Builders
Connection options are used by the low-level builders:
#![allow(unused)] fn main() { use nmrs::builders::ConnectionBuilder; use nmrs::ConnectionOptions; let opts = ConnectionOptions::new(true) .with_priority(5) .with_retries(3); let settings = ConnectionBuilder::new("802-11-wireless", "MyNetwork") .options(&opts) .ipv4_auto() .ipv6_auto() .build(); }
The high-level NetworkManager API uses ConnectionOptions::default() internally. For custom options, use the builder APIs directly.
Next Steps
- Custom Timeouts – control how long operations wait
- Builders Module – low-level connection building
- D-Bus Architecture – how settings are sent to NetworkManager
D-Bus Architecture
nmrs communicates with NetworkManager over the D-Bus system bus. Understanding this architecture helps with debugging and explains why certain operations work the way they do.
Overview
┌─────────────┐ D-Bus (system bus) ┌──────────────────┐
│ Your App │ ◄────────────────────────► │ NetworkManager │
│ (nmrs) │ │ Daemon │
└─────────────┘ └──────────────────┘
│ │
│ zbus (Rust D-Bus library) │
│ │
▼ ▼
nmrs::dbus module D-Bus interfaces:
(proxy types) - org.freedesktop.NetworkManager
- org.freedesktop.NetworkManager.Device
- org.freedesktop.NetworkManager.Device.Wireless
- org.freedesktop.NetworkManager.AccessPoint
- org.freedesktop.NetworkManager.Connection.Active
- org.freedesktop.NetworkManager.Settings
- ...
How nmrs Uses D-Bus
Connection Establishment
When you call NetworkManager::new(), nmrs connects to the system D-Bus using zbus:
#![allow(unused)] fn main() { let nm = NetworkManager::new().await?; // Internally: zbus::Connection::system().await }
This creates a persistent D-Bus connection that's shared across all operations.
Method Calls
API methods like list_devices() translate to D-Bus method calls:
nmrs: nm.list_devices()
→ D-Bus: GetDevices() on org.freedesktop.NetworkManager
← D-Bus: Array of device object paths
→ D-Bus: Get properties for each device path
← D-Bus: Device properties (interface, type, state, etc.)
→ nmrs: Vec<Device>
Signal Monitoring
monitor_network_changes() subscribes to D-Bus signals:
nmrs: nm.monitor_network_changes(callback)
→ D-Bus: Subscribe to AccessPointAdded/Removed and AP Strength changes
← D-Bus: Signal whenever an AP appears, disappears, or changes strength
→ nmrs: Invoke callback
Connection Settings
When connecting to a network, nmrs builds a settings dictionary and sends it via D-Bus:
nmrs: nm.connect("MyWiFi", None, WifiSecurity::WpaPsk { psk: "..." })
→ Build settings HashMap
→ D-Bus: AddAndActivateConnection(settings, device_path, specific_object)
← D-Bus: Active connection path
→ D-Bus: Monitor StateChanged signal
← D-Bus: State transitions until Activated or Failed
→ nmrs: Ok(()) or Err(ConnectionError)
D-Bus Proxy Types
nmrs wraps D-Bus interfaces in typed proxy structs (defined in nmrs::dbus):
| Proxy | D-Bus Interface | Purpose |
|---|---|---|
NMProxy | org.freedesktop.NetworkManager | Main NM interface |
NMDeviceProxy | org.freedesktop.NetworkManager.Device | Device properties and control |
NMWirelessProxy | org.freedesktop.NetworkManager.Device.Wireless | Wi-Fi scanning, AP list |
NMAccessPointProxy | org.freedesktop.NetworkManager.AccessPoint | AP signal, SSID, security |
NMActiveConnectionProxy | org.freedesktop.NetworkManager.Connection.Active | Active connection state |
NMWiredProxy | org.freedesktop.NetworkManager.Device.Wired | Wired device properties |
NMBluetoothProxy | org.freedesktop.NetworkManager.Device.Bluetooth | Bluetooth properties |
These are internal types — you interact with them through the high-level NetworkManager API.
D-Bus Errors
D-Bus communication errors surface as ConnectionError::Dbus or ConnectionError::DbusOperation:
#![allow(unused)] fn main() { use nmrs::ConnectionError; match result { Err(ConnectionError::Dbus(e)) => { eprintln!("D-Bus error: {}", e); } Err(ConnectionError::DbusOperation { context, source }) => { eprintln!("{}: {}", context, source); } _ => {} } }
Common causes:
- NetworkManager is not running
- D-Bus system bus is not available
- Insufficient permissions (PolicyKit)
Permissions
NetworkManager uses PolicyKit for authorization. Most operations require the user to be in the network group or to have appropriate PolicyKit rules. See Requirements for setup details.
Debugging D-Bus
Monitor D-Bus Traffic
Use dbus-monitor to see raw D-Bus messages:
sudo dbus-monitor --system "interface='org.freedesktop.NetworkManager'"
Check NetworkManager State
nmcli general status
nmcli device status
nmcli connection show
Verify D-Bus Service
busctl list | grep NetworkManager
Next Steps
- Logging and Debugging – enable nmrs debug logging
- Architecture – internal code structure
Logging and Debugging
nmrs uses the log crate for structured logging. You can enable logging to see what nmrs is doing internally, which is invaluable for debugging connection issues.
Enabling Logging
nmrs produces log messages but doesn't configure a logger — that's up to your application. The simplest option is env_logger:
[dependencies]
nmrs = "2.2"
env_logger = "0.11"
log = "0.4"
use nmrs::NetworkManager; #[tokio::main] async fn main() -> nmrs::Result<()> { env_logger::init(); let nm = NetworkManager::new().await?; nm.scan_networks(None).await?; Ok(()) }
Run with:
RUST_LOG=nmrs=debug cargo run
Log Levels
| Level | Content |
|---|---|
error | Connection failures, D-Bus errors |
warn | Unexpected states, fallback behavior |
info | Connection events, state transitions |
debug | D-Bus method calls, scan results, settings |
trace | Raw D-Bus messages, detailed internal state |
Level Examples
# Only errors
RUST_LOG=nmrs=error cargo run
# Info and above
RUST_LOG=nmrs=info cargo run
# Full debug output
RUST_LOG=nmrs=debug cargo run
# Everything including D-Bus internals
RUST_LOG=nmrs=trace cargo run
# Debug nmrs + info for zbus
RUST_LOG=nmrs=debug,zbus=info cargo run
Debugging Connection Issues
When a connection fails, enable debug logging to see the full sequence:
RUST_LOG=nmrs=debug cargo run
This will show:
- Which device was selected
- Whether a saved connection was found
- The settings dictionary sent to NetworkManager
- State transitions during activation
- The specific error or reason for failure
Debugging D-Bus Issues
If you suspect a D-Bus communication problem, enable trace logging for both nmrs and zbus:
RUST_LOG=nmrs=trace,zbus=debug cargo run
You can also use system tools:
# Monitor NetworkManager D-Bus traffic
sudo dbus-monitor --system "interface='org.freedesktop.NetworkManager'"
# Check NetworkManager journal logs
journalctl -u NetworkManager -f
# Check wpa_supplicant logs (for Wi-Fi auth issues)
journalctl -u wpa_supplicant -f
Using with Other Loggers
nmrs works with any logger that implements the log facade:
tracing (with compatibility layer)
[dependencies]
tracing = "0.1"
tracing-subscriber = "0.3"
tracing-log = "0.2"
use tracing_subscriber; fn main() { tracing_subscriber::fmt() .with_env_filter("nmrs=debug") .init(); // ... }
simplelog
[dependencies]
simplelog = "0.12"
use simplelog::*; fn main() { TermLogger::init( LevelFilter::Debug, Config::default(), TerminalMode::Mixed, ColorChoice::Auto, ).unwrap(); // ... }
Next Steps
- D-Bus Architecture – understand the communication layer
- Troubleshooting – common issues and fixes
Basic WiFi Scanner
This example demonstrates building a simple but complete WiFi network scanner using nmrs.
Features
- Lists all available WiFi networks
- Shows signal strength with visual indicators
- Displays security types
- Filters by signal strength
- Auto-refreshes every few seconds
Complete Code
use nmrs::{NetworkManager, models::Network}; use std::time::Duration; use tokio::time::sleep; #[tokio::main] async fn main() -> nmrs::Result<()> { // Initialize NetworkManager let nm = NetworkManager::new().await?; println!("WiFi Network Scanner"); println!("===================\n"); // Scan loop loop { // Clear screen (Unix/Linux) print!("\x1B[2J\x1B[1;1H"); // Get networks let mut networks = nm.list_networks(None).await?; // Sort by signal strength (strongest first) networks.sort_by(|a, b| { b.strength.unwrap_or(0).cmp(&a.strength.unwrap_or(0)) }); // Display header println!("WiFi Network Scanner - {} networks found\n", networks.len()); println!("{:<30} {:>10} {:>8} {:<20}", "SSID", "Signal", "Band", "Security"); println!("{}", "-".repeat(70)); // Display each network for net in networks { print_network(&net); } println!("\n{}", "-".repeat(70)); println!("Press Ctrl+C to exit"); // Wait before next scan sleep(Duration::from_secs(5)).await; } } fn print_network(net: &Network) { let signal = net.strength.unwrap_or(0); let signal_bar = signal_strength_bar(signal); let band = match net.frequency { Some(freq) if freq > 5000 => "5GHz", Some(_) => "2.4GHz", None => "Unknown", }; let security = match &net.security { nmrs::WifiSecurity::Open => "Open", nmrs::WifiSecurity::WpaPsk { .. } => "WPA-PSK", nmrs::WifiSecurity::WpaEap { .. } => "WPA-EAP", }; println!("{:<30} {:>3}% {} {:>8} {:<20}", truncate_ssid(&net.ssid, 30), signal, signal_bar, band, security ); } fn signal_strength_bar(strength: u8) -> String { let bars = match strength { 80..=100 => "▂▄▆█", 60..=79 => "▂▄▆▁", 40..=59 => "▂▄▁▁", 20..=39 => "▂▁▁▁", _ => "▁▁▁▁", }; let color = match strength { 70..=100 => "\x1b[32m", // Green 40..=69 => "\x1b[33m", // Yellow _ => "\x1b[31m", // Red }; format!("{}{}\x1b[0m", color, bars) } fn truncate_ssid(ssid: &str, max_len: usize) -> String { if ssid.len() <= max_len { ssid.to_string() } else { format!("{}...", &ssid[..max_len - 3]) } }
Running the Example
Add to your Cargo.toml:
[dependencies]
nmrs = "2.0.0"
tokio = { version = "1", features = ["full"] }
Run with:
cargo run
Sample Output
WiFi Network Scanner - 8 networks found
SSID Signal Band Security
----------------------------------------------------------------------
MyHomeNetwork 92% ▂▄▆█ 5GHz WPA-PSK
CoffeeShop_Guest 78% ▂▄▆▁ 2.4GHz Open
Neighbor-5G 65% ▂▄▆▁ 5GHz WPA-PSK
Corporate_WiFi 58% ▂▄▁▁ 5GHz WPA-EAP
Guest_Network 45% ▂▄▁▁ 2.4GHz Open
FarAwayNetwork 22% ▂▁▁▁ 2.4GHz WPA-PSK
----------------------------------------------------------------------
Press Ctrl+C to exit
Enhancements
Filter by Signal Strength
#![allow(unused)] fn main() { // Only show networks with signal > 30% let networks: Vec<_> = networks .into_iter() .filter(|n| n.strength.unwrap_or(0) > 30) .collect(); }
Group by Frequency Band
#![allow(unused)] fn main() { let mut networks_2_4ghz = Vec::new(); let mut networks_5ghz = Vec::new(); for net in networks { match net.frequency { Some(freq) if freq > 5000 => networks_5ghz.push(net), Some(_) => networks_2_4ghz.push(net), None => {} } } println!("\n5GHz Networks:"); for net in networks_5ghz { print_network(&net); } println!("\n2.4GHz Networks:"); for net in networks_2_4ghz { print_network(&net); } }
Add Connection Capability
#![allow(unused)] fn main() { use std::io::{self, Write}; // After displaying networks print!("\nEnter number to connect (or 0 to skip): "); io::stdout().flush()?; let mut input = String::new(); io::stdin().read_line(&mut input)?; if let Ok(choice) = input.trim().parse::<usize>() { if choice > 0 && choice <= networks.len() { let selected = &networks[choice - 1]; // Get password if needed match &selected.security { nmrs::WifiSecurity::Open => { nm.connect(&selected.ssid, None, nmrs::WifiSecurity::Open).await?; println!("Connected to {}", selected.ssid); } _ => { print!("Enter password: "); io::stdout().flush()?; let mut password = String::new(); io::stdin().read_line(&mut password)?; nm.connect(&selected.ssid, None, nmrs::WifiSecurity::WpaPsk { psk: password.trim().to_string() }).await?; println!("Connected to {}", selected.ssid); } } } } }
Export to JSON
#![allow(unused)] fn main() { use serde::{Serialize, Deserialize}; #[derive(Serialize)] struct NetworkExport { ssid: String, signal: u8, frequency: Option<u32>, security: String, } // Convert networks to exportable format let exports: Vec<NetworkExport> = networks.iter().map(|n| { NetworkExport { ssid: n.ssid.clone(), signal: n.strength.unwrap_or(0), frequency: n.frequency, security: format!("{:?}", n.security), } }).collect(); // Write to file let json = serde_json::to_string_pretty(&exports)?; std::fs::write("networks.json", json)?; println!("Exported to networks.json"); }
Related Examples
See Also
WiFi Auto-Connect
This example demonstrates a program that automatically connects to a preferred network from a priority-ordered list.
Features
- Scans for available networks
- Matches against a list of preferred networks (in priority order)
- Connects to the highest-priority available network
- Falls back to lower-priority networks if the preferred one isn't found
Code
use nmrs::{NetworkManager, WifiSecurity, ConnectionError}; use std::collections::HashMap; struct PreferredNetwork { ssid: String, security: WifiSecurity, } #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; // Define preferred networks in priority order (highest first) let preferred = vec![ PreferredNetwork { ssid: "HomeWiFi".into(), security: WifiSecurity::WpaPsk { psk: std::env::var("HOME_WIFI_PSK").unwrap_or_default(), }, }, PreferredNetwork { ssid: "OfficeWiFi".into(), security: WifiSecurity::WpaPsk { psk: std::env::var("OFFICE_WIFI_PSK").unwrap_or_default(), }, }, PreferredNetwork { ssid: "CafeOpen".into(), security: WifiSecurity::Open, }, ]; // Check if already connected if let Some(ssid) = nm.current_ssid().await { if preferred.iter().any(|p| p.ssid == ssid) { println!("Already connected to preferred network: {}", ssid); return Ok(()); } } // Scan and list visible networks println!("Scanning for networks..."); nm.scan_networks(None).await?; let visible = nm.list_networks(None).await?; let visible_ssids: HashMap<&str, &nmrs::Network> = visible .iter() .map(|n| (n.ssid.as_str(), n)) .collect(); // Try each preferred network in order for pref in &preferred { if let Some(net) = visible_ssids.get(pref.ssid.as_str()) { println!( "Found '{}' ({}%) — connecting...", pref.ssid, net.strength.unwrap_or(0), ); match nm.connect(&pref.ssid, None, pref.security.clone()).await { Ok(_) => { println!("Connected to '{}'!", pref.ssid); return Ok(()); } Err(ConnectionError::AuthFailed) => { eprintln!("Auth failed for '{}', trying next...", pref.ssid); continue; } Err(e) => { eprintln!("Failed to connect to '{}': {}", pref.ssid, e); continue; } } } } eprintln!("No preferred networks found"); Ok(()) }
Running
HOME_WIFI_PSK="my_home_password" OFFICE_WIFI_PSK="office_pass" cargo run --example wifi_auto_connect
How It Works
- Checks if already connected to a preferred network
- Scans for visible networks
- Iterates through the preferred list in order
- Attempts to connect to the first match
- On auth failure, tries the next preferred network
- Reports if no preferred network was found
Enhancements
- Persistent loop: Wrap in a loop with a timer to continuously monitor and reconnect
- Signal threshold: Skip networks below a minimum signal strength
- Saved profiles: Check
has_saved_connection()first to avoid needing passwords - Monitoring: Use
monitor_network_changes()to react to new networks appearing
Enterprise WiFi
This example connects to a WPA-Enterprise (802.1X) network using PEAP/MSCHAPv2 — the most common configuration in corporate environments.
Features
- Builds EAP options with the builder pattern
- Configures certificate validation
- Uses extended timeouts for enterprise authentication
- Handles authentication-specific errors
Code
use nmrs::{ EapMethod, EapOptions, NetworkManager, Phase2, TimeoutConfig, WifiSecurity, ConnectionError, }; use std::time::Duration; #[tokio::main] async fn main() -> nmrs::Result<()> { // Enterprise auth can be slow — use a longer timeout let config = TimeoutConfig::new() .with_connection_timeout(Duration::from_secs(60)); let nm = NetworkManager::with_config(config).await?; // Build EAP configuration let eap = EapOptions::builder() .identity( std::env::var("EAP_IDENTITY") .expect("Set EAP_IDENTITY (e.g., user@company.com)"), ) .password( std::env::var("EAP_PASSWORD") .expect("Set EAP_PASSWORD"), ) .method(EapMethod::Peap) .phase2(Phase2::Mschapv2) .anonymous_identity("anonymous@company.com") .domain_suffix_match("company.com") .system_ca_certs(true) .build(); let ssid = std::env::var("EAP_SSID") .unwrap_or_else(|_| "CorpWiFi".into()); println!("Connecting to enterprise network '{}'...", ssid); match nm.connect(&ssid, None, WifiSecurity::WpaEap { opts: eap }).await { Ok(_) => { println!("Connected!"); if let Some((ssid, freq)) = nm.current_connection_info().await { let band = match freq { Some(f) if f > 5000 => "5 GHz", Some(_) => "2.4 GHz", None => "unknown", }; println!(" Network: {} ({})", ssid, band); } } Err(ConnectionError::AuthFailed) => { eprintln!("Authentication failed — check username and password"); } Err(ConnectionError::SupplicantConfigFailed) => { eprintln!("Supplicant config error — check EAP method and phase2"); } Err(ConnectionError::SupplicantTimeout) => { eprintln!("RADIUS server not responding — check CA cert and domain"); } Err(ConnectionError::Timeout) => { eprintln!("Connection timed out — enterprise auth may need more time"); } Err(e) => eprintln!("Error: {}", e), } Ok(()) }
Running
EAP_IDENTITY="user@company.com" \
EAP_PASSWORD="my_password" \
EAP_SSID="CorpWiFi" \
cargo run --example enterprise_wifi
Variations
TTLS/PAP Configuration
Some networks use TTLS with PAP instead of PEAP:
#![allow(unused)] fn main() { let eap = EapOptions::builder() .identity("student@university.edu") .password("password") .method(EapMethod::Ttls) .phase2(Phase2::Pap) .ca_cert_path("file:///etc/ssl/certs/university-ca.pem") .build(); }
Custom CA Certificate
If your organization provides a specific CA certificate:
#![allow(unused)] fn main() { let eap = EapOptions::builder() .identity("user@company.com") .password("password") .method(EapMethod::Peap) .phase2(Phase2::Mschapv2) .ca_cert_path("file:///usr/local/share/ca-certificates/corp-ca.pem") .domain_suffix_match("company.com") .build(); }
Common Issues
| Problem | Solution |
|---|---|
AuthFailed | Verify username format (email vs plain username) and password |
SupplicantConfigFailed | Check EAP method — ask IT which to use |
SupplicantTimeout | Verify CA cert path and domain suffix match |
| Connection is slow | Increase timeout with TimeoutConfig |
WireGuard VPN Client
This example demonstrates a complete WireGuard VPN client that connects, displays connection information, and cleanly disconnects.
Features
- Builds VPN configuration with
WireGuardConfig - Connects and retrieves VPN details
- Displays IP configuration and DNS
- Cleanly disconnects on completion
Code
use nmrs::{NetworkManager, WireGuardConfig, WireGuardPeer}; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; // Check for existing VPN connections let existing = nm.list_vpn_connections().await?; if !existing.is_empty() { println!("Existing VPN connections:"); for vpn in &existing { println!(" {} ({:?}) — {:?}", vpn.name, vpn.vpn_type, vpn.state); } println!(); } // Build WireGuard peer configuration let peer = WireGuardPeer::new( std::env::var("WG_SERVER_PUBKEY") .expect("Set WG_SERVER_PUBKEY"), std::env::var("WG_ENDPOINT") .unwrap_or_else(|_| "vpn.example.com:51820".into()), vec!["0.0.0.0/0".into()], ) .with_persistent_keepalive(25); // Build configuration let config = WireGuardConfig::new( "ExampleVPN", std::env::var("WG_ENDPOINT") .unwrap_or_else(|_| "vpn.example.com:51820".into()), std::env::var("WG_PRIVATE_KEY") .expect("Set WG_PRIVATE_KEY"), std::env::var("WG_ADDRESS") .unwrap_or_else(|_| "10.0.0.2/24".into()), vec![peer], ) .with_dns(vec!["1.1.1.1".into(), "8.8.8.8".into()]) .with_mtu(1420); // Connect println!("Connecting to VPN..."); nm.connect_vpn(config).await?; println!("Connected!\n"); // Show VPN details let info = nm.get_vpn_info("ExampleVPN").await?; println!("VPN Connection Details:"); println!(" Name: {}", info.name); println!(" Kind: {:?}", info.vpn_kind); println!(" State: {:?}", info.state); println!(" Interface: {:?}", info.interface); println!(" Gateway: {:?}", info.gateway); println!(" IPv4: {:?}", info.ip4_address); println!(" IPv6: {:?}", info.ip6_address); println!(" DNS: {:?}", info.dns_servers); // Wait for user input before disconnecting println!("\nPress Enter to disconnect..."); let mut input = String::new(); std::io::stdin().read_line(&mut input).ok(); // Disconnect nm.disconnect_vpn("ExampleVPN").await?; println!("VPN disconnected"); // Optionally remove the profile // nm.forget_vpn("ExampleVPN").await?; Ok(()) }
Running
WG_SERVER_PUBKEY="HIgo9xNzJMWLKAShlKl6/bUT1VI9Q0SDBXGtLXkPFXc=" \
WG_PRIVATE_KEY="YBk6X3pP8KjKz7+HFWzVHNqL3qTZq8hX9VxFQJ4zVmM=" \
WG_ENDPOINT="vpn.example.com:51820" \
WG_ADDRESS="10.0.0.2/24" \
cargo run --example wireguard_client
Sample Output
Connecting to VPN...
Connected!
VPN Connection Details:
Name: ExampleVPN
Kind: WireGuard
State: Activated
Interface: Some("wg-examplevpn")
Gateway: Some("vpn.example.com")
IPv4: Some("10.0.0.2/24")
IPv6: None
DNS: ["1.1.1.1", "8.8.8.8"]
Press Enter to disconnect...
Split Tunnel Variation
Route only specific subnets through the VPN:
#![allow(unused)] fn main() { let peer = WireGuardPeer::new( "server_public_key", "vpn.example.com:51820", vec![ "10.0.0.0/8".into(), "192.168.0.0/16".into(), ], ); }
Multiple Peers
Connect through multiple WireGuard servers:
#![allow(unused)] fn main() { let peer1 = WireGuardPeer::new( "peer1_pubkey", "us-east.vpn.example.com:51820", vec!["10.1.0.0/16".into()], ); let peer2 = WireGuardPeer::new( "peer2_pubkey", "eu-west.vpn.example.com:51820", vec!["10.2.0.0/16".into()], ); let config = WireGuardConfig::new( "MultiPeerVPN", "us-east.vpn.example.com:51820", "client_private_key", "10.0.0.2/24", vec![peer1, peer2], ); }
OpenVPN Client
This example demonstrates a complete OpenVPN client that authenticates with password+TLS, connects, displays VPN details, and cleanly disconnects.
Features
- Builds OpenVPN config with password+TLS authentication
- Reads credentials from environment variables
- Connects and retrieves VPN details
- Displays IP configuration, DNS, and gateway
- Cleanly disconnects on completion
Code
use nmrs::{NetworkManager, OpenVpnConfig, OpenVpnAuthType}; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; // Read credentials from environment let remote = std::env::var("OVPN_REMOTE") .unwrap_or_else(|_| "vpn.example.com".into()); let port: u16 = std::env::var("OVPN_PORT") .unwrap_or_else(|_| "1194".into()) .parse() .expect("OVPN_PORT must be a valid port number"); let username = std::env::var("OVPN_USER") .expect("Set OVPN_USER"); let password = std::env::var("OVPN_PASS") .expect("Set OVPN_PASS"); let ca_path = std::env::var("OVPN_CA") .unwrap_or_else(|_| "/etc/openvpn/ca.crt".into()); let cert_path = std::env::var("OVPN_CERT") .unwrap_or_else(|_| "/etc/openvpn/client.crt".into()); let key_path = std::env::var("OVPN_KEY") .unwrap_or_else(|_| "/etc/openvpn/client.key".into()); // Build OpenVPN config (password+TLS) let config = OpenVpnConfig::new("CorpVPN", &remote, port, false) .with_auth_type(OpenVpnAuthType::PasswordTls) .with_username(&username) .with_password(&password) .with_ca_cert(&ca_path) .with_client_cert(&cert_path) .with_client_key(&key_path); // Connect println!("Connecting to OpenVPN ({remote}:{port})..."); nm.connect_vpn(config).await?; println!("Connected!\n"); // Show VPN details let info = nm.get_vpn_info("CorpVPN").await?; println!("VPN Connection Details:"); println!(" Name: {}", info.name); println!(" Kind: {:?}", info.vpn_kind); println!(" State: {:?}", info.state); println!(" Interface: {:?}", info.interface); println!(" Gateway: {:?}", info.gateway); println!(" IPv4: {:?}", info.ip4_address); println!(" IPv6: {:?}", info.ip6_address); println!(" DNS: {:?}", info.dns_servers); if let Some(details) = &info.details { println!(" Details: {:?}", details); } // Wait for user input before disconnecting println!("\nPress Enter to disconnect..."); let mut input = String::new(); std::io::stdin().read_line(&mut input).ok(); // Disconnect nm.disconnect_vpn("CorpVPN").await?; println!("VPN disconnected"); Ok(()) }
Running
OVPN_REMOTE="vpn.example.com" \
OVPN_PORT="1194" \
OVPN_USER="alice" \
OVPN_PASS="hunter2" \
OVPN_CA="/etc/openvpn/ca.crt" \
OVPN_CERT="/etc/openvpn/client.crt" \
OVPN_KEY="/etc/openvpn/client.key" \
cargo run --example openvpn_client
Sample Output
Connecting to OpenVPN (vpn.example.com:1194)...
Connected!
VPN Connection Details:
Name: CorpVPN
Kind: Plugin
State: Activated
Interface: Some("tun0")
Gateway: Some("vpn.example.com")
IPv4: Some("10.8.0.2")
IPv6: None
DNS: ["10.8.0.1"]
Press Enter to disconnect...
Error Handling
#![allow(unused)] fn main() { use nmrs::{NetworkManager, OpenVpnConfig, OpenVpnAuthType, ConnectionError}; async fn connect_with_retry(nm: &NetworkManager) -> nmrs::Result<()> { let config = OpenVpnConfig::new("CorpVPN", "vpn.example.com", 1194, false) .with_auth_type(OpenVpnAuthType::PasswordTls) .with_username("alice") .with_password("hunter2") .with_ca_cert("/etc/openvpn/ca.crt") .with_client_cert("/etc/openvpn/client.crt") .with_client_key("/etc/openvpn/client.key"); match nm.connect_vpn(config).await { Ok(_) => { println!("VPN connected"); Ok(()) } Err(ConnectionError::AuthFailed) => { eprintln!("Authentication failed — check username/password and certificates"); Err(ConnectionError::AuthFailed) } Err(ConnectionError::Timeout) => { eprintln!("Connection timed out — check server address and port"); Err(ConnectionError::Timeout) } Err(ConnectionError::VpnFailed) => { eprintln!("VPN activation failed — verify OpenVPN plugin is installed"); Err(ConnectionError::VpnFailed) } Err(e) => { eprintln!("Unexpected error: {e}"); Err(e) } } } }
TLS-Only Variation
For certificate-only authentication (no username/password):
#![allow(unused)] fn main() { use nmrs::{OpenVpnConfig, OpenVpnAuthType}; let config = OpenVpnConfig::new("TlsOnlyVPN", "vpn.example.com", 1194, false) .with_auth_type(OpenVpnAuthType::Tls) .with_ca_cert("/etc/openvpn/ca.crt") .with_client_cert("/etc/openvpn/client.crt") .with_client_key("/etc/openvpn/client.key"); nm.connect_vpn(config).await?; }
Password-Only Variation
For username/password authentication without client certificates:
#![allow(unused)] fn main() { use nmrs::{OpenVpnConfig, OpenVpnAuthType}; let config = OpenVpnConfig::new("PassVPN", "vpn.example.com", 1194, false) .with_auth_type(OpenVpnAuthType::Password) .with_username("alice") .with_password("hunter2") .with_ca_cert("/etc/openvpn/ca.crt"); nm.connect_vpn(config).await?; }
Next Steps
.ovpnFile Import — import existing OpenVPN configurations- VPN Connections Guide — comprehensive VPN overview
- OpenVPN Setup — detailed OpenVPN configuration
- WireGuard VPN Client — WireGuard example
.ovpn File Import
This example shows how to import an existing .ovpn configuration file into NetworkManager using nmrs.
Features
- Import
.ovpnfiles with a single method call - Builder approach for more control over the import
- Automatic handling of inline certificates
- Error handling for parse failures
One-Liner Import
The simplest way to import an .ovpn file:
use nmrs::NetworkManager; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; // Import with credentials nm.import_ovpn("client.ovpn", Some("alice"), Some("hunter2")).await?; println!("VPN imported and connected!"); Ok(()) }
If the .ovpn file uses certificate-only authentication, pass None for username and password:
#![allow(unused)] fn main() { nm.import_ovpn("client.ovpn", None, None).await?; }
Builder Approach
For more control over the import process, use OpenVpnBuilder:
use nmrs::{NetworkManager, OpenVpnBuilder}; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; let config = OpenVpnBuilder::from_ovpn_file("client.ovpn")? .username("alice") .password("hunter2") .build()?; nm.connect_vpn(config).await?; println!("VPN connected!"); Ok(()) }
The builder extracts remote, port, protocol, certificates, and other settings from the .ovpn file automatically.
Inline Certificates
Many .ovpn files embed certificates directly rather than referencing external files:
<ca>
-----BEGIN CERTIFICATE-----
MIIBxTCCAWugAwIBAgIJAJ...
-----END CERTIFICATE-----
</ca>
<cert>
-----BEGIN CERTIFICATE-----
MIICCzCCAZGgAwIBAgIRAP...
-----END CERTIFICATE-----
</cert>
<key>
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w...
-----END PRIVATE KEY-----
</key>
from_ovpn_file handles these automatically — inline certificates are extracted and written to temporary files that NetworkManager can reference. No extra steps needed:
#![allow(unused)] fn main() { let config = OpenVpnBuilder::from_ovpn_file("inline-certs.ovpn")? .username("alice") .password("hunter2") .build()?; }
Error Handling
Handle parse failures and missing fields gracefully:
#![allow(unused)] fn main() { use nmrs::{OpenVpnBuilder, ConnectionError}; fn import_config(path: &str) -> nmrs::Result<()> { let config = match OpenVpnBuilder::from_ovpn_file(path) { Ok(builder) => builder.build()?, Err(ConnectionError::InvalidConfig(msg)) => { eprintln!("Failed to parse {path}: {msg}"); return Err(ConnectionError::InvalidConfig(msg)); } Err(ConnectionError::NotFound) => { eprintln!("File not found: {path}"); return Err(ConnectionError::NotFound); } Err(e) => { eprintln!("Import error: {e}"); return Err(e); } }; println!("Successfully parsed configuration"); Ok(()) } }
Common parse errors:
| Error | Cause |
|---|---|
InvalidConfig | Missing remote directive, malformed options, or invalid certificate data |
NotFound | The .ovpn file does not exist at the given path |
AuthFailed | Credentials required but not provided |
Complete Example
use nmrs::{NetworkManager, OpenVpnBuilder}; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; let ovpn_path = std::env::args() .nth(1) .unwrap_or_else(|| "client.ovpn".into()); let username = std::env::var("OVPN_USER").ok(); let password = std::env::var("OVPN_PASS").ok(); println!("Importing {ovpn_path}..."); // Builder approach for maximum control let mut builder = OpenVpnBuilder::from_ovpn_file(&ovpn_path)?; if let Some(user) = &username { builder = builder.username(user); } if let Some(pass) = &password { builder = builder.password(pass); } let config = builder.build()?; nm.connect_vpn(config).await?; println!("Connected! Checking VPN info..."); let vpns = nm.list_vpn_connections().await?; for vpn in &vpns { if vpn.active { println!(" Active: {} ({:?})", vpn.name, vpn.vpn_type); } } Ok(()) }
Next Steps
- OpenVPN Client Example — build OpenVPN config from scratch
- VPN Connections Guide — comprehensive VPN overview
- OpenVPN Setup — detailed OpenVPN configuration
Network Monitor Dashboard
This example creates a real-time network monitoring dashboard that reacts to network and device changes using D-Bus signals.
Features
- Monitors network list changes in real-time
- Monitors device state changes
- Refreshes network list on changes
- Displays current connection status
Code
use nmrs::NetworkManager; use std::sync::Arc; use tokio::sync::Notify; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; println!("=== Network Monitor Dashboard ===\n"); // Print initial state print_status(&nm).await; // Set up change notifications let network_notify = Arc::new(Notify::new()); let device_notify = Arc::new(Notify::new()); // Monitor network changes (access points) let notify = network_notify.clone(); let nm_clone = nm.clone(); tokio::spawn(async move { if let Err(e) = nm_clone.monitor_network_changes(move || { notify.notify_one(); }).await { eprintln!("Network monitor error: {}", e); } }); // Monitor device changes (state transitions) let notify = device_notify.clone(); let nm_clone = nm.clone(); tokio::spawn(async move { if let Err(e) = nm_clone.monitor_device_changes(move || { notify.notify_one(); }).await { eprintln!("Device monitor error: {}", e); } }); // React to changes loop { tokio::select! { _ = network_notify.notified() => { println!("\n--- Network list changed ---"); print_networks(&nm).await; } _ = device_notify.notified() => { println!("\n--- Device state changed ---"); print_status(&nm).await; } } } } async fn print_status(nm: &NetworkManager) { // Current connection match nm.current_ssid().await { Some(ssid) => println!("Connected to: {}", ssid), None => println!("Not connected to Wi-Fi"), } // Wi-Fi state if let Ok(state) = nm.wifi_state().await { println!("Wi-Fi enabled: {}", state.enabled); } // Devices if let Ok(devices) = nm.list_devices().await { println!("\nDevices:"); for dev in &devices { println!(" {} — {} [{}]", dev.interface, dev.device_type, dev.state); } } println!(); } async fn print_networks(nm: &NetworkManager) { if let Ok(networks) = nm.list_networks(None).await { println!("Visible networks ({}):", networks.len()); for net in &networks { let security = if net.is_eap { "EAP" } else if net.is_psk { "PSK" } else { "Open" }; println!( " {:30} {:>3}% {}", net.ssid, net.strength.unwrap_or(0), security, ); } } println!(); }
Running
cargo run --example network_monitor
Sample Output
=== Network Monitor Dashboard ===
Connected to: HomeWiFi
Wi-Fi enabled: true
Wi-Fi hardware enabled: true
Devices:
wlan0 — Wi-Fi [Activated]
eth0 — Ethernet [Disconnected]
lo — Loopback [Unmanaged]
--- Network list changed ---
Visible networks (5):
HomeWiFi 87% PSK
Neighbor5G 42% PSK
CafeGuest 31% Open
OfficeNet 25% EAP
IoT_Network 15% PSK
--- Device state changed ---
Connected to: HomeWiFi
Wi-Fi enabled: true
Wi-Fi hardware enabled: true
Devices:
wlan0 — Wi-Fi [Activated]
eth0 — Ethernet [Activated]
lo — Loopback [Unmanaged]
Enhancements
- Debouncing: D-Bus signals can fire rapidly. Add a debounce timer to avoid refreshing too frequently.
- Detailed view: Call
show_details()on networks for channel, speed, and security info. - History: Keep a log of state transitions with timestamps.
- Alerts: Trigger notifications when connection drops or a specific network appears.
Connection Manager
This example implements a basic connection manager that provides an interactive CLI for managing Wi-Fi, Ethernet, and VPN connections.
Features
- List and scan networks
- Connect and disconnect Wi-Fi
- Manage VPN connections
- List devices and saved profiles
- Interactive menu-driven interface
Code
use nmrs::{NetworkManager, WifiSecurity, ConnectionError}; use std::io::{self, Write}; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; loop { println!("\n=== nmrs Connection Manager ==="); println!("1. Scan networks"); println!("2. List visible networks"); println!("3. Connect to Wi-Fi"); println!("4. Disconnect Wi-Fi"); println!("5. Current connection"); println!("6. List devices"); println!("7. List saved connections"); println!("8. Forget a connection"); println!("9. List VPN connections"); println!("0. Exit"); print!("\nChoice: "); io::stdout().flush().ok(); let choice = read_line(); match choice.trim() { "1" => scan(&nm).await, "2" => list_networks(&nm).await, "3" => connect_wifi(&nm).await, "4" => disconnect(&nm).await, "5" => current(&nm).await, "6" => devices(&nm).await, "7" => saved(&nm).await, "8" => forget(&nm).await, "9" => vpns(&nm).await, "0" => break, _ => println!("Invalid choice"), } } Ok(()) } async fn scan(nm: &NetworkManager) { println!("Scanning..."); match nm.scan_networks(None).await { Ok(_) => println!("Scan complete"), Err(e) => eprintln!("Scan failed: {}", e), } } async fn list_networks(nm: &NetworkManager) { match nm.list_networks(None).await { Ok(networks) => { println!("\n{:<5} {:<30} {:>6} {:>10}", "#", "SSID", "Signal", "Security"); println!("{}", "-".repeat(55)); for (i, net) in networks.iter().enumerate() { let sec = if net.is_eap { "EAP" } else if net.is_psk { "PSK" } else { "Open" }; println!("{:<5} {:<30} {:>5}% {:>10}", i + 1, net.ssid, net.strength.unwrap_or(0), sec); } } Err(e) => eprintln!("Error: {}", e), } } async fn connect_wifi(nm: &NetworkManager) { print!("SSID: "); io::stdout().flush().ok(); let ssid = read_line(); let ssid = ssid.trim(); print!("Password (empty for open): "); io::stdout().flush().ok(); let password = read_line(); let password = password.trim(); let security = if password.is_empty() { WifiSecurity::Open } else { WifiSecurity::WpaPsk { psk: password.into() } }; println!("Connecting to '{}'...", ssid); match nm.connect(ssid, None, security).await { Ok(_) => println!("Connected!"), Err(ConnectionError::AuthFailed) => eprintln!("Wrong password"), Err(ConnectionError::NotFound) => eprintln!("Network not found"), Err(ConnectionError::Timeout) => eprintln!("Connection timed out"), Err(e) => eprintln!("Error: {}", e), } } async fn disconnect(nm: &NetworkManager) { match nm.disconnect(None).await { Ok(_) => println!("Disconnected"), Err(e) => eprintln!("Error: {}", e), } } async fn current(nm: &NetworkManager) { match nm.current_network().await { Ok(Some(net)) => { println!("Connected to: {} ({}%)", net.ssid, net.strength.unwrap_or(0)); } Ok(None) => println!("Not connected"), Err(e) => eprintln!("Error: {}", e), } } async fn devices(nm: &NetworkManager) { match nm.list_devices().await { Ok(devices) => { for dev in &devices { println!("{:<10} {:<12} {:<15} {}", dev.interface, format!("{}", dev.device_type), format!("{}", dev.state), dev.identity.current_mac, ); } } Err(e) => eprintln!("Error: {}", e), } } async fn saved(nm: &NetworkManager) { match nm.list_saved_connections().await { Ok(connections) => { for conn in &connections { println!(" {} ({})", conn.id, conn.connection_type); } } Err(e) => eprintln!("Error: {}", e), } } async fn forget(nm: &NetworkManager) { print!("Connection name to forget: "); io::stdout().flush().ok(); let name = read_line(); let name = name.trim(); match nm.forget(name).await { Ok(_) => println!("Forgot '{}'", name), Err(e) => eprintln!("Error: {}", e), } } async fn vpns(nm: &NetworkManager) { match nm.list_vpn_connections().await { Ok(vpns) => { if vpns.is_empty() { println!("No VPN connections"); } for vpn in &vpns { println!(" {} ({:?}) — {:?}", vpn.name, vpn.vpn_type, vpn.state); } } Err(e) => eprintln!("Error: {}", e), } } fn read_line() -> String { let mut input = String::new(); io::stdin().read_line(&mut input).unwrap_or_default(); input }
Running
cargo run --example connection_manager
Enhancements
- VPN connect/disconnect: Add menu options for VPN operations
- Bluetooth: Add Bluetooth device listing and connection
- Network details: Show
NetworkInfofor selected networks - Color output: Use a crate like
coloredfor terminal formatting - Persistent config: Store preferred networks in a config file
Core Types
This page lists the primary types exported by nmrs. For complete API documentation, see docs.rs/nmrs.
NetworkManager
The main entry point for all operations.
#![allow(unused)] fn main() { use nmrs::NetworkManager; let nm = NetworkManager::new().await?; let nm = NetworkManager::with_config(config).await?; }
Clone— clones share the same D-Bus connectionSend + Sync— safe to share across tasks- See NetworkManager API for all methods
Result Type
#![allow(unused)] fn main() { pub type Result<T> = std::result::Result<T, ConnectionError>; }
All public methods return nmrs::Result<T>.
Wi-Fi Types
| Type | Description |
|---|---|
Network | A discovered Wi-Fi network (SSID, signal, security flags) |
NetworkInfo | Detailed network information (channel, speed, bars) |
AccessPoint | A single AP with BSSID, frequency, and security flags |
WifiDevice | A Wi-Fi device with interface, MAC, state, and active SSID |
WifiScope | Per-interface operations scope (from nm.wifi("wlan1")) |
WifiSecurity | Authentication type: Open, WpaPsk, WpaEap |
EapOptions | Enterprise Wi-Fi (802.1X) configuration |
EapOptionsBuilder | Builder for EapOptions |
EapMethod | Outer EAP method: Peap, Ttls |
Phase2 | Inner auth method: Mschapv2, Pap |
Device Types
| Type | Description |
|---|---|
Device | A network device (interface, type, state, MAC) |
DeviceIdentity | Device MAC addresses (permanent and current) |
DeviceType | Device kind: Wifi, Ethernet, Bluetooth, WifiP2P, Loopback, Other(u32) |
DeviceState | Operational state: Disconnected, Activated, Failed, etc. |
Radio / Airplane-Mode Types
| Type | Description |
|---|---|
RadioState | Combined software (enabled) and hardware (hardware_enabled) radio state |
AirplaneModeState | Aggregated state across Wi-Fi, WWAN, and Bluetooth |
VPN Types
| Type | Description |
|---|---|
VpnConfig | Sealed trait for VPN configurations |
VpnConfiguration | Dispatch enum: WireGuard(WireGuardConfig) or OpenVpn(OpenVpnConfig) |
WireGuardConfig | WireGuard VPN configuration |
WireGuardPeer | WireGuard peer configuration |
OpenVpnConfig | OpenVPN configuration |
OpenVpnAuthType | OpenVPN auth: Password, Tls, PasswordTls, StaticKey |
OpenVpnCompression | Compression mode: No, Lz4, Lz4V2, Yes |
OpenVpnProxy | Proxy: Http { ... }, Socks { ... } |
VpnRoute | Static IPv4 route for split tunneling |
VpnType | Protocol-specific metadata (data-carrying enum) |
VpnKind | Plugin (OpenVPN, etc.) vs WireGuard |
VpnConnection | A saved/active VPN connection with rich metadata |
VpnConnectionInfo | Detailed active VPN info (IP, DNS, gateway, protocol details) |
VpnDetails | Protocol-specific active connection details |
VpnCredentials | Deprecated — use WireGuardConfig instead |
Connectivity Types
| Type | Description |
|---|---|
ConnectivityState | NM connectivity: Full, Portal, Limited, None, Unknown |
ConnectivityReport | Full report with state, check URI, and captive portal URL |
Saved Connection Types
| Type | Description |
|---|---|
SavedConnection | Full decoded saved profile |
SavedConnectionBrief | Lightweight profile (uuid, id, type) |
SettingsSummary | Decoded settings within a profile |
SettingsPatch | Partial update for update_saved_connection |
Bluetooth Types
| Type | Description |
|---|---|
BluetoothDevice | A Bluetooth device with BlueZ info |
BluetoothIdentity | Bluetooth MAC + network role for connecting |
BluetoothNetworkRole | Role: PanU, Dun |
Configuration Types
| Type | Description |
|---|---|
TimeoutConfig | Connection/disconnection timeouts |
ConnectionOptions | Autoconnect, priority, retry settings |
Error Types
| Type | Description |
|---|---|
ConnectionError | All possible error variants |
StateReason | Device state reason codes |
ConnectionStateReason | Activation/deactivation reason codes |
ActiveConnectionState | Connection lifecycle states |
Builder Types
| Type | Description |
|---|---|
ConnectionBuilder | Base connection settings builder |
WifiConnectionBuilder | Wi-Fi connection builder |
WireGuardBuilder | WireGuard VPN builder |
OpenVpnBuilder | OpenVPN builder (also imports .ovpn files) |
IpConfig | IP address with CIDR prefix |
Route | Static route configuration |
WifiBand | Wi-Fi band: Bg (2.4 GHz), A (5 GHz) |
WifiMode | Wi-Fi mode: Infrastructure, Adhoc, Ap |
Re-exports
nmrs re-exports commonly used types at the crate root for convenience:
#![allow(unused)] fn main() { use nmrs::{ NetworkManager, WifiScope, WifiSecurity, EapOptions, EapMethod, Phase2, WireGuardConfig, WireGuardPeer, OpenVpnConfig, OpenVpnAuthType, VpnConfig, VpnConfiguration, VpnType, VpnKind, TimeoutConfig, ConnectionOptions, ConnectionError, DeviceType, DeviceState, RadioState, AirplaneModeState, ConnectivityState, ConnectivityReport, }; }
Less commonly used types are available through the models and builders modules:
#![allow(unused)] fn main() { use nmrs::models::{BluetoothIdentity, BluetoothNetworkRole, BluetoothDevice}; use nmrs::builders::{ConnectionBuilder, WireGuardBuilder, OpenVpnBuilder, IpConfig, Route}; }
NetworkManager API
The NetworkManager struct is the primary entry point for all nmrs operations. It manages a D-Bus connection to the NetworkManager daemon.
Construction
#![allow(unused)] fn main() { use nmrs::{NetworkManager, TimeoutConfig}; use std::time::Duration; // Default timeouts (30s connect, 10s disconnect) let nm = NetworkManager::new().await?; // Custom timeouts let config = TimeoutConfig::new() .with_connection_timeout(Duration::from_secs(60)) .with_disconnect_timeout(Duration::from_secs(20)); let nm = NetworkManager::with_config(config).await?; // Read current config let config = nm.timeout_config(); }
Wi-Fi Methods
| Method | Returns | Description |
|---|---|---|
scan_networks(interface) | Result<()> | Trigger active Wi-Fi scan (None = all devices) |
list_networks(interface) | Result<Vec<Network>> | List visible networks (None = all devices) |
list_access_points(interface) | Result<Vec<AccessPoint>> | List individual APs by BSSID |
connect(ssid, interface, security) | Result<()> | Connect to a Wi-Fi network |
connect_to_bssid(ssid, bssid, interface, security) | Result<()> | Connect to a specific AP |
disconnect(interface) | Result<()> | Disconnect from current network (None = first Wi-Fi device) |
current_network() | Result<Option<Network>> | Get current Wi-Fi network |
current_ssid() | Option<String> | Get current SSID |
current_connection_info() | Option<(String, Option<u32>)> | Get SSID + frequency |
is_connected(ssid) | Result<bool> | Check if connected to a specific network |
show_details(network) | Result<NetworkInfo> | Get detailed network info |
Per-Device Wi-Fi Methods
| Method | Returns | Description |
|---|---|---|
list_wifi_devices() | Result<Vec<WifiDevice>> | List all Wi-Fi devices |
wifi_device_by_interface(name) | Result<WifiDevice> | Look up a Wi-Fi device by name |
wifi(interface) | WifiScope | Build a scope pinned to one interface |
set_wifi_enabled(interface, bool) | Result<()> | Enable/disable one Wi-Fi radio |
Radio / Airplane-Mode Methods
| Method | Returns | Description |
|---|---|---|
wifi_state() | Result<RadioState> | Software + hardware Wi-Fi state |
wwan_state() | Result<RadioState> | Software + hardware WWAN state |
bluetooth_radio_state() | Result<RadioState> | Software + hardware Bluetooth state |
airplane_mode_state() | Result<AirplaneModeState> | Aggregated across all radios |
set_wireless_enabled(bool) | Result<()> | Global Wi-Fi software toggle |
set_wwan_enabled(bool) | Result<()> | Global WWAN toggle |
set_bluetooth_radio_enabled(bool) | Result<()> | Toggle all BlueZ adapters |
set_airplane_mode(bool) | Result<()> | Toggle all three radios |
wait_for_wifi_ready() | Result<()> | Wait for Wi-Fi device to become ready |
Ethernet Methods
| Method | Returns | Description |
|---|---|---|
connect_wired() | Result<()> | Connect first available Ethernet device |
VPN Methods
| Method | Returns | Description |
|---|---|---|
connect_vpn(config) | Result<()> | Connect with a WireGuardConfig or OpenVpnConfig |
import_ovpn(path, user, pass) | Result<()> | Import .ovpn file and connect |
connect_vpn_by_uuid(uuid) | Result<()> | Activate a saved VPN by UUID |
connect_vpn_by_id(id) | Result<()> | Activate a saved VPN by display name |
disconnect_vpn(name) | Result<()> | Disconnect a VPN by name |
disconnect_vpn_by_uuid(uuid) | Result<()> | Disconnect a VPN by UUID |
list_vpn_connections() | Result<Vec<VpnConnection>> | List all saved VPNs |
active_vpn_connections() | Result<Vec<VpnConnection>> | List only active VPNs |
forget_vpn(name) | Result<()> | Delete a saved VPN profile |
get_vpn_info(name) | Result<VpnConnectionInfo> | Get active VPN details |
Connectivity Methods
| Method | Returns | Description |
|---|---|---|
connectivity() | Result<ConnectivityState> | Current NM connectivity state |
check_connectivity() | Result<ConnectivityState> | Force a connectivity re-check |
connectivity_report() | Result<ConnectivityReport> | Full report with captive portal URL |
captive_portal_url() | Result<Option<String>> | Captive portal URL if in Portal state |
Bluetooth Methods
| Method | Returns | Description |
|---|---|---|
list_bluetooth_devices() | Result<Vec<BluetoothDevice>> | List Bluetooth devices |
connect_bluetooth(name, identity) | Result<()> | Connect to a Bluetooth device |
forget_bluetooth(name) | Result<()> | Delete a Bluetooth profile |
Device Methods
| Method | Returns | Description |
|---|---|---|
list_devices() | Result<Vec<Device>> | List all network devices |
list_wireless_devices() | Result<Vec<Device>> | List Wi-Fi devices |
list_wired_devices() | Result<Vec<Device>> | List Ethernet devices |
get_device_by_interface(name) | Result<OwnedObjectPath> | Find device by interface name |
is_connecting() | Result<bool> | Check if any device is connecting |
Connection Profile Methods
| Method | Returns | Description |
|---|---|---|
list_saved_connections() | Result<Vec<SavedConnection>> | Full decode of all saved profiles |
list_saved_connections_brief() | Result<Vec<SavedConnectionBrief>> | Lightweight profile listing |
list_saved_connection_ids() | Result<Vec<String>> | Just the profile names |
get_saved_connection(uuid) | Result<SavedConnection> | Load one profile by UUID |
get_saved_connection_raw(uuid) | Result<HashMap<...>> | Raw GetSettings map |
delete_saved_connection(uuid) | Result<()> | Delete a profile by UUID |
update_saved_connection(uuid, patch) | Result<()> | Merge a SettingsPatch into a profile |
reload_saved_connections() | Result<()> | Re-read profiles from disk |
has_saved_connection(ssid) | Result<bool> | Check if a Wi-Fi profile exists |
get_saved_connection_path(ssid) | Result<Option<OwnedObjectPath>> | Get profile D-Bus path |
forget(ssid) | Result<()> | Delete a Wi-Fi profile |
Monitoring Methods
| Method | Returns | Description |
|---|---|---|
monitor_network_changes(callback) | Result<()> | Watch for AP and signal strength changes (runs forever) |
monitor_device_changes(callback) | Result<()> | Watch for device state changes (runs forever) |
Thread Safety
NetworkManager is Clone, Send, and Sync. Clones share the same D-Bus connection.
Important: Concurrent connection operations (calling connect() from multiple tasks) are not supported. Use is_connecting() to guard against this.
Full API Reference
For complete documentation with all method signatures, see docs.rs/nmrs.
Models Module
The models module contains all data types used by nmrs. These are re-exported at the crate root and through nmrs::models.
Device Models
Device
Represents a network device managed by NetworkManager.
#![allow(unused)] fn main() { pub struct Device { pub path: String, // D-Bus object path pub interface: String, // e.g., "wlan0", "eth0" pub identity: DeviceIdentity, pub device_type: DeviceType, pub state: DeviceState, pub managed: Option<bool>, pub driver: Option<String>, pub ip4_address: Option<String>, pub ip6_address: Option<String>, } }
Methods: is_wireless(), is_wired(), is_bluetooth()
DeviceIdentity
#![allow(unused)] fn main() { pub struct DeviceIdentity { pub permanent_mac: String, pub current_mac: String, } }
DeviceType
#![allow(unused)] fn main() { pub enum DeviceType { Ethernet, Wifi, WifiP2P, Loopback, Bluetooth, Other(u32), } }
Methods: supports_scanning(), requires_specific_object(), has_global_enabled_state(), connection_type_str(), to_code()
DeviceState
#![allow(unused)] fn main() { pub enum DeviceState { Unmanaged, Unavailable, Disconnected, Prepare, Config, NeedAuth, IpConfig, IpCheck, Secondaries, Activated, Deactivating, Failed, Other(u32), } }
Methods: is_transitional()
Wi-Fi Models
Network
A discovered Wi-Fi network.
#![allow(unused)] fn main() { pub struct Network { pub device: String, pub ssid: String, pub bssid: Option<String>, pub strength: Option<u8>, pub frequency: Option<u32>, pub secured: bool, pub is_psk: bool, pub is_eap: bool, pub ip4_address: Option<String>, pub ip6_address: Option<String>, } }
AccessPoint
A single AP with per-BSSID details.
#![allow(unused)] fn main() { pub struct AccessPoint { pub ssid: String, pub bssid: String, pub strength: u8, pub frequency_mhz: u32, pub device: String, // ... security flags and device state } }
WifiDevice
A Wi-Fi device discovered by list_wifi_devices().
#![allow(unused)] fn main() { pub struct WifiDevice { pub interface: String, pub mac: String, pub state: DeviceState, pub active_ssid: Option<String>, } }
WifiScope
Per-interface operations scope returned by nm.wifi("wlan1").
Methods: interface(), scan(), list_networks(), list_access_points(), connect(ssid, creds), connect_to_bssid(ssid, bssid, creds), disconnect(), set_enabled(bool), forget(ssid)
NetworkInfo
Detailed network information from show_details().
#![allow(unused)] fn main() { pub struct NetworkInfo { pub ssid: String, pub bssid: String, pub strength: u8, pub freq: Option<u32>, pub channel: Option<u16>, pub mode: String, pub rate_mbps: Option<u32>, pub bars: String, // e.g., "▂▄▆█" pub security: String, pub status: String, pub ip4_address: Option<String>, pub ip6_address: Option<String>, } }
WifiSecurity
#![allow(unused)] fn main() { pub enum WifiSecurity { Open, WpaPsk { psk: String }, WpaEap { opts: EapOptions }, } }
Methods: secured(), is_psk(), is_eap()
EapOptions
Enterprise Wi-Fi configuration.
#![allow(unused)] fn main() { pub struct EapOptions { pub identity: String, pub password: String, pub anonymous_identity: Option<String>, pub domain_suffix_match: Option<String>, pub ca_cert_path: Option<String>, pub system_ca_certs: bool, pub method: EapMethod, pub phase2: Phase2, } }
Constructors: new(identity, password), builder()
EapMethod / Phase2
#![allow(unused)] fn main() { pub enum EapMethod { Peap, Ttls } pub enum Phase2 { Mschapv2, Pap } }
Radio / Airplane-Mode Models
RadioState
#![allow(unused)] fn main() { pub struct RadioState { pub enabled: bool, // software toggle pub hardware_enabled: bool, // rfkill state } }
AirplaneModeState
Aggregated state across Wi-Fi, WWAN, and Bluetooth radios. Methods: is_airplane_mode().
VPN Models
WireGuardConfig
WireGuard VPN configuration (replaces the deprecated VpnCredentials).
#![allow(unused)] fn main() { pub struct WireGuardConfig { pub name: String, pub gateway: String, pub private_key: String, pub address: String, pub peers: Vec<WireGuardPeer>, pub dns: Option<Vec<String>>, pub mtu: Option<u32>, pub uuid: Option<Uuid>, } }
Constructor: new(name, gateway, private_key, address, peers)
Builder methods: .with_dns(vec), .with_mtu(u32), .with_uuid(uuid)
OpenVpnConfig
OpenVPN configuration.
#![allow(unused)] fn main() { pub struct OpenVpnConfig { pub name: String, pub remote: String, pub port: u16, pub tcp: bool, pub auth_type: Option<OpenVpnAuthType>, pub ca_cert: Option<String>, pub client_cert: Option<String>, pub client_key: Option<String>, pub username: Option<String>, pub password: Option<String>, pub compression: Option<OpenVpnCompression>, pub proxy: Option<OpenVpnProxy>, // ... many more TLS and routing fields } }
Constructor: new(name, remote, port, tcp)
Builder methods: .with_auth_type(), .with_username(), .with_password(), .with_ca_cert(), .with_client_cert(), .with_client_key(), .with_dns(), .with_mtu(), .with_compression(), .with_proxy(), .with_tls_auth(), .with_tls_crypt(), .with_redirect_gateway(), .with_routes(), and many more.
OpenVpnAuthType
#![allow(unused)] fn main() { pub enum OpenVpnAuthType { Password, Tls, PasswordTls, StaticKey } }
OpenVpnCompression
#![allow(unused)] fn main() { pub enum OpenVpnCompression { No, Lzo, Lz4, Lz4V2, Yes } }
OpenVpnProxy
#![allow(unused)] fn main() { pub enum OpenVpnProxy { Http { server, port, username, password, retry }, Socks { server, port, retry }, } }
VpnRoute
#![allow(unused)] fn main() { pub struct VpnRoute { pub dest: String, pub prefix: u32, pub next_hop: Option<String>, pub metric: Option<u32>, } }
Constructor: new(dest, prefix). Builder methods: .next_hop(gw), .metric(m).
WireGuardPeer
#![allow(unused)] fn main() { pub struct WireGuardPeer { pub public_key: String, pub gateway: String, pub allowed_ips: Vec<String>, pub preshared_key: Option<String>, pub persistent_keepalive: Option<u32>, } }
VpnType
Protocol-specific metadata decoded from NM settings (data-carrying enum):
#![allow(unused)] fn main() { pub enum VpnType { WireGuard { private_key, peer_public_key, endpoint, allowed_ips, ... }, OpenVpn { remote, connection_type, user_name, ca, cert, key, ... }, OpenConnect { gateway, user_name, protocol, ... }, StrongSwan { address, method, user_name, ... }, Pptp { gateway, user_name, ... }, L2tp { gateway, user_name, ipsec_enabled, ... }, Generic { service_type, data, secrets, ... }, } }
VpnKind
#![allow(unused)] fn main() { pub enum VpnKind { Plugin, WireGuard } }
VpnConnection
A saved or active VPN connection with rich metadata.
#![allow(unused)] fn main() { pub struct VpnConnection { pub uuid: String, pub id: String, pub name: String, pub vpn_type: VpnType, pub state: DeviceState, pub interface: Option<String>, pub active: bool, pub user_name: Option<String>, pub password_flags: VpnSecretFlags, pub service_type: String, pub kind: VpnKind, } }
VpnConnectionInfo
Detailed active VPN information.
#![allow(unused)] fn main() { pub struct VpnConnectionInfo { pub name: String, pub vpn_kind: VpnKind, pub state: DeviceState, pub interface: Option<String>, pub gateway: Option<String>, pub ip4_address: Option<String>, pub ip6_address: Option<String>, pub dns_servers: Vec<String>, pub details: Option<VpnDetails>, } }
VpnDetails
#![allow(unused)] fn main() { pub enum VpnDetails { WireGuard { public_key, endpoint }, OpenVpn { remote, port, protocol, cipher, auth, compression }, } }
Saved Connection Models
SavedConnection
Full decoded saved profile from list_saved_connections().
Fields: uuid, id, connection_type, interface_name, autoconnect, timestamp, settings.
SavedConnectionBrief
Lightweight: uuid, id, connection_type.
SettingsPatch
Partial update for update_saved_connection.
Connectivity Models
ConnectivityState
#![allow(unused)] fn main() { pub enum ConnectivityState { Unknown, None, Portal, Limited, Full } }
ConnectivityReport
#![allow(unused)] fn main() { pub struct ConnectivityReport { pub state: ConnectivityState, pub check_enabled: bool, pub check_uri: Option<String>, pub captive_portal_url: Option<String>, } }
Bluetooth Models
BluetoothDevice
#![allow(unused)] fn main() { pub struct BluetoothDevice { pub bdaddr: String, pub name: Option<String>, pub alias: Option<String>, pub bt_caps: u32, pub state: DeviceState, } }
BluetoothIdentity
#![allow(unused)] fn main() { pub struct BluetoothIdentity { pub bdaddr: String, pub bt_device_type: BluetoothNetworkRole, } }
BluetoothNetworkRole
#![allow(unused)] fn main() { pub enum BluetoothNetworkRole { PanU, Dun } }
Configuration Models
TimeoutConfig
#![allow(unused)] fn main() { pub struct TimeoutConfig { pub connection_timeout: Duration, // default: 30s pub disconnect_timeout: Duration, // default: 10s } }
ConnectionOptions
#![allow(unused)] fn main() { pub struct ConnectionOptions { pub autoconnect: bool, pub autoconnect_priority: Option<i32>, pub autoconnect_retries: Option<i32>, } }
Non-Exhaustive Types
All enums and structs in nmrs are marked #[non_exhaustive]. Always include a wildcard arm in match expressions and don't construct structs directly (use constructors/builders).
Full API Reference
For complete documentation with all method signatures and trait implementations, see docs.rs/nmrs.
Builders Module
The builders module provides low-level APIs for constructing NetworkManager connection settings. Most users should use the high-level NetworkManager API instead — these builders are for advanced use cases where you need fine-grained control.
ConnectionBuilder
The base builder for all connection types. Handles common sections: connection, ipv4, ipv6.
#![allow(unused)] fn main() { use nmrs::builders::ConnectionBuilder; let settings = ConnectionBuilder::new("802-3-ethernet", "MyConnection") .autoconnect(true) .autoconnect_priority(10) .ipv4_auto() .ipv6_auto() .build(); }
Methods
| Method | Description |
|---|---|
new(type, id) | Create with connection type and name |
uuid(uuid) | Set specific UUID |
interface_name(name) | Restrict to a specific interface |
autoconnect(bool) | Enable/disable auto-connect |
autoconnect_priority(i32) | Set priority (higher = preferred) |
autoconnect_retries(i32) | Set retry limit |
options(&ConnectionOptions) | Apply options struct |
ipv4_auto() | DHCP for IPv4 |
ipv4_manual(Vec<IpConfig>) | Static IPv4 addresses |
ipv4_disabled() | Disable IPv4 |
ipv4_link_local() | Link-local IPv4 (169.254.x.x) |
ipv4_shared() | Internet connection sharing |
ipv4_dns(Vec<Ipv4Addr>) | Set DNS servers |
ipv4_gateway(Ipv4Addr) | Set gateway |
ipv4_routes(Vec<Route>) | Add static routes |
ipv6_auto() | SLAAC/DHCPv6 |
ipv6_manual(Vec<IpConfig>) | Static IPv6 addresses |
ipv6_ignore() | Disable IPv6 |
ipv6_link_local() | Link-local IPv6 only |
ipv6_dns(Vec<Ipv6Addr>) | Set IPv6 DNS |
ipv6_gateway(Ipv6Addr) | Set IPv6 gateway |
ipv6_routes(Vec<Route>) | Add IPv6 static routes |
with_section(name, HashMap) | Add custom settings section |
update_section(name, closure) | Modify existing section |
build() | Produce the settings dictionary |
IpConfig
#![allow(unused)] fn main() { use nmrs::builders::IpConfig; let ip = IpConfig::new("192.168.1.100", 24); }
Route
#![allow(unused)] fn main() { use nmrs::builders::Route; let route = Route::new("10.0.0.0", 8) .next_hop("192.168.1.1") .metric(100); }
WifiConnectionBuilder
Builds Wi-Fi connection settings with security configuration.
#![allow(unused)] fn main() { use nmrs::builders::WifiConnectionBuilder; let settings = WifiConnectionBuilder::new("MyNetwork") .wpa_psk("my_password") .band(nmrs::builders::WifiBand::A) // 5 GHz .ipv4_auto() .build(); }
WifiBand / WifiMode
#![allow(unused)] fn main() { pub enum WifiBand { Bg, A } // 2.4 GHz, 5 GHz pub enum WifiMode { Infrastructure, Adhoc, Ap } }
WireGuardBuilder
Builds WireGuard VPN connection settings with validation.
#![allow(unused)] fn main() { use nmrs::builders::WireGuardBuilder; use nmrs::WireGuardPeer; let peer = WireGuardPeer::new( "HIgo9xNzJMWLKAShlKl6/bUT1VI9Q0SDBXGtLXkPFXc=", "vpn.example.com:51820", vec!["0.0.0.0/0".into()], ); let settings = WireGuardBuilder::new("MyVPN") .private_key("YBk6X3pP8KjKz7+HFWzVHNqL3qTZq8hX9VxFQJ4zVmM=") .address("10.0.0.2/24") .add_peer(peer) .dns(vec!["1.1.1.1".into()]) .mtu(1420) .autoconnect(false) .build()?; }
The build() method validates all fields and returns Result<Settings, ConnectionError>.
Validation
| Check | Error |
|---|---|
| Private key format | InvalidPrivateKey |
| Address CIDR format | InvalidAddress |
| At least one peer | InvalidPeers |
| Peer public key format | InvalidPublicKey |
| Gateway host:port format | InvalidGateway |
| Peer allowed IPs non-empty | InvalidPeers |
Builder Functions
Convenience functions that wrap the builders:
#![allow(unused)] fn main() { use nmrs::builders::{build_wifi_connection, build_ethernet_connection}; use nmrs::{WifiSecurity, ConnectionOptions}; // Wi-Fi let wifi = build_wifi_connection("MyNetwork", &WifiSecurity::Open, &ConnectionOptions::default()); // Ethernet let eth = build_ethernet_connection("eth0", &ConnectionOptions::default()); }
When to Use Builders
Use the builders when you need:
- Custom IP configuration (static IP, DNS, routes)
- Specific Wi-Fi band or mode settings
- Custom connection sections (bridge, bond, VLAN)
- Fine-grained control over the settings dictionary
For standard connections, the NetworkManager API handles everything automatically.
OpenVpnBuilder
Builds OpenVPN connection settings from an OpenVpnConfig or by importing a .ovpn file.
From Configuration
#![allow(unused)] fn main() { use nmrs::{OpenVpnConfig, OpenVpnAuthType}; let config = OpenVpnConfig::new("CorpVPN", "vpn.example.com", 1194, false) .with_auth_type(OpenVpnAuthType::PasswordTls) .with_username("user") .with_password("secret") .with_ca_cert("/etc/openvpn/ca.crt") .with_client_cert("/etc/openvpn/client.crt") .with_client_key("/etc/openvpn/client.key"); }
From .ovpn File
#![allow(unused)] fn main() { use nmrs::builders::OpenVpnBuilder; let config = OpenVpnBuilder::from_ovpn_file("client.ovpn")? .username("user") .password("secret") .build()?; }
Or use the high-level API directly:
#![allow(unused)] fn main() { let nm = NetworkManager::new().await?; nm.import_ovpn("client.ovpn", Some("user"), Some("secret")).await?; }
Full API Reference
See docs.rs/nmrs for complete builder documentation.
Error Types
nmrs uses a single error enum, ConnectionError, for all operations. It implements std::error::Error, Display, and Debug.
ConnectionError
#![allow(unused)] fn main() { #[non_exhaustive] pub enum ConnectionError { // D-Bus errors Dbus(zbus::Error), DbusOperation { context: String, source: zbus::Error }, // Network not found NotFound, // Authentication AuthFailed, MissingPassword, SupplicantConfigFailed, SupplicantTimeout, // Connection lifecycle DhcpFailed, Timeout, Stuck(String), // Device errors NoWifiDevice, NoWiredDevice, WifiNotReady, NoBluetoothDevice, NoSavedConnection, // Device/activation failures with reason codes DeviceFailed(StateReason), ActivationFailed(ConnectionStateReason), // Per-device errors WifiInterfaceNotFound { interface: String }, NotAWifiDevice { interface: String }, // Radio errors HardwareRadioKilled, BluezUnavailable, // VPN errors NoVpnConnection, VpnFailed(String), VpnIdAmbiguous { id: String }, IncompleteBuilder(String), InvalidPrivateKey(String), InvalidPublicKey(String), InvalidAddress(String), InvalidGateway(String), InvalidPeers(String), // Connectivity ConnectivityCheckDisabled, // BSSID ApBssidNotFound { ssid: String, bssid: String }, InvalidBssid(String), // Other InvalidUtf8(Utf8Error), } }
Error Categories
User-Facing Errors
These indicate issues the user can fix:
| Error | User Action |
|---|---|
NotFound | Move closer to the network or check SSID spelling |
AuthFailed | Check password or credentials |
MissingPassword | Provide a non-empty password |
Timeout | Retry or increase timeout |
DhcpFailed | Check network infrastructure |
NoWifiDevice | Check that a Wi-Fi adapter is installed |
NoWiredDevice | Check that an Ethernet adapter exists |
Validation Errors
These indicate invalid input to nmrs:
| Error | Fix |
|---|---|
InvalidPrivateKey | Check WireGuard key format (base64, ~44 chars) |
InvalidPublicKey | Check peer public key format |
InvalidAddress | Use CIDR notation (e.g., 10.0.0.2/24) |
InvalidGateway | Use host:port format |
InvalidPeers | Add at least one peer with allowed IPs |
System Errors
These indicate infrastructure issues:
| Error | Investigation |
|---|---|
Dbus | Is NetworkManager running? Is D-Bus accessible? |
DbusOperation | Check context for what operation failed |
SupplicantConfigFailed | Check wpa_supplicant configuration |
SupplicantTimeout | Check RADIUS server connectivity |
WifiNotReady | Wi-Fi device still initializing |
Stuck | NetworkManager in unexpected state |
DeviceFailed | Check the StateReason for details |
ActivationFailed | Check the ConnectionStateReason for details |
StateReason
Low-level device state reason codes from NetworkManager. Used in DeviceFailed:
Common values include reasons like "supplicant disconnect", "DHCP failure", "firmware missing", "carrier dropped", and many others. These map directly to NetworkManager's NM_DEVICE_STATE_REASON_* constants.
ConnectionStateReason
Activation/deactivation reason codes. Used in ActivationFailed:
Common values include reasons like "user disconnected", "carrier dropped", "connection removed", "dependency failed", and others. These map to NetworkManager's NM_ACTIVE_CONNECTION_STATE_REASON_* constants.
ActiveConnectionState
The lifecycle state of an active connection:
#![allow(unused)] fn main() { pub enum ActiveConnectionState { Unknown, Activating, Activated, Deactivating, Deactivated, Other(u32), } }
Error Handling Patterns
Simple Propagation
#![allow(unused)] fn main() { async fn connect() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; nm.connect("MyWiFi", None, WifiSecurity::Open).await?; Ok(()) } }
Specific Error Handling
#![allow(unused)] fn main() { match nm.connect("MyWiFi", None, security).await { Ok(_) => println!("Connected"), Err(ConnectionError::AuthFailed) => eprintln!("Wrong password"), Err(ConnectionError::NotFound) => eprintln!("Network not found"), Err(e) => eprintln!("Error: {}", e), } }
With anyhow
#![allow(unused)] fn main() { use anyhow::{Context, Result}; async fn connect() -> Result<()> { let nm = NetworkManager::new().await .context("Failed to connect to NetworkManager")?; nm.connect("MyWiFi", None, WifiSecurity::Open).await .context("Failed to connect to MyWiFi")?; Ok(()) } }
Full API Reference
See docs.rs/nmrs for complete error documentation.
Contributing
Thank you for wanting to contribute to nmrs!
Guidelines
I'm fairly accepting to all PRs, only with a couple caveats:
- Do not submit low-effort or autogenerated code. If you absolutely must, please disclose how you used AI otherwise I will close the PR.
- Please try to (when possible) contribute to an issue. This is not a hard ask, I'll still consider your contribution if it makes sense.
Requirements
To run or develop nmrs you need:
- Rust (stable) via
rustup - A running
NetworkManagerinstance
I also provide a Dockerfile you can build if you don't use Linux and use MacOS instead.
To run tests:
docker compose run test
To run an interactive shell:
docker compose run shell
If you decide to run the shell, ensure you run all commands from within the nmrs directory, not root.
cargo test -p nmrs # run library tests
cargo build -p nmrs # build the library
cargo check # you get the point...
When your branch falls behind master
If the respective branch for a PR goes out of sync, I prefer you rebase. I've exposed this setting for you to automatically do so as a contributor on any PR you open.
Issues and Commit Message Hygiene
When you've made changes and are ready to commit, I prefer that you follow the standards explained at Conventional Commits.
I additionally request that you format your commits as such:
type((some issue number)): changes made
For example:
fix(#24): fixed bug where something was happening
Obviously, if there is no issue number to attach, no need to add anything there.
Lastly, please ensure you make atomic commits.
All issues are acceptable. If a situation arises where a request or concern is not valid, I will respond directly to the issue.
Tests
All tests must pass before a merge takes place.
Ensure NetworkManager is running
sudo systemctl start NetworkManager
Test everything (unit + integration)
cargo test --all-features
Integration tests
These require WiFi hardware. Please make sure you run this locally before your PR to ensure everything works.
cargo test --test integration_test --all-features
If you do not have access to WiFi hardware (for whatever odd reason that is), you can do something like this:
sudo modprobe mac80211_hwsim radios=2
cargo test --test integration_test --all-features
sudo modprobe -r mac80211_hwsim
Note: This method only works on Linux
Documentation
When adding new features or changing existing APIs:
- Update rustdoc comments in the source code
- Add or update examples in the
examples/directory - Update this mdBook documentation if user-facing changes are made
- Update the CHANGELOG.md
To build the documentation locally:
# API documentation
cargo doc --open --no-deps
# User guide (this book)
cd docs
mdbook build
mdbook serve --open
Code Style
- Follow standard Rust formatting:
cargo fmt - Pass clippy checks:
cargo clippy -- -D warnings - No unsafe code (enforced by workspace lints)
- Add doc comments for public APIs
- Write tests for new functionality
License
All contributions fall under the dual MIT/Apache-2.0 license.
Getting Help
- Join our Discord server
- Open an issue for questions or bugs
- Check existing issues and PRs for similar work
Architecture
This page describes the internal architecture of the nmrs library. Understanding this helps when contributing or debugging.
Crate Structure
nmrs/src/
├── lib.rs # Crate root: re-exports, Result type alias
├── api/ # Public API layer
│ ├── mod.rs
│ ├── network_manager.rs # NetworkManager struct and methods
│ ├── models/ # Data types (Device, Network, etc.)
│ │ ├── mod.rs
│ │ ├── device.rs
│ │ ├── wifi.rs
│ │ ├── vpn.rs
│ │ ├── bluetooth.rs
│ │ ├── config.rs
│ │ ├── error.rs
│ │ ├── connection_state.rs
│ │ └── state_reason.rs
│ └── builders/ # Connection settings builders
│ ├── mod.rs
│ ├── connection_builder.rs
│ ├── wifi.rs
│ ├── wifi_builder.rs
│ ├── vpn.rs
│ ├── wireguard_builder.rs
│ └── bluetooth.rs
├── core/ # Business logic
│ ├── mod.rs
│ ├── connection.rs # Wi-Fi/Ethernet connect/disconnect
│ ├── connection_settings.rs # Saved connection management
│ ├── device.rs # Device listing, Wi-Fi control
│ ├── scan.rs # Wi-Fi scanning
│ ├── vpn.rs # VPN connect/disconnect/list
│ ├── bluetooth.rs # Bluetooth connections
│ └── state_wait.rs # Wait for state transitions
├── dbus/ # D-Bus proxy types
│ ├── mod.rs
│ ├── main_nm.rs # NetworkManager proxy
│ ├── device.rs # Device proxy
│ ├── wireless.rs # Wireless device proxy
│ ├── access_point.rs # Access point proxy
│ ├── active_connection.rs # Active connection proxy
│ ├── wired.rs # Wired device proxy
│ └── bluetooth.rs # Bluetooth device proxy
├── monitoring/ # D-Bus signal monitoring
│ ├── mod.rs
│ ├── network.rs # AP added/removed signals
│ ├── device.rs # Device state change signals
│ ├── wifi.rs # Current connection info
│ ├── bluetooth.rs # Bluetooth signals
│ ├── info.rs # Network detail retrieval
│ └── transport.rs # Signal transport
├── types/ # Constants and registries
│ ├── mod.rs
│ ├── constants.rs # NM device type codes
│ └── device_type_registry.rs # Device type capabilities
└── util/ # Utilities
├── mod.rs
├── utils.rs # Channel calculation, SSID decoding, etc.
└── validation.rs # Input validation
Layer Architecture
┌──────────────────────────────────────────────────────────┐
│ Your Application │
├──────────────────────────────────────────────────────────┤
│ api/network_manager.rs ← Public API (NetworkManager) │
│ api/models/ ← Public data types │
│ api/builders/ ← Public connection builders │
├──────────────────────────────────────────────────────────┤
│ core/ ← Business logic (not public) │
│ monitoring/ ← Signal monitoring (not public) │
├──────────────────────────────────────────────────────────┤
│ dbus/ ← D-Bus proxy types (not public) │
│ util/ ← Utilities (not public) │
│ types/ ← Constants (not public) │
├──────────────────────────────────────────────────────────┤
│ zbus ← D-Bus library │
├──────────────────────────────────────────────────────────┤
│ D-Bus System Bus → NetworkManager Daemon │
└──────────────────────────────────────────────────────────┘
API Layer
The api module defines the public interface:
NetworkManagerdelegates tocorefunctionsmodelsdefine all public data typesbuildersconstruct NM settings dictionaries
Core Layer
The core module contains the actual business logic:
connection.rshandles Wi-Fi/Ethernet connect/disconnectscan.rshandles network scanning and listingvpn.rshandles WireGuard VPN operationsstate_wait.rsuses D-Bus signals to wait for state transitions
D-Bus Layer
The dbus module defines typed proxy structs generated with zbus::proxy macros. Each proxy corresponds to a NetworkManager D-Bus interface.
Monitoring Layer
The monitoring module subscribes to D-Bus signals for real-time updates:
- Network list changes (AP added/removed)
- Device state changes
- Active connection state
Key Design Decisions
Signal-Based State Waiting
Instead of polling, nmrs uses D-Bus signals to wait for state transitions. When you call connect(), it:
- Sends the
AddAndActivateConnectionD-Bus call - Subscribes to
StateChangedsignals on the device - Awaits the signal with a timeout
- Returns success on
Activated, or maps the failure reason to aConnectionError
This is more efficient and responsive than polling.
Non-Exhaustive Types
All public enums and structs are #[non_exhaustive]. This allows adding new fields, variants, and error types without breaking downstream code.
Connection Reuse
When connecting to a network, nmrs checks for an existing saved profile first. If found, it activates the saved profile rather than creating a new one. This preserves user settings and avoids duplicate profiles.
Validation
Input validation happens at two levels:
- Model constructors (e.g.,
BluetoothIdentity::new()validates MAC format) - Builder build methods (e.g.,
WireGuardBuilder::build()validates keys and addresses)
Next Steps
- Testing – how to run tests
- Contributing – development workflow
Testing
nmrs includes unit tests, integration tests, and model tests. Since many operations require a running NetworkManager daemon, tests are divided into offline and online categories.
Running Tests
Unit Tests
Unit tests cover validation, model construction, and builder logic. They run without NetworkManager:
cd nmrs
cargo test
Specific Test Modules
# Model tests
cargo test --lib api::models::tests
# Builder tests
cargo test --lib api::builders
# Validation tests
cargo test --lib util::validation
Integration Tests
Integration tests require a running NetworkManager instance:
cargo test --test integration_test
cargo test --test validation_test
Note: Integration tests that interact with real hardware may fail in CI or on systems without Wi-Fi adapters.
Test Categories
Model Tests (api/models/tests.rs)
Comprehensive tests for all data types:
- Device type conversions and display formatting
- Device state conversions and transitional state detection
- Wi-Fi security type construction and methods
- EAP options construction (direct and builder)
- VPN credentials construction (direct and builder)
- WireGuard peer configuration
- Bluetooth identity validation
- Timeout config and connection options
- Error type formatting
Builder Tests
Each builder module includes its own tests:
connection_builder.rs— base settings, IPv4/IPv6 configuration, custom sectionswireguard_builder.rs— WireGuard settings, validation, multiple peerswifi_builder.rs— Wi-Fi settings, bands, modes
Validation Tests
util/validation.rs tests input validation:
- SSID validation
- Connection name validation
- Wi-Fi security validation (empty passwords, etc.)
- VPN credential validation
- Bluetooth address validation
Writing Tests
Offline Tests
For logic that doesn't require D-Bus:
#![allow(unused)] fn main() { #[cfg(test)] mod tests { use super::*; #[test] fn test_device_type_from_code() { assert_eq!(DeviceType::from(1), DeviceType::Ethernet); assert_eq!(DeviceType::from(2), DeviceType::Wifi); } } }
Async Tests
For code that uses async:
#![allow(unused)] fn main() { #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_something_async() { // Test async logic } } }
Docker Testing
For reproducible testing with a real NetworkManager instance:
docker build -t nmrs-test .
docker run --privileged nmrs-test cargo test
The project includes a Dockerfile configured for testing.
CI/CD
Tests run automatically via GitHub Actions on every push and pull request. The CI workflow:
- Checks formatting (
cargo fmt --check) - Runs clippy (
cargo clippy) - Runs unit tests (
cargo test) - Builds documentation (
mdbook build)
Next Steps
- Contributing – contribution guidelines
- Architecture – understand the codebase
Release Process
This page documents the release process for nmrs.
Versioning
nmrs follows Semantic Versioning:
- Major (X.0.0) — breaking API changes
- Minor (0.X.0) — new features, backward-compatible
- Patch (0.0.X) — bug fixes, backward-compatible
Changelog
See nmrs/CHANGELOG.md for the full changelog.
Release Checklist
- Update version in
Cargo.toml - Update the changelog
- Run
cargo test - Run
cargo clippy - Run
cargo fmt --check - Build documentation (
mdbook buildindocs/) - Create a git tag:
git tag v2.2.0 - Push the tag:
git push origin v2.2.0 - Publish to crates.io:
cargo publish -p nmrs
Distribution Channels
| Channel | Package |
|---|---|
| crates.io | nmrs library |
API Stability
- All public types are
#[non_exhaustive]— new fields/variants can be added in minor releases - Existing API signatures are preserved across minor releases
- Deprecated items are documented and kept for at least one minor release
Next Steps
- Contributing – how to contribute
- Changelog – full version history
Troubleshooting
Common issues and their solutions when using nmrs.
Connection Issues
"D-Bus error" on startup
Symptom: ConnectionError::Dbus when calling NetworkManager::new()
Causes:
- NetworkManager is not running
- D-Bus system bus is not accessible
- Insufficient permissions
Solutions:
# Check if NetworkManager is running
systemctl status NetworkManager
# Start NetworkManager if not running
sudo systemctl start NetworkManager
sudo systemctl enable NetworkManager
# Check D-Bus
busctl list | grep NetworkManager
"network not found" (NotFound)
Symptom: ConnectionError::NotFound when connecting
Solutions:
- Verify the SSID is spelled correctly (case-sensitive)
- Trigger a scan first:
nm.scan_networks(None).await? - Check if Wi-Fi is enabled:
nm.wifi_state().await?returns aRadioStatewith.enabledand.hardware_enabledfields - Check if the network is in range
- For hidden networks, the network won't appear in scans but should still connect
"authentication failed" (AuthFailed)
Symptom: ConnectionError::AuthFailed when connecting
Solutions:
- Verify the password is correct
- For WPA-Enterprise, check username format (some networks require
user@domain, others justuser) - Delete the saved profile and retry:
nm.forget("SSID").await? - Check if the AP has MAC filtering enabled
"connection timeout" (Timeout)
Symptom: ConnectionError::Timeout
Solutions:
- Increase the timeout:
#![allow(unused)] fn main() { let config = TimeoutConfig::new() .with_connection_timeout(Duration::from_secs(60)); let nm = NetworkManager::with_config(config).await?; } - Enterprise Wi-Fi (WPA-EAP) often needs longer timeouts
- Check if another connection operation is in progress:
nm.is_connecting().await? - Check signal strength — weak signals cause timeouts
"DHCP failed" (DhcpFailed)
Symptom: ConnectionError::DhcpFailed
Solutions:
- Check if the DHCP server is working (try connecting with another device)
- Try releasing and renewing: disconnect and reconnect
- Check for IP address conflicts on the network
"no Wi-Fi device found" (NoWifiDevice)
Symptom: ConnectionError::NoWifiDevice
Solutions:
# Check if a Wi-Fi adapter is detected
ip link show
nmcli device status
# Check if the driver is loaded
lspci -k | grep -A 3 -i network
# Check rfkill
rfkill list
sudo rfkill unblock wifi
VPN Issues
"invalid WireGuard private key"
Solutions:
- Ensure the key is base64-encoded (44 characters, ending in
=) - Don't include quotes around the key
- Generate a valid key:
wg genkey
"invalid address"
Solutions:
- Include CIDR notation:
10.0.0.2/24(not just10.0.0.2) - Verify the IP is valid
"invalid VPN gateway"
Solutions:
- Use
host:portformat:vpn.example.com:51820 - Verify the port is a valid number (1–65535)
VPN connects but no traffic
Solutions:
- Check
allowed_ips— use0.0.0.0/0for full tunnel - Verify DNS settings:
nm.get_vpn_info("MyVPN").await?.dns_servers - Check the WireGuard interface:
ip addr show wg-*
Bluetooth Issues
No Bluetooth devices found
Solutions:
# Check Bluetooth service
systemctl status bluetooth
# Check if adapter is detected
bluetoothctl show
# Make sure the device is paired
bluetoothctl paired-devices
- Devices must be paired before nmrs can see them
- Use
bluetoothctl pair <MAC>to pair
Permission Issues
PolicyKit errors
If operations fail with permission errors:
# Check your groups
groups
# Add yourself to the network group
sudo usermod -aG network $USER
# Log out and back in
Or create a PolicyKit rule at /etc/polkit-1/rules.d/50-nmrs.rules:
polkit.addRule(function(action, subject) {
if (action.id.indexOf("org.freedesktop.NetworkManager.") == 0 &&
subject.isInGroup("network")) {
return polkit.Result.YES;
}
});
Debug Logging
Enable debug logging to diagnose issues:
RUST_LOG=nmrs=debug cargo run
For D-Bus level details:
RUST_LOG=nmrs=trace,zbus=debug cargo run
Monitor NetworkManager's own logs:
journalctl -u NetworkManager -f
Getting Help
- Discord: discord.gg/Sk3VfrHrN4
- GitHub Issues: github.com/cachebag/nmrs/issues
- API Docs: docs.rs/nmrs
FAQ
General
What is nmrs?
nmrs is a Rust library for managing network connections on Linux via NetworkManager's D-Bus interface. It provides a safe, async API for Wi-Fi, Ethernet, Bluetooth, and VPN management.
What does nmrs stand for?
NetworkManager Rust — nmrs.
Is nmrs production-ready?
Yes. nmrs is at version 2.2.0 with a stable API. All public types are also #[non_exhaustive] to allow backward-compatible additions.
What Linux distributions are supported?
Any distribution that runs NetworkManager. This includes Ubuntu, Fedora, Arch Linux, Debian, openSUSE, NixOS, and many others.
Does nmrs work on macOS or Windows?
No. nmrs is Linux-specific since it communicates with NetworkManager over D-Bus, which is a Linux service.
Library
Which async runtime should I use?
nmrs works with any async runtime (Tokio, async-std, smol, GLib). Tokio is recommended and used in all examples. See Async Runtime Support.
Can I use nmrs without an async runtime?
No. D-Bus communication is inherently async. You can use block_on() from smol or tokio::runtime::Runtime::block_on() if you need a synchronous wrapper.
Is NetworkManager the only way to manage Wi-Fi on Linux?
No, but it's the most widely used network management daemon. Other options include iwd, connman, and wpa_supplicant (direct). nmrs specifically targets NetworkManager.
Do I need root permissions?
Usually no. NetworkManager uses PolicyKit for authorization, and most desktop Linux setups grant network management permissions to the logged-in user. If you're running in a headless environment, you may need to configure PolicyKit rules. See Requirements.
Can I connect to multiple networks simultaneously?
A device can only have one active connection at a time. However, you can have different connections on different devices (e.g., Wi-Fi on wlan0 and Ethernet on eth0 simultaneously).
Can I make concurrent connection calls?
No. Concurrent connection operations (calling connect() from multiple tasks) are not supported. Use is_connecting() to check before starting a new connection.
How do I handle saved connections?
When nmrs connects to a network, NetworkManager saves the profile. On subsequent connections, the saved profile is reused automatically. You don't need to provide credentials again. Use forget() to delete a saved profile.
VPN
Which VPN protocols are supported?
WireGuard and OpenVPN. WireGuard uses NetworkManager's native kernel integration (no plugin needed). OpenVPN requires the networkmanager-openvpn plugin.
Do I need the WireGuard kernel module?
Yes. WireGuard is built into the Linux kernel since version 5.6. On older kernels, install the wireguard module. NetworkManager's WireGuard support requires NM 1.16+.
Can I import a .ovpn file?
Yes. Use nm.import_ovpn("client.ovpn", Some("user"), Some("pass")).await? to parse and activate an OpenVPN profile in one call. Inline certificates are extracted and persisted automatically.
Can I import a .conf WireGuard file?
Not directly. Extract the values from the config file and pass them to WireGuardConfig::new().
Troubleshooting
Where can I get help?
- Discord: discord.gg/Sk3VfrHrN4
- GitHub Issues: github.com/cachebag/nmrs/issues
- Troubleshooting Guide: Troubleshooting
Changelog
See the full changelog on GitHub: nmrs CHANGELOG
nmrs (Library) Highlights
2.2.0
- Concurrency protection —
is_connecting()API WirelessHardwareEnabledproperty support- BDADDR to BlueZ path resolution
- Mixed WPA1+WPA2 network support
2.1.0
#[must_use]annotations on public builder APIs
2.0.1
- IPv6 address support for devices and networks
WifiModeenum for builder API- Input validation for SSIDs, credentials, and addresses
- Idempotent
forget_vpn()behavior
2.0.0
- Bluetooth support (PAN and DUN)
- Configurable timeouts via
TimeoutConfig VpnCredentialsandEapOptionsbuilder patternsConnectionOptionsfor autoconnect configurationConnectionBuilderfor advanced connection settingsWireGuardBuilderwith validation
1.x
- WireGuard VPN support
- VPN error handling improvements
- Docker image for testing
- Initial release with Wi-Fi and Ethernet support
License
nmrs is dual-licensed under your choice of either:
- MIT License (LICENSE-MIT)
- Apache License, Version 2.0 (LICENSE-APACHE)
What This Means
You may use, copy, modify, and distribute nmrs under the terms of either license. Choose whichever is more convenient for your project:
- MIT is simpler and more permissive — just include the copyright notice
- Apache 2.0 provides additional protections including patent grants
Using nmrs in Your Project
Open Source Projects
Both licenses are compatible with most open source licenses. If your project uses MIT, Apache 2.0, BSD, or GPL, you can use nmrs without issues.
Commercial Projects
Both licenses are permissive and allow commercial use. You can use nmrs in proprietary software.
Contributing
Contributions to nmrs are accepted under the same dual license. By submitting a pull request, you agree to license your contribution under both MIT and Apache 2.0.
Third-Party Dependencies
nmrs depends on several third-party crates, each with their own licenses:
| Crate | License |
|---|---|
zbus | MIT |
zvariant | MIT |
serde | MIT OR Apache-2.0 |
thiserror | MIT OR Apache-2.0 |
uuid | MIT OR Apache-2.0 |
log | MIT OR Apache-2.0 |
futures | MIT OR Apache-2.0 |
tokio | MIT |
base64 | MIT OR Apache-2.0 |
async-trait | MIT OR Apache-2.0 |
All dependencies use permissive licenses compatible with nmrs's dual license.