diff --git a/.github/workflows/contributors.yml b/.github/workflows/contributors.yml
new file mode 100644
index 00000000..dedb3a2c
--- /dev/null
+++ b/.github/workflows/contributors.yml
@@ -0,0 +1,25 @@
+name: Contributors
+
+on:
+ workflow_dispatch:
+ push:
+ branches:
+ - master
+
+permissions:
+ contents: write
+ pull-requests: write
+
+jobs:
+ contributors:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Update contributors in README
+ uses: akhilmhdh/contributors-readme-action@v2.3.11
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ readme_path: README.md
+ image_size: 100
+ columns_per_row: 6
+ commit_message: "docs: update contributors in README"
diff --git a/Cargo.lock b/Cargo.lock
index f6e3610d..24c779a2 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -963,7 +963,7 @@ dependencies = [
[[package]]
name = "nmrs"
-version = "2.4.0"
+version = "3.0.0"
dependencies = [
"async-trait",
"base64",
diff --git a/Cargo.toml b/Cargo.toml
index 4c43d014..2c52bf51 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -12,7 +12,6 @@ license = "MIT"
repository = "https://github.com/cachebag/nmrs"
[workspace.lints.rust]
-unsafe_code = "forbid"
# This shouldn't apply to nmrs-gui
# missing_docs = "warn"
unused = { level = "warn", priority = -1 }
@@ -33,7 +32,7 @@ uuid = { version = "1.23.1", features = ["v4", "v5"] }
futures = "0.3.32"
futures-timer = "3.0.3"
base64 = "0.22.1"
-nmrs = { path = "nmrs", version = "2.2" }
+nmrs = { path = "nmrs", version = "3.0" }
gtk = { version = "0.11.2", package = "gtk4" }
glib = "0.22.5"
dirs = "6.0.0"
diff --git a/README.md b/README.md
index a060bdea..7d5e5d9f 100644
--- a/README.md
+++ b/README.md
@@ -43,7 +43,7 @@ async fn main() -> nmrs::Result<()> {
let nm = NetworkManager::new().await?;
// Scan for networks
- let networks = nm.list_networks().await?;
+ let networks = nm.list_networks(None).await?;
for net in networks {
println!(
@@ -70,7 +70,7 @@ async fn main() -> nmrs::Result<()> {
let nm = NetworkManager::new().await?;
// Connect to a network
- nm.connect("MyNetwork", WifiSecurity::WpaPsk {
+ nm.connect("MyNetwork", None, WifiSecurity::WpaPsk {
psk: "password123".into()
}).await?;
@@ -94,7 +94,7 @@ use nmrs::{NetworkManager, WifiSecurity, ConnectionError};
async fn main() -> nmrs::Result<()> {
let nm = NetworkManager::new().await?;
- match nm.connect("MyNetwork", WifiSecurity::WpaPsk {
+ match nm.connect("MyNetwork", None, WifiSecurity::WpaPsk {
psk: "wrong_password".into()
}).await {
Ok(_) => println!("Connected successfully"),
@@ -212,12 +212,12 @@ Edit `~/.config/nmrs/style.css` to customize the interface. There are also pre-d
- [x] Active Connection
- [x] Settings
- [x] Settings Connection
-- [ ] Agent Manager
+- [x] Agent Manager
- [ ] Checkpoint
- [ ] DNS Manager
- [ ] PPP
-- [ ] Secret Agent
-- [X] VPN Connection (WireGuard)
+- [x] Secret Agent
+- [x] VPN Connection (WireGuard + plugin VPNs)
- [ ] VPN Plugin
- [ ] Wi-Fi P2P
- [ ] WiMAX NSP
@@ -246,3 +246,34 @@ You may use, copy, modify, and distribute this software under the terms of eithe
See the following files for full license texts:
- [MIT License](./LICENSE-MIT)
- [Apache License 2.0](./LICENSE-APACHE)
+
+## Contributors
+
+Thank you to everyone who has helped build, test, document, and review `nmrs`.
+
+
+
+
diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md
index 2e44f7df..e0f82f1f 100644
--- a/docs/src/SUMMARY.md
+++ b/docs/src/SUMMARY.md
@@ -16,8 +16,10 @@
- [WPA-PSK Networks](./guide/wifi-wpa-psk.md)
- [WPA-EAP (Enterprise)](./guide/wifi-enterprise.md)
- [Hidden Networks](./guide/wifi-hidden.md)
+ - [Per-Device Wi-Fi Scoping](./guide/wifi-per-device.md)
- [VPN Connections](./guide/vpn.md)
- [WireGuard Setup](./guide/vpn-wireguard.md)
+ - [OpenVPN Setup](./guide/vpn-openvpn.md)
- [VPN Management](./guide/vpn-management.md)
- [Ethernet Management](./guide/ethernet.md)
- [Bluetooth](./guide/bluetooth.md)
@@ -40,6 +42,8 @@
- [WiFi Auto-Connect](./examples/wifi-auto-connect.md)
- [Enterprise WiFi](./examples/enterprise-wifi.md)
- [WireGuard VPN Client](./examples/wireguard-client.md)
+- [OpenVPN Client](./examples/openvpn-client.md)
+- [.ovpn File Import](./examples/ovpn-import.md)
- [Network Monitor Dashboard](./examples/network-monitor.md)
- [Connection Manager](./examples/connection-manager.md)
diff --git a/docs/src/advanced/async-runtimes.md b/docs/src/advanced/async-runtimes.md
index 8f884848..cd2036a0 100644
--- a/docs/src/advanced/async-runtimes.md
+++ b/docs/src/advanced/async-runtimes.md
@@ -12,7 +12,7 @@ use nmrs::NetworkManager;
#[tokio::main]
async fn main() -> nmrs::Result<()> {
let nm = NetworkManager::new().await?;
- let networks = nm.list_networks().await?;
+ let networks = nm.list_networks(None).await?;
println!("{} networks found", networks.len());
Ok(())
}
@@ -49,7 +49,7 @@ use nmrs::NetworkManager;
#[async_std::main]
async fn main() -> nmrs::Result<()> {
let nm = NetworkManager::new().await?;
- let networks = nm.list_networks().await?;
+ let networks = nm.list_networks(None).await?;
println!("{} networks found", networks.len());
Ok(())
}
@@ -69,7 +69,7 @@ use nmrs::NetworkManager;
fn main() -> nmrs::Result<()> {
smol::block_on(async {
let nm = NetworkManager::new().await?;
- let networks = nm.list_networks().await?;
+ let networks = nm.list_networks(None).await?;
println!("{} networks found", networks.len());
Ok(())
})
@@ -93,7 +93,7 @@ use nmrs::NetworkManager;
glib::MainContext::default().spawn_local(async {
let nm = NetworkManager::new().await.unwrap();
- let networks = nm.list_networks().await.unwrap();
+ let networks = nm.list_networks(None).await.unwrap();
for net in &networks {
println!("{}: {}%", net.ssid, net.strength.unwrap_or(0));
}
diff --git a/docs/src/advanced/dbus.md b/docs/src/advanced/dbus.md
index 5ce8d026..d0ef978a 100644
--- a/docs/src/advanced/dbus.md
+++ b/docs/src/advanced/dbus.md
@@ -65,7 +65,7 @@ nmrs: nm.monitor_network_changes(callback)
When connecting to a network, nmrs builds a settings dictionary and sends it via D-Bus:
```
-nmrs: nm.connect("MyWiFi", WifiSecurity::WpaPsk { psk: "..." })
+nmrs: nm.connect("MyWiFi", None, WifiSecurity::WpaPsk { psk: "..." })
→ Build settings HashMap
→ D-Bus: AddAndActivateConnection(settings, device_path, specific_object)
← D-Bus: Active connection path
diff --git a/docs/src/advanced/logging.md b/docs/src/advanced/logging.md
index 63fad831..d360f38e 100644
--- a/docs/src/advanced/logging.md
+++ b/docs/src/advanced/logging.md
@@ -21,7 +21,7 @@ async fn main() -> nmrs::Result<()> {
env_logger::init();
let nm = NetworkManager::new().await?;
- nm.scan_networks().await?;
+ nm.scan_networks(None).await?;
Ok(())
}
diff --git a/docs/src/advanced/timeouts.md b/docs/src/advanced/timeouts.md
index 03805a41..2a642538 100644
--- a/docs/src/advanced/timeouts.md
+++ b/docs/src/advanced/timeouts.md
@@ -98,7 +98,7 @@ let config = TimeoutConfig::new()
let nm = NetworkManager::with_config(config).await?;
-match nm.connect("SlowNetwork", WifiSecurity::Open).await {
+match nm.connect("SlowNetwork", None, WifiSecurity::Open).await {
Ok(_) => println!("Connected!"),
Err(ConnectionError::Timeout) => {
eprintln!("Connection timed out — try a longer timeout");
diff --git a/docs/src/api/builders.md b/docs/src/api/builders.md
index 9e410cd6..278c747a 100644
--- a/docs/src/api/builders.md
+++ b/docs/src/api/builders.md
@@ -128,17 +128,14 @@ The `build()` method validates all fields and returns `Result nmrs::Result<()> {
let nm = NetworkManager::new().await?;
- nm.connect("MyWiFi", WifiSecurity::Open).await?;
+ nm.connect("MyWiFi", None, WifiSecurity::Open).await?;
Ok(())
}
```
@@ -135,7 +152,7 @@ async fn connect() -> nmrs::Result<()> {
### Specific Error Handling
```rust
-match nm.connect("MyWiFi", security).await {
+match nm.connect("MyWiFi", None, security).await {
Ok(_) => println!("Connected"),
Err(ConnectionError::AuthFailed) => eprintln!("Wrong password"),
Err(ConnectionError::NotFound) => eprintln!("Network not found"),
@@ -151,7 +168,7 @@ use anyhow::{Context, Result};
async fn connect() -> Result<()> {
let nm = NetworkManager::new().await
.context("Failed to connect to NetworkManager")?;
- nm.connect("MyWiFi", WifiSecurity::Open).await
+ nm.connect("MyWiFi", None, WifiSecurity::Open).await
.context("Failed to connect to MyWiFi")?;
Ok(())
}
diff --git a/docs/src/api/models.md b/docs/src/api/models.md
index d7470403..1bca2e16 100644
--- a/docs/src/api/models.md
+++ b/docs/src/api/models.md
@@ -82,6 +82,40 @@ pub struct Network {
}
```
+### AccessPoint
+
+A single AP with per-BSSID details.
+
+```rust
+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()`.
+
+```rust
+pub struct WifiDevice {
+ pub interface: String,
+ pub mac: String,
+ pub state: DeviceState,
+ pub active_ssid: Option,
+}
+```
+
+### 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()`.
@@ -141,13 +175,29 @@ pub enum EapMethod { Peap, Ttls }
pub enum Phase2 { Mschapv2, Pap }
```
+## Radio / Airplane-Mode Models
+
+### RadioState
+
+```rust
+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
-### VpnCredentials
+### WireGuardConfig
+
+WireGuard VPN configuration (replaces the deprecated `VpnCredentials`).
```rust
-pub struct VpnCredentials {
- pub vpn_type: VpnType,
+pub struct WireGuardConfig {
pub name: String,
pub gateway: String,
pub private_key: String,
@@ -159,7 +209,67 @@ pub struct VpnCredentials {
}
```
-Constructors: `new(...)`, `builder()`
+Constructor: `new(name, gateway, private_key, address, peers)`
+Builder methods: `.with_dns(vec)`, `.with_mtu(u32)`, `.with_uuid(uuid)`
+
+### OpenVpnConfig
+
+OpenVPN configuration.
+
+```rust
+pub struct OpenVpnConfig {
+ pub name: String,
+ pub remote: String,
+ pub port: u16,
+ pub tcp: bool,
+ pub auth_type: Option,
+ pub ca_cert: Option,
+ pub client_cert: Option,
+ pub client_key: Option,
+ pub username: Option,
+ pub password: Option,
+ pub compression: Option,
+ pub proxy: Option,
+ // ... 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
+
+```rust
+pub enum OpenVpnAuthType { Password, Tls, PasswordTls, StaticKey }
+```
+
+### OpenVpnCompression
+
+```rust
+pub enum OpenVpnCompression { No, Lzo, Lz4, Lz4V2, Yes }
+```
+
+### OpenVpnProxy
+
+```rust
+pub enum OpenVpnProxy {
+ Http { server, port, username, password, retry },
+ Socks { server, port, retry },
+}
+```
+
+### VpnRoute
+
+```rust
+pub struct VpnRoute {
+ pub dest: String,
+ pub prefix: u32,
+ pub next_hop: Option,
+ pub metric: Option,
+}
+```
+
+Constructor: `new(dest, prefix)`. Builder methods: `.next_hop(gw)`, `.metric(m)`.
### WireGuardPeer
@@ -173,25 +283,107 @@ pub struct WireGuardPeer {
}
```
-### VpnConnection / VpnConnectionInfo
+### VpnType
+
+Protocol-specific metadata decoded from NM settings (data-carrying enum):
+
+```rust
+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
+
+```rust
+pub enum VpnKind { Plugin, WireGuard }
+```
+
+### VpnConnection
+
+A saved or active VPN connection with rich metadata.
```rust
pub struct VpnConnection {
+ pub uuid: String,
+ pub id: String,
pub name: String,
pub vpn_type: VpnType,
pub state: DeviceState,
pub interface: Option,
+ pub active: bool,
+ pub user_name: Option,
+ pub password_flags: VpnSecretFlags,
+ pub service_type: String,
+ pub kind: VpnKind,
}
+```
+### VpnConnectionInfo
+
+Detailed active VPN information.
+
+```rust
pub struct VpnConnectionInfo {
pub name: String,
- pub vpn_type: VpnType,
+ pub vpn_kind: VpnKind,
pub state: DeviceState,
pub interface: Option,
pub gateway: Option,
pub ip4_address: Option,
pub ip6_address: Option,
pub dns_servers: Vec,
+ pub details: Option,
+}
+```
+
+### VpnDetails
+
+```rust
+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
+
+```rust
+pub enum ConnectivityState { Unknown, None, Portal, Limited, Full }
+```
+
+### ConnectivityReport
+
+```rust
+pub struct ConnectivityReport {
+ pub state: ConnectivityState,
+ pub check_enabled: bool,
+ pub check_uri: Option,
+ pub captive_portal_url: Option,
}
```
diff --git a/docs/src/api/network-manager.md b/docs/src/api/network-manager.md
index 544c3322..cd5d018f 100644
--- a/docs/src/api/network-manager.md
+++ b/docs/src/api/network-manager.md
@@ -25,16 +25,41 @@ let config = nm.timeout_config();
| Method | Returns | Description |
|--------|---------|-------------|
-| `scan_networks()` | `Result<()>` | Trigger active Wi-Fi scan |
-| `list_networks()` | `Result>` | List visible networks |
-| `connect(ssid, security)` | `Result<()>` | Connect to a Wi-Fi network |
-| `disconnect()` | `Result<()>` | Disconnect from current network |
+| `scan_networks(interface)` | `Result<()>` | Trigger active Wi-Fi scan (`None` = all devices) |
+| `list_networks(interface)` | `Result>` | List visible networks (`None` = all devices) |
+| `list_access_points(interface)` | `Result>` | 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