From 27a1ccf23c848b1cb1a0df9b88605508ef2f1f9c Mon Sep 17 00:00:00 2001 From: Akrm Al-Hakimi Date: Sat, 9 May 2026 09:29:35 -0400 Subject: [PATCH] chore(#357): update docs --- docs/book.toml | 2 +- docs/src/advanced/async-runtimes.md | 6 +- docs/src/advanced/logging.md | 2 +- docs/src/api/errors.md | 74 +++++++--- docs/src/api/models.md | 35 +++-- docs/src/api/types.md | 2 +- docs/src/appendix/changelog.md | 72 +++++++--- docs/src/appendix/faq.md | 5 +- docs/src/examples/wifi-scanner.md | 2 +- docs/src/getting-started/installation.md | 8 +- docs/src/getting-started/quick-start.md | 100 ++++++++------ docs/src/getting-started/requirements.md | 5 +- docs/src/guide/devices.md | 9 ++ docs/src/guide/error-handling.md | 43 +++++- docs/src/guide/profiles.md | 66 ++++++++- docs/src/guide/vpn.md | 4 +- docs/src/guide/wifi.md | 78 +++++++---- nmrs/src/api/builders/mod.rs | 112 ++++++++------- nmrs/src/api/builders/vpn.rs | 168 ++++++++++------------- nmrs/src/api/models/error.rs | 22 +-- nmrs/src/lib.rs | 160 ++++++++++++++------- 21 files changed, 624 insertions(+), 351 deletions(-) diff --git a/docs/book.toml b/docs/book.toml index 2391d774..e8395bb1 100644 --- a/docs/book.toml +++ b/docs/book.toml @@ -6,7 +6,7 @@ language = "en" src = "src" [rust] -edition = "2021" +edition = "2024" [build] build-dir = "book" diff --git a/docs/src/advanced/async-runtimes.md b/docs/src/advanced/async-runtimes.md index 71f755ec..e7c6dba4 100644 --- a/docs/src/advanced/async-runtimes.md +++ b/docs/src/advanced/async-runtimes.md @@ -22,7 +22,7 @@ Add to your `Cargo.toml`: ```toml [dependencies] -nmrs = "2.2" +nmrs = "3.1" tokio = { version = "1", features = ["macros", "rt-multi-thread"] } ``` @@ -55,7 +55,7 @@ async fn main() -> nmrs::Result<()> { ```toml [dependencies] -nmrs = "2.2" +nmrs = "3.1" async-std = { version = "1", features = ["attributes"] } ``` @@ -76,7 +76,7 @@ fn main() -> nmrs::Result<()> { ```toml [dependencies] -nmrs = "2.2" +nmrs = "3.1" smol = "2" ``` diff --git a/docs/src/advanced/logging.md b/docs/src/advanced/logging.md index d360f38e..6490a6dc 100644 --- a/docs/src/advanced/logging.md +++ b/docs/src/advanced/logging.md @@ -8,7 +8,7 @@ nmrs produces log messages but doesn't configure a logger — that's up to your ```toml [dependencies] -nmrs = "2.2" +nmrs = "3.1" env_logger = "0.11" log = "0.4" ``` diff --git a/docs/src/api/errors.md b/docs/src/api/errors.md index 49e1c9b5..83cb6d7d 100644 --- a/docs/src/api/errors.md +++ b/docs/src/api/errors.md @@ -1,18 +1,23 @@ # Error Types -nmrs uses a single error enum, `ConnectionError`, for all operations. It implements `std::error::Error`, `Display`, and `Debug`. +nmrs uses a single error enum, `ConnectionError`, for all operations. It +implements `std::error::Error`, `Display`, and `Debug`, and is also +re-exported as the source type for the [`nmrs::Result`](./types.md#result-type) +alias. ## ConnectionError ```rust #[non_exhaustive] pub enum ConnectionError { - // D-Bus errors + // D-Bus Dbus(zbus::Error), DbusOperation { context: String, source: zbus::Error }, - // Network not found + // Network discovery NotFound, + ApBssidNotFound { ssid: String, bssid: String }, + InvalidBssid(String), // Authentication AuthFailed, @@ -24,49 +29,62 @@ pub enum ConnectionError { DhcpFailed, Timeout, Stuck(String), + DeviceFailed(StateReason), + ActivationFailed(ConnectionStateReason), - // Device errors + // Devices 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 + // Radios / airplane mode HardwareRadioKilled, - BluezUnavailable, + BluezUnavailable(String), + BluetoothToggleFailed(String), - // VPN errors + // Saved profiles + NoSavedConnection, + SavedConnectionNotFound(String), + MalformedSavedConnection(String), + IncompleteBuilder(String), + + // VPN NoVpnConnection, + VpnNotFound(String), + VpnIdAmbiguous(String), VpnFailed(String), - VpnIdAmbiguous { id: String }, - IncompleteBuilder(String), InvalidPrivateKey(String), InvalidPublicKey(String), InvalidAddress(String), InvalidGateway(String), InvalidPeers(String), + ParseError(OvpnParseError), + + // VLAN + InvalidVlanId { id: u16 }, + + // Generic input validation + InvalidInput { field: String, reason: String }, // Connectivity ConnectivityCheckDisabled, - // BSSID - ApBssidNotFound { ssid: String, bssid: String }, - InvalidBssid(String), + // Secret agent + AgentRegistration { context: String }, + AgentNotRegistered, + AgentAlreadyRegistered, - // Other - InvalidUtf8(Utf8Error), + // Encoding + InvalidUtf8(std::str::Utf8Error), } ``` +> `ConnectionError` is `#[non_exhaustive]`, so always include a wildcard +> arm in `match` expressions. + ## Error Categories ### User-Facing Errors @@ -109,6 +127,20 @@ These indicate infrastructure issues: | `Stuck` | NetworkManager in unexpected state | | `DeviceFailed` | Check the `StateReason` for details | | `ActivationFailed` | Check the `ConnectionStateReason` for details | +| `BluezUnavailable` | BlueZ not running or no Bluetooth adapters present | +| `BluetoothToggleFailed` | Adapter exists but failed to power on/off | +| `MalformedSavedConnection` | Saved profile is missing required keys; consider deleting it | +| `AgentRegistration` | Secret agent failed to register; check `context` | + +### Secret Agent Errors + +These come from the [`agent`](../../agent/index.html) module: + +| Error | Meaning | +|-------|---------| +| `AgentRegistration { context }` | `register()` failed (e.g. NetworkManager not reachable) | +| `AgentNotRegistered` | Tried to use a handle whose registration was already torn down | +| `AgentAlreadyRegistered` | Another `nmrs` agent in the process is using the same identifier | ## StateReason diff --git a/docs/src/api/models.md b/docs/src/api/models.md index 1bca2e16..e2128a6f 100644 --- a/docs/src/api/models.md +++ b/docs/src/api/models.md @@ -22,7 +22,7 @@ pub struct Device { } ``` -Methods: `is_wireless()`, `is_wired()`, `is_bluetooth()` +Methods: `is_wireless()`, `is_wired()`, `is_bluetooth()`, `is_loopback()`, `is_vlan()` ### DeviceIdentity @@ -42,6 +42,7 @@ pub enum DeviceType { WifiP2P, Loopback, Bluetooth, + Vlan, Other(u32), } ``` @@ -65,20 +66,28 @@ Methods: `is_transitional()` ### Network -A discovered Wi-Fi network. +A discovered Wi-Fi network. Networks sharing an SSID on the same device +are grouped, keeping the strongest AP as the representative; the merged +peers are still recorded in `bssids`. ```rust pub struct Network { pub device: String, pub ssid: String, - pub bssid: Option, - pub strength: Option, - pub frequency: Option, + pub bssid: Option, // best BSSID (strongest AP) + pub strength: Option, // 0..=100 + pub frequency: Option, // MHz pub secured: bool, pub is_psk: bool, pub is_eap: bool, - pub ip4_address: Option, + pub is_hotspot: bool, + pub ip4_address: Option, // populated only when this is the active network pub ip6_address: Option, + pub best_bssid: String, // mirror of `bssid` for the strongest AP + pub bssids: Vec, // every BSSID seen for this SSID, strongest first + pub is_active: bool, // true if currently connected + pub known: bool, // true if a saved profile exists for this SSID + pub security_features: SecurityFeatures, // decoded security flag triplet } ``` @@ -103,9 +112,15 @@ A Wi-Fi device discovered by `list_wifi_devices()`. ```rust pub struct WifiDevice { - pub interface: String, - pub mac: String, + pub path: OwnedObjectPath, + pub interface: String, // e.g. "wlan0" + pub hw_address: String, // current MAC (may be randomized) + pub permanent_hw_address: Option, + pub driver: Option, pub state: DeviceState, + pub managed: bool, + pub autoconnect: bool, + pub is_active: bool, pub active_ssid: Option, } ``` @@ -407,9 +422,13 @@ pub struct BluetoothDevice { pub struct BluetoothIdentity { pub bdaddr: String, pub bt_device_type: BluetoothNetworkRole, + pub adapter: Option, // e.g. Some("hci1"); defaults to "hci0" } ``` +Constructors: `new(bdaddr, role)` and `with_adapter(bdaddr, role, adapter)`. +Both validate the MAC and return [`ConnectionError`] on bad input. + ### BluetoothNetworkRole ```rust diff --git a/docs/src/api/types.md b/docs/src/api/types.md index cec8d917..f17b61ac 100644 --- a/docs/src/api/types.md +++ b/docs/src/api/types.md @@ -66,7 +66,7 @@ All public methods return `nmrs::Result`. | `WireGuardPeer` | WireGuard peer configuration | | `OpenVpnConfig` | OpenVPN configuration | | `OpenVpnAuthType` | OpenVPN auth: `Password`, `Tls`, `PasswordTls`, `StaticKey` | -| `OpenVpnCompression` | Compression mode: `No`, `Lz4`, `Lz4V2`, `Yes` | +| `OpenVpnCompression` | Compression mode: `No`, `Lzo` (deprecated), `Lz4`, `Lz4V2`, `Yes` | | `OpenVpnProxy` | Proxy: `Http { ... }`, `Socks { ... }` | | `VpnRoute` | Static IPv4 route for split tunneling | | `VpnType` | Protocol-specific metadata (data-carrying enum) | diff --git a/docs/src/appendix/changelog.md b/docs/src/appendix/changelog.md index df6550e7..8b26834e 100644 --- a/docs/src/appendix/changelog.md +++ b/docs/src/appendix/changelog.md @@ -4,32 +4,62 @@ See the full changelog on GitHub: [**nmrs** CHANGELOG](https://github.com/cacheb ## nmrs (Library) Highlights -### 2.2.0 +### 3.1.0 -- Concurrency protection — `is_connecting()` API -- `WirelessHardwareEnabled` property support -- BDADDR to BlueZ path resolution -- Mixed WPA1+WPA2 network support +- Loopback device support and a new `DeviceType::Vlan` variant +- VLAN (802.1Q) support: `VlanConfig` model and `build_vlan_connection` +- `RadioState::present` reports whether a radio actually exists on the host +- `airplane_mode_state()` and `set_airplane_mode()` are now correct on + Wi-Fi-only / Bluetooth-less hosts (BlueZ-missing is treated as a no-op) +- `set_bluetooth_radio_enabled` now waits for the adapter `Powered` + property to flip before returning -### 2.1.0 +### 3.0.x -- `#[must_use]` annotations on public builder APIs +- `nmrs::agent` module — register a NetworkManager **secret agent** + (`SecretAgent`, `SecretAgentBuilder`, `SecretRequest`, `SecretResponder`, + …) for Wi-Fi/VPN/802.1X credential prompts over D-Bus +- Per-Wi-Fi-device scoping: `WifiDevice`, `list_wifi_devices()`, + `wifi_device_by_interface()`, and `nm.wifi("wlan1")` → `WifiScope` +- New `interface: Option<&str>` parameter on `connect`, `connect_to_bssid`, + `disconnect`, `scan_networks`, and `list_networks` (**3.0 break**) +- `set_wifi_enabled(interface, bool)` now toggles a single radio; + `set_wireless_enabled(bool)` is the global software killswitch +- Airplane-mode surface: `RadioState`, `AirplaneModeState`, `wifi_state`, + `wwan_state`, `bluetooth_radio_state`, plus rfkill awareness +- Connectivity surface: `connectivity()`, `check_connectivity()`, + `connectivity_report()`, `captive_portal_url()`, `ConnectivityCheckDisabled` +- Generic VPN model: `VpnType` is now data-carrying (WireGuard, OpenVPN, + OpenConnect, strongSwan, PPTP, L2TP, Generic). `VpnKind` distinguishes + plugin VPNs from kernel WireGuard. `VpnConnection` gained `uuid`, + `active`, `user_name`, `password_flags`, `service_type`. New + `connect_vpn_by_uuid`, `connect_vpn_by_id`, `disconnect_vpn_by_uuid`, + `active_vpn_connections`. +- OpenVPN end-to-end: full `OpenVpnConfig` with TLS hardening, ciphers, + proxy, routes, `redirect_gateway`; `OpenVpnBuilder`; `.ovpn` import + via `nm.import_ovpn(path, user, pass)`; cert-store handling for inline + certs. +- Saved profile management: `list_saved_connections{,_brief,_ids}`, + `get_saved_connection{,_raw}`, `delete_saved_connection`, + `update_saved_connection`, `reload_saved_connections`, + `SavedConnection`, `SavedConnectionBrief`, `SettingsSummary`, + `SettingsPatch`. +- `AccessPoint` model + `list_access_points(interface)` for per-BSSID + enumeration. +- `ConnectionError::IncompleteBuilder` for builders missing required + fields; `Builder::build()` returns `Result` instead of panicking. +- Edition 2024, MSRV bumped to 1.90.0 (3.0.1). -### 2.0.1 +### 2.x -- 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 +- Concurrency protection (`is_connecting()`), `WirelessHardwareEnabled`, + BDADDR → BlueZ path resolution, mixed-mode WPA1+WPA2 (2.2.0) +- `#[must_use]` annotations on public builder APIs (2.1.0) +- IPv6 address support, `WifiMode` builder, input validation for SSIDs/ + credentials/addresses, idempotent `forget_vpn()` (2.0.1) +- Bluetooth support (PAN and DUN), configurable `TimeoutConfig`, + `VpnCredentials` / `EapOptions` builder patterns, `ConnectionOptions`, + `ConnectionBuilder`, `WireGuardBuilder` with validation (2.0.0) ### 1.x diff --git a/docs/src/appendix/faq.md b/docs/src/appendix/faq.md index 689e4eeb..614cf9d8 100644 --- a/docs/src/appendix/faq.md +++ b/docs/src/appendix/faq.md @@ -12,7 +12,10 @@ nmrs is a Rust library for managing network connections on Linux via NetworkMana ### 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. +Yes. nmrs is at version 3.1.x with a stable API. All public types are +marked `#[non_exhaustive]` to allow backward-compatible additions, and +the public surface is enforced in CI with +[`cargo-semver-checks`](https://crates.io/crates/cargo-semver-checks). ### What Linux distributions are supported? diff --git a/docs/src/examples/wifi-scanner.md b/docs/src/examples/wifi-scanner.md index 4860528e..a68e6c5b 100644 --- a/docs/src/examples/wifi-scanner.md +++ b/docs/src/examples/wifi-scanner.md @@ -115,7 +115,7 @@ Add to your `Cargo.toml`: ```toml [dependencies] -nmrs = "2.0.0" +nmrs = "3.1" tokio = { version = "1", features = ["full"] } ``` diff --git a/docs/src/getting-started/installation.md b/docs/src/getting-started/installation.md index f9083fd4..f04517c4 100644 --- a/docs/src/getting-started/installation.md +++ b/docs/src/getting-started/installation.md @@ -14,9 +14,15 @@ Or manually add to your `Cargo.toml`: ```toml [dependencies] -nmrs = "2.0.0" +nmrs = "3.1" +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } ``` +`nmrs` is async and ships no runtime of its own. The examples in this book +use [Tokio](https://tokio.rs/) but `nmrs` works with any reactor that is +compatible with the [`zbus`](https://docs.rs/zbus) executor (Tokio, +`async-std`, `smol`, …). See [Async Runtime Support](../advanced/async-runtimes.md). + ### From Source Clone and build from source: diff --git a/docs/src/getting-started/quick-start.md b/docs/src/getting-started/quick-start.md index accf4db6..c93264e0 100644 --- a/docs/src/getting-started/quick-start.md +++ b/docs/src/getting-started/quick-start.md @@ -5,7 +5,7 @@ This guide will get you up and running with nmrs in minutes. ## Prerequisites Make sure you have: -- Rust installed (1.78.0+) +- Rust 1.90.0 or later installed - NetworkManager running on your Linux system - Basic familiarity with async Rust @@ -14,34 +14,44 @@ Make sure you have: ```bash cargo new nmrs-demo cd nmrs-demo -cargo add nmrs tokio --features tokio/full +cargo add nmrs +cargo add tokio --features macros,rt-multi-thread ``` ## Your First nmrs Program -Let's create a simple program that lists available WiFi networks: +Let's create a simple program that lists available Wi-Fi networks: ```rust use nmrs::NetworkManager; #[tokio::main] async fn main() -> nmrs::Result<()> { - // Initialize NetworkManager connection let nm = NetworkManager::new().await?; - - // List all available networks + + // `None` enumerates every Wi-Fi device. Pass `Some("wlan0")` to scope + // the listing to a single interface (see "Per-Device Wi-Fi Scoping"). let networks = nm.list_networks(None).await?; - - // Print network information - for network in networks { + + for net in networks { + let kind = if net.is_eap { + "WPA-EAP" + } else if net.is_psk { + "WPA-PSK" + } else if net.secured { + "Other" + } else { + "Open" + }; + println!( - "SSID: {:<20} Signal: {:>3}% Security: {:?}", - network.ssid, - network.strength.unwrap_or(0), - network.security + "SSID: {:<20} Signal: {:>3}% Security: {}", + net.ssid, + net.strength.unwrap_or(0), + kind, ); } - + Ok(()) } ``` @@ -208,54 +218,49 @@ async fn main() -> nmrs::Result<()> { 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}% {:?}", + "{:2}. {:<25} Signal: {:>3}% secured={}", i + 1, net.ssid, net.strength.unwrap_or(0), - net.security + net.secured, ); } - - // 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() - } + + // `Network` exposes `secured` / `is_psk` / `is_eap` rather than a single + // `WifiSecurity` value. Use them to pick the right authentication path. + let security = if !selected.secured { + WifiSecurity::Open + } else { + 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!"); - + println!("Connected successfully"); + Ok(()) } ``` @@ -272,13 +277,18 @@ 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: +`nmrs` is built on top of [`zbus`], which supports several async executors. +The choice of runtime is made by `zbus` at compile time; `nmrs` itself does +not bring its own runtime. The examples in this book use Tokio, but +async-std or smol works just as well. + +[`zbus`]: https://docs.rs/zbus ### async-std ```toml [dependencies] -nmrs = "2.0.0" +nmrs = "3.1" async-std = { version = "1.12", features = ["attributes"] } ``` @@ -295,7 +305,7 @@ async fn main() -> nmrs::Result<()> { ```toml [dependencies] -nmrs = "2.0.0" +nmrs = "3.1" smol = "2.0" ``` @@ -308,3 +318,5 @@ fn main() -> nmrs::Result<()> { }) } ``` + +See [Async Runtime Support](../advanced/async-runtimes.md) for a deeper look. diff --git a/docs/src/getting-started/requirements.md b/docs/src/getting-started/requirements.md index 897a3106..7d9100a3 100644 --- a/docs/src/getting-started/requirements.md +++ b/docs/src/getting-started/requirements.md @@ -219,8 +219,9 @@ cargo update | nmrs Version | Minimum Rust | NetworkManager | Notable Features | |--------------|--------------|----------------|------------------| -| 3.0.0 | 1.90.0 | 1.0+ | Edition 2024 | -| 2.0.0 | 1.78.0 | 1.0+ | Full API rewrite | +| 3.1.x | 1.90.0 | 1.0+ | Per-interface Wi-Fi scoping, OpenVPN support, secret agent, airplane mode | +| 3.0.x | 1.90.0 | 1.0+ | Edition 2024, breaking API revamp | +| 2.x | 1.78.0 | 1.0+ | Full API rewrite | | 1.x | 1.70.0 | 1.0+ | Initial release | ## Next Steps diff --git a/docs/src/guide/devices.md b/docs/src/guide/devices.md index 6e8564bc..6dc1437d 100644 --- a/docs/src/guide/devices.md +++ b/docs/src/guide/devices.md @@ -49,6 +49,7 @@ use nmrs::DeviceType; | `DeviceType::Bluetooth` | Bluetooth network device | | `DeviceType::WifiP2P` | Wi-Fi Direct (peer-to-peer) | | `DeviceType::Loopback` | Loopback interface (localhost) | +| `DeviceType::Vlan` | 802.1Q virtual VLAN | | `DeviceType::Other(u32)` | Unknown type with raw code | ### Type Helper Methods @@ -67,6 +68,14 @@ if device.is_wired() { if device.is_bluetooth() { println!("{} is a Bluetooth device", device.interface); } + +if device.is_loopback() { + println!("{} is the loopback interface", device.interface); +} + +if device.is_vlan() { + println!("{} is a VLAN", device.interface); +} ``` `DeviceType` also provides capability queries: diff --git a/docs/src/guide/error-handling.md b/docs/src/guide/error-handling.md index 48e3fab5..fecc2be8 100644 --- a/docs/src/guide/error-handling.md +++ b/docs/src/guide/error-handling.md @@ -14,17 +14,23 @@ All public API methods return `nmrs::Result`. ## ConnectionError Variants +For a complete listing of every variant and its payload, see the +[Error Types reference](../api/errors.md). The tables below group the +most commonly handled variants by category. + ### Network & Wi-Fi Errors | Variant | Description | |---------|-------------| | `NotFound` | Network not visible during scan | +| `ApBssidNotFound { ssid, bssid }` | No AP matching both the SSID and BSSID | +| `InvalidBssid(String)` | Invalid BSSID format | | `AuthFailed` | Wrong password or rejected credentials | | `MissingPassword` | Empty password provided | | `NoWifiDevice` | No Wi-Fi adapter found | | `WifiNotReady` | Wi-Fi device not ready in time | -| `WifiInterfaceNotFound` | Specified Wi-Fi interface doesn't exist | -| `NotAWifiDevice` | Interface exists but isn't Wi-Fi | +| `WifiInterfaceNotFound { interface }` | Specified Wi-Fi interface doesn't exist | +| `NotAWifiDevice { interface }` | Interface exists but isn't Wi-Fi | | `HardwareRadioKilled` | Hardware kill switch is on | | `NoWiredDevice` | No Ethernet adapter found | | `DhcpFailed` | Failed to obtain an IP address via DHCP | @@ -42,27 +48,49 @@ All public API methods return `nmrs::Result`. | Variant | Description | |---------|-------------| -| `NoVpnConnection` | VPN not found or not active | +| `NoVpnConnection` | No VPN connection (or not active) | +| `VpnNotFound(String)` | VPN connection not found by UUID/name | | `VpnFailed(String)` | VPN connection failed with details | -| `VpnIdAmbiguous` | Multiple VPNs share the same name | -| `IncompleteBuilder` | VPN builder missing required fields | +| `VpnIdAmbiguous(String)` | Multiple VPNs share the same name; use UUID | +| `IncompleteBuilder(String)` | VPN/Wi-Fi 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 | +| `ParseError(OvpnParseError)` | Failed to parse a `.ovpn` file | ### Bluetooth Errors | Variant | Description | |---------|-------------| | `NoBluetoothDevice` | No Bluetooth adapter found | +| `BluezUnavailable(String)` | BlueZ not running or no adapters | +| `BluetoothToggleFailed(String)` | Adapter exists but failed to power on/off | -### Profile Errors +### Profile & Settings Errors | Variant | Description | |---------|-------------| | `NoSavedConnection` | No saved profile for the requested network | +| `SavedConnectionNotFound(String)` | No saved profile with that UUID | +| `MalformedSavedConnection(String)` | Saved settings missing/invalid keys | +| `InvalidVlanId { id }` | VLAN ID outside `1..=4094` | +| `InvalidInput { field, reason }` | Generic config-field validation failure | + +### Connectivity Errors + +| Variant | Description | +|---------|-------------| +| `ConnectivityCheckDisabled` | NM connectivity checks are disabled in config | + +### Secret Agent Errors + +| Variant | Description | +|---------|-------------| +| `AgentRegistration { context }` | Secret agent failed to register with NM | +| `AgentNotRegistered` | Used a handle whose registration was already torn down | +| `AgentAlreadyRegistered` | Identifier collision with another agent in this process | ### Low-Level Errors @@ -183,7 +211,8 @@ async fn connect() -> Result<()> { | Variant | Description | |---------|-------------| | `HardwareRadioKilled` | Hardware kill switch is on; Wi-Fi cannot be enabled until the switch is toggled | -| `BluezUnavailable` | Bluetooth D-Bus service (BlueZ) is not running or unreachable | +| `BluezUnavailable(String)` | Bluetooth stack (BlueZ) is not running or no adapters are present | +| `BluetoothToggleFailed(String)` | A BlueZ adapter exists but failed to power on/off when toggling airplane mode | ## Non-Exhaustive diff --git a/docs/src/guide/profiles.md b/docs/src/guide/profiles.md index cf608920..5d80889f 100644 --- a/docs/src/guide/profiles.md +++ b/docs/src/guide/profiles.md @@ -4,6 +4,15 @@ NetworkManager stores connection profiles for every network you've connected to. ## Listing Saved Connections +`nmrs` exposes three flavors of "list the saved profiles", trading off +detail for cost: + +| Method | Cost | Returns | +|--------|------|---------| +| [`list_saved_connections`](../api/network-manager.md#connection-profile-methods) | One `GetSettings` call per profile, full decode | `Vec` | +| [`list_saved_connections_brief`](../api/network-manager.md#connection-profile-methods) | One `GetSettings` per profile, minimal decode | `Vec` | +| [`list_saved_connection_ids`](../api/network-manager.md#connection-profile-methods) | Same calls as `_brief`, only the names | `Vec` | + ```rust use nmrs::NetworkManager; @@ -11,16 +20,23 @@ use nmrs::NetworkManager; 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); + // Full decode — useful for showing IP / autoconnect / security in a UI. + for conn in nm.list_saved_connections().await? { + println!(" {:<32} {:<12} {}", conn.id, conn.connection_type, conn.uuid); + } + + // Lightweight — just names and types, useful for menus. + for brief in nm.list_saved_connections_brief().await? { + println!(" {:<32} ({})", brief.id, brief.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. +Each `SavedConnection` includes the profile `id` (display name), `uuid`, +`connection_type` (`"802-11-wireless"`, `"vpn"`, `"wireguard"`, `"bluetooth"`, +…), and a decoded [`SettingsSummary`](../api/models.md#settingspatch). ## Checking for a Saved Connection @@ -79,9 +95,49 @@ nm.forget_vpn("MyVPN").await?; nm.forget_bluetooth("My Phone").await?; ``` +## Loading a Single Profile by UUID + +```rust +let nm = NetworkManager::new().await?; + +let profile = nm.get_saved_connection("a1b2c3d4-...").await?; +println!("{} ({}) autoconnect={}", profile.id, profile.connection_type, profile.autoconnect); +``` + +For the raw `GetSettings` map (advanced consumers building their own +decoder), use `nm.get_saved_connection_raw(uuid)`. + +## Updating a Profile + +`update_saved_connection` merges a [`SettingsPatch`](../api/models.md#settingspatch) +into an existing profile via NM's `Update` / `UpdateUnsaved` methods. +This is the right call to flip `autoconnect`, change a priority, or +update DNS without rebuilding the entire profile. + +## Deleting by UUID + +When the profile UUID is known, you can delete it directly: + +```rust +nm.delete_saved_connection("a1b2c3d4-...").await?; +``` + +For SSID-based deletion (which also disconnects first if active), use +`forget`, `forget_vpn`, or `forget_bluetooth` as shown above. + +## Reloading from Disk + +If you've edited keyfiles in `/etc/NetworkManager/system-connections/` +out-of-band, ask NetworkManager to re-read them: + +```rust +nm.reload_saved_connections().await?; +``` + ## Getting the D-Bus Path -For advanced use cases, you can retrieve the D-Bus object path of a saved connection: +For advanced use cases, you can retrieve the D-Bus object path of a saved +connection by SSID: ```rust let nm = NetworkManager::new().await?; diff --git a/docs/src/guide/vpn.md b/docs/src/guide/vpn.md index 96a382d9..9d1ac00b 100644 --- a/docs/src/guide/vpn.md +++ b/docs/src/guide/vpn.md @@ -231,8 +231,8 @@ match nm.connect_vpn(config).await { eprintln!("Connection timed out — check gateway address"); } - Err(ConnectionError::VpnFailed) => { - eprintln!("VPN activation failed — check plugin or config"); + Err(ConnectionError::VpnFailed(reason)) => { + eprintln!("VPN activation failed — check plugin or config: {reason}"); } Err(ConnectionError::NotFound) => { diff --git a/docs/src/guide/wifi.md b/docs/src/guide/wifi.md index 8593d66e..3a419ebc 100644 --- a/docs/src/guide/wifi.md +++ b/docs/src/guide/wifi.md @@ -83,15 +83,25 @@ nm.connect("CorpWiFi", None, WifiSecurity::WpaEap { ## Network Information -The `Network` struct contains detailed information about discovered networks: +The `Network` struct contains detailed information about discovered networks +(see the [Models reference](../api/models.md#network) for the full layout): ```rust pub struct Network { - pub ssid: String, // Network name - pub strength: Option, // Signal strength (0-100) - pub security: WifiSecurity, // Security type - pub frequency: Option, // Frequency in MHz - pub hwaddress: Option, // BSSID/MAC address + pub device: String, // owning Wi-Fi interface (e.g. "wlan0") + pub ssid: String, // network name + pub bssid: Option, // BSSID of the strongest AP + pub strength: Option, // signal strength (0–100) + pub frequency: Option, // MHz + pub secured: bool, // requires authentication + pub is_psk: bool, // WPA-PSK + pub is_eap: bool, // WPA-EAP / 802.1X + pub is_hotspot: bool, + pub bssids: Vec, // all merged BSSIDs (strongest first) + pub is_active: bool, + pub known: bool, // a saved profile exists for this SSID + pub security_features: SecurityFeatures, + // ... } ``` @@ -102,42 +112,54 @@ 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"); - } + let quality = match strength { + 70..=100 => "Excellent", + 50..=69 => "Good", + _ => "Weak", + }; + println!(" Signal: {}% ({})", strength, quality); } - + if let Some(freq) = net.frequency { let band = if freq > 5000 { "5GHz" } else { "2.4GHz" }; println!(" Band: {}", band); } + + let kind = if net.is_eap { + "WPA-EAP" + } else if net.is_psk { + "WPA-PSK" + } else if net.secured { + "Other (secured)" + } else { + "Open" + }; + println!(" Security: {}", kind); } ``` ## Connection Options -Customize connection behavior with `ConnectionOptions`: +`ConnectionOptions` controls the high-level behavior of profiles created by +[`NetworkManager`](../api/network-manager.md): ```rust -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()]); +use nmrs::ConnectionOptions; -// Note: Advanced connection options require using builders directly -// See the Advanced Topics section for details +let opts = ConnectionOptions::new(true) // autoconnect + .with_priority(10) // higher = preferred + .with_retries(3); // 0 means never retry, None = unlimited ``` +This struct intentionally only covers the connection-management knobs +NetworkManager exposes per-profile. To configure DHCP method, manual IP +addresses, custom DNS servers, or static routes you need a builder — see +the [`ConnectionBuilder`](../api/builders.md#connectionbuilder) reference, +or use [`WifiConnectionBuilder`](../api/builders.md#wificonnectionbuilder) +for Wi-Fi-specific defaults. + ## WiFi Radio Control Enable or disable WiFi hardware: @@ -217,10 +239,10 @@ match nm.connect("Network", None, WifiSecurity::WpaPsk { eprintln!("Failed to get IP address"); } - Err(ConnectionError::NoSecrets) => { + Err(ConnectionError::MissingPassword) => { eprintln!("Missing password or credentials"); } - + Err(e) => eprintln!("Error: {}", e), } ``` diff --git a/nmrs/src/api/builders/mod.rs b/nmrs/src/api/builders/mod.rs index ac7e9db4..eee9e494 100644 --- a/nmrs/src/api/builders/mod.rs +++ b/nmrs/src/api/builders/mod.rs @@ -1,71 +1,81 @@ //! Connection builders for different network types. //! -//! This module provides functions to construct NetworkManager connection settings -//! dictionaries for various connection types. These settings are used with -//! NetworkManager's D-Bus API to create and activate connections. +//! This module provides two complementary APIs for constructing NetworkManager +//! settings dictionaries: //! -//! # Available Builders +//! - **Fluent builder types** that support method chaining and perform +//! validation when `.build()` is called. +//! - **Free `build_*` functions** that take already-prepared structs and +//! produce the same settings map directly. //! -//! - [`wifi`] - WiFi connection builders (WPA-PSK, WPA-EAP, Open) -//! - [`vpn`] - VPN connection builders (WireGuard) -//! - Ethernet builders (via [`build_ethernet_connection`]) +//! # Fluent builders //! -//! # When to Use These +//! - [`ConnectionBuilder`] — generic, low-level builder used as the foundation +//! for all other builders. Supports IPv4/IPv6 method, manual addresses, +//! routes, DNS, autoconnect, MTU, and more. See [`IpConfig`] / [`Route`]. +//! - [`WifiConnectionBuilder`] — Wi-Fi (open / WPA-PSK / WPA-EAP), with band, +//! channel, hidden SSID, BSSID pinning, and AP-mode shortcuts. See +//! [`WifiBand`] / [`WifiMode`]. +//! - [`WireGuardBuilder`] — kernel-level WireGuard tunnels. +//! - [`OpenVpnBuilder`] — NM-plugin OpenVPN connections, with +//! [`from_ovpn_file`](OpenVpnBuilder::from_ovpn_file) for `.ovpn` import. //! -//! Most users should use the high-level [`NetworkManager`](crate::NetworkManager) API -//! instead of calling these builders directly. These are exposed for advanced use cases -//! where you need fine-grained control over connection settings. +//! # Free functions +//! +//! - [`build_wifi_connection`] / [`build_ethernet_connection`] (in [`wifi`]) +//! - [`build_wireguard_connection`] / [`build_openvpn_connection`] (in [`vpn`]) +//! - [`build_bluetooth_connection`] (in [`bluetooth`]) +//! - [`build_vlan_connection`] (in [`vlan`]) +//! +//! # When to use these +//! +//! Most users should use the high-level +//! [`NetworkManager`](crate::NetworkManager) API instead of calling these +//! builders directly. They are exposed for advanced use cases where you +//! need fine-grained control over the raw settings dictionary before +//! handing it to NetworkManager's `AddConnection` or +//! `AddAndActivateConnection` D-Bus methods. //! //! # Examples //! -//! ```ignore -//! use nmrs::builders::{build_wifi_connection, build_wireguard_connection, build_ethernet_connection}; -//! use nmrs::{WifiSecurity, ConnectionOptions, VpnCredentials, VpnKind, WireGuardPeer}; +//! ## Wi-Fi (free function) +//! +//! ```rust +//! use nmrs::builders::{build_ethernet_connection, build_wifi_connection}; +//! use nmrs::{ConnectionOptions, WifiSecurity}; //! -//! let opts = ConnectionOptions { -//! autoconnect: true, -//! autoconnect_priority: Some(10), -//! autoconnect_retries: Some(3), -//! }; +//! let opts = ConnectionOptions::new(true).with_priority(10); //! -//! // Build WiFi connection settings -//! let wifi_settings = build_wifi_connection( +//! let wifi = build_wifi_connection( //! "MyNetwork", //! &WifiSecurity::WpaPsk { psk: "password".into() }, -//! &opts +//! &opts, //! ); +//! let eth = build_ethernet_connection("eth0", &opts); +//! ``` +//! +//! ## WireGuard (fluent builder) +//! +//! ```rust +//! use nmrs::builders::WireGuardBuilder; +//! use nmrs::WireGuardPeer; +//! +//! let peer = WireGuardPeer::new( +//! "HIgo9xNzJMWLKAShlKl6/bUT1VI9Q0SDBXGtLXkPFXc=", +//! "vpn.example.com:51820", +//! vec!["0.0.0.0/0".into()], +//! ).with_persistent_keepalive(25); //! -//! // Build Ethernet connection settings -//! let eth_settings = build_ethernet_connection("eth0", &opts); -//! // Build WireGuard VPN connection settings -//! let opts = ConnectionOptions { -//! autoconnect: true, -//! autoconnect_priority: Some(10), -//! autoconnect_retries: Some(3), -//! }; -//! -//! let creds = VpnCredentials { -//! vpn_type: VpnKind::WireGuard, -//! name: "MyVPN".into(), -//! gateway: "vpn.example.com:51820".into(), -//! private_key: "YBk6X3pP8KjKz7+HFWzVHNqL3qTZq8hX9VxFQJ4zVmM=".into(), -//! address: "10.0.0.2/24".into(), -//! peers: vec![WireGuardPeer { -//! public_key: "HIgo9xNzJMWLKAShlKl6/bUT1VI9Q0SDBXGtLXkPFXc=".into(), -//! gateway: "vpn.example.com:51820".into(), -//! allowed_ips: vec!["0.0.0.0/0".into()], -//! preshared_key: None, -//! persistent_keepalive: Some(25), -//! }], -//! dns: None, -//! mtu: None, -//! uuid: None, -//! }; -//! -//! let vpn_settings = build_wireguard_connection(&creds, &opts).unwrap(); +//! 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()]) +//! .build() +//! .expect("WireGuardBuilder is fully configured"); //! ``` //! -//! These settings can then be passed to NetworkManager's +//! The returned settings can then be passed to NetworkManager's //! `AddConnection` or `AddAndActivateConnection` D-Bus methods. pub mod bluetooth; diff --git a/nmrs/src/api/builders/vpn.rs b/nmrs/src/api/builders/vpn.rs index fe93338e..d7eb127b 100644 --- a/nmrs/src/api/builders/vpn.rs +++ b/nmrs/src/api/builders/vpn.rs @@ -130,6 +130,31 @@ fn string_pairs_to_dict( Ok(dict) } +/// Pushes `(key, value.clone())` onto `out` if `value` is `Some`. +/// +/// Convenience wrapper around the common pattern of mapping an +/// `Option` field on an [`OpenVpnConfig`] to an entry in the flat +/// `vpn.data` dict. +fn push_opt_str(out: &mut Vec<(String, String)>, key: &str, value: Option<&String>) { + if let Some(v) = value { + out.push((key.to_string(), v.clone())); + } +} + +/// Pushes `(key, value.to_string())` onto `out` if `value` is `Some`. +/// +/// Used for numeric/boolean OpenVPN options that NetworkManager stores as +/// strings (`tunnel-mtu`, `ping`, `connect-timeout`, …). +fn push_opt_display( + out: &mut Vec<(String, String)>, + key: &str, + value: Option, +) { + if let Some(v) = value { + out.push((key.to_string(), v.to_string())); + } +} + /// Builds OpenVPN connection settings for NetworkManager. /// /// Returns a settings dictionary suitable for `AddAndActivateConnection`. @@ -182,110 +207,71 @@ pub fn build_openvpn_connection( vpn_data.push(("proto-tcp".into(), "yes".into())); } - if let Some(ref username) = config.username { - vpn_data.push(("username".into(), username.clone())); - } - if let Some(ref auth) = config.auth { - vpn_data.push(("auth".into(), auth.clone())); - } - if let Some(ref cipher) = config.cipher { - vpn_data.push(("cipher".into(), cipher.clone())); - } - if let Some(mtu) = config.mtu { - vpn_data.push(("tunnel-mtu".into(), mtu.to_string())); - } + push_opt_str(&mut vpn_data, "username", config.username.as_ref()); + push_opt_str(&mut vpn_data, "auth", config.auth.as_ref()); + push_opt_str(&mut vpn_data, "cipher", config.cipher.as_ref()); + push_opt_display(&mut vpn_data, "tunnel-mtu", config.mtu); // certs - if let Some(ref ca) = config.ca_cert { - vpn_data.push(("ca".into(), ca.clone())); - } - if let Some(ref cert) = config.client_cert { - vpn_data.push(("cert".into(), cert.clone())); - } - if let Some(ref key) = config.client_key { - vpn_data.push(("key".into(), key.clone())); - } + push_opt_str(&mut vpn_data, "ca", config.ca_cert.as_ref()); + push_opt_str(&mut vpn_data, "cert", config.client_cert.as_ref()); + push_opt_str(&mut vpn_data, "key", config.client_key.as_ref()); if let Some(ref compression) = config.compression { #[allow(deprecated)] - match compression { - OpenVpnCompression::No => { - vpn_data.push(("compress".into(), "no".into())); - } - OpenVpnCompression::Lzo => { - vpn_data.push(("comp-lzo".into(), "yes".into())); - } - OpenVpnCompression::Lz4 => { - vpn_data.push(("compress".into(), "lz4".into())); - } - OpenVpnCompression::Lz4V2 => { - vpn_data.push(("compress".into(), "lz4-v2".into())); - } - OpenVpnCompression::Yes => { - vpn_data.push(("compress".into(), "yes".into())); - } - } + let (key, value) = match compression { + OpenVpnCompression::No => ("compress", "no"), + OpenVpnCompression::Lzo => ("comp-lzo", "yes"), + OpenVpnCompression::Lz4 => ("compress", "lz4"), + OpenVpnCompression::Lz4V2 => ("compress", "lz4-v2"), + OpenVpnCompression::Yes => ("compress", "yes"), + }; + vpn_data.push((key.into(), value.into())); } // TLS hardening options if let Some(ref key) = config.tls_auth_key { vpn_data.push(("tls-auth".into(), key.clone())); - if let Some(dir) = config.tls_auth_direction { - vpn_data.push(("ta-dir".into(), dir.to_string())); - } - } - // FIXME: surely, there must be a better way to do this - if let Some(ref key) = config.tls_crypt { - vpn_data.push(("tls-crypt".into(), key.clone())); - } - if let Some(ref key) = config.tls_crypt_v2 { - vpn_data.push(("tls-crypt-v2".into(), key.clone())); - } - if let Some(ref ver) = config.tls_version_min { - vpn_data.push(("tls-version-min".into(), ver.clone())); - } - if let Some(ref ver) = config.tls_version_max { - vpn_data.push(("tls-version-max".into(), ver.clone())); - } - if let Some(ref cipher) = config.tls_cipher { - vpn_data.push(("tls-cipher".into(), cipher.clone())); - } - if let Some(ref cert_type) = config.remote_cert_tls { - vpn_data.push(("remote-cert-tls".into(), cert_type.clone())); - } + push_opt_display(&mut vpn_data, "ta-dir", config.tls_auth_direction); + } + push_opt_str(&mut vpn_data, "tls-crypt", config.tls_crypt.as_ref()); + push_opt_str(&mut vpn_data, "tls-crypt-v2", config.tls_crypt_v2.as_ref()); + push_opt_str( + &mut vpn_data, + "tls-version-min", + config.tls_version_min.as_ref(), + ); + push_opt_str( + &mut vpn_data, + "tls-version-max", + config.tls_version_max.as_ref(), + ); + push_opt_str(&mut vpn_data, "tls-cipher", config.tls_cipher.as_ref()); + push_opt_str( + &mut vpn_data, + "remote-cert-tls", + config.remote_cert_tls.as_ref(), + ); if let Some((ref name, ref name_type)) = config.verify_x509_name { vpn_data.push(("verify-x509-name".into(), name.clone())); vpn_data.push(("verify-x509-type".into(), name_type.clone())); } - if let Some(ref path) = config.crl_verify { - vpn_data.push(("crl-verify".into(), path.clone())); - } - - if let Some(v) = config.ping { - vpn_data.push(("ping".into(), v.to_string())); - } - if let Some(v) = config.ping_exit { - vpn_data.push(("ping-exit".into(), v.to_string())); - } - if let Some(v) = config.ping_restart { - vpn_data.push(("ping-restart".into(), v.to_string())); - } - if let Some(v) = config.reneg_seconds { - vpn_data.push(("reneg-sec".into(), v.to_string())); - } - if let Some(v) = config.connect_timeout { - vpn_data.push(("connect-timeout".into(), v.to_string())); - } - if let Some(ref s) = config.data_ciphers { - vpn_data.push(("data-ciphers".into(), s.clone())); - } - if let Some(ref s) = config.data_ciphers_fallback { - vpn_data.push(("data-ciphers-fallback".into(), s.clone())); - } + push_opt_str(&mut vpn_data, "crl-verify", config.crl_verify.as_ref()); + + push_opt_display(&mut vpn_data, "ping", config.ping); + push_opt_display(&mut vpn_data, "ping-exit", config.ping_exit); + push_opt_display(&mut vpn_data, "ping-restart", config.ping_restart); + push_opt_display(&mut vpn_data, "reneg-sec", config.reneg_seconds); + push_opt_display(&mut vpn_data, "connect-timeout", config.connect_timeout); + push_opt_str(&mut vpn_data, "data-ciphers", config.data_ciphers.as_ref()); + push_opt_str( + &mut vpn_data, + "data-ciphers-fallback", + config.data_ciphers_fallback.as_ref(), + ); if config.ncp_disable { vpn_data.push(("ncp-disable".into(), "yes".into())); } - // holy moly if let Some(ref proxy) = config.proxy { match proxy { @@ -339,12 +325,8 @@ pub fn build_openvpn_connection( let data_dict = string_pairs_to_dict(vpn_data)?; let mut vpn_secrets: Vec<(String, String)> = Vec::new(); - if let Some(ref password) = config.password { - vpn_secrets.push(("password".into(), password.clone())); - } - if let Some(ref key_password) = config.key_password { - vpn_secrets.push(("cert-pass".into(), key_password.clone())); - } + push_opt_str(&mut vpn_secrets, "password", config.password.as_ref()); + push_opt_str(&mut vpn_secrets, "cert-pass", config.key_password.as_ref()); let mut vpn: HashMap<&'static str, Value<'static>> = HashMap::new(); vpn.insert( diff --git a/nmrs/src/api/models/error.rs b/nmrs/src/api/models/error.rs index e4082338..ebe2ecf2 100644 --- a/nmrs/src/api/models/error.rs +++ b/nmrs/src/api/models/error.rs @@ -146,7 +146,7 @@ pub enum ConnectionError { #[error("invalid UTF-8 in SSID: {0}")] InvalidUtf8(#[from] std::str::Utf8Error), - /// No VPN connection found + /// No VPN connection found. #[error("no VPN connection found")] NoVpnConnection, @@ -158,38 +158,40 @@ pub enum ConnectionError { #[error("multiple VPN connections named '{0}', use UUID")] VpnIdAmbiguous(String), - /// Invalid IP address or CIDR notation + /// Invalid IP address or CIDR notation. #[error("invalid address: {0}")] InvalidAddress(String), - /// Invalid VPN peer configuration + /// Invalid VPN peer configuration. #[error("invalid peer configuration: {0}")] InvalidPeers(String), - /// Invalid WireGuard private key format + /// Invalid WireGuard private key format. #[error("invalid WireGuard private key: {0}")] InvalidPrivateKey(String), - /// Invalid WireGuard public key format + /// Invalid WireGuard public key format. #[error("invalid WireGuard public key: {0}")] InvalidPublicKey(String), - /// Invalid VPN gateway format (should be host:port) + /// Invalid VPN gateway format (should be `host:port`). #[error("invalid VPN gateway: {0}")] InvalidGateway(String), - /// VPN connection failed + /// VPN connection failed. #[error("VPN connection failed: {0}")] VpnFailed(String), - /// Bluetooth device not found + /// Bluetooth device not found. #[error("Bluetooth device not found")] NoBluetoothDevice, - /// A D-Bus operation failed with context about what was being attempted + /// A D-Bus operation failed, with context about what was being attempted. #[error("{context}: {source}")] DbusOperation { + /// Human-readable description of the operation that failed. context: String, + /// The underlying `zbus` error. #[source] source: zbus::Error, }, @@ -209,7 +211,7 @@ pub enum ConnectionError { #[error("secret agent already registered under this identifier")] AgentAlreadyRegistered, - /// An error occured while parsing a configuration + /// An error occurred while parsing a configuration. #[error("error while parsing a configuration: {0}")] ParseError(OvpnParseError), diff --git a/nmrs/src/lib.rs b/nmrs/src/lib.rs index 861cc6f4..f7dd6003 100644 --- a/nmrs/src/lib.rs +++ b/nmrs/src/lib.rs @@ -97,33 +97,55 @@ //! //! The main entry point is [`NetworkManager`], which provides methods for: //! - Listing and managing network devices -//! - Scanning for available WiFi networks -//! - Connecting to networks (WiFi, Ethernet, VPN) +//! - Scanning for available Wi-Fi networks +//! - Connecting to networks (Wi-Fi, Ethernet, Bluetooth PAN, VPN) //! - Managing saved connection profiles -//! - Real-time monitoring of network changes +//! - Real-time monitoring of network and device changes +//! - Querying connectivity state and captive-portal URLs +//! - Toggling Wi-Fi/WWAN/Bluetooth radios and airplane mode //! //! ## Models //! -//! The [`models`] module contains all types, enums, and errors: -//! - [`Device`] - Represents a network device (WiFi, Ethernet, etc.) -//! - [`Network`] - Represents a discovered WiFi network -//! - [`WifiSecurity`] - Security types (Open, WPA-PSK, WPA-EAP) -//! - [`VpnCredentials`] - Legacy VPN connection credentials -//! - [`VpnType`] - Protocol-specific VPN metadata (WireGuard, OpenVPN, strongSwan, etc.) -//! - [`VpnKind`] - Plugin-based vs kernel WireGuard distinction -//! - [`VpnConnection`] - VPN connection information -//! - [`VpnDetails`] - Protocol-specific VPN details (WireGuard / OpenVPN) -//! - [`WireGuardConfig`] - WireGuard connection configuration -//! - [`WireGuardPeer`] - WireGuard peer configuration -//! - [`OpenVpnConfig`] - OpenVPN connection configuration -//! - [`OpenVpnAuthType`] - OpenVPN authentication types -//! - [`ConnectionError`] - Comprehensive error types +//! The [`models`] module contains all types, enums, and errors. The most +//! commonly used items are also re-exported at the crate root: +//! +//! - [`Device`] / [`DeviceType`] / [`DeviceState`] — network devices and their state +//! - [`Network`] / [`AccessPoint`] / [`NetworkInfo`] — discovered Wi-Fi data +//! - [`WifiDevice`] — per-Wi-Fi-device summary +//! - [`WifiSecurity`] / [`EapOptions`] / [`EapMethod`] / [`Phase2`] — Wi-Fi security +//! - [`ConnectionOptions`] / [`TimeoutConfig`] — connection knobs +//! - [`WireGuardConfig`] / [`WireGuardPeer`] — WireGuard configuration +//! - [`OpenVpnConfig`] / [`OpenVpnAuthType`] / [`OpenVpnProxy`] — OpenVPN configuration +//! - [`VpnConfig`] / [`VpnConfiguration`] — generic VPN dispatch trait/enum +//! - [`VpnConnection`] / [`VpnConnectionInfo`] / [`VpnDetails`] / [`VpnType`] / [`VpnKind`] — saved or active VPN data +//! - [`SavedConnection`] / [`SavedConnectionBrief`] / [`SettingsSummary`] / [`SettingsPatch`] — saved profile management +//! - [`AirplaneModeState`] / [`RadioState`] — radio/rfkill state +//! - [`BluetoothDevice`] / [`BluetoothIdentity`] / [`BluetoothNetworkRole`] — Bluetooth networking +//! - [`ConnectivityState`] / [`ConnectivityReport`] — internet connectivity +//! - [`ConnectionError`] / [`StateReason`] / [`ConnectionStateReason`] — errors +//! +//! [`VpnCredentials`] is still re-exported but is **deprecated**; new code +//! should use [`WireGuardConfig`] together with [`NetworkManager::connect_vpn`]. //! //! ## Connection Builders //! -//! The [`builders`] module provides functions to construct connection settings -//! for different network types. These are typically used internally but exposed -//! for advanced use cases. +//! The [`builders`] module provides both fluent builder types +//! ([`builders::ConnectionBuilder`], [`builders::WifiConnectionBuilder`], +//! [`builders::WireGuardBuilder`], [`builders::OpenVpnBuilder`]) and +//! free functions (`build_wifi_connection`, `build_ethernet_connection`, +//! `build_wireguard_connection`, `build_openvpn_connection`, +//! `build_bluetooth_connection`, `build_vlan_connection`) for constructing +//! NetworkManager settings dictionaries. Most callers should reach for the +//! higher-level [`NetworkManager`] API; these builders are exposed for +//! advanced use cases that need to assemble the raw settings dictionary +//! before calling a D-Bus method directly. +//! +//! ## Secret Agent +//! +//! The [`agent`] module lets a consumer register a NetworkManager **secret +//! agent** to handle interactive credential prompts (Wi-Fi passwords, VPN +//! tokens, 802.1X passwords) over D-Bus. See the module docs for the +//! three-stream model and a full example. //! //! # Examples //! @@ -288,24 +310,37 @@ pub mod agent; // Public API // ============================================================================ -/// Connection builders for WiFi, Ethernet, and VPN connections. +/// Connection builders for Wi-Fi, Ethernet, Bluetooth, VLAN, and VPN connections. /// -/// This module provides functions to construct NetworkManager connection settings -/// dictionaries. These are primarily used internally but exposed for advanced use cases. +/// This module provides two complementary APIs for constructing NetworkManager +/// settings dictionaries: /// -/// # Examples +/// - **Fluent builder types** — [`ConnectionBuilder`](builders::ConnectionBuilder), +/// [`WifiConnectionBuilder`](builders::WifiConnectionBuilder), +/// [`WireGuardBuilder`](builders::WireGuardBuilder), and +/// [`OpenVpnBuilder`](builders::OpenVpnBuilder), which support method +/// chaining and validation at `.build()`. +/// - **Free functions** — `build_wifi_connection`, `build_ethernet_connection`, +/// `build_wireguard_connection`, `build_openvpn_connection`, +/// `build_bluetooth_connection`, and `build_vlan_connection`, which are +/// handy for one-shot construction. +/// +/// Most callers should prefer [`NetworkManager`](crate::NetworkManager)'s +/// high-level methods such as [`connect`](crate::NetworkManager::connect) +/// and [`connect_vpn`](crate::NetworkManager::connect_vpn). Use these +/// builders only when you need to feed a raw settings dictionary to +/// NetworkManager's `AddConnection` or `AddAndActivateConnection` D-Bus +/// methods directly. +/// +/// # Example /// /// ```rust /// use nmrs::builders::build_wifi_connection; -/// use nmrs::{WifiSecurity, ConnectionOptions}; +/// use nmrs::{ConnectionOptions, WifiSecurity}; /// /// let opts = ConnectionOptions::new(true); -/// -/// let settings = build_wifi_connection( -/// "MyNetwork", -/// &WifiSecurity::Open, -/// &opts -/// ); +/// let settings = build_wifi_connection("MyNetwork", &WifiSecurity::Open, &opts); +/// // `settings` can be passed straight to NetworkManager via D-Bus. /// ``` pub mod builders { pub use crate::api::builders::*; @@ -313,34 +348,59 @@ pub mod builders { /// Types, enums, and errors for NetworkManager operations. /// -/// This module contains all the public types used throughout the crate: +/// This module re-exports every public data type used by the crate. +/// The same types are also re-exported at the crate root for convenience +/// (so `nmrs::Device` and `nmrs::models::Device` refer to the same type), +/// with the exceptions of [`NetworkManager`](crate::NetworkManager) and +/// [`WifiScope`](crate::WifiScope), which live only at the crate root. /// -/// # Core Types -/// - [`NetworkManager`] - Main API entry point -/// - [`Device`] - Network device representation -/// - [`Network`] - WiFi network representation -/// - [`NetworkInfo`] - Detailed network information +/// # Core Data Types +/// - [`Device`] — Network device representation +/// - [`Network`] — Wi-Fi network representation (SSID-grouped) +/// - [`AccessPoint`] — Per-BSSID access point details +/// - [`NetworkInfo`] — Detailed network information returned by `show_details` +/// - [`WifiDevice`] — Wi-Fi-specific device summary +/// - [`BluetoothDevice`] — Discovered Bluetooth peer +/// - [`SavedConnection`] / [`SavedConnectionBrief`] — Saved profile snapshots +/// - [`SettingsSummary`] / [`SettingsPatch`] — Decoded NM settings & update patches +/// - [`VpnConnection`] / [`VpnConnectionInfo`] / [`VpnDetails`] — Active or saved VPN data /// /// # Configuration -/// - [`WifiSecurity`] - WiFi security types (Open, WPA-PSK, WPA-EAP) -/// - [`EapOptions`] - Enterprise authentication options -/// - [`ConnectionOptions`] - Connection settings (autoconnect, priority, etc.) -/// - [`TimeoutConfig`] - Timeout configuration for network operations +/// - [`WifiSecurity`] — Wi-Fi security types (Open, WPA-PSK, WPA-EAP) +/// - [`EapOptions`] — Enterprise authentication options +/// - [`ConnectionOptions`] — Connection settings (autoconnect, priority, retries) +/// - [`TimeoutConfig`] — Timeout configuration for connection operations +/// - [`WireGuardConfig`] / [`WireGuardPeer`] — WireGuard tunnel configuration +/// - [`OpenVpnConfig`] — OpenVPN plugin configuration +/// - [`VlanConfig`] — VLAN tagging configuration +/// - [`BluetoothIdentity`] — Bluetooth target (bdaddr + role) +/// - [`VpnConfig`] / [`VpnConfiguration`] — Trait & enum used by `connect_vpn` /// /// # Enums -/// - [`DeviceType`] - Device types (Ethernet, WiFi, etc.) -/// - [`DeviceState`] - Device states (Disconnected, Activated, etc.) -/// - [`EapMethod`] - EAP authentication methods -/// - [`Phase2`] - Phase 2 authentication for EAP +/// - [`DeviceType`] — Device types (Ethernet, Wi-Fi, Bluetooth, etc.) +/// - [`DeviceState`] — Device states (Disconnected, Activated, etc.) +/// - [`ActiveConnectionState`] — State of an active connection +/// - [`ConnectivityState`] — NM-reported internet connectivity +/// - [`RadioState`] / [`AirplaneModeState`] — Radio/rfkill state +/// - [`ApMode`] — Access point operating mode +/// - [`BluetoothNetworkRole`] — PAN-U / NAP / DUN roles +/// - [`EapMethod`] — EAP authentication methods +/// - [`Phase2`] — Phase 2 authentication for EAP +/// - [`OpenVpnAuthType`] / [`OpenVpnConnectionType`] / [`OpenVpnCompression`] — OpenVPN auth/transport options +/// - [`OpenVpnProxy`] — OpenVPN HTTP/SOCKS proxy configuration +/// - [`VpnKind`] / [`VpnType`] — Plugin vs. kernel WireGuard, plus protocol-specific metadata +/// - [`VpnSecretFlags`] — NM secret flags for VPN credentials +/// - [`WifiKeyMgmt`] / [`WifiSecuritySummary`] / [`SecurityFeatures`] — Decoded Wi-Fi security info +/// - [`ConnectType`] — How a `connect_vpn` call resolved (saved vs. new) /// /// # Errors -/// - [`ConnectionError`] - Comprehensive error type for all operations -/// - [`StateReason`] - Device state change reasons -/// - [`ConnectionStateReason`] - Connection state change reasons +/// - [`ConnectionError`] — Comprehensive error type for all operations +/// - [`StateReason`] — Device state change reasons +/// - [`ConnectionStateReason`] — Connection state change reasons /// /// # Helper Functions -/// - [`reason_to_error`] - Convert device state reason to error -/// - [`connection_state_reason_to_error`] - Convert connection state reason to error +/// - [`reason_to_error`] — Convert a device state reason to a [`ConnectionError`] +/// - [`connection_state_reason_to_error`] — Convert an active-connection state reason to a [`ConnectionError`] pub mod models { pub use crate::api::models::*; }