• 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
6import pyshark
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    try:
176        for frame in capture_frames:
177            rate = _fetch_frame_field_value(frame, FRAME_FIELD_RADIOTAP_DATARATE)
178            if rate:
179                rate = atof(rate)
180            else:
181                logging.debug('Capture frame missing rate: %s', frame)
182
183            frametime = frame.sniff_time
184
185            mcs_index = _fetch_frame_field_value(
186                frame, FRAME_FIELD_RADIOTAP_MCS_INDEX)
187            if mcs_index:
188                mcs_index = int(mcs_index)
189
190            source_addr = _fetch_frame_field_value(
191                frame, FRAME_FIELD_WLAN_SOURCE_ADDR)
192
193            # Get the SSID for any probe requests
194            frame_type = _fetch_frame_field_value(
195                frame, FRAME_FIELD_WLAN_FRAME_TYPE)
196            if (frame_type in [WLAN_BEACON_FRAME_TYPE, WLAN_PROBE_REQ_FRAME_TYPE]):
197                ssid = _fetch_frame_field_value(frame, FRAME_FIELD_WLAN_MGMT_SSID)
198                # Since the SSID name is a variable length field, there seems to be
199                # a bug in the pyshark parsing, it returns 'SSID: ' instead of ''
200                # for broadcast SSID's.
201                if ssid == PYSHARK_BROADCAST_SSID:
202                    ssid = BROADCAST_SSID
203            else:
204                ssid = None
205
206            frames.append(Frame(frametime, rate, mcs_index, ssid, source_addr,
207                                frame_type=frame_type))
208    except pyshark.capture.capture.TSharkCrashException as e:
209        # tcpdump sometimes produces captures with an incomplete packet when passed SIGINT.
210        # tshark will crash when it reads this incomplete packet and return a non-zero exit code.
211        # pyshark will throw a TSharkCrashException due to this exit code from tshark.
212        # Instead of throwing away all packets, let's ignore the malformed packet and continue to
213        # analyze packets and return the successfully analyzed ones.
214        # This is a band aid fix for b/158311775 as we would ideally fix the tcpdump issue
215        # in the first place.
216        logging.info("Frame capture issue")
217    return frames
218
219
220def get_probe_ssids(local_pcap_path, probe_sender=None):
221    """
222    Get the SSIDs that were named in 802.11 probe requests frames.
223
224    Parse a pcap, returning all the SSIDs named in 802.11 probe
225    request frames. If |probe_sender| is specified, only probes
226    from that MAC address will be considered.
227
228    @param pcap_path: string path to a local pcap file on the host.
229    @param remote_host: Host object (if the file is remote).
230    @param probe_sender: MAC address of the device sending probes.
231
232    @return: A frozenset of the SSIDs that were probed.
233
234    """
235    if probe_sender:
236        display_filter = '%s and wlan.addr==%s' % (
237                WLAN_PROBE_REQ_ACCEPTOR, probe_sender)
238    else:
239        display_filter = WLAN_PROBE_REQ_ACCEPTOR
240
241    frames = get_frames(local_pcap_path, display_filter, reject_bad_fcs=True)
242
243    return frozenset(
244            [frame.ssid for frame in frames if frame.ssid is not None])
245