682 lines
30 KiB
Python
Executable File
682 lines
30 KiB
Python
Executable File
#!/usr/bin/python2
|
|
# Copyright 2015 The Chromium OS Authors. All rights reserved.
|
|
# Use of this source code is governed by a BSD-style license that can be
|
|
# found in the LICENSE file.
|
|
|
|
import collections
|
|
import pyshark
|
|
import os
|
|
|
|
class PacketCapture(object):
|
|
""" Class to manage the packet capture file access from a chaos test. """
|
|
|
|
def __init__(self, file_name):
|
|
self._file_name = file_name
|
|
|
|
def get_output(self, display_filter=None, summaries=True, decryption=None):
|
|
"""
|
|
Gets the packets from a trace file as Pyshark packet objects for
|
|
further analysis.
|
|
|
|
@param display_filer: Tshark filter to be used for extracting the
|
|
relevant packets.
|
|
@param summaries: Flag to indicate whether to extract only the summaries
|
|
of packet or not.
|
|
@param decryption: Decryption key to be used on the trace file.
|
|
@returns List of pyshark packet objects.
|
|
|
|
"""
|
|
capture = pyshark.FileCapture(self._file_name,
|
|
display_filter=display_filter,
|
|
only_summaries=summaries,
|
|
decryption_key=decryption,
|
|
encryption_type='wpa-pwd')
|
|
capture.load_packets()
|
|
return capture
|
|
|
|
def get_packet_number(self, index, summary):
|
|
"""
|
|
Gets the packet that appears index |index| in the capture file.
|
|
|
|
@param index: Extract this index from the capture file.
|
|
@param summary: Flag to indicate whether to extract only the summary
|
|
of the packet or not.
|
|
|
|
@returns pyshark packet object or None.
|
|
|
|
"""
|
|
display_filter = "frame.number == %d" % index
|
|
capture = pyshark.FileCapture(self._file_name,
|
|
display_filter=display_filter,
|
|
only_summaries=summary)
|
|
capture.load_packets()
|
|
if not capture:
|
|
return None
|
|
return capture[0]
|
|
|
|
def get_packet_after(self, packet):
|
|
"""
|
|
Gets the packet that appears next in the capture file.
|
|
|
|
@param packet: Reference packet -- the packet after this one will
|
|
be retrieved.
|
|
|
|
@returns pyshark packet object or None.
|
|
|
|
"""
|
|
return self.get_packet_number(int(packet.number) + 1, summary=False)
|
|
|
|
def count_packets_with_display_filter(self, display_filter):
|
|
"""
|
|
Counts the number of packets which match the provided display filter.
|
|
|
|
@param display_filer: Tshark filter to be used for extracting the
|
|
relevant packets.
|
|
@returns Number of packets which match the filter.
|
|
|
|
"""
|
|
output = self.get_output(display_filter=display_filter)
|
|
return len(output)
|
|
|
|
def count_packets_from(self, mac_addresses):
|
|
"""
|
|
Counts the number of packets sent from a given entity using MAC address.
|
|
|
|
@param mac_address: Mac address of the entity.
|
|
@returns Number of packets which matched the MAC address filter.
|
|
|
|
"""
|
|
filter = ' or '.join(['wlan.ta==%s' % addr for addr in mac_addresses])
|
|
return self.count_packets_with_display_filter(filter)
|
|
|
|
def count_packets_to(self, mac_addresses):
|
|
"""
|
|
Counts the number of packets sent to a given entity using MAC address.
|
|
|
|
@param mac_address: Mac address of the entity.
|
|
@returns Number of packets which matched the MAC address filter.
|
|
|
|
"""
|
|
filter = ' or '.join(['wlan.ra==%s' % addr for addr in mac_addresses])
|
|
return self.count_packets_with_display_filter(filter)
|
|
|
|
def count_packets_from_or_to(self, mac_addresses):
|
|
"""
|
|
Counts the number of packets sent to/from a given entity using MAC
|
|
address.
|
|
|
|
@param mac_address: Mac address of the entity.
|
|
@returns Number of packets which matched the MAC address filter.
|
|
|
|
"""
|
|
filter = ' or '.join(['wlan.addr==%s' % addr for addr in mac_addresses])
|
|
return self.count_packets_with_display_filter(filter)
|
|
|
|
def count_beacons_from(self, mac_addresses):
|
|
"""
|
|
Counts the number of beacon packets sent from a AP using MAC address.
|
|
|
|
@param mac_address: Mac address of the AP.
|
|
@returns Number of packets which matched the MAC address filter.
|
|
|
|
"""
|
|
filter = ' or '.join(['wlan.ta==%s' % addr for addr in mac_addresses])
|
|
filter = '(%s) and wlan.fc.type_subtype == 0x0008' % (filter)
|
|
return self.count_packets_with_display_filter(filter)
|
|
|
|
def get_filtered_packets(self, ap, dut, summaries, decryption):
|
|
"""
|
|
Gets the packets sent to/from the DUT from a trace file as Pyshark
|
|
packet objects for further analysis.
|
|
|
|
@param summaries: Flag to indicate whether to extract only the summaries
|
|
of packet or not.
|
|
@param dut: Mac address of the DUT.
|
|
@param ap: Mac address of the AP.
|
|
@param decryption: Decryption key to be used on the trace file.
|
|
@returns List of pyshark packet objects.
|
|
|
|
"""
|
|
filter = 'wlan.addr==%s' % dut
|
|
packets = self.get_output(display_filter=filter, summaries=summaries,
|
|
decryption=decryption)
|
|
return packets
|
|
|
|
|
|
class WifiStateMachineAnalyzer(object):
|
|
""" Class to analyze the Wifi Protocol exhcange from a chaos test. """
|
|
|
|
STATE_INIT = "INIT"
|
|
STATE_PROBE_REQ = "PROBE_REQ"
|
|
STATE_PROBE_RESP = "PROBE_RESP"
|
|
STATE_AUTH_REQ = "AUTH_REQ"
|
|
STATE_AUTH_RESP = "AUTH_RESP"
|
|
STATE_ASSOC_REQ = "ASSOC_REQ"
|
|
STATE_ASSOC_RESP = "ASSOC_RESP"
|
|
STATE_KEY_MESSAGE_1 = "KEY_MESSAGE_1"
|
|
STATE_KEY_MESSAGE_2 = "KEY_MESSAGE_2"
|
|
STATE_KEY_MESSAGE_3 = "KEY_MESSAGE_3"
|
|
STATE_KEY_MESSAGE_4 = "KEY_MESSAGE_4"
|
|
STATE_DHCP_DISCOVER = "DHCP_DISCOVER"
|
|
STATE_DHCP_OFFER = "DHCP_OFFER"
|
|
STATE_DHCP_REQ = "DHCP_REQ"
|
|
STATE_DHCP_REQ_ACK = "DHCP_REQ_ACK"
|
|
STATE_END = "END"
|
|
|
|
|
|
PACKET_MATCH_WLAN_FRAME_TYPE = "wlan.fc_type_subtype"
|
|
PACKET_MATCH_WLAN_FRAME_RETRY_FLAG = "wlan.fc_retry"
|
|
PACKET_MATCH_WLAN_MANAGEMENT_REASON_CODE = "wlan_mgt.fixed_reason_code"
|
|
PACKET_MATCH_WLAN_MANAGEMENT_STATUS_CODE = "wlan_mgt.fixed_status_code"
|
|
PACKET_MATCH_WLAN_TRANSMITTER = "wlan.ta"
|
|
PACKET_MATCH_LLC_TYPE = "llc.type"
|
|
PACKET_MATCH_EAP_TYPE = "eapol.type"
|
|
PACKET_MATCH_EAP_KEY_INFO_INSTALL = "eapol.keydes_key_info_install"
|
|
PACKET_MATCH_EAP_KEY_INFO_ACK = "eapol.keydes_key_info_key_ack"
|
|
PACKET_MATCH_EAP_KEY_INFO_MIC = "eapol.keydes_key_info_key_mic"
|
|
PACKET_MATCH_EAP_KEY_INFO_SECURE = "eapol.keydes_key_info_secure"
|
|
PACKET_MATCH_IP_PROTOCOL_TYPE = "ip.proto"
|
|
PACKET_MATCH_DHCP_MESSAGE_TYPE = "bootp.option_dhcp"
|
|
PACKET_MATCH_RADIOTAP_DATA_RATE = "radiotap.datarate"
|
|
|
|
WLAN_PROBE_REQ_FRAME_TYPE = '0x04'
|
|
WLAN_PROBE_RESP_FRAME_TYPE = '0x05'
|
|
WLAN_AUTH_REQ_FRAME_TYPE = '0x0b'
|
|
WLAN_AUTH_RESP_FRAME_TYPE = '0x0b'
|
|
WLAN_ASSOC_REQ_FRAME_TYPE = '0x00'
|
|
WLAN_ASSOC_RESP_FRAME_TYPE = '0x01'
|
|
WLAN_ACK_FRAME_TYPE = '0x1d'
|
|
WLAN_DEAUTH_REQ_FRAME_TYPE = '0x0c'
|
|
WLAN_DISASSOC_REQ_FRAME_TYPE = '0x0a'
|
|
WLAN_QOS_DATA_FRAME_TYPE = '0x28'
|
|
WLAN_MANAGEMENT_STATUS_CODE_SUCCESS = '0x0000'
|
|
WLAN_BROADCAST_ADDRESS = 'ff:ff:ff:ff:ff:ff'
|
|
WLAN_FRAME_CONTROL_TYPE_MANAGEMENT = '0'
|
|
|
|
WLAN_FRAME_RETRY = '1'
|
|
|
|
LLC_AUTH_TYPE = '0x888e'
|
|
|
|
EAP_KEY_TYPE = '0x03'
|
|
|
|
IP_UDP_PROTOCOL_TYPE = '17'
|
|
|
|
DHCP_DISCOVER_MESSAGE_TYPE = '1'
|
|
DHCP_OFFER_MESSAGE_TYPE = '2'
|
|
DHCP_REQUEST_MESSAGE_TYPE = '3'
|
|
DHCP_ACK_MESSAGE_TYPE = '5'
|
|
|
|
DIR_TO_DUT = 0
|
|
DIR_FROM_DUT = 1
|
|
DIR_DUT_TO_AP = 2
|
|
DIR_AP_TO_DUT = 3
|
|
DIR_ACK = 4
|
|
|
|
# State Info Tuples (Name, Direction, Match fields, Next State)
|
|
StateInfo = collections.namedtuple(
|
|
'StateInfo', ['name', 'direction', 'match_fields', 'next_state'])
|
|
STATE_INFO_INIT = StateInfo("INIT", 0, {}, STATE_PROBE_REQ)
|
|
STATE_INFO_PROBE_REQ = StateInfo("WLAN PROBE REQUEST",
|
|
DIR_FROM_DUT,
|
|
{ PACKET_MATCH_WLAN_FRAME_TYPE:
|
|
WLAN_PROBE_REQ_FRAME_TYPE },
|
|
STATE_PROBE_RESP)
|
|
STATE_INFO_PROBE_RESP = StateInfo("WLAN PROBE RESPONSE",
|
|
DIR_AP_TO_DUT,
|
|
{ PACKET_MATCH_WLAN_FRAME_TYPE:
|
|
WLAN_PROBE_RESP_FRAME_TYPE },
|
|
STATE_AUTH_REQ)
|
|
STATE_INFO_AUTH_REQ = StateInfo("WLAN AUTH REQUEST",
|
|
DIR_DUT_TO_AP,
|
|
{ PACKET_MATCH_WLAN_FRAME_TYPE:
|
|
WLAN_AUTH_REQ_FRAME_TYPE },
|
|
STATE_AUTH_RESP)
|
|
STATE_INFO_AUTH_RESP = StateInfo(
|
|
"WLAN AUTH RESPONSE",
|
|
DIR_AP_TO_DUT,
|
|
{ PACKET_MATCH_WLAN_FRAME_TYPE: WLAN_AUTH_REQ_FRAME_TYPE,
|
|
PACKET_MATCH_WLAN_MANAGEMENT_STATUS_CODE:
|
|
WLAN_MANAGEMENT_STATUS_CODE_SUCCESS },
|
|
STATE_ASSOC_REQ)
|
|
STATE_INFO_ASSOC_REQ = StateInfo("WLAN ASSOC REQUEST",
|
|
DIR_DUT_TO_AP,
|
|
{ PACKET_MATCH_WLAN_FRAME_TYPE:
|
|
WLAN_ASSOC_REQ_FRAME_TYPE },
|
|
STATE_ASSOC_RESP)
|
|
STATE_INFO_ASSOC_RESP = StateInfo(
|
|
"WLAN ASSOC RESPONSE",
|
|
DIR_AP_TO_DUT,
|
|
{ PACKET_MATCH_WLAN_FRAME_TYPE: WLAN_ASSOC_RESP_FRAME_TYPE,
|
|
PACKET_MATCH_WLAN_MANAGEMENT_STATUS_CODE:
|
|
WLAN_MANAGEMENT_STATUS_CODE_SUCCESS },
|
|
STATE_KEY_MESSAGE_1)
|
|
STATE_INFO_KEY_MESSAGE_1 = StateInfo("WPA KEY MESSAGE 1",
|
|
DIR_AP_TO_DUT,
|
|
{ PACKET_MATCH_LLC_TYPE:
|
|
LLC_AUTH_TYPE,
|
|
PACKET_MATCH_EAP_KEY_INFO_INSTALL:
|
|
'0',
|
|
PACKET_MATCH_EAP_KEY_INFO_ACK:
|
|
'1',
|
|
PACKET_MATCH_EAP_KEY_INFO_MIC:
|
|
'0',
|
|
PACKET_MATCH_EAP_KEY_INFO_SECURE:
|
|
'0' },
|
|
STATE_KEY_MESSAGE_2)
|
|
STATE_INFO_KEY_MESSAGE_2 = StateInfo("WPA KEY MESSAGE 2",
|
|
DIR_DUT_TO_AP,
|
|
{ PACKET_MATCH_LLC_TYPE:
|
|
LLC_AUTH_TYPE,
|
|
PACKET_MATCH_EAP_KEY_INFO_INSTALL:
|
|
'0',
|
|
PACKET_MATCH_EAP_KEY_INFO_ACK:
|
|
'0',
|
|
PACKET_MATCH_EAP_KEY_INFO_MIC:
|
|
'1',
|
|
PACKET_MATCH_EAP_KEY_INFO_SECURE:
|
|
'0' },
|
|
STATE_KEY_MESSAGE_3)
|
|
STATE_INFO_KEY_MESSAGE_3 = StateInfo("WPA KEY MESSAGE 3",
|
|
DIR_AP_TO_DUT,
|
|
{ PACKET_MATCH_LLC_TYPE:
|
|
LLC_AUTH_TYPE,
|
|
PACKET_MATCH_EAP_KEY_INFO_INSTALL:
|
|
'1',
|
|
PACKET_MATCH_EAP_KEY_INFO_ACK:
|
|
'1',
|
|
PACKET_MATCH_EAP_KEY_INFO_MIC:
|
|
'1',
|
|
PACKET_MATCH_EAP_KEY_INFO_SECURE:
|
|
'1' },
|
|
STATE_KEY_MESSAGE_4)
|
|
STATE_INFO_KEY_MESSAGE_4 = StateInfo("WPA KEY MESSAGE 4",
|
|
DIR_DUT_TO_AP,
|
|
{ PACKET_MATCH_LLC_TYPE:
|
|
LLC_AUTH_TYPE,
|
|
PACKET_MATCH_EAP_KEY_INFO_INSTALL:
|
|
'0',
|
|
PACKET_MATCH_EAP_KEY_INFO_ACK:
|
|
'0',
|
|
PACKET_MATCH_EAP_KEY_INFO_MIC:
|
|
'1',
|
|
PACKET_MATCH_EAP_KEY_INFO_SECURE:
|
|
'1' },
|
|
STATE_DHCP_DISCOVER)
|
|
STATE_INFO_DHCP_DISCOVER = StateInfo("DHCP DISCOVER",
|
|
DIR_DUT_TO_AP,
|
|
{ PACKET_MATCH_IP_PROTOCOL_TYPE:
|
|
IP_UDP_PROTOCOL_TYPE,
|
|
PACKET_MATCH_DHCP_MESSAGE_TYPE:
|
|
DHCP_DISCOVER_MESSAGE_TYPE },
|
|
STATE_DHCP_OFFER)
|
|
STATE_INFO_DHCP_OFFER = StateInfo("DHCP OFFER",
|
|
DIR_AP_TO_DUT,
|
|
{ PACKET_MATCH_IP_PROTOCOL_TYPE:
|
|
IP_UDP_PROTOCOL_TYPE,
|
|
PACKET_MATCH_DHCP_MESSAGE_TYPE:
|
|
DHCP_OFFER_MESSAGE_TYPE },
|
|
STATE_DHCP_REQ)
|
|
STATE_INFO_DHCP_REQ = StateInfo("DHCP REQUEST",
|
|
DIR_DUT_TO_AP,
|
|
{ PACKET_MATCH_IP_PROTOCOL_TYPE:
|
|
IP_UDP_PROTOCOL_TYPE,
|
|
PACKET_MATCH_DHCP_MESSAGE_TYPE:
|
|
DHCP_REQUEST_MESSAGE_TYPE },
|
|
STATE_DHCP_REQ_ACK)
|
|
STATE_INFO_DHCP_REQ_ACK = StateInfo("DHCP ACK",
|
|
DIR_AP_TO_DUT,
|
|
{ PACKET_MATCH_IP_PROTOCOL_TYPE:
|
|
IP_UDP_PROTOCOL_TYPE,
|
|
PACKET_MATCH_DHCP_MESSAGE_TYPE:
|
|
DHCP_ACK_MESSAGE_TYPE },
|
|
STATE_END)
|
|
STATE_INFO_END = StateInfo("END", 0, {}, STATE_END)
|
|
# Master State Table Map of State Infos
|
|
STATE_INFO_MAP = {STATE_INIT: STATE_INFO_INIT,
|
|
STATE_PROBE_REQ: STATE_INFO_PROBE_REQ,
|
|
STATE_PROBE_RESP: STATE_INFO_PROBE_RESP,
|
|
STATE_AUTH_REQ: STATE_INFO_AUTH_REQ,
|
|
STATE_AUTH_RESP: STATE_INFO_AUTH_RESP,
|
|
STATE_ASSOC_REQ: STATE_INFO_ASSOC_REQ,
|
|
STATE_ASSOC_RESP: STATE_INFO_ASSOC_RESP,
|
|
STATE_KEY_MESSAGE_1:STATE_INFO_KEY_MESSAGE_1,
|
|
STATE_KEY_MESSAGE_2:STATE_INFO_KEY_MESSAGE_2,
|
|
STATE_KEY_MESSAGE_3:STATE_INFO_KEY_MESSAGE_3,
|
|
STATE_KEY_MESSAGE_4:STATE_INFO_KEY_MESSAGE_4,
|
|
STATE_DHCP_DISCOVER:STATE_INFO_DHCP_DISCOVER,
|
|
STATE_DHCP_OFFER: STATE_INFO_DHCP_OFFER,
|
|
STATE_DHCP_REQ: STATE_INFO_DHCP_REQ,
|
|
STATE_DHCP_REQ_ACK: STATE_INFO_DHCP_REQ_ACK,
|
|
STATE_END: STATE_INFO_END}
|
|
|
|
# Packet Details Tuples (User friendly name, Field name)
|
|
PacketDetail = collections.namedtuple(
|
|
"PacketDetail", ["friendly_name", "field_name"])
|
|
PACKET_DETAIL_REASON_CODE = PacketDetail(
|
|
"Reason Code",
|
|
PACKET_MATCH_WLAN_MANAGEMENT_REASON_CODE)
|
|
PACKET_DETAIL_STATUS_CODE = PacketDetail(
|
|
"Status Code",
|
|
PACKET_MATCH_WLAN_MANAGEMENT_STATUS_CODE)
|
|
PACKET_DETAIL_SENDER = PacketDetail(
|
|
"Sender", PACKET_MATCH_WLAN_TRANSMITTER)
|
|
|
|
# Error State Info Tuples (Name, Match fields)
|
|
ErrorStateInfo = collections.namedtuple(
|
|
'ErrorStateInfo', ['name', 'match_fields', 'details'])
|
|
ERROR_STATE_INFO_DEAUTH = ErrorStateInfo("WLAN DEAUTH REQUEST",
|
|
{ PACKET_MATCH_WLAN_FRAME_TYPE:
|
|
WLAN_DEAUTH_REQ_FRAME_TYPE },
|
|
[ PACKET_DETAIL_SENDER,
|
|
PACKET_DETAIL_REASON_CODE ])
|
|
ERROR_STATE_INFO_DEASSOC = ErrorStateInfo("WLAN DISASSOC REQUEST",
|
|
{ PACKET_MATCH_WLAN_FRAME_TYPE:
|
|
WLAN_DISASSOC_REQ_FRAME_TYPE },
|
|
[ PACKET_DETAIL_SENDER,
|
|
PACKET_DETAIL_REASON_CODE ])
|
|
# Master State Table Tuple of Error State Infos
|
|
ERROR_STATE_INFO_TUPLE = (ERROR_STATE_INFO_DEAUTH, ERROR_STATE_INFO_DEASSOC)
|
|
|
|
# These warnings actually match successful states, but since the we
|
|
# check forwards and backwards through the state machine for the successful
|
|
# version of these packets, they can only match a failure.
|
|
WARNING_INFO_AUTH_REJ = ErrorStateInfo(
|
|
"WLAN AUTH REJECTED",
|
|
{ PACKET_MATCH_WLAN_FRAME_TYPE: WLAN_AUTH_REQ_FRAME_TYPE },
|
|
[ PACKET_DETAIL_STATUS_CODE ])
|
|
WARNING_INFO_ASSOC_REJ = ErrorStateInfo(
|
|
"WLAN ASSOC REJECTED",
|
|
{ PACKET_MATCH_WLAN_FRAME_TYPE: WLAN_ASSOC_RESP_FRAME_TYPE },
|
|
[ PACKET_DETAIL_STATUS_CODE ])
|
|
|
|
# Master Table Tuple of warning information.
|
|
WARNING_INFO_TUPLE = (WARNING_INFO_AUTH_REJ, WARNING_INFO_ASSOC_REJ)
|
|
|
|
|
|
def __init__(self, ap_macs, dut_mac, filtered_packets, capture, logger):
|
|
self._current_state = self._get_state(self.STATE_INIT)
|
|
self._reached_states = []
|
|
self._skipped_states = []
|
|
self._packets = filtered_packets
|
|
self._capture = capture
|
|
self._dut_mac = dut_mac
|
|
self._ap_macs = ap_macs
|
|
self._log = logger
|
|
self._acks = []
|
|
|
|
@property
|
|
def acks(self):
|
|
return self._acks
|
|
|
|
def _get_state(self, state):
|
|
return self.STATE_INFO_MAP[state]
|
|
|
|
def _get_next_state(self, state):
|
|
return self._get_state(state.next_state)
|
|
|
|
def _get_curr_next_state(self):
|
|
return self._get_next_state(self._current_state)
|
|
|
|
def _fetch_packet_field_value(self, packet, field):
|
|
layer_object = packet
|
|
for layer in field.split('.'):
|
|
try:
|
|
layer_object = getattr(layer_object, layer)
|
|
except AttributeError:
|
|
return None
|
|
return layer_object
|
|
|
|
def _match_packet_fields(self, packet, fields):
|
|
for field, exp_value in fields.items():
|
|
value = self._fetch_packet_field_value(packet, field)
|
|
if exp_value != value:
|
|
return False
|
|
return True
|
|
|
|
def _fetch_packet_data_rate(self, packet):
|
|
return self._fetch_packet_field_value(packet,
|
|
self.PACKET_MATCH_RADIOTAP_DATA_RATE)
|
|
|
|
def _does_packet_match_state(self, state, packet):
|
|
fields = state.match_fields
|
|
if self._match_packet_fields(packet, fields):
|
|
if state.direction == self.DIR_TO_DUT:
|
|
# This should have receiver addr of DUT
|
|
if packet.wlan.ra == self._dut_mac:
|
|
return True
|
|
elif state.direction == self.DIR_FROM_DUT:
|
|
# This should have transmitter addr of DUT
|
|
if packet.wlan.ta == self._dut_mac:
|
|
return True
|
|
elif state.direction == self.DIR_AP_TO_DUT:
|
|
# This should have receiver addr of DUT &
|
|
# transmitter addr of AP's
|
|
if ((packet.wlan.ra == self._dut_mac) and
|
|
(packet.wlan.ta in self._ap_macs)):
|
|
return True
|
|
elif state.direction == self.DIR_DUT_TO_AP:
|
|
# This should have transmitter addr of DUT &
|
|
# receiver addr of AP's
|
|
if ((packet.wlan.ta == self._dut_mac) and
|
|
(packet.wlan.ra in self._ap_macs)):
|
|
return True
|
|
return False
|
|
|
|
def _does_packet_match_error_state(self, state, packet):
|
|
fields = state.match_fields
|
|
return self._match_packet_fields(packet, fields)
|
|
|
|
def _get_packet_detail(self, details, packet):
|
|
attributes = []
|
|
attributes.append("Packet number: %s" % packet.number)
|
|
for detail in details:
|
|
value = self._fetch_packet_field_value(packet, detail.field_name)
|
|
attributes.append("%s: %s" % (detail.friendly_name, value))
|
|
return attributes
|
|
|
|
def _does_packet_match_ack_state(self, packet):
|
|
fields = { self.PACKET_MATCH_WLAN_FRAME_TYPE: self.WLAN_ACK_FRAME_TYPE }
|
|
return self._match_packet_fields(packet, fields)
|
|
|
|
def _does_packet_contain_retry_flag(self, packet):
|
|
fields = { self.PACKET_MATCH_WLAN_FRAME_RETRY_FLAG:
|
|
self.WLAN_FRAME_RETRY }
|
|
return self._match_packet_fields(packet, fields)
|
|
|
|
def _check_for_ack(self, state, packet):
|
|
if (packet.wlan.da == self.WLAN_BROADCAST_ADDRESS and
|
|
packet.wlan.fc_type == self.WLAN_FRAME_CONTROL_TYPE_MANAGEMENT):
|
|
# Broadcast management frames are not ACKed.
|
|
return True
|
|
next_packet = self._capture.get_packet_after(packet)
|
|
if not next_packet or not (
|
|
(self._does_packet_match_ack_state(next_packet)) and
|
|
(next_packet.wlan.addr == packet.wlan.ta)):
|
|
msg = "WARNING! Missing ACK for state: " + \
|
|
state.name + "."
|
|
self._log.log_to_output_file(msg)
|
|
return False
|
|
self._acks.append(int(next_packet.number))
|
|
return True
|
|
|
|
def _check_for_error(self, packet):
|
|
for error_state in self.ERROR_STATE_INFO_TUPLE:
|
|
if self._does_packet_match_error_state(error_state, packet):
|
|
error_attributes = self._get_packet_detail(error_state.details,
|
|
packet)
|
|
msg = "ERROR! State Machine encountered error due to " + \
|
|
error_state.name + ", " + \
|
|
", ".join(error_attributes) + "."
|
|
self._log.log_to_output_file(msg)
|
|
return True
|
|
return False
|
|
|
|
def _check_for_warning(self, packet):
|
|
for warning in self.WARNING_INFO_TUPLE:
|
|
if self._does_packet_match_error_state(warning, packet):
|
|
error_attributes = self._get_packet_detail(warning.details,
|
|
packet)
|
|
msg = "WARNING! " + warning.name + " found, " + \
|
|
", ".join(error_attributes) + "."
|
|
self._log.log_to_output_file(msg)
|
|
return True
|
|
return False
|
|
|
|
def _check_for_repeated_state(self, packet):
|
|
for state in self._reached_states:
|
|
if self._does_packet_match_state(state, packet):
|
|
msg = "WARNING! Repeated State: " + \
|
|
state.name + ", Packet number: " + \
|
|
str(packet.number)
|
|
if self._does_packet_contain_retry_flag(packet):
|
|
msg += " due to retransmission."
|
|
else:
|
|
msg += "."
|
|
self._log.log_to_output_file(msg)
|
|
|
|
def _is_from_previous_state(self, packet):
|
|
for state in self._reached_states + self._skipped_states:
|
|
if self._does_packet_match_state(state, packet):
|
|
return True
|
|
return False
|
|
|
|
def _step(self, reached_state, packet):
|
|
# We missed a few packets in between
|
|
if self._current_state != reached_state:
|
|
msg = "WARNING! Missed states: "
|
|
skipped_state = self._current_state
|
|
while skipped_state != reached_state:
|
|
msg += skipped_state.name + ", "
|
|
self._skipped_states.append(skipped_state)
|
|
skipped_state = self._get_next_state(skipped_state)
|
|
msg = msg[:-2]
|
|
msg += "."
|
|
self._log.log_to_output_file(msg)
|
|
msg = "Found state: " + reached_state.name
|
|
if packet:
|
|
msg += ", Packet number: " + str(packet.number) + \
|
|
", Data rate: " + str(self._fetch_packet_data_rate(packet))+\
|
|
"Mbps."
|
|
else:
|
|
msg += "."
|
|
self._log.log_to_output_file(msg)
|
|
# Ignore the Init state in the reached states
|
|
if packet:
|
|
self._reached_states.append(reached_state)
|
|
self._current_state = self._get_next_state(reached_state)
|
|
|
|
def _step_init(self):
|
|
#self.log_to_output_file("Starting Analysis")
|
|
self._current_state = self._get_curr_next_state()
|
|
|
|
def analyze(self):
|
|
""" Starts the analysis of the Wifi Protocol Exchange. """
|
|
|
|
# Start the state machine iteration
|
|
self._step_init()
|
|
packet_iterator = iter(self._packets)
|
|
for packet in packet_iterator:
|
|
self._check_for_repeated_state(packet)
|
|
# Try to look ahead in the state machine to account for occasional
|
|
# packet capture misses.
|
|
next_state = self._current_state
|
|
while next_state != self.STATE_INFO_END:
|
|
if self._does_packet_match_state(next_state, packet):
|
|
self._step(next_state, packet)
|
|
self._check_for_ack(next_state, packet)
|
|
break
|
|
next_state = self._get_next_state(next_state)
|
|
if self._current_state == self.STATE_INFO_END:
|
|
self._log.log_to_output_file("State Machine completed!")
|
|
return True
|
|
if self._check_for_error(packet):
|
|
return False
|
|
if not self._is_from_previous_state(packet):
|
|
self._check_for_warning(packet)
|
|
msg = "ERROR! State Machine halted at " + self._current_state.name + \
|
|
" state."
|
|
self._log.log_to_output_file(msg)
|
|
return False
|
|
|
|
|
|
class ChaosCaptureAnalyzer(object):
|
|
""" Class to analyze the packet capture from a chaos test . """
|
|
|
|
def __init__(self, ap_bssids, ap_ssid, dut_mac, logger):
|
|
self._ap_bssids = ap_bssids
|
|
self._ap_ssid = ap_ssid
|
|
self._dut_mac = dut_mac
|
|
self._log = logger
|
|
|
|
def _validate_ap_presence(self, capture, bssids, ssid):
|
|
beacon_count = capture.count_beacons_from(bssids)
|
|
if not beacon_count:
|
|
packet_count = capture.count_packets_from(bssids)
|
|
if not packet_count:
|
|
self._log.log_to_output_file(
|
|
"No packets at all from AP BSSIDs %r!" % bssids)
|
|
else:
|
|
self._log.log_to_output_file(
|
|
"No beacons from AP BSSIDs %r but %d packets!" %
|
|
(bssids, packet_count))
|
|
return False
|
|
self._log.log_to_output_file("AP BSSIDs: %s, SSID: %s." %
|
|
(bssids, ssid))
|
|
self._log.log_to_output_file("AP beacon count: %d." % beacon_count)
|
|
return True
|
|
|
|
def _validate_dut_presence(self, capture, dut_mac):
|
|
tx_count = capture.count_packets_from([dut_mac])
|
|
if not tx_count:
|
|
self._log.log_to_output_file(
|
|
"No packets Tx at all from DUT MAC %r!" % dut_mac)
|
|
return False
|
|
rx_count = capture.count_packets_to([dut_mac])
|
|
self._log.log_to_output_file("DUT MAC: %s." % dut_mac)
|
|
self._log.log_to_output_file(
|
|
"DUT packet count Tx: %d, Rx: %d." % (tx_count, rx_count))
|
|
return True
|
|
|
|
def _ack_interleave(self, packets, capture, acks):
|
|
"""Generator that interleaves packets with their associated ACKs."""
|
|
for packet in packets:
|
|
packet_number = int(packet.no)
|
|
while acks and acks[0] < packet_number:
|
|
# ACK packet does not appear in the filtered capture.
|
|
yield capture.get_packet_number(acks.pop(0), summary=True)
|
|
if acks and acks[0] == packet_number:
|
|
# ACK packet also appears in the capture.
|
|
acks.pop(0)
|
|
yield packet
|
|
|
|
def analyze(self, trace):
|
|
"""
|
|
Starts the analysis of the Chaos capture.
|
|
|
|
@param trace: Packet capture file path to analyze.
|
|
|
|
"""
|
|
basename = os.path.basename(trace)
|
|
self._log.log_start_section("Packet Capture File: %s" % basename)
|
|
capture = PacketCapture(trace)
|
|
bssids = self._ap_bssids
|
|
ssid = self._ap_ssid
|
|
if not self._validate_ap_presence(capture, bssids, ssid):
|
|
return
|
|
dut_mac = self._dut_mac
|
|
if not self._validate_dut_presence(capture, dut_mac):
|
|
return
|
|
decryption = 'chromeos:%s' % ssid
|
|
self._log.log_start_section("WLAN Protocol Verification")
|
|
filtered_packets = capture.get_filtered_packets(
|
|
bssids, dut_mac, False, decryption)
|
|
wifi_state_machine = WifiStateMachineAnalyzer(
|
|
bssids, dut_mac, filtered_packets, capture, self._log)
|
|
wifi_state_machine.analyze()
|
|
self._log.log_start_section("Filtered Packet Capture Summary")
|
|
filtered_packets = capture.get_filtered_packets(
|
|
bssids, dut_mac, True, decryption)
|
|
for packet in self._ack_interleave(
|
|
filtered_packets, capture, wifi_state_machine.acks):
|
|
self._log.log_to_output_file("%s" % (packet))
|