Home | History | Annotate | Download | only in network
      1 # Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 import logging
      6 
      7 from locale import *
      8 
      9 PYSHARK_LOAD_TIMEOUT = 2
     10 FRAME_FIELD_RADIOTAP_DATARATE = 'radiotap.datarate'
     11 FRAME_FIELD_RADIOTAP_MCS_INDEX = 'radiotap.mcs_index'
     12 FRAME_FIELD_WLAN_FRAME_TYPE = 'wlan.fc_type_subtype'
     13 FRAME_FIELD_WLAN_SOURCE_ADDR = 'wlan.sa'
     14 FRAME_FIELD_WLAN_MGMT_SSID = 'wlan_mgt.ssid'
     15 RADIOTAP_KNOWN_BAD_FCS_REJECTOR = (
     16     'not radiotap.flags.badfcs or radiotap.flags.badfcs==0')
     17 RADIOTAP_LOW_SIGNAL_REJECTOR = ('radiotap.dbm_antsignal > -85')
     18 WLAN_BEACON_FRAME_TYPE = '0x08'
     19 WLAN_BEACON_ACCEPTOR = 'wlan.fc.type_subtype==0x08'
     20 WLAN_PROBE_REQ_FRAME_TYPE = '0x04'
     21 WLAN_PROBE_REQ_ACCEPTOR = 'wlan.fc.type_subtype==0x04'
     22 PYSHARK_BROADCAST_SSID = 'SSID: '
     23 BROADCAST_SSID = ''
     24 
     25 setlocale(LC_ALL, '')
     26 
     27 class Frame(object):
     28     """A frame from a packet capture."""
     29     TIME_FORMAT = "%H:%M:%S.%f"
     30 
     31 
     32     def __init__(self, frametime, bit_rate, mcs_index, ssid, source_addr):
     33         self._datetime = frametime
     34         self._bit_rate = bit_rate
     35         self._mcs_index = mcs_index
     36         self._ssid = ssid
     37         self._source_addr = source_addr
     38 
     39 
     40     @property
     41     def time_datetime(self):
     42         """The time of the frame, as a |datetime| object."""
     43         return self._datetime
     44 
     45 
     46     @property
     47     def bit_rate(self):
     48         """The bitrate used to transmit the frame, as an int."""
     49         return self._bit_rate
     50 
     51 
     52     @property
     53     def mcs_index(self):
     54         """
     55         The MCS index used to transmit the frame, as an int.
     56 
     57         The value may be None, if the frame was not transmitted
     58         using 802.11n modes.
     59         """
     60         return self._mcs_index
     61 
     62 
     63     @property
     64     def ssid(self):
     65         """
     66         The SSID of the frame, as a string.
     67 
     68         The value may be None, if the frame does not have an SSID.
     69         """
     70         return self._ssid
     71 
     72 
     73     @property
     74     def source_addr(self):
     75         """The source address of the frame, as a string."""
     76         return self._source_addr
     77 
     78 
     79     @property
     80     def time_string(self):
     81         """The time of the frame, in local time, as a string."""
     82         return self._datetime.strftime(self.TIME_FORMAT)
     83 
     84 
     85 def _fetch_frame_field_value(frame, field):
     86     """
     87     Retrieve the value of |field| within the |frame|.
     88 
     89     @param frame: Pyshark packet object corresponding to a captured frame.
     90     @param field: Field for which the value needs to be extracted from |frame|.
     91 
     92     @return Value extracted from the frame if the field exists, else None.
     93 
     94     """
     95     layer_object = frame
     96     for layer in field.split('.'):
     97         try:
     98             layer_object = getattr(layer_object, layer)
     99         except AttributeError:
    100             return None
    101     return layer_object
    102 
    103 
    104 def _open_capture(pcap_path, display_filter):
    105     """
    106     Get pyshark packet object parsed contents of a pcap file.
    107 
    108     @param pcap_path: string path to pcap file.
    109     @param display_filter: string filter to apply to captured frames.
    110 
    111     @return list of Pyshark packet objects.
    112 
    113     """
    114     import pyshark
    115     capture = pyshark.FileCapture(
    116         input_file=pcap_path, display_filter=display_filter)
    117     capture.load_packets(timeout=PYSHARK_LOAD_TIMEOUT)
    118     return capture
    119 
    120 
    121 def get_frames(local_pcap_path, display_filter, reject_bad_fcs=True,
    122                reject_low_signal=False):
    123     """
    124     Get a parsed representation of the contents of a pcap file.
    125     If the RF shielding in the wificell or other chambers is imperfect,
    126     we'll see packets from the external environment in the packet capture
    127     and tests that assert if the packet capture has certain properties
    128     (i.e. only packets of a certain kind) will fail. A good way to reject
    129     these packets ("leakage from the outside world") is to look at signal
    130     strength. The DUT is usually either next to the AP or <5ft from the AP
    131     in these chambers. A signal strength of < -85 dBm in an incoming packet
    132     should imply it is leakage. The reject_low_signal option is turned off by
    133     default and external packets are part of the capture by default.
    134     Be careful to not turn on this option in an attenuated setup, where the
    135     DUT/AP packets will also have a low signal (i.e. network_WiFi_AttenPerf).
    136 
    137     @param local_pcap_path: string path to a local pcap file on the host.
    138     @param display_filter: string filter to apply to captured frames.
    139     @param reject_bad_fcs: bool, for frames with bad Frame Check Sequence.
    140     @param reject_low_signal: bool, for packets with signal < -85 dBm. These
    141                               are likely from the external environment and show
    142                               up due to poor shielding in the RF chamber.
    143 
    144     @return list of Frame structs.
    145 
    146     """
    147     if reject_bad_fcs is True:
    148         display_filter = '(%s) and (%s)' % (RADIOTAP_KNOWN_BAD_FCS_REJECTOR,
    149                                             display_filter)
    150 
    151     if reject_low_signal is True:
    152         display_filter = '(%s) and (%s)' % (RADIOTAP_LOW_SIGNAL_REJECTOR,
    153                                             display_filter)
    154 
    155     logging.debug('Capture: %s, Filter: %s', local_pcap_path, display_filter)
    156     capture_frames = _open_capture(local_pcap_path, display_filter)
    157     frames = []
    158     logging.info('Parsing frames')
    159 
    160     for frame in capture_frames:
    161         rate = _fetch_frame_field_value(frame, FRAME_FIELD_RADIOTAP_DATARATE)
    162         if rate:
    163             rate = atof(rate)
    164         else:
    165             logging.debug('Capture frame missing rate: %s', frame)
    166 
    167         frametime = frame.sniff_time
    168 
    169         mcs_index = _fetch_frame_field_value(
    170             frame, FRAME_FIELD_RADIOTAP_MCS_INDEX)
    171         if mcs_index:
    172             mcs_index = int(mcs_index)
    173 
    174         source_addr = _fetch_frame_field_value(
    175             frame, FRAME_FIELD_WLAN_SOURCE_ADDR)
    176 
    177         # Get the SSID for any probe requests
    178         frame_type = _fetch_frame_field_value(
    179             frame, FRAME_FIELD_WLAN_FRAME_TYPE)
    180         if (frame_type in [WLAN_BEACON_FRAME_TYPE, WLAN_PROBE_REQ_FRAME_TYPE]):
    181             ssid = _fetch_frame_field_value(frame, FRAME_FIELD_WLAN_MGMT_SSID)
    182             # Since the SSID name is a variable length field, there seems to be
    183             # a bug in the pyshark parsing, it returns 'SSID: ' instead of ''
    184             # for broadcast SSID's.
    185             if ssid == PYSHARK_BROADCAST_SSID:
    186                 ssid = BROADCAST_SSID
    187         else:
    188             ssid = None
    189 
    190         frames.append(Frame(frametime, rate, mcs_index, ssid, source_addr))
    191 
    192     return frames
    193 
    194 
    195 def get_probe_ssids(local_pcap_path, probe_sender=None):
    196     """
    197     Get the SSIDs that were named in 802.11 probe requests frames.
    198 
    199     Parse a pcap, returning all the SSIDs named in 802.11 probe
    200     request frames. If |probe_sender| is specified, only probes
    201     from that MAC address will be considered.
    202 
    203     @param pcap_path: string path to a local pcap file on the host.
    204     @param remote_host: Host object (if the file is remote).
    205     @param probe_sender: MAC address of the device sending probes.
    206 
    207     @return: A frozenset of the SSIDs that were probed.
    208 
    209     """
    210     if probe_sender:
    211         display_filter = '%s and wlan.addr==%s' % (
    212                 WLAN_PROBE_REQ_ACCEPTOR, probe_sender)
    213     else:
    214         display_filter = WLAN_PROBE_REQ_ACCEPTOR
    215 
    216     frames = get_frames(local_pcap_path, display_filter, reject_bad_fcs=True)
    217 
    218     return frozenset(
    219             [frame.ssid for frame in frames if frame.ssid is not None])
    220