Playing tag with DFS

If you've ever worked in the fixed wireless space then dynamic frequency selection, or DFS, might sound familiar. DFS is a channel allocation scheme for the 5.8 GHz frequency band intended to prevent interference with other C-band usages such as aircraft radar, weather radar, and satellite communications.

By requiring device certification, DFS channels remain relatively noise-free when compared the the rest of the 5.8 GHz band. This can permit greater RF performance without needing access to FCC-licensed bands.

Caveats!

Devices certified to use DFS must passively listen to the requested channel to ensure no radar is present before transmitting. The fun begins when the device detects radar whilst operating on a DFS channel. When this happens, the device must immediately stop transmitting on the selected channel and move to a non-DFS center frequency. Performance will likely degrade as the noise present in the rest of the 5.8 GHz band returns. This is often called a "radar strike."

I have a Ubiquiti LTU Rocket access point at about 2000' AGL in Santa Barbara County and rely on DFS for adequate performance in this noisy environment. When a radar strike occurs and the radio moves to a non-DFS channel, it will not automatically revert to the previously selected DFS channel. Often the quickest way to get back to the DFS channel is to—believe it or not—reboot the radio.

A heavy-handed solution

dfs-bot is a brute-force, cron-friendly container to steer a wandering radio back to the trail.

I use Ubiquiti's UISP as a largely hands-free network management system which exposes a generous API. dfs-bot uses this API to both determine if the active center frequency has deviated from the target DFS channel, and to reboot the radio to coax it back into its happy place. The alpine-based container uses cron to run the following bash every four hours or so.

Core logic:

#!/usr/bin/env bash
set -e

# Check if necessary environment variables are set
if [[ -z $UISP_DOMAIN || -z $DEVICE_ID || -z $TARGET_FREQ ]]; then
echo "One or more variables are undefined."
echo "The following must be set:"
echo "- UISP_DOMAIN"
echo "- DEVICE_ID"
echo "- TARGET_FREQ"
exit 1
fi

# Check if UISP API token exists
if [[ -z $UISP_API_TOKEN && ! -f "/run/secrets/uisp_api_token" ]]; then
echo "The UISP API token cannot be found."
echo "The following must be set:"
echo "- UISP_API_TOKEN (will also check /run/secrets/uisp_api_token)"
exit 1
elif [[ -z $UISP_API_TOKEN ]]; then
export UISP_API_TOKEN=$(cat /run/secrets/uisp_api_token)
fi

# Define API routes
status_route="${UISP_DOMAIN}/nms/api/v2.1/devices/airmaxes/${DEVICE_ID}"
restart_route="${UISP_DOMAIN}/nms/api/v2.1/devices/${DEVICE_ID}/restart"

# Variable to store current device center frequency
device_freq=$(curl -s -X GET $status_route -H "accept: application/json" -H "x-auth-token: ${UISP_API_TOKEN}" | jq '.airmax.frequencyCenter')

# Let's see if any DFS events have occured
if [[ $device_freq != $TARGET_FREQ ]]; then
echo "Frequency off target [${device_freq}]. Rebooting access point..."
curl -s -X POST $restart_route -H "accept: application/json" -H "x-auth-token: ${UISP_API_TOKEN}"
else
echo "Frequency on target [${TARGET_FREQ}]. No reboot needed."
fi

Usage

Clone the repository:

git clone git@github.com:raylas/dfs-bot.git

Configure cron job in cronjobs:

0 */4 * * * /dfs_bot.sh

Build image:

docker build dfs-bot:latest .

Run container:

docker run \
-e UISP_DOMAIN=<uisp_domain> \
-e UISP_API_TOKEN=<api_token> \
-e DEVICE_ID=<device_id> \
-e TARGET_FREQ=<target_frequency> \
dfs-bot:latest

Docker Compose:

docker-compose up -d