diff --git a/package/network/services/hostapd/files/wpa_supplicant.uc b/package/network/services/hostapd/files/wpa_supplicant.uc index 0672218b0b3..13a23569431 100644 --- a/package/network/services/hostapd/files/wpa_supplicant.uc +++ b/package/network/services/hostapd/files/wpa_supplicant.uc @@ -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; diff --git a/package/network/services/hostapd/src/wpa_supplicant/ucode.c b/package/network/services/hostapd/src/wpa_supplicant/ucode.c index 132db18de21..30e875467ff 100644 --- a/package/network/services/hostapd/src/wpa_supplicant/ucode.c +++ b/package/network/services/hostapd/src/wpa_supplicant/ucode.c @@ -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 },