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