Introduction

Welcome to the nmrs documentation! This guide will help you understand and use nmrs, a powerful Rust library for managing network connections on Linux via NetworkManager.

What is nmrs?

nmrs is a high-level, async Rust API for NetworkManager over D-Bus. It provides:

  • Simple WiFi Management - Scan, connect, and manage wireless networks
  • VPN Support - WireGuard and OpenVPN VPN support
  • Ethernet Control - Manage wired network connections
  • Bluetooth - Connect to Bluetooth network devices
  • Real-Time Monitoring - Event-driven network state updates
  • Type Safety - Comprehensive error handling with specific failure reasons
  • Async/Await - Built on modern async Rust with runtime flexibility

Why nmrs?

  • Safe Abstractions - No unsafe code, leveraging Rust's type system
  • Async-First - Built for modern async Rust applications
  • Signal-Based - Efficient D-Bus signal monitoring instead of polling
  • Well-Documented - Comprehensive docs with examples for every feature
  • Runtime Agnostic - Works with Tokio, async-std, smol, and more

Quick Example

Here's a taste of what nmrs can do:

use nmrs::{NetworkManager, WifiSecurity};

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;
    
    // Scan for networks
    let networks = nm.list_networks(None).await?;
    for net in networks {
        println!("{} - {}%", net.ssid, net.strength.unwrap_or(0));
    }
    
    // Connect to a network
    nm.connect("MyWiFi", None, WifiSecurity::WpaPsk {
        psk: "password123".into()
    }).await?;
    
    Ok(())
}

Community

License

nmrs is dual-licensed under MIT and Apache 2.0, giving you flexibility in how you use it.


Ready to get started? Head to the Installation guide!

Installation

This guide covers installation for the nmrs library.

Using Cargo

The easiest way to add nmrs to your project:

cargo add nmrs

Or manually add to your Cargo.toml:

[dependencies]
nmrs = "2.0.0"

From Source

Clone and build from source:

git clone https://github.com/cachebag/nmrs.git
cd nmrs/nmrs
cargo build --release

Verify Installation

Create a simple test to verify nmrs is working:

use nmrs::NetworkManager;

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;
    println!("nmrs is working!");
    Ok(())
}

System Requirements

  • Operating System: Linux (any modern distribution)
  • Rust: 1.90.0 or later
  • NetworkManager: Version 1.0 or later, running and accessible via D-Bus
  • D-Bus: System bus must be available

Permissions

nmrs requires permission to manage network connections. On most systems, this is handled by PolicyKit. Ensure your user is in the appropriate groups:

# Check if you're in the network group
groups

# Add yourself to the network group if needed (requires logout/login)
sudo usermod -aG network $USER

Verify NetworkManager

Ensure NetworkManager is running:

systemctl status NetworkManager

If it's not running:

sudo systemctl start NetworkManager
sudo systemctl enable NetworkManager  # Start on boot

Next Steps

Quick Start

This guide will get you up and running with nmrs in minutes.

Prerequisites

Make sure you have:

  • Rust installed (1.78.0+)
  • NetworkManager running on your Linux system
  • Basic familiarity with async Rust

Create a New Project

cargo new nmrs-demo
cd nmrs-demo
cargo add nmrs tokio --features tokio/full

Your First nmrs Program

Let's create a simple program that lists available WiFi networks:

use nmrs::NetworkManager;

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    // Initialize NetworkManager connection
    let nm = NetworkManager::new().await?;
    
    // List all available networks
    let networks = nm.list_networks(None).await?;
    
    // Print network information
    for network in networks {
        println!(
            "SSID: {:<20} Signal: {:>3}% Security: {:?}",
            network.ssid,
            network.strength.unwrap_or(0),
            network.security
        );
    }
    
    Ok(())
}

Run it:

cargo run

You should see output like:

SSID: MyHomeNetwork       Signal:  85% Security: WpaPsk
SSID: CoffeeShopWiFi      Signal:  62% Security: Open
SSID: Neighbor5G          Signal:  45% Security: WpaEap

Connecting to a Network

Now let's connect to a WiFi network:

use nmrs::{NetworkManager, WifiSecurity};

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;
    
    // Connect to a WPA-PSK protected network
    nm.connect("MyHomeNetwork", None, WifiSecurity::WpaPsk {
        psk: "your_password_here".into()
    }).await?;
    
    println!("Connected successfully!");
    
    // Verify the connection
    if let Some(ssid) = nm.current_ssid().await {
        println!("Current network: {}", ssid);
    }
    
    Ok(())
}

Error Handling

nmrs provides detailed error types for better error handling:

use nmrs::{NetworkManager, WifiSecurity, ConnectionError};

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;
    
    match nm.connect("MyNetwork", None, WifiSecurity::WpaPsk {
        psk: "password123".into()
    }).await {
        Ok(_) => println!("✓ Connected successfully"),
        Err(ConnectionError::AuthFailed) => {
            eprintln!("✗ Authentication failed - wrong password?");
        }
        Err(ConnectionError::NotFound) => {
            eprintln!("✗ Network not found or out of range");
        }
        Err(ConnectionError::Timeout) => {
            eprintln!("✗ Connection timed out");
        }
        Err(ConnectionError::DhcpFailed) => {
            eprintln!("✗ Failed to obtain IP address");
        }
        Err(e) => eprintln!("✗ Error: {}", e),
    }
    
    Ok(())
}

Device Management

List all network devices:

use nmrs::NetworkManager;

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;
    
    let devices = nm.list_devices().await?;
    
    for device in devices {
        println!(
            "Interface: {:<10} Type: {:<10} State: {:?}",
            device.interface,
            device.device_type,
            device.state
        );
    }
    
    Ok(())
}

Working with Connection Profiles

List saved connection profiles:

use nmrs::NetworkManager;

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;
    
    let profiles = nm.list_saved_connection_ids().await?;
    
    println!("Saved connections:");
    for profile in profiles {
        println!("  - {}", profile);
    }
    
    Ok(())
}

Real-Time Monitoring

Monitor network changes:

use nmrs::NetworkManager;
use std::sync::Arc;

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = Arc::new(NetworkManager::new().await?);
    let nm_clone = nm.clone();
    
    // Monitor network changes
    nm.monitor_network_changes(move || {
        println!("Networks changed! Scanning...");
        // In a real app, you'd update your UI here
    }).await?;
    
    // Keep the program running
    tokio::signal::ctrl_c().await.ok();
    Ok(())
}

Complete Example: Network Scanner

Here's a complete example that puts it all together:

use nmrs::{NetworkManager, WifiSecurity};
use std::io::{self, Write};

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;
    
    println!("Scanning for networks...\n");
    let networks = nm.list_networks(None).await?;
    
    // Display networks with numbering
    for (i, net) in networks.iter().enumerate() {
        println!(
            "{:2}. {:<25} Signal: {:>3}% {:?}",
            i + 1,
            net.ssid,
            net.strength.unwrap_or(0),
            net.security
        );
    }
    
    // Get user input
    print!("\nEnter network number to connect (or 0 to exit): ");
    io::stdout().flush().unwrap();
    
    let mut input = String::new();
    io::stdin().read_line(&mut input).unwrap();
    
    let choice: usize = input.trim().parse().unwrap_or(0);
    
    if choice == 0 || choice > networks.len() {
        println!("Exiting...");
        return Ok(());
    }
    
    let selected = &networks[choice - 1];
    
    // Ask for password if needed
    let security = match selected.security {
        nmrs::models::WifiSecurity::Open => WifiSecurity::Open,
        _ => {
            print!("Enter password: ");
            io::stdout().flush().unwrap();
            let mut password = String::new();
            io::stdin().read_line(&mut password).unwrap();
            WifiSecurity::WpaPsk {
                psk: password.trim().to_string()
            }
        }
    };
    
    // Connect
    println!("Connecting to {}...", selected.ssid);
    nm.connect(&selected.ssid, None, security).await?;
    
    println!("✓ Connected successfully!");
    
    Ok(())
}

Next Steps

Now that you've got the basics, explore more features:

Using Different Async Runtimes

nmrs works with any async runtime. Here are examples with popular runtimes:

async-std

[dependencies]
nmrs = "2.0.0"
async-std = { version = "1.12", features = ["attributes"] }
#[async_std::main]
async fn main() -> nmrs::Result<()> {
    let nm = nmrs::NetworkManager::new().await?;
    // ... your code
    Ok(())
}

smol

[dependencies]
nmrs = "2.0.0"
smol = "2.0"
fn main() -> nmrs::Result<()> {
    smol::block_on(async {
        let nm = nmrs::NetworkManager::new().await?;
        // ... your code
        Ok(())
    })
}

Requirements

This page details all the requirements needed to use nmrs effectively.

System Requirements

Operating System

nmrs is Linux-only and requires:

  • Any modern Linux distribution (kernel 3.10+)
  • NetworkManager 1.0 or later
  • D-Bus system bus

Tested on:

  • Arch Linux
  • Ubuntu 20.04+
  • Fedora 35+
  • Debian 11+
  • NixOS

NetworkManager

NetworkManager must be:

  • Installed on your system
  • Running and accessible via D-Bus
  • Version 1.0 or later (1.46+ recommended for latest features)

Check your NetworkManager version:

NetworkManager --version

Ensure it's running:

systemctl status NetworkManager

D-Bus

The D-Bus system bus must be available and running. This is standard on all modern Linux distributions.

Verify D-Bus is working:

dbus-send --system --print-reply \
  --dest=org.freedesktop.NetworkManager \
  /org/freedesktop/NetworkManager \
  org.freedesktop.DBus.Properties.Get \
  string:'org.freedesktop.NetworkManager' \
  string:'Version'

Rust Requirements

  • Rust: 1.90.0 or later
  • Edition: 2024

The library uses stable Rust features only.

Dependencies

nmrs Library Dependencies

The library depends on:

  • 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.

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 VersionMinimum RustNetworkManagerNotable Features
3.0.01.90.01.0+Edition 2024
2.0.01.78.01.0+Full API rewrite
1.x1.70.01.0+Initial 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(None).await?;
    
    // Connect to WPA-PSK network
    nm.connect("MyWiFi", None, WifiSecurity::WpaPsk {
        psk: "password".into()
    }).await?;
    
    // Get current connection
    if let Some(ssid) = nm.current_ssid().await {
        println!("Connected to: {}", ssid);
    }
    
    // Disconnect
    nm.disconnect(None).await?;
    
    Ok(())
}

Security Types

nmrs supports all major WiFi security protocols:

Open Networks

No authentication required:

#![allow(unused)]
fn main() {
nm.connect("FreeWiFi", None, WifiSecurity::Open).await?;
}

WPA-PSK (Personal)

Password-based authentication:

#![allow(unused)]
fn main() {
nm.connect("HomeWiFi", None, WifiSecurity::WpaPsk {
    psk: "your_password".into()
}).await?;
}

WPA-EAP (Enterprise)

802.1X authentication with various methods:

#![allow(unused)]
fn main() {
use nmrs::{WifiSecurity, EapOptions, EapMethod, Phase2};

let eap_opts = EapOptions::new("user@company.com", "password")
    .with_method(EapMethod::Peap)
    .with_phase2(Phase2::Mschapv2)
    .with_domain_suffix_match("company.com");

nm.connect("CorpWiFi", None, WifiSecurity::WpaEap {
    opts: eap_opts
}).await?;
}

Network Information

The Network struct contains detailed information about discovered networks:

#![allow(unused)]
fn main() {
pub struct Network {
    pub ssid: String,              // Network name
    pub strength: Option<u8>,      // Signal strength (0-100)
    pub security: WifiSecurity,    // Security type
    pub frequency: Option<u32>,    // Frequency in MHz
    pub hwaddress: Option<String>, // BSSID/MAC address
}
}

Example usage:

#![allow(unused)]
fn main() {
let networks = nm.list_networks(None).await?;

for net in networks {
    println!("SSID: {}", net.ssid);
    
    if let Some(strength) = net.strength {
        println!("  Signal: {}%", strength);
        
        if strength > 70 {
            println!("  Quality: Excellent");
        } else if strength > 50 {
            println!("  Quality: Good");
        } else {
            println!("  Quality: Weak");
        }
    }
    
    if let Some(freq) = net.frequency {
        let band = if freq > 5000 { "5GHz" } else { "2.4GHz" };
        println!("  Band: {}", band);
    }
}
}

Connection Options

Customize connection behavior with ConnectionOptions:

#![allow(unused)]
fn main() {
use nmrs::{NetworkManager, WifiSecurity, ConnectionOptions};

let opts = ConnectionOptions::new(true)  // autoconnect
    .with_priority(10)                   // higher = preferred
    .with_ipv4_method("auto")            // DHCP
    .with_dns(vec!["1.1.1.1".into(), "8.8.8.8".into()]);

// Note: Advanced connection options require using builders directly
// See the Advanced Topics section for details
}

WiFi Radio Control

Enable or disable WiFi hardware:

#![allow(unused)]
fn main() {
// Disable WiFi (airplane mode)
nm.set_wireless_enabled(false).await?;

// Enable WiFi
nm.set_wireless_enabled(true).await?;

// Check WiFi status
let state = nm.wifi_state().await?;
println!("WiFi is {}", if state.enabled { "enabled" } else { "disabled" });
println!("Hardware switch is {}", if state.hardware_enabled { "on" } else { "off" });
}

Network Scanning

Trigger a fresh scan:

#![allow(unused)]
fn main() {
// Request a scan (may take a few seconds)
nm.scan_networks(None).await?;

// Wait a moment for scan to complete
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;

// Get updated results
let networks = nm.list_networks(None).await?;
}

Detecting Connection State

Check your current WiFi status:

#![allow(unused)]
fn main() {
// Get current SSID
if let Some(ssid) = nm.current_ssid().await {
    println!("Connected to: {}", ssid);
} else {
    println!("Not connected");
}

// Get detailed network info
if let Some(network) = nm.current_network().await? {
    println!("SSID: {}", network.ssid);
    println!("Signal: {}%", network.strength.unwrap_or(0));
}
}

Error Handling

WiFi operations can fail for various reasons. Handle them gracefully:

#![allow(unused)]
fn main() {
use nmrs::ConnectionError;

match nm.connect("Network", None, WifiSecurity::WpaPsk {
    psk: "pass".into()
}).await {
    Ok(_) => println!("Connected!"),
    
    Err(ConnectionError::AuthFailed) => {
        eprintln!("Wrong password");
    }
    
    Err(ConnectionError::NotFound) => {
        eprintln!("Network not found - out of range?");
    }
    
    Err(ConnectionError::Timeout) => {
        eprintln!("Connection timed out");
    }
    
    Err(ConnectionError::DhcpFailed) => {
        eprintln!("Failed to get IP address");
    }
    
    Err(ConnectionError::NoSecrets) => {
        eprintln!("Missing password or credentials");
    }
    
    Err(e) => eprintln!("Error: {}", e),
}
}

Real-Time Updates

Monitor WiFi networks in real-time:

#![allow(unused)]
fn main() {
use std::sync::Arc;

let nm = Arc::new(NetworkManager::new().await?);
let nm_clone = nm.clone();

nm.monitor_network_changes(move || {
    println!("Network list changed!");
    // In a GUI app, you'd trigger a UI refresh here
}).await?;

// Monitor device state (connection/disconnection)
nm.monitor_device_changes(|| {
    println!("Device state changed!");
}).await?;
}

Best Practices

1. Cache the NetworkManager Instance

#![allow(unused)]
fn main() {
// Good - reuse the same instance
let nm = NetworkManager::new().await?;
nm.list_networks(None).await?;
nm.connect("WiFi", None, WifiSecurity::Open).await?;

// Avoid - creating multiple instances
let nm1 = NetworkManager::new().await?;
nm1.list_networks(None).await?;
let nm2 = NetworkManager::new().await?; // Unnecessary
nm2.connect("WiFi", None, WifiSecurity::Open).await?;
}

2. Handle Signal Strength

#![allow(unused)]
fn main() {
// Always check for None
if let Some(strength) = network.strength {
    println!("Signal: {}%", strength);
} else {
    println!("Signal: Unknown");
}
}

3. Use Timeouts

#![allow(unused)]
fn main() {
use tokio::time::{timeout, Duration};

// Wrap operations in timeouts
match timeout(Duration::from_secs(30), nm.connect("WiFi", None, security)).await {
    Ok(Ok(_)) => println!("Connected"),
    Ok(Err(e)) => eprintln!("Connection failed: {}", e),
    Err(_) => eprintln!("Operation timed out"),
}
}

4. Monitor for Disconnections

#![allow(unused)]
fn main() {
// Keep monitoring in the background
tokio::spawn(async move {
    loop {
        if nm.current_ssid().await.is_none() {
            eprintln!("Disconnected!");
            // Attempt reconnection logic
        }
        tokio::time::sleep(Duration::from_secs(5)).await;
    }
});
}

Next Steps

Scanning Networks

nmrs provides two approaches to discovering Wi-Fi networks: triggering an active scan and listing cached results.

Triggering a Scan

scan_networks() instructs all wireless devices to perform an active 802.11 probe scan. This sends probe requests on each channel and waits for responses from nearby access points.

use nmrs::NetworkManager;

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;

    // Trigger an active scan on all wireless devices
    nm.scan_networks(None).await?;

    Ok(())
}

Note: Scanning is asynchronous at the hardware level. After scan_networks() returns, NetworkManager continues to receive beacon frames and probe responses for a short period. You may want to add a brief delay before listing networks if you need the freshest results.

Listing Networks

list_networks() returns all Wi-Fi networks currently known to NetworkManager. This includes results from the most recent scan as well as networks that NetworkManager has cached from prior scans.

use nmrs::NetworkManager;

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;

    let networks = nm.list_networks(None).await?;
    for net in &networks {
        println!("{:30} {}%", net.ssid, net.strength.unwrap_or(0));
    }

    Ok(())
}

The Network Struct

Each discovered network is represented by the Network struct:

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(None).await?;
    if let Some(network) = networks.first() {
        let info = nm.show_details(network).await?;

        println!("SSID:      {}", info.ssid);
        println!("BSSID:     {}", info.bssid);
        println!("Signal:    {} {}", info.strength, info.bars);
        println!("Frequency: {:?} MHz", info.freq);
        println!("Channel:   {:?}", info.channel);
        println!("Mode:      {}", info.mode);
        println!("Speed:     {:?} Mbps", info.rate_mbps);
        println!("Security:  {}", info.security);
        println!("Status:    {}", info.status);
    }

    Ok(())
}

The NetworkInfo struct returned by show_details() includes:

  • bars – a visual signal-strength indicator (e.g., "▂▄▆█")
  • channel – the Wi-Fi channel number derived from frequency
  • rate_mbps – link speed when connected
  • security – human-readable security description
  • status – connection status string

Scan + List Pattern

The most common pattern is to trigger a scan, then list the results:

use nmrs::NetworkManager;

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;

    nm.scan_networks(None).await?;
    let networks = nm.list_networks(None).await?;

    for net in &networks {
        let security = if net.is_eap {
            "EAP"
        } else if net.is_psk {
            "PSK"
        } else {
            "Open"
        };

        let band = match net.frequency {
            Some(f) if f > 5900 => "6 GHz",
            Some(f) if f > 5000 => "5 GHz",
            Some(_) => "2.4 GHz",
            None => "?",
        };

        println!(
            "{:30} {:>3}%  {:>7}  {}",
            net.ssid,
            net.strength.unwrap_or(0),
            band,
            security,
        );
    }

    Ok(())
}

Network Deduplication

When multiple access points broadcast the same SSID (common in mesh or enterprise deployments), nmrs merges them into a single Network entry. The entry retains the strongest signal, while security flags are combined with a logical OR. This means a single SSID entry might show both is_psk and is_eap as true if different APs advertise different capabilities.

Next Steps

Connecting to Networks

This page covers the general flow for connecting to Wi-Fi networks with nmrs. For security-specific details, see the dedicated pages on WPA-PSK, WPA-EAP, and Hidden Networks.

Basic Connection Flow

Connecting to a Wi-Fi network requires two things: the SSID and the security credentials.

use nmrs::{NetworkManager, WifiSecurity};

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;

    // Open network (no password)
    nm.connect("CafeWiFi", None, WifiSecurity::Open).await?;

    // WPA-PSK network (password)
    nm.connect("HomeWiFi", None, WifiSecurity::WpaPsk {
        psk: "my_password".into(),
    }).await?;

    Ok(())
}

What Happens During Connect

When you call connect(), nmrs performs the following steps:

  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", None, WifiSecurity::WpaPsk {
    psk: "password".into(),
}).await?;
}

Disconnecting

#![allow(unused)]
fn main() {
let nm = NetworkManager::new().await?;

// Disconnect from the current Wi-Fi network
nm.disconnect(None).await?;
}

disconnect() deactivates the current wireless connection and waits for the device to reach the Disconnected state. If no connection is active, it returns Ok(()).

Saved Connections

When nmrs connects to a network, NetworkManager saves a connection profile. On subsequent connections to the same SSID, the saved profile is reused automatically.

#![allow(unused)]
fn main() {
let nm = NetworkManager::new().await?;

// Check if a profile exists
if nm.has_saved_connection("HomeWiFi").await? {
    println!("Profile exists — will reconnect without needing credentials");
}

// Connect using saved profile (WifiSecurity value is ignored if profile exists)
nm.connect("HomeWiFi", None, WifiSecurity::Open).await?;
}

See Connection Profiles for more on managing saved connections.

Error Handling

connect() returns specific error variants for different failure modes:

#![allow(unused)]
fn main() {
use nmrs::{NetworkManager, WifiSecurity, ConnectionError};

let nm = NetworkManager::new().await?;

match nm.connect("MyNetwork", None, WifiSecurity::WpaPsk {
    psk: "password".into(),
}).await {
    Ok(_) => println!("Connected!"),
    Err(ConnectionError::NotFound) => {
        eprintln!("Network not visible — is it in range?");
    }
    Err(ConnectionError::AuthFailed) => {
        eprintln!("Wrong password");
    }
    Err(ConnectionError::Timeout) => {
        eprintln!("Connection timed out — try increasing the timeout");
    }
    Err(ConnectionError::DhcpFailed) => {
        eprintln!("Failed to get an IP address");
    }
    Err(e) => eprintln!("Connection failed: {}", e),
}
}

See Error Handling for a full reference of error types.

Next Steps

WPA-PSK Networks

WPA-PSK (Wi-Fi Protected Access with Pre-Shared Key) is the most common security type for home and small-office Wi-Fi networks. You provide a password, and nmrs handles the WPA handshake.

Connecting with a Password

use nmrs::{NetworkManager, WifiSecurity};

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;

    nm.connect("HomeWiFi", None, WifiSecurity::WpaPsk {
        psk: "my_secure_password".into(),
    }).await?;

    println!("Connected!");
    Ok(())
}

The WifiSecurity::WpaPsk variant works with WPA, WPA2, and WPA3 Personal networks. NetworkManager negotiates the strongest supported protocol automatically.

Password Requirements

  • Must not be empty — ConnectionError::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", None, WifiSecurity::WpaPsk {
        psk: password,
    }).await?;

    Ok(())
}

Reconnecting to Saved Networks

After the first successful connection, NetworkManager saves the credentials in a connection profile. Subsequent connections to the same SSID will reuse the saved profile automatically — you don't need to provide the password again:

#![allow(unused)]
fn main() {
let nm = NetworkManager::new().await?;

if nm.has_saved_connection("HomeWiFi").await? {
    // Saved profile exists; password is stored in it.
    // The WifiSecurity value is ignored when a saved profile exists.
    nm.connect("HomeWiFi", None, WifiSecurity::Open).await?;
}
}

Error Handling

The most common errors for WPA-PSK connections:

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", None, WifiSecurity::WpaPsk {
    psk: "password".into(),
}).await {
    Ok(_) => println!("Connected!"),
    Err(ConnectionError::AuthFailed) => {
        eprintln!("Wrong password — check and try again");
    }
    Err(ConnectionError::MissingPassword) => {
        eprintln!("Password cannot be empty");
    }
    Err(e) => eprintln!("Error: {}", e),
}
}

Next Steps

WPA-EAP (Enterprise)

WPA-EAP (802.1X) is used by corporate and university networks that require individual user credentials rather than a shared password. nmrs supports PEAP and EAP-TTLS with configurable inner authentication methods.

Quick Start

use nmrs::{NetworkManager, WifiSecurity, EapOptions, EapMethod, Phase2};

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;

    let eap = EapOptions::new("user@company.com", "my_password")
        .with_method(EapMethod::Peap)
        .with_phase2(Phase2::Mschapv2);

    nm.connect("CorpWiFi", None, WifiSecurity::WpaEap { opts: eap }).await?;

    println!("Connected to enterprise WiFi!");
    Ok(())
}

EAP Methods

nmrs supports two outer EAP methods:

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", None, 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", None, WifiSecurity::Open).await?;

    // Hidden WPA-PSK network
    nm.connect("SecretLab", None, WifiSecurity::WpaPsk {
        psk: "lab_password".into(),
    }).await?;

    Ok(())
}

How It Works

When you call connect() with an SSID:

  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", None, WifiSecurity::WpaEap {
    opts: eap,
}).await?;
}

Reconnecting

After the first successful connection, NetworkManager saves the profile with the hidden flag. Subsequent connections to the same SSID will reconnect automatically using the saved profile, even though the network doesn't appear in scan results.

Considerations

  • Privacy: Hidden networks are not truly hidden — the SSID is transmitted during the association process. They provide obscurity, not security.
  • Battery impact: Devices probing for hidden networks transmit more frequently, which can reduce battery life on mobile devices.
  • First connection: The initial connection may be slower than visible networks because NetworkManager must perform a directed probe.

Next Steps

Per-Device Wi-Fi Scoping

Many machines have more than one Wi-Fi radio — a built-in card plus a USB dongle, a laptop in a dock with a secondary adapter, or an IoT gateway with dual radios on different bands. By default, nmrs routes every Wi-Fi operation through whichever device NetworkManager returns first. That works on single-radio systems, but on multi-radio setups you need to control which adapter scans, connects, or gets disabled.

nmrs 3.0 introduces per-device scoping so you can target a specific interface by name.

Listing Wi-Fi Devices

Start by discovering the available radios:

use nmrs::NetworkManager;

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;

    let devices = nm.list_wifi_devices().await?;
    for dev in &devices {
        println!("{} ({})", dev.interface, dev.mac);
        println!("  State: {:?}", dev.state);
        if let Some(ssid) = &dev.active_ssid {
            println!("  Connected to: {}", ssid);
        }
    }

    Ok(())
}

Each WifiDevice contains:

FieldTypeDescription
interfaceStringInterface name (wlan0, wlp2s0, …)
macStringHardware MAC address
stateDeviceStateCurrent operational state
active_ssidOption<String>SSID of the active connection, if any

You can also look up a single device directly:

#![allow(unused)]
fn main() {
let dev = nm.wifi_device_by_interface("wlan1").await?;
println!("{} is {:?}", dev.interface, dev.state);
}

The WifiScope Pattern

The most ergonomic way to work with a specific radio is WifiScope. Call nm.wifi("wlan1") to get a scope pinned to that interface, then chain operations without repeating the interface name:

use nmrs::{NetworkManager, WifiSecurity};

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;

    let scope = nm.wifi("wlan1");

    scope.scan().await?;
    let networks = scope.list_networks().await?;
    for net in &networks {
        println!("{} ({}%)", net.ssid, net.strength.unwrap_or(0));
    }

    scope.connect("HomeWiFi", WifiSecurity::WpaPsk {
        psk: "hunter2".into(),
    }).await?;

    Ok(())
}

WifiScope delegates to NetworkManager under the hood but locks every call to a single interface. The available methods are:

MethodDescription
scope.interface()Returns the interface name this scope is pinned to
scope.scan()Trigger a scan on this device
scope.list_networks()List networks visible to this device
scope.list_access_points()List raw access points (including duplicates per BSSID)
scope.connect(ssid, creds)Connect through this device
scope.connect_to_bssid(ssid, bssid, creds)Connect to a specific BSSID through this device
scope.disconnect()Disconnect this device
scope.set_enabled(bool)Enable or disable this device
scope.forget(ssid)Remove a saved connection from this device

Because the interface is already captured, none of these methods take an interface parameter.

BSSID targeting

When the same SSID is broadcast by multiple access points, use connect_to_bssid to force a specific one:

#![allow(unused)]
fn main() {
let scope = nm.wifi("wlan0");

let aps = scope.list_access_points().await?;
if let Some(best) = aps.iter().max_by_key(|ap| ap.strength.unwrap_or(0)) {
    scope.connect_to_bssid(
        &best.ssid,
        &best.hwaddress.as_deref().unwrap_or_default(),
        WifiSecurity::WpaPsk { psk: "password".into() },
    ).await?;
}
}

Per-Interface vs Global Operations

nmrs distinguishes between operations that target one device and operations that affect the entire Wi-Fi subsystem.

OperationPer-deviceGlobal
Enable/disable radionm.set_wifi_enabled("wlan1", true)nm.set_wireless_enabled(false)
Scannm.scan_networks(Some("wlan1"))nm.scan_networks(None) (scans all)
List networksnm.list_networks(Some("wlan1"))nm.list_networks(None) (merges all)
Connectnm.connect("ssid", Some("wlan1"), creds)nm.connect("ssid", None, creds)
Disconnectnm.disconnect(Some("wlan1"))nm.disconnect(None) (all devices)

When you pass None, nmrs falls back to the original behavior: pick the first Wi-Fi device for single-device operations, or aggregate across all devices for scans and listings.

Per-Device Enable/Disable

There are two distinct toggles:

  • set_wireless_enabled(bool) flips NetworkManager's global WirelessEnabled property. This affects every Wi-Fi radio on the system — equivalent to airplane-mode for Wi-Fi.
  • set_wifi_enabled(interface, bool) targets a single radio. It sets Autoconnect = false and disconnects the device (to disable) or re-enables autoconnect (to enable). The rest of the system's Wi-Fi radios are unaffected.
#![allow(unused)]
fn main() {
let nm = NetworkManager::new().await?;

// Disable only the USB dongle
nm.set_wifi_enabled("wlan1", false).await?;

// The built-in radio stays online
let dev = nm.wifi_device_by_interface("wlan0").await?;
assert_ne!(dev.state, nmrs::DeviceState::Unavailable);
}

Using WifiScope:

#![allow(unused)]
fn main() {
let scope = nm.wifi("wlan1");
scope.set_enabled(false).await?;
}

Note: set_wifi_enabled is not the same as the global set_wireless_enabled. The global toggle controls the NM-level WirelessEnabled property (equivalent to nmcli radio wifi off), while per-device disable works through the device's autoconnect and disconnect mechanism.

Direct Method Approach

If you don't want a WifiScope, every Wi-Fi method on NetworkManager accepts an optional interface name. Pass None for single-radio behavior or Some("wlan1") to target a device:

use nmrs::{NetworkManager, WifiSecurity};

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;

    // Scan on a specific interface
    nm.scan_networks(Some("wlan1")).await?;

    // List networks from a specific interface
    let networks = nm.list_networks(Some("wlan1")).await?;

    // Connect through a specific interface
    nm.connect("OfficeWiFi", Some("wlan1"), WifiSecurity::WpaPsk {
        psk: "secret".into(),
    }).await?;

    // Disconnect a specific interface
    nm.disconnect(Some("wlan1")).await?;

    // Or use None to get the default (first device) behavior
    nm.scan_networks(None).await?;
    nm.connect("HomeWiFi", None, WifiSecurity::Open).await?;

    Ok(())
}

Error Handling

Two error variants are specific to per-device scoping:

#![allow(unused)]
fn main() {
use nmrs::ConnectionError;

let nm = NetworkManager::new().await?;

match nm.wifi_device_by_interface("wlan99").await {
    Ok(dev) => println!("Found: {}", dev.interface),
    Err(ConnectionError::WifiInterfaceNotFound { interface }) => {
        eprintln!("No Wi-Fi device named '{}'", interface);
    }
    Err(e) => eprintln!("Unexpected error: {}", e),
}
}
VariantMeaning
WifiInterfaceNotFound { interface }No network device with that name exists
NotAWifiDevice { interface }The interface exists but is not a Wi-Fi device (e.g., eth0)

NotAWifiDevice fires when you pass a valid interface name that belongs to an Ethernet or Bluetooth adapter:

#![allow(unused)]
fn main() {
match nm.wifi_device_by_interface("eth0").await {
    Err(ConnectionError::NotAWifiDevice { interface }) => {
        eprintln!("'{}' is not a Wi-Fi interface", interface);
    }
    _ => {}
}
}

Next Steps

VPN Connections

nmrs provides full support for WireGuard and OpenVPN connections through NetworkManager. This guide covers everything you need to know about managing VPNs with nmrs.

Overview

VPN support includes:

  • WireGuard — Modern, fast, secure VPN protocol (native NetworkManager integration)
  • OpenVPN — Widely deployed VPN protocol (via NetworkManager OpenVPN plugin)
  • .ovpn Import — Import existing OpenVPN configuration files
  • Profile Management — Save and reuse VPN configurations
  • Connection Control — Connect, disconnect, monitor VPN status
  • Multiple Peers — Support for multiple WireGuard peers
  • Custom DNS — Override DNS servers for VPN connections
  • MTU Configuration — Optimize packet sizes

WireGuard Quick Start

use nmrs::{NetworkManager, WireGuardConfig, WireGuardPeer};

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;

    let peer = WireGuardPeer::new(
        "server_public_key",
        "vpn.example.com:51820",
        vec!["0.0.0.0/0".into()],
    ).with_persistent_keepalive(25);

    let config = WireGuardConfig::new(
        "MyVPN",
        "vpn.example.com:51820",
        "your_private_key",
        "10.0.0.2/24",
        vec![peer],
    ).with_dns(vec!["1.1.1.1".into()]);

    nm.connect_vpn(config).await?;
    println!("Connected to WireGuard VPN!");

    Ok(())
}

OpenVPN Quick Start

use nmrs::{NetworkManager, OpenVpnConfig, OpenVpnAuthType};

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;

    let config = OpenVpnConfig::new("CorpVPN", "vpn.example.com", 1194, false)
        .with_auth_type(OpenVpnAuthType::PasswordTls)
        .with_username("user")
        .with_password("secret")
        .with_ca_cert("/etc/openvpn/ca.crt")
        .with_client_cert("/etc/openvpn/client.crt")
        .with_client_key("/etc/openvpn/client.key");

    nm.connect_vpn(config).await?;
    println!("Connected to OpenVPN!");

    Ok(())
}

.ovpn File Import

Import an existing OpenVPN configuration file directly:

#![allow(unused)]
fn main() {
nm.import_ovpn("client.ovpn", Some("user"), Some("secret")).await?;
}

For certificate-only configs that don't require credentials:

#![allow(unused)]
fn main() {
nm.import_ovpn("client.ovpn", None, None).await?;
}

See the .ovpn Import Example for builder-based import and inline certificate handling.

VPN Operations

Connect

#![allow(unused)]
fn main() {
// WireGuard
nm.connect_vpn(wireguard_config).await?;

// OpenVPN
nm.connect_vpn(openvpn_config).await?;
}

Connect by Name or UUID

Reconnect to a saved VPN profile without rebuilding the config:

#![allow(unused)]
fn main() {
nm.connect_vpn_by_id("MyVPN").await?;
nm.connect_vpn_by_uuid("a1b2c3d4-e5f6-...").await?;
}

Disconnect

#![allow(unused)]
fn main() {
nm.disconnect_vpn("MyVPN").await?;
nm.disconnect_vpn_by_uuid("a1b2c3d4-e5f6-...").await?;
}

List VPN Connections

#![allow(unused)]
fn main() {
let vpns = nm.list_vpn_connections().await?;

for vpn in &vpns {
    println!("{} ({:?}) — active: {}", vpn.name, vpn.vpn_type, vpn.active);
    if let Some(iface) = &vpn.interface {
        println!("  Interface: {iface}");
    }
}
}

list_vpn_connections() returns Vec<VpnConnection> with fields:

FieldTypeDescription
uuidStringConnection UUID
idStringConnection name (alias for name)
nameStringConnection profile name
vpn_typeVpnTypeVPN protocol (WireGuard, OpenVpn, etc.)
stateDeviceStateCurrent state (Activated, Disconnected, etc.)
interfaceOption<String>Network interface when active
activeboolWhether the connection is currently active
kindVpnKindVpnKind::Plugin (OpenVPN) or VpnKind::WireGuard

Active VPN Connections

Get only currently active VPN connections:

#![allow(unused)]
fn main() {
let active = nm.active_vpn_connections().await?;

for vpn in &active {
    println!("Active: {} ({:?})", vpn.name, vpn.vpn_type);
}
}

Get VPN Information

#![allow(unused)]
fn main() {
let info = nm.get_vpn_info("MyVPN").await?;

println!("Name:      {}", info.name);
println!("Kind:      {:?}", info.vpn_kind);
println!("State:     {:?}", info.state);
println!("Interface: {:?}", info.interface);
println!("Gateway:   {:?}", info.gateway);
println!("IPv4:      {:?}", info.ip4_address);
println!("IPv6:      {:?}", info.ip6_address);
println!("DNS:       {:?}", info.dns_servers);

if let Some(details) = &info.details {
    println!("Details:   {:?}", details);
}
}

Remove a VPN Profile

#![allow(unused)]
fn main() {
nm.forget_vpn("MyVPN").await?;
}

Routing Configuration

Route All Traffic (WireGuard)

#![allow(unused)]
fn main() {
let peer = WireGuardPeer::new(
    "public_key",
    "vpn.example.com:51820",
    vec!["0.0.0.0/0".into()],
);
}

Split Tunnel (WireGuard)

Route only specific networks through VPN:

#![allow(unused)]
fn main() {
let peer = WireGuardPeer::new(
    "public_key",
    "vpn.example.com:51820",
    vec![
        "10.0.0.0/8".into(),
        "192.168.0.0/16".into(),
    ],
);
}

IPv6 Support

#![allow(unused)]
fn main() {
let peer = WireGuardPeer::new(
    "public_key",
    "vpn.example.com:51820",
    vec![
        "0.0.0.0/0".into(),
        "::/0".into(),
    ],
);
}

Error Handling

#![allow(unused)]
fn main() {
use nmrs::ConnectionError;

match nm.connect_vpn(config).await {
    Ok(_) => println!("VPN connected"),

    Err(ConnectionError::AuthFailed) => {
        eprintln!("Authentication failed — check keys or credentials");
    }

    Err(ConnectionError::Timeout) => {
        eprintln!("Connection timed out — check gateway address");
    }

    Err(ConnectionError::VpnFailed) => {
        eprintln!("VPN activation failed — check plugin or config");
    }

    Err(ConnectionError::NotFound) => {
        eprintln!("VPN gateway not reachable");
    }

    Err(e) => eprintln!("VPN error: {e}"),
}
}

Complete Example

use nmrs::{NetworkManager, WireGuardConfig, WireGuardPeer};

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;

    // Check if already connected
    let active = nm.active_vpn_connections().await?;
    if let Some(vpn) = active.first() {
        println!("Already connected to: {}", vpn.name);
        return Ok(());
    }

    // Create WireGuard configuration
    let peer = WireGuardPeer::new(
        std::env::var("WG_PUBLIC_KEY")?,
        std::env::var("WG_ENDPOINT")?,
        vec!["0.0.0.0/0".into()],
    ).with_persistent_keepalive(25);

    let config = WireGuardConfig::new(
        "AutoVPN",
        &std::env::var("WG_ENDPOINT")?,
        &std::env::var("WG_PRIVATE_KEY")?,
        &std::env::var("WG_ADDRESS")?,
        vec![peer],
    ).with_dns(vec!["1.1.1.1".into(), "8.8.8.8".into()]);

    // Connect
    println!("Connecting to VPN...");
    nm.connect_vpn(config).await?;

    // Verify connection
    let info = nm.get_vpn_info("AutoVPN").await?;
    println!("Connected! VPN IP: {:?}", info.ip4_address);

    // Keep connection alive
    println!("Press Ctrl+C to disconnect...");
    tokio::signal::ctrl_c().await?;

    // Disconnect
    nm.disconnect_vpn("AutoVPN").await?;
    println!("Disconnected from VPN");

    Ok(())
}

Advanced Topics

For more advanced VPN usage, see:

Security Best Practices

  1. Never hardcode keys or passwords — 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 (WireGuard)
  4. Protect certificates — Store OpenVPN certs with restrictive file permissions (chmod 600)
  5. Use TLS authentication — Prefer PasswordTls or Tls over Password alone for OpenVPN
  6. Verify endpoints — Ensure gateway addresses are correct
  7. Monitor connection — Check VPN status regularly

Troubleshooting

VPN Won't Connect

#![allow(unused)]
fn main() {
let vpns = nm.list_vpn_connections().await?;
for vpn in &vpns {
    println!("{}: {:?} (active: {})", vpn.name, vpn.state, vpn.active);
}
}

Connection Drops (WireGuard)

Use persistent keepalive:

#![allow(unused)]
fn main() {
let peer = peer.with_persistent_keepalive(25);
}

DNS Not Working

Explicitly set DNS servers:

#![allow(unused)]
fn main() {
let config = config.with_dns(vec![
    "1.1.1.1".into(),
    "8.8.8.8".into(),
]);
}

OpenVPN Plugin Not Found

Ensure the NetworkManager OpenVPN plugin is installed:

# Arch Linux
sudo pacman -S networkmanager-openvpn

# Debian/Ubuntu
sudo apt install network-manager-openvpn

# Fedora
sudo dnf install NetworkManager-openvpn

Next Steps

WireGuard Setup

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, WireGuardConfig, WireGuardPeer};

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;

    let peer = WireGuardPeer::new(
        "SERVER_PUBLIC_KEY_BASE64",
        "vpn.example.com:51820",
        vec!["0.0.0.0/0".into()],
    ).with_persistent_keepalive(25);

    let config = WireGuardConfig::new(
        "MyVPN",
        "vpn.example.com:51820",
        "CLIENT_PRIVATE_KEY_BASE64",
        "10.0.0.2/24",
        vec![peer],
    ).with_dns(vec!["1.1.1.1".into()]);

    nm.connect_vpn(config).await?;

    println!("VPN connected!");
    Ok(())
}

Understanding WireGuard Concepts

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)

WireGuardConfig Fields

FieldRequiredDescription
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 Configuration

#![allow(unused)]
fn main() {
use nmrs::{WireGuardConfig, WireGuardPeer};

let peer = WireGuardPeer::new(
    "HIgo9xNzJMWLKAShlKl6/bUT1VI9Q0SDBXGtLXkPFXc=",
    "vpn.example.com:51820",
    vec!["0.0.0.0/0".into(), "::/0".into()],
).with_persistent_keepalive(25)
 .with_preshared_key("OPTIONAL_PSK_BASE64");

let config = WireGuardConfig::new(
    "HomeVPN",
    "vpn.example.com:51820",
    "YBk6X3pP8KjKz7+HFWzVHNqL3qTZq8hX9VxFQJ4zVmM=",
    "10.0.0.2/24",
    vec![peer],
).with_dns(vec!["1.1.1.1".into(), "8.8.8.8".into()])
 .with_mtu(1420);
}

WireGuardPeer Configuration

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

OpenVPN Setup

OpenVPN is a widely-deployed, battle-tested VPN protocol that uses TLS for key exchange and supports a variety of authentication methods. nmrs provides full OpenVPN support through the NetworkManager OpenVPN plugin, letting you create, import, and manage OpenVPN connections programmatically.

Prerequisites

  • NetworkManager 1.2+
  • The OpenVPN plugin for NetworkManager:
    • Fedora / RHEL: NetworkManager-openvpn
    • Debian / Ubuntu: network-manager-openvpn
    • Arch Linux: networkmanager-openvpn
  • OpenVPN certificates and/or credentials from your VPN provider

Quick Start

Connect to an OpenVPN server using password + TLS certificate authentication:

use nmrs::{NetworkManager, OpenVpnConfig, OpenVpnAuthType};

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;

    let config = OpenVpnConfig::new("CorpVPN", "vpn.example.com", 1194, false)
        .with_auth_type(OpenVpnAuthType::PasswordTls)
        .with_ca_cert("/etc/openvpn/ca.crt")
        .with_client_cert("/etc/openvpn/client.crt")
        .with_client_key("/etc/openvpn/client.key")
        .with_username("alice")
        .with_password("hunter2")
        .with_dns(vec!["1.1.1.1".into()]);

    nm.connect_vpn(config).await?;

    println!("VPN connected!");
    Ok(())
}

Authentication Types

OpenVPN supports four authentication modes, selected with OpenVpnAuthType:

VariantDescriptionRequired Fields
PasswordUsername/password onlyusername
TlsTLS certificate onlyca_cert, client_cert, client_key
PasswordTlsPassword + TLS certificatesusername, ca_cert, client_cert, client_key
StaticKeyPre-shared static key(static key file via TLS auth)

Password Authentication

#![allow(unused)]
fn main() {
use nmrs::{OpenVpnConfig, OpenVpnAuthType};

let config = OpenVpnConfig::new("SimpleVPN", "vpn.example.com", 1194, false)
    .with_auth_type(OpenVpnAuthType::Password)
    .with_username("alice")
    .with_password("secret")
    .with_ca_cert("/etc/openvpn/ca.crt");
}

TLS Certificate Authentication

#![allow(unused)]
fn main() {
use nmrs::{OpenVpnConfig, OpenVpnAuthType};

let config = OpenVpnConfig::new("CertVPN", "vpn.example.com", 1194, false)
    .with_auth_type(OpenVpnAuthType::Tls)
    .with_ca_cert("/etc/openvpn/ca.crt")
    .with_client_cert("/etc/openvpn/client.crt")
    .with_client_key("/etc/openvpn/client.key");
}

If the client key is encrypted:

#![allow(unused)]
fn main() {
let config = config.with_key_password("keyfile-passphrase");
}

Password + TLS Authentication

The most common configuration for corporate VPNs — the server verifies both your certificate and your credentials:

#![allow(unused)]
fn main() {
use nmrs::{OpenVpnConfig, OpenVpnAuthType};

let config = OpenVpnConfig::new("CorpVPN", "vpn.example.com", 443, true)
    .with_auth_type(OpenVpnAuthType::PasswordTls)
    .with_ca_cert("/etc/openvpn/ca.crt")
    .with_client_cert("/etc/openvpn/client.crt")
    .with_client_key("/etc/openvpn/client.key")
    .with_username("alice")
    .with_password("secret");
}

Configuration Reference

FieldBuilder MethodRequiredDescription
nameconstructorYesConnection profile name
remoteconstructorYesServer hostname or IP
portconstructorYesServer port (typically 1194 or 443)
tcpconstructorYestrue for TCP, false for UDP
auth_typewith_auth_typeNoAuthentication mode (see above)
ca_certwith_ca_certNo*Path to CA certificate
client_certwith_client_certNo*Path to client certificate
client_keywith_client_keyNo*Path to client private key
key_passwordwith_key_passwordNoPassword for encrypted key file
usernamewith_usernameNo*Username for password auth
passwordwith_passwordNoPassword for password auth
cipherwith_cipherNoData channel cipher (e.g. "AES-256-GCM")
authwith_authNoHMAC digest algorithm (e.g. "SHA256")
dnswith_dnsNoDNS servers while connected
mtuwith_mtuNoMTU size
uuidwith_uuidNoCustom UUID (auto-generated if omitted)
compressionwith_compressionNoCompression mode (see below)
proxywith_proxyNoProxy configuration
redirect_gatewaywith_redirect_gatewayNoFull tunnel (false by default)
routeswith_routesNoSplit tunnel routes

* Required depending on the chosen auth_type.

Importing .ovpn Files

If you already have an .ovpn configuration file, you can import it directly.

High-Level Import

The simplest approach — import and connect in one call:

use nmrs::NetworkManager;

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;

    nm.import_ovpn("corp.ovpn", Some("alice"), Some("secret")).await?;

    println!("Connected via imported .ovpn profile");
    Ok(())
}

import_ovpn parses the file, creates a NetworkManager connection profile, and activates it. The connection name defaults to the filename stem (e.g., corp from corp.ovpn).

Builder-Based Import

For more control, use OpenVpnBuilder::from_ovpn_file to parse the file into a builder, then customise before connecting:

use nmrs::builders::OpenVpnBuilder;
use nmrs::NetworkManager;

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;

    let config = OpenVpnBuilder::from_ovpn_file("corp.ovpn")?
        .username("alice")
        .dns(vec!["1.1.1.1".into()])
        .mtu(1400)
        .remote_cert_tls("server")
        .build()?;

    nm.connect_vpn(config).await?;

    Ok(())
}

You can also parse from a string with OpenVpnBuilder::from_ovpn_str(content, name) if the configuration is fetched from a remote source.

TLS Hardening

OpenVPN's TLS layer can be hardened with several options. These are independent of the authentication type and can be combined.

TLS Auth

Adds an HMAC firewall to the control channel, providing DoS protection. Both sides must share the same static key and agree on direction:

#![allow(unused)]
fn main() {
let config = config
    .with_tls_auth("/etc/openvpn/ta.key", Some(1));
}

TLS-Crypt

Encrypts and authenticates the entire control channel with a pre-shared key — stronger than tls-auth because the TLS handshake itself is hidden:

#![allow(unused)]
fn main() {
let config = config
    .with_tls_crypt("/etc/openvpn/tls-crypt.key");
}

TLS-Crypt-v2

Per-client key wrapping, allowing the server to issue unique keys to each client while retaining the benefits of TLS-Crypt:

#![allow(unused)]
fn main() {
let config = config
    .with_tls_crypt_v2("/etc/openvpn/client-tls-crypt-v2.key");
}

Note: tls-auth, tls-crypt, and tls-crypt-v2 are mutually exclusive. Use only one.

Certificate Verification

Verify the server's certificate identity to prevent man-in-the-middle attacks:

#![allow(unused)]
fn main() {
use nmrs::OpenVpnConfig;

let config = OpenVpnConfig::new("SecureVPN", "vpn.example.com", 1194, false)
    .with_auth_type(nmrs::OpenVpnAuthType::Tls)
    .with_ca_cert("/etc/openvpn/ca.crt")
    .with_client_cert("/etc/openvpn/client.crt")
    .with_client_key("/etc/openvpn/client.key")
    .with_remote_cert_tls("server")
    .with_verify_x509_name("vpn.example.com", "name");
}
MethodPurpose
with_remote_cert_tls("server")Require the remote cert to have server (TLS Web Server) usage
with_verify_x509_name(name, type)Verify the CN or subject of the server certificate
with_tls_version_min("1.2")Enforce minimum TLS version
with_tls_version_max("1.3")Cap the maximum TLS version
with_tls_cipher(suite)Restrict control-channel cipher suites

Split Tunneling

By default, redirect_gateway is false — only traffic matching explicit routes goes through the VPN.

Full Tunnel

Route all traffic through the VPN:

#![allow(unused)]
fn main() {
let config = config.with_redirect_gateway(true);
}

Split Tunnel with Routes

Route only specific networks through the VPN using VpnRoute:

#![allow(unused)]
fn main() {
use nmrs::{OpenVpnConfig, OpenVpnAuthType, VpnRoute};

let config = OpenVpnConfig::new("OfficeVPN", "vpn.example.com", 1194, false)
    .with_auth_type(OpenVpnAuthType::Tls)
    .with_ca_cert("/etc/openvpn/ca.crt")
    .with_client_cert("/etc/openvpn/client.crt")
    .with_client_key("/etc/openvpn/client.key")
    .with_routes(vec![
        VpnRoute::new("10.0.0.0", 8),
        VpnRoute::new("192.168.1.0", 24).next_hop("10.0.0.1").metric(100),
    ]);
}
VpnRoute MethodDescription
VpnRoute::new(dest, prefix)Destination network and CIDR prefix length
.next_hop(gateway)Optional gateway for the route
.metric(m)Optional route metric (lower = higher priority)

Compression

OpenVPN supports several compression algorithms, but compression is disabled by default for security reasons.

#![allow(unused)]
fn main() {
use nmrs::OpenVpnCompression;

let config = config.with_compression(OpenVpnCompression::No);
}
VariantDescription
NoDisabled (recommended default)
LzoLZO compression (deprecated)
Lz4LZ4 compression
Lz4V2LZ4 v2 compression
YesAdaptive compression

Security Warning: Enabling compression on an OpenVPN tunnel that carries TLS traffic (HTTPS, etc.) exposes the connection to the VORACLE attack. An attacker who can observe encrypted VPN traffic and induce the victim to visit attacker-controlled content can recover plaintext via compression oracle side-channels. Leave compression disabled unless you have a specific need and understand the risk.

Proxy Support

Route OpenVPN traffic through an HTTP or SOCKS proxy:

HTTP Proxy

#![allow(unused)]
fn main() {
use nmrs::OpenVpnProxy;

let config = config.with_proxy(OpenVpnProxy::Http {
    server: "proxy.example.com".into(),
    port: 8080,
    username: Some("proxyuser".into()),
    password: Some("proxypass".into()),
    retry: true,
});
}

SOCKS Proxy

#![allow(unused)]
fn main() {
use nmrs::OpenVpnProxy;

let config = config.with_proxy(OpenVpnProxy::Socks {
    server: "socks.example.com".into(),
    port: 1080,
    retry: false,
});
}

When using a proxy, TCP mode (tcp: true in the constructor) is typically required.

Error Handling

Handle OpenVPN-specific errors:

use nmrs::{ConnectionError, NetworkManager, OpenVpnConfig, OpenVpnAuthType};

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;

    let config = OpenVpnConfig::new("CorpVPN", "vpn.example.com", 1194, false)
        .with_auth_type(OpenVpnAuthType::PasswordTls)
        .with_ca_cert("/etc/openvpn/ca.crt")
        .with_client_cert("/etc/openvpn/client.crt")
        .with_client_key("/etc/openvpn/client.key")
        .with_username("alice")
        .with_password("secret");

    match nm.connect_vpn(config).await {
        Ok(()) => println!("VPN connected"),

        Err(ConnectionError::VpnFailed(msg)) => {
            eprintln!("OpenVPN activation failed: {msg}");
        }

        Err(ConnectionError::AuthFailed) => {
            eprintln!("Authentication failed — check username/password and certificates");
        }

        Err(ConnectionError::Timeout) => {
            eprintln!("Connection timed out — verify server address and port");
        }

        Err(ConnectionError::InvalidGateway(msg)) => {
            eprintln!("Bad server address: {msg}");
        }

        Err(e) => eprintln!("Unexpected error: {e}"),
    }

    Ok(())
}
ErrorCause
VpnFailedPlugin missing, config rejected, or activation failed
AuthFailedBad username/password or certificate rejected
TimeoutServer unreachable or handshake timed out
InvalidGatewayEmpty or invalid remote address

Next Steps

VPN Management

Once you've set up a WireGuard or OpenVPN connection, nmrs provides methods to list, inspect, connect, disconnect, and remove VPN profiles.

Listing VPN Connections

use nmrs::NetworkManager;

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;

    let vpns = nm.list_vpn_connections().await?;
    for vpn in &vpns {
        println!("{}: {:?} [{:?}] (active: {})",
            vpn.name,
            vpn.vpn_type,
            vpn.state,
            vpn.active,
        );
        if let Some(iface) = &vpn.interface {
            println!("  Interface: {iface}");
        }
    }

    Ok(())
}

list_vpn_connections() returns all saved VPN profiles with their current state. The VpnConnection struct contains:

FieldTypeDescription
uuidStringConnection UUID
idStringConnection name (alias for name)
nameStringConnection profile name
vpn_typeVpnTypeVPN protocol — a data-carrying enum with WireGuard, OpenVpn, and other variants
stateDeviceStateCurrent state (Activated, Disconnected, etc.)
interfaceOption<String>Network interface when active (e.g., wg0, tun0)
activeboolWhether the connection is currently active
kindVpnKindVpnKind::Plugin (OpenVPN) or VpnKind::WireGuard

Active VPN Connections

Get only currently active VPN connections:

#![allow(unused)]
fn main() {
let active = nm.active_vpn_connections().await?;

for vpn in &active {
    println!("Active: {} ({:?}) on {:?}", vpn.name, vpn.vpn_type, vpn.interface);
}
}

Getting VPN Details

For an active VPN connection, retrieve detailed information including IP configuration:

#![allow(unused)]
fn main() {
let nm = NetworkManager::new().await?;

let info = nm.get_vpn_info("MyVPN").await?;

println!("Name:      {}", info.name);
println!("Kind:      {:?}", info.vpn_kind);
println!("State:     {:?}", info.state);
println!("Interface: {:?}", info.interface);
println!("Gateway:   {:?}", info.gateway);
println!("IPv4:      {:?}", info.ip4_address);
println!("IPv6:      {:?}", info.ip6_address);
println!("DNS:       {:?}", info.dns_servers);

if let Some(details) = &info.details {
    println!("Details:   {:?}", details);
}
}

The VpnConnectionInfo struct provides:

FieldTypeDescription
nameStringConnection name
vpn_kindVpnKindVpnKind::Plugin or VpnKind::WireGuard
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
detailsOption<VpnDetails>Additional VPN-specific details

Note: get_vpn_info() returns ConnectionError::NoVpnConnection if the VPN is not currently active.

Connecting to a Saved VPN

Reconnect to an existing VPN profile by name or UUID without rebuilding the config:

#![allow(unused)]
fn main() {
let nm = NetworkManager::new().await?;

// By profile name
nm.connect_vpn_by_id("MyVPN").await?;

// By UUID
nm.connect_vpn_by_uuid("a1b2c3d4-e5f6-7890-abcd-ef1234567890").await?;
}

Disconnecting a VPN

#![allow(unused)]
fn main() {
let nm = NetworkManager::new().await?;

// By name
nm.disconnect_vpn("MyVPN").await?;

// By UUID
nm.disconnect_vpn_by_uuid("a1b2c3d4-e5f6-7890-abcd-ef1234567890").await?;

println!("VPN disconnected");
}

disconnect_vpn() deactivates the VPN connection by name. If the VPN is not currently active or doesn't exist, it returns Ok(()) — the operation is idempotent.

Removing a VPN Profile

To permanently delete a saved VPN connection:

#![allow(unused)]
fn main() {
let nm = NetworkManager::new().await?;

nm.forget_vpn("MyVPN").await?;
println!("VPN profile deleted");
}

forget_vpn() searches for a saved VPN profile with the given name. If the VPN is currently connected, it disconnects first, then deletes the profile. Returns Ok(()) if no matching profile is found.

Complete Example

use nmrs::{NetworkManager, WireGuardConfig, WireGuardPeer};

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;

    // List existing VPNs
    println!("Saved VPN connections:");
    for vpn in nm.list_vpn_connections().await? {
        println!("  {} ({:?}) - {:?} [active: {}]",
            vpn.name, vpn.vpn_type, vpn.state, vpn.active);
    }

    // Connect a new WireGuard VPN
    let peer = WireGuardPeer::new(
        "SERVER_PUBLIC_KEY",
        "vpn.example.com:51820",
        vec!["0.0.0.0/0".into()],
    ).with_persistent_keepalive(25);

    let config = WireGuardConfig::new(
        "ExampleVPN",
        "vpn.example.com:51820",
        "CLIENT_PRIVATE_KEY",
        "10.0.0.2/24",
        vec![peer],
    ).with_dns(vec!["1.1.1.1".into()]);

    nm.connect_vpn(config).await?;

    // Show details
    let info = nm.get_vpn_info("ExampleVPN").await?;
    println!("\nConnected: IP = {:?}", info.ip4_address);

    // Disconnect
    nm.disconnect_vpn("ExampleVPN").await?;
    println!("Disconnected");

    // Clean up
    nm.forget_vpn("ExampleVPN").await?;
    println!("Profile removed");

    Ok(())
}

Error Handling

ErrorMethodMeaning
NoVpnConnectionget_vpn_infoVPN not active
VpnFailedconnect_vpnConnection activation failed
InvalidPrivateKeyconnect_vpnBad WireGuard key
InvalidAddressconnect_vpnBad IP/CIDR
InvalidGatewayconnect_vpnBad endpoint format
AuthFailedconnect_vpnOpenVPN authentication failed
InvalidConfigconnect_vpnOpenVPN configuration error (missing certs, bad options)

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

Check and control the Wi-Fi radio globally:

#![allow(unused)]
fn main() {
let nm = NetworkManager::new().await?;

// Check current state (software + hardware)
let state = nm.wifi_state().await?;
println!("Wi-Fi enabled: {}", state.enabled);
println!("Wi-Fi hardware enabled: {}", state.hardware_enabled);

// Global toggle
nm.set_wireless_enabled(false).await?;  // Disable
nm.set_wireless_enabled(true).await?;   // Enable
}

Note: wifi_state().hardware_enabled reflects the rfkill state. If the hardware switch is off, enabling Wi-Fi via software will have no effect.

For per-device Wi-Fi enable/disable, see Per-Device Scoping.

Waiting for Wi-Fi Ready

After enabling Wi-Fi, the device may take a moment to become ready:

#![allow(unused)]
fn main() {
let nm = NetworkManager::new().await?;

nm.set_wireless_enabled(true).await?;
nm.wait_for_wifi_ready().await?;

// Now safe to scan and connect
nm.scan_networks(None).await?;
}

Finding a Device by Interface Name

#![allow(unused)]
fn main() {
let nm = NetworkManager::new().await?;

let device_path = nm.get_device_by_interface("wlan0").await?;
println!("D-Bus path: {}", device_path.as_str());
}

Device Identity

Each device has both a permanent (factory) and current MAC address:

#![allow(unused)]
fn main() {
for device in nm.list_devices().await? {
    println!("{}: permanent={}, current={}",
        device.interface,
        device.identity.permanent_mac,
        device.identity.current_mac,
    );
}
}

If MAC randomization is enabled, the current MAC will differ from the permanent one.

Checking Connection Progress

Before starting a new connection, check if any device is currently connecting:

#![allow(unused)]
fn main() {
if nm.is_connecting().await? {
    println!("A connection operation is in progress");
}
}

Next Steps

Connection Profiles

NetworkManager stores connection profiles for every network you've connected to. These profiles contain the configuration needed to reconnect — SSID, credentials, IP settings, and more. nmrs provides methods to list, query, and remove these profiles.

Listing Saved Connections

use nmrs::NetworkManager;

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;

    let connections = nm.list_saved_connections().await?;
    for conn in &connections {
        println!("  {} ({})", conn.id, conn.connection_type);
    }

    Ok(())
}

list_saved_connections() returns full SavedConnection objects for all saved connection profiles across all connection types — Wi-Fi, Ethernet, VPN, and Bluetooth. Each SavedConnection includes the profile id (name), connection_type, and other metadata.

Checking for a Saved Connection

#![allow(unused)]
fn main() {
let nm = NetworkManager::new().await?;

if nm.has_saved_connection("HomeWiFi").await? {
    println!("Profile exists for HomeWiFi");
} else {
    println!("No saved profile — credentials will be needed");
}
}

How Saved Profiles Affect Connection

When you call connect() with an SSID that has a saved profile, nmrs activates the saved profile directly. This means:

  • Credentials are already stored — the 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", None, WifiSecurity::WpaPsk {
    psk: "password".into(),
}).await?;

// Later reconnection — saved profile is used, security parameter is ignored
nm.connect("HomeWiFi", None, WifiSecurity::Open).await?;
}

Forgetting (Deleting) Connections

Wi-Fi Connections

#![allow(unused)]
fn main() {
let nm = NetworkManager::new().await?;

nm.forget("HomeWiFi").await?;
println!("Wi-Fi profile deleted");
}

If currently connected to that network, forget() disconnects first, then deletes all saved profiles matching the SSID.

VPN Connections

#![allow(unused)]
fn main() {
nm.forget_vpn("MyVPN").await?;
}

Bluetooth Connections

#![allow(unused)]
fn main() {
nm.forget_bluetooth("My Phone").await?;
}

Getting the D-Bus Path

For advanced use cases, you can retrieve the D-Bus object path of a saved connection:

#![allow(unused)]
fn main() {
let nm = NetworkManager::new().await?;

if let Some(path) = nm.get_saved_connection_path("HomeWiFi").await? {
    println!("D-Bus path: {}", path.as_str());
}
}

Profile Lifecycle

  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 changes (access points appearing or disappearing, or signal strength changing):

use nmrs::NetworkManager;

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;

    // This runs indefinitely — spawn it as a background task
    nm.monitor_network_changes(|| {
        println!("Network list changed!");
    }).await?;

    Ok(())
}

monitor_network_changes() subscribes to D-Bus signals for access point additions, removals, and signal strength updates on all Wi-Fi devices. The callback fires whenever the visible network list or signal data changes.

Device State Monitoring

Subscribe to device state changes (connected, disconnected, cable plugged in, etc.):

use nmrs::NetworkManager;

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;

    nm.monitor_device_changes(|| {
        println!("Device state changed!");
    }).await?;

    Ok(())
}

monitor_device_changes() subscribes to state change signals on all network devices — both wired and wireless.

Running Monitors as Background Tasks

Both monitoring functions run indefinitely. In a real application, spawn them as background tasks:

With Tokio

use nmrs::NetworkManager;

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;

    // Spawn network monitor
    let nm_clone = nm.clone();
    tokio::spawn(async move {
        if let Err(e) = nm_clone.monitor_network_changes(|| {
            println!("Networks changed");
        }).await {
            eprintln!("Network monitor error: {}", e);
        }
    });

    // Spawn device monitor
    let nm_clone = nm.clone();
    tokio::spawn(async move {
        if let Err(e) = nm_clone.monitor_device_changes(|| {
            println!("Device state changed");
        }).await {
            eprintln!("Device monitor error: {}", e);
        }
    });

    // Your main application logic here
    loop {
        tokio::time::sleep(std::time::Duration::from_secs(60)).await;
    }
}

With GTK/GLib (for GUI applications)

#![allow(unused)]
fn main() {
use nmrs::NetworkManager;

// Inside a GTK application
let nm = NetworkManager::new().await?;

glib::MainContext::default().spawn_local({
    let nm = nm.clone();
    async move {
        let _ = nm.monitor_network_changes(|| {
            println!("Networks changed — refresh the UI!");
        }).await;
    }
});

glib::MainContext::default().spawn_local({
    let nm = nm.clone();
    async move {
        let _ = nm.monitor_device_changes(|| {
            println!("Device changed — update status!");
        }).await;
    }
});
}

Thread Safety

NetworkManager is Clone and can be safely shared across async tasks. Each clone shares the same underlying D-Bus connection, making it lightweight to pass into multiple monitoring tasks.

Practical Pattern: Refresh on Change

A common pattern is to refresh your application state whenever a change is detected:

use nmrs::NetworkManager;
use std::sync::Arc;
use tokio::sync::Notify;

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;
    let notify = Arc::new(Notify::new());

    // Monitor for changes
    let notify_clone = notify.clone();
    let nm_clone = nm.clone();
    tokio::spawn(async move {
        let _ = nm_clone.monitor_network_changes(move || {
            notify_clone.notify_one();
        }).await;
    });

    // React to changes
    loop {
        notify.notified().await;

        let networks = nm.list_networks(None).await?;
        println!("Updated: {} networks visible", networks.len());
    }
}

What Triggers Each Monitor

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
WifiInterfaceNotFoundSpecified Wi-Fi interface doesn't exist
NotAWifiDeviceInterface exists but isn't Wi-Fi
HardwareRadioKilledHardware kill switch is on
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
VpnIdAmbiguousMultiple VPNs share the same name
IncompleteBuilderVPN builder missing required fields
InvalidPrivateKey(String)Bad WireGuard private key
InvalidPublicKey(String)Bad WireGuard public key
InvalidAddress(String)Bad IP address or CIDR notation
InvalidGateway(String)Bad gateway format (host:port)
InvalidPeers(String)Invalid peer configuration

Bluetooth Errors

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", None, WifiSecurity::Open).await?;
    Ok(())
}

Pattern Matching

Handle specific errors differently:

#![allow(unused)]
fn main() {
use nmrs::{NetworkManager, WifiSecurity, ConnectionError};

let nm = NetworkManager::new().await?;

match nm.connect("MyWiFi", None, WifiSecurity::WpaPsk {
    psk: "password".into(),
}).await {
    Ok(_) => println!("Connected!"),
    Err(ConnectionError::NotFound) => {
        eprintln!("Network not in range");
    }
    Err(ConnectionError::AuthFailed) => {
        eprintln!("Wrong password");
    }
    Err(ConnectionError::Timeout) => {
        eprintln!("Connection timed out");
    }
    Err(ConnectionError::DhcpFailed) => {
        eprintln!("Connected to AP but DHCP failed");
    }
    Err(ConnectionError::NoWifiDevice) => {
        eprintln!("No Wi-Fi adapter found");
    }
    Err(e) => eprintln!("Unexpected error: {}", e),
}
}

Retry Logic

Implement retries for transient failures:

#![allow(unused)]
fn main() {
use nmrs::{NetworkManager, WifiSecurity, ConnectionError};

let nm = NetworkManager::new().await?;

for attempt in 1..=3 {
    match nm.connect("MyWiFi", None, WifiSecurity::WpaPsk {
        psk: "password".into(),
    }).await {
        Ok(_) => {
            println!("Connected on attempt {}", attempt);
            break;
        }
        Err(ConnectionError::Timeout) if attempt < 3 => {
            eprintln!("Attempt {} timed out, retrying...", attempt);
            continue;
        }
        Err(e) => return Err(e),
    }
}
}

VPN Error Handling

#![allow(unused)]
fn main() {
use nmrs::{NetworkManager, ConnectionError};

let nm = NetworkManager::new().await?;

match nm.get_vpn_info("MyVPN").await {
    Ok(info) => println!("VPN IP: {:?}", info.ip4_address),
    Err(ConnectionError::NoVpnConnection) => {
        eprintln!("VPN is not active");
    }
    Err(e) => eprintln!("Error: {}", e),
}
}

Converting to Other Error Types

ConnectionError implements std::error::Error and Display, so it works with error handling crates like anyhow:

#![allow(unused)]
fn main() {
use anyhow::Result;
use nmrs::NetworkManager;

async fn connect() -> Result<()> {
    let nm = NetworkManager::new().await?;
    nm.connect("MyWiFi", None, nmrs::WifiSecurity::Open).await?;
    Ok(())
}
}

Radio / Airplane-mode Errors

VariantDescription
HardwareRadioKilledHardware kill switch is on; Wi-Fi cannot be enabled until the switch is toggled
BluezUnavailableBluetooth D-Bus service (BlueZ) is not running or unreachable

Non-Exhaustive

ConnectionError is marked #[non_exhaustive], which means new variants may be added in future versions without a breaking change. Always include a wildcard arm in match expressions:

#![allow(unused)]
fn main() {
match result {
    Err(ConnectionError::AuthFailed) => { /* ... */ }
    Err(ConnectionError::NotFound) => { /* ... */ }
    Err(e) => { /* catch-all for current and future variants */ }
    Ok(_) => {}
}
}

Next Steps

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(None).await?;
    println!("{} networks found", networks.len());
    Ok(())
}

Add to your Cargo.toml:

[dependencies]
nmrs = "2.2"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

Single-Threaded Tokio

For lightweight applications, you can use the current-thread runtime:

#[tokio::main(flavor = "current_thread")]
async fn main() -> nmrs::Result<()> {
    let nm = nmrs::NetworkManager::new().await?;
    // ...
    Ok(())
}

async-std

use nmrs::NetworkManager;

#[async_std::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;
    let networks = nm.list_networks(None).await?;
    println!("{} networks found", networks.len());
    Ok(())
}
[dependencies]
nmrs = "2.2"
async-std = { version = "1", features = ["attributes"] }

smol

use nmrs::NetworkManager;

fn main() -> nmrs::Result<()> {
    smol::block_on(async {
        let nm = NetworkManager::new().await?;
        let networks = nm.list_networks(None).await?;
        println!("{} networks found", networks.len());
        Ok(())
    })
}
[dependencies]
nmrs = "2.2"
smol = "2"

GLib/GTK (for GUI applications)

When building GTK4 applications, use the GLib main context:

#![allow(unused)]
fn main() {
use nmrs::NetworkManager;

// Inside a GTK application's async context
glib::MainContext::default().spawn_local(async {
    let nm = NetworkManager::new().await.unwrap();

    let networks = nm.list_networks(None).await.unwrap();
    for net in &networks {
        println!("{}: {}%", net.ssid, net.strength.unwrap_or(0));
    }
});
}

How It Works

nmrs uses zbus for D-Bus communication, which itself uses an internal async runtime. When you call NetworkManager::new().await, zbus establishes a connection to the system D-Bus. This connection is runtime-agnostic — zbus handles the async I/O internally.

The NetworkManager struct is Clone and Send, so you can share it across tasks regardless of which runtime you use.

Thread Safety

NetworkManager is:

  • Clone — clones share the same D-Bus connection (cheap)
  • Send — can be moved across 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", None, WifiSecurity::Open).await {
    Ok(_) => println!("Connected!"),
    Err(ConnectionError::Timeout) => {
        eprintln!("Connection timed out — try a longer timeout");
    }
    Err(e) => eprintln!("Error: {}", e),
}
}

What Timeouts Affect

Timeouts apply to all operations that wait for NetworkManager state transitions:

  • connect() — Wi-Fi connection 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 and AP Strength changes
  ← D-Bus: Signal whenever an AP appears, disappears, or changes strength
  → nmrs: Invoke callback

Connection Settings

When connecting to a network, nmrs builds a settings dictionary and sends it via D-Bus:

nmrs: nm.connect("MyWiFi", None, WifiSecurity::WpaPsk { psk: "..." })
  → Build settings HashMap
  → D-Bus: AddAndActivateConnection(settings, device_path, specific_object)
  ← D-Bus: Active connection path
  → D-Bus: Monitor StateChanged signal
  ← D-Bus: State transitions until Activated or Failed
  → nmrs: Ok(()) or Err(ConnectionError)

D-Bus Proxy Types

nmrs wraps D-Bus interfaces in typed proxy structs (defined in nmrs::dbus):

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(None).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(None).await?;
        
        // Sort by signal strength (strongest first)
        networks.sort_by(|a, b| {
            b.strength.unwrap_or(0).cmp(&a.strength.unwrap_or(0))
        });
        
        // Display header
        println!("WiFi Network Scanner - {} networks found\n", networks.len());
        println!("{:<30} {:>10} {:>8} {:<20}", 
                 "SSID", "Signal", "Band", "Security");
        println!("{}", "-".repeat(70));
        
        // Display each network
        for net in networks {
            print_network(&net);
        }
        
        println!("\n{}", "-".repeat(70));
        println!("Press Ctrl+C to exit");
        
        // Wait before next scan
        sleep(Duration::from_secs(5)).await;
    }
}

fn print_network(net: &Network) {
    let signal = net.strength.unwrap_or(0);
    let signal_bar = signal_strength_bar(signal);
    
    let band = match net.frequency {
        Some(freq) if freq > 5000 => "5GHz",
        Some(_) => "2.4GHz",
        None => "Unknown",
    };
    
    let security = match &net.security {
        nmrs::WifiSecurity::Open => "Open",
        nmrs::WifiSecurity::WpaPsk { .. } => "WPA-PSK",
        nmrs::WifiSecurity::WpaEap { .. } => "WPA-EAP",
    };
    
    println!("{:<30} {:>3}% {} {:>8} {:<20}",
             truncate_ssid(&net.ssid, 30),
             signal,
             signal_bar,
             band,
             security
    );
}

fn signal_strength_bar(strength: u8) -> String {
    let bars = match strength {
        80..=100 => "▂▄▆█",
        60..=79  => "▂▄▆▁",
        40..=59  => "▂▄▁▁",
        20..=39  => "▂▁▁▁",
        _        => "▁▁▁▁",
    };
    
    let color = match strength {
        70..=100 => "\x1b[32m", // Green
        40..=69  => "\x1b[33m", // Yellow
        _        => "\x1b[31m", // Red
    };
    
    format!("{}{}\x1b[0m", color, bars)
}

fn truncate_ssid(ssid: &str, max_len: usize) -> String {
    if ssid.len() <= max_len {
        ssid.to_string()
    } else {
        format!("{}...", &ssid[..max_len - 3])
    }
}

Running the Example

Add to your Cargo.toml:

[dependencies]
nmrs = "2.0.0"
tokio = { version = "1", features = ["full"] }

Run with:

cargo run

Sample Output

WiFi Network Scanner - 8 networks found

SSID                           Signal     Band Security            
----------------------------------------------------------------------
MyHomeNetwork                   92% ▂▄▆█  5GHz WPA-PSK            
CoffeeShop_Guest                78% ▂▄▆▁  2.4GHz Open              
Neighbor-5G                     65% ▂▄▆▁  5GHz WPA-PSK            
Corporate_WiFi                  58% ▂▄▁▁  5GHz WPA-EAP            
Guest_Network                   45% ▂▄▁▁  2.4GHz Open              
FarAwayNetwork                  22% ▂▁▁▁  2.4GHz WPA-PSK            

----------------------------------------------------------------------
Press Ctrl+C to exit

Enhancements

Filter by Signal Strength

#![allow(unused)]
fn main() {
// Only show networks with signal > 30%
let networks: Vec<_> = networks
    .into_iter()
    .filter(|n| n.strength.unwrap_or(0) > 30)
    .collect();
}

Group by Frequency Band

#![allow(unused)]
fn main() {
let mut networks_2_4ghz = Vec::new();
let mut networks_5ghz = Vec::new();

for net in networks {
    match net.frequency {
        Some(freq) if freq > 5000 => networks_5ghz.push(net),
        Some(_) => networks_2_4ghz.push(net),
        None => {}
    }
}

println!("\n5GHz Networks:");
for net in networks_5ghz {
    print_network(&net);
}

println!("\n2.4GHz Networks:");
for net in networks_2_4ghz {
    print_network(&net);
}
}

Add Connection Capability

#![allow(unused)]
fn main() {
use std::io::{self, Write};

// After displaying networks
print!("\nEnter number to connect (or 0 to skip): ");
io::stdout().flush()?;

let mut input = String::new();
io::stdin().read_line(&mut input)?;

if let Ok(choice) = input.trim().parse::<usize>() {
    if choice > 0 && choice <= networks.len() {
        let selected = &networks[choice - 1];
        
        // Get password if needed
        match &selected.security {
            nmrs::WifiSecurity::Open => {
                nm.connect(&selected.ssid, None, nmrs::WifiSecurity::Open).await?;
                println!("Connected to {}", selected.ssid);
            }
            _ => {
                print!("Enter password: ");
                io::stdout().flush()?;
                let mut password = String::new();
                io::stdin().read_line(&mut password)?;
                
                nm.connect(&selected.ssid, None, nmrs::WifiSecurity::WpaPsk {
                    psk: password.trim().to_string()
                }).await?;
                println!("Connected to {}", selected.ssid);
            }
        }
    }
}
}

Export to JSON

#![allow(unused)]
fn main() {
use serde::{Serialize, Deserialize};

#[derive(Serialize)]
struct NetworkExport {
    ssid: String,
    signal: u8,
    frequency: Option<u32>,
    security: String,
}

// Convert networks to exportable format
let exports: Vec<NetworkExport> = networks.iter().map(|n| {
    NetworkExport {
        ssid: n.ssid.clone(),
        signal: n.strength.unwrap_or(0),
        frequency: n.frequency,
        security: format!("{:?}", n.security),
    }
}).collect();

// Write to file
let json = serde_json::to_string_pretty(&exports)?;
std::fs::write("networks.json", json)?;
println!("Exported to networks.json");
}

See Also

WiFi Auto-Connect

This example demonstrates a program that automatically connects to a preferred network from a priority-ordered list.

Features

  • Scans for available networks
  • Matches against a list of preferred networks (in priority order)
  • Connects to the highest-priority available network
  • Falls back to lower-priority networks if the preferred one isn't found

Code

use nmrs::{NetworkManager, WifiSecurity, ConnectionError};
use std::collections::HashMap;

struct PreferredNetwork {
    ssid: String,
    security: WifiSecurity,
}

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;

    // Define preferred networks in priority order (highest first)
    let preferred = vec![
        PreferredNetwork {
            ssid: "HomeWiFi".into(),
            security: WifiSecurity::WpaPsk {
                psk: std::env::var("HOME_WIFI_PSK").unwrap_or_default(),
            },
        },
        PreferredNetwork {
            ssid: "OfficeWiFi".into(),
            security: WifiSecurity::WpaPsk {
                psk: std::env::var("OFFICE_WIFI_PSK").unwrap_or_default(),
            },
        },
        PreferredNetwork {
            ssid: "CafeOpen".into(),
            security: WifiSecurity::Open,
        },
    ];

    // Check if already connected
    if let Some(ssid) = nm.current_ssid().await {
        if preferred.iter().any(|p| p.ssid == ssid) {
            println!("Already connected to preferred network: {}", ssid);
            return Ok(());
        }
    }

    // Scan and list visible networks
    println!("Scanning for networks...");
    nm.scan_networks(None).await?;
    let visible = nm.list_networks(None).await?;

    let visible_ssids: HashMap<&str, &nmrs::Network> = visible
        .iter()
        .map(|n| (n.ssid.as_str(), n))
        .collect();

    // Try each preferred network in order
    for pref in &preferred {
        if let Some(net) = visible_ssids.get(pref.ssid.as_str()) {
            println!(
                "Found '{}' ({}%) — connecting...",
                pref.ssid,
                net.strength.unwrap_or(0),
            );

            match nm.connect(&pref.ssid, None, pref.security.clone()).await {
                Ok(_) => {
                    println!("Connected to '{}'!", pref.ssid);
                    return Ok(());
                }
                Err(ConnectionError::AuthFailed) => {
                    eprintln!("Auth failed for '{}', trying next...", pref.ssid);
                    continue;
                }
                Err(e) => {
                    eprintln!("Failed to connect to '{}': {}", pref.ssid, e);
                    continue;
                }
            }
        }
    }

    eprintln!("No preferred networks found");
    Ok(())
}

Running

HOME_WIFI_PSK="my_home_password" OFFICE_WIFI_PSK="office_pass" cargo run --example wifi_auto_connect

How It Works

  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, None, WifiSecurity::WpaEap { opts: eap }).await {
        Ok(_) => {
            println!("Connected!");

            if let Some((ssid, freq)) = nm.current_connection_info().await {
                let band = match freq {
                    Some(f) if f > 5000 => "5 GHz",
                    Some(_) => "2.4 GHz",
                    None => "unknown",
                };
                println!("  Network: {} ({})", ssid, band);
            }
        }
        Err(ConnectionError::AuthFailed) => {
            eprintln!("Authentication failed — check username and password");
        }
        Err(ConnectionError::SupplicantConfigFailed) => {
            eprintln!("Supplicant config error — check EAP method and phase2");
        }
        Err(ConnectionError::SupplicantTimeout) => {
            eprintln!("RADIUS server not responding — check CA cert and domain");
        }
        Err(ConnectionError::Timeout) => {
            eprintln!("Connection timed out — enterprise auth may need more time");
        }
        Err(e) => eprintln!("Error: {}", e),
    }

    Ok(())
}

Running

EAP_IDENTITY="user@company.com" \
EAP_PASSWORD="my_password" \
EAP_SSID="CorpWiFi" \
cargo run --example enterprise_wifi

Variations

TTLS/PAP Configuration

Some networks use TTLS with PAP instead of PEAP:

#![allow(unused)]
fn main() {
let eap = EapOptions::builder()
    .identity("student@university.edu")
    .password("password")
    .method(EapMethod::Ttls)
    .phase2(Phase2::Pap)
    .ca_cert_path("file:///etc/ssl/certs/university-ca.pem")
    .build();
}

Custom CA Certificate

If your organization provides a specific CA certificate:

#![allow(unused)]
fn main() {
let eap = EapOptions::builder()
    .identity("user@company.com")
    .password("password")
    .method(EapMethod::Peap)
    .phase2(Phase2::Mschapv2)
    .ca_cert_path("file:///usr/local/share/ca-certificates/corp-ca.pem")
    .domain_suffix_match("company.com")
    .build();
}

Common Issues

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 configuration with WireGuardConfig
  • Connects and retrieves VPN details
  • Displays IP configuration and DNS
  • Cleanly disconnects on completion

Code

use nmrs::{NetworkManager, WireGuardConfig, WireGuardPeer};

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;

    // Check for existing VPN connections
    let existing = nm.list_vpn_connections().await?;
    if !existing.is_empty() {
        println!("Existing VPN connections:");
        for vpn in &existing {
            println!("  {} ({:?}) — {:?}", vpn.name, vpn.vpn_type, vpn.state);
        }
        println!();
    }

    // Build WireGuard peer configuration
    let peer = WireGuardPeer::new(
        std::env::var("WG_SERVER_PUBKEY")
            .expect("Set WG_SERVER_PUBKEY"),
        std::env::var("WG_ENDPOINT")
            .unwrap_or_else(|_| "vpn.example.com:51820".into()),
        vec!["0.0.0.0/0".into()],
    )
    .with_persistent_keepalive(25);

    // Build configuration
    let config = WireGuardConfig::new(
        "ExampleVPN",
        std::env::var("WG_ENDPOINT")
            .unwrap_or_else(|_| "vpn.example.com:51820".into()),
        std::env::var("WG_PRIVATE_KEY")
            .expect("Set WG_PRIVATE_KEY"),
        std::env::var("WG_ADDRESS")
            .unwrap_or_else(|_| "10.0.0.2/24".into()),
        vec![peer],
    )
    .with_dns(vec!["1.1.1.1".into(), "8.8.8.8".into()])
    .with_mtu(1420);

    // Connect
    println!("Connecting to VPN...");
    nm.connect_vpn(config).await?;
    println!("Connected!\n");

    // Show VPN details
    let info = nm.get_vpn_info("ExampleVPN").await?;
    println!("VPN Connection Details:");
    println!("  Name:      {}", info.name);
    println!("  Kind:      {:?}", info.vpn_kind);
    println!("  State:     {:?}", info.state);
    println!("  Interface: {:?}", info.interface);
    println!("  Gateway:   {:?}", info.gateway);
    println!("  IPv4:      {:?}", info.ip4_address);
    println!("  IPv6:      {:?}", info.ip6_address);
    println!("  DNS:       {:?}", info.dns_servers);

    // Wait for user input before disconnecting
    println!("\nPress Enter to disconnect...");
    let mut input = String::new();
    std::io::stdin().read_line(&mut input).ok();

    // Disconnect
    nm.disconnect_vpn("ExampleVPN").await?;
    println!("VPN disconnected");

    // Optionally remove the profile
    // nm.forget_vpn("ExampleVPN").await?;

    Ok(())
}

Running

WG_SERVER_PUBKEY="HIgo9xNzJMWLKAShlKl6/bUT1VI9Q0SDBXGtLXkPFXc=" \
WG_PRIVATE_KEY="YBk6X3pP8KjKz7+HFWzVHNqL3qTZq8hX9VxFQJ4zVmM=" \
WG_ENDPOINT="vpn.example.com:51820" \
WG_ADDRESS="10.0.0.2/24" \
cargo run --example wireguard_client

Sample Output

Connecting to VPN...
Connected!

VPN Connection Details:
  Name:      ExampleVPN
  Kind:      WireGuard
  State:     Activated
  Interface: Some("wg-examplevpn")
  Gateway:   Some("vpn.example.com")
  IPv4:      Some("10.0.0.2/24")
  IPv6:      None
  DNS:       ["1.1.1.1", "8.8.8.8"]

Press Enter to disconnect...

Split Tunnel Variation

Route only specific subnets through the VPN:

#![allow(unused)]
fn main() {
let peer = WireGuardPeer::new(
    "server_public_key",
    "vpn.example.com:51820",
    vec![
        "10.0.0.0/8".into(),
        "192.168.0.0/16".into(),
    ],
);
}

Multiple Peers

Connect through multiple WireGuard servers:

#![allow(unused)]
fn main() {
let peer1 = WireGuardPeer::new(
    "peer1_pubkey",
    "us-east.vpn.example.com:51820",
    vec!["10.1.0.0/16".into()],
);

let peer2 = WireGuardPeer::new(
    "peer2_pubkey",
    "eu-west.vpn.example.com:51820",
    vec!["10.2.0.0/16".into()],
);

let config = WireGuardConfig::new(
    "MultiPeerVPN",
    "us-east.vpn.example.com:51820",
    "client_private_key",
    "10.0.0.2/24",
    vec![peer1, peer2],
);
}

OpenVPN Client

This example demonstrates a complete OpenVPN client that authenticates with password+TLS, connects, displays VPN details, and cleanly disconnects.

Features

  • Builds OpenVPN config with password+TLS authentication
  • Reads credentials from environment variables
  • Connects and retrieves VPN details
  • Displays IP configuration, DNS, and gateway
  • Cleanly disconnects on completion

Code

use nmrs::{NetworkManager, OpenVpnConfig, OpenVpnAuthType};

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;

    // Read credentials from environment
    let remote = std::env::var("OVPN_REMOTE")
        .unwrap_or_else(|_| "vpn.example.com".into());
    let port: u16 = std::env::var("OVPN_PORT")
        .unwrap_or_else(|_| "1194".into())
        .parse()
        .expect("OVPN_PORT must be a valid port number");
    let username = std::env::var("OVPN_USER")
        .expect("Set OVPN_USER");
    let password = std::env::var("OVPN_PASS")
        .expect("Set OVPN_PASS");
    let ca_path = std::env::var("OVPN_CA")
        .unwrap_or_else(|_| "/etc/openvpn/ca.crt".into());
    let cert_path = std::env::var("OVPN_CERT")
        .unwrap_or_else(|_| "/etc/openvpn/client.crt".into());
    let key_path = std::env::var("OVPN_KEY")
        .unwrap_or_else(|_| "/etc/openvpn/client.key".into());

    // Build OpenVPN config (password+TLS)
    let config = OpenVpnConfig::new("CorpVPN", &remote, port, false)
        .with_auth_type(OpenVpnAuthType::PasswordTls)
        .with_username(&username)
        .with_password(&password)
        .with_ca_cert(&ca_path)
        .with_client_cert(&cert_path)
        .with_client_key(&key_path);

    // Connect
    println!("Connecting to OpenVPN ({remote}:{port})...");
    nm.connect_vpn(config).await?;
    println!("Connected!\n");

    // Show VPN details
    let info = nm.get_vpn_info("CorpVPN").await?;
    println!("VPN Connection Details:");
    println!("  Name:      {}", info.name);
    println!("  Kind:      {:?}", info.vpn_kind);
    println!("  State:     {:?}", info.state);
    println!("  Interface: {:?}", info.interface);
    println!("  Gateway:   {:?}", info.gateway);
    println!("  IPv4:      {:?}", info.ip4_address);
    println!("  IPv6:      {:?}", info.ip6_address);
    println!("  DNS:       {:?}", info.dns_servers);

    if let Some(details) = &info.details {
        println!("  Details:   {:?}", details);
    }

    // Wait for user input before disconnecting
    println!("\nPress Enter to disconnect...");
    let mut input = String::new();
    std::io::stdin().read_line(&mut input).ok();

    // Disconnect
    nm.disconnect_vpn("CorpVPN").await?;
    println!("VPN disconnected");

    Ok(())
}

Running

OVPN_REMOTE="vpn.example.com" \
OVPN_PORT="1194" \
OVPN_USER="alice" \
OVPN_PASS="hunter2" \
OVPN_CA="/etc/openvpn/ca.crt" \
OVPN_CERT="/etc/openvpn/client.crt" \
OVPN_KEY="/etc/openvpn/client.key" \
cargo run --example openvpn_client

Sample Output

Connecting to OpenVPN (vpn.example.com:1194)...
Connected!

VPN Connection Details:
  Name:      CorpVPN
  Kind:      Plugin
  State:     Activated
  Interface: Some("tun0")
  Gateway:   Some("vpn.example.com")
  IPv4:      Some("10.8.0.2")
  IPv6:      None
  DNS:       ["10.8.0.1"]

Press Enter to disconnect...

Error Handling

#![allow(unused)]
fn main() {
use nmrs::{NetworkManager, OpenVpnConfig, OpenVpnAuthType, ConnectionError};

async fn connect_with_retry(nm: &NetworkManager) -> nmrs::Result<()> {
    let config = OpenVpnConfig::new("CorpVPN", "vpn.example.com", 1194, false)
        .with_auth_type(OpenVpnAuthType::PasswordTls)
        .with_username("alice")
        .with_password("hunter2")
        .with_ca_cert("/etc/openvpn/ca.crt")
        .with_client_cert("/etc/openvpn/client.crt")
        .with_client_key("/etc/openvpn/client.key");

    match nm.connect_vpn(config).await {
        Ok(_) => {
            println!("VPN connected");
            Ok(())
        }
        Err(ConnectionError::AuthFailed) => {
            eprintln!("Authentication failed — check username/password and certificates");
            Err(ConnectionError::AuthFailed)
        }
        Err(ConnectionError::Timeout) => {
            eprintln!("Connection timed out — check server address and port");
            Err(ConnectionError::Timeout)
        }
        Err(ConnectionError::VpnFailed) => {
            eprintln!("VPN activation failed — verify OpenVPN plugin is installed");
            Err(ConnectionError::VpnFailed)
        }
        Err(e) => {
            eprintln!("Unexpected error: {e}");
            Err(e)
        }
    }
}
}

TLS-Only Variation

For certificate-only authentication (no username/password):

#![allow(unused)]
fn main() {
use nmrs::{OpenVpnConfig, OpenVpnAuthType};

let config = OpenVpnConfig::new("TlsOnlyVPN", "vpn.example.com", 1194, false)
    .with_auth_type(OpenVpnAuthType::Tls)
    .with_ca_cert("/etc/openvpn/ca.crt")
    .with_client_cert("/etc/openvpn/client.crt")
    .with_client_key("/etc/openvpn/client.key");

nm.connect_vpn(config).await?;
}

Password-Only Variation

For username/password authentication without client certificates:

#![allow(unused)]
fn main() {
use nmrs::{OpenVpnConfig, OpenVpnAuthType};

let config = OpenVpnConfig::new("PassVPN", "vpn.example.com", 1194, false)
    .with_auth_type(OpenVpnAuthType::Password)
    .with_username("alice")
    .with_password("hunter2")
    .with_ca_cert("/etc/openvpn/ca.crt");

nm.connect_vpn(config).await?;
}

Next Steps

.ovpn File Import

This example shows how to import an existing .ovpn configuration file into NetworkManager using nmrs.

Features

  • Import .ovpn files with a single method call
  • Builder approach for more control over the import
  • Automatic handling of inline certificates
  • Error handling for parse failures

One-Liner Import

The simplest way to import an .ovpn file:

use nmrs::NetworkManager;

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;

    // Import with credentials
    nm.import_ovpn("client.ovpn", Some("alice"), Some("hunter2")).await?;
    println!("VPN imported and connected!");

    Ok(())
}

If the .ovpn file uses certificate-only authentication, pass None for username and password:

#![allow(unused)]
fn main() {
nm.import_ovpn("client.ovpn", None, None).await?;
}

Builder Approach

For more control over the import process, use OpenVpnBuilder:

use nmrs::{NetworkManager, OpenVpnBuilder};

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;

    let config = OpenVpnBuilder::from_ovpn_file("client.ovpn")?
        .username("alice")
        .password("hunter2")
        .build()?;

    nm.connect_vpn(config).await?;
    println!("VPN connected!");

    Ok(())
}

The builder extracts remote, port, protocol, certificates, and other settings from the .ovpn file automatically.

Inline Certificates

Many .ovpn files embed certificates directly rather than referencing external files:

<ca>
-----BEGIN CERTIFICATE-----
MIIBxTCCAWugAwIBAgIJAJ...
-----END CERTIFICATE-----
</ca>

<cert>
-----BEGIN CERTIFICATE-----
MIICCzCCAZGgAwIBAgIRAP...
-----END CERTIFICATE-----
</cert>

<key>
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w...
-----END PRIVATE KEY-----
</key>

from_ovpn_file handles these automatically — inline certificates are extracted and written to temporary files that NetworkManager can reference. No extra steps needed:

#![allow(unused)]
fn main() {
let config = OpenVpnBuilder::from_ovpn_file("inline-certs.ovpn")?
    .username("alice")
    .password("hunter2")
    .build()?;
}

Error Handling

Handle parse failures and missing fields gracefully:

#![allow(unused)]
fn main() {
use nmrs::{OpenVpnBuilder, ConnectionError};

fn import_config(path: &str) -> nmrs::Result<()> {
    let config = match OpenVpnBuilder::from_ovpn_file(path) {
        Ok(builder) => builder.build()?,
        Err(ConnectionError::InvalidConfig(msg)) => {
            eprintln!("Failed to parse {path}: {msg}");
            return Err(ConnectionError::InvalidConfig(msg));
        }
        Err(ConnectionError::NotFound) => {
            eprintln!("File not found: {path}");
            return Err(ConnectionError::NotFound);
        }
        Err(e) => {
            eprintln!("Import error: {e}");
            return Err(e);
        }
    };

    println!("Successfully parsed configuration");
    Ok(())
}
}

Common parse errors:

ErrorCause
InvalidConfigMissing remote directive, malformed options, or invalid certificate data
NotFoundThe .ovpn file does not exist at the given path
AuthFailedCredentials required but not provided

Complete Example

use nmrs::{NetworkManager, OpenVpnBuilder};

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;

    let ovpn_path = std::env::args()
        .nth(1)
        .unwrap_or_else(|| "client.ovpn".into());

    let username = std::env::var("OVPN_USER").ok();
    let password = std::env::var("OVPN_PASS").ok();

    println!("Importing {ovpn_path}...");

    // Builder approach for maximum control
    let mut builder = OpenVpnBuilder::from_ovpn_file(&ovpn_path)?;

    if let Some(user) = &username {
        builder = builder.username(user);
    }
    if let Some(pass) = &password {
        builder = builder.password(pass);
    }

    let config = builder.build()?;
    nm.connect_vpn(config).await?;

    println!("Connected! Checking VPN info...");
    let vpns = nm.list_vpn_connections().await?;
    for vpn in &vpns {
        if vpn.active {
            println!("  Active: {} ({:?})", vpn.name, vpn.vpn_type);
        }
    }

    Ok(())
}

Next Steps

Network Monitor Dashboard

This example creates a real-time network monitoring dashboard that reacts to network and device changes using D-Bus signals.

Features

  • Monitors network list changes in real-time
  • Monitors device state changes
  • Refreshes network list on changes
  • Displays current connection status

Code

use nmrs::NetworkManager;
use std::sync::Arc;
use tokio::sync::Notify;

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;

    println!("=== Network Monitor Dashboard ===\n");

    // Print initial state
    print_status(&nm).await;

    // Set up change notifications
    let network_notify = Arc::new(Notify::new());
    let device_notify = Arc::new(Notify::new());

    // Monitor network changes (access points)
    let notify = network_notify.clone();
    let nm_clone = nm.clone();
    tokio::spawn(async move {
        if let Err(e) = nm_clone.monitor_network_changes(move || {
            notify.notify_one();
        }).await {
            eprintln!("Network monitor error: {}", e);
        }
    });

    // Monitor device changes (state transitions)
    let notify = device_notify.clone();
    let nm_clone = nm.clone();
    tokio::spawn(async move {
        if let Err(e) = nm_clone.monitor_device_changes(move || {
            notify.notify_one();
        }).await {
            eprintln!("Device monitor error: {}", e);
        }
    });

    // React to changes
    loop {
        tokio::select! {
            _ = network_notify.notified() => {
                println!("\n--- Network list changed ---");
                print_networks(&nm).await;
            }
            _ = device_notify.notified() => {
                println!("\n--- Device state changed ---");
                print_status(&nm).await;
            }
        }
    }
}

async fn print_status(nm: &NetworkManager) {
    // Current connection
    match nm.current_ssid().await {
        Some(ssid) => println!("Connected to: {}", ssid),
        None => println!("Not connected to Wi-Fi"),
    }

    // Wi-Fi state
    if let Ok(state) = nm.wifi_state().await {
        println!("Wi-Fi enabled: {}", state.enabled);
    }

    // Devices
    if let Ok(devices) = nm.list_devices().await {
        println!("\nDevices:");
        for dev in &devices {
            println!("  {} — {} [{}]", dev.interface, dev.device_type, dev.state);
        }
    }

    println!();
}

async fn print_networks(nm: &NetworkManager) {
    if let Ok(networks) = nm.list_networks(None).await {
        println!("Visible networks ({}):", networks.len());
        for net in &networks {
            let security = if net.is_eap {
                "EAP"
            } else if net.is_psk {
                "PSK"
            } else {
                "Open"
            };
            println!(
                "  {:30} {:>3}%  {}",
                net.ssid,
                net.strength.unwrap_or(0),
                security,
            );
        }
    }
    println!();
}

Running

cargo run --example network_monitor

Sample Output

=== Network Monitor Dashboard ===

Connected to: HomeWiFi
Wi-Fi enabled: true
Wi-Fi hardware enabled: true

Devices:
  wlan0 — Wi-Fi [Activated]
  eth0 — Ethernet [Disconnected]
  lo — Loopback [Unmanaged]

--- Network list changed ---
Visible networks (5):
  HomeWiFi                        87%  PSK
  Neighbor5G                      42%  PSK
  CafeGuest                       31%  Open
  OfficeNet                       25%  EAP
  IoT_Network                     15%  PSK

--- Device state changed ---
Connected to: HomeWiFi
Wi-Fi enabled: true
Wi-Fi hardware enabled: true

Devices:
  wlan0 — Wi-Fi [Activated]
  eth0 — Ethernet [Activated]
  lo — Loopback [Unmanaged]

Enhancements

  • Debouncing: D-Bus signals can fire rapidly. Add a debounce timer to avoid refreshing too frequently.
  • Detailed view: Call show_details() on networks for channel, speed, and security info.
  • History: Keep a log of state transitions with timestamps.
  • Alerts: Trigger notifications when connection drops or a specific network appears.

Connection Manager

This example implements a basic connection manager that provides an interactive CLI for managing Wi-Fi, Ethernet, and VPN connections.

Features

  • List and scan networks
  • Connect and disconnect Wi-Fi
  • Manage VPN connections
  • List devices and saved profiles
  • Interactive menu-driven interface

Code

use nmrs::{NetworkManager, WifiSecurity, ConnectionError};
use std::io::{self, Write};

#[tokio::main]
async fn main() -> nmrs::Result<()> {
    let nm = NetworkManager::new().await?;

    loop {
        println!("\n=== nmrs Connection Manager ===");
        println!("1. Scan networks");
        println!("2. List visible networks");
        println!("3. Connect to Wi-Fi");
        println!("4. Disconnect Wi-Fi");
        println!("5. Current connection");
        println!("6. List devices");
        println!("7. List saved connections");
        println!("8. Forget a connection");
        println!("9. List VPN connections");
        println!("0. Exit");
        print!("\nChoice: ");
        io::stdout().flush().ok();

        let choice = read_line();
        match choice.trim() {
            "1" => scan(&nm).await,
            "2" => list_networks(&nm).await,
            "3" => connect_wifi(&nm).await,
            "4" => disconnect(&nm).await,
            "5" => current(&nm).await,
            "6" => devices(&nm).await,
            "7" => saved(&nm).await,
            "8" => forget(&nm).await,
            "9" => vpns(&nm).await,
            "0" => break,
            _ => println!("Invalid choice"),
        }
    }

    Ok(())
}

async fn scan(nm: &NetworkManager) {
    println!("Scanning...");
    match nm.scan_networks(None).await {
        Ok(_) => println!("Scan complete"),
        Err(e) => eprintln!("Scan failed: {}", e),
    }
}

async fn list_networks(nm: &NetworkManager) {
    match nm.list_networks(None).await {
        Ok(networks) => {
            println!("\n{:<5} {:<30} {:>6} {:>10}",
                "#", "SSID", "Signal", "Security");
            println!("{}", "-".repeat(55));
            for (i, net) in networks.iter().enumerate() {
                let sec = if net.is_eap { "EAP" }
                    else if net.is_psk { "PSK" }
                    else { "Open" };
                println!("{:<5} {:<30} {:>5}% {:>10}",
                    i + 1, net.ssid, net.strength.unwrap_or(0), sec);
            }
        }
        Err(e) => eprintln!("Error: {}", e),
    }
}

async fn connect_wifi(nm: &NetworkManager) {
    print!("SSID: ");
    io::stdout().flush().ok();
    let ssid = read_line();
    let ssid = ssid.trim();

    print!("Password (empty for open): ");
    io::stdout().flush().ok();
    let password = read_line();
    let password = password.trim();

    let security = if password.is_empty() {
        WifiSecurity::Open
    } else {
        WifiSecurity::WpaPsk { psk: password.into() }
    };

    println!("Connecting to '{}'...", ssid);
    match nm.connect(ssid, None, security).await {
        Ok(_) => println!("Connected!"),
        Err(ConnectionError::AuthFailed) => eprintln!("Wrong password"),
        Err(ConnectionError::NotFound) => eprintln!("Network not found"),
        Err(ConnectionError::Timeout) => eprintln!("Connection timed out"),
        Err(e) => eprintln!("Error: {}", e),
    }
}

async fn disconnect(nm: &NetworkManager) {
    match nm.disconnect(None).await {
        Ok(_) => println!("Disconnected"),
        Err(e) => eprintln!("Error: {}", e),
    }
}

async fn current(nm: &NetworkManager) {
    match nm.current_network().await {
        Ok(Some(net)) => {
            println!("Connected to: {} ({}%)",
                net.ssid, net.strength.unwrap_or(0));
        }
        Ok(None) => println!("Not connected"),
        Err(e) => eprintln!("Error: {}", e),
    }
}

async fn devices(nm: &NetworkManager) {
    match nm.list_devices().await {
        Ok(devices) => {
            for dev in &devices {
                println!("{:<10} {:<12} {:<15} {}",
                    dev.interface,
                    format!("{}", dev.device_type),
                    format!("{}", dev.state),
                    dev.identity.current_mac,
                );
            }
        }
        Err(e) => eprintln!("Error: {}", e),
    }
}

async fn saved(nm: &NetworkManager) {
    match nm.list_saved_connections().await {
        Ok(connections) => {
            for conn in &connections {
                println!("  {} ({})", conn.id, conn.connection_type);
            }
        }
        Err(e) => eprintln!("Error: {}", e),
    }
}

async fn forget(nm: &NetworkManager) {
    print!("Connection name to forget: ");
    io::stdout().flush().ok();
    let name = read_line();
    let name = name.trim();

    match nm.forget(name).await {
        Ok(_) => println!("Forgot '{}'", name),
        Err(e) => eprintln!("Error: {}", e),
    }
}

async fn vpns(nm: &NetworkManager) {
    match nm.list_vpn_connections().await {
        Ok(vpns) => {
            if vpns.is_empty() {
                println!("No VPN connections");
            }
            for vpn in &vpns {
                println!("  {} ({:?}) — {:?}",
                    vpn.name, vpn.vpn_type, vpn.state);
            }
        }
        Err(e) => eprintln!("Error: {}", e),
    }
}

fn read_line() -> String {
    let mut input = String::new();
    io::stdin().read_line(&mut input).unwrap_or_default();
    input
}

Running

cargo run --example connection_manager

Enhancements

  • VPN connect/disconnect: Add menu options for VPN operations
  • Bluetooth: Add Bluetooth device listing and connection
  • Network details: Show NetworkInfo for selected networks
  • Color output: Use a crate like colored for terminal formatting
  • Persistent config: Store preferred networks in a config file

Core Types

This page lists the primary types exported by nmrs. For complete API documentation, see docs.rs/nmrs.

NetworkManager

The main entry point for all operations.

#![allow(unused)]
fn main() {
use nmrs::NetworkManager;

let nm = NetworkManager::new().await?;
let nm = NetworkManager::with_config(config).await?;
}
  • Clone — clones share the same D-Bus 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)
AccessPointA single AP with BSSID, frequency, and security flags
WifiDeviceA Wi-Fi device with interface, MAC, state, and active SSID
WifiScopePer-interface operations scope (from nm.wifi("wlan1"))
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.

Radio / Airplane-Mode Types

TypeDescription
RadioStateCombined software (enabled) and hardware (hardware_enabled) radio state
AirplaneModeStateAggregated state across Wi-Fi, WWAN, and Bluetooth

VPN Types

TypeDescription
VpnConfigSealed trait for VPN configurations
VpnConfigurationDispatch enum: WireGuard(WireGuardConfig) or OpenVpn(OpenVpnConfig)
WireGuardConfigWireGuard VPN configuration
WireGuardPeerWireGuard peer configuration
OpenVpnConfigOpenVPN configuration
OpenVpnAuthTypeOpenVPN auth: Password, Tls, PasswordTls, StaticKey
OpenVpnCompressionCompression mode: No, Lz4, Lz4V2, Yes
OpenVpnProxyProxy: Http { ... }, Socks { ... }
VpnRouteStatic IPv4 route for split tunneling
VpnTypeProtocol-specific metadata (data-carrying enum)
VpnKindPlugin (OpenVPN, etc.) vs WireGuard
VpnConnectionA saved/active VPN connection with rich metadata
VpnConnectionInfoDetailed active VPN info (IP, DNS, gateway, protocol details)
VpnDetailsProtocol-specific active connection details
VpnCredentialsDeprecated — use WireGuardConfig instead

Connectivity Types

TypeDescription
ConnectivityStateNM connectivity: Full, Portal, Limited, None, Unknown
ConnectivityReportFull report with state, check URI, and captive portal URL

Saved Connection Types

TypeDescription
SavedConnectionFull decoded saved profile
SavedConnectionBriefLightweight profile (uuid, id, type)
SettingsSummaryDecoded settings within a profile
SettingsPatchPartial update for update_saved_connection

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
OpenVpnBuilderOpenVPN builder (also imports .ovpn files)
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, WifiScope,
    WifiSecurity, EapOptions, EapMethod, Phase2,
    WireGuardConfig, WireGuardPeer,
    OpenVpnConfig, OpenVpnAuthType,
    VpnConfig, VpnConfiguration, VpnType, VpnKind,
    TimeoutConfig, ConnectionOptions,
    ConnectionError, DeviceType, DeviceState,
    RadioState, AirplaneModeState,
    ConnectivityState, ConnectivityReport,
};
}

Less commonly used types are available through the models and builders modules:

#![allow(unused)]
fn main() {
use nmrs::models::{BluetoothIdentity, BluetoothNetworkRole, BluetoothDevice};
use nmrs::builders::{ConnectionBuilder, WireGuardBuilder, OpenVpnBuilder, IpConfig, Route};
}

NetworkManager API

The NetworkManager struct is the primary entry point for all nmrs operations. It manages a D-Bus connection to the NetworkManager daemon.

Construction

#![allow(unused)]
fn main() {
use nmrs::{NetworkManager, TimeoutConfig};
use std::time::Duration;

// Default timeouts (30s connect, 10s disconnect)
let nm = NetworkManager::new().await?;

// Custom timeouts
let config = TimeoutConfig::new()
    .with_connection_timeout(Duration::from_secs(60))
    .with_disconnect_timeout(Duration::from_secs(20));
let nm = NetworkManager::with_config(config).await?;

// Read current config
let config = nm.timeout_config();
}

Wi-Fi Methods

MethodReturnsDescription
scan_networks(interface)Result<()>Trigger active Wi-Fi scan (None = all devices)
list_networks(interface)Result<Vec<Network>>List visible networks (None = all devices)
list_access_points(interface)Result<Vec<AccessPoint>>List individual APs by BSSID
connect(ssid, interface, security)Result<()>Connect to a Wi-Fi network
connect_to_bssid(ssid, bssid, interface, security)Result<()>Connect to a specific AP
disconnect(interface)Result<()>Disconnect from current network (None = first Wi-Fi device)
current_network()Result<Option<Network>>Get current Wi-Fi network
current_ssid()Option<String>Get current SSID
current_connection_info()Option<(String, Option<u32>)>Get SSID + frequency
is_connected(ssid)Result<bool>Check if connected to a specific network
show_details(network)Result<NetworkInfo>Get detailed network info

Per-Device Wi-Fi Methods

MethodReturnsDescription
list_wifi_devices()Result<Vec<WifiDevice>>List all Wi-Fi devices
wifi_device_by_interface(name)Result<WifiDevice>Look up a Wi-Fi device by name
wifi(interface)WifiScopeBuild a scope pinned to one interface
set_wifi_enabled(interface, bool)Result<()>Enable/disable one Wi-Fi radio

Radio / Airplane-Mode Methods

MethodReturnsDescription
wifi_state()Result<RadioState>Software + hardware Wi-Fi state
wwan_state()Result<RadioState>Software + hardware WWAN state
bluetooth_radio_state()Result<RadioState>Software + hardware Bluetooth state
airplane_mode_state()Result<AirplaneModeState>Aggregated across all radios
set_wireless_enabled(bool)Result<()>Global Wi-Fi software toggle
set_wwan_enabled(bool)Result<()>Global WWAN toggle
set_bluetooth_radio_enabled(bool)Result<()>Toggle all BlueZ adapters
set_airplane_mode(bool)Result<()>Toggle all three radios
wait_for_wifi_ready()Result<()>Wait for Wi-Fi device to become ready

Ethernet Methods

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

VPN Methods

MethodReturnsDescription
connect_vpn(config)Result<()>Connect with a WireGuardConfig or OpenVpnConfig
import_ovpn(path, user, pass)Result<()>Import .ovpn file and connect
connect_vpn_by_uuid(uuid)Result<()>Activate a saved VPN by UUID
connect_vpn_by_id(id)Result<()>Activate a saved VPN by display name
disconnect_vpn(name)Result<()>Disconnect a VPN by name
disconnect_vpn_by_uuid(uuid)Result<()>Disconnect a VPN by UUID
list_vpn_connections()Result<Vec<VpnConnection>>List all saved VPNs
active_vpn_connections()Result<Vec<VpnConnection>>List only active VPNs
forget_vpn(name)Result<()>Delete a saved VPN profile
get_vpn_info(name)Result<VpnConnectionInfo>Get active VPN details

Connectivity Methods

MethodReturnsDescription
connectivity()Result<ConnectivityState>Current NM connectivity state
check_connectivity()Result<ConnectivityState>Force a connectivity re-check
connectivity_report()Result<ConnectivityReport>Full report with captive portal URL
captive_portal_url()Result<Option<String>>Captive portal URL if in Portal state

Bluetooth Methods

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

Connection Profile Methods

MethodReturnsDescription
list_saved_connections()Result<Vec<SavedConnection>>Full decode of all saved profiles
list_saved_connections_brief()Result<Vec<SavedConnectionBrief>>Lightweight profile listing
list_saved_connection_ids()Result<Vec<String>>Just the profile names
get_saved_connection(uuid)Result<SavedConnection>Load one profile by UUID
get_saved_connection_raw(uuid)Result<HashMap<...>>Raw GetSettings map
delete_saved_connection(uuid)Result<()>Delete a profile by UUID
update_saved_connection(uuid, patch)Result<()>Merge a SettingsPatch into a profile
reload_saved_connections()Result<()>Re-read profiles from disk
has_saved_connection(ssid)Result<bool>Check if a Wi-Fi profile exists
get_saved_connection_path(ssid)Result<Option<OwnedObjectPath>>Get profile D-Bus path
forget(ssid)Result<()>Delete a Wi-Fi profile

Monitoring Methods

MethodReturnsDescription
monitor_network_changes(callback)Result<()>Watch for AP and signal strength changes (runs forever)
monitor_device_changes(callback)Result<()>Watch for device state changes (runs forever)

Thread Safety

NetworkManager is Clone, Send, and Sync. Clones share the same D-Bus connection.

Important: Concurrent connection operations (calling connect() from multiple tasks) are not supported. Use is_connecting() to guard against this.

Full API Reference

For complete documentation with all method signatures, see docs.rs/nmrs.

Models Module

The models module contains all data types used by nmrs. These are re-exported at the crate root and through nmrs::models.

Device Models

Device

Represents a network device managed by NetworkManager.

#![allow(unused)]
fn main() {
pub struct Device {
    pub path: String,           // D-Bus object path
    pub interface: String,      // e.g., "wlan0", "eth0"
    pub identity: DeviceIdentity,
    pub device_type: DeviceType,
    pub state: DeviceState,
    pub managed: Option<bool>,
    pub driver: Option<String>,
    pub ip4_address: Option<String>,
    pub ip6_address: Option<String>,
}
}

Methods: is_wireless(), is_wired(), is_bluetooth()

DeviceIdentity

#![allow(unused)]
fn main() {
pub struct DeviceIdentity {
    pub permanent_mac: String,
    pub current_mac: String,
}
}

DeviceType

#![allow(unused)]
fn main() {
pub enum DeviceType {
    Ethernet,
    Wifi,
    WifiP2P,
    Loopback,
    Bluetooth,
    Other(u32),
}
}

Methods: supports_scanning(), requires_specific_object(), has_global_enabled_state(), connection_type_str(), to_code()

DeviceState

#![allow(unused)]
fn main() {
pub enum DeviceState {
    Unmanaged, Unavailable, Disconnected,
    Prepare, Config, NeedAuth, IpConfig, IpCheck, Secondaries,
    Activated, Deactivating, Failed,
    Other(u32),
}
}

Methods: is_transitional()

Wi-Fi Models

Network

A discovered Wi-Fi network.

#![allow(unused)]
fn main() {
pub struct Network {
    pub device: String,
    pub ssid: String,
    pub bssid: Option<String>,
    pub strength: Option<u8>,
    pub frequency: Option<u32>,
    pub secured: bool,
    pub is_psk: bool,
    pub is_eap: bool,
    pub ip4_address: Option<String>,
    pub ip6_address: Option<String>,
}
}

AccessPoint

A single AP with per-BSSID details.

#![allow(unused)]
fn main() {
pub struct AccessPoint {
    pub ssid: String,
    pub bssid: String,
    pub strength: u8,
    pub frequency_mhz: u32,
    pub device: String,
    // ... security flags and device state
}
}

WifiDevice

A Wi-Fi device discovered by list_wifi_devices().

#![allow(unused)]
fn main() {
pub struct WifiDevice {
    pub interface: String,
    pub mac: String,
    pub state: DeviceState,
    pub active_ssid: Option<String>,
}
}

WifiScope

Per-interface operations scope returned by nm.wifi("wlan1").

Methods: interface(), scan(), list_networks(), list_access_points(), connect(ssid, creds), connect_to_bssid(ssid, bssid, creds), disconnect(), set_enabled(bool), forget(ssid)

NetworkInfo

Detailed network information from show_details().

#![allow(unused)]
fn main() {
pub struct NetworkInfo {
    pub ssid: String,
    pub bssid: String,
    pub strength: u8,
    pub freq: Option<u32>,
    pub channel: Option<u16>,
    pub mode: String,
    pub rate_mbps: Option<u32>,
    pub bars: String,         // e.g., "▂▄▆█"
    pub security: String,
    pub status: String,
    pub ip4_address: Option<String>,
    pub ip6_address: Option<String>,
}
}

WifiSecurity

#![allow(unused)]
fn main() {
pub enum WifiSecurity {
    Open,
    WpaPsk { psk: String },
    WpaEap { opts: EapOptions },
}
}

Methods: secured(), is_psk(), is_eap()

EapOptions

Enterprise Wi-Fi configuration.

#![allow(unused)]
fn main() {
pub struct EapOptions {
    pub identity: String,
    pub password: String,
    pub anonymous_identity: Option<String>,
    pub domain_suffix_match: Option<String>,
    pub ca_cert_path: Option<String>,
    pub system_ca_certs: bool,
    pub method: EapMethod,
    pub phase2: Phase2,
}
}

Constructors: new(identity, password), builder()

EapMethod / Phase2

#![allow(unused)]
fn main() {
pub enum EapMethod { Peap, Ttls }
pub enum Phase2 { Mschapv2, Pap }
}

Radio / Airplane-Mode Models

RadioState

#![allow(unused)]
fn main() {
pub struct RadioState {
    pub enabled: bool,           // software toggle
    pub hardware_enabled: bool,  // rfkill state
}
}

AirplaneModeState

Aggregated state across Wi-Fi, WWAN, and Bluetooth radios. Methods: is_airplane_mode().

VPN Models

WireGuardConfig

WireGuard VPN configuration (replaces the deprecated VpnCredentials).

#![allow(unused)]
fn main() {
pub struct WireGuardConfig {
    pub name: String,
    pub gateway: String,
    pub private_key: String,
    pub address: String,
    pub peers: Vec<WireGuardPeer>,
    pub dns: Option<Vec<String>>,
    pub mtu: Option<u32>,
    pub uuid: Option<Uuid>,
}
}

Constructor: new(name, gateway, private_key, address, peers) Builder methods: .with_dns(vec), .with_mtu(u32), .with_uuid(uuid)

OpenVpnConfig

OpenVPN configuration.

#![allow(unused)]
fn main() {
pub struct OpenVpnConfig {
    pub name: String,
    pub remote: String,
    pub port: u16,
    pub tcp: bool,
    pub auth_type: Option<OpenVpnAuthType>,
    pub ca_cert: Option<String>,
    pub client_cert: Option<String>,
    pub client_key: Option<String>,
    pub username: Option<String>,
    pub password: Option<String>,
    pub compression: Option<OpenVpnCompression>,
    pub proxy: Option<OpenVpnProxy>,
    // ... many more TLS and routing fields
}
}

Constructor: new(name, remote, port, tcp) Builder methods: .with_auth_type(), .with_username(), .with_password(), .with_ca_cert(), .with_client_cert(), .with_client_key(), .with_dns(), .with_mtu(), .with_compression(), .with_proxy(), .with_tls_auth(), .with_tls_crypt(), .with_redirect_gateway(), .with_routes(), and many more.

OpenVpnAuthType

#![allow(unused)]
fn main() {
pub enum OpenVpnAuthType { Password, Tls, PasswordTls, StaticKey }
}

OpenVpnCompression

#![allow(unused)]
fn main() {
pub enum OpenVpnCompression { No, Lzo, Lz4, Lz4V2, Yes }
}

OpenVpnProxy

#![allow(unused)]
fn main() {
pub enum OpenVpnProxy {
    Http { server, port, username, password, retry },
    Socks { server, port, retry },
}
}

VpnRoute

#![allow(unused)]
fn main() {
pub struct VpnRoute {
    pub dest: String,
    pub prefix: u32,
    pub next_hop: Option<String>,
    pub metric: Option<u32>,
}
}

Constructor: new(dest, prefix). Builder methods: .next_hop(gw), .metric(m).

WireGuardPeer

#![allow(unused)]
fn main() {
pub struct WireGuardPeer {
    pub public_key: String,
    pub gateway: String,
    pub allowed_ips: Vec<String>,
    pub preshared_key: Option<String>,
    pub persistent_keepalive: Option<u32>,
}
}

VpnType

Protocol-specific metadata decoded from NM settings (data-carrying enum):

#![allow(unused)]
fn main() {
pub enum VpnType {
    WireGuard { private_key, peer_public_key, endpoint, allowed_ips, ... },
    OpenVpn { remote, connection_type, user_name, ca, cert, key, ... },
    OpenConnect { gateway, user_name, protocol, ... },
    StrongSwan { address, method, user_name, ... },
    Pptp { gateway, user_name, ... },
    L2tp { gateway, user_name, ipsec_enabled, ... },
    Generic { service_type, data, secrets, ... },
}
}

VpnKind

#![allow(unused)]
fn main() {
pub enum VpnKind { Plugin, WireGuard }
}

VpnConnection

A saved or active VPN connection with rich metadata.

#![allow(unused)]
fn main() {
pub struct VpnConnection {
    pub uuid: String,
    pub id: String,
    pub name: String,
    pub vpn_type: VpnType,
    pub state: DeviceState,
    pub interface: Option<String>,
    pub active: bool,
    pub user_name: Option<String>,
    pub password_flags: VpnSecretFlags,
    pub service_type: String,
    pub kind: VpnKind,
}
}

VpnConnectionInfo

Detailed active VPN information.

#![allow(unused)]
fn main() {
pub struct VpnConnectionInfo {
    pub name: String,
    pub vpn_kind: VpnKind,
    pub state: DeviceState,
    pub interface: Option<String>,
    pub gateway: Option<String>,
    pub ip4_address: Option<String>,
    pub ip6_address: Option<String>,
    pub dns_servers: Vec<String>,
    pub details: Option<VpnDetails>,
}
}

VpnDetails

#![allow(unused)]
fn main() {
pub enum VpnDetails {
    WireGuard { public_key, endpoint },
    OpenVpn { remote, port, protocol, cipher, auth, compression },
}
}

Saved Connection Models

SavedConnection

Full decoded saved profile from list_saved_connections().

Fields: uuid, id, connection_type, interface_name, autoconnect, timestamp, settings.

SavedConnectionBrief

Lightweight: uuid, id, connection_type.

SettingsPatch

Partial update for update_saved_connection.

Connectivity Models

ConnectivityState

#![allow(unused)]
fn main() {
pub enum ConnectivityState { Unknown, None, Portal, Limited, Full }
}

ConnectivityReport

#![allow(unused)]
fn main() {
pub struct ConnectivityReport {
    pub state: ConnectivityState,
    pub check_enabled: bool,
    pub check_uri: Option<String>,
    pub captive_portal_url: Option<String>,
}
}

Bluetooth Models

BluetoothDevice

#![allow(unused)]
fn main() {
pub struct BluetoothDevice {
    pub bdaddr: String,
    pub name: Option<String>,
    pub alias: Option<String>,
    pub bt_caps: u32,
    pub state: DeviceState,
}
}

BluetoothIdentity

#![allow(unused)]
fn main() {
pub struct BluetoothIdentity {
    pub bdaddr: String,
    pub bt_device_type: BluetoothNetworkRole,
}
}

BluetoothNetworkRole

#![allow(unused)]
fn main() {
pub enum BluetoothNetworkRole { PanU, Dun }
}

Configuration Models

TimeoutConfig

#![allow(unused)]
fn main() {
pub struct TimeoutConfig {
    pub connection_timeout: Duration,  // default: 30s
    pub disconnect_timeout: Duration,  // default: 10s
}
}

ConnectionOptions

#![allow(unused)]
fn main() {
pub struct ConnectionOptions {
    pub autoconnect: bool,
    pub autoconnect_priority: Option<i32>,
    pub autoconnect_retries: Option<i32>,
}
}

Non-Exhaustive Types

All enums and structs in nmrs are marked #[non_exhaustive]. Always include a wildcard arm in match expressions and don't construct structs directly (use constructors/builders).

Full API Reference

For complete documentation with all method signatures and trait implementations, see docs.rs/nmrs.

Builders Module

The builders module provides low-level APIs for constructing NetworkManager connection settings. Most users should use the high-level NetworkManager API instead — these builders are for advanced use cases where you need fine-grained control.

ConnectionBuilder

The base builder for all connection types. Handles common sections: connection, ipv4, ipv6.

#![allow(unused)]
fn main() {
use nmrs::builders::ConnectionBuilder;

let settings = ConnectionBuilder::new("802-3-ethernet", "MyConnection")
    .autoconnect(true)
    .autoconnect_priority(10)
    .ipv4_auto()
    .ipv6_auto()
    .build();
}

Methods

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};
use nmrs::{WifiSecurity, ConnectionOptions};

// Wi-Fi
let wifi = build_wifi_connection("MyNetwork", &WifiSecurity::Open, &ConnectionOptions::default());

// Ethernet
let eth = build_ethernet_connection("eth0", &ConnectionOptions::default());
}

When to Use Builders

Use the builders when you need:

  • Custom IP configuration (static IP, DNS, routes)
  • Specific Wi-Fi band or mode settings
  • Custom connection sections (bridge, bond, VLAN)
  • Fine-grained control over the settings dictionary

For standard connections, the NetworkManager API handles everything automatically.

OpenVpnBuilder

Builds OpenVPN connection settings from an OpenVpnConfig or by importing a .ovpn file.

From Configuration

#![allow(unused)]
fn main() {
use nmrs::{OpenVpnConfig, OpenVpnAuthType};

let config = OpenVpnConfig::new("CorpVPN", "vpn.example.com", 1194, false)
    .with_auth_type(OpenVpnAuthType::PasswordTls)
    .with_username("user")
    .with_password("secret")
    .with_ca_cert("/etc/openvpn/ca.crt")
    .with_client_cert("/etc/openvpn/client.crt")
    .with_client_key("/etc/openvpn/client.key");
}

From .ovpn File

#![allow(unused)]
fn main() {
use nmrs::builders::OpenVpnBuilder;

let config = OpenVpnBuilder::from_ovpn_file("client.ovpn")?
    .username("user")
    .password("secret")
    .build()?;
}

Or use the high-level API directly:

#![allow(unused)]
fn main() {
let nm = NetworkManager::new().await?;
nm.import_ovpn("client.ovpn", Some("user"), Some("secret")).await?;
}

Full API Reference

See docs.rs/nmrs for complete builder documentation.

Error Types

nmrs uses a single error enum, ConnectionError, for all operations. It implements std::error::Error, Display, and Debug.

ConnectionError

#![allow(unused)]
fn main() {
#[non_exhaustive]
pub enum ConnectionError {
    // D-Bus errors
    Dbus(zbus::Error),
    DbusOperation { context: String, source: zbus::Error },

    // Network not found
    NotFound,

    // Authentication
    AuthFailed,
    MissingPassword,
    SupplicantConfigFailed,
    SupplicantTimeout,

    // Connection lifecycle
    DhcpFailed,
    Timeout,
    Stuck(String),

    // Device errors
    NoWifiDevice,
    NoWiredDevice,
    WifiNotReady,
    NoBluetoothDevice,
    NoSavedConnection,

    // Device/activation failures with reason codes
    DeviceFailed(StateReason),
    ActivationFailed(ConnectionStateReason),

    // Per-device errors
    WifiInterfaceNotFound { interface: String },
    NotAWifiDevice { interface: String },

    // Radio errors
    HardwareRadioKilled,
    BluezUnavailable,

    // VPN errors
    NoVpnConnection,
    VpnFailed(String),
    VpnIdAmbiguous { id: String },
    IncompleteBuilder(String),
    InvalidPrivateKey(String),
    InvalidPublicKey(String),
    InvalidAddress(String),
    InvalidGateway(String),
    InvalidPeers(String),

    // Connectivity
    ConnectivityCheckDisabled,

    // BSSID
    ApBssidNotFound { ssid: String, bssid: String },
    InvalidBssid(String),

    // Other
    InvalidUtf8(Utf8Error),
}
}

Error Categories

User-Facing Errors

These indicate issues the user can fix:

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", None, WifiSecurity::Open).await?;
    Ok(())
}
}

Specific Error Handling

#![allow(unused)]
fn main() {
match nm.connect("MyWiFi", None, security).await {
    Ok(_) => println!("Connected"),
    Err(ConnectionError::AuthFailed) => eprintln!("Wrong password"),
    Err(ConnectionError::NotFound) => eprintln!("Network not found"),
    Err(e) => eprintln!("Error: {}", e),
}
}

With anyhow

#![allow(unused)]
fn main() {
use anyhow::{Context, Result};

async fn connect() -> Result<()> {
    let nm = NetworkManager::new().await
        .context("Failed to connect to NetworkManager")?;
    nm.connect("MyWiFi", None, WifiSecurity::Open).await
        .context("Failed to connect to MyWiFi")?;
    Ok(())
}
}

Full API Reference

See docs.rs/nmrs for complete error documentation.

Contributing

Thank you for wanting to contribute to nmrs!

Guidelines

I'm fairly accepting to all PRs, only with a couple caveats:

  • Do not submit low-effort or autogenerated code. If you absolutely must, please disclose how you used AI otherwise I will close the PR.
  • Please try to (when possible) contribute to an issue. This is not a hard ask, I'll still consider your contribution if it makes sense.

Requirements

To run or develop nmrs you need:

  • Rust (stable) via rustup
  • A running 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

If you decide to run the shell, ensure you run all commands from within the nmrs directory, not root.

cargo test -p nmrs           # run library tests
cargo build -p nmrs          # build the library
cargo check                  # you get the point...

When your branch falls behind master

If the respective branch for a PR goes out of sync, I prefer you rebase. I've exposed this setting for you to automatically do so as a contributor on any PR you open.

Issues and Commit Message Hygiene

When you've made changes and are ready to commit, I prefer that you follow the standards explained at Conventional Commits.

I additionally request that you format your commits as such:
type((some issue number)): changes made

For example:

fix(#24): fixed bug where something was happening

Obviously, if there is no issue number to attach, no need to add anything there.

Lastly, please ensure you make atomic commits.

All issues are acceptable. If a situation arises where a request or concern is not valid, I will respond directly to the issue.

Tests

All tests must pass before a merge takes place.

Ensure NetworkManager is running

sudo systemctl start NetworkManager

Test everything (unit + integration)

cargo test --all-features

Integration tests

These require WiFi hardware. Please make sure you run this locally before your PR to ensure everything works.

cargo test --test integration_test --all-features

If you do not have access to WiFi hardware (for whatever odd reason that is), you can do something like this:

sudo modprobe mac80211_hwsim radios=2
cargo test --test integration_test --all-features
sudo modprobe -r mac80211_hwsim

Note: This method only works on Linux

Documentation

When adding new features or changing existing APIs:

  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.

Versioning

nmrs follows Semantic Versioning:

  • Major (X.0.0) — breaking API changes
  • Minor (0.X.0) — new features, backward-compatible
  • Patch (0.0.X) — bug fixes, backward-compatible

Changelog

See nmrs/CHANGELOG.md for the full changelog.

Release Checklist

  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

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(None).await?
  • Check if Wi-Fi is enabled: nm.wifi_state().await? returns a RadioState with .enabled and .hardware_enabled fields
  • 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

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?

WireGuard and OpenVPN. WireGuard uses NetworkManager's native kernel integration (no plugin needed). OpenVPN requires the networkmanager-openvpn plugin.

Do I need the WireGuard kernel module?

Yes. WireGuard is built into the Linux kernel since version 5.6. On older kernels, install the wireguard module. NetworkManager's WireGuard support requires NM 1.16+.

Can I import a .ovpn file?

Yes. Use nm.import_ovpn("client.ovpn", Some("user"), Some("pass")).await? to parse and activate an OpenVPN profile in one call. Inline certificates are extracted and persisted automatically.

Can I import a .conf WireGuard file?

Not directly. Extract the values from the config file and pass them to WireGuardConfig::new().

Troubleshooting

Where can I get help?

Changelog

See the full changelog on GitHub: nmrs CHANGELOG

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

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.