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