diff mbox

[3/3] Network steering feature

Message ID CACdfkSKuetPMBoCL7VsY3V9=4nUmYCRa+-8Ns0_g=zAtWGF-cQ@mail.gmail.com
State Changes Requested
Headers show

Commit Message

David Weidenkopf July 29, 2016, 3:48 p.m. UTC
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".
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.

Design
------
The network steering system executes a protocol state machine on each participating AP. The APs exchange protocol messages, using the DS, to determine if a client STA should transition to a different AP. APs collect client STA RSSI information from probe requests. When a client STA associates to a participating AP, the network steering system begins tracking the client STA. The associated AP begins distributing its client STA RSSI information to other participating APs. As this RSSI information is received, the receiving AP compares it to the latest RSSI value it received from probe requests. If its local RSSI value is better, it contacts the AP that sent the client STA RSSI and requests the client be transitioned to itself.

State Machine
-------------
The table below specifies the state machine that is executed on each AP for each client. An AP creates a state machine when a client associates with it, when it receives a score for the client from another AP, or it receives a probe request from the client. The top row contains the events defined in the system. The left column contains the current state. The cells contain the new state for a given event. Cells are left blank when there is no state change.
+--------------------------------------------------------------------------------------------------------------------------+
| Current state/Event | Associated | Disassociated | PeerIsWorse | PeerNotWorse | CloseClient | ClosedClient | Timeout     |
|--------------------------------------------------------------------------------------------------------------------------|
| Idle                | Associated |               | Confirming  | Rejected     | Rejected    |              |             |
|--------------------------------------------------------------------------------------------------------------------------|
| Confirming          | Associated |               | Confirming  | Rejected     |             | Associating  | Idle        |
|--------------------------------------------------------------------------------------------------------------------------|
| Associating         | Associated | Idle          | Confirming  |              | Rejected    |              |             |
|--------------------------------------------------------------------------------------------------------------------------|
| Associated          |            | Idle          | Associated  |              | Rejecting   |              | Associated  |
|--------------------------------------------------------------------------------------------------------------------------|
| Rejecting           |            | Rejected      | Confirming  |              | Rejecting   |              | Associating |
|--------------------------------------------------------------------------------------------------------------------------|
| Rejected            |            |               | Confirming  |              | Rejected    |              | Associating |
+--------------------------------------------------------------------------------------------------------------------------+

States
------
Idle - AP will allow the client to associate with it.
Confirming - AP has told another AP to blacklist the client and is waiting for it to tell us that it has blacklisted the client.
Associating - A remote AP has confirmed that it has blacklisted the client; AP is now waiting on an associate.
Associated - The client is associated to this AP.
Rejecting - The AP has blacklisted the client is waiting on a disassociate and will then send out a closed packet to remotes.
Rejected - The client is blacklisted and disassociated.
Events
Associated - A client STA has associated with the AP.
Disassociated -The client has either gone away or associated with a different AP.
TimeOut - Used to limit how long an AP waits on an event (e.g. Re).
CloseClient - The AP has been told to blacklist the client.
ClosedClient - A remote AP has confirmed that it has blacklisted the client.
PeerIsWorse - A remote AP sent a client score packet with a score worse than our local score.
PeerNotWorse - A remote AP sent a client score packet with a score the same as (or better) than our local score.

Packet Descriptions
-------------------
All packets share the same magic number and packet version. Packets received that have a later packet version number or do not match the magic number for the packet shall be ignored. All multi-byte data shall be in network byte order (big-endian byte order). Some values for packets are known and are specified, other values will need to be filled as required. Those values are shown as blanks in this design document.

HEADER
The packet header will be at the head of each set of transmitted TLVs.
+-------------------------------------------------------------------------------+
|Size (in bytes) | Description                                          | Value |
|-------------------------------------------------------------------------------|
|     1          | PACKET_MAGIC                                         |  48   |
|-------------------------------------------------------------------------------|
|     1          | PACKET_VERSION                                       |   1   |
|-------------------------------------------------------------------------------|
|                | Size of entire packet which includes one or more     |       |
|     2          | TLVS not including the magic number or the version   |       |
|                | (e.g. entire packet size - 2 bytes)                  |       |
|-------------------------------------------------------------------------------|
|     2          | Packet serial number which is a monotonically        |       |
|                | increasing number with each packet that is sent.     |       |
+-------------------------------------------------------------------------------+

SCORE_TYPE
This TLV is used to flood the scores of associated clients to all the other APs in the DS. The type of this TLV is 0.
+-------------------------------------------------------------------------------+
|Size (in bytes) | Description                                          | Value |
|-------------------------------------------------------------------------------|
|     1          | Type                                                 |  0    |
|-------------------------------------------------------------------------------|
|     6          | MAC address of client this score is being            |       |
|                | announced for                                        |       |
|-------------------------------------------------------------------------------|
|     6          | MAC address of BSSID that this score is being        |       |
|                | announced for (this is necessary for multiple        |       |
|                | BSSID APs).                                          |       |
|-------------------------------------------------------------------------------|
|     2          | Score of the client                                  |       |
|-------------------------------------------------------------------------------|
|     4          | msecs since association to BSSID                     |       |
+-------------------------------------------------------------------------------+

CLOSE_CLIENT_TYPE
This TLV is used to cause a remote AP to close out a client per the protocol state machine. The type of this TLV is 1.
+-------------------------------------------------------------------------------+
|Size (in bytes) | Description                                         | Value  |
|-------------------------------------------------------------------------------|
|     1          | Type                                                 |  1    |
|-------------------------------------------------------------------------------|
|     6          | MAC address of client to close on destination AP     |       |
|-------------------------------------------------------------------------------|
|     6          | MAC address of BSSID that is sending this TLV        |       |
|-------------------------------------------------------------------------------|
|     6          | MAC address of BSSID that is being sent this TLV     |       |
|-------------------------------------------------------------------------------|
|     1          | The channel used by the sending BSSID                |       |
+-------------------------------------------------------------------------------+

CLOSED_CLIENT_TYPE This TLV is used to notify the close-requesting AP that this AP has successfully closed the client per the protocol state machine. The type of this TLV is 2.
+-------------------------------------------------------------------------------+
|Size (in bytes) | Description                                          | Value |
|-------------------------------------------------------------------------------|
|     1          | Type                                                  |  2   |
|-------------------------------------------------------------------------------|
|     6          | MAC address of the closed client                      |      |
|-------------------------------------------------------------------------------|
|     6          | MAC address of BSSID that sent the close              |      |
+-------------------------------------------------------------------------------+

Implementation
--------------
The network steering system is implemented as a separate module in order to allow simple build-time removal of the code and avoid disruption to the existing codebase. The module includes several public APIs that are used by hostap. The network steering system includes a state machine implemented using the hostap state machine implementation. The hostap state machine macros have been complemented with additional macros to improve code readability. Each configured AP sends unicast messages using L2 frames. The steering system uses blacklist entries on the AP to deny client STA association when the system determines an AP to be a poor choice for the client STA. The system will use BSS Transition request messages, when supported by the client STA, to direct the client STA to the best AP.

Integration
-----------
The network steering module has several public APIs that hostap uses to communicate with the network steering system.
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);

Configuration
-------------
The network steering system relies on two configuration variables to function. First, the net_steering_mode variable is used to control the behavior of the steering system. The default is “off”. Other values for net_steering_mode are: “suggest” and “force”. In “suggest” mode, the system will never blacklist a client, and will only use BSS Transition Request messages to steer client STAs. In “force” mode, the system will use both blacklist and BSS Transition Request messages to steer client STAs. Second, the list of peer APs that participate in the steering protocol are determined by the (802.11-2012 Chapter 12) mobility domain members specified in the r0kh list.

Test Results
------------
Test results show that the network steering system can maintain client STA throughput when the RF environment for the associated AP degrades. The network steering system tolerates client-STA directed transitions (roaming).
diff mbox

Patch

From e49ba3f2aa3b5e6d5d80f4a81cf3f27ed6e7caa2 Mon Sep 17 00:00:00 2001
From: David Weidenkopf <dweidenkopf@cococorp.com>
Date: Thu, 14 Jul 2016 10:54:06 -0700
Subject: [PATCH] Steering: Implement network steering capability for multi-AP
 deployments. Requires station blacklist support and BSS Transition (802.11v).

Signed-off-by: David Weidenkopf <dweidenkopf@cococorp.com>
---
 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 | 1528 +++++++++++++++++++++++++++++++++++++++++++++++++
 src/ap/net_steering.h |   13 +
 src/ap/sta_info.c     |    5 +
 src/utils/wpa_debug.h |    9 +-
 11 files changed, 1605 insertions(+), 10 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 baa7819..1b31cdf 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 6dc7e8c..eed617b 100644
--- a/hostapd/config_file.c
+++ b/hostapd/config_file.c
@@ -2558,6 +2558,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 0ae9a6e..bee724a 100644
--- a/src/ap/ap_config.h
+++ b/src/ap/ap_config.h
@@ -340,6 +340,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 30f57f4..980a409 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);
@@ -1077,6 +1083,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 11f12f9..4a0c182 100644
--- a/src/ap/ieee802_11.c
+++ b/src/ap/ieee802_11.c
@@ -43,9 +43,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;
@@ -2036,7 +2036,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;
@@ -2227,8 +2227,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;
@@ -2246,6 +2246,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;
@@ -2665,12 +2670,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..cab3203
--- /dev/null
+++ b/src/ap/net_steering.c
@@ -0,0 +1,1528 @@ 
+#include "net_steering.h"
+#include "utils/includes.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 <sys/ioctl.h>
+#include <assert.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)
+{
+	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\n",
+				nsb->hapd);
+	}
+
+	struct net_steering_client* client = (struct net_steering_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"\n",
+				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\n",
+				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\n",
+				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\n",
+				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) ? TRUE : FALSE;
+}
+
+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 = NULL;
+
+	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 = sizeof(*magic) + sizeof(*version) + sizeof(*sn) + sizeof(*packet_len);
+
+	/* TODO maybe using wpabuf would make this code simpler */
+	if (len < header_len) return 0;
+
+	const u8* 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 = sizeof(*tlv_type) + sizeof(*tlv_len);
+	const u8* 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 = 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 = ETH_ALEN + ETH_ALEN + sizeof(*score) + sizeof(*association_msecs);
+	const u8* tmp = buf;
+
+	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;
+}
+
+
+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)
+{
+	/* unused */
+	(void) da;
+	(void) ie;
+	(void) ie_len;
+
+	struct net_steering_bss* nsb = ctx;
+	struct net_steering_client *client = NULL;
+
+	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) {
+		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\n",
+				MAC2STR(sa));
+			return 0;
+		}
+		u16 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\n",
+			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..4274528
--- /dev/null
+++ b/src/ap/net_steering.h
@@ -0,0 +1,13 @@ 
+#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
diff --git a/src/ap/sta_info.c b/src/ap/sta_info.c
index c36842b..b2ad573 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..480e11b 100644
--- a/src/utils/wpa_debug.h
+++ b/src/utils/wpa_debug.h
@@ -300,10 +300,13 @@  void hostapd_logger_register_cb(hostapd_logger_cb_func func);
 #define HOSTAPD_MODULE_IEEE80211	0x00000001
 #define HOSTAPD_MODULE_IEEE8021X	0x00000002
 #define HOSTAPD_MODULE_RADIUS		0x00000004
-#define HOSTAPD_MODULE_WPA		0x00000008
+#define HOSTAPD_MODULE_WPA			0x00000008
 #define HOSTAPD_MODULE_DRIVER		0x00000010
-#define HOSTAPD_MODULE_IAPP		0x00000020
-#define HOSTAPD_MODULE_MLME		0x00000040
+#define HOSTAPD_MODULE_IAPP			0x00000020
+#define HOSTAPD_MODULE_MLME			0x00000040
+#ifdef CONFIG_NET_STEERING
+#define HOSTAPD_MODULE_NET_STEERING	0x00000080
+#endif
 
 enum hostapd_logger_level {
 	HOSTAPD_LEVEL_DEBUG_VERBOSE = 0,
-- 
1.9.1