| /* |
| * |
| * Wireless daemon for Linux |
| * |
| * Copyright (C) 2013-2019 Intel Corporation. All rights reserved. |
| * |
| * This library is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU Lesser General Public |
| * License as published by the Free Software Foundation; either |
| * version 2.1 of the License, or (at your option) any later version. |
| * |
| * This library is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * Lesser General Public License for more details. |
| * |
| * You should have received a copy of the GNU Lesser General Public |
| * License along with this library; if not, write to the Free Software |
| * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
| * |
| */ |
| |
| #ifdef HAVE_CONFIG_H |
| #include <config.h> |
| #endif |
| |
| #define _GNU_SOURCE |
| #include <stdlib.h> |
| #include <alloca.h> |
| #include <stdio.h> |
| #include <linux/rtnetlink.h> |
| #include <net/if_arp.h> |
| #include <linux/if.h> |
| #include <linux/if_packet.h> |
| #include <linux/if_ether.h> |
| #include <linux/filter.h> |
| #include <arpa/inet.h> |
| #include <sys/socket.h> |
| #include <errno.h> |
| |
| #include <ell/ell.h> |
| |
| #include "ell/useful.h" |
| |
| #include "linux/nl80211.h" |
| |
| #include "src/iwd.h" |
| #include "src/module.h" |
| #include "src/wiphy.h" |
| #include "src/ie.h" |
| #include "src/mpdu.h" |
| #include "src/eapol.h" |
| #include "src/handshake.h" |
| #include "src/crypto.h" |
| #include "src/scan.h" |
| #include "src/netdev.h" |
| #include "src/ft.h" |
| #include "src/util.h" |
| #include "src/watchlist.h" |
| #include "src/sae.h" |
| #include "src/nl80211util.h" |
| #include "src/nl80211cmd.h" |
| #include "src/owe.h" |
| #include "src/fils.h" |
| #include "src/auth-proto.h" |
| #include "src/frame-xchg.h" |
| #include "src/diagnostic.h" |
| #include "src/band.h" |
| #include "src/pmksa.h" |
| |
| #ifndef ENOTSUPP |
| #define ENOTSUPP 524 |
| #endif |
| |
| enum connection_type { |
| CONNECTION_TYPE_SOFTMAC, |
| CONNECTION_TYPE_FULLMAC, |
| CONNECTION_TYPE_SAE_OFFLOAD, |
| CONNECTION_TYPE_PSK_OFFLOAD, |
| CONNECTION_TYPE_8021X_OFFLOAD, |
| }; |
| |
| static uint32_t unicast_watch; |
| |
| struct netdev_handshake_state { |
| struct handshake_state super; |
| uint32_t pairwise_new_key_cmd_id; |
| uint32_t group_new_key_cmd_id; |
| uint32_t group_management_new_key_cmd_id; |
| uint32_t set_station_cmd_id; |
| uint32_t set_pmk_cmd_id; |
| uint32_t pairwise_set_key_tx_cmd_id; |
| bool ptk_installed; |
| bool gtk_installed; |
| bool igtk_installed; |
| bool complete; |
| struct netdev *netdev; |
| enum connection_type type; |
| }; |
| |
| struct netdev_ext_key_info { |
| uint16_t proto; |
| bool noencrypt; |
| struct eapol_frame frame[]; |
| }; |
| |
| struct netdev { |
| uint32_t index; |
| uint64_t wdev_id; |
| char name[IFNAMSIZ]; |
| uint32_t type; |
| uint8_t addr[ETH_ALEN]; |
| struct wiphy *wiphy; |
| unsigned int ifi_flags; |
| uint32_t frequency; |
| |
| netdev_event_func_t event_filter; |
| netdev_connect_cb_t connect_cb; |
| netdev_disconnect_cb_t disconnect_cb; |
| netdev_neighbor_report_cb_t neighbor_report_cb; |
| netdev_command_cb_t adhoc_cb; |
| void *user_data; |
| struct eapol_sm *sm; |
| struct auth_proto *ap; |
| struct owe_sm *owe_sm; |
| struct handshake_state *handshake; |
| uint32_t connect_cmd_id; |
| uint32_t disconnect_cmd_id; |
| uint32_t join_adhoc_cmd_id; |
| uint32_t leave_adhoc_cmd_id; |
| uint32_t set_interface_cmd_id; |
| uint32_t rekey_offload_cmd_id; |
| uint32_t qos_map_cmd_id; |
| uint32_t mac_change_cmd_id; |
| uint32_t get_oci_cmd_id; |
| uint32_t get_link_cmd_id; |
| uint32_t power_save_cmd_id; |
| uint32_t set_cqm_cmd_id; |
| enum netdev_result result; |
| uint16_t last_code; /* reason or status, depending on result */ |
| struct l_timeout *neighbor_report_timeout; |
| struct l_timeout *sa_query_timeout; |
| struct l_timeout *sa_query_delay; |
| struct l_timeout *group_handshake_timeout; |
| uint16_t sa_query_id; |
| int8_t rssi_levels[16]; |
| uint8_t rssi_levels_num; |
| uint8_t cur_rssi_level_idx; |
| int8_t cur_rssi; |
| struct l_timeout *rssi_poll_timeout; |
| 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; |
| netdev_destroy_func_t set_powered_destroy; |
| |
| uint32_t get_station_cmd_id; |
| netdev_get_station_cb_t get_station_cb; |
| void *get_station_data; |
| netdev_destroy_func_t get_station_destroy; |
| |
| struct l_idle *disconnect_idle; |
| |
| struct watchlist station_watches; |
| |
| struct l_io *pae_io; /* for drivers without EAPoL over NL80211 */ |
| |
| struct l_genl_msg *connect_cmd; |
| struct l_genl_msg *auth_cmd; |
| struct wiphy_radio_work_item work; |
| |
| struct netdev_ext_key_info *ext_key_info; |
| |
| int low_signal_threshold; |
| int low_signal_threshold_5ghz; |
| |
| bool connected : 1; |
| bool associated : 1; |
| bool operational : 1; |
| bool rekey_offload_support : 1; |
| bool pae_over_nl80211 : 1; |
| bool in_ft : 1; |
| bool cur_rssi_low : 1; |
| bool use_4addr : 1; |
| bool ignore_connect_event : 1; |
| bool expect_connect_failure : 1; |
| bool aborting : 1; |
| bool events_ready : 1; |
| bool retry_auth : 1; |
| bool in_reassoc : 1; |
| bool privacy : 1; |
| bool cqm_poll_fallback : 1; |
| bool external_auth : 1; |
| }; |
| |
| struct netdev_preauth_state { |
| netdev_preauthenticate_cb_t cb; |
| void *user_data; |
| struct netdev *netdev; |
| }; |
| |
| struct netdev_watch { |
| uint32_t id; |
| netdev_watch_func_t callback; |
| void *user_data; |
| }; |
| |
| static struct l_netlink *rtnl = NULL; |
| static struct l_genl_family *nl80211; |
| static struct l_queue *netdev_list; |
| static struct watchlist netdev_watches; |
| static bool mac_per_ssid; |
| /* Threshold RSSI for roaming to trigger, configurable in main.conf */ |
| static int LOW_SIGNAL_THRESHOLD; |
| static int LOW_SIGNAL_THRESHOLD_5GHZ; |
| static int CRITICAL_SIGNAL_THRESHOLD; |
| static int CRITICAL_SIGNAL_THRESHOLD_5GHZ; |
| |
| static unsigned int iov_ie_append(struct iovec *iov, |
| unsigned int n_iov, unsigned int c, |
| const uint8_t *ie, size_t len) |
| { |
| if (L_WARN_ON(c >= n_iov)) |
| return n_iov; |
| |
| if (!ie) |
| return c; |
| |
| iov[c].iov_base = (void *) ie; |
| iov[c].iov_len = len; |
| |
| return c + 1u; |
| } |
| |
| const char *netdev_iftype_to_string(uint32_t iftype) |
| { |
| switch (iftype) { |
| case NL80211_IFTYPE_ADHOC: |
| return "ad-hoc"; |
| case NL80211_IFTYPE_STATION: |
| return "station"; |
| case NL80211_IFTYPE_AP: |
| return "ap"; |
| case NL80211_IFTYPE_P2P_CLIENT: |
| return "p2p-client"; |
| case NL80211_IFTYPE_P2P_GO: |
| return "p2p-go"; |
| case NL80211_IFTYPE_P2P_DEVICE: |
| return "p2p-device"; |
| default: |
| break; |
| } |
| |
| return NULL; |
| } |
| |
| static inline bool is_offload(struct handshake_state *hs) |
| { |
| struct netdev_handshake_state *nhs = |
| l_container_of(hs, struct netdev_handshake_state, super); |
| |
| if (!nhs) |
| return false; |
| |
| switch (nhs->type) { |
| case CONNECTION_TYPE_SOFTMAC: |
| case CONNECTION_TYPE_FULLMAC: |
| /* |
| * 8021x offload does not quite fit into the same category of PSK |
| * offload. First the netdev_connect_event comes prior to EAP meaning |
| * the handshake is not done at this point. In addition it still |
| * requires EAP take place in userspace meaning IWD needs an eapol_sm. |
| * Because of this, and our prior use of 'is_offload', it does not fit |
| * into the same category and will need to be handled specially. |
| */ |
| case CONNECTION_TYPE_8021X_OFFLOAD: |
| return false; |
| case CONNECTION_TYPE_SAE_OFFLOAD: |
| case CONNECTION_TYPE_PSK_OFFLOAD: |
| return true; |
| } |
| |
| return false; |
| } |
| |
| static unsigned int netdev_populate_common_ies(struct netdev *netdev, |
| struct handshake_state *hs, |
| struct l_genl_msg *msg, |
| struct iovec *iov, |
| unsigned int n_iov, |
| unsigned int c_iov) |
| { |
| const uint8_t *extended_capabilities; |
| const uint8_t *rm_enabled_capabilities; |
| |
| extended_capabilities = wiphy_get_extended_capabilities(netdev->wiphy, |
| netdev->type); |
| c_iov = iov_ie_append(iov, n_iov, c_iov, extended_capabilities, |
| IE_LEN(extended_capabilities)); |
| |
| rm_enabled_capabilities = |
| wiphy_get_rm_enabled_capabilities(netdev->wiphy); |
| c_iov = iov_ie_append(iov, n_iov, c_iov, rm_enabled_capabilities, |
| IE_LEN(rm_enabled_capabilities)); |
| |
| if (rm_enabled_capabilities) |
| l_genl_msg_append_attr(msg, NL80211_ATTR_USE_RRM, 0, NULL); |
| |
| c_iov = iov_ie_append(iov, n_iov, c_iov, |
| hs->vendor_ies, hs->vendor_ies_len); |
| |
| c_iov = iov_ie_append(iov, n_iov, c_iov, hs->fils_ip_req_ie, |
| IE_LEN(hs->fils_ip_req_ie)); |
| |
| return c_iov; |
| } |
| |
| /* Cancels ongoing GTK/IGTK related commands (if any) */ |
| static void netdev_handshake_state_cancel_rekey( |
| struct netdev_handshake_state *nhs) |
| { |
| if (nhs->group_new_key_cmd_id) { |
| l_genl_family_cancel(nl80211, nhs->group_new_key_cmd_id); |
| nhs->group_new_key_cmd_id = 0; |
| } |
| |
| if (nhs->group_management_new_key_cmd_id) { |
| l_genl_family_cancel(nl80211, |
| nhs->group_management_new_key_cmd_id); |
| nhs->group_management_new_key_cmd_id = 0; |
| } |
| } |
| |
| static void netdev_handshake_state_cancel_all( |
| struct netdev_handshake_state *nhs) |
| { |
| if (nhs->pairwise_new_key_cmd_id) { |
| l_genl_family_cancel(nl80211, nhs->pairwise_new_key_cmd_id); |
| nhs->pairwise_new_key_cmd_id = 0; |
| } |
| |
| netdev_handshake_state_cancel_rekey(nhs); |
| |
| if (nhs->set_station_cmd_id) { |
| l_genl_family_cancel(nl80211, nhs->set_station_cmd_id); |
| nhs->set_station_cmd_id = 0; |
| } |
| |
| if (nhs->set_pmk_cmd_id) { |
| l_genl_family_cancel(nl80211, nhs->set_pmk_cmd_id); |
| nhs->set_pmk_cmd_id = 0; |
| } |
| |
| if (nhs->pairwise_set_key_tx_cmd_id) { |
| l_genl_family_cancel(nl80211, nhs->pairwise_set_key_tx_cmd_id); |
| nhs->pairwise_set_key_tx_cmd_id = 0; |
| } |
| } |
| |
| static void netdev_handshake_state_free(struct handshake_state *hs) |
| { |
| struct netdev_handshake_state *nhs = |
| l_container_of(hs, struct netdev_handshake_state, super); |
| |
| netdev_handshake_state_cancel_all(nhs); |
| l_free(nhs); |
| } |
| |
| struct handshake_state *netdev_handshake_state_new(struct netdev *netdev) |
| { |
| struct netdev_handshake_state *nhs; |
| |
| nhs = l_new(struct netdev_handshake_state, 1); |
| |
| nhs->super.ifindex = netdev->index; |
| nhs->super.free = netdev_handshake_state_free; |
| nhs->super.refcount = 1; |
| |
| nhs->netdev = netdev; |
| /* |
| * Since GTK/IGTK are optional (NO_GROUP_TRAFFIC), we set them as |
| * 'installed' upon initialization. If/When the gtk/igtk callback is |
| * called they will get set to false until we have received a successful |
| * callback from nl80211. From these callbacks we can check that all |
| * the keys have been installed, and only then trigger the handshake |
| * complete callback. |
| */ |
| nhs->gtk_installed = true; |
| nhs->igtk_installed = true; |
| |
| return &nhs->super; |
| } |
| |
| struct wiphy *netdev_get_wiphy(struct netdev *netdev) |
| { |
| return netdev->wiphy; |
| } |
| |
| const uint8_t *netdev_get_address(struct netdev *netdev) |
| { |
| return netdev->addr; |
| } |
| |
| uint32_t netdev_get_ifindex(struct netdev *netdev) |
| { |
| return netdev->index; |
| } |
| |
| uint64_t netdev_get_wdev_id(struct netdev *netdev) |
| { |
| return netdev->wdev_id; |
| } |
| |
| enum netdev_iftype netdev_get_iftype(struct netdev *netdev) |
| { |
| return netdev->type; |
| } |
| |
| const char *netdev_get_name(struct netdev *netdev) |
| { |
| return netdev->name; |
| } |
| |
| bool netdev_get_is_up(struct netdev *netdev) |
| { |
| bool powered = (netdev->ifi_flags & IFF_UP) != 0; |
| |
| /* |
| * If we are in the middle of changing the MAC we are in somewhat of a |
| * no mans land. Technically the iface may be down, but since we are |
| * not emitting any netdev DOWN events we want netdev_get_is_up to |
| * reflect the same state. Once MAC changing finishes any pending |
| * DOWN events will be emitted. |
| */ |
| if (netdev->mac_change_cmd_id && !powered) |
| return true; |
| |
| return powered; |
| } |
| |
| struct handshake_state *netdev_get_handshake(struct netdev *netdev) |
| { |
| return netdev->handshake; |
| } |
| |
| const char *netdev_get_path(struct netdev *netdev) |
| { |
| static char path[256]; |
| |
| L_WARN_ON(snprintf(path, sizeof(path), "%s/%u", |
| wiphy_get_path(netdev->wiphy), |
| netdev->index) >= (int) sizeof(path)); |
| path[sizeof(path) - 1] = '\0'; |
| |
| return path; |
| } |
| |
| uint8_t netdev_get_rssi_level_idx(struct netdev *netdev) |
| { |
| return netdev->cur_rssi_level_idx; |
| } |
| |
| int netdev_get_low_signal_threshold(uint32_t frequency) |
| { |
| if (frequency > 4000) |
| return LOW_SIGNAL_THRESHOLD_5GHZ; |
| |
| return LOW_SIGNAL_THRESHOLD; |
| } |
| |
| static void netdev_set_powered_result(int error, uint16_t type, |
| const void *data, |
| uint32_t len, void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| |
| if (netdev->set_powered_cb) |
| netdev->set_powered_cb(netdev, error, |
| netdev->set_powered_user_data); |
| |
| netdev->set_powered_cb = NULL; |
| } |
| |
| static void netdev_set_powered_destroy(void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| |
| netdev->set_powered_cmd_id = 0; |
| |
| if (netdev->set_powered_destroy) |
| netdev->set_powered_destroy(netdev->set_powered_user_data); |
| |
| netdev->set_powered_destroy = NULL; |
| netdev->set_powered_user_data = NULL; |
| } |
| |
| int netdev_set_powered(struct netdev *netdev, bool powered, |
| netdev_command_cb_t callback, void *user_data, |
| netdev_destroy_func_t destroy) |
| { |
| if (netdev->set_powered_cmd_id || |
| netdev->set_interface_cmd_id) |
| return -EBUSY; |
| |
| netdev->set_powered_cmd_id = |
| l_rtnl_set_powered(rtnl, netdev->index, powered, |
| netdev_set_powered_result, netdev, |
| netdev_set_powered_destroy); |
| if (!netdev->set_powered_cmd_id) |
| return -EIO; |
| |
| netdev->set_powered_cb = callback; |
| netdev->set_powered_user_data = user_data; |
| netdev->set_powered_destroy = destroy; |
| |
| return 0; |
| } |
| |
| static bool netdev_parse_bitrate(struct l_genl_attr *attr, |
| enum diagnostic_mcs_type *type_out, |
| uint32_t *rate_out, |
| uint8_t *mcs_out) |
| { |
| uint16_t type, len; |
| const void *data; |
| uint32_t rate = 0; |
| uint8_t mcs = 0; |
| enum diagnostic_mcs_type mcs_type = DIAGNOSTIC_MCS_TYPE_NONE; |
| |
| while (l_genl_attr_next(attr, &type, &len, &data)) { |
| switch (type) { |
| case NL80211_RATE_INFO_BITRATE32: |
| if (len != 4) |
| return false; |
| |
| rate = l_get_u32(data); |
| |
| break; |
| |
| case NL80211_RATE_INFO_MCS: |
| if (len != 1) |
| return false; |
| |
| mcs = l_get_u8(data); |
| mcs_type = DIAGNOSTIC_MCS_TYPE_HT; |
| |
| break; |
| |
| case NL80211_RATE_INFO_VHT_MCS: |
| if (len != 1) |
| return false; |
| |
| mcs = l_get_u8(data); |
| mcs_type = DIAGNOSTIC_MCS_TYPE_VHT; |
| |
| break; |
| |
| case NL80211_RATE_INFO_HE_MCS: |
| if (len != 1) |
| return false; |
| |
| mcs = l_get_u8(data); |
| mcs_type = DIAGNOSTIC_MCS_TYPE_HE; |
| |
| break; |
| } |
| } |
| |
| if (!rate) |
| return false; |
| |
| *type_out = mcs_type; |
| *rate_out = rate; |
| |
| if (mcs_type != DIAGNOSTIC_MCS_TYPE_NONE) |
| *mcs_out = mcs; |
| |
| return true; |
| } |
| |
| static bool netdev_parse_sta_info(struct l_genl_attr *attr, |
| struct diagnostic_station_info *info) |
| { |
| uint16_t type, len; |
| const void *data; |
| struct l_genl_attr nested; |
| |
| while (l_genl_attr_next(attr, &type, &len, &data)) { |
| switch (type) { |
| case NL80211_STA_INFO_SIGNAL: |
| if (len != 1) |
| return false; |
| |
| info->cur_rssi = *(const int8_t *) data; |
| info->have_cur_rssi = true; |
| |
| break; |
| case NL80211_STA_INFO_SIGNAL_AVG: |
| if (len != 1) |
| return false; |
| |
| info->avg_rssi = *(const int8_t *) data; |
| info->have_avg_rssi = true; |
| |
| break; |
| case NL80211_STA_INFO_RX_BITRATE: |
| if (!l_genl_attr_recurse(attr, &nested)) |
| return false; |
| |
| if (!netdev_parse_bitrate(&nested, &info->rx_mcs_type, |
| &info->rx_bitrate, |
| &info->rx_mcs)) |
| continue; |
| |
| info->have_rx_bitrate = true; |
| |
| if (info->rx_mcs_type != DIAGNOSTIC_MCS_TYPE_NONE) |
| info->have_rx_mcs = true; |
| |
| break; |
| |
| case NL80211_STA_INFO_TX_BITRATE: |
| if (!l_genl_attr_recurse(attr, &nested)) |
| return false; |
| |
| if (!netdev_parse_bitrate(&nested, &info->tx_mcs_type, |
| &info->tx_bitrate, |
| &info->tx_mcs)) |
| continue; |
| |
| info->have_tx_bitrate = true; |
| |
| if (info->tx_mcs_type != DIAGNOSTIC_MCS_TYPE_NONE) |
| info->have_tx_mcs = true; |
| |
| break; |
| case NL80211_STA_INFO_EXPECTED_THROUGHPUT: |
| if (len != 4) |
| return false; |
| |
| info->expected_throughput = l_get_u32(data); |
| info->have_expected_throughput = true; |
| |
| break; |
| case NL80211_STA_INFO_INACTIVE_TIME: |
| if (len != 4) |
| return false; |
| |
| info->inactive_time = l_get_u32(data); |
| info->have_inactive_time = true; |
| |
| break; |
| case NL80211_STA_INFO_CONNECTED_TIME: |
| if (len != 4) |
| return false; |
| |
| info->connected_time = l_get_u32(data); |
| info->have_connected_time = true; |
| |
| break; |
| } |
| } |
| |
| return true; |
| } |
| |
| static void netdev_set_rssi_level_idx(struct netdev *netdev) |
| { |
| uint8_t new_level; |
| |
| for (new_level = 0; new_level < netdev->rssi_levels_num; new_level++) |
| if (netdev->cur_rssi >= netdev->rssi_levels[new_level]) |
| break; |
| |
| netdev->cur_rssi_level_idx = new_level; |
| } |
| |
| static void netdev_cqm_event_rssi_value(struct netdev *netdev, int rssi_val) |
| { |
| bool new_rssi_low; |
| uint8_t prev_rssi_level_idx = netdev->cur_rssi_level_idx; |
| int threshold = netdev->frequency > 4000 ? |
| netdev->low_signal_threshold_5ghz : |
| netdev->low_signal_threshold; |
| |
| if (!netdev->connected) |
| return; |
| |
| if (rssi_val > 127) |
| rssi_val = 127; |
| else if (rssi_val < -127) |
| rssi_val = -127; |
| |
| netdev->cur_rssi = rssi_val; |
| |
| if (!netdev->event_filter) |
| return; |
| |
| new_rssi_low = rssi_val < threshold; |
| if (netdev->cur_rssi_low != new_rssi_low) { |
| int event = new_rssi_low ? |
| NETDEV_EVENT_RSSI_THRESHOLD_LOW : |
| NETDEV_EVENT_RSSI_THRESHOLD_HIGH; |
| |
| netdev->cur_rssi_low = new_rssi_low; |
| netdev->event_filter(netdev, event, NULL, netdev->user_data); |
| } |
| |
| if (!netdev->rssi_levels_num) |
| return; |
| |
| netdev_set_rssi_level_idx(netdev); |
| if (netdev->cur_rssi_level_idx != prev_rssi_level_idx) |
| netdev->event_filter(netdev, NETDEV_EVENT_RSSI_LEVEL_NOTIFY, |
| &netdev->cur_rssi_level_idx, |
| netdev->user_data); |
| } |
| |
| static void netdev_rssi_poll_cb(struct l_genl_msg *msg, void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| struct l_genl_attr attr, nested; |
| uint16_t type, len; |
| const void *data; |
| bool found; |
| struct diagnostic_station_info info; |
| uint8_t prev_rssi_level_idx = netdev->cur_rssi_level_idx; |
| |
| netdev->rssi_poll_cmd_id = 0; |
| |
| if (!l_genl_attr_init(&attr, msg)) |
| goto done; |
| |
| found = false; |
| while (l_genl_attr_next(&attr, &type, &len, &data)) { |
| if (type != NL80211_ATTR_STA_INFO) |
| continue; |
| |
| if (!l_genl_attr_recurse(&attr, &nested)) |
| goto done; |
| |
| if (!netdev_parse_sta_info(&nested, &info)) |
| goto done; |
| |
| found = true; |
| break; |
| } |
| |
| if (!found || !info.have_cur_rssi) |
| goto done; |
| |
| netdev->cur_rssi = info.cur_rssi; |
| |
| /* |
| * If the CMD_SET_CQM call failed RSSI polling was started. In this case |
| * we should behave just like its a CQM event and check both the RSSI |
| * level indexes and the HIGH/LOW thresholds. |
| */ |
| if (netdev->cqm_poll_fallback) { |
| netdev_cqm_event_rssi_value(netdev, info.cur_rssi); |
| goto done; |
| } |
| |
| /* Otherwise just update the level notifications, CQM events work */ |
| netdev_set_rssi_level_idx(netdev); |
| if (netdev->cur_rssi_level_idx != prev_rssi_level_idx) |
| netdev->event_filter(netdev, NETDEV_EVENT_RSSI_LEVEL_NOTIFY, |
| &netdev->cur_rssi_level_idx, |
| netdev->user_data); |
| |
| done: |
| /* Rearm timer */ |
| l_timeout_modify(netdev->rssi_poll_timeout, 6); |
| } |
| |
| static void netdev_rssi_poll(struct l_timeout *timeout, void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| struct l_genl_msg *msg; |
| |
| msg = l_genl_msg_new_sized(NL80211_CMD_GET_STATION, 64); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_IFINDEX, 4, &netdev->index); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_MAC, ETH_ALEN, |
| netdev->handshake->aa); |
| |
| netdev->rssi_poll_cmd_id = l_genl_family_send(nl80211, msg, |
| netdev_rssi_poll_cb, |
| netdev, NULL); |
| } |
| |
| /* To be called whenever operational or rssi_levels_num are updated */ |
| static void netdev_rssi_polling_update(struct netdev *netdev) |
| { |
| if (!netdev->cqm_poll_fallback && wiphy_has_ext_feature(netdev->wiphy, |
| NL80211_EXT_FEATURE_CQM_RSSI_LIST)) |
| return; |
| |
| if (netdev->operational && (netdev->rssi_levels_num > 0 || |
| netdev->cqm_poll_fallback)) { |
| if (netdev->rssi_poll_timeout) |
| return; |
| |
| netdev->rssi_poll_timeout = |
| l_timeout_create(1, netdev_rssi_poll, netdev, NULL); |
| } else { |
| if (!netdev->rssi_poll_timeout) |
| return; |
| |
| l_timeout_remove(netdev->rssi_poll_timeout); |
| netdev->rssi_poll_timeout = NULL; |
| |
| if (netdev->rssi_poll_cmd_id) { |
| l_genl_family_cancel(nl80211, netdev->rssi_poll_cmd_id); |
| netdev->rssi_poll_cmd_id = 0; |
| } |
| } |
| } |
| |
| static void netdev_preauth_destroy(void *data) |
| { |
| struct netdev_preauth_state *state = data; |
| |
| if (state->cb) |
| state->cb(state->netdev, NETDEV_RESULT_ABORTED, NULL, |
| state->user_data); |
| |
| l_free(state); |
| } |
| |
| static void netdev_connect_free(struct netdev *netdev) |
| { |
| if (netdev->work.id) |
| wiphy_radio_work_done(netdev->wiphy, netdev->work.id); |
| |
| if (netdev->sm) { |
| eapol_sm_free(netdev->sm); |
| netdev->sm = NULL; |
| } |
| |
| if (netdev->ap) { |
| auth_proto_free(netdev->ap); |
| netdev->ap = NULL; |
| } |
| |
| if (netdev->owe_sm) { |
| owe_sm_free(netdev->owe_sm); |
| netdev->owe_sm = NULL; |
| } |
| |
| eapol_preauth_cancel(netdev->index); |
| |
| if (netdev->handshake) { |
| handshake_state_unref(netdev->handshake); |
| netdev->handshake = NULL; |
| } |
| |
| if (netdev->ext_key_info) { |
| l_free(netdev->ext_key_info); |
| netdev->ext_key_info = NULL; |
| } |
| |
| if (netdev->neighbor_report_cb) { |
| netdev->neighbor_report_cb(netdev, -ENOTCONN, NULL, 0, |
| netdev->user_data); |
| netdev->neighbor_report_cb = NULL; |
| l_timeout_remove(netdev->neighbor_report_timeout); |
| } |
| |
| if (netdev->sa_query_timeout) { |
| l_timeout_remove(netdev->sa_query_timeout); |
| netdev->sa_query_timeout = NULL; |
| } |
| |
| if (netdev->sa_query_delay) { |
| l_timeout_remove(netdev->sa_query_delay); |
| netdev->sa_query_delay = NULL; |
| } |
| |
| if (netdev->group_handshake_timeout) { |
| l_timeout_remove(netdev->group_handshake_timeout); |
| netdev->group_handshake_timeout = NULL; |
| } |
| |
| netdev->associated = false; |
| netdev->operational = false; |
| netdev->connected = false; |
| netdev->connect_cb = NULL; |
| netdev->event_filter = NULL; |
| netdev->user_data = NULL; |
| netdev->result = NETDEV_RESULT_OK; |
| netdev->last_code = 0; |
| netdev->in_ft = false; |
| netdev->in_reassoc = false; |
| netdev->ignore_connect_event = false; |
| netdev->expect_connect_failure = false; |
| netdev->cur_rssi_low = false; |
| netdev->privacy = false; |
| netdev->external_auth = false; |
| |
| if (netdev->connect_cmd) { |
| l_genl_msg_unref(netdev->connect_cmd); |
| netdev->connect_cmd = NULL; |
| } |
| |
| netdev_rssi_polling_update(netdev); |
| |
| if (netdev->connect_cmd_id) { |
| l_genl_family_cancel(nl80211, netdev->connect_cmd_id); |
| netdev->connect_cmd_id = 0; |
| } else if (netdev->disconnect_cmd_id) { |
| l_genl_family_cancel(nl80211, netdev->disconnect_cmd_id); |
| netdev->disconnect_cmd_id = 0; |
| } |
| |
| if (netdev->get_oci_cmd_id) { |
| l_genl_family_cancel(nl80211, netdev->get_oci_cmd_id); |
| netdev->get_oci_cmd_id = 0; |
| } |
| |
| /* Reset thresholds back to default */ |
| netdev->low_signal_threshold = LOW_SIGNAL_THRESHOLD; |
| netdev->low_signal_threshold_5ghz = LOW_SIGNAL_THRESHOLD_5GHZ; |
| } |
| |
| static void netdev_connect_failed(struct netdev *netdev, |
| enum netdev_result result, |
| uint16_t status_or_reason) |
| { |
| netdev_connect_cb_t connect_cb = netdev->connect_cb; |
| void *connect_data = netdev->user_data; |
| |
| /* Done this way to allow re-entrant netdev_connect calls */ |
| netdev_connect_free(netdev); |
| |
| if (connect_cb) |
| connect_cb(netdev, result, &status_or_reason, connect_data); |
| } |
| |
| static void netdev_connect_failed_cb(struct l_genl_msg *msg, void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| |
| netdev->disconnect_cmd_id = 0; |
| netdev_connect_failed(netdev, netdev->result, netdev->last_code); |
| } |
| |
| static void netdev_send_and_fail_connection(struct netdev *netdev, |
| enum netdev_result result, |
| uint16_t status_code, |
| struct l_genl_msg *msg) |
| { |
| netdev->result = result; |
| netdev->last_code = status_code; |
| |
| netdev->disconnect_cmd_id = |
| l_genl_family_send(nl80211, msg, netdev_connect_failed_cb, |
| netdev, NULL); |
| } |
| |
| static void netdev_disconnect_and_fail_connection(struct netdev *netdev, |
| enum netdev_result result, |
| uint16_t status_code) |
| { |
| struct l_genl_msg *msg = nl80211_build_disconnect(netdev->index, |
| MMPDU_REASON_CODE_UNSPECIFIED); |
| |
| netdev_send_and_fail_connection(netdev, result, status_code, msg); |
| } |
| |
| static void netdev_deauth_and_fail_connection(struct netdev *netdev, |
| enum netdev_result result, |
| uint16_t status_code) |
| { |
| struct l_genl_msg *msg = nl80211_build_deauthenticate(netdev->index, |
| netdev->handshake->aa, |
| MMPDU_REASON_CODE_UNSPECIFIED); |
| |
| netdev_send_and_fail_connection(netdev, result, status_code, msg); |
| } |
| |
| /* |
| * If we have a connection callback pending, either through netdev_connect |
| * or netdev_reassociate, then invoke that callback with the @result and |
| * @status_or_reason. Otherwise, invoke the event callback with the @event |
| * and @status_or_reason. |
| * |
| * This is useful for situations where handshaking or setting keys somehow |
| * fails (perhaps due to rekeying), or if the device is removed / brought |
| * down when keys are being set as a result of a rekey |
| */ |
| static void netdev_disconnected(struct netdev *netdev, |
| enum netdev_result result, |
| enum netdev_event event, |
| uint16_t status_or_reason) |
| { |
| netdev_event_func_t event_filter = netdev->event_filter; |
| void *event_data = netdev->user_data; |
| |
| if (netdev->connect_cb) { |
| netdev_connect_failed(netdev, result, status_or_reason); |
| return; |
| } |
| |
| netdev_connect_free(netdev); |
| |
| if (event_filter) |
| event_filter(netdev, event, &status_or_reason, event_data); |
| } |
| |
| static void netdev_disconnect_by_sme_cb(struct l_genl_msg *msg, void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| |
| netdev->disconnect_cmd_id = 0; |
| netdev_disconnected(netdev, netdev->result, |
| NETDEV_EVENT_DISCONNECT_BY_SME, netdev->last_code); |
| } |
| |
| static void netdev_disconnect_by_sme(struct netdev *netdev, |
| enum netdev_result result, |
| uint16_t reason_code) |
| { |
| struct l_genl_msg *msg = nl80211_build_disconnect(netdev->index, |
| reason_code); |
| |
| netdev->result = result; |
| netdev->last_code = reason_code; |
| |
| netdev->disconnect_cmd_id = l_genl_family_send(nl80211, msg, |
| netdev_disconnect_by_sme_cb, |
| netdev, NULL); |
| } |
| |
| static void netdev_free(void *data) |
| { |
| struct netdev *netdev = data; |
| |
| l_debug("Freeing netdev %s[%d]", netdev->name, netdev->index); |
| |
| netdev->ifi_flags &= ~IFF_UP; |
| |
| if (netdev->events_ready) |
| WATCHLIST_NOTIFY(&netdev_watches, netdev_watch_func_t, |
| netdev, NETDEV_WATCH_EVENT_DEL); |
| |
| if (netdev->neighbor_report_cb) { |
| netdev->neighbor_report_cb(netdev, -ENODEV, NULL, 0, |
| netdev->user_data); |
| netdev->neighbor_report_cb = NULL; |
| l_timeout_remove(netdev->neighbor_report_timeout); |
| } |
| |
| if (netdev->connected || netdev->connect_cmd_id || netdev->work.id) |
| netdev_connect_free(netdev); |
| |
| if (netdev->disconnect_cmd_id) { |
| l_genl_family_cancel(nl80211, netdev->disconnect_cmd_id); |
| netdev->disconnect_cmd_id = 0; |
| |
| if (netdev->disconnect_cb) |
| netdev->disconnect_cb(netdev, true, netdev->user_data); |
| |
| netdev->disconnect_cb = NULL; |
| netdev->user_data = NULL; |
| } |
| |
| if (netdev->disconnect_idle) { |
| l_idle_remove(netdev->disconnect_idle); |
| netdev->disconnect_idle = NULL; |
| } |
| |
| if (netdev->join_adhoc_cmd_id) { |
| l_genl_family_cancel(nl80211, netdev->join_adhoc_cmd_id); |
| netdev->join_adhoc_cmd_id = 0; |
| } |
| |
| if (netdev->leave_adhoc_cmd_id) { |
| l_genl_family_cancel(nl80211, netdev->leave_adhoc_cmd_id); |
| netdev->leave_adhoc_cmd_id = 0; |
| } |
| |
| if (netdev->set_powered_cmd_id) { |
| l_netlink_cancel(rtnl, netdev->set_powered_cmd_id); |
| netdev->set_powered_cmd_id = 0; |
| } |
| |
| if (netdev->rekey_offload_cmd_id) { |
| l_genl_family_cancel(nl80211, netdev->rekey_offload_cmd_id); |
| netdev->rekey_offload_cmd_id = 0; |
| } |
| |
| if (netdev->qos_map_cmd_id) { |
| l_genl_family_cancel(nl80211, netdev->qos_map_cmd_id); |
| netdev->qos_map_cmd_id = 0; |
| } |
| |
| if (netdev->mac_change_cmd_id) { |
| l_netlink_cancel(rtnl, netdev->mac_change_cmd_id); |
| netdev->mac_change_cmd_id = 0; |
| } |
| |
| if (netdev->get_station_cmd_id) { |
| l_genl_family_cancel(nl80211, netdev->get_station_cmd_id); |
| netdev->get_station_cmd_id = 0; |
| } |
| |
| if (netdev->set_cqm_cmd_id) { |
| l_genl_family_cancel(nl80211, netdev->set_cqm_cmd_id); |
| netdev->set_cqm_cmd_id = 0; |
| } |
| |
| if (netdev->fw_roam_bss) |
| scan_bss_free(netdev->fw_roam_bss); |
| |
| if (netdev->get_link_cmd_id) { |
| l_netlink_cancel(rtnl, netdev->get_link_cmd_id); |
| netdev->get_link_cmd_id = 0; |
| } |
| |
| if (netdev->rssi_poll_timeout) |
| l_timeout_remove(netdev->rssi_poll_timeout); |
| |
| scan_wdev_remove(netdev->wdev_id); |
| frame_watch_wdev_remove(netdev->wdev_id); |
| |
| watchlist_destroy(&netdev->station_watches); |
| |
| l_io_destroy(netdev->pae_io); |
| |
| l_free(netdev); |
| } |
| |
| static void netdev_shutdown_one(void *data, void *user_data) |
| { |
| struct netdev *netdev = data; |
| |
| if (netdev_get_is_up(netdev)) |
| l_rtnl_set_powered(rtnl, netdev->index, false, |
| NULL, NULL, NULL); |
| } |
| |
| static bool netdev_match(const void *a, const void *b) |
| { |
| const struct netdev *netdev = a; |
| uint32_t ifindex = L_PTR_TO_UINT(b); |
| |
| return (netdev->index == ifindex); |
| } |
| |
| struct netdev *netdev_find(int ifindex) |
| { |
| return l_queue_find(netdev_list, netdev_match, L_UINT_TO_PTR(ifindex)); |
| } |
| |
| static void netdev_cqm_event_rssi_threshold(struct netdev *netdev, |
| uint32_t rssi_event) |
| { |
| int event; |
| |
| if (!netdev->operational) |
| return; |
| |
| if (rssi_event != NL80211_CQM_RSSI_THRESHOLD_EVENT_LOW && |
| rssi_event != NL80211_CQM_RSSI_THRESHOLD_EVENT_HIGH) |
| return; |
| |
| if (!netdev->event_filter) |
| return; |
| |
| netdev->cur_rssi_low = |
| (rssi_event == NL80211_CQM_RSSI_THRESHOLD_EVENT_LOW); |
| event = netdev->cur_rssi_low ? NETDEV_EVENT_RSSI_THRESHOLD_LOW : |
| NETDEV_EVENT_RSSI_THRESHOLD_HIGH; |
| |
| netdev->event_filter(netdev, event, NULL, netdev->user_data); |
| } |
| |
| static void netdev_cqm_event(struct l_genl_msg *msg, struct netdev *netdev) |
| { |
| struct l_genl_attr attr; |
| struct l_genl_attr nested; |
| uint16_t type, len; |
| const void *data; |
| uint32_t *rssi_event = NULL; |
| int32_t *rssi_val = NULL; |
| uint32_t *pkt_event = NULL; |
| bool beacon_loss = false; |
| |
| if (!l_genl_attr_init(&attr, msg)) |
| return; |
| |
| while (l_genl_attr_next(&attr, &type, &len, &data)) { |
| switch (type) { |
| case NL80211_ATTR_CQM: |
| if (!l_genl_attr_recurse(&attr, &nested)) |
| return; |
| |
| while (l_genl_attr_next(&nested, &type, &len, &data)) { |
| switch (type) { |
| case NL80211_ATTR_CQM_RSSI_THRESHOLD_EVENT: |
| if (len != 4) |
| continue; |
| |
| rssi_event = (uint32_t *) data; |
| break; |
| |
| case NL80211_ATTR_CQM_PKT_LOSS_EVENT: |
| if (len != 4) |
| continue; |
| |
| pkt_event = (uint32_t *) data; |
| break; |
| |
| case NL80211_ATTR_CQM_BEACON_LOSS_EVENT: |
| beacon_loss = true; |
| break; |
| |
| case NL80211_ATTR_CQM_RSSI_LEVEL: |
| if (len != 4) |
| continue; |
| |
| rssi_val = (int32_t *) data; |
| break; |
| default: |
| l_debug("Unknown CQM event: %d", type); |
| } |
| } |
| |
| break; |
| } |
| } |
| |
| if (rssi_event) { |
| if (rssi_val) { |
| l_debug("Signal change event (above=%d signal=%d)", |
| *rssi_event, *rssi_val); |
| netdev_cqm_event_rssi_value(netdev, *rssi_val); |
| } else { |
| l_debug("Signal change event (above=%d)", *rssi_event); |
| netdev_cqm_event_rssi_threshold(netdev, *rssi_event); |
| } |
| } else if (pkt_event && netdev->event_filter) |
| netdev->event_filter(netdev, NETDEV_EVENT_PACKET_LOSS_NOTIFY, |
| pkt_event, netdev->user_data); |
| else if (beacon_loss && netdev->event_filter) |
| netdev->event_filter(netdev, NETDEV_EVENT_BEACON_LOSS_NOTIFY, |
| NULL, netdev->user_data); |
| } |
| |
| static void netdev_rekey_offload_event(struct l_genl_msg *msg, |
| struct netdev *netdev) |
| { |
| struct l_genl_attr attr; |
| struct l_genl_attr nested; |
| uint16_t type, len; |
| const void *data; |
| uint64_t replay_ctr; |
| |
| if (!l_genl_attr_init(&attr, msg)) |
| return; |
| |
| while (l_genl_attr_next(&attr, &type, &len, &data)) { |
| if (type != NL80211_ATTR_REKEY_DATA) |
| continue; |
| |
| if (!l_genl_attr_recurse(&attr, &nested)) |
| return; |
| |
| while (l_genl_attr_next(&nested, &type, &len, &data)) { |
| if (type != NL80211_REKEY_DATA_REPLAY_CTR) |
| continue; |
| |
| if (len != sizeof(uint64_t)) { |
| l_warn("Invalid replay_ctr"); |
| return; |
| } |
| |
| replay_ctr = *((uint64_t *) data); |
| __eapol_update_replay_counter(netdev->index, |
| netdev->addr, |
| netdev->handshake->aa, |
| replay_ctr); |
| return; |
| } |
| } |
| } |
| |
| static void netdev_disconnect_event(struct l_genl_msg *msg, |
| struct netdev *netdev) |
| { |
| struct l_genl_attr attr; |
| uint16_t type, len; |
| const void *data; |
| uint16_t reason_code = 0; |
| bool disconnect_by_ap = false; |
| enum netdev_event event; |
| |
| l_debug(""); |
| |
| if (!netdev->connected || netdev->disconnect_cmd_id > 0) |
| return; |
| |
| if (!l_genl_attr_init(&attr, msg)) { |
| l_error("attr init failed"); |
| return; |
| } |
| |
| while (l_genl_attr_next(&attr, &type, &len, &data)) { |
| switch (type) { |
| case NL80211_ATTR_REASON_CODE: |
| if (len != sizeof(uint16_t)) |
| l_warn("Invalid reason code attribute"); |
| else |
| reason_code = *((uint16_t *) data); |
| |
| break; |
| |
| case NL80211_ATTR_DISCONNECTED_BY_AP: |
| disconnect_by_ap = true; |
| break; |
| } |
| } |
| |
| /* |
| * Only ignore this event if issued by the kernel since this is |
| * normal when using CMD_AUTH/ASSOC. |
| */ |
| if (!disconnect_by_ap && (netdev->in_ft || netdev->in_reassoc)) |
| return; |
| |
| l_info("Received Deauthentication event, reason: %hu, from_ap: %s", |
| reason_code, disconnect_by_ap ? "true" : "false"); |
| |
| event = disconnect_by_ap ? NETDEV_EVENT_DISCONNECT_BY_AP : |
| NETDEV_EVENT_DISCONNECT_BY_SME; |
| |
| netdev_disconnected(netdev, NETDEV_RESULT_DISCONNECTED, |
| event, reason_code); |
| } |
| |
| static void netdev_cmd_disconnect_cb(struct l_genl_msg *msg, void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| void *disconnect_data; |
| netdev_disconnect_cb_t disconnect_cb; |
| bool r; |
| |
| netdev->disconnect_cmd_id = 0; |
| netdev->aborting = false; |
| |
| if (!netdev->disconnect_cb) { |
| netdev->user_data = NULL; |
| return; |
| } |
| |
| disconnect_data = netdev->user_data; |
| disconnect_cb = netdev->disconnect_cb; |
| netdev->user_data = NULL; |
| netdev->disconnect_cb = NULL; |
| |
| if (l_genl_msg_get_error(msg) < 0) |
| r = false; |
| else |
| r = true; |
| |
| disconnect_cb(netdev, r, disconnect_data); |
| } |
| |
| static void netdev_deauthenticate_event(struct l_genl_msg *msg, |
| struct netdev *netdev) |
| { |
| const struct mmpdu_header *hdr = NULL; |
| const struct mmpdu_deauthentication *deauth; |
| uint16_t reason_code; |
| struct iovec iov; |
| |
| l_debug(""); |
| |
| /* |
| * If we got to the association phase, process the connect event |
| * instead |
| */ |
| if (!netdev->connected || netdev->associated) |
| return; |
| |
| /* |
| * Handle the bizarre case of AP accepting authentication, then |
| * deauthenticating immediately afterwards |
| */ |
| |
| if (L_WARN_ON(nl80211_parse_attrs(msg, NL80211_ATTR_FRAME, &iov, |
| NL80211_ATTR_UNSPEC) < 0)) |
| return; |
| |
| hdr = mpdu_validate(iov.iov_base, iov.iov_len); |
| if (L_WARN_ON(!hdr)) |
| return; |
| |
| /* Ignore any locally generated frames */ |
| if (!memcmp(hdr->address_2, netdev->addr, sizeof(netdev->addr))) |
| return; |
| |
| deauth = mmpdu_body(hdr); |
| reason_code = L_LE16_TO_CPU(deauth->reason_code); |
| |
| l_info("deauth event, src="MAC" dest="MAC" bssid="MAC" reason=%u", |
| MAC_STR(hdr->address_2), MAC_STR(hdr->address_1), |
| MAC_STR(hdr->address_3), reason_code); |
| |
| netdev_connect_failed(netdev, NETDEV_RESULT_AUTHENTICATION_FAILED, |
| reason_code); |
| } |
| |
| static void netdev_operstate_cb(int error, uint16_t type, |
| const void *data, |
| uint32_t len, void *user_data) |
| { |
| if (!error) |
| return; |
| |
| l_debug("netdev: %u, error: %s", L_PTR_TO_UINT(user_data), |
| strerror(-error)); |
| } |
| |
| static void netdev_connect_ok(struct netdev *netdev) |
| { |
| l_debug(""); |
| |
| l_rtnl_set_linkmode_and_operstate(rtnl, netdev->index, |
| IF_LINK_MODE_DORMANT, IF_OPER_UP, |
| netdev_operstate_cb, |
| L_UINT_TO_PTR(netdev->index), NULL); |
| |
| 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; |
| } else if (netdev->connect_cb) { |
| netdev->connect_cb(netdev, NETDEV_RESULT_OK, NULL, |
| netdev->user_data); |
| netdev->connect_cb = NULL; |
| netdev->in_ft = false; |
| } else |
| l_warn("Connection event without a connect callback!"); |
| |
| netdev_rssi_polling_update(netdev); |
| |
| if (netdev->work.id) |
| wiphy_radio_work_done(netdev->wiphy, netdev->work.id); |
| } |
| |
| static void netdev_setting_keys_failed(struct netdev_handshake_state *nhs, |
| int err) |
| { |
| struct netdev *netdev = nhs->netdev; |
| struct l_genl_msg *msg; |
| |
| /* |
| * Something went wrong with our sequence: |
| * 1. new_key(ptk) |
| * 2. new_key(gtk) [optional] |
| * 3. new_key(igtk) [optional] |
| * 4. rekey offload [optional] |
| * 5. set_station |
| * |
| * Cancel all pending commands, then de-authenticate |
| */ |
| netdev_handshake_state_cancel_all(nhs); |
| |
| if (netdev->rekey_offload_cmd_id) { |
| l_genl_family_cancel(nl80211, netdev->rekey_offload_cmd_id); |
| netdev->rekey_offload_cmd_id = 0; |
| } |
| |
| if (netdev->group_handshake_timeout) { |
| l_timeout_remove(netdev->group_handshake_timeout); |
| netdev->group_handshake_timeout = NULL; |
| } |
| |
| switch (netdev->type) { |
| case NL80211_IFTYPE_STATION: |
| case NL80211_IFTYPE_P2P_CLIENT: |
| /* |
| * If we failed due to the netdev being brought down, |
| * just abort the connection and do not try to send a |
| * CMD_DISCONNECT |
| */ |
| if (err == -ENETDOWN) { |
| netdev_disconnected(netdev, NETDEV_RESULT_ABORTED, |
| NETDEV_EVENT_DISCONNECT_BY_SME, |
| MMPDU_STATUS_CODE_UNSPECIFIED); |
| return; |
| } |
| |
| netdev_disconnect_by_sme(netdev, |
| NETDEV_RESULT_KEY_SETTING_FAILED, |
| MMPDU_REASON_CODE_UNSPECIFIED); |
| break; |
| case NL80211_IFTYPE_AP: |
| if (err == -ENETDOWN) |
| return; |
| |
| msg = nl80211_build_del_station(netdev->index, |
| nhs->super.spa, MMPDU_REASON_CODE_UNSPECIFIED, |
| MPDU_MANAGEMENT_SUBTYPE_DEAUTHENTICATION); |
| if (!l_genl_family_send(nl80211, msg, NULL, NULL, NULL)) |
| l_error("error sending DEL_STATION"); |
| |
| break; |
| } |
| |
| handshake_event(&nhs->super, HANDSHAKE_EVENT_SETTING_KEYS_FAILED, &err); |
| } |
| |
| static bool netdev_match_addr(const void *a, const void *b) |
| { |
| const struct netdev *netdev = a; |
| const uint8_t *addr = b; |
| |
| return memcmp(netdev->addr, addr, ETH_ALEN) == 0; |
| } |
| |
| static struct netdev *netdev_find_by_address(const uint8_t *addr) |
| { |
| return l_queue_find(netdev_list, netdev_match_addr, addr); |
| } |
| |
| static void netdev_pmksa_driver_add(const struct pmksa *pmksa) |
| { |
| struct l_genl_msg *msg; |
| struct netdev *netdev = netdev_find_by_address(pmksa->spa); |
| uint32_t expiration = (uint32_t)pmksa->expiration; |
| |
| if (!netdev) |
| return; |
| |
| /* Only need to set the PMKSA into the kernel for fullmac drivers */ |
| if (wiphy_supports_cmds_auth_assoc(netdev->wiphy)) |
| return; |
| |
| l_debug("Adding PMKSA to kernel"); |
| |
| msg = l_genl_msg_new(NL80211_CMD_SET_PMKSA); |
| |
| l_genl_msg_append_attr(msg, NL80211_ATTR_IFINDEX, 4, &netdev->index); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_PMKID, 16, pmksa->pmkid); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_MAC, ETH_ALEN, pmksa->aa); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_SSID, |
| pmksa->ssid_len, pmksa->ssid); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_PMK_LIFETIME, 4, &expiration); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_PMK, |
| pmksa->pmk_len, pmksa->pmk); |
| |
| if (!l_genl_family_send(nl80211, msg, NULL, NULL, NULL)) |
| l_error("error sending SET_PMKSA"); |
| } |
| |
| static void netdev_pmksa_driver_remove(const struct pmksa *pmksa) |
| { |
| struct l_genl_msg *msg; |
| struct netdev *netdev = netdev_find_by_address(pmksa->spa); |
| |
| if (!netdev) |
| return; |
| |
| /* Only need to set the PMKSA into the kernel for fullmac drivers */ |
| if (wiphy_supports_cmds_auth_assoc(netdev->wiphy)) |
| return; |
| |
| l_debug("Removing PMKSA from kernel"); |
| |
| msg = l_genl_msg_new(NL80211_CMD_DEL_PMKSA); |
| |
| l_genl_msg_append_attr(msg, NL80211_ATTR_IFINDEX, 4, &netdev->index); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_PMKID, 16, pmksa->pmkid); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_MAC, ETH_ALEN, pmksa->aa); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_SSID, |
| pmksa->ssid_len, pmksa->ssid); |
| |
| if (!l_genl_family_send(nl80211, msg, NULL, NULL, NULL)) |
| l_error("error sending DEL_PMKSA"); |
| } |
| |
| static void netdev_flush_pmksa(struct netdev *netdev) |
| { |
| struct l_genl_msg *msg; |
| |
| /* |
| * We only utilize the kernel's PMKSA cache for fullmac cards, |
| * so no need to flush if this is a softmac. |
| */ |
| if (wiphy_supports_cmds_auth_assoc(netdev->wiphy)) |
| return; |
| |
| msg = l_genl_msg_new(NL80211_CMD_FLUSH_PMKSA); |
| |
| l_genl_msg_append_attr(msg, NL80211_ATTR_IFINDEX, 4, &netdev->index); |
| |
| if (!l_genl_family_send(nl80211, msg, NULL, NULL, NULL)) |
| l_error("Failed to flush PMKSA for %u", netdev->index); |
| } |
| |
| static void netdev_pmksa_driver_flush(void) |
| { |
| const struct l_queue_entry *e; |
| |
| for (e = l_queue_get_entries(netdev_list); e; e = e->next) { |
| struct netdev *netdev = e->data; |
| |
| netdev_flush_pmksa(netdev); |
| } |
| } |
| |
| static void try_handshake_complete(struct netdev_handshake_state *nhs) |
| { |
| l_debug("ptk_installed: %u, gtk_installed: %u, igtk_installed: %u", |
| nhs->ptk_installed, |
| nhs->gtk_installed, |
| nhs->igtk_installed); |
| |
| if (nhs->ptk_installed && nhs->gtk_installed && nhs->igtk_installed) { |
| l_debug("nhs->complete: %u", nhs->complete); |
| |
| if (nhs->complete) { |
| handshake_event(&nhs->super, |
| HANDSHAKE_EVENT_REKEY_COMPLETE); |
| return; |
| } |
| |
| nhs->complete = true; |
| |
| l_debug("Invoking handshake_event()"); |
| |
| handshake_state_cache_pmksa(&nhs->super); |
| |
| if (handshake_event(&nhs->super, HANDSHAKE_EVENT_COMPLETE)) |
| return; |
| |
| if (nhs->netdev->type == NL80211_IFTYPE_STATION || |
| nhs->netdev->type == NL80211_IFTYPE_P2P_CLIENT) |
| netdev_connect_ok(nhs->netdev); |
| } |
| } |
| |
| static void netdev_set_station_cb(struct l_genl_msg *msg, void *user_data) |
| { |
| struct netdev_handshake_state *nhs = user_data; |
| struct netdev *netdev = nhs->netdev; |
| int err; |
| |
| l_debug(""); |
| |
| nhs->set_station_cmd_id = 0; |
| nhs->ptk_installed = true; |
| |
| if (netdev->type == NL80211_IFTYPE_STATION && !netdev->connected) |
| return; |
| |
| err = l_genl_msg_get_error(msg); |
| if (err == -EOPNOTSUPP || err == -ENOTSUPP) |
| goto done; |
| |
| if (err < 0) { |
| const char *ext_error = l_genl_msg_get_extended_error(msg); |
| |
| l_error("Set Station failed for ifindex %d:%s", netdev->index, |
| ext_error ? ext_error : strerror(-err)); |
| |
| netdev_setting_keys_failed(nhs, err); |
| return; |
| } |
| |
| done: |
| try_handshake_complete(nhs); |
| } |
| |
| static void netdev_new_group_key_cb(struct l_genl_msg *msg, void *data) |
| { |
| struct netdev_handshake_state *nhs = data; |
| struct netdev *netdev = nhs->netdev; |
| int err = l_genl_msg_get_error(msg); |
| |
| l_debug("ifindex: %u, err: %d", netdev->index, err); |
| nhs->group_new_key_cmd_id = 0; |
| |
| if (err < 0) { |
| const char *ext_error = l_genl_msg_get_extended_error(msg); |
| |
| l_error("New Key for Group Key failed for ifindex: %d:%s", |
| netdev->index, |
| ext_error ? ext_error : strerror(-err)); |
| |
| netdev_setting_keys_failed(nhs, err); |
| return; |
| } |
| |
| nhs->gtk_installed = true; |
| try_handshake_complete(nhs); |
| } |
| |
| static void netdev_new_group_management_key_cb(struct l_genl_msg *msg, |
| void *data) |
| { |
| struct netdev_handshake_state *nhs = data; |
| struct netdev *netdev = nhs->netdev; |
| int err = l_genl_msg_get_error(msg); |
| |
| l_debug("ifindex: %u, err: %d", netdev->index, err); |
| nhs->group_management_new_key_cmd_id = 0; |
| |
| if (err < 0) { |
| const char *ext_error = l_genl_msg_get_extended_error(msg); |
| |
| l_error("New Key for Group Mgmt failed for ifindex: %d:%s", |
| netdev->index, |
| ext_error ? ext_error : strerror(-err)); |
| |
| netdev_setting_keys_failed(nhs, err); |
| return; |
| } |
| |
| nhs->igtk_installed = true; |
| try_handshake_complete(nhs); |
| } |
| |
| static bool netdev_copy_tk(uint8_t *tk_buf, const uint8_t *tk, |
| uint32_t cipher, bool authenticator) |
| { |
| switch (cipher) { |
| case CRYPTO_CIPHER_CCMP: |
| case CRYPTO_CIPHER_GCMP: |
| case CRYPTO_CIPHER_GCMP_256: |
| case CRYPTO_CIPHER_CCMP_256: |
| /* |
| * 802.11-2020 12.8.3 Mapping PTK to CCMP keys: |
| * "A STA shall use the temporal key as the CCMP key |
| * for MPDUs between the two communicating STAs." |
| * |
| * Similar verbiage in 12.8.8 |
| */ |
| memcpy(tk_buf, tk, crypto_cipher_key_len(cipher)); |
| break; |
| case CRYPTO_CIPHER_TKIP: |
| /* |
| * 802.11-2016 12.8.1 Mapping PTK to TKIP keys: |
| * "A STA shall use bits 0-127 of the temporal key as its |
| * input to the TKIP Phase 1 and Phase 2 mixing functions. |
| * |
| * A STA shall use bits 128-191 of the temporal key as |
| * the michael key for MSDUs from the Authenticator's STA |
| * to the Supplicant's STA. |
| * |
| * A STA shall use bits 192-255 of the temporal key as |
| * the michael key for MSDUs from the Supplicant's STA |
| * to the Authenticator's STA." |
| */ |
| if (authenticator) { |
| memcpy(tk_buf + NL80211_TKIP_DATA_OFFSET_ENCR_KEY, |
| tk, 16); |
| memcpy(tk_buf + NL80211_TKIP_DATA_OFFSET_TX_MIC_KEY, |
| tk + 16, 8); |
| memcpy(tk_buf + NL80211_TKIP_DATA_OFFSET_RX_MIC_KEY, |
| tk + 24, 8); |
| } else { |
| memcpy(tk_buf + NL80211_TKIP_DATA_OFFSET_ENCR_KEY, |
| tk, 16); |
| memcpy(tk_buf + NL80211_TKIP_DATA_OFFSET_RX_MIC_KEY, |
| tk + 16, 8); |
| memcpy(tk_buf + NL80211_TKIP_DATA_OFFSET_TX_MIC_KEY, |
| tk + 24, 8); |
| } |
| break; |
| default: |
| l_error("Unexpected cipher: %x", cipher); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| static const uint8_t *netdev_choose_key_address( |
| struct netdev_handshake_state *nhs) |
| { |
| return (nhs->super.authenticator) ? nhs->super.spa : nhs->super.aa; |
| } |
| |
| static void netdev_set_gtk(struct handshake_state *hs, uint16_t key_index, |
| const uint8_t *gtk, uint8_t gtk_len, |
| const uint8_t *rsc, uint8_t rsc_len, |
| uint32_t cipher) |
| { |
| struct netdev_handshake_state *nhs = |
| l_container_of(hs, struct netdev_handshake_state, super); |
| struct netdev *netdev = nhs->netdev; |
| uint8_t gtk_buf[32]; |
| struct l_genl_msg *msg; |
| const uint8_t *addr = (netdev->type == NL80211_IFTYPE_ADHOC) ? |
| nhs->super.aa : NULL; |
| |
| nhs->gtk_installed = false; |
| |
| l_debug("ifindex=%d key_idx=%u", netdev->index, key_index); |
| |
| if (crypto_cipher_key_len(cipher) != gtk_len) { |
| l_error("Unexpected key length: %d", gtk_len); |
| netdev_setting_keys_failed(nhs, -ERANGE); |
| return; |
| } |
| |
| if (!netdev_copy_tk(gtk_buf, gtk, cipher, hs->authenticator)) { |
| netdev_setting_keys_failed(nhs, -ENOENT); |
| return; |
| } |
| |
| if (hs->wait_for_gtk) { |
| l_timeout_remove(netdev->group_handshake_timeout); |
| netdev->group_handshake_timeout = NULL; |
| } |
| |
| msg = nl80211_build_new_key_group(netdev->index, cipher, key_index, |
| gtk_buf, gtk_len, rsc, rsc_len, addr); |
| |
| nhs->group_new_key_cmd_id = |
| l_genl_family_send(nl80211, msg, netdev_new_group_key_cb, |
| nhs, NULL); |
| |
| if (nhs->group_new_key_cmd_id > 0) |
| return; |
| |
| l_genl_msg_unref(msg); |
| netdev_setting_keys_failed(nhs, -EIO); |
| } |
| |
| static void netdev_set_igtk(struct handshake_state *hs, uint16_t key_index, |
| const uint8_t *igtk, uint8_t igtk_len, |
| const uint8_t *ipn, uint8_t ipn_len, |
| uint32_t cipher) |
| { |
| struct netdev_handshake_state *nhs = |
| l_container_of(hs, struct netdev_handshake_state, super); |
| uint8_t igtk_buf[32]; |
| struct netdev *netdev = nhs->netdev; |
| struct l_genl_msg *msg; |
| |
| nhs->igtk_installed = false; |
| |
| l_debug("ifindex=%d key_idx=%u", netdev->index, key_index); |
| |
| if (crypto_cipher_key_len(cipher) != igtk_len) { |
| l_error("Unexpected key length: %d", igtk_len); |
| netdev_setting_keys_failed(nhs, -ERANGE); |
| return; |
| } |
| |
| switch (cipher) { |
| case CRYPTO_CIPHER_BIP_CMAC: |
| case CRYPTO_CIPHER_BIP_GMAC: |
| case CRYPTO_CIPHER_BIP_GMAC_256: |
| case CRYPTO_CIPHER_BIP_CMAC_256: |
| memcpy(igtk_buf, igtk, igtk_len); |
| break; |
| default: |
| l_error("Unexpected cipher: %x", cipher); |
| netdev_setting_keys_failed(nhs, -ENOENT); |
| return; |
| } |
| |
| if (key_index == 0x0400 || key_index == 0x0500) { |
| l_warn("Received an invalid IGTK key index (%04hx)" |
| " that is likely in" |
| " big endian format. Trying to fix and" |
| " proceed anyway", key_index); |
| key_index = bswap_16(key_index); |
| } |
| |
| msg = nl80211_build_new_key_group(netdev->index, cipher, key_index, |
| igtk_buf, igtk_len, ipn, ipn_len, NULL); |
| |
| nhs->group_management_new_key_cmd_id = |
| l_genl_family_send(nl80211, msg, |
| netdev_new_group_management_key_cb, |
| nhs, NULL); |
| |
| if (nhs->group_management_new_key_cmd_id > 0) |
| return; |
| |
| l_genl_msg_unref(msg); |
| netdev_setting_keys_failed(nhs, -EIO); |
| } |
| |
| static struct l_genl_msg *netdev_build_cmd_set_key_tx(struct netdev *netdev) |
| { |
| uint8_t key_mode = NL80211_KEY_SET_TX; |
| struct l_genl_msg *msg = l_genl_msg_new_sized(NL80211_CMD_SET_KEY, 512); |
| |
| l_genl_msg_append_attr(msg, NL80211_ATTR_MAC, ETH_ALEN, |
| netdev->handshake->aa); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_IFINDEX, 4, &netdev->index); |
| |
| l_genl_msg_enter_nested(msg, NL80211_ATTR_KEY); |
| l_genl_msg_append_attr(msg, NL80211_KEY_IDX, 1, |
| &netdev->handshake->active_tk_index); |
| l_genl_msg_append_attr(msg, NL80211_KEY_MODE, 1, &key_mode); |
| l_genl_msg_leave_nested(msg); |
| |
| return msg; |
| } |
| |
| static void netdev_new_pairwise_key_cb(struct l_genl_msg *msg, void *data) |
| { |
| struct netdev_handshake_state *nhs = data; |
| struct netdev *netdev = nhs->netdev; |
| const uint8_t *addr = netdev_choose_key_address(nhs); |
| int err = l_genl_msg_get_error(msg); |
| |
| nhs->pairwise_new_key_cmd_id = 0; |
| |
| if (err < 0) { |
| const char *ext_error = l_genl_msg_get_extended_error(msg); |
| |
| l_error("New Key for Pairwise Key failed for ifindex: %d:%s", |
| netdev->index, |
| ext_error ? ext_error : strerror(-err)); |
| goto error; |
| } |
| |
| /* |
| * Set the AUTHORIZED flag using a SET_STATION command even if |
| * we're already operational, it will not hurt during re-keying |
| * and is necessary after an FT. |
| */ |
| msg = nl80211_build_set_station_authorized(netdev->index, addr); |
| |
| nhs->set_station_cmd_id = |
| l_genl_family_send(nl80211, msg, netdev_set_station_cb, |
| nhs, NULL); |
| if (nhs->set_station_cmd_id > 0) |
| return; |
| |
| l_genl_msg_unref(msg); |
| err = -EIO; |
| error: |
| netdev_setting_keys_failed(nhs, err); |
| } |
| |
| static struct l_genl_msg *netdev_build_control_port_frame(struct netdev *netdev, |
| const uint8_t *to, |
| uint16_t proto, |
| bool unencrypted, |
| const void *body, |
| size_t body_len) |
| { |
| struct l_genl_msg *msg; |
| |
| msg = l_genl_msg_new_sized(NL80211_CMD_CONTROL_PORT_FRAME, |
| 128 + body_len); |
| |
| l_genl_msg_append_attr(msg, NL80211_ATTR_IFINDEX, 4, &netdev->index); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_FRAME, body_len, body); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_CONTROL_PORT_ETHERTYPE, 2, |
| &proto); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_MAC, ETH_ALEN, to); |
| |
| if (unencrypted) |
| l_genl_msg_append_attr(msg, |
| NL80211_ATTR_CONTROL_PORT_NO_ENCRYPT, 0, NULL); |
| |
| return msg; |
| } |
| |
| static void netdev_control_port_frame_cb(struct l_genl_msg *msg, |
| void *user_data) |
| { |
| int err = l_genl_msg_get_error(msg); |
| const char *ext_error; |
| |
| if (err >= 0) |
| return; |
| |
| ext_error = l_genl_msg_get_extended_error(msg); |
| l_error("CMD_CONTROL_PORT failed: %s", |
| ext_error ? ext_error : strerror(-err)); |
| } |
| |
| static int netdev_control_port_write_pae(struct netdev *netdev, |
| const uint8_t *dest, |
| uint16_t proto, |
| const struct eapol_frame *ef, |
| bool noencrypt) |
| { |
| int fd = l_io_get_fd(netdev->pae_io); |
| struct sockaddr_ll sll; |
| size_t frame_size = sizeof(struct eapol_header) + |
| L_BE16_TO_CPU(ef->header.packet_len); |
| ssize_t r; |
| |
| memset(&sll, 0, sizeof(sll)); |
| sll.sll_family = AF_PACKET; |
| sll.sll_ifindex = netdev->index; |
| sll.sll_protocol = htons(proto); |
| sll.sll_halen = ETH_ALEN; |
| memcpy(sll.sll_addr, dest, ETH_ALEN); |
| |
| r = sendto(fd, ef, frame_size, 0, |
| (struct sockaddr *) &sll, sizeof(sll)); |
| if (r < 0) |
| l_error("EAPoL write socket: %s", strerror(errno)); |
| |
| return r; |
| } |
| |
| static int netdev_control_port_frame(uint32_t ifindex, |
| const uint8_t *dest, uint16_t proto, |
| const struct eapol_frame *ef, |
| bool noencrypt, |
| void *user_data) |
| { |
| struct l_genl_msg *msg; |
| struct netdev *netdev; |
| size_t frame_size; |
| |
| netdev = netdev_find(ifindex); |
| if (!netdev) |
| return -ENOENT; |
| |
| frame_size = sizeof(struct eapol_header) + |
| L_BE16_TO_CPU(ef->header.packet_len); |
| |
| if (!netdev->pae_over_nl80211) |
| return netdev_control_port_write_pae(netdev, dest, proto, |
| ef, noencrypt); |
| |
| msg = netdev_build_control_port_frame(netdev, dest, proto, noencrypt, |
| ef, frame_size); |
| if (!msg) |
| return -ENOMEM; |
| |
| if (!l_genl_family_send(nl80211, msg, netdev_control_port_frame_cb, |
| netdev, NULL)) { |
| l_genl_msg_unref(msg); |
| return -EINVAL; |
| } |
| |
| return 0; |
| } |
| |
| static int netdev_set_key_tx(struct netdev *netdev) |
| { |
| struct netdev_handshake_state *nhs = l_container_of(netdev->handshake, |
| struct netdev_handshake_state, super); |
| struct l_genl_msg *msg = netdev_build_cmd_set_key_tx(netdev); |
| |
| nhs->pairwise_set_key_tx_cmd_id = l_genl_family_send(nl80211, msg, |
| netdev_new_pairwise_key_cb, |
| nhs, NULL); |
| if (nhs->pairwise_set_key_tx_cmd_id > 0) |
| return 0; |
| |
| l_genl_msg_unref(msg); |
| |
| return -EIO; |
| } |
| |
| static void netdev_new_rx_pairwise_key_cb(struct l_genl_msg *msg, void *data) |
| { |
| struct netdev_handshake_state *nhs = data; |
| struct netdev *netdev = nhs->netdev; |
| struct netdev_ext_key_info *info = netdev->ext_key_info; |
| int err = l_genl_msg_get_error(msg); |
| |
| nhs->pairwise_new_key_cmd_id = 0; |
| |
| if (err < 0) { |
| const char *ext_error = l_genl_msg_get_extended_error(msg); |
| |
| l_error("New Key for RX Pairwise Key failed for ifindex: %d:%s", |
| netdev->index, |
| ext_error ? ext_error : strerror(-err)); |
| goto error; |
| } |
| |
| if (!info) |
| return; |
| |
| err = netdev_control_port_write_pae(netdev, nhs->super.aa, info->proto, |
| info->frame, info->noencrypt); |
| l_free(netdev->ext_key_info); |
| netdev->ext_key_info = NULL; |
| |
| if (err < 0) |
| goto error; |
| |
| err = netdev_set_key_tx(netdev); |
| if (err < 0) |
| goto error; |
| |
| return; |
| |
| error: |
| netdev_setting_keys_failed(nhs, err); |
| } |
| |
| static void netdev_group_timeout_cb(struct l_timeout *timeout, void *user_data) |
| { |
| struct netdev_handshake_state *nhs = user_data; |
| |
| /* |
| * There was a problem with the ptk, this should have triggered a key |
| * setting failure event already. |
| */ |
| if (!nhs->ptk_installed) |
| return; |
| |
| /* |
| * If this happens, we never completed the group handshake. We can still |
| * complete the connection, but we will not have group traffic. |
| */ |
| l_warn("completing connection with no group traffic on ifindex %d", |
| nhs->netdev->index); |
| |
| nhs->complete = true; |
| |
| if (handshake_event(&nhs->super, HANDSHAKE_EVENT_COMPLETE)) |
| return; |
| |
| netdev_connect_ok(nhs->netdev); |
| } |
| |
| static void netdev_set_tk(struct handshake_state *hs, uint8_t key_index, |
| const uint8_t *tk, uint32_t cipher) |
| { |
| struct netdev_handshake_state *nhs = |
| l_container_of(hs, struct netdev_handshake_state, super); |
| uint8_t tk_buf[32]; |
| struct netdev *netdev = nhs->netdev; |
| struct l_genl_msg *msg; |
| const uint8_t *addr = netdev_choose_key_address(nhs); |
| int err; |
| |
| nhs->ptk_installed = false; |
| |
| /* |
| * WPA1 does the group handshake after the 4-way finishes so we can't |
| * rely on the gtk/igtk being set immediately after the ptk. Since |
| * 'gtk_installed' is initially set to true (to handle NO_GROUP_TRAFFIC) |
| * we must set it false so we don't notify that the connection was |
| * successful until we get the gtk/igtk callbacks. Note that we do not |
| * need to set igtk_installed false because the igtk could not happen at |
| * all. |
| */ |
| if (hs->wait_for_gtk) { |
| nhs->gtk_installed = false; |
| |
| netdev->group_handshake_timeout = l_timeout_create(2, |
| netdev_group_timeout_cb, nhs, NULL); |
| } |
| |
| /* |
| * 802.11 Section 4.10.4.3: |
| * Because in an IBSS there are two 4-way handshakes between |
| * any two Supplicants and Authenticators, the pairwise key used |
| * between any two STAs is from the 4-way handshake initiated |
| * by the STA Authenticator with the higher MAC address... |
| */ |
| if (netdev->type == NL80211_IFTYPE_ADHOC && |
| memcmp(nhs->super.aa, nhs->super.spa, 6) < 0) { |
| nhs->ptk_installed = true; |
| try_handshake_complete(nhs); |
| return; |
| } |
| |
| l_debug("ifindex=%d key_idx=%u", netdev->index, key_index); |
| |
| err = -ENOENT; |
| if (!netdev_copy_tk(tk_buf, tk, cipher, hs->authenticator)) |
| goto invalid_key; |
| |
| msg = nl80211_build_new_key_pairwise(netdev->index, cipher, addr, |
| tk_buf, crypto_cipher_key_len(cipher), |
| key_index); |
| nhs->pairwise_new_key_cmd_id = |
| l_genl_family_send(nl80211, msg, netdev_new_pairwise_key_cb, |
| nhs, NULL); |
| if (nhs->pairwise_new_key_cmd_id > 0) |
| return; |
| |
| err = -EIO; |
| l_genl_msg_unref(msg); |
| invalid_key: |
| netdev_setting_keys_failed(nhs, err); |
| } |
| |
| static void netdev_set_ext_tk(struct handshake_state *hs, uint8_t key_idx, |
| const uint8_t *tk, uint32_t cipher, |
| const struct eapol_frame *step4, uint16_t proto, |
| bool noencrypt) |
| { |
| struct netdev_handshake_state *nhs = |
| l_container_of(hs, struct netdev_handshake_state, super); |
| uint8_t tk_buf[32]; |
| struct netdev *netdev = nhs->netdev; |
| struct l_genl_msg *msg; |
| const uint8_t *addr = netdev_choose_key_address(nhs); |
| int err; |
| size_t frame_size = sizeof(struct eapol_header) + |
| L_BE16_TO_CPU(step4->header.packet_len); |
| |
| err = -ENOENT; |
| if (!netdev_copy_tk(tk_buf, tk, cipher, hs->authenticator)) |
| goto error; |
| |
| msg = nl80211_build_new_rx_key_pairwise(netdev->index, cipher, addr, |
| tk_buf, crypto_cipher_key_len(cipher), |
| hs->active_tk_index); |
| nhs->pairwise_new_key_cmd_id = |
| l_genl_family_send(nl80211, msg, netdev_new_rx_pairwise_key_cb, |
| nhs, NULL); |
| |
| if (!nhs->pairwise_new_key_cmd_id) |
| goto io_error; |
| |
| /* |
| * Without control port we cannot guarantee the order that messages go |
| * out and must wait for NEW_KEY to call back before sending message 4 |
| */ |
| if (!netdev->pae_over_nl80211) { |
| netdev->ext_key_info = l_malloc( |
| sizeof(struct netdev_ext_key_info) + |
| frame_size); |
| memcpy(netdev->ext_key_info->frame, step4, frame_size); |
| netdev->ext_key_info->proto = proto; |
| netdev->ext_key_info->noencrypt = noencrypt; |
| return; |
| } |
| |
| /* |
| * Otherwise, order of messages will be guaranteed. Therefore we can |
| * send send message 4, and set the TK to TX (below) without waiting for |
| * any callbacks |
| */ |
| err = netdev_control_port_frame(netdev->index, netdev->handshake->aa, |
| proto, step4, noencrypt, NULL); |
| if (err < 0) |
| goto error; |
| |
| /* Then toggle to RX + TX */ |
| err = netdev_set_key_tx(netdev); |
| if (err < 0) |
| goto error; |
| |
| return; |
| |
| io_error: |
| err = -EIO; |
| l_genl_msg_unref(msg); |
| |
| error: |
| netdev_setting_keys_failed(nhs, err); |
| } |
| |
| static void netdev_set_pmk_cb(struct l_genl_msg *msg, void *user_data) |
| { |
| struct netdev_handshake_state *nhs = user_data; |
| struct netdev *netdev = nhs->netdev; |
| int err = l_genl_msg_get_error(msg); |
| |
| nhs->set_pmk_cmd_id = 0; |
| |
| if (err < 0) { |
| l_error("Error with SET_PMK/SET_STATION"); |
| netdev_setting_keys_failed(nhs, err); |
| return; |
| } |
| |
| if (handshake_event(netdev->handshake, HANDSHAKE_EVENT_SETTING_KEYS)) |
| return; |
| |
| netdev_connect_ok(netdev); |
| } |
| |
| static void netdev_set_pmk(struct handshake_state *hs, const uint8_t *pmk, |
| size_t pmk_len) |
| { |
| struct l_genl_msg *msg; |
| struct netdev_handshake_state *nhs = l_container_of(hs, |
| struct netdev_handshake_state, super); |
| struct netdev *netdev = nhs->netdev; |
| |
| /* Only relevant for 8021x offload */ |
| if (nhs->type != CONNECTION_TYPE_8021X_OFFLOAD) |
| return; |
| |
| msg = l_genl_msg_new(NL80211_CMD_SET_PMK); |
| |
| l_genl_msg_append_attr(msg, NL80211_ATTR_IFINDEX, 4, &netdev->index); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_MAC, 6, netdev->handshake->aa); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_PMK, |
| netdev->handshake->pmk_len, |
| netdev->handshake->pmk); |
| |
| nhs->set_pmk_cmd_id = l_genl_family_send(nl80211, msg, |
| netdev_set_pmk_cb, |
| nhs, NULL); |
| if (!nhs->set_pmk_cmd_id) { |
| l_error("Failed to set SET_PMK"); |
| netdev_setting_keys_failed(nhs, -EIO); |
| return; |
| } |
| } |
| |
| void netdev_handshake_failed(struct handshake_state *hs, uint16_t reason_code) |
| { |
| struct netdev_handshake_state *nhs = |
| l_container_of(hs, struct netdev_handshake_state, super); |
| struct netdev *netdev = nhs->netdev; |
| struct l_genl_msg *msg; |
| |
| l_error("4-Way handshake failed for ifindex: %d, reason: %u", |
| netdev->index, reason_code); |
| |
| netdev->sm = NULL; |
| |
| switch (netdev->type) { |
| case NL80211_IFTYPE_STATION: |
| case NL80211_IFTYPE_P2P_CLIENT: |
| netdev_disconnect_by_sme(netdev, NETDEV_RESULT_HANDSHAKE_FAILED, |
| reason_code); |
| break; |
| case NL80211_IFTYPE_AP: |
| case NL80211_IFTYPE_P2P_GO: |
| msg = nl80211_build_del_station(netdev->index, nhs->super.spa, |
| reason_code, |
| MPDU_MANAGEMENT_SUBTYPE_DEAUTHENTICATION); |
| if (!l_genl_family_send(nl80211, msg, NULL, NULL, NULL)) |
| l_error("error sending DEL_STATION"); |
| } |
| |
| if (netdev->work.id) |
| wiphy_radio_work_done(netdev->wiphy, netdev->work.id); |
| } |
| |
| static void hardware_rekey_cb(struct l_genl_msg *msg, void *data) |
| { |
| struct netdev *netdev = data; |
| int err; |
| |
| netdev->rekey_offload_cmd_id = 0; |
| |
| err = l_genl_msg_get_error(msg); |
| if (err < 0) { |
| if (err == -EOPNOTSUPP) { |
| l_error("hardware_rekey not supported"); |
| netdev->rekey_offload_support = false; |
| } |
| |
| /* |
| * TODO: Ignore all other errors for now, until WoWLAN is |
| * supported properly |
| */ |
| } |
| } |
| |
| static void netdev_set_rekey_offload(uint32_t ifindex, |
| const uint8_t *kek, |
| const uint8_t *kck, |
| uint64_t replay_counter, |
| void *user_data) |
| { |
| struct netdev *netdev; |
| struct l_genl_msg *msg; |
| |
| netdev = netdev_find(ifindex); |
| if (!netdev) |
| return; |
| |
| if (netdev->type != NL80211_IFTYPE_STATION) |
| return; |
| |
| if (!netdev->rekey_offload_support) |
| return; |
| |
| l_debug("%d", netdev->index); |
| msg = nl80211_build_rekey_offload(ifindex, kek, kck, replay_counter); |
| netdev->rekey_offload_cmd_id = l_genl_family_send(nl80211, msg, |
| hardware_rekey_cb, |
| netdev, NULL); |
| } |
| |
| static void netdev_qos_map_cb(struct l_genl_msg *msg, void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| int err = l_genl_msg_get_error(msg); |
| const char *ext_error; |
| |
| netdev->qos_map_cmd_id = 0; |
| |
| if (err >= 0) |
| return; |
| |
| ext_error = l_genl_msg_get_extended_error(msg); |
| l_error("Could not set QoS Map in kernel: %s", |
| ext_error ? ext_error : strerror(-err)); |
| } |
| |
| /* |
| * TODO: Fix this in the kernel: |
| * |
| * The QoS Map is really of no use to IWD. The kernel requires it to map QoS |
| * network values properly to what it puts into the IP header. The way we have |
| * to let the kernel know is to receive the IE, then give it right back... |
| * |
| * The kernel/driver/firmware *could* simply obtain this information as the |
| * frame comes in and not require userspace to forward it back... but that's a |
| * battle for another day. |
| */ |
| static void netdev_send_qos_map_set(struct netdev *netdev, |
| const uint8_t *qos_set, size_t qos_len) |
| { |
| struct l_genl_msg *msg; |
| |
| if (!wiphy_supports_qos_set_map(netdev->wiphy)) { |
| l_warn("AP sent QoS Map, but capability was not advertised!"); |
| return; |
| } |
| |
| /* |
| * Since this IE comes in on either a management frame or during |
| * Association response we could have potentially already set this. |
| */ |
| if (netdev->qos_map_cmd_id) |
| return; |
| |
| msg = l_genl_msg_new_sized(NL80211_CMD_SET_QOS_MAP, 128 + qos_len); |
| |
| l_genl_msg_append_attr(msg, NL80211_ATTR_IFINDEX, 4, &netdev->index); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_QOS_MAP, qos_len, qos_set); |
| |
| netdev->qos_map_cmd_id = l_genl_family_send(nl80211, msg, |
| netdev_qos_map_cb, |
| netdev, NULL); |
| } |
| |
| static void netdev_get_oci_cb(struct l_genl_msg *msg, void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| int err = l_genl_msg_get_error(msg); |
| _auto_(l_free) struct band_chandef *chandef = |
| l_new(struct band_chandef, 1); |
| |
| netdev->get_oci_cmd_id = 0; |
| |
| if (err < 0) { |
| const char *ext_error = l_genl_msg_get_extended_error(msg); |
| |
| l_error("Could not get OCI info: %s", |
| ext_error ? ext_error : strerror(-err)); |
| goto done; |
| } |
| |
| if (nl80211_parse_chandef(msg, chandef) < 0) { |
| l_debug("Couldn't parse operating channel info."); |
| goto done; |
| } |
| |
| l_debug("Obtained OCI: freq: %u, width: %u, center1: %u, center2: %u", |
| chandef->frequency, chandef->channel_width, |
| chandef->center1_frequency, chandef->center2_frequency); |
| |
| handshake_state_set_chandef(netdev->handshake, l_steal_ptr(chandef)); |
| |
| done: |
| if (netdev->ap) { |
| /* |
| * Can't do much here. IWD assumes every kernel/driver supports |
| * this. There is no way of detecting support either. |
| */ |
| if (L_WARN_ON(err < 0)) |
| netdev_connect_failed(netdev, |
| NETDEV_RESULT_AUTHENTICATION_FAILED, |
| MMPDU_STATUS_CODE_UNSPECIFIED); |
| else |
| auth_proto_rx_oci(netdev->ap); |
| |
| return; |
| } |
| |
| L_WARN_ON(!eapol_start(netdev->sm)); |
| } |
| |
| static int netdev_get_oci(void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| struct l_genl_msg *msg = |
| l_genl_msg_new_sized(NL80211_CMD_GET_INTERFACE, 64); |
| |
| l_debug(""); |
| |
| l_genl_msg_append_attr(msg, NL80211_ATTR_IFINDEX, 4, &netdev->index); |
| |
| netdev->get_oci_cmd_id = l_genl_family_send(nl80211, msg, |
| netdev_get_oci_cb, netdev, |
| NULL); |
| if (!netdev->get_oci_cmd_id) { |
| l_genl_msg_unref(msg); |
| return -EIO; |
| } |
| |
| return 0; |
| } |
| |
| static void parse_request_ies(struct netdev *netdev, const uint8_t *ies, |
| size_t ies_len) |
| { |
| struct ie_tlv_iter iter; |
| const void *data; |
| const uint8_t *rsnxe = NULL; |
| |
| l_debug(""); |
| |
| /* |
| * The driver may have modified the IEs we passed to CMD_CONNECT |
| * before sending them out, the actual IE sent is reflected in the |
| * ATTR_REQ_IE sequence. These are the values EAPoL will need to use. |
| */ |
| ie_tlv_iter_init(&iter, ies, ies_len); |
| |
| while (ie_tlv_iter_next(&iter)) { |
| data = ie_tlv_iter_get_data(&iter); |
| |
| switch (ie_tlv_iter_get_tag(&iter)) { |
| case IE_TYPE_RSN: |
| handshake_state_set_supplicant_ie(netdev->handshake, |
| data - 2); |
| break; |
| case IE_TYPE_RSNX: |
| if (!rsnxe) |
| rsnxe = data - 2; |
| break; |
| case IE_TYPE_VENDOR_SPECIFIC: |
| if (!is_ie_wpa_ie(data, ie_tlv_iter_get_length(&iter))) |
| break; |
| |
| handshake_state_set_supplicant_ie(netdev->handshake, |
| data - 2); |
| break; |
| case IE_TYPE_MOBILITY_DOMAIN: |
| handshake_state_set_mde(netdev->handshake, data - 2); |
| break; |
| } |
| } |
| |
| /* RSNXE element might be omitted when FTing */ |
| handshake_state_set_supplicant_rsnxe(netdev->handshake, rsnxe); |
| } |
| |
| static void netdev_driver_connected(struct netdev *netdev) |
| { |
| netdev->connected = true; |
| |
| /* |
| * We register the eapol state machine here, in case the PAE |
| * socket receives EAPoL packets before the nl80211 socket |
| * receives the connected event. The logical sequence of |
| * events can be reversed (e.g. connect_event, then PAE data) |
| * due to scheduling |
| */ |
| if (netdev->sm) |
| eapol_register(netdev->sm); |
| } |
| |
| static struct l_genl_msg *netdev_build_cmd_connect(struct netdev *netdev, |
| struct handshake_state *hs, |
| const uint8_t *prev_bssid) |
| { |
| struct netdev_handshake_state *nhs = |
| l_container_of(hs, struct netdev_handshake_state, super); |
| /* |
| * Choose Open system auth type if PMKSA caching is used for an SAE AKM: |
| * |
| * IEEE 802.11-2020 Table 9-151 |
| * - SAE authentication: |
| * 3 (SAE) for SAE Authentication |
| * 0 (open) for PMKSA caching |
| * - FT authentication over SAE: |
| * 3 (SAE) for FT Initial Mobility Domain Association |
| * 0 (open) for FT Initial Mobility Domain Association over |
| * PMKSA caching |
| */ |
| uint32_t auth_type = IE_AKM_IS_SAE(hs->akm_suite) && !hs->have_pmksa ? |
| NL80211_AUTHTYPE_SAE : |
| NL80211_AUTHTYPE_OPEN_SYSTEM; |
| enum mpdu_management_subtype subtype = prev_bssid ? |
| MPDU_MANAGEMENT_SUBTYPE_REASSOCIATION_REQUEST : |
| MPDU_MANAGEMENT_SUBTYPE_ASSOCIATION_REQUEST; |
| struct l_genl_msg *msg; |
| struct iovec iov[64]; |
| unsigned int n_iov = L_ARRAY_SIZE(iov); |
| unsigned int c_iov = 0; |
| bool is_rsn = hs->supplicant_ie != NULL; |
| uint8_t owe_dh_ie[5 + L_ECC_SCALAR_MAX_BYTES]; |
| size_t dh_ie_len; |
| |
| msg = l_genl_msg_new_sized(NL80211_CMD_CONNECT, 512); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_IFINDEX, 4, &netdev->index); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_WIPHY_FREQ, |
| 4, &netdev->frequency); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_MAC, ETH_ALEN, hs->aa); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_SSID, hs->ssid_len, hs->ssid); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_AUTH_TYPE, 4, &auth_type); |
| |
| switch (nhs->type) { |
| case CONNECTION_TYPE_SOFTMAC: |
| break; |
| case CONNECTION_TYPE_FULLMAC: |
| l_genl_msg_append_attr(msg, |
| NL80211_ATTR_EXTERNAL_AUTH_SUPPORT, 0, NULL); |
| break; |
| case CONNECTION_TYPE_SAE_OFFLOAD: |
| l_genl_msg_append_attr(msg, NL80211_ATTR_SAE_PASSWORD, |
| strlen(hs->passphrase), hs->passphrase); |
| break; |
| case CONNECTION_TYPE_PSK_OFFLOAD: |
| l_genl_msg_append_attr(msg, NL80211_ATTR_PMK, 32, hs->pmk); |
| break; |
| case CONNECTION_TYPE_8021X_OFFLOAD: |
| l_genl_msg_append_attr(msg, NL80211_ATTR_WANT_1X_4WAY_HS, |
| 0, NULL); |
| } |
| |
| if (prev_bssid) |
| l_genl_msg_append_attr(msg, NL80211_ATTR_PREV_BSSID, ETH_ALEN, |
| prev_bssid); |
| |
| if (netdev->privacy) |
| l_genl_msg_append_attr(msg, NL80211_ATTR_PRIVACY, 0, NULL); |
| |
| l_genl_msg_append_attr(msg, NL80211_ATTR_SOCKET_OWNER, 0, NULL); |
| |
| if (is_rsn) { |
| nl80211_append_rsn_attributes(msg, hs); |
| c_iov = iov_ie_append(iov, n_iov, c_iov, hs->supplicant_ie, |
| IE_LEN(hs->supplicant_ie)); |
| } |
| |
| if (is_rsn || hs->settings_8021x) { |
| l_genl_msg_append_attr(msg, NL80211_ATTR_CONTROL_PORT, |
| 0, NULL); |
| |
| if (netdev->pae_over_nl80211) |
| l_genl_msg_append_attr(msg, |
| NL80211_ATTR_CONTROL_PORT_OVER_NL80211, |
| 0, NULL); |
| } |
| |
| if (netdev->owe_sm) { |
| owe_build_dh_ie(netdev->owe_sm, owe_dh_ie, &dh_ie_len); |
| c_iov = iov_ie_append(iov, n_iov, c_iov, owe_dh_ie, dh_ie_len); |
| } |
| |
| c_iov = iov_ie_append(iov, n_iov, c_iov, hs->mde, IE_LEN(hs->mde)); |
| c_iov = netdev_populate_common_ies(netdev, hs, msg, iov, n_iov, c_iov); |
| |
| mpdu_sort_ies(subtype, iov, c_iov); |
| |
| if (c_iov) |
| l_genl_msg_append_attrv(msg, NL80211_ATTR_IE, iov, c_iov); |
| |
| return msg; |
| } |
| |
| static void netdev_cmd_connect_cb(struct l_genl_msg *msg, void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| |
| netdev->connect_cmd_id = 0; |
| |
| if (l_genl_msg_get_error(msg) >= 0) { |
| /* |
| * connected should be false if the connect event hasn't come |
| * in yet. i.e. the CMD_CONNECT ack arrived first (typical). |
| * Mark the connection as 'connected' |
| */ |
| if (!netdev->connected) |
| netdev_driver_connected(netdev); |
| |
| return; |
| } |
| |
| netdev_connect_failed(netdev, NETDEV_RESULT_ASSOCIATION_FAILED, |
| MMPDU_STATUS_CODE_UNSPECIFIED); |
| } |
| |
| static bool netdev_retry_owe(struct netdev *netdev) |
| { |
| struct l_genl_msg *connect_cmd; |
| |
| if (!owe_next_group(netdev->owe_sm)) |
| return false; |
| |
| if (netdev->event_filter) |
| netdev->event_filter(netdev, NETDEV_EVENT_ECC_GROUP_RETRY, |
| NULL, netdev->user_data); |
| |
| connect_cmd = netdev_build_cmd_connect(netdev, netdev->handshake, NULL); |
| |
| netdev->connect_cmd_id = l_genl_family_send(nl80211, connect_cmd, |
| netdev_cmd_connect_cb, netdev, |
| NULL); |
| |
| if (netdev->connect_cmd_id > 0) |
| return true; |
| |
| l_genl_msg_unref(connect_cmd); |
| return false; |
| } |
| |
| static void netdev_connect_event(struct l_genl_msg *msg, struct netdev *netdev) |
| { |
| struct l_genl_attr attr; |
| uint16_t type, len; |
| const void *data; |
| const uint16_t *status_code = NULL; |
| const uint8_t *ies = NULL; |
| size_t ies_len = 0; |
| struct ie_tlv_iter iter; |
| const uint8_t *resp_ies = NULL; |
| size_t resp_ies_len = 0; |
| struct handshake_state *hs = netdev->handshake; |
| bool timeout = false; |
| uint32_t timeout_reason = 0; |
| |
| l_debug(""); |
| |
| if (netdev->aborting) |
| return; |
| |
| if (netdev->ignore_connect_event) |
| return; |
| |
| l_debug("aborting and ignore_connect_event not set, proceed"); |
| |
| /* Work around mwifiex which sends a Connect Event prior to the Ack */ |
| if (netdev->connect_cmd_id) |
| netdev_driver_connected(netdev); |
| |
| if (!netdev->connected) { |
| l_warn("Unexpected connection related event -- " |
| "is another supplicant running?"); |
| return; |
| } |
| |
| if (!l_genl_attr_init(&attr, msg)) { |
| l_debug("attr init failed"); |
| goto error; |
| } |
| |
| while (l_genl_attr_next(&attr, &type, &len, &data)) { |
| switch (type) { |
| case NL80211_ATTR_TIMED_OUT: |
| timeout = true; |
| break; |
| case NL80211_ATTR_TIMEOUT_REASON: |
| if (len != 4) |
| break; |
| |
| timeout_reason = l_get_u32(data); |
| break; |
| case NL80211_ATTR_STATUS_CODE: |
| if (len == sizeof(uint16_t)) |
| status_code = data; |
| break; |
| case NL80211_ATTR_REQ_IE: |
| ies = data; |
| ies_len = len; |
| break; |
| case NL80211_ATTR_RESP_IE: |
| resp_ies = data; |
| resp_ies_len = len; |
| break; |
| } |
| } |
| |
| if (timeout) { |
| iwd_notice(IWD_NOTICE_CONNECT_TIMEOUT, "reason: %u", |
| timeout_reason); |
| goto error; |
| } |
| |
| if (netdev->expect_connect_failure) { |
| /* |
| * The kernel may think we are connected when we are actually |
| * expecting a failure here, e.g. if Authenticate/Associate had |
| * previously failed. If so we need to deauth to let the kernel |
| * know. |
| */ |
| if (status_code && *status_code == 0) |
| goto deauth; |
| else |
| goto error; |
| } |
| |
| l_debug("expect_connect_failure not set, proceed"); |
| |
| if (netdev->owe_sm && status_code && *status_code == |
| MMPDU_STATUS_CODE_UNSUPP_FINITE_CYCLIC_GROUP) { |
| if (!netdev_retry_owe(netdev)) |
| goto error; |
| |
| return; |
| } |
| |
| /* AP Rejected the authenticate / associate */ |
| if (!status_code || *status_code != 0) |
| goto error; |
| |
| if (!ies) |
| goto process_resp_ies; |
| |
| parse_request_ies(netdev, ies, ies_len); |
| |
| process_resp_ies: |
| if (resp_ies) { |
| const uint8_t *fte = NULL; |
| const uint8_t *qos_set = NULL; |
| const uint8_t *owe_dh = NULL; |
| size_t owe_dh_len = 0; |
| size_t qos_len = 0; |
| struct ie_ft_info ft_info; |
| struct ie_rsn_info info; |
| bool owe_akm_found = false; |
| |
| ie_tlv_iter_init(&iter, resp_ies, resp_ies_len); |
| |
| while (ie_tlv_iter_next(&iter)) { |
| data = ie_tlv_iter_get_data(&iter); |
| |
| switch (ie_tlv_iter_get_tag(&iter)) { |
| case IE_TYPE_FAST_BSS_TRANSITION: |
| fte = data - 2; |
| break; |
| case IE_TYPE_QOS_MAP_SET: |
| qos_set = data; |
| qos_len = ie_tlv_iter_get_length(&iter); |
| break; |
| case IE_TYPE_FILS_IP_ADDRESS: |
| if (hs->fils_ip_resp_ie) { |
| l_debug("Duplicate response FILS IP " |
| "Address Assignment IE"); |
| l_free(hs->fils_ip_resp_ie); |
| } |
| |
| hs->fils_ip_resp_ie = l_memdup(data - 3, |
| ie_tlv_iter_get_length(&iter) + 3); |
| break; |
| case IE_TYPE_OWE_DH_PARAM: |
| if (!netdev->owe_sm) |
| continue; |
| |
| owe_dh = data; |
| owe_dh_len = ie_tlv_iter_get_length(&iter); |
| |
| break; |
| |
| case IE_TYPE_RSN: |
| if (!netdev->owe_sm) |
| continue; |
| |
| if (ie_parse_rsne(&iter, &info) < 0) { |
| l_error("could not parse RSN IE"); |
| goto deauth; |
| } |
| |
| /* |
| * RFC 8110 Section 4.2 |
| * An AP agreeing to do OWE MUST include the |
| * OWE AKM in the RSN element portion of the |
| * 802.11 association response. |
| */ |
| if (info.akm_suites != IE_RSN_AKM_SUITE_OWE) { |
| l_error("OWE AKM not included"); |
| goto deauth; |
| } |
| |
| owe_akm_found = true; |
| |
| break; |
| } |
| } |
| |
| if (netdev->owe_sm) { |
| if (!owe_dh) { |
| l_error("OWE DH element not found"); |
| goto deauth; |
| } |
| |
| if (!owe_akm_found) |
| l_warn("OWE AKM was not included in the RSNE. " |
| "This AP is out of spec!"); |
| |
| if (L_WARN_ON(owe_process_dh_ie(netdev->owe_sm, owe_dh, |
| owe_dh_len) != 0)) |
| goto deauth; |
| |
| owe_sm_free(netdev->owe_sm); |
| netdev->owe_sm = NULL; |
| } |
| |
| if (fte) { |
| uint32_t kck_len = |
| handshake_state_get_kck_len(hs); |
| /* |
| * If we are here, then most likely we have a FullMac |
| * hw performing initial mobility association. We need |
| * to set the FTE element or the handshake will fail |
| * The firmware accepted the FTE element, so do not |
| * sanitize the contents and just assume they're okay. |
| */ |
| if (ie_parse_fast_bss_transition_from_data(fte, |
| fte[1] + 2, kck_len, &ft_info) >= 0) { |
| handshake_state_set_authenticator_fte(hs, fte); |
| handshake_state_set_kh_ids(hs, |
| ft_info.r0khid, |
| ft_info.r0khid_len, |
| ft_info.r1khid); |
| } else { |
| l_info("CMD_CONNECT Succeeded, but parsing FTE" |
| " failed. Expect handshake failure"); |
| } |
| } |
| |
| if (qos_set) |
| netdev_send_qos_map_set(netdev, qos_set, qos_len); |
| } |
| |
| l_debug("Request / Response IEs parsed"); |
| |
| if (netdev->sm) { |
| if (!hs->chandef) { |
| if (netdev_get_oci(netdev) < 0) |
| goto deauth; |
| } else if (!eapol_start(netdev->sm)) |
| goto deauth; |
| |
| return; |
| } |
| |
| /* Allow station to sync the PSK to disk */ |
| if (is_offload(hs) && handshake_event(hs, HANDSHAKE_EVENT_SETTING_KEYS)) |
| return; |
| |
| netdev_connect_ok(netdev); |
| return; |
| |
| error: |
| netdev_connect_failed(netdev, NETDEV_RESULT_ASSOCIATION_FAILED, |
| (status_code) ? *status_code : |
| MMPDU_STATUS_CODE_UNSPECIFIED); |
| return; |
| |
| deauth: |
| netdev_disconnect_and_fail_connection(netdev, |
| NETDEV_RESULT_ASSOCIATION_FAILED, |
| MMPDU_STATUS_CODE_UNSPECIFIED); |
| } |
| |
| static struct l_genl_msg *netdev_build_cmd_associate_common( |
| struct netdev *netdev) |
| { |
| struct handshake_state *hs = netdev->handshake; |
| bool is_rsn = hs->supplicant_ie != NULL; |
| struct l_genl_msg *msg; |
| |
| msg = l_genl_msg_new_sized(NL80211_CMD_ASSOCIATE, 600); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_IFINDEX, 4, &netdev->index); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_WIPHY_FREQ, 4, |
| &netdev->frequency); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_MAC, ETH_ALEN, hs->aa); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_SSID, hs->ssid_len, hs->ssid); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_SOCKET_OWNER, 0, NULL); |
| |
| if (is_rsn) |
| nl80211_append_rsn_attributes(msg, hs); |
| |
| if (is_rsn || hs->settings_8021x) { |
| l_genl_msg_append_attr(msg, NL80211_ATTR_CONTROL_PORT, |
| 0, NULL); |
| |
| if (netdev->pae_over_nl80211) |
| l_genl_msg_append_attr(msg, |
| NL80211_ATTR_CONTROL_PORT_OVER_NL80211, |
| 0, NULL); |
| } |
| |
| return msg; |
| } |
| |
| static void netdev_cmd_ft_reassociate_cb(struct l_genl_msg *msg, |
| void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| int err = l_genl_msg_get_error(msg); |
| |
| netdev->connect_cmd_id = 0; |
| |
| l_debug("%d", err); |
| |
| if (err >= 0) |
| return; |
| |
| /* |
| * TODO: It is possible to not trigger a disconnect here and maintain |
| * the current connection. The issue is that IWD has already |
| * modified the handshake and we've lost all reference to the old |
| * BSS keys. |
| * |
| * This could be remedied in the future by creating an entirely |
| * new handshake_state object for the association and only when |
| * the ack indicates success do we clear out the old object. |
| */ |
| netdev_disconnect_and_fail_connection(netdev, |
| NETDEV_RESULT_ASSOCIATION_FAILED, |
| MMPDU_STATUS_CODE_UNSPECIFIED); |
| } |
| |
| static bool kernel_will_retry_auth(uint16_t status_code, |
| uint16_t alg, uint16_t trans) |
| { |
| /* |
| * Kernel keeps re-trying auth frames until told to stop |
| * when authentication succeeds and under certain SAE-related |
| * circumstances. Detect these cases. |
| */ |
| |
| if (status_code == 0) |
| return true; |
| |
| if (alg != MMPDU_AUTH_ALGO_SAE) |
| return false; |
| |
| if (status_code == MMPDU_STATUS_CODE_ANTI_CLOGGING_TOKEN_REQ) |
| return true; |
| |
| if (trans == 1 && (status_code == MMPDU_STATUS_CODE_SAE_PK || |
| status_code == MMPDU_STATUS_CODE_SAE_HASH_TO_ELEMENT)) |
| return true; |
| |
| return false; |
| } |
| |
| static void netdev_ensure_eapol_registered(struct netdev *netdev) |
| { |
| if (netdev->sm) |
| return; |
| |
| netdev->sm = eapol_sm_new(netdev->handshake); |
| eapol_register(netdev->sm); |
| } |
| |
| static void netdev_authenticate_event(struct l_genl_msg *msg, |
| struct netdev *netdev) |
| { |
| struct l_genl_attr attr; |
| uint16_t type, len; |
| const void *data; |
| const uint8_t *frame = NULL; |
| size_t frame_len = 0; |
| int ret; |
| uint16_t status_code = MMPDU_STATUS_CODE_UNSPECIFIED; |
| |
| l_debug(""); |
| |
| if (netdev->aborting) |
| return; |
| |
| if (!netdev->connected) { |
| l_warn("Unexpected connection related event -- " |
| "is another supplicant running?"); |
| return; |
| } |
| |
| if (netdev->event_filter) |
| netdev->event_filter(netdev, NETDEV_EVENT_AUTHENTICATING, |
| NULL, netdev->user_data); |
| |
| /* |
| * During Fast Transition we use the authenticate event to start the |
| * reassociation step because the FTE necessary before we can build |
| * the FT Associate command is included in the attached frame and is |
| * not available in the Authenticate command callback. |
| */ |
| if (!netdev->ap) |
| return; |
| |
| if (!l_genl_attr_init(&attr, msg)) { |
| l_debug("attr init failed"); |
| |
| goto auth_error; |
| } |
| |
| while (l_genl_attr_next(&attr, &type, &len, &data)) { |
| switch (type) { |
| case NL80211_ATTR_TIMED_OUT: |
| iwd_notice(IWD_NOTICE_AUTH_TIMEOUT); |
| |
| if (auth_proto_auth_timeout(netdev->ap)) |
| return; |
| |
| goto auth_error; |
| |
| case NL80211_ATTR_FRAME: |
| if (frame) |
| goto auth_error; |
| |
| frame = data; |
| frame_len = len; |
| break; |
| } |
| } |
| |
| if (L_WARN_ON(!frame)) |
| goto auth_error; |
| |
| if (netdev->ap) { |
| const struct mmpdu_header *hdr; |
| const struct mmpdu_authentication *auth; |
| bool retry; |
| |
| hdr = mpdu_validate(frame, frame_len); |
| if (L_WARN_ON(!hdr)) |
| goto auth_error; |
| |
| auth = mmpdu_body(hdr); |
| status_code = L_CPU_TO_LE16(auth->status); |
| |
| ret = auth_proto_rx_authenticate(netdev->ap, frame, frame_len); |
| |
| /* |
| * Allows station to persist settings so it does not retry |
| * the higher order ECC group again |
| */ |
| if (status_code == |
| MMPDU_STATUS_CODE_UNSUPP_FINITE_CYCLIC_GROUP && |
| netdev->event_filter) |
| netdev->event_filter(netdev, |
| NETDEV_EVENT_ECC_GROUP_RETRY, |
| NULL, netdev->user_data); |
| |
| /* We have sent another CMD_AUTHENTICATE / CMD_ASSOCIATE */ |
| if (ret == 0) { |
| netdev_ensure_eapol_registered(netdev); |
| return; |
| } |
| |
| if (ret == -EAGAIN) |
| return; |
| |
| retry = kernel_will_retry_auth(status_code, |
| L_CPU_TO_LE16(auth->algorithm), |
| L_CPU_TO_LE16(auth->transaction_sequence)); |
| |
| /* |
| * Spec wants us to silently drop these frames, |
| * if the kernel will keep retrying, let it |
| */ |
| if ((ret == -ENOMSG || ret == -EBADMSG) && retry) |
| return; |
| |
| if (ret > 0) |
| status_code = (uint16_t)ret; |
| |
| /* |
| * We have encountered a fatal error, if the kernel wants |
| * to keep retrying, tell it to stop |
| */ |
| if (retry) { |
| netdev_deauth_and_fail_connection(netdev, |
| NETDEV_RESULT_ASSOCIATION_FAILED, |
| MMPDU_STATUS_CODE_UNSPECIFIED); |
| return; |
| } |
| } |
| |
| auth_error: |
| netdev_connect_failed(netdev, NETDEV_RESULT_AUTHENTICATION_FAILED, |
| status_code); |
| } |
| |
| static void netdev_associate_event(struct l_genl_msg *msg, |
| struct netdev *netdev) |
| { |
| struct l_genl_attr attr; |
| uint16_t type, len; |
| const void *data; |
| size_t frame_len = 0; |
| const uint8_t *frame = NULL; |
| uint16_t status_code = MMPDU_STATUS_CODE_UNSPECIFIED; |
| int ret; |
| const struct mmpdu_header *hdr; |
| const struct mmpdu_association_response *assoc; |
| |
| l_debug(""); |
| |
| if (!netdev->connected || netdev->aborting) |
| return; |
| |
| if (netdev->event_filter) |
| netdev->event_filter(netdev, NETDEV_EVENT_ASSOCIATING, |
| NULL, netdev->user_data); |
| |
| if (!netdev->ap && !netdev->in_ft) { |
| netdev->associated = true; |
| netdev->in_reassoc = false; |
| return; |
| } |
| |
| if (!l_genl_attr_init(&attr, msg)) { |
| l_debug("attr init failed"); |
| return; |
| } |
| |
| while (l_genl_attr_next(&attr, &type, &len, &data)) { |
| switch (type) { |
| case NL80211_ATTR_TIMED_OUT: |
| iwd_notice(IWD_NOTICE_ASSOC_TIMEOUT); |
| |
| if (auth_proto_assoc_timeout(netdev->ap)) |
| return; |
| |
| /* |
| * There will be no connect event when Associate times |
| * out. The failed connection must be explicitly |
| * initiated here. |
| */ |
| netdev_connect_failed(netdev, |
| NETDEV_RESULT_ASSOCIATION_FAILED, |
| status_code); |
| return; |
| |
| case NL80211_ATTR_FRAME: |
| frame = data; |
| frame_len = len; |
| |
| break; |
| } |
| } |
| |
| if (L_WARN_ON(!frame)) |
| goto assoc_failed; |
| |
| hdr = mpdu_validate(frame, frame_len); |
| if (L_WARN_ON(!hdr)) |
| goto assoc_failed; |
| |
| assoc = mmpdu_body(hdr); |
| status_code = L_CPU_TO_LE16(assoc->status_code); |
| |
| if (netdev->ap) |
| ret = auth_proto_rx_associate(netdev->ap, frame, |
| frame_len); |
| else |
| ret = __ft_rx_associate(netdev->index, frame, |
| frame_len); |
| if (ret == 0) { |
| bool fils = !!(netdev->handshake->akm_suite & |
| (IE_RSN_AKM_SUITE_FILS_SHA256 | |
| IE_RSN_AKM_SUITE_FILS_SHA384 | |
| IE_RSN_AKM_SUITE_FT_OVER_FILS_SHA384 | |
| IE_RSN_AKM_SUITE_FT_OVER_FILS_SHA256)); |
| |
| if (netdev->ap) { |
| auth_proto_free(netdev->ap); |
| netdev->ap = NULL; |
| } |
| |
| /* Just in case this was a retry */ |
| netdev->ignore_connect_event = false; |
| |
| /* |
| * If in FT and/or FILS we don't force an initial 4-way |
| * handshake and instead just keep the EAPoL state |
| * machine for the rekeys. |
| */ |
| if (netdev->in_ft || fils) |
| eapol_sm_set_require_handshake(netdev->sm, |
| false); |
| |
| netdev->in_reassoc = false; |
| netdev->associated = true; |
| return; |
| } else if (ret == -EAGAIN) { |
| /* |
| * Here to support OWE retries. OWE will retry |
| * internally, but a connect event will still be emitted |
| */ |
| netdev->ignore_connect_event = true; |
| return; |
| } else if (ret > 0) |
| status_code = (uint16_t)ret; |
| |
| assoc_failed: |
| netdev->result = NETDEV_RESULT_ASSOCIATION_FAILED; |
| netdev->last_code = status_code; |
| netdev->expect_connect_failure = true; |
| } |
| |
| static struct l_genl_msg *netdev_build_cmd_authenticate(struct netdev *netdev, |
| uint32_t auth_type) |
| { |
| struct handshake_state *hs = netdev->handshake; |
| struct l_genl_msg *msg; |
| |
| msg = l_genl_msg_new_sized(NL80211_CMD_AUTHENTICATE, 512); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_IFINDEX, 4, &netdev->index); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_WIPHY_FREQ, |
| 4, &netdev->frequency); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_MAC, ETH_ALEN, |
| netdev->handshake->aa); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_SSID, hs->ssid_len, hs->ssid); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_AUTH_TYPE, 4, &auth_type); |
| |
| return msg; |
| } |
| |
| static void netdev_scan_cb(struct l_genl_msg *msg, void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| |
| if (l_genl_msg_get_error(msg) < 0) { |
| netdev_connect_failed(netdev, |
| NETDEV_RESULT_AUTHENTICATION_FAILED, |
| MMPDU_STATUS_CODE_UNSPECIFIED); |
| return; |
| } |
| |
| netdev->retry_auth = true; |
| } |
| |
| static void netdev_auth_cb(struct l_genl_msg *msg, void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| struct handshake_state *hs = netdev->handshake; |
| int err = l_genl_msg_get_error(msg); |
| struct l_genl_msg *scan_msg; |
| |
| if (!err) { |
| l_genl_msg_unref(netdev->auth_cmd); |
| netdev->auth_cmd = NULL; |
| return; |
| } |
| |
| l_debug("Error during auth: %d", err); |
| |
| if (!netdev->auth_cmd || err != -ENOENT) |
| goto failed; |
| |
| /* Kernel can't find the BSS in its cache, scan and retry */ |
| scan_msg = scan_build_trigger_scan_bss(netdev->index, netdev->wiphy, |
| netdev->frequency, |
| hs->ssid, hs->ssid_len); |
| |
| if (l_genl_family_send(nl80211, scan_msg, |
| netdev_scan_cb, netdev, NULL) > 0) |
| return; |
| |
| l_genl_msg_unref(scan_msg); |
| failed: |
| netdev_connect_failed(netdev, NETDEV_RESULT_AUTHENTICATION_FAILED, |
| MMPDU_STATUS_CODE_UNSPECIFIED); |
| } |
| |
| static void netdev_new_scan_results_event(struct l_genl_msg *msg, |
| struct netdev *netdev) |
| { |
| if (!netdev->retry_auth) |
| return; |
| |
| l_debug(""); |
| |
| if (!l_genl_family_send(nl80211, netdev->auth_cmd, |
| netdev_auth_cb, netdev, NULL)) { |
| netdev_connect_failed(netdev, |
| NETDEV_RESULT_AUTHENTICATION_FAILED, |
| MMPDU_STATUS_CODE_UNSPECIFIED); |
| return; |
| } |
| |
| netdev->auth_cmd = NULL; |
| } |
| |
| static void netdev_assoc_cb(struct l_genl_msg *msg, void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| int err = l_genl_msg_get_error(msg); |
| |
| if (err < 0) { |
| l_error("Error sending CMD_ASSOCIATE (%d)", err); |
| |
| netdev_connect_failed(netdev, NETDEV_RESULT_ASSOCIATION_FAILED, |
| MMPDU_STATUS_CODE_UNSPECIFIED); |
| } |
| } |
| |
| static void netdev_sae_tx_authenticate(const uint8_t *body, |
| size_t body_len, void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| struct l_genl_msg *msg; |
| |
| msg = netdev_build_cmd_authenticate(netdev, NL80211_AUTHTYPE_SAE); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_AUTH_DATA, body_len, body); |
| |
| if (!l_genl_family_send(nl80211, msg, netdev_auth_cb, netdev, NULL)) { |
| l_genl_msg_unref(msg); |
| netdev_connect_failed(netdev, |
| NETDEV_RESULT_AUTHENTICATION_FAILED, |
| MMPDU_STATUS_CODE_UNSPECIFIED); |
| return; |
| } |
| |
| /* |
| * Sometimes due to the way the scheduling works out, netdev_auth_cb |
| * is sent after the SAE Authentication reply from the AP arrives. |
| * Do not leak auth_cmd if this occurs. Note that if auth_cmd is not |
| * NULL and we are here, there's no further reason to save off auth_cmd. |
| * This is done only if the kernel's cache lacks the BSS we are trying |
| * to communicate with. |
| */ |
| if (netdev->auth_cmd) { |
| l_genl_msg_unref(netdev->auth_cmd); |
| netdev->auth_cmd = NULL; |
| } else |
| netdev->auth_cmd = l_genl_msg_ref(msg); |
| } |
| |
| static void netdev_sae_tx_associate(void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| struct handshake_state *hs = netdev->handshake; |
| struct l_genl_msg *msg; |
| struct iovec iov[64]; |
| unsigned int n_iov = L_ARRAY_SIZE(iov); |
| unsigned int n_used = 0; |
| enum mpdu_management_subtype subtype = |
| MPDU_MANAGEMENT_SUBTYPE_ASSOCIATION_REQUEST; |
| |
| msg = netdev_build_cmd_associate_common(netdev); |
| |
| n_used = iov_ie_append(iov, n_iov, n_used, hs->supplicant_ie, |
| IE_LEN(hs->supplicant_ie)); |
| n_used = iov_ie_append(iov, n_iov, n_used, hs->mde, IE_LEN(hs->mde)); |
| n_used = iov_ie_append(iov, n_iov, n_used, hs->supplicant_rsnxe, |
| IE_LEN(hs->supplicant_rsnxe)); |
| n_used = netdev_populate_common_ies(netdev, hs, msg, |
| iov, n_iov, n_used); |
| mpdu_sort_ies(subtype, iov, n_used); |
| |
| l_genl_msg_append_attrv(msg, NL80211_ATTR_IE, iov, n_used); |
| |
| /* If doing a non-FT Reassociation */ |
| if (netdev->in_reassoc) |
| l_genl_msg_append_attr(msg, NL80211_ATTR_PREV_BSSID, 6, |
| netdev->ap->prev_bssid); |
| |
| if (!l_genl_family_send(nl80211, msg, netdev_assoc_cb, netdev, NULL)) { |
| l_genl_msg_unref(msg); |
| netdev_connect_failed(netdev, NETDEV_RESULT_ASSOCIATION_FAILED, |
| MMPDU_STATUS_CODE_UNSPECIFIED); |
| } |
| } |
| |
| static void netdev_fils_tx_authenticate(const uint8_t *body, |
| size_t body_len, |
| void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| struct l_genl_msg *msg; |
| |
| msg = netdev_build_cmd_authenticate(netdev, NL80211_AUTHTYPE_FILS_SK); |
| |
| l_genl_msg_append_attr(msg, NL80211_ATTR_AUTH_DATA, body_len, body); |
| |
| if (!l_genl_family_send(nl80211, msg, netdev_auth_cb, |
| netdev, NULL)) { |
| l_genl_msg_unref(msg); |
| netdev_connect_failed(netdev, |
| NETDEV_RESULT_AUTHENTICATION_FAILED, |
| MMPDU_STATUS_CODE_UNSPECIFIED); |
| return; |
| } |
| |
| /* See comment in netdev_sae_tx_authenticate */ |
| if (netdev->auth_cmd) { |
| l_genl_msg_unref(netdev->auth_cmd); |
| netdev->auth_cmd = NULL; |
| } else |
| netdev->auth_cmd = l_genl_msg_ref(msg); |
| } |
| |
| static void netdev_fils_tx_associate(struct iovec *fils_iov, size_t n_fils_iov, |
| const uint8_t *kek, size_t kek_len, |
| const uint8_t *nonces, |
| size_t nonces_len, |
| void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| struct handshake_state *hs = netdev->handshake; |
| struct l_genl_msg *msg; |
| struct iovec iov[64]; |
| unsigned int n_iov = L_ARRAY_SIZE(iov); |
| unsigned int c_iov = 0; |
| enum mpdu_management_subtype subtype = |
| MPDU_MANAGEMENT_SUBTYPE_ASSOCIATION_REQUEST; |
| |
| msg = netdev_build_cmd_associate_common(netdev); |
| c_iov = netdev_populate_common_ies(netdev, hs, msg, iov, n_iov, c_iov); |
| |
| if (!L_WARN_ON(n_iov - c_iov < n_fils_iov)) { |
| memcpy(iov + c_iov, fils_iov, sizeof(*fils_iov) * n_fils_iov); |
| c_iov += n_fils_iov; |
| } |
| |
| mpdu_sort_ies(subtype, iov, c_iov); |
| |
| l_genl_msg_append_attrv(msg, NL80211_ATTR_IE, iov, c_iov); |
| |
| l_genl_msg_append_attr(msg, NL80211_ATTR_FILS_KEK, kek_len, kek); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_FILS_NONCES, |
| nonces_len, nonces); |
| |
| /* If doing a non-FT Reassociation */ |
| if (netdev->in_reassoc) |
| l_genl_msg_append_attr(msg, NL80211_ATTR_PREV_BSSID, 6, |
| netdev->ap->prev_bssid); |
| |
| if (!l_genl_family_send(nl80211, msg, netdev_assoc_cb, |
| netdev, NULL)) { |
| l_genl_msg_unref(msg); |
| netdev_connect_failed(netdev, NETDEV_RESULT_ASSOCIATION_FAILED, |
| MMPDU_STATUS_CODE_UNSPECIFIED); |
| } |
| } |
| |
| static void netdev_external_auth_frame_cb(struct l_genl_msg *msg, |
| void *user_data) |
| { |
| int error = l_genl_msg_get_error(msg); |
| |
| if (error < 0) |
| l_debug("Failed to send External Auth Frame: %s(%d)", |
| strerror(-error), -error); |
| } |
| |
| static void netdev_external_auth_sae_tx_authenticate(const uint8_t *body, |
| size_t body_len, void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| struct handshake_state *hs = netdev->handshake; |
| uint16_t frame_type = MPDU_MANAGEMENT_SUBTYPE_AUTHENTICATION << 4; |
| struct iovec iov[2]; |
| struct l_genl_msg *msg; |
| uint8_t algorithm[2] = { 0x03, 0x00 }; |
| |
| l_debug(""); |
| |
| iov[0].iov_base = &algorithm; |
| iov[0].iov_len = sizeof(algorithm); |
| iov[1].iov_base = (void *) body; |
| iov[1].iov_len = body_len; |
| |
| msg = nl80211_build_cmd_frame(netdev->index, frame_type, |
| hs->spa, hs->aa, 0, iov, 2); |
| |
| if (l_genl_family_send(nl80211, msg, netdev_external_auth_frame_cb, |
| netdev, NULL) > 0) |
| return; |
| |
| l_genl_msg_unref(msg); |
| } |
| |
| static void netdev_external_auth_cb(struct l_genl_msg *msg, void *user_data) |
| { |
| int error = l_genl_msg_get_error(msg); |
| |
| if (error < 0) |
| l_debug("Failed to send External Auth: %s(%d)", |
| strerror(-error), -error); |
| } |
| |
| static void netdev_send_external_auth(struct netdev *netdev, |
| uint16_t status_code) |
| { |
| struct handshake_state *hs = netdev->handshake; |
| struct l_genl_msg *msg = |
| nl80211_build_external_auth(netdev->index, status_code, |
| hs->ssid, hs->ssid_len, hs->aa); |
| |
| if (l_genl_family_send(nl80211, msg, netdev_external_auth_cb, |
| netdev, NULL) > 0) |
| return; |
| |
| l_genl_msg_unref(msg); |
| } |
| |
| static void netdev_external_auth_sae_tx_associate(void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| |
| l_debug(""); |
| |
| netdev_send_external_auth(netdev, MMPDU_STATUS_CODE_SUCCESS); |
| netdev_ensure_eapol_registered(netdev); |
| |
| /* |
| * Free the auth proto now. With external auth there is no associate |
| * event which is where this normally gets cleaned up. |
| */ |
| auth_proto_free(netdev->ap); |
| netdev->ap = NULL; |
| } |
| |
| struct rtnl_data { |
| struct netdev *netdev; |
| uint8_t addr[ETH_ALEN]; |
| int ref; |
| }; |
| |
| static int netdev_begin_connection(struct netdev *netdev) |
| { |
| struct netdev_handshake_state *nhs = |
| l_container_of(netdev->handshake, |
| struct netdev_handshake_state, super); |
| |
| if (netdev->connect_cmd) { |
| netdev->connect_cmd_id = l_genl_family_send(nl80211, |
| netdev->connect_cmd, |
| netdev_cmd_connect_cb, netdev, |
| NULL); |
| |
| if (!netdev->connect_cmd_id) |
| goto failed; |
| |
| netdev->connect_cmd = NULL; |
| } |
| |
| /* |
| * Set the supplicant address now, this may have already been done for |
| * a non-randomized address connect, but if we are randomizing we need |
| * to set it again as the address should have now changed. |
| */ |
| handshake_state_set_supplicant_address(netdev->handshake, netdev->addr); |
| |
| if (netdev->ap && nhs->type == CONNECTION_TYPE_SOFTMAC) { |
| if (!auth_proto_start(netdev->ap)) |
| goto failed; |
| |
| /* |
| * set connected since the auth protocols cannot do |
| * so internally |
| */ |
| netdev->connected = true; |
| } |
| |
| return 0; |
| |
| failed: |
| netdev_connect_failed(netdev, NETDEV_RESULT_ABORTED, |
| MMPDU_STATUS_CODE_UNSPECIFIED); |
| return -EIO; |
| } |
| |
| static void netdev_mac_change_failed(struct netdev *netdev, int error) |
| { |
| l_error("Error setting mac address on %d: %s", netdev->index, |
| strerror(-error)); |
| |
| /* |
| * If the interface is down and we failed to up it we need to notify |
| * any watchers since we have been skipping the notification while |
| * mac_change_cmd_id was set. |
| */ |
| if (!netdev_get_is_up(netdev)) { |
| WATCHLIST_NOTIFY(&netdev_watches, netdev_watch_func_t, |
| netdev, NETDEV_WATCH_EVENT_DOWN); |
| |
| netdev_connect_failed(netdev, NETDEV_RESULT_ABORTED, |
| MMPDU_STATUS_CODE_UNSPECIFIED); |
| return; |
| } |
| |
| /* If the interface is up we can still try and connect */ |
| l_info("Failed to change the MAC, continuing with connection"); |
| |
| if (netdev_begin_connection(netdev) < 0) |
| l_error("netdev_begin_connection() error in mac_change_failed"); |
| } |
| |
| static void netdev_mac_destroy(void *user_data) |
| { |
| struct rtnl_data *req = user_data; |
| |
| req->ref--; |
| |
| /* still pending requests? */ |
| if (req->ref) |
| return; |
| |
| l_free(req); |
| } |
| |
| static void netdev_mac_power_up_cb(int error, uint16_t type, |
| const void *data, uint32_t len, |
| void *user_data) |
| { |
| struct rtnl_data *req = user_data; |
| struct netdev *netdev = req->netdev; |
| |
| netdev->mac_change_cmd_id = 0; |
| |
| if (error) { |
| l_error("Error changing per-network MAC on interface %u: %s", |
| netdev->index, strerror(-error)); |
| netdev_mac_change_failed(netdev, error); |
| return; |
| } |
| |
| /* Pick up where we left off in netdev_connect_commmon */ |
| if (netdev_begin_connection(netdev) < 0) |
| l_error("netdev_begin_connection() error in mac_power_up_cb"); |
| } |
| |
| static void netdev_mac_power_down_cb(int error, uint16_t type, |
| const void *data, uint32_t len, |
| void *user_data) |
| { |
| struct rtnl_data *req = user_data; |
| struct netdev *netdev = req->netdev; |
| |
| netdev->mac_change_cmd_id = 0; |
| |
| if (error) { |
| l_error("Error taking interface %u down for per-network MAC " |
| "generation: %s", netdev->index, strerror(-error)); |
| netdev_mac_change_failed(netdev, error); |
| return; |
| } |
| |
| netdev->mac_change_cmd_id = l_rtnl_set_mac(rtnl, netdev->index, |
| req->addr, true, |
| netdev_mac_power_up_cb, req, |
| netdev_mac_destroy); |
| if (!netdev->mac_change_cmd_id) { |
| netdev_mac_change_failed(netdev, -EIO); |
| return; |
| } |
| |
| req->ref++; |
| } |
| |
| /* |
| * TODO: There are some potential race conditions that are being ignored. There |
| * is nothing that IWD itself can do to solve these, they require kernel |
| * changes: |
| * |
| * 1. A perfectly timed ifdown could be ignored. If an external process |
| * brings down an interface just before calling this function we would only |
| * get a single newlink event since there is no state change doing a second |
| * ifdown (nor an error from the kernel). This newlink event would be ignored |
| * since IWD thinks its from our own doing. This would result in IWD changing |
| * the MAC and bringing the interface back up which would look very strange |
| * and unexpected to someone who just tried to ifdown an interface. |
| * |
| * 2. A perfectly timed ifup could result in a failed connection. If an external |
| * process ifup's just after IWD ifdown's but before changing the MAC this |
| * would cause the MAC change to fail. This failure would result in a failed |
| * connection. |
| * |
| * Returns 0 if a MAC change procedure was started. |
| * Returns -EALREADY if the requested MAC matched our current MAC |
| * Returns -EIO if there was an IO error when powering down |
| */ |
| static int netdev_start_powered_mac_change(struct netdev *netdev) |
| { |
| struct rtnl_data *req; |
| uint8_t new_addr[6]; |
| bool powered = wiphy_has_ext_feature(netdev->wiphy, |
| NL80211_EXT_FEATURE_POWERED_ADDR_CHANGE); |
| |
| /* No address set in handshake, use per-network MAC generation */ |
| if (l_memeqzero(netdev->handshake->spa, ETH_ALEN)) |
| wiphy_generate_address_from_ssid(netdev->wiphy, |
| netdev->handshake->ssid, |
| netdev->handshake->ssid_len, |
| new_addr); |
| else |
| memcpy(new_addr, netdev->handshake->spa, ETH_ALEN); |
| |
| /* |
| * MAC has already been changed previously, no need to again |
| */ |
| if (!memcmp(new_addr, netdev->addr, sizeof(new_addr))) |
| return -EALREADY; |
| |
| req = l_new(struct rtnl_data, 1); |
| req->netdev = netdev; |
| /* This message will need to be unreffed upon any error */ |
| req->ref++; |
| memcpy(req->addr, new_addr, sizeof(req->addr)); |
| |
| if (powered) |
| netdev->mac_change_cmd_id = l_rtnl_set_mac(rtnl, netdev->index, |
| req->addr, false, |
| netdev_mac_power_up_cb, req, |
| netdev_mac_destroy); |
| else |
| netdev->mac_change_cmd_id = l_rtnl_set_powered(rtnl, |
| netdev->index, false, |
| netdev_mac_power_down_cb, req, |
| netdev_mac_destroy); |
| |
| if (!netdev->mac_change_cmd_id) { |
| l_free(req); |
| |
| return -EIO; |
| } |
| |
| l_debug("Setting generated address on ifindex: %d to: "MAC" (%s)", |
| netdev->index, MAC_STR(req->addr), |
| powered ? "powered" : "power-down"); |
| |
| return 0; |
| } |
| |
| static struct l_genl_msg *netdev_build_cmd_cqm_rssi_update( |
| struct netdev *netdev, |
| const int8_t *levels, |
| size_t levels_num) |
| { |
| struct l_genl_msg *msg; |
| uint32_t hyst = 5; |
| int thold_count; |
| int32_t thold_list[levels_num + 2]; |
| int threshold = netdev->frequency > 4000 ? |
| netdev->low_signal_threshold_5ghz : |
| netdev->low_signal_threshold; |
| |
| if (levels_num == 0) { |
| thold_list[0] = threshold; |
| thold_count = 1; |
| } else { |
| /* |
| * Build the list of all the threshold values we care about: |
| * - the low/high level threshold, |
| * - the value ranges requested by |
| * netdev_set_rssi_report_levels |
| */ |
| unsigned int i; |
| bool low_sig_added = false; |
| |
| thold_count = 0; |
| for (i = 0; i < levels_num; i++) { |
| int32_t val = levels[levels_num - i - 1]; |
| |
| if (i && thold_list[thold_count - 1] >= val) |
| return NULL; |
| |
| if (val >= threshold && !low_sig_added) { |
| thold_list[thold_count++] = threshold; |
| low_sig_added = true; |
| |
| /* Duplicate values are not allowed */ |
| if (val == threshold) |
| continue; |
| } |
| |
| thold_list[thold_count++] = val; |
| } |
| } |
| |
| msg = l_genl_msg_new_sized(NL80211_CMD_SET_CQM, 32 + thold_count * 4); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_IFINDEX, 4, &netdev->index); |
| l_genl_msg_enter_nested(msg, NL80211_ATTR_CQM); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_CQM_RSSI_THOLD, |
| thold_count * 4, thold_list); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_CQM_RSSI_HYST, 4, &hyst); |
| l_genl_msg_leave_nested(msg); |
| |
| return msg; |
| } |
| |
| static void netdev_cmd_set_cqm_cb(struct l_genl_msg *msg, void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| int err = l_genl_msg_get_error(msg); |
| const char *ext_error; |
| |
| netdev->set_cqm_cmd_id = 0; |
| |
| if (err >= 0) { |
| /* |
| * Looking at some driver code it appears that the -ENOTSUP CQM |
| * failure could be transient. Just in case, reset the fallback |
| * flag if CQM happens to start working again. |
| */ |
| if (netdev->cqm_poll_fallback) { |
| l_debug("CMD_SET_CQM succeeded, stop polling fallback"); |
| |
| if (netdev->rssi_poll_timeout) { |
| l_timeout_remove(netdev->rssi_poll_timeout); |
| netdev->rssi_poll_timeout = NULL; |
| } |
| |
| netdev->cqm_poll_fallback = false; |
| } |
| |
| return; |
| } |
| |
| /* |
| * Some drivers enable beacon filtering but also use software CQM which |
| * mac80211 detects and returns -ENOTSUP. There is no way to check this |
| * ahead of time so if we see this start polling in order to get RSSI |
| * updates. |
| */ |
| if (err == -ENOTSUP) { |
| l_debug("CMD_SET_CQM not supported, falling back to polling"); |
| netdev->cqm_poll_fallback = true; |
| |
| if (netdev->rssi_poll_timeout) |
| return; |
| |
| netdev->rssi_poll_timeout = l_timeout_create(1, |
| netdev_rssi_poll, netdev, NULL); |
| |
| return; |
| } |
| |
| ext_error = l_genl_msg_get_extended_error(msg); |
| l_error("CMD_SET_CQM failed: %s", |
| ext_error ? ext_error : strerror(-err)); |
| } |
| |
| static int netdev_cqm_rssi_update(struct netdev *netdev) |
| { |
| struct l_genl_msg *msg; |
| struct netdev_handshake_state *nhs = l_container_of(netdev->handshake, |
| struct netdev_handshake_state, super); |
| |
| /* |
| * Fullmac cards handle roaming in firmware, there is no need to set |
| * CQM thresholds |
| */ |
| if (nhs->type == CONNECTION_TYPE_FULLMAC) |
| return 0; |
| |
| l_debug(""); |
| |
| if (netdev->set_cqm_cmd_id) |
| return -EBUSY; |
| |
| if (!wiphy_has_ext_feature(netdev->wiphy, |
| NL80211_EXT_FEATURE_CQM_RSSI_LIST)) |
| msg = netdev_build_cmd_cqm_rssi_update(netdev, NULL, 0); |
| else |
| msg = netdev_build_cmd_cqm_rssi_update(netdev, |
| netdev->rssi_levels, |
| netdev->rssi_levels_num); |
| if (!msg) |
| return -EINVAL; |
| |
| netdev->set_cqm_cmd_id = l_genl_family_send(nl80211, msg, |
| netdev_cmd_set_cqm_cb, |
| netdev, NULL); |
| if (!netdev->set_cqm_cmd_id) { |
| l_genl_msg_unref(msg); |
| return -EIO; |
| } |
| |
| return 0; |
| } |
| |
| static int netdev_set_signal_thresholds(struct netdev *netdev, int threshold, |
| int threshold_5ghz) |
| { |
| int current = netdev->frequency > 4000 ? |
| netdev->low_signal_threshold_5ghz : |
| netdev->low_signal_threshold; |
| int new = netdev->frequency > 4000 ? threshold_5ghz : threshold; |
| |
| if (current == new) |
| return -EALREADY; |
| |
| l_debug("changing low signal threshold to %d", new); |
| |
| netdev->low_signal_threshold = threshold; |
| netdev->low_signal_threshold_5ghz = threshold_5ghz; |
| |
| return netdev_cqm_rssi_update(netdev); |
| } |
| |
| int netdev_lower_signal_threshold(struct netdev *netdev) |
| { |
| return netdev_set_signal_thresholds(netdev, CRITICAL_SIGNAL_THRESHOLD, |
| CRITICAL_SIGNAL_THRESHOLD_5GHZ); |
| } |
| |
| int netdev_raise_signal_threshold(struct netdev *netdev) |
| { |
| return netdev_set_signal_thresholds(netdev, LOW_SIGNAL_THRESHOLD, |
| LOW_SIGNAL_THRESHOLD_5GHZ); |
| } |
| |
| static bool netdev_connection_work_ready(struct wiphy_radio_work_item *item) |
| { |
| struct netdev *netdev = l_container_of(item, struct netdev, work); |
| |
| netdev->retry_auth = false; |
| |
| if (mac_per_ssid) { |
| int ret = netdev_start_powered_mac_change(netdev); |
| |
| if (!ret) |
| return false; |
| else if (ret != -EALREADY) |
| goto failed; |
| } |
| |
| if (netdev_begin_connection(netdev) < 0) |
| return true; |
| |
| return false; |
| |
| failed: |
| netdev_connect_failed(netdev, NETDEV_RESULT_ABORTED, |
| MMPDU_STATUS_CODE_UNSPECIFIED); |
| |
| return true; |
| } |
| |
| static void netdev_connection_work_destroy(struct wiphy_radio_work_item *item) |
| { |
| struct netdev *netdev = l_container_of(item, struct netdev, work); |
| |
| if (netdev->auth_cmd) { |
| l_genl_msg_unref(netdev->auth_cmd); |
| netdev->auth_cmd = NULL; |
| } |
| |
| netdev->retry_auth = false; |
| } |
| |
| static const struct wiphy_radio_work_item_ops connect_work_ops = { |
| .do_work = netdev_connection_work_ready, |
| .destroy = netdev_connection_work_destroy, |
| }; |
| |
| static int netdev_handshake_state_setup_connection_type( |
| struct handshake_state *hs) |
| { |
| struct netdev_handshake_state *nhs = l_container_of(hs, |
| struct netdev_handshake_state, super); |
| struct wiphy *wiphy = nhs->netdev->wiphy; |
| bool softmac = wiphy_supports_cmds_auth_assoc(wiphy); |
| bool canroam = wiphy_supports_firmware_roam(wiphy); |
| |
| if (hs->supplicant_ie == NULL) |
| goto softmac; |
| |
| /* |
| * Sanity check that any FT AKMs are set only on softmac or on |
| * devices that support firmware roam |
| */ |
| if (L_WARN_ON(IE_AKM_IS_FT(hs->akm_suite) && !softmac && !canroam)) |
| return -ENOTSUP; |
| |
| switch (hs->akm_suite) { |
| case IE_RSN_AKM_SUITE_PSK: |
| case IE_RSN_AKM_SUITE_FT_USING_PSK: |
| case IE_RSN_AKM_SUITE_PSK_SHA256: |
| if (wiphy_has_ext_feature(wiphy, |
| NL80211_EXT_FEATURE_4WAY_HANDSHAKE_STA_PSK)) |
| goto psk_offload; |
| |
| /* fall through */ |
| case IE_RSN_AKM_SUITE_OWE: |
| if (softmac) |
| goto softmac; |
| |
| goto fullmac; |
| case IE_RSN_AKM_SUITE_8021X: |
| case IE_RSN_AKM_SUITE_FT_OVER_8021X: |
| case IE_RSN_AKM_SUITE_8021X_SHA256: |
| case IE_RSN_AKM_SUITE_8021X_SUITE_B_SHA256: |
| case IE_RSN_AKM_SUITE_8021X_SUITE_B_SHA384: |
| case IE_RSN_AKM_SUITE_FT_OVER_8021X_SHA384: |
| if (wiphy_has_ext_feature(wiphy, |
| NL80211_EXT_FEATURE_4WAY_HANDSHAKE_STA_1X)) |
| goto offload_1x; |
| |
| if (softmac) |
| goto softmac; |
| |
| goto fullmac; |
| case IE_RSN_AKM_SUITE_SAE_SHA256: |
| case IE_RSN_AKM_SUITE_FT_OVER_SAE_SHA256: |
| if (wiphy_has_ext_feature(wiphy, |
| NL80211_EXT_FEATURE_SAE_OFFLOAD)) |
| goto sae_offload; |
| |
| if (softmac && wiphy_has_feature(wiphy, NL80211_FEATURE_SAE)) |
| goto softmac; |
| |
| /* FullMAC uses EXTERNAL_AUTH and reuses this feature bit */ |
| if (wiphy_has_feature(wiphy, NL80211_FEATURE_SAE)) |
| goto fullmac; |
| |
| return -ENOTSUP; |
| case IE_RSN_AKM_SUITE_FILS_SHA256: |
| case IE_RSN_AKM_SUITE_FILS_SHA384: |
| case IE_RSN_AKM_SUITE_FT_OVER_FILS_SHA256: |
| case IE_RSN_AKM_SUITE_FT_OVER_FILS_SHA384: |
| /* FILS has no offload in any upstream driver */ |
| if (softmac && wiphy_has_ext_feature(wiphy, |
| NL80211_EXT_FEATURE_FILS_STA)) |
| goto softmac; |
| |
| return -ENOTSUP; |
| case IE_RSN_AKM_SUITE_TDLS: |
| case IE_RSN_AKM_SUITE_AP_PEER_KEY_SHA256: |
| case IE_RSN_AKM_SUITE_OSEN: |
| return -ENOTSUP; |
| } |
| |
| return -ENOTSUP; |
| |
| softmac: |
| nhs->type = CONNECTION_TYPE_SOFTMAC; |
| return 0; |
| fullmac: |
| nhs->type = CONNECTION_TYPE_FULLMAC; |
| return 0; |
| sae_offload: |
| nhs->type = CONNECTION_TYPE_SAE_OFFLOAD; |
| return 0; |
| psk_offload: |
| nhs->type = CONNECTION_TYPE_PSK_OFFLOAD; |
| return 0; |
| offload_1x: |
| nhs->type = CONNECTION_TYPE_8021X_OFFLOAD; |
| return 0; |
| } |
| |
| static void netdev_connect_common(struct netdev *netdev, |
| const struct scan_bss *bss, |
| const struct scan_bss *prev_bss, |
| struct handshake_state *hs, |
| netdev_event_func_t event_filter, |
| netdev_connect_cb_t cb, void *user_data) |
| { |
| struct netdev_handshake_state *nhs = l_container_of(hs, |
| struct netdev_handshake_state, super); |
| struct l_genl_msg *cmd_connect = NULL; |
| struct eapol_sm *sm = NULL; |
| bool is_rsn = hs->supplicant_ie != NULL; |
| const uint8_t *prev_bssid = prev_bss ? prev_bss->addr : NULL; |
| |
| netdev->frequency = bss->frequency; |
| netdev->privacy = bss->capability & IE_BSS_CAP_PRIVACY; |
| handshake_state_set_authenticator_address(hs, bss->addr); |
| |
| if (!is_rsn) |
| goto build_cmd_connect; |
| |
| /* For OWE, always use the CMD_CONNECT path */ |
| if (IE_AKM_IS_OWE(hs->akm_suite)) { |
| netdev->owe_sm = owe_sm_new(hs); |
| goto build_cmd_connect; |
| } |
| |
| if (IE_AKM_IS_FILS(hs->akm_suite)) { |
| netdev->ap = fils_sm_new(hs, netdev_fils_tx_authenticate, |
| netdev_fils_tx_associate, |
| netdev_get_oci, |
| netdev); |
| goto done; |
| } |
| |
| /* |
| * If SAE, and we have a valid PMKSA cache we can skip the entire SAE |
| * protocol and authenticate using the cached keys. |
| */ |
| if (IE_AKM_IS_SAE(hs->akm_suite) && hs->have_pmksa) { |
| l_debug("Skipping SAE by using PMKSA cache"); |
| goto build_cmd_connect; |
| } |
| |
| if (!IE_AKM_IS_SAE(hs->akm_suite) || |
| nhs->type == CONNECTION_TYPE_SAE_OFFLOAD) |
| goto build_cmd_connect; |
| |
| if (nhs->type == CONNECTION_TYPE_SOFTMAC) |
| netdev->ap = sae_sm_new(hs, netdev_sae_tx_authenticate, |
| netdev_sae_tx_associate, |
| netdev); |
| else { |
| netdev->ap = |
| sae_sm_new(hs, netdev_external_auth_sae_tx_authenticate, |
| netdev_external_auth_sae_tx_associate, |
| netdev); |
| sae_sm_force_default_group(netdev->ap); |
| sae_sm_force_hunt_and_peck(netdev->ap); |
| } |
| |
| if (sae_sm_is_h2e(netdev->ap)) { |
| uint8_t own_rsnxe[20]; |
| |
| if (wiphy_get_rsnxe(netdev->wiphy, |
| own_rsnxe, sizeof(own_rsnxe))) { |
| set_bit(own_rsnxe + 2, IE_RSNX_SAE_H2E); |
| handshake_state_set_supplicant_rsnxe(hs, |
| own_rsnxe); |
| } |
| } |
| |
| if (nhs->type == CONNECTION_TYPE_SOFTMAC) |
| goto done; |
| |
| build_cmd_connect: |
| cmd_connect = netdev_build_cmd_connect(netdev, hs, prev_bssid); |
| |
| if (!is_offload(hs) && (is_rsn || hs->settings_8021x)) { |
| sm = eapol_sm_new(hs); |
| |
| if (nhs->type == CONNECTION_TYPE_8021X_OFFLOAD) |
| eapol_sm_set_require_handshake(sm, false); |
| } |
| done: |
| netdev->connect_cmd = cmd_connect; |
| netdev->event_filter = event_filter; |
| netdev->connect_cb = cb; |
| netdev->user_data = user_data; |
| netdev->handshake = hs; |
| netdev->sm = sm; |
| netdev->cur_rssi = bss->signal_strength / 100; |
| netdev->low_signal_threshold = LOW_SIGNAL_THRESHOLD; |
| netdev->low_signal_threshold_5ghz = LOW_SIGNAL_THRESHOLD_5GHZ; |
| |
| if (netdev->rssi_levels_num) |
| netdev_set_rssi_level_idx(netdev); |
| |
| netdev_cqm_rssi_update(netdev); |
| |
| if (!wiphy_has_ext_feature(netdev->wiphy, |
| NL80211_EXT_FEATURE_CAN_REPLACE_PTK0)) |
| handshake_state_set_no_rekey(hs, true); |
| |
| wiphy_radio_work_insert(netdev->wiphy, &netdev->work, |
| WIPHY_WORK_PRIORITY_CONNECT, &connect_work_ops); |
| } |
| |
| int netdev_connect(struct netdev *netdev, const struct scan_bss *bss, |
| struct handshake_state *hs, |
| netdev_event_func_t event_filter, |
| netdev_connect_cb_t cb, void *user_data) |
| { |
| if (!(netdev->ifi_flags & IFF_UP)) |
| return -ENETDOWN; |
| |
| if (netdev->type != NL80211_IFTYPE_STATION && |
| netdev->type != NL80211_IFTYPE_P2P_CLIENT) |
| return -ENOTSUP; |
| |
| if (netdev->connected || netdev->connect_cmd_id || netdev->work.id) |
| return -EISCONN; |
| |
| if (netdev_handshake_state_setup_connection_type(hs) < 0) |
| return -ENOTSUP; |
| |
| netdev_connect_common(netdev, bss, NULL, hs, |
| event_filter, cb, user_data); |
| |
| return 0; |
| } |
| |
| static void disconnect_idle(struct l_idle *idle, void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| |
| l_idle_remove(idle); |
| netdev->disconnect_idle = NULL; |
| |
| netdev->disconnect_cb(netdev, true, netdev->user_data); |
| } |
| |
| int netdev_disconnect(struct netdev *netdev, |
| netdev_disconnect_cb_t cb, void *user_data) |
| { |
| struct l_genl_msg *disconnect; |
| bool send_disconnect = true; |
| bool deauth = false; |
| uint8_t aa[6]; |
| |
| if (!(netdev->ifi_flags & IFF_UP)) |
| return -ENETDOWN; |
| |
| if (netdev->type != NL80211_IFTYPE_STATION && |
| netdev->type != NL80211_IFTYPE_P2P_CLIENT) |
| return -ENOTSUP; |
| |
| if (netdev->disconnect_cmd_id) |
| return -EINPROGRESS; |
| |
| /* Only perform this if we haven't successfully fully associated yet */ |
| if (!netdev->operational) { |
| /* |
| * Three possibilities here: |
| * 1. We do not actually have a connect in progress (work.id |
| * is zero), then we can bail out early with an error. |
| * 2. We have sent CMD_CONNECT but not fully connected. The |
| * CMD_CONNECT needs to be canceled and a disconnect or |
| * deauth should be sent. |
| * 3. Queued up the connect work, but haven't sent CMD_CONNECT |
| * to the kernel. This case we do not need to send a |
| * disconnect. |
| */ |
| if (!netdev->work.id) |
| return -ENOTCONN; |
| |
| if (netdev->connect_cmd_id) { |
| l_genl_family_cancel(nl80211, netdev->connect_cmd_id); |
| netdev->connect_cmd_id = 0; |
| } else if (!wiphy_radio_work_is_running(netdev->wiphy, |
| netdev->work.id)) |
| send_disconnect = false; |
| |
| if (netdev->handshake && !netdev->associated) { |
| memcpy(aa, netdev->handshake->aa, 6); |
| deauth = true; |
| } |
| |
| netdev_connect_failed(netdev, NETDEV_RESULT_ABORTED, |
| MMPDU_REASON_CODE_UNSPECIFIED); |
| } else { |
| netdev_connect_free(netdev); |
| } |
| |
| if (send_disconnect) { |
| if (deauth) |
| disconnect = nl80211_build_deauthenticate( |
| netdev->index, aa, |
| MMPDU_REASON_CODE_DEAUTH_LEAVING); |
| else |
| disconnect = nl80211_build_disconnect(netdev->index, |
| MMPDU_REASON_CODE_DEAUTH_LEAVING); |
| |
| netdev->disconnect_cmd_id = l_genl_family_send(nl80211, |
| disconnect, netdev_cmd_disconnect_cb, |
| netdev, NULL); |
| |
| if (!netdev->disconnect_cmd_id) { |
| l_genl_msg_unref(disconnect); |
| return -EIO; |
| } |
| |
| netdev->disconnect_cb = cb; |
| netdev->user_data = user_data; |
| netdev->aborting = true; |
| } else if (cb) { |
| netdev->disconnect_cb = cb; |
| netdev->user_data = user_data; |
| netdev->disconnect_idle = l_idle_create(disconnect_idle, |
| netdev, NULL); |
| } |
| |
| return 0; |
| } |
| |
| int netdev_reassociate(struct netdev *netdev, const struct scan_bss *target_bss, |
| const struct scan_bss *orig_bss, |
| struct handshake_state *hs, |
| netdev_event_func_t event_filter, |
| netdev_connect_cb_t cb, void *user_data) |
| { |
| struct handshake_state *old_hs; |
| struct eapol_sm *old_sm; |
| |
| old_sm = netdev->sm; |
| old_hs = netdev->handshake; |
| |
| if (netdev_handshake_state_setup_connection_type(hs) < 0) |
| return -ENOTSUP; |
| |
| netdev->associated = false; |
| netdev->operational = false; |
| netdev->connected = false; |
| netdev->in_reassoc = true; |
| |
| netdev_connect_common(netdev, target_bss, orig_bss, hs, |
| event_filter, cb, user_data); |
| |
| if (netdev->ap) |
| memcpy(netdev->ap->prev_bssid, orig_bss->addr, ETH_ALEN); |
| |
| netdev_rssi_polling_update(netdev); |
| |
| if (old_sm) |
| eapol_sm_free(old_sm); |
| |
| if (old_hs) |
| handshake_state_unref(old_hs); |
| |
| return 0; |
| } |
| |
| static void netdev_join_adhoc_cb(struct l_genl_msg *msg, void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| |
| netdev->join_adhoc_cmd_id = 0; |
| |
| if (netdev->adhoc_cb) |
| netdev->adhoc_cb(netdev, l_genl_msg_get_error(msg), |
| netdev->user_data); |
| } |
| |
| int netdev_join_adhoc(struct netdev *netdev, const char *ssid, |
| struct iovec *extra_ie, size_t extra_ie_elems, |
| bool control_port, netdev_command_cb_t cb, |
| void *user_data) |
| { |
| struct l_genl_msg *cmd; |
| uint32_t ifindex = netdev->index; |
| uint32_t ch_freq = band_channel_to_freq(6, BAND_FREQ_2_4_GHZ); |
| uint32_t ch_type = NL80211_CHAN_HT20; |
| |
| if (netdev->type != NL80211_IFTYPE_ADHOC) { |
| l_error("iftype is invalid for adhoc: %u", |
| netdev_get_iftype(netdev)); |
| return -ENOTSUP; |
| } |
| |
| if (netdev->join_adhoc_cmd_id || netdev->leave_adhoc_cmd_id) |
| return -EBUSY; |
| |
| netdev->adhoc_cb = cb; |
| netdev->user_data = user_data; |
| |
| cmd = l_genl_msg_new_sized(NL80211_CMD_JOIN_IBSS, 128); |
| |
| l_genl_msg_append_attr(cmd, NL80211_ATTR_IFINDEX, 4, &ifindex); |
| l_genl_msg_append_attr(cmd, NL80211_ATTR_SSID, strlen(ssid), ssid); |
| l_genl_msg_append_attr(cmd, NL80211_ATTR_WIPHY_FREQ, 4, &ch_freq); |
| l_genl_msg_append_attr(cmd, NL80211_ATTR_WIPHY_CHANNEL_TYPE, 4, |
| &ch_type); |
| l_genl_msg_append_attrv(cmd, NL80211_ATTR_IE, extra_ie, extra_ie_elems); |
| l_genl_msg_append_attr(cmd, NL80211_ATTR_SOCKET_OWNER, 0, NULL); |
| |
| if (control_port) { |
| l_genl_msg_append_attr(cmd, NL80211_ATTR_CONTROL_PORT, 0, NULL); |
| |
| if (netdev->pae_over_nl80211) |
| l_genl_msg_append_attr(cmd, |
| NL80211_ATTR_CONTROL_PORT_OVER_NL80211, |
| 0, NULL); |
| } |
| |
| netdev->join_adhoc_cmd_id = l_genl_family_send(nl80211, cmd, |
| netdev_join_adhoc_cb, netdev, NULL); |
| |
| if (!netdev->join_adhoc_cmd_id) { |
| netdev->adhoc_cb = NULL; |
| netdev->user_data = NULL; |
| return -EIO; |
| } |
| |
| return 0; |
| } |
| |
| static void netdev_leave_adhoc_cb(struct l_genl_msg *msg, void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| |
| netdev->leave_adhoc_cmd_id = 0; |
| |
| if (netdev->adhoc_cb) |
| netdev->adhoc_cb(netdev, l_genl_msg_get_error(msg), |
| netdev->user_data); |
| |
| netdev->adhoc_cb = NULL; |
| } |
| |
| int netdev_leave_adhoc(struct netdev *netdev, netdev_command_cb_t cb, |
| void *user_data) |
| { |
| struct l_genl_msg *cmd; |
| |
| if (netdev->type != NL80211_IFTYPE_ADHOC) { |
| l_error("iftype is invalid for adhoc: %u", |
| netdev_get_iftype(netdev)); |
| return -ENOTSUP; |
| } |
| |
| if (netdev->join_adhoc_cmd_id || netdev->leave_adhoc_cmd_id) |
| return -EBUSY; |
| |
| netdev->adhoc_cb = cb; |
| netdev->user_data = user_data; |
| |
| cmd = l_genl_msg_new_sized(NL80211_CMD_LEAVE_IBSS, 64); |
| |
| l_genl_msg_append_attr(cmd, NL80211_ATTR_IFINDEX, 4, &netdev->index); |
| |
| netdev->leave_adhoc_cmd_id = l_genl_family_send(nl80211, cmd, |
| netdev_leave_adhoc_cb, netdev, |
| NULL); |
| |
| if (!netdev->leave_adhoc_cmd_id) |
| return -EIO; |
| |
| return 0; |
| } |
| |
| static uint32_t netdev_send_action_framev(struct netdev *netdev, |
| const uint8_t *to, |
| struct iovec *iov, size_t iov_len, |
| uint32_t freq, |
| l_genl_msg_func_t callback, |
| void *user_data) |
| { |
| uint32_t id; |
| struct l_genl_msg *msg = nl80211_build_cmd_frame(netdev->index, |
| 0x00d0, |
| netdev->addr, |
| to, freq, |
| iov, iov_len); |
| |
| id = l_genl_family_send(nl80211, msg, callback, user_data, NULL); |
| |
| if (!id) |
| l_genl_msg_unref(msg); |
| |
| return id; |
| } |
| |
| static uint32_t netdev_send_action_frame(struct netdev *netdev, |
| const uint8_t *to, |
| const uint8_t *body, size_t body_len, |
| uint32_t freq, |
| l_genl_msg_func_t callback, |
| void *user_data) |
| { |
| struct iovec iov[1]; |
| |
| iov[0].iov_base = (void *)body; |
| iov[0].iov_len = body_len; |
| |
| return netdev_send_action_framev(netdev, to, iov, 1, freq, callback, |
| user_data); |
| } |
| |
| static void netdev_ft_frame_cb(struct l_genl_msg *msg, void *user_data) |
| { |
| if (l_genl_msg_get_error(msg) < 0) |
| l_debug("Failed to send FT-Frame"); |
| } |
| |
| static int netdev_tx_ft_frame(uint32_t ifindex, uint16_t frame_type, |
| uint32_t frequency, const uint8_t *dest, |
| struct iovec *iov, size_t iov_len) |
| { |
| struct netdev *netdev = netdev_find(ifindex); |
| struct l_genl_msg *msg = nl80211_build_cmd_frame(netdev->index, |
| frame_type, |
| netdev->addr, dest, |
| frequency, |
| iov, iov_len); |
| |
| /* |
| * Even though the kernel is doing offchannel for Authentication this |
| * flag is still required otherwise the kernel gives -EBUSY. |
| */ |
| l_genl_msg_append_attr(msg, NL80211_ATTR_OFFCHANNEL_TX_OK, 0, NULL); |
| |
| if (!l_genl_family_send(nl80211, msg, netdev_ft_frame_cb, |
| netdev, NULL)) { |
| l_genl_msg_unref(msg); |
| return -EIO; |
| } |
| |
| return 0; |
| } |
| |
| int netdev_ft_reassociate(struct netdev *netdev, |
| const struct scan_bss *target_bss, |
| const struct scan_bss *orig_bss, |
| netdev_event_func_t event_filter, |
| netdev_connect_cb_t cb, void *user_data) |
| { |
| struct handshake_state *hs = netdev->handshake; |
| struct netdev_handshake_state *nhs; |
| struct l_genl_msg *msg; |
| struct iovec iov[64]; |
| unsigned int n_iov = L_ARRAY_SIZE(iov); |
| unsigned int c_iov = 0; |
| enum mpdu_management_subtype subtype = |
| MPDU_MANAGEMENT_SUBTYPE_REASSOCIATION_REQUEST; |
| |
| /* |
| * At this point there is no going back with FT so reset all the flags |
| * needed to associate with a new BSS. |
| */ |
| netdev->frequency = target_bss->frequency; |
| netdev->handshake->active_tk_index = 0; |
| netdev->associated = false; |
| netdev->operational = false; |
| netdev->in_ft = true; |
| netdev->event_filter = event_filter; |
| netdev->connect_cb = cb; |
| netdev->user_data = user_data; |
| netdev->low_signal_threshold = LOW_SIGNAL_THRESHOLD; |
| netdev->low_signal_threshold_5ghz = LOW_SIGNAL_THRESHOLD_5GHZ; |
| |
| /* |
| * Cancel commands that could be running because of EAPoL activity |
| * like re-keying, this way the callbacks for those commands don't |
| * have to check if failures resulted from the transition. |
| */ |
| nhs = l_container_of(netdev->handshake, |
| struct netdev_handshake_state, super); |
| |
| /* reset key states just as we do in initialization */ |
| nhs->complete = false; |
| nhs->ptk_installed = false; |
| nhs->gtk_installed = true; |
| nhs->igtk_installed = true; |
| |
| if (nhs->group_new_key_cmd_id) { |
| l_genl_family_cancel(nl80211, nhs->group_new_key_cmd_id); |
| nhs->group_new_key_cmd_id = 0; |
| } |
| |
| if (nhs->group_management_new_key_cmd_id) { |
| l_genl_family_cancel(nl80211, |
| nhs->group_management_new_key_cmd_id); |
| nhs->group_management_new_key_cmd_id = 0; |
| } |
| |
| if (netdev->rekey_offload_cmd_id) { |
| l_genl_family_cancel(nl80211, netdev->rekey_offload_cmd_id); |
| netdev->rekey_offload_cmd_id = 0; |
| } |
| |
| netdev_rssi_polling_update(netdev); |
| netdev_cqm_rssi_update(netdev); |
| |
| if (netdev->sm) { |
| eapol_sm_free(netdev->sm); |
| netdev->sm = NULL; |
| |
| netdev_ensure_eapol_registered(netdev); |
| } |
| |
| msg = netdev_build_cmd_associate_common(netdev); |
| |
| c_iov = netdev_populate_common_ies(netdev, hs, msg, iov, n_iov, c_iov); |
| |
| if (hs->supplicant_ie) |
| c_iov = iov_ie_append(iov, n_iov, c_iov, hs->supplicant_ie, |
| IE_LEN(hs->supplicant_ie)); |
| |
| if (hs->supplicant_fte) |
| c_iov = iov_ie_append(iov, n_iov, c_iov, hs->supplicant_fte, |
| IE_LEN(hs->supplicant_fte)); |
| |
| if (hs->mde) |
| c_iov = iov_ie_append(iov, n_iov, c_iov, hs->mde, |
| IE_LEN(hs->mde)); |
| |
| mpdu_sort_ies(subtype, iov, c_iov); |
| |
| l_genl_msg_append_attr(msg, NL80211_ATTR_PREV_BSSID, ETH_ALEN, |
| orig_bss->addr); |
| l_genl_msg_append_attrv(msg, NL80211_ATTR_IE, iov, c_iov); |
| |
| netdev->connect_cmd_id = l_genl_family_send(nl80211, msg, |
| netdev_cmd_ft_reassociate_cb, |
| netdev, NULL); |
| if (!netdev->connect_cmd_id) { |
| l_genl_msg_unref(msg); |
| |
| return -EIO; |
| } |
| |
| return 0; |
| } |
| |
| static void netdev_ft_response_frame_event(const struct mmpdu_header *hdr, |
| const void *body, size_t body_len, |
| int rssi, void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| |
| if (!netdev->connected) |
| return; |
| |
| __ft_rx_action(netdev->index, (const uint8_t *)hdr, |
| mmpdu_header_len(hdr) + body_len); |
| } |
| |
| static void netdev_ft_auth_response_frame_event(const struct mmpdu_header *hdr, |
| const void *body, size_t body_len, |
| int rssi, void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| |
| if (!netdev->connected) |
| return; |
| |
| __ft_rx_authenticate(netdev->index, (const uint8_t *)hdr, |
| mmpdu_header_len(hdr) + body_len); |
| } |
| |
| static void netdev_qos_map_frame_event(const struct mmpdu_header *hdr, |
| const void *body, size_t body_len, |
| int rssi, void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| |
| /* No point telling the kernel */ |
| if (!netdev->connected) |
| return; |
| |
| if (memcmp(netdev->handshake->aa, hdr->address_2, ETH_ALEN)) |
| return; |
| |
| if (body_len < 5) |
| return; |
| |
| if (l_get_u8(body + 2) != IE_TYPE_QOS_MAP_SET) |
| return; |
| |
| netdev_send_qos_map_set(netdev, body + 4, body_len - 4); |
| } |
| |
| static void netdev_sae_external_auth_frame_event(const struct mmpdu_header *hdr, |
| const void *body, size_t body_len, |
| int rssi, void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| const struct mmpdu_authentication *auth; |
| uint16_t status_code = MMPDU_STATUS_CODE_UNSPECIFIED; |
| int ret; |
| |
| if (!netdev->external_auth) |
| return; |
| |
| if (!netdev->ap) |
| return; |
| |
| auth = mmpdu_body(hdr); |
| /* |
| * Allows station to persist settings so it does not retry |
| * the higher order ECC group again |
| */ |
| if (L_CPU_TO_LE16(auth->status) == |
| MMPDU_STATUS_CODE_UNSUPP_FINITE_CYCLIC_GROUP && |
| netdev->event_filter) |
| netdev->event_filter(netdev, NETDEV_EVENT_ECC_GROUP_RETRY, |
| NULL, netdev->user_data); |
| |
| ret = auth_proto_rx_authenticate(netdev->ap, (const void *) hdr, |
| mmpdu_header_len(hdr) + body_len); |
| |
| switch (ret) { |
| case 0: |
| case -EAGAIN: |
| return; |
| case -ENOMSG: |
| case -EBADMSG: |
| return; |
| default: |
| break; |
| } |
| |
| if (ret > 0) |
| status_code = (uint16_t)ret; |
| |
| netdev_send_external_auth(netdev, status_code); |
| } |
| |
| static void netdev_preauth_cb(const uint8_t *pmk, void *user_data) |
| { |
| struct netdev_preauth_state *preauth = user_data; |
| netdev_preauthenticate_cb_t cb = preauth->cb; |
| |
| preauth->cb = NULL; |
| |
| cb(preauth->netdev, |
| pmk ? NETDEV_RESULT_OK : NETDEV_RESULT_HANDSHAKE_FAILED, |
| pmk, preauth->user_data); |
| } |
| |
| int netdev_preauthenticate(struct netdev *netdev, |
| const struct scan_bss *target_bss, |
| netdev_preauthenticate_cb_t cb, void *user_data) |
| { |
| struct netdev_preauth_state *preauth; |
| |
| if (!netdev->operational) |
| return -ENOTCONN; |
| |
| preauth = l_new(struct netdev_preauth_state, 1); |
| |
| if (!eapol_preauth_start(target_bss->addr, netdev->handshake, |
| netdev_preauth_cb, preauth, |
| netdev_preauth_destroy)) { |
| l_free(preauth); |
| |
| return -EIO; |
| } |
| |
| preauth->cb = cb; |
| preauth->user_data = user_data; |
| preauth->netdev = netdev; |
| |
| return 0; |
| } |
| |
| static void netdev_neighbor_report_req_cb(struct l_genl_msg *msg, |
| void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| |
| if (!netdev->neighbor_report_cb) |
| return; |
| |
| if (l_genl_msg_get_error(msg) < 0) { |
| netdev->neighbor_report_cb(netdev, l_genl_msg_get_error(msg), |
| NULL, 0, netdev->user_data); |
| |
| netdev->neighbor_report_cb = NULL; |
| |
| l_timeout_remove(netdev->neighbor_report_timeout); |
| } |
| } |
| |
| static void netdev_neighbor_report_timeout(struct l_timeout *timeout, |
| void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| |
| netdev->neighbor_report_cb(netdev, -ETIMEDOUT, NULL, 0, |
| netdev->user_data); |
| |
| netdev->neighbor_report_cb = NULL; |
| |
| l_timeout_remove(netdev->neighbor_report_timeout); |
| } |
| |
| int netdev_neighbor_report_req(struct netdev *netdev, |
| netdev_neighbor_report_cb_t cb) |
| { |
| const uint8_t action_frame[] = { |
| 0x05, /* Category: Radio Measurement */ |
| 0x04, /* Radio Measurement Action: Neighbor Report Request */ |
| 0x01, /* Dialog Token: a non-zero value (unused) */ |
| }; |
| |
| if (netdev->neighbor_report_cb || !netdev->connected) |
| return -EBUSY; |
| |
| if (!netdev_send_action_frame(netdev, netdev->handshake->aa, |
| action_frame, sizeof(action_frame), |
| netdev->frequency, |
| netdev_neighbor_report_req_cb, |
| netdev)) |
| return -EIO; |
| |
| netdev->neighbor_report_cb = cb; |
| |
| /* Set a 3-second timeout */ |
| netdev->neighbor_report_timeout = |
| l_timeout_create(3, netdev_neighbor_report_timeout, |
| netdev, NULL); |
| |
| return 0; |
| } |
| |
| static void netdev_neighbor_report_frame_event(const struct mmpdu_header *hdr, |
| const void *body, size_t body_len, |
| int rssi, void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| |
| if (body_len < 3) { |
| l_debug("Neighbor Report frame too short"); |
| return; |
| } |
| |
| if (!netdev->neighbor_report_cb) |
| return; |
| |
| /* |
| * Don't use the dialog token (byte 3), return the first Neighbor |
| * Report Response received. |
| * |
| * Byte 1 is 0x05 for Radio Measurement, byte 2 is 0x05 for |
| * Neighbor Report. |
| */ |
| |
| netdev->neighbor_report_cb(netdev, 0, body + 3, body_len - 3, |
| netdev->user_data); |
| netdev->neighbor_report_cb = NULL; |
| |
| l_timeout_remove(netdev->neighbor_report_timeout); |
| } |
| |
| static void netdev_sa_query_resp_cb(struct l_genl_msg *msg, void *user_data) |
| { |
| int err = l_genl_msg_get_error(msg); |
| const char *ext_error; |
| |
| if (err >= 0) |
| return; |
| |
| ext_error = l_genl_msg_get_extended_error(msg); |
| l_debug("error sending SA Query request: %s", |
| ext_error ? ext_error : strerror(-err)); |
| } |
| |
| static int netdev_build_oci(struct netdev *netdev, uint8_t *out) |
| { |
| out[0] = IE_TYPE_EXTENSION; |
| out[1] = 4; |
| out[2] = IE_TYPE_OCI & 0xff; |
| |
| return oci_from_chandef(netdev->handshake->chandef, out + 3); |
| } |
| |
| static void netdev_sa_query_timeout(struct l_timeout *timeout, |
| void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| |
| l_info("SA Query timed out, connection is invalid. Disconnecting..."); |
| |
| l_timeout_remove(netdev->sa_query_timeout); |
| netdev->sa_query_timeout = NULL; |
| |
| netdev_disconnect_by_sme(netdev, NETDEV_RESULT_ABORTED, |
| MMPDU_REASON_CODE_PREV_AUTH_NOT_VALID); |
| } |
| |
| static void netdev_sa_query_req_cb(struct l_genl_msg *msg, void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| int err = l_genl_msg_get_error(msg); |
| const char *ext_error; |
| |
| if (err >= 0) |
| return; |
| |
| ext_error = l_genl_msg_get_extended_error(msg); |
| l_debug("error sending SA Query request: %s", |
| ext_error ? ext_error : strerror(-err)); |
| |
| l_timeout_remove(netdev->sa_query_timeout); |
| netdev->sa_query_timeout = NULL; |
| } |
| |
| static bool netdev_send_sa_query_request(struct netdev *netdev) |
| { |
| uint8_t req[10]; |
| uint8_t *ptr = req; |
| |
| ptr[0] = 0x08; /* Category: SA Query */ |
| ptr[1] = 0x00; /* SA Query Action: Request */ |
| |
| /* Transaction ID */ |
| l_getrandom(ptr + 2, 2); |
| |
| ptr += 4; |
| |
| if (netdev->handshake->supplicant_ocvc && |
| netdev->handshake->authenticator_ocvc) { |
| if (netdev_build_oci(netdev, ptr) < 0) { |
| l_debug("Could not build OCI"); |
| return false; |
| } |
| |
| ptr += 6; |
| } |
| |
| if (!netdev_send_action_frame(netdev, netdev->handshake->aa, req, |
| ptr - req, netdev->frequency, |
| netdev_sa_query_req_cb, netdev)) { |
| l_error("error sending SA Query action frame"); |
| return false; |
| } |
| |
| netdev->sa_query_id = l_get_u16(req + 2); |
| netdev->sa_query_timeout = l_timeout_create(3, |
| netdev_sa_query_timeout, netdev, NULL); |
| |
| return true; |
| } |
| |
| static void netdev_sa_query_req_frame_event(const struct mmpdu_header *hdr, |
| const void *body, size_t body_len, |
| int rssi, void *user_data) |
| { |
| uint8_t sa_resp[10]; |
| uint8_t *ptr = sa_resp; |
| uint16_t transaction; |
| const uint8_t *oci; |
| struct netdev *netdev = user_data; |
| bool ocvc; |
| |
| if (body_len < 4) { |
| l_debug("SA Query request too short"); |
| return; |
| } |
| |
| if (!netdev->connected) |
| return; |
| |
| ocvc = netdev->handshake->supplicant_ocvc && |
| netdev->handshake->authenticator_ocvc; |
| |
| /* only care about SA Queries from our connected AP */ |
| if (memcmp(hdr->address_2, netdev->handshake->aa, 6)) |
| return; |
| |
| transaction = l_get_u16(body + 2); |
| |
| body_len -= 4; |
| |
| if (ocvc) { |
| /* |
| * IEEE 802.11 Section 11.13 |
| * |
| * "A STA that supports the SA Query procedure and receives an |
| * SA Query Request frame shall respond with an SA Query |
| * Response frame if none of the following are true... |
| * - OCI element is not present in the request or |
| * - Operating channel information indicated does not match the |
| * current channel information (see 12.2.9)." |
| */ |
| if (ie_parse_oci(body + 4, body_len, &oci) < 0) { |
| l_debug("Could not parse OCI"); |
| return; |
| } |
| |
| if (oci_verify(oci, netdev->handshake->chandef) < 0) { |
| l_debug("Could not verify OCI"); |
| return; |
| } |
| } |
| |
| ptr[0] = 0x08; /* SA Query */ |
| ptr[1] = 0x01; /* Response */ |
| memcpy(ptr + 2, &transaction, 2); |
| |
| ptr += 4; |
| |
| /* |
| * IEEE 802.11 Section 11.13 |
| * |
| * "A STA that responds with an SA Query Response frame to a STA that |
| * indicated OCVC capability shall include OCI element in the response |
| * frame if dot11RSNAOperatingChannelValidationActivated is true" |
| */ |
| if (ocvc) { |
| if (netdev_build_oci(netdev, ptr) < 0) { |
| l_debug("Could not build OCI"); |
| return; |
| } |
| |
| ptr += 6; |
| } |
| |
| l_info("received SA Query request from "MAC", transaction=%u", |
| MAC_STR(hdr->address_2), transaction); |
| |
| if (!netdev_send_action_frame(netdev, netdev->handshake->aa, |
| sa_resp, ptr - sa_resp, |
| netdev->frequency, |
| netdev_sa_query_resp_cb, netdev)) { |
| l_error("error sending SA Query response"); |
| return; |
| } |
| } |
| |
| static void netdev_sa_query_resp_frame_event(const struct mmpdu_header *hdr, |
| const void *body, size_t body_len, |
| int rssi, void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| const uint8_t *ptr = body; |
| const uint8_t *oci; |
| |
| if (!netdev->connected) |
| return; |
| |
| if (body_len < 4) { |
| l_debug("SA Query frame too short"); |
| return; |
| } |
| |
| ptr += 2; |
| |
| l_debug("SA Query src="MAC" dest="MAC" bssid="MAC" transaction=%u", |
| MAC_STR(hdr->address_2), MAC_STR(hdr->address_1), |
| MAC_STR(hdr->address_3), l_get_u16(ptr)); |
| |
| if (!netdev->sa_query_timeout) { |
| l_debug("no SA Query request sent"); |
| return; |
| } |
| |
| /* check if this is from our connected BSS */ |
| if (memcmp(hdr->address_2, netdev->handshake->aa, 6)) { |
| l_debug("received SA Query from non-connected AP"); |
| return; |
| } |
| |
| if (memcmp(ptr, &netdev->sa_query_id, 2)) { |
| l_debug("SA Query transaction ID's did not match"); |
| return; |
| } |
| |
| if (!(netdev->handshake->supplicant_ocvc && |
| netdev->handshake->authenticator_ocvc)) |
| goto keep_alive; |
| |
| ptr += 2; |
| body_len -= 4; |
| |
| /* |
| * IEEE 802.11 Section 11.13 |
| * |
| * "When a non-AP or non-PCP STA receives the SA Query Response frame |
| * from a STA that indicated OCVC capability, it shall ensure that OCI |
| * element is present in the response and the channel information in the |
| * OCI element matches current operating channel parameters |
| * (see 12.2.9). Otherwise, the receiving STA shall deem the response |
| * as invalid and discard it" |
| */ |
| if (ie_parse_oci(ptr, body_len, &oci) < 0) { |
| l_debug("Invalid OCI element"); |
| return; |
| } |
| |
| if (oci_verify(oci, netdev->handshake->chandef) < 0) { |
| l_debug("Could not verify OCI element"); |
| return; |
| } |
| |
| keep_alive: |
| l_info("SA Query response from connected BSS received, " |
| "keeping the connection active"); |
| |
| l_timeout_remove(netdev->sa_query_timeout); |
| netdev->sa_query_timeout = NULL; |
| } |
| |
| static void netdev_unprot_disconnect_event(struct l_genl_msg *msg, |
| struct netdev *netdev) |
| { |
| const struct mmpdu_header *hdr = NULL; |
| struct l_genl_attr attr; |
| uint16_t type; |
| uint16_t len; |
| const void *data; |
| uint16_t reason_code; |
| |
| if (!netdev->connected) |
| return; |
| |
| /* ignore excessive disassociate requests */ |
| if (netdev->sa_query_timeout) { |
| l_debug("SA Query already in progress, ignoring"); |
| return; |
| } |
| |
| if (!l_genl_attr_init(&attr, msg)) |
| return; |
| |
| while (l_genl_attr_next(&attr, &type, &len, &data)) { |
| switch (type) { |
| case NL80211_ATTR_FRAME: |
| hdr = mpdu_validate(data, len); |
| break; |
| } |
| } |
| |
| /* check that ATTR_FRAME was actually included */ |
| if (!hdr) |
| return; |
| |
| reason_code = l_get_le16(mmpdu_body(hdr)); |
| |
| l_info("unprotected disconnect event, src="MAC" dest="MAC |
| " bssid="MAC" reason=%u", |
| MAC_STR(hdr->address_2), MAC_STR(hdr->address_1), |
| MAC_STR(hdr->address_3), reason_code); |
| |
| if (memcmp(hdr->address_2, netdev->handshake->aa, 6)) { |
| l_debug("received invalid disassociate frame"); |
| return; |
| } |
| |
| if (reason_code != MMPDU_REASON_CODE_CLASS2_FRAME_FROM_NONAUTH_STA && |
| reason_code != |
| MMPDU_REASON_CODE_CLASS3_FRAME_FROM_NONASSOC_STA) { |
| l_debug("invalid reason code %u", reason_code); |
| return; |
| } |
| |
| netdev_send_sa_query_request(netdev); |
| } |
| |
| static void netdev_station_event(struct l_genl_msg *msg, |
| struct netdev *netdev, bool added) |
| { |
| struct l_genl_attr attr; |
| uint16_t type; |
| uint16_t len; |
| const void *data; |
| const uint8_t *mac = NULL; |
| |
| if (netdev_get_iftype(netdev) != NETDEV_IFTYPE_ADHOC) |
| return; |
| |
| if (!l_genl_attr_init(&attr, msg)) |
| return; |
| |
| while (l_genl_attr_next(&attr, &type, &len, &data)) { |
| switch (type) { |
| case NL80211_ATTR_MAC: |
| mac = data; |
| break; |
| } |
| } |
| |
| if (!mac) { |
| l_error("%s station event did not include MAC attribute", |
| added ? "new" : "del"); |
| return; |
| } |
| |
| WATCHLIST_NOTIFY(&netdev->station_watches, |
| netdev_station_watch_func_t, netdev, mac, added); |
| } |
| |
| static struct netdev *netdev_from_message(struct l_genl_msg *msg) |
| { |
| uint32_t ifindex; |
| |
| if (nl80211_parse_attrs(msg, NL80211_ATTR_IFINDEX, &ifindex, |
| NL80211_ATTR_UNSPEC) < 0) |
| return NULL; |
| |
| return netdev_find(ifindex); |
| } |
| |
| static void netdev_scan_notify(struct l_genl_msg *msg, void *user_data) |
| { |
| struct netdev *netdev; |
| |
| netdev = netdev_from_message(msg); |
| if (!netdev) |
| return; |
| |
| switch (l_genl_msg_get_command(msg)) { |
| case NL80211_CMD_NEW_SCAN_RESULTS: |
| netdev_new_scan_results_event(msg, netdev); |
| break; |
| } |
| } |
| |
| 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 we happened to be disconnected prior to GET_SCAN coming back |
| * just bail out now. This disconnect should already have been handled. |
| */ |
| if (!netdev->connected) |
| return false; |
| |
| if (err < 0) { |
| l_error("Failed to get scan after roam (%d)", err); |
| 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"); |
| return false; |
| } |
| |
| netdev->fw_roam_bss = bss; |
| |
| handshake_state_set_authenticator_ie(netdev->handshake, bss->rsne); |
| |
| if (is_offload(netdev->handshake)) { |
| netdev_connect_ok(netdev); |
| return false; |
| } |
| |
| if (netdev->sm) |
| L_WARN_ON(!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 void 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); |
| struct l_genl_attr attr; |
| uint16_t type, len; |
| const void *data; |
| const uint8_t *mac = NULL; |
| |
| l_debug(""); |
| |
| netdev->operational = false; |
| |
| if (L_WARN_ON(!l_genl_attr_init(&attr, msg))) |
| return; |
| |
| while (l_genl_attr_next(&attr, &type, &len, &data)) { |
| switch (type) { |
| case NL80211_ATTR_MAC: |
| mac = data; |
| break; |
| case NL80211_ATTR_REQ_IE: |
| parse_request_ies(netdev, data, len); |
| break; |
| } |
| } |
| |
| if (!mac) { |
| l_error("Failed to parse ATTR_MAC from CMD_ROAM"); |
| return; |
| } |
| |
| /* Handshake completed in firmware, just get the roamed BSS */ |
| if (is_offload(netdev->handshake)) |
| goto get_fw_scan; |
| |
| /* Reset handshake state */ |
| nhs->complete = false; |
| nhs->ptk_installed = false; |
| nhs->gtk_installed = true; |
| nhs->igtk_installed = true; |
| netdev->handshake->ptk_complete = false; |
| |
| get_fw_scan: |
| handshake_state_set_authenticator_address(netdev->handshake, mac); |
| |
| if (L_WARN_ON(!scan_get_firmware_scan(netdev->wdev_id, |
| netdev_get_fw_scan_cb, |
| netdev, NULL))) |
| return; |
| |
| if (netdev->event_filter) |
| netdev->event_filter(netdev, NETDEV_EVENT_ROAMING, |
| NULL, netdev->user_data); |
| } |
| |
| static void netdev_send_sa_query_delay(struct l_timeout *timeout, |
| void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| |
| netdev_send_sa_query_request(netdev); |
| |
| l_timeout_remove(netdev->sa_query_delay); |
| netdev->sa_query_delay = NULL; |
| } |
| |
| static void netdev_channel_switch_event(struct l_genl_msg *msg, |
| struct netdev *netdev) |
| { |
| _auto_(l_free) struct band_chandef *chandef = NULL; |
| |
| if (netdev->type != NL80211_IFTYPE_STATION) |
| return; |
| |
| if (L_WARN_ON(!netdev->connected)) |
| return; |
| |
| chandef = l_new(struct band_chandef, 1); |
| |
| if (nl80211_parse_chandef(msg, chandef) < 0) { |
| l_debug("Couldn't parse operating channel info."); |
| return; |
| } |
| |
| netdev->frequency = chandef->frequency; |
| |
| l_debug("Channel switch event, frequency: %u", netdev->frequency); |
| |
| handshake_state_set_chandef(netdev->handshake, l_steal_ptr(chandef)); |
| |
| /* |
| * IEEE 802.11-2020 11.9.3.2 |
| * "If the STA chooses to perform the specified switch and |
| * dot11RSNAOperatingChannelValidationActivated is true and the AP has |
| * indicated OCVC capability, after switching to the new channel the STA |
| * shall wait a random delay uniformly-distributed in the range between |
| * zero and 5000us, and then initiate the SA query procedure" |
| */ |
| if (netdev->handshake->supplicant_ocvc && |
| netdev->handshake->authenticator_ocvc) |
| netdev->sa_query_delay = l_timeout_create_ms( |
| l_getrandom_uint32() % 5, |
| netdev_send_sa_query_delay, |
| netdev, NULL); |
| |
| if (!netdev->event_filter) |
| return; |
| |
| netdev->event_filter(netdev, NETDEV_EVENT_CHANNEL_SWITCHED, |
| &netdev->frequency, netdev->user_data); |
| } |
| |
| static void netdev_michael_mic_failure(struct l_genl_msg *msg, |
| struct netdev *netdev) |
| { |
| uint8_t idx; |
| uint32_t type; |
| |
| if (nl80211_parse_attrs(msg, NL80211_ATTR_KEY_IDX, &idx, |
| NL80211_ATTR_KEY_TYPE, &type, |
| NL80211_ATTR_UNSPEC) < 0) |
| return; |
| |
| l_debug("ifindex=%u key_idx=%u type=%u", netdev->index, idx, type); |
| } |
| |
| #define MAX_COMEBACK_DELAY 1200 |
| |
| static void netdev_assoc_comeback(struct l_genl_msg *msg, |
| struct netdev *netdev) |
| { |
| const uint8_t *mac; |
| uint32_t timeout; |
| |
| if (L_WARN_ON(!netdev->connected)) |
| return; |
| |
| if (nl80211_parse_attrs(msg, NL80211_ATTR_MAC, &mac, |
| NL80211_ATTR_TIMEOUT, &timeout, |
| NL80211_ATTR_UNSPEC) < 0) |
| return; |
| |
| if (L_WARN_ON(memcmp(mac, netdev->handshake->aa, ETH_ALEN))) |
| return; |
| |
| if (timeout <= MAX_COMEBACK_DELAY) { |
| l_debug(MAC" requested an association comeback delay of %u TU", |
| MAC_STR(netdev->handshake->aa), timeout); |
| return; |
| } |
| |
| l_debug("Comeback delay of %u exceeded maximum of %u, deauthenticating", |
| timeout, MAX_COMEBACK_DELAY); |
| |
| netdev_deauth_and_fail_connection(netdev, |
| NETDEV_RESULT_ASSOCIATION_FAILED, |
| MMPDU_STATUS_CODE_REFUSED_TEMPORARILY); |
| } |
| |
| static void netdev_mlme_notify(struct l_genl_msg *msg, void *user_data) |
| { |
| struct netdev *netdev = NULL; |
| uint8_t cmd; |
| |
| cmd = l_genl_msg_get_command(msg); |
| l_debug("MLME notification %s(%u)", nl80211cmd_to_string(cmd), cmd); |
| |
| netdev = netdev_from_message(msg); |
| if (!netdev) |
| return; |
| |
| switch (cmd) { |
| case NL80211_CMD_AUTHENTICATE: |
| netdev_authenticate_event(msg, netdev); |
| break; |
| case NL80211_CMD_DEAUTHENTICATE: |
| netdev_deauthenticate_event(msg, netdev); |
| break; |
| case NL80211_CMD_ASSOCIATE: |
| netdev_associate_event(msg, netdev); |
| break; |
| case NL80211_CMD_ROAM: |
| netdev_roam_event(msg, netdev); |
| break; |
| case NL80211_CMD_CH_SWITCH_NOTIFY: |
| netdev_channel_switch_event(msg, netdev); |
| break; |
| case NL80211_CMD_CONNECT: |
| netdev_connect_event(msg, netdev); |
| break; |
| case NL80211_CMD_DISCONNECT: |
| netdev_disconnect_event(msg, netdev); |
| break; |
| case NL80211_CMD_NOTIFY_CQM: |
| netdev_cqm_event(msg, netdev); |
| break; |
| case NL80211_CMD_SET_REKEY_OFFLOAD: |
| netdev_rekey_offload_event(msg, netdev); |
| break; |
| case NL80211_CMD_UNPROT_DEAUTHENTICATE: |
| case NL80211_CMD_UNPROT_DISASSOCIATE: |
| netdev_unprot_disconnect_event(msg, netdev); |
| break; |
| case NL80211_CMD_NEW_STATION: |
| netdev_station_event(msg, netdev, true); |
| break; |
| case NL80211_CMD_DEL_STATION: |
| netdev_station_event(msg, netdev, false); |
| break; |
| case NL80211_CMD_MICHAEL_MIC_FAILURE: |
| netdev_michael_mic_failure(msg, netdev); |
| break; |
| case NL80211_CMD_ASSOC_COMEBACK: |
| netdev_assoc_comeback(msg, netdev); |
| break; |
| } |
| } |
| |
| static void netdev_pae_destroy(void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| |
| netdev->pae_io = NULL; |
| } |
| |
| static bool netdev_pae_read(struct l_io *io, void *user_data) |
| { |
| int fd = l_io_get_fd(io); |
| struct sockaddr_ll sll; |
| socklen_t sll_len; |
| ssize_t bytes; |
| uint8_t frame[IEEE80211_MAX_DATA_LEN]; |
| |
| memset(&sll, 0, sizeof(sll)); |
| sll_len = sizeof(sll); |
| |
| bytes = recvfrom(fd, frame, sizeof(frame), 0, |
| (struct sockaddr *) &sll, &sll_len); |
| if (bytes <= 0) { |
| l_error("EAPoL read socket: %s", strerror(errno)); |
| return false; |
| } |
| |
| if (sll.sll_halen != ETH_ALEN) |
| return true; |
| |
| __eapol_rx_packet(sll.sll_ifindex, sll.sll_addr, |
| ntohs(sll.sll_protocol), frame, bytes, false); |
| |
| return true; |
| } |
| |
| static void netdev_control_port_frame_event(struct l_genl_msg *msg, |
| struct netdev *netdev) |
| { |
| struct l_genl_attr attr; |
| uint16_t type; |
| uint16_t len; |
| const void *data; |
| const uint8_t *frame = NULL; |
| uint16_t frame_len = 0; |
| const uint8_t *src = NULL; |
| uint16_t proto = 0; |
| bool unencrypted = false; |
| |
| l_debug(""); |
| |
| if (!l_genl_attr_init(&attr, msg)) |
| return; |
| |
| while (l_genl_attr_next(&attr, &type, &len, &data)) { |
| switch (type) { |
| case NL80211_ATTR_FRAME: |
| if (frame) |
| return; |
| |
| frame = data; |
| frame_len = len; |
| break; |
| case NL80211_ATTR_MAC: |
| if (src) |
| return; |
| |
| src = data; |
| break; |
| case NL80211_ATTR_CONTROL_PORT_ETHERTYPE: |
| if (len != sizeof(proto)) |
| return; |
| |
| proto = *((const uint16_t *) data); |
| break; |
| case NL80211_ATTR_CONTROL_PORT_NO_ENCRYPT: |
| unencrypted = true; |
| break; |
| } |
| } |
| |
| if (!src || !frame || !proto) |
| return; |
| |
| __eapol_rx_packet(netdev->index, src, proto, |
| frame, frame_len, unencrypted); |
| } |
| |
| static void netdev_external_auth_event(struct l_genl_msg *msg, |
| struct netdev *netdev) |
| { |
| const uint8_t *bssid; |
| struct iovec ssid; |
| uint32_t akm; |
| uint32_t action; |
| struct handshake_state *hs = netdev->handshake; |
| |
| if (L_WARN_ON(nl80211_parse_attrs(msg, NL80211_ATTR_AKM_SUITES, &akm, |
| NL80211_ATTR_EXTERNAL_AUTH_ACTION, &action, |
| NL80211_ATTR_BSSID, &bssid, |
| NL80211_ATTR_SSID, &ssid, |
| NL80211_ATTR_UNSPEC) < 0)) |
| return; |
| |
| if (!L_IN_SET(action, NL80211_EXTERNAL_AUTH_START, |
| NL80211_EXTERNAL_AUTH_ABORT)) |
| return; |
| |
| /* kernel sends SAE_SHA256 AKM in BE order for legacy reasons */ |
| if (!L_IN_SET(akm, CRYPTO_AKM_SAE_SHA256, CRYPTO_AKM_FT_OVER_SAE_SHA256, |
| L_CPU_TO_BE32(CRYPTO_AKM_SAE_SHA256))) { |
| l_warn("Unknown AKM: %08x", akm); |
| return; |
| } |
| |
| if (action == NL80211_EXTERNAL_AUTH_ABORT) { |
| l_warn("External Auth Aborted"); |
| goto error; |
| } |
| |
| if (hs->ssid_len != ssid.iov_len || |
| memcmp(hs->ssid, ssid.iov_base, hs->ssid_len)) { |
| l_warn("Target SSID mismatch"); |
| goto error; |
| } |
| |
| if (memcmp(hs->aa, bssid, ETH_ALEN)) { |
| l_warn("Target BSSID mismatch"); |
| goto error; |
| } |
| |
| if (auth_proto_start(netdev->ap)) { |
| netdev->external_auth = true; |
| return; |
| } |
| |
| error: |
| netdev_send_external_auth(netdev, MMPDU_STATUS_CODE_UNSPECIFIED); |
| } |
| |
| static void netdev_unicast_notify(struct l_genl_msg *msg, void *user_data) |
| { |
| struct netdev *netdev = NULL; |
| struct l_genl_attr attr; |
| uint16_t type, len; |
| const void *data; |
| uint8_t cmd; |
| |
| cmd = l_genl_msg_get_command(msg); |
| if (!cmd) |
| return; |
| |
| l_debug("Unicast notification %s(%u)", nl80211cmd_to_string(cmd), cmd); |
| |
| if (!l_genl_attr_init(&attr, msg)) |
| return; |
| |
| while (l_genl_attr_next(&attr, &type, &len, &data)) { |
| switch (type) { |
| case NL80211_ATTR_IFINDEX: |
| if (len != sizeof(uint32_t)) { |
| l_warn("Invalid interface index attribute"); |
| return; |
| } |
| |
| netdev = netdev_find(*((uint32_t *) data)); |
| break; |
| } |
| } |
| |
| if (!netdev) |
| return; |
| |
| switch (cmd) { |
| case NL80211_CMD_CONTROL_PORT_FRAME: |
| netdev_control_port_frame_event(msg, netdev); |
| break; |
| case NL80211_CMD_EXTERNAL_AUTH: |
| netdev_external_auth_event(msg, netdev); |
| break; |
| } |
| } |
| |
| int netdev_set_rssi_report_levels(struct netdev *netdev, const int8_t *levels, |
| size_t levels_num) |
| { |
| struct l_genl_msg *cmd_set_cqm; |
| |
| l_debug("ifindex: %d, num_levels: %zu", netdev->index, levels_num); |
| |
| if (levels_num > L_ARRAY_SIZE(netdev->rssi_levels)) |
| return -ENOSPC; |
| |
| if (!wiphy_has_ext_feature(netdev->wiphy, |
| NL80211_EXT_FEATURE_CQM_RSSI_LIST)) |
| goto done; |
| |
| cmd_set_cqm = netdev_build_cmd_cqm_rssi_update(netdev, levels, |
| levels_num); |
| if (!cmd_set_cqm) |
| return -EINVAL; |
| |
| netdev->set_cqm_cmd_id = l_genl_family_send(nl80211, cmd_set_cqm, |
| netdev_cmd_set_cqm_cb, |
| netdev, NULL); |
| if (!netdev->set_cqm_cmd_id) { |
| l_genl_msg_unref(cmd_set_cqm); |
| return -EIO; |
| } |
| |
| done: |
| netdev->rssi_levels_num = levels_num; |
| |
| if (levels_num) { |
| memcpy(netdev->rssi_levels, levels, levels_num); |
| |
| if (netdev->connected) |
| netdev_set_rssi_level_idx(netdev); |
| } |
| |
| netdev_rssi_polling_update(netdev); |
| |
| return 0; |
| } |
| |
| static void netdev_get_station_cb(struct l_genl_msg *msg, void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| struct l_genl_attr attr, nested; |
| uint16_t type, len; |
| const void *data; |
| struct diagnostic_station_info info; |
| |
| netdev->get_station_cmd_id = 0; |
| |
| if (!l_genl_attr_init(&attr, msg)) |
| goto parse_error; |
| |
| memset(&info, 0, sizeof(info)); |
| |
| while (l_genl_attr_next(&attr, &type, &len, &data)) { |
| switch (type) { |
| case NL80211_ATTR_STA_INFO: |
| if (!l_genl_attr_recurse(&attr, &nested)) |
| goto parse_error; |
| |
| if (!netdev_parse_sta_info(&nested, &info)) |
| goto parse_error; |
| |
| break; |
| |
| case NL80211_ATTR_MAC: |
| if (len != 6) |
| goto parse_error; |
| |
| memcpy(info.addr, data, 6); |
| |
| break; |
| } |
| } |
| |
| if (netdev->get_station_cb) |
| netdev->get_station_cb(&info, netdev->get_station_data); |
| |
| return; |
| |
| parse_error: |
| if (netdev->get_station_cb) |
| netdev->get_station_cb(NULL, netdev->get_station_data); |
| } |
| |
| static void netdev_get_station_destroy(void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| |
| netdev->get_station_cmd_id = 0; |
| |
| if (netdev->get_station_destroy) |
| netdev->get_station_destroy(netdev->get_station_data); |
| } |
| |
| int netdev_get_station(struct netdev *netdev, const uint8_t *mac, |
| netdev_get_station_cb_t cb, void *user_data, |
| netdev_destroy_func_t destroy) |
| { |
| struct l_genl_msg *msg; |
| |
| if (netdev->get_station_cmd_id) |
| return -EBUSY; |
| |
| msg = l_genl_msg_new_sized(NL80211_CMD_GET_STATION, 64); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_IFINDEX, 4, &netdev->index); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_MAC, ETH_ALEN, mac); |
| |
| netdev->get_station_cmd_id = l_genl_family_send(nl80211, msg, |
| netdev_get_station_cb, netdev, |
| netdev_get_station_destroy); |
| if (!netdev->get_station_cmd_id) { |
| l_genl_msg_unref(msg); |
| return -EIO; |
| } |
| |
| netdev->get_station_cb = cb; |
| netdev->get_station_data = user_data; |
| netdev->get_station_destroy = destroy; |
| |
| return 0; |
| } |
| |
| int netdev_get_current_station(struct netdev *netdev, |
| netdev_get_station_cb_t cb, void *user_data, |
| netdev_destroy_func_t destroy) |
| { |
| if (!netdev->handshake) |
| return -ENOTCONN; |
| |
| return netdev_get_station(netdev, netdev->handshake->aa, cb, |
| user_data, destroy); |
| } |
| |
| int netdev_get_all_stations(struct netdev *netdev, netdev_get_station_cb_t cb, |
| void *user_data, netdev_destroy_func_t destroy) |
| { |
| struct l_genl_msg *msg; |
| |
| if (netdev->get_station_cmd_id) |
| return -EBUSY; |
| |
| msg = l_genl_msg_new_sized(NL80211_CMD_GET_STATION, 64); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_IFINDEX, 4, &netdev->index); |
| |
| netdev->get_station_cmd_id = l_genl_family_dump(nl80211, msg, |
| netdev_get_station_cb, netdev, |
| netdev_get_station_destroy); |
| if (!netdev->get_station_cmd_id) { |
| l_genl_msg_unref(msg); |
| return -EIO; |
| } |
| |
| netdev->get_station_cb = cb; |
| netdev->get_station_data = user_data; |
| netdev->get_station_destroy = destroy; |
| |
| return 0; |
| } |
| |
| static void netdev_add_station_frame_watches(struct netdev *netdev) |
| { |
| static const uint8_t action_neighbor_report_prefix[2] = { 0x05, 0x05 }; |
| static const uint8_t action_sa_query_resp_prefix[2] = { 0x08, 0x01 }; |
| static const uint8_t action_sa_query_req_prefix[2] = { 0x08, 0x00 }; |
| static const uint8_t action_ft_response_prefix[] = { 0x06, 0x02 }; |
| static const uint8_t auth_ft_response_prefix[] = { 0x02, 0x00 }; |
| static const uint8_t action_qos_map_prefix[] = { 0x01, 0x04 }; |
| static const uint8_t auth_sae_prefix[] = { 0x03, 0x00 }; |
| uint64_t wdev = netdev->wdev_id; |
| |
| /* Subscribe to Management -> Action -> RM -> Neighbor Report frames */ |
| frame_watch_add(wdev, 0, 0x00d0, action_neighbor_report_prefix, |
| sizeof(action_neighbor_report_prefix), false, |
| netdev_neighbor_report_frame_event, netdev, NULL); |
| |
| frame_watch_add(wdev, 0, 0x00d0, action_sa_query_resp_prefix, |
| sizeof(action_sa_query_resp_prefix), false, |
| netdev_sa_query_resp_frame_event, netdev, NULL); |
| |
| frame_watch_add(wdev, 0, 0x00d0, action_sa_query_req_prefix, |
| sizeof(action_sa_query_req_prefix), false, |
| netdev_sa_query_req_frame_event, netdev, NULL); |
| |
| frame_watch_add(wdev, 0, 0x00d0, action_ft_response_prefix, |
| sizeof(action_ft_response_prefix), false, |
| netdev_ft_response_frame_event, netdev, NULL); |
| |
| frame_watch_add(wdev, 0, 0x00b0, auth_ft_response_prefix, |
| sizeof(auth_ft_response_prefix), false, |
| netdev_ft_auth_response_frame_event, netdev, NULL); |
| |
| if (wiphy_supports_qos_set_map(netdev->wiphy)) |
| frame_watch_add(wdev, 0, 0x00d0, action_qos_map_prefix, |
| sizeof(action_qos_map_prefix), false, |
| netdev_qos_map_frame_event, netdev, NULL); |
| |
| if (!wiphy_supports_cmds_auth_assoc(netdev->wiphy) && |
| wiphy_has_feature(netdev->wiphy, NL80211_FEATURE_SAE)) |
| frame_watch_add(wdev, 0, 0x00b0, |
| auth_sae_prefix, sizeof(auth_sae_prefix), |
| false, netdev_sae_external_auth_frame_event, |
| netdev, NULL); |
| } |
| |
| static void netdev_setup_interface(struct netdev *netdev) |
| { |
| switch (netdev->type) { |
| case NL80211_IFTYPE_STATION: |
| netdev_add_station_frame_watches(netdev); |
| break; |
| default: |
| break; |
| } |
| } |
| |
| static void netdev_set_interface_event(struct l_genl_msg *msg, |
| struct netdev *netdev) |
| { |
| uint32_t iftype; |
| uint64_t wdev_id; |
| |
| if (nl80211_parse_attrs(msg, NL80211_ATTR_IFTYPE, &iftype, |
| NL80211_ATTR_WDEV, &wdev_id, |
| NL80211_ATTR_UNSPEC) < 0) |
| return; |
| |
| if (iftype == netdev->type) |
| return; |
| |
| l_debug("Interface type changed from %s to %s", |
| netdev_iftype_to_string(netdev->type), |
| netdev_iftype_to_string(iftype)); |
| netdev->type = iftype; |
| frame_watch_wdev_remove(wdev_id); |
| |
| netdev_setup_interface(netdev); |
| |
| WATCHLIST_NOTIFY(&netdev_watches, netdev_watch_func_t, |
| netdev, NETDEV_WATCH_EVENT_IFTYPE_CHANGE); |
| } |
| |
| static void netdev_config_notify(struct l_genl_msg *msg, void *user_data) |
| { |
| struct netdev *netdev; |
| |
| netdev = netdev_from_message(msg); |
| if (!netdev) |
| return; |
| |
| switch (l_genl_msg_get_command(msg)) { |
| case NL80211_CMD_SET_INTERFACE: |
| netdev_set_interface_event(msg, netdev); |
| break; |
| } |
| } |
| |
| static struct l_genl_msg *netdev_build_cmd_set_interface(struct netdev *netdev, |
| uint32_t iftype) |
| { |
| struct l_genl_msg *msg = |
| l_genl_msg_new_sized(NL80211_CMD_SET_INTERFACE, 32); |
| |
| l_genl_msg_append_attr(msg, NL80211_ATTR_IFINDEX, 4, &netdev->index); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_IFTYPE, 4, &iftype); |
| |
| return msg; |
| } |
| |
| struct netdev_set_iftype_request { |
| netdev_command_cb_t cb; |
| void *user_data; |
| netdev_destroy_func_t destroy; |
| uint32_t pending_type; |
| uint32_t ref; |
| struct netdev *netdev; |
| bool bring_up; |
| }; |
| |
| static void netdev_set_iftype_request_destroy(void *user_data) |
| { |
| struct netdev_set_iftype_request *req = user_data; |
| struct netdev *netdev = req->netdev; |
| |
| req->ref--; |
| if (req->ref) |
| return; |
| |
| netdev->set_powered_cmd_id = 0; |
| netdev->set_interface_cmd_id = 0; |
| |
| if (req->destroy) |
| req->destroy(req->user_data); |
| |
| l_free(req); |
| } |
| |
| static void netdev_set_iftype_up_cb(int error, uint16_t type, |
| const void *data, |
| uint32_t len, void *user_data) |
| { |
| struct netdev_set_iftype_request *req = user_data; |
| struct netdev *netdev = req->netdev; |
| |
| if (req->cb) |
| req->cb(netdev, error, req->user_data); |
| } |
| |
| static void netdev_set_iftype_cb(struct l_genl_msg *msg, void *user_data) |
| { |
| struct netdev_set_iftype_request *req = user_data; |
| struct netdev *netdev = req->netdev; |
| int error = l_genl_msg_get_error(msg); |
| |
| if (error != 0) |
| goto done; |
| |
| /* If the netdev was down originally, we're done */ |
| if (!req->bring_up) |
| goto done; |
| |
| netdev->set_powered_cmd_id = |
| l_rtnl_set_powered(rtnl, netdev->index, true, |
| netdev_set_iftype_up_cb, req, |
| netdev_set_iftype_request_destroy); |
| if (!netdev->set_powered_cmd_id) { |
| error = -EIO; |
| goto done; |
| } |
| |
| req->ref++; |
| netdev->set_interface_cmd_id = 0; |
| return; |
| |
| done: |
| if (req->cb) |
| req->cb(netdev, error, req->user_data); |
| } |
| |
| static void netdev_set_iftype_down_cb(int error, uint16_t type, |
| const void *data, |
| uint32_t len, void *user_data) |
| { |
| struct netdev_set_iftype_request *req = user_data; |
| struct netdev *netdev = req->netdev; |
| struct l_genl_msg *msg; |
| |
| if (error != 0) |
| goto error; |
| |
| msg = netdev_build_cmd_set_interface(netdev, req->pending_type); |
| netdev->set_interface_cmd_id = |
| l_genl_family_send(nl80211, msg, netdev_set_iftype_cb, req, |
| netdev_set_iftype_request_destroy); |
| if (!netdev->set_interface_cmd_id) { |
| l_genl_msg_unref(msg); |
| error = -EIO; |
| goto error; |
| } |
| |
| req->ref++; |
| netdev->set_powered_cmd_id = 0; |
| return; |
| |
| error: |
| if (req->cb) |
| req->cb(netdev, error, req->user_data); |
| } |
| |
| int netdev_set_iftype(struct netdev *netdev, enum netdev_iftype type, |
| netdev_command_cb_t cb, void *user_data, |
| netdev_destroy_func_t destroy) |
| { |
| uint32_t iftype; |
| struct netdev_set_iftype_request *req; |
| |
| switch (type) { |
| case NETDEV_IFTYPE_AP: |
| iftype = NL80211_IFTYPE_AP; |
| break; |
| case NETDEV_IFTYPE_ADHOC: |
| iftype = NL80211_IFTYPE_ADHOC; |
| break; |
| case NETDEV_IFTYPE_STATION: |
| iftype = NL80211_IFTYPE_STATION; |
| break; |
| default: |
| l_error("unsupported iftype %u", type); |
| return -EINVAL; |
| } |
| |
| if (netdev->set_powered_cmd_id || |
| netdev->set_interface_cmd_id) |
| return -EBUSY; |
| |
| req = l_new(struct netdev_set_iftype_request, 1); |
| req->cb = cb; |
| req->user_data = user_data; |
| req->destroy = destroy; |
| req->pending_type = iftype; |
| req->netdev = netdev; |
| req->ref = 1; |
| req->bring_up = netdev_get_is_up(netdev); |
| |
| if (!req->bring_up) { |
| struct l_genl_msg *msg = |
| netdev_build_cmd_set_interface(netdev, iftype); |
| |
| netdev->set_interface_cmd_id = |
| l_genl_family_send(nl80211, msg, |
| netdev_set_iftype_cb, req, |
| netdev_set_iftype_request_destroy); |
| if (netdev->set_interface_cmd_id) |
| return 0; |
| |
| l_genl_msg_unref(msg); |
| } else { |
| netdev->set_powered_cmd_id = |
| l_rtnl_set_powered(rtnl, netdev->index, false, |
| netdev_set_iftype_down_cb, req, |
| netdev_set_iftype_request_destroy); |
| if (netdev->set_powered_cmd_id) |
| return 0; |
| } |
| |
| l_free(req); |
| return -EIO; |
| } |
| |
| static void netdev_bridge_port_event(const struct ifinfomsg *ifi, int bytes, |
| bool added) |
| { |
| struct netdev *netdev; |
| struct rtattr *attr; |
| uint32_t master = 0; |
| |
| netdev = netdev_find(ifi->ifi_index); |
| if (!netdev) |
| return; |
| |
| for (attr = IFLA_RTA(ifi); RTA_OK(attr, bytes); |
| attr = RTA_NEXT(attr, bytes)) { |
| switch (attr->rta_type) { |
| case IFLA_MASTER: |
| memcpy(&master, RTA_DATA(attr), sizeof(master)); |
| break; |
| } |
| } |
| |
| l_debug("netdev: %d %s bridge: %d", ifi->ifi_index, |
| (added ? "added to" : "removed from"), master); |
| } |
| |
| struct set_4addr_cb_data { |
| struct netdev *netdev; |
| bool value; |
| netdev_command_cb_t callback; |
| void *user_data; |
| netdev_destroy_func_t destroy; |
| }; |
| |
| static void netdev_set_4addr_cb(struct l_genl_msg *msg, void *user_data) |
| { |
| struct set_4addr_cb_data *cb_data = user_data; |
| int error = l_genl_msg_get_error(msg); |
| |
| if (!cb_data) |
| return; |
| |
| /* cache the value that has just been set */ |
| if (!error) |
| cb_data->netdev->use_4addr = cb_data->value; |
| |
| cb_data->callback(cb_data->netdev, error, cb_data->user_data); |
| } |
| |
| static void netdev_set_4addr_destroy(void *user_data) |
| { |
| struct set_4addr_cb_data *cb_data = user_data; |
| |
| if (!cb_data) |
| return; |
| |
| if (cb_data->destroy) |
| cb_data->destroy(cb_data->user_data); |
| |
| l_free(cb_data); |
| } |
| |
| int netdev_set_4addr(struct netdev *netdev, bool use_4addr, |
| netdev_command_cb_t cb, void *user_data, |
| netdev_destroy_func_t destroy) |
| { |
| struct set_4addr_cb_data *cb_data = NULL; |
| uint8_t attr_4addr = (use_4addr ? 1 : 0); |
| struct l_genl_msg *msg; |
| |
| l_debug("netdev: %d use_4addr: %d", netdev->index, use_4addr); |
| |
| msg = l_genl_msg_new_sized(NL80211_CMD_SET_INTERFACE, 32); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_IFINDEX, 4, &netdev->index); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_4ADDR, 1, &attr_4addr); |
| |
| if (cb) { |
| cb_data = l_new(struct set_4addr_cb_data, 1); |
| cb_data->netdev = netdev; |
| cb_data->value = use_4addr; |
| cb_data->callback = cb; |
| cb_data->user_data = user_data; |
| cb_data->destroy = destroy; |
| } |
| |
| if (!l_genl_family_send(nl80211, msg, netdev_set_4addr_cb, cb_data, |
| netdev_set_4addr_destroy)) { |
| l_error("CMD_SET_INTERFACE (4addr) failed"); |
| |
| l_genl_msg_unref(msg); |
| l_free(cb_data); |
| |
| return -EIO; |
| } |
| |
| return 0; |
| } |
| |
| bool netdev_get_4addr(struct netdev *netdev) |
| { |
| return netdev->use_4addr; |
| } |
| |
| static void netdev_newlink_notify(const struct ifinfomsg *ifi, int bytes) |
| { |
| struct netdev *netdev; |
| bool old_up, new_up; |
| char old_name[IFNAMSIZ]; |
| uint8_t old_addr[ETH_ALEN]; |
| struct rtattr *attr; |
| uint8_t *operstate = NULL; |
| uint8_t *linkmode = NULL; |
| |
| if (ifi->ifi_family == AF_BRIDGE) { |
| netdev_bridge_port_event(ifi, bytes, true); |
| return; |
| } |
| |
| netdev = netdev_find(ifi->ifi_index); |
| if (!netdev) |
| return; |
| |
| old_up = netdev_get_is_up(netdev); |
| strcpy(old_name, netdev->name); |
| memcpy(old_addr, netdev->addr, ETH_ALEN); |
| |
| netdev->ifi_flags = ifi->ifi_flags; |
| |
| for (attr = IFLA_RTA(ifi); RTA_OK(attr, bytes); |
| attr = RTA_NEXT(attr, bytes)) { |
| switch (attr->rta_type) { |
| case IFLA_IFNAME: |
| strcpy(netdev->name, RTA_DATA(attr)); |
| break; |
| case IFLA_ADDRESS: |
| if (RTA_PAYLOAD(attr) < ETH_ALEN) |
| break; |
| |
| memcpy(netdev->addr, RTA_DATA(attr), ETH_ALEN); |
| break; |
| case IFLA_OPERSTATE: |
| operstate = RTA_DATA(attr); |
| break; |
| case IFLA_LINKMODE: |
| linkmode = RTA_DATA(attr); |
| break; |
| } |
| } |
| |
| if (!netdev->events_ready) /* Did we send NETDEV_WATCH_EVENT_NEW yet? */ |
| return; |
| |
| /* |
| * Sometimes the driver sends the Association / Connect event on the |
| * nl80211 interface before the driver is ready to accept IF_OPER_UP |
| * setting on the rtnl interface. This results in our initial |
| * IF_OPER_UP setting being ignored. In this case the driver will |
| * send a New Link event with a stale OperState. Detect this case and |
| * try to re-set IF_OPER_UP. |
| */ |
| if (linkmode && *linkmode == 1 && |
| operstate && *operstate == IF_OPER_DORMANT && |
| netdev->operational) { |
| l_debug("Retrying setting OperState to IF_OPER_UP"); |
| l_rtnl_set_linkmode_and_operstate(rtnl, netdev->index, |
| IF_LINK_MODE_DORMANT, IF_OPER_UP, |
| netdev_operstate_cb, |
| L_UINT_TO_PTR(netdev->index), NULL); |
| } |
| |
| new_up = netdev_get_is_up(netdev); |
| |
| if (!new_up) |
| netdev_connect_free(netdev); |
| |
| /* |
| * If mac_change_cmd_id is set we are in the process of changing the |
| * MAC address and this event is a result of powering down/up. In this |
| * case we do not want to emit a netdev DOWN/UP event as this would |
| * cause other modules to behave as such. We do, however, want to emit |
| * address changes so other modules get the new MAC address updated. |
| */ |
| if (old_up != new_up && !netdev->mac_change_cmd_id) |
| WATCHLIST_NOTIFY(&netdev_watches, netdev_watch_func_t, |
| netdev, new_up ? NETDEV_WATCH_EVENT_UP : |
| NETDEV_WATCH_EVENT_DOWN); |
| |
| if (strcmp(old_name, netdev->name)) |
| WATCHLIST_NOTIFY(&netdev_watches, netdev_watch_func_t, |
| netdev, NETDEV_WATCH_EVENT_NAME_CHANGE); |
| |
| if (memcmp(old_addr, netdev->addr, ETH_ALEN)) |
| WATCHLIST_NOTIFY(&netdev_watches, netdev_watch_func_t, |
| netdev, NETDEV_WATCH_EVENT_ADDRESS_CHANGE); |
| } |
| |
| static void netdev_dellink_notify(const struct ifinfomsg *ifi, int bytes) |
| { |
| struct netdev *netdev; |
| |
| if (ifi->ifi_family == AF_BRIDGE) { |
| netdev_bridge_port_event(ifi, bytes, false); |
| return; |
| } |
| |
| netdev = l_queue_remove_if(netdev_list, netdev_match, |
| L_UINT_TO_PTR(ifi->ifi_index)); |
| if (!netdev) |
| return; |
| |
| netdev_free(netdev); |
| } |
| |
| static void netdev_disable_ps_cb(struct l_genl_msg *msg, void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| int err = l_genl_msg_get_error(msg); |
| |
| netdev->power_save_cmd_id = 0; |
| |
| /* Can't do anything about it but inform the user */ |
| if (err < 0) |
| l_error("Failed to disable power save for ifindex %u (%s: %d)", |
| netdev->index, strerror(-err), err); |
| else |
| l_debug("Disabled power save for ifindex %u", netdev->index); |
| |
| WATCHLIST_NOTIFY(&netdev_watches, netdev_watch_func_t, |
| netdev, NETDEV_WATCH_EVENT_NEW); |
| netdev->events_ready = true; |
| } |
| |
| static bool netdev_disable_power_save(struct netdev *netdev) |
| { |
| struct l_genl_msg *msg = l_genl_msg_new(NL80211_CMD_SET_POWER_SAVE); |
| uint32_t disabled = NL80211_PS_DISABLED; |
| |
| l_genl_msg_append_attr(msg, NL80211_ATTR_IFINDEX, 4, &netdev->index); |
| l_genl_msg_append_attr(msg, NL80211_ATTR_PS_STATE, 4, &disabled); |
| |
| netdev->power_save_cmd_id = l_genl_family_send(nl80211, msg, |
| netdev_disable_ps_cb, |
| netdev, NULL); |
| if (!netdev->power_save_cmd_id) { |
| l_error("Failed to send SET_POWER_SAVE (-EIO)"); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| static void netdev_initial_up_cb(int error, uint16_t type, const void *data, |
| uint32_t len, void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| |
| netdev->set_powered_cmd_id = 0; |
| |
| if (!error) |
| netdev->ifi_flags |= IFF_UP; |
| else { |
| l_error("Error bringing interface %i up: %s", netdev->index, |
| strerror(-error)); |
| |
| if (error != -ERFKILL) |
| return; |
| } |
| |
| l_rtnl_set_linkmode_and_operstate(rtnl, netdev->index, |
| IF_LINK_MODE_DORMANT, IF_OPER_DOWN, |
| netdev_operstate_cb, |
| L_UINT_TO_PTR(netdev->index), NULL); |
| |
| /* |
| * we don't know the initial status of the 4addr property on this |
| * netdev, therefore we set it to zero by default. |
| */ |
| netdev_set_4addr(netdev, netdev->use_4addr, NULL, NULL, NULL); |
| |
| l_debug("Interface %i initialized", netdev->index); |
| |
| scan_wdev_add(netdev->wdev_id); |
| |
| if (wiphy_power_save_disabled(netdev->wiphy)) { |
| /* Wait to issue EVENT_NEW until power save is disabled */ |
| if (netdev_disable_power_save(netdev)) |
| return; |
| } |
| |
| WATCHLIST_NOTIFY(&netdev_watches, netdev_watch_func_t, |
| netdev, NETDEV_WATCH_EVENT_NEW); |
| netdev->events_ready = true; |
| } |
| |
| static bool netdev_check_set_mac(struct netdev *netdev) |
| { |
| if (l_memeqzero(netdev->set_mac_once, 6)) |
| return false; |
| |
| l_debug("Setting initial address on ifindex: %d to: " MAC, |
| netdev->index, MAC_STR(netdev->set_mac_once)); |
| netdev->set_powered_cmd_id = |
| l_rtnl_set_mac(rtnl, netdev->index, netdev->set_mac_once, true, |
| netdev_initial_up_cb, netdev, NULL); |
| memset(netdev->set_mac_once, 0, 6); |
| return true; |
| } |
| |
| static void netdev_initial_down_cb(int error, uint16_t type, const void *data, |
| uint32_t len, void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| |
| if (!error) |
| netdev->ifi_flags &= ~IFF_UP; |
| else { |
| l_error("Error taking interface %i down: %s", netdev->index, |
| strerror(-error)); |
| |
| netdev->set_powered_cmd_id = 0; |
| return; |
| } |
| |
| if (netdev_check_set_mac(netdev)) |
| return; |
| |
| netdev->set_powered_cmd_id = |
| l_rtnl_set_powered(rtnl, netdev->index, true, |
| netdev_initial_up_cb, netdev, NULL); |
| } |
| |
| static void netdev_getlink_cb(int error, uint16_t type, const void *data, |
| uint32_t len, void *user_data) |
| { |
| struct netdev *netdev = user_data; |
| const struct ifinfomsg *ifi = data; |
| unsigned int bytes; |
| l_netlink_command_func_t cb; |
| bool powered; |
| |
| netdev->get_link_cmd_id = 0; |
| |
| if (error != 0) { |
| l_error("RTM_GETLINK error %i: %s", error, strerror(-error)); |
| return; |
| } |
| |
| if (ifi->ifi_type != ARPHRD_ETHER || type != RTM_NEWLINK) { |
| l_debug("Non-ethernet address or not newlink message -- " |
| "ifi_type: %i, type: %i", ifi->ifi_type, type); |
| return; |
| } |
| |
| if (L_WARN_ON((uint32_t)ifi->ifi_index != netdev->index)) |
| return; |
| |
| bytes = len - NLMSG_ALIGN(sizeof(struct ifinfomsg)); |
| |
| netdev_newlink_notify(ifi, bytes); |
| |
| /* |
| * If the interface is UP, reset it to ensure a clean state. |
| * Otherwise, if we need to set a random mac, do so. If not, just |
| * bring the interface UP. |
| */ |
| powered = netdev_get_is_up(netdev); |
| |
| if (!powered && netdev_check_set_mac(netdev)) |
| return; |
| |
| cb = powered ? netdev_initial_down_cb : netdev_initial_up_cb; |
| |
| netdev->set_powered_cmd_id = |
| l_rtnl_set_powered(rtnl, ifi->ifi_index, !powered, cb, netdev, |
| NULL); |
| } |
| |
| static struct l_io *pae_open(uint32_t ifindex) |
| { |
| /* |
| * BPF filter to match skb->dev->type == 1 (ARPHRD_ETHER) and |
| * match skb->protocol == 0x888e (PAE) or 0x88c7 (preauthentication). |
| */ |
| struct sock_filter pae_filter[] = { |
| { 0x20, 0, 0, 0xfffff008 }, /* ld #ifidx */ |
| { 0x15, 0, 6, 0x00000000 }, /* jne #0, drop */ |
| { 0x28, 0, 0, 0xfffff01c }, /* ldh #hatype */ |
| { 0x15, 0, 4, 0x00000001 }, /* jne #1, drop */ |
| { 0x28, 0, 0, 0xfffff000 }, /* ldh #proto */ |
| { 0x15, 1, 0, 0x0000888e }, /* je #0x888e, keep */ |
| { 0x15, 0, 1, 0x000088c7 }, /* jne #0x88c7, drop */ |
| { 0x06, 0, 0, 0xffffffff }, /* keep: ret #-1 */ |
| { 0x06, 0, 0, 0000000000 }, /* drop: ret #0 */ |
| }; |
| |
| const struct sock_fprog pae_fprog = { |
| .len = L_ARRAY_SIZE(pae_filter), |
| .filter = pae_filter |
| }; |
| |
| struct l_io *io; |
| int fd; |
| |
| fd = socket(PF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, |
| htons(ETH_P_ALL)); |
| if (fd < 0) |
| return NULL; |
| |
| /* |
| * Here we modify the k value in the BPF program above to match the |
| * given ifindex. We do it this way instead of using bind to attach |
| * to a specific interface index to avoid having to re-open the fd |
| * whenever the device is powered down / up |
| */ |
| |
| pae_filter[1].k = ifindex; |
| |
| if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, |
| &pae_fprog, sizeof(pae_fprog)) < 0) |
| goto error; |
| |
| io = l_io_new(fd); |
| l_io_set_close_on_destroy(io, true); |
| |
| return io; |
| |
| error: |
| close(fd); |
| return NULL; |
| } |
| |
| static void netdev_get_link(struct netdev *netdev) |
| { |
| struct ifinfomsg ifi; |
| struct l_netlink_message *nlm = |
| l_netlink_message_new_sized(RTM_GETLINK, 0, sizeof(ifi)); |
| |
| memset(&ifi, 0, sizeof(ifi)); |
| ifi.ifi_family = AF_UNSPEC; |
| ifi.ifi_index = netdev->index; |
| |
| l_netlink_message_add_header(nlm, &ifi, sizeof(ifi)); |
| |
| netdev->get_link_cmd_id = l_netlink_send(rtnl, nlm, netdev_getlink_cb, |
| netdev, NULL); |
| L_WARN_ON(netdev->get_link_cmd_id == 0); |
| } |
| |
| struct netdev *netdev_create_from_genl(struct l_genl_msg *msg, |
| const uint8_t *set_mac) |
| { |
| const char *ifname; |
| const uint8_t *ifaddr; |
| uint32_t ifindex; |
| uint32_t iftype; |
| uint64_t wdev; |
| uint32_t wiphy_id; |
| struct netdev *netdev; |
| struct wiphy *wiphy = NULL; |
| struct l_io *pae_io = NULL; |
| |
| if (nl80211_parse_attrs(msg, NL80211_ATTR_IFINDEX, &ifindex, |
| NL80211_ATTR_WDEV, &wdev, |
| NL80211_ATTR_IFNAME, &ifname, |
| NL80211_ATTR_WIPHY, &wiphy_id, |
| NL80211_ATTR_IFTYPE, &iftype, |
| NL80211_ATTR_MAC, &ifaddr, |
| NL80211_ATTR_UNSPEC) < 0) { |
| l_warn("Required attributes missing"); |
| return NULL; |
| } |
| |
| wiphy = wiphy_find(wiphy_id); |
| if (!wiphy) { |
| l_warn("No wiphy: %d", wiphy_id); |
| return NULL; |
| } |
| |
| if (netdev_find(ifindex)) { |
| l_debug("Skipping duplicate netdev %s[%d]", ifname, ifindex); |
| return NULL; |
| } |
| |
| if (!wiphy_control_port_enabled(wiphy)) { |
| pae_io = pae_open(ifindex); |
| if (!pae_io) { |
| l_error("Unable to open PAE interface"); |
| return NULL; |
| } |
| } |
| |
| netdev = l_new(struct netdev, 1); |
| netdev->index = ifindex; |
| netdev->wdev_id = wdev; |
| netdev->type = iftype; |
| netdev->rekey_offload_support = true; |
| memcpy(netdev->addr, ifaddr, sizeof(netdev->addr)); |
| l_strlcpy(netdev->name, ifname, IFNAMSIZ); |
| netdev->wiphy = wiphy; |
| netdev->pae_over_nl80211 = pae_io == NULL; |
| netdev->low_signal_threshold = LOW_SIGNAL_THRESHOLD; |
| netdev->low_signal_threshold_5ghz = LOW_SIGNAL_THRESHOLD_5GHZ; |
| |
| if (set_mac) |
| memcpy(netdev->set_mac_once, set_mac, 6); |
| |
| if (pae_io) { |
| netdev->pae_io = pae_io; |
| l_io_set_read_handler(netdev->pae_io, netdev_pae_read, netdev, |
| netdev_pae_destroy); |
| } |
| |
| watchlist_init(&netdev->station_watches, NULL); |
| |
| l_queue_push_tail(netdev_list, netdev); |
| |
| l_debug("Created interface %s[%d %" PRIx64 "]", netdev->name, |
| netdev->index, netdev->wdev_id); |
| |
| netdev_setup_interface(netdev); |
| |
| netdev_get_link(netdev); |
| |
| /* |
| * Call the netdev-specific variant to flush only this devices PMKSA |
| * cache in the kernel. This will make IWD's cache and the kernel's |
| * cache consistent, i.e. no entries |
| * |
| * TODO: If we ever are storing PMKSA's on disk we would first need to |
| * flush, then add all the PMKSA entries at this time. |
| */ |
| netdev_flush_pmksa(netdev); |
| |
| return netdev; |
| } |
| |
| bool netdev_destroy(struct netdev *netdev) |
| { |
| if (!l_queue_remove(netdev_list, netdev)) |
| return false; |
| |
| netdev_free(netdev); |
| return true; |
| } |
| |
| static void netdev_link_notify(uint16_t type, const void *data, uint32_t len, |
| void *user_data) |
| { |
| const struct ifinfomsg *ifi = data; |
| unsigned int bytes; |
| |
| if (ifi->ifi_type != ARPHRD_ETHER) |
| return; |
| |
| l_debug("event %u on ifindex %u", type, ifi->ifi_index); |
| |
| bytes = len - NLMSG_ALIGN(sizeof(struct ifinfomsg)); |
| |
| switch (type) { |
| case RTM_NEWLINK: |
| netdev_newlink_notify(ifi, bytes); |
| break; |
| case RTM_DELLINK: |
| netdev_dellink_notify(ifi, bytes); |
| break; |
| } |
| } |
| |
| uint32_t netdev_station_watch_add(struct netdev *netdev, |
| netdev_station_watch_func_t func, void *user_data) |
| { |
| return watchlist_add(&netdev->station_watches, func, user_data, NULL); |
| } |
| |
| bool netdev_station_watch_remove(struct netdev *netdev, uint32_t id) |
| { |
| return watchlist_remove(&netdev->station_watches, id); |
| } |
| |
| uint32_t netdev_watch_add(netdev_watch_func_t func, |
| void *user_data, netdev_destroy_func_t destroy) |
| { |
| return watchlist_add(&netdev_watches, func, user_data, destroy); |
| } |
| |
| bool netdev_watch_remove(uint32_t id) |
| { |
| return watchlist_remove(&netdev_watches, id); |
| } |
| |
| static int netdev_init(void) |
| { |
| struct l_genl *genl = iwd_get_genl(); |
| const struct l_settings *settings = iwd_get_config(); |
| const char *rand_addr_str; |
| |
| if (rtnl) |
| return -EALREADY; |
| |
| rtnl = iwd_get_rtnl(); |
| |
| if (!l_netlink_register(rtnl, RTNLGRP_LINK, |
| netdev_link_notify, NULL, NULL)) { |
| l_error("Failed to register for RTNL link notifications"); |
| goto fail_netlink; |
| } |
| |
| nl80211 = l_genl_family_new(genl, NL80211_GENL_NAME); |
| if (!nl80211) { |
| l_error("Failed to obtain nl80211"); |
| goto fail_netlink; |
| } |
| |
| if (!l_settings_get_int(settings, NETDEV_ROAM_THRESHOLD, |
| &LOW_SIGNAL_THRESHOLD)) |
| LOW_SIGNAL_THRESHOLD = -70; |
| |
| if (!l_settings_get_int(settings, NETDEV_ROAM_THRESHOLD_5G, |
| &LOW_SIGNAL_THRESHOLD_5GHZ)) |
| LOW_SIGNAL_THRESHOLD_5GHZ = -76; |
| |
| if (!l_settings_get_int(settings, NETDEV_CRITICAL_ROAM_THRESHOLD, |
| &CRITICAL_SIGNAL_THRESHOLD)) |
| CRITICAL_SIGNAL_THRESHOLD = -80; |
| |
| if (!l_settings_get_int(settings, NETDEV_CRITICAL_ROAM_THRESHOLD_5G, |
| &CRITICAL_SIGNAL_THRESHOLD_5GHZ)) |
| CRITICAL_SIGNAL_THRESHOLD_5GHZ = -82; |
| |
| rand_addr_str = l_settings_get_value(settings, |
| NETDEV_ADDRESS_RANDOMIZATION); |
| if (rand_addr_str && !strcmp(rand_addr_str, "network")) |
| mac_per_ssid = true; |
| |
| watchlist_init(&netdev_watches, NULL); |
| netdev_list = l_queue_new(); |
| |
| __handshake_set_install_tk_func(netdev_set_tk); |
| __handshake_set_install_gtk_func(netdev_set_gtk); |
| __handshake_set_install_igtk_func(netdev_set_igtk); |
| __handshake_set_install_ext_tk_func(netdev_set_ext_tk); |
| |
| __eapol_set_rekey_offload_func(netdev_set_rekey_offload); |
| __eapol_set_tx_packet_func(netdev_control_port_frame); |
| __eapol_set_install_pmk_func(netdev_set_pmk); |
| |
| __ft_set_tx_frame_func(netdev_tx_ft_frame); |
| |
| __pmksa_set_driver_callbacks(netdev_pmksa_driver_add, |
| netdev_pmksa_driver_remove, |
| netdev_pmksa_driver_flush); |
| |
| unicast_watch = l_genl_add_unicast_watch(genl, NL80211_GENL_NAME, |
| netdev_unicast_notify, |
| NULL, NULL); |
| if (!unicast_watch) |
| l_error("Registering for unicast notification failed"); |
| |
| if (!l_genl_family_register(nl80211, "mlme", netdev_mlme_notify, |
| NULL, NULL)) |
| l_error("Registering for MLME notification failed"); |
| |
| if (!l_genl_family_register(nl80211, "scan", netdev_scan_notify, |
| NULL, NULL)) |
| l_error("Registering for scan notifications failed"); |
| |
| if (!l_genl_family_register(nl80211, "config", netdev_config_notify, |
| NULL, NULL)) |
| l_error("Registering for config notifications failed"); |
| |
| return 0; |
| |
| fail_netlink: |
| rtnl = NULL; |
| |
| return -EIO; |
| } |
| |
| static void netdev_exit(void) |
| { |
| struct l_genl *genl = iwd_get_genl(); |
| |
| if (!rtnl) |
| return; |
| |
| l_genl_remove_unicast_watch(genl, unicast_watch); |
| |
| watchlist_destroy(&netdev_watches); |
| l_queue_destroy(netdev_list, netdev_free); |
| netdev_list = NULL; |
| |
| l_genl_family_free(nl80211); |
| nl80211 = NULL; |
| |
| rtnl = NULL; |
| } |
| |
| void netdev_shutdown(void) |
| { |
| struct netdev *netdev; |
| |
| if (!rtnl) |
| return; |
| |
| l_queue_foreach(netdev_list, netdev_shutdown_one, NULL); |
| |
| while ((netdev = l_queue_peek_head(netdev_list))) { |
| netdev_free(netdev); |
| l_queue_pop_head(netdev_list); |
| } |
| } |
| |
| IWD_MODULE(netdev, netdev_init, netdev_exit); |
| IWD_MODULE_DEPENDS(netdev, eapol); |
| IWD_MODULE_DEPENDS(netdev, frame_xchg); |
| IWD_MODULE_DEPENDS(netdev, wiphy); |
| IWD_MODULE_DEPENDS(netdev, ft); |