5f5db9366c
Implement processing of the Multi-Link element in the (Re)Association Request frame, including processing of the Per-STA Profile subelement. After handling the basic parsing of the element and extracting the information about the requested links, handle the link specific processing for each link: - Find the interface with the corresponding link ID. - Process the station profile in the interface. - Prepare the Per-STA Profile subelement to be included in the Multi-Link element in the (Re)Association Response frame. Signed-off-by: Ilan Peer <ilan.peer@intel.com> Signed-off-by: Andrei Otcheretianski <andrei.otcheretianski@intel.com>
1139 lines
28 KiB
C
1139 lines
28 KiB
C
/*
|
|
* hostapd / IEEE 802.11be EHT
|
|
* Copyright (c) 2021-2022, Qualcomm Innovation Center, Inc.
|
|
*
|
|
* This software may be distributed under the terms of the BSD license.
|
|
* See README for more details.
|
|
*/
|
|
|
|
#include "utils/includes.h"
|
|
#include "utils/common.h"
|
|
#include "crypto/dh_groups.h"
|
|
#include "hostapd.h"
|
|
#include "sta_info.h"
|
|
#include "ieee802_11.h"
|
|
|
|
|
|
static u16 ieee80211_eht_ppet_size(u16 ppe_thres_hdr, const u8 *phy_cap_info)
|
|
{
|
|
u8 ru;
|
|
u16 sz = 0;
|
|
|
|
if ((phy_cap_info[EHT_PHYCAP_PPE_THRESHOLD_PRESENT_IDX] &
|
|
EHT_PHYCAP_PPE_THRESHOLD_PRESENT) == 0)
|
|
return 0;
|
|
|
|
ru = (ppe_thres_hdr &
|
|
EHT_PPE_THRES_RU_INDEX_MASK) >> EHT_PPE_THRES_RU_INDEX_SHIFT;
|
|
while (ru) {
|
|
if (ru & 0x1)
|
|
sz++;
|
|
ru >>= 1;
|
|
}
|
|
|
|
sz = sz * (1 + ((ppe_thres_hdr & EHT_PPE_THRES_NSS_MASK) >>
|
|
EHT_PPE_THRES_NSS_SHIFT));
|
|
sz = (sz * 6) + 9;
|
|
if (sz % 8)
|
|
sz += 8;
|
|
sz /= 8;
|
|
|
|
return sz;
|
|
}
|
|
|
|
|
|
static u8 ieee80211_eht_mcs_set_size(enum hostapd_hw_mode mode, u8 opclass,
|
|
u8 he_oper_chwidth, const u8 *he_phy_cap,
|
|
const u8 *eht_phy_cap)
|
|
{
|
|
u8 sz = EHT_PHYCAP_MCS_NSS_LEN_20MHZ_PLUS;
|
|
bool band24, band5, band6;
|
|
u8 he_phy_cap_chwidth = ~HE_PHYCAP_CHANNEL_WIDTH_MASK;
|
|
|
|
switch (he_oper_chwidth) {
|
|
case CONF_OPER_CHWIDTH_80P80MHZ:
|
|
he_phy_cap_chwidth |=
|
|
HE_PHYCAP_CHANNEL_WIDTH_SET_80PLUS80MHZ_IN_5G;
|
|
/* fall through */
|
|
case CONF_OPER_CHWIDTH_160MHZ:
|
|
he_phy_cap_chwidth |= HE_PHYCAP_CHANNEL_WIDTH_SET_160MHZ_IN_5G;
|
|
/* fall through */
|
|
case CONF_OPER_CHWIDTH_80MHZ:
|
|
case CONF_OPER_CHWIDTH_USE_HT:
|
|
he_phy_cap_chwidth |= HE_PHYCAP_CHANNEL_WIDTH_SET_40MHZ_IN_2G |
|
|
HE_PHYCAP_CHANNEL_WIDTH_SET_40MHZ_80MHZ_IN_5G;
|
|
break;
|
|
}
|
|
|
|
he_phy_cap_chwidth &= he_phy_cap[HE_PHYCAP_CHANNEL_WIDTH_SET_IDX];
|
|
|
|
band24 = mode == HOSTAPD_MODE_IEEE80211B ||
|
|
mode == HOSTAPD_MODE_IEEE80211G ||
|
|
mode == NUM_HOSTAPD_MODES;
|
|
band5 = mode == HOSTAPD_MODE_IEEE80211A ||
|
|
mode == NUM_HOSTAPD_MODES;
|
|
band6 = is_6ghz_op_class(opclass);
|
|
|
|
if (band24 &&
|
|
(he_phy_cap_chwidth & HE_PHYCAP_CHANNEL_WIDTH_SET_40MHZ_IN_2G) == 0)
|
|
return EHT_PHYCAP_MCS_NSS_LEN_20MHZ_ONLY;
|
|
|
|
if (band5 &&
|
|
(he_phy_cap_chwidth &
|
|
(HE_PHYCAP_CHANNEL_WIDTH_SET_40MHZ_80MHZ_IN_5G |
|
|
HE_PHYCAP_CHANNEL_WIDTH_SET_160MHZ_IN_5G |
|
|
HE_PHYCAP_CHANNEL_WIDTH_SET_80PLUS80MHZ_IN_5G)) == 0)
|
|
return EHT_PHYCAP_MCS_NSS_LEN_20MHZ_ONLY;
|
|
|
|
if (band5 &&
|
|
(he_phy_cap_chwidth &
|
|
(HE_PHYCAP_CHANNEL_WIDTH_SET_160MHZ_IN_5G |
|
|
HE_PHYCAP_CHANNEL_WIDTH_SET_80PLUS80MHZ_IN_5G)))
|
|
sz += EHT_PHYCAP_MCS_NSS_LEN_20MHZ_PLUS;
|
|
|
|
if (band6 &&
|
|
(eht_phy_cap[EHT_PHYCAP_320MHZ_IN_6GHZ_SUPPORT_IDX] &
|
|
EHT_PHYCAP_320MHZ_IN_6GHZ_SUPPORT_MASK))
|
|
sz += EHT_PHYCAP_MCS_NSS_LEN_20MHZ_PLUS;
|
|
|
|
return sz;
|
|
}
|
|
|
|
|
|
size_t hostapd_eid_eht_capab_len(struct hostapd_data *hapd,
|
|
enum ieee80211_op_mode opmode)
|
|
{
|
|
struct hostapd_hw_modes *mode;
|
|
struct eht_capabilities *eht_cap;
|
|
size_t len = 3 + 2 + EHT_PHY_CAPAB_LEN;
|
|
|
|
mode = hapd->iface->current_mode;
|
|
if (!mode)
|
|
return 0;
|
|
|
|
eht_cap = &mode->eht_capab[opmode];
|
|
if (!eht_cap->eht_supported)
|
|
return 0;
|
|
|
|
len += ieee80211_eht_mcs_set_size(mode->mode, hapd->iconf->op_class,
|
|
hapd->iconf->he_oper_chwidth,
|
|
mode->he_capab[opmode].phy_cap,
|
|
eht_cap->phy_cap);
|
|
len += ieee80211_eht_ppet_size(WPA_GET_LE16(&eht_cap->ppet[0]),
|
|
eht_cap->phy_cap);
|
|
|
|
return len;
|
|
}
|
|
|
|
|
|
u8 * hostapd_eid_eht_capab(struct hostapd_data *hapd, u8 *eid,
|
|
enum ieee80211_op_mode opmode)
|
|
{
|
|
struct hostapd_hw_modes *mode;
|
|
struct eht_capabilities *eht_cap;
|
|
struct ieee80211_eht_capabilities *cap;
|
|
size_t mcs_nss_len, ppe_thresh_len;
|
|
u8 *pos = eid, *length_pos;
|
|
|
|
mode = hapd->iface->current_mode;
|
|
if (!mode)
|
|
return eid;
|
|
|
|
eht_cap = &mode->eht_capab[opmode];
|
|
if (!eht_cap->eht_supported)
|
|
return eid;
|
|
|
|
*pos++ = WLAN_EID_EXTENSION;
|
|
length_pos = pos++;
|
|
*pos++ = WLAN_EID_EXT_EHT_CAPABILITIES;
|
|
|
|
cap = (struct ieee80211_eht_capabilities *) pos;
|
|
os_memset(cap, 0, sizeof(*cap));
|
|
cap->mac_cap = host_to_le16(eht_cap->mac_cap);
|
|
os_memcpy(cap->phy_cap, eht_cap->phy_cap, EHT_PHY_CAPAB_LEN);
|
|
|
|
if (!is_6ghz_op_class(hapd->iconf->op_class))
|
|
cap->phy_cap[EHT_PHYCAP_320MHZ_IN_6GHZ_SUPPORT_IDX] &=
|
|
~EHT_PHYCAP_320MHZ_IN_6GHZ_SUPPORT_MASK;
|
|
if (!hapd->iface->conf->eht_phy_capab.su_beamformer)
|
|
cap->phy_cap[EHT_PHYCAP_SU_BEAMFORMER_IDX] &=
|
|
~EHT_PHYCAP_SU_BEAMFORMER;
|
|
|
|
if (!hapd->iface->conf->eht_phy_capab.su_beamformee)
|
|
cap->phy_cap[EHT_PHYCAP_SU_BEAMFORMEE_IDX] &=
|
|
~EHT_PHYCAP_SU_BEAMFORMEE;
|
|
|
|
if (!hapd->iface->conf->eht_phy_capab.mu_beamformer)
|
|
cap->phy_cap[EHT_PHYCAP_MU_BEAMFORMER_IDX] &=
|
|
~EHT_PHYCAP_MU_BEAMFORMER_MASK;
|
|
|
|
pos = cap->optional;
|
|
|
|
mcs_nss_len = ieee80211_eht_mcs_set_size(mode->mode,
|
|
hapd->iconf->op_class,
|
|
hapd->iconf->he_oper_chwidth,
|
|
mode->he_capab[opmode].phy_cap,
|
|
eht_cap->phy_cap);
|
|
if (mcs_nss_len) {
|
|
os_memcpy(pos, eht_cap->mcs, mcs_nss_len);
|
|
pos += mcs_nss_len;
|
|
}
|
|
|
|
ppe_thresh_len = ieee80211_eht_ppet_size(
|
|
WPA_GET_LE16(&eht_cap->ppet[0]),
|
|
eht_cap->phy_cap);
|
|
if (ppe_thresh_len) {
|
|
os_memcpy(pos, eht_cap->ppet, ppe_thresh_len);
|
|
pos += ppe_thresh_len;
|
|
}
|
|
|
|
*length_pos = pos - (eid + 2);
|
|
return pos;
|
|
}
|
|
|
|
|
|
u8 * hostapd_eid_eht_operation(struct hostapd_data *hapd, u8 *eid)
|
|
{
|
|
struct hostapd_config *conf = hapd->iconf;
|
|
struct ieee80211_eht_operation *oper;
|
|
u8 *pos = eid, seg0 = 0, seg1 = 0;
|
|
enum oper_chan_width chwidth;
|
|
size_t elen = 1 + 4 + 3;
|
|
|
|
if (!hapd->iface->current_mode)
|
|
return eid;
|
|
|
|
if (hapd->iconf->punct_bitmap)
|
|
elen += EHT_OPER_DISABLED_SUBCHAN_BITMAP_SIZE;
|
|
|
|
*pos++ = WLAN_EID_EXTENSION;
|
|
*pos++ = 1 + elen;
|
|
*pos++ = WLAN_EID_EXT_EHT_OPERATION;
|
|
|
|
oper = (struct ieee80211_eht_operation *) pos;
|
|
oper->oper_params = EHT_OPER_INFO_PRESENT;
|
|
|
|
/* TODO: Fill in appropriate EHT-MCS max Nss information */
|
|
oper->basic_eht_mcs_nss_set[0] = 0x11;
|
|
oper->basic_eht_mcs_nss_set[1] = 0x00;
|
|
oper->basic_eht_mcs_nss_set[2] = 0x00;
|
|
oper->basic_eht_mcs_nss_set[3] = 0x00;
|
|
|
|
if (is_6ghz_op_class(conf->op_class))
|
|
chwidth = op_class_to_ch_width(conf->op_class);
|
|
else
|
|
chwidth = conf->eht_oper_chwidth;
|
|
|
|
seg0 = hostapd_get_oper_centr_freq_seg0_idx(conf);
|
|
|
|
switch (chwidth) {
|
|
case CONF_OPER_CHWIDTH_320MHZ:
|
|
oper->oper_info.control |= EHT_OPER_CHANNEL_WIDTH_320MHZ;
|
|
seg1 = seg0;
|
|
if (hapd->iconf->channel < seg0)
|
|
seg0 -= 16;
|
|
else
|
|
seg0 += 16;
|
|
break;
|
|
case CONF_OPER_CHWIDTH_160MHZ:
|
|
oper->oper_info.control |= EHT_OPER_CHANNEL_WIDTH_160MHZ;
|
|
seg1 = seg0;
|
|
if (hapd->iconf->channel < seg0)
|
|
seg0 -= 8;
|
|
else
|
|
seg0 += 8;
|
|
break;
|
|
case CONF_OPER_CHWIDTH_80MHZ:
|
|
oper->oper_info.control |= EHT_OPER_CHANNEL_WIDTH_80MHZ;
|
|
break;
|
|
case CONF_OPER_CHWIDTH_USE_HT:
|
|
if (seg0)
|
|
oper->oper_info.control |= EHT_OPER_CHANNEL_WIDTH_40MHZ;
|
|
break;
|
|
default:
|
|
return eid;
|
|
}
|
|
|
|
oper->oper_info.ccfs0 = seg0 ? seg0 : hapd->iconf->channel;
|
|
oper->oper_info.ccfs1 = seg1;
|
|
|
|
if (hapd->iconf->punct_bitmap) {
|
|
oper->oper_params |= EHT_OPER_DISABLED_SUBCHAN_BITMAP_PRESENT;
|
|
oper->oper_info.disabled_chan_bitmap =
|
|
host_to_le16(hapd->iconf->punct_bitmap);
|
|
}
|
|
|
|
return pos + elen;
|
|
}
|
|
|
|
|
|
static bool check_valid_eht_mcs_nss(struct hostapd_data *hapd, const u8 *ap_mcs,
|
|
const u8 *sta_mcs, u8 mcs_count, u8 map_len)
|
|
{
|
|
unsigned int i, j;
|
|
|
|
for (i = 0; i < mcs_count; i++) {
|
|
ap_mcs += i * 3;
|
|
sta_mcs += i * 3;
|
|
|
|
for (j = 0; j < map_len; j++) {
|
|
if (((ap_mcs[j] >> 4) & 0xFF) == 0)
|
|
continue;
|
|
|
|
if ((sta_mcs[j] & 0xFF) == 0)
|
|
continue;
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
wpa_printf(MSG_DEBUG,
|
|
"No matching EHT MCS found between AP TX and STA RX");
|
|
return false;
|
|
}
|
|
|
|
|
|
static bool check_valid_eht_mcs(struct hostapd_data *hapd,
|
|
const u8 *sta_eht_capab,
|
|
enum ieee80211_op_mode opmode)
|
|
{
|
|
struct hostapd_hw_modes *mode;
|
|
const struct ieee80211_eht_capabilities *capab;
|
|
const u8 *ap_mcs, *sta_mcs;
|
|
u8 mcs_count = 1;
|
|
|
|
mode = hapd->iface->current_mode;
|
|
if (!mode)
|
|
return true;
|
|
|
|
ap_mcs = mode->eht_capab[opmode].mcs;
|
|
capab = (const struct ieee80211_eht_capabilities *) sta_eht_capab;
|
|
sta_mcs = capab->optional;
|
|
|
|
if (ieee80211_eht_mcs_set_size(mode->mode, hapd->iconf->op_class,
|
|
hapd->iconf->he_oper_chwidth,
|
|
mode->he_capab[opmode].phy_cap,
|
|
mode->eht_capab[opmode].phy_cap) ==
|
|
EHT_PHYCAP_MCS_NSS_LEN_20MHZ_ONLY)
|
|
return check_valid_eht_mcs_nss(
|
|
hapd, ap_mcs, sta_mcs, 1,
|
|
EHT_PHYCAP_MCS_NSS_LEN_20MHZ_ONLY);
|
|
|
|
switch (hapd->iface->conf->eht_oper_chwidth) {
|
|
case CONF_OPER_CHWIDTH_320MHZ:
|
|
mcs_count++;
|
|
/* fall through */
|
|
case CONF_OPER_CHWIDTH_80P80MHZ:
|
|
case CONF_OPER_CHWIDTH_160MHZ:
|
|
mcs_count++;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return check_valid_eht_mcs_nss(hapd, ap_mcs, sta_mcs, mcs_count,
|
|
EHT_PHYCAP_MCS_NSS_LEN_20MHZ_PLUS);
|
|
}
|
|
|
|
|
|
static bool ieee80211_invalid_eht_cap_size(enum hostapd_hw_mode mode,
|
|
u8 opclass, u8 he_oper_chwidth,
|
|
const u8 *he_cap, const u8 *eht_cap,
|
|
size_t len)
|
|
{
|
|
const struct ieee80211_he_capabilities *he_capab;
|
|
struct ieee80211_eht_capabilities *cap;
|
|
const u8 *he_phy_cap;
|
|
size_t cap_len;
|
|
u16 ppe_thres_hdr;
|
|
|
|
he_capab = (const struct ieee80211_he_capabilities *) he_cap;
|
|
he_phy_cap = he_capab->he_phy_capab_info;
|
|
cap = (struct ieee80211_eht_capabilities *) eht_cap;
|
|
cap_len = sizeof(*cap) - sizeof(cap->optional);
|
|
if (len < cap_len)
|
|
return true;
|
|
|
|
cap_len += ieee80211_eht_mcs_set_size(mode, opclass, he_oper_chwidth,
|
|
he_phy_cap, cap->phy_cap);
|
|
if (len < cap_len)
|
|
return true;
|
|
|
|
ppe_thres_hdr = len > cap_len + 1 ?
|
|
WPA_GET_LE16(&eht_cap[cap_len]) : 0x01ff;
|
|
cap_len += ieee80211_eht_ppet_size(ppe_thres_hdr, cap->phy_cap);
|
|
|
|
return len < cap_len;
|
|
}
|
|
|
|
|
|
u16 copy_sta_eht_capab(struct hostapd_data *hapd, struct sta_info *sta,
|
|
enum ieee80211_op_mode opmode,
|
|
const u8 *he_capab, size_t he_capab_len,
|
|
const u8 *eht_capab, size_t eht_capab_len)
|
|
{
|
|
struct hostapd_hw_modes *c_mode = hapd->iface->current_mode;
|
|
enum hostapd_hw_mode mode = c_mode ? c_mode->mode : NUM_HOSTAPD_MODES;
|
|
|
|
if (!hapd->iconf->ieee80211be || hapd->conf->disable_11be ||
|
|
!he_capab || he_capab_len < IEEE80211_HE_CAPAB_MIN_LEN ||
|
|
!eht_capab ||
|
|
ieee80211_invalid_eht_cap_size(mode, hapd->iconf->op_class,
|
|
hapd->iconf->he_oper_chwidth,
|
|
he_capab, eht_capab,
|
|
eht_capab_len) ||
|
|
!check_valid_eht_mcs(hapd, eht_capab, opmode)) {
|
|
sta->flags &= ~WLAN_STA_EHT;
|
|
os_free(sta->eht_capab);
|
|
sta->eht_capab = NULL;
|
|
return WLAN_STATUS_SUCCESS;
|
|
}
|
|
|
|
os_free(sta->eht_capab);
|
|
sta->eht_capab = os_memdup(eht_capab, eht_capab_len);
|
|
if (!sta->eht_capab) {
|
|
sta->eht_capab_len = 0;
|
|
return WLAN_STATUS_UNSPECIFIED_FAILURE;
|
|
}
|
|
|
|
sta->flags |= WLAN_STA_EHT;
|
|
sta->eht_capab_len = eht_capab_len;
|
|
|
|
return WLAN_STATUS_SUCCESS;
|
|
}
|
|
|
|
|
|
void hostapd_get_eht_capab(struct hostapd_data *hapd,
|
|
const struct ieee80211_eht_capabilities *src,
|
|
struct ieee80211_eht_capabilities *dest,
|
|
size_t len)
|
|
{
|
|
if (!src || !dest)
|
|
return;
|
|
|
|
if (len > sizeof(*dest))
|
|
len = sizeof(*dest);
|
|
/* TODO: mask out unsupported features */
|
|
|
|
os_memset(dest, 0, sizeof(*dest));
|
|
os_memcpy(dest, src, len);
|
|
}
|
|
|
|
|
|
u8 * hostapd_eid_eht_basic_ml(struct hostapd_data *hapd, u8 *eid,
|
|
struct sta_info *info, bool include_mld_id)
|
|
{
|
|
struct wpabuf *buf;
|
|
u16 control;
|
|
u8 *pos = eid;
|
|
const u8 *ptr;
|
|
size_t len, slice_len;
|
|
u8 link_id;
|
|
u8 common_info_len;
|
|
|
|
/*
|
|
* As the Multi-Link element can exceed the size of 255 bytes need to
|
|
* first build it and then handle fragmentation.
|
|
*/
|
|
buf = wpabuf_alloc(1024);
|
|
if (!buf)
|
|
return pos;
|
|
|
|
/* Multi-Link Control field */
|
|
control = MULTI_LINK_CONTROL_TYPE_BASIC |
|
|
BASIC_MULTI_LINK_CTRL_PRES_LINK_ID |
|
|
BASIC_MULTI_LINK_CTRL_PRES_BSS_PARAM_CH_COUNT |
|
|
BASIC_MULTI_LINK_CTRL_PRES_EML_CAPA |
|
|
BASIC_MULTI_LINK_CTRL_PRES_MLD_CAPA;
|
|
|
|
/*
|
|
* Set the basic Multi-Link common information. Hard code the common
|
|
* info length to 13 based on the length of the present fields:
|
|
* Length (1) + MLD address (6) + Link ID (1) +
|
|
* BSS Parameters Change Count (1) + EML Capabilities (2) +
|
|
* MLD Capabilities and Operations (2)
|
|
*/
|
|
common_info_len = 13;
|
|
|
|
if (include_mld_id) {
|
|
/* AP MLD ID */
|
|
control |= BASIC_MULTI_LINK_CTRL_PRES_AP_MLD_ID;
|
|
common_info_len++;
|
|
}
|
|
|
|
wpabuf_put_le16(buf, control);
|
|
|
|
wpabuf_put_u8(buf, common_info_len);
|
|
|
|
/* Own MLD MAC Address */
|
|
wpabuf_put_data(buf, hapd->mld_addr, ETH_ALEN);
|
|
|
|
/* Own Link ID */
|
|
wpabuf_put_u8(buf, hapd->mld_link_id);
|
|
|
|
/* Currently hard code the BSS Parameters Change Count to 0x1 */
|
|
wpabuf_put_u8(buf, 0x1);
|
|
|
|
wpa_printf(MSG_DEBUG, "MLD: EML Capabilities=0x%x",
|
|
hapd->iface->mld_eml_capa);
|
|
wpabuf_put_le16(buf, hapd->iface->mld_eml_capa);
|
|
|
|
wpa_printf(MSG_DEBUG, "MLD: MLD Capabilities and Operations=0x%x",
|
|
hapd->iface->mld_mld_capa);
|
|
wpabuf_put_le16(buf, hapd->iface->mld_mld_capa);
|
|
|
|
if (include_mld_id) {
|
|
wpa_printf(MSG_DEBUG, "MLD: AP MLD ID=0x%x",
|
|
hapd->conf->mld_id);
|
|
wpabuf_put_u8(buf, hapd->conf->mld_id);
|
|
}
|
|
|
|
if (!info)
|
|
goto out;
|
|
|
|
/* Add link info for the other links */
|
|
for (link_id = 0; link_id < MAX_NUM_MLD_LINKS; link_id++) {
|
|
struct mld_link_info *link = &info->mld_info.links[link_id];
|
|
struct hostapd_data *link_bss;
|
|
|
|
/*
|
|
* control (2) + station info length (1) + MAC address (6) +
|
|
* beacon interval (2) + TSF offset (8) + DTIM info (2) + BSS
|
|
* parameters change counter (1) + station profile length.
|
|
*/
|
|
const size_t fixed_len = 22;
|
|
size_t total_len = fixed_len + link->resp_sta_profile_len;
|
|
|
|
/* Skip the local one */
|
|
if (link_id == hapd->mld_link_id || !link->valid)
|
|
continue;
|
|
|
|
link_bss = hostapd_mld_get_link_bss(hapd, link_id);
|
|
if (!link_bss) {
|
|
wpa_printf(MSG_ERROR,
|
|
"MLD: Couldn't find link BSS - skip it");
|
|
continue;
|
|
}
|
|
|
|
/* Per-STA Profile subelement */
|
|
wpabuf_put_u8(buf, EHT_ML_SUB_ELEM_PER_STA_PROFILE);
|
|
|
|
if (total_len <= 255)
|
|
wpabuf_put_u8(buf, total_len);
|
|
else
|
|
wpabuf_put_u8(buf, 255);
|
|
|
|
/* STA Control */
|
|
control = (link_id & 0xf) |
|
|
EHT_PER_STA_CTRL_MAC_ADDR_PRESENT_MSK |
|
|
EHT_PER_STA_CTRL_COMPLETE_PROFILE_MSK |
|
|
EHT_PER_STA_CTRL_TSF_OFFSET_PRESENT_MSK |
|
|
EHT_PER_STA_CTRL_BEACON_INTERVAL_PRESENT_MSK |
|
|
EHT_PER_STA_CTRL_DTIM_INFO_PRESENT_MSK |
|
|
EHT_PER_STA_CTRL_BSS_PARAM_CNT_PRESENT_MSK;
|
|
wpabuf_put_le16(buf, control);
|
|
|
|
/* STA Info */
|
|
|
|
/* STA Info Length */
|
|
wpabuf_put_u8(buf, fixed_len - 2);
|
|
wpabuf_put_data(buf, link->local_addr, ETH_ALEN);
|
|
wpabuf_put_le16(buf, link_bss->iconf->beacon_int);
|
|
|
|
/* TSF Offset */
|
|
/*
|
|
* TODO: Currently setting TSF offset to zero. However, this
|
|
* information needs to come from the driver.
|
|
*/
|
|
wpabuf_put_le64(buf, 0);
|
|
|
|
/* DTIM Info */
|
|
wpabuf_put_le16(buf, link_bss->conf->dtim_period);
|
|
|
|
/* BSS Parameters Change Count */
|
|
/* TODO: Currently hard code the BSS Parameters Change Count to
|
|
* 0x1 */
|
|
wpabuf_put_u8(buf, 0x1);
|
|
|
|
/* Fragment the sub element if needed */
|
|
if (total_len <= 255) {
|
|
wpabuf_put_data(buf, link->resp_sta_profile,
|
|
link->resp_sta_profile_len);
|
|
} else {
|
|
ptr = link->resp_sta_profile;
|
|
len = link->resp_sta_profile_len;
|
|
|
|
slice_len = 255 - fixed_len;
|
|
|
|
wpabuf_put_data(buf, ptr, slice_len);
|
|
len -= slice_len;
|
|
ptr += slice_len;
|
|
|
|
while (len) {
|
|
if (len <= 255)
|
|
slice_len = len;
|
|
else
|
|
slice_len = 255;
|
|
|
|
wpabuf_put_u8(buf, EHT_ML_SUB_ELEM_FRAGMENT);
|
|
wpabuf_put_u8(buf, slice_len);
|
|
wpabuf_put_data(buf, ptr, slice_len);
|
|
|
|
len -= slice_len;
|
|
ptr += slice_len;
|
|
}
|
|
}
|
|
}
|
|
|
|
out:
|
|
/* Fragment the Multi-Link element, if needed */
|
|
len = wpabuf_len(buf);
|
|
ptr = wpabuf_head(buf);
|
|
|
|
if (len <= 254)
|
|
slice_len = len;
|
|
else
|
|
slice_len = 254;
|
|
|
|
*pos++ = WLAN_EID_EXTENSION;
|
|
*pos++ = slice_len + 1;
|
|
*pos++ = WLAN_EID_EXT_MULTI_LINK;
|
|
os_memcpy(pos, ptr, slice_len);
|
|
|
|
ptr += slice_len;
|
|
pos += slice_len;
|
|
len -= slice_len;
|
|
|
|
while (len) {
|
|
if (len <= 255)
|
|
slice_len = len;
|
|
else
|
|
slice_len = 255;
|
|
|
|
*pos++ = WLAN_EID_FRAGMENT;
|
|
*pos++ = slice_len;
|
|
os_memcpy(pos, ptr, slice_len);
|
|
|
|
ptr += slice_len;
|
|
pos += slice_len;
|
|
len -= slice_len;
|
|
}
|
|
|
|
wpabuf_free(buf);
|
|
return pos;
|
|
}
|
|
|
|
|
|
struct wpabuf * hostapd_ml_auth_resp(struct hostapd_data *hapd)
|
|
{
|
|
struct wpabuf *buf = wpabuf_alloc(12);
|
|
|
|
if (!buf)
|
|
return NULL;
|
|
|
|
wpabuf_put_u8(buf, WLAN_EID_EXTENSION);
|
|
wpabuf_put_u8(buf, 10);
|
|
wpabuf_put_u8(buf, WLAN_EID_EXT_MULTI_LINK);
|
|
wpabuf_put_le16(buf, MULTI_LINK_CONTROL_TYPE_BASIC);
|
|
wpabuf_put_u8(buf, ETH_ALEN + 1);
|
|
wpabuf_put_data(buf, hapd->mld_addr, ETH_ALEN);
|
|
|
|
return buf;
|
|
}
|
|
|
|
|
|
static const u8 *
|
|
sae_commit_skip_fixed_fields(const struct ieee80211_mgmt *mgmt, size_t len,
|
|
const u8 *pos, u16 status_code)
|
|
{
|
|
u16 group;
|
|
size_t prime_len;
|
|
struct crypto_ec *ec;
|
|
|
|
if (status_code != WLAN_STATUS_SAE_HASH_TO_ELEMENT)
|
|
return pos;
|
|
|
|
/* SAE H2E commit message (group, scalar, FFE) */
|
|
if (len < 2) {
|
|
wpa_printf(MSG_DEBUG,
|
|
"EHT: SAE Group is not present");
|
|
return NULL;
|
|
}
|
|
|
|
group = WPA_GET_LE16(pos);
|
|
pos += 2;
|
|
|
|
/* TODO: How to parse when the group is unknown? */
|
|
ec = crypto_ec_init(group);
|
|
if (!ec) {
|
|
const struct dh_group *dh = dh_groups_get(group);
|
|
|
|
if (!dh) {
|
|
wpa_printf(MSG_DEBUG, "EHT: Unknown SAE group %u",
|
|
group);
|
|
return NULL;
|
|
}
|
|
|
|
prime_len = dh->prime_len;
|
|
} else {
|
|
prime_len = crypto_ec_prime_len(ec);
|
|
}
|
|
|
|
wpa_printf(MSG_DEBUG, "EHT: SAE scalar length is %zu", prime_len);
|
|
|
|
/* scalar */
|
|
pos += prime_len;
|
|
|
|
if (ec) {
|
|
pos += prime_len * 2;
|
|
crypto_ec_deinit(ec);
|
|
} else {
|
|
pos += prime_len;
|
|
}
|
|
|
|
if (pos - mgmt->u.auth.variable > (int) len) {
|
|
wpa_printf(MSG_DEBUG,
|
|
"EHT: Too short SAE commit Authentication frame");
|
|
return NULL;
|
|
}
|
|
|
|
wpa_hexdump(MSG_DEBUG, "EHT: SAE: Authentication frame elements",
|
|
pos, (int) len - (pos - mgmt->u.auth.variable));
|
|
|
|
return pos;
|
|
}
|
|
|
|
|
|
static const u8 *
|
|
sae_confirm_skip_fixed_fields(struct hostapd_data *hapd,
|
|
const struct ieee80211_mgmt *mgmt, size_t len,
|
|
const u8 *pos, u16 status_code)
|
|
{
|
|
struct sta_info *sta;
|
|
|
|
if (status_code == WLAN_STATUS_REJECTED_WITH_SUGGESTED_BSS_TRANSITION)
|
|
return pos;
|
|
|
|
/* send confirm integer */
|
|
pos += 2;
|
|
|
|
/*
|
|
* At this stage we should already have an MLD station and actually SA
|
|
* will be replaced with the MLD MAC address by the driver.
|
|
*/
|
|
sta = ap_get_sta(hapd, mgmt->sa);
|
|
if (!sta) {
|
|
wpa_printf(MSG_DEBUG, "SAE: No MLD STA for SAE confirm");
|
|
return NULL;
|
|
}
|
|
|
|
if (!sta->sae || sta->sae->state < SAE_COMMITTED || !sta->sae->tmp) {
|
|
if (sta->sae)
|
|
wpa_printf(MSG_DEBUG, "SAE: Invalid state=%u",
|
|
sta->sae->state);
|
|
else
|
|
wpa_printf(MSG_DEBUG, "SAE: No SAE context");
|
|
return NULL;
|
|
}
|
|
|
|
wpa_printf(MSG_DEBUG, "SAE: confirm: kck_len=%zu",
|
|
sta->sae->tmp->kck_len);
|
|
|
|
pos += sta->sae->tmp->kck_len;
|
|
|
|
if (pos - mgmt->u.auth.variable > (int) len) {
|
|
wpa_printf(MSG_DEBUG,
|
|
"EHT: Too short SAE confirm Authentication frame");
|
|
return NULL;
|
|
}
|
|
|
|
return pos;
|
|
}
|
|
|
|
|
|
static const u8 * auth_skip_fixed_fields(struct hostapd_data *hapd,
|
|
const struct ieee80211_mgmt *mgmt,
|
|
size_t len)
|
|
{
|
|
u16 auth_alg = le_to_host16(mgmt->u.auth.auth_alg);
|
|
u16 auth_transaction = le_to_host16(mgmt->u.auth.auth_transaction);
|
|
u16 status_code = le_to_host16(mgmt->u.auth.status_code);
|
|
const u8 *pos = mgmt->u.auth.variable;
|
|
|
|
/* Skip fixed fields as based on IEE P802.11-REVme/D3.0, Table 9-69
|
|
* (Presence of fields and elements in Authentications frames) */
|
|
switch (auth_alg) {
|
|
case WLAN_AUTH_OPEN:
|
|
return pos;
|
|
case WLAN_AUTH_SAE:
|
|
if (auth_transaction == 1) {
|
|
if (status_code == WLAN_STATUS_SUCCESS) {
|
|
wpa_printf(MSG_DEBUG,
|
|
"EHT: SAE H2E is mandatory for MLD");
|
|
goto out;
|
|
}
|
|
|
|
return sae_commit_skip_fixed_fields(mgmt, len, pos,
|
|
status_code);
|
|
} else if (auth_transaction == 2) {
|
|
return sae_confirm_skip_fixed_fields(hapd, mgmt, len,
|
|
pos, status_code);
|
|
}
|
|
|
|
return pos;
|
|
/* TODO: Support additional algorithms that can be used for MLO */
|
|
case WLAN_AUTH_FT:
|
|
case WLAN_AUTH_FILS_SK:
|
|
case WLAN_AUTH_FILS_SK_PFS:
|
|
case WLAN_AUTH_FILS_PK:
|
|
case WLAN_AUTH_PASN:
|
|
default:
|
|
break;
|
|
}
|
|
|
|
out:
|
|
wpa_printf(MSG_DEBUG,
|
|
"TODO: Authentication algorithm %u not supported with MLD",
|
|
auth_alg);
|
|
return NULL;
|
|
}
|
|
|
|
|
|
const u8 * hostapd_process_ml_auth(struct hostapd_data *hapd,
|
|
const struct ieee80211_mgmt *mgmt,
|
|
size_t len)
|
|
{
|
|
struct ieee802_11_elems elems;
|
|
const u8 *pos;
|
|
|
|
if (!hapd->conf->mld_ap)
|
|
return NULL;
|
|
|
|
len -= offsetof(struct ieee80211_mgmt, u.auth.variable);
|
|
|
|
pos = auth_skip_fixed_fields(hapd, mgmt, len);
|
|
if (!pos)
|
|
return NULL;
|
|
|
|
if (ieee802_11_parse_elems(pos,
|
|
(int)len - (pos - mgmt->u.auth.variable),
|
|
&elems, 0) == ParseFailed) {
|
|
wpa_printf(MSG_DEBUG,
|
|
"MLD: Failed parsing Authentication frame");
|
|
}
|
|
|
|
if (!elems.basic_mle || !elems.basic_mle_len)
|
|
return NULL;
|
|
|
|
return get_basic_mle_mld_addr(elems.basic_mle, elems.basic_mle_len);
|
|
}
|
|
|
|
|
|
static int hostapd_mld_validate_assoc_info(struct hostapd_data *hapd,
|
|
struct mld_info *info)
|
|
{
|
|
u8 i, link_id;
|
|
|
|
if (!info->mld_sta) {
|
|
wpa_printf(MSG_DEBUG, "MLD: Not a non-AP MLD");
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Iterate over the links negotiated in the (Re)Association Request
|
|
* frame and validate that they are indeed valid links in the local AP
|
|
* MLD.
|
|
*
|
|
* While at it, also update the local address for the links in the
|
|
* mld_info, so it could be easily available for later flows, e.g., for
|
|
* the RSN Authenticator, etc.
|
|
*/
|
|
for (link_id = 0; link_id < MAX_NUM_MLD_LINKS; link_id++) {
|
|
struct hostapd_data *other_hapd;
|
|
|
|
if (!info->links[link_id].valid)
|
|
continue;
|
|
|
|
for (i = 0; i < hapd->iface->interfaces->count; i++) {
|
|
other_hapd = hapd->iface->interfaces->iface[i]->bss[0];
|
|
|
|
if (hapd == other_hapd)
|
|
continue;
|
|
|
|
if (other_hapd->conf->mld_ap &&
|
|
other_hapd->conf->mld_id == hapd->conf->mld_id &&
|
|
link_id == other_hapd->mld_link_id)
|
|
break;
|
|
}
|
|
|
|
if (i == hapd->iface->interfaces->count &&
|
|
link_id != hapd->mld_link_id) {
|
|
wpa_printf(MSG_DEBUG, "MLD: Invalid link ID=%u",
|
|
link_id);
|
|
return -1;
|
|
}
|
|
|
|
if (i < hapd->iface->interfaces->count)
|
|
os_memcpy(info->links[link_id].local_addr,
|
|
other_hapd->own_addr,
|
|
ETH_ALEN);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
u16 hostapd_process_ml_assoc_req(struct hostapd_data *hapd,
|
|
struct ieee802_11_elems *elems,
|
|
struct sta_info *sta)
|
|
{
|
|
struct wpabuf *mlbuf;
|
|
const struct ieee80211_eht_ml *ml;
|
|
const struct eht_ml_basic_common_info *common_info;
|
|
size_t ml_len, common_info_len;
|
|
struct mld_link_info *link_info;
|
|
struct mld_info *info = &sta->mld_info;
|
|
const u8 *pos;
|
|
int ret = -1;
|
|
u16 ml_control;
|
|
|
|
mlbuf = ieee802_11_defrag_mle(elems, MULTI_LINK_CONTROL_TYPE_BASIC);
|
|
if (!mlbuf)
|
|
return WLAN_STATUS_SUCCESS;
|
|
|
|
ml = wpabuf_head(mlbuf);
|
|
ml_len = wpabuf_len(mlbuf);
|
|
|
|
ml_control = le_to_host16(ml->ml_control);
|
|
if ((ml_control & MULTI_LINK_CONTROL_TYPE_MASK) !=
|
|
MULTI_LINK_CONTROL_TYPE_BASIC) {
|
|
wpa_printf(MSG_DEBUG, "MLD: Invalid ML type=%u",
|
|
ml_control & MULTI_LINK_CONTROL_TYPE_MASK);
|
|
goto out;
|
|
}
|
|
|
|
/* Common Info length and MLD MAC address must always be present */
|
|
common_info_len = 1 + ETH_ALEN;
|
|
|
|
if (ml_control & BASIC_MULTI_LINK_CTRL_PRES_LINK_ID) {
|
|
wpa_printf(MSG_DEBUG, "MLD: Link ID info not expected");
|
|
goto out;
|
|
}
|
|
|
|
if (ml_control & BASIC_MULTI_LINK_CTRL_PRES_BSS_PARAM_CH_COUNT) {
|
|
wpa_printf(MSG_DEBUG, "MLD: BSS params change not expected");
|
|
goto out;
|
|
}
|
|
|
|
if (ml_control & BASIC_MULTI_LINK_CTRL_PRES_MSD_INFO) {
|
|
wpa_printf(MSG_DEBUG, "MLD: Sync delay not expected");
|
|
goto out;
|
|
}
|
|
|
|
if (ml_control & BASIC_MULTI_LINK_CTRL_PRES_EML_CAPA) {
|
|
common_info_len += 2;
|
|
} else {
|
|
wpa_printf(MSG_DEBUG, "MLD: EML capabilities not present");
|
|
}
|
|
|
|
if (ml_control & BASIC_MULTI_LINK_CTRL_PRES_MLD_CAPA) {
|
|
common_info_len += 2;
|
|
|
|
} else {
|
|
wpa_printf(MSG_DEBUG, "MLD: MLD capabilities not present");
|
|
goto out;
|
|
}
|
|
|
|
wpa_printf(MSG_DEBUG, "MLD: expected_common_info_len=%lu",
|
|
common_info_len);
|
|
|
|
if (sizeof(*ml) + common_info_len > ml_len) {
|
|
wpa_printf(MSG_DEBUG, "MLD: Not enough bytes for common info");
|
|
goto out;
|
|
}
|
|
|
|
common_info = (const struct eht_ml_basic_common_info *) ml->variable;
|
|
|
|
/* Common information length includes the length octet */
|
|
if (common_info->len != common_info_len) {
|
|
wpa_printf(MSG_DEBUG,
|
|
"MLD: Invalid common info len=%u (expected %zu)",
|
|
common_info->len, common_info_len);
|
|
goto out;
|
|
}
|
|
|
|
pos = common_info->variable;
|
|
|
|
if (ml_control & BASIC_MULTI_LINK_CTRL_PRES_EML_CAPA) {
|
|
info->common_info.eml_capa = WPA_GET_LE16(pos);
|
|
pos += 2;
|
|
} else {
|
|
info->common_info.eml_capa = 0;
|
|
}
|
|
|
|
info->common_info.mld_capa = WPA_GET_LE16(pos);
|
|
pos += 2;
|
|
|
|
wpa_printf(MSG_DEBUG, "MLD: addr=" MACSTR ", eml=0x%x, mld=0x%x",
|
|
MAC2STR(info->common_info.mld_addr),
|
|
info->common_info.eml_capa, info->common_info.mld_capa);
|
|
|
|
/* Check the MLD MAC Address */
|
|
if (os_memcmp(info->common_info.mld_addr, common_info->mld_addr,
|
|
ETH_ALEN) != 0) {
|
|
wpa_printf(MSG_DEBUG,
|
|
"MLD: MLD address mismatch between authentication ("
|
|
MACSTR ") and association (" MACSTR ")",
|
|
MAC2STR(info->common_info.mld_addr),
|
|
MAC2STR(common_info->mld_addr));
|
|
goto out;
|
|
}
|
|
|
|
info->links[hapd->mld_link_id].valid = true;
|
|
|
|
/* Parse the link info field */
|
|
ml_len -= sizeof(*ml) + common_info_len;
|
|
|
|
while (ml_len > 2) {
|
|
size_t sub_elem_len = *(pos + 1);
|
|
size_t sta_info_len;
|
|
u16 control;
|
|
|
|
wpa_printf(MSG_DEBUG, "MLD: sub element len=%zu",
|
|
sub_elem_len);
|
|
|
|
if (2 + sub_elem_len > ml_len) {
|
|
wpa_printf(MSG_DEBUG,
|
|
"MLD: Invalid link info len: %zu %zu",
|
|
2 + sub_elem_len, ml_len);
|
|
goto out;
|
|
}
|
|
|
|
if (*pos == MULTI_LINK_SUB_ELEM_ID_VENDOR) {
|
|
wpa_printf(MSG_DEBUG,
|
|
"MLD: Skip vendor specific subelement");
|
|
|
|
pos += 2 + sub_elem_len;
|
|
ml_len -= 2 + sub_elem_len;
|
|
continue;
|
|
}
|
|
|
|
if (*pos != MULTI_LINK_SUB_ELEM_ID_PER_STA_PROFILE) {
|
|
wpa_printf(MSG_DEBUG,
|
|
"MLD: Unexpected Multi-Link element subelement ID=%u",
|
|
*pos);
|
|
goto out;
|
|
}
|
|
|
|
/* Skip the subelement ID and the length */
|
|
pos += 2;
|
|
ml_len -= 2;
|
|
|
|
/* Get the station control field */
|
|
if (sub_elem_len < 2) {
|
|
wpa_printf(MSG_DEBUG,
|
|
"MLD: Too short Per-STA Profile subelement");
|
|
goto out;
|
|
}
|
|
control = WPA_GET_LE16(pos);
|
|
link_info = &info->links[control &
|
|
EHT_PER_STA_CTRL_LINK_ID_MSK];
|
|
pos += 2;
|
|
ml_len -= 2;
|
|
sub_elem_len -= 2;
|
|
|
|
if (!(control & EHT_PER_STA_CTRL_COMPLETE_PROFILE_MSK)) {
|
|
wpa_printf(MSG_DEBUG,
|
|
"MLD: Per-STA complete profile expected");
|
|
goto out;
|
|
}
|
|
|
|
if (!(control & EHT_PER_STA_CTRL_MAC_ADDR_PRESENT_MSK)) {
|
|
wpa_printf(MSG_DEBUG,
|
|
"MLD: Per-STA MAC address not present");
|
|
goto out;
|
|
}
|
|
|
|
if ((control & (EHT_PER_STA_CTRL_BEACON_INTERVAL_PRESENT_MSK |
|
|
EHT_PER_STA_CTRL_DTIM_INFO_PRESENT_MSK))) {
|
|
wpa_printf(MSG_DEBUG,
|
|
"MLD: Beacon/DTIM interval not expected");
|
|
goto out;
|
|
}
|
|
|
|
/* The length octet and the MAC address must be present */
|
|
sta_info_len = 1 + ETH_ALEN;
|
|
|
|
if (control & EHT_PER_STA_CTRL_NSTR_LINK_PAIR_PRESENT_MSK) {
|
|
if (control & EHT_PER_STA_CTRL_NSTR_BM_SIZE_MSK)
|
|
link_info->nstr_bitmap_len = 2;
|
|
else
|
|
link_info->nstr_bitmap_len = 1;
|
|
}
|
|
|
|
sta_info_len += link_info->nstr_bitmap_len;
|
|
|
|
if (sta_info_len > ml_len || sta_info_len != *pos ||
|
|
sta_info_len > sub_elem_len) {
|
|
wpa_printf(MSG_DEBUG, "MLD: Invalid STA Info length");
|
|
goto out;
|
|
}
|
|
|
|
/* skip the length */
|
|
pos++;
|
|
ml_len--;
|
|
|
|
/* get the link address */
|
|
os_memcpy(link_info->peer_addr, pos, ETH_ALEN);
|
|
wpa_printf(MSG_DEBUG,
|
|
"MLD: assoc: link id=%u, addr=" MACSTR,
|
|
control & EHT_PER_STA_CTRL_LINK_ID_MSK,
|
|
MAC2STR(link_info->peer_addr));
|
|
|
|
pos += ETH_ALEN;
|
|
ml_len -= ETH_ALEN;
|
|
|
|
/* Get the NSTR bitmap */
|
|
if (link_info->nstr_bitmap_len) {
|
|
os_memcpy(link_info->nstr_bitmap, pos,
|
|
link_info->nstr_bitmap_len);
|
|
pos += link_info->nstr_bitmap_len;
|
|
ml_len -= link_info->nstr_bitmap_len;
|
|
}
|
|
|
|
sub_elem_len -= sta_info_len;
|
|
|
|
wpa_printf(MSG_DEBUG, "MLD: STA Profile len=%zu", sub_elem_len);
|
|
if (sub_elem_len > ml_len)
|
|
goto out;
|
|
|
|
if (sub_elem_len > 2)
|
|
link_info->capability = WPA_GET_LE16(pos);
|
|
|
|
pos += sub_elem_len;
|
|
ml_len -= sub_elem_len;
|
|
|
|
wpa_printf(MSG_DEBUG, "MLD: link ctrl=0x%x, " MACSTR
|
|
", nstr bitmap len=%lu",
|
|
control, MAC2STR(link_info->peer_addr),
|
|
link_info->nstr_bitmap_len);
|
|
|
|
link_info->valid = true;
|
|
}
|
|
|
|
if (ml_len) {
|
|
wpa_printf(MSG_DEBUG, "MLD: %zu bytes left after parsing. fail",
|
|
ml_len);
|
|
goto out;
|
|
}
|
|
|
|
ret = hostapd_mld_validate_assoc_info(hapd, info);
|
|
out:
|
|
wpabuf_free(mlbuf);
|
|
if (ret) {
|
|
os_memset(info, 0, sizeof(*info));
|
|
return WLAN_STATUS_UNSPECIFIED_FAILURE;
|
|
}
|
|
|
|
return WLAN_STATUS_SUCCESS;
|
|
}
|