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 - Full WireGuard VPN integration
- 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
Project Structure
The nmrs project consists of two main components:
nmrs (Library)
The core Rust library providing NetworkManager bindings and network management capabilities. This is what you'll use if you're building applications that need to manage network connections programmatically.
nmrs-gui (Application)
A beautiful, Wayland-compatible GTK4 graphical interface for NetworkManager. Perfect for desktop users who want a modern network management GUI.
Why nmrs?
For Developers
- 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
For Users (nmrs-gui)
- Modern UI - Clean GTK4 interface with multiple themes
- Wayland Native - First-class Wayland support
- Lightweight - Fast and efficient
- Customizable - CSS-based theming system
- DE Integration - Works great with tiling WMs (Hyprland, Sway, i3)
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().await?; for net in networks { println!("{} - {}%", net.ssid, net.strength.unwrap_or(0)); } // Connect to a network nm.connect("MyWiFi", 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 both the nmrs library (for developers) and nmrs-gui (for end users).
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(()) }
nmrs-gui Application
Arch Linux (AUR)
For Arch Linux users, install from the AUR:
yay -S nmrs
# or
paru -S nmrs
Nix/NixOS
Install via Nix:
nix-shell -p nmrs
Or add to your NixOS configuration:
environment.systemPackages = with pkgs; [
nmrs
];
From Source (GUI)
Requirements:
- Rust 1.85.1 or later
- GTK4 development libraries
- libadwaita
# Install dependencies (Arch Linux)
sudo pacman -S gtk4 libadwaita
# Install dependencies (Ubuntu/Debian)
sudo apt install libgtk-4-dev libadwaita-1-dev
# Install dependencies (Fedora)
sudo dnf install gtk4-devel libadwaita-devel
# Build and install
git clone https://github.com/cachebag/nmrs.git
cd nmrs
cargo build --release -p nmrs-gui
sudo cp target/release/nmrs-gui /usr/local/bin/nmrs
System Requirements
For the Library (nmrs)
- Operating System: Linux (any modern distribution)
- Rust: 1.78.0 or later
- NetworkManager: Version 1.0 or later, running and accessible via D-Bus
- D-Bus: System bus must be available
For the GUI (nmrs-gui)
All of the above, plus:
- Rust: 1.85.1 or later
- GTK4: Version 4.0 or later
- libadwaita: For modern GNOME styling
- Wayland or X11: Display server
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
- Library Users: Continue to the Quick Start guide
- GUI Users: See the GUI Configuration 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().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", 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", 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_connections().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().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, 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
For nmrs Library
- Rust: 1.78.0 or later
- Edition: 2021
The library uses stable Rust features only.
For nmrs-gui
- Rust: 1.85.1 or later
- Edition: 2021
The GUI requires a newer Rust version due to GTK4 bindings.
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.
nmrs-gui Dependencies
Additional system libraries required:
Arch Linux:
sudo pacman -S gtk4 libadwaita
Ubuntu/Debian:
sudo apt install libgtk-4-dev libadwaita-1-dev build-essential
Fedora:
sudo dnf install gtk4-devel libadwaita-devel
NixOS:
# Handled automatically by the Nix package
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 Library
| nmrs Version | Minimum Rust | NetworkManager | Notable Features |
|---|---|---|---|
| 2.0.0 | 1.78.0 | 1.0+ | Full API rewrite |
| 1.x | 1.70.0 | 1.0+ | Initial release |
nmrs-gui
| GUI Version | Minimum Rust | GTK | Notable Features |
|---|---|---|---|
| 1.1.0 | 1.85.1 | 4.0 | Themes support |
| 1.0.0 | 1.82.0 | 4.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().await?; // Connect to WPA-PSK network nm.connect("MyWiFi", WifiSecurity::WpaPsk { psk: "password".into() }).await?; // Get current connection if let Some(ssid) = nm.current_ssid().await { println!("Connected to: {}", ssid); } // Disconnect nm.disconnect().await?; Ok(()) }
Security Types
nmrs supports all major WiFi security protocols:
Open Networks
No authentication required:
#![allow(unused)] fn main() { nm.connect("FreeWiFi", WifiSecurity::Open).await?; }
WPA-PSK (Personal)
Password-based authentication:
#![allow(unused)] fn main() { nm.connect("HomeWiFi", 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", 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().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_wifi_enabled(false).await?; // Enable WiFi nm.set_wifi_enabled(true).await?; // Check WiFi status let enabled = nm.is_wifi_enabled().await?; println!("WiFi is {}", if enabled { "enabled" } else { "disabled" }); }
Network Scanning
Trigger a fresh scan:
#![allow(unused)] fn main() { // Request a scan (may take a few seconds) nm.request_scan().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().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(info) = nm.current_network_info().await? { println!("SSID: {}", info.ssid); println!("IP: {:?}", info.ip4_address); println!("Gateway: {:?}", info.gateway); println!("DNS: {:?}", info.dns); } }
Error Handling
WiFi operations can fail for various reasons. Handle them gracefully:
#![allow(unused)] fn main() { use nmrs::ConnectionError; match nm.connect("Network", 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
Best Practices
1. Cache the NetworkManager Instance
#![allow(unused)] fn main() { // Good - reuse the same instance let nm = NetworkManager::new().await?; nm.list_networks().await?; nm.connect("WiFi", WifiSecurity::Open).await?; // Avoid - creating multiple instances let nm1 = NetworkManager::new().await?; nm1.list_networks().await?; let nm2 = NetworkManager::new().await?; // Unnecessary nm2.connect("WiFi", 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", 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().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().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().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().await?; let networks = nm.list_networks().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", WifiSecurity::Open).await?; // WPA-PSK network (password) nm.connect("HomeWiFi", 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", 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().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", 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", 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", 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", 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", 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", 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", 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", 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", WifiSecurity::Open).await?; // Hidden WPA-PSK network nm.connect("SecretLab", 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", 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
VPN Connections
nmrs provides full support for WireGuard VPN 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
- 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
Quick Start
Basic WireGuard VPN connection:
use nmrs::{NetworkManager, VpnCredentials, VpnType, WireGuardPeer}; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; // Create a WireGuard peer let peer = WireGuardPeer::new( "server_public_key_here", "vpn.example.com:51820", vec!["0.0.0.0/0".into()], // Route all traffic through VPN ).with_persistent_keepalive(25); // Create VPN credentials let creds = VpnCredentials::new( VpnType::WireGuard, "MyVPN", "vpn.example.com:51820", "your_private_key_here", "10.0.0.2/24", // Your VPN IP vec![peer], ).with_dns(vec!["1.1.1.1".into()]); // Connect nm.connect_vpn(creds).await?; println!("Connected to VPN!"); Ok(()) }
VPN Credentials
The VpnCredentials struct contains all necessary VPN configuration:
#![allow(unused)] fn main() { pub struct VpnCredentials { pub vpn_type: VpnType, 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<String>, } }
Creating Credentials
#![allow(unused)] fn main() { use nmrs::{VpnCredentials, VpnType, WireGuardPeer}; let peer = WireGuardPeer::new( "base64_public_key", "vpn.example.com:51820", vec!["0.0.0.0/0".into()], ); let creds = VpnCredentials::new( VpnType::WireGuard, "WorkVPN", // Connection name "vpn.example.com:51820", // Gateway "base64_private_key", // Your private key "10.0.0.2/24", // Your VPN IP address vec![peer], // WireGuard peers ); }
With Custom DNS
#![allow(unused)] fn main() { let creds = VpnCredentials::new( VpnType::WireGuard, "MyVPN", "vpn.example.com:51820", "private_key", "10.0.0.2/24", vec![peer], ).with_dns(vec![ "1.1.1.1".into(), "8.8.8.8".into(), ]); }
With Custom MTU
#![allow(unused)] fn main() { let creds = creds.with_mtu(1420); // Standard WireGuard MTU }
WireGuard Peers
Each WireGuard connection can have multiple peers:
#![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>, } }
Creating Peers
#![allow(unused)] fn main() { use nmrs::WireGuardPeer; // Basic peer let peer = WireGuardPeer::new( "peer_public_key", "vpn.example.com:51820", vec!["0.0.0.0/0".into()], ); // Peer with keepalive let peer = WireGuardPeer::new( "peer_public_key", "vpn.example.com:51820", vec!["0.0.0.0/0".into()], ).with_persistent_keepalive(25); // Peer with preshared key let peer = peer.with_preshared_key("base64_preshared_key"); }
Multiple Peers
#![allow(unused)] fn main() { let peer1 = WireGuardPeer::new( "peer1_public_key", "vpn1.example.com:51820", vec!["10.0.0.0/8".into()], ); let peer2 = WireGuardPeer::new( "peer2_public_key", "vpn2.example.com:51820", vec!["192.168.0.0/16".into()], ); let creds = VpnCredentials::new( VpnType::WireGuard, "MultiPeerVPN", "vpn1.example.com:51820", "private_key", "10.0.0.2/24", vec![peer1, peer2], // Multiple peers ); }
VPN Operations
Connect to VPN
#![allow(unused)] fn main() { nm.connect_vpn(creds).await?; }
Disconnect from VPN
#![allow(unused)] fn main() { nm.disconnect_vpn("MyVPN").await?; }
List VPN Connections
#![allow(unused)] fn main() { let vpns = nm.list_vpn_connections().await?; for vpn in vpns { println!("Name: {}", vpn.name); println!("Type: {:?}", vpn.vpn_type); println!("State: {:?}", vpn.state); } }
Get VPN Information
#![allow(unused)] fn main() { let info = nm.get_vpn_info("MyVPN").await?; println!("VPN State: {:?}", info.state); if let Some(ip) = info.ip4_address { println!("VPN IP: {}", ip); } if let Some(device) = info.device { println!("Device: {}", device); } }
Check if VPN is Active
#![allow(unused)] fn main() { let vpns = nm.list_vpn_connections().await?; let active = vpns.iter().any(|v| { matches!(v.state, nmrs::models::ActiveConnectionState::Activated) }); if active { println!("VPN is active"); } else { println!("VPN is not active"); } }
Routing Configuration
Route All Traffic
Send all traffic through the VPN:
#![allow(unused)] fn main() { let peer = WireGuardPeer::new( "public_key", "vpn.example.com:51820", vec!["0.0.0.0/0".into()], // All IPv4 ); }
Split Tunnel
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(), // Private network "192.168.0.0/16".into(), // Another private network ], ); }
IPv6 Support
#![allow(unused)] fn main() { let peer = WireGuardPeer::new( "public_key", "vpn.example.com:51820", vec![ "0.0.0.0/0".into(), // All IPv4 "::/0".into(), // All IPv6 ], ); }
Error Handling
Handle VPN-specific errors:
#![allow(unused)] fn main() { use nmrs::ConnectionError; match nm.connect_vpn(creds).await { Ok(_) => println!("VPN connected"), Err(ConnectionError::AuthFailed) => { eprintln!("VPN authentication failed - check keys"); } Err(ConnectionError::Timeout) => { eprintln!("VPN connection timed out - check gateway"); } Err(ConnectionError::NotFound) => { eprintln!("VPN gateway not reachable"); } Err(e) => eprintln!("VPN error: {}", e), } }
Complete Example
Here's a complete VPN client:
use nmrs::{NetworkManager, VpnCredentials, VpnType, WireGuardPeer}; #[tokio::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; // Check if already connected let vpns = nm.list_vpn_connections().await?; if let Some(active_vpn) = vpns.iter().find(|v| { matches!(v.state, nmrs::models::ActiveConnectionState::Activated) }) { println!("Already connected to: {}", active_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 creds = VpnCredentials::new( VpnType::WireGuard, "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(creds).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 guide
- VPN Management - Managing VPN profiles
- Examples - Complete VPN client example
Security Best Practices
- Never hardcode keys - Use environment variables or secure storage
- Rotate keys regularly - Update WireGuard keys periodically
- Use preshared keys - Add extra layer of security with PSK
- Verify endpoints - Ensure gateway addresses are correct
- Monitor connection - Check VPN status regularly
Troubleshooting
VPN Won't Connect
#![allow(unused)] fn main() { // Check if WireGuard is available // NetworkManager should handle this automatically // Verify your credentials are correct println!("Gateway: {}", creds.gateway); println!("Address: {}", creds.address); // Don't print private keys! }
Connection Drops
Use persistent keepalive:
#![allow(unused)] fn main() { let peer = peer.with_persistent_keepalive(25); // Send keepalive every 25s }
DNS Not Working
Explicitly set DNS servers:
#![allow(unused)] fn main() { let creds = creds.with_dns(vec![ "1.1.1.1".into(), "8.8.8.8".into(), ]); }
Next Steps
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, VpnCredentials, VpnType, 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 creds = VpnCredentials::new( VpnType::WireGuard, "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(creds).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) |
VpnCredentials Fields
| Field | Required | Description |
|---|---|---|
vpn_type | Yes | Must be VpnType::WireGuard |
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 Credentials
Direct Constructor
#![allow(unused)] fn main() { use nmrs::{VpnCredentials, VpnType, 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 creds = VpnCredentials::new( VpnType::WireGuard, "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); }
Builder Pattern
The builder pattern avoids positional parameter confusion:
#![allow(unused)] fn main() { use nmrs::{VpnCredentials, WireGuardPeer}; let peer = WireGuardPeer::new( "HIgo9xNzJMWLKAShlKl6/bUT1VI9Q0SDBXGtLXkPFXc=", "vpn.example.com:51820", vec!["0.0.0.0/0".into()], ).with_persistent_keepalive(25); let creds = VpnCredentials::builder() .name("HomeVPN") .wireguard() .gateway("vpn.example.com:51820") .private_key("YBk6X3pP8KjKz7+HFWzVHNqL3qTZq8hX9VxFQJ4zVmM=") .address("10.0.0.2/24") .add_peer(peer) .with_dns(vec!["1.1.1.1".into()]) .with_mtu(1420) .build(); }
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
- Custom Timeouts – adjust VPN connection timeouts
- Error Handling – handle VPN-specific errors
VPN Management
Once you've set up a WireGuard VPN connection, nmrs provides methods to list, inspect, 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!("{}: {:?} [{:?}]", vpn.name, vpn.vpn_type, vpn.state, ); 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 |
|---|---|---|
name | String | Connection profile name |
vpn_type | VpnType | VPN protocol (currently WireGuard) |
state | DeviceState | Current state (Activated, Disconnected, etc.) |
interface | Option<String> | Network interface when active (e.g., wg0) |
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!("Type: {:?}", info.vpn_type); 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); }
The VpnConnectionInfo struct provides:
| Field | Type | Description |
|---|---|---|
name | String | Connection name |
vpn_type | VpnType | VPN protocol |
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 |
Note:
get_vpn_info()returnsConnectionError::NoVpnConnectionif the VPN is not currently active.
Disconnecting a VPN
#![allow(unused)] fn main() { let nm = NetworkManager::new().await?; nm.disconnect_vpn("MyVPN").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, VpnCredentials, 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!(" {} ({:?}) - {:?}", vpn.name, vpn.vpn_type, vpn.state); } // Connect let peer = WireGuardPeer::new( "SERVER_PUBLIC_KEY", "vpn.example.com:51820", vec!["0.0.0.0/0".into()], ).with_persistent_keepalive(25); let creds = VpnCredentials::builder() .name("ExampleVPN") .wireguard() .gateway("vpn.example.com:51820") .private_key("CLIENT_PRIVATE_KEY") .address("10.0.0.2/24") .add_peer(peer) .with_dns(vec!["1.1.1.1".into()]) .build(); nm.connect_vpn(creds).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 |
Next Steps
- WireGuard Setup – credential 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
Enable or disable the Wi-Fi radio globally:
#![allow(unused)] fn main() { let nm = NetworkManager::new().await?; // Check current state let enabled = nm.wifi_enabled().await?; println!("Wi-Fi enabled: {}", enabled); // Check hardware switch (rfkill) let hw_enabled = nm.wifi_hardware_enabled().await?; println!("Wi-Fi hardware enabled: {}", hw_enabled); // Toggle Wi-Fi nm.set_wifi_enabled(false).await?; // Disable nm.set_wifi_enabled(true).await?; // Enable }
Note:
wifi_hardware_enabled()reflects the rfkill state. If the hardware switch is off, enabling Wi-Fi via software will have no effect.
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_wifi_enabled(true).await?; nm.wait_for_wifi_ready().await?; // Now safe to scan and connect nm.scan_networks().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 name in &connections { println!(" {}", name); } Ok(()) }
list_saved_connections() returns the names of all saved connection profiles across all connection types — Wi-Fi, Ethernet, VPN, and Bluetooth.
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", WifiSecurity::WpaPsk { psk: "password".into(), }).await?; // Later reconnection — saved profile is used, security parameter is ignored nm.connect("HomeWiFi", 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 list changes (access points appearing or disappearing):
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 and removals on all Wi-Fi devices. The callback fires whenever the visible network list 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().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 |
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 |
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", 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", 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", 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", nmrs::WifiSecurity::Open).await?; Ok(()) } }
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().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(()) }
This is what nmrs-gui uses internally.
async-std
use nmrs::NetworkManager; #[async_std::main] async fn main() -> nmrs::Result<()> { let nm = NetworkManager::new().await?; let networks = nm.list_networks().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().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().await.unwrap(); for net in &networks { println!("{}: {}%", net.ssid, net.strength.unwrap_or(0)); } }); }
This is how nmrs-gui integrates nmrs into its GTK4 interface.
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", 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 signals
← D-Bus: Signal whenever an AP appears or disappears
→ 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", 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().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().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, 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, 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().await?; let visible = nm.list_networks().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, 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, 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 credentials with the builder pattern
- Connects and retrieves VPN details
- Displays IP configuration and DNS
- Cleanly disconnects on completion
Code
use nmrs::{NetworkManager, VpnCredentials, 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 credentials let creds = VpnCredentials::builder() .name("ExampleVPN") .wireguard() .gateway( std::env::var("WG_ENDPOINT") .unwrap_or_else(|_| "vpn.example.com:51820".into()), ) .private_key( std::env::var("WG_PRIVATE_KEY") .expect("Set WG_PRIVATE_KEY"), ) .address( std::env::var("WG_ADDRESS") .unwrap_or_else(|_| "10.0.0.2/24".into()), ) .add_peer(peer) .with_dns(vec!["1.1.1.1".into(), "8.8.8.8".into()]) .with_mtu(1420) .build(); // Connect println!("Connecting to VPN..."); nm.connect_vpn(creds).await?; println!("Connected!\n"); // Show VPN details let info = nm.get_vpn_info("ExampleVPN").await?; println!("VPN Connection Details:"); println!(" Name: {}", info.name); println!(" Type: {:?}", info.vpn_type); 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
Type: 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 creds = VpnCredentials::builder() .name("MultiPeerVPN") .wireguard() .gateway("us-east.vpn.example.com:51820") .private_key("client_private_key") .address("10.0.0.2/24") .add_peer(peer1) .add_peer(peer2) .build(); }
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(enabled) = nm.wifi_enabled().await { println!("Wi-Fi enabled: {}", 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().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
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
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().await { Ok(_) => println!("Scan complete"), Err(e) => eprintln!("Scan failed: {}", e), } } async fn list_networks(nm: &NetworkManager) { match nm.list_networks().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, 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().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 name in &connections { println!(" {}", name); } } 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
GUI Overview
nmrs-gui is a Wayland-compatible GTK4 graphical interface for NetworkManager. It provides a modern, lightweight network management UI that integrates well with tiling window managers.
Features
- Wi-Fi Management — scan, connect, disconnect, and view network details
- Ethernet Support — view and manage wired connections
- Enterprise Wi-Fi — WPA-EAP/802.1X with password, username, and certificate support
- Multiple Themes — Gruvbox, Nord, Dracula, Catppuccin, Tokyo Night (light and dark)
- Custom Styling — CSS-based customization via
~/.config/nmrs/style.css - Real-Time Updates — D-Bus signal monitoring for live network and device state
- Single Instance — file lock ensures only one instance runs at a time
- Wayland Native — first-class Wayland support, also works on X11
Architecture
nmrs-gui
├── Main entry (single-instance lock, GTK Application)
├── CSS loading (bundled → theme → user overrides)
├── Header
│ ├── Wi-Fi label and status
│ ├── Theme selector dropdown
│ ├── Refresh button
│ ├── Light/Dark toggle
│ └── Wi-Fi enable/disable switch
├── Network list view
│ ├── Grouped by SSID + band
│ ├── Signal strength indicators
│ └── Double-click to connect, arrow for details
├── Network details page
│ ├── SSID, status, signal, BSSID
│ ├── Frequency, channel, mode, speed
│ ├── Security type
│ └── Forget button
├── Wired device list view
│ └── Double-click to connect, arrow for details
├── Wired details page
│ ├── Interface, state, type, MAC, driver
│ └── Managed status
├── Connect modal
│ ├── Password field
│ ├── EAP username (for enterprise)
│ ├── CA certificate path
│ └── System CA checkbox
└── D-Bus monitors
├── Network changes (debounced refresh)
└── Device changes (debounced refresh)
Screenshots
nmrs-gui uses a clean, minimal interface that adapts to your chosen theme and color scheme.
Requirements
- GTK4 4.0+
- Linux with NetworkManager running
- Wayland or X11 display server
Next Steps
- Installation — install nmrs-gui
- Configuration — customize behavior
- Themes — choose and customize themes
- Waybar Integration — launch from your status bar
GUI Installation
Arch Linux (AUR)
The easiest way to install on Arch Linux:
yay -S nmrs
# or
paru -S nmrs
Nix/NixOS
Nix Shell
nix-shell -p nmrs
NixOS Configuration
Add to your NixOS configuration:
environment.systemPackages = with pkgs; [
nmrs
];
Nix Flake
nix run github:cachebag/nmrs
From Source
Dependencies
Install build dependencies for your distribution:
Arch Linux:
sudo pacman -S gtk4 libadwaita rust
Ubuntu/Debian:
sudo apt install libgtk-4-dev libadwaita-1-dev build-essential
Fedora:
sudo dnf install gtk4-devel libadwaita-devel rust cargo
Build
git clone https://github.com/cachebag/nmrs.git
cd nmrs
cargo build --release -p nmrs-gui
Install
sudo cp target/release/nmrs-gui /usr/local/bin/nmrs
Verification
After installation, launch nmrs-gui:
nmrs
The window should appear showing available Wi-Fi networks.
System Requirements
- Rust: 1.94.0+ (for building from source)
- GTK4: 4.0+
- NetworkManager: running and accessible via D-Bus
- Display: Wayland or X11
- Linux: any modern distribution
Desktop Entry
If you installed from source, you may want to create a desktop entry:
[Desktop Entry]
Name=nmrs
Comment=NetworkManager GUI
Exec=nmrs
Icon=network-wireless
Type=Application
Categories=Network;Settings;
Save as ~/.local/share/applications/nmrs.desktop.
Next Steps
- Configuration — customize the GUI
- Themes — change the visual theme
- Waybar Integration — launch from your status bar
GUI Configuration
nmrs-gui stores its configuration in ~/.config/nmrs/.
Configuration Files
| File | Purpose |
|---|---|
~/.config/nmrs/theme | Selected theme name |
~/.config/nmrs/style.css | Custom CSS overrides |
Theme Selection
The active theme is stored in ~/.config/nmrs/theme as a plain text string:
gruvbox— Gruvbox themenord— Nord themedracula— Dracula themecatppuccin— Catppuccin themetokyo— Tokyo Night themelight— System default, light modedark— System default, dark mode
You can change the theme through the GUI using the dropdown in the header bar, or by editing the file directly.
Custom CSS
nmrs-gui creates a default style.css at ~/.config/nmrs/style.css on first launch. Edit this file to customize the interface.
CSS Loading Order
- Bundled stylesheet — base styles at application priority
- Selected theme — theme overrides at user priority
- User stylesheet —
~/.config/nmrs/style.cssat user priority (highest)
Since user CSS loads last, it always takes precedence over themes.
CSS Variables
Themes define CSS variables that you can override:
/* Override theme colors */
:root {
--bg-primary: #1a1b26;
--bg-secondary: #24283b;
--bg-tertiary: #414868;
--text-primary: #c0caf5;
--text-secondary: #a9b1d6;
--text-tertiary: #565f89;
--border-color: #3b4261;
--border-color-hover: #545c7e;
--accent-color: #7aa2f7;
--success-color: #9ece6a;
--warning-color: #e0af68;
--error-color: #f7768e;
}
Example Customizations
Larger font:
window {
font-size: 16px;
}
Custom accent color:
:root {
--accent-color: #ff6b6b;
}
Rounded corners:
.network-row {
border-radius: 8px;
}
Tiling Window Manager Configuration
Hyprland
Add to ~/.config/hypr/hyprland.conf:
windowrule = float, class:org.nmrs.ui
windowrule = size 400 600, class:org.nmrs.ui
windowrule = center, class:org.nmrs.ui
Sway
Add to ~/.config/sway/config:
for_window [app_id="org.nmrs.ui"] floating enable
for_window [app_id="org.nmrs.ui"] resize set width 400 height 600
i3
Add to ~/.config/i3/config:
for_window [class="org.nmrs.ui"] floating enable
for_window [class="org.nmrs.ui"] resize set 400 600
Signal Strength Indicators
nmrs-gui uses CSS classes for signal strength:
| Class | Signal Range |
|---|---|
network-good | Strong signal |
network-okay | Medium signal |
network-poor | Weak signal |
You can style these in your CSS:
.network-good { color: var(--success-color); }
.network-okay { color: var(--warning-color); }
.network-poor { color: var(--error-color); }
Application ID
The GTK application ID is org.nmrs.ui. Use this for window rules and desktop integration.
Single Instance
nmrs-gui uses a file lock to ensure only one instance runs at a time. If you try to launch a second instance, it will exit silently.
Next Steps
- Themes — explore available themes
- Waybar Integration — launch from your status bar
Themes
nmrs-gui ships with five themes, each with light and dark variants. Themes are selected from the dropdown in the header bar.
Available Themes
Gruvbox
A retro groove color scheme with warm colors. Inspired by the popular Vim color scheme.
Nord
An arctic, north-bluish color palette. Clean and minimal with cool blue tones.
Dracula
A dark theme with vibrant colors. Popular across many editors and terminal emulators.
Catppuccin
A soothing pastel theme with four flavors. nmrs uses the Mocha (dark) and Latte (light) variants.
Tokyo Night
Inspired by the lights of Tokyo at night. A clean dark theme with vibrant accents.
Light/Dark Mode
Each theme has both light and dark variants. Toggle between them using the light/dark button in the header bar. The toggle saves your preference to ~/.config/nmrs/theme.
You can also use the system default by setting light or dark in the theme file, which uses the GTK4 default appearance.
Theme CSS Variables
All themes override the same set of CSS variables:
| Variable | Purpose |
|---|---|
--bg-primary | Main background color |
--bg-secondary | Secondary/card background |
--bg-tertiary | Tertiary/hover background |
--text-primary | Main text color |
--text-secondary | Secondary text color |
--text-tertiary | Muted text color |
--border-color | Default border color |
--border-color-hover | Hover state border color |
--accent-color | Primary accent (links, active items) |
--success-color | Success indicators (connected) |
--warning-color | Warning indicators (weak signal) |
--error-color | Error indicators (disconnected) |
Creating a Custom Theme
You can create your own theme by overriding CSS variables in ~/.config/nmrs/style.css:
:root {
--bg-primary: #0d1117;
--bg-secondary: #161b22;
--bg-tertiary: #21262d;
--text-primary: #e6edf3;
--text-secondary: #8b949e;
--text-tertiary: #484f58;
--border-color: #30363d;
--border-color-hover: #484f58;
--accent-color: #58a6ff;
--success-color: #3fb950;
--warning-color: #d29922;
--error-color: #f85149;
}
Since user CSS loads after the selected theme, your overrides will always take effect.
Theme Storage
- Theme selection:
~/.config/nmrs/theme(plain text, e.g.,nord) - User CSS:
~/.config/nmrs/style.css
Next Steps
- Configuration — full configuration reference
- Waybar Integration — launch from your status bar
Waybar Integration
Waybar is a popular status bar for Wayland compositors. You can configure it to launch nmrs-gui when clicking the network module.
Basic Configuration
Add to your Waybar config (~/.config/waybar/config):
"network": {
"on-click": "nmrs"
}
This launches nmrs-gui when you click the network module in Waybar.
Full Network Module Example
"network": {
"format-wifi": "{icon} {essid}",
"format-ethernet": " {ifname}",
"format-disconnected": " Disconnected",
"format-icons": ["", "", "", "", ""],
"tooltip-format-wifi": "{essid} ({signalStrength}%)\n{ipaddr}/{cidr}",
"tooltip-format-ethernet": "{ifname}\n{ipaddr}/{cidr}",
"on-click": "nmrs",
"interval": 5
}
Keybinding Integration
You can also bind nmrs-gui to a keyboard shortcut.
Hyprland
bind = $mainMod, N, exec, nmrs
windowrule = float, class:org.nmrs.ui
windowrule = size 400 600, class:org.nmrs.ui
windowrule = center, class:org.nmrs.ui
Sway
bindsym $mod+n exec nmrs
for_window [app_id="org.nmrs.ui"] floating enable
for_window [app_id="org.nmrs.ui"] resize set width 400 height 600
i3
bindsym $mod+n exec nmrs
for_window [class="org.nmrs.ui"] floating enable
Single Instance Behavior
nmrs-gui enforces single-instance mode. If you click the Waybar module while nmrs-gui is already open, the second instance will exit immediately and the existing window remains. This means you can safely bind the launch command to a click handler without worrying about duplicate windows.
Next Steps
- Configuration — customize the interface
- Themes — change the visual theme
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) |
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. |
VPN Types
| Type | Description |
|---|---|
VpnType | VPN protocol: WireGuard |
VpnCredentials | Full VPN configuration for connecting |
VpnCredentialsBuilder | Builder for VpnCredentials |
WireGuardPeer | WireGuard peer configuration |
VpnConnection | A saved/active VPN connection |
VpnConnectionInfo | Detailed VPN info (IP, DNS, gateway) |
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 |
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, WifiSecurity, EapOptions, EapMethod, Phase2, VpnCredentials, VpnType, WireGuardPeer, TimeoutConfig, ConnectionOptions, ConnectionError, DeviceType, DeviceState, }; }
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, 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() | Result<()> | Trigger active Wi-Fi scan |
list_networks() | Result<Vec<Network>> | List visible networks |
connect(ssid, security) | Result<()> | Connect to a Wi-Fi network |
disconnect() | Result<()> | Disconnect from current network |
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 |
Ethernet Methods
| Method | Returns | Description |
|---|---|---|
connect_wired() | Result<()> | Connect first available Ethernet device |
VPN Methods
| Method | Returns | Description |
|---|---|---|
connect_vpn(creds) | Result<()> | Connect to a VPN |
disconnect_vpn(name) | Result<()> | Disconnect a VPN by name |
list_vpn_connections() | Result<Vec<VpnConnection>> | List all saved VPNs |
forget_vpn(name) | Result<()> | Delete a saved VPN profile |
get_vpn_info(name) | Result<VpnConnectionInfo> | Get active VPN details |
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 |
Wi-Fi Control Methods
| Method | Returns | Description |
|---|---|---|
wifi_enabled() | Result<bool> | Check if Wi-Fi is enabled |
set_wifi_enabled(bool) | Result<()> | Enable/disable Wi-Fi |
wifi_hardware_enabled() | Result<bool> | Check hardware radio state (rfkill) |
wait_for_wifi_ready() | Result<()> | Wait for Wi-Fi device to become ready |
Connection Profile Methods
| Method | Returns | Description |
|---|---|---|
list_saved_connections() | Result<Vec<String>> | List all saved profiles |
has_saved_connection(ssid) | Result<bool> | Check if a 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 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>, } }
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 } }
VPN Models
VpnCredentials
#![allow(unused)] fn main() { pub struct VpnCredentials { pub vpn_type: VpnType, 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>, } }
Constructors: new(...), builder()
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>, } }
VpnConnection / VpnConnectionInfo
#![allow(unused)] fn main() { pub struct VpnConnection { pub name: String, pub vpn_type: VpnType, pub state: DeviceState, pub interface: Option<String>, } pub struct VpnConnectionInfo { pub name: String, pub vpn_type: VpnType, 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>, } }
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, build_wireguard_connection}; use nmrs::{WifiSecurity, ConnectionOptions, VpnCredentials}; // Wi-Fi let wifi = build_wifi_connection("MyNetwork", &WifiSecurity::Open, &ConnectionOptions::default()); // Ethernet let eth = build_ethernet_connection("eth0", &ConnectionOptions::default()); // WireGuard (returns Result) let wg = build_wireguard_connection(&creds, &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.
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), // VPN errors NoVpnConnection, VpnFailed(String), InvalidPrivateKey(String), InvalidPublicKey(String), InvalidAddress(String), InvalidGateway(String), InvalidPeers(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", WifiSecurity::Open).await?; Ok(()) } }
Specific Error Handling
#![allow(unused)] fn main() { match nm.connect("MyWiFi", 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", 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
It goes without saying that this image only works with nmrs. nmrs-gui requires GTK deps which in that case, you are better off just running a VM or learning how to use Linux on a machine instead.
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...
To develop nmrs-gui, you'll need:
- GTK4 and libadwaita development libraries
- A Wayland compositor
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 and nmrs-gui.
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
The library (nmrs) and GUI (nmrs-gui) are versioned independently.
Current Versions
| Crate | Version |
|---|---|
nmrs | 2.2.0 |
nmrs-gui | 1.1.0 |
Changelogs
Each crate maintains its own changelog:
nmrs/CHANGELOG.md— Core librarynmrs-gui/CHANGELOG.md— GUI application
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
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().await? - Check if Wi-Fi is enabled:
nm.wifi_enabled().await? - 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
GUI Issues
nmrs-gui won't start
- Check if another instance is running (single-instance lock)
- Verify GTK4 is installed:
pkg-config --modversion gtk4 - Check for missing libraries:
ldd $(which nmrs) | grep "not found"
Theme not loading
- Check
~/.config/nmrs/themecontains a valid theme name - Delete the file to reset to defaults
Custom CSS not working
- Verify the file exists:
~/.config/nmrs/style.css - User CSS loads last and should override themes
- Check CSS syntax in the file
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?
Currently only WireGuard. OpenVPN support is planned and actively being developed.
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 .conf WireGuard file?
Not directly. You need to extract the values from the config file and pass them to VpnCredentials. Direct .conf file import is not yet implemented.
GUI
Is nmrs-gui required to use nmrs?
No. nmrs is a library crate. nmrs-gui is a separate application built on top of it. You can use the library without the GUI.
Does nmrs-gui work on X11?
Yes. While it's designed for Wayland, it works on X11 through GTK4's X11 backend.
Can I use nmrs-gui with a tiling window manager?
Yes! nmrs-gui is designed to work well with tiling WMs like Hyprland, Sway, and i3. Add a floating window rule for org.nmrs.ui. See Configuration.
How do I change the theme?
Use the theme dropdown in the header bar, or edit ~/.config/nmrs/theme. See Themes.
Troubleshooting
Where can I get help?
- Discord: discord.gg/Sk3VfrHrN4
- GitHub Issues: github.com/cachebag/nmrs/issues
- Troubleshooting Guide: Troubleshooting
Changelog
Each crate maintains its own changelog. See the full changelogs on GitHub:
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
nmrs-gui (Application) Highlights
1.1.0
- Binary name fix for
.desktopfiles and Nix
0.5.0-beta
- Ethernet support
- UI freeze fixes
- WPA-EAP certificate path support
0.4.0-beta
- Five themes: Catppuccin, Dracula, Gruvbox, Nord, Tokyo Night
--versionflag- Crate rename to
nmrs-gui
0.3.0-beta
- System default light/dark toggle
0.2.0-beta
- Default CSS file creation
- Nix dependencies
- Connection success feedback
0.1.0-beta
- Initial GTK4 GUI
- Basic and advanced network detail pages
- Refresh functionality
- Desktop entry and AUR 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.