1#!/usr/bin/python 2# Copyright 2015 The Chromium OS Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6import collections 7import pyshark 8import os 9 10class PacketCapture(object): 11 """ Class to manage the packet capture file access from a chaos test. """ 12 13 def __init__(self, file_name): 14 self._file_name = file_name 15 16 def get_output(self, display_filter=None, summaries=True, decryption=None): 17 """ 18 Gets the packets from a trace file as Pyshark packet objects for 19 further analysis. 20 21 @param display_filer: Tshark filter to be used for extracting the 22 relevant packets. 23 @param summaries: Flag to indicate whether to extract only the summaries 24 of packet or not. 25 @param decryption: Decryption key to be used on the trace file. 26 @returns List of pyshark packet objects. 27 28 """ 29 capture = pyshark.FileCapture(self._file_name, 30 display_filter=display_filter, 31 only_summaries=summaries, 32 decryption_key=decryption, 33 encryption_type='wpa-pwd') 34 capture.load_packets() 35 return capture 36 37 def get_packet_number(self, index, summary): 38 """ 39 Gets the packet that appears index |index| in the capture file. 40 41 @param index: Extract this index from the capture file. 42 @param summary: Flag to indicate whether to extract only the summary 43 of the packet or not. 44 45 @returns pyshark packet object or None. 46 47 """ 48 display_filter = "frame.number == %d" % index 49 capture = pyshark.FileCapture(self._file_name, 50 display_filter=display_filter, 51 only_summaries=summary) 52 capture.load_packets() 53 if not capture: 54 return None 55 return capture[0] 56 57 def get_packet_after(self, packet): 58 """ 59 Gets the packet that appears next in the capture file. 60 61 @param packet: Reference packet -- the packet after this one will 62 be retrieved. 63 64 @returns pyshark packet object or None. 65 66 """ 67 return self.get_packet_number(int(packet.number) + 1, summary=False) 68 69 def count_packets_with_display_filter(self, display_filter): 70 """ 71 Counts the number of packets which match the provided display filter. 72 73 @param display_filer: Tshark filter to be used for extracting the 74 relevant packets. 75 @returns Number of packets which match the filter. 76 77 """ 78 output = self.get_output(display_filter=display_filter) 79 return len(output) 80 81 def count_packets_from(self, mac_addresses): 82 """ 83 Counts the number of packets sent from a given entity using MAC address. 84 85 @param mac_address: Mac address of the entity. 86 @returns Number of packets which matched the MAC address filter. 87 88 """ 89 filter = ' or '.join(['wlan.ta==%s' % addr for addr in mac_addresses]) 90 return self.count_packets_with_display_filter(filter) 91 92 def count_packets_to(self, mac_addresses): 93 """ 94 Counts the number of packets sent to a given entity using MAC address. 95 96 @param mac_address: Mac address of the entity. 97 @returns Number of packets which matched the MAC address filter. 98 99 """ 100 filter = ' or '.join(['wlan.ra==%s' % addr for addr in mac_addresses]) 101 return self.count_packets_with_display_filter(filter) 102 103 def count_packets_from_or_to(self, mac_addresses): 104 """ 105 Counts the number of packets sent to/from a given entity using MAC 106 address. 107 108 @param mac_address: Mac address of the entity. 109 @returns Number of packets which matched the MAC address filter. 110 111 """ 112 filter = ' or '.join(['wlan.addr==%s' % addr for addr in mac_addresses]) 113 return self.count_packets_with_display_filter(filter) 114 115 def count_beacons_from(self, mac_addresses): 116 """ 117 Counts the number of beacon packets sent from a AP using MAC address. 118 119 @param mac_address: Mac address of the AP. 120 @returns Number of packets which matched the MAC address filter. 121 122 """ 123 filter = ' or '.join(['wlan.ta==%s' % addr for addr in mac_addresses]) 124 filter = '(%s) and wlan.fc.type_subtype == 0x0008' % (filter) 125 return self.count_packets_with_display_filter(filter) 126 127 def get_filtered_packets(self, ap, dut, summaries, decryption): 128 """ 129 Gets the packets sent to/from the DUT from a trace file as Pyshark 130 packet objects for further analysis. 131 132 @param summaries: Flag to indicate whether to extract only the summaries 133 of packet or not. 134 @param dut: Mac address of the DUT. 135 @param ap: Mac address of the AP. 136 @param decryption: Decryption key to be used on the trace file. 137 @returns List of pyshark packet objects. 138 139 """ 140 filter = 'wlan.addr==%s' % dut 141 packets = self.get_output(display_filter=filter, summaries=summaries, 142 decryption=decryption) 143 return packets 144 145 146class WifiStateMachineAnalyzer(object): 147 """ Class to analyze the Wifi Protocol exhcange from a chaos test. """ 148 149 STATE_INIT = "INIT" 150 STATE_PROBE_REQ = "PROBE_REQ" 151 STATE_PROBE_RESP = "PROBE_RESP" 152 STATE_AUTH_REQ = "AUTH_REQ" 153 STATE_AUTH_RESP = "AUTH_RESP" 154 STATE_ASSOC_REQ = "ASSOC_REQ" 155 STATE_ASSOC_RESP = "ASSOC_RESP" 156 STATE_KEY_MESSAGE_1 = "KEY_MESSAGE_1" 157 STATE_KEY_MESSAGE_2 = "KEY_MESSAGE_2" 158 STATE_KEY_MESSAGE_3 = "KEY_MESSAGE_3" 159 STATE_KEY_MESSAGE_4 = "KEY_MESSAGE_4" 160 STATE_DHCP_DISCOVER = "DHCP_DISCOVER" 161 STATE_DHCP_OFFER = "DHCP_OFFER" 162 STATE_DHCP_REQ = "DHCP_REQ" 163 STATE_DHCP_REQ_ACK = "DHCP_REQ_ACK" 164 STATE_END = "END" 165 166 167 PACKET_MATCH_WLAN_FRAME_TYPE = "wlan.fc_type_subtype" 168 PACKET_MATCH_WLAN_FRAME_RETRY_FLAG = "wlan.fc_retry" 169 PACKET_MATCH_WLAN_MANAGEMENT_REASON_CODE = "wlan_mgt.fixed_reason_code" 170 PACKET_MATCH_WLAN_MANAGEMENT_STATUS_CODE = "wlan_mgt.fixed_status_code" 171 PACKET_MATCH_WLAN_TRANSMITTER = "wlan.ta" 172 PACKET_MATCH_LLC_TYPE = "llc.type" 173 PACKET_MATCH_EAP_TYPE = "eapol.type" 174 PACKET_MATCH_EAP_KEY_INFO_INSTALL = "eapol.keydes_key_info_install" 175 PACKET_MATCH_EAP_KEY_INFO_ACK = "eapol.keydes_key_info_key_ack" 176 PACKET_MATCH_EAP_KEY_INFO_MIC = "eapol.keydes_key_info_key_mic" 177 PACKET_MATCH_EAP_KEY_INFO_SECURE = "eapol.keydes_key_info_secure" 178 PACKET_MATCH_IP_PROTOCOL_TYPE = "ip.proto" 179 PACKET_MATCH_DHCP_MESSAGE_TYPE = "bootp.option_dhcp" 180 PACKET_MATCH_RADIOTAP_DATA_RATE = "radiotap.datarate" 181 182 WLAN_PROBE_REQ_FRAME_TYPE = '0x04' 183 WLAN_PROBE_RESP_FRAME_TYPE = '0x05' 184 WLAN_AUTH_REQ_FRAME_TYPE = '0x0b' 185 WLAN_AUTH_RESP_FRAME_TYPE = '0x0b' 186 WLAN_ASSOC_REQ_FRAME_TYPE = '0x00' 187 WLAN_ASSOC_RESP_FRAME_TYPE = '0x01' 188 WLAN_ACK_FRAME_TYPE = '0x1d' 189 WLAN_DEAUTH_REQ_FRAME_TYPE = '0x0c' 190 WLAN_DISASSOC_REQ_FRAME_TYPE = '0x0a' 191 WLAN_QOS_DATA_FRAME_TYPE = '0x28' 192 WLAN_MANAGEMENT_STATUS_CODE_SUCCESS = '0x0000' 193 WLAN_BROADCAST_ADDRESS = 'ff:ff:ff:ff:ff:ff' 194 WLAN_FRAME_CONTROL_TYPE_MANAGEMENT = '0' 195 196 WLAN_FRAME_RETRY = '1' 197 198 LLC_AUTH_TYPE = '0x888e' 199 200 EAP_KEY_TYPE = '0x03' 201 202 IP_UDP_PROTOCOL_TYPE = '17' 203 204 DHCP_DISCOVER_MESSAGE_TYPE = '1' 205 DHCP_OFFER_MESSAGE_TYPE = '2' 206 DHCP_REQUEST_MESSAGE_TYPE = '3' 207 DHCP_ACK_MESSAGE_TYPE = '5' 208 209 DIR_TO_DUT = 0 210 DIR_FROM_DUT = 1 211 DIR_DUT_TO_AP = 2 212 DIR_AP_TO_DUT = 3 213 DIR_ACK = 4 214 215 # State Info Tuples (Name, Direction, Match fields, Next State) 216 StateInfo = collections.namedtuple( 217 'StateInfo', ['name', 'direction', 'match_fields', 'next_state']) 218 STATE_INFO_INIT = StateInfo("INIT", 0, {}, STATE_PROBE_REQ) 219 STATE_INFO_PROBE_REQ = StateInfo("WLAN PROBE REQUEST", 220 DIR_FROM_DUT, 221 { PACKET_MATCH_WLAN_FRAME_TYPE: 222 WLAN_PROBE_REQ_FRAME_TYPE }, 223 STATE_PROBE_RESP) 224 STATE_INFO_PROBE_RESP = StateInfo("WLAN PROBE RESPONSE", 225 DIR_AP_TO_DUT, 226 { PACKET_MATCH_WLAN_FRAME_TYPE: 227 WLAN_PROBE_RESP_FRAME_TYPE }, 228 STATE_AUTH_REQ) 229 STATE_INFO_AUTH_REQ = StateInfo("WLAN AUTH REQUEST", 230 DIR_DUT_TO_AP, 231 { PACKET_MATCH_WLAN_FRAME_TYPE: 232 WLAN_AUTH_REQ_FRAME_TYPE }, 233 STATE_AUTH_RESP) 234 STATE_INFO_AUTH_RESP = StateInfo( 235 "WLAN AUTH RESPONSE", 236 DIR_AP_TO_DUT, 237 { PACKET_MATCH_WLAN_FRAME_TYPE: WLAN_AUTH_REQ_FRAME_TYPE, 238 PACKET_MATCH_WLAN_MANAGEMENT_STATUS_CODE: 239 WLAN_MANAGEMENT_STATUS_CODE_SUCCESS }, 240 STATE_ASSOC_REQ) 241 STATE_INFO_ASSOC_REQ = StateInfo("WLAN ASSOC REQUEST", 242 DIR_DUT_TO_AP, 243 { PACKET_MATCH_WLAN_FRAME_TYPE: 244 WLAN_ASSOC_REQ_FRAME_TYPE }, 245 STATE_ASSOC_RESP) 246 STATE_INFO_ASSOC_RESP = StateInfo( 247 "WLAN ASSOC RESPONSE", 248 DIR_AP_TO_DUT, 249 { PACKET_MATCH_WLAN_FRAME_TYPE: WLAN_ASSOC_RESP_FRAME_TYPE, 250 PACKET_MATCH_WLAN_MANAGEMENT_STATUS_CODE: 251 WLAN_MANAGEMENT_STATUS_CODE_SUCCESS }, 252 STATE_KEY_MESSAGE_1) 253 STATE_INFO_KEY_MESSAGE_1 = StateInfo("WPA KEY MESSAGE 1", 254 DIR_AP_TO_DUT, 255 { PACKET_MATCH_LLC_TYPE: 256 LLC_AUTH_TYPE, 257 PACKET_MATCH_EAP_KEY_INFO_INSTALL: 258 '0', 259 PACKET_MATCH_EAP_KEY_INFO_ACK: 260 '1', 261 PACKET_MATCH_EAP_KEY_INFO_MIC: 262 '0', 263 PACKET_MATCH_EAP_KEY_INFO_SECURE: 264 '0' }, 265 STATE_KEY_MESSAGE_2) 266 STATE_INFO_KEY_MESSAGE_2 = StateInfo("WPA KEY MESSAGE 2", 267 DIR_DUT_TO_AP, 268 { PACKET_MATCH_LLC_TYPE: 269 LLC_AUTH_TYPE, 270 PACKET_MATCH_EAP_KEY_INFO_INSTALL: 271 '0', 272 PACKET_MATCH_EAP_KEY_INFO_ACK: 273 '0', 274 PACKET_MATCH_EAP_KEY_INFO_MIC: 275 '1', 276 PACKET_MATCH_EAP_KEY_INFO_SECURE: 277 '0' }, 278 STATE_KEY_MESSAGE_3) 279 STATE_INFO_KEY_MESSAGE_3 = StateInfo("WPA KEY MESSAGE 3", 280 DIR_AP_TO_DUT, 281 { PACKET_MATCH_LLC_TYPE: 282 LLC_AUTH_TYPE, 283 PACKET_MATCH_EAP_KEY_INFO_INSTALL: 284 '1', 285 PACKET_MATCH_EAP_KEY_INFO_ACK: 286 '1', 287 PACKET_MATCH_EAP_KEY_INFO_MIC: 288 '1', 289 PACKET_MATCH_EAP_KEY_INFO_SECURE: 290 '1' }, 291 STATE_KEY_MESSAGE_4) 292 STATE_INFO_KEY_MESSAGE_4 = StateInfo("WPA KEY MESSAGE 4", 293 DIR_DUT_TO_AP, 294 { PACKET_MATCH_LLC_TYPE: 295 LLC_AUTH_TYPE, 296 PACKET_MATCH_EAP_KEY_INFO_INSTALL: 297 '0', 298 PACKET_MATCH_EAP_KEY_INFO_ACK: 299 '0', 300 PACKET_MATCH_EAP_KEY_INFO_MIC: 301 '1', 302 PACKET_MATCH_EAP_KEY_INFO_SECURE: 303 '1' }, 304 STATE_DHCP_DISCOVER) 305 STATE_INFO_DHCP_DISCOVER = StateInfo("DHCP DISCOVER", 306 DIR_DUT_TO_AP, 307 { PACKET_MATCH_IP_PROTOCOL_TYPE: 308 IP_UDP_PROTOCOL_TYPE, 309 PACKET_MATCH_DHCP_MESSAGE_TYPE: 310 DHCP_DISCOVER_MESSAGE_TYPE }, 311 STATE_DHCP_OFFER) 312 STATE_INFO_DHCP_OFFER = StateInfo("DHCP OFFER", 313 DIR_AP_TO_DUT, 314 { PACKET_MATCH_IP_PROTOCOL_TYPE: 315 IP_UDP_PROTOCOL_TYPE, 316 PACKET_MATCH_DHCP_MESSAGE_TYPE: 317 DHCP_OFFER_MESSAGE_TYPE }, 318 STATE_DHCP_REQ) 319 STATE_INFO_DHCP_REQ = StateInfo("DHCP REQUEST", 320 DIR_DUT_TO_AP, 321 { PACKET_MATCH_IP_PROTOCOL_TYPE: 322 IP_UDP_PROTOCOL_TYPE, 323 PACKET_MATCH_DHCP_MESSAGE_TYPE: 324 DHCP_REQUEST_MESSAGE_TYPE }, 325 STATE_DHCP_REQ_ACK) 326 STATE_INFO_DHCP_REQ_ACK = StateInfo("DHCP ACK", 327 DIR_AP_TO_DUT, 328 { PACKET_MATCH_IP_PROTOCOL_TYPE: 329 IP_UDP_PROTOCOL_TYPE, 330 PACKET_MATCH_DHCP_MESSAGE_TYPE: 331 DHCP_ACK_MESSAGE_TYPE }, 332 STATE_END) 333 STATE_INFO_END = StateInfo("END", 0, {}, STATE_END) 334 # Master State Table Map of State Infos 335 STATE_INFO_MAP = {STATE_INIT: STATE_INFO_INIT, 336 STATE_PROBE_REQ: STATE_INFO_PROBE_REQ, 337 STATE_PROBE_RESP: STATE_INFO_PROBE_RESP, 338 STATE_AUTH_REQ: STATE_INFO_AUTH_REQ, 339 STATE_AUTH_RESP: STATE_INFO_AUTH_RESP, 340 STATE_ASSOC_REQ: STATE_INFO_ASSOC_REQ, 341 STATE_ASSOC_RESP: STATE_INFO_ASSOC_RESP, 342 STATE_KEY_MESSAGE_1:STATE_INFO_KEY_MESSAGE_1, 343 STATE_KEY_MESSAGE_2:STATE_INFO_KEY_MESSAGE_2, 344 STATE_KEY_MESSAGE_3:STATE_INFO_KEY_MESSAGE_3, 345 STATE_KEY_MESSAGE_4:STATE_INFO_KEY_MESSAGE_4, 346 STATE_DHCP_DISCOVER:STATE_INFO_DHCP_DISCOVER, 347 STATE_DHCP_OFFER: STATE_INFO_DHCP_OFFER, 348 STATE_DHCP_REQ: STATE_INFO_DHCP_REQ, 349 STATE_DHCP_REQ_ACK: STATE_INFO_DHCP_REQ_ACK, 350 STATE_END: STATE_INFO_END} 351 352 # Packet Details Tuples (User friendly name, Field name) 353 PacketDetail = collections.namedtuple( 354 "PacketDetail", ["friendly_name", "field_name"]) 355 PACKET_DETAIL_REASON_CODE = PacketDetail( 356 "Reason Code", 357 PACKET_MATCH_WLAN_MANAGEMENT_REASON_CODE) 358 PACKET_DETAIL_STATUS_CODE = PacketDetail( 359 "Status Code", 360 PACKET_MATCH_WLAN_MANAGEMENT_STATUS_CODE) 361 PACKET_DETAIL_SENDER = PacketDetail( 362 "Sender", PACKET_MATCH_WLAN_TRANSMITTER) 363 364 # Error State Info Tuples (Name, Match fields) 365 ErrorStateInfo = collections.namedtuple( 366 'ErrorStateInfo', ['name', 'match_fields', 'details']) 367 ERROR_STATE_INFO_DEAUTH = ErrorStateInfo("WLAN DEAUTH REQUEST", 368 { PACKET_MATCH_WLAN_FRAME_TYPE: 369 WLAN_DEAUTH_REQ_FRAME_TYPE }, 370 [ PACKET_DETAIL_SENDER, 371 PACKET_DETAIL_REASON_CODE ]) 372 ERROR_STATE_INFO_DEASSOC = ErrorStateInfo("WLAN DISASSOC REQUEST", 373 { PACKET_MATCH_WLAN_FRAME_TYPE: 374 WLAN_DISASSOC_REQ_FRAME_TYPE }, 375 [ PACKET_DETAIL_SENDER, 376 PACKET_DETAIL_REASON_CODE ]) 377 # Master State Table Tuple of Error State Infos 378 ERROR_STATE_INFO_TUPLE = (ERROR_STATE_INFO_DEAUTH, ERROR_STATE_INFO_DEASSOC) 379 380 # These warnings actually match successful states, but since the we 381 # check forwards and backwards through the state machine for the successful 382 # version of these packets, they can only match a failure. 383 WARNING_INFO_AUTH_REJ = ErrorStateInfo( 384 "WLAN AUTH REJECTED", 385 { PACKET_MATCH_WLAN_FRAME_TYPE: WLAN_AUTH_REQ_FRAME_TYPE }, 386 [ PACKET_DETAIL_STATUS_CODE ]) 387 WARNING_INFO_ASSOC_REJ = ErrorStateInfo( 388 "WLAN ASSOC REJECTED", 389 { PACKET_MATCH_WLAN_FRAME_TYPE: WLAN_ASSOC_RESP_FRAME_TYPE }, 390 [ PACKET_DETAIL_STATUS_CODE ]) 391 392 # Master Table Tuple of warning information. 393 WARNING_INFO_TUPLE = (WARNING_INFO_AUTH_REJ, WARNING_INFO_ASSOC_REJ) 394 395 396 def __init__(self, ap_macs, dut_mac, filtered_packets, capture, logger): 397 self._current_state = self._get_state(self.STATE_INIT) 398 self._reached_states = [] 399 self._skipped_states = [] 400 self._packets = filtered_packets 401 self._capture = capture 402 self._dut_mac = dut_mac 403 self._ap_macs = ap_macs 404 self._log = logger 405 self._acks = [] 406 407 @property 408 def acks(self): 409 return self._acks 410 411 def _get_state(self, state): 412 return self.STATE_INFO_MAP[state] 413 414 def _get_next_state(self, state): 415 return self._get_state(state.next_state) 416 417 def _get_curr_next_state(self): 418 return self._get_next_state(self._current_state) 419 420 def _fetch_packet_field_value(self, packet, field): 421 layer_object = packet 422 for layer in field.split('.'): 423 try: 424 layer_object = getattr(layer_object, layer) 425 except AttributeError: 426 return None 427 return layer_object 428 429 def _match_packet_fields(self, packet, fields): 430 for field, exp_value in fields.items(): 431 value = self._fetch_packet_field_value(packet, field) 432 if exp_value != value: 433 return False 434 return True 435 436 def _fetch_packet_data_rate(self, packet): 437 return self._fetch_packet_field_value(packet, 438 self.PACKET_MATCH_RADIOTAP_DATA_RATE) 439 440 def _does_packet_match_state(self, state, packet): 441 fields = state.match_fields 442 if self._match_packet_fields(packet, fields): 443 if state.direction == self.DIR_TO_DUT: 444 # This should have receiver addr of DUT 445 if packet.wlan.ra == self._dut_mac: 446 return True 447 elif state.direction == self.DIR_FROM_DUT: 448 # This should have transmitter addr of DUT 449 if packet.wlan.ta == self._dut_mac: 450 return True 451 elif state.direction == self.DIR_AP_TO_DUT: 452 # This should have receiver addr of DUT & 453 # transmitter addr of AP's 454 if ((packet.wlan.ra == self._dut_mac) and 455 (packet.wlan.ta in self._ap_macs)): 456 return True 457 elif state.direction == self.DIR_DUT_TO_AP: 458 # This should have transmitter addr of DUT & 459 # receiver addr of AP's 460 if ((packet.wlan.ta == self._dut_mac) and 461 (packet.wlan.ra in self._ap_macs)): 462 return True 463 return False 464 465 def _does_packet_match_error_state(self, state, packet): 466 fields = state.match_fields 467 return self._match_packet_fields(packet, fields) 468 469 def _get_packet_detail(self, details, packet): 470 attributes = [] 471 attributes.append("Packet number: %s" % packet.number) 472 for detail in details: 473 value = self._fetch_packet_field_value(packet, detail.field_name) 474 attributes.append("%s: %s" % (detail.friendly_name, value)) 475 return attributes 476 477 def _does_packet_match_ack_state(self, packet): 478 fields = { self.PACKET_MATCH_WLAN_FRAME_TYPE: self.WLAN_ACK_FRAME_TYPE } 479 return self._match_packet_fields(packet, fields) 480 481 def _does_packet_contain_retry_flag(self, packet): 482 fields = { self.PACKET_MATCH_WLAN_FRAME_RETRY_FLAG: 483 self.WLAN_FRAME_RETRY } 484 return self._match_packet_fields(packet, fields) 485 486 def _check_for_ack(self, state, packet): 487 if (packet.wlan.da == self.WLAN_BROADCAST_ADDRESS and 488 packet.wlan.fc_type == self.WLAN_FRAME_CONTROL_TYPE_MANAGEMENT): 489 # Broadcast management frames are not ACKed. 490 return True 491 next_packet = self._capture.get_packet_after(packet) 492 if not next_packet or not ( 493 (self._does_packet_match_ack_state(next_packet)) and 494 (next_packet.wlan.addr == packet.wlan.ta)): 495 msg = "WARNING! Missing ACK for state: " + \ 496 state.name + "." 497 self._log.log_to_output_file(msg) 498 return False 499 self._acks.append(int(next_packet.number)) 500 return True 501 502 def _check_for_error(self, packet): 503 for error_state in self.ERROR_STATE_INFO_TUPLE: 504 if self._does_packet_match_error_state(error_state, packet): 505 error_attributes = self._get_packet_detail(error_state.details, 506 packet) 507 msg = "ERROR! State Machine encountered error due to " + \ 508 error_state.name + ", " + \ 509 ", ".join(error_attributes) + "." 510 self._log.log_to_output_file(msg) 511 return True 512 return False 513 514 def _check_for_warning(self, packet): 515 for warning in self.WARNING_INFO_TUPLE: 516 if self._does_packet_match_error_state(warning, packet): 517 error_attributes = self._get_packet_detail(warning.details, 518 packet) 519 msg = "WARNING! " + warning.name + " found, " + \ 520 ", ".join(error_attributes) + "." 521 self._log.log_to_output_file(msg) 522 return True 523 return False 524 525 def _check_for_repeated_state(self, packet): 526 for state in self._reached_states: 527 if self._does_packet_match_state(state, packet): 528 msg = "WARNING! Repeated State: " + \ 529 state.name + ", Packet number: " + \ 530 str(packet.number) 531 if self._does_packet_contain_retry_flag(packet): 532 msg += " due to retransmission." 533 else: 534 msg += "." 535 self._log.log_to_output_file(msg) 536 537 def _is_from_previous_state(self, packet): 538 for state in self._reached_states + self._skipped_states: 539 if self._does_packet_match_state(state, packet): 540 return True 541 return False 542 543 def _step(self, reached_state, packet): 544 # We missed a few packets in between 545 if self._current_state != reached_state: 546 msg = "WARNING! Missed states: " 547 skipped_state = self._current_state 548 while skipped_state != reached_state: 549 msg += skipped_state.name + ", " 550 self._skipped_states.append(skipped_state) 551 skipped_state = self._get_next_state(skipped_state) 552 msg = msg[:-2] 553 msg += "." 554 self._log.log_to_output_file(msg) 555 msg = "Found state: " + reached_state.name 556 if packet: 557 msg += ", Packet number: " + str(packet.number) + \ 558 ", Data rate: " + str(self._fetch_packet_data_rate(packet))+\ 559 "Mbps." 560 else: 561 msg += "." 562 self._log.log_to_output_file(msg) 563 # Ignore the Init state in the reached states 564 if packet: 565 self._reached_states.append(reached_state) 566 self._current_state = self._get_next_state(reached_state) 567 568 def _step_init(self): 569 #self.log_to_output_file("Starting Analysis") 570 self._current_state = self._get_curr_next_state() 571 572 def analyze(self): 573 """ Starts the analysis of the Wifi Protocol Exchange. """ 574 575 # Start the state machine iteration 576 self._step_init() 577 packet_iterator = iter(self._packets) 578 for packet in packet_iterator: 579 self._check_for_repeated_state(packet) 580 # Try to look ahead in the state machine to account for occasional 581 # packet capture misses. 582 next_state = self._current_state 583 while next_state != self.STATE_INFO_END: 584 if self._does_packet_match_state(next_state, packet): 585 self._step(next_state, packet) 586 self._check_for_ack(next_state, packet) 587 break 588 next_state = self._get_next_state(next_state) 589 if self._current_state == self.STATE_INFO_END: 590 self._log.log_to_output_file("State Machine completed!") 591 return True 592 if self._check_for_error(packet): 593 return False 594 if not self._is_from_previous_state(packet): 595 self._check_for_warning(packet) 596 msg = "ERROR! State Machine halted at " + self._current_state.name + \ 597 " state." 598 self._log.log_to_output_file(msg) 599 return False 600 601 602class ChaosCaptureAnalyzer(object): 603 """ Class to analyze the packet capture from a chaos test . """ 604 605 def __init__(self, ap_bssids, ap_ssid, dut_mac, logger): 606 self._ap_bssids = ap_bssids 607 self._ap_ssid = ap_ssid 608 self._dut_mac = dut_mac 609 self._log = logger 610 611 def _validate_ap_presence(self, capture, bssids, ssid): 612 beacon_count = capture.count_beacons_from(bssids) 613 if not beacon_count: 614 packet_count = capture.count_packets_from(bssids) 615 if not packet_count: 616 self._log.log_to_output_file( 617 "No packets at all from AP BSSIDs %r!" % bssids) 618 else: 619 self._log.log_to_output_file( 620 "No beacons from AP BSSIDs %r but %d packets!" % 621 (bssids, packet_count)) 622 return False 623 self._log.log_to_output_file("AP BSSIDs: %s, SSID: %s." % 624 (bssids, ssid)) 625 self._log.log_to_output_file("AP beacon count: %d." % beacon_count) 626 return True 627 628 def _validate_dut_presence(self, capture, dut_mac): 629 tx_count = capture.count_packets_from([dut_mac]) 630 if not tx_count: 631 self._log.log_to_output_file( 632 "No packets Tx at all from DUT MAC %r!" % dut_mac) 633 return False 634 rx_count = capture.count_packets_to([dut_mac]) 635 self._log.log_to_output_file("DUT MAC: %s." % dut_mac) 636 self._log.log_to_output_file( 637 "DUT packet count Tx: %d, Rx: %d." % (tx_count, rx_count)) 638 return True 639 640 def _ack_interleave(self, packets, capture, acks): 641 """Generator that interleaves packets with their associated ACKs.""" 642 for packet in packets: 643 packet_number = int(packet.no) 644 while acks and acks[0] < packet_number: 645 # ACK packet does not appear in the filtered capture. 646 yield capture.get_packet_number(acks.pop(0), summary=True) 647 if acks and acks[0] == packet_number: 648 # ACK packet also appears in the capture. 649 acks.pop(0) 650 yield packet 651 652 def analyze(self, trace): 653 """ 654 Starts the analysis of the Chaos capture. 655 656 @param trace: Packet capture file path to analyze. 657 658 """ 659 basename = os.path.basename(trace) 660 self._log.log_start_section("Packet Capture File: %s" % basename) 661 capture = PacketCapture(trace) 662 bssids = self._ap_bssids 663 ssid = self._ap_ssid 664 if not self._validate_ap_presence(capture, bssids, ssid): 665 return 666 dut_mac = self._dut_mac 667 if not self._validate_dut_presence(capture, dut_mac): 668 return 669 decryption = 'chromeos:%s' % ssid 670 self._log.log_start_section("WLAN Protocol Verification") 671 filtered_packets = capture.get_filtered_packets( 672 bssids, dut_mac, False, decryption) 673 wifi_state_machine = WifiStateMachineAnalyzer( 674 bssids, dut_mac, filtered_packets, capture, self._log) 675 wifi_state_machine.analyze() 676 self._log.log_start_section("Filtered Packet Capture Summary") 677 filtered_packets = capture.get_filtered_packets( 678 bssids, dut_mac, True, decryption) 679 for packet in self._ack_interleave( 680 filtered_packets, capture, wifi_state_machine.acks): 681 self._log.log_to_output_file("%s" % (packet)) 682