netdev: station: support full mac roaming

Roaming on a full mac card is quite different than soft mac
and needs to be specially handled. The process starts with
the CMD_ROAM event, which tells us the driver is already
roamed and associated with a new AP. After this it expects
the 4-way handshake to be initiated. This in itself is quite
simple, the complexity comes with how this is piped into IWD.

After CMD_ROAM fires its assumed that a scan result is
available in the kernel, which is obtained using a newly
added scan API scan_get_firmware_scan. The only special
bit of this is that it does not 'schedule' a scan but simply
calls GET_SCAN. This is treated special and will not be
queued behind any other pending scan requests. This lets us
reuse some parsing code paths in scan and initialize a
scan_bss object which ultimately gets handed to station so
it can update connected_bss/bss_list.

For consistency station must also transition to a roaming state.
Since this roam is all handled by netdev two new events were
added, NETDEV_EVENT_ROAMING and NETDEV_EVENT_ROAMED. Both allow
station to transition between roaming/connected states, and ROAMED
provides station with the new scan_bss to replace connected_bss.
This commit is contained in:
James Prestwood 2021-03-15 10:29:29 -07:00 committed by Denis Kenzior
parent e8c87c8b42
commit 133347440e
3 changed files with 173 additions and 3 deletions

View File

@ -128,6 +128,8 @@ struct netdev {
uint32_t rssi_poll_cmd_id;
uint8_t set_mac_once[6];
struct scan_bss *fw_roam_bss;
uint32_t set_powered_cmd_id;
netdev_command_cb_t set_powered_cb;
void *set_powered_user_data;
@ -770,6 +772,9 @@ static void netdev_free(void *data)
netdev->get_station_cmd_id = 0;
}
if (netdev->fw_roam_bss)
scan_bss_free(netdev->fw_roam_bss);
if (netdev->events_ready)
WATCHLIST_NOTIFY(&netdev_watches, netdev_watch_func_t,
netdev, NETDEV_WATCH_EVENT_DEL);
@ -1188,6 +1193,17 @@ static void netdev_connect_ok(struct netdev *netdev)
netdev->operational = true;
if (netdev->fw_roam_bss) {
if (netdev->event_filter)
netdev->event_filter(netdev, NETDEV_EVENT_ROAMED,
netdev->fw_roam_bss,
netdev->user_data);
else
scan_bss_free(netdev->fw_roam_bss);
netdev->fw_roam_bss = NULL;
}
if (netdev->connect_cb) {
netdev->connect_cb(netdev, NETDEV_RESULT_OK, NULL,
netdev->user_data);
@ -1807,6 +1823,7 @@ static void netdev_connect_event(struct l_genl_msg *msg, struct netdev *netdev)
struct ie_tlv_iter iter;
const uint8_t *resp_ies = NULL;
size_t resp_ies_len;
uint8_t cmd = l_genl_msg_get_command(msg);
l_debug("");
@ -1860,9 +1877,16 @@ static void netdev_connect_event(struct l_genl_msg *msg, struct netdev *netdev)
goto error;
}
/* AP Rejected the authenticate / associate */
if (!status_code || *status_code != 0)
goto error;
/*
* A CMD_ROAM event will not have a status code, since it indicates
* the hardware has already roamed. A failed roam on fullmac should
* result in an explicit disconnect event.
*/
if (cmd == NL80211_CMD_CONNECT) {
/* AP Rejected the authenticate / associate */
if (!status_code || *status_code != 0)
goto error;
}
if (!ies)
goto process_resp_ies;
@ -1948,6 +1972,18 @@ process_resp_ies:
}
if (netdev->sm) {
/*
* Let station know about the roam so a state change can occur.
*/
if (cmd == NL80211_CMD_ROAM) {
if (netdev->event_filter)
netdev->event_filter(netdev,
NETDEV_EVENT_ROAMING,
NULL, netdev->user_data);
/* EAPoL started after GET_SCAN */
return;
}
/*
* Start processing EAPoL frames now that the state machine
* has all the input data even in FT mode.
@ -3905,6 +3941,111 @@ static void netdev_scan_notify(struct l_genl_msg *msg, void *user_data)
}
}
static bool match_addr(const void *a, const void *b)
{
const struct scan_bss *bss = a;
return memcmp(bss->addr, b, 6) == 0;
}
static bool netdev_get_fw_scan_cb(int err, struct l_queue *bss_list,
const struct scan_freq_set *freqs,
void *user_data)
{
struct netdev *netdev = user_data;
struct scan_bss *bss = NULL;
/*
* If there was a failure in netdev_connect_event this would reset
* the connect state (netdev_connect_free) causing the sm to be freed.
* In this case we should just ignore this and allow the disconnect
* logic to continue.
*/
if (!netdev->sm)
return false;
if (err < 0) {
l_error("Failed to get scan after roam (%d)", err);
netdev_connect_failed(netdev, NETDEV_RESULT_ABORTED,
MMPDU_REASON_CODE_UNSPECIFIED);
return false;
}
/*
* We don't actually need the entire list since we only provide
* station with the roamed BSS. We can remove the BSS we want and by
* returning false scan will keep ownership of the list.
*/
bss = l_queue_remove_if(bss_list, match_addr, netdev->handshake->aa);
if (!bss) {
l_error("Roam target BSS not found in scan results");
netdev_connect_failed(netdev, NETDEV_RESULT_ABORTED,
MMPDU_REASON_CODE_UNSPECIFIED);
return false;
}
netdev->fw_roam_bss = bss;
handshake_state_set_authenticator_ie(netdev->handshake, bss->rsne);
eapol_start(netdev->sm);
return false;
}
/*
* CMD_ROAM indicates that the driver has already roamed/associated with a new
* AP. This event is nearly identical to the CMD_CONNECT event which is why
* netdev_connect_event will handle all the parsing of IE's just as it does
* normally.
*
* Using GET_SCAN we can grab all the required scan_bss data, create that object
* and provide it to station.
*
* The current handshake/netdev_handshake objects are reused after being
* reset to allow eapol to happen again without it thinking this is a re-key.
*/
static bool netdev_roam_event(struct l_genl_msg *msg, struct netdev *netdev)
{
struct netdev_handshake_state *nhs =
l_container_of(netdev->handshake,
struct netdev_handshake_state,
super);
const uint8_t *mac;
l_debug("");
netdev->operational = false;
if (nl80211_parse_attrs(msg, NL80211_ATTR_MAC, &mac,
NL80211_ATTR_UNSPEC) < 0) {
l_error("Failed to parse ATTR_MAC from CMD_ROAM");
goto failed;
}
/* Reset handshake state */
nhs->complete = false;
nhs->ptk_installed = false;
nhs->gtk_installed = true;
nhs->igtk_installed = true;
handshake_state_set_authenticator_address(netdev->handshake, mac);
netdev->handshake->ptk_complete = false;
if (!scan_get_firmware_scan(netdev->wdev_id, netdev_get_fw_scan_cb,
netdev, NULL))
goto failed;
return true;
failed:
l_error("Failed to roam to new BSS");
netdev_connect_failed(netdev, NETDEV_RESULT_ABORTED,
MMPDU_REASON_CODE_UNSPECIFIED);
return false;
}
static void netdev_mlme_notify(struct l_genl_msg *msg, void *user_data)
{
struct netdev *netdev = NULL;
@ -3946,6 +4087,10 @@ static void netdev_mlme_notify(struct l_genl_msg *msg, void *user_data)
case NL80211_CMD_ASSOCIATE:
netdev_associate_event(msg, netdev);
break;
case NL80211_CMD_ROAM:
if (!netdev_roam_event(msg, netdev))
return;
/* fall through */
case NL80211_CMD_CONNECT:
netdev_connect_event(msg, netdev);
break;

View File

@ -41,6 +41,8 @@ enum netdev_result {
enum netdev_event {
NETDEV_EVENT_AUTHENTICATING,
NETDEV_EVENT_ASSOCIATING,
NETDEV_EVENT_ROAMING,
NETDEV_EVENT_ROAMED,
NETDEV_EVENT_DISCONNECT_BY_AP,
NETDEV_EVENT_DISCONNECT_BY_SME,
NETDEV_EVENT_RSSI_THRESHOLD_LOW,

View File

@ -2291,6 +2291,23 @@ static void station_ok_rssi(struct station *station)
station->roam_min_time.tv_sec = 0;
}
static void station_event_roamed(struct station *station, struct scan_bss *new)
{
struct scan_bss *stale;
/* Remove new BSS if it exists in past scan results */
stale = l_queue_remove_if(station->bss_list, bss_match_bssid,
new->addr);
if (stale)
scan_bss_free(stale);
station->connected_bss = new;
l_queue_insert(station->bss_list, new, scan_bss_rank_compare, NULL);
station_enter_state(station, STATION_STATE_CONNECTED);
}
static void station_rssi_level_changed(struct station *station,
uint8_t level_idx);
@ -2319,6 +2336,12 @@ static void station_netdev_event(struct netdev *netdev, enum netdev_event event,
case NETDEV_EVENT_RSSI_LEVEL_NOTIFY:
station_rssi_level_changed(station, l_get_u8(event_data));
break;
case NETDEV_EVENT_ROAMING:
station_enter_state(station, STATION_STATE_ROAMING);
break;
case NETDEV_EVENT_ROAMED:
station_event_roamed(station, (struct scan_bss *) event_data);
break;
}
}