Re: [Patch 3/3] Network steering feature

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



On Fri, Jul 29, 2016 at 08:48:20AM -0700, David Weidenkopf wrote:
> Introduction
> ----------------
> The network steering system steers client STAs to the infrastructure AP
> with the best RSSI. APs collaborate and can use several methods to direct
> client STAs to transition to the best AP.
> 
> Please review the attached txt for further details regarding this RFC.
> 
> This RFC builds upon the previous two patches, "BSS transition with candidate
> list" and "Client station blacklist support".

Could you please clarify where this protocol definitions for messages
exchanged between APs comes from? Should the RFC-Network-Steering.txt be
part of the commit to document the protocol and how it is to be
configured? Especially the use of r0kh list (a parameter specific to FT)
is quite unexpected.. I'm also not exactly happy with choosing an
ethertype "random from unassigned".. Is it clear that this protocol
would never need to be routed between APs that are not reachable within
the same physical network (e.g., any need for IP routing)?

I don't see any kind of authentication in the protocol. What protects
this from injection of packets from unauthorized devices? Couldn't all
associated stations in the network send these messages to trigger APs to
react for arbitrary MAC addresses, e.g., to cause denial of service or
improve the network conditions for themselves?

I started going through the details in all three patches and noticed
that the four new files added (patch 1/3 and 3/3) do not include any
copyright or license statements. Could you please provide an updated
version with such statements added (and hopefully with the same license
that is used throughout the repository)? I cannot apply any of the
patches without clear understanding on the licensing terms.

I'm attaching the snapshot version that I have in my review branch after
fixing couple of small issues and cleaning up some coding style issues
(far from complete on that front, though).

Please also note that there are number of allocation calls (at least
wpabuf_alloc()) that could fail and need to be checked against NULL
before dereferencing the pointer.

-- 
Jouni Malinen                                            PGP id EFC895FA
>From bddc0a23f7c6d72f944907c6eb5ca303d9c0b4af Mon Sep 17 00:00:00 2001
From: David Weidenkopf <dweidenkopf@xxxxxxxxxxxx>
Date: Thu, 7 Jul 2016 14:11:43 -0700
Subject: [PATCH 1/3] WNM: hostapd client station blacklist support.

New CLI commands to add, remove, clear, and show the blacklist.

Signed-off-by: Henry M. Bennett <hbennett@xxxxxxxxxxxx>
Signed-off-by: Alexis Green <agreen@xxxxxxxxxxxx>
Signed-off-by: David Weidenkopf <dweidenkopf@xxxxxxxxxxxx>
---
 hostapd/Makefile        |   1 +
 hostapd/ctrl_iface.c    |  18 ++++++
 hostapd/hostapd_cli.c   |  68 ++++++++++++++++++++++-
 src/ap/beacon.c         |   6 ++
 src/ap/ctrl_iface_ap.c  |  65 +++++++++++++++++++++-
 src/ap/ctrl_iface_ap.h  |   8 ++-
 src/ap/hostapd.c        |   1 +
 src/ap/hostapd.h        |   3 +
 src/ap/ieee802_11.c     |  11 ++++
 src/ap/sta_blacklist.c  | 145 ++++++++++++++++++++++++++++++++++++++++++++++++
 src/ap/sta_blacklist.h  |  18 ++++++
 wpa_supplicant/Makefile |   1 +
 12 files changed, 342 insertions(+), 3 deletions(-)
 create mode 100644 src/ap/sta_blacklist.c
 create mode 100644 src/ap/sta_blacklist.h

diff --git a/hostapd/Makefile b/hostapd/Makefile
index ba094ba..6231d4a 100644
--- a/hostapd/Makefile
+++ b/hostapd/Makefile
@@ -86,6 +86,7 @@ OBJS += ../src/ap/beacon.o
 OBJS += ../src/ap/bss_load.o
 OBJS += ../src/ap/neighbor_db.o
 OBJS += ../src/ap/rrm.o
+OBJS += ../src/ap/sta_blacklist.o
 
 OBJS_c = hostapd_cli.o
 OBJS_c += ../src/common/wpa_ctrl.o
diff --git a/hostapd/ctrl_iface.c b/hostapd/ctrl_iface.c
index 5f3d6bd..7889283 100644
--- a/hostapd/ctrl_iface.c
+++ b/hostapd/ctrl_iface.c
@@ -2519,6 +2519,24 @@ static int hostapd_ctrl_iface_receive_process(struct hostapd_data *hapd,
 	} else if (os_strncmp(buf, "LOG_LEVEL", 9) == 0) {
 		reply_len = hostapd_ctrl_iface_log_level(
 			hapd, buf + 9, reply, reply_size);
+	/* BEGIN HOSTAPD BLACKLIST SUPPORT */
+	} else if (os_strncmp(buf, "BLACKLIST_ADD ", 14) == 0) {
+			if (hostapd_ctrl_iface_blacklist_add(hapd, buf + 14))
+				reply_len = -1;
+	} else if (os_strncmp(buf, "BLACKLIST_RM ", 13) == 0) {
+			if (hostapd_ctrl_iface_blacklist_rm(hapd, buf + 13))
+				reply_len = -1;
+	} else if (os_strcmp(buf, "BLACKLIST_SHOW") == 0) {
+		reply_len = hostapd_ctrl_iface_blacklist_show(hapd, reply,
+						reply_size);
+		if(reply_len == 0) {
+			os_memcpy(reply, "EMPTY\n", 6);
+			reply_len = 6;
+		}
+	} else if (os_strcmp(buf, "BLACKLIST_CLR") == 0) {
+		if(hostapd_ctrl_iface_blacklist_clr(hapd))
+			reply_len = -1;
+	/* END HOSTAPD BLACKLIST SUPPORT */
 #ifdef NEED_AP_MLME
 	} else if (os_strcmp(buf, "TRACK_STA_LIST") == 0) {
 		reply_len = hostapd_ctrl_iface_track_sta_list(
diff --git a/hostapd/hostapd_cli.c b/hostapd/hostapd_cli.c
index 04819d1..8b9d176 100644
--- a/hostapd/hostapd_cli.c
+++ b/hostapd/hostapd_cli.c
@@ -300,6 +300,65 @@ static int hostapd_cli_cmd_new_sta(struct wpa_ctrl *ctrl, int argc,
 }
 
 
+/* BEGIN HOSTAPD BLACKLIST SUPPORT */
+static int hostapd_cli_cmd_blacklist_add_hostapd(struct wpa_ctrl *ctrl, int argc,
+					char *argv[])
+{
+	char buf[64];
+
+	if (argc != 1) {
+		printf("Invalid 'blacklist_add_hostapd' command - exactly one argument, STA address, is required.\n");
+		return -1;
+	}
+
+	os_snprintf(buf, sizeof(buf), "BLACKLIST_ADD %s", argv[0]);
+	return wpa_ctrl_command(ctrl, buf);
+}
+
+static int hostapd_cli_cmd_blacklist_rm_hostapd(struct wpa_ctrl *ctrl, int argc,
+					char *argv[])
+{
+	char buf[64];
+
+	if (argc != 1) {
+		printf("Invalid 'blacklist_rm_hostapd' command - exactly one argument, STA address, is required.\n");
+		return -1;
+	}
+
+	os_snprintf(buf, sizeof(buf), "BLACKLIST_RM %s", argv[0]);
+	return wpa_ctrl_command(ctrl, buf);
+}
+
+static int hostapd_cli_cmd_blacklist_show(struct wpa_ctrl *ctrl, int argc,
+					char *argv[])
+{
+	char buf[64];
+
+	if (argc > 0) {
+		printf("Invalid 'blacklist_show' command - this command takes no arguments.\n");
+		return -1;
+	}
+
+	os_snprintf(buf, sizeof(buf), "BLACKLIST_SHOW");
+	return wpa_ctrl_command(ctrl, buf);
+}
+
+static int hostapd_cli_cmd_blacklist_clr_hostapd(struct wpa_ctrl *ctrl, int argc,
+					char *argv[])
+{
+	char buf[64];
+
+	if (argc != 0) {
+		printf("Invalid 'blacklist_add_hostapd' command - exactly zero arguments is required.\n");
+		return -1;
+	}
+
+	os_snprintf(buf, sizeof(buf), "BLACKLIST_CLR");
+	return wpa_ctrl_command(ctrl, buf);
+}
+
+/* END HOSTAPD BLACKLIST SUPPORT */
+
 static int hostapd_cli_cmd_deauthenticate(struct wpa_ctrl *ctrl, int argc,
 					  char *argv[])
 {
@@ -1122,7 +1181,6 @@ static int hostapd_cli_cmd_erp_flush(struct wpa_ctrl *ctrl, int argc,
 	return wpa_ctrl_command(ctrl, "ERP_FLUSH");
 }
 
-
 static int hostapd_cli_cmd_log_level(struct wpa_ctrl *ctrl, int argc,
 				     char *argv[])
 {
@@ -1271,6 +1329,14 @@ static const struct hostapd_cli_cmd hostapd_cli_commands[] = {
 	{ "disassociate", hostapd_cli_cmd_disassociate,
 	  hostapd_complete_disassociate,
 	  "<addr> = disassociate a station" },
+	{ "blacklist_add", hostapd_cli_cmd_blacklist_add_hostapd, NULL,
+	  "<addr> = blacklist a station from an AP" },
+	{ "blacklist_rm", hostapd_cli_cmd_blacklist_rm_hostapd, NULL,
+	  "<addr> = remove a station from the blacklist" },
+	{ "blacklist_show", hostapd_cli_cmd_blacklist_show, NULL,
+	  "show entire blacklist" },
+	{ "blacklist_clr", hostapd_cli_cmd_blacklist_clr_hostapd, NULL,
+	  "clear all stations from the blacklist" },
 #ifdef CONFIG_IEEE80211W
 	{ "sa_query", hostapd_cli_cmd_sa_query, NULL,
 	  "<addr> = send SA Query to a station" },
diff --git a/src/ap/beacon.c b/src/ap/beacon.c
index 0a006f9..8d3d185 100644
--- a/src/ap/beacon.c
+++ b/src/ap/beacon.c
@@ -29,6 +29,7 @@
 #include "beacon.h"
 #include "hs20.h"
 #include "dfs.h"
+#include "sta_blacklist.h"
 
 
 #ifdef NEED_AP_MLME
@@ -840,6 +841,11 @@ void handle_probe_req(struct hostapd_data *hapd,
 	}
 #endif /* CONFIG_P2P */
 
+	if(sta_blacklist_present(hapd, mgmt->sa)) {
+		wpa_printf(MSG_DEBUG, "ignoring probe request from " MACSTR " due to blacklist", MAC2STR(mgmt->sa));
+		return;
+	}
+
 	/* TODO: verify that supp_rates contains at least one matching rate
 	 * with AP configuration */
 
diff --git a/src/ap/ctrl_iface_ap.c b/src/ap/ctrl_iface_ap.c
index 23c8c60..23e7d27 100644
--- a/src/ap/ctrl_iface_ap.c
+++ b/src/ap/ctrl_iface_ap.c
@@ -23,7 +23,7 @@
 #include "ctrl_iface_ap.h"
 #include "ap_drv_ops.h"
 #include "mbo_ap.h"
-
+#include "sta_blacklist.h"
 
 static int hostapd_get_sta_tx_rx(struct hostapd_data *hapd,
 				 struct sta_info *sta,
@@ -558,6 +558,69 @@ int hostapd_ctrl_iface_status(struct hostapd_data *hapd, char *buf,
 	return len;
 }
 
+int hostapd_ctrl_iface_blacklist_add(struct hostapd_data *hapd,
+					const char *txtaddr)
+{
+	u8 addr[ETH_ALEN];
+	int ret = -1;
+
+	wpa_dbg(hapd->msg_ctx, MSG_DEBUG, "CTRL_IFACE BLACKLIST_ADD %s",
+		txtaddr);
+
+	if (hwaddr_aton(txtaddr, addr))
+		return -1;
+
+	ret = sta_blacklist_add(hapd, addr);
+
+	return ret;
+}
+
+int hostapd_ctrl_iface_blacklist_rm(struct hostapd_data *hapd,
+					const char *txtaddr)
+{
+	u8 addr[ETH_ALEN];
+	int ret = -1;
+
+	wpa_dbg(hapd->msg_ctx, MSG_DEBUG, "CTRL_IFACE BLACKLIST_RM %s",
+		txtaddr);
+
+	if (hwaddr_aton(txtaddr, addr))
+		return -1;
+
+	ret = sta_blacklist_rm(hapd, addr);
+
+	return ret;
+}
+
+int hostapd_ctrl_iface_blacklist_show(struct hostapd_data *hapd, char *buf,
+		size_t buflen)
+{
+	int len = 0, ret = 0;
+	struct sta_blacklist *e = hapd->blacklist;
+
+	while (e) {
+		ret = os_snprintf(buf + len, buflen - len,
+				  MACSTR "\n",
+				  MAC2STR(e->sta));
+		if (ret < 0 || (size_t) ret >= buflen - len)
+			return len;
+		len += ret;
+		e = e->next;
+	}
+
+	return ret;
+}
+
+int hostapd_ctrl_iface_blacklist_clr(struct hostapd_data *hapd)
+{
+	int ret = -1;
+
+	wpa_dbg(hapd->msg_ctx, MSG_DEBUG, "CTRL_IFACE BLACKLIST_CLR");
+
+	ret = sta_blacklist_clear(hapd);
+
+	return ret;
+}
 
 int hostapd_parse_csa_settings(const char *pos,
 			       struct csa_settings *settings)
diff --git a/src/ap/ctrl_iface_ap.h b/src/ap/ctrl_iface_ap.h
index 6095d7d..346c6f3 100644
--- a/src/ap/ctrl_iface_ap.h
+++ b/src/ap/ctrl_iface_ap.h
@@ -29,5 +29,11 @@ int hostapd_ctrl_iface_stop_ap(struct hostapd_data *hapd);
 int hostapd_ctrl_iface_pmksa_list(struct hostapd_data *hapd, char *buf,
 				  size_t len);
 void hostapd_ctrl_iface_pmksa_flush(struct hostapd_data *hapd);
-
+int hostapd_ctrl_iface_blacklist_add(struct hostapd_data *hapd,
+					const char *txtaddr);
+int hostapd_ctrl_iface_blacklist_rm(struct hostapd_data *hapd,
+					const char *txtaddr);
+int hostapd_ctrl_iface_blacklist_show(struct hostapd_data *hapd, char *buf,
+		size_t buflen);
+int hostapd_ctrl_iface_blacklist_clr(struct hostapd_data *hapd);
 #endif /* CTRL_IFACE_AP_H */
diff --git a/src/ap/hostapd.c b/src/ap/hostapd.c
index a09d423..a4c0d27 100644
--- a/src/ap/hostapd.c
+++ b/src/ap/hostapd.c
@@ -1840,6 +1840,7 @@ dfs_offload:
 	for (j = 0; j < iface->num_bss; j++)
 		hostapd_set_own_neighbor_report(iface->bss[j]);
 
+	hapd->blacklist = NULL;
 	return 0;
 
 fail:
diff --git a/src/ap/hostapd.h b/src/ap/hostapd.h
index 195679e..a77c97f 100644
--- a/src/ap/hostapd.h
+++ b/src/ap/hostapd.h
@@ -19,6 +19,7 @@ struct radius_server_data;
 struct upnp_wps_device_sm;
 struct hostapd_data;
 struct sta_info;
+struct sta_blacklist;
 struct ieee80211_ht_capabilities;
 struct full_dynamic_vlan;
 enum wps_event;
@@ -304,6 +305,8 @@ struct hostapd_data {
 	u8 range_req_token;
 	unsigned int lci_req_active:1;
 	unsigned int range_req_active:1;
+
+	struct sta_blacklist *blacklist; /* Client station blacklist support */
 };
 
 
diff --git a/src/ap/ieee802_11.c b/src/ap/ieee802_11.c
index 2ecd78f..6c9b455 100644
--- a/src/ap/ieee802_11.c
+++ b/src/ap/ieee802_11.c
@@ -28,6 +28,7 @@
 #include "beacon.h"
 #include "ieee802_11_auth.h"
 #include "sta_info.h"
+#include "sta_blacklist.h"
 #include "ieee802_1x.h"
 #include "wpa_auth.h"
 #include "pmksa_cache_auth.h"
@@ -1045,6 +1046,16 @@ static void handle_auth(struct hostapd_data *hapd,
 	}
 #endif /* CONFIG_NO_RC4 */
 
+	/* Check for existence in blacklist */
+	if (sta_blacklist_present(hapd, mgmt->sa)) {
+		hostapd_logger(hapd, mgmt->sa, HOSTAPD_MODULE_IEEE80211,
+			       HOSTAPD_LEVEL_INFO,
+			       "authentication rejected due to blacklist");
+
+		resp = WLAN_STATUS_AP_UNABLE_TO_HANDLE_NEW_STA;
+		goto fail;
+	}
+
 	if (hapd->tkip_countermeasures) {
 		resp = WLAN_REASON_MICHAEL_MIC_FAILURE;
 		goto fail;
diff --git a/src/ap/sta_blacklist.c b/src/ap/sta_blacklist.c
new file mode 100644
index 0000000..4ce1552
--- /dev/null
+++ b/src/ap/sta_blacklist.c
@@ -0,0 +1,145 @@
+/* FIX: copyright and license statements */
+
+#include "utils/includes.h"
+
+#include "utils/common.h"
+#include "common/ieee802_11_defs.h"
+#include "eapol_auth/eapol_auth_sm.h"
+#include "hostapd.h"
+#include "ieee802_1x.h"
+#include "wpa_auth.h"
+#include "ieee802_11.h"
+#include "sta_info.h"
+#include "sta_blacklist.h"
+
+/**
+ * sta_blacklist_get - Get the blacklist entry for a BSSID
+ * @wpa_s: Pointer to wpa_supplicant data
+ * @bssid: BSSID
+ * Returns: Matching blacklist entry for the BSSID or %NULL if not found
+ */
+struct sta_blacklist * sta_blacklist_get(struct hostapd_data *hapd,
+					 const u8 *sta) {
+	struct sta_blacklist *e;
+
+	if (!hapd || !sta)
+		return NULL;
+
+	e = hapd->blacklist;
+	while (e) {
+		if (os_memcmp(e->sta, sta, ETH_ALEN) == 0)
+			return e;
+		e = e->next;
+	}
+
+	return NULL;
+}
+
+
+int sta_blacklist_present(struct hostapd_data *hapd, const u8 *sta)
+{
+	return sta_blacklist_get(hapd, sta) != NULL;
+}
+
+
+/**
+ * wpa_blacklist_add - Add an BSSID to the blacklist
+ * @wpa_s: Pointer to wpa_supplicant data
+ * @bssid: BSSID to be added to the blacklist
+ * Returns: Current blacklist count on success, -1 on failure
+ *
+ * This function adds the specified BSSID to the blacklist or increases the
+ * blacklist count if the BSSID was already listed. It should be called when
+ * an association attempt fails either due to the selected BSS rejecting
+ * association or due to timeout.
+ *
+ * This blacklist is used to force %wpa_supplicant to go through all available
+ * BSSes before retrying to associate with an BSS that rejected or timed out
+ * association. It does not prevent the listed BSS from being used; it only
+ * changes the order in which they are tried.
+ */
+int sta_blacklist_add(struct hostapd_data *hapd, const u8 *sta)
+{
+	struct sta_blacklist *e;
+
+	if (!hapd || !sta)
+		return -1;
+
+	e = sta_blacklist_get(hapd, sta);
+	if (e)
+		return 0;
+
+	e = os_zalloc(sizeof(*e));
+	if (!e)
+		return -1;
+	os_memcpy(e->sta, sta, ETH_ALEN);
+
+	e->next = hapd->blacklist;
+	hapd->blacklist = e;
+
+	hostapd_logger(hapd, NULL, HOSTAPD_MODULE_IEEE80211,
+		       HOSTAPD_LEVEL_INFO,
+		       "Added BSSID " MACSTR " into blacklist", MAC2STR(sta));
+
+	return 0;
+}
+
+
+/**
+ * wpa_blacklist_del - Remove an BSSID from the blacklist
+ * @wpa_s: Pointer to wpa_supplicant data
+ * @bssid: BSSID to be removed from the blacklist
+ * Returns: 0 on success, -1 on failure
+ */
+int sta_blacklist_rm(struct hostapd_data *hapd, const u8 *sta)
+{
+	struct sta_blacklist *e, *prev = NULL;
+
+	if (!hapd || !sta)
+		return -1;
+
+	e = hapd->blacklist;
+	while (e) {
+		if (os_memcmp(e->sta, sta, ETH_ALEN) == 0) {
+			if (!prev)
+				hapd->blacklist = e->next;
+			else
+				prev->next = e->next;
+
+			hostapd_logger(hapd, NULL, HOSTAPD_MODULE_IEEE80211,
+				       HOSTAPD_LEVEL_INFO,
+				       "Removed BSSID " MACSTR
+				       " from blacklist", MAC2STR(sta));
+
+			os_free(e);
+			return 0;
+		}
+		prev = e;
+		e = e->next;
+	}
+	return -1;
+}
+
+
+/**
+ * wpa_blacklist_clear - Clear the blacklist of all entries
+ * @wpa_s: Pointer to wpa_supplicant data
+ */
+int sta_blacklist_clear(struct hostapd_data *hapd)
+{
+	struct sta_blacklist *e, *prev;
+
+	e = hapd->blacklist;
+	hapd->blacklist = NULL;
+	while (e) {
+		prev = e;
+		e = e->next;
+		hostapd_logger(hapd, NULL, HOSTAPD_MODULE_IEEE80211,
+			       HOSTAPD_LEVEL_INFO,
+			       "Removed BSSID " MACSTR " from blacklist",
+			       MAC2STR(prev->sta));
+		os_free(prev);
+	}
+
+	return 0;
+}
diff --git a/src/ap/sta_blacklist.h b/src/ap/sta_blacklist.h
new file mode 100644
index 0000000..6846c41
--- /dev/null
+++ b/src/ap/sta_blacklist.h
@@ -0,0 +1,18 @@
+/* FIX: copyright and license statements */
+
+#ifndef STA_BLACKLIST_H
+#define STA_BLACKLIST_H
+
+struct sta_blacklist {
+	struct sta_blacklist *next;
+	u8 sta[ETH_ALEN];
+};
+
+struct sta_blacklist * sta_blacklist_get(struct hostapd_data *hapd,
+					 const u8 *sta);
+int sta_blacklist_present(struct hostapd_data *hapd, const u8 *sta);
+int sta_blacklist_add(struct hostapd_data *hapd, const u8 *sta);
+int sta_blacklist_rm(struct hostapd_data *hapd, const u8 *sta);
+int sta_blacklist_clear(struct hostapd_data *hapd);
+
+#endif /* STA_BLACKLIST_H */
diff --git a/wpa_supplicant/Makefile b/wpa_supplicant/Makefile
index f3e86c1..ee6e39b 100644
--- a/wpa_supplicant/Makefile
+++ b/wpa_supplicant/Makefile
@@ -847,6 +847,7 @@ OBJS += ../src/ap/bss_load.o
 OBJS += ../src/ap/eap_user_db.o
 OBJS += ../src/ap/neighbor_db.o
 OBJS += ../src/ap/rrm.o
+OBJS += ../src/ap/sta_blacklist.o
 ifdef CONFIG_IEEE80211N
 OBJS += ../src/ap/ieee802_11_ht.o
 ifdef CONFIG_IEEE80211AC
-- 
1.9.1

>From 4ad255323b3e0bc42a43f341c592b62d3afc98ec Mon Sep 17 00:00:00 2001
From: David Weidenkopf <dweidenkopf@xxxxxxxxxxxx>
Date: Thu, 7 Jul 2016 17:33:19 -0700
Subject: [PATCH 2/3] WNM: BSS transition (802.11v) support

New CLI command to use the BSS transition function.

Signed-off-by: Henry M. Bennett <hbennett@xxxxxxxxxxxx>
Signed-off-by: Alexis Green <agreen@xxxxxxxxxxxx>
Signed-off-by: David Weidenkopf <dweidenkopf@xxxxxxxxxxxx>
---
 hostapd/ctrl_iface.c         |  63 ++++++++++++++++++
 hostapd/hostapd_cli.c        |  20 ++++++
 src/ap/ieee802_11.c          |  28 +++++++-
 src/ap/sta_info.h            |   1 +
 src/ap/wnm_ap.c              | 152 ++++++++++++++++++++++++++++++++++++++++---
 src/ap/wnm_ap.h              |   4 ++
 src/common/ieee802_11_defs.h |  50 ++++++++++++++
 7 files changed, 306 insertions(+), 12 deletions(-)

diff --git a/hostapd/ctrl_iface.c b/hostapd/ctrl_iface.c
index 7889283..d29b240 100644
--- a/hostapd/ctrl_iface.c
+++ b/hostapd/ctrl_iface.c
@@ -1070,6 +1070,66 @@ static int hostapd_ctrl_iface_bss_tm_req(struct hostapd_data *hapd,
 	return ret;
 }
 
+static int hostapd_ctrl_iface_bss_transition(struct hostapd_data *hapd,
+					   const char *cmd)
+{
+	u8 addr[ETH_ALEN];
+	u8 ap_addr[ETH_ALEN];
+	const char *timerstr, *apstr, *channelstr;
+	int disassoc_timer;
+	u8 dest_ap_channel;
+	struct sta_info *sta;
+
+	if (hwaddr_aton(cmd, addr))
+		return -1;
+
+	sta = ap_get_sta(hapd, addr);
+	if (sta == NULL) {
+		hostapd_logger(hapd, NULL, HOSTAPD_MODULE_IEEE80211,
+			HOSTAPD_LEVEL_WARNING, " not found for BSS transition message");
+
+		//wpa_printf(MSG_DEBUG, "Station " MACSTR
+		//	   " not found for BSS transition message",
+		//	   MAC2STR(addr));
+		return -1;
+	}
+
+	timerstr = cmd + 17;
+	if (*timerstr != ' ') {
+		hostapd_logger(hapd, NULL, HOSTAPD_MODULE_IEEE80211,
+			HOSTAPD_LEVEL_WARNING, "time not found");
+		return -1;
+	}
+	timerstr++; // skip the space
+	disassoc_timer = atoi(timerstr);
+	if (disassoc_timer < 0 || disassoc_timer > 65535) {
+		hostapd_logger(hapd, NULL, HOSTAPD_MODULE_IEEE80211,
+			HOSTAPD_LEVEL_WARNING, "time out of range");
+		return -1;
+	}
+
+	apstr = os_strchr(timerstr, ' ');
+	if (apstr == NULL) {
+		hostapd_logger(hapd, NULL, HOSTAPD_MODULE_IEEE80211,
+			HOSTAPD_LEVEL_WARNING, "dest AP not found");
+		return -1;
+	}
+	apstr++; // skip the space
+	if (hwaddr_aton(apstr, ap_addr)) {
+		hostapd_logger(hapd, NULL, HOSTAPD_MODULE_IEEE80211,
+			HOSTAPD_LEVEL_WARNING, "dest AP couldn't be decoded");
+		return -1;
+	}
+
+	channelstr = os_strchr(apstr, ' ');
+	channelstr++; // skip the space
+	sscanf(channelstr, "%hhu", &dest_ap_channel); // C99 way of reading in an unsigned char
+	//dest_ap_channel = atoi(channelstr);
+
+	return wnm_send_bss_tm_req2(hapd, sta, disassoc_timer, ap_addr, dest_ap_channel);
+}
+
+
 #endif /* CONFIG_WNM */
 
 
@@ -2446,6 +2506,9 @@ static int hostapd_ctrl_iface_receive_process(struct hostapd_data *hapd,
 	} else if (os_strncmp(buf, "BSS_TM_REQ ", 11) == 0) {
 		if (hostapd_ctrl_iface_bss_tm_req(hapd, buf + 11))
 			reply_len = -1;
+	} else if (os_strncmp(buf, "BSS_TRANSITION ", 15) == 0) {
+		if (hostapd_ctrl_iface_bss_transition(hapd, buf + 15))
+			reply_len = -1;
 #endif /* CONFIG_WNM */
 	} else if (os_strcmp(buf, "GET_CONFIG") == 0) {
 		reply_len = hostapd_ctrl_iface_get_config(hapd, reply,
diff --git a/hostapd/hostapd_cli.c b/hostapd/hostapd_cli.c
index 8b9d176..523426a 100644
--- a/hostapd/hostapd_cli.c
+++ b/hostapd/hostapd_cli.c
@@ -709,6 +709,24 @@ static int hostapd_cli_cmd_ess_disassoc(struct wpa_ctrl *ctrl, int argc,
 	return wpa_ctrl_command(ctrl, buf);
 }
 
+static int hostapd_cli_cmd_bss_transition(struct wpa_ctrl *ctrl, int argc,
+					char *argv[])
+{
+	char buf[300];
+	int res;
+
+	if (argc < 4) {
+		printf("Invalid 'bss_transition' command - four arguments (STA "
+		       "addr, disassoc beacon timer, AP addr, AP channel) are needed\n");
+		return -1;
+	}
+
+	res = os_snprintf(buf, sizeof(buf), "BSS_TRANSITION %s %s %s %s",
+			  argv[0], argv[1], argv[2], argv[3]);
+	if (res < 0 || res >= (int) sizeof(buf))
+		return -1;
+	return wpa_ctrl_command(ctrl, buf);
+}
 
 static int hostapd_cli_cmd_bss_tm_req(struct wpa_ctrl *ctrl, int argc,
 				      char *argv[])
@@ -1367,6 +1385,8 @@ static const struct hostapd_cli_cmd hostapd_cli_commands[] = {
 	{ "wps_get_status", hostapd_cli_cmd_wps_get_status, NULL,
 	  "= show current WPS status" },
 #endif /* CONFIG_WPS */
+	{ "bss_transition", hostapd_cli_cmd_bss_transition, NULL,
+	  "<addr> <beacon timer count> <AP addr> <AP channel> = disassociate a WNM station telling the station which AP to connect to" },
 	{ "disassoc_imminent", hostapd_cli_cmd_disassoc_imminent, NULL, NULL },
 	{ "ess_disassoc", hostapd_cli_cmd_ess_disassoc, NULL, NULL },
 	{ "bss_tm_req", hostapd_cli_cmd_bss_tm_req, NULL, NULL },
diff --git a/src/ap/ieee802_11.c b/src/ap/ieee802_11.c
index 6c9b455..926d632 100644
--- a/src/ap/ieee802_11.c
+++ b/src/ap/ieee802_11.c
@@ -1505,6 +1505,19 @@ static u16 check_ext_capab(struct hostapd_data *hapd, struct sta_info *sta,
 			sta->qos_map_enabled = 1;
 	}
 #endif /* CONFIG_INTERWORKING */
+#ifdef CONFIG_WNM
+	// BIT#19 BSS Transition
+	// The STA sets the BSS Transition field to 1 when
+	// dot11MgmtOptionBSSTransitionActivated is true, and sets it to 0 otherwise. See
+	// 10.23.6.
+	if (ext_capab_ie_len >= 3) {
+		if (ext_capab_ie[2] & 0x8) {
+			sta->dot11MgmtOptionBSSTransitionActivated = 1;
+			hostapd_logger(hapd, sta->addr, HOSTAPD_MODULE_IEEE80211,
+				       HOSTAPD_LEVEL_INFO, "WNM: dot11MgmtOptionBSSTransitionActivated is true\n");
+		}
+	}
+#endif
 
 	if (ext_capab_ie_len > 0)
 		sta->ecsa_supported = !!(ext_capab_ie[0] & BIT(2));
@@ -3039,8 +3052,19 @@ int ieee802_11_get_mib(struct hostapd_data *hapd, char *buf, size_t buflen)
 int ieee802_11_get_mib_sta(struct hostapd_data *hapd, struct sta_info *sta,
 			   char *buf, size_t buflen)
 {
-	/* TODO */
-	return 0;
+	int len = 0, ret = 0;
+
+	if(sta->dot11MgmtOptionBSSTransitionActivated)
+		ret = os_snprintf(
+			buf + len, buflen - len,
+			"dot11MgmtOptionBSSTransitionActivated=%u\n",
+			sta->dot11MgmtOptionBSSTransitionActivated);
+
+	if (ret < 0 || (size_t) ret >= buflen - len)
+		return len;
+	len += ret;
+
+	return len;
 }
 
 
diff --git a/src/ap/sta_info.h b/src/ap/sta_info.h
index cf3fbb1..3de5a40 100644
--- a/src/ap/sta_info.h
+++ b/src/ap/sta_info.h
@@ -99,6 +99,7 @@ struct sta_info {
 	u8 sae_auth_retry;
 #endif /* CONFIG_MESH */
 
+	unsigned int dot11MgmtOptionBSSTransitionActivated:1;
 	unsigned int nonerp_set:1;
 	unsigned int no_short_slot_time_set:1;
 	unsigned int no_short_preamble_set:1;
diff --git a/src/ap/wnm_ap.c b/src/ap/wnm_ap.c
index 41d50ce..0a57af9 100644
--- a/src/ap/wnm_ap.c
+++ b/src/ap/wnm_ap.c
@@ -1,6 +1,7 @@
 /*
  * hostapd - WNM
  * Copyright (c) 2011-2014, Qualcomm Atheros, Inc.
+ * Copyright (c) 2015, CoCo Communications, Inc.
  *
  * This software may be distributed under the terms of the BSD license.
  * See README for more details.
@@ -17,9 +18,15 @@
 #include "ap/ap_config.h"
 #include "ap/ap_drv_ops.h"
 #include "ap/wpa_auth.h"
+#include "ap/wpa_auth_i.h"
+#include "radius/radius.h"
+#include "ap/accounting.h"
+#include "ap/ap_mlme.h"
+#include "ap/ieee802_1x.h"
 #include "mbo_ap.h"
 #include "wnm_ap.h"
 
+#define WNM_DEAUTH_DELAY_USEC 10
 #define MAX_TFS_IE_LEN  1024
 
 
@@ -319,9 +326,11 @@ static void ieee802_11_rx_bss_trans_mgmt_query(struct hostapd_data *hapd,
 	dialog_token = *pos++;
 	reason = *pos++;
 
-	wpa_printf(MSG_DEBUG, "WNM: BSS Transition Management Query from "
-		   MACSTR " dialog_token=%u reason=%u",
-		   MAC2STR(addr), dialog_token, reason);
+	hostapd_logger(hapd, NULL, HOSTAPD_MODULE_IEEE80211,
+		       HOSTAPD_LEVEL_INFO,
+		       "WNM: BSS Transition Management Query from " MACSTR
+		       " dialog_token=%u reason=%u",
+		       MAC2STR(addr), dialog_token, reason);
 
 	wpa_hexdump(MSG_DEBUG, "WNM: BSS Transition Candidate List Entries",
 		    pos, end - pos);
@@ -336,6 +345,7 @@ static void ieee802_11_rx_bss_trans_mgmt_resp(struct hostapd_data *hapd,
 {
 	u8 dialog_token, status_code, bss_termination_delay;
 	const u8 *pos, *end;
+	struct sta_info *sta;
 
 	if (len < 3) {
 		wpa_printf(MSG_DEBUG, "WNM: Ignore too short BSS Transition Management Response from "
@@ -349,7 +359,8 @@ static void ieee802_11_rx_bss_trans_mgmt_resp(struct hostapd_data *hapd,
 	status_code = *pos++;
 	bss_termination_delay = *pos++;
 
-	wpa_printf(MSG_DEBUG, "WNM: BSS Transition Management Response from "
+	hostapd_logger(hapd, NULL, HOSTAPD_MODULE_IEEE80211,
+			   HOSTAPD_LEVEL_INFO, "WNM: BSS Transition Management Response from "
 		   MACSTR " dialog_token=%u status_code=%u "
 		   "bss_termination_delay=%u", MAC2STR(addr), dialog_token,
 		   status_code, bss_termination_delay);
@@ -359,14 +370,38 @@ static void ieee802_11_rx_bss_trans_mgmt_resp(struct hostapd_data *hapd,
 			wpa_printf(MSG_DEBUG, "WNM: not enough room for Target BSSID field");
 			return;
 		}
-		wpa_printf(MSG_DEBUG, "WNM: Target BSSID: " MACSTR,
-			   MAC2STR(pos));
-		wpa_msg(hapd->msg_ctx, MSG_INFO, BSS_TM_RESP MACSTR
-			" status_code=%u bss_termination_delay=%u target_bssid="
-			MACSTR,
-			MAC2STR(addr), status_code, bss_termination_delay,
+		hostapd_logger(hapd, NULL, HOSTAPD_MODULE_IEEE80211,
+			HOSTAPD_LEVEL_INFO,  "WNM: BSS Transition Accepted - target BSSID: " MACSTR,
 			MAC2STR(pos));
+
+		sta = ap_get_sta(hapd, addr);
+		if (sta == NULL) {
+			hostapd_logger(hapd, NULL, HOSTAPD_MODULE_IEEE80211,
+				HOSTAPD_LEVEL_WARNING, "sta "MACSTR" not found for BSS transition response",
+				MAC2STR(addr));
+			return;
+		}
+
+		ap_sta_set_authorized(hapd, sta, 0);
+		sta->flags &= ~WLAN_STA_ASSOC;
+		wpa_auth_sm_event(sta->wpa_sm, WPA_DISASSOC);
+		ieee802_1x_notify_port_enabled(sta->eapol_sm, 0);
+		sta->acct_terminate_cause = RADIUS_ACCT_TERMINATE_CAUSE_USER_REQUEST;
+		accounting_sta_stop(hapd, sta);
+		ieee802_1x_free_station(hapd, sta);
+
+		hostapd_logger(hapd, sta->addr, HOSTAPD_MODULE_IEEE80211,
+					   HOSTAPD_LEVEL_INFO, "WNM: disassociated due to accepted BSS transition request");
+
+		sta->timeout_next = STA_DEAUTH;
+		eloop_cancel_timeout(ap_handle_timer, hapd, sta);
+		eloop_register_timeout(0, WNM_DEAUTH_DELAY_USEC, ap_handle_timer, hapd, sta);
+		mlme_disassociate_indication(hapd, sta, WLAN_REASON_DISASSOC_STA_HAS_LEFT);
+
 		pos += ETH_ALEN;
+	} else if (status_code == WNM_BSS_TM_REJECT_NO_SUITABLE_CANDIDATES) {
+		hostapd_logger(hapd, NULL, HOSTAPD_MODULE_IEEE80211,
+					   HOSTAPD_LEVEL_INFO,  "WNM: BSS Transition Rejected - No Suitable Candidates");
 	} else {
 		wpa_msg(hapd->msg_ctx, MSG_INFO, BSS_TM_RESP MACSTR
 			" status_code=%u bss_termination_delay=%u",
@@ -629,3 +664,100 @@ int wnm_send_bss_tm_req(struct hostapd_data *hapd, struct sta_info *sta,
 
 	return 0;
 }
+
+
+/*
+ * IEEE Std 802.11-2012 10.23.6.3 BSS transition management request
+ * The AP may send an unsolicited BSS Transition Management Request frame to a
+ * non-AP STA at any time if the non-AP STA indicates that it supports the
+ * BSS Transition Management capability in the Extended Capabilities element.
+ * A non-AP STA that supports BSS transition management shall respond to an
+ * individually addressed BSS Transition Management Request frame with a BSS
+ * Transition Management Response frame.
+ * This version will create the neighbor report using the passed in bssid and
+ * channel.
+*/
+int wnm_send_bss_tm_req2(struct hostapd_data *hapd,
+			 struct sta_info *sta, int disassoc_timer,
+			 const u8 *bssid, u8 ap_channel)
+{
+	static const u8 valid_int = 255;
+	u8 report_ie_len;
+	u8 op_class, channel;
+	struct wnm_neighbor_report_element* report_ie;
+	u8 req_mode = 0;
+	u8* pos = NULL;
+
+	/*
+	 * IEEE Std 802.11-2012
+	 * The BSS Transition Candidate List Entries field of a BSS Transition
+	 * Management Response frame contains zero or more Neighbor Report
+	 * elements describing the non-AP STA’s preferences for target BSS
+	 * candidates. The Preference field value of a Neighbor Report element
+	 * used in a BSS Transition Management Response frame shall be between
+	 * 1 and 255. The value of 0 is reserved. The values between 1 and 255
+	 * provide the indication of order, with 255 indicating the most
+	 * preferred BSS within the given candidate list, decreasing numbers
+	 * representing decreasing preference relative only to entries with
+	 * lower values of the Preference field, and equal numbers representing
+	 * equal preference. The non-AP STA should not list any BSS that is not
+	 * considered as a target BSS candidate.
+	 *
+	 * Contains the description of candidate BSS transition APs and their
+	 * capabilities as described in 8.4.2.39.
+	 */
+	/* add 3 octets for candidate preference */
+	report_ie = os_zalloc(sizeof(struct wnm_neighbor_report_element) + 3);
+	if (!report_ie)
+		return -1;
+
+	req_mode = WNM_BSS_TM_REQ_ABRIDGED |
+		WNM_BSS_TM_REQ_PREF_CAND_LIST_INCLUDED;
+	if (disassoc_timer > 0)
+		req_mode |= WNM_BSS_TM_REQ_DISASSOC_IMMINENT;
+
+	report_ie_len = sizeof(struct wnm_neighbor_report_element);
+	report_ie->eid = WLAN_EID_NEIGHBOR_REPORT;
+	report_ie->len = report_ie_len - 2 + 3;
+	os_memcpy(report_ie->bssid, bssid, ETH_ALEN);
+
+	if (ieee80211_freq_to_channel_ext(hapd->iface->freq,
+					  hapd->iconf->secondary_channel,
+					  hapd->iconf->vht_oper_chwidth,
+					  &op_class, &channel) !=
+	    NUM_HOSTAPD_MODES)
+		report_ie->operating_class = op_class;
+
+	report_ie->channel_number = ap_channel;
+
+	/*
+	 * The AP Reachability field indicates whether the AP identified by this
+	 * BSSID is reachable by the STA that requested the neighbor report. For
+	 * example, the AP identified by this BSSID is reachable for the
+	 * exchange of preauthentication frames as described in 11.5.9.2.
+	 *
+	 * The Security bit, if 1, indicates that the AP identified by this
+	 * BSSID supports the same security provisioning as used by the STA in
+	 * its current association.
+	 */
+	report_ie->bssid_info[0] = WNM_REACHABILITY_REACHABLE | WNM_SECURITY;
+
+	pos = (u8 *) report_ie;
+	pos += report_ie_len;
+
+	/*  BSS Transition Candidate Preference subelement field */
+	*pos++ = 3;   /* Subelement ID */
+	*pos++ = 1;   /* length */
+	*pos++ = 255; /* Preference */
+	report_ie_len += 3;
+
+	hostapd_logger(hapd, NULL, HOSTAPD_MODULE_IEEE80211,
+		       HOSTAPD_LEVEL_INFO,
+		       "WNM: Send BSS Transition Management Request client "
+		       MACSTR " (disassoc_timer=%d) to AP " MACSTR,
+		       MAC2STR(sta->addr), disassoc_timer, MAC2STR(bssid));
+
+	return wnm_send_bss_tm_req(hapd, sta, req_mode, disassoc_timer,
+				   valid_int, NULL, NULL, (u8 *) report_ie,
+				   report_ie_len, NULL, 0);
+}
diff --git a/src/ap/wnm_ap.h b/src/ap/wnm_ap.h
index a44eadb..8e0c05e 100644
--- a/src/ap/wnm_ap.h
+++ b/src/ap/wnm_ap.h
@@ -1,6 +1,7 @@
 /*
  * IEEE 802.11v WNM related functions and structures
  * Copyright (c) 2011-2014, Qualcomm Atheros, Inc.
+ * Copyright (c) 2015, CoCo Communications, Inc.
  *
  * This software may be distributed under the terms of the BSD license.
  * See README for more details.
@@ -23,5 +24,8 @@ int wnm_send_bss_tm_req(struct hostapd_data *hapd, struct sta_info *sta,
 			const u8 *bss_term_dur, const char *url,
 			const u8 *nei_rep, size_t nei_rep_len,
 			const u8 *mbo_attrs, size_t mbo_len);
+int wnm_send_bss_tm_req2(struct hostapd_data *hapd,
+			 struct sta_info *sta, int disassoc_timer,
+			 const u8 *ap_addr, u8 ap_channel);
 
 #endif /* WNM_AP_H */
diff --git a/src/common/ieee802_11_defs.h b/src/common/ieee802_11_defs.h
index 02d2ad7..b4343c5 100644
--- a/src/common/ieee802_11_defs.h
+++ b/src/common/ieee802_11_defs.h
@@ -1433,6 +1433,56 @@ enum wnm_action {
 #define WNM_BSS_TM_REQ_DISASSOC_IMMINENT BIT(2)
 #define WNM_BSS_TM_REQ_BSS_TERMINATION_INCLUDED BIT(3)
 #define WNM_BSS_TM_REQ_ESS_DISASSOC_IMMINENT BIT(4)
+/* IEEE Std 802.11-2012 - Figure 8-141 Measurement Report field */
+#define WNM_REACHABILITY_NOT		0x1
+#define WNM_REACHABILITY_UNKNOWN	0x2
+#define WNM_REACHABILITY_REACHABLE	0x3
+
+#define WNM_SECURITY				0x4
+
+#define WNM_MOBILITY_DOMAIN			0x4
+
+/* IEEE 802.11-2012 - 8.4.2.39 Neighbor Report element */
+struct wnm_neighbor_report_element {
+	u8 eid;     /* WLAN_EID_NEIGHBOR_REPORT */
+	u8 len;
+	u8 bssid[6];
+	/*
+	u8 info_ap_reachability:2;
+	u8 info_security:1;
+	u8 info_key_scope:1;
+	u8 info_spectrum_management:1;
+	u8 info_QoS:1;
+	u8 info_APSD:1;
+	u8 info_rm:1;
+	u8 info_delayed_block_ack:1;
+	u8 info_immediate_block_ack:1;
+	u8 info_mobility_domain:1;
+	u8 info_high_throughput:1;
+	u8 info_reserved1:4;
+	*/
+	u8 bssid_info[4];
+	u8 operating_class;
+	u8 channel_number;
+	u8 PHY_type;
+	u8 subelements[0];	/* Optional Subelements */
+} STRUCT_PACKED;
+
+/* IEEE 802.11-2012 Table 8-115 Optional subelement IDs for neighbor report */
+struct wnm_neighbor_report_candidate_preference_subelement {
+	u8 sub_eid; // 3
+	u8 len; // 1
+	u8 preference; // 0 is ignore, 255 is highest
+} STRUCT_PACKED;
+
+/* IEEE 802.11-2012 - 8.4.2.38 AP Channel Report element */
+struct wnm_ap_channel_report_element {
+	u8 eid;     /* WLAN_EID_CHANNEL_REPORT */
+	u8 len;
+	u8 operating_class;
+	u8 channel_list[0];
+} STRUCT_PACKED;
+
 
 /* IEEE Std 802.11-2012 - Table 8-253 */
 enum bss_trans_mgmt_status_code {
-- 
1.9.1

>From 6fcb4d853bca1bbcd27cc115c62403e35587523a Mon Sep 17 00:00:00 2001
From: David Weidenkopf <dweidenkopf@xxxxxxxxxxxx>
Date: Thu, 14 Jul 2016 10:54:06 -0700
Subject: [PATCH 3/3] Steering: Implement network steering capability for
 multi-AP deployments

Requires station blacklist support and BSS Transition (IEEE 802.11v).

Signed-off-by: David Weidenkopf <dweidenkopf@xxxxxxxxxxxx>
---
 hostapd/Makefile      |    5 +
 hostapd/config_file.c |    6 +
 hostapd/defconfig     |    7 +
 hostapd/main.c        |    7 +-
 src/ap/ap_config.h    |    5 +
 src/ap/hostapd.c      |   13 +
 src/ap/ieee802_11.c   |   17 +-
 src/ap/net_steering.c | 1645 +++++++++++++++++++++++++++++++++++++++++++++++++
 src/ap/net_steering.h |   16 +
 src/ap/sta_info.c     |    5 +
 src/utils/wpa_debug.h |    1 +
 11 files changed, 1720 insertions(+), 7 deletions(-)
 create mode 100644 src/ap/net_steering.c
 create mode 100644 src/ap/net_steering.h

diff --git a/hostapd/Makefile b/hostapd/Makefile
index 6231d4a..1ad8619 100644
--- a/hostapd/Makefile
+++ b/hostapd/Makefile
@@ -74,6 +74,11 @@ OBJS += ../src/ap/ieee802_1x.o
 OBJS += ../src/ap/ap_config.o
 OBJS += ../src/ap/eap_user_db.o
 OBJS += ../src/ap/ieee802_11_auth.o
+ifdef CONFIG_NET_STEERING
+CFLAGS += -DCONFIG_NET_STEERING
+OBJS += ../src/ap/net_steering.o
+endif
+
 OBJS += ../src/ap/sta_info.o
 OBJS += ../src/ap/wpa_auth.o
 OBJS += ../src/ap/tkip_countermeasures.o
diff --git a/hostapd/config_file.c b/hostapd/config_file.c
index 5079f69..236b9a7 100644
--- a/hostapd/config_file.c
+++ b/hostapd/config_file.c
@@ -2560,6 +2560,12 @@ static int hostapd_config_fill(struct hostapd_config *conf,
 	} else if (os_strcmp(buf, "ft_over_ds") == 0) {
 		bss->ft_over_ds = atoi(pos);
 #endif /* CONFIG_IEEE80211R */
+#ifdef CONFIG_NET_STEERING
+	} else if (os_strcmp(buf, "net_steering_mode") == 0) {
+		os_free(bss->net_steeering_mode);
+		/* can be "off", "suggest", or "force" */
+		bss->net_steeering_mode = os_strdup(pos);
+#endif /* CONFIG_NET_STEERING */
 #ifndef CONFIG_NO_CTRL_IFACE
 	} else if (os_strcmp(buf, "ctrl_interface") == 0) {
 		os_free(bss->ctrl_interface);
diff --git a/hostapd/defconfig b/hostapd/defconfig
index f7b60e0..6108138 100644
--- a/hostapd/defconfig
+++ b/hostapd/defconfig
@@ -337,3 +337,10 @@ CONFIG_IPV6=y
 # These extentions facilitate efficient use of multiple frequency bands
 # available to the AP and the devices that may associate with it.
 #CONFIG_MBO=y
+
+
+# Network Steering support
+# The network steering system steers client STAs to the infrastructure AP with the
+# best RSSI. APs collaborate and can use several methods to direct client STAs to
+# transition to the best AP.
+#CONFIG_NET_STEERING=y
diff --git a/hostapd/main.c b/hostapd/main.c
index 2c8dbd3..edd572b 100644
--- a/hostapd/main.c
+++ b/hostapd/main.c
@@ -85,6 +85,11 @@ static void hostapd_logger_cb(void *ctx, const u8 *addr, unsigned int module,
 	case HOSTAPD_MODULE_MLME:
 		module_str = "MLME";
 		break;
+#ifdef CONFIG_NET_STEERING
+	case HOSTAPD_MODULE_NET_STEERING:
+		module_str = "STEER";
+		break;
+#endif
 	default:
 		module_str = NULL;
 		break;
@@ -144,7 +149,7 @@ static void hostapd_logger_cb(void *ctx, const u8 *addr, unsigned int module,
 
 
 /**
- * hostapd_driver_init - Preparate driver interface
+ * hostapd_driver_init - Prepare driver interface
  */
 static int hostapd_driver_init(struct hostapd_iface *iface)
 {
diff --git a/src/ap/ap_config.h b/src/ap/ap_config.h
index 8c8f7e2..d4570a7 100644
--- a/src/ap/ap_config.h
+++ b/src/ap/ap_config.h
@@ -341,6 +341,11 @@ struct hostapd_bss_config {
 	int ft_over_ds;
 #endif /* CONFIG_IEEE80211R */
 
+#ifdef CONFIG_NET_STEERING
+	/* can be "off", "suggest", or "force" */
+	char *net_steeering_mode;
+#endif /* CONFIG_NET_STEERING */
+
 	char *ctrl_interface; /* directory for UNIX domain sockets */
 #ifndef CONFIG_NATIVE_WINDOWS
 	gid_t ctrl_interface_gid;
diff --git a/src/ap/hostapd.c b/src/ap/hostapd.c
index a4c0d27..75e41bb 100644
--- a/src/ap/hostapd.c
+++ b/src/ap/hostapd.c
@@ -42,6 +42,7 @@
 #include "bss_load.h"
 #include "x_snoop.h"
 #include "dhcp_snoop.h"
+#include "net_steering.h"
 #include "ndisc_snoop.h"
 #include "neighbor_db.h"
 #include "rrm.h"
@@ -296,6 +297,11 @@ static void hostapd_free_hapd_data(struct hostapd_data *hapd)
 	hapd->radius_das = NULL;
 #endif /* CONFIG_NO_RADIUS */
 
+#ifdef CONFIG_NET_STEERING
+	net_steering_deinit(hapd);
+#endif
+
+
 	hostapd_deinit_wps(hapd);
 
 	authsrv_deinit(hapd);
@@ -1078,6 +1084,13 @@ static int hostapd_setup_bss(struct hostapd_data *hapd, int first)
 		wpa_printf(MSG_ERROR, "Accounting initialization failed.");
 		return -1;
 	}
+#ifdef CONFIG_NET_STEERING
+	if (net_steering_init(hapd) < 0) {
+		wpa_printf(MSG_ERROR, "Failed to initialize net steering");
+		return -1;
+	}
+#endif /* CONFIG_NET_STEERING */
+
 
 	if (conf->ieee802_11f &&
 	    (hapd->iapp = iapp_init(hapd, conf->iapp_iface)) == NULL) {
diff --git a/src/ap/ieee802_11.c b/src/ap/ieee802_11.c
index 926d632..cd25b4f 100644
--- a/src/ap/ieee802_11.c
+++ b/src/ap/ieee802_11.c
@@ -44,9 +44,9 @@
 #include "ieee802_11.h"
 #include "dfs.h"
 #include "mbo_ap.h"
+#include "net_steering.h"
 #include "rrm.h"
 
-
 u8 * hostapd_eid_supp_rates(struct hostapd_data *hapd, u8 *eid)
 {
 	u8 *pos = eid;
@@ -2076,7 +2076,7 @@ static u16 send_assoc_resp(struct hostapd_data *hapd, struct sta_info *sta,
 
 static void handle_assoc(struct hostapd_data *hapd,
 			 const struct ieee80211_mgmt *mgmt, size_t len,
-			 int reassoc)
+			 int ssi_signal, int reassoc)
 {
 	u16 capab_info, listen_interval, seq_ctrl, fc;
 	u16 resp = WLAN_STATUS_SUCCESS, reply_res;
@@ -2267,8 +2267,8 @@ static void handle_assoc(struct hostapd_data *hapd,
 #endif /* CONFIG_IEEE80211N */
 
 	hostapd_logger(hapd, sta->addr, HOSTAPD_MODULE_IEEE80211,
-		       HOSTAPD_LEVEL_DEBUG,
-		       "association OK (aid %d)", sta->aid);
+			HOSTAPD_LEVEL_DEBUG, "association OK (aid %d) on channel %d BSSID "MACSTR,
+			sta->aid, hapd->iconf->channel, MAC2STR(mgmt->bssid));
 	/* Station will be marked associated, after it acknowledges AssocResp
 	 */
 	sta->flags |= WLAN_STA_ASSOC_REQ_OK;
@@ -2286,6 +2286,11 @@ static void handle_assoc(struct hostapd_data *hapd,
 	}
 #endif /* CONFIG_IEEE80211W */
 
+#ifdef CONFIG_NET_STEERING
+	/* TODO pass in ssi_signal */
+	net_steering_association(hapd, sta, ssi_signal);
+#endif  /* CONFIG_NET_STEERING */
+
 	/* Make sure that the previously registered inactivity timer will not
 	 * remove the STA immediately. */
 	sta->timeout_next = STA_NULLFUNC;
@@ -2701,12 +2706,12 @@ int ieee802_11_mgmt(struct hostapd_data *hapd, const u8 *buf, size_t len,
 		break;
 	case WLAN_FC_STYPE_ASSOC_REQ:
 		wpa_printf(MSG_DEBUG, "mgmt::assoc_req");
-		handle_assoc(hapd, mgmt, len, 0);
+		handle_assoc(hapd, mgmt, len, fi->ssi_signal, 0);
 		ret = 1;
 		break;
 	case WLAN_FC_STYPE_REASSOC_REQ:
 		wpa_printf(MSG_DEBUG, "mgmt::reassoc_req");
-		handle_assoc(hapd, mgmt, len, 1);
+		handle_assoc(hapd, mgmt, len, fi->ssi_signal, 1);
 		ret = 1;
 		break;
 	case WLAN_FC_STYPE_DISASSOC:
diff --git a/src/ap/net_steering.c b/src/ap/net_steering.c
new file mode 100644
index 0000000..17f4ce6
--- /dev/null
+++ b/src/ap/net_steering.c
@@ -0,0 +1,1645 @@
+/* FIX: copyright and license statements */
+
+#include "utils/includes.h"
+#include <sys/ioctl.h>
+#include <assert.h>
+
+#include "utils/state_machine.h"
+#include "utils/common.h"
+#include "utils/wpa_debug.h"
+#include "utils/wpabuf.h"
+#include "utils/list.h"
+#include "utils/eloop.h"
+#include "utils/os.h"
+#include "hostapd.h"
+#include "ap_config.h"
+#include "sta_blacklist.h"
+#include "sta_info.h"
+#include "wpa_auth.h"
+#include "wnm_ap.h"
+#include "ap_drv_ops.h"
+#include "ctrl_iface_ap.h"
+#include "common/defs.h"
+#include "l2_packet/l2_packet.h"
+#include "net_steering.h"
+
+
+#define MAX_FRAME_SIZE 1024
+#define MACSTRLEN 18 /* 6 * 2 + 5 seps + NULL */
+
+static const u16 proto = 0x8267; /* chosen at random from unassigned */
+static const u8 tlv_magic = 48;
+static const u8 tlv_version = 1;
+static const u16 max_score = -1;
+
+static const u32 flood_timeout_secs = 1;
+static const u32 client_timeout_secs = 10;
+static const u32 probe_timeout_secs = 34;
+
+static const char *mode_off = "off";
+static const char *mode_suggest = "suggest";
+static const char *mode_force = "force";
+
+/* Can't change the values of these without bumping the tlv version */
+enum {
+	TLV_SCORE = 0,
+	TLV_CLOSE_CLIENT = 1,
+	TLV_CLOSED_CLIENT = 2,
+	TLV_MAP = 3,
+	TLV_CLIENT_FLAGS = 4,
+};
+
+/* Pre decls */
+struct net_steering_client;
+struct net_steering_bss;
+static void do_flood_score(struct net_steering_client *client);
+static void flood_score(void *eloop_data, void *user_ctx);
+static void client_timeout(void *eloop_data, void *user_ctx);
+static void probe_timeout(void *eloop_data, void *user_ctx);
+
+
+/*
+ * Use this so we can track additional data for stas and avoid adding more members to sta_info
+ * It does mean that we need to be concerned about the lifetime of sta_info objects tracked
+ * by hapd
+ * Also, this struct is used to track stas we hear about from other APs via score messages
+ */
+struct net_steering_client
+{
+	struct dl_list list;
+	/*
+	 * This will point to a sta in the hapd list pointed to by the nsb.
+	 * May be NULL if client is not associated
+	 */
+	struct sta_info* sta;
+	struct net_steering_bss* nsb;
+	u16 score;
+
+	enum {
+        /*
+         * AP will allow the client to associate with it.
+         */
+		STEERING_IDLE,
+		/*
+		 * AP has told another AP to blacklist the client and is waiting for it
+		 * to tell us that it has blacklisted the client.
+		 */
+		STEERING_CONFIRMING,
+		/*
+		 * A remote AP has confirmed that it has blacklisted the client; AP is
+		 * now waiting on an associate.
+		 */
+		STEERING_ASSOCIATING,
+		/*
+		 * The client is using this AP to communicate with other devices.
+		 */
+		STEERING_ASSOCIATED,
+		/*
+		 * The AP has blacklisted the client is waiting on a disassociate and will
+		 * then send out a closed packet to remotes.
+		 */
+		STEERING_REJECTING,
+	    /*
+	     * The client is blacklisted and disassociated.
+	     */
+		STEERING_REJECTED,
+	} STEERING_state;
+
+	enum {
+		/*
+		 * The client has started to use this AP to communicate with other devices.
+		 * Note that we don't attempt to explicitly model the client so (dis)associate
+		 * events just appear on APs.
+		 */
+		STEERING_E_ASSOCIATED,
+		/*
+		 * The client has either gone away or associated with a different AP.
+		 */
+		STEERING_E_DISASSOCIATED,
+		/*
+		 * A remote AP sent a client score packet with a score worse than our local score.
+		 */
+		STEERING_E_PEER_IS_WORSE,
+		/*
+		 * A remote AP sent a client score packet with a score the same as (or better) than our local score.
+		 */
+		STEERING_E_PEER_NOT_WORSE,
+		/*
+		 * A remote AP sent a client score that is the maximum possible.
+		 */
+		STEERING_E_PEER_LOST_CLIENT,
+		/*
+		 * The AP has been told to blacklist/transition the client.
+		 */
+		STEERING_E_CLOSE_CLIENT,
+		/*
+		 * A remote AP has confirmed that it has blacklisted/transitioned the client.
+		 */
+		STEERING_E_CLOSED_CLIENT,
+		/*
+		 * Used to limit how long an AP waits on an event (e.g. ClosedClientPacket).
+		 */
+		STEERING_E_TIMEOUT,
+	} STEERING_event;
+
+	/* state machine support */
+	unsigned int changed;
+
+	/* The mac addr of the client. This is necessary since we may not have a sta_info for this client */
+	u8 addr[ETH_ALEN];
+
+	/*
+	 * tracks the sender bssid for close messages
+	 */
+	u8 close_bssid[ETH_ALEN];
+
+	/*
+	 * tracks the remote bssid for received scores
+	 */
+	u8 remote_bssid[ETH_ALEN];
+
+	/*
+	 * tracks the locally adjusted association timer for the remote ap that has the client associated
+	 */
+	struct os_time remote_time;
+
+	/*
+	 * tracks the time of association of the client
+	 */
+	struct os_time association_time;
+
+	/*
+	 * channel used for Fast BSS Transition
+	 */
+	u8 remote_channel;
+};
+
+/* One context per bss */
+struct net_steering_bss {
+	/* supports a dl_list of net_steering_bss */
+	struct dl_list list;
+	/* contains the list of clients */
+	struct dl_list clients;
+	/* bss data structure */
+	struct hostapd_data *hapd;
+	/* frame serial number TODO can we get rid of this, else use it and manage wraparound? */
+	u16 frame_sn;
+	/* the steering control channel */
+	struct l2_packet_data *control;
+
+	enum {
+		MODE_OFF = 0,
+		MODE_SUGGEST = 1,
+		MODE_FORCE = 2,
+	} mode;
+};
+
+
+static struct dl_list nsb_list = DL_LIST_HEAD_INIT(nsb_list);
+
+
+static const char * state_to_str(int state)
+{
+	switch (state) {
+	case STEERING_IDLE:
+		return "IDLE";
+	case STEERING_CONFIRMING:
+		return "CONFIRMING";
+	case STEERING_ASSOCIATING:
+		return "ASSOCIATING";
+	case STEERING_ASSOCIATED:
+		return "ASSOCIATED";
+	case STEERING_REJECTING:
+		return "REJECTING";
+	case STEERING_REJECTED:
+		return "REJECTED";
+	default:
+		return "UNKNOWN";
+	}
+}
+
+
+static const char * event_to_str(int event)
+{
+	switch (event) {
+	case STEERING_E_ASSOCIATED:
+		return "E_ASSOCIATED";
+	case STEERING_E_DISASSOCIATED:
+		return "E_DISASSOCIATED";
+	case STEERING_E_PEER_IS_WORSE:
+		return "E_PEER_IS_WORSE";
+	case STEERING_E_PEER_NOT_WORSE:
+		return "E_PEER_NOT_WORSE";
+	case STEERING_E_PEER_LOST_CLIENT:
+		return "E_PEER_LOST_CLIENT";
+	case STEERING_E_CLOSE_CLIENT:
+		return "E_CLOSE_CLIENT";
+	case STEERING_E_CLOSED_CLIENT:
+		return "E_CLOSED_CLIENT";
+	case STEERING_E_TIMEOUT:
+		return "E_TIMEOUT";
+	default:
+		return "UNKNOWN";
+	}
+}
+
+
+static void client_set_remote_bssid(struct net_steering_client *client,
+				    const u8 *bssid)
+{
+	os_memcpy(client->remote_bssid, bssid, ETH_ALEN);
+}
+
+
+static const u8 * client_get_remote_bssid(struct net_steering_client *client)
+{
+	return client->remote_bssid;
+}
+
+
+static void client_set_close_bssid(struct net_steering_client *client,
+				   const u8 *bssid)
+{
+	os_memcpy(client->close_bssid, bssid, ETH_ALEN);
+}
+
+
+static const u8 * client_get_close_bssid(struct net_steering_client *client)
+{
+	return client->close_bssid;
+}
+
+
+static void client_clear_close_bssid(struct net_steering_client *client)
+{
+	os_memset(client->close_bssid, 0, ETH_ALEN);
+}
+
+
+static const u8* client_get_local_bssid(struct net_steering_client *client)
+{
+	return client->nsb->hapd->conf->bssid;
+}
+
+
+static const u8 * client_get_mac(struct net_steering_client *client)
+{
+	/* assumption is that this is always filled in from sta or via received
+	 * tlv */
+	return client->addr;
+}
+
+
+static struct net_steering_client * client_create(struct net_steering_bss *nsb,
+						  const u8 *addr)
+{
+	struct net_steering_client *client;
+
+	assert(nsb->hapd->conf->bssid);
+	if (!nsb->hapd || !nsb->hapd->conf || !nsb->hapd->conf->bssid) {
+		hostapd_logger(nsb->hapd, NULL, HOSTAPD_MODULE_NET_STEERING,
+			       HOSTAPD_LEVEL_WARNING,
+			       "hapd pointers! are suspect %p",
+				nsb->hapd);
+	}
+
+	client = os_zalloc(sizeof(*client));
+	if (!client) {
+		hostapd_logger(nsb->hapd, NULL, HOSTAPD_MODULE_NET_STEERING,
+			       HOSTAPD_LEVEL_WARNING,
+			       "Failed to create client " MACSTR " for bssid "
+			       MACSTR,
+			       MAC2STR(addr), MAC2STR(nsb->hapd->conf->bssid));
+		return NULL;
+	}
+
+	client->nsb = nsb;
+	client->remote_time.sec = 0;
+	client->remote_time.usec = 0;
+	client->score = max_score;
+	client->STEERING_state = STEERING_IDLE;
+	os_memcpy(client->addr, addr, ETH_ALEN);
+
+	dl_list_add(&nsb->clients, &client->list);
+
+	return client;
+}
+
+
+static void start_flood_timer(struct net_steering_client *client)
+{
+	if (eloop_register_timeout(flood_timeout_secs, 0, flood_score, client,
+				   NULL)) {
+		hostapd_logger(client->nsb->hapd, NULL,
+			       HOSTAPD_MODULE_NET_STEERING,
+			       HOSTAPD_LEVEL_WARNING,
+			       "client " MACSTR " failed to schedule flood",
+			       MAC2STR(client_get_mac(client)));
+	}
+}
+
+
+static void stop_flood_timer(struct net_steering_client *client)
+{
+	client->score = max_score;
+	/* It is safe if a timer is already canceled */
+	eloop_cancel_timeout(flood_score, client, NULL);
+}
+
+
+static void client_start_timer(struct net_steering_client *client)
+{
+	if (eloop_register_timeout(client_timeout_secs, 0, client_timeout,
+				   client, NULL)) {
+		hostapd_logger(client->nsb->hapd,
+			       client_get_local_bssid(client),
+			       HOSTAPD_MODULE_NET_STEERING,
+			       HOSTAPD_LEVEL_WARNING,
+			       "client " MACSTR " failed to schedule timeout",
+			       MAC2STR(client_get_mac(client)));
+	}
+}
+
+
+static void client_stop_timer(struct net_steering_client *client)
+{
+	/* It is safe if a timer is already canceled */
+	eloop_cancel_timeout(client_timeout, client, NULL);
+}
+
+
+static void client_start_probe_timer(struct net_steering_client *client)
+{
+	if (eloop_register_timeout(probe_timeout_secs, 0, probe_timeout, client,
+				   NULL)) {
+		hostapd_logger(client->nsb->hapd,
+			       client_get_local_bssid(client),
+			       HOSTAPD_MODULE_NET_STEERING,
+			       HOSTAPD_LEVEL_WARNING,
+			       "client " MACSTR
+			       " failed to schedule probe timeout",
+			       MAC2STR(client_get_mac(client)));
+	}
+}
+
+
+static void client_stop_probe_timer(struct net_steering_client *client)
+{
+	/* It is safe if a timer is already canceled */
+	eloop_cancel_timeout(probe_timeout, client, NULL);
+}
+
+
+static Boolean
+client_supports_bss_transition(struct net_steering_client *client)
+{
+	return client->sta->dot11MgmtOptionBSSTransitionActivated == 1;
+}
+
+
+static void client_associate(struct net_steering_client *client,
+			     struct sta_info *sta)
+{
+	client->sta = sta;
+	os_memcpy(client->addr, client->sta->addr, ETH_ALEN);
+
+	/* now that the client is associated, cancel probe timer */
+	client_stop_probe_timer(client);
+}
+
+
+static void client_disassociate(struct net_steering_client *client)
+{
+	client->sta = NULL;
+	os_memset(client->remote_bssid, 0, ETH_ALEN);
+	client->remote_time.sec = 0;
+	client->remote_time.usec = 0;
+
+	/* now that the client is disassociated, set probe timer */
+	client_start_probe_timer(client);
+}
+
+
+static Boolean client_is_associated(struct net_steering_client *client)
+{
+	return client->sta && client->STEERING_state == STEERING_ASSOCIATED;
+}
+
+
+static void client_delete(struct net_steering_client *client)
+{
+	stop_flood_timer(client);
+	client_stop_timer(client);
+	client_stop_probe_timer(client);
+
+	dl_list_del(&client->list);
+	os_memset(client, 0, sizeof(*client));
+	os_free(client);
+}
+
+
+static u16 compute_score(int rssi)
+{
+	return (u16) abs(rssi);
+}
+
+
+static struct net_steering_client * client_find(struct net_steering_bss *nsb,
+						const u8* sta)
+{
+	struct net_steering_client *client;
+
+	dl_list_for_each(client, &nsb->clients, struct net_steering_client,
+			 list) {
+		if (os_memcmp(sta, client->addr, ETH_ALEN) == 0)
+			return client;
+	}
+	return NULL;
+}
+
+
+static size_t parse_header(const u8 *buf, size_t len, u8 *magic, u8 *version,
+			   u16 *packet_len, u16 *sn)
+{
+	static u16 header_len;
+	const u8 *tmp;
+
+	header_len = sizeof(*magic) + sizeof(*version) + sizeof(*sn) +
+		sizeof(*packet_len);
+
+	/* TODO maybe using wpabuf would make this code simpler */
+	if (len < header_len)
+		return 0;
+
+	tmp = buf;
+	os_memcpy(magic, tmp, sizeof(*magic));
+	tmp += sizeof(*magic);
+
+	os_memcpy(version, tmp, sizeof(*version));
+	tmp += sizeof(*version);
+
+	os_memcpy(packet_len, tmp, sizeof(*packet_len));
+	*packet_len = ntohs(*packet_len);
+	tmp += sizeof(*packet_len);
+
+	os_memcpy(sn, tmp, sizeof(*sn));
+	*sn = ntohs(*sn);
+	tmp += sizeof(*sn);
+
+	return tmp - buf;
+}
+
+
+static void put_tlv_header(struct wpabuf *buf, u8 tlv_type, u8 tlv_len)
+{
+	wpabuf_put_u8(buf, tlv_type);
+	wpabuf_put_u8(buf, tlv_len);
+}
+
+
+static size_t parse_tlv_header(const u8 *buf, size_t len, u8 *tlv_type,
+			       u8 *tlv_len)
+{
+	static const size_t header_len;
+	const u8 *tmp;
+
+	header_len = sizeof(*tlv_type) + sizeof(*tlv_len);
+	tmp = buf;
+
+	if (len < header_len)
+		return 0;
+
+	os_memcpy(tlv_type, tmp, sizeof(*tlv_type));
+	tmp += sizeof(*tlv_type);
+
+	os_memcpy(tlv_len, tmp, sizeof(*tlv_len));
+	tmp += sizeof(*tlv_len);
+
+	return tmp - buf;
+}
+
+
+static void put_score(struct wpabuf *buf, const u8 *sta, const u8 *bssid,
+		      u16 score, u32 association_msecs)
+{
+	static u8 score_len;
+
+	score_len = ETH_ALEN + ETH_ALEN + sizeof(score) +
+		sizeof(association_msecs);
+
+	put_tlv_header(buf, TLV_SCORE, score_len);
+	wpabuf_put_data(buf, sta, ETH_ALEN);
+	wpabuf_put_data(buf, bssid, ETH_ALEN);
+	score = htons(score);
+	wpabuf_put_data(buf, &score, sizeof(score));
+	association_msecs = htonl(association_msecs);
+	wpabuf_put_data(buf, &association_msecs, sizeof(association_msecs));
+}
+
+
+static void put_close_client(struct wpabuf *buf, const u8 *sta, const u8 *bssid,
+			     const u8 *remote_bssid, u8 channel)
+{
+	static u8 close_len = ETH_ALEN + ETH_ALEN + ETH_ALEN + sizeof(channel);
+
+	put_tlv_header(buf, TLV_CLOSE_CLIENT, close_len);
+	wpabuf_put_data(buf, sta, ETH_ALEN);
+	wpabuf_put_data(buf, bssid, ETH_ALEN);
+	wpabuf_put_data(buf, remote_bssid, ETH_ALEN);
+	wpabuf_put_u8(buf, channel);
+}
+
+
+static void put_closed_client(struct wpabuf *buf, const u8 *sta,
+			      const u8 *bssid)
+{
+	static u8 close_len = ETH_ALEN + ETH_ALEN;
+
+	put_tlv_header(buf, TLV_CLOSED_CLIENT, close_len);
+	wpabuf_put_data(buf, sta, ETH_ALEN);
+	wpabuf_put_data(buf, bssid, ETH_ALEN);
+}
+
+
+static size_t parse_score(const u8 *buf, size_t len, u8 *sta, u8 *bssid,
+			  u16 *score, u32 *association_msecs)
+{
+	static u8 score_len;
+	const u8 *tmp = buf;
+
+	score_len = ETH_ALEN + ETH_ALEN + sizeof(*score) +
+		sizeof(*association_msecs);
+	if (len < score_len)
+		return 0;
+
+	os_memcpy(sta, tmp, ETH_ALEN);
+	tmp += ETH_ALEN;
+	os_memcpy(bssid, tmp, ETH_ALEN);
+	tmp += ETH_ALEN;
+	os_memcpy(score, tmp, sizeof(*score));
+	*score = ntohs(*score);
+	tmp += sizeof(*score);
+	os_memcpy(association_msecs, tmp, sizeof(*association_msecs));
+	*association_msecs = ntohl(*association_msecs);
+	tmp += sizeof(*association_msecs);
+	return tmp - buf;
+}
+
+
+static size_t parse_close_client(const u8 *buf, size_t len, u8 *sta, u8 *bssid,
+				 u8 *target_bssid, u8 *channel)
+{
+	static u8 close_len = ETH_ALEN + ETH_ALEN + ETH_ALEN + sizeof(*channel);
+	const u8 *tmp = buf;
+
+	if (len < close_len)
+		return 0;
+
+	os_memcpy(sta, tmp, ETH_ALEN);
+	tmp += ETH_ALEN;
+	os_memcpy(bssid, tmp, ETH_ALEN);
+	tmp += ETH_ALEN;
+	os_memcpy(target_bssid, tmp, ETH_ALEN);
+	tmp += ETH_ALEN;
+	os_memcpy(channel, tmp, sizeof(*channel));
+	tmp += sizeof(*channel);
+
+	return tmp - buf;
+}
+
+
+static size_t parse_closed_client(const u8 *buf, size_t len, u8 *sta,
+				  u8 *target_bssid)
+{
+	static u8 closed_len = ETH_ALEN + ETH_ALEN;
+	const u8 *tmp = buf;
+
+	if (len < closed_len)
+		return 0;
+
+	os_memcpy(sta, tmp, ETH_ALEN);
+	tmp += ETH_ALEN;
+	os_memcpy(target_bssid, tmp, ETH_ALEN);
+	tmp += ETH_ALEN;
+
+	return tmp - buf;
+}
+
+
+static int probe_req_cb(void *ctx, const u8 *sa, const u8 *da, const u8 *bssid,
+			const u8 *ie, size_t ie_len, int ssi_signal)
+{
+	struct net_steering_bss *nsb = ctx;
+	struct net_steering_client *client;
+
+	/* unused */
+	(void) da;
+	(void) ie;
+	(void) ie_len;
+
+
+	assert(nsb->hapd);
+	/* look up the client in our list */
+	client = client_find(nsb, sa);
+
+	/* if we found the client, or this probe is directed at this bss */
+	if (client || os_memcmp(nsb->hapd->conf->bssid, bssid, ETH_ALEN) == 0) {
+		u16 score;
+
+		if (!client)
+			client = client_create(nsb, sa);
+		if (!client) {
+			hostapd_logger(nsb->hapd, nsb->hapd->conf->bssid,
+				       HOSTAPD_MODULE_NET_STEERING,
+				       HOSTAPD_LEVEL_INFO,
+				       "Failed to create client for " MACSTR
+				       " on received probe",
+				       MAC2STR(sa));
+			return 0;
+		}
+
+		score = compute_score(ssi_signal);
+		if (score != client->score) {
+			hostapd_logger(nsb->hapd, nsb->hapd->conf->bssid,
+				       HOSTAPD_MODULE_NET_STEERING,
+				       HOSTAPD_LEVEL_DEBUG,
+				       "Probe request from " MACSTR " RSSI=%d",
+				       MAC2STR(client_get_mac(client)),
+				       ssi_signal);
+			/* if client is associated, publish score changes
+			 * immediately */
+			client->score = score;
+
+			/* don't flood until the score is updated */
+			if (client_is_associated(client))
+				do_flood_score(client);
+		}
+
+		if (!client_is_associated(client)) {
+			/* set our timer for the next probe */
+			client_stop_probe_timer(client);
+			client_start_probe_timer(client);
+		}
+	}
+	return 0;
+}
+
+
+static void header_put(struct wpabuf *buf, u16 sn)
+{
+	u16 len = 0;
+
+	wpabuf_put_u8(buf, tlv_magic);
+	wpabuf_put_u8(buf, tlv_version);
+	wpabuf_put_data(buf, &len, sizeof(len));
+	sn = htons(sn);
+	wpabuf_put_data(buf, &sn, sizeof(sn));
+}
+
+
+/* write the total length into the header */
+static void header_finalize(struct wpabuf *buf)
+{
+	u16 *p = (u16 *) (wpabuf_mhead_u8(buf) + (sizeof(tlv_magic) +
+						  sizeof(tlv_version)));
+	*p = htons(wpabuf_len(buf));
+}
+
+
+static void flood_message(struct net_steering_bss *nsb,
+			  const struct wpabuf *buf)
+{
+	struct ft_remote_r0kh *r0kh = nsb->hapd->conf->r0kh_list;
+	int ret;
+
+	assert(nsb->hapd->own_addr);
+	assert(buf);
+
+	// TODO use broadcast, but figure out how to handle multiple ess
+	while (r0kh) {
+		u8* dst = r0kh->addr;
+		// don't send to ourself
+		if (os_memcmp(dst, nsb->hapd->own_addr, ETH_ALEN) != 0) {
+
+			ret = l2_packet_send(nsb->control, dst, proto, wpabuf_head(buf), wpabuf_len(buf));
+			if (ret < 0) {
+				hostapd_logger(nsb->hapd, nsb->hapd->conf->bssid, HOSTAPD_MODULE_NET_STEERING,
+				HOSTAPD_LEVEL_WARNING, "Failed send to "MACSTR" : error %d\n",
+				MAC2STR(dst), ret);
+			}
+		} else {
+			/*
+			hostapd_logger(nsb->hapd, nsb->hapd->own_addr, HOSTAPD_MODULE_NET_STEERING,
+			HOSTAPD_LEVEL_WARNING, "Don't send to myself "MACSTR"\n",
+			MAC2STR(nsb->hapd->own_addr));
+			*/
+		}
+		r0kh = r0kh->next;
+	}
+}
+
+static void flood_closed_client(struct net_steering_client *client)
+{
+	struct net_steering_bss* nsb = client->nsb;
+	struct wpabuf* buf;
+
+	buf = wpabuf_alloc(MAX_FRAME_SIZE);
+	header_put(buf, nsb->frame_sn++);
+	put_closed_client(buf, client_get_mac(client), client_get_local_bssid(client));
+	header_finalize(buf);
+
+	hostapd_logger(nsb->hapd, client_get_local_bssid(client), HOSTAPD_MODULE_NET_STEERING,
+			HOSTAPD_LEVEL_DEBUG, "sending closed client "MACSTR" to "MACSTR"\n",
+			MAC2STR(client_get_mac(client)), MAC2STR(client_get_close_bssid(client)));
+
+	flood_message(nsb, buf);
+	wpabuf_free(buf);
+
+	client_clear_close_bssid(client);
+}
+
+static void flood_close_client(struct net_steering_client *client)
+{
+	struct net_steering_bss* nsb = client->nsb;
+	struct wpabuf* buf;
+
+	buf = wpabuf_alloc(MAX_FRAME_SIZE);
+	header_put(buf, nsb->frame_sn++);
+	put_close_client(buf, client_get_mac(client), client_get_local_bssid(client),
+			client_get_remote_bssid(client), client->nsb->hapd->iconf->channel);
+	header_finalize(buf);
+
+	hostapd_logger(nsb->hapd, client_get_local_bssid(client), HOSTAPD_MODULE_NET_STEERING,
+			HOSTAPD_LEVEL_DEBUG, "sending close client "MACSTR" for "MACSTR"\n",
+			MAC2STR(client_get_mac(client)),
+			MAC2STR(client_get_remote_bssid(client)));
+
+	flood_message(nsb, buf);
+	wpabuf_free(buf);
+}
+
+static void do_flood_score(struct net_steering_client *client)
+{
+	struct net_steering_bss* nsb = client->nsb;
+	struct wpabuf* buf;
+
+	if (client->score == max_score) {
+		hostapd_logger(nsb->hapd, client_get_local_bssid(client), HOSTAPD_MODULE_NET_STEERING,
+			HOSTAPD_LEVEL_DEBUG, "skip flooding "MACSTR" max score %d\n",
+			MAC2STR(client_get_mac(client)), client->score);
+	} else {
+		struct os_time now_t;
+		struct os_time delta_t;
+		os_memset(&delta_t, 0, sizeof(delta_t));
+
+		os_get_time(&now_t);
+		os_time_sub(&now_t, &client->association_time, &delta_t);
+		// TODO need to deal with wraparound, currently about 49 days
+		u32 associated_msecs = (delta_t.sec * 1000) + (delta_t.usec / 1000);
+
+		hostapd_logger(nsb->hapd, client_get_local_bssid(client), HOSTAPD_MODULE_NET_STEERING,
+			HOSTAPD_LEVEL_DEBUG, "sending "MACSTR" score %d associated %lu\n",
+			MAC2STR(client_get_mac(client)), (int)client->score, (unsigned long)associated_msecs);
+
+		buf = wpabuf_alloc(MAX_FRAME_SIZE);
+		header_put(buf, nsb->frame_sn++);
+		put_score(buf, client_get_mac(client), client_get_local_bssid(client), client->score, associated_msecs);
+		header_finalize(buf);
+
+		flood_message(nsb, buf);
+		wpabuf_free(buf);
+	}
+}
+
+static void flood_score(void *eloop_data, void *user_ctx)
+{
+	(void) user_ctx;
+	struct net_steering_client* client = (struct net_steering_client*) eloop_data;
+	do_flood_score(client);
+	start_flood_timer(client);
+}
+
+static void do_client_disassociate(struct net_steering_client* client)
+{
+	static const int transition_timeout = 0;
+
+	char mac[MACSTRLEN];
+	if (!snprintf(mac, MACSTRLEN, MACSTR, MAC2STR(client_get_mac(client)))) return;
+
+	if (client_is_associated(client))
+	{
+		if (client->nsb->mode == MODE_SUGGEST || client_supports_bss_transition(client)) {
+			hostapd_logger(client->nsb->hapd, client_get_local_bssid(client), HOSTAPD_MODULE_NET_STEERING,
+				HOSTAPD_LEVEL_INFO, "Fast BSS transition for "MACSTR" to "MACSTR" on channel %d\n",
+				MAC2STR(client_get_mac(client)), MAC2STR(client_get_close_bssid(client)), client->remote_channel);
+
+			wnm_send_bss_tm_req2(client->nsb->hapd, client->sta, transition_timeout,
+					client_get_close_bssid(client), client->remote_channel);
+		} else {
+			hostapd_logger(client->nsb->hapd, client_get_local_bssid(client), HOSTAPD_MODULE_NET_STEERING,
+				HOSTAPD_LEVEL_INFO, "Disassociate "MACSTR"\n", MAC2STR(client_get_mac(client)));
+
+			if (hostapd_ctrl_iface_disassociate(client->nsb->hapd, mac))
+			{
+				hostapd_logger(client->nsb->hapd, client_get_local_bssid(client), HOSTAPD_MODULE_NET_STEERING,
+					HOSTAPD_LEVEL_WARNING, "Failed to disassociate %s\n", mac);
+			}
+		}
+	} else {
+		hostapd_logger(client->nsb->hapd, client_get_local_bssid(client), HOSTAPD_MODULE_NET_STEERING,
+			HOSTAPD_LEVEL_WARNING, "Cannot disassociate "MACSTR", not associated\n",
+			MAC2STR(client_get_mac(client)));
+	}
+}
+
+static void do_client_blacklist_add(struct net_steering_client* client)
+{
+	if (client->nsb->mode == MODE_FORCE) {
+		char mac[MACSTRLEN];
+		if (!snprintf(mac, MACSTRLEN, MACSTR, MAC2STR(client_get_mac(client)))) return;
+
+		hostapd_logger(client->nsb->hapd, client_get_local_bssid(client), HOSTAPD_MODULE_NET_STEERING,
+			HOSTAPD_LEVEL_WARNING, "Blacklist add "MACSTR"\n",
+			MAC2STR(client_get_mac(client)));
+
+		if (hostapd_ctrl_iface_blacklist_add(client->nsb->hapd, mac)) {
+			hostapd_logger(client->nsb->hapd, client_get_local_bssid(client), HOSTAPD_MODULE_NET_STEERING,
+				HOSTAPD_LEVEL_WARNING, "Failed to blacklist %s\n", mac);
+		}
+	}
+}
+
+static void do_client_blacklist_rm(struct net_steering_client* client)
+{
+	if (client->nsb->mode == MODE_FORCE) {
+		char mac[MACSTRLEN];
+		if (!snprintf(mac, MACSTRLEN, MACSTR, MAC2STR(client_get_mac(client)))) return;
+
+		hostapd_logger(client->nsb->hapd, client_get_local_bssid(client), HOSTAPD_MODULE_NET_STEERING,
+			HOSTAPD_LEVEL_WARNING, "Blacklist remove "MACSTR"\n", MAC2STR(client_get_mac(client)));
+
+		if (hostapd_ctrl_iface_blacklist_rm(client->nsb->hapd, mac))
+		{
+			hostapd_logger(client->nsb->hapd, client_get_local_bssid(client), HOSTAPD_MODULE_NET_STEERING,
+				HOSTAPD_LEVEL_WARNING, "Failed to remove %s from blacklist\n", mac);
+		}
+	}
+}
+
+#define STATE_MACHINE_DATA struct net_steering_client
+#define STATE_MACHINE_DEBUG_PREFIX "STEERING"
+#define STATE_MACHINE_ADDR sm->addr
+
+#define SM_EVENT(machine, fromstate, e_event, tostate) \
+static void sm_ ## machine ## _ ## fromstate ## _on_ ## e_event ## _ ## tostate(STATE_MACHINE_DATA *sm, \
+			int global)
+
+/* TODO maybe use a switch, but that would take a more complex set of macros */
+#define SM_TRANSITION(machine, fromstate, e_event, tostate) \
+	if (sm->machine ## _state == machine ## _ ## fromstate && event == machine ## _ ## e_event) { \
+		hostapd_logger(sm->nsb->hapd, sm->nsb->hapd->conf->bssid, HOSTAPD_MODULE_NET_STEERING, \
+		HOSTAPD_LEVEL_DEBUG, "%s => %s "MACSTR" %s\n",\
+		state_to_str(sm->machine ## _state), state_to_str(machine ## _ ## tostate), \
+		MAC2STR(client_get_mac(sm)), event_to_str(machine ## _ ## e_event)); \
+		sm_ ## machine ## _ ## fromstate ## _on ## _ ## e_event ## _ ## tostate(sm, 0); \
+		SM_ENTER(STEERING, tostate); \
+		return; \
+	}
+
+#define SM_TRANS_NOOP(machine, fromstate, e_event, tostate) \
+	if (sm->machine ## _state == machine ## _ ## fromstate && event == machine ## _ ## e_event) { \
+		hostapd_logger(sm->nsb->hapd, sm->nsb->hapd->conf->bssid, HOSTAPD_MODULE_NET_STEERING, \
+		HOSTAPD_LEVEL_DEBUG, "%s => %s "MACSTR" %s noop\n",\
+		state_to_str(sm->machine ## _state), state_to_str(machine ## _ ## tostate), \
+		MAC2STR(client_get_mac(sm)), event_to_str(machine ## _ ## e_event)); \
+		SM_ENTER(STEERING, tostate); \
+		return; \
+	}
+
+#define SM_STEP_EVENT(machine) \
+static void sm_ ## machine ## _do_Event(STATE_MACHINE_DATA *sm, int event)
+
+#define SM_STEP_EVENT_RUN(machine, event, sm) \
+	sm_ ## machine ## _do_Event(sm, machine ## _ ## event);
+
+SM_STATE(STEERING, IDLE) { SM_ENTRY(STEERING, IDLE); }
+SM_STATE(STEERING, CONFIRMING) { SM_ENTRY(STEERING, CONFIRMING); }
+SM_STATE(STEERING, ASSOCIATING) { SM_ENTRY(STEERING, ASSOCIATING); }
+SM_STATE(STEERING, ASSOCIATED) { SM_ENTRY(STEERING, ASSOCIATED); }
+SM_STATE(STEERING, REJECTING) { SM_ENTRY(STEERING, REJECTING); }
+SM_STATE(STEERING, REJECTED) { SM_ENTRY(STEERING, REJECTED); }
+
+SM_EVENT(STEERING, IDLE, E_ASSOCIATED, ASSOCIATED)
+{
+	start_flood_timer(sm);
+}
+
+SM_EVENT(STEERING, IDLE, E_PEER_IS_WORSE, CONFIRMING)
+{
+	flood_close_client(sm);
+}
+
+SM_EVENT(STEERING, IDLE, E_PEER_NOT_WORSE, REJECTED)
+{
+	do_client_blacklist_add(sm);
+	client_start_timer(sm);
+}
+
+SM_EVENT(STEERING, IDLE, E_CLOSE_CLIENT, REJECTED)
+{
+	flood_close_client(sm);
+	do_client_blacklist_add(sm);
+	client_start_timer(sm);
+}
+
+SM_EVENT(STEERING, CONFIRMING, E_PEER_IS_WORSE, CONFIRMING)
+{
+	flood_close_client(sm);
+}
+
+SM_EVENT(STEERING, CONFIRMING, E_ASSOCIATED, ASSOCIATED)
+{
+	start_flood_timer(sm);
+}
+
+SM_EVENT(STEERING, ASSOCIATING, E_ASSOCIATED, ASSOCIATED)
+{
+	start_flood_timer(sm);
+}
+
+SM_EVENT(STEERING, ASSOCIATING, E_PEER_IS_WORSE, ASSOCIATING)
+{
+	flood_close_client(sm);
+}
+
+SM_EVENT(STEERING, ASSOCIATING, E_CLOSE_CLIENT, REJECTED)
+{
+	flood_closed_client(sm);
+	do_client_blacklist_add(sm);
+	client_start_timer(sm);
+}
+
+
+SM_EVENT(STEERING, ASSOCIATED, E_CLOSE_CLIENT, REJECTING)
+{
+	do_client_blacklist_add(sm);
+	do_client_disassociate(sm);
+	client_start_timer(sm);
+	stop_flood_timer(sm);
+}
+
+SM_EVENT(STEERING, ASSOCIATED, E_DISASSOCIATED, IDLE)
+{
+	stop_flood_timer(sm);
+}
+
+SM_EVENT(STEERING, ASSOCIATED, E_PEER_IS_WORSE, ASSOCIATED)
+{
+	flood_close_client(sm);
+}
+
+SM_EVENT(STEERING, REJECTING, E_DISASSOCIATED, REJECTED)
+{
+	flood_closed_client(sm);
+	client_stop_timer(sm); /* exiting REJECTING */
+	client_start_timer(sm); /* enter REJECTED */
+}
+
+SM_EVENT(STEERING, REJECTING, E_PEER_IS_WORSE, CONFIRMING)
+{
+	do_client_blacklist_rm(sm);
+	flood_close_client(sm);
+	client_stop_timer(sm);
+}
+
+SM_EVENT(STEERING, REJECTING, E_PEER_LOST_CLIENT, CONFIRMING)
+{
+	do_client_blacklist_rm(sm);
+	client_stop_timer(sm);
+}
+
+SM_EVENT(STEERING, REJECTING, E_TIMEOUT, ASSOCIATING)
+{
+	do_client_blacklist_rm(sm);
+	client_stop_timer(sm);
+}
+
+SM_EVENT(STEERING, REJECTED, E_PEER_IS_WORSE, CONFIRMING)
+{
+	do_client_blacklist_rm(sm);
+	flood_close_client(sm);
+	client_stop_timer(sm);
+}
+
+SM_EVENT(STEERING, REJECTED, E_PEER_LOST_CLIENT, CONFIRMING)
+{
+	do_client_blacklist_rm(sm);
+	flood_close_client(sm);
+	client_stop_timer(sm);
+}
+
+SM_EVENT(STEERING, REJECTED, E_CLOSE_CLIENT, REJECTED)
+{
+	flood_close_client(sm);
+}
+
+SM_EVENT(STEERING, REJECTED, E_TIMEOUT, ASSOCIATING)
+{
+	do_client_blacklist_rm(sm);
+	client_stop_timer(sm);
+}
+
+
+/*
+
+From original Alloy specification
+Old State	 Event			 New State
+--------------------------------------------------
+Idle,		 Associated,	 Associated
+Idle,		 PeerIsWorse,	 Confirming
+Idle,		 PeerNotWorse,	 Rejected
+Idle,		 PeerLostClient, Associating
+Idle,		 CloseClient,	 Rejected
+
+Confirming,	 ClosedClient,	 Associating
+Confirming,	 Associated,	 Associated
+Confirming,	 TimeOut,	     Idle
+Confirming,	 PeerIsWorse,	 Confirming
+Confirming,	 PeerNotWorse,	 Rejected
+
+Associating, Associated,	 Associated
+Associating, Disassociated,	 Idle
+Associating, PeerIsWorse,	 Associating
+Associating, CloseClient,	 Rejected
+
+Associated,	 CloseClient,	 Rejecting
+Associated,	 Disassociated,	 Idle
+Associated,	 PeerIsWorse,	 Associated
+Associated,	 Timer,          Associated
+
+Rejecting,	 CloseClient,	 Rejecting
+Rejecting,	 Disassociated,	 Rejected
+Rejecting,	 PeerIsWorse,	 Confirming
+Rejecting,	 PeerLostClient, Confirming
+Rejecting,	 TimeOut,        Associating
+
+Rejected,	 PeerIsWorse,    Confirming
+Rejected,	 PeerLostClient, Confirming
+Rejected,	 CloseClient,    Rejected
+Rejected,	 TimeOut,        Associating
+
+	-- NOTEs:
+	-- 1) The client is only blacklisted in Rejecting and Rejected.
+	-- 2) The Associated timer should fire immediately after transitioning to associated and then on an interval.
+	-- 3) The TimeOutEvent in Rejecting+Rejected fires if we haven't gotten a score recently.
+	-- 4) Events that don't match the above should be no-ops. Note that this does happen, for example if a MAP is in
+	-- Confirming and gets PeerIsWorsePacket it will send out CloseClientPacket which means it will get two
+	-- ClosedClientPacket, one of which should be ignored.
+	-- 5) The fsm should pop into existence when a MAP links up with the client and go away after being inactive.
+*/
+
+/*
+Idle
+{
+	associated			Associated 		{}
+	peer_is_worse		Confirming 		{unicast_close_client();}
+	peer_not_worse		Rejected 		{blacklist();}
+	peer_lost_client	Associating 	{}
+	close_client		Rejected 		{unicast_closed_client(); blacklist();}
+	Default				Idle			{}
+}
+
+Confirming
+{
+	closed_client		Associating 	{}
+	associated			Associated 		{}
+	time_out			Idle 			{}
+	peer_is_worse		Confirming 		{unicast_close_client();}
+	peer_not_worse		Rejected 		{blacklist();}
+	Default				Confirming		{}
+}
+
+Associating
+{
+	associated			Associated 		{}
+	disassociated		Idle 			{}
+	peer_is_worse		Associating 	{unicast_close_client();}
+	close_client		Rejected 		{unicast_closed_client(); blacklist();}
+	Default				Associating		{}
+}
+
+Associated
+Entry {fast_flooding(); flood_score();}
+Exit  {slow_flooding();}
+{
+	close_client		Rejecting 		{blacklist(); disassociate(); clear_remotes();}
+	disassociated		Idle 			{flood_peer_lost_client();}
+	peer_is_worse		Associated 		{unicast_close_client();}
+	Default				Associated		{}
+}
+
+Rejecting
+Entry {start_timeout();}
+Exit  {stop_timeout();}
+{
+	close_client		Rejecting 		{}
+	disassociated		Rejected 		{unicast_closed_client();}
+	peer_is_worse		Confirming 		{unicast_close_client(); unblacklist();}
+	peer_lost_client	Confirming 		{unblacklist();}
+	time_out			Associating 	{unblacklist();}
+	Default				Rejecting		{}
+}
+
+Rejected
+Entry {start_timeout();}
+Exit  {stop_timeout();}
+{
+	peer_is_worse		Confirming 		{unicast_close_client(); unblacklist();}
+	peer_lost_client	Confirming 		{unblacklist();}
+	close_client		Rejected 		{unicast_closed_client();}
+	time_out			Associating 	{unblacklist();}
+	Default				Rejected		{}
+}
+*/
+
+SM_STEP_EVENT(STEERING)
+{
+	/* Define transitions for every event that has an action to perform */
+	/* Use no-ops for cases where only a state change is required */
+	/* Beware of states that have entry/exit actions defined */
+	SM_TRANSITION(STEERING, IDLE, E_ASSOCIATED, ASSOCIATED);
+	SM_TRANSITION(STEERING, IDLE, E_PEER_IS_WORSE, CONFIRMING);
+	SM_TRANSITION(STEERING, IDLE, E_PEER_NOT_WORSE, REJECTED);
+	SM_TRANS_NOOP(STEERING, IDLE, E_PEER_LOST_CLIENT, ASSOCIATING);
+	SM_TRANSITION(STEERING, IDLE, E_CLOSE_CLIENT, REJECTED);
+
+	SM_TRANS_NOOP(STEERING, CONFIRMING, E_CLOSED_CLIENT, ASSOCIATING);
+	SM_TRANSITION(STEERING, CONFIRMING, E_ASSOCIATED, ASSOCIATED);
+	SM_TRANS_NOOP(STEERING, CONFIRMING, E_TIMEOUT, IDLE);
+	SM_TRANSITION(STEERING, CONFIRMING, E_PEER_IS_WORSE, CONFIRMING);
+	// This transition is invalid because if we are confirming, we have closed the client
+	// and therefore we don't want to blacklist via rejected, so ignore this event
+	//SM_TRANSITION(STEERING, CONFIRMING, E_PEER_NOT_WORSE, REJECTED);
+
+	SM_TRANSITION(STEERING, ASSOCIATING, E_ASSOCIATED, ASSOCIATED);
+	SM_TRANS_NOOP(STEERING, ASSOCIATING, E_DISASSOCIATED, IDLE);
+	SM_TRANSITION(STEERING, ASSOCIATING, E_PEER_IS_WORSE, ASSOCIATING);
+	SM_TRANSITION(STEERING, ASSOCIATING, E_CLOSE_CLIENT, REJECTED);
+
+	SM_TRANSITION(STEERING, ASSOCIATED, E_CLOSE_CLIENT, REJECTING);
+	SM_TRANSITION(STEERING, ASSOCIATED, E_DISASSOCIATED, IDLE);
+	SM_TRANSITION(STEERING, ASSOCIATED, E_PEER_IS_WORSE, ASSOCIATED);
+
+	SM_TRANSITION(STEERING, REJECTING, E_DISASSOCIATED, REJECTED);
+	SM_TRANSITION(STEERING, REJECTING, E_PEER_IS_WORSE, CONFIRMING);
+	SM_TRANSITION(STEERING, REJECTING, E_PEER_LOST_CLIENT, CONFIRMING);
+	SM_TRANSITION(STEERING, REJECTING, E_TIMEOUT, ASSOCIATING);
+
+	SM_TRANSITION(STEERING, REJECTED, E_PEER_IS_WORSE, CONFIRMING);
+	SM_TRANSITION(STEERING, REJECTED, E_PEER_LOST_CLIENT, CONFIRMING);
+	SM_TRANSITION(STEERING, REJECTED, E_CLOSE_CLIENT, REJECTED);
+	SM_TRANSITION(STEERING, REJECTED, E_TIMEOUT, ASSOCIATING);
+
+	/* By design, the default response is no state change */
+	hostapd_logger(sm->nsb->hapd, sm->nsb->hapd->conf->bssid, HOSTAPD_MODULE_NET_STEERING,
+		HOSTAPD_LEVEL_DEBUG_VERBOSE,
+		"Client "MACSTR" default handler for %s - %s\n",
+		MAC2STR(sm->addr), state_to_str(sm->STEERING_state), event_to_str(event));
+}
+
+static void client_timeout(void *eloop_data, void *user_ctx)
+{
+	(void) user_ctx;
+	struct net_steering_client* client = (struct net_steering_client*) eloop_data;
+
+	SM_STEP_EVENT_RUN(STEERING, E_TIMEOUT, client);
+}
+
+static void probe_timeout(void *eloop_data, void *user_ctx)
+{
+	(void) user_ctx;
+	struct net_steering_client* client = (struct net_steering_client*) eloop_data;
+
+	hostapd_logger(client->nsb->hapd, NULL, HOSTAPD_MODULE_NET_STEERING,
+				HOSTAPD_LEVEL_INFO,
+				"Probe timeout for client "MACSTR" score=%d\n",
+				MAC2STR(client_get_mac(client)), client->score);
+
+	client->score = max_score;
+}
+static void compare_scores(struct net_steering_client *client, u16 score)
+{
+	/* now consider the score if it is from the true owner of the client */
+	if (client->score < score) {
+		SM_STEP_EVENT_RUN(STEERING, E_PEER_IS_WORSE, client);
+	} else {
+		SM_STEP_EVENT_RUN(STEERING, E_PEER_NOT_WORSE, client);
+	}
+}
+
+static void receive_score(struct net_steering_bss* nsb, const u8* sta, const u8* bssid,
+		u16 score, u32 association_msecs)
+{
+	struct net_steering_client *client = NULL;
+
+	client = client_find(nsb, sta);
+	if (!client) {
+		client = client_create(nsb, sta);
+		if (!client) {
+			hostapd_logger(nsb->hapd, NULL, HOSTAPD_MODULE_NET_STEERING,
+				HOSTAPD_LEVEL_INFO,
+				"Failed to create client for "MACSTR"\n",
+				MAC2STR(sta));
+			return;
+		}
+	}
+
+	hostapd_logger(nsb->hapd, nsb->hapd->conf->bssid, HOSTAPD_MODULE_NET_STEERING,
+			HOSTAPD_LEVEL_DEBUG, MACSTR" sent score for "MACSTR" %d %lu local %d\n",
+			MAC2STR(bssid), MAC2STR(client_get_mac(client)), score, (unsigned long) association_msecs, client->score);
+
+	/* we only care about scores when the client is not associated */
+	if (client_is_associated(client)) return;
+
+	/* if we receive a score from a new AP for this client */
+	/* Establish if the AP has newer information than the current one */
+	if (os_memcmp(bssid, client_get_remote_bssid(client), ETH_ALEN) != 0) {
+		struct os_time now_t;
+		struct os_time association_t;
+		struct os_time local_t;
+
+		os_memset(&now_t, 0, sizeof(now_t));
+		os_memset(&association_t, 0, sizeof(association_t));
+		os_memset(&local_t, 0, sizeof(local_t));
+
+		os_get_time(&now_t);
+
+		association_t.sec = association_msecs / 1000;
+		association_t.usec = (association_msecs % 1000) * 1000;
+
+		/*
+		 * Compute a local time that is corrected with associated time from remote.
+		 * This allows us to determine the remote AP with the most recent information
+		 * regarding the client, and consequently which AP's scores should be evaluated.
+		 */
+		os_time_sub(&now_t, &association_t, &local_t);
+
+		hostapd_logger(nsb->hapd, nsb->hapd->conf->bssid, HOSTAPD_MODULE_NET_STEERING,
+				HOSTAPD_LEVEL_DEBUG, MACSTR" current %ld %ld received %ld %ld\n",
+				MAC2STR(bssid), client->remote_time.sec, client->remote_time.usec, local_t.sec, local_t.usec);
+
+		/* should we switch which AP is believed to be associated with the client? */
+		/* only if last remote time is before local time (giving us newer info) */
+		if (os_time_before(&client->remote_time, &local_t)) {
+			hostapd_logger(nsb->hapd, nsb->hapd->conf->bssid, HOSTAPD_MODULE_NET_STEERING,
+					HOSTAPD_LEVEL_INFO, MACSTR" is associated with client "MACSTR"\n",
+					MAC2STR(bssid), MAC2STR(client_get_mac(client)));
+
+			client->remote_time.sec = local_t.sec;
+			client->remote_time.usec = local_t.usec;
+			client_set_remote_bssid(client, bssid);
+			compare_scores(client, score);
+		}
+	} else {
+		/* if this score is from the same AP, then check it */
+		compare_scores(client, score);
+	}
+}
+
+static void receive_close_client(struct net_steering_bss* nsb, const u8* sta,
+		const u8* bssid, const u8* target_bssid, u8 ap_channel)
+{
+	struct net_steering_client *client = NULL;
+
+	if (os_memcmp(nsb->hapd->conf->bssid, target_bssid, ETH_ALEN) == 0) {
+
+		client = client_find(nsb, sta);
+		if (!client) {
+			hostapd_logger(nsb->hapd, nsb->hapd->conf->bssid, HOSTAPD_MODULE_NET_STEERING,
+							HOSTAPD_LEVEL_DEBUG,
+							"Close client can't find client "MACSTR"\n",
+							MAC2STR(sta));
+			return;
+		}
+
+		client->remote_channel = ap_channel;
+		client_set_close_bssid(client, bssid);
+
+		SM_STEP_EVENT_RUN(STEERING, E_CLOSE_CLIENT, client);
+	}
+}
+
+static void receive_closed_client(struct net_steering_bss* nsb, const u8* sta, const u8* target_bssid)
+{
+	struct net_steering_client *client = NULL;
+
+ 	if (os_memcmp(nsb->hapd->conf->bssid, target_bssid, ETH_ALEN) == 0) {
+
+		client = client_find(nsb, sta);
+		if (!client) {
+			return;
+		}
+
+		SM_STEP_EVENT_RUN(STEERING, E_CLOSED_CLIENT, client);
+	}
+}
+
+static void receive(void *ctx, const u8 *src_addr, const u8 *buf, size_t len)
+{
+	struct net_steering_bss* nsb = ctx;
+	u16 sn = 0;
+	u8 magic, version, type_tlv, tlv_len = 0;
+	u16 packet_len = 0;
+	u16 score = 0;
+	u32 association_msecs = 0;
+	u8 sta[ETH_ALEN];
+	u8 bssid[ETH_ALEN];
+	u8 target_bssid[ETH_ALEN];
+	u8 ap_channel = 0;
+	size_t num_read = 0;
+	const u8* buf_pos = buf;
+
+	num_read = parse_header(buf_pos, len, &magic, &version, &packet_len, &sn);
+	if (!num_read) {
+		hostapd_logger(nsb->hapd, NULL, HOSTAPD_MODULE_NET_STEERING,
+				HOSTAPD_LEVEL_DEBUG,
+				"Dropping short message from "MACSTR": %d bytes\n",
+				MAC2STR(src_addr), len);
+		return;
+	}
+
+	if (len < packet_len) {
+		hostapd_logger(nsb->hapd, NULL, HOSTAPD_MODULE_NET_STEERING,
+				HOSTAPD_LEVEL_DEBUG,
+				"Dropping short message from "MACSTR": recv %d bytes, expected %d\n",
+				MAC2STR(src_addr), len, packet_len);
+		return;
+	}
+
+	if (tlv_version != version || tlv_magic != magic) {
+		hostapd_logger(nsb->hapd, NULL, HOSTAPD_MODULE_NET_STEERING,
+				HOSTAPD_LEVEL_DEBUG,
+				"Dropping invalid message from "MACSTR": magic %d version %d\n",
+				MAC2STR(src_addr), magic, version);
+		return;
+	}
+	buf_pos += num_read;
+
+	while (buf_pos < buf + packet_len) {
+		os_memset(sta, 0, ETH_ALEN);
+		os_memset(bssid, 0, ETH_ALEN);
+		os_memset(target_bssid, 0, ETH_ALEN);
+
+		num_read = parse_tlv_header(buf_pos, packet_len-(buf_pos-buf), &type_tlv, &tlv_len);
+		if (!num_read) {
+			hostapd_logger(nsb->hapd, NULL, HOSTAPD_MODULE_NET_STEERING,
+					HOSTAPD_LEVEL_DEBUG, "Could not parse tlv header from "MACSTR"\n",
+					MAC2STR(src_addr));
+			return;
+		}
+		buf_pos += num_read;
+
+		switch (type_tlv)
+		{
+		case TLV_SCORE:
+			num_read = parse_score(buf_pos, tlv_len, sta, bssid, &score, &association_msecs);
+			if (!num_read) {
+				hostapd_logger(nsb->hapd, NULL, HOSTAPD_MODULE_NET_STEERING,
+						HOSTAPD_LEVEL_DEBUG, "Could not parse score from "MACSTR"\n",
+						MAC2STR(src_addr));
+				return;
+			}
+			buf_pos += num_read;
+
+			receive_score(nsb, sta, bssid, score, association_msecs);
+			break;
+		case TLV_CLOSE_CLIENT:
+			num_read = parse_close_client(buf_pos, tlv_len, sta, bssid, target_bssid, &ap_channel);
+			if (!num_read) {
+				hostapd_logger(nsb->hapd, NULL, HOSTAPD_MODULE_NET_STEERING,
+						HOSTAPD_LEVEL_DEBUG, "Could not parse close client from "MACSTR"\n",
+						MAC2STR(src_addr));
+				return;
+			}
+			buf_pos += num_read;
+
+			hostapd_logger(nsb->hapd, nsb->hapd->conf->bssid, HOSTAPD_MODULE_NET_STEERING,
+					HOSTAPD_LEVEL_DEBUG, MACSTR" says "MACSTR" should close client "MACSTR"\n",
+					MAC2STR(bssid), MAC2STR(target_bssid), MAC2STR(sta));
+
+			receive_close_client(nsb, sta, bssid, target_bssid, ap_channel);
+			break;
+		case TLV_CLOSED_CLIENT:
+			num_read = parse_closed_client(buf_pos, tlv_len, sta, target_bssid);
+			if (!num_read) {
+				hostapd_logger(nsb->hapd, NULL, HOSTAPD_MODULE_NET_STEERING,
+						HOSTAPD_LEVEL_DEBUG, "Could not parse closed client from "MACSTR"\n",
+						MAC2STR(src_addr));
+				return;
+			}
+			buf_pos += num_read;
+
+			hostapd_logger(nsb->hapd, nsb->hapd->conf->bssid, HOSTAPD_MODULE_NET_STEERING,
+					HOSTAPD_LEVEL_DEBUG, MACSTR" closed client "MACSTR"\n",
+					MAC2STR(target_bssid), MAC2STR(sta));
+
+			receive_closed_client(nsb, sta, target_bssid);
+			break;
+		default:
+			// skip unknown tlvs
+			buf_pos += tlv_len;
+			hostapd_logger(nsb->hapd, NULL, HOSTAPD_MODULE_NET_STEERING,
+					HOSTAPD_LEVEL_WARNING, "Dropping unknown tlv type %d len %d from "MACSTR" : %d\n",
+					type_tlv, tlv_len, MAC2STR(src_addr), ntohs(sn));
+			break;
+		}
+	}
+
+	//hostapd_logger(nsb->hapd, NULL, HOSTAPD_MODULE_NET_STEERING,
+	//		HOSTAPD_LEVEL_DEBUG, "Received %d bytes from "MACSTR" : %d\n",
+	//		len, MAC2STR(src_addr), ntohs(sn));
+}
+
+
+void net_steering_disassociation(struct hostapd_data *hapd, struct sta_info *sta)
+{
+	struct net_steering_bss* nsb = NULL;
+	struct net_steering_client *client, *ctmp;
+
+	if (dl_list_empty(&nsb_list)) return;
+
+	// find the context
+	dl_list_for_each(nsb, &nsb_list, struct net_steering_bss, list) {
+		if (nsb->hapd == hapd) break;
+	}
+
+	if (!nsb) {
+		hostapd_logger(hapd, hapd->conf->bssid, HOSTAPD_MODULE_NET_STEERING,
+			HOSTAPD_LEVEL_WARNING, "Association to unknown bss "MACSTR"\n",
+			MAC2STR(hapd->conf->bssid));
+		return;
+	}
+
+	// find the client and clean it up
+	dl_list_for_each_safe(client, ctmp, &nsb->clients, struct net_steering_client, list) {
+		if (os_memcmp(client_get_mac(client), sta->addr, ETH_ALEN) == 0) {
+
+			hostapd_logger(nsb->hapd, nsb->hapd->conf->bssid, HOSTAPD_MODULE_NET_STEERING,
+						HOSTAPD_LEVEL_INFO, MACSTR" disassociated from "MACSTR" remote is "MACSTR"\n",
+						MAC2STR(sta->addr), MAC2STR(nsb->hapd->conf->bssid),
+						MAC2STR(client_get_close_bssid(client)));
+
+			SM_STEP_EVENT_RUN(STEERING, E_DISASSOCIATED, client);
+			// DO NOT clean up until after the disassociate event has been processed
+			client_disassociate(client);
+			break;
+		}
+	}
+}
+
+void net_steering_association(struct hostapd_data *hapd, struct sta_info *sta, int rssi)
+{
+	struct net_steering_bss* nsb = NULL;
+	struct net_steering_client* client = NULL;
+
+	if (dl_list_empty(&nsb_list)) return;
+
+	dl_list_for_each(nsb, &nsb_list, struct net_steering_bss, list) {
+		if (nsb->hapd == hapd) break;
+	}
+
+	if (!nsb) {
+		hostapd_logger(hapd, hapd->conf->bssid, HOSTAPD_MODULE_NET_STEERING,
+			HOSTAPD_LEVEL_WARNING, "Association to unknown bss "MACSTR"\n",
+			MAC2STR(hapd->conf->bssid));
+		return;
+	}
+
+	hostapd_logger(nsb->hapd, nsb->hapd->conf->bssid, HOSTAPD_MODULE_NET_STEERING,
+				HOSTAPD_LEVEL_INFO, MACSTR" associated to "MACSTR" signal=%d\n",
+				MAC2STR(sta->addr), MAC2STR(nsb->hapd->conf->bssid), rssi);
+
+	if (sta->dot11MgmtOptionBSSTransitionActivated) {
+		hostapd_logger(nsb->hapd, nsb->hapd->conf->bssid, HOSTAPD_MODULE_NET_STEERING,
+			HOSTAPD_LEVEL_DEBUG, "Client "MACSTR" supports Fast BSS transition\n",
+			MAC2STR(sta->addr));
+	}
+
+	client = client_find(nsb, sta->addr);
+	if (!client)
+	{
+		client = client_create(nsb, sta->addr);
+		if (!client) {
+			hostapd_logger(nsb->hapd, nsb->hapd->conf->bssid, HOSTAPD_MODULE_NET_STEERING,
+				HOSTAPD_LEVEL_WARNING, "Failed to create client "MACSTR" on bssid "MACSTR"\n",
+				MAC2STR(sta->addr), MAC2STR(hapd->conf->bssid));
+
+			return;
+		}
+	}
+
+	os_memset(&client->remote_time, 0, sizeof(client->remote_time));
+	os_memset(&client->remote_bssid, 0, ETH_ALEN);
+
+	os_get_time(&client->association_time);
+	client->score = compute_score(rssi);
+	client_associate(client, sta);
+	do_flood_score(client);
+	SM_STEP_EVENT_RUN(STEERING, E_ASSOCIATED, client);
+}
+
+void net_steering_deinit(struct hostapd_data *hapd)
+{
+	struct net_steering_bss *nsb, *tmp;
+
+	if (dl_list_empty(&nsb_list)) return;
+
+	dl_list_for_each_safe(nsb, tmp, &nsb_list, struct net_steering_bss, list) {
+		if (nsb->hapd == hapd) {
+			struct net_steering_client *client, *ctmp;
+			if (nsb->control != NULL) {
+				l2_packet_deinit(nsb->control);
+				wpa_printf(MSG_DEBUG, "net_steering_deinit - l2_packet_deinit");
+			}
+
+			// free all clients
+			dl_list_for_each_safe(client, ctmp, &nsb->clients, struct net_steering_client, list) {
+				client_delete(client);
+			}
+
+			dl_list_del(&nsb->list);
+			os_memset(nsb, 0, sizeof(*nsb));
+			os_free(nsb);
+			break;
+		}
+	}
+}
+
+int net_steering_init(struct hostapd_data *hapd)
+{
+	struct net_steering_bss* nsb = NULL;
+
+	/* see if there is any configuration */
+	if (!hapd->conf->net_steeering_mode) {
+		hostapd_logger(hapd, hapd->conf->bssid, HOSTAPD_MODULE_NET_STEERING,
+				HOSTAPD_LEVEL_WARNING, "no configuration, steering disabled.\n");
+		return 0;
+	}
+	/* if configuration is off, do nothing */
+	if (os_strcmp(hapd->conf->net_steeering_mode, mode_off) == 0) {
+		hostapd_logger(hapd, hapd->conf->bssid, HOSTAPD_MODULE_NET_STEERING,
+				HOSTAPD_LEVEL_WARNING, "configured off, steering disabled.\n");
+		return 0;
+	}
+
+#ifdef CONFIG_IEEE80211R
+	// We piggy-back on fast transition configuration, and use that config to identify our peer APs
+	if (!hapd->conf->r0kh_list) {
+		hostapd_logger(hapd, hapd->conf->bssid, HOSTAPD_MODULE_NET_STEERING,
+				HOSTAPD_LEVEL_WARNING, "No FT key holders configured, steering disabled.\n");
+		return 0;
+	}
+#else
+	hostapd_logger(hapd, hapd->conf->bssid, HOSTAPD_MODULE_NET_STEERING,
+			HOSTAPD_LEVEL_WARNING, "FT feature not included in this build, steering disabled.\n");
+	/* Signaling an error since user enabled steering, but R is not included */
+	return -1;
+#endif
+
+	nsb = (struct net_steering_bss*) os_zalloc(sizeof(*nsb));
+
+	if (!nsb) return -1;
+
+	// TODO: what if there is no bridge in use? use iface?
+	nsb->control = l2_packet_init(hapd->conf->bridge, NULL, proto, receive, nsb, 0);
+	if (nsb->control == NULL) {
+		hostapd_logger(nsb->hapd, nsb->hapd->conf->bssid, HOSTAPD_MODULE_NET_STEERING,
+				HOSTAPD_LEVEL_WARNING, "net_steering_init - l2_packet_init failed for %s\n",
+				hapd->conf->bridge);
+
+		os_memset(nsb, 0, sizeof(*nsb));
+		os_free(nsb);
+		return -1;
+	}
+
+	nsb->hapd = hapd;
+	dl_list_init(&nsb->clients);
+
+	// add the context to the end of the list
+	dl_list_add(&nsb_list, &nsb->list);
+	hostapd_register_probereq_cb(hapd, probe_req_cb, nsb);
+
+	hostapd_logger(nsb->hapd, nsb->hapd->conf->bssid, HOSTAPD_MODULE_NET_STEERING,
+			HOSTAPD_LEVEL_INFO, "ready on %s, own addr "MACSTR": mode: %s\n",
+			hapd->conf->bridge, MAC2STR(nsb->hapd->own_addr),
+			hapd->conf->net_steeering_mode);
+
+	if (os_strcmp(hapd->conf->net_steeering_mode, mode_suggest) == 0)
+		nsb->mode = MODE_SUGGEST;
+	else if (os_strcmp(hapd->conf->net_steeering_mode, mode_force) == 0)
+		nsb->mode = MODE_FORCE;
+	else
+		nsb->mode = MODE_FORCE;
+
+	return 0;
+}
diff --git a/src/ap/net_steering.h b/src/ap/net_steering.h
new file mode 100644
index 0000000..19ec088
--- /dev/null
+++ b/src/ap/net_steering.h
@@ -0,0 +1,16 @@
+/* FIX: copyright and license statements */
+
+#ifndef NETSTEERING_H
+#define NETSTEERING_H
+
+struct sta_info;
+struct hostapd_data;
+
+int net_steering_init(struct hostapd_data *hapd);
+void net_steering_deinit(struct hostapd_data *hapd);
+void net_steering_association(struct hostapd_data *hapd, struct sta_info *sta,
+			      int rssi);
+void net_steering_disassociation(struct hostapd_data *hapd,
+				 struct sta_info *sta);
+
+#endif /* NETSTEERING_H */
diff --git a/src/ap/sta_info.c b/src/ap/sta_info.c
index c36842b..41e0cd8 100644
--- a/src/ap/sta_info.c
+++ b/src/ap/sta_info.c
@@ -34,6 +34,7 @@
 #include "wnm_ap.h"
 #include "mbo_ap.h"
 #include "ndisc_snoop.h"
+#include "net_steering.h"
 #include "sta_info.h"
 #include "vlan.h"
 
@@ -297,6 +298,10 @@ void ap_free_sta(struct hostapd_data *hapd, struct sta_info *sta)
 	p2p_group_notif_disassoc(hapd->p2p_group, sta->addr);
 #endif /* CONFIG_P2P */
 
+#ifdef CONFIG_NET_STEERING
+	net_steering_disassociation(hapd, sta);
+#endif /* CONFIG_NET_STEERING */
+
 #ifdef CONFIG_INTERWORKING
 	if (sta->gas_dialog) {
 		int i;
diff --git a/src/utils/wpa_debug.h b/src/utils/wpa_debug.h
index 17d8f96..612adc0 100644
--- a/src/utils/wpa_debug.h
+++ b/src/utils/wpa_debug.h
@@ -304,6 +304,7 @@ void hostapd_logger_register_cb(hostapd_logger_cb_func func);
 #define HOSTAPD_MODULE_DRIVER		0x00000010
 #define HOSTAPD_MODULE_IAPP		0x00000020
 #define HOSTAPD_MODULE_MLME		0x00000040
+#define HOSTAPD_MODULE_NET_STEERING	0x00000080
 
 enum hostapd_logger_level {
 	HOSTAPD_LEVEL_DEBUG_VERBOSE = 0,
-- 
1.9.1

_______________________________________________
Hostap mailing list
Hostap@xxxxxxxxxxxxxxxxxxx
http://lists.infradead.org/mailman/listinfo/hostap

[Index of Archives]     [Linux Wireless]     [Linux Kernel]     [ATH6KL]     [Linux Bluetooth]     [Linux Netdev]     [Kernel Newbies]     [IDE]     [Security]     [Git]     [Netfilter]     [Bugtraq]     [Yosemite News]     [MIPS Linux]     [ARM Linux]     [Linux Security]     [Linux RAID]     [Linux ATA RAID]     [Samba]     [Device Mapper]

  Powered by Linux