• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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)