diff --git a/Makefile.am b/Makefile.am
index fcdd342a..3dd426d6 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -406,7 +406,10 @@ tools_hwsim_SOURCES = tools/hwsim.c src/mpdu.h \
 					src/common.h src/common.c \
 					src/band.h src/band.c \
 					src/crypto.h src/crypto.c \
-					src/dbus.h src/dbus.c
+					src/dbus.h src/dbus.c \
+					src/watchlist.h src/watchlist.c \
+					src/module.h src/module.c \
+					tools/rules.c
 
 tools_hwsim_LDADD = $(ell_ldadd)
 tools_hwsim_CFLAGS = -DDBUS_SERVICE=\"net.connman.hwsim\"
diff --git a/tools/hwsim.c b/tools/hwsim.c
index 402b3622..7973eaf6 100644
--- a/tools/hwsim.c
+++ b/tools/hwsim.c
@@ -46,14 +46,15 @@
 #include "src/nl80211util.h"
 #include "src/nl80211cmd.h"
 #include "src/dbus.h"
+#include "src/watchlist.h"
+#include "src/module.h"
+#include "hwsim.h"
 
 #define HWSIM_SERVICE "net.connman.hwsim"
 
 #define HWSIM_RADIO_MANAGER_INTERFACE HWSIM_SERVICE ".RadioManager"
 #define HWSIM_RADIO_INTERFACE HWSIM_SERVICE ".Radio"
 #define HWSIM_INTERFACE_INTERFACE HWSIM_SERVICE ".Interface"
-#define HWSIM_RULE_MANAGER_INTERFACE HWSIM_SERVICE ".RuleManager"
-#define HWSIM_RULE_INTERFACE HWSIM_SERVICE ".Rule"
 
 enum {
 	HWSIM_CMD_UNSPEC,
@@ -112,37 +113,7 @@ enum {
 		(1 << NL80211_IFTYPE_MESH_POINT) \
 	)
 
-enum hwsim_tx_control_flags {
-	HWSIM_TX_CTL_REQ_TX_STATUS		= 1 << 0,
-	HWSIM_TX_CTL_NO_ACK			= 1 << 1,
-	HWSIM_TX_STAT_ACK			= 1 << 2,
-};
-
 #define IEEE80211_TX_RATE_TABLE_SIZE	4
-#define HWSIM_DELAY_MIN_MS		1
-#define HWSIM_MAX_PREFIX_LEN		128
-
-struct hwsim_rule {
-	unsigned int id;
-	uint8_t source[ETH_ALEN];
-	uint8_t destination[ETH_ALEN];
-	bool source_any : 1;
-	bool destination_any : 1;
-	bool bidirectional : 1;
-	bool drop : 1;
-	bool drop_ack : 1;
-	bool enabled : 1;
-	uint32_t frequency;
-	int priority;
-	int signal;
-	int delay;
-	uint8_t *prefix;
-	size_t prefix_len;
-	uint8_t *match;
-	size_t match_len;
-	uint16_t match_offset;
-	int match_times; /* negative value indicates unused */
-};
 
 struct hwsim_support {
 	const char *name;
@@ -169,8 +140,7 @@ static bool p2p_attr;
 static bool no_register = false;
 static const char *radio_name_attr;
 static struct l_dbus *dbus;
-static struct l_queue *rules;
-static unsigned int next_rule_id;
+static struct watchlist hwsim_watch;
 
 static uint32_t hwsim_iftypes = HWSIM_DEFAULT_IFTYPES;
 static const uint32_t hwsim_supported_ciphers[] = {
@@ -222,6 +192,12 @@ static void do_debug(const char *str, void *user_data)
 	l_info("%s%s", prefix, str);
 }
 
+unsigned int hwsim_watch_register(hwsim_frame_cb_t frame_cb, void *user_data,
+			hwsim_destroy_cb_t destroy)
+{
+	return watchlist_add(&hwsim_watch, frame_cb, user_data, destroy);
+}
+
 static void hwsim_disable_support(const char *disable,
 		const struct hwsim_support *map, uint32_t *mask)
 {
@@ -420,30 +396,6 @@ static void list_callback(struct l_genl_msg *msg, void *user_data)
 		l_free(hwname);
 }
 
-struct radio_info_rec {
-	int32_t id;
-	uint32_t wiphy_id;
-	char alpha2[2];
-	bool p2p;
-	bool custom_regdom;
-	uint32_t regdom_idx;
-	int channels;
-	uint8_t addrs[2][ETH_ALEN];
-	char *name;
-	bool ap_only;
-	struct l_dbus_message *pending;
-	uint32_t cmd_id;
-};
-
-struct interface_info_rec {
-	uint32_t id;
-	struct radio_info_rec *radio_rec;
-	uint8_t addr[ETH_ALEN];
-	char *name;
-	uint32_t iftype;
-	int ref;
-};
-
 static struct l_queue *radio_info;
 static struct l_queue *interface_info;
 
@@ -542,15 +494,6 @@ static const char *interface_get_path(const struct interface_info_rec *rec)
 	return path;
 }
 
-static const char *rule_get_path(struct hwsim_rule *rule)
-{
-	static char path[16];
-
-	snprintf(path, sizeof(path), "/rule%u", rule->id);
-
-	return path;
-}
-
 static bool parse_addresses(const uint8_t *buf, size_t len,
 				struct radio_info_rec *rec)
 {
@@ -1161,123 +1104,13 @@ static void rtnl_link_notify(uint16_t type, const void *data, uint32_t len,
 	}
 }
 
-struct hwsim_tx_info {
-	int8_t idx;
-	uint8_t count;
-};
-
-struct hwsim_frame {
-	int refcount;
-	uint8_t src_ether_addr[ETH_ALEN];
-	uint8_t dst_ether_addr[ETH_ALEN];
-	struct radio_info_rec *src_radio;
-	struct radio_info_rec *ack_radio;
-	uint32_t flags;
-	const uint64_t *cookie;
-	int32_t signal;
-	uint32_t frequency;
-	uint16_t tx_info_len;
-	const struct hwsim_tx_info *tx_info;
-	uint16_t payload_len;
-	const uint8_t *payload;
-	bool acked;
-	struct l_genl_msg *msg;
-	int pending_callback_count;
-};
-
-static bool radio_match_addr(const struct radio_info_rec *radio,
-				const uint8_t *addr)
-{
-	if (!radio || util_is_broadcast_address(addr))
-		return !radio && util_is_broadcast_address(addr);
-
-	return !memcmp(addr, radio->addrs[0], ETH_ALEN) ||
-		!memcmp(addr, radio->addrs[1], ETH_ALEN);
-}
-
-static void process_rules(const struct radio_info_rec *src_radio,
-				const struct radio_info_rec *dst_radio,
-				struct hwsim_frame *frame, bool ack, bool *drop,
-				uint32_t *delay)
-{
-	const struct l_queue_entry *rule_entry;
-
-	for (rule_entry = l_queue_get_entries(rules); rule_entry;
-			rule_entry = rule_entry->next) {
-		struct hwsim_rule *rule = rule_entry->data;
-
-		if (!rule->enabled)
-			continue;
-
-		if (!rule->source_any &&
-				!radio_match_addr(src_radio, rule->source) &&
-				(!rule->bidirectional ||
-				 !radio_match_addr(dst_radio, rule->source)))
-			continue;
-
-		if (!rule->destination_any &&
-				!radio_match_addr(dst_radio,
-							rule->destination) &&
-				(!rule->bidirectional ||
-				 !radio_match_addr(src_radio,
-							rule->destination)))
-			continue;
-
-		/*
-		 * If source matches only because rule->bidirectional was
-		 * true, make sure destination is "any" or matches source
-		 * radio's address.
-		 */
-		if (!rule->source_any && rule->bidirectional &&
-				radio_match_addr(dst_radio, rule->source))
-			if (!rule->destination_any &&
-					!radio_match_addr(dst_radio,
-							rule->destination))
-				continue;
-
-		if (rule->frequency && rule->frequency != frame->frequency)
-			continue;
-
-		if (rule->prefix && frame->payload_len >= rule->prefix_len) {
-			if (memcmp(rule->prefix, frame->payload,
-					rule->prefix_len) != 0)
-				continue;
-		}
-
-		if (rule->match && frame->payload_len >=
-					rule->match_len + rule->match_offset) {
-			if (memcmp(rule->match,
-					frame->payload + rule->match_offset,
-					rule->match_len))
-				continue;
-		}
-
-		/* Rule deemed to match frame, apply any changes */
-		if (rule->match_times == 0)
-			continue;
-
-		if (rule->signal)
-			frame->signal = rule->signal / 100;
-
-		/* Don't drop if this is an ACK, unless drop_ack is set */
-		if (!ack || (ack && rule->drop_ack))
-			*drop = rule->drop;
-
-		if (delay)
-			*delay = rule->delay;
-
-		if (rule->match_times > 0)
-			rule->match_times--;
-	}
-}
-
 struct send_frame_info {
 	struct hwsim_frame *frame;
 	struct radio_info_rec *radio;
 	void *user_data;
 };
 
-static bool send_frame_tx_info(struct hwsim_frame *frame)
+bool hwsim_send_tx_info(struct hwsim_frame *frame)
 {
 	struct l_genl_msg *msg;
 
@@ -1299,92 +1132,47 @@ static bool send_frame_tx_info(struct hwsim_frame *frame)
 	return true;
 }
 
-static bool send_frame(struct send_frame_info *info,
-			l_genl_msg_func_t callback,
-			l_genl_destroy_func_t destroy)
+uint32_t hwsim_send_frame(struct hwsim_frame *frame,
+				struct radio_info_rec *radio,
+				l_genl_msg_func_t callback,
+				void *user_data,
+				l_genl_destroy_func_t destroy)
 {
 	struct l_genl_msg *msg;
 	uint32_t rx_rate = 2;
-	unsigned int id;
 
 	msg = l_genl_msg_new_sized(HWSIM_CMD_FRAME,
-					128 + info->frame->payload_len);
+					128 + frame->payload_len);
 	l_genl_msg_append_attr(msg, HWSIM_ATTR_ADDR_RECEIVER, ETH_ALEN,
-				info->radio->addrs[1]);
-	l_genl_msg_append_attr(msg, HWSIM_ATTR_FRAME, info->frame->payload_len,
-				info->frame->payload);
+				radio->addrs[1]);
+	l_genl_msg_append_attr(msg, HWSIM_ATTR_FRAME, frame->payload_len,
+				frame->payload);
 	l_genl_msg_append_attr(msg, HWSIM_ATTR_RX_RATE, 4,
 				&rx_rate);
 	l_genl_msg_append_attr(msg, HWSIM_ATTR_SIGNAL, 4,
-				&info->frame->signal);
+				&frame->signal);
 	l_genl_msg_append_attr(msg, HWSIM_ATTR_FREQ, 4,
-				&info->frame->frequency);
+				&frame->frequency);
 
-	id = l_genl_family_send(hwsim, msg, callback, info, destroy);
-	if (!id) {
-		l_error("Sending HWSIM_CMD_FRAME failed");
-		return false;
-	}
-
-	return true;
+	return l_genl_family_send(hwsim, msg, callback, user_data, destroy);
 }
 
-static struct hwsim_frame *hwsim_frame_ref(struct hwsim_frame *frame)
+struct hwsim_frame *hwsim_frame_ref(struct hwsim_frame *frame)
 {
 	__sync_fetch_and_add(&frame->refcount, 1);
 
 	return frame;
 }
 
-static void hwsim_frame_unref(struct hwsim_frame *frame)
+void hwsim_frame_unref(struct hwsim_frame *frame)
 {
 	if (__sync_sub_and_fetch(&frame->refcount, 1))
 		return;
 
-	if (!frame->pending_callback_count) {
-		/*
-		 * Apparently done with this frame, send tx info and signal
-		 * the returning of an ACK frame in the opposite direction.
-		 */
-
-		if (!(frame->flags & HWSIM_TX_CTL_NO_ACK) && frame->acked) {
-			bool drop = false;
-
-			process_rules(frame->ack_radio, frame->src_radio,
-					frame, true, &drop, NULL);
-
-			if (!drop)
-				frame->flags |= HWSIM_TX_STAT_ACK;
-		}
-
-		if (frame->src_radio)
-			send_frame_tx_info(frame);
-	}
-
 	l_genl_msg_unref(frame->msg);
 	l_free(frame);
 }
 
-static void send_frame_callback(struct l_genl_msg *msg, void *user_data)
-{
-	struct send_frame_info *info = user_data;
-
-	if (l_genl_msg_get_error(msg) == 0) {
-		info->frame->acked = true;
-		info->frame->ack_radio = info->radio;
-	}
-
-	info->frame->pending_callback_count--;
-}
-
-static void send_frame_destroy(void *user_data)
-{
-	struct send_frame_info *info = user_data;
-
-	hwsim_frame_unref(info->frame);
-	l_free(info);
-}
-
 static void send_custom_frame_callback(struct l_genl_msg *msg, void *user_data)
 {
 	struct send_frame_info *info = user_data;
@@ -1438,8 +1226,8 @@ static bool send_custom_frame(const uint8_t *addr, uint32_t freq,
 	if (!info->radio)
 		goto error;
 
-	if (!send_frame(info, send_custom_frame_callback,
-			send_custom_frame_destroy))
+	if (!hwsim_send_frame(frame, info->radio, send_custom_frame_callback,
+				info, send_custom_frame_destroy))
 		goto error;
 
 	return true;
@@ -1451,118 +1239,6 @@ error:
 	return false;
 }
 
-static void frame_delay_callback(struct l_timeout *timeout, void *user_data)
-{
-	struct send_frame_info *send_info = user_data;
-
-	if (send_frame(send_info, send_frame_callback,
-					send_frame_destroy))
-		send_info->frame->pending_callback_count++;
-	else
-		send_frame_destroy(send_info);
-
-	if (timeout)
-		l_timeout_remove(timeout);
-}
-
-
-/*
- * Process frames in a similar way to how the kernel built-in hwsim medium
- * does this, with an additional optimization for unicast frames and
- * additional modifications to frames decided by user-configurable rules.
- */
-static void process_frame(struct hwsim_frame *frame)
-{
-	const struct l_queue_entry *entry;
-	bool drop_mcast = false;
-	bool beacon = false;
-
-	if (util_is_broadcast_address(frame->dst_ether_addr))
-		process_rules(frame->src_radio, NULL, frame, false,
-				&drop_mcast, NULL);
-
-	if (frame->payload_len >= 2 &&
-			frame->payload[0] == 0x80 &&
-			frame->payload[1] == 0x00)
-		beacon = true;
-
-	for (entry = l_queue_get_entries(radio_info); entry;
-			entry = entry->next) {
-		struct radio_info_rec *radio = entry->data;
-		struct send_frame_info *send_info;
-		bool drop = drop_mcast;
-		uint32_t delay = 0;
-		const struct l_queue_entry *i;
-
-		if (radio == frame->src_radio)
-			continue;
-
-		/*
-		 * The kernel hwsim medium passes multicast frames to all
-		 * radios that are on the same frequency as this frame but
-		 * the netlink medium API only lets userspace pass frames to
-		 * radios by known hardware address.  It does check that the
-		 * receiving radio is on the same frequency though so we can
-		 * send to all known addresses.
-		 *
-		 * If the frame's Receiver Address (RA) is a multicast
-		 * address, then send the frame to every radio that is
-		 * registered.  If it's a unicast address then optimize
-		 * by only forwarding the frame to the radios that have
-		 * at least one interface with this specific address.
-		 */
-		if (!util_is_broadcast_address(frame->dst_ether_addr)) {
-			for (i = l_queue_get_entries(interface_info);
-					i; i = i->next) {
-				struct interface_info_rec *interface = i->data;
-
-				if (interface->radio_rec != radio)
-					continue;
-
-				if (!memcmp(interface->addr,
-						frame->dst_ether_addr,
-						ETH_ALEN))
-					break;
-			}
-
-			if (!i)
-				continue;
-		}
-
-		process_rules(frame->src_radio, radio, frame, false,
-				&drop, &delay);
-
-		if (drop)
-			continue;
-
-		/*
-		 * Don't bother sending beacons to other AP interfaces
-		 * if the AP interface is the only one on this phy
-		 */
-		if (beacon && radio->ap_only)
-			continue;
-
-		send_info = l_new(struct send_frame_info, 1);
-		send_info->radio = radio;
-		send_info->frame = hwsim_frame_ref(frame);
-
-		if (delay) {
-			if (!l_timeout_create_ms(delay, frame_delay_callback,
-							send_info, NULL)) {
-				l_error("Error delaying frame %ums, "
-						"frame will be dropped", delay);
-				send_frame_destroy(send_info);
-			}
-		} else
-			frame_delay_callback(NULL, send_info);
-
-	}
-
-	hwsim_frame_unref(frame);
-}
-
-
-
 static void hwsim_frame_event(struct l_genl_msg *msg)
 {
 	struct hwsim_frame *frame;
@@ -1649,17 +1325,6 @@ static void hwsim_frame_event(struct l_genl_msg *msg)
 	frame->signal = -30;
 	frame->msg = l_genl_msg_ref(msg);
 	frame->refcount = 1;
-
-	frame->src_radio = l_queue_find(radio_info, radio_info_match_addr1,
-					transmitter);
-	if (!frame->src_radio) {
-		l_error("Unknown transmitter address %s, probably need to "
-			"update radio dump code for this kernel",
-			util_address_to_string(transmitter));
-		hwsim_frame_unref(frame);
-		return;
-	}
-
 	frame->frequency = *(uint32_t *) freq;
 	frame->flags = *(uint32_t *) flags;
 
@@ -1668,7 +1333,12 @@ static void hwsim_frame_event(struct l_genl_msg *msg)
 	memcpy(frame->src_ether_addr, mpdu->address_2, ETH_ALEN);
 	memcpy(frame->dst_ether_addr, mpdu->address_1, ETH_ALEN);
 
-	process_frame(frame);
+	frame->src_radio = l_queue_find(radio_info, radio_info_match_addr1,
+					transmitter);
+
+	WATCHLIST_NOTIFY(&hwsim_watch, hwsim_frame_cb_t, frame);
+
+	hwsim_frame_unref(frame);
 }
 
 static bool get_tx_rx_addrs(struct l_genl_msg *msg, const uint8_t **tx_out,
@@ -2170,653 +1840,6 @@ static void setup_interface_interface(struct l_dbus_interface *interface)
 					interface_property_get_address, NULL);
 }
 
-static int rule_compare_priority(const void *a, const void *b, void *user)
-{
-	const struct hwsim_rule *rule_a = a;
-	const struct hwsim_rule *rule_b = b;
-
-	return (rule_a->priority > rule_b->priority) ? 1 : -1;
-}
-
-static struct l_dbus_message *rule_add(struct l_dbus *dbus,
-					struct l_dbus_message *message,
-					void *user_data)
-{
-	struct hwsim_rule *rule;
-	const char *path;
-	struct l_dbus_message *reply;
-
-	rule = l_new(struct hwsim_rule, 1);
-	rule->id = next_rule_id++;
-	rule->source_any = true;
-	rule->destination_any = true;
-	rule->delay = 0;
-	rule->enabled = false;
-	rule->match_times = -1;
-	rule->drop_ack = true;
-
-	if (!rules)
-		rules = l_queue_new();
-
-	l_queue_insert(rules, rule, rule_compare_priority, NULL);
-	path = rule_get_path(rule);
-
-	if (!l_dbus_object_add_interface(dbus, path,
-					HWSIM_RULE_INTERFACE, rule))
-		l_info("Unable to add the %s interface to %s",
-				HWSIM_RULE_INTERFACE, path);
-
-	if (!l_dbus_object_add_interface(dbus, path,
-					L_DBUS_INTERFACE_PROPERTIES, NULL))
-		l_info("Unable to add the %s interface to %s",
-				L_DBUS_INTERFACE_PROPERTIES, path);
-
-	reply = l_dbus_message_new_method_return(message);
-	l_dbus_message_set_arguments(reply, "o", path);
-
-	return reply;
-}
-
-static void setup_rule_manager_interface(struct l_dbus_interface *interface)
-{
-	l_dbus_interface_method(interface, "AddRule", 0,
-				rule_add, "o", "", "path");
-}
-
-static void destroy_rule(void *user_data)
-{
-	struct hwsim_rule *rule = user_data;
-
-	if (rule->prefix)
-		l_free(rule->prefix);
-
-	if (rule->match)
-		l_free(rule->match);
-
-	l_free(rule);
-}
-
-static struct l_dbus_message *rule_remove(struct l_dbus *dbus,
-						struct l_dbus_message *message,
-						void *user_data)
-{
-	struct hwsim_rule *rule = user_data;
-	const char *path;
-
-	path = rule_get_path(rule);
-	l_queue_remove(rules, rule);
-
-	destroy_rule(rule);
-
-	l_dbus_unregister_object(dbus, path);
-
-	return l_dbus_message_new_method_return(message);
-}
-
-static bool rule_property_get_source(struct l_dbus *dbus,
-					struct l_dbus_message *message,
-					struct l_dbus_message_builder *builder,
-					void *user_data)
-{
-	struct hwsim_rule *rule = user_data;
-	const char *str;
-
-	if (rule->source_any)
-		str = "any";
-	else
-		str = util_address_to_string(rule->source);
-
-	l_dbus_message_builder_append_basic(builder, 's', str);
-
-	return true;
-}
-
-static struct l_dbus_message *rule_property_set_source(struct l_dbus *dbus,
-					struct l_dbus_message *message,
-					struct l_dbus_message_iter *new_value,
-					l_dbus_property_complete_cb_t complete,
-					void *user_data)
-{
-	struct hwsim_rule *rule = user_data;
-	const char *str;
-
-	if (!l_dbus_message_iter_get_variant(new_value, "s", &str))
-		return dbus_error_invalid_args(message);
-
-	if (!strcmp(str, "any"))
-		rule->source_any = true;
-	else {
-		if (!util_string_to_address(str, rule->source))
-			return dbus_error_invalid_args(message);
-
-		rule->source_any = false;
-	}
-
-	return l_dbus_message_new_method_return(message);
-}
-
-static bool rule_property_get_destination(struct l_dbus *dbus,
-					struct l_dbus_message *message,
-					struct l_dbus_message_builder *builder,
-					void *user_data)
-{
-	struct hwsim_rule *rule = user_data;
-	const char *str;
-
-	if (rule->destination_any)
-		str = "any";
-	else if (util_is_broadcast_address(rule->destination))
-		str = "multicast";
-	else
-		str = util_address_to_string(rule->destination);
-
-	l_dbus_message_builder_append_basic(builder, 's', str);
-
-	return true;
-}
-
-static struct l_dbus_message *rule_property_set_destination(
-					struct l_dbus *dbus,
-					struct l_dbus_message *message,
-					struct l_dbus_message_iter *new_value,
-					l_dbus_property_complete_cb_t complete,
-					void *user_data)
-{
-	struct hwsim_rule *rule = user_data;
-	const char *str;
-
-	if (!l_dbus_message_iter_get_variant(new_value, "s", &str))
-		return dbus_error_invalid_args(message);
-
-	if (!strcmp(str, "any"))
-		rule->destination_any = true;
-	else if (!strcmp(str, "multicast")) {
-		rule->destination[0] = 0x80;
-		rule->destination_any = false;
-	} else {
-		if (!util_string_to_address(str, rule->destination))
-			return dbus_error_invalid_args(message);
-
-		rule->destination_any = false;
-	}
-
-	return l_dbus_message_new_method_return(message);
-}
-
-static bool rule_property_get_bidirectional(struct l_dbus *dbus,
-					struct l_dbus_message *message,
-					struct l_dbus_message_builder *builder,
-					void *user_data)
-{
-	struct hwsim_rule *rule = user_data;
-	bool bval = rule->bidirectional;
-
-	l_dbus_message_builder_append_basic(builder, 'b', &bval);
-
-	return true;
-}
-
-static struct l_dbus_message *rule_property_set_bidirectional(
-					struct l_dbus *dbus,
-					struct l_dbus_message *message,
-					struct l_dbus_message_iter *new_value,
-					l_dbus_property_complete_cb_t complete,
-					void *user_data)
-{
-	struct hwsim_rule *rule = user_data;
-	bool bval;
-
-	if (!l_dbus_message_iter_get_variant(new_value, "b", &bval))
-		return dbus_error_invalid_args(message);
-
-	rule->bidirectional = bval;
-
-	return l_dbus_message_new_method_return(message);
-}
-
-static bool rule_property_get_frequency(struct l_dbus *dbus,
-					struct l_dbus_message *message,
-					struct l_dbus_message_builder *builder,
-					void *user_data)
-{
-	struct hwsim_rule *rule = user_data;
-
-	l_dbus_message_builder_append_basic(builder, 'u', &rule->frequency);
-
-	return true;
-}
-
-static struct l_dbus_message *rule_property_set_frequency(struct l_dbus *dbus,
-					struct l_dbus_message *message,
-					struct l_dbus_message_iter *new_value,
-					l_dbus_property_complete_cb_t complete,
-					void *user_data)
-{
-	struct hwsim_rule *rule = user_data;
-
-	if (!l_dbus_message_iter_get_variant(new_value, "u", &rule->frequency))
-		return dbus_error_invalid_args(message);
-
-	return l_dbus_message_new_method_return(message);
-}
-
-static bool rule_property_get_priority(struct l_dbus *dbus,
-					struct l_dbus_message *message,
-					struct l_dbus_message_builder *builder,
-					void *user_data)
-{
-	struct hwsim_rule *rule = user_data;
-	int16_t intval = rule->priority;
-
-	l_dbus_message_builder_append_basic(builder, 'n', &intval);
-
-	return true;
-}
-
-static struct l_dbus_message *rule_property_set_priority(struct l_dbus *dbus,
-					struct l_dbus_message *message,
-					struct l_dbus_message_iter *new_value,
-					l_dbus_property_complete_cb_t complete,
-					void *user_data)
-{
-	struct hwsim_rule *rule = user_data;
-	int16_t intval;
-
-	if (!l_dbus_message_iter_get_variant(new_value, "n", &intval))
-		return dbus_error_invalid_args(message);
-
-	rule->priority = intval;
-	l_queue_remove(rules, rule);
-	l_queue_insert(rules, rule, rule_compare_priority, NULL);
-
-	return l_dbus_message_new_method_return(message);
-}
-
-static bool rule_property_get_signal(struct l_dbus *dbus,
-					struct l_dbus_message *message,
-					struct l_dbus_message_builder *builder,
-					void *user_data)
-{
-	struct hwsim_rule *rule = user_data;
-	int16_t intval = rule->signal;
-
-	l_dbus_message_builder_append_basic(builder, 'n', &intval);
-
-	return true;
-}
-
-static struct l_dbus_message *rule_property_set_signal(struct l_dbus *dbus,
-					struct l_dbus_message *message,
-					struct l_dbus_message_iter *new_value,
-					l_dbus_property_complete_cb_t complete,
-					void *user_data)
-{
-	struct hwsim_rule *rule = user_data;
-	int16_t intval;
-
-	if (!l_dbus_message_iter_get_variant(new_value, "n", &intval) ||
-			intval > 0 || intval < -10000)
-		return dbus_error_invalid_args(message);
-
-	rule->signal = intval;
-
-	return l_dbus_message_new_method_return(message);
-}
-
-static bool rule_property_get_drop(struct l_dbus *dbus,
-					struct l_dbus_message *message,
-					struct l_dbus_message_builder *builder,
-					void *user_data)
-{
-	struct hwsim_rule *rule = user_data;
-	bool bval = rule->drop;
-
-	l_dbus_message_builder_append_basic(builder, 'b', &bval);
-
-	return true;
-}
-
-static struct l_dbus_message *rule_property_set_drop(
-					struct l_dbus *dbus,
-					struct l_dbus_message *message,
-					struct l_dbus_message_iter *new_value,
-					l_dbus_property_complete_cb_t complete,
-					void *user_data)
-{
-	struct hwsim_rule *rule = user_data;
-	bool bval;
-
-	if (!l_dbus_message_iter_get_variant(new_value, "b", &bval))
-		return dbus_error_invalid_args(message);
-
-	rule->drop = bval;
-
-	return l_dbus_message_new_method_return(message);
-}
-
-static bool rule_property_get_delay(struct l_dbus *dbus,
-					struct l_dbus_message *message,
-					struct l_dbus_message_builder *builder,
-					void *user_data)
-{
-	struct hwsim_rule *rule = user_data;
-
-	l_dbus_message_builder_append_basic(builder, 'u', &rule->delay);
-
-	return true;
-}
-
-static struct l_dbus_message *rule_property_set_delay(
-					struct l_dbus *dbus,
-					struct l_dbus_message *message,
-					struct l_dbus_message_iter *new_value,
-					l_dbus_property_complete_cb_t complete,
-					void *user_data)
-{
-	struct hwsim_rule *rule = user_data;
-	uint32_t val;
-
-	if (!l_dbus_message_iter_get_variant(new_value, "u", &val) ||
-				val < HWSIM_DELAY_MIN_MS)
-		return dbus_error_invalid_args(message);
-
-	rule->delay = val;
-
-	return l_dbus_message_new_method_return(message);
-}
-
-static bool rule_property_get_prefix(struct l_dbus *dbus,
-					struct l_dbus_message *message,
-					struct l_dbus_message_builder *builder,
-					void *user_data)
-{
-	struct hwsim_rule *rule = user_data;
-	size_t i;
-
-	l_dbus_message_builder_enter_array(builder, "y");
-
-	for (i = 0; i < rule->prefix_len; i++)
-		l_dbus_message_builder_append_basic(builder, 'y',
-							rule->prefix + i);
-
-	l_dbus_message_builder_leave_array(builder);
-
-	return true;
-}
-
-static struct l_dbus_message *rule_property_set_prefix(
-					struct l_dbus *dbus,
-					struct l_dbus_message *message,
-					struct l_dbus_message_iter *new_value,
-					l_dbus_property_complete_cb_t complete,
-					void *user_data)
-{
-	struct hwsim_rule *rule = user_data;
-	struct l_dbus_message_iter iter;
-	const uint8_t *prefix;
-	uint32_t len;
-
-	if (!l_dbus_message_iter_get_variant(new_value, "ay", &iter))
-		goto invalid_args;
-
-	if (!l_dbus_message_iter_get_fixed_array(&iter,
-						(const void **)&prefix, &len))
-		goto invalid_args;
-
-	if (len > HWSIM_MAX_PREFIX_LEN)
-		goto invalid_args;
-
-	if (rule->prefix)
-		l_free(rule->prefix);
-
-	rule->prefix = l_memdup(prefix, len);
-	rule->prefix_len = len;
-
-	return l_dbus_message_new_method_return(message);
-
-invalid_args:
-	return dbus_error_invalid_args(message);
-}
-
-static bool rule_property_get_match(struct l_dbus *dbus,
-					struct l_dbus_message *message,
-					struct l_dbus_message_builder *builder,
-					void *user_data)
-{
-	struct hwsim_rule *rule = user_data;
-	size_t i;
-
-	l_dbus_message_builder_enter_array(builder, "y");
-
-	for (i = 0; i < rule->match_len; i++)
-		l_dbus_message_builder_append_basic(builder, 'y',
-							rule->match + i);
-
-	l_dbus_message_builder_leave_array(builder);
-
-	return true;
-}
-
-static struct l_dbus_message *rule_property_set_match(
-					struct l_dbus *dbus,
-					struct l_dbus_message *message,
-					struct l_dbus_message_iter *new_value,
-					l_dbus_property_complete_cb_t complete,
-					void *user_data)
-{
-	struct hwsim_rule *rule = user_data;
-	struct l_dbus_message_iter iter;
-	const uint8_t *match;
-	uint32_t len;
-
-	if (!l_dbus_message_iter_get_variant(new_value, "ay", &iter))
-		goto invalid_args;
-
-	if (!l_dbus_message_iter_get_fixed_array(&iter,
-						(const void **)&match, &len))
-		goto invalid_args;
-
-	if (len > HWSIM_MAX_PREFIX_LEN)
-		goto invalid_args;
-
-	if (rule->match)
-		l_free(rule->match);
-
-	rule->match = l_memdup(match, len);
-	rule->match_len = len;
-
-	return l_dbus_message_new_method_return(message);
-
-invalid_args:
-	return dbus_error_invalid_args(message);
-}
-
-static bool rule_property_get_match_offset(struct l_dbus *dbus,
-					struct l_dbus_message *message,
-					struct l_dbus_message_builder *builder,
-					void *user_data)
-{
-	struct hwsim_rule *rule = user_data;
-	uint16_t val = rule->match_offset;
-
-	l_dbus_message_builder_append_basic(builder, 'q', &val);
-
-	return true;
-}
-
-static struct l_dbus_message *rule_property_set_match_offset(
-					struct l_dbus *dbus,
-					struct l_dbus_message *message,
-					struct l_dbus_message_iter *new_value,
-					l_dbus_property_complete_cb_t complete,
-					void *user_data)
-{
-	struct hwsim_rule *rule = user_data;
-	uint16_t val;
-
-	if (!l_dbus_message_iter_get_variant(new_value, "q", &val))
-		return dbus_error_invalid_args(message);
-
-	rule->match_offset = val;
-
-	return l_dbus_message_new_method_return(message);
-}
-
-static bool rule_property_get_enabled(struct l_dbus *dbus,
-					struct l_dbus_message *message,
-					struct l_dbus_message_builder *builder,
-					void *user_data)
-{
-	struct hwsim_rule *rule = user_data;
-	bool bval = rule->enabled;
-
-	l_dbus_message_builder_append_basic(builder, 'b', &bval);
-
-	return true;
-}
-
-static struct l_dbus_message *rule_property_set_enabled(
-					struct l_dbus *dbus,
-					struct l_dbus_message *message,
-					struct l_dbus_message_iter *new_value,
-					l_dbus_property_complete_cb_t complete,
-					void *user_data)
-{
-	struct hwsim_rule *rule = user_data;
-	bool bval;
-
-	if (!l_dbus_message_iter_get_variant(new_value, "b", &bval))
-		return dbus_error_invalid_args(message);
-
-	rule->enabled = bval;
-
-	return l_dbus_message_new_method_return(message);
-}
-
-static bool rule_property_get_match_times(struct l_dbus *dbus,
-					struct l_dbus_message *message,
-					struct l_dbus_message_builder *builder,
-					void *user_data)
-{
-	struct hwsim_rule *rule = user_data;
-	uint16_t val = rule->match_times;
-
-	l_dbus_message_builder_append_basic(builder, 'q', &val);
-
-	return true;
-}
-
-static struct l_dbus_message *rule_property_set_match_times(
-					struct l_dbus *dbus,
-					struct l_dbus_message *message,
-					struct l_dbus_message_iter *new_value,
-					l_dbus_property_complete_cb_t complete,
-					void *user_data)
-{
-	struct hwsim_rule *rule = user_data;
-	uint16_t val;
-
-	if (!l_dbus_message_iter_get_variant(new_value, "q", &val))
-		return dbus_error_invalid_args(message);
-
-	rule->match_times = val;
-
-	return l_dbus_message_new_method_return(message);
-}
-
-static bool rule_property_get_drop_ack(struct l_dbus *dbus,
-					struct l_dbus_message *message,
-					struct l_dbus_message_builder *builder,
-					void *user_data)
-{
-	struct hwsim_rule *rule = user_data;
-	bool bval = rule->drop_ack;
-
-	l_dbus_message_builder_append_basic(builder, 'b', &bval);
-
-	return true;
-}
-
-static struct l_dbus_message *rule_property_set_drop_ack(
-					struct l_dbus *dbus,
-					struct l_dbus_message *message,
-					struct l_dbus_message_iter *new_value,
-					l_dbus_property_complete_cb_t complete,
-					void *user_data)
-{
-	struct hwsim_rule *rule = user_data;
-	bool bval;
-
-	if (!l_dbus_message_iter_get_variant(new_value, "b", &bval))
-		return dbus_error_invalid_args(message);
-
-	rule->drop_ack = bval;
-
-	return l_dbus_message_new_method_return(message);
-}
-
-static void setup_rule_interface(struct l_dbus_interface *interface)
-{
-	l_dbus_interface_method(interface, "Remove", 0, rule_remove, "", "");
-
-	l_dbus_interface_property(interface, "Source",
-					L_DBUS_PROPERTY_FLAG_AUTO_EMIT, "s",
-					rule_property_get_source,
-					rule_property_set_source);
-	l_dbus_interface_property(interface, "Destination",
-					L_DBUS_PROPERTY_FLAG_AUTO_EMIT, "s",
-					rule_property_get_destination,
-					rule_property_set_destination);
-	l_dbus_interface_property(interface, "Bidirectional",
-					L_DBUS_PROPERTY_FLAG_AUTO_EMIT, "b",
-					rule_property_get_bidirectional,
-					rule_property_set_bidirectional);
-	l_dbus_interface_property(interface, "Frequency",
-					L_DBUS_PROPERTY_FLAG_AUTO_EMIT, "u",
-					rule_property_get_frequency,
-					rule_property_set_frequency);
-	l_dbus_interface_property(interface, "Priority",
-					L_DBUS_PROPERTY_FLAG_AUTO_EMIT, "n",
-					rule_property_get_priority,
-					rule_property_set_priority);
-	l_dbus_interface_property(interface, "SignalStrength",
-					L_DBUS_PROPERTY_FLAG_AUTO_EMIT, "n",
-					rule_property_get_signal,
-					rule_property_set_signal);
-	l_dbus_interface_property(interface, "Drop",
-					L_DBUS_PROPERTY_FLAG_AUTO_EMIT, "b",
-					rule_property_get_drop,
-					rule_property_set_drop);
-	l_dbus_interface_property(interface, "Delay",
-					L_DBUS_PROPERTY_FLAG_AUTO_EMIT, "u",
-					rule_property_get_delay,
-					rule_property_set_delay);
-	l_dbus_interface_property(interface, "Prefix",
-					L_DBUS_PROPERTY_FLAG_AUTO_EMIT, "ay",
-					rule_property_get_prefix,
-					rule_property_set_prefix);
-	l_dbus_interface_property(interface, "MatchBytes",
-					L_DBUS_PROPERTY_FLAG_AUTO_EMIT, "ay",
-					rule_property_get_match,
-					rule_property_set_match);
-	l_dbus_interface_property(interface, "MatchBytesOffset",
-					L_DBUS_PROPERTY_FLAG_AUTO_EMIT, "q",
-					rule_property_get_match_offset,
-					rule_property_set_match_offset);
-	l_dbus_interface_property(interface, "Enabled",
-					L_DBUS_PROPERTY_FLAG_AUTO_EMIT, "b",
-					rule_property_get_enabled,
-					rule_property_set_enabled);
-	l_dbus_interface_property(interface, "MatchTimes",
-					L_DBUS_PROPERTY_FLAG_AUTO_EMIT, "q",
-					rule_property_get_match_times,
-					rule_property_set_match_times);
-	l_dbus_interface_property(interface, "DropAck",
-					L_DBUS_PROPERTY_FLAG_AUTO_EMIT, "b",
-					rule_property_get_drop_ack,
-					rule_property_set_drop_ack);
-}
-
 static void request_name_callback(struct l_dbus *dbus, bool success,
 					bool queued, void *user_data)
 {
@@ -2839,6 +1862,15 @@ static void disconnect_callback(void *user_data)
 	l_main_quit();
 }
 
+const struct l_queue_entry *hwsim_get_radios(void)
+{
+	return l_queue_get_entries(radio_info);
+}
+const struct l_queue_entry *hwsim_get_interfaces(void)
+{
+	return l_queue_get_entries(interface_info);
+}
+
 static bool setup_dbus_hwsim(void)
 {
 	dbus = l_dbus_new_default(L_DBUS_SYSTEM_BUS);
@@ -2872,21 +1904,6 @@ static bool setup_dbus_hwsim(void)
 		return false;
 	}
 
-	if (!l_dbus_register_interface(dbus, HWSIM_RULE_MANAGER_INTERFACE,
-					setup_rule_manager_interface,
-					NULL, false)) {
-		l_error("Unable to register the %s interface",
-			HWSIM_RULE_MANAGER_INTERFACE);
-		return false;
-	}
-
-	if (!l_dbus_register_interface(dbus, HWSIM_RULE_INTERFACE,
-					setup_rule_interface, NULL, false)) {
-		l_error("Unable to register the %s interface",
-			HWSIM_RULE_INTERFACE);
-		return false;
-	}
-
 	if (!l_dbus_object_add_interface(dbus, "/",
 						HWSIM_RADIO_MANAGER_INTERFACE,
 						NULL)) {
@@ -2895,14 +1912,6 @@ static bool setup_dbus_hwsim(void)
 		return false;
 	}
 
-	if (!l_dbus_object_add_interface(dbus, "/",
-						HWSIM_RULE_MANAGER_INTERFACE,
-						NULL)) {
-		l_info("Unable to add the %s interface to /",
-			HWSIM_RULE_MANAGER_INTERFACE);
-		return false;
-	}
-
 	l_dbus_set_ready_handler(dbus, ready_callback, dbus, NULL);
 	l_dbus_set_disconnect_handler(dbus, disconnect_callback, NULL, NULL);
 
@@ -2983,6 +1992,7 @@ static void hwsim_ready(void)
 	struct l_genl_msg *msg;
 	size_t msg_size;
 	uint32_t radio_id;
+	uint8_t addr[6];
 
 	switch (action) {
 	case ACTION_LIST:
@@ -3039,6 +2049,10 @@ static void hwsim_ready(void)
 					sizeof(uint32_t) * hwsim_num_ciphers,
 					hwsim_ciphers);
 
+		addr[0] = 0x02;
+		l_getrandom(addr + 1, 5);
+		l_genl_msg_append_attr(msg, HWSIM_ATTR_PERM_ADDR, 6, addr);
+
 		l_genl_family_send(hwsim, msg, create_callback, NULL, NULL);
 
 		break;
@@ -3056,6 +2070,9 @@ static void hwsim_ready(void)
 		if (!setup_dbus_hwsim())
 			goto error;
 
+		if (iwd_modules_init())
+			goto error;
+
 		if (!l_genl_family_register(hwsim, "config", hwsim_config,
 						NULL, NULL)) {
 			l_error("Failed to create hwsim config listener\n");
@@ -3094,7 +2111,6 @@ static void family_discovered(const struct l_genl_family_info *info,
 	else if (!strcmp(l_genl_family_info_get_name(info), NL80211_GENL_NAME))
 		nl80211 = l_genl_family_new(genl, NL80211_GENL_NAME);
 }
-
 static void discovery_done(void *user_data)
 {
 	if (!hwsim) {
@@ -3265,15 +2281,20 @@ int main(int argc, char *argv[])
 		goto done;
 	}
 
+	watchlist_init(&hwsim_watch, NULL);
+
 	exit_status = l_main_run_with_signal(signal_handler, NULL);
 
+	watchlist_destroy(&hwsim_watch);
+
+	iwd_modules_exit();
+
 	l_genl_family_free(hwsim);
 	l_genl_family_free(nl80211);
 	l_genl_unref(genl);
 
 	l_dbus_destroy(dbus);
 	hwsim_radio_cache_cleanup();
-	l_queue_destroy(rules, destroy_rule);
 
 	l_netlink_destroy(rtnl);
 
diff --git a/tools/hwsim.h b/tools/hwsim.h
new file mode 100644
index 00000000..480270b9
--- /dev/null
+++ b/tools/hwsim.h
@@ -0,0 +1,99 @@
+/*
+ *
+ *  Wireless daemon for Linux
+ *
+ *  Copyright (C) 2023  Intel Corporation. All rights reserved.
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU Lesser General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.1 of the License, or (at your option) any later version.
+ *
+ *  This library is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *  Lesser General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Lesser General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+struct l_genl_msg;
+struct l_dbus;
+
+#define HWSIM_SERVICE "net.connman.hwsim"
+
+enum hwsim_tx_control_flags {
+	HWSIM_TX_CTL_REQ_TX_STATUS		= 1 << 0,
+	HWSIM_TX_CTL_NO_ACK			= 1 << 1,
+	HWSIM_TX_STAT_ACK			= 1 << 2,
+};
+
+struct radio_info_rec {
+	int32_t id;
+	uint32_t wiphy_id;
+	char alpha2[2];
+	bool p2p;
+	bool custom_regdom;
+	uint32_t regdom_idx;
+	int channels;
+	uint8_t addrs[2][ETH_ALEN];
+	char *name;
+	bool ap_only;
+	struct l_dbus_message *pending;
+	uint32_t cmd_id;
+};
+
+struct interface_info_rec {
+	uint32_t id;
+	struct radio_info_rec *radio_rec;
+	uint8_t addr[ETH_ALEN];
+	char *name;
+	uint32_t iftype;
+	int ref;
+};
+
+struct hwsim_tx_info {
+	int8_t idx;
+	uint8_t count;
+};
+
+struct hwsim_frame {
+	int refcount;
+	uint8_t src_ether_addr[ETH_ALEN];
+	uint8_t dst_ether_addr[ETH_ALEN];
+	struct radio_info_rec *src_radio;
+	struct radio_info_rec *ack_radio;
+	uint32_t flags;
+	const uint64_t *cookie;
+	int32_t signal;
+	uint32_t frequency;
+	uint16_t tx_info_len;
+	const struct hwsim_tx_info *tx_info;
+	uint16_t payload_len;
+	const uint8_t *payload;
+	bool acked;
+	struct l_genl_msg *msg;
+	int pending_callback_count;
+};
+
+typedef void (*hwsim_frame_cb_t)(struct hwsim_frame *frame, void *user_data);
+typedef void (*hwsim_destroy_cb_t)(void *user_data);
+
+unsigned int hwsim_watch_register(hwsim_frame_cb_t frame_cb, void *user_data,
+			hwsim_destroy_cb_t destroy);
+
+uint32_t hwsim_send_frame(struct hwsim_frame *frame,
+				struct radio_info_rec *radio,
+				l_genl_msg_func_t callback,
+				void *user_data,
+				l_genl_destroy_func_t destroy);
+bool hwsim_send_tx_info(struct hwsim_frame *frame);
+
+const struct l_queue_entry *hwsim_get_radios(void);
+const struct l_queue_entry *hwsim_get_interfaces(void);
+
+struct hwsim_frame *hwsim_frame_ref(struct hwsim_frame *frame);
+void hwsim_frame_unref(struct hwsim_frame *frame);
diff --git a/tools/rules.c b/tools/rules.c
new file mode 100644
index 00000000..291b2079
--- /dev/null
+++ b/tools/rules.c
@@ -0,0 +1,1017 @@
+/*
+ *
+ *  Wireless daemon for Linux
+ *
+ *  Copyright (C) 2023  Intel Corporation. All rights reserved.
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU Lesser General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.1 of the License, or (at your option) any later version.
+ *
+ *  This library is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *  Lesser General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Lesser General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <linux/if_ether.h>
+
+#include <ell/ell.h>
+#include "ell/useful.h"
+
+#include "src/module.h"
+#include "src/util.h"
+#include "src/dbus.h"
+#include "hwsim.h"
+
+#define HWSIM_MAX_PREFIX_LEN		128
+#define HWSIM_DELAY_MIN_MS		1
+
+#define HWSIM_RULE_MANAGER_INTERFACE HWSIM_SERVICE ".RuleManager"
+#define HWSIM_RULE_INTERFACE HWSIM_SERVICE ".Rule"
+
+struct hwsim_rule {
+	unsigned int id;
+	uint8_t source[ETH_ALEN];
+	uint8_t destination[ETH_ALEN];
+	bool source_any : 1;
+	bool destination_any : 1;
+	bool bidirectional : 1;
+	bool drop : 1;
+	bool drop_ack : 1;
+	bool enabled : 1;
+	uint32_t frequency;
+	int priority;
+	int signal;
+	int delay;
+	uint8_t *prefix;
+	size_t prefix_len;
+	uint8_t *match;
+	size_t match_len;
+	uint16_t match_offset;
+	int match_times; /* negative value indicates unused */
+};
+
+struct delay_frame_info {
+	struct hwsim_frame *frame;
+	struct radio_info_rec *radio;
+};
+
+static struct l_queue *rules;
+static unsigned int next_rule_id;
+
+static const char *rule_get_path(struct hwsim_rule *rule)
+{
+	static char path[16];
+
+	snprintf(path, sizeof(path), "/rule%u", rule->id);
+
+	return path;
+}
+
+static int rule_compare_priority(const void *a, const void *b, void *user)
+{
+	const struct hwsim_rule *rule_a = a;
+	const struct hwsim_rule *rule_b = b;
+
+	return (rule_a->priority > rule_b->priority) ? 1 : -1;
+}
+
+static struct l_dbus_message *rule_add(struct l_dbus *dbus,
+					struct l_dbus_message *message,
+					void *user_data)
+{
+	struct hwsim_rule *rule;
+	const char *path;
+	struct l_dbus_message *reply;
+
+	rule = l_new(struct hwsim_rule, 1);
+	rule->id = next_rule_id++;
+	rule->source_any = true;
+	rule->destination_any = true;
+	rule->delay = 0;
+	rule->enabled = false;
+	rule->match_times = -1;
+	rule->drop_ack = true;
+
+	if (!rules)
+		rules = l_queue_new();
+
+	l_queue_insert(rules, rule, rule_compare_priority, NULL);
+	path = rule_get_path(rule);
+
+	if (!l_dbus_object_add_interface(dbus, path,
+					HWSIM_RULE_INTERFACE, rule))
+		l_info("Unable to add the %s interface to %s",
+				HWSIM_RULE_INTERFACE, path);
+
+	if (!l_dbus_object_add_interface(dbus, path,
+					L_DBUS_INTERFACE_PROPERTIES, NULL))
+		l_info("Unable to add the %s interface to %s",
+				L_DBUS_INTERFACE_PROPERTIES, path);
+
+	reply = l_dbus_message_new_method_return(message);
+	l_dbus_message_set_arguments(reply, "o", path);
+
+	return reply;
+}
+
+static void setup_rule_manager_interface(struct l_dbus_interface *interface)
+{
+	l_dbus_interface_method(interface, "AddRule", 0,
+				rule_add, "o", "", "path");
+}
+
+static void destroy_rule(void *user_data)
+{
+	struct hwsim_rule *rule = user_data;
+
+	if (rule->prefix)
+		l_free(rule->prefix);
+
+	if (rule->match)
+		l_free(rule->match);
+
+	l_free(rule);
+}
+
+static struct l_dbus_message *rule_remove(struct l_dbus *dbus,
+						struct l_dbus_message *message,
+						void *user_data)
+{
+	struct hwsim_rule *rule = user_data;
+	const char *path;
+
+	path = rule_get_path(rule);
+	l_queue_remove(rules, rule);
+
+	destroy_rule(rule);
+
+	l_dbus_unregister_object(dbus, path);
+
+	return l_dbus_message_new_method_return(message);
+}
+
+static bool rule_property_get_source(struct l_dbus *dbus,
+					struct l_dbus_message *message,
+					struct l_dbus_message_builder *builder,
+					void *user_data)
+{
+	struct hwsim_rule *rule = user_data;
+	const char *str;
+
+	if (rule->source_any)
+		str = "any";
+	else
+		str = util_address_to_string(rule->source);
+
+	l_dbus_message_builder_append_basic(builder, 's', str);
+
+	return true;
+}
+
+static struct l_dbus_message *rule_property_set_source(struct l_dbus *dbus,
+					struct l_dbus_message *message,
+					struct l_dbus_message_iter *new_value,
+					l_dbus_property_complete_cb_t complete,
+					void *user_data)
+{
+	struct hwsim_rule *rule = user_data;
+	const char *str;
+
+	if (!l_dbus_message_iter_get_variant(new_value, "s", &str))
+		return dbus_error_invalid_args(message);
+
+	if (!strcmp(str, "any"))
+		rule->source_any = true;
+	else {
+		if (!util_string_to_address(str, rule->source))
+			return dbus_error_invalid_args(message);
+
+		rule->source_any = false;
+	}
+
+	return l_dbus_message_new_method_return(message);
+}
+
+static bool rule_property_get_destination(struct l_dbus *dbus,
+					struct l_dbus_message *message,
+					struct l_dbus_message_builder *builder,
+					void *user_data)
+{
+	struct hwsim_rule *rule = user_data;
+	const char *str;
+
+	if (rule->destination_any)
+		str = "any";
+	else if (util_is_broadcast_address(rule->destination))
+		str = "multicast";
+	else
+		str = util_address_to_string(rule->destination);
+
+	l_dbus_message_builder_append_basic(builder, 's', str);
+
+	return true;
+}
+
+static struct l_dbus_message *rule_property_set_destination(
+					struct l_dbus *dbus,
+					struct l_dbus_message *message,
+					struct l_dbus_message_iter *new_value,
+					l_dbus_property_complete_cb_t complete,
+					void *user_data)
+{
+	struct hwsim_rule *rule = user_data;
+	const char *str;
+
+	if (!l_dbus_message_iter_get_variant(new_value, "s", &str))
+		return dbus_error_invalid_args(message);
+
+	if (!strcmp(str, "any"))
+		rule->destination_any = true;
+	else if (!strcmp(str, "multicast")) {
+		rule->destination[0] = 0x80;
+		rule->destination_any = false;
+	} else {
+		if (!util_string_to_address(str, rule->destination))
+			return dbus_error_invalid_args(message);
+
+		rule->destination_any = false;
+	}
+
+	return l_dbus_message_new_method_return(message);
+}
+
+static bool rule_property_get_bidirectional(struct l_dbus *dbus,
+					struct l_dbus_message *message,
+					struct l_dbus_message_builder *builder,
+					void *user_data)
+{
+	struct hwsim_rule *rule = user_data;
+	bool bval = rule->bidirectional;
+
+	l_dbus_message_builder_append_basic(builder, 'b', &bval);
+
+	return true;
+}
+
+static struct l_dbus_message *rule_property_set_bidirectional(
+					struct l_dbus *dbus,
+					struct l_dbus_message *message,
+					struct l_dbus_message_iter *new_value,
+					l_dbus_property_complete_cb_t complete,
+					void *user_data)
+{
+	struct hwsim_rule *rule = user_data;
+	bool bval;
+
+	if (!l_dbus_message_iter_get_variant(new_value, "b", &bval))
+		return dbus_error_invalid_args(message);
+
+	rule->bidirectional = bval;
+
+	return l_dbus_message_new_method_return(message);
+}
+
+static bool rule_property_get_frequency(struct l_dbus *dbus,
+					struct l_dbus_message *message,
+					struct l_dbus_message_builder *builder,
+					void *user_data)
+{
+	struct hwsim_rule *rule = user_data;
+
+	l_dbus_message_builder_append_basic(builder, 'u', &rule->frequency);
+
+	return true;
+}
+
+static struct l_dbus_message *rule_property_set_frequency(struct l_dbus *dbus,
+					struct l_dbus_message *message,
+					struct l_dbus_message_iter *new_value,
+					l_dbus_property_complete_cb_t complete,
+					void *user_data)
+{
+	struct hwsim_rule *rule = user_data;
+
+	if (!l_dbus_message_iter_get_variant(new_value, "u", &rule->frequency))
+		return dbus_error_invalid_args(message);
+
+	return l_dbus_message_new_method_return(message);
+}
+
+static bool rule_property_get_priority(struct l_dbus *dbus,
+					struct l_dbus_message *message,
+					struct l_dbus_message_builder *builder,
+					void *user_data)
+{
+	struct hwsim_rule *rule = user_data;
+	int16_t intval = rule->priority;
+
+	l_dbus_message_builder_append_basic(builder, 'n', &intval);
+
+	return true;
+}
+
+static struct l_dbus_message *rule_property_set_priority(struct l_dbus *dbus,
+					struct l_dbus_message *message,
+					struct l_dbus_message_iter *new_value,
+					l_dbus_property_complete_cb_t complete,
+					void *user_data)
+{
+	struct hwsim_rule *rule = user_data;
+	int16_t intval;
+
+	if (!l_dbus_message_iter_get_variant(new_value, "n", &intval))
+		return dbus_error_invalid_args(message);
+
+	rule->priority = intval;
+	l_queue_remove(rules, rule);
+	l_queue_insert(rules, rule, rule_compare_priority, NULL);
+
+	return l_dbus_message_new_method_return(message);
+}
+
+static bool rule_property_get_signal(struct l_dbus *dbus,
+					struct l_dbus_message *message,
+					struct l_dbus_message_builder *builder,
+					void *user_data)
+{
+	struct hwsim_rule *rule = user_data;
+	int16_t intval = rule->signal;
+
+	l_dbus_message_builder_append_basic(builder, 'n', &intval);
+
+	return true;
+}
+
+static struct l_dbus_message *rule_property_set_signal(struct l_dbus *dbus,
+					struct l_dbus_message *message,
+					struct l_dbus_message_iter *new_value,
+					l_dbus_property_complete_cb_t complete,
+					void *user_data)
+{
+	struct hwsim_rule *rule = user_data;
+	int16_t intval;
+
+	if (!l_dbus_message_iter_get_variant(new_value, "n", &intval) ||
+			intval > 0 || intval < -10000)
+		return dbus_error_invalid_args(message);
+
+	rule->signal = intval;
+
+	return l_dbus_message_new_method_return(message);
+}
+
+static bool rule_property_get_drop(struct l_dbus *dbus,
+					struct l_dbus_message *message,
+					struct l_dbus_message_builder *builder,
+					void *user_data)
+{
+	struct hwsim_rule *rule = user_data;
+	bool bval = rule->drop;
+
+	l_dbus_message_builder_append_basic(builder, 'b', &bval);
+
+	return true;
+}
+
+static struct l_dbus_message *rule_property_set_drop(
+					struct l_dbus *dbus,
+					struct l_dbus_message *message,
+					struct l_dbus_message_iter *new_value,
+					l_dbus_property_complete_cb_t complete,
+					void *user_data)
+{
+	struct hwsim_rule *rule = user_data;
+	bool bval;
+
+	if (!l_dbus_message_iter_get_variant(new_value, "b", &bval))
+		return dbus_error_invalid_args(message);
+
+	rule->drop = bval;
+
+	return l_dbus_message_new_method_return(message);
+}
+
+static bool rule_property_get_delay(struct l_dbus *dbus,
+					struct l_dbus_message *message,
+					struct l_dbus_message_builder *builder,
+					void *user_data)
+{
+	struct hwsim_rule *rule = user_data;
+
+	l_dbus_message_builder_append_basic(builder, 'u', &rule->delay);
+
+	return true;
+}
+
+static struct l_dbus_message *rule_property_set_delay(
+					struct l_dbus *dbus,
+					struct l_dbus_message *message,
+					struct l_dbus_message_iter *new_value,
+					l_dbus_property_complete_cb_t complete,
+					void *user_data)
+{
+	struct hwsim_rule *rule = user_data;
+	uint32_t val;
+
+	if (!l_dbus_message_iter_get_variant(new_value, "u", &val) ||
+				val < HWSIM_DELAY_MIN_MS)
+		return dbus_error_invalid_args(message);
+
+	rule->delay = val;
+
+	return l_dbus_message_new_method_return(message);
+}
+
+static bool rule_property_get_prefix(struct l_dbus *dbus,
+					struct l_dbus_message *message,
+					struct l_dbus_message_builder *builder,
+					void *user_data)
+{
+	struct hwsim_rule *rule = user_data;
+	size_t i;
+
+	l_dbus_message_builder_enter_array(builder, "y");
+
+	for (i = 0; i < rule->prefix_len; i++)
+		l_dbus_message_builder_append_basic(builder, 'y',
+							rule->prefix + i);
+
+	l_dbus_message_builder_leave_array(builder);
+
+	return true;
+}
+
+static struct l_dbus_message *rule_property_set_prefix(
+					struct l_dbus *dbus,
+					struct l_dbus_message *message,
+					struct l_dbus_message_iter *new_value,
+					l_dbus_property_complete_cb_t complete,
+					void *user_data)
+{
+	struct hwsim_rule *rule = user_data;
+	struct l_dbus_message_iter iter;
+	const uint8_t *prefix;
+	uint32_t len;
+
+	if (!l_dbus_message_iter_get_variant(new_value, "ay", &iter))
+		goto invalid_args;
+
+	if (!l_dbus_message_iter_get_fixed_array(&iter,
+						(const void **)&prefix, &len))
+		goto invalid_args;
+
+	if (len > HWSIM_MAX_PREFIX_LEN)
+		goto invalid_args;
+
+	if (rule->prefix)
+		l_free(rule->prefix);
+
+	rule->prefix = l_memdup(prefix, len);
+	rule->prefix_len = len;
+
+	return l_dbus_message_new_method_return(message);
+
+invalid_args:
+	return dbus_error_invalid_args(message);
+}
+
+static bool rule_property_get_match(struct l_dbus *dbus,
+					struct l_dbus_message *message,
+					struct l_dbus_message_builder *builder,
+					void *user_data)
+{
+	struct hwsim_rule *rule = user_data;
+	size_t i;
+
+	l_dbus_message_builder_enter_array(builder, "y");
+
+	for (i = 0; i < rule->match_len; i++)
+		l_dbus_message_builder_append_basic(builder, 'y',
+							rule->match + i);
+
+	l_dbus_message_builder_leave_array(builder);
+
+	return true;
+}
+
+static struct l_dbus_message *rule_property_set_match(
+					struct l_dbus *dbus,
+					struct l_dbus_message *message,
+					struct l_dbus_message_iter *new_value,
+					l_dbus_property_complete_cb_t complete,
+					void *user_data)
+{
+	struct hwsim_rule *rule = user_data;
+	struct l_dbus_message_iter iter;
+	const uint8_t *match;
+	uint32_t len;
+
+	if (!l_dbus_message_iter_get_variant(new_value, "ay", &iter))
+		goto invalid_args;
+
+	if (!l_dbus_message_iter_get_fixed_array(&iter,
+						(const void **)&match, &len))
+		goto invalid_args;
+
+	if (len > HWSIM_MAX_PREFIX_LEN)
+		goto invalid_args;
+
+	if (rule->match)
+		l_free(rule->match);
+
+	rule->match = l_memdup(match, len);
+	rule->match_len = len;
+
+	return l_dbus_message_new_method_return(message);
+
+invalid_args:
+	return dbus_error_invalid_args(message);
+}
+
+static bool rule_property_get_match_offset(struct l_dbus *dbus,
+					struct l_dbus_message *message,
+					struct l_dbus_message_builder *builder,
+					void *user_data)
+{
+	struct hwsim_rule *rule = user_data;
+	uint16_t val = rule->match_offset;
+
+	l_dbus_message_builder_append_basic(builder, 'q', &val);
+
+	return true;
+}
+
+static struct l_dbus_message *rule_property_set_match_offset(
+					struct l_dbus *dbus,
+					struct l_dbus_message *message,
+					struct l_dbus_message_iter *new_value,
+					l_dbus_property_complete_cb_t complete,
+					void *user_data)
+{
+	struct hwsim_rule *rule = user_data;
+	uint16_t val;
+
+	if (!l_dbus_message_iter_get_variant(new_value, "q", &val))
+		return dbus_error_invalid_args(message);
+
+	rule->match_offset = val;
+
+	return l_dbus_message_new_method_return(message);
+}
+
+static bool rule_property_get_enabled(struct l_dbus *dbus,
+					struct l_dbus_message *message,
+					struct l_dbus_message_builder *builder,
+					void *user_data)
+{
+	struct hwsim_rule *rule = user_data;
+	bool bval = rule->enabled;
+
+	l_dbus_message_builder_append_basic(builder, 'b', &bval);
+
+	return true;
+}
+
+static struct l_dbus_message *rule_property_set_enabled(
+					struct l_dbus *dbus,
+					struct l_dbus_message *message,
+					struct l_dbus_message_iter *new_value,
+					l_dbus_property_complete_cb_t complete,
+					void *user_data)
+{
+	struct hwsim_rule *rule = user_data;
+	bool bval;
+
+	if (!l_dbus_message_iter_get_variant(new_value, "b", &bval))
+		return dbus_error_invalid_args(message);
+
+	rule->enabled = bval;
+
+	return l_dbus_message_new_method_return(message);
+}
+
+static bool rule_property_get_match_times(struct l_dbus *dbus,
+					struct l_dbus_message *message,
+					struct l_dbus_message_builder *builder,
+					void *user_data)
+{
+	struct hwsim_rule *rule = user_data;
+	uint16_t val = rule->match_times;
+
+	l_dbus_message_builder_append_basic(builder, 'q', &val);
+
+	return true;
+}
+
+static struct l_dbus_message *rule_property_set_match_times(
+					struct l_dbus *dbus,
+					struct l_dbus_message *message,
+					struct l_dbus_message_iter *new_value,
+					l_dbus_property_complete_cb_t complete,
+					void *user_data)
+{
+	struct hwsim_rule *rule = user_data;
+	uint16_t val;
+
+	if (!l_dbus_message_iter_get_variant(new_value, "q", &val))
+		return dbus_error_invalid_args(message);
+
+	rule->match_times = val;
+
+	return l_dbus_message_new_method_return(message);
+}
+
+static bool rule_property_get_drop_ack(struct l_dbus *dbus,
+					struct l_dbus_message *message,
+					struct l_dbus_message_builder *builder,
+					void *user_data)
+{
+	struct hwsim_rule *rule = user_data;
+	bool bval = rule->drop_ack;
+
+	l_dbus_message_builder_append_basic(builder, 'b', &bval);
+
+	return true;
+}
+
+static struct l_dbus_message *rule_property_set_drop_ack(
+					struct l_dbus *dbus,
+					struct l_dbus_message *message,
+					struct l_dbus_message_iter *new_value,
+					l_dbus_property_complete_cb_t complete,
+					void *user_data)
+{
+	struct hwsim_rule *rule = user_data;
+	bool bval;
+
+	if (!l_dbus_message_iter_get_variant(new_value, "b", &bval))
+		return dbus_error_invalid_args(message);
+
+	rule->drop_ack = bval;
+
+	return l_dbus_message_new_method_return(message);
+}
+
+static void setup_rule_interface(struct l_dbus_interface *interface)
+{
+	l_dbus_interface_method(interface, "Remove", 0, rule_remove, "", "");
+
+	l_dbus_interface_property(interface, "Source",
+					L_DBUS_PROPERTY_FLAG_AUTO_EMIT, "s",
+					rule_property_get_source,
+					rule_property_set_source);
+	l_dbus_interface_property(interface, "Destination",
+					L_DBUS_PROPERTY_FLAG_AUTO_EMIT, "s",
+					rule_property_get_destination,
+					rule_property_set_destination);
+	l_dbus_interface_property(interface, "Bidirectional",
+					L_DBUS_PROPERTY_FLAG_AUTO_EMIT, "b",
+					rule_property_get_bidirectional,
+					rule_property_set_bidirectional);
+	l_dbus_interface_property(interface, "Frequency",
+					L_DBUS_PROPERTY_FLAG_AUTO_EMIT, "u",
+					rule_property_get_frequency,
+					rule_property_set_frequency);
+	l_dbus_interface_property(interface, "Priority",
+					L_DBUS_PROPERTY_FLAG_AUTO_EMIT, "n",
+					rule_property_get_priority,
+					rule_property_set_priority);
+	l_dbus_interface_property(interface, "SignalStrength",
+					L_DBUS_PROPERTY_FLAG_AUTO_EMIT, "n",
+					rule_property_get_signal,
+					rule_property_set_signal);
+	l_dbus_interface_property(interface, "Drop",
+					L_DBUS_PROPERTY_FLAG_AUTO_EMIT, "b",
+					rule_property_get_drop,
+					rule_property_set_drop);
+	l_dbus_interface_property(interface, "Delay",
+					L_DBUS_PROPERTY_FLAG_AUTO_EMIT, "u",
+					rule_property_get_delay,
+					rule_property_set_delay);
+	l_dbus_interface_property(interface, "Prefix",
+					L_DBUS_PROPERTY_FLAG_AUTO_EMIT, "ay",
+					rule_property_get_prefix,
+					rule_property_set_prefix);
+	l_dbus_interface_property(interface, "MatchBytes",
+					L_DBUS_PROPERTY_FLAG_AUTO_EMIT, "ay",
+					rule_property_get_match,
+					rule_property_set_match);
+	l_dbus_interface_property(interface, "MatchBytesOffset",
+					L_DBUS_PROPERTY_FLAG_AUTO_EMIT, "q",
+					rule_property_get_match_offset,
+					rule_property_set_match_offset);
+	l_dbus_interface_property(interface, "Enabled",
+					L_DBUS_PROPERTY_FLAG_AUTO_EMIT, "b",
+					rule_property_get_enabled,
+					rule_property_set_enabled);
+	l_dbus_interface_property(interface, "MatchTimes",
+					L_DBUS_PROPERTY_FLAG_AUTO_EMIT, "q",
+					rule_property_get_match_times,
+					rule_property_set_match_times);
+	l_dbus_interface_property(interface, "DropAck",
+					L_DBUS_PROPERTY_FLAG_AUTO_EMIT, "b",
+					rule_property_get_drop_ack,
+					rule_property_set_drop_ack);
+}
+
+static bool radio_match_addr(const struct radio_info_rec *radio,
+				const uint8_t *addr)
+{
+	if (!radio || util_is_broadcast_address(addr))
+		return !radio && util_is_broadcast_address(addr);
+
+	return !memcmp(addr, radio->addrs[0], ETH_ALEN) ||
+		!memcmp(addr, radio->addrs[1], ETH_ALEN);
+}
+
+static void process_rules(const struct radio_info_rec *src_radio,
+				const struct radio_info_rec *dst_radio,
+				struct hwsim_frame *frame, bool ack, bool *drop,
+				uint32_t *delay)
+{
+	const struct l_queue_entry *rule_entry;
+
+	for (rule_entry = l_queue_get_entries(rules); rule_entry;
+			rule_entry = rule_entry->next) {
+		struct hwsim_rule *rule = rule_entry->data;
+
+		if (!rule->enabled)
+			continue;
+
+		if (!rule->source_any &&
+				!radio_match_addr(src_radio, rule->source) &&
+				(!rule->bidirectional ||
+				 !radio_match_addr(dst_radio, rule->source)))
+			continue;
+
+		if (!rule->destination_any &&
+				!radio_match_addr(dst_radio,
+							rule->destination) &&
+				(!rule->bidirectional ||
+				 !radio_match_addr(src_radio,
+							rule->destination)))
+			continue;
+
+		/*
+		 * If source matches only because rule->bidirectional was
+		 * true, make sure destination is "any" or matches source
+		 * radio's address.
+		 */
+		if (!rule->source_any && rule->bidirectional &&
+				radio_match_addr(dst_radio, rule->source))
+			if (!rule->destination_any &&
+					!radio_match_addr(dst_radio,
+							rule->destination))
+				continue;
+
+		if (rule->frequency && rule->frequency != frame->frequency)
+			continue;
+
+		if (rule->prefix && frame->payload_len >= rule->prefix_len) {
+			if (memcmp(rule->prefix, frame->payload,
+					rule->prefix_len) != 0)
+				continue;
+		}
+
+		if (rule->match && frame->payload_len >=
+					rule->match_len + rule->match_offset) {
+			if (memcmp(rule->match,
+					frame->payload + rule->match_offset,
+					rule->match_len))
+				continue;
+		}
+
+		/* Rule deemed to match frame, apply any changes */
+		if (rule->match_times == 0)
+			continue;
+
+		if (rule->signal)
+			frame->signal = rule->signal / 100;
+
+		/* Don't drop if this is an ACK, unless drop_ack is set */
+		if (!ack || (ack && rule->drop_ack))
+			*drop = rule->drop;
+
+		if (delay)
+			*delay = rule->delay;
+
+		if (rule->match_times > 0)
+			rule->match_times--;
+	}
+}
+
+static void send_frame_callback(struct l_genl_msg *msg, void *user_data)
+{
+	struct delay_frame_info *info = user_data;
+
+	if (l_genl_msg_get_error(msg) == 0) {
+		info->frame->acked = true;
+		info->frame->ack_radio = info->radio;
+	}
+
+	info->frame->pending_callback_count--;
+}
+
+static void ack_frame(struct hwsim_frame *frame)
+{
+	if (!frame->pending_callback_count) {
+		/*
+		 * Apparently done with this frame, send tx info and signal
+		 * the returning of an ACK frame in the opposite direction.
+		 */
+
+		if (!(frame->flags & HWSIM_TX_CTL_NO_ACK) && frame->acked) {
+			bool drop = false;
+
+			process_rules(frame->ack_radio, frame->src_radio,
+					frame, true, &drop, NULL);
+
+			if (!drop)
+				frame->flags |= HWSIM_TX_STAT_ACK;
+		}
+
+		if (frame->src_radio)
+			hwsim_send_tx_info(frame);
+	}
+}
+
+static void send_frame_destroy(void *user_data)
+{
+	struct delay_frame_info *info = user_data;
+	struct hwsim_frame *frame = info->frame;
+
+	if (frame->refcount > 1) {
+		hwsim_frame_unref(frame);
+		l_free(info);
+
+		return;
+	}
+
+	ack_frame(frame);
+}
+
+static void frame_delay_callback(struct l_timeout *timeout, void *user_data)
+{
+	struct delay_frame_info *info = user_data;
+
+	if (hwsim_send_frame(info->frame, info->radio, send_frame_callback,
+				info, send_frame_destroy))
+		info->frame->pending_callback_count++;
+	else
+		send_frame_destroy(info);
+
+	if (timeout)
+		l_timeout_remove(timeout);
+}
+
+/*
+ * Process frames in a similar way to how the kernel built-in hwsim medium
+ * does this, with an additional optimization for unicast frames and
+ * additional modifications to frames decided by user-configurable rules.
+ */
+static void rules_process_frame(struct hwsim_frame *frame, void *user_data)
+{
+	const struct l_queue_entry *entry;
+	bool drop_mcast = false;
+	bool beacon = false;
+
+	/* Unknown source, ignore */
+	if (!frame->src_radio)
+		return;
+
+	if (util_is_broadcast_address(frame->dst_ether_addr))
+		process_rules(frame->src_radio, NULL, frame, false,
+				&drop_mcast, NULL);
+
+	if (frame->payload_len >= 2 &&
+			frame->payload[0] == 0x80 &&
+			frame->payload[1] == 0x00)
+		beacon = true;
+
+	for (entry = hwsim_get_radios(); entry; entry = entry->next) {
+		struct radio_info_rec *radio = entry->data;
+		struct delay_frame_info *info;
+		bool drop = drop_mcast;
+		uint32_t delay = 0;
+		const struct l_queue_entry *i;
+
+		if (radio == frame->src_radio)
+			continue;
+
+		/*
+		 * The kernel hwsim medium passes multicast frames to all
+		 * radios that are on the same frequency as this frame but
+		 * the netlink medium API only lets userspace pass frames to
+		 * radios by known hardware address.  It does check that the
+		 * receiving radio is on the same frequency though so we can
+		 * send to all known addresses.
+		 *
+		 * If the frame's Receiver Address (RA) is a multicast
+		 * address, then send the frame to every radio that is
+		 * registered.  If it's a unicast address then optimize
+		 * by only forwarding the frame to the radios that have
+		 * at least one interface with this specific address.
+		 */
+		if (!util_is_broadcast_address(frame->dst_ether_addr)) {
+			for (i = hwsim_get_interfaces(); i; i = i->next) {
+				struct interface_info_rec *interface = i->data;
+
+				if (interface->radio_rec != radio)
+					continue;
+
+				if (!memcmp(interface->addr,
+						frame->dst_ether_addr,
+						ETH_ALEN))
+					break;
+			}
+
+			if (!i)
+				continue;
+		}
+
+		process_rules(frame->src_radio, radio, frame, false,
+				&drop, &delay);
+
+		if (drop)
+			continue;
+
+		/*
+		 * Don't bother sending beacons to other AP interfaces
+		 * if the AP interface is the only one on this phy
+		 */
+		if (beacon && radio->ap_only)
+			continue;
+
+		info = l_new(struct delay_frame_info, 1);
+		info->radio = radio;
+		info->frame = hwsim_frame_ref(frame);
+
+		if (delay) {
+			if (!l_timeout_create_ms(delay, frame_delay_callback,
+							info, NULL)) {
+				l_error("Error delaying frame %ums, "
+						"frame will be dropped", delay);
+				hwsim_frame_unref(info->frame);
+				l_free(info);
+			}
+		} else
+			frame_delay_callback(NULL, info);
+	}
+
+	/*
+	 * If the frame was dropped to all radios we still need to signal this
+	 * information to the kernel.
+	 */
+	ack_frame(frame);
+}
+
+static int rules_init(void)
+{
+	struct l_dbus *dbus = dbus_get_bus();
+
+	l_debug("");
+
+	if (!l_dbus_register_interface(dbus, HWSIM_RULE_MANAGER_INTERFACE,
+					setup_rule_manager_interface,
+					NULL, false)) {
+		l_error("Unable to register the %s interface",
+			HWSIM_RULE_MANAGER_INTERFACE);
+		return -EINVAL;
+	}
+
+	if (!l_dbus_register_interface(dbus, HWSIM_RULE_INTERFACE,
+					setup_rule_interface, NULL, false)) {
+		l_error("Unable to register the %s interface",
+			HWSIM_RULE_INTERFACE);
+		return -EINVAL;
+	}
+
+	if (!l_dbus_object_add_interface(dbus, "/",
+						HWSIM_RULE_MANAGER_INTERFACE,
+						NULL)) {
+		l_info("Unable to add the %s interface to /",
+			HWSIM_RULE_MANAGER_INTERFACE);
+		return -EINVAL;
+	}
+
+	hwsim_watch_register(rules_process_frame, NULL, NULL);
+
+	return 0;
+}
+
+static void rules_exit(void)
+{
+	l_queue_destroy(rules, destroy_rule);
+}
+
+IWD_MODULE(rules, rules_init, rules_exit);
