wpa_supplicant: keep a station's association on a credential-only change

A station whose configuration changed only in its credentials was removed
and added again, or with RECONFIGURE deauthenticated, although the new
settings could still allow the association in use. For example, a change
from PSK and SAE to SAE only, while the station is associated with SAE,
made the station associate a second time.

The iface method network_update() reads the regenerated file with
wpa_config_read() and, where its one network is the current one and the
association in use (SSID, AKM, protocol, ciphers, PMF, the effective
password and SAE PWE) still fits it, moves the credential fields into the
current network without the PMKSA flush that SET_NETWORK does. Otherwise
it refuses, and the caller removes and adds the interface as before.
wpa_supplicant.uc takes this path for a plain station and a station MLD
whose configuration differs only in the credential lines, and applies the
MLD's freq_list again after it.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
This commit is contained in:
Felix Fietkau committed 2026-10-08 09:50:38 +02:00
1 parent 22e7917d8e
commit f9c2013aed
2 files changed
+205

No files matched your search

@@ -67,6 +67,62 @@ function iface_start(phydev, iface, macaddr_list)
iface.running = true;
}
// a change of these lines needs no new netdev
const network_update_keys = [
"ssid", "psk", "sae_password", "key_mgmt", "ieee80211w", "sae_pwe",
"proto", "pairwise", "group",
];
// the options of a station MLD that only produce those lines
const mld_network_keys = [
"ssid", "key", "encryption", "ieee80211w", "sae_pwe", "sae_ext_key",
];
const config_line_re = /^(\t?)([a-z0-9_]+)=/;
function config_equal_except(config_a, config_b, keys)
{
let rest_a = { ...config_a };
let rest_b = { ...config_b };
for (let key in keys) {
delete rest_a[key];
delete rest_b[key];
}
return is_equal(rest_a, rest_b);
}
// every radio of a station MLD writes its own freq_list into the shared file
function config_line_keep(line)
{
let m = match(line, config_line_re);
if (!m)
return true;
if (!m[1])
return m[2] != "freq_list";
return index(network_update_keys, m[2]) < 0;
}
function config_data_strip(config_data)
{
return filter(split(config_data ?? "", "\n"), config_line_keep);
}
function iface_network_apply(ifname, old_data, new_data, freq_list)
{
let iface = wpas.interfaces[ifname];
if (!iface ||
!is_equal(config_data_strip(old_data), config_data_strip(new_data)) ||
!iface.network_update())
return false;
wpas.printf(`Update network of interface ${ifname} in place`);
if (length(freq_list) > 0)
iface.config('freq_list', freq_list);
return true;
}
function iface_cb(new_if, old_if)
{
if (old_if && new_if && is_equal(old_if.config, new_if.config)) {
@@ -74,6 +130,14 @@ function iface_cb(new_if, old_if)
return;
}
if (old_if?.running && new_if && new_if.config.mode == "sta" &&
config_equal_except(old_if.config, new_if.config, [ "config_data" ]) &&
iface_network_apply(old_if.config.iface, old_if.config.config_data,
new_if.config.config_data)) {
new_if.running = true;
return;
}
if (new_if && old_if)
wpas.printf(`Update configuration for interface ${old_if.config.iface}`);
else if (old_if)
@@ -233,6 +297,7 @@ function mld_add(data, phy_list)
return;
}
data.config_data = readfile(first_config?.config);
wpas.add_iface(first_config);
let iface = wpas.interfaces[name];
@@ -262,6 +327,21 @@ function mld_add_links(data)
mld_add(data);
}
function mld_reload(data, phy_list)
{
let first_config = data.phy_config[mld_first_phy(data)];
let config_data = readfile(first_config?.config);
if (iface_network_apply(data.name, data.config_data, config_data,
data.freq_list)) {
data.config_data = config_data;
return;
}
mld_remove(data);
mld_add(data, phy_list);
}
function mld_set_config(config)
{
let prev_mld = { ...wpas.data.mld };
@@ -279,6 +359,15 @@ function mld_set_config(config)
continue;
}
if (prev && config_equal_except(prev.config, data, mld_network_keys)) {
wpas.printf(`Update network of MLD interface ${name}`);
prev.config = data;
prev.reload = true;
new_mld[name] = prev;
delete prev_mld[name];
continue;
}
let radio_mask = 0;
for (let r in data.radios)
if (r != null)
@@ -360,7 +449,12 @@ function mld_start() {
let phy_list = {};
for (let name, data in wpas.data.mld) {
wpas.printf(`MLD interface ${name} present=${data.radio_mask_present} up=${data.radio_mask_up}`);
let reload = data.reload;
delete data.reload;
let add_mask = data.radio_mask_present & ~data.radio_mask_up;
if (!add_mask && reload && data.radio_mask_up)
mld_reload(data, phy_list);
if (!add_mask)
continue;
@@ -4,6 +4,8 @@
#include "utils/base64.h"
#include "drivers/driver.h"
#include "ap/hostapd.h"
#include "common/sae.h"
#include "rsn_supp/wpa.h"
#include "wpa_supplicant_i.h"
#include "wps_supplicant.h"
#include "ctrl_iface.h"
@@ -785,6 +787,114 @@ uc_wpas_iface_config(uc_vm_t *vm, size_t nargs)
return ret;
}
#ifdef CONFIG_SAE
static bool
uc_wpas_sae_password_equal(struct wpa_ssid *a, struct wpa_ssid *b)
{
const char *pw_a = a->sae_password ? a->sae_password : a->passphrase;
const char *pw_b = b->sae_password ? b->sae_password : b->passphrase;
return pw_a && pw_b && !os_strcmp(pw_a, pw_b);
}
#endif /* CONFIG_SAE */
static const char *
uc_wpas_network_mismatch(struct wpa_supplicant *wpa_s, struct wpa_ssid *ssid)
{
struct wpa_ssid *cur = wpa_s->current_ssid;
int pmf = wpas_get_ssid_pmf(wpa_s, ssid);
int pmf_used = wpa_sm_pmf_enabled(wpa_s->wpa);
if (cur->ssid_len != ssid->ssid_len ||
os_memcmp(cur->ssid, ssid->ssid, ssid->ssid_len) != 0)
return "SSID";
if (!(ssid->key_mgmt & wpa_s->key_mgmt))
return "AKM";
if (!(ssid->proto & wpa_s->wpa_proto))
return "protocol";
if (!(ssid->pairwise_cipher & wpa_s->pairwise_cipher) ||
!(ssid->group_cipher & wpa_s->group_cipher))
return "cipher";
if ((pmf == MGMT_FRAME_PROTECTION_REQUIRED && !pmf_used) ||
(pmf == NO_MGMT_FRAME_PROTECTION && pmf_used))
return "PMF";
if (cur->ext_psk || ssid->ext_psk)
return "external password";
if (wpa_key_mgmt_wpa_psk_no_sae(wpa_s->key_mgmt) &&
(!cur->psk_set || !ssid->psk_set ||
os_memcmp(cur->psk, ssid->psk, PMK_LEN) != 0))
return "PSK";
#ifdef CONFIG_SAE
if (wpa_key_mgmt_sae(wpa_s->key_mgmt) &&
(!uc_wpas_sae_password_equal(cur, ssid) ||
wpas_get_ssid_sae_pwe(wpa_s, cur) !=
wpas_get_ssid_sae_pwe(wpa_s, ssid)))
return "SAE password or PWE";
#endif /* CONFIG_SAE */
return NULL;
}
/*
* The PMKSA cache stays: flushing it deauthenticates a station that holds
* an SAE PMKSA.
*/
static void
uc_wpas_network_move(struct wpa_ssid *cur, struct wpa_ssid *ssid)
{
str_clear_free(cur->passphrase);
cur->passphrase = ssid->passphrase;
ssid->passphrase = NULL;
str_clear_free(cur->sae_password);
cur->sae_password = ssid->sae_password;
ssid->sae_password = NULL;
os_memcpy(cur->psk, ssid->psk, PMK_LEN);
cur->psk_set = ssid->psk_set;
cur->key_mgmt = ssid->key_mgmt;
cur->ieee80211w = ssid->ieee80211w;
cur->sae_pwe = ssid->sae_pwe;
cur->proto = ssid->proto;
cur->pairwise_cipher = ssid->pairwise_cipher;
cur->group_cipher = ssid->group_cipher;
#ifdef CONFIG_SAE
sae_deinit_pt(cur->pt);
cur->pt = NULL;
#endif /* CONFIG_SAE */
}
static uc_value_t *
uc_wpas_iface_network_update(uc_vm_t *vm, size_t nargs)
{
struct wpa_supplicant *wpa_s = uc_fn_thisval("wpas.iface");
struct wpa_config *conf;
const char *mismatch = "configuration";
if (!wpa_s)
return NULL;
if (!wpa_s->current_ssid || wpa_s->current_ssid != wpa_s->conf->ssid ||
wpa_s->conf->ssid->next || wpa_s->wpa_state != WPA_COMPLETED ||
!wpa_key_mgmt_wpa_psk(wpa_s->key_mgmt) || !wpa_s->confname)
return ucv_boolean_new(false);
conf = wpa_config_read(wpa_s->confname, NULL, false, false);
if (!conf)
return ucv_boolean_new(false);
if (conf->ssid && !conf->ssid->next)
mismatch = uc_wpas_network_mismatch(wpa_s, conf->ssid);
if (mismatch)
wpa_printf(MSG_INFO, "%s: new network does not fit the association: %s",
wpa_s->ifname, mismatch);
else
uc_wpas_network_move(wpa_s->current_ssid, conf->ssid);
wpa_config_free(conf);
return ucv_boolean_new(!mismatch);
}
int wpas_ucode_init(struct wpa_global *gl)
{
static const uc_function_list_t global_fns[] = {
@@ -798,6 +908,7 @@ int wpas_ucode_init(struct wpa_global *gl)
{ "status", uc_wpas_iface_status },
{ "ctrl", uc_wpas_iface_ctrl },
{ "config", uc_wpas_iface_config },
{ "network_update", uc_wpas_iface_network_update },
{ "wps_set_m7", uc_wpas_iface_wps_set_m7 },
#ifdef CONFIG_DPP
{ "dpp_send_action", uc_wpas_iface_dpp_send_action },