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