Initial support for bandsteering.
Upon seeing a STA probe on 5GHz, 2.4GHz associations will be blocked
for five seconds with WLAN_STATUS_ASSOC_REJECTED_TEMPORARILY, unless
this has happened in the last ten minutes (in which case we assume the
client may be re-probing for a good reason and let it connect on
2.4Ghz).
Files containing the timestamps until which associations should be
blocked live in a directory specified by -S.
A corresonding change to /bin/wifi, which will make sure this
directory exists, will be required (otherwise the writes will fail).
Change-Id: I877be3f82ef29221dcfa5a8c410a9ea65b1deef7
diff --git a/hostapd/Makefile b/hostapd/Makefile
index b243227..90645dc 100644
--- a/hostapd/Makefile
+++ b/hostapd/Makefile
@@ -909,6 +909,8 @@
OBJS += ../src/ap/fingerprint.o
endif
+OBJS += ../src/ap/steering.o
+
ALL=hostapd hostapd_cli
all: verify_config $(ALL)
diff --git a/hostapd/main.c b/hostapd/main.c
index 3b383a6..68366b9 100644
--- a/hostapd/main.c
+++ b/hostapd/main.c
@@ -37,6 +37,7 @@
static struct hapd_global global;
char *alivemon_path = NULL;
char *fingerprint_path = NULL;
+char *steering_timestamp_path = NULL;
#ifndef CONFIG_NO_HOSTAPD_LOGGER
@@ -459,6 +460,8 @@
" (records all messages regardless of debug verbosity)\n"
#endif /* CONFIG_DEBUG_LINUX_TRACING */
" -t include timestamps in some debug messages\n"
+ " -S enable 5GHz bandsteering, storing delay timestamps in the\n"
+ " specified directory\n"
" -v show hostapd version\n");
exit(1);
@@ -566,7 +569,7 @@
interfaces.global_ctrl_sock = -1;
for (;;) {
- c = getopt(argc, argv, "b:A:Bde:f:F:hKP:Ttu:vg:G:");
+ c = getopt(argc, argv, "b:A:Bde:f:F:hKP:Ttu:vg:G:S:");
if (c < 0)
break;
switch (c) {
@@ -633,6 +636,9 @@
case 'u':
return gen_uuid(optarg);
#endif /* CONFIG_WPS */
+ case 'S':
+ steering_timestamp_path = optarg;
+ break;
default:
usage();
break;
diff --git a/src/ap/beacon.c b/src/ap/beacon.c
index 1f2c7f5..c9d8362 100644
--- a/src/ap/beacon.c
+++ b/src/ap/beacon.c
@@ -28,6 +28,7 @@
#include "beacon.h"
#include "hs20.h"
#include "dfs.h"
+#include "ap/steering.h"
#ifdef NEED_AP_MLME
@@ -543,6 +544,43 @@
size_t i, resp_len;
int noack;
enum ssid_match_result res;
+ struct os_reltime now, prev_bandsteer_until, bandsteer_until;
+
+
+ /* If we are seeing a 5GHz (A) probe request, block 2.4GHz (G) for a short
+ * duration (e.g. a few seconds), unless we have done so recently (e.g. a few
+ * minutes). The idea is to steer clients towards the 5GHz interface, but if
+ * they are re-probing within a few minutes of our last steering attempt then
+ * they may be having some kind of connection problem or otherwise know
+ * something we don't, so at that point we do the conservative thing and let
+ * them connect on 2.4GHz if they want.
+ */
+ if (steering_timestamp_path) {
+ os_get_reltime(&now);
+ if (garbage_collect_timestamp_files() == -1) {
+ wpa_printf(MSG_ERROR,
+ "Garbage collecting steering timestamp files failed: %s",
+ strerror(errno));
+ }
+ /* TODO: Consider bandsteering more agressively (e.g. a longer delay or
+ * shorter expiration) for devices which have previously successfully
+ * associated on 5GHz.
+ */
+ else if (hapd->iconf->hw_mode == HOSTAPD_MODE_IEEE80211A) {
+ if (!read_timestamp_file(mgmt, hapd, &prev_bandsteer_until) ||
+ os_reltime_expired(&now, &prev_bandsteer_until,
+ BANDSTEERING_EXPIRATION_SECONDS)) {
+ bandsteer_until.sec = now.sec + BANDSTEERING_DELAY_SECONDS;
+ bandsteer_until.usec = now.usec;
+ if (!write_timestamp_file(mgmt, hapd, bandsteer_until)) {
+ wpa_printf(MSG_ERROR, "Failed to write bandsteering timestamp file.");
+ } else {
+ wpa_printf(MSG_INFO, "Set bandsteering delay for " MACSTR,
+ MAC2STR(mgmt->sa));
+ }
+ }
+ }
+ }
ie = mgmt->u.probe_req.variable;
if (len < IEEE80211_HDRLEN + sizeof(mgmt->u.probe_req))
diff --git a/src/ap/ieee802_11.c b/src/ap/ieee802_11.c
index 7fa3919..0e62016 100644
--- a/src/ap/ieee802_11.c
+++ b/src/ap/ieee802_11.c
@@ -41,6 +41,7 @@
#include "ieee802_11.h"
#include "dfs.h"
#include "rm.h"
+#include "ap/steering.h"
u8 * hostapd_eid_supp_rates(struct hostapd_data *hapd, u8 *eid)
@@ -1572,6 +1573,7 @@
const u8 *pos;
int left, i;
struct sta_info *sta;
+ struct os_reltime now, bandsteer_until;
if (len < IEEE80211_HDRLEN + (reassoc ? sizeof(mgmt->u.reassoc_req) :
sizeof(mgmt->u.assoc_req))) {
@@ -1600,6 +1602,19 @@
}
#endif /* CONFIG_TESTING_OPTIONS */
+ if (steering_timestamp_path) {
+ if (hapd->iconf->hw_mode == HOSTAPD_MODE_IEEE80211G) {
+ os_get_reltime(&now);
+ if (read_timestamp_file(mgmt, hapd, &bandsteer_until) &&
+ os_reltime_before(&now, &bandsteer_until)) {
+ wpa_printf(MSG_INFO, "Rejecting " MACSTR " until %d sec %d usec",
+ MAC2STR(mgmt->sa), bandsteer_until.sec, bandsteer_until.usec);
+ resp = WLAN_STATUS_ASSOC_REJECTED_TEMPORARILY;
+ goto fail;
+ }
+ }
+ }
+
fc = le_to_host16(mgmt->frame_control);
seq_ctrl = le_to_host16(mgmt->seq_ctrl);
diff --git a/src/ap/steering.c b/src/ap/steering.c
new file mode 100644
index 0000000..93ca4fd
--- /dev/null
+++ b/src/ap/steering.c
@@ -0,0 +1,176 @@
+/*
+ * hostapd / Interface steering
+ * Copyright (c) 2015 Google, Inc.
+ *
+ * This software may be distributed under the terms of the BSD license.
+ * See README for more details.
+ */
+
+#include "includes.h"
+
+#include <sys/stat.h>
+#include <unistd.h>
+#include <dirent.h>
+#include "common.h"
+#include "common/defs.h"
+#include "common/ieee802_11_defs.h"
+#include "hostapd.h"
+#include "steering.h"
+
+static int get_timestamp_filename(const struct ieee80211_mgmt *mgmt,
+ const struct hostapd_data *hapd, char *buf,
+ size_t len) {
+ if (steering_timestamp_path == NULL) {
+ return 0;
+ }
+
+ if (os_snprintf(buf, len, "%s/" COMPACT_MACSTR ".%d",
+ steering_timestamp_path, MAC2STR(mgmt->sa),
+ hapd->iconf->hw_mode) < 0) {
+ wpa_printf(MSG_ERROR, "os_snprintf couldn't format filename: %s",
+ strerror(errno));
+ return 0;
+ }
+
+ return 1;
+}
+
+int write_timestamp_file(const struct ieee80211_mgmt *mgmt,
+ const struct hostapd_data *hapd,
+ const struct os_reltime timestamp) {
+ FILE *f;
+ char filename[1024], tmp_filename[1024];
+ int success = 0;
+
+ if (!get_timestamp_filename(mgmt, hapd, filename, sizeof(filename))) {
+ return 0;
+ }
+
+ /* Create a temporary filename to prevent multiple interfaces on the same band
+ * from touching each others' writes.
+ */
+ if (os_snprintf(tmp_filename, sizeof(tmp_filename), "%s%s", filename,
+ os_strrchr(hapd->iface->config_fname, '.')) < 0) {
+ wpa_printf(MSG_ERROR, "os_snprintf couldn't format temp filename: %s",
+ strerror(errno));
+ }
+
+ if ((f = fopen(tmp_filename, "w")) == NULL) {
+ wpa_printf(MSG_ERROR, "fopen(%s) for write: %s", tmp_filename,
+ strerror(errno));
+ return 0;
+ }
+
+ if (fprintf(f, "%d %d", timestamp.sec, timestamp.usec) < 0) {
+ wpa_printf(MSG_ERROR, "fprintf to %s: %s", tmp_filename, strerror(errno));
+ } else {
+ success = 1;
+ }
+
+ if (fclose(f) == EOF) {
+ wpa_printf(MSG_ERROR, "fclose(%s): %s", tmp_filename, strerror(errno));
+ return 0;
+ }
+
+ if (rename(tmp_filename, filename) != 0) {
+ wpa_printf(MSG_ERROR, "rename(%s, %s): %s", tmp_filename, filename,
+ strerror(errno));
+ return 0;
+ }
+
+ return success;
+}
+
+int read_timestamp_file(const struct ieee80211_mgmt *mgmt,
+ const struct hostapd_data *hapd,
+ struct os_reltime *timestamp) {
+ FILE *f;
+ char filename[1024];
+ int success = 0;
+ struct stat st;
+
+ if (!get_timestamp_filename(mgmt, hapd, filename, sizeof(filename))) {
+ return 0;
+ }
+
+ if (stat(filename, &st) == -1) {
+ return 0;
+ }
+
+ f = fopen(filename, "r");
+ if (f == NULL) {
+ wpa_printf(MSG_ERROR, "open(%s) for read: %s", filename, strerror(errno));
+ return 0;
+ }
+
+ if (fscanf(f, "%d %d", ×tamp->sec, ×tamp->usec) < 0) {
+ wpa_printf(MSG_ERROR, "fscanf from %s: %s", filename, strerror(errno));
+ } else {
+ success = 1;
+ }
+
+ if (fclose(f) == EOF) {
+ wpa_printf(MSG_ERROR, "fclose(%s): %s", filename, strerror(errno));
+ return 0;
+ }
+
+ return success;
+}
+
+int file_ctime_lt(const struct dirent **a, const struct dirent **b) {
+ struct stat astat, bstat;
+
+ /* If we can't stat both of the files, give up and say they're equivalent. */
+ if (stat((*a)->d_name, &astat) == -1 || stat((*b)->d_name, &bstat) == -1) {
+ return 0;
+ }
+
+ return astat.st_ctime - bstat.st_ctime;
+}
+
+int garbage_collect_timestamp_files() {
+ int num_timestamp_files = 0, num_timestamp_files_deleted = 0, i = 0;
+ struct dirent **namelist;
+ char original_cwd[1024];
+ char *filename;
+ int error = 0;
+
+ if (getcwd(original_cwd, sizeof(original_cwd)) == NULL) {
+ wpa_printf(MSG_ERROR, "getcwd(): %s", strerror(errno));
+ return -1;
+ }
+
+ if (chdir(steering_timestamp_path) == -1) {
+ wpa_printf(MSG_ERROR, "chdir(%s): %s",
+ steering_timestamp_path, strerror(errno));
+ return -1;
+ }
+
+ num_timestamp_files = scandir(steering_timestamp_path, &namelist, NULL,
+ file_ctime_lt);
+ for (i = 0; i < num_timestamp_files; ++i) {
+ if (MAX_STEERING_TIMESTAMP_FILES <
+ /* The -2 is because scandir includes "." and "..". */
+ (num_timestamp_files - 2) - num_timestamp_files_deleted) {
+ filename = namelist[i]->d_name;
+ if (filename[0] != '.' && !error) {
+ if (unlink(filename) == -1) {
+ wpa_printf(MSG_ERROR, "unlink(%s): %s",
+ unlink(filename), strerror(errno));
+ error = 1;
+ } else {
+ ++num_timestamp_files_deleted;
+ }
+ }
+ }
+ os_free(namelist[i]);
+ }
+ os_free(namelist);
+
+ if (chdir(original_cwd) == -1) {
+ wpa_printf(MSG_ERROR, "chdir(%s): %s", original_cwd, strerror(errno));
+ return -1;
+ }
+
+ return error ? -1 : num_timestamp_files_deleted;
+}
diff --git a/src/ap/steering.h b/src/ap/steering.h
new file mode 100644
index 0000000..36b4e09
--- /dev/null
+++ b/src/ap/steering.h
@@ -0,0 +1,49 @@
+/*
+ * hostapd / Interface steering
+ * Copyright (c) 2015 Google, Inc.
+ *
+ * This software may be distributed under the terms of the BSD license.
+ * See README for more details.
+ */
+
+#ifndef STEERING_H
+#define STEERING_H
+
+#define MAX_STEERING_TIMESTAMP_FILES 100
+/* 10 seconds is long enough to scan all channels on both bands at least twice
+ * at 100ms/channel.
+ */
+#define BANDSTEERING_DELAY_SECONDS 10
+#define BANDSTEERING_EXPIRATION_SECONDS 120
+
+extern char *steering_timestamp_path;
+
+struct hostapd_data;
+struct ieee80211_mgmt;
+struct os_reltime;
+struct dirent;
+
+/**
+ * Writes timestamp for the interface specified by hw_mode and the source
+ * address in mgmt. Returns 1 iff the write succeeded.
+ */
+int write_timestamp_file(const struct ieee80211_mgmt *mgmt,
+ const struct hostapd_data *hapd,
+ const struct os_reltime timestamp);
+
+/**
+ * Reads a timestamp for the interface specified by hw_mode and the source
+ * address in mgmt, putting the result in timestamp. Returns 1 iff the read
+ * succeeded.
+ */
+int read_timestamp_file(const struct ieee80211_mgmt *mgmt,
+ const struct hostapd_data *hapd,
+ struct os_reltime *timestamp);
+
+/**
+ * Delete all but the most recent MAX_TIMESTAMP_FILES files in
+ * steering_timestamp_path. Returns the number of files deleted.
+ */
+int garbage_collect_timestamp_files();
+
+#endif /* STEERING_H */