Recently, I've been working on dynamic channel selection based on channel utilization. One problem I encountered is: how to switch both AP and devices' channel without interrupting existing TCP connection.

First Intuitive Solution

I have a router (TP-LINK TL-WDR3500) running OpenWrt. Wireless configurations, e.g., SSID, channel, tx power, are managed in Openwrt's UCI system. More specifically, all Wifi configurations are stored in file located in /etc/config/wireless. In my case, the file looks like this:

config wifi-device 'radio0'
    option type 'mac80211'
    option hwmode '11g'
    option path 'platform/ar934x_wmac'
    option htmode 'HT20'
    list ht_capab 'LDPC'
    list ht_capab 'SHORT-GI-20'
    list ht_capab 'SHORT-GI-40'
    list ht_capab 'TX-STBC'
    list ht_capab 'RX-STBC1'
    list ht_capab 'DSSS_CCK-40'
    option txpower '27'
    option channel '11'

config wifi-iface
    option device 'radio0'
    option network 'lan'
    option mode 'ap'
    option ssid 'PocketSniffer'
    option encryption 'psk2'
    option key 'XXXX'

OpenWrt provides a command called wifi, that can reload these configurations. So my first solution is to uci command to change the configuration and use wifi command to reload them.

def set_channel(channel) :
  args = ['uci', 'set']

  if channel <= 11 :
    args.append('wireless.radio0.channel=' + str(channel))
  else :
    args.append('wireless.radio1.channel=' + str(channel))

  subprocess.call(args)
  subprocess.call(['uci', 'commit'])
  subprocess.call(['wifi'])

This will work in the sense that it can change the AP's channel. But the problem is, the wifi command will actually shut down the interface completely and restart it. So any devices that connected to this AP will be de-associated.

What's the Problem?

From client's side of view, when the AP switches to another channel, here is what happend:

  • Receive de-authentication frame from AP (ops, this AP is gone)
  • Do active scan on every channel (probe-wait)
  • Figure out a best AP to associate
  • Send authentication and association request to newly selected AP

This is much like a typical handover process where a device switches between two geographically co-located APs. Just that in this case, the two APs are actually the same physical AP with different channel.

A. Mishra et al provides a thorough study on the handover process. In short, the process can take up to a few hundred milliseconds, and any on-going TCP connections will lost.

This is undesired because the channel switch cost (extra latency and breaking TCP connection) may neutralize the benefit of switching channel itself.

Ideally, after channel switch, any authentication info at AP side should remain, so that clients don't have to re-authenticate, and any established TCP connection should also be kept. These requirements make sense because, after all, channel is just medium to exchange data. Channel switch should NOT affect any up layer state.

The Final Solution

After a bit research, I found that IEEE 802.11 standard (section 10.9.8 in 2012 standard) actually already defined the mechanism to let AP announce the channel switch event and also let clients switch channel accordingly - all happened in MAC layer. This feature quite fits our needs.

And the good new is that this feature has already been implemented in most recent driver that adopting CFG80211 interface, and is exposed to user space tools, such as hostapd or wpa_supplicant.

The OpenWrt running on our router use hostapd as user space authenticator. And it provides a command line tool called hostapd_cli to interact with the hostapd daemon. There is a command in hostapd_cli called chan_swtich that does precisely what we wanted.

def set_channel(channel) :

  # do not use the wifi command to switch channel, but still maintain the
  # channel coheraence of the configuration file

  args = ['uci', 'set']

  if channel <= 11 :
    args.append('wireless.radio0.channel=' + str(channel))
  else :
    args.append('wireless.radio1.channel=' + str(channel))

  subprocess.call(args)
  subprocess.call(['uci', 'commit'])

  # this is the command that actually switches channel

  with open(os.devnull, 'wb') as f :
    cmd = 'chan_switch 1 ' + str(channel2freq(channel)) + '\n'
    p = subprocess.Popen('hostapd_cli', stdin=subprocess.PIPE, stdout=f, stderr=f)
    p.stdin.write(cmd)
    time.sleep(3)
    p.kill()

Here we still update the configuration file to maintain consistence between it and the hostapd daemon. But instead of using wifi command to reload the configuration, we use the chan_swtich command to change the channel.

chan_switch takes a minimum of two arguments. The first is a cs_count, meaning switch channel after how many beacon frames. The second is frequency. More usage info can be obtained by typing chan_switch without any arguments in hostapd_cli.