* Add hotspot contract layer with capability-gated dispatch. Establish the interface and manager plumbing for hotspot support so backends can opt in without expanding the core Backend contract. Only backends that implement HotspotBackend get hotspot state propagated; others are forced to unsupported regardless of what they self-report. * Add IPC handlers and API docs for hotspot actions. Wire up configure/ start/ stop hotspot through the request router so the QML service layer can drive hotspot operations. Bump API version to 27 and document the capability-gating contract clients should follow. * Implement NetworkManager hotspot backend. Implement HotspotBackend on NetworkManagerBackend with DMS-owned profile management, AP capability detection, and band validation. Device resolution is deferred to StartHotspot when no device is specified, so profiles survive hardware changes. * Isolate client Wi-Fi state from AP-mode connections. Filter AP-mode profiles and access points out of all client Wi-Fi paths so the DMS hotspot (and user-created APs) never appear as saved networks, visible networks, or connected state. This protects existing client behavior before hotspot controls are exposed in the UI. * Prefer idle radios for automatic hotspot device selection. * Add hotspot properties and methods to QML service layer. Expose hotspot state and actions through DMSNetworkService, NetworkService, and LegacyNetworkService. Capability is gated on API version and backend-reported support, not backend name checks, and stays stable when Wi-Fi radio is disabled. * Add hotspot controls to Settings and Control Center. Settings shows a hotspot setup card as a sibling in the Wi-Fi tab with SSID, password, device, band, save, and start/ stop controls. Control Center shows a compact row that toggles a configured hotspot or routes to Settings for initial setup. Both stay visible when Wi-Fi is disabled, explaining the requirement instead of hiding. * Add translator context to hotspot strings.
18 KiB
NetworkManager API Documentation
Overview
The network manager API provides methods for managing WiFi connections, monitoring network state, and handling credential prompts through NetworkManager or iwd (and systemd-networkd for ethernet only). Communication occurs over a message-based protocol (websocket, IPC, etc.) with event subscriptions for state updates.
API Methods
network.hotspot.configure
Create or update the DMS-managed hotspot profile. Hotspot support is capability-gated: clients should require API v28+, hotspotSupported: true, and hotspotAvailable: true from network state before showing hotspot controls.
For this implementation, only hotspot-capable backends such as NetworkManager should accept this action. Unsupported backends return an error such as hotspot not supported by active network backend.
Configuration changes are rejected while the DMS hotspot is active or activating; stop it before updating the profile.
Request:
{
"method": "network.hotspot.configure",
"params": {
"ssid": "Dank Hotspot",
"password": "optional-password",
"device": "wlan0",
"band": "bg"
}
}
Parameters:
ssid(string, required): Hotspot SSID to advertise.password(string, optional): WPA-PSK password. Omit for an open hotspot when the backend allows it.device(string, optional): Wi-Fi interface name to use, for examplewlan0. When omitted, the backend picks an AP-capable radio at start time, preferring one that is already hosting the hotspot, then an idle radio, and only as a last resort a radio carrying an active connection (which NetworkManager will disconnect). Network state exposesapCapableon eachwifiDevicesentry so clients can predict this choice.band(string, optional): Requested NetworkManager band:bgfor 2.4GHz orafor 5GHz.
Response:
{
"success": true,
"message": "hotspot configured"
}
network.hotspot.start
Start the previously configured DMS-managed hotspot profile. This action does not accept or require SSID/password parameters; call network.hotspot.configure first when changing hotspot settings.
A successful response only means the activation was requested; the outcome is reported asynchronously through network state updates. While activation is in flight, hotspotActivating is true; on success hotspotEnabled becomes true; on failure hotspotActivating returns to false and hotspotLastError carries one of hotspot-ip-config-failed (IP sharing setup failed, commonly a missing dnsmasq, which NetworkManager's shared IPv4 method requires), hotspot-supplicant-failed (the Wi-Fi driver could not start AP mode), or hotspot-failed. hotspotLastError is cleared on the next successful start.
Request:
{
"method": "network.hotspot.start"
}
Response:
{
"success": true,
"message": "hotspot started"
}
network.hotspot.stop
Stop the active DMS-managed hotspot connection. It must not stop arbitrary user-created hotspot profiles.
Request:
{
"method": "network.hotspot.stop"
}
Response:
{
"success": true,
"message": "hotspot stopped"
}
network.hotspot.getSecrets
Retrieve the stored password of the DMS-managed hotspot profile, for prefilling edit forms. Returns an empty string for an open hotspot. Network state exposes hotspotSecured so clients can tell an open hotspot apart from a secured one without fetching the secret.
Request:
{
"method": "network.hotspot.getSecrets"
}
Response:
{
"password": "the-stored-psk"
}
network.wifi.connect
Initiate a WiFi connection.
Request:
{
"method": "network.wifi.connect",
"params": {
"ssid": "NetworkName",
"password": "optional-password",
"interactive": true
}
}
Parameters:
ssid(string, required): Network SSIDpassword(string, optional): Pre-shared key for WPA/WPA2/WPA3 networksinteractive(boolean, optional): Enable credential prompting if authentication fails or password is missing. Automatically set totruewhen connecting to secured networks without providing a password.
Response:
{
"success": true,
"message": "connecting"
}
Behavior:
- Returns immediately; connection happens asynchronously
- State updates delivered via
networkservice subscription - Credential prompts delivered via
network.credentialsservice subscription
network.credentials.submit
Submit credentials in response to a prompt.
Request:
{
"method": "network.credentials.submit",
"params": {
"token": "correlation-token",
"secrets": {
"psk": "password"
},
"save": true
}
}
Parameters:
token(string, required): Token from credential promptsecrets(object, required): Key-value map of credential fieldssave(boolean, optional): Whether to persist credentials (default: false)
Common secret fields:
psk: Pre-shared key for WPA2/WPA3 personal networksidentity: Username for 802.1X enterprise networkspassword: Password for 802.1X enterprise networks
network.credentials.cancel
Cancel a credential prompt.
Request:
{
"method": "network.credentials.cancel",
"params": {
"token": "correlation-token"
}
}
Event Subscriptions
Subscribing to Events
Subscribe to receive network state updates and credential prompts:
{
"method": "subscribe",
"params": {
"services": ["network", "network.credentials"]
}
}
Both services are required for full connection handling. Missing network.credentials means credential prompts won't be received.
network Service Events
State updates are sent whenever network configuration changes:
{
"service": "network",
"data": {
"networkStatus": "wifi",
"isConnecting": false,
"connectingSSID": "",
"wifiConnected": true,
"wifiSSID": "MyNetwork",
"wifiIP": "192.168.1.100",
"lastError": ""
}
}
State fields:
networkStatus: Current connection type (wifi,ethernet,disconnected)isConnecting: Whether a connection attempt is in progressconnectingSSID: SSID being connected to (empty when idle)wifiConnected: Whether associated with an access pointwifiSSID: Currently connected network namewifiIP: Assigned IP address (empty until DHCP completes)savedWifiNetworks(API v26+): Saved WiFi profiles exposed at SSID granularity. If a backend has multiple profiles for the same SSID, DMS merges them into one SSID-level entry. Clients talking to older servers should derive saved visible networks fromwifiNetworksentries wheresavedis true.savedWifiNetworks[].outOfRange(API v26+): Whether the saved profile is not currently visible in scan results. Fallback entries derived fromwifiNetworksshould be treated as visible (outOfRange: false).hotspotSupported(API v28+): Whether the active backend implements hotspot actions.hotspotAvailable(API v28+): Whether hotspot support is usable on this backend/device set. For NetworkManager this means at least one AP-capable managed Wi-Fi device exists, independent of Wi-Fi radio enabled state.hotspotConfigured(API v28+): Whether the DMS-managed hotspot profile exists.hotspotEnabled(API v28+): Whether the DMS-managed hotspot profile is currently active.hotspotActivating(API v28+): Whether hotspot activation is currently in progress.hotspotSecured(API v28+): Whether the configured hotspot uses password-based security.hotspotSSID(API v28+): Configured DMS hotspot SSID.hotspotDevice(API v28+): Optional configured Wi-Fi device for the DMS hotspot.hotspotBand(API v28+): Optional configured hotspot band (bgora).hotspotLastError(API v28+): Machine-readable error from the most recent failed hotspot activation. Cleared when the next start succeeds.lastError: Error message from last failed connection attempt
network.credentials Service Events
Credential prompts are sent when authentication is required:
{
"service": "network.credentials",
"data": {
"token": "unique-prompt-id",
"ssid": "NetworkName",
"setting": "802-11-wireless-security",
"fields": ["psk"],
"hints": ["wpa3", "sae"],
"reason": "Credentials required"
}
}
Prompt fields:
token: Unique identifier for this prompt (use in submit/cancel)ssid: Network requesting credentialssetting: Authentication type (802-11-wireless-securityfor personal WiFi,802-1xfor enterprise)fields: Array of required credential field nameshints: Additional context about the network typereason: Human-readable explanation (e.g., "Previous password was incorrect")
Connection Flow
Typical Timeline
T+0ms Call network.wifi.connect
T+10ms Receive {"success": true, "message": "connecting"}
T+100ms State update: isConnecting=true, connectingSSID="Network"
T+500ms Credential prompt (if needed)
T+1000ms Submit credentials
T+3000ms State update: wifiConnected=true, wifiIP="192.168.x.x"
State Machine
IDLE
|
| network.wifi.connect
v
CONNECTING (isConnecting=true, connectingSSID set)
|
+-- Needs credentials
| |
| v
| PROMPTING (credential prompt event)
| |
| | network.credentials.submit
| v
| back to CONNECTING
|
+-- Success
| |
| v
| CONNECTED (wifiConnected=true, wifiIP set, isConnecting=false)
|
+-- Failure
|
v
ERROR (isConnecting=false, !wifiConnected, lastError set)
Connection Success Detection
A connection is successful when all of the following are true:
wifiConnectedistruewifiIPis set and non-emptywifiSSIDmatches the target networkisConnectingisfalse
Do not rely on wifiConnected alone - the device may be associated with an access point but not have an IP address yet.
Example:
function isConnectionComplete(state, targetSSID) {
return state.wifiConnected &&
state.wifiIP &&
state.wifiIP !== "" &&
state.wifiSSID === targetSSID &&
!state.isConnecting;
}
Error Handling
Error Detection
Errors occur when a connection attempt stops without success:
function checkForFailure(state, wasConnecting, targetSSID) {
// Was connecting, now idle, but not connected
if (wasConnecting &&
!state.isConnecting &&
state.connectingSSID === "" &&
!state.wifiConnected) {
return state.lastError || "Connection failed";
}
return null;
}
Common Error Scenarios
Wrong Password
Detection methods:
- Quick failure (< 3 seconds from start)
lastErrorcontains "password", "auth", or "secrets"- Second credential prompt with
reason: "Previous password was incorrect"
Handling:
if (prompt.reason === "Previous password was incorrect") {
// Show error, clear password field, re-focus input
}
Network Out of Range
Detection:
lastErrorcontains "not-found" or "connection-attempt-failed"
Connection Timeout
Detection:
isConnectingremains true for > 30 seconds
Implementation:
let timeout = setTimeout(() => {
if (currentState.isConnecting) {
handleTimeout();
}
}, 30000);
DHCP Failure
Detection:
wifiConnectedis truewifiIPis empty after 15+ seconds
Error Message Translation
Map technical errors to user-friendly messages:
| lastError value | Meaning | User message |
|---|---|---|
secrets-required |
Password needed | "Please enter password" |
authentication-failed |
Wrong password | "Incorrect password" |
connection-removed |
Profile deleted | "Network configuration removed" |
connection-attempt-failed |
Generic failure | "Failed to connect" |
network-not-found |
Out of range | "Network not found" |
(timeout) |
Timeout | "Connection timed out" |
Credential Handling
Secret Agent Architecture
The credential system uses a broker pattern:
NetworkManager -> SecretAgent -> PromptBroker -> UI -> User
^
|
User Response
|
NetworkManager <- SecretAgent <- PromptBroker <- UI
Implementing a Broker
type CustomBroker struct {
ui UIInterface
pending map[string]chan network.PromptReply
}
func (b *CustomBroker) Ask(ctx context.Context, req network.PromptRequest) (string, error) {
token := generateToken()
b.pending[token] = make(chan network.PromptReply, 1)
// Send to UI
b.ui.ShowCredentialPrompt(token, req)
return token, nil
}
func (b *CustomBroker) Wait(ctx context.Context, token string) (network.PromptReply, error) {
select {
case <-ctx.Done():
return network.PromptReply{}, errors.New("timeout")
case reply := <-b.pending[token]:
return reply, nil
}
}
func (b *CustomBroker) Resolve(token string, reply network.PromptReply) error {
if ch, ok := b.pending[token]; ok {
ch <- reply
close(ch)
delete(b.pending, token)
}
return nil
}
Credential Field Types
Personal WiFi (802-11-wireless-security):
- Fields:
["psk"] - UI: Single password input
Enterprise WiFi (802-1x):
- Fields:
["identity", "password"] - UI: Username and password inputs
Building Secrets Object
function buildSecrets(setting, fields, formData) {
let secrets = {};
if (setting === "802-11-wireless-security") {
secrets.psk = formData.password;
} else if (setting === "802-1x") {
secrets.identity = formData.username;
secrets.password = formData.password;
}
return secrets;
}
Best Practices
Track Target Network
Always store which network you're connecting to:
let targetSSID = null;
function connect(ssid) {
targetSSID = ssid;
// send request
}
function onStateUpdate(state) {
if (!targetSSID) return;
if (state.wifiSSID === targetSSID && state.wifiConnected && state.wifiIP) {
// Success for the network we care about
targetSSID = null;
}
}
Implement Timeouts
Never wait indefinitely for a connection:
const CONNECTION_TIMEOUT = 30000; // 30 seconds
const DHCP_TIMEOUT = 15000; // 15 seconds
let timer = setTimeout(() => {
if (stillConnecting) {
handleTimeout();
}
}, CONNECTION_TIMEOUT);
Handle Credential Re-prompts
Wrong passwords trigger a second prompt:
function onCredentialPrompt(prompt) {
if (prompt.reason.includes("incorrect")) {
// Show error, but keep dialog open
showError("Wrong password");
clearPasswordField();
} else {
// First time prompt
showDialog(prompt);
}
}
Clean Up State
Reset tracking variables on success, failure, or cancellation:
function cleanup() {
clearTimeout(timer);
targetSSID = null;
closeDialogs();
}
Subscribe to Both Services
Missing network.credentials means prompts won't arrive:
// Correct
services: ["network", "network.credentials"]
// Wrong - will miss credential prompts
services: ["network"]
Testing
Connection Test Checklist
- Connect to open network
- Connect to WPA2 network with password provided
- Connect to WPA2 network without password (triggers prompt)
- Enter wrong password (verify error and re-prompt)
- Cancel credential prompt
- Connection timeout after 30 seconds
- DHCP timeout detection
- Network out of range
- Reconnect to already-configured network
Verifying Secret Agent Setup
Check connection profile flags:
nmcli connection show "NetworkName" | grep flags
# Should show: 802-11-wireless-security.psk-flags: 1 (agent-owned)
Check agent registration in logs:
INFO: Registered with NetworkManager as secret agent
Security
- Never log credential values (passwords, PSKs)
- Clear password fields when dialogs close
- Implement prompt timeouts (default: 2 minutes)
- Validate user input before submission
- Use secure channels for credential transmission
Troubleshooting
Credential prompt doesn't appear
Check:
- Subscribed to both
networkandnetwork.credentials - Connection has
interactive: true - Secret flags set to AGENT_OWNED (value: 1)
- Broker registered successfully
Connection succeeds without prompting
Cause: NetworkManager found saved credentials
Solution: Delete existing connection first, or use different credentials
State updates seem delayed
Expected behavior: State changes occur in rapid succession during connection
Solution: Debounce UI updates; only act on final state
Multiple rapid credential prompts
Cause: Connection profile has incorrect flags or conflicting agents
Solution:
- Check only one agent is running
- Verify psk-flags value
- Check NetworkManager logs for agent conflicts
Data Structures Reference
PromptRequest
type PromptRequest struct {
SSID string `json:"ssid"`
SettingName string `json:"setting"`
Fields []string `json:"fields"`
Hints []string `json:"hints"`
Reason string `json:"reason"`
}
PromptReply
type PromptReply struct {
Secrets map[string]string `json:"secrets"`
Save bool `json:"save"`
Cancel bool `json:"cancel"`
}
NetworkState
type NetworkState struct {
NetworkStatus string `json:"networkStatus"`
IsConnecting bool `json:"isConnecting"`
ConnectingSSID string `json:"connectingSSID"`
WifiConnected bool `json:"wifiConnected"`
WifiSSID string `json:"wifiSSID"`
WifiIP string `json:"wifiIP"`
LastError string `json:"lastError"`
}