The Autotest infrastructure provides packet capture functionality which we can use to intercept and view WiFi packets that are sent between the Device-Under-Test (DUT) and router during a test. In this codelab we will analyze the packet sequence during the connection process to learn the basics of 802.11 connection protocols.
This codelab can be completed from either a personal testing setup or a dedicated setup in our testing lab, but there are a few special considerations in each case. For instance, some of the commands in this lab will use the variable ${DUT_HOSTNAME}
, and the value of this variable is dependent on the testing setup that you use. Further considerations are included below in the instructions for each option.
Our testing lab setups are operated through the skylab infrastructure. If you don't have the skylab tool installed on your machine, follow the instrucions under Advanced users: Manual installation in the skylab tools guide.
Once you have the skylab tool, you'll need to run the login command and follow its instructions to get started.
skylab login
For this codelab, you will need to use a wificell
test setup. Available DUTs can be found on the skylab portal. To find a wificell test setup, visit the portal and filter for label-wificell = true (the filter should already be set when you click the link). You'll need to find a setup who's current task is idle with dut_state ready, and then lock it while in use. To lock a DUT in the skylab use this command to lease it for the specified number of minutes (60 minutes should suffice for this codelab, but if your lease expires you can simply lease your DUT again):
skylab lease-dut -minutes ${NUM_MINUTES} ${DUT_NAME}
*** note Note: There are several similar fields on the bot page that can potentially be confused. Bots are listed by their id field in the skylab search portal, which usually takes a form similar to crossk-chromeos15-row2-rack4-host6
. dut_name is referred to in this document by the variable ${DUT_NAME}
, and is typically the id without crossk
, e.g. chromeos15-row2-rack4-host6
. The hostname for a DUT (${DUT_HOSTNAME}
in this doc) is not shown on the skylab bot page, but it is the dut_name with '.cros' appended e.g. chromeos15-row2-rack4-host6.cros
.
Autotest requires a working build of the board type being tested on, so it is best to pick a board for which you have already built an image on your machine.
Autotest will automatically determine the hostnames of the router and packet capture device but if you want to access them directly, say through ssh, you can use the hostnames ${DUT_NAME}-router.cros and ${DUT_NAME}-pcap.cros respectively. You can access each with ssh through the root user with password test0000
.
Lastly, Autotest may have issues with hosts that have the chameleon
label. If you are having chameleon issues, the current workaround is to set enable_ssh_tunnel_for_chameleon: True in src/third_party/autotest/files/global_config.ini
.
For a local test setup, you'll need a flashed DUT and two flashed Google-made wifi routers that run Chrome OS, all running special test images. The Google-made routers can be either of the boards whirlwind
or gale
, and see network_WiFi_UpdateRouter for what images they should be running. In order for Autotest to determine the hostnames of your router and packet capture device, you'll have to designate their IP addresses within your chroot. Assign the IP address of your DUT to 'dut', and the IPs of your routers to 'dut-router' and 'dut-pcap' by adding lines like these to /etc/hosts
:
xxx.xxx.xxx.xxx dut-router xxx.xxx.xxx.xxx dut-pcap xxx.xxx.xxx.xxx dut
Now, you can use ${DUT_HOSTNAME} = 'dut' and Autotest will use your hosts file to find the other devices. The final consideration when using a local testing setup is that the designated testbeds are contained in shielding boxes which isolate them from other signals, while your local setup is probably held in open air. This means that your packet capture device will also pick up packets from any other devices broadcasting in your area. This will make the packet feed noisier, but you can still find all the packets involved in the connection process so its not a dealbreaker for this codelab.
network_WiFi_SimpleConnect is a very simple test that connects and disconnects a DUT from a router, so its ideal for our purposes in this codelab. The test itself is held at server/site_tests/network_WiFi_SimpleConnect/
in the Autotest repository. Briefly look through this file to get a sense for what it is doing.
Before you make any changes to code, be sure to start a new branch within the Autotest repository.
Our first goal is to initiate packet capture and record all of the frames that our pcap device sees throughout the test. Conveniently, network_WiFi_SimpleConnect already utilizes a pcap device, which is accessed at self.context.capture_host
. Before the testing starts, the test begins capturing packets by calling start_capture()
on the capture device, and after the test completes, stop_capture()
completes the capturing process. stop_capture()
returns a list of filepaths that hold the captured packets, so lets store the results of this function in a variable:
capture_results = self.context.capture_host.stop_capture()
The pcap file is accessible at capture_results[0].local_pcap_path
, so lets print out a dump of our captured packets. Add these lines after the call to stop_capture()
:
packets = open(capture_results[0].local_pcap_path, 'r') logging.info(packets.read()) packets.close()
Now, lets run the test and see what we can learn:
test_that --fast -b ${BOARD} ${DUT_HOSTNAME} network_WiFi_SimpleConnect.wifi_check5HT20
That's a lot of garbage. The packets aren't going to be much use to us in their current state. In the next section, we'll use Wireshark to translate the packets into a readable form that we can study.
Pyshark is a wrapper for Wireshark within Python, and we'll be using it in this codelab to interperet our captured packets. Learn more at the Pyshark documentation page.
Delete the lines you just added and replace them with calls to Pyshark that will parse and translate the packets, then write the packets to a file:
import pyshark capture = pyshark.FileCapture( input_file=capture_results[0].local_pcap_path) capture.load_packets(timeout=2) packet_file = open('/tmp/pcap', 'w') for packet in capture: packet_file.write(str(packet)) packet_file.close()
Run the Autotest again and open /tmp/pcap
. Look at that, tons of human-readable data! Maybe even a little too much? Right now we're getting the entirety of every packet, but we only need a few fields. As a final step, we're going to parse out the needed fields from each packet so we can digest some relevant information about the connection process. Add the following methods to the global scope of network_WiFi_SimpleConnect:
def _fetch_frame_field_value(frame, field): layer_object = frame for layer in field.split('.'): try: layer_object = getattr(layer_object, layer) except AttributeError: return None return layer_object """ Parses input frames and stores frames of type listed in filter_types. If filter_types is empty, stores all parsed frames. """ def parse_frames(capture_frames, filter_types): frames = [] for frame in capture_frames: frame_type = _fetch_frame_field_value( frame, 'wlan.fc_type_subtype') if filter_types and frame_type not in filter_types: continue frametime = frame.sniff_time source_addr = _fetch_frame_field_value( frame, 'wlan.sa') dest_addr = _fetch_frame_field_value( frame, 'wlan.da') frames.append([frametime, source_addr, dest_addr, frame_type]) return frames
Using these functions, you can retrieve a timestamp, the source address, the destination address, and the frame subtype for every packet that your pcap device captured over the course of the test. The keywords within parse_frames()
('wlan.sa', 'wlan.da', 'wlan.fc_type_subtype'), are special Wireshark filters that correspond to the relevant data we are looking for. There are over 242000 such filters which you can find in the wireshark docs.
Now we just need to call parse_frames()
and upgrade our packet logging logic. Replace the file logging logic from above with the following code which parses the frames into a much more readable format:
frameTypesToFilter = {} frames = parse_frames(capture, frameTypesToFilter) packet_file = open('/tmp/pcap', 'w') packet_file.write('{:^28s}|{:^19s}|{:^19s}|{:^6s}\n'.format( 'Timestamp', 'Source Address', 'Receiver Address', 'Type')) packet_file.write('---------------------------------------------------------------------------\n') for packet in frames: packet_file.write('{:^28s}|{:^19s}|{:^19s}|{:^6s}\n'.format( str(packet[0]), packet[1], packet[2], packet[3])) packet_file.close()
This time when we run the test we can very concisely see every single packet that our pcap device captured, and we get only the data which is relevant to our purposes. Later on we'll populate frameTypesToFilter
to single out the frames that are relevant to the connection/disconnection process, but first let's look deeper into the frames themselves.
Before we start analyzing the packets, we need some background on 802.11 frames. The state machine below represents the 802.11 connection/disconnection protocol. As you can see, a connection's state is determined by the authentication and association status between its devices. The types of packets that a device is able to send and receive are dependent on the state of its connections.
In order to ensure security, users must be authenticated to a network before they are allowed to use the network. The authentication process itself is not strictly defined by the 802.11 protocol, but it usually consists of a robust cryptographic exchange that allows the network to trust the user. Once a user has been authenticated to the network, it is trusted, but it is still not actually a member of the network until it has been associated. Association can be thought of as the proccess of actually joining the network, and also acts as a sort of registration that allows the network to determine which access point to use for a given user.
Class 1 frames can be sent in any state, and they are used to support the basic operations of 802.11 connections. Class 1 frames are called Management Frames and they allow devices to find a network and negotiate their connection status.
Some class 1 frames:
Class 2 frames can only be sent from a successfully authenticated device, which means they can be sent in states 2 and 3. Class 2 frames are called Control Frames, and their purpose is to allow authenticated devices to negotiate the sending of data between them. Request to send (RTS), clear to send (CTS), and acknowledge (ACK) are all examples of class 2 frames.
Class 3 frames can only be sent from an authenticated and associated device, meaning they can only be sent while in state 3. Class 3 frames are Data Frames and they make up all of the actual bulk of wireless communication. All frames which are used to send non-meta data between devices are data frames.
Now that we have a basic understanding of 802.11 frame classes, we can use our captured packets to study the 802.11 connection/disconection protocol in action. Near the bottom of this page is a set of lookup tables that outline every type of frame in the 802.11 protocol, which you can use to determine what kind of packets we picked up.
Solutions and hints to the questions below can be found after the lookup tables at the bottom of this page, but please do your best to answer them yourself before referring to the solutions.
Lets see if we can answer some basic questions about your configuration based on the context of the captured packets:
Now, try to find the frames where the DUT and router negotiate their connection. Depending on how noisy your setup is this could be somewhat difficult, but you should be able to see the authentication/association process in action by looking for some key frame types. (Hint: look for a class 3 frame being sent from your DUT to your router, and work back to see the frames that got them there). Study the process and compare the flow to the frame class descriptions above.
We can populate framesToFilter
with frame type codes (i.e. '0x04') to show only the frames that are a part of the connection process. Based on what you know about the 802.11 state machine, begin filtering for frames that you know are relevant. Do not include beacon frames (type 0x08) because while they are a part of the connection process, there are so many of them that they will clog up the output. After improving the filter, add the following code to the bottom of network_WiFi_SimpleConnect to produce another output file which only shows the frametypes so the testing script can parse them.
output_file = open('/tmp/filtered_pcap', 'w') for packet in frames: output_file.write(str(packet[3]) + '\n') output_file.close()
Now, test your ouput file by running the testing python script:
python wifi-basics-codelab-pcap-test.py
This script will check your output to see if you've isolated the correct frames and that the entire connection sequence can be seen. If the script fails, keep adjusting your filter until it succeeds. After you have passed the test, review /tmp/pcap
again to see the entire process in action. Finally, refer back to the questions in section 4 one last time to see if you've gained any new insight into the 802.11 protocol.
Subtype Value | Hex Encoding | Subtype Name |
---|---|---|
0000 | 0x00 | Association Request |
0001 | 0x01 | Association Response |
0010 | 0x02 | Reassociation Request |
0011 | 0x03 | Reassociation Response |
0100 | 0x04 | Probe Request |
0101 | 0x05 | Probe Response |
1000 | 0x08 | Beacon |
1001 | 0x09 | ATIM |
1010 | 0x0a | Dissossiation |
1011 | 0x0b | Authentication |
1100 | 0x0c | Deauthentication |
1101 | 0x0d | Action |
Subtype Value | Hex Encoding | Subtype Name |
---|---|---|
1000 | 0x18 | Block Acknowedgement Request |
1001 | 0x19 | Block Acknowledgement |
1010 | 0x1a | Power Save (PS)-Poll |
1011 | 0x1b | RTS |
1100 | 0x1c | CTS |
1101 | 0x1d | Acknowledgement (ACK) |
1110 | 0x1e | Contention-Free (CF)-End |
1111 | 0x1f | CF-End+CF-ACK |
Subtype Value | Hex Encoding | Subtype Name |
---|---|---|
0000 | 0x20 | Data |
0001 | 0x21 | Data + CF-Ack |
0010 | 0x22 | Data + CF-Poll |
0011 | 0x23 | Data + CF-Ack+CF-Poll |
0100 | 0x24 | Null Data (no data transmitted) |
0101 | 0x25 | CF-Ack (no data transmitted) |
0110 | 0x26 | CF-Poll (no data transmitted) |
0111 | 0x27 | CF-Ack + CF-Poll (no data transmitted) |
1000 | 0x28 | QoS Data |
1001 | 0x29 | Qos Data + CF-Ack |
1010 | 0x2a | QoS Data + CF-Poll |
1011 | 0x2b | QoS Data + CF-Ack + CF-Poll |
1100 | 0x2c | QoS Null (no data transmitted) |
1101 | 0x2d | Qos CF-Ack (no data transmitted) |
1110 | 0x2e | QoS CF-Poll (no data transmitted) |
1111 | 0x2f | QoS CF-Ack + CF-Poll (no data transmitted) |
The testing script is looking for a particular packet sequence that shows the DUT and router connecting to each other. The golden connection sequence is as follows:
In practice, we have noticed that many of the recorded connection sequences do not include an Assoc Request packet, so the script is tolerant of that case.
Finally, the script also verifies that no non-relevant frames were included, so any non class 1 frames in the output file will cause failure. (Although, only the frames in the sequence above above are strictly required.)