# Copyright (c) 2013 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. import logging from autotest_lib.client.common_lib import error PYSHARK_LOAD_TIMEOUT = 2 FRAME_FIELD_RADIOTAP_DATARATE = 'radiotap.datarate' FRAME_FIELD_RADIOTAP_MCS_INDEX = 'radiotap.mcs_index' FRAME_FIELD_WLAN_FRAME_TYPE = 'wlan.fc_type_subtype' FRAME_FIELD_WLAN_MGMT_SSID = 'wlan_mgt.ssid' RADIOTAP_KNOWN_BAD_FCS_REJECTOR = ( 'not radiotap.flags.badfcs or radiotap.flags.badfcs==0') WLAN_BEACON_FRAME_TYPE = '0x08' WLAN_BEACON_ACCEPTOR = 'wlan.fc.type_subtype==0x08' WLAN_PROBE_REQ_FRAME_TYPE = '0x04' WLAN_PROBE_REQ_ACCEPTOR = 'wlan.fc.type_subtype==0x04' PYSHARK_BROADCAST_SSID = 'SSID: ' BROADCAST_SSID = '' class Frame(object): """A frame from a packet capture.""" TIME_FORMAT = "%H:%M:%S.%f" def __init__(self, frametime, bit_rate, mcs_index, ssid): self._datetime = frametime self._bit_rate = bit_rate self._mcs_index = mcs_index self._ssid = ssid @property def time_datetime(self): """The time of the frame, as a |datetime| object.""" return self._datetime @property def bit_rate(self): """The bitrate used to transmit the frame, as an int.""" return self._bit_rate @property def mcs_index(self): """ The MCS index used to transmit the frame, as an int. The value may be None, if the frame was not transmitted using 802.11n modes. """ return self._mcs_index @property def ssid(self): """ The SSID of the frame, as a string. The value may be None, if the frame does not have an SSID. """ return self._ssid @property def time_string(self): """The time of the frame, in local time, as a string.""" return self._datetime.strftime(self.TIME_FORMAT) def _fetch_frame_field_value(frame, field): """ Retrieve the value of |field| within the |frame|. @param frame: Pyshark packet object corresponding to a captured frame. @param field: Field for which the value needs to be extracted from |frame|. @return Value extracted from the frame if the field exists, else None. """ layer_object = frame for layer in field.split('.'): try: layer_object = getattr(layer_object, layer) except AttributeError: return None return layer_object def _open_capture(pcap_path, display_filter): """ Get pyshark packet object parsed contents of a pcap file. @param pcap_path: string path to pcap file. @param display_filter: string filter to apply to captured frames. @return list of Pyshark packet objects. """ import pyshark capture = pyshark.FileCapture( input_file=pcap_path, display_filter=display_filter) capture.load_packets(timeout=PYSHARK_LOAD_TIMEOUT) return capture def get_frames(local_pcap_path, display_filter, bad_fcs): """ Get a parsed representation of the contents of a pcap file. @param local_pcap_path: string path to a local pcap file on the host. @param diplay_filter: string filter to apply to captured frames. @param bad_fcs: string 'include' or 'discard' @return list of Frame structs. """ if bad_fcs == 'include': display_filter = display_filter elif bad_fcs == 'discard': display_filter = '(%s) and (%s)' % (RADIOTAP_KNOWN_BAD_FCS_REJECTOR, display_filter) else: raise error.TestError('Invalid value for bad_fcs arg: %s.' % bad_fcs) logging.debug('Capture: %s, Filter: %s', local_pcap_path, display_filter) capture_frames = _open_capture(local_pcap_path, display_filter) frames = [] logging.info('Parsing frames') for frame in capture_frames: rate = _fetch_frame_field_value(frame, FRAME_FIELD_RADIOTAP_DATARATE) if rate: rate = float(rate) else: logging.debug('Found bad capture frame: %s', frame) continue frametime = frame.sniff_time mcs_index = _fetch_frame_field_value( frame, FRAME_FIELD_RADIOTAP_MCS_INDEX) if mcs_index: mcs_index = int(mcs_index) # Get the SSID for any probe requests frame_type = _fetch_frame_field_value( frame, FRAME_FIELD_WLAN_FRAME_TYPE) if (frame_type in [WLAN_BEACON_FRAME_TYPE, WLAN_PROBE_REQ_FRAME_TYPE]): ssid = _fetch_frame_field_value(frame, FRAME_FIELD_WLAN_MGMT_SSID) # Since the SSID name is a variable length field, there seems to be # a bug in the pyshark parsing, it returns 'SSID: ' instead of '' # for broadcast SSID's. if ssid == PYSHARK_BROADCAST_SSID: ssid = BROADCAST_SSID else: ssid = None frames.append(Frame(frametime, rate, mcs_index, ssid)) return frames def get_probe_ssids(local_pcap_path, probe_sender=None): """ Get the SSIDs that were named in 802.11 probe requests frames. Parse a pcap, returning all the SSIDs named in 802.11 probe request frames. If |probe_sender| is specified, only probes from that MAC address will be considered. @param pcap_path: string path to a local pcap file on the host. @param remote_host: Host object (if the file is remote). @param probe_sender: MAC address of the device sending probes. @return: A frozenset of the SSIDs that were probed. """ if probe_sender: diplay_filter = '%s and wlan.addr==%s' % ( WLAN_PROBE_REQ_ACCEPTOR, probe_sender) else: diplay_filter = WLAN_PROBE_REQ_ACCEPTOR frames = get_frames(local_pcap_path, diplay_filter, bad_fcs='discard') return frozenset( [frame.ssid for frame in frames if frame.ssid is not None])