Skip to content

Commit b8eaf5d

Browse files
martinpittclaude
andcommitted
networkmanager: Show error when connecting without stored password
When connecting to a Wi-Fi network with a saved connection but no stored password (no `psk=` in `.nmconnection` file), the Networking page did not show any error or other visible status change. Sadly, the NetworkManager API does not make this easy: It transitions through device states (via D-Bus signals) very quickly (PREPARE → CONFIG → NEED_AUTH → FAILED → DISCONNECTED), and React batches all these property updates together. By the time the `useEffect` runs, the device has already returned to DISCONNECTED state and the interesting failure reason (NM_DEVICE_STATE_REASON_NO_SECRETS) already was reset and the state is just NM_DEVICE_STATE_DISCONNECTED. Fix this by capturing the failure reason in the D-Bus signal handler and storing it on the device object. The UI can then retrieve and consume this cached reason when checking for failures. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent a97eaff commit b8eaf5d

File tree

3 files changed

+90
-4
lines changed

3 files changed

+90
-4
lines changed

pkg/networkmanager/interfaces.js

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { show_modal_dialog } from "cockpit-components-dialog.jsx";
1717

1818
const _ = cockpit.gettext;
1919

20-
function show_error_dialog(title, message) {
20+
export function show_error_dialog(title, message) {
2121
const props = {
2222
id: "error-popup",
2323
title,
@@ -332,7 +332,8 @@ export function NetworkManagerModel() {
332332

333333
if (signal == "PropertiesChanged") {
334334
push_refresh();
335-
set_object_properties(obj, remove_signatures(args[0]));
335+
const props = remove_signatures(args[0]);
336+
set_object_properties(obj, props);
336337
pop_refresh();
337338
} else if (type.signals && type.signals[signal])
338339
type.signals[signal](obj, args);
@@ -399,6 +400,21 @@ export function NetworkManagerModel() {
399400
Object.keys(interfaces).forEach(iface => {
400401
const props = interfaces[iface];
401402

403+
/* Capture connection failure reasons for devices
404+
NM transitions through PREPARE → CONFIG → NEED_AUTH → FAILED → DISCONNECTED very
405+
quickly, and React batches these updates, so the UI often misses the FAILED state.
406+
Store the failure reason here at the D-Bus event level so we can retrieve it later. */
407+
if (path.includes("/Devices/") && props?.StateReason) {
408+
const obj = peek_object(path);
409+
if (obj) {
410+
const [state, reason] = props.StateReason;
411+
if (state === 120 && reason !== 0) {
412+
utils.debug("Captured", obj.Interface, "failure, reason:", reason);
413+
priv(obj).lastFailureReason = reason;
414+
}
415+
}
416+
}
417+
402418
if (props)
403419
interface_properties(path, iface, props);
404420
else
@@ -1150,6 +1166,7 @@ export function NetworkManagerModel() {
11501166
Interface: { },
11511167
StateText: { prop: "State", conv: device_state_to_text, def: _("Unknown") },
11521168
State: { },
1169+
StateReason: { def: [0, 0] }, // [state, reason] tuple
11531170
HwAddress: { },
11541171
AvailableConnections: { conv: conv_Array(conv_Object(type_Connection)), def: [] },
11551172
ActiveConnection: { conv: conv_Object(type_ActiveConnection) },
@@ -1170,13 +1187,15 @@ export function NetworkManagerModel() {
11701187

11711188
prototype: {
11721189
activate: function(connection, specific_object) {
1190+
priv(this).lastFailureReason = undefined; // Clear stale failure reason from previous attempts
11731191
return call_object_method(get_object("/org/freedesktop/NetworkManager", type_Manager),
11741192
"org.freedesktop.NetworkManager", "ActivateConnection",
11751193
objpath(connection), objpath(this), objpath(specific_object))
11761194
.then(([active_connection]) => active_connection);
11771195
},
11781196

11791197
activate_with_settings: function(settings, specific_object) {
1198+
priv(this).lastFailureReason = undefined; // Clear stale failure reason from previous attempts
11801199
try {
11811200
return call_object_method(get_object("/org/freedesktop/NetworkManager", type_Manager),
11821201
"org.freedesktop.NetworkManager", "AddAndActivateConnection",
@@ -1203,6 +1222,13 @@ export function NetworkManagerModel() {
12031222
// RequestScan can fail if a scan was recently done, that's OK
12041223
console.warn("request_scan: scan failed for", this.Interface + ":", error.toString());
12051224
});
1225+
},
1226+
1227+
// Get and clear the last connection failure reason
1228+
consume_failure_reason: function() {
1229+
const reason = priv(this).lastFailureReason;
1230+
priv(this).lastFailureReason = undefined;
1231+
return reason;
12061232
}
12071233
},
12081234

pkg/networkmanager/network-interface.jsx

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ import {
6060
is_managed,
6161
render_active_connection,
6262
settings_applier,
63+
show_error_dialog,
6364
show_unexpected_error,
6465
syn_click,
6566
with_checkpoint,
@@ -274,6 +275,7 @@ export const NetworkInterfacePage = ({
274275
const [isScanning, setIsScanning] = useState(false);
275276
const [prevAPCount, setPrevAPCount] = useState(0);
276277
const [networkSearch, setNetworkSearch] = useState("");
278+
const [pendingConnection, setPendingConnection] = useState(null); // ssid
277279

278280
const dev_name = iface.Name;
279281
const dev = iface.Device;
@@ -291,6 +293,33 @@ export const NetworkInterfacePage = ({
291293
}
292294
});
293295

296+
// Monitor device state to detect connection success/failure
297+
useEffect(() => {
298+
if (!dev || !pendingConnection)
299+
return;
300+
301+
// Successfully connected
302+
if (dev.ActiveAccessPoint?.Ssid === pendingConnection && dev.State === 100) { // NM_DEVICE_STATE_ACTIVATED
303+
setPendingConnection(null);
304+
return;
305+
}
306+
307+
// Check for failure - if there's a captured failure reason, the connection attempt failed
308+
if (dev.State === 30 && !dev.ActiveConnection) { // NM_DEVICE_STATE_DISCONNECTED
309+
const failureReason = dev.consume_failure_reason();
310+
if (failureReason !== undefined) {
311+
utils.debug("Connection to", pendingConnection, "failed with captured reason:", failureReason);
312+
show_error_dialog(
313+
cockpit.format(_("Failed to connect to $0"), pendingConnection),
314+
failureReason === 7 // NM_DEVICE_STATE_REASON_NO_SECRETS
315+
? _("Network password is not stored. Please forget and reconnect to this network.")
316+
: _("Connection failed. Check your credentials.")
317+
);
318+
setPendingConnection(null);
319+
}
320+
}
321+
}, [dev, dev?.State, dev?.StateReason, dev?.ActiveConnection, dev?.ActiveAccessPoint?.Ssid, pendingConnection]);
322+
294323
// WiFi scanning: re-enable button when APs change or after timeout
295324
useEffect(() => {
296325
if (isScanning) {
@@ -861,9 +890,13 @@ export const NetworkInterfacePage = ({
861890
if (ap.Connection) {
862891
// Activate existing connection (which already has password if needed)
863892
utils.debug("Activating existing connection for", ap.Ssid);
893+
setPendingConnection(ap.Ssid);
864894
ap.Connection.activate(dev, ap)
865-
.then(() => utils.debug("Connected successfully to", ap.Ssid))
866-
.catch(show_unexpected_error);
895+
.then(() => utils.debug("Connection activation started for", ap.Ssid))
896+
.catch(error => {
897+
setPendingConnection(null);
898+
show_unexpected_error(error);
899+
});
867900
return;
868901
}
869902

test/verify/check-networkmanager-wifi

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,33 @@ wpa_passphrase=hidden99""")
226226
# OpenNet is known but not connected
227227
b.wait_visible("tr[data-ssid='OpenNet'] .nm-icon-known")
228228

229+
# Test connection failure when password is not stored
230+
# Disconnect first
231+
b.click("tr[data-ssid='ZE WIFI!'] button[aria-label='Disconnect']")
232+
b.wait_visible("tr[data-ssid='ZE WIFI!'] button[aria-label='Connect']")
233+
# Remove the password from the connection file and restart NM
234+
m.execute("sed -i '/^psk=/d' /etc/NetworkManager/system-connections/'ZE WIFI!.nmconnection'")
235+
m.execute("systemctl restart NetworkManager")
236+
b.reload()
237+
b.enter_page("/network")
238+
# Try to connect - shows error about missing password
239+
b.click("tr[data-ssid='ZE WIFI!'] button[aria-label='Connect']")
240+
b.wait_in_text("#error-popup", "Failed to connect to ZE WIFI!")
241+
b.wait_in_text("#error-popup", "Network password is not stored")
242+
b.click("#error-popup button:contains('Close')")
243+
b.wait_not_present("#error-popup")
244+
# Restore the connection with password for subsequent tests
245+
b.click("tr[data-ssid='ZE WIFI!'] button.pf-v6-c-menu-toggle")
246+
b.click(".pf-v6-c-menu button[aria-label='Forget']")
247+
b.wait_not_present("tr[data-ssid='ZE WIFI!'] .nm-icon-known")
248+
b.click("tr[data-ssid='ZE WIFI!'] button[aria-label='Connect']")
249+
b.wait_in_text("#network-wifi-connect-dialog", "Connect to ZE WIFI!")
250+
b.set_input_text("#network-wifi-connect-password-input", "12345678")
251+
b.click("#network-wifi-connect-connect")
252+
b.wait_not_present("#network-wifi-connect-dialog")
253+
b.wait_visible("tr[data-ssid='ZE WIFI!'] button[aria-label='Disconnect']")
254+
b.wait_visible("tr[data-ssid='ZE WIFI!'] .nm-icon-connected")
255+
229256
# connecting to OpenNet disconnects from ZE WIFI!
230257
self.assertIn("ZE WIFI!", m.execute("nmcli d show wlan1 | grep GENERAL.CONNECTION"))
231258
b.click("tr[data-ssid='OpenNet'] button[aria-label='Connect']")

0 commit comments

Comments
 (0)