1#!/usr/bin/env python3 2# 3# Copyright 2019 - The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the 'License'); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an 'AS IS' BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17import csv 18import os 19import posixpath 20import time 21import zipfile 22import acts_contrib.test_utils.wifi.wifi_test_utils as wutils 23 24from acts import context 25from acts import logger 26from acts import utils 27from acts.controllers.utils_lib import ssh 28 29WifiEnums = wutils.WifiEnums 30SNIFFER_TIMEOUT = 6 31MEDIUM_SLEEP = 3 32SHORT_SLEEP = 1 33 34 35def create(configs): 36 """Factory method for sniffer. 37 Args: 38 configs: list of dicts with sniffer settings. 39 Settings must contain the following : ssh_settings, type, OS, interface. 40 41 Returns: 42 objs: list of sniffer class objects. 43 """ 44 objs = [] 45 for config in configs: 46 try: 47 if config['type'] == 'tshark': 48 if config['os'] == 'unix': 49 objs.append(TsharkSnifferOnUnix(config)) 50 elif config['os'] == 'linux': 51 objs.append(TsharkSnifferOnLinux(config)) 52 elif config['os'] == 'android': 53 objs.append(TsharkSnifferOnAndroid(config)) 54 else: 55 raise RuntimeError('Wrong sniffer config') 56 57 elif config['type'] == 'mock': 58 objs.append(MockSniffer(config)) 59 except KeyError: 60 raise KeyError('Invalid sniffer configurations') 61 return objs 62 63 64def destroy(objs): 65 return 66 67 68class OtaSnifferBase(object): 69 """Base class defining common sniffers functions.""" 70 71 _log_file_counter = 0 72 73 @property 74 def started(self): 75 raise NotImplementedError('started must be specified.') 76 77 def start_capture(self, network, duration=30): 78 """Starts the sniffer Capture. 79 80 Args: 81 network: dict containing network information such as SSID, etc. 82 duration: duration of sniffer capture in seconds. 83 """ 84 raise NotImplementedError('start_capture must be specified.') 85 86 def stop_capture(self, tag=''): 87 """Stops the sniffer Capture. 88 89 Args: 90 tag: string to tag sniffer capture file name with. 91 """ 92 raise NotImplementedError('stop_capture must be specified.') 93 94 def _get_remote_dump_path(self): 95 """Returns name of the sniffer dump file.""" 96 remote_file_name = 'sniffer_dump.{}'.format( 97 self.sniffer_output_file_type) 98 remote_dump_path = posixpath.join(posixpath.sep, 'tmp', 99 remote_file_name) 100 return remote_dump_path 101 102 def _get_full_file_path(self, tag=None): 103 """Returns the full file path for the sniffer capture dump file. 104 105 Returns the full file path (on test machine) for the sniffer capture 106 dump file. 107 108 Args: 109 tag: The tag appended to the sniffer capture dump file . 110 """ 111 tags = [tag, 'count', OtaSnifferBase._log_file_counter] 112 out_file_name = 'Sniffer_Capture_%s.%s' % ('_'.join([ 113 str(x) for x in tags if x != '' and x is not None 114 ]), self.sniffer_output_file_type) 115 OtaSnifferBase._log_file_counter += 1 116 117 file_path = os.path.join(self.log_path, out_file_name) 118 return file_path 119 120 @property 121 def log_path(self): 122 current_context = context.get_current_context() 123 full_out_dir = os.path.join(current_context.get_full_output_path(), 124 'sniffer_captures') 125 126 # Ensure the directory exists. 127 os.makedirs(full_out_dir, exist_ok=True) 128 129 return full_out_dir 130 131 132class MockSniffer(OtaSnifferBase): 133 """Class that implements mock sniffer for test development and debug.""" 134 135 def __init__(self, config): 136 self.log = logger.create_tagged_trace_logger('Mock Sniffer') 137 138 def start_capture(self, network, duration=30): 139 """Starts sniffer capture on the specified machine. 140 141 Args: 142 network: dict of network credentials. 143 duration: duration of the sniff. 144 """ 145 self.log.debug('Starting sniffer.') 146 147 def stop_capture(self): 148 """Stops the sniffer. 149 150 Returns: 151 log_file: name of processed sniffer. 152 """ 153 154 self.log.debug('Stopping sniffer.') 155 log_file = self._get_full_file_path() 156 with open(log_file, 'w') as file: 157 file.write('this is a sniffer dump.') 158 return log_file 159 160 161class TsharkSnifferBase(OtaSnifferBase): 162 """Class that implements Tshark based sniffer controller. """ 163 164 TYPE_SUBTYPE_DICT = { 165 '0': 'Association Requests', 166 '1': 'Association Responses', 167 '2': 'Reassociation Requests', 168 '3': 'Resssociation Responses', 169 '4': 'Probe Requests', 170 '5': 'Probe Responses', 171 '8': 'Beacon', 172 '9': 'ATIM', 173 '10': 'Disassociations', 174 '11': 'Authentications', 175 '12': 'Deauthentications', 176 '13': 'Actions', 177 '24': 'Block ACK Requests', 178 '25': 'Block ACKs', 179 '26': 'PS-Polls', 180 '27': 'RTS', 181 '28': 'CTS', 182 '29': 'ACK', 183 '30': 'CF-Ends', 184 '31': 'CF-Ends/CF-Acks', 185 '32': 'Data', 186 '33': 'Data+CF-Ack', 187 '34': 'Data+CF-Poll', 188 '35': 'Data+CF-Ack+CF-Poll', 189 '36': 'Null', 190 '37': 'CF-Ack', 191 '38': 'CF-Poll', 192 '39': 'CF-Ack+CF-Poll', 193 '40': 'QoS Data', 194 '41': 'QoS Data+CF-Ack', 195 '42': 'QoS Data+CF-Poll', 196 '43': 'QoS Data+CF-Ack+CF-Poll', 197 '44': 'QoS Null', 198 '46': 'QoS CF-Poll (Null)', 199 '47': 'QoS CF-Ack+CF-Poll (Null)' 200 } 201 202 TSHARK_COLUMNS = [ 203 'frame_number', 'frame_time_relative', 'mactime', 'frame_len', 'rssi', 204 'channel', 'ta', 'ra', 'bssid', 'type', 'subtype', 'duration', 'seq', 205 'retry', 'pwrmgmt', 'moredata', 'ds', 'phy', 'radio_datarate', 206 'vht_datarate', 'radiotap_mcs_index', 'vht_mcs', 'wlan_data_rate', 207 '11n_mcs_index', '11ac_mcs', '11n_bw', '11ac_bw', 'vht_nss', 'mcs_gi', 208 'vht_gi', 'vht_coding', 'ba_bm', 'fc_status', 'bf_report' 209 ] 210 211 TSHARK_OUTPUT_COLUMNS = [ 212 'frame_number', 'frame_time_relative', 'mactime', 'ta', 'ra', 'bssid', 213 'rssi', 'channel', 'frame_len', 'Info', 'radio_datarate', 214 'radiotap_mcs_index', 'pwrmgmt', 'phy', 'vht_nss', 'vht_mcs', 215 'vht_datarate', '11ac_mcs', '11ac_bw', 'vht_gi', 'vht_coding', 216 'wlan_data_rate', '11n_mcs_index', '11n_bw', 'mcs_gi', 'type', 217 'subtype', 'duration', 'seq', 'retry', 'moredata', 'ds', 'ba_bm', 218 'fc_status', 'bf_report' 219 ] 220 221 TSHARK_FIELDS_LIST = [ 222 'frame.number', 'frame.time_relative', 'radiotap.mactime', 'frame.len', 223 'radiotap.dbm_antsignal', 'wlan_radio.channel', 'wlan.ta', 'wlan.ra', 224 'wlan.bssid', 'wlan.fc.type', 'wlan.fc.type_subtype', 'wlan.duration', 225 'wlan.seq', 'wlan.fc.retry', 'wlan.fc.pwrmgt', 'wlan.fc.moredata', 226 'wlan.fc.ds', 'wlan_radio.phy', 'radiotap.datarate', 227 'radiotap.vht.datarate.0', 'radiotap.mcs.index', 'radiotap.vht.mcs.0', 228 'wlan_radio.data_rate', 'wlan_radio.11n.mcs_index', 229 'wlan_radio.11ac.mcs', 'wlan_radio.11n.bandwidth', 230 'wlan_radio.11ac.bandwidth', 'radiotap.vht.nss.0', 'radiotap.mcs.gi', 231 'radiotap.vht.gi', 'radiotap.vht.coding.0', 'wlan.ba.bm', 232 'wlan.fcs.status', 'wlan.vht.compressed_beamforming_report.snr' 233 ] 234 235 def __init__(self, config): 236 self.sniffer_proc_pid = None 237 self.log = logger.create_tagged_trace_logger('Tshark Sniffer') 238 self.ssh_config = config['ssh_config'] 239 self.sniffer_os = config['os'] 240 self.run_as_sudo = config.get('run_as_sudo', False) 241 self.sniffer_output_file_type = config['output_file_type'] 242 self.sniffer_snap_length = config['snap_length'] 243 self.sniffer_interface = config['interface'] 244 self.sniffer_disabled = False 245 246 #Logging into sniffer 247 self.log.info('Logging into sniffer.') 248 self._sniffer_server = ssh.connection.SshConnection( 249 ssh.settings.from_config(self.ssh_config)) 250 # Get tshark params 251 self.tshark_fields = self._generate_tshark_fields( 252 self.TSHARK_FIELDS_LIST) 253 self.tshark_path = self._sniffer_server.run('which tshark').stdout 254 255 @property 256 def _started(self): 257 return self.sniffer_proc_pid is not None 258 259 def _scan_for_networks(self): 260 """Scans for wireless networks on the sniffer.""" 261 raise NotImplementedError 262 263 def _get_tshark_command(self, duration): 264 """Frames the appropriate tshark command. 265 266 Args: 267 duration: duration to sniff for. 268 269 Returns: 270 tshark_command : appropriate tshark command. 271 """ 272 tshark_command = '{} -l -i {} -I -t u -a duration:{}'.format( 273 self.tshark_path, self.sniffer_interface, int(duration)) 274 if self.run_as_sudo: 275 tshark_command = 'sudo {}'.format(tshark_command) 276 277 return tshark_command 278 279 def _get_sniffer_command(self, tshark_command): 280 """ 281 Frames the appropriate sniffer command. 282 283 Args: 284 tshark_command: framed tshark command 285 286 Returns: 287 sniffer_command: appropriate sniffer command 288 """ 289 if self.sniffer_output_file_type in ['pcap', 'pcapng']: 290 sniffer_command = ' {tshark} -s {snaplength} -w {log_file} '.format( 291 tshark=tshark_command, 292 snaplength=self.sniffer_snap_length, 293 log_file=self._get_remote_dump_path()) 294 295 elif self.sniffer_output_file_type == 'csv': 296 sniffer_command = '{tshark} {fields} > {log_file}'.format( 297 tshark=tshark_command, 298 fields=self.tshark_fields, 299 log_file=self._get_remote_dump_path()) 300 301 else: 302 raise KeyError('Sniffer output file type not configured correctly') 303 304 return sniffer_command 305 306 def _generate_tshark_fields(self, fields): 307 """Generates tshark fields to be appended to the tshark command. 308 309 Args: 310 fields: list of tshark fields to be appended to the tshark command. 311 312 Returns: 313 tshark_fields: string of tshark fields to be appended 314 to the tshark command. 315 """ 316 tshark_fields = "-T fields -y IEEE802_11_RADIO -E separator='^'" 317 for field in fields: 318 tshark_fields = tshark_fields + ' -e {}'.format(field) 319 return tshark_fields 320 321 def _configure_sniffer(self, network, chan, bw): 322 """ Connects to a wireless network using networksetup utility. 323 324 Args: 325 network: dictionary of network credentials; SSID and password. 326 """ 327 raise NotImplementedError 328 329 def _run_tshark(self, sniffer_command): 330 """Starts the sniffer. 331 332 Args: 333 sniffer_command: sniffer command to execute. 334 """ 335 self.log.debug('Starting sniffer.') 336 sniffer_job = self._sniffer_server.run_async(sniffer_command) 337 self.sniffer_proc_pid = sniffer_job.stdout 338 339 def _stop_tshark(self): 340 """ Stops the sniffer.""" 341 self.log.debug('Stopping sniffer') 342 343 # while loop to kill the sniffer process 344 stop_time = time.time() + SNIFFER_TIMEOUT 345 while time.time() < stop_time: 346 # Wait before sending more kill signals 347 time.sleep(0.1) 348 try: 349 # Returns 1 if process was killed 350 self._sniffer_server.run( 351 'ps aux| grep {} | grep -v grep'.format( 352 self.sniffer_proc_pid)) 353 except: 354 return 355 try: 356 # Returns error if process was killed already 357 self._sniffer_server.run('sudo kill -15 {}'.format( 358 str(self.sniffer_proc_pid))) 359 except: 360 # Except is hit when tshark is already dead but we will break 361 # out of the loop when confirming process is dead using ps aux 362 pass 363 self.log.warning('Could not stop sniffer. Trying with SIGKILL.') 364 try: 365 self.log.debug('Killing sniffer with SIGKILL.') 366 self._sniffer_server.run('sudo kill -9 {}'.format( 367 str(self.sniffer_proc_pid))) 368 except: 369 self.log.debug('Sniffer process may have stopped succesfully.') 370 371 def _process_tshark_dump(self, log_file): 372 """ Process tshark dump for better readability. 373 374 Processes tshark dump for better readability and saves it to a file. 375 Adds an info column at the end of each row. Format of the info columns: 376 subtype of the frame, sequence no and retry status. 377 378 Args: 379 log_file : unprocessed sniffer output 380 Returns: 381 log_file : processed sniffer output 382 """ 383 temp_dump_file = os.path.join(self.log_path, 'sniffer_temp_dump.csv') 384 utils.exe_cmd('cp {} {}'.format(log_file, temp_dump_file)) 385 386 with open(temp_dump_file, 'r') as input_csv, open(log_file, 387 'w') as output_csv: 388 reader = csv.DictReader(input_csv, 389 fieldnames=self.TSHARK_COLUMNS, 390 delimiter='^') 391 writer = csv.DictWriter(output_csv, 392 fieldnames=self.TSHARK_OUTPUT_COLUMNS, 393 delimiter='\t') 394 writer.writeheader() 395 for row in reader: 396 if row['subtype'] in self.TYPE_SUBTYPE_DICT: 397 row['Info'] = '{sub} S={seq} retry={retry_status}'.format( 398 sub=self.TYPE_SUBTYPE_DICT[row['subtype']], 399 seq=row['seq'], 400 retry_status=row['retry']) 401 else: 402 row['Info'] = '{} S={} retry={}\n'.format( 403 row['subtype'], row['seq'], row['retry']) 404 writer.writerow(row) 405 406 utils.exe_cmd('rm -f {}'.format(temp_dump_file)) 407 return log_file 408 409 def start_capture(self, network, chan, bw, duration=60): 410 """Starts sniffer capture on the specified machine. 411 412 Args: 413 network: dict describing network to sniff on. 414 duration: duration of sniff. 415 """ 416 # Checking for existing sniffer processes 417 if self._started: 418 self.log.debug('Sniffer already running') 419 return 420 421 # Configure sniffer 422 self._configure_sniffer(network, chan, bw) 423 tshark_command = self._get_tshark_command(duration) 424 sniffer_command = self._get_sniffer_command(tshark_command) 425 426 # Starting sniffer capture by executing tshark command 427 self._run_tshark(sniffer_command) 428 429 def stop_capture(self, tag=''): 430 """Stops the sniffer. 431 432 Args: 433 tag: tag to be appended to the sniffer output file. 434 Returns: 435 log_file: path to sniffer dump. 436 """ 437 # Checking if there is an ongoing sniffer capture 438 if not self._started: 439 self.log.debug('No sniffer process running') 440 return 441 # Killing sniffer process 442 self._stop_tshark() 443 444 # Processing writing capture output to file 445 log_file = self._get_full_file_path(tag) 446 self._sniffer_server.run('sudo chmod 777 {}'.format( 447 self._get_remote_dump_path())) 448 self._sniffer_server.pull_file(log_file, self._get_remote_dump_path()) 449 450 if self.sniffer_output_file_type == 'csv': 451 log_file = self._process_tshark_dump(log_file) 452 if self.sniffer_output_file_type == 'pcap': 453 zip_file_path = log_file[:-4] + "zip" 454 zipfile.ZipFile(zip_file_path, 'w', zipfile.ZIP_DEFLATED).write( 455 log_file, arcname=log_file.split('/')[-1]) 456 os.remove(log_file) 457 458 self.sniffer_proc_pid = None 459 return log_file 460 461 462class TsharkSnifferOnUnix(TsharkSnifferBase): 463 """Class that implements Tshark based sniffer controller on Unix systems.""" 464 465 def _scan_for_networks(self): 466 """Scans the wireless networks on the sniffer. 467 468 Returns: 469 scan_results : output of the scan command. 470 """ 471 scan_command = '/usr/local/bin/airport -s' 472 scan_result = self._sniffer_server.run(scan_command).stdout 473 474 return scan_result 475 476 def _configure_sniffer(self, network, chan, bw): 477 """Connects to a wireless network using networksetup utility. 478 479 Args: 480 network: dictionary of network credentials; SSID and password. 481 """ 482 483 self.log.debug('Connecting to network {}'.format(network['SSID'])) 484 485 if 'password' not in network: 486 network['password'] = '' 487 488 connect_command = 'networksetup -setairportnetwork en0 {} {}'.format( 489 network['SSID'], network['password']) 490 self._sniffer_server.run(connect_command) 491 492 493class TsharkSnifferOnLinux(TsharkSnifferBase): 494 """Class that implements Tshark based sniffer controller on Linux.""" 495 496 def __init__(self, config): 497 super().__init__(config) 498 self._init_sniffer() 499 self.channel = None 500 self.bandwidth = None 501 502 def _init_sniffer(self): 503 """Function to configure interface for the first time""" 504 self._sniffer_server.run('sudo modprobe -r iwlwifi') 505 self._sniffer_server.run('sudo dmesg -C') 506 self._sniffer_server.run('cat /dev/null | sudo tee /var/log/syslog') 507 self._sniffer_server.run('sudo modprobe iwlwifi debug=0x1') 508 # Wait for wifi config changes before trying to further configuration 509 # e.g. setting monitor mode (which will fail if above is not complete) 510 time.sleep(SHORT_SLEEP) 511 512 def start_capture(self, network, chan, bw, duration=60): 513 """Starts sniffer capture on the specified machine. 514 515 Args: 516 network: dict describing network to sniff on. 517 duration: duration of sniff. 518 """ 519 # If sniffer doesnt support the channel, return 520 if '6g' in str(chan): 521 self.log.debug('Channel not supported on sniffer') 522 return 523 # Checking for existing sniffer processes 524 if self._started: 525 self.log.debug('Sniffer already running') 526 return 527 528 # Configure sniffer 529 self._configure_sniffer(network, chan, bw) 530 tshark_command = self._get_tshark_command(duration) 531 sniffer_command = self._get_sniffer_command(tshark_command) 532 533 # Starting sniffer capture by executing tshark command 534 self._run_tshark(sniffer_command) 535 536 def set_monitor_mode(self, chan, bw): 537 """Function to configure interface to monitor mode 538 539 Brings up the sniffer wireless interface in monitor mode and 540 tunes it to the appropriate channel and bandwidth 541 542 Args: 543 chan: primary channel (int) to tune the sniffer to 544 bw: bandwidth (int) to tune the sniffer to 545 """ 546 if chan == self.channel and bw == self.bandwidth: 547 return 548 549 self.channel = chan 550 self.bandwidth = bw 551 552 channel_map = { 553 80: { 554 tuple(range(36, 50, 2)): 42, 555 tuple(range(52, 66, 2)): 58, 556 tuple(range(100, 114, 2)): 106, 557 tuple(range(116, 130, 2)): 122, 558 tuple(range(132, 146, 2)): 138, 559 tuple(range(149, 163, 2)): 155 560 }, 561 40: { 562 (36, 38, 40): 38, 563 (44, 46, 48): 46, 564 (52, 54, 56): 54, 565 (60, 62, 64): 62, 566 (100, 102, 104): 102, 567 (108, 110, 112): 108, 568 (116, 118, 120): 118, 569 (124, 126, 128): 126, 570 (132, 134, 136): 134, 571 (140, 142, 144): 142, 572 (149, 151, 153): 151, 573 (157, 159, 161): 159 574 }, 575 160: { 576 (36, 38, 40): 50 577 } 578 } 579 580 if chan <= 13: 581 primary_freq = WifiEnums.channel_2G_to_freq[chan] 582 else: 583 primary_freq = WifiEnums.channel_5G_to_freq[chan] 584 585 self._sniffer_server.run('sudo ifconfig {} down'.format( 586 self.sniffer_interface)) 587 self._sniffer_server.run('sudo iwconfig {} mode monitor'.format( 588 self.sniffer_interface)) 589 self._sniffer_server.run('sudo ifconfig {} up'.format( 590 self.sniffer_interface)) 591 592 if bw in channel_map: 593 for tuple_chan in channel_map[bw]: 594 if chan in tuple_chan: 595 center_freq = WifiEnums.channel_5G_to_freq[channel_map[bw] 596 [tuple_chan]] 597 self._sniffer_server.run( 598 'sudo iw dev {} set freq {} {} {}'.format( 599 self.sniffer_interface, primary_freq, bw, 600 center_freq)) 601 602 else: 603 self._sniffer_server.run('sudo iw dev {} set freq {}'.format( 604 self.sniffer_interface, primary_freq)) 605 606 def _configure_sniffer(self, network, chan, bw): 607 """ Connects to a wireless network using networksetup utility. 608 609 Args: 610 network: dictionary of network credentials; SSID and password. 611 """ 612 613 self.log.debug('Setting monitor mode on Ch {}, bw {}'.format(chan, bw)) 614 self.set_monitor_mode(chan, bw) 615 616 617class TsharkSnifferOnAndroid(TsharkSnifferBase): 618 """Class that implements Tshark based sniffer controller on Linux.""" 619 620 def __init__(self, config): 621 super().__init__(config) 622 self._init_sniffer(config) 623 self.channel = None 624 self.bandwidth = None 625 626 def _init_sniffer(self, config): 627 """Function to configure interface for the first time""" 628 tshark_D_output = self._sniffer_server.run('tshark -D').stdout 629 self.sniffer_sn = config['serial'] 630 time.sleep(MEDIUM_SLEEP) 631 if config['interface'] in tshark_D_output: 632 self.log.info("Target sniffer interface {} detected".format( 633 config['interface'])) 634 else: 635 self.log.error('Target sniffer interface {} NOT detected'.format( 636 config['interface'])) 637 # Wait for wifi config changes before trying to further configuration 638 # e.g. setting monitor mode (which will fail if above is not complete) 639 time.sleep(SHORT_SLEEP) 640 641 def start_capture(self, network, chan, bw, duration=60): 642 """Starts sniffer capture on the specified machine. 643 644 Args: 645 network: dict describing network to sniff on. 646 duration: duration of sniff. 647 """ 648 # If sniffer doesnt support the channel, return 649 # Checking for existing sniffer processes 650 if self._started: 651 return 652 653 # Configure sniffer 654 self._configure_sniffer(network, chan, bw) 655 tshark_command = self._get_tshark_command(duration) 656 sniffer_command = self._get_sniffer_command(tshark_command) 657 658 # Starting sniffer capture by executing tshark command 659 self._run_tshark(sniffer_command) 660 661 def set_monitor_mode(self, chan, bw): 662 """Function to configure interface to monitor mode 663 664 Brings up the sniffer wireless interface in monitor mode and 665 tunes it to the appropriate channel and bandwidth 666 667 Args: 668 chan: primary channel (int) to tune the sniffer to 669 bw: bandwidth (int) to tune the sniffer to 670 """ 671 if chan == self.channel and bw == self.bandwidth: 672 return 673 674 self.channel = chan 675 self.bandwidth = bw 676 677 self._sniffer_server.run('adb -s {} shell wl chanspec {}/{}'.format( 678 self.sniffer_sn, chan, bw)) 679 time.sleep(SHORT_SLEEP) 680 sniffer_chanspec = self._sniffer_server.run( 681 'adb -s {} shell wl chanspec'.format(self.sniffer_sn)).stdout 682 time.sleep(SHORT_SLEEP) 683 self.log.info("Sniffer channel: {}".format(sniffer_chanspec)) 684 685 def _configure_sniffer(self, network, chan, bw): 686 """ Connects to a wireless network using networksetup utility. 687 688 Args: 689 network: dictionary of network credentials; SSID and password. 690 """ 691 692 self.log.debug('Setting monitor mode on Ch {}, bw {}'.format(chan, bw)) 693 self.set_monitor_mode(chan, bw)