1# Copyright 2019 The Chromium OS Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5import collections 6import logging 7import operator 8import re 9 10from autotest_lib.client.common_lib import error 11from autotest_lib.client.common_lib import utils 12from autotest_lib.client.common_lib.cros.network import iw_event_logger 13 14# These must mirror the values in 'iw list' output. 15CHAN_FLAG_DISABLED = 'disabled' 16CHAN_FLAG_NO_IR = 'no IR' 17CHAN_FLAG_PASSIVE_SCAN = 'passive scan' 18CHAN_FLAG_RADAR_DETECT = 'radar detection' 19DEV_MODE_AP = 'AP' 20DEV_MODE_IBSS = 'IBSS' 21DEV_MODE_MONITOR = 'monitor' 22DEV_MODE_MESH_POINT = 'mesh point' 23DEV_MODE_STATION = 'managed' 24SUPPORTED_DEV_MODES = (DEV_MODE_AP, DEV_MODE_IBSS, DEV_MODE_MONITOR, 25 DEV_MODE_MESH_POINT, DEV_MODE_STATION) 26 27class _PrintableWidth: 28 """Printable width constant objects used by packet_capturer.""" 29 def __init__(self, name): 30 self._name = name 31 32 def __repr__(self): 33 return '\'%s\'' % self._name 34 35 def __str__(self): 36 return self._name 37 38WIDTH_HT20 = _PrintableWidth('HT20') 39WIDTH_HT40_PLUS = _PrintableWidth('HT40+') 40WIDTH_HT40_MINUS = _PrintableWidth('HT40-') 41WIDTH_VHT80 = _PrintableWidth('VHT80') 42WIDTH_VHT160 = _PrintableWidth('VHT160') 43WIDTH_VHT80_80 = _PrintableWidth('VHT80+80') 44 45VHT160_CENTER_CHANNELS = ('50','114') 46 47SECURITY_OPEN = 'open' 48SECURITY_WEP = 'wep' 49SECURITY_WPA = 'wpa' 50SECURITY_WPA2 = 'wpa2' 51# Mixed mode security is WPA2/WPA 52SECURITY_MIXED = 'mixed' 53 54# Table of lookups between the output of item 'secondary channel offset:' from 55# iw <device> scan to constants. 56 57HT_TABLE = {'no secondary': WIDTH_HT20, 58 'above': WIDTH_HT40_PLUS, 59 'below': WIDTH_HT40_MINUS} 60 61IwBand = collections.namedtuple( 62 'Band', ['num', 'frequencies', 'frequency_flags', 'mcs_indices']) 63IwBss = collections.namedtuple('IwBss', ['bss', 'frequency', 'ssid', 'security', 64 'width', 'signal']) 65IwNetDev = collections.namedtuple('IwNetDev', ['phy', 'if_name', 'if_type']) 66IwTimedScan = collections.namedtuple('IwTimedScan', ['time', 'bss_list']) 67 68# The fields for IwPhy are as follows: 69# name: string name of the phy, such as "phy0" 70# bands: list of IwBand objects. 71# modes: List of strings containing interface modes supported, such as "AP". 72# commands: List of strings containing nl80211 commands supported, such as 73# "authenticate". 74# features: List of strings containing nl80211 features supported, such as 75# "T-DLS". 76# max_scan_ssids: Maximum number of SSIDs which can be scanned at once. 77IwPhy = collections.namedtuple( 78 'Phy', ['name', 'bands', 'modes', 'commands', 'features', 79 'max_scan_ssids', 'avail_tx_antennas', 'avail_rx_antennas', 80 'supports_setting_antenna_mask', 'support_vht']) 81 82DEFAULT_COMMAND_IW = 'iw' 83 84# Redirect stderr to stdout on Cros since adb commands cannot distinguish them 85# on Brillo. 86IW_TIME_COMMAND_FORMAT = '(time -p %s) 2>&1' 87IW_TIME_COMMAND_OUTPUT_START = 'real' 88 89IW_LINK_KEY_BEACON_INTERVAL = 'beacon int' 90IW_LINK_KEY_DTIM_PERIOD = 'dtim period' 91IW_LINK_KEY_FREQUENCY = 'freq' 92IW_LINK_KEY_SIGNAL = 'signal' 93IW_LINK_KEY_RX_BITRATE = 'rx bitrate' 94IW_LINK_KEY_RX_DROPS = 'rx drop misc' 95IW_LINK_KEY_RX_PACKETS = 'rx packets' 96IW_LINK_KEY_TX_BITRATE = 'tx bitrate' 97IW_LINK_KEY_TX_FAILURES = 'tx failed' 98IW_LINK_KEY_TX_PACKETS = 'tx packets' 99IW_LINK_KEY_TX_RETRIES = 'tx retries' 100IW_LOCAL_EVENT_LOG_FILE = './debug/iw_event_%d.log' 101 102# Strings from iw/util.c describing supported HE features 103HE_MAC_PLUS_HTC_HE = '+HTC HE Supported' 104HE_MAC_TWT_REQUESTER = 'TWT Requester' 105HE_MAC_TWT_RESPONDER = 'TWT Responder' 106HE_MAC_DYNAMIC_BA_FRAGMENTATION = 'Dynamic BA Fragementation Level' 107HE_MAC_MAX_MSDUS = 'Maximum number of MSDUS Fragments' 108HE_MAC_MIN_PAYLOAD_128 = 'Minimum Payload size of 128 bytes' 109HE_MAC_TRIGGER_FRAME_PADDING = 'Trigger Frame MAC Padding Duration' 110HE_MAC_MULTI_TID_AGGREGATION = 'Multi-TID Aggregation Support' 111HE_MAC_ALL_ACK = 'All Ack' 112HE_MAC_TRS = 'TRS' 113HE_MAC_BSR = 'BSR' 114HE_MAC_TWT_BROADCAST = 'Broadcast TWT' 115HE_MAC_32_BIT_BA_BITMAP = '32-bit BA Bitmap' 116HE_MAC_MU_CASCADING = 'MU Cascading' 117HE_MAC_ACK_AGGREGATION = 'Ack-Enabled Aggregation' 118HE_MAC_OM_CONTROL = 'OM Control' 119HE_MAC_OFDMA_RA = 'OFDMA RA' 120HE_MAC_MAX_AMPDU_LENGTH_EXPONENT = 'Maximum A-MPDU Length Exponent' 121HE_MAC_AMSDU_FRAGMENTATION = 'A-MSDU Fragmentation' 122HE_MAC_FLEXIBLE_TWT = 'Flexible TWT Scheduling' 123HE_MAC_RX_CONTROL_FRAME_TO_MULTIBSS = 'RX Control Frame to MultiBSS' 124HE_MAC_BSRP_BQRP_AMPDU_AGGREGATION = 'BSRP BQRP A-MPDU Aggregation' 125HE_MAC_QTP = 'QTP' 126HE_MAC_BQR = 'BQR' 127HE_MAC_SRP_RESPONDER_ROLE = 'SRP Responder Role' 128HE_MAC_NDP_FEEDBACK_REPORT = 'NDP Feedback Report' 129HE_MAC_OPS = 'OPS' 130HE_MAC_AMSDU_IN_AMPDU = 'A-MSDU in A-MPDU' 131HE_MAC_MULTI_TID_AGGREGATION_TX = 'Multi-TID Aggregation TX' 132HE_MAC_SUBCHANNEL_SELECTIVE = 'HE Subchannel Selective Transmission' 133HE_MAC_UL_2X966_TONE_RU = 'UL 2x996-Tone RU' 134HE_MAC_OM_CONTROL_DISABLE_RX = 'OM Control UL MU Data Disable RX' 135 136HE_PHY_24HE40 = 'HE40/2.4GHz' 137HE_PHY_5HE40_80 = 'HE40/HE80/5GHz' 138HE_PHY_5HE160 = 'HE160/5GHz' 139HE_PHY_5HE160_80_80 = 'HE160/HE80+80/5GHz' 140HE_PHY_242_TONE_RU_24 = '242 tone RUs/2.4GHz' 141HE_PHY_242_TONE_RU_5 = '242 tone RUs/5GHz' 142HE_PHY_PUNCTURED_PREAMBLE_RX = 'Punctured Preamble RX' 143HE_PHY_DEVICE_CLASS = 'Device Class' 144HE_PHY_LDPC_CODING_IN_PAYLOAD = 'LDPC Coding in Payload' 145HE_PHY_HE_SU_PPDU_1X_HE_LTF_08_GI = 'HE SU PPDU with 1x HE-LTF and 0.8us GI' 146HE_PHY_HE_MIDAMBLE_RX_MAX_NSTS = 'Midamble Rx Max NSTS' 147HE_PHY_NDP_4X_HE_LTF_32_GI = 'NDP with 4x HE-LTF and 3.2us GI' 148HE_PHY_STBC_TX_LEQ_80 = 'STBC Tx <= 80MHz' 149HE_PHY_STBC_RX_LEQ_80 = 'STBC Rx <= 80MHz' 150HE_PHY_DOPPLER_TX = 'Doppler Tx' 151HE_PHY_DOPPLER_RX = 'Doppler Rx' 152HE_PHY_FULL_BAND_UL_MU_MIMO = 'Full Bandwidth UL MU-MIMO' 153HE_PHY_PART_BAND_UL_MU_MIMO = 'Partial Bandwidth UL MU-MIMO' 154HE_PHY_DCM_MAX_CONSTELLATION = 'DCM Max Constellation' 155HE_PHY_DCM_MAX_NSS_TX = 'DCM Max NSS Tx' 156HE_PHY_DCM_MAX_CONSTELLATION_RX = 'DCM Max Constellation Rx' 157HE_PHY_DCM_MAX_NSS_RX = 'DCM Max NSS Rx' 158HE_PHY_RX_MU_PPDU_FROM_NON_AP = 'Rx HE MU PPDU from Non-AP STA' 159HE_PHY_SU_BEAMFORMER = 'SU Beamformer' 160HE_PHY_SU_BEAMFORMEE = 'SU Beamformee' 161HE_PHY_MU_BEAMFORMER = 'MU Beamformer' 162HE_PHY_BEAMFORMEE_STS_LEQ_80 = 'Beamformee STS <= 80Mhz' 163HE_PHY_BEAMFORMEE_STS_GT_80 = 'Beamformee STS > 80Mhz' 164HE_PHY_SOUNDING_DIMENSIONS_LEQ_80 = 'Sounding Dimensions <= 80Mhz' 165HE_PHY_SOUNDING_DIMENSIONS_GT_80 = 'Sounding Dimensions > 80Mhz' 166HE_PHY_NG_EQ_16_SU_FB = 'Ng = 16 SU Feedback' 167HE_PHY_NG_EQ_16_MU_FB = 'Ng = 16 MU Feedback' 168HE_PHY_CODEBOOK_SIZE_SU_FB = 'Codebook Size SU Feedback' 169HE_PHY_CODEBOOK_SIZE_MU_FB = 'Codebook Size MU Feedback' 170HE_PHY_TRIGGERED_SU_BEAMFORMING_FB = 'Triggered SU Beamforming Feedback' 171HE_PHY_TRIGGERED_MU_BEAMFORMING_FB = 'Triggered MU Beamforming Feedback' 172HE_PHY_TRIGGERED_CQI_FB = 'Triggered CQI Feedback' 173HE_PHY_PART_BAND_EXT_RANGE = 'Partial Bandwidth Extended Range' 174HE_PHY_PART_BAND_DL_MU_MIMO = 'Partial Bandwidth DL MU-MIMO' 175HE_PHY_PPE_THRESHOLD = 'PPE Threshold Present' 176HE_PHY_SRP_SR = 'SRP-based SR' 177HE_PHY_POWER_BOOST_FACTOR_AR = 'Power Boost Factor ar' 178HE_PHY_SU_PPDU_4X_HE_LTF_08_GI = 'HE SU PPDU & HE PPDU 4x HE-LTF 0.8us GI' 179HE_PHY_MAX_NC = 'Max NC' 180HE_PHY_STBC_TX_GT_80 = 'STBC Tx > 80MHz' 181HE_PHY_STBC_RX_GT_80 = 'STBC Rx > 80MHz' 182HE_PHY_ER_SU_PPDU_4X_HE_LTF_08_GI = 'HE ER SU PPDU 4x HE-LTF 0.8us GI' 183HE_PHY_20_IN_44_PPDU_24 = '20MHz in 40MHz HE PPDU 2.4GHz' 184HE_PHY_20_IN_160_80_80 = '20MHz in 160/80+80MHz HE PPDU' 185HE_PHY_80_IN_160_80_80 = '80MHz in 160/80+80MHz HE PPDU' 186HE_PHY_ER_SU_PPDU_1X_HE_LTF_08_GI = 'HE ER SU PPDU 1x HE-LTF 0.8us GI' 187HE_PHY_MIDAMBLE_RX_2X_AND_1X_HE_LTF = 'Midamble Rx 2x & 1x HE-LTF' 188HE_PHY_DCM_MAX_BW = 'DCM Max BW' 189HE_PHY_LONGER_THAN_16HE_OFDM_SYM = 'Longer Than 16HE SIG-B OFDM Symbols' 190HE_PHY_NON_TRIGGERED_CQI_FB = 'Non-Triggered CQI Feedback' 191HE_PHY_TX_1024_QAM = 'TX 1024-QAM' 192HE_PHY_RX_1024_QAM = 'RX 1024-QAM' 193HE_PHY_RX_FULL_BW_SU_USING_MU_COMPRESSION_SIGB = \ 194 'RX Full BW SU Using HE MU PPDU with Compression SIGB' 195HE_PHY_RX_FULL_BW_SU_USING_MU_NON_COMPRESSION_SIGB = \ 196 'RX Full BW SU Using HE MU PPDU with Non-Compression SIGB' 197 198 199def _get_all_link_keys(link_information): 200 """Parses link or station dump output for link key value pairs. 201 202 Link or station dump information is in the format below: 203 204 Connected to 74:e5:43:10:4f:c0 (on wlan0) 205 SSID: PMKSACaching_4m9p5_ch1 206 freq: 5220 207 RX: 5370 bytes (37 packets) 208 TX: 3604 bytes (15 packets) 209 signal: -59 dBm 210 tx bitrate: 13.0 MBit/s MCS 1 211 212 bss flags: short-slot-time 213 dtim period: 5 214 beacon int: 100 215 216 @param link_information: string containing the raw link or station dump 217 information as reported by iw. Note that this parsing assumes a single 218 entry, in the case of multiple entries (e.g. listing stations from an 219 AP, or listing mesh peers), the entries must be split on a per 220 peer/client basis before this parsing operation. 221 @return a dictionary containing all the link key/value pairs. 222 223 """ 224 link_key_value_pairs = {} 225 keyval_regex = re.compile(r'^\s+(.*):\s+(.*)$') 226 for link_key in link_information.splitlines()[1:]: 227 match = re.search(keyval_regex, link_key) 228 if match: 229 # Station dumps can contain blank lines. 230 link_key_value_pairs[match.group(1)] = match.group(2) 231 return link_key_value_pairs 232 233 234def _extract_bssid(link_information, interface_name, station_dump=False): 235 """Get the BSSID that |interface_name| is associated with. 236 237 See doc for _get_all_link_keys() for expected format of the station or link 238 information entry. 239 240 @param link_information: string containing the raw link or station dump 241 information as reported by iw. Note that this parsing assumes a single 242 entry, in the case of multiple entries (e.g. listing stations from an AP 243 or listing mesh peers), the entries must be split on a per peer/client 244 basis before this parsing operation. 245 @param interface_name: string name of interface (e.g. 'wlan0'). 246 @param station_dump: boolean indicator of whether the link information is 247 from a 'station dump' query. If False, it is assumed the string is from 248 a 'link' query. 249 @return string bssid of the current association, or None if no matching 250 association information is found. 251 252 """ 253 # We're looking for a line like this when parsing the output of a 'link' 254 # query: 255 # Connected to 04:f0:21:03:7d:bb (on wlan0) 256 # We're looking for a line like this when parsing the output of a 257 # 'station dump' query: 258 # Station 04:f0:21:03:7d:bb (on mesh-5000mhz) 259 identifier = 'Station' if station_dump else 'Connected to' 260 search_re = r'%s ([0-9a-fA-F:]{17}) \(on %s\)' % (identifier, 261 interface_name) 262 match = re.match(search_re, link_information) 263 if match is None: 264 return None 265 return match.group(1) 266 267 268class IwRunner(object): 269 """Defines an interface to the 'iw' command.""" 270 271 272 def __init__(self, remote_host=None, command_iw=DEFAULT_COMMAND_IW): 273 self._run = utils.run 274 self._host = remote_host 275 if remote_host: 276 self._run = remote_host.run 277 self._command_iw = command_iw 278 self._log_id = 0 279 280 281 def _parse_scan_results(self, output): 282 """Parse the output of the 'scan' and 'scan dump' commands. 283 284 Here is an example of what a single network would look like for 285 the input parameter. Some fields have been removed in this example: 286 BSS 00:11:22:33:44:55(on wlan0) 287 freq: 2447 288 beacon interval: 100 TUs 289 signal: -46.00 dBm 290 Information elements from Probe Response frame: 291 SSID: my_open_network 292 Extended supported rates: 24.0 36.0 48.0 54.0 293 HT capabilities: 294 Capabilities: 0x0c 295 HT20 296 HT operation: 297 * primary channel: 8 298 * secondary channel offset: no secondary 299 * STA channel width: 20 MHz 300 RSN: * Version: 1 301 * Group cipher: CCMP 302 * Pairwise ciphers: CCMP 303 * Authentication suites: PSK 304 * Capabilities: 1-PTKSA-RC 1-GTKSA-RC (0x0000) 305 306 @param output: string command output. 307 308 @returns a list of IwBss namedtuples; None if the scan fails 309 310 """ 311 bss = None 312 frequency = None 313 ssid = None 314 ht = None 315 vht = None 316 signal = None 317 security = None 318 supported_securities = [] 319 bss_list = [] 320 # TODO(crbug.com/1032892): The parsing logic here wasn't really designed 321 # for the presence of multiple information elements like HT, VHT, and 322 # (eventually) HE. We should eventually update it to check that we are 323 # in the right section (e.g., verify the '* channel width' match is a 324 # match in the VHT section and not a different section). Also, we should 325 # probably add in VHT20, and VHT40 whenever we finish this bug. 326 for line in output.splitlines(): 327 line = line.strip() 328 bss_match = re.match('BSS ([0-9a-f:]+)', line) 329 if bss_match: 330 if bss != None: 331 security = self.determine_security(supported_securities) 332 iwbss = IwBss(bss, frequency, ssid, security, 333 vht if vht else ht, signal) 334 bss_list.append(iwbss) 335 bss = frequency = ssid = security = ht = vht = None 336 supported_securities = [] 337 bss = bss_match.group(1) 338 if line.startswith('freq:'): 339 frequency = int(line.split()[1]) 340 if line.startswith('signal:'): 341 signal = float(line.split()[1]) 342 if line.startswith('SSID: '): 343 _, ssid = line.split(': ', 1) 344 if line.startswith('* secondary channel offset'): 345 ht = HT_TABLE[line.split(':')[1].strip()] 346 # Checking for the VHT channel width based on IEEE 802.11-2016 347 # Table 9-252. 348 if line.startswith('* channel width:'): 349 chan_width_subfield = line.split(':')[1].strip()[0] 350 if chan_width_subfield == '1': 351 vht = WIDTH_VHT80 352 # 2 and 3 are deprecated but are included here for older APs. 353 if chan_width_subfield == '2': 354 vht = WIDTH_VHT160 355 if chan_width_subfield == '3': 356 vht = WIDTH_VHT80_80 357 if line.startswith('* center freq segment 2:'): 358 center_chan_two = line.split(':')[1].strip() 359 if vht == WIDTH_VHT80: 360 if center_chan_two in VHT160_CENTER_CHANNELS: 361 vht = WIDTH_VHT160 362 elif center_chan_two != '0': 363 vht = WIDTH_VHT80_80 364 if line.startswith('WPA'): 365 supported_securities.append(SECURITY_WPA) 366 if line.startswith('RSN'): 367 supported_securities.append(SECURITY_WPA2) 368 security = self.determine_security(supported_securities) 369 bss_list.append(IwBss(bss, frequency, ssid, security, 370 vht if vht else ht, signal)) 371 return bss_list 372 373 374 def _parse_scan_time(self, output): 375 """ 376 Parse the scan time in seconds from the output of the 'time -p "scan"' 377 command. 378 379 'time -p' Command output format is below: 380 real 0.01 381 user 0.01 382 sys 0.00 383 384 @param output: string command output. 385 386 @returns float time in seconds. 387 388 """ 389 output_lines = output.splitlines() 390 for line_num, line in enumerate(output_lines): 391 line = line.strip() 392 if (line.startswith(IW_TIME_COMMAND_OUTPUT_START) and 393 output_lines[line_num + 1].startswith('user') and 394 output_lines[line_num + 2].startswith('sys')): 395 return float(line.split()[1]) 396 raise error.TestFail('Could not parse scan time.') 397 398 399 def add_interface(self, phy, interface, interface_type): 400 """ 401 Add an interface to a WiFi PHY. 402 403 @param phy: string name of PHY to add an interface to. 404 @param interface: string name of interface to add. 405 @param interface_type: string type of interface to add (e.g. 'monitor'). 406 407 """ 408 self._run('%s phy %s interface add %s type %s' % 409 (self._command_iw, phy, interface, interface_type)) 410 411 412 def disconnect_station(self, interface): 413 """ 414 Disconnect a STA from a network. 415 416 @param interface: string name of interface to disconnect. 417 418 """ 419 self._run('%s dev %s disconnect' % (self._command_iw, interface)) 420 421 422 def get_current_bssid(self, interface_name): 423 """Get the BSSID that |interface_name| is associated with. 424 425 @param interface_name: string name of interface (e.g. 'wlan0'). 426 @return string bssid of our current association, or None. 427 428 """ 429 result = self._run('%s dev %s link' % 430 (self._command_iw, interface_name), 431 ignore_status=True) 432 if result.exit_status: 433 # See comment in get_link_value. 434 return None 435 436 return _extract_bssid(result.stdout, interface_name) 437 438 439 def get_interface(self, interface_name): 440 """Get full information about an interface given an interface name. 441 442 @param interface_name: string name of interface (e.g. 'wlan0'). 443 @return IwNetDev tuple. 444 445 """ 446 matching_interfaces = [iw_if for iw_if in self.list_interfaces() 447 if iw_if.if_name == interface_name] 448 if len(matching_interfaces) != 1: 449 raise error.TestFail('Could not find interface named %s' % 450 interface_name) 451 452 return matching_interfaces[0] 453 454 455 def get_link_value(self, interface, iw_link_key): 456 """Get the value of a link property for |interface|. 457 458 Checks the link using iw, and parses the result to return a link key. 459 460 @param iw_link_key: string one of IW_LINK_KEY_* defined above. 461 @param interface: string desired value of iw link property. 462 @return string containing the corresponding link property value, None 463 if there was a parsing error or the iw command failed. 464 465 """ 466 result = self._run('%s dev %s link' % (self._command_iw, interface), 467 ignore_status=True) 468 if result.exit_status: 469 # When roaming, there is a period of time for mac80211 based drivers 470 # when the driver is 'associated' with an SSID but not a particular 471 # BSS. This causes iw to return an error code (-2) when attempting 472 # to retrieve information specific to the BSS. This does not happen 473 # in mwifiex drivers. 474 return None 475 actual_value = _get_all_link_keys(result.stdout).get(iw_link_key) 476 if actual_value is not None: 477 logging.info('Found iw link key %s with value %s.', 478 iw_link_key, actual_value) 479 return actual_value 480 481 482 def get_station_dump(self, interface): 483 """Gets information about connected peers. 484 485 Returns information about the currently connected peers. When the host 486 is in station mode, it returns a single entry, with information about 487 the link to the AP it is currently connected to. If the host is in mesh 488 or AP mode, it can return multiple entries, one for each connected 489 station, or mesh peer. 490 491 @param interface: string name of interface to get peer information 492 from. 493 @return a list of dictionaries with link information about each 494 connected peer (ordered by peer mac address). 495 496 """ 497 result = self._run('%s dev %s station dump' % 498 (self._command_iw, interface)) 499 parts = re.split(r'^Station ', result.stdout, flags=re.MULTILINE)[1:] 500 peer_list_raw = ['Station ' + x for x in parts] 501 parsed_peer_info = [] 502 503 for peer in peer_list_raw: 504 peer_link_keys = _get_all_link_keys(peer) 505 rssi_str = peer_link_keys.get(IW_LINK_KEY_SIGNAL, '0') 506 rssi_int = int(rssi_str.split()[0]) 507 508 tx_bitrate = peer_link_keys.get(IW_LINK_KEY_TX_BITRATE, '0') 509 tx_failures = int(peer_link_keys.get(IW_LINK_KEY_TX_FAILURES, 0)) 510 tx_packets = int(peer_link_keys.get(IW_LINK_KEY_TX_PACKETS, 0)) 511 tx_retries = int(peer_link_keys.get(IW_LINK_KEY_TX_RETRIES, 0)) 512 513 rx_bitrate = peer_link_keys.get(IW_LINK_KEY_RX_BITRATE, '0') 514 rx_drops = int(peer_link_keys.get(IW_LINK_KEY_RX_DROPS, 0)) 515 rx_packets = int(peer_link_keys.get(IW_LINK_KEY_RX_PACKETS, 0)) 516 517 mac = _extract_bssid(link_information=peer, 518 interface_name=interface, 519 station_dump=True) 520 521 # If any of these are missing, they will be None 522 peer_info = {'rssi_int': rssi_int, 523 'rssi_str': rssi_str, 524 'tx_bitrate': tx_bitrate, 525 'tx_failures': tx_failures, 526 'tx_packets': tx_packets, 527 'tx_retries': tx_retries, 528 'rx_bitrate': rx_bitrate, 529 'rx_drops': rx_drops, 530 'rx_packets': rx_packets, 531 'mac': mac} 532 533 # don't evaluate if tx_packets 0 534 if tx_packets: 535 peer_info['tx_retry_rate'] = tx_retries / float(tx_packets) 536 peer_info['tx_failure_rate'] = tx_failures / float(tx_packets) 537 538 # don't evaluate if rx_packets is 0 539 if rx_packets: 540 peer_info['rx_drop_rate'] = rx_drops / float(rx_packets) 541 542 parsed_peer_info.append(peer_info) 543 return sorted(parsed_peer_info, key=operator.itemgetter('mac')) 544 545 546 def get_operating_mode(self, interface): 547 """Gets the operating mode for |interface|. 548 549 @param interface: string name of interface to get peer information 550 about. 551 552 @return string one of DEV_MODE_* defined above, or None if no mode is 553 found, or if an unsupported mode is found. 554 555 """ 556 ret = self._run('%s dev %s info' % (self._command_iw, interface)) 557 mode_regex = r'^\s*type (.*)$' 558 match = re.search(mode_regex, ret.stdout, re.MULTILINE) 559 if match: 560 operating_mode = match.group(1) 561 if operating_mode in SUPPORTED_DEV_MODES: 562 return operating_mode 563 logging.warning( 564 'Unsupported operating mode %s found for interface: %s. ' 565 'Supported modes: %s', operating_mode, interface, 566 SUPPORTED_DEV_MODES) 567 return None 568 569 570 def get_radio_config(self, interface): 571 """Gets the channel information of a specfic interface using iw. 572 573 @param interface: string name of interface to get radio information 574 from. 575 576 @return dictionary containing the channel information. 577 578 """ 579 channel_config = {} 580 ret = self._run('%s dev %s info' % (self._command_iw, interface)) 581 channel_config_regex = (r'^\s*channel ([0-9]+) \(([0-9]+) MHz\), ' 582 'width: ([2,4,8]0) MHz, center1: ([0-9]+) MHz') 583 match = re.search(channel_config_regex, ret.stdout, re.MULTILINE) 584 585 if match: 586 channel_config['number'] = int(match.group(1)) 587 channel_config['freq'] = int(match.group(2)) 588 channel_config['width'] = int(match.group(3)) 589 channel_config['center1_freq'] = int(match.group(4)) 590 591 return channel_config 592 593 594 def ibss_join(self, interface, ssid, frequency): 595 """ 596 Join a WiFi interface to an IBSS. 597 598 @param interface: string name of interface to join to the IBSS. 599 @param ssid: string SSID of IBSS to join. 600 @param frequency: int frequency of IBSS in Mhz. 601 602 """ 603 self._run('%s dev %s ibss join %s %d' % 604 (self._command_iw, interface, ssid, frequency)) 605 606 607 def ibss_leave(self, interface): 608 """ 609 Leave an IBSS. 610 611 @param interface: string name of interface to remove from the IBSS. 612 613 """ 614 self._run('%s dev %s ibss leave' % (self._command_iw, interface)) 615 616 617 def list_interfaces(self, desired_if_type=None): 618 """List WiFi related interfaces on this system. 619 620 @param desired_if_type: string type of interface to filter 621 our returned list of interfaces for (e.g. 'managed'). 622 623 @return list of IwNetDev tuples. 624 625 """ 626 627 # Parse output in the following format: 628 # 629 # $ adb shell iw dev 630 # phy#0 631 # Unnamed/non-netdev interface 632 # wdev 0x2 633 # addr aa:bb:cc:dd:ee:ff 634 # type P2P-device 635 # Interface wlan0 636 # ifindex 4 637 # wdev 0x1 638 # addr aa:bb:cc:dd:ee:ff 639 # ssid Whatever 640 # type managed 641 642 output = self._run('%s dev' % self._command_iw).stdout 643 interfaces = [] 644 phy = None 645 if_name = None 646 if_type = None 647 for line in output.splitlines(): 648 m = re.match('phy#([0-9]+)', line) 649 if m: 650 phy = 'phy%d' % int(m.group(1)) 651 if_name = None 652 if_type = None 653 continue 654 if not phy: 655 continue 656 m = re.match('[\s]*Interface (.*)', line) 657 if m: 658 if_name = m.group(1) 659 continue 660 if not if_name: 661 continue 662 # Common values for type are 'managed', 'monitor', and 'IBSS'. 663 m = re.match('[\s]*type ([a-zA-Z]+)', line) 664 if m: 665 if_type = m.group(1) 666 interfaces.append(IwNetDev(phy=phy, if_name=if_name, 667 if_type=if_type)) 668 # One phy may have many interfaces, so don't reset it. 669 if_name = None 670 671 if desired_if_type: 672 interfaces = [interface for interface in interfaces 673 if interface.if_type == desired_if_type] 674 return interfaces 675 676 677 def list_phys(self): 678 """ 679 List WiFi PHYs on the given host. 680 681 @return list of IwPhy tuples. 682 683 """ 684 output = self._run('%s list' % self._command_iw).stdout 685 686 pending_phy_name = None 687 current_band = None 688 current_section = None 689 all_phys = [] 690 691 def add_pending_phy(): 692 """Add the pending phy into |all_phys|.""" 693 bands = tuple(IwBand(band.num, 694 tuple(band.frequencies), 695 dict(band.frequency_flags), 696 tuple(band.mcs_indices)) 697 for band in pending_phy_bands) 698 new_phy = IwPhy(pending_phy_name, 699 bands, 700 tuple(pending_phy_modes), 701 tuple(pending_phy_commands), 702 tuple(pending_phy_features), 703 pending_phy_max_scan_ssids, 704 pending_phy_tx_antennas, 705 pending_phy_rx_antennas, 706 pending_phy_tx_antennas and pending_phy_rx_antennas, 707 pending_phy_support_vht) 708 all_phys.append(new_phy) 709 710 for line in output.splitlines(): 711 match_phy = re.search('Wiphy (.*)', line) 712 if match_phy: 713 if pending_phy_name: 714 add_pending_phy() 715 pending_phy_name = match_phy.group(1) 716 pending_phy_bands = [] 717 pending_phy_modes = [] 718 pending_phy_commands = [] 719 pending_phy_features = [] 720 pending_phy_max_scan_ssids = None 721 pending_phy_tx_antennas = 0 722 pending_phy_rx_antennas = 0 723 pending_phy_support_vht = False 724 continue 725 726 match_section = re.match('\s*(\w.*):\s*$', line) 727 if match_section: 728 current_section = match_section.group(1) 729 match_band = re.match('Band (\d+)', current_section) 730 if match_band: 731 current_band = IwBand(num=int(match_band.group(1)), 732 frequencies=[], 733 frequency_flags={}, 734 mcs_indices=[]) 735 pending_phy_bands.append(current_band) 736 continue 737 738 # Check for max_scan_ssids. This isn't a section, but it 739 # also isn't within a section. 740 match_max_scan_ssids = re.match('\s*max # scan SSIDs: (\d+)', 741 line) 742 if match_max_scan_ssids and pending_phy_name: 743 pending_phy_max_scan_ssids = int( 744 match_max_scan_ssids.group(1)) 745 continue 746 747 if (current_section == 'Supported interface modes' and 748 pending_phy_name): 749 mode_match = re.search('\* (\w+)', line) 750 if mode_match: 751 pending_phy_modes.append(mode_match.group(1)) 752 continue 753 754 if current_section == 'Supported commands' and pending_phy_name: 755 command_match = re.search('\* (\w+)', line) 756 if command_match: 757 pending_phy_commands.append(command_match.group(1)) 758 continue 759 760 if (current_section is not None and 761 current_section.startswith('VHT Capabilities') and 762 pending_phy_name): 763 pending_phy_support_vht = True 764 continue 765 766 match_avail_antennas = re.match('\s*Available Antennas: TX (\S+)' 767 ' RX (\S+)', line) 768 if match_avail_antennas and pending_phy_name: 769 pending_phy_tx_antennas = int( 770 match_avail_antennas.group(1), 16) 771 pending_phy_rx_antennas = int( 772 match_avail_antennas.group(2), 16) 773 continue 774 775 match_device_support = re.match('\s*Device supports (.*)\.', line) 776 if match_device_support and pending_phy_name: 777 pending_phy_features.append(match_device_support.group(1)) 778 continue 779 780 if not all([current_band, pending_phy_name, 781 line.startswith('\t')]): 782 continue 783 784 # E.g. 785 # * 2412 MHz [1] (20.0 dBm) 786 # * 2467 MHz [12] (20.0 dBm) (passive scan) 787 # * 2472 MHz [13] (disabled) 788 # * 5260 MHz [52] (19.0 dBm) (no IR, radar detection) 789 match_chan_info = re.search( 790 r'(?P<frequency>\d+) MHz' 791 r' (?P<chan_num>\[\d+\])' 792 r'(?: \((?P<tx_power_limit>[0-9.]+ dBm)\))?' 793 r'(?: \((?P<flags>[a-zA-Z, ]+)\))?', line) 794 if match_chan_info: 795 frequency = int(match_chan_info.group('frequency')) 796 current_band.frequencies.append(frequency) 797 flags_string = match_chan_info.group('flags') 798 if flags_string: 799 current_band.frequency_flags[frequency] = frozenset( 800 flags_string.split(',')) 801 else: 802 # Populate the dict with an empty set, to make 803 # things uniform for client code. 804 current_band.frequency_flags[frequency] = frozenset() 805 continue 806 807 # re_mcs needs to match something like: 808 # HT TX/RX MCS rate indexes supported: 0-15, 32 809 if re.search('HT TX/RX MCS rate indexes supported: ', line): 810 rate_string = line.split(':')[1].strip() 811 for piece in rate_string.split(','): 812 if piece.find('-') > 0: 813 # Must be a range like ' 0-15' 814 begin, end = piece.split('-') 815 for index in range(int(begin), int(end) + 1): 816 current_band.mcs_indices.append(index) 817 else: 818 # Must be a single rate like '32 ' 819 current_band.mcs_indices.append(int(piece)) 820 if pending_phy_name: 821 add_pending_phy() 822 return all_phys 823 824 825 def remove_interface(self, interface, ignore_status=False): 826 """ 827 Remove a WiFi interface from a PHY. 828 829 @param interface: string name of interface (e.g. mon0) 830 @param ignore_status: boolean True iff we should ignore failures 831 to remove the interface. 832 833 """ 834 self._run('%s dev %s del' % (self._command_iw, interface), 835 ignore_status=ignore_status) 836 837 838 def determine_security(self, supported_securities): 839 """Determines security from the given list of supported securities. 840 841 @param supported_securities: list of supported securities from scan 842 843 """ 844 if not supported_securities: 845 security = SECURITY_OPEN 846 elif len(supported_securities) == 1: 847 security = supported_securities[0] 848 else: 849 security = SECURITY_MIXED 850 return security 851 852 853 def scan(self, interface, frequencies=(), ssids=()): 854 """Performs a scan. 855 856 @param interface: the interface to run the iw command against 857 @param frequencies: list of int frequencies in Mhz to scan. 858 @param ssids: list of string SSIDs to send probe requests for. 859 860 @returns a list of IwBss namedtuples; None if the scan fails 861 862 """ 863 scan_result = self.timed_scan(interface, frequencies, ssids) 864 if scan_result is None: 865 return None 866 return scan_result.bss_list 867 868 869 def timed_scan(self, interface, frequencies=(), ssids=()): 870 """Performs a timed scan. 871 872 @param interface: the interface to run the iw command against 873 @param frequencies: list of int frequencies in Mhz to scan. 874 @param ssids: list of string SSIDs to send probe requests for. 875 876 @returns a IwTimedScan namedtuple; None if the scan fails 877 878 """ 879 freq_param = '' 880 if frequencies: 881 freq_param = ' freq %s' % ' '.join(map(str, frequencies)) 882 ssid_param = '' 883 if ssids: 884 ssid_param = ' ssid "%s"' % '" "'.join(ssids) 885 886 iw_command = '%s dev %s scan%s%s' % (self._command_iw, 887 interface, freq_param, ssid_param) 888 command = IW_TIME_COMMAND_FORMAT % iw_command 889 scan = self._run(command, ignore_status=True) 890 if scan.exit_status != 0: 891 # The device was busy 892 logging.debug('scan exit_status: %d', scan.exit_status) 893 return None 894 if not scan.stdout: 895 raise error.TestFail('Missing scan parse time') 896 897 if scan.stdout.startswith(IW_TIME_COMMAND_OUTPUT_START): 898 logging.debug('Empty scan result') 899 bss_list = [] 900 else: 901 bss_list = self._parse_scan_results(scan.stdout) 902 scan_time = self._parse_scan_time(scan.stdout) 903 return IwTimedScan(scan_time, bss_list) 904 905 906 def scan_dump(self, interface): 907 """Dump the contents of the scan cache. 908 909 Note that this does not trigger a scan. Instead, it returns 910 the kernel's idea of what BSS's are currently visible. 911 912 @param interface: the interface to run the iw command against 913 914 @returns a list of IwBss namedtuples; None if the scan fails 915 916 """ 917 result = self._run('%s dev %s scan dump' % (self._command_iw, 918 interface)) 919 return self._parse_scan_results(result.stdout) 920 921 922 def set_tx_power(self, interface, power): 923 """ 924 Set the transmission power for an interface. 925 926 @param interface: string name of interface to set Tx power on. 927 @param power: string power parameter. (e.g. 'auto'). 928 929 """ 930 self._run('%s dev %s set txpower %s' % 931 (self._command_iw, interface, power)) 932 933 934 def set_freq(self, interface, freq): 935 """ 936 Set the frequency for an interface. 937 938 @param interface: string name of interface to set frequency on. 939 @param freq: int frequency 940 941 """ 942 self._run('%s dev %s set freq %d' % 943 (self._command_iw, interface, freq)) 944 945 946 def set_regulatory_domain(self, domain_string): 947 """ 948 Set the regulatory domain of the current machine. Note that 949 the regulatory change happens asynchronously to the exit of 950 this function. 951 952 @param domain_string: string regulatory domain name (e.g. 'US'). 953 954 """ 955 self._run('%s reg set %s' % (self._command_iw, domain_string)) 956 957 958 def get_regulatory_domain(self): 959 """ 960 Get the regulatory domain of the current machine. 961 962 @returns a string containing the 2-letter regulatory domain name 963 (e.g. 'US'). 964 965 """ 966 output = self._run('%s reg get' % self._command_iw).stdout 967 m = re.search('^country (..):', output, re.MULTILINE) 968 if not m: 969 return None 970 return m.group(1) 971 972 973 def is_regulatory_self_managed(self): 974 """ 975 Determine if any WiFi device on the system manages its own regulatory 976 info (NL80211_ATTR_WIPHY_SELF_MANAGED_REG). 977 978 @returns True if self-managed, False otherwise. 979 """ 980 output = self._run('%s reg get' % self._command_iw).stdout 981 m = re.search('^phy#.*\(self-managed\)', output, re.MULTILINE) 982 return not m is None 983 984 985 def wait_for_scan_result(self, interface, bsses=(), ssids=(), 986 timeout_seconds=30, wait_for_all=False): 987 """Returns a list of IWBSS objects for given list of bsses or ssids. 988 989 This method will scan for a given timeout and return all of the networks 990 that have a matching ssid or bss. If wait_for_all is true and all 991 networks are not found within the given timeout an empty list will 992 be returned. 993 994 @param interface: which interface to run iw against 995 @param bsses: a list of BSS strings 996 @param ssids: a list of ssid strings 997 @param timeout_seconds: the amount of time to wait in seconds 998 @param wait_for_all: True to wait for all listed bsses or ssids; False 999 to return if any of the networks were found 1000 1001 @returns a list of IwBss collections that contain the given bss or ssid; 1002 if the scan is empty or returns an error code None is returned. 1003 1004 """ 1005 1006 logging.info('Performing a scan with a max timeout of %d seconds.', 1007 timeout_seconds) 1008 1009 # If the in-progress scan takes more than 30 seconds to 1010 # complete it will most likely never complete; abort. 1011 # See crbug.com/309148 1012 scan_results = list() 1013 try: 1014 scan_results = utils.poll_for_condition( 1015 condition=lambda: self.scan(interface), 1016 timeout=timeout_seconds, 1017 sleep_interval=5, # to allow in-progress scans to complete 1018 desc='Timed out getting IWBSSes that match desired') 1019 except utils.TimeoutError as e: 1020 pass 1021 1022 if not scan_results: # empty list or None 1023 return None 1024 1025 # get all IWBSSes from the scan that match any of the desired 1026 # ssids or bsses passed in 1027 matching_iwbsses = filter( 1028 lambda iwbss: iwbss.ssid in ssids or iwbss.bss in bsses, 1029 scan_results) 1030 if wait_for_all: 1031 found_bsses = [iwbss.bss for iwbss in matching_iwbsses] 1032 found_ssids = [iwbss.ssid for iwbss in matching_iwbsses] 1033 # if an expected bss or ssid was not found, and it was required 1034 # by the caller that all expected be found, return empty list 1035 if any(bss not in found_bsses for bss in bsses) or any( 1036 ssid not in found_ssids for ssid in ssids): 1037 return list() 1038 return list(matching_iwbsses) 1039 1040 1041 def set_antenna_bitmap(self, phy, tx_bitmap, rx_bitmap): 1042 """Set antenna chain mask on given phy (radio). 1043 1044 This function will set the antennas allowed to use for TX and 1045 RX on the |phy| based on the |tx_bitmap| and |rx_bitmap|. 1046 This command is only allowed when the interfaces on the phy are down. 1047 1048 @param phy: phy name 1049 @param tx_bitmap: bitmap of allowed antennas to use for TX 1050 @param rx_bitmap: bitmap of allowed antennas to use for RX 1051 1052 """ 1053 command = '%s phy %s set antenna %d %d' % (self._command_iw, phy, 1054 tx_bitmap, rx_bitmap) 1055 self._run(command) 1056 1057 1058 def get_event_logger(self): 1059 """Create and return a IwEventLogger object. 1060 1061 @returns a IwEventLogger object. 1062 1063 """ 1064 local_file = IW_LOCAL_EVENT_LOG_FILE % (self._log_id) 1065 self._log_id += 1 1066 return iw_event_logger.IwEventLogger(self._host, self._command_iw, 1067 local_file) 1068 1069 1070 def vht_supported(self): 1071 """Returns True if VHT is supported; False otherwise.""" 1072 result = self._run('%s list' % self._command_iw).stdout 1073 if 'VHT Capabilities' in result: 1074 return True 1075 return False 1076 1077 1078 def he_supported(self): 1079 """Returns True if HE (802.11ax) is supported; False otherwise.""" 1080 result = self._run('%s list' % self._command_iw).stdout 1081 if 'HE MAC Capabilities' in result: 1082 return True 1083 return False 1084 1085 1086 def frequency_supported(self, frequency): 1087 """Returns True if the given frequency is supported; False otherwise. 1088 1089 @param frequency: int Wifi frequency to check if it is supported by 1090 DUT. 1091 """ 1092 phys = self.list_phys() 1093 for phy in phys: 1094 for band in phy.bands: 1095 if frequency in band.frequencies: 1096 return True 1097 return False 1098 1099 1100 def get_fragmentation_threshold(self, phy): 1101 """Returns the fragmentation threshold for |phy|. 1102 1103 @param phy: phy name 1104 """ 1105 ret = self._run('%s phy %s info' % (self._command_iw, phy)) 1106 frag_regex = r'^\s+Fragmentation threshold:\s+([0-9]+)$' 1107 match = re.search(frag_regex, ret.stdout, re.MULTILINE) 1108 1109 if match: 1110 return int(match.group(1)) 1111 1112 return None 1113 1114 1115 def get_info(self, phy=None): 1116 """ 1117 Returns the output of 'iw phy info' for |phy|, or 'iw list' if no phy 1118 specified. 1119 1120 @param phy: optional string giving the name of the phy 1121 @return string stdout of the command run 1122 """ 1123 if phy and phy not in [iw_phy.name for iw_phy in self.list_phys()]: 1124 logging.info('iw could not find phy %s', phy) 1125 return None 1126 1127 if phy: 1128 out = self._run('%s phy %s info' % (self._command_iw, phy)).stdout 1129 else: 1130 out = self._run('%s list' % self._command_iw).stdout 1131 if 'Wiphy' in out: 1132 return out 1133 return None