# Codelab: Using wifi Autotests to learn 802.11 basics

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.

## Setup

### Prerequisites

* Access to a wifi test setup (local or in test lab). Read the
  [wificell documentation] for some background.
* Understanding of Autotest and the [test_that] command.

### Configuration Considerations

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.

#### Using the wifi testing labs

Our testing lab setups are operated through the skylab infrastructure. If you
don't have the skylab tool installed on your machine, follow the instructions
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.

```bash
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):

```bash
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`.

#### Using a local testing setup

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`:

```bash
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.

### Let's get started

[network_WiFi_SimpleConnect] is a very simple test that connects and disconnects
a DUT from a router, so it's 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.

#### 1. Gather pcap data

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
let's store the results of this function in a variable:

```python3
capture_results = self.context.capture_host.stop_capture()
```

The pcap file is accessible at `capture_results[0].local_pcap_path`, so let's
print out a dump of our captured packets. Add these lines after the call to
`stop_capture()`:

```python3
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:

```bash
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.

#### 2. Use Wireshark to analyze the packets

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:

```python3

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]:

```python3

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:

```python3

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.

#### 3. Learn some 802.11 background

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.

![State Machine](assets/wifi-state-machine.gif)

##### Authentication and Association

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

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:**

* *Beacons* are frames that access points send out on a regular interval to
broadcast their existence to the world. Devices are only aware of access points
because they can see the beacon frames they send.
* Devices respond to beacons with *Probe Requests* which in turn let the
network know of their existence. The probe request also includes a list of all
data rates the device supports, which the network can use to check for
compatibility with those supported by the access point.
* Access points respond with *Probe Responses* which either confirm or deny
compatibility.
* If the two are compatible, they can engage in the authentication/association
process as explained above with various *Association* and *Authentication*
frames.

##### Class 2 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

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.

#### 4. Let's analyze some packets

Now that we have a basic understanding of 802.11 frame classes, we can use our
captured packets to study the 802.11 connection/disconnection 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:

1. What is the MAC address of your router? (you may already know this, but
   try to infer from the context of the packets)
1. What is the MAC address of your DUT?
1. What is the beacon interval (time between beacons) of your router?
1. What could a receiver address of *ff:ff:ff:ff:ff:ff* indicate?

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.

#### 5. Filter the frames and check your results

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.

```python3

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:

```bash
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.

## Lookup Tables

### Management Frames (Class 1)

| 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         | Disassociation         |
| 1011          | 0x0b         | Authentication         |
| 1100          | 0x0c         | Deauthentication       |
| 1101          | 0x0d         | Action                 |

### Control Frames (Class 2)

| 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                |

### Data Frames (Class 3)

| 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) |

## Solutions and hints

### Configuration questions

1. Your router should be sending many beacon packets (type 0x08 frames), so
   look for the source address of the frames of type 0x08.
1. Your DUT can be recognized as the device which has a "conversation" with
   your router. I.e. you should be able to see one IP which is the
   sender/receiver of several different management frames (0x00, 0x01, etc.)
   with your router.
1. The beacon interval is the time a device waits between sending beacon
   frames. You can determine this interval for a device by finding the time
   that passes between two beacons being transmitted by the
   device. The beacon interval for your router is most likely 100ms.
1. A receiver address of *ff:ff:ff:ff:ff:ff* indicates that the frame is
   being broadcasted to any receiver that can hear it. This is pattern is
   used for beacon frames because these frames are intended as a sort of 'ping'
   to all nearby devices.

### Packet filter solution

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:

1. Probe Request: 0x04
1. Probe Response: 0x05
1. Authentication: 0x0b
1. Assoc Request: 0x00
1. Assoc Response: 0x01
1. Deauthentication: 0x0c

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 are strictly required.)

[chameleon issues]: https://crbug.com/964549
[lookup tables]: #lookup-tables
[network_WiFi_UpdateRouter]: ../server/site_tests/network_WiFi_UpdateRouter/network_WiFi_UpdateRouter.py
[network_WiFi_SimpleConnect]: ../server/site_tests/network_WiFi_SimpleConnect/network_WiFi_SimpleConnect.py
[Pyshark documentation]: https://kiminewt.github.io/pyshark/
[section 4]: #4_let_s-analyze-some-packets
[skylab portal]: https://chromeos-swarming.appspot.com/botlist?c=id&c=task&c=dut_state&c=label-board&c=label-model&c=label-pool&c=os&c=provisionable-cros-version&c=status&d=asc&f=label-wificell%3ATrue&k=label-wificell&s=id
[skylab tools guide]: http://go/skylab-cli
[solutions and hints]: #solutions-and-hints
[test_that]: ./test-that.md
[wificell documentation]: ./wificell.md
[wireshark docs]: https://www.wireshark.org/docs/dfref/