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