• 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'
22PYSHARK_BROADCAST_SSID = 'SSID: '
23BROADCAST_SSID = ''
24
25setlocale(LC_ALL, '')
26
27class 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
85def _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
104def _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
121def 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
195def 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