Commit 0d7043f3 authored by David S. Miller's avatar David S. Miller
Browse files

Merge tag 'mac80211-next-for-net-next-2020-03-20' of...

Merge tag 'mac80211-next-for-net-next-2020-03-20' of git://git.kernel.org/pub/scm/linux/kernel/git/jberg/mac80211-next



Johannes Berg says:

====================
Another set of changes:
 * HE ranging (fine timing measurement) API support
 * hwsim gets virtio support, for use with wmediumd,
   to be able to simulate with multiple machines
 * eapol-over-nl80211 improvements to exclude preauth
 * IBSS reset support, to recover connections from
   userspace
 * and various others.
====================

Signed-off-by: default avatarDavid S. Miller <davem@davemloft.net>
parents ffe10e67 8fa180bb
Loading
Loading
Loading
Loading
+309 −18
Original line number Diff line number Diff line
@@ -4,7 +4,7 @@
 * Copyright (c) 2008, Jouni Malinen <j@w1.fi>
 * Copyright (c) 2011, Javier Lopez <jlopex@gmail.com>
 * Copyright (c) 2016 - 2017 Intel Deutschland GmbH
 * Copyright (C) 2018 Intel Corporation
 * Copyright (C) 2018 - 2020 Intel Corporation
 */

/*
@@ -33,6 +33,9 @@
#include <net/netns/generic.h>
#include <linux/rhashtable.h>
#include <linux/nospec.h>
#include <linux/virtio.h>
#include <linux/virtio_ids.h>
#include <linux/virtio_config.h>
#include "mac80211_hwsim.h"

#define WARN_QUEUE 100
@@ -613,14 +616,14 @@ static const struct genl_multicast_group hwsim_mcgrps[] = {
/* MAC80211_HWSIM netlink policy */

static const struct nla_policy hwsim_genl_policy[HWSIM_ATTR_MAX + 1] = {
	[HWSIM_ATTR_ADDR_RECEIVER] = { .type = NLA_UNSPEC, .len = ETH_ALEN },
	[HWSIM_ATTR_ADDR_TRANSMITTER] = { .type = NLA_UNSPEC, .len = ETH_ALEN },
	[HWSIM_ATTR_ADDR_RECEIVER] = NLA_POLICY_ETH_ADDR_COMPAT,
	[HWSIM_ATTR_ADDR_TRANSMITTER] = NLA_POLICY_ETH_ADDR_COMPAT,
	[HWSIM_ATTR_FRAME] = { .type = NLA_BINARY,
			       .len = IEEE80211_MAX_DATA_LEN },
	[HWSIM_ATTR_FLAGS] = { .type = NLA_U32 },
	[HWSIM_ATTR_RX_RATE] = { .type = NLA_U32 },
	[HWSIM_ATTR_SIGNAL] = { .type = NLA_U32 },
	[HWSIM_ATTR_TX_INFO] = { .type = NLA_UNSPEC,
	[HWSIM_ATTR_TX_INFO] = { .type = NLA_BINARY,
				 .len = IEEE80211_TX_MAX_RATES *
					sizeof(struct hwsim_tx_rate)},
	[HWSIM_ATTR_COOKIE] = { .type = NLA_U64 },
@@ -630,15 +633,61 @@ static const struct nla_policy hwsim_genl_policy[HWSIM_ATTR_MAX + 1] = {
	[HWSIM_ATTR_REG_CUSTOM_REG] = { .type = NLA_U32 },
	[HWSIM_ATTR_REG_STRICT_REG] = { .type = NLA_FLAG },
	[HWSIM_ATTR_SUPPORT_P2P_DEVICE] = { .type = NLA_FLAG },
	[HWSIM_ATTR_USE_CHANCTX] = { .type = NLA_FLAG },
	[HWSIM_ATTR_DESTROY_RADIO_ON_CLOSE] = { .type = NLA_FLAG },
	[HWSIM_ATTR_RADIO_NAME] = { .type = NLA_STRING },
	[HWSIM_ATTR_NO_VIF] = { .type = NLA_FLAG },
	[HWSIM_ATTR_FREQ] = { .type = NLA_U32 },
	[HWSIM_ATTR_PERM_ADDR] = { .type = NLA_UNSPEC, .len = ETH_ALEN },
	[HWSIM_ATTR_TX_INFO_FLAGS] = { .type = NLA_BINARY },
	[HWSIM_ATTR_PERM_ADDR] = NLA_POLICY_ETH_ADDR_COMPAT,
	[HWSIM_ATTR_IFTYPE_SUPPORT] = { .type = NLA_U32 },
	[HWSIM_ATTR_CIPHER_SUPPORT] = { .type = NLA_BINARY },
};

#if IS_REACHABLE(CONFIG_VIRTIO)

/* MAC80211_HWSIM virtio queues */
static struct virtqueue *hwsim_vqs[HWSIM_NUM_VQS];
static bool hwsim_virtio_enabled;
static spinlock_t hwsim_virtio_lock;

static void hwsim_virtio_rx_work(struct work_struct *work);
static DECLARE_WORK(hwsim_virtio_rx, hwsim_virtio_rx_work);

static int hwsim_tx_virtio(struct mac80211_hwsim_data *data,
			   struct sk_buff *skb)
{
	struct scatterlist sg[1];
	unsigned long flags;
	int err;

	spin_lock_irqsave(&hwsim_virtio_lock, flags);
	if (!hwsim_virtio_enabled) {
		err = -ENODEV;
		goto out_free;
	}

	sg_init_one(sg, skb->head, skb_end_offset(skb));
	err = virtqueue_add_outbuf(hwsim_vqs[HWSIM_VQ_TX], sg, 1, skb,
				   GFP_ATOMIC);
	if (err)
		goto out_free;
	virtqueue_kick(hwsim_vqs[HWSIM_VQ_TX]);
	spin_unlock_irqrestore(&hwsim_virtio_lock, flags);
	return 0;

out_free:
	spin_unlock_irqrestore(&hwsim_virtio_lock, flags);
	nlmsg_free(skb);
	return err;
}
#else
/* cause a linker error if this ends up being needed */
extern int hwsim_tx_virtio(struct mac80211_hwsim_data *data,
			   struct sk_buff *skb);
#define hwsim_virtio_enabled false
#endif

static void mac80211_hwsim_tx_frame(struct ieee80211_hw *hw,
				    struct sk_buff *skb,
				    struct ieee80211_channel *chan);
@@ -1138,8 +1187,14 @@ static void mac80211_hwsim_tx_frame_nl(struct ieee80211_hw *hw,
		goto nla_put_failure;

	genlmsg_end(skb, msg_head);

	if (hwsim_virtio_enabled) {
		if (hwsim_tx_virtio(data, skb))
			goto err_free_txskb;
	} else {
		if (hwsim_unicast_netgroup(data, skb, dst_portid))
			goto err_free_txskb;
	}

	/* Enqueue the packet */
	skb_queue_tail(&data->pending, my_skb);
@@ -1441,7 +1496,7 @@ static void mac80211_hwsim_tx(struct ieee80211_hw *hw,
	/* wmediumd mode check */
	_portid = READ_ONCE(data->wmediumd);

	if (_portid)
	if (_portid || hwsim_virtio_enabled)
		return mac80211_hwsim_tx_frame_nl(hw, skb, _portid);

	/* NO wmediumd detected, perfect medium simulation */
@@ -1547,7 +1602,7 @@ static void mac80211_hwsim_tx_frame(struct ieee80211_hw *hw,

	mac80211_hwsim_monitor_rx(hw, skb, chan);

	if (_pid)
	if (_pid || hwsim_virtio_enabled)
		return mac80211_hwsim_tx_frame_nl(hw, skb, _pid);

	mac80211_hwsim_tx_frame_no_nl(hw, skb, chan);
@@ -3293,11 +3348,14 @@ static int hwsim_tx_info_frame_received_nl(struct sk_buff *skb_2,
	if (!data2)
		goto out;

	if (hwsim_net_get_netgroup(genl_info_net(info)) != data2->netgroup)
	if (!hwsim_virtio_enabled) {
		if (hwsim_net_get_netgroup(genl_info_net(info)) !=
		    data2->netgroup)
			goto out;

		if (info->snd_portid != data2->wmediumd)
			goto out;
	}

	/* look for the skb matching the cookie passed back from user */
	skb_queue_walk_safe(&data2->pending, skb, tmp) {
@@ -3387,11 +3445,14 @@ static int hwsim_cloned_frame_received_nl(struct sk_buff *skb_2,
	if (!data2)
		goto out;

	if (hwsim_net_get_netgroup(genl_info_net(info)) != data2->netgroup)
	if (!hwsim_virtio_enabled) {
		if (hwsim_net_get_netgroup(genl_info_net(info)) !=
		    data2->netgroup)
			goto out;

		if (info->snd_portid != data2->wmediumd)
			goto out;
	}

	/* check if radio is configured properly */

@@ -3932,6 +3993,229 @@ static void hwsim_exit_netlink(void)
	genl_unregister_family(&hwsim_genl_family);
}

#if IS_REACHABLE(CONFIG_VIRTIO)
static void hwsim_virtio_tx_done(struct virtqueue *vq)
{
	unsigned int len;
	struct sk_buff *skb;
	unsigned long flags;

	spin_lock_irqsave(&hwsim_virtio_lock, flags);
	while ((skb = virtqueue_get_buf(vq, &len)))
		nlmsg_free(skb);
	spin_unlock_irqrestore(&hwsim_virtio_lock, flags);
}

static int hwsim_virtio_handle_cmd(struct sk_buff *skb)
{
	struct nlmsghdr *nlh;
	struct genlmsghdr *gnlh;
	struct nlattr *tb[HWSIM_ATTR_MAX + 1];
	struct genl_info info = {};
	int err;

	nlh = nlmsg_hdr(skb);
	gnlh = nlmsg_data(nlh);
	err = genlmsg_parse(nlh, &hwsim_genl_family, tb, HWSIM_ATTR_MAX,
			    hwsim_genl_policy, NULL);
	if (err) {
		pr_err_ratelimited("hwsim: genlmsg_parse returned %d\n", err);
		return err;
	}

	info.attrs = tb;

	switch (gnlh->cmd) {
	case HWSIM_CMD_FRAME:
		hwsim_cloned_frame_received_nl(skb, &info);
		break;
	case HWSIM_CMD_TX_INFO_FRAME:
		hwsim_tx_info_frame_received_nl(skb, &info);
		break;
	default:
		pr_err_ratelimited("hwsim: invalid cmd: %d\n", gnlh->cmd);
		return -EPROTO;
	}
	return 0;
}

static void hwsim_virtio_rx_work(struct work_struct *work)
{
	struct virtqueue *vq;
	unsigned int len;
	struct sk_buff *skb;
	struct scatterlist sg[1];
	int err;
	unsigned long flags;

	spin_lock_irqsave(&hwsim_virtio_lock, flags);
	if (!hwsim_virtio_enabled)
		goto out_unlock;

	skb = virtqueue_get_buf(hwsim_vqs[HWSIM_VQ_RX], &len);
	if (!skb)
		goto out_unlock;
	spin_unlock_irqrestore(&hwsim_virtio_lock, flags);

	skb->data = skb->head;
	skb_set_tail_pointer(skb, len);
	hwsim_virtio_handle_cmd(skb);

	spin_lock_irqsave(&hwsim_virtio_lock, flags);
	if (!hwsim_virtio_enabled) {
		nlmsg_free(skb);
		goto out_unlock;
	}
	vq = hwsim_vqs[HWSIM_VQ_RX];
	sg_init_one(sg, skb->head, skb_end_offset(skb));
	err = virtqueue_add_inbuf(vq, sg, 1, skb, GFP_KERNEL);
	if (WARN(err, "virtqueue_add_inbuf returned %d\n", err))
		nlmsg_free(skb);
	else
		virtqueue_kick(vq);
	schedule_work(&hwsim_virtio_rx);

out_unlock:
	spin_unlock_irqrestore(&hwsim_virtio_lock, flags);
}

static void hwsim_virtio_rx_done(struct virtqueue *vq)
{
	schedule_work(&hwsim_virtio_rx);
}

static int init_vqs(struct virtio_device *vdev)
{
	vq_callback_t *callbacks[HWSIM_NUM_VQS] = {
		[HWSIM_VQ_TX] = hwsim_virtio_tx_done,
		[HWSIM_VQ_RX] = hwsim_virtio_rx_done,
	};
	const char *names[HWSIM_NUM_VQS] = {
		[HWSIM_VQ_TX] = "tx",
		[HWSIM_VQ_RX] = "rx",
	};

	return virtio_find_vqs(vdev, HWSIM_NUM_VQS,
			       hwsim_vqs, callbacks, names, NULL);
}

static int fill_vq(struct virtqueue *vq)
{
	int i, err;
	struct sk_buff *skb;
	struct scatterlist sg[1];

	for (i = 0; i < virtqueue_get_vring_size(vq); i++) {
		skb = genlmsg_new(GENLMSG_DEFAULT_SIZE, GFP_KERNEL);
		if (!skb)
			return -ENOMEM;

		sg_init_one(sg, skb->head, skb_end_offset(skb));
		err = virtqueue_add_inbuf(vq, sg, 1, skb, GFP_KERNEL);
		if (err) {
			nlmsg_free(skb);
			return err;
		}
	}
	virtqueue_kick(vq);
	return 0;
}

static void remove_vqs(struct virtio_device *vdev)
{
	int i;

	vdev->config->reset(vdev);

	for (i = 0; i < ARRAY_SIZE(hwsim_vqs); i++) {
		struct virtqueue *vq = hwsim_vqs[i];
		struct sk_buff *skb;

		while ((skb = virtqueue_detach_unused_buf(vq)))
			nlmsg_free(skb);
	}

	vdev->config->del_vqs(vdev);
}

static int hwsim_virtio_probe(struct virtio_device *vdev)
{
	int err;
	unsigned long flags;

	spin_lock_irqsave(&hwsim_virtio_lock, flags);
	if (hwsim_virtio_enabled) {
		spin_unlock_irqrestore(&hwsim_virtio_lock, flags);
		return -EEXIST;
	}
	spin_unlock_irqrestore(&hwsim_virtio_lock, flags);

	err = init_vqs(vdev);
	if (err)
		return err;

	err = fill_vq(hwsim_vqs[HWSIM_VQ_RX]);
	if (err)
		goto out_remove;

	spin_lock_irqsave(&hwsim_virtio_lock, flags);
	hwsim_virtio_enabled = true;
	spin_unlock_irqrestore(&hwsim_virtio_lock, flags);

	schedule_work(&hwsim_virtio_rx);
	return 0;

out_remove:
	remove_vqs(vdev);
	return err;
}

static void hwsim_virtio_remove(struct virtio_device *vdev)
{
	hwsim_virtio_enabled = false;

	cancel_work_sync(&hwsim_virtio_rx);

	remove_vqs(vdev);
}

/* MAC80211_HWSIM virtio device id table */
static const struct virtio_device_id id_table[] = {
	{ VIRTIO_ID_MAC80211_HWSIM, VIRTIO_DEV_ANY_ID },
	{ 0 }
};
MODULE_DEVICE_TABLE(virtio, id_table);

static struct virtio_driver virtio_hwsim = {
	.driver.name = KBUILD_MODNAME,
	.driver.owner = THIS_MODULE,
	.id_table = id_table,
	.probe = hwsim_virtio_probe,
	.remove = hwsim_virtio_remove,
};

static int hwsim_register_virtio_driver(void)
{
	spin_lock_init(&hwsim_virtio_lock);

	return register_virtio_driver(&virtio_hwsim);
}

static void hwsim_unregister_virtio_driver(void)
{
	unregister_virtio_driver(&virtio_hwsim);
}
#else
static inline int hwsim_register_virtio_driver(void)
{
	return 0;
}

static inline void hwsim_unregister_virtio_driver(void)
{
}
#endif

static int __init init_mac80211_hwsim(void)
{
	int i, err;
@@ -3960,10 +4244,14 @@ static int __init init_mac80211_hwsim(void)
	if (err)
		goto out_unregister_driver;

	err = hwsim_register_virtio_driver();
	if (err)
		goto out_exit_netlink;

	hwsim_class = class_create(THIS_MODULE, "mac80211_hwsim");
	if (IS_ERR(hwsim_class)) {
		err = PTR_ERR(hwsim_class);
		goto out_exit_netlink;
		goto out_exit_virtio;
	}

	for (i = 0; i < radios; i++) {
@@ -4075,6 +4363,8 @@ out_free_mon:
	free_netdev(hwsim_mon);
out_free_radios:
	mac80211_hwsim_free();
out_exit_virtio:
	hwsim_unregister_virtio_driver();
out_exit_netlink:
	hwsim_exit_netlink();
out_unregister_driver:
@@ -4091,6 +4381,7 @@ static void __exit exit_mac80211_hwsim(void)
{
	pr_debug("mac80211_hwsim: unregister radios\n");

	hwsim_unregister_virtio_driver();
	hwsim_exit_netlink();

	mac80211_hwsim_free();
+21 −0
Original line number Diff line number Diff line
@@ -3,6 +3,7 @@
 * mac80211_hwsim - software simulator of 802.11 radio(s) for mac80211
 * Copyright (c) 2008, Jouni Malinen <j@w1.fi>
 * Copyright (c) 2011, Javier Lopez <jlopex@gmail.com>
 * Copyright (C) 2020 Intel Corporation
 */

#ifndef __MAC80211_HWSIM_H
@@ -245,4 +246,24 @@ struct hwsim_tx_rate_flag {
	s8 idx;
	u16 flags;
} __packed;

/**
 * DOC: Frame transmission support over virtio
 *
 * Frame transmission is also supported over virtio to allow communication
 * with external entities.
 */

/**
 * enum hwsim_vqs - queues for virtio frame transmission
 *
 * @HWSIM_VQ_TX: send frames to external entity
 * @HWSIM_VQ_RX: receive frames and transmission info reports
 * @HWSIM_NUM_VQS: enum limit
 */
enum {
	HWSIM_VQ_TX,
	HWSIM_VQ_RX,
	HWSIM_NUM_VQS,
};
#endif /* __MAC80211_HWSIM_H */
+10 −2
Original line number Diff line number Diff line
@@ -436,10 +436,18 @@ static int virt_wifi_net_device_stop(struct net_device *dev)
	return 0;
}

static int virt_wifi_net_device_get_iflink(const struct net_device *dev)
{
	struct virt_wifi_netdev_priv *priv = netdev_priv(dev);

	return priv->lowerdev->ifindex;
}

static const struct net_device_ops virt_wifi_ops = {
	.ndo_start_xmit = virt_wifi_start_xmit,
	.ndo_open	= virt_wifi_net_device_open,
	.ndo_stop	= virt_wifi_net_device_stop,
	.ndo_get_iflink = virt_wifi_net_device_get_iflink,
};

/* Invoked as part of rtnl lock release. */
+32 −4
Original line number Diff line number Diff line
@@ -7,7 +7,7 @@
 * Copyright 2006-2010	Johannes Berg <johannes@sipsolutions.net>
 * Copyright 2013-2014 Intel Mobile Communications GmbH
 * Copyright 2015-2017	Intel Deutschland GmbH
 * Copyright (C) 2018-2019 Intel Corporation
 * Copyright (C) 2018-2020 Intel Corporation
 */

#include <linux/netdevice.h>
@@ -924,6 +924,7 @@ struct cfg80211_crypto_settings {
	__be16 control_port_ethertype;
	bool control_port_no_encrypt;
	bool control_port_over_nl80211;
	bool control_port_no_preauth;
	struct key_params *wep_keys;
	int wep_tx_key;
	const u8 *psk;
@@ -1054,6 +1055,7 @@ enum cfg80211_ap_settings_flags {
 * @flags: flags, as defined in enum cfg80211_ap_settings_flags
 * @he_obss_pd: OBSS Packet Detection settings
 * @he_bss_color: BSS Color settings
 * @he_oper: HE operation IE (or %NULL if HE isn't enabled)
 */
struct cfg80211_ap_settings {
	struct cfg80211_chan_def chandef;
@@ -1078,6 +1080,7 @@ struct cfg80211_ap_settings {
	const struct ieee80211_ht_cap *ht_cap;
	const struct ieee80211_vht_cap *vht_cap;
	const struct ieee80211_he_cap_elem *he_cap;
	const struct ieee80211_he_operation *he_oper;
	bool ht_required, vht_required;
	bool twt_responder;
	u32 flags;
@@ -2696,6 +2699,17 @@ enum wiphy_params_flags {
 * @cache_id: 2-octet cache identifier advertized by a FILS AP identifying the
 *	scope of PMKSA. This is valid only if @ssid_len is non-zero (may be
 *	%NULL).
 * @pmk_lifetime: Maximum lifetime for PMKSA in seconds
 *	(dot11RSNAConfigPMKLifetime) or 0 if not specified.
 *	The configured PMKSA must not be used for PMKSA caching after
 *	expiration and any keys derived from this PMK become invalid on
 *	expiration, i.e., the current association must be dropped if the PMK
 *	used for it expires.
 * @pmk_reauth_threshold: Threshold time for reauthentication (percentage of
 *	PMK lifetime, dot11RSNAConfigPMKReauthThreshold) or 0 if not specified.
 *	Drivers are expected to trigger a full authentication instead of using
 *	this PMKSA for caching when reassociating to a new BSS after this
 *	threshold to generate a new PMK before the current one expires.
 */
struct cfg80211_pmksa {
	const u8 *bssid;
@@ -2705,6 +2719,8 @@ struct cfg80211_pmksa {
	const u8 *ssid;
	size_t ssid_len;
	const u8 *cache_id;
	u32 pmk_lifetime;
	u8 pmk_reauth_threshold;
};

/**
@@ -3269,6 +3285,12 @@ struct cfg80211_pmsr_result {
 * @ftmr_retries: number of retries for FTM request
 * @request_lci: request LCI information
 * @request_civicloc: request civic location information
 * @trigger_based: use trigger based ranging for the measurement
 *		 If neither @trigger_based nor @non_trigger_based is set,
 *		 EDCA based ranging will be used.
 * @non_trigger_based: use non trigger based ranging for the measurement
 *		 If neither @trigger_based nor @non_trigger_based is set,
 *		 EDCA based ranging will be used.
 *
 * See also nl80211 for the respective attribute documentation.
 */
@@ -3278,7 +3300,9 @@ struct cfg80211_pmsr_ftm_request_peer {
	u8 requested:1,
	   asap:1,
	   request_lci:1,
	   request_civicloc:1;
	   request_civicloc:1,
	   trigger_based:1,
	   non_trigger_based:1;
	u8 num_bursts_exp;
	u8 burst_duration;
	u8 ftms_per_burst;
@@ -3404,7 +3428,7 @@ struct cfg80211_update_owe_info {
 * @set_default_key: set the default key on an interface
 *
 * @set_default_mgmt_key: set the default management frame key on an interface

 *
 * @set_default_beacon_key: set the default Beacon frame key on an interface
 *
 * @set_rekey_data: give the data necessary for GTK rekeying to the driver
@@ -4434,6 +4458,8 @@ struct wiphy_iftype_ext_capab {
 *	forbid using the value 15 to let the responder pick)
 * @ftm.max_ftms_per_burst: maximum FTMs per burst supported (set to 0 if
 *	not limited)
 * @ftm.trigger_based: trigger based ranging measurement is supported
 * @ftm.non_trigger_based: non trigger based ranging measurement is supported
 */
struct cfg80211_pmsr_capabilities {
	unsigned int max_peers;
@@ -4449,7 +4475,9 @@ struct cfg80211_pmsr_capabilities {
		   asap:1,
		   non_asap:1,
		   request_lci:1,
		   request_civicloc:1;
		   request_civicloc:1,
		   trigger_based:1,
		   non_trigger_based:1;
	} ftm;
};

+5 −0
Original line number Diff line number Diff line
@@ -1987,6 +1987,7 @@ struct ieee80211_sta_txpwr {
 * @support_p2p_ps: indicates whether the STA supports P2P PS mechanism or not.
 * @max_rc_amsdu_len: Maximum A-MSDU size in bytes recommended by rate control.
 * @max_tid_amsdu_len: Maximum A-MSDU size in bytes for this TID
 * @txpwr: the station tx power configuration
 * @txq: per-TID data TX queues (if driver uses the TXQ abstraction); note that
 *	the last entry (%IEEE80211_NUM_TIDS) is used for non-data frames
 */
@@ -3451,6 +3452,10 @@ enum ieee80211_reconfig_type {
 *	in AP mode, this callback will not be called when the flag
 *	%IEEE80211_HW_AP_LINK_PS is set. Must be atomic.
 *
 * @sta_set_txpwr: Configure the station tx power. This callback set the tx
 *	power for the station.
 *	This callback can sleep.
 *
 * @sta_state: Notifies low level driver about state transition of a
 *	station (which can be the AP, a client, IBSS/WDS/mesh peer etc.)
 *	This callback is mutually exclusive with @sta_add/@sta_remove.
Loading