From a3267aaf7bfab66604b2a6e69973301bb7f9b867 Mon Sep 17 00:00:00 2001 From: Jan Fooken Date: Tue, 4 Nov 2025 09:32:58 +0100 Subject: [PATCH 1/5] device: add private getter for property auth-retries --- src/core/devices/nm-device.c | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/core/devices/nm-device.c b/src/core/devices/nm-device.c index ad6602c05a..03343f3fef 100644 --- a/src/core/devices/nm-device.c +++ b/src/core/devices/nm-device.c @@ -18905,14 +18905,14 @@ nm_device_get_supplicant_timeout(NMDevice *self) SUPPLICANT_DEFAULT_TIMEOUT); } -gboolean -nm_device_auth_retries_try_next(NMDevice *self) +static int +_device_get_auth_retries(NMDevice *self) { NMDevicePrivate *priv; NMSettingConnection *s_con; int auth_retries; - g_return_val_if_fail(NM_IS_DEVICE(self), FALSE); + g_return_val_if_fail(NM_IS_DEVICE(self), 0); priv = NM_DEVICE_GET_PRIVATE(self); auth_retries = priv->auth_retries; @@ -18944,13 +18944,29 @@ nm_device_auth_retries_try_next(NMDevice *self) priv->auth_retries = auth_retries; } + return auth_retries; +} + +gboolean +nm_device_auth_retries_try_next(NMDevice *self) +{ + NMDevicePrivate *priv; + int auth_retries; + + g_return_val_if_fail(NM_IS_DEVICE(self), FALSE); + + priv = NM_DEVICE_GET_PRIVATE(self); + auth_retries = _device_get_auth_retries(self); + if (auth_retries == NM_DEVICE_AUTH_RETRIES_INFINITY) return TRUE; if (auth_retries <= 0) { nm_assert(auth_retries == 0); return FALSE; } + priv->auth_retries--; + return TRUE; } From 6dc51ddf01afe790bcf48d93192a0e535987334e Mon Sep 17 00:00:00 2001 From: Jan Fooken Date: Tue, 4 Nov 2025 09:35:27 +0100 Subject: [PATCH 2/5] device: add public method nm_device_auth_retries_has_next Devices don't know whether they have authentication retries left, so they can only make decisions ad-hoc after calling nm_device_auth_retries_try_next. Giving devices a way to determine whether the current attempt is their last attempt, allows them to make decisions before failing a connection. --- src/core/devices/nm-device.c | 18 ++++++++++++++++++ src/core/devices/nm-device.h | 1 + 2 files changed, 19 insertions(+) diff --git a/src/core/devices/nm-device.c b/src/core/devices/nm-device.c index 03343f3fef..a00f04b579 100644 --- a/src/core/devices/nm-device.c +++ b/src/core/devices/nm-device.c @@ -18947,6 +18947,24 @@ _device_get_auth_retries(NMDevice *self) return auth_retries; } +gboolean +nm_device_auth_retries_has_next(NMDevice *self) +{ + int auth_retries; + + g_return_val_if_fail(NM_IS_DEVICE(self), FALSE); + + auth_retries = _device_get_auth_retries(self); + + if (auth_retries == NM_DEVICE_AUTH_RETRIES_INFINITY) + return TRUE; + + if (auth_retries > 0) + return TRUE; + + return FALSE; +} + gboolean nm_device_auth_retries_try_next(NMDevice *self) { diff --git a/src/core/devices/nm-device.h b/src/core/devices/nm-device.h index 2f287953eb..c8069d7c1d 100644 --- a/src/core/devices/nm-device.h +++ b/src/core/devices/nm-device.h @@ -791,6 +791,7 @@ void nm_device_update_permanent_hw_address(NMDevice *self, gboolean force_fr void nm_device_update_dynamic_ip_setup(NMDevice *self, const char *reason); guint nm_device_get_supplicant_timeout(NMDevice *self); +gboolean nm_device_auth_retries_has_next(NMDevice *self); gboolean nm_device_auth_retries_try_next(NMDevice *self); gboolean nm_device_hw_addr_get_cloned(NMDevice *self, From 746a5902ad85ec0611a3e6ebfd7b68b45621a40b Mon Sep 17 00:00:00 2001 From: Jan Fooken Date: Tue, 4 Nov 2025 09:43:06 +0100 Subject: [PATCH 3/5] wifi: use authentication retry mechanism While NetworkManager tries it's best to determine whether a new PSK is needed, it can still run into edge cases. One of these edge cases is that a device can leave the range of an access point and therefore fail a 4-way handshake. Because these cases can't be confidently detected, a device which was previously connected, should try to exhaust it's authentication retries before requesting new secrets. This leads to less user-facing prompts while increasing the time from PSK change to prompt. --- src/core/devices/wifi/nm-device-wifi.c | 90 ++++++++++++++++++++++---- 1 file changed, 78 insertions(+), 12 deletions(-) diff --git a/src/core/devices/wifi/nm-device-wifi.c b/src/core/devices/wifi/nm-device-wifi.c index 1fd096426e..6a9e5655c8 100644 --- a/src/core/devices/wifi/nm-device-wifi.c +++ b/src/core/devices/wifi/nm-device-wifi.c @@ -2251,6 +2251,26 @@ wps_timeout_cb(gpointer user_data) return G_SOURCE_REMOVE; } +static gboolean +wifi_connection_is_new(NMDeviceWifi *self) +{ + NMDevice *device = NM_DEVICE(self); + NMActRequest *req; + NMSettingsConnection *connection; + guint64 timestamp = 0; + + req = nm_device_get_act_request(device); + g_return_val_if_fail(NM_IS_ACT_REQUEST(req), TRUE); + + connection = nm_act_request_get_settings_connection(req); + g_return_val_if_fail(NM_IS_SETTINGS_CONNECTION(connection), TRUE); + + if (nm_settings_connection_get_timestamp(connection, ×tamp) && timestamp != 0) + return FALSE; + + return TRUE; +} + static void wifi_secrets_get_secrets(NMDeviceWifi *self, const char *setting_name, @@ -2405,10 +2425,11 @@ handle_8021x_or_psk_auth_fail(NMDeviceWifi *self, NMSupplicantInterfaceState old_state, int disconnect_reason) { - NMDevice *device = NM_DEVICE(self); - NMActRequest *req; - const char *setting_name = NULL; - gboolean handled = FALSE; + NMDevice *device = NM_DEVICE(self); + NMActRequest *req; + const char *setting_name = NULL; + NMSecretAgentGetSecretsFlags secret_flags = NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION + | NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW; g_return_val_if_fail(new_state == NM_SUPPLICANT_INTERFACE_STATE_DISCONNECTED, FALSE); @@ -2418,8 +2439,7 @@ handle_8021x_or_psk_auth_fail(NMDeviceWifi *self, req = nm_device_get_act_request(NM_DEVICE(self)); g_return_val_if_fail(req != NULL, FALSE); - if (need_new_8021x_secrets(self, old_state, &setting_name) - || need_new_wpa_psk(self, old_state, disconnect_reason, &setting_name)) { + if (need_new_8021x_secrets(self, old_state, &setting_name)) { nm_act_request_clear_secrets(req); _LOGI(LOGD_DEVICE | LOGD_WIFI, @@ -2429,14 +2449,54 @@ handle_8021x_or_psk_auth_fail(NMDeviceWifi *self, nm_device_state_changed(device, NM_DEVICE_STATE_NEED_AUTH, NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT); - wifi_secrets_get_secrets(self, - setting_name, - NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION - | NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW); - handled = TRUE; + wifi_secrets_get_secrets(self, setting_name, secret_flags); + return TRUE; } - return handled; + if (need_new_wpa_psk(self, old_state, disconnect_reason, &setting_name)) { + nm_act_request_clear_secrets(req); + cleanup_association_attempt(self, TRUE); + + if (wifi_connection_is_new(self)) { + _LOGI(LOGD_DEVICE | LOGD_WIFI, + "Activation: (wifi) new connection disconnected during association, asking for " + "new key"); + nm_device_state_changed(device, + NM_DEVICE_STATE_NEED_AUTH, + NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT); + wifi_secrets_get_secrets(self, setting_name, secret_flags); + return TRUE; + } + + if (!nm_device_auth_retries_try_next(device)) { + nm_device_state_changed(device, + NM_DEVICE_STATE_FAILED, + NM_DEVICE_STATE_REASON_NO_SECRETS); + return TRUE; + } + + if (nm_device_auth_retries_has_next(device)) { + secret_flags &= ~NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW; + _LOGI( + LOGD_DEVICE | LOGD_WIFI, + "Activation: (wifi) disconnected during association, reauthenticating connection"); + } else { + _LOGI(LOGD_DEVICE | LOGD_WIFI, + "Activation: (wifi) disconnected during association, asking for new key"); + } + + nm_device_state_changed(device, + NM_DEVICE_STATE_NEED_AUTH, + NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT); + wifi_secrets_get_secrets(self, setting_name, secret_flags); + + return TRUE; + } + + _LOGI(LOGD_DEVICE | LOGD_WIFI, + "Activation: (wifi) disconnected during association, retrying connection"); + + return FALSE; } static gboolean @@ -2868,6 +2928,12 @@ supplicant_iface_notify_wpa_psk_mismatch_cb(NMSupplicantInterface *iface, NMDevi if (nm_device_get_state(device) != NM_DEVICE_STATE_CONFIG) return; + if (!wifi_connection_is_new(self) && nm_device_auth_retries_has_next(device)) { + _LOGI(LOGD_DEVICE | LOGD_WIFI, + "Activation: (wifi) psk mismatch reported by supplicant, retrying connection"); + return; + } + _LOGI(LOGD_DEVICE | LOGD_WIFI, "Activation: (wifi) psk mismatch reported by supplicant, asking for new key"); From b4fc8550f5f3242f9b69071997dda73148b539ad Mon Sep 17 00:00:00 2001 From: Jan Fooken Date: Mon, 10 Nov 2025 10:25:34 +0100 Subject: [PATCH 4/5] man: wifi: Document connection.auth-retry for WPA connections Remove the mentioned limitation of limiting authentication retires to 802.1X connections and add information about the introduced secret prompting behaviour. --- src/libnm-core-impl/nm-setting-connection.c | 5 ++--- src/libnmc-setting/settings-docs.h.in | 2 +- src/nmcli/gen-metadata-nm-settings-nmcli.xml.in | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/libnm-core-impl/nm-setting-connection.c b/src/libnm-core-impl/nm-setting-connection.c index 0ad97846df..96dff14601 100644 --- a/src/libnm-core-impl/nm-setting-connection.c +++ b/src/libnm-core-impl/nm-setting-connection.c @@ -3328,9 +3328,8 @@ nm_setting_connection_class_init(NMSettingConnectionClass *klass) * * The number of retries for the authentication. Zero means to try indefinitely; -1 means * to use a global default. If the global default is not set, the authentication - * retries for 3 times before failing the connection. - * - * Currently, this only applies to 802-1x authentication. + * retries for 3 times before failing the connection. Connections using a pre-shared key + * to authenticate will only prompt for a new key during the last authentication attempt. * * Since: 1.10 **/ diff --git a/src/libnmc-setting/settings-docs.h.in b/src/libnmc-setting/settings-docs.h.in index 0df43b0794..e02a93e43d 100644 --- a/src/libnmc-setting/settings-docs.h.in +++ b/src/libnmc-setting/settings-docs.h.in @@ -1,6 +1,6 @@ /* Generated file. Do not edit. */ -#define DESCRIBE_DOC_NM_SETTING_CONNECTION_AUTH_RETRIES N_("The number of retries for the authentication. Zero means to try indefinitely; -1 means to use a global default. If the global default is not set, the authentication retries for 3 times before failing the connection. Currently, this only applies to 802-1x authentication.") +#define DESCRIBE_DOC_NM_SETTING_CONNECTION_AUTH_RETRIES N_("The number of retries for the authentication. Zero means to try indefinitely; -1 means to use a global default. If the global default is not set, the authentication retries for 3 times before failing the connection. Connections using a pre-shared key to authenticate will only prompt for a new key during the last authentication attempt.") #define DESCRIBE_DOC_NM_SETTING_CONNECTION_AUTOCONNECT N_("Whether or not the connection should be automatically connected by NetworkManager when the resources for the connection are available. TRUE to automatically activate the connection, FALSE to require manual intervention to activate the connection. Autoconnect happens when the circumstances are suitable. That means for example that the device is currently managed and not active. Autoconnect thus never replaces or competes with an already active profile. Note that autoconnect is not implemented for VPN profiles. See \"secondaries\" as an alternative to automatically connect VPN profiles. If multiple profiles are ready to autoconnect on the same device, the one with the better \"connection.autoconnect-priority\" is chosen. If the priorities are equal, then the most recently connected profile is activated. If the profiles were not connected earlier or their \"connection.timestamp\" is identical, the choice is undefined. Depending on \"connection.multi-connect\", a profile can (auto)connect only once at a time or multiple times.") #define DESCRIBE_DOC_NM_SETTING_CONNECTION_AUTOCONNECT_PORTS N_("Whether or not ports of this connection should be automatically brought up when NetworkManager activates this connection. This only has a real effect for controller connections. The properties \"autoconnect\", \"autoconnect-priority\" and \"autoconnect-retries\" are unrelated to this setting. The permitted values are: 0: leave port connections untouched, 1: activate all the port connections with this connection, -1: default. If -1 (default) is set, global connection.autoconnect-ports is read to determine the real value. If it is default as well, this fallbacks to 0.") #define DESCRIBE_DOC_NM_SETTING_CONNECTION_AUTOCONNECT_PRIORITY N_("The autoconnect priority in range -999 to 999. If the connection is set to autoconnect, connections with higher priority will be preferred. The higher number means higher priority. Defaults to 0. Note that this property only matters if there are more than one candidate profile to select for autoconnect. In case of equal priority, the profile used most recently is chosen.") diff --git a/src/nmcli/gen-metadata-nm-settings-nmcli.xml.in b/src/nmcli/gen-metadata-nm-settings-nmcli.xml.in index 7b718c9e1f..8733cc995b 100644 --- a/src/nmcli/gen-metadata-nm-settings-nmcli.xml.in +++ b/src/nmcli/gen-metadata-nm-settings-nmcli.xml.in @@ -653,7 +653,7 @@ format="choice (NMConnectionMultiConnect)" values="default (0), single (1), manual-multiple (2), multiple (3)" /> Date: Mon, 10 Nov 2025 10:27:46 +0100 Subject: [PATCH 5/5] NEWS: WPA connections now respect connection.auth-retry --- NEWS | 3 +++ 1 file changed, 3 insertions(+) diff --git a/NEWS b/NEWS index e38470b041..65260a14dc 100644 --- a/NEWS +++ b/NEWS @@ -44,6 +44,9 @@ USE AT YOUR OWN RISK. NOT RECOMMENDED FOR PRODUCTION USE! * Change the default value of the ipv4.dhcp-ipv6-only-preferred property to a new value "auto" which automatically enables the option when CLAT is enabled ("yes" or "auto") in the connection profile. +* WIFI connections using wpa-psk respect the setting connection.auth-retry + and only prompt for new secrets during the last authentication attempt before + failing. ============================================= NetworkManager-1.56