@vasakgroup/plugin-network-manager
v2.1.3
Published
A Tauri plugin to manage network connections using networkmanager and systemd-networkd.
Readme
Tauri Network Manager Plugin
Linux-first Tauri plugin to manage network state, Wi-Fi, and VPN through NetworkManager over D-Bus.
Table of Contents
- Features
- Architecture
- Requirements
- Installation
- Permissions
- Quick Start
- API Reference
- TypeScript Usage
- Event System
- Error Handling
- Caching
- Types
- Package Exports
- Testing & Build
- Contributing
Features
Wi-Fi
- Scan & list nearby access points with signal strength and security detection
- Connect to open / WEP / WPA-PSK / WPA2-PSK / WPA3-PSK / WPA-EAP networks
- Disconnect from current Wi-Fi
- List and delete saved Wi-Fi connections
- Request explicit scans
VPN
- List, create, update, delete VPN profiles (OpenVPN, WireGuard, L2TP, PPTP, SSTP, IKEv2, FortiSSL, OpenConnect, Generic)
- Connect / disconnect VPN by UUID
- Read current VPN status, gateway, IP configuration
Network State
- Read current active network (Ethernet / Wi-Fi) — SSID, IP, MAC, signal, security
- Enable / disable wireless and global networking
- Check wireless hardware availability
Monitoring
- Bandwidth stats (download/upload speed, total bytes, uptime)
- Real-time events via Tauri's event system:
network-changed— network state transitionvpn-changed,vpn-connected,vpn-disconnected,vpn-failed
Architecture
flowchart TB
subgraph Frontend["Tauri Frontend (TypeScript)"]
TS["guest-js/index.ts<br/>invoke() / listen()"]
end
subgraph IPC["Tauri Bridge"]
IPC_BRIDGE["Plugin IPC"]
end
subgraph Rust["Rust Plugin (tauri-plugin-network-manager)"]
direction TB
CMDS["commands.rs<br/>19 #[tauri::command]"]
DESKTOP["desktop.rs<br/>D-Bus logic"]
HELPERS["nm_helpers.rs<br/>security detection"]
STATS["network_stats.rs<br/>bandwidth tracker"]
LIB["lib.rs<br/>state mgmt · caching<br/>event emitter"]
ERR["error.rs<br/>typed errors"]
MODELS["models.rs<br/>data structures"]
CMDS --> DESKTOP
DESKTOP --> HELPERS
DESKTOP --> STATS
LIB --> CMDS
DESKTOP --> ERR
DESKTOP --> MODELS
end
subgraph NM["NetworkManager (D-Bus System Bus)"]
NM_MAIN["org.freedesktop.NetworkManager<br/>AddAndActivateConnection<br/>ActivateConnection<br/>DeactivateConnection"]
NM_SETTINGS["org.freedesktop.NetworkManager.Settings<br/>AddConnection · Update · Delete<br/>ListConnections"]
NM_DEVICE["org.freedesktop.NetworkManager.Device<br/>DeviceType · HwAddress · Ip4Config"]
NM_WIFI["org.freedesktop.NetworkManager.Device.Wireless<br/>AccessPoints · ActiveAccessPoint<br/>RequestScan"]
NM_AP["org.freedesktop.NetworkManager.AccessPoint<br/>Ssid · Strength · Flags<br/>WpaFlags · RsnFlags · KeyMgmt"]
NM_ACTIVE["org.freedesktop.NetworkManager.Connection.Active<br/>State · Type · Devices · Uuid"]
end
Frontend <--> IPC
IPC <--> Rust
DESKTOP -- "zbus 4 (blocking)" --> NM_MAIN
DESKTOP --> NM_SETTINGS
DESKTOP --> NM_DEVICE
DESKTOP --> NM_WIFI
DESKTOP --> NM_AP
DESKTOP --> NM_ACTIVEKey modules
| Module | Responsibility |
|---|---|
| lib.rs | Tauri plugin entry point, state management (Arc<RwLock>), network change event emitter with 250 ms debounce |
| desktop.rs | All D-Bus calls via zbus 4: network state, Wi-Fi scan/connect, VPN CRUD, signal listening |
| commands.rs | 19 #[tauri::command] functions bridging IPC to desktop.rs |
| nm_helpers.rs | Security type detection (KeyMgmt / WpaFlags / RsnFlags), SSID byte→string conversion, connectivity check |
| network_stats.rs | Bandwidth tracker reading /sys/class/net/<iface>/statistics |
| error.rs | Typed error enum with thiserror, D-Bus error mapping |
| models.rs | All data structures: NetworkInfo, WiFiConnectionConfig, VpnProfile, etc. |
Requirements
- Linux with NetworkManager running on D-Bus system bus
- Tauri 2 project
- Rust 1.77.2+
- System D-Bus libraries (
libdbus-1-devor equivalent)
Installation
Cargo
[dependencies]
tauri-plugin-network-manager = { git = "https://github.com/Vasak-OS/tauri-plugin-network-manager" }NPM (guest bindings)
bun add @vasakgroup/plugin-network-manager
# or
npm install @vasakgroup/plugin-network-manager
# or
pnpm add @vasakgroup/plugin-network-managerRegister the plugin
// src-tauri/src/lib.rs
fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_network_manager::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}Permissions
By default the plugin allows read + Wi-Fi connect/disconnect operations. VPN mutations are opt-in.
Default permission set
// src-tauri/capabilities/default.json
{
"identifier": "default",
"windows": ["main"],
"permissions": [
"network-manager:default"
]
}The default set includes:
get-network-state,list-wifi-networks,rescan-wificonnect-to-wifi,disconnect-from-wifiget-saved-wifi-networks,delete-wifi-connectiontoggle-network-state,get-wireless-enabled,set-wireless-enabledis-wireless-available,list-vpn-profiles,get-vpn-status
VPN management permission
{
"permissions": [
"network-manager:vpn_management",
// or individually:
"network-manager:allow-connect-vpn",
"network-manager:allow-disconnect-vpn",
"network-manager:allow-create-vpn-profile",
"network-manager:allow-update-vpn-profile",
"network-manager:allow-delete-vpn-profile"
]
}Quick Start
Wi-Fi — scan and connect
import {
listWifiNetworks,
connectToWifi,
disconnectFromWifi,
WiFiSecurityType,
} from '@vasakgroup/plugin-network-manager';
// 1. Scan available networks
const networks = await listWifiNetworks();
console.log(networks.map(n => `${n.ssid} (${n.signal_strength}%)`));
// 2. Connect to a WPA2-PSK network
await connectToWifi({
ssid: 'MyNetwork',
password: 'supersecret',
securityType: WiFiSecurityType.WPA2_PSK,
});
// 3. Disconnect
await disconnectFromWifi();VPN — create, connect, and listen
import {
createVpnProfile,
connectVpn,
getVpnStatus,
disconnectVpn,
deleteVpnProfile,
} from '@vasakgroup/plugin-network-manager';
import { listen } from '@tauri-apps/api/event';
import type { VpnEventPayload } from '@vasakgroup/plugin-network-manager';
// Listen for VPN state changes
const unlisten = await listen<VpnEventPayload>('vpn-changed', (e) => {
console.log('VPN state:', e.payload.status.state);
});
// Create and connect
const profile = await createVpnProfile({
id: 'Office VPN',
vpn_type: 'wire-guard',
settings: { endpoint: 'vpn.example.com:51820' },
secrets: { private_key: '***' },
});
await connectVpn(profile.uuid);
console.log(await getVpnStatus());
// Cleanup
await disconnectVpn(profile.uuid);
await deleteVpnProfile(profile.uuid);
unlisten();Network monitoring
import { getNetworkStats } from '@vasakgroup/plugin-network-manager';
const stats = await getNetworkStats();
console.log({
download: `${stats.download_speed} B/s`,
upload: `${stats.upload_speed} B/s`,
totalDown: `${(stats.total_downloaded / 1e6).toFixed(1)} MB`,
uptime: `${stats.connection_duration}s`,
});API Reference
getCurrentNetworkState(): Promise<NetworkInfo>
Returns the currently active network connection info, or a default if disconnected.
listWifiNetworks(options?: ListWifiNetworksOptions): Promise<NetworkInfo[]>
Scans and returns all visible access points, deduplicated by SSID, sorted by signal strength.
| Option | Type | Default | Description |
|---|---|---|---|
| forceRefresh | boolean | false | Bypass in-memory cache |
| ttlMs | number | 3000 | Cache TTL in milliseconds (250–30000) |
rescanWifi(): Promise<NetworkInfo[]>
Triggers a RequestScan D-Bus call on all wireless devices and returns fresh results.
connectToWifi(config: ConnectToWifiInput | WiFiConnectionConfig): Promise<void>
Accepts both camelCase (frontend-friendly) and snake_case (Rust wire) formats.
| Field | Type | Required | Description |
|---|---|---|---|
| ssid | string | yes | Network SSID |
| password | string | no | PSK / password |
| securityType / security_type | WiFiSecurityType | yes | See WiFiSecurityType |
| username | string | no | WPA-EAP identity |
Supported security types:
| Value | D-Bus key-mgmt | Notes |
|---|---|---|
| None | — | Open network |
| Wep | none | Static WEP with wep-key0 |
| WpaPsk | wpa-psk | WPA1-PSK |
| Wpa2Psk | wpa-psk + proto=["rsn"] | WPA2-PSK |
| Wpa3Psk | sae | WPA3-SAE |
| WpaEap | wpa-eap | Enterprise WPA-EAP |
disconnectFromWifi(): Promise<void>
Deactivates the active Wi-Fi connection.
getSavedWifiNetworks(): Promise<NetworkInfo[]>
Returns saved Wi-Fi profiles (known networks) from NM settings, with SSID and detected security type.
deleteWifiConnection(ssid: string): Promise<void>
Deletes the first saved connection matching the given SSID.
toggleNetwork(enabled: boolean): Promise<boolean>
Enables or disables all networking. Returns the new state.
getWirelessEnabled(): Promise<boolean>
Returns whether Wi-Fi radio is enabled.
setWirelessEnabled(enabled: boolean): Promise<void>
Enables or disables Wi-Fi radio.
isWirelessAvailable(): Promise<boolean>
Returns true if at least one Wi-Fi adapter is present.
listVpnProfiles(): Promise<VpnProfile[]>
Returns all VPN profiles sorted by ID.
getVpnStatus(): Promise<VpnStatus>
Returns the current active VPN state, profile info, gateway, and IP.
connectVpn(uuid: string): Promise<void>
Activates a VPN by UUID. Throws VpnAlreadyConnected if already connected to the same profile.
disconnectVpn(uuid?: string): Promise<void>
Deactivates a VPN. If uuid is omitted, deactivates the currently active VPN (VpnNotActive if none).
createVpnProfile(config: VpnCreateInput): Promise<VpnProfile>
Creates a new VPN profile in NM settings. Requires vpn_management permission.
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | yes | Profile display name |
| vpn_type | VpnType | yes | See VpnType |
| autoconnect | boolean | no | Auto-connect on boot |
| username | string | no | VPN username |
| gateway | string | no | Remote gateway address |
| ca_cert_path | string | no | CA certificate path |
| user_cert_path | string | no | User certificate path |
| private_key_path | string | no | Private key path |
| private_key_password | string | no | Private key passphrase |
| settings | Record<string, string> | no | Custom VPN settings |
| secrets | Record<string, string> | no | Custom VPN secrets (e.g. psk, private_key) |
updateVpnProfile(config: VpnUpdateInput): Promise<VpnProfile>
Updates an existing VPN profile. All fields except uuid are optional.
| Field | Type | Description |
|---|---|---|
| uuid | string | Profile UUID to update (required) |
| id | string | New display name |
| autoconnect | boolean | New auto-connect preference |
| username | string | New username |
| gateway | string | New remote address |
| ca_cert_path | string | New CA cert path |
| user_cert_path | string | New user cert path |
| private_key_path | string | New private key path |
| private_key_password | string | New passphrase |
| settings | Record<string, string> | Merged into existing settings |
| secrets | Record<string, string> | Merged into existing secrets |
deleteVpnProfile(uuid: string): Promise<void>
Deletes a VPN profile by UUID.
getNetworkStats(): Promise<NetworkStats>
Returns bandwidth statistics for the active interface.
interface NetworkStats {
download_speed: number; // bytes/sec
upload_speed: number; // bytes/sec
total_downloaded: number; // bytes
total_uploaded: number; // bytes
connection_duration: number; // seconds
interface: string; // e.g. "wlan0"
}getNetworkInterfaces(): Promise<string[]>
Returns all non-loopback network interface names.
Event System
The plugin emits Tauri events when network state changes, with a 250 ms debounce window to coalesce rapid transitions.
Events
| Event | Payload | When |
|---|---|---|
| network-changed | NetworkInfo | Any network state transition |
| vpn-changed | VpnEventPayload | VPN state transition |
| vpn-connected | VpnEventPayload | Transition to Connected |
| vpn-disconnected | VpnEventPayload | Transition to Disconnected |
| vpn-failed | VpnEventPayload | Transition to Failed |
Example
import { listen } from '@tauri-apps/api/event';
import type { NetworkInfo, VpnEventPayload } from '@vasakgroup/plugin-network-manager';
// Network changes
const unlistenNet = await listen<NetworkInfo>('network-changed', (event) => {
const n = event.payload;
const status = n.is_connected ? `connected to ${n.ssid}` : 'disconnected';
console.log(`[${n.connection_type}] ${status} (${n.ip_address})`);
});
// VPN failures
const unlistenFail = await listen<VpnEventPayload>('vpn-failed', (event) => {
console.error(`VPN failed: ${event.payload.reason}`);
});
// Cleanup when leaving component
// unlistenNet();
// unlistenFail();Error Handling
All commands throw a typed NetworkManagerError with a code property:
import { connectToWifi, NetworkManagerErrorCode } from '@vasakgroup/plugin-network-manager';
try {
await connectToWifi({ ssid: '...', password: '...', securityType: 'wpa2-psk' });
} catch (e) {
if (e.code === NetworkManagerErrorCode.CONNECTION_FAILED) {
showToast('Connection failed. Check password.');
} else if (e.code === NetworkManagerErrorCode.UNSUPPORTED_SECURITY) {
showToast('This security type is not supported.');
} else {
showToast(`Unexpected error: ${e.message}`);
}
}Error codes
| Code | Meaning |
|---|---|
| NOT_INITIALIZED | Plugin not initialized |
| NO_CONNECTION | No active network |
| PERMISSION_DENIED | Capability not granted |
| UNSUPPORTED_SECURITY | Security type not implemented |
| CONNECTION_FAILED | D-Bus connection call failed |
| NETWORK_NOT_FOUND | SSID not in saved networks |
| OPERATION_FAILED | Generic failure |
| VPN_PROFILE_NOT_FOUND | UUID not in settings |
| VPN_ALREADY_CONNECTED | Already connected to that profile |
| VPN_AUTH_FAILED | Secret/authentication error |
| VPN_INVALID_CONFIG | Bad profile parameters |
| VPN_ACTIVATION_FAILED | ActivateConnection D-Bus error |
| VPN_PLUGIN_UNAVAILABLE | NM plugin not found |
| VPN_NOT_ACTIVE | No active VPN to disconnect |
| UNKNOWN | Fallback |
Caching
Wi-Fi scan results are cached in-memory for 3 seconds by default to avoid redundant D-Bus calls.
// Bypass cache
const fresh = await listWifiNetworks({ forceRefresh: true });
// Custom TTL (500 ms)
const fast = await listWifiNetworks({ ttlMs: 500 });
// rescanWifi always invalidates cache and returns fresh dataTypes
WiFiSecurityType
type WiFiSecurityType =
| 'none'
| 'wep'
| 'wpa-psk'
| 'wpa-eap'
| 'wpa2-psk'
| 'wpa3-psk';VpnType
type VpnType =
| 'open-vpn'
| 'wire-guard'
| 'l2tp'
| 'pptp'
| 'sstp'
| 'ikev2'
| 'fortisslvpn'
| 'open-connect'
| 'generic';VpnConnectionState
type VpnConnectionState =
| 'disconnected'
| 'connecting'
| 'connected'
| 'disconnecting'
| 'failed'
| 'unknown';NetworkInfo
interface NetworkInfo {
name: string;
ssid: string;
connection_type: string; // "wifi" | "Ethernet" | "Unknown"
icon: string; // icon name for the UI
ip_address: string; // "0.0.0.0" when disconnected
mac_address: string;
signal_strength: number; // 0–100
security_type: WiFiSecurityType;
is_connected: boolean;
}VpnProfile
interface VpnProfile {
id: string;
uuid: string;
vpn_type: VpnType;
interface_name: string | null;
autoconnect: boolean;
editable: boolean;
last_error: string | null;
}VpnStatus
interface VpnStatus {
state: VpnConnectionState;
active_profile_id: string | null;
active_profile_uuid: string | null;
active_profile_name: string | null;
ip_address: string | null;
gateway: string | null;
since_unix_ms: number | null;
}VpnEventPayload
interface VpnEventPayload {
status: VpnStatus;
profile: VpnProfile | null;
reason: string | null; // populated on 'vpn-failed'
}Package Exports
The NPM package provides ESM, CJS, and TypeScript declarations:
{
"main": "./dist-js/index.cjs",
"module": "./dist-js/index.js",
"types": "./dist-js/index.d.ts",
"exports": {
"types": "./dist-js/index.d.ts",
"import": "./dist-js/index.js",
"require": "./dist-js/index.cjs"
}
}Testing & Build
# Build the Rust crate
cargo build
# Run Rust tests
cargo test
# Build NPM guest package
bun run build
# Run smoke tests (requires Tauri project)
bun run testContributing
- Fork the repository
- Create a feature branch (
git checkout -b feat/my-feature) - Commit your changes (
git commit -am 'feat: add ...') - Push (
git push origin feat/my-feature) - Open a Pull Request
Development tips
- Use
RUST_LOG=debugorRUST_LOG=traceto enable D-Bus call logging - Monitor NM D-Bus traffic:
busctl monitor org.freedesktop.NetworkManager - Test Wi-Fi connection flow:
nmcli dev wifi listthennmcli connection add ...
License
GPL-3.0-or-later — see LICENSE.
