Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

network

NetworkManager: connectivity, Wi-Fi and wired state, scanned access points and join progress.

text {
    content = mantle.network:map(function(network)
        if not network.connected then
            return "offline"
        elseif network.ssid == "Ethernet" then
            return "wired"
        end
        local wifi = network.wifi_devices[1] -- the primary interface
        return string.format("%s %d%%", network.ssid or "", wifi and wifi.strength or 0)
    end, ""),
}

State

mantle.network:get() returns NetworkState, nil before the first push. A field marked ? may be absent.

mantle.network’s payload.

FieldTypeDescription
connect_error?JoinErrorThe last failed join on any Wi-Fi device, or nil before any or after a success. Kept until the next connect, cancel_connect or abort_connect; check its ssid before showing it.
connectedbooleanA connection carries the default route; false means offline.
connecting_ssid?stringThe current join’s SSID on any Wi-Fi device, or nil; clears on a verdict or abort_connect.
ethernet_enabledbooleanA wired device is activated; set_ethernet_enabled’s read-back, unlike carrier.
ethernet_ip?stringThe first activated wired device’s IPv4 address without prefix, or nil.
ethernet_presentbooleanAt least one wired device exists, cable or not.
ethernet_speed?integerThat wired device’s link speed in Mb/s; nil when unknown or none is activated.
networking_enabledbooleanNetworkManager networking is on (NetworkingEnabled).
password_ssid?stringThe join SSID awaiting a password from a network/connect secure field on any device, or nil. Also set after a rejected key; cleared when a join starts or by cancel_connect.
ssid?string"Ethernet" when the default route is wired, else the associated SSID, else nil. An association still getting an address has an ssid while connected is false.
vpn_error?VpnErrorThe last failed VPN activation, or nil before any or after the next connect_vpn.
vpn_secret?VpnSecretRequestThe one pending VPN secret request, or nil. Answer it with network/vpn_secret secure fields or drop it with cancel_vpn_secret.
vpnsVpnInfo[]Saved VPN and WireGuard profiles, ordered by name.
wifi_devicesWifiDeviceInfo[]Every Wi-Fi interface, primary first. IDs are interface names, never NetworkManager object paths.
wifi_enabledbooleanWi-Fi radio power (WirelessEnabled); can be true with no Wi-Fi hardware, see wifi_present.
wifi_presentbooleanAt least one Wi-Fi device exists.

AccessPointInfo

One scanned network in available_networks.

FieldTypeDescription
activebooleanThe Wi-Fi device is associated with this SSID.
bandstring"2.4 GHz", "5 GHz", "6 GHz", or empty for a frequency outside those bands.
savedbooleanA saved NetworkManager profile compatible with this device names this SSID.
securebooleanNeeds a key: WEP, WPA or RSN.
ssidstringNetwork name, "" for hidden networks; one entry per SSID, from its strongest access point.
strengthintegerSignal strength, 0 to 100.

JoinError

A failed join, as connect_error.

FieldTypeDescription
messagestringDisplay text, such as "wrong password" or "network not found".
ssidstringThe network the join was for.

VpnError

A failed VPN activation, as vpn_error.

FieldTypeDescription
messagestringDisplay text, such as "login failed" or "connection timed out".
uuidstringThe profile the activation was for.

VpnInfo

One saved NetworkManager VPN profile in vpns.

FieldTypeDescription
activatingbooleanThe profile is being activated, including while it waits for a secret.
activebooleanThe profile is activated.
idstringProfile name.
kindVpnKind
uuidstringProfile UUID; pass it to connect_vpn and disconnect_vpn.

VpnKind

vpns[].kind.

ValueDescription
"vpn"A plugin VPN.
"wireguard"A native WireGuard profile.

VpnSecretRequest

NetworkManager asking for VPN secrets, as vpn_secret.

FieldTypeDescription
fieldsstring[]Secret keys still to enter, such as "password"; each entered key leaves the list.
idstringRequest ID. A secure field for fields[i] uses name = request.id .. "/" .. fields[i].
retrybooleanNetworkManager rejected the previous secrets and asks again.
uuidstringUUID of the profile being activated.
vpn_idstringName of that profile.

WifiDeviceInfo

One Wi-Fi interface, with its own scan, join, address and access points. connected means activated here, while the flat connected means a default route; ssid never uses "Ethernet".

FieldTypeDescription
available_networksAccessPointInfo[]Visible networks on this interface: one per SSID, at most 20, ordered associated, then saved, then strongest.
connect_error?JoinErrorFailed join on this interface, or nil; cleared by the next join or cancel_connect.
connectedbooleanThis interface has an activated connection.
connecting_ssid?stringSSID of this interface’s current join, or nil.
idstringInterface name, such as "wlan0"; pass it to actions ending in _device.
password_ssid?stringSSID awaiting a password on this interface, or nil.
scanningbooleanA scan is in flight on this interface.
ssid?stringAssociated SSID, or nil.
strengthintegerAssociated access point strength, 0 to 100.
wifi_ip?stringIPv4 address without prefix, or nil.

Actions

Call each as mantle.network:<action>(arguments...); ? marks an argument you may omit.

ActionArgumentsDescription
set_networking_enabledenabled: booleanTurns NetworkManager networking on or off.
set_wifi_enabledenabled: booleanPowers the Wi-Fi radio.
set_ethernet_enabledenabled: booleanfalse disconnects every wired device; true activates each one’s autoconnect profile, and a device without one stays down.
scanRequests a Wi-Fi scan; a no-op without Wi-Fi hardware.
scan_deviceid: stringRequests a scan on the named Wi-Fi interface.
connectssid: string, hidden: booleanJoins a network. Without a saved profile, a secured, hidden or out-of-range one sets password_ssid and waits for a key.
connect_devicessid: string, hidden: boolean, id: stringJoins through the named Wi-Fi interface; a removed ID is never retargeted.
cancel_connectDrops the one current password request, on any Wi-Fi device; a join already running continues.
abort_connectStops the one current join on any Wi-Fi device, deleting a profile the join created.
forgetssid: stringDeletes every saved profile for this SSID.
disconnect_wifiDisconnects Wi-Fi; NetworkManager does not autoconnect it again until the next join.
disconnect_wifi_deviceid: stringDisconnects the named Wi-Fi interface.
connect_vpnuuid: stringActivates the saved VPN or WireGuard profile with this UUID. A missing secret raises vpn_secret.
disconnect_vpnuuid: stringDeactivates the VPN or WireGuard profile with this UUID.
cancel_vpn_secretDeclines the pending vpn_secret request, failing that activation.

Backend

ContractBehavior
UpdatesEvery manager, device-list, device-state, access-point, association and saved-profile change re-reads the whole state from NetworkManager. A hotplugged adapter rescans the device set
Deviceswifi_devices lists each Wi-Fi interface by name, primary first, with its association, scan, address and access points; {} without Wi-Fi hardware. Flat join fields describe the one join on any device. Actions without _device use the primary. Wired fields describe the first activated wired device
TogglesNetworking through Enable, Wi-Fi through WirelessEnabled
ScanRequestScan. The device’s scanning turns true on the call and false when LastScan moves or NetworkManager refuses
Access pointsThe associated one’s strength is live. The others’ are read when they appear and after each scan, when NetworkManager updates them
ConnectA saved profile or an open network in range joins at once. Anything else sets password_ssid and waits for the key from a secure_submit = { capability = "network", action = "connect" } field (secure fields); the key never reaches Lua
Join verdictWatched for up to 45 s. A rejected key sets password_ssid again. A new network’s profile, key included, is saved when the join starts and stays after a rejection; a key retyped for a saved profile reaches disk only once NetworkManager accepts it
Abortabort_connect deletes a profile the join created, else deactivates the join
VPN profilesvpns lists saved profiles of type vpn (plugin VPNs) and wireguard, by name. active and activating follow NetworkManager’s active connections, so a VPN started elsewhere shows too. The list needs a profile change or restart to notice an edited profile
VPN activationconnect_vpn(uuid) calls ActivateConnection and watches the verdict for 45 s, not counting time on that profile’s secret prompt; at 45 s it deactivates the attempt. A failure lands in vpn_error; disconnect_vpn or cancel_vpn_secret during the attempt sets none. A second connect_vpn for a profile already waiting does nothing. Only profiles NetworkManager manages: no VPN client the shell runs itself
VPN secretsThe engine registers a NetworkManager secret agent, answering only NetworkManager itself. When an activation needs secrets, vpn_secret lists the fields to enter and waits for secure_submit fields named request.id .. "/" .. field; the last one answers NetworkManager. One request at a time: a second is refused. cancel_vpn_secret or a 120 s wait fails the activation. Nothing is stored; NetworkManager asks again when a secret is agent-owned
VPN secret scopeOnly vpn and wireguard requests are answered; other connection types get “no secrets”, as with no agent. Fields come from NetworkManager’s hints, else from the profile’s <key>-flags entries marked agent-owned or not saved. WireGuard asks for private-key only; a peer’s preshared key and a plugin’s x-vpn-message text are not handled. Registering needs polkit’s org.freedesktop.NetworkManager.network-control; without it a warning is logged and VPN activation still works for secrets NetworkManager already stores. NetworkManager takes one agent per identifier per user, so only the first running shell gets prompts; a second logs the same warning
MissingStays nil. The next generation’s first read retries

Use scan_device(id), connect_device(ssid, hidden, id) and disconnect_wifi_device(id) to target an entry in wifi_devices. An unknown or removed ID is never switched to another device: all three log a warning and return. Without Wi-Fi hardware, scan and disconnect_wifi do nothing; connect reports an error in the flat connect_error field. A password prompt keeps its selected device through submission and activation. Only one password prompt or join attempt is tracked across all devices. Starting another settles the previous join first.

How do I…

Ask for a Wi-Fi password

A click on an available_networks entry of a wifi_devices interface calls mantle.network:connect(entry.ssid, false); a secured one without a saved profile then sets password_ssid. Show the field while it is set. Escape clears a secure field and keeps it armed, then calls its on_cancel, the place to call cancel_connect:

local asking = mantle.network:map(function(network)
    return network.password_ssid ~= nil
end, false)

return column {
    visible = asking,
    spacing = 6,
    children = {
        text {
            content = mantle.network:map(function(network)
                return network.password_ssid and ("Password for " .. network.password_ssid) or ""
            end, ""),
        },
        textfield {
            width = 240,
            height = 24,
            placeholder = "Password",
            secure_submit = { capability = "network", action = "connect" },
            on_cancel = function() mantle.network:cancel_connect() end,
        },
    },
}

List and toggle a VPN

Which profile counts as the preferred one is the shell’s call; vpns carries what it needs:

local vpns = mantle.network:map(function(network) return network.vpns or {} end, {})

return column {
    spacing = 4,
    children = vpns:map(function(list)
        local rows = {}
        for _, vpn in ipairs(list) do
            rows[#rows + 1] = row {
                on_click = function()
                    if vpn.active or vpn.activating then
                        mantle.network:disconnect_vpn(vpn.uuid)
                    else
                        mantle.network:connect_vpn(vpn.uuid)
                    end
                end,
                children = { text { content = vpn.id .. (vpn.active and " (on)" or "") } },
            }
        end
        return rows
    end),
}

Ask for a VPN secret

NetworkManager’s request sets vpn_secret; show one secure field per entry of fields. cancel_vpn_secret drops the whole request, not one field, and fails the activation. Here a first Escape clears the field and a second cancels. retry is true after rejected secrets:

local request = mantle.network:map(function(network) return network.vpn_secret end)

return column {
    visible = request:map(function(secret) return secret ~= nil end),
    spacing = 6,
    children = request:map(function(secret)
        local rows = {}
        for _, field in ipairs(secret and secret.fields or {}) do
            rows[#rows + 1] = textfield {
                width = 240,
                height = 24,
                placeholder = field,
                secure_submit = { capability = "network", action = "vpn_secret", name = secret.id .. "/" .. field },
                on_cancel = function(cleared)
                    if not cleared then mantle.network:cancel_vpn_secret() end
                end,
            }
        end
        return rows
    end),
}

Know whether a join worked

A failed connect lands in connect_error:

text {
    foreground = "#F38BA8",
    content = mantle.network:map(function(network)
        local failure = network.connect_error
        return failure and (failure.ssid .. ": " .. failure.message) or ""
    end, ""),
}

Gotchas

TrapFix
A wifi_devices entry’s available_networks has a row with ssid == ""Hidden networks broadcast no name; they merge into one nameless row. Skip it and join hidden networks with connect(ssid, true)
A hidden network asks for a password even when openIts security is unknown until it answers. Enter on the empty field joins it as open

Source: supervisor/src/capabilities/network/