Linux for Network Engineers: How to do channel hopping during WiFi packet capturing

By May 19, 2021Linux

In a previous post we talked about how to set a WiFi interface to monitor mode manually, but also through a script. In this post, we’ll talk about how to set the channel and the channel width on a monitor interface but also now to do channel hopping while running a packet capture.

For the purposes of this post I use the same setup with the previous post in terms of hardware, WiFi dongle, driver and iw version.

Supported Channels and Widths

In order to be able to set 80 MHz channels with iw, you need version  4.14 or greater. In my case, I have installed version 5.9:

iw --version
iw version 5.9

The syntax of settings channels has changed after version 4.14, and in order to get the correct syntax you can enter an incomplete command to get the help output as follows:

netbeez$ iw dev wlan0 set channel
Usage:  iw [options] dev <devname> set channel <channel> [NOHT|HT20|HT40+|HT40-|5MHz|10MHz|80MHz]
Options:
        --debug         enable netlink debugging

Once you set the interface to monitor mode the channel and channel width are left at the same configuration they were while the interface was connected to your WiFi network (if it was ever). In my case, it was on channel 149 at 80MHz:

netbeez$ iw wlan0 info
Interface wlan0
        ifindex 3
        wdev 0x1
        addr 20:0d:b0:47:57:79
        type monitor
        wiphy 0
        channel 149 (5745 MHz), width: 80 MHz, center1: 5775 MHz
        txpower 18.00 dBm

As always, when setting a channel and channel width we have to make sure that 

  1. They have acceptable values (e.g. we can’t set channel 1 at 40- MHz)
  2. The driver and hardware support the requested channel and width values

The first step it to determine what channels your setup supports with the following:

netbeez$ iw list
Wiphy phy0
        wiphy index: 0
        max # scan SSIDs: 9
        max scan IEs length: 2304 bytes
        max # sched scan SSIDs: 0
        max # match sets: 0
        max # scan plans: 1
        max scan plan interval: -1
. . . 
. . .
. . .
             Frequencies:
                        * 2412 MHz [1] (20.0 dBm)
                        * 2417 MHz [2] (20.0 dBm)
                        * 2422 MHz [3] (20.0 dBm)
                        * 2427 MHz [4] (20.0 dBm)
                        * 2432 MHz [5] (20.0 dBm)
                        * 2437 MHz [6] (20.0 dBm)
                        * 2442 MHz [7] (20.0 dBm)
                        * 2447 MHz [8] (20.0 dBm)
                        * 2452 MHz [9] (20.0 dBm)
                        * 2457 MHz [10] (20.0 dBm)
                        * 2462 MHz [11] (20.0 dBm)
                        * 2467 MHz [12] (20.0 dBm) (no IR)
                        * 2472 MHz [13] (20.0 dBm) (no IR)
                        * 2484 MHz [14] (20.0 dBm) (no IR)

. . .
. . .
. . .
            Frequencies:
                        * 5180 MHz [36] (20.0 dBm) (no IR)
                        * 5200 MHz [40] (20.0 dBm) (no IR)
                        * 5220 MHz [44] (20.0 dBm) (no IR)
                        * 5240 MHz [48] (20.0 dBm) (no IR)
                        * 5260 MHz [52] (20.0 dBm) (no IR, radar detection)
                        * 5280 MHz [56] (20.0 dBm) (no IR, radar detection)
                        * 5300 MHz [60] (20.0 dBm) (no IR, radar detection)
                        * 5320 MHz [64] (20.0 dBm) (no IR, radar detection)
                        * 5500 MHz [100] (20.0 dBm) (no IR, radar detection)
                        * 5520 MHz [104] (20.0 dBm) (no IR, radar detection)
                        * 5540 MHz [108] (20.0 dBm) (no IR, radar detection)
                        * 5560 MHz [112] (20.0 dBm) (no IR, radar detection)
                        * 5580 MHz [116] (20.0 dBm) (no IR, radar detection)
                        * 5600 MHz [120] (20.0 dBm) (no IR, radar detection)
                        * 5620 MHz [124] (20.0 dBm) (no IR, radar detection)
                        * 5640 MHz [128] (20.0 dBm) (no IR, radar detection)
                        * 5660 MHz [132] (20.0 dBm) (no IR, radar detection)
                        * 5680 MHz [136] (20.0 dBm) (no IR, radar detection)
                        * 5700 MHz [140] (20.0 dBm) (no IR, radar detection)
                        * 5720 MHz [144] (20.0 dBm) (no IR, radar detection)
                        * 5745 MHz [149] (20.0 dBm) (no IR)
                        * 5765 MHz [153] (20.0 dBm) (no IR)
                        * 5785 MHz [157] (20.0 dBm) (no IR)
                        * 5805 MHz [161] (20.0 dBm) (no IR)
                        * 5825 MHz [165] (20.0 dBm) (no IR)
                        * 5845 MHz [169] (disabled)
                        * 5865 MHz [173] (disabled)
                        * 5885 MHz [177] (disabled)

. . .
. . .
. . .

This output is very useful, but it doesn’t specify explicitly what widths are allowed and supported on each channel. It can be determined by trying to set each width for each channel to see if the driver allows you to avoid throwing any errors or warning messages.  The following two commands will set the channel and also confirm that the channel has been set properly:

netbeez$ iw dev wlan0 set channel 149 80MHz
netbeez$ iw wlan0 info
Interface wlan0
        ifindex 3
        wdev 0x1
        addr 20:0d:b0:47:57:79
        type monitor
        wiphy 0
        channel 149 (5745 MHz), width: 80 MHz, center1: 5715 MHz
        txpower 18.00 dBm

If you choose a channel and width combination that is not acceptable you will get an error as follows:

netbeez$ iw dev wlan0 set channel 1 80MHz
command failed: Invalid argument (-22)

Given that there are 14 channels in 2.4 GHz, 25 in 5GHz, and that you have four channel options (HT20, HT40+, HT40-, 80MHz) you would need to run 156 commands to get that information. Of course, that’s not practical and here is a rudimentary script that takes the channel option as input and prints in the output if the specific channel and width combination are acceptable:

#!/bin/bash

channels_24="1 2 3 4 5 6 7 8 9 10 11 12 13 14"
channels_50="36 40 44 48 52 56 60 64 100 104 108 112 116 120 124 128 132 136 140 144 149 153 157 161 165"

channel_width_20="HT20"
channel_width_40plus="HT40+"
channel_width_40minus="HT40-"
channel_width_80="80MHz"

channels="${channels_24} ${channels_50}"

if [[ "${1}" != "HT20" ]] && [[ "${1}" != "HT40-" ]] && [[ "${1}" != "HT40+" ]] && [[ "${1}" != "80MHz" ]]; then
    echo "Wrong input"
    echo "Available channel widths HT20, HT40+, HT40-, 80MHz"
    exit 1
else
    width="${1}"
fi

iw dev wlan0 set type monitor
ifconfig wlan0 up

for channel in ${channels}; do
    echo "Setting channel ${channel}, ${width}"
    if /home/netbeez/iw dev wlan0 set channel "${channel}" "${width}"; then
        if iw wlan0 info | grep "channel ${channel}"; then
            echo "OK:     channel ${channel}, ${width}"
        else
            echo "OK:     channel ${channel}, ${width} not set properly"
        fi
    else
        echo "ERROR: channel ${channel}, ${channel_width}"
    fi
done

And you get an output as follows:

netbeez$ ./test_channels.sh HT40-
Setting channel 1, HT40-
command failed: Invalid argument (-22)
ERROR: channel 1,
Setting channel 2, HT40-
command failed: Invalid argument (-22)
ERROR: channel 2,
Setting channel 3, HT40-
command failed: Invalid argument (-22)
ERROR: channel 3,
Setting channel 4, HT40-
command failed: Invalid argument (-22)
ERROR: channel 4,
Setting channel 5, HT40-
        channel 5 (2432 MHz), width: 40 MHz, center1: 2422 MHz
OK:     channel 5, HT40-
. . .
. . .
. . .
Setting channel 13, HT40-
        channel 13 (2472 MHz), width: 40 MHz, center1: 2462 MHz
OK:     channel 13, HT40-
Setting channel 14, HT40-
command failed: Invalid argument (-22)
ERROR: channel 14,
. . .
. . .
. . .
Setting channel 144, HT40-
        channel 144 (5720 MHz), width: 40 MHz, center1: 5710 MHz
OK:     channel 144, HT40-
Setting channel 149, HT40-
command failed: Invalid argument (-22)
ERROR: channel 149,
Setting channel 153, HT40-
        channel 153 (5765 MHz), width: 40 MHz, center1: 5755 MHz
OK:     channel 153, HT40-
Setting channel 157, HT40-
        channel 157 (5785 MHz), width: 40 MHz, center1: 5775 MHz
OK:     channel 157, HT40-
Setting channel 161, HT40-
        channel 161 (5805 MHz), width: 40 MHz, center1: 5795 MHz
OK:     channel 161, HT40-
Setting channel 165, HT40-
        channel 165 (5825 MHz), width: 40 MHz, center1: 5815 MHz
OK:     channel 165, HT40-

As you can see, in 2.4 GHz you can set a 40- width in channels 5 to 13.

To get a complete list run the script with the following inputs:

netbeez$ ./test_channels.sh HT20
netbeez$ ./test_channels.sh HT40+
netbeez$ ./test_channels.sh HT40-
netbeez$ ./test_channels.sh 80MHz

Configure Channel and Width

Now that you know what channels and widths are supported by your hardware, you can simply set the capture channel and width with the following:

netbeez$ iw dev wlan0 set channel  6 HT20

And start a basic capture without filters on interface “wlan0” as follows:

netbeez$ tcpdump -i wlan0 -w capture.pcap
tcpdump: listening on wlan0, link-type IEEE802_11_RADIO (802.11 plus radiotap header), capture size 262144 bytes
^C1224 packets captured
1281 packets received by filter
0 packets dropped by kernel
9 packets dropped by interface

I let this run for a few seconds and I stopped it by hitting Ctrl+C. Now you can open this file with a tool like wireshark for further analysis.

Channel Hopping

To do channel hopping, you’d have to change the channel while the capture is running. Practically, that’s not feasible to do manually especially if you need a dwell time in the order of milliseconds. 

Here is a rudimentary script that does the channel hopping across all allowable channels based on the channel width and dwell time of 100 ms

#!/bin/bash

channels_24="1 2 3 4 5 6 7 8 9 10 11 12 13 14"
channels_50="36 40 44 48 52 56 60 64 100 104 108 112 116 120 124 128 132 136 140 144 149 153 157 161 165"

channel_width_20="HT20"
channel_width_40plus="HT40+"
channel_width_40minus="HT40-"
channel_width_80="80MHz"

channels="${channels_24} ${channels_50}"
width="${1}"

if [[ "${1}" = "HT20" ]]; then
    channels="${channels_24} ${channels_50}"
elif [[ "${1}" = "HT40+" ]]; then
    channels="1 2 3 4 5 6 7 8 9 36 40 44 48 52 56 60 100 104 108 112 116 120 124 128 132 136 140 153 157"
elif [[ "${1}" = "HT40-" ]]; then
    channels="5 6 7 8 9 10 11 12 13 40 44 48 52 56 60 64  104 108 112 116 120 124 128 132 136 140 144 153 157 161"
elif [[ "${1}" = "80MHz" ]]; then
    channels="${channels_50}"
fi

iw dev wlan0 set type monitor
ifconfig wlan0 up

for channel in ${channels}; do
    echo "Setting channel ${channel}, ${width}"
    iw dev wlan0 set channel "${channel}" "${width}"
    sleep 0.1
done

To run this script in parallel with the tcpdump capture you have either to use two different terminals (one for the channel hopping script and one for the tcpdump capture) or to run the channel hopping script as a background process and then launch the tcpdump capturing command as follows:

netbeez$ ./set_channels.sh HT20 &
[2] 15917
netbeez$ tcpdump -i wlan0 -w capture.pcap
tcpdump: listening on wlan0, link-type IEEE802_11_RADIO (802.11 plus radiotap header), capture size 262144 bytes
^C930 packets captured
930 packets received by filter
0 packets dropped by kernel

Once you stop tcpdump with “Ctrl+C” you can stop the channel hopping script with

netbeez$ kill 15917