From 575d0fa55471e4cd125e091d362766c96e06537f Mon Sep 17 00:00:00 2001 From: Denis Kenzior Date: Tue, 4 Sep 2018 13:48:18 -0500 Subject: [PATCH] station: Move roam triggering & scanning code Move the roam initiation (signal loss, ap directed roaming) and scanning details into station from device. Certain device functions have been exposed temporarily to make this possible. --- src/device.c | 601 ++------------------------------------------------ src/device.h | 2 + src/station.c | 569 +++++++++++++++++++++++++++++++++++++++++++++++ src/station.h | 20 ++ 4 files changed, 612 insertions(+), 580 deletions(-) diff --git a/src/device.c b/src/device.c index bd177288..0bcf9b28 100644 --- a/src/device.c +++ b/src/device.c @@ -26,7 +26,6 @@ #include #include -#include #include #include @@ -55,9 +54,6 @@ struct device { struct network *connected_network; struct l_dbus_message *connect_pending; struct l_dbus_message *disconnect_pending; - struct timespec roam_min_time; - struct l_timeout *roam_trigger_timeout; - uint32_t roam_scan_id; uint8_t preauth_bssid[ETH_ALEN]; struct signal_agent *signal_agent; @@ -68,10 +64,6 @@ struct device { bool powered : 1; bool scanning : 1; bool autoconnect : 1; - bool preparing_roam : 1; - bool signal_low : 1; - bool roam_no_orig_ap : 1; - bool ap_directed_roaming : 1; uint32_t ap_roam_watch; }; @@ -85,8 +77,6 @@ struct signal_agent { static struct l_queue *device_list; static uint32_t netdev_watch; -static void device_roam_timeout_rearm(struct device *device, int seconds); - static void device_netdev_event(struct netdev *netdev, enum netdev_event event, void *user_data); @@ -255,14 +245,7 @@ static void device_reset_connection_state(struct device *device) station->state == STATION_STATE_ROAMING) network_disconnected(network); - l_timeout_remove(device->roam_trigger_timeout); - device->roam_trigger_timeout = NULL; - device->preparing_roam = false; - device->signal_low = false; - device->roam_min_time.tv_sec = 0; - - if (device->roam_scan_id) - scan_cancel(device->index, device->roam_scan_id); + station_roam_state_clear(station); device->connected_bss = NULL; station->connected_bss = NULL; @@ -275,7 +258,7 @@ static void device_reset_connection_state(struct device *device) IWD_NETWORK_INTERFACE, "Connected"); } -static void device_disassociated(struct device *device) +void device_disassociated(struct device *device) { l_debug("%d", device->index); @@ -303,30 +286,6 @@ static void device_disconnect_event(struct device *device) device_disassociated(device); } -static void device_roam_failed(struct device *device) -{ - struct station *station = device->station; - - /* - * If we're still connected to the old BSS, only clear preparing_roam - * and reattempt in 60 seconds if signal level is still low at that - * time. Otherwise (we'd already started negotiating with the - * transition target, preparing_roam is false, state is roaming) we - * are now disconnected. - */ - - l_debug("%d", device->index); - - device->preparing_roam = false; - device->roam_no_orig_ap = false; - device->ap_directed_roaming = false; - - if (station->state == STATION_STATE_ROAMING) - device_disassociated(device); - else if (device->signal_low) - device_roam_timeout_rearm(device, 60); -} - static void device_reassociate_cb(struct netdev *netdev, enum netdev_result result, void *user_data) @@ -340,17 +299,10 @@ static void device_reassociate_cb(struct netdev *netdev, return; if (result == NETDEV_RESULT_OK) { - /* - * New signal high/low notification should occur on the next - * beacon from new AP. - */ - device->signal_low = false; - device->roam_min_time.tv_sec = 0; - device->roam_no_orig_ap = false; - + station_roamed(station); device_enter_state(device, STATION_STATE_CONNECTED); } else - device_roam_failed(device); + station_roam_failed(station); } static void device_fast_transition_cb(struct netdev *netdev, @@ -366,17 +318,10 @@ static void device_fast_transition_cb(struct netdev *netdev, return; if (result == NETDEV_RESULT_OK) { - /* - * New signal high/low notification should occur on the next - * beacon from new AP. - */ - device->signal_low = false; - device->roam_min_time.tv_sec = 0; - device->roam_no_orig_ap = false; - + station_roamed(station); device_enter_state(device, STATION_STATE_CONNECTED); } else - device_roam_failed(device); + station_roam_failed(station); } static void device_transition_reassociate(struct device *device, @@ -389,14 +334,13 @@ static void device_transition_reassociate(struct device *device, new_hs, device_netdev_event, device_reassociate_cb, device) < 0) { handshake_state_free(new_hs); - - device_roam_failed(device); + station_roam_failed(station); return; } device->connected_bss = bss; station->connected_bss = bss; - device->preparing_roam = false; + station->preparing_roam = false; device_enter_state(device, STATION_STATE_ROAMING); } @@ -420,15 +364,14 @@ static void device_preauthenticate_cb(struct netdev *netdev, l_debug("%d, result: %d", device->index, result); - if (!device->preparing_roam || result == NETDEV_RESULT_ABORTED) + if (!station->preparing_roam || result == NETDEV_RESULT_ABORTED) return; bss = l_queue_find(device->station->bss_list, bss_match_bssid, device->preauth_bssid); if (!bss) { l_error("Roam target BSS not found"); - - device_roam_failed(device); + station_roam_failed(station); return; } @@ -436,7 +379,7 @@ static void device_preauthenticate_cb(struct netdev *netdev, if (!new_hs) { l_error("device_handshake_setup failed"); - device_roam_failed(device); + station_roam_failed(station); return; } @@ -474,7 +417,7 @@ static void device_preauthenticate_cb(struct netdev *netdev, device_transition_reassociate(device, bss, new_hs); } -static void device_transition_start(struct device *device, struct scan_bss *bss) +void device_transition_start(struct device *device, struct scan_bss *bss) { struct handshake_state *hs = netdev_get_handshake(device->netdev); struct network *connected = device_get_connected_network(device); @@ -488,7 +431,7 @@ static void device_transition_start(struct device *device, struct scan_bss *bss) util_address_to_string(bss->addr)); /* Reset AP roam flag, at this point the roaming behaves the same */ - device->ap_directed_roaming = false; + station->ap_directed_roaming = false; if (hs->mde) ie_parse_mobility_domain_from_data(hs->mde, hs->mde[1] + 2, @@ -508,13 +451,13 @@ static void device_transition_start(struct device *device, struct scan_bss *bss) */ if (netdev_fast_transition(device->netdev, bss, device_fast_transition_cb) < 0) { - device_roam_failed(device); + station_roam_failed(station); return; } device->connected_bss = bss; station->connected_bss = bss; - device->preparing_roam = false; + station->preparing_roam = false; device_enter_state(device, STATION_STATE_ROAMING); return; @@ -530,7 +473,7 @@ static void device_transition_start(struct device *device, struct scan_bss *bss) * the current association." */ if (security == SECURITY_8021X && - !device->roam_no_orig_ap && + !station->roam_no_orig_ap && scan_bss_get_rsn_info(device->connected_bss, &cur_rsne) >= 0 && scan_bss_get_rsn_info(bss, &target_rsne) >= 0 && @@ -555,402 +498,13 @@ static void device_transition_start(struct device *device, struct scan_bss *bss) new_hs = station_handshake_setup(station, connected, bss); if (!new_hs) { l_error("device_handshake_setup failed in reassociation"); - - device_roam_failed(device); + station_roam_failed(station); return; } device_transition_reassociate(device, bss, new_hs); } -static void device_roam_scan_triggered(int err, void *user_data) -{ - struct device *device = user_data; - - if (err) { - device_roam_failed(device); - return; - } - - /* - * Do not update the Scanning property as we won't be updating the - * list of networks. - */ -} - -static bool device_roam_scan_notify(uint32_t wiphy_id, uint32_t ifindex, - int err, struct l_queue *bss_list, - void *userdata) -{ - struct device *device = userdata; - struct network *network = device->connected_network; - struct handshake_state *hs = netdev_get_handshake(device->netdev); - struct scan_bss *bss; - struct scan_bss *best_bss = NULL; - double best_bss_rank = 0.0; - static const double RANK_FT_FACTOR = 1.3; - uint16_t mdid; - enum security orig_security, security; - bool seen = false; - - if (err) { - device_roam_failed(device); - - return false; - } - - /* - * Do not call device_set_scan_results because this may have been - * a partial scan. We could at most update the current networks' BSS - * list in its device->networks entry. - */ - - orig_security = network_get_security(network); - - if (hs->mde) - ie_parse_mobility_domain_from_data(hs->mde, hs->mde[1] + 2, - &mdid, NULL, NULL); - - /* - * BSSes in the bss_list come already ranked with their initial - * association preference rank value. We only need to add preference - * for BSSes that are within the FT Mobility Domain so as to favor - * Fast Roaming, if it is supported. - */ - - while ((bss = l_queue_pop_head(bss_list))) { - double rank; - struct ie_rsn_info info; - int r; - - /* Skip the BSS we are connected to if doing an AP roam */ - if (device->ap_directed_roaming && !memcmp(bss->addr, - device->connected_bss->addr, 6)) - goto next; - - /* Skip result if it is not part of the ESS */ - - if (bss->ssid_len != hs->ssid_len || - memcmp(bss->ssid, hs->ssid, hs->ssid_len)) - goto next; - - memset(&info, 0, sizeof(info)); - r = scan_bss_get_rsn_info(bss, &info); - if (r < 0) { - if (r != -ENOENT) - goto next; - - security = security_determine(bss->capability, NULL); - } else - security = security_determine(bss->capability, &info); - - if (security != orig_security) - goto next; - - seen = true; - - if (!wiphy_can_connect(device->wiphy, bss)) - goto next; - - rank = bss->rank; - - if (hs->mde && bss->mde_present && l_get_le16(bss->mde) == mdid) - rank *= RANK_FT_FACTOR; - - if (rank > best_bss_rank) { - if (best_bss) - scan_bss_free(best_bss); - - best_bss = bss; - best_bss_rank = rank; - - continue; - } - -next: - scan_bss_free(bss); - } - - l_queue_destroy(bss_list, NULL); - - if (!seen) - goto fail_free_bss; - - /* See if we have anywhere to roam to */ - if (!best_bss || scan_bss_addr_eq(best_bss, device->connected_bss)) - goto fail_free_bss; - - bss = network_bss_find_by_addr(network, best_bss->addr); - if (bss) { - scan_bss_free(best_bss); - best_bss = bss; - } else { - network_bss_add(network, best_bss); - l_queue_push_tail(device->station->bss_list, best_bss); - } - - device_transition_start(device, best_bss); - - return true; - -fail_free_bss: - if (best_bss) - scan_bss_free(best_bss); - - device_roam_failed(device); - - return true; -} - -static void device_roam_scan_destroy(void *userdata) -{ - struct device *device = userdata; - - device->roam_scan_id = 0; -} - -static void device_roam_scan(struct device *device, - struct scan_freq_set *freq_set) -{ - struct scan_parameters params = { .freqs = freq_set, .flush = true }; - - if (device->connected_network) - /* Use direct probe request */ - params.ssid = network_get_ssid(device->connected_network); - - device->roam_scan_id = scan_active_full(device->index, ¶ms, - device_roam_scan_triggered, - device_roam_scan_notify, device, - device_roam_scan_destroy); - - if (!device->roam_scan_id) - device_roam_failed(device); -} - -static uint32_t device_freq_from_neighbor_report(const uint8_t *country, - struct ie_neighbor_report_info *info, enum scan_band *out_band) -{ - enum scan_band band; - uint32_t freq; - - if (info->oper_class == 0) { - /* - * Some Cisco APs report all operating class values as 0 - * in the Neighbor Report Responses. Work around this by - * using the most likely operating class for the channel - * number as the 2.4GHz and 5GHz bands happen to mostly - * use channels in two disjoint ranges. - */ - if (info->channel_num >= 1 && info->channel_num <= 14) - band = SCAN_BAND_2_4_GHZ; - else if (info->channel_num >= 36 && info->channel_num <= 169) - band = SCAN_BAND_5_GHZ; - else { - l_debug("Ignored: 0 oper class with an unusual " - "channel number"); - - return 0; - } - } else { - band = scan_oper_class_to_band(country, info->oper_class); - if (!band) { - l_debug("Ignored: unsupported oper class"); - - return 0; - } - } - - freq = scan_channel_to_freq(info->channel_num, band); - if (!freq) { - l_debug("Ignored: unsupported channel"); - - return 0; - } - - if (out_band) - *out_band = band; - - return freq; -} - -static void device_neighbor_report_cb(struct netdev *netdev, int err, - const uint8_t *reports, - size_t reports_len, void *user_data) -{ - struct device *device = user_data; - struct ie_tlv_iter iter; - int count_md = 0, count_no_md = 0; - struct scan_freq_set *freq_set_md, *freq_set_no_md; - uint32_t current_freq = 0; - struct handshake_state *hs = netdev_get_handshake(device->netdev); - - /* - * Check if we're still attempting to roam -- if dbus Disconnect - * had been called in the meantime we just abort the attempt. - */ - if (!device->preparing_roam || err == -ENODEV) - return; - - if (!reports || err) { - /* Have to do a full scan */ - device_roam_scan(device, NULL); - - return; - } - - freq_set_md = scan_freq_set_new(); - freq_set_no_md = scan_freq_set_new(); - - ie_tlv_iter_init(&iter, reports, reports_len); - - /* First see if any of the reports contain the MD bit set */ - while (ie_tlv_iter_next(&iter)) { - struct ie_neighbor_report_info info; - uint32_t freq; - enum scan_band band; - const uint8_t *cc = NULL; - - if (ie_tlv_iter_get_tag(&iter) != IE_TYPE_NEIGHBOR_REPORT) - continue; - - if (ie_parse_neighbor_report(&iter, &info) < 0) - continue; - - l_debug("Neighbor report received for %s: ch %i " - "(oper class %i), %s", - util_address_to_string(info.addr), - (int) info.channel_num, (int) info.oper_class, - info.md ? "MD set" : "MD not set"); - - if (device->connected_bss->cc_present) - cc = device->connected_bss->cc; - - freq = device_freq_from_neighbor_report(cc, &info, &band); - if (!freq) - continue; - - /* Skip if the band is not supported */ - if (!(band & wiphy_get_supported_bands(device->wiphy))) - continue; - - if (!memcmp(info.addr, device->connected_bss->addr, ETH_ALEN)) { - /* - * If this report is for the current AP, don't add - * it to any of the lists yet. We will need to scan - * its channel because it may still be the best ranked - * or the only visible AP. - */ - current_freq = freq; - - continue; - } - - /* Add the frequency to one of the lists */ - if (info.md && hs->mde) { - scan_freq_set_add(freq_set_md, freq); - - count_md += 1; - } else { - scan_freq_set_add(freq_set_no_md, freq); - - count_no_md += 1; - } - } - - if (!current_freq) - current_freq = device->connected_bss->frequency; - - /* - * If there are neighbor reports with the MD bit set then the bit - * is probably valid so scan only the frequencies of the neighbors - * with that bit set, which will allow us to use Fast Transition. - * Some APs, such as those based on hostapd do not set the MD bit - * even if the neighbor is within the MD. - * - * In any case we only select the frequencies here and will check - * the IEs in the scan results as the authoritative information - * on whether we can use Fast Transition, and rank BSSes based on - * that. - * - * TODO: possibly save the neighbors from outside the MD and if - * none of the ones in the MD end up working, try a non-FT - * transition to those neighbors. We should be using a - * blacklisting mechanism (for both initial connection and - * transitions) so that cound_md would not count the - * BSSes already used and when it goes down to 0 we'd - * automatically fall back to the non-FT candidates and then to - * full scan. - */ - if (count_md) { - scan_freq_set_add(freq_set_md, current_freq); - - device_roam_scan(device, freq_set_md); - } else if (count_no_md) { - scan_freq_set_add(freq_set_no_md, current_freq); - - device_roam_scan(device, freq_set_no_md); - } else - device_roam_scan(device, NULL); - - scan_freq_set_free(freq_set_md); - scan_freq_set_free(freq_set_no_md); -} - -static void device_roam_trigger_cb(struct l_timeout *timeout, void *user_data) -{ - struct device *device = user_data; - - l_debug("%d", device->index); - - l_timeout_remove(device->roam_trigger_timeout); - device->roam_trigger_timeout = NULL; - - device->preparing_roam = true; - - /* - * If current BSS supports Neighbor Reports, narrow the scan down - * to channels occupied by known neighbors in the ESS. This isn't - * 100% reliable as the neighbor lists are not required to be - * complete or current. It is likely still better than doing a - * full scan. 10.11.10.1: "A neighbor report may not be exhaustive - * either by choice, or due to the fact that there may be neighbor - * APs not known to the AP." - */ - if (device->connected_bss->cap_rm_neighbor_report && - !device->roam_no_orig_ap) - if (netdev_neighbor_report_req(device->netdev, - device_neighbor_report_cb) == 0) - return; - - /* Otherwise do a full scan for target BSS candidates */ - device_roam_scan(device, NULL); -} - -static void device_roam_timeout_rearm(struct device *device, int seconds) -{ - struct timespec now, min_timeout; - - clock_gettime(CLOCK_MONOTONIC, &now); - - min_timeout = now; - min_timeout.tv_sec += seconds; - - if (device->roam_min_time.tv_sec < min_timeout.tv_sec || - (device->roam_min_time.tv_sec == min_timeout.tv_sec && - device->roam_min_time.tv_nsec < min_timeout.tv_nsec)) - device->roam_min_time = min_timeout; - - seconds = device->roam_min_time.tv_sec - now.tv_sec + - (device->roam_min_time.tv_nsec > now.tv_nsec ? 1 : 0); - - device->roam_trigger_timeout = - l_timeout_create(seconds, device_roam_trigger_cb, device, NULL); -} - -#define WNM_REQUEST_MODE_PREFERRED_CANDIDATE_LIST (1 << 0) -#define WNM_REQUEST_MODE_TERMINATION_IMMINENT (1 << 3) -#define WNM_REQUEST_MODE_ESS_DISASSOCIATION_IMMINENT (1 << 4) - static void device_ap_roam_frame_event(struct netdev *netdev, const struct mmpdu_header *hdr, const void *body, size_t body_len, @@ -958,104 +512,8 @@ static void device_ap_roam_frame_event(struct netdev *netdev, { struct device *device = user_data; struct station *station = device->station; - uint32_t pos = 0; - uint8_t req_mode; - uint16_t dtimer; - uint8_t valid_interval; - if (device->preparing_roam || station->state == STATION_STATE_ROAMING) - return; - - if (body_len < 7) - goto format_error; - - /* - * First two bytes are checked by the frame watch (WNM category and - * WNM action). The third is the dialog token which is not relevant - * because we did not send a BSS transition query -- so skip these - * first three bytes. - */ - pos += 3; - - req_mode = l_get_u8(body + pos); - pos++; - - /* - * TODO: Disassociation timer and validity interval are currently not - * used since the BSS transition request is being handled immediately. - */ - dtimer = l_get_le16(body + pos); - pos += 2; - valid_interval = l_get_u8(body + pos); - pos++; - - l_debug("BSS transition received from AP: Disassociation Time: %u, " - "Validity interval: %u", dtimer, valid_interval); - - /* check req_mode for optional values */ - if (req_mode & WNM_REQUEST_MODE_TERMINATION_IMMINENT) { - if (pos + 12 > body_len) - goto format_error; - - pos += 12; - } - - if (req_mode & WNM_REQUEST_MODE_ESS_DISASSOCIATION_IMMINENT ) { - uint8_t url_len; - - if (pos + 1 > body_len) - goto format_error; - - url_len = l_get_u8(body + pos); - pos++; - - if (pos + url_len > body_len) - goto format_error; - - pos += url_len; - } - - device->ap_directed_roaming = true; - device->preparing_roam = true; - - l_timeout_remove(device->roam_trigger_timeout); - device->roam_trigger_timeout = NULL; - - if (req_mode & WNM_REQUEST_MODE_PREFERRED_CANDIDATE_LIST) - device_neighbor_report_cb(device->netdev, 0, body + pos, - body_len - pos, device); - else - device_roam_scan(device, NULL); - - return; - -format_error: - l_debug("bad AP roam frame formatting"); -} - -static void device_lost_beacon(struct device *device) -{ - struct station *station = device->station; - - l_debug("%d", device->index); - - if (station->state != STATION_STATE_ROAMING && - station->state != STATION_STATE_CONNECTED) - return; - - /* - * Tell the roam mechanism to not bother requesting Neighbor Reports, - * preauthenticating or performing other over-the-DS type of - * authentication to target AP, even while device->connected_bss is - * still non-NULL. The current connection is in a serious condition - * and we might wasting our time with those mechanisms. - */ - device->roam_no_orig_ap = true; - - if (device->preparing_roam || station->state == STATION_STATE_ROAMING) - return; - - device_roam_trigger_cb(NULL, device); + station_ap_directed_roam(station, hdr, body, body_len); } static void device_connect_cb(struct netdev *netdev, enum netdev_result result, @@ -1143,32 +601,17 @@ static void device_netdev_event(struct netdev *netdev, enum netdev_event event, l_debug("Associating"); break; case NETDEV_EVENT_LOST_BEACON: - device_lost_beacon(device); + station_lost_beacon(station); break; case NETDEV_EVENT_DISCONNECT_BY_AP: case NETDEV_EVENT_DISCONNECT_BY_SME: device_disconnect_event(device); break; case NETDEV_EVENT_RSSI_THRESHOLD_LOW: - if (device->signal_low) - break; - - device->signal_low = true; - - if (device->preparing_roam || - station->state == STATION_STATE_ROAMING) - break; - - /* Set a 5-second initial timeout */ - device_roam_timeout_rearm(device, 5); - + station_low_rssi(station); break; case NETDEV_EVENT_RSSI_THRESHOLD_HIGH: - l_timeout_remove(device->roam_trigger_timeout); - device->roam_trigger_timeout = NULL; - - device->signal_low = false; - + station_ok_rssi(station); break; case NETDEV_EVENT_RSSI_LEVEL_NOTIFY: if (device->signal_agent) @@ -2185,8 +1628,6 @@ static void device_free(void *user) dbus = dbus_get_bus(); l_dbus_unregister_object(dbus, device_get_path(device)); - l_timeout_remove(device->roam_trigger_timeout); - scan_ifindex_remove(device->index); netdev_frame_watch_remove(device->netdev, device->ap_roam_watch); diff --git a/src/device.h b/src/device.h index 7ac0c296..82805cd3 100644 --- a/src/device.h +++ b/src/device.h @@ -33,6 +33,8 @@ const char *device_get_path(struct device *device); bool device_is_busy(struct device *device); struct wiphy *device_get_wiphy(struct device *device); +void device_disassociated(struct device *device); +void device_transition_start(struct device *device, struct scan_bss *bss); void device_set_scan_results(struct device *device, struct l_queue *bss_list, bool add_to_autoconnect); struct network *device_network_find(struct device *device, const char *ssid, diff --git a/src/station.c b/src/station.c index 517e0f8c..be0d383b 100644 --- a/src/station.c +++ b/src/station.c @@ -26,12 +26,15 @@ #include #include +#include +#include #include #include "src/util.h" #include "src/iwd.h" #include "src/common.h" +#include "src/device.h" #include "src/watchlist.h" #include "src/scan.h" #include "src/netdev.h" @@ -543,6 +546,570 @@ bool station_remove_state_watch(struct station *station, uint32_t id) return watchlist_remove(&station->state_watches, id); } +void station_roam_state_clear(struct station *station) +{ + l_timeout_remove(station->roam_trigger_timeout); + station->roam_trigger_timeout = NULL; + station->preparing_roam = false; + station->signal_low = false; + station->roam_min_time.tv_sec = 0; + + if (station->roam_scan_id) + scan_cancel(netdev_get_ifindex(station->netdev), + station->roam_scan_id); +} + +static void station_roam_timeout_rearm(struct station *station, int seconds); + +void station_roamed(struct station *station) +{ + /* + * New signal high/low notification should occur on the next + * beacon from new AP. + */ + station->signal_low = false; + station->roam_min_time.tv_sec = 0; + station->roam_no_orig_ap = false; +} + +void station_roam_failed(struct station *station) +{ + struct device *device = netdev_get_device(station->netdev); + /* + * If we're still connected to the old BSS, only clear preparing_roam + * and reattempt in 60 seconds if signal level is still low at that + * time. Otherwise (we'd already started negotiating with the + * transition target, preparing_roam is false, state is roaming) we + * are now disconnected. + */ + + l_debug("%d", netdev_get_ifindex(station->netdev)); + + station->preparing_roam = false; + station->roam_no_orig_ap = false; + station->ap_directed_roaming = false; + + if (station->state == STATION_STATE_ROAMING) + device_disassociated(device); + else if (station->signal_low) + station_roam_timeout_rearm(station, 60); +} + +static void station_roam_scan_triggered(int err, void *user_data) +{ + struct station *station = user_data; + + if (err) { + station_roam_failed(station); + return; + } + + /* + * Do not update the Scanning property as we won't be updating the + * list of networks. + */ +} + +static bool station_roam_scan_notify(uint32_t wiphy_id, uint32_t ifindex, + int err, struct l_queue *bss_list, + void *userdata) +{ + struct station *station = userdata; + struct device *device = netdev_get_device(station->netdev); + struct network *network = station->connected_network; + struct handshake_state *hs = netdev_get_handshake(station->netdev); + struct scan_bss *bss; + struct scan_bss *best_bss = NULL; + double best_bss_rank = 0.0; + static const double RANK_FT_FACTOR = 1.3; + uint16_t mdid; + enum security orig_security, security; + bool seen = false; + + if (err) { + station_roam_failed(station); + return false; + } + + /* + * Do not call station_set_scan_results because this may have been + * a partial scan. We could at most update the current networks' BSS + * list in its station->networks entry. + */ + + orig_security = network_get_security(network); + + if (hs->mde) + ie_parse_mobility_domain_from_data(hs->mde, hs->mde[1] + 2, + &mdid, NULL, NULL); + + /* + * BSSes in the bss_list come already ranked with their initial + * association preference rank value. We only need to add preference + * for BSSes that are within the FT Mobility Domain so as to favor + * Fast Roaming, if it is supported. + */ + + while ((bss = l_queue_pop_head(bss_list))) { + double rank; + struct ie_rsn_info info; + int r; + + /* Skip the BSS we are connected to if doing an AP roam */ + if (station->ap_directed_roaming && !memcmp(bss->addr, + station->connected_bss->addr, 6)) + goto next; + + /* Skip result if it is not part of the ESS */ + + if (bss->ssid_len != hs->ssid_len || + memcmp(bss->ssid, hs->ssid, hs->ssid_len)) + goto next; + + memset(&info, 0, sizeof(info)); + r = scan_bss_get_rsn_info(bss, &info); + if (r < 0) { + if (r != -ENOENT) + goto next; + + security = security_determine(bss->capability, NULL); + } else + security = security_determine(bss->capability, &info); + + if (security != orig_security) + goto next; + + seen = true; + + if (!wiphy_can_connect(station->wiphy, bss)) + goto next; + + rank = bss->rank; + + if (hs->mde && bss->mde_present && l_get_le16(bss->mde) == mdid) + rank *= RANK_FT_FACTOR; + + if (rank > best_bss_rank) { + if (best_bss) + scan_bss_free(best_bss); + + best_bss = bss; + best_bss_rank = rank; + + continue; + } + +next: + scan_bss_free(bss); + } + + l_queue_destroy(bss_list, NULL); + + if (!seen) + goto fail_free_bss; + + /* See if we have anywhere to roam to */ + if (!best_bss || scan_bss_addr_eq(best_bss, station->connected_bss)) + goto fail_free_bss; + + bss = network_bss_find_by_addr(network, best_bss->addr); + if (bss) { + scan_bss_free(best_bss); + best_bss = bss; + } else { + network_bss_add(network, best_bss); + l_queue_push_tail(station->bss_list, best_bss); + } + + device_transition_start(device, best_bss); + + return true; + +fail_free_bss: + if (best_bss) + scan_bss_free(best_bss); + + station_roam_failed(station); + + return true; +} + +static void station_roam_scan_destroy(void *userdata) +{ + struct station *station = userdata; + + station->roam_scan_id = 0; +} + +static void station_roam_scan(struct station *station, + struct scan_freq_set *freq_set) +{ + struct scan_parameters params = { .freqs = freq_set, .flush = true }; + + if (station->connected_network) + /* Use direct probe request */ + params.ssid = network_get_ssid(station->connected_network); + + station->roam_scan_id = + scan_active_full(netdev_get_ifindex(station->netdev), ¶ms, + station_roam_scan_triggered, + station_roam_scan_notify, station, + station_roam_scan_destroy); + + if (!station->roam_scan_id) + station_roam_failed(station); +} + +static uint32_t station_freq_from_neighbor_report(const uint8_t *country, + struct ie_neighbor_report_info *info, enum scan_band *out_band) +{ + enum scan_band band; + uint32_t freq; + + if (info->oper_class == 0) { + /* + * Some Cisco APs report all operating class values as 0 + * in the Neighbor Report Responses. Work around this by + * using the most likely operating class for the channel + * number as the 2.4GHz and 5GHz bands happen to mostly + * use channels in two disjoint ranges. + */ + if (info->channel_num >= 1 && info->channel_num <= 14) + band = SCAN_BAND_2_4_GHZ; + else if (info->channel_num >= 36 && info->channel_num <= 169) + band = SCAN_BAND_5_GHZ; + else { + l_debug("Ignored: 0 oper class with an unusual " + "channel number"); + + return 0; + } + } else { + band = scan_oper_class_to_band(country, info->oper_class); + if (!band) { + l_debug("Ignored: unsupported oper class"); + + return 0; + } + } + + freq = scan_channel_to_freq(info->channel_num, band); + if (!freq) { + l_debug("Ignored: unsupported channel"); + + return 0; + } + + if (out_band) + *out_band = band; + + return freq; +} + +static void station_neighbor_report_cb(struct netdev *netdev, int err, + const uint8_t *reports, + size_t reports_len, void *user_data) +{ + struct station *station = user_data; + struct ie_tlv_iter iter; + int count_md = 0, count_no_md = 0; + struct scan_freq_set *freq_set_md, *freq_set_no_md; + uint32_t current_freq = 0; + struct handshake_state *hs = netdev_get_handshake(station->netdev); + + /* + * Check if we're still attempting to roam -- if dbus Disconnect + * had been called in the meantime we just abort the attempt. + */ + if (!station->preparing_roam || err == -ENODEV) + return; + + if (!reports || err) { + /* Have to do a full scan */ + station_roam_scan(station, NULL); + + return; + } + + freq_set_md = scan_freq_set_new(); + freq_set_no_md = scan_freq_set_new(); + + ie_tlv_iter_init(&iter, reports, reports_len); + + /* First see if any of the reports contain the MD bit set */ + while (ie_tlv_iter_next(&iter)) { + struct ie_neighbor_report_info info; + uint32_t freq; + enum scan_band band; + const uint8_t *cc = NULL; + + if (ie_tlv_iter_get_tag(&iter) != IE_TYPE_NEIGHBOR_REPORT) + continue; + + if (ie_parse_neighbor_report(&iter, &info) < 0) + continue; + + l_debug("Neighbor report received for %s: ch %i " + "(oper class %i), %s", + util_address_to_string(info.addr), + (int) info.channel_num, (int) info.oper_class, + info.md ? "MD set" : "MD not set"); + + if (station->connected_bss->cc_present) + cc = station->connected_bss->cc; + + freq = station_freq_from_neighbor_report(cc, &info, &band); + if (!freq) + continue; + + /* Skip if the band is not supported */ + if (!(band & wiphy_get_supported_bands(station->wiphy))) + continue; + + if (!memcmp(info.addr, + station->connected_bss->addr, ETH_ALEN)) { + /* + * If this report is for the current AP, don't add + * it to any of the lists yet. We will need to scan + * its channel because it may still be the best ranked + * or the only visible AP. + */ + current_freq = freq; + + continue; + } + + /* Add the frequency to one of the lists */ + if (info.md && hs->mde) { + scan_freq_set_add(freq_set_md, freq); + + count_md += 1; + } else { + scan_freq_set_add(freq_set_no_md, freq); + + count_no_md += 1; + } + } + + if (!current_freq) + current_freq = station->connected_bss->frequency; + + /* + * If there are neighbor reports with the MD bit set then the bit + * is probably valid so scan only the frequencies of the neighbors + * with that bit set, which will allow us to use Fast Transition. + * Some APs, such as those based on hostapd do not set the MD bit + * even if the neighbor is within the MD. + * + * In any case we only select the frequencies here and will check + * the IEs in the scan results as the authoritative information + * on whether we can use Fast Transition, and rank BSSes based on + * that. + * + * TODO: possibly save the neighbors from outside the MD and if + * none of the ones in the MD end up working, try a non-FT + * transition to those neighbors. We should be using a + * blacklisting mechanism (for both initial connection and + * transitions) so that cound_md would not count the + * BSSes already used and when it goes down to 0 we'd + * automatically fall back to the non-FT candidates and then to + * full scan. + */ + if (count_md) { + scan_freq_set_add(freq_set_md, current_freq); + + station_roam_scan(station, freq_set_md); + } else if (count_no_md) { + scan_freq_set_add(freq_set_no_md, current_freq); + + station_roam_scan(station, freq_set_no_md); + } else + station_roam_scan(station, NULL); + + scan_freq_set_free(freq_set_md); + scan_freq_set_free(freq_set_no_md); +} + +static void station_roam_trigger_cb(struct l_timeout *timeout, void *user_data) +{ + struct station *station = user_data; + + l_debug("%d", netdev_get_ifindex(station->netdev)); + + l_timeout_remove(station->roam_trigger_timeout); + station->roam_trigger_timeout = NULL; + station->preparing_roam = true; + + /* + * If current BSS supports Neighbor Reports, narrow the scan down + * to channels occupied by known neighbors in the ESS. This isn't + * 100% reliable as the neighbor lists are not required to be + * complete or current. It is likely still better than doing a + * full scan. 10.11.10.1: "A neighbor report may not be exhaustive + * either by choice, or due to the fact that there may be neighbor + * APs not known to the AP." + */ + if (station->connected_bss->cap_rm_neighbor_report && + !station->roam_no_orig_ap) + if (!netdev_neighbor_report_req(station->netdev, + station_neighbor_report_cb)) + return; + + /* Otherwise do a full scan for target BSS candidates */ + station_roam_scan(station, NULL); +} + +static void station_roam_timeout_rearm(struct station *station, int seconds) +{ + struct timespec now, min_timeout; + + clock_gettime(CLOCK_MONOTONIC, &now); + + min_timeout = now; + min_timeout.tv_sec += seconds; + + if (station->roam_min_time.tv_sec < min_timeout.tv_sec || + (station->roam_min_time.tv_sec == min_timeout.tv_sec && + station->roam_min_time.tv_nsec < min_timeout.tv_nsec)) + station->roam_min_time = min_timeout; + + seconds = station->roam_min_time.tv_sec - now.tv_sec + + (station->roam_min_time.tv_nsec > now.tv_nsec ? 1 : 0); + + station->roam_trigger_timeout = + l_timeout_create(seconds, station_roam_trigger_cb, + station, NULL); +} + +void station_lost_beacon(struct station *station) +{ + l_debug("%d", netdev_get_ifindex(station->netdev)); + + if (station->state != STATION_STATE_ROAMING && + station->state != STATION_STATE_CONNECTED) + return; + + /* + * Tell the roam mechanism to not bother requesting Neighbor Reports, + * preauthenticating or performing other over-the-DS type of + * authentication to target AP, even while station->connected_bss is + * still non-NULL. The current connection is in a serious condition + * and we might wasting our time with those mechanisms. + */ + station->roam_no_orig_ap = true; + + if (station->preparing_roam || station->state == STATION_STATE_ROAMING) + return; + + station_roam_trigger_cb(NULL, station); +} + +#define WNM_REQUEST_MODE_PREFERRED_CANDIDATE_LIST (1 << 0) +#define WNM_REQUEST_MODE_TERMINATION_IMMINENT (1 << 3) +#define WNM_REQUEST_MODE_ESS_DISASSOCIATION_IMMINENT (1 << 4) + +void station_ap_directed_roam(struct station *station, + const struct mmpdu_header *hdr, + const void *body, size_t body_len) +{ + uint32_t pos = 0; + uint8_t req_mode; + uint16_t dtimer; + uint8_t valid_interval; + + if (station->preparing_roam || station->state == STATION_STATE_ROAMING) + return; + + if (body_len < 7) + goto format_error; + + /* + * First two bytes are checked by the frame watch (WNM category and + * WNM action). The third is the dialog token which is not relevant + * because we did not send a BSS transition query -- so skip these + * first three bytes. + */ + pos += 3; + + req_mode = l_get_u8(body + pos); + pos++; + + /* + * TODO: Disassociation timer and validity interval are currently not + * used since the BSS transition request is being handled immediately. + */ + dtimer = l_get_le16(body + pos); + pos += 2; + valid_interval = l_get_u8(body + pos); + pos++; + + l_debug("BSS transition received from AP: Disassociation Time: %u, " + "Validity interval: %u", dtimer, valid_interval); + + /* check req_mode for optional values */ + if (req_mode & WNM_REQUEST_MODE_TERMINATION_IMMINENT) { + if (pos + 12 > body_len) + goto format_error; + + pos += 12; + } + + if (req_mode & WNM_REQUEST_MODE_ESS_DISASSOCIATION_IMMINENT ) { + uint8_t url_len; + + if (pos + 1 > body_len) + goto format_error; + + url_len = l_get_u8(body + pos); + pos++; + + if (pos + url_len > body_len) + goto format_error; + + pos += url_len; + } + + station->ap_directed_roaming = true; + station->preparing_roam = true; + + l_timeout_remove(station->roam_trigger_timeout); + station->roam_trigger_timeout = NULL; + + if (req_mode & WNM_REQUEST_MODE_PREFERRED_CANDIDATE_LIST) + station_neighbor_report_cb(station->netdev, 0, body + pos, + body_len - pos,station); + else + station_roam_scan(station, NULL); + + return; + +format_error: + l_debug("bad AP roam frame formatting"); +} + +void station_low_rssi(struct station *station) +{ + if (station->signal_low) + return; + + station->signal_low = true; + + if (station->preparing_roam || + station->state == STATION_STATE_ROAMING) + return; + + /* Set a 5-second initial timeout */ + station_roam_timeout_rearm(station, 5); +} + +void station_ok_rssi(struct station *station) +{ + l_timeout_remove(station->roam_trigger_timeout); + station->roam_trigger_timeout = NULL; + + station->signal_low = false; +} + struct station *station_find(uint32_t ifindex) { const struct l_queue_entry *entry; @@ -587,6 +1154,8 @@ void station_free(struct station *station) if (!l_queue_remove(station_list, station)) return; + l_timeout_remove(station->roam_trigger_timeout); + l_queue_destroy(station->networks_sorted, NULL); l_hashmap_destroy(station->networks, network_free); l_queue_destroy(station->bss_list, bss_free); diff --git a/src/station.h b/src/station.h index 22cb86b3..5e05d347 100644 --- a/src/station.h +++ b/src/station.h @@ -50,10 +50,19 @@ struct station { struct l_hashmap *networks; struct l_queue *networks_sorted; + /* Roaming related members */ + struct timespec roam_min_time; + struct l_timeout *roam_trigger_timeout; + uint32_t roam_scan_id; + struct wiphy *wiphy; struct netdev *netdev; bool seen_hidden_networks : 1; + bool preparing_roam : 1; + bool signal_low : 1; + bool roam_no_orig_ap : 1; + bool ap_directed_roaming : 1; }; void station_autoconnect_next(struct station *station); @@ -83,6 +92,17 @@ uint32_t station_add_state_watch(struct station *station, station_destroy_func_t destroy); bool station_remove_state_watch(struct station *station, uint32_t id); +void station_roam_state_clear(struct station *station); +void station_roam_failed(struct station *station); +void station_roamed(struct station *station); +void station_lost_beacon(struct station *station); +void station_ap_directed_roam(struct station *station, + const struct mmpdu_header *hdr, + const void *body, size_t body_len); + +void station_low_rssi(struct station *station); +void station_ok_rssi(struct station *station); + struct station *station_find(uint32_t ifindex); struct station *station_create(struct wiphy *wiphy, struct netdev *netdev); void station_free(struct station *station);