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

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

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:

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:

  • zbus 5.x - D-Bus communication
  • tokio or another async runtime
  • serde - Serialization
  • thiserror - Error handling
  • futures - 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

  • docker and docker-compose (for containerized testing)
  • WiFi hardware or mac80211_hwsim kernel module

Building Documentation

  • mdbook for this documentation
  • cargo-doc for 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:

  1. Check PolicyKit rules: /usr/share/polkit-1/actions/org.freedesktop.NetworkManager.policy
  2. Ensure D-Bus is accessible: ls -l /var/run/dbus/system_bus_socket
  3. 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 VersionMinimum RustNetworkManagerNotable Features
2.0.01.78.01.0+Full API rewrite
1.x1.70.01.0+Initial release

nmrs-gui

GUI VersionMinimum RustGTKNotable Features
1.1.01.85.14.0Themes support
1.0.01.82.04.0Initial release

Next Steps

Once you have all requirements met:

  1. Install nmrs
  2. Follow the Quick Start guide
  3. 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?;
}

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

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:

FieldTypeDescription
deviceStringInterface name (e.g., "wlan0")
ssidStringNetwork name
bssidOption<String>Access point MAC address
strengthOption<u8>Signal strength (0–100)
frequencyOption<u32>Frequency in MHz
securedboolWhether the network requires authentication
is_pskboolWPA-PSK (password) authentication
is_eapboolWPA-EAP (enterprise) authentication
ip4_addressOption<String>IPv4 address if connected
ip6_addressOption<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

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:

  1. Validates the SSID and credentials
  2. Searches for the network among visible access points
  3. Checks for a saved connection profile matching the SSID
  4. Creates a new connection profile if none exists, or reuses the saved one
  5. Activates the connection via NetworkManager
  6. Waits for the device to reach the Activated state
  7. 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

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::MissingPassword is 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:

ErrorMeaning
ConnectionError::AuthFailedWrong password
ConnectionError::MissingPasswordEmpty password string
ConnectionError::NotFoundNetwork not in range
ConnectionError::TimeoutConnection took too long
ConnectionError::DhcpFailedConnected 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)

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:

MethodDescriptionCommon Use
EapMethod::PeapProtected EAP — tunnels inner auth in TLSCorporate networks
EapMethod::TtlsTunneled TLS — flexible inner authUniversities, ISPs

Phase 2 (Inner Authentication)

The inner authentication runs inside the TLS tunnel established by the outer method:

MethodDescriptionTypical Pairing
Phase2::Mschapv2MS-CHAPv2 — challenge-responsePEAP
Phase2::PapPAP — 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

OptionRequiredDescription
identityYesUsername (usually email)
passwordYesUser password
methodYesOuter EAP method (PEAP or TTLS)
phase2YesInner authentication (MSCHAPv2 or PAP)
anonymous_identityNoOuter identity for privacy (sent in the clear)
domain_suffix_matchNoVerify server certificate domain
ca_cert_pathNoPath to CA certificate (file:// URL)
system_ca_certsNoUse 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_certs or ca_cert_path in 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

SymptomLikely Cause
AuthFailedWrong username/password, or server rejected credentials
SupplicantConfigFailedMisconfigured EAP method or phase2
SupplicantTimeoutServer not responding — check CA cert and domain
TimeoutAuthentication 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

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:

  1. nmrs first checks if there is a saved connection profile for that SSID — if so, it activates the saved profile directly
  2. If no saved profile exists, it searches the visible access point list
  3. 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

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:

Security Best Practices

  1. Never hardcode keys - Use environment variables or secure storage
  2. Rotate keys regularly - Update WireGuard keys periodically
  3. Use preshared keys - Add extra layer of security with PSK
  4. Verify endpoints - Ensure gateway addresses are correct
  5. 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 wireguard kernel 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

ConceptDescription
Private KeyYour client's secret key (base64, 44 chars). Never share this.
Public KeyThe server's public key (base64, 44 chars). Provided by server admin.
EndpointServer address in host:port format (e.g., vpn.example.com:51820)
AddressYour client's IP within the VPN tunnel (e.g., 10.0.0.2/24)
Allowed IPsIP ranges to route through the tunnel. 0.0.0.0/0 routes everything.
DNSDNS servers to use while the VPN is active
Persistent KeepaliveSeconds between keepalive packets (helps with NAT traversal)

VpnCredentials Fields

FieldRequiredDescription
vpn_typeYesMust be VpnType::WireGuard
nameYesConnection profile name
gatewayYesServer endpoint (host:port)
private_keyYesClient private key (base64)
addressYesClient IP with CIDR (10.0.0.2/24)
peersYesAt least one WireGuardPeer
dnsNoDNS servers for the VPN
mtuNoMTU size (typical: 1420)
uuidNoCustom 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

FieldRequiredDescription
public_keyYesPeer's WireGuard public key (base64)
gatewayYesPeer endpoint (host:port)
allowed_ipsYesIP ranges to route through this peer
preshared_keyNoAdditional shared secret for post-quantum security
persistent_keepaliveNoKeepalive 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

ConfigurationEffect
["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:port format 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:

ErrorCause
InvalidPrivateKeyKey missing, wrong length, or invalid base64
InvalidPublicKeyPeer key invalid
InvalidAddressMissing CIDR prefix or invalid IP
InvalidGatewayMissing port or invalid format
InvalidPeersNo 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

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:

FieldTypeDescription
nameStringConnection profile name
vpn_typeVpnTypeVPN protocol (currently WireGuard)
stateDeviceStateCurrent state (Activated, Disconnected, etc.)
interfaceOption<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:

FieldTypeDescription
nameStringConnection name
vpn_typeVpnTypeVPN protocol
stateDeviceStateCurrent state
interfaceOption<String>Interface name
gatewayOption<String>VPN gateway address
ip4_addressOption<String>Assigned IPv4 address
ip6_addressOption<String>Assigned IPv6 address
dns_serversVec<String>Active DNS servers

Note: get_vpn_info() returns ConnectionError::NoVpnConnection if 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

ErrorMethodMeaning
NoVpnConnectionget_vpn_infoVPN not active
VpnFailedconnect_vpnConnection activation failed
InvalidPrivateKeyconnect_vpnBad WireGuard key
InvalidAddressconnect_vpnBad IP/CIDR
InvalidGatewayconnect_vpnBad endpoint format

Next Steps

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

ErrorMeaning
ConnectionError::NoWiredDeviceNo Ethernet adapter found
ConnectionError::TimeoutDHCP or activation took too long
ConnectionError::DhcpFailedFailed 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():

  1. nmrs finds the first managed wired device
  2. Checks for an existing saved connection for that device
  3. If a saved connection exists, activates it
  4. If no saved connection exists, creates a new profile with DHCP and activates it
  5. Waits for the connection to reach Activated state

The connection profile is saved for future use, so the device will auto-connect when a cable is plugged in.

Next Steps

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 bluetoothctl or 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:

FieldTypeDescription
bdaddrStringBluetooth MAC address
nameOption<String>Device name from BlueZ
aliasOption<String>User-friendly alias
bt_capsu32Bluetooth capability flags
stateDeviceStateCurrent 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

RoleDescription
BluetoothNetworkRole::PanUPersonal Area Network User — most common for phone tethering
BluetoothNetworkRole::DunDial-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

ErrorMeaning
ConnectionError::NoBluetoothDeviceNo Bluetooth adapter found
ConnectionError::InvalidAddressInvalid Bluetooth MAC address format
ConnectionError::TimeoutConnection took too long
ConnectionError::NoSavedConnectionNo 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

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:

FieldTypeDescription
pathStringD-Bus object path
interfaceStringInterface name (e.g., wlan0, eth0)
identityDeviceIdentityMAC addresses (permanent and current)
device_typeDeviceTypeType of device
stateDeviceStateCurrent operational state
managedOption<bool>Whether NetworkManager manages this device
driverOption<String>Kernel driver name
ip4_addressOption<String>IPv4 address with CIDR (when connected)
ip6_addressOption<String>IPv6 address with CIDR (when connected)

Device Types

#![allow(unused)]
fn main() {
use nmrs::DeviceType;
}
VariantDescription
DeviceType::WifiWi-Fi (802.11) wireless adapter
DeviceType::EthernetWired Ethernet interface
DeviceType::BluetoothBluetooth network device
DeviceType::WifiP2PWi-Fi Direct (peer-to-peer)
DeviceType::LoopbackLoopback 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;
}
StateDescription
UnmanagedNot managed by NetworkManager
UnavailableManaged but not ready (e.g., Wi-Fi disabled)
DisconnectedAvailable but not connected
PreparePreparing to connect
ConfigBeing configured
NeedAuthWaiting for credentials
IpConfigRequesting IP configuration
IpCheckVerifying IP connectivity
SecondariesWaiting for secondary connections
ActivatedFully connected and operational
DeactivatingDisconnecting
FailedConnection 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

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 WifiSecurity value 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

  1. Created — when you first connect to a network, NetworkManager creates a profile
  2. Persisted — profiles are saved to /etc/NetworkManager/system-connections/
  3. Reused — subsequent connections to the same SSID use the saved profile
  4. Updated — if you connect with different credentials, the profile may be updated
  5. Deleted — calling forget(), forget_vpn(), or forget_bluetooth() removes it

Next Steps

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

MonitorTriggers
monitor_network_changesAccess point added, access point removed, signal strength change
monitor_device_changesDevice state change (connected, disconnected, etc.), cable plug/unplug

Next Steps

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

VariantDescription
NotFoundNetwork not visible during scan
AuthFailedWrong password or rejected credentials
MissingPasswordEmpty password provided
NoWifiDeviceNo Wi-Fi adapter found
WifiNotReadyWi-Fi device not ready in time
NoWiredDeviceNo Ethernet adapter found
DhcpFailedFailed to obtain an IP address via DHCP
TimeoutOperation timed out waiting for activation
Stuck(String)Connection stuck in an unexpected state

Authentication Errors

VariantDescription
SupplicantConfigFailedwpa_supplicant configuration error
SupplicantTimeoutwpa_supplicant timed out during auth

VPN Errors

VariantDescription
NoVpnConnectionVPN 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

VariantDescription
NoBluetoothDeviceNo Bluetooth adapter found

Profile Errors

VariantDescription
NoSavedConnectionNo saved profile for the requested network

Low-Level Errors

VariantDescription
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

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 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 threads
  • Sync — can be shared via Arc (though Clone is 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

nmrs uses timeouts to prevent operations from hanging indefinitely. You can customize these timeouts for different network environments.

Default Timeouts

TimeoutDefaultPurpose
connection_timeout30 secondsHow long to wait for a connection to activate
disconnect_timeout10 secondsHow 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 activation
  • connect_wired() — Ethernet connection activation
  • connect_bluetooth() — Bluetooth connection activation
  • connect_vpn() — VPN connection activation
  • disconnect() — Wi-Fi disconnection

The disconnect_timeout applies to the waiting period after requesting disconnection.

Next Steps

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

FieldTypeDefaultDescription
autoconnectbooltrueConnect automatically when available
autoconnect_priorityOption<i32>None (0)Higher values are preferred when multiple networks are available
autoconnect_retriesOption<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.).

PriorityUse Case
0 (default)Normal connections
1–10Preferred connections
-1 to -10Fallback connections

How Retries Work

autoconnect_retries limits how many times NetworkManager will try to auto-connect a failing connection:

  • None (default) — unlimited retries
  • Some(0) — never auto-retry
  • Some(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

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):

ProxyD-Bus InterfacePurpose
NMProxyorg.freedesktop.NetworkManagerMain NM interface
NMDeviceProxyorg.freedesktop.NetworkManager.DeviceDevice properties and control
NMWirelessProxyorg.freedesktop.NetworkManager.Device.WirelessWi-Fi scanning, AP list
NMAccessPointProxyorg.freedesktop.NetworkManager.AccessPointAP signal, SSID, security
NMActiveConnectionProxyorg.freedesktop.NetworkManager.Connection.ActiveActive connection state
NMWiredProxyorg.freedesktop.NetworkManager.Device.WiredWired device properties
NMBluetoothProxyorg.freedesktop.NetworkManager.Device.BluetoothBluetooth 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

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

LevelContent
errorConnection failures, D-Bus errors
warnUnexpected states, fallback behavior
infoConnection events, state transitions
debugD-Bus method calls, scan results, settings
traceRaw 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

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");
}

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

  1. Checks if already connected to a preferred network
  2. Scans for visible networks
  3. Iterates through the preferred list in order
  4. Attempts to connect to the first match
  5. On auth failure, tries the next preferred network
  6. 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

ProblemSolution
AuthFailedVerify username format (email vs plain username) and password
SupplicantConfigFailedCheck EAP method — ask IT which to use
SupplicantTimeoutVerify CA cert path and domain suffix match
Connection is slowIncrease 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 NetworkInfo for selected networks
  • Color output: Use a crate like colored for 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

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

GUI Configuration

nmrs-gui stores its configuration in ~/.config/nmrs/.

Configuration Files

FilePurpose
~/.config/nmrs/themeSelected theme name
~/.config/nmrs/style.cssCustom CSS overrides

Theme Selection

The active theme is stored in ~/.config/nmrs/theme as a plain text string:

  • gruvbox — Gruvbox theme
  • nord — Nord theme
  • dracula — Dracula theme
  • catppuccin — Catppuccin theme
  • tokyo — Tokyo Night theme
  • light — System default, light mode
  • dark — 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

  1. Bundled stylesheet — base styles at application priority
  2. Selected theme — theme overrides at user priority
  3. User stylesheet~/.config/nmrs/style.css at 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:

ClassSignal Range
network-goodStrong signal
network-okayMedium signal
network-poorWeak 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

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:

VariablePurpose
--bg-primaryMain background color
--bg-secondarySecondary/card background
--bg-tertiaryTertiary/hover background
--text-primaryMain text color
--text-secondarySecondary text color
--text-tertiaryMuted text color
--border-colorDefault border color
--border-color-hoverHover state border color
--accent-colorPrimary accent (links, active items)
--success-colorSuccess indicators (connected)
--warning-colorWarning indicators (weak signal)
--error-colorError 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

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

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 connection
  • Send + 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

TypeDescription
NetworkA discovered Wi-Fi network (SSID, signal, security flags)
NetworkInfoDetailed network information (channel, speed, bars)
WifiSecurityAuthentication type: Open, WpaPsk, WpaEap
EapOptionsEnterprise Wi-Fi (802.1X) configuration
EapOptionsBuilderBuilder for EapOptions
EapMethodOuter EAP method: Peap, Ttls
Phase2Inner auth method: Mschapv2, Pap

Device Types

TypeDescription
DeviceA network device (interface, type, state, MAC)
DeviceIdentityDevice MAC addresses (permanent and current)
DeviceTypeDevice kind: Wifi, Ethernet, Bluetooth, WifiP2P, Loopback, Other(u32)
DeviceStateOperational state: Disconnected, Activated, Failed, etc.

VPN Types

TypeDescription
VpnTypeVPN protocol: WireGuard
VpnCredentialsFull VPN configuration for connecting
VpnCredentialsBuilderBuilder for VpnCredentials
WireGuardPeerWireGuard peer configuration
VpnConnectionA saved/active VPN connection
VpnConnectionInfoDetailed VPN info (IP, DNS, gateway)

Bluetooth Types

TypeDescription
BluetoothDeviceA Bluetooth device with BlueZ info
BluetoothIdentityBluetooth MAC + network role for connecting
BluetoothNetworkRoleRole: PanU, Dun

Configuration Types

TypeDescription
TimeoutConfigConnection/disconnection timeouts
ConnectionOptionsAutoconnect, priority, retry settings

Error Types

TypeDescription
ConnectionErrorAll possible error variants
StateReasonDevice state reason codes
ConnectionStateReasonActivation/deactivation reason codes
ActiveConnectionStateConnection lifecycle states

Builder Types

TypeDescription
ConnectionBuilderBase connection settings builder
WifiConnectionBuilderWi-Fi connection builder
WireGuardBuilderWireGuard VPN builder
IpConfigIP address with CIDR prefix
RouteStatic route configuration
WifiBandWi-Fi band: Bg (2.4 GHz), A (5 GHz)
WifiModeWi-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

MethodReturnsDescription
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

MethodReturnsDescription
connect_wired()Result<()>Connect first available Ethernet device

VPN Methods

MethodReturnsDescription
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

MethodReturnsDescription
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

MethodReturnsDescription
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

MethodReturnsDescription
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

MethodReturnsDescription
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

MethodReturnsDescription
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

MethodDescription
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

CheckError
Private key formatInvalidPrivateKey
Address CIDR formatInvalidAddress
At least one peerInvalidPeers
Peer public key formatInvalidPublicKey
Gateway host:port formatInvalidGateway
Peer allowed IPs non-emptyInvalidPeers

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:

ErrorUser Action
NotFoundMove closer to the network or check SSID spelling
AuthFailedCheck password or credentials
MissingPasswordProvide a non-empty password
TimeoutRetry or increase timeout
DhcpFailedCheck network infrastructure
NoWifiDeviceCheck that a Wi-Fi adapter is installed
NoWiredDeviceCheck that an Ethernet adapter exists

Validation Errors

These indicate invalid input to nmrs:

ErrorFix
InvalidPrivateKeyCheck WireGuard key format (base64, ~44 chars)
InvalidPublicKeyCheck peer public key format
InvalidAddressUse CIDR notation (e.g., 10.0.0.2/24)
InvalidGatewayUse host:port format
InvalidPeersAdd at least one peer with allowed IPs

System Errors

These indicate infrastructure issues:

ErrorInvestigation
DbusIs NetworkManager running? Is D-Bus accessible?
DbusOperationCheck context for what operation failed
SupplicantConfigFailedCheck wpa_supplicant configuration
SupplicantTimeoutCheck RADIUS server connectivity
WifiNotReadyWi-Fi device still initializing
StuckNetworkManager in unexpected state
DeviceFailedCheck the StateReason for details
ActivationFailedCheck 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 NetworkManager instance

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:

  1. Update rustdoc comments in the source code
  2. Add or update examples in the examples/ directory
  3. Update this mdBook documentation if user-facing changes are made
  4. 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:

  • NetworkManager delegates to core functions
  • models define all public data types
  • builders construct NM settings dictionaries

Core Layer

The core module contains the actual business logic:

  • connection.rs handles Wi-Fi/Ethernet connect/disconnect
  • scan.rs handles network scanning and listing
  • vpn.rs handles WireGuard VPN operations
  • state_wait.rs uses 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:

  1. Sends the AddAndActivateConnection D-Bus call
  2. Subscribes to StateChanged signals on the device
  3. Awaits the signal with a timeout
  4. Returns success on Activated, or maps the failure reason to a ConnectionError

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

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 sections
  • wireguard_builder.rs — WireGuard settings, validation, multiple peers
  • wifi_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:

  1. Checks formatting (cargo fmt --check)
  2. Runs clippy (cargo clippy)
  3. Runs unit tests (cargo test)
  4. Builds documentation (mdbook build)

Next Steps

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

CrateVersion
nmrs2.2.0
nmrs-gui1.1.0

Changelogs

Each crate maintains its own changelog:

Release Checklist

  1. Update version in Cargo.toml
  2. Update the changelog
  3. Run cargo test
  4. Run cargo clippy
  5. Run cargo fmt --check
  6. Build documentation (mdbook build in docs/)
  7. Create a git tag: git tag v2.2.0
  8. Push the tag: git push origin v2.2.0
  9. Publish to crates.io: cargo publish -p nmrs

Distribution Channels

ChannelPackage
crates.ionmrs library
AURnmrs (GUI binary)
NixNix flake

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

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 just user)
  • 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 just 10.0.0.2)
  • Verify the IP is valid

"invalid VPN gateway"

Solutions:

  • Use host:port format: vpn.example.com:51820
  • Verify the port is a valid number (1–65535)

VPN connects but no traffic

Solutions:

  • Check allowed_ips — use 0.0.0.0/0 for 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/theme contains 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

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?

Changelog

Each crate maintains its own changelog. See the full changelogs on GitHub:

nmrs (Library) Highlights

2.2.0

  • Concurrency protection — is_connecting() API
  • WirelessHardwareEnabled property 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
  • WifiMode enum 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
  • VpnCredentials and EapOptions builder patterns
  • ConnectionOptions for autoconnect configuration
  • ConnectionBuilder for advanced connection settings
  • WireGuardBuilder with 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 .desktop files 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
  • --version flag
  • 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:

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:

CrateLicense
zbusMIT
zvariantMIT
serdeMIT OR Apache-2.0
thiserrorMIT OR Apache-2.0
uuidMIT OR Apache-2.0
logMIT OR Apache-2.0
futuresMIT OR Apache-2.0
tokioMIT
base64MIT OR Apache-2.0
async-traitMIT OR Apache-2.0

All dependencies use permissive licenses compatible with nmrs's dual license.