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