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", &timestamp->sec, &timestamp->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 */