tcpdump_analyzer.py revision bbec804e4c63caf1b8e58ddaf3dc1bc7933442da
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 autotest_lib.client.common_lib import error
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_MGMT_SSID = 'wlan_mgt.ssid'
14RADIOTAP_KNOWN_BAD_FCS_REJECTOR = (
15    'not radiotap.flags.badfcs or radiotap.flags.badfcs==0')
16WLAN_PROBE_REQ_FRAME_TYPE = '0x04'
17WLAN_PROBE_REQ_ACCEPTOR = 'wlan.fc.type_subtype==0x04'
18PYSHARK_BROADCAST_SSID = 'SSID: '
19BROADCAST_SSID = ''
20
21
22class Frame(object):
23    """A frame from a packet capture."""
24    TIME_FORMAT = "%H:%M:%S.%f"
25
26
27    def __init__(self, frametime, bit_rate, mcs_index, probe_ssid):
28        self._datetime = frametime
29        self._bit_rate = bit_rate
30        self._mcs_index = mcs_index
31        self._probe_ssid = probe_ssid
32
33
34    @property
35    def time_datetime(self):
36        """The time of the frame, as a |datetime| object."""
37        return self._datetime
38
39
40    @property
41    def bit_rate(self):
42        """The bitrate used to transmit the frame, as an int."""
43        return self._bit_rate
44
45
46    @property
47    def mcs_index(self):
48        """
49        The MCS index used to transmit the frame, as an int.
50
51        The value may be None, if the frame was not transmitted
52        using 802.11n modes.
53        """
54        return self._mcs_index
55
56
57    @property
58    def probe_ssid(self):
59        """
60        The SSID of the probe request, as a string.
61
62        The value may be None, if the frame is not a probe request.
63        """
64        return self._probe_ssid
65
66
67    @property
68    def time_string(self):
69        """The time of the frame, in local time, as a string."""
70        return self._datetime.strftime(self.TIME_FORMAT)
71
72
73def _fetch_frame_field_value(frame, field):
74    """
75    Retrieve the value of |field| within the |frame|.
76
77    @param frame: Pyshark packet object corresponding to a captured frame.
78    @param field: Field for which the value needs to be extracted from |frame|.
79
80    @return Value extracted from the frame if the field exists, else None.
81
82    """
83    layer_object = frame
84    for layer in field.split('.'):
85        try:
86            layer_object = getattr(layer_object, layer)
87        except AttributeError:
88            return None
89    return layer_object
90
91
92def _match_frame_field_with_value(frame, field, match_value):
93    """
94    Check if the value of |field| within the |frame| matches |match_value|.
95
96    @param frame: Pyshark packet object corresponding to a captured frame.
97    @param field: Field for which the value needs to be extracted from |frame|.
98    @param match_value: Value to be matched.
99
100    @return True if |match_value| macthes the value retrieved from the frame,
101            False otherwise.
102
103    """
104    value = _fetch_frame_field_value(frame, field)
105    return (match_value == value)
106
107
108def _open_capture(pcap_path, display_filter):
109    """
110    Get pyshark packet object parsed contents of a pcap file.
111
112    @param pcap_path: string path to pcap file.
113    @param display_filter: string filter to apply to captured frames.
114
115    @return list of Pyshark packet objects.
116
117    """
118    import pyshark
119    capture = pyshark.FileCapture(input_file=pcap_path,
120                                  display_filter=display_filter)
121    capture.load_packets(timeout=PYSHARK_LOAD_TIMEOUT)
122    return capture
123
124
125def get_frames(local_pcap_path, display_filter, bad_fcs):
126    """
127    Get a parsed representation of the contents of a pcap file.
128
129    @param local_pcap_path: string path to a local pcap file on the host.
130    @param diplay_filter: string filter to apply to captured frames.
131    @param bad_fcs: string 'include' or 'discard'
132
133    @return list of Frame structs.
134
135    """
136    if bad_fcs == 'include':
137        display_filter = display_filter
138    elif bad_fcs == 'discard':
139        display_filter = '(%s) and (%s)' % (RADIOTAP_KNOWN_BAD_FCS_REJECTOR,
140                                            display_filter)
141    else:
142        raise error.TestError('Invalid value for bad_fcs arg: %s.' % bad_fcs)
143
144    logging.debug('Capture: %s, Filter: %s', local_pcap_path, display_filter)
145    capture_frames = _open_capture(local_pcap_path, display_filter)
146    frames = []
147    logging.info('Parsing frames')
148
149    for frame in capture_frames:
150        rate = _fetch_frame_field_value(frame, FRAME_FIELD_RADIOTAP_DATARATE)
151        if rate:
152            rate = float(rate)
153        else:
154            logging.debug('Found bad capture frame: %s', frame)
155            continue
156
157        frametime = frame.sniff_time
158
159        mcs_index = _fetch_frame_field_value(frame, FRAME_FIELD_RADIOTAP_MCS_INDEX)
160        if mcs_index:
161            mcs_index = int(mcs_index)
162
163        # Get the SSID for any probe requests
164        is_probe_req = _match_frame_field_with_value(
165                frame, FRAME_FIELD_WLAN_FRAME_TYPE, WLAN_PROBE_REQ_FRAME_TYPE)
166        if is_probe_req:
167            probe_ssid = _fetch_frame_field_value(
168                    frame, FRAME_FIELD_WLAN_MGMT_SSID)
169            # Since the SSID name is a variable length field, there seems to be
170            # a bug in the pyshark parsing, it returns 'SSID: ' instead of ''
171            # for broadcast SSID's.
172            if probe_ssid == PYSHARK_BROADCAST_SSID:
173                probe_ssid = BROADCAST_SSID
174        else:
175            probe_ssid = None
176
177        frames.append(Frame(frametime, rate, mcs_index, probe_ssid))
178
179    return frames
180
181
182def get_probe_ssids(local_pcap_path, probe_sender=None):
183    """
184    Get the SSIDs that were named in 802.11 probe requests frames.
185
186    Parse a pcap, returning all the SSIDs named in 802.11 probe
187    request frames. If |probe_sender| is specified, only probes
188    from that MAC address will be considered.
189
190    @param pcap_path: string path to a local pcap file on the host.
191    @param remote_host: Host object (if the file is remote).
192    @param probe_sender: MAC address of the device sending probes.
193
194    @return: A frozenset of the SSIDs that were probed.
195
196    """
197    if probe_sender:
198        diplay_filter = '%s and wlan.addr==%s' % (
199                WLAN_PROBE_REQ_ACCEPTOR, probe_sender)
200    else:
201        diplay_filter = WLAN_PROBE_REQ_ACCEPTOR
202
203    frames = get_frames(local_pcap_path, diplay_filter, bad_fcs='discard')
204
205    return frozenset(
206            [frame.probe_ssid for frame in frames
207             if frame.probe_ssid is not None])
208