diff mbox series

[v3,6/8] wifi: ath12k: support ARP and NS offload

Message ID 20240530072714.25671-7-quic_bqiang@quicinc.com (mailing list archive)
State Changes Requested
Delegated to: Kalle Valo
Headers show
Series wifi: ath12k: add support for WoW | expand

Commit Message

Baochen Qiang May 30, 2024, 7:27 a.m. UTC
Support ARP and NS offload in WoW state.

Tested this way: put machine A with QCA6390 to WoW state,
ping/ping6 machine A from another machine B, check sniffer to see
any ARP response and Neighbor Advertisement from machine A.

Tested-on: WCN7850 hw2.0 PCI WLAN.HMT.1.0-03427-QCAHMTSWPL_V1.0_V2.0_SILICONZ-1.15378.4

Signed-off-by: Baochen Qiang <quic_bqiang@quicinc.com>
---
 drivers/net/wireless/ath/ath12k/core.h |   1 +
 drivers/net/wireless/ath/ath12k/mac.c  |  15 ++
 drivers/net/wireless/ath/ath12k/wmi.c  | 148 ++++++++++++++++++++
 drivers/net/wireless/ath/ath12k/wmi.h  |  66 +++++++++
 drivers/net/wireless/ath/ath12k/wow.c  | 185 +++++++++++++++++++++++++
 5 files changed, 415 insertions(+)

Comments

Jeff Johnson May 30, 2024, 6:26 p.m. UTC | #1
On 5/30/2024 12:27 AM, Baochen Qiang wrote:
> Support ARP and NS offload in WoW state.
> 
> Tested this way: put machine A with QCA6390 to WoW state,
> ping/ping6 machine A from another machine B, check sniffer to see
> any ARP response and Neighbor Advertisement from machine A.
> 
> Tested-on: WCN7850 hw2.0 PCI WLAN.HMT.1.0-03427-QCAHMTSWPL_V1.0_V2.0_SILICONZ-1.15378.4
> 
> Signed-off-by: Baochen Qiang <quic_bqiang@quicinc.com>
> ---
>  drivers/net/wireless/ath/ath12k/core.h |   1 +
>  drivers/net/wireless/ath/ath12k/mac.c  |  15 ++
>  drivers/net/wireless/ath/ath12k/wmi.c  | 148 ++++++++++++++++++++
>  drivers/net/wireless/ath/ath12k/wmi.h  |  66 +++++++++
>  drivers/net/wireless/ath/ath12k/wow.c  | 185 +++++++++++++++++++++++++
>  5 files changed, 415 insertions(+)
> 
> diff --git a/drivers/net/wireless/ath/ath12k/core.h b/drivers/net/wireless/ath/ath12k/core.h
> index af5c489b9fd2..f98c3e7c413e 100644
> --- a/drivers/net/wireless/ath/ath12k/core.h
> +++ b/drivers/net/wireless/ath/ath12k/core.h
> @@ -285,6 +285,7 @@ struct ath12k_vif {
>  	u32 punct_bitmap;
>  	bool ps;
>  	struct ath12k_vif_cache *cache;
> +	struct inet6_dev *idev;
>  };
>  
>  struct ath12k_vif_iter {
> diff --git a/drivers/net/wireless/ath/ath12k/mac.c b/drivers/net/wireless/ath/ath12k/mac.c
> index 1d45692c3102..70311b94fef8 100644
> --- a/drivers/net/wireless/ath/ath12k/mac.c
> +++ b/drivers/net/wireless/ath/ath12k/mac.c
> @@ -6,6 +6,7 @@
>  
>  #include <net/mac80211.h>
>  #include <linux/etherdevice.h>
> +
>  #include "mac.h"
>  #include "core.h"
>  #include "debug.h"
> @@ -8505,6 +8506,16 @@ static int ath12k_mac_op_remain_on_channel(struct ieee80211_hw *hw,
>  	return ret;
>  }
>  
> +static __maybe_unused void ath12k_mac_op_ipv6_changed(struct ieee80211_hw *hw,
> +						      struct ieee80211_vif *vif,
> +						      struct inet6_dev *idev)
> +{
> +	struct ath12k_vif *arvif = ath12k_vif_to_arvif(vif);
> +
> +	/* just cache here, would revisit it during WoW offload */
> +	arvif->idev = idev;

so there is no possibility that this can later disappear, leaving us with a
dangling pointer? if so, suggest you enhance the comment to describe why this
is safe

But alternately, if we no longer act upon this immediately when it changes,
why do we need to cache this? Can we just get to this by back referencing the
netdev from the arvif?

> +}
> +
>  static const struct ieee80211_ops ath12k_ops = {
>  	.tx				= ath12k_mac_op_tx,
>  	.wake_tx_queue			= ieee80211_handle_wake_tx_queue,
> @@ -8547,6 +8558,10 @@ static const struct ieee80211_ops ath12k_ops = {
>  	.resume				= ath12k_wow_op_resume,
>  	.set_wakeup			= ath12k_wow_op_set_wakeup,
>  #endif
> +
> +#if IS_ENABLED(CONFIG_IPV6)
> +	.ipv6_addr_change		= ath12k_mac_op_ipv6_changed,
> +#endif
>  };
>  
>  static void ath12k_mac_update_ch_list(struct ath12k *ar,
> diff --git a/drivers/net/wireless/ath/ath12k/wmi.c b/drivers/net/wireless/ath/ath12k/wmi.c
> index 851f320e73c7..8148e518969e 100644
> --- a/drivers/net/wireless/ath/ath12k/wmi.c
> +++ b/drivers/net/wireless/ath/ath12k/wmi.c
> @@ -7780,3 +7780,151 @@ int ath12k_wmi_wow_config_pno(struct ath12k *ar, u32 vdev_id,
>  
>  	return ath12k_wmi_cmd_send(ar->wmi, skb, WMI_NETWORK_LIST_OFFLOAD_CONFIG_CMDID);
>  }
> +
> +static void ath12k_wmi_fill_ns_offload(struct ath12k *ar,
> +				       struct wmi_arp_ns_offload_arg *offload,
> +				       void **ptr,
> +				       bool enable,
> +				       bool ext)
> +{
> +	struct wmi_ns_offload_params *ns;
> +	struct wmi_tlv *tlv;
> +	void *buf_ptr = *ptr;
> +	u32 ns_cnt, ns_ext_tuples;
> +	int i, max_offloads;
> +
> +	ns_cnt = offload->ipv6_count;
> +
> +	tlv  = buf_ptr;
> +
> +	if (ext) {
> +		ns_ext_tuples = offload->ipv6_count - WMI_MAX_NS_OFFLOADS;
> +		tlv->header = ath12k_wmi_tlv_hdr(WMI_TAG_ARRAY_STRUCT,
> +						 ns_ext_tuples * sizeof(*ns));
> +		i = WMI_MAX_NS_OFFLOADS;
> +		max_offloads = offload->ipv6_count;
> +	} else {
> +		tlv->header = ath12k_wmi_tlv_hdr(WMI_TAG_ARRAY_STRUCT,
> +						 WMI_MAX_NS_OFFLOADS * sizeof(*ns));
> +		i = 0;
> +		max_offloads = WMI_MAX_NS_OFFLOADS;
> +	}
> +
> +	buf_ptr += sizeof(*tlv);
> +
> +	for (; i < max_offloads; i++) {
> +		ns = buf_ptr;
> +		ns->tlv_header = ath12k_wmi_tlv_cmd_hdr(WMI_TAG_NS_OFFLOAD_TUPLE,
> +							sizeof(*ns));
> +
> +		if (enable) {
> +			if (i < ns_cnt)
> +				ns->flags |= cpu_to_le32(WMI_NSOL_FLAGS_VALID);
> +
> +			memcpy(ns->target_ipaddr[0], offload->ipv6_addr[i], 16);
> +			memcpy(ns->solicitation_ipaddr, offload->self_ipv6_addr[i], 16);
> +
> +			if (offload->ipv6_type[i])
> +				ns->flags |= cpu_to_le32(WMI_NSOL_FLAGS_IS_IPV6_ANYCAST);
> +
> +			memcpy(ns->target_mac.addr, offload->mac_addr, ETH_ALEN);
> +
> +			if (!is_zero_ether_addr(ns->target_mac.addr))
> +				ns->flags |= cpu_to_le32(WMI_NSOL_FLAGS_MAC_VALID);
> +
> +			ath12k_dbg(ar->ab, ATH12K_DBG_WMI,
> +				   "wmi index %d ns_solicited %pI6 target %pI6",
> +				   i, ns->solicitation_ipaddr,
> +				   ns->target_ipaddr[0]);
> +		}
> +
> +		buf_ptr += sizeof(*ns);
> +	}
> +
> +	*ptr = buf_ptr;
> +}
> +
> +static void ath12k_wmi_fill_arp_offload(struct ath12k *ar,
> +					struct wmi_arp_ns_offload_arg *offload,
> +					void **ptr,
> +					bool enable)
> +{
> +	struct wmi_arp_offload_params *arp;
> +	struct wmi_tlv *tlv;
> +	void *buf_ptr = *ptr;
> +	int i;
> +
> +	/* fill arp tuple */
> +	tlv = buf_ptr;
> +	tlv->header = ath12k_wmi_tlv_hdr(WMI_TAG_ARRAY_STRUCT,
> +					 WMI_MAX_ARP_OFFLOADS * sizeof(*arp));
> +	buf_ptr += sizeof(*tlv);
> +
> +	for (i = 0; i < WMI_MAX_ARP_OFFLOADS; i++) {
> +		arp = buf_ptr;
> +		arp->tlv_header = ath12k_wmi_tlv_cmd_hdr(WMI_TAG_ARP_OFFLOAD_TUPLE,
> +							 sizeof(*arp));
> +
> +		if (enable && i < offload->ipv4_count) {
> +			/* Copy the target ip addr and flags */
> +			arp->flags = cpu_to_le32(WMI_ARPOL_FLAGS_VALID);
> +			memcpy(arp->target_ipaddr, offload->ipv4_addr[i], 4);
> +
> +			ath12k_dbg(ar->ab, ATH12K_DBG_WMI, "wmi arp offload address %pI4",
> +				   arp->target_ipaddr);
> +		}
> +
> +		buf_ptr += sizeof(*arp);
> +	}
> +
> +	*ptr = buf_ptr;
> +}
> +
> +int ath12k_wmi_arp_ns_offload(struct ath12k *ar,
> +			      struct ath12k_vif *arvif,
> +			      struct wmi_arp_ns_offload_arg *offload,
> +			      bool enable)
> +{
> +	struct wmi_set_arp_ns_offload_cmd *cmd;
> +	struct wmi_tlv *tlv;
> +	struct sk_buff *skb;
> +	void *buf_ptr;
> +	size_t len;
> +	u8 ns_cnt, ns_ext_tuples = 0;
> +
> +	ns_cnt = offload->ipv6_count;
> +
> +	len = sizeof(*cmd) +
> +	      sizeof(*tlv) +
> +	      WMI_MAX_NS_OFFLOADS * sizeof(struct wmi_ns_offload_params) +
> +	      sizeof(*tlv) +
> +	      WMI_MAX_ARP_OFFLOADS * sizeof(struct wmi_arp_offload_params);
> +
> +	if (ns_cnt > WMI_MAX_NS_OFFLOADS) {
> +		ns_ext_tuples = ns_cnt - WMI_MAX_NS_OFFLOADS;
> +		len += sizeof(*tlv) +
> +		       ns_ext_tuples * sizeof(struct wmi_ns_offload_params);
> +	}
> +
> +	skb = ath12k_wmi_alloc_skb(ar->wmi->wmi_ab, len);
> +	if (!skb)
> +		return -ENOMEM;
> +
> +	buf_ptr = skb->data;
> +	cmd = buf_ptr;
> +	cmd->tlv_header = ath12k_wmi_tlv_cmd_hdr(WMI_TAG_SET_ARP_NS_OFFLOAD_CMD,
> +						 sizeof(*cmd));
> +	cmd->flags = cpu_to_le32(0);
> +	cmd->vdev_id = cpu_to_le32(arvif->vdev_id);
> +	cmd->num_ns_ext_tuples = cpu_to_le32(ns_ext_tuples);
> +
> +	buf_ptr += sizeof(*cmd);
> +
> +	ath12k_wmi_fill_ns_offload(ar, offload, &buf_ptr, enable, 0);
> +	ath12k_wmi_fill_arp_offload(ar, offload, &buf_ptr, enable);
> +
> +	if (ns_ext_tuples)
> +		ath12k_wmi_fill_ns_offload(ar, offload, &buf_ptr, enable, 1);
> +
> +	return ath12k_wmi_cmd_send(ar->wmi, skb, WMI_SET_ARP_NS_OFFLOAD_CMDID);
> +}
> diff --git a/drivers/net/wireless/ath/ath12k/wmi.h b/drivers/net/wireless/ath/ath12k/wmi.h
> index 477e2a37eb9c..0542d599d33d 100644
> --- a/drivers/net/wireless/ath/ath12k/wmi.h
> +++ b/drivers/net/wireless/ath/ath12k/wmi.h
> @@ -24,6 +24,7 @@
>  
>  struct ath12k_base;
>  struct ath12k;
> +struct ath12k_vif;
>  
>  /* There is no signed version of __le32, so for a temporary solution come
>   * up with our own version. The idea is from fs/ntfs/endian.h.
> @@ -5316,6 +5317,66 @@ struct wmi_hw_data_filter_arg {
>  	u32 hw_filter_bitmap;
>  };
>  
> +#define WMI_IPV6_UC_TYPE     0
> +#define WMI_IPV6_AC_TYPE     1
> +
> +#define WMI_IPV6_MAX_COUNT   16
> +#define WMI_IPV4_MAX_COUNT   2
> +
> +struct wmi_arp_ns_offload_arg {
> +	u8  ipv4_addr[WMI_IPV4_MAX_COUNT][4];
> +	u32 ipv4_count;
> +	u32 ipv6_count;
> +	u8  ipv6_addr[WMI_IPV6_MAX_COUNT][16];
> +	u8  self_ipv6_addr[WMI_IPV6_MAX_COUNT][16];
> +	u8  ipv6_type[WMI_IPV6_MAX_COUNT];
> +	bool ipv6_valid[WMI_IPV6_MAX_COUNT];
> +	u8  mac_addr[ETH_ALEN];
> +};
> +
> +#define WMI_MAX_NS_OFFLOADS           2
> +#define WMI_MAX_ARP_OFFLOADS          2
> +
> +#define WMI_ARPOL_FLAGS_VALID              BIT(0)
> +#define WMI_ARPOL_FLAGS_MAC_VALID          BIT(1)
> +#define WMI_ARPOL_FLAGS_REMOTE_IP_VALID    BIT(2)
> +
> +struct wmi_arp_offload_params {
> +	__le32 tlv_header;
> +	__le32 flags;
> +	u8 target_ipaddr[4];
> +	u8 remote_ipaddr[4];
> +	struct ath12k_wmi_mac_addr_params target_mac;
> +} __packed;
> +
> +#define WMI_NSOL_FLAGS_VALID               BIT(0)
> +#define WMI_NSOL_FLAGS_MAC_VALID           BIT(1)
> +#define WMI_NSOL_FLAGS_REMOTE_IP_VALID     BIT(2)
> +#define WMI_NSOL_FLAGS_IS_IPV6_ANYCAST     BIT(3)
> +
> +#define WMI_NSOL_MAX_TARGET_IPS    2
> +
> +struct wmi_ns_offload_params {
> +	__le32 tlv_header;
> +	__le32 flags;
> +	u8 target_ipaddr[WMI_NSOL_MAX_TARGET_IPS][16];
> +	u8 solicitation_ipaddr[16];
> +	u8 remote_ipaddr[16];
> +	struct ath12k_wmi_mac_addr_params target_mac;
> +} __packed;
> +
> +struct wmi_set_arp_ns_offload_cmd {
> +	__le32 tlv_header;
> +	__le32 flags;
> +	__le32 vdev_id;
> +	__le32 num_ns_ext_tuples;
> +	/* The TLVs follow:
> +	 * wmi_ns_offload_params  ns[WMI_MAX_NS_OFFLOADS];
> +	 * wmi_arp_offload_params arp[WMI_MAX_ARP_OFFLOADS];
> +	 * wmi_ns_offload_params  ns_ext[num_ns_ext_tuples];
> +	 */
> +} __packed;
> +
>  void ath12k_wmi_init_qcn9274(struct ath12k_base *ab,
>  			     struct ath12k_wmi_resource_config_arg *config);
>  void ath12k_wmi_init_wcn7850(struct ath12k_base *ab,
> @@ -5481,4 +5542,9 @@ int ath12k_wmi_wow_config_pno(struct ath12k *ar, u32 vdev_id,
>  			      struct wmi_pno_scan_req_arg  *pno_scan);
>  int ath12k_wmi_hw_data_filter_cmd(struct ath12k *ar,
>  				  struct wmi_hw_data_filter_arg *arg);
> +int ath12k_wmi_arp_ns_offload(struct ath12k *ar,
> +			      struct ath12k_vif *arvif,
> +			      struct wmi_arp_ns_offload_arg *offload,
> +			      bool enable);
> +
>  #endif
> diff --git a/drivers/net/wireless/ath/ath12k/wow.c b/drivers/net/wireless/ath/ath12k/wow.c
> index 77658a01bb41..92bcafd517e7 100644
> --- a/drivers/net/wireless/ath/ath12k/wow.c
> +++ b/drivers/net/wireless/ath/ath12k/wow.c
> @@ -5,6 +5,9 @@
>   */
>  
>  #include <linux/delay.h>
> +#include <linux/inetdevice.h>
> +#include <net/if_inet6.h>
> +#include <net/ipv6.h>
>  
>  #include "mac.h"
>  
> @@ -591,6 +594,174 @@ static int ath12k_wow_clear_hw_filter(struct ath12k *ar)
>  	return 0;
>  }
>  
> +static void ath12k_wow_generate_ns_mc_addr(struct ath12k_base *ab,
> +					   struct wmi_arp_ns_offload_arg *offload)
> +{
> +	int i;
> +
> +	for (i = 0; i < offload->ipv6_count; i++) {
> +		offload->self_ipv6_addr[i][0] = 0xff;
> +		offload->self_ipv6_addr[i][1] = 0x02;
> +		offload->self_ipv6_addr[i][11] = 0x01;
> +		offload->self_ipv6_addr[i][12] = 0xff;
> +		offload->self_ipv6_addr[i][13] =
> +					offload->ipv6_addr[i][13];
> +		offload->self_ipv6_addr[i][14] =
> +					offload->ipv6_addr[i][14];
> +		offload->self_ipv6_addr[i][15] =
> +					offload->ipv6_addr[i][15];
> +		ath12k_dbg(ab, ATH12K_DBG_WOW, "NS solicited addr %pI6\n",
> +			   offload->self_ipv6_addr[i]);
> +	}
> +}
> +
> +static void ath12k_wow_prepare_ns_offload(struct ath12k_vif *arvif,
> +					  struct wmi_arp_ns_offload_arg *offload)
> +{
> +	struct inet6_dev *idev = arvif->idev;

as noted above does it make more sense to get the netdev associated with the
arvif and then use in6_dev_get(net_device) to get the inet6_dev rather than
caching the pointer from the ipv6_addr_changed() callback?

> +	struct ath12k_base *ab = arvif->ar->ab;
> +	struct inet6_ifaddr *ifa6;
> +	struct ifacaddr6 *ifaca6;
> +	u32 count = 0, scope;
> +	struct list_head *p;
> +
> +	if (!idev)
> +		return;
> +
> +	ath12k_dbg(ab, ATH12K_DBG_WOW, "wow prepare ns offload\n");
> +
> +	read_lock_bh(&idev->lock);
> +
> +	/* get unicast address */
> +	list_for_each(p, &idev->addr_list) {
> +		if (count >= WMI_IPV6_MAX_COUNT)
> +			goto unlock;
> +
> +		ifa6 = list_entry(p, struct inet6_ifaddr, if_list);
> +		if (ifa6->flags & IFA_F_DADFAILED)
> +			continue;
> +
> +		scope = ipv6_addr_src_scope(&ifa6->addr);
> +		if (scope != IPV6_ADDR_SCOPE_LINKLOCAL &&
> +		    scope != IPV6_ADDR_SCOPE_GLOBAL) {
> +			ath12k_dbg(ab, ATH12K_DBG_WOW,
> +				   "Unsupported ipv6 scope: %d\n", scope);
> +			continue;
> +		}
> +
> +		memcpy(offload->ipv6_addr[count], &ifa6->addr.s6_addr,
> +		       sizeof(ifa6->addr.s6_addr));
> +		offload->ipv6_type[count] = WMI_IPV6_UC_TYPE;
> +		ath12k_dbg(ab, ATH12K_DBG_WOW, "mac count %d ipv6 uc %pI6 scope %d\n",
> +			   count, offload->ipv6_addr[count],
> +			   scope);
> +		count++;
> +	}
> +
> +	/* get anycast address */
> +	rcu_read_lock();
> +
> +	for (ifaca6 = rcu_dereference(idev->ac_list); ifaca6;
> +	     ifaca6 = rcu_dereference(ifaca6->aca_next)) {
> +		if (count >= WMI_IPV6_MAX_COUNT) {
> +			rcu_read_unlock();
> +			goto unlock;
> +		}
> +
> +		scope = ipv6_addr_src_scope(&ifaca6->aca_addr);
> +		if (scope != IPV6_ADDR_SCOPE_LINKLOCAL &&
> +		    scope != IPV6_ADDR_SCOPE_GLOBAL) {
> +			ath12k_dbg(ab, ATH12K_DBG_WOW,
> +				   "Unsupported ipv scope: %d\n", scope);
> +			continue;
> +		}
> +
> +		memcpy(offload->ipv6_addr[count], &ifaca6->aca_addr,
> +		       sizeof(ifaca6->aca_addr));
> +		offload->ipv6_type[count] = WMI_IPV6_AC_TYPE;
> +		ath12k_dbg(ab, ATH12K_DBG_WOW, "mac count %d ipv6 ac %pI6 scope %d\n",
> +			   count, offload->ipv6_addr[count],
> +			   scope);
> +		count++;
> +	}
> +
> +	rcu_read_unlock();
> +
> +unlock:
> +	read_unlock_bh(&idev->lock);
> +
> +	offload->ipv6_count = count;
> +	ath12k_wow_generate_ns_mc_addr(ab, offload);
> +}
> +
> +static void ath12k_wow_prepare_arp_offload(struct ath12k_vif *arvif,
> +					   struct wmi_arp_ns_offload_arg *offload)
> +{
> +	struct ieee80211_vif *vif = arvif->vif;
> +	struct ieee80211_vif_cfg vif_cfg = vif->cfg;
> +	struct ath12k_base *ab = arvif->ar->ab;
> +	u32 ipv4_cnt;
> +
> +	ath12k_dbg(ab, ATH12K_DBG_WOW, "wow prepare arp offload\n");
> +
> +	ipv4_cnt = min(vif_cfg.arp_addr_cnt, WMI_IPV4_MAX_COUNT);
> +	memcpy(offload->ipv4_addr, vif_cfg.arp_addr_list, ipv4_cnt * sizeof(u32));
> +	offload->ipv4_count = ipv4_cnt;
> +
> +	ath12k_dbg(ab, ATH12K_DBG_WOW,
> +		   "wow arp_addr_cnt %d vif->addr %pM, offload_addr %pI4\n",
> +		   vif_cfg.arp_addr_cnt, vif->addr, offload->ipv4_addr);
> +}
> +
> +static int ath12k_wow_arp_ns_offload(struct ath12k *ar, bool enable)
> +{
> +	struct wmi_arp_ns_offload_arg *offload;
> +	struct ath12k_vif *arvif;
> +	int ret;
> +
> +	lockdep_assert_held(&ar->conf_mutex);
> +
> +	offload = kmalloc(sizeof(*offload), GFP_KERNEL);
> +	if (!offload)
> +		return -ENOMEM;
> +
> +	list_for_each_entry(arvif, &ar->arvifs, list) {
> +		if (arvif->vdev_type != WMI_VDEV_TYPE_STA)
> +			continue;
> +
> +		memset(offload, 0, sizeof(*offload));
> +
> +		memcpy(offload->mac_addr, arvif->vif->addr, ETH_ALEN);
> +		ath12k_wow_prepare_ns_offload(arvif, offload);
> +		ath12k_wow_prepare_arp_offload(arvif, offload);
> +
> +		ret = ath12k_wmi_arp_ns_offload(ar, arvif, offload, enable);
> +		if (ret) {
> +			ath12k_warn(ar->ab, "failed to set arp ns offload vdev %i: enable %d, ret %d\n",
> +				    arvif->vdev_id, enable, ret);
> +			return ret;
> +		}
> +	}
> +
> +	kfree(offload);
> +
> +	return 0;
> +}
> +
> +static int ath12k_wow_protocol_offload(struct ath12k *ar, bool enable)
> +{
> +	int ret;
> +
> +	ret = ath12k_wow_arp_ns_offload(ar, enable);
> +	if (ret) {
> +		ath12k_warn(ar->ab, "failed to offload ARP and NS %d %d\n",
> +			    enable, ret);
> +		return ret;
> +	}
> +
> +	return 0;
> +}
> +
>  int ath12k_wow_op_suspend(struct ieee80211_hw *hw,
>  			  struct cfg80211_wowlan *wowlan)
>  {
> @@ -614,6 +785,13 @@ int ath12k_wow_op_suspend(struct ieee80211_hw *hw,
>  		goto cleanup;
>  	}
>  
> +	ret = ath12k_wow_protocol_offload(ar, true);
> +	if (ret) {
> +		ath12k_warn(ar->ab, "failed to set wow protocol offload events: %d\n",
> +			    ret);
> +		goto cleanup;
> +	}
> +
>  	ret = ath12k_mac_wait_tx_complete(ar);
>  	if (ret) {
>  		ath12k_warn(ar->ab, "failed to wait tx complete: %d\n", ret);
> @@ -700,6 +878,13 @@ int ath12k_wow_op_resume(struct ieee80211_hw *hw)
>  		goto exit;
>  	}
>  
> +	ret = ath12k_wow_protocol_offload(ar, false);
> +	if (ret) {
> +		ath12k_warn(ar->ab, "failed to clear wow protocol offload events: %d\n",
> +			    ret);
> +		goto exit;
> +	}
> +
>  exit:
>  	if (ret) {
>  		switch (ah->state) {
Baochen Qiang May 31, 2024, 3:42 a.m. UTC | #2
On 5/31/2024 2:26 AM, Jeff Johnson wrote:
> On 5/30/2024 12:27 AM, Baochen Qiang wrote:
>> Support ARP and NS offload in WoW state.
>>
>> Tested this way: put machine A with QCA6390 to WoW state,
>> ping/ping6 machine A from another machine B, check sniffer to see
>> any ARP response and Neighbor Advertisement from machine A.
>>
>> Tested-on: WCN7850 hw2.0 PCI WLAN.HMT.1.0-03427-QCAHMTSWPL_V1.0_V2.0_SILICONZ-1.15378.4
>>
>> Signed-off-by: Baochen Qiang <quic_bqiang@quicinc.com>
>> ---
>>  drivers/net/wireless/ath/ath12k/core.h |   1 +
>>  drivers/net/wireless/ath/ath12k/mac.c  |  15 ++
>>  drivers/net/wireless/ath/ath12k/wmi.c  | 148 ++++++++++++++++++++
>>  drivers/net/wireless/ath/ath12k/wmi.h  |  66 +++++++++
>>  drivers/net/wireless/ath/ath12k/wow.c  | 185 +++++++++++++++++++++++++
>>  5 files changed, 415 insertions(+)
>>
>> diff --git a/drivers/net/wireless/ath/ath12k/core.h b/drivers/net/wireless/ath/ath12k/core.h
>> index af5c489b9fd2..f98c3e7c413e 100644
>> --- a/drivers/net/wireless/ath/ath12k/core.h
>> +++ b/drivers/net/wireless/ath/ath12k/core.h
>> @@ -285,6 +285,7 @@ struct ath12k_vif {
>>  	u32 punct_bitmap;
>>  	bool ps;
>>  	struct ath12k_vif_cache *cache;
>> +	struct inet6_dev *idev;
>>  };
>>  
>>  struct ath12k_vif_iter {
>> diff --git a/drivers/net/wireless/ath/ath12k/mac.c b/drivers/net/wireless/ath/ath12k/mac.c
>> index 1d45692c3102..70311b94fef8 100644
>> --- a/drivers/net/wireless/ath/ath12k/mac.c
>> +++ b/drivers/net/wireless/ath/ath12k/mac.c
>> @@ -6,6 +6,7 @@
>>  
>>  #include <net/mac80211.h>
>>  #include <linux/etherdevice.h>
>> +
>>  #include "mac.h"
>>  #include "core.h"
>>  #include "debug.h"
>> @@ -8505,6 +8506,16 @@ static int ath12k_mac_op_remain_on_channel(struct ieee80211_hw *hw,
>>  	return ret;
>>  }
>>  
>> +static __maybe_unused void ath12k_mac_op_ipv6_changed(struct ieee80211_hw *hw,
>> +						      struct ieee80211_vif *vif,
>> +						      struct inet6_dev *idev)
>> +{
>> +	struct ath12k_vif *arvif = ath12k_vif_to_arvif(vif);
>> +
>> +	/* just cache here, would revisit it during WoW offload */
>> +	arvif->idev = idev;
> 
> so there is no possibility that this can later disappear, leaving us with a
> dangling pointer? if so, suggest you enhance the comment to describe why this
> is safe
> 
> But alternately, if we no longer act upon this immediately when it changes,
> why do we need to cache this? Can we just get to this by back referencing the
> netdev from the arvif?
makes sense. will remove this callback in next version.

> 
>> +}
>> +
>>  static const struct ieee80211_ops ath12k_ops = {
>>  	.tx				= ath12k_mac_op_tx,
>>  	.wake_tx_queue			= ieee80211_handle_wake_tx_queue,
>> @@ -8547,6 +8558,10 @@ static const struct ieee80211_ops ath12k_ops = {
>>  	.resume				= ath12k_wow_op_resume,
>>  	.set_wakeup			= ath12k_wow_op_set_wakeup,
>>  #endif
>> +
>> +#if IS_ENABLED(CONFIG_IPV6)
>> +	.ipv6_addr_change		= ath12k_mac_op_ipv6_changed,
>> +#endif
>>  };
>>  
>>  static void ath12k_mac_update_ch_list(struct ath12k *ar,
>> diff --git a/drivers/net/wireless/ath/ath12k/wmi.c b/drivers/net/wireless/ath/ath12k/wmi.c
>> index 851f320e73c7..8148e518969e 100644
>> --- a/drivers/net/wireless/ath/ath12k/wmi.c
>> +++ b/drivers/net/wireless/ath/ath12k/wmi.c
>> @@ -7780,3 +7780,151 @@ int ath12k_wmi_wow_config_pno(struct ath12k *ar, u32 vdev_id,
>>  
>>  	return ath12k_wmi_cmd_send(ar->wmi, skb, WMI_NETWORK_LIST_OFFLOAD_CONFIG_CMDID);
>>  }
>> +
>> +static void ath12k_wmi_fill_ns_offload(struct ath12k *ar,
>> +				       struct wmi_arp_ns_offload_arg *offload,
>> +				       void **ptr,
>> +				       bool enable,
>> +				       bool ext)
>> +{
>> +	struct wmi_ns_offload_params *ns;
>> +	struct wmi_tlv *tlv;
>> +	void *buf_ptr = *ptr;
>> +	u32 ns_cnt, ns_ext_tuples;
>> +	int i, max_offloads;
>> +
>> +	ns_cnt = offload->ipv6_count;
>> +
>> +	tlv  = buf_ptr;
>> +
>> +	if (ext) {
>> +		ns_ext_tuples = offload->ipv6_count - WMI_MAX_NS_OFFLOADS;
>> +		tlv->header = ath12k_wmi_tlv_hdr(WMI_TAG_ARRAY_STRUCT,
>> +						 ns_ext_tuples * sizeof(*ns));
>> +		i = WMI_MAX_NS_OFFLOADS;
>> +		max_offloads = offload->ipv6_count;
>> +	} else {
>> +		tlv->header = ath12k_wmi_tlv_hdr(WMI_TAG_ARRAY_STRUCT,
>> +						 WMI_MAX_NS_OFFLOADS * sizeof(*ns));
>> +		i = 0;
>> +		max_offloads = WMI_MAX_NS_OFFLOADS;
>> +	}
>> +
>> +	buf_ptr += sizeof(*tlv);
>> +
>> +	for (; i < max_offloads; i++) {
>> +		ns = buf_ptr;
>> +		ns->tlv_header = ath12k_wmi_tlv_cmd_hdr(WMI_TAG_NS_OFFLOAD_TUPLE,
>> +							sizeof(*ns));
>> +
>> +		if (enable) {
>> +			if (i < ns_cnt)
>> +				ns->flags |= cpu_to_le32(WMI_NSOL_FLAGS_VALID);
>> +
>> +			memcpy(ns->target_ipaddr[0], offload->ipv6_addr[i], 16);
>> +			memcpy(ns->solicitation_ipaddr, offload->self_ipv6_addr[i], 16);
>> +
>> +			if (offload->ipv6_type[i])
>> +				ns->flags |= cpu_to_le32(WMI_NSOL_FLAGS_IS_IPV6_ANYCAST);
>> +
>> +			memcpy(ns->target_mac.addr, offload->mac_addr, ETH_ALEN);
>> +
>> +			if (!is_zero_ether_addr(ns->target_mac.addr))
>> +				ns->flags |= cpu_to_le32(WMI_NSOL_FLAGS_MAC_VALID);
>> +
>> +			ath12k_dbg(ar->ab, ATH12K_DBG_WMI,
>> +				   "wmi index %d ns_solicited %pI6 target %pI6",
>> +				   i, ns->solicitation_ipaddr,
>> +				   ns->target_ipaddr[0]);
>> +		}
>> +
>> +		buf_ptr += sizeof(*ns);
>> +	}
>> +
>> +	*ptr = buf_ptr;
>> +}
>> +
>> +static void ath12k_wmi_fill_arp_offload(struct ath12k *ar,
>> +					struct wmi_arp_ns_offload_arg *offload,
>> +					void **ptr,
>> +					bool enable)
>> +{
>> +	struct wmi_arp_offload_params *arp;
>> +	struct wmi_tlv *tlv;
>> +	void *buf_ptr = *ptr;
>> +	int i;
>> +
>> +	/* fill arp tuple */
>> +	tlv = buf_ptr;
>> +	tlv->header = ath12k_wmi_tlv_hdr(WMI_TAG_ARRAY_STRUCT,
>> +					 WMI_MAX_ARP_OFFLOADS * sizeof(*arp));
>> +	buf_ptr += sizeof(*tlv);
>> +
>> +	for (i = 0; i < WMI_MAX_ARP_OFFLOADS; i++) {
>> +		arp = buf_ptr;
>> +		arp->tlv_header = ath12k_wmi_tlv_cmd_hdr(WMI_TAG_ARP_OFFLOAD_TUPLE,
>> +							 sizeof(*arp));
>> +
>> +		if (enable && i < offload->ipv4_count) {
>> +			/* Copy the target ip addr and flags */
>> +			arp->flags = cpu_to_le32(WMI_ARPOL_FLAGS_VALID);
>> +			memcpy(arp->target_ipaddr, offload->ipv4_addr[i], 4);
>> +
>> +			ath12k_dbg(ar->ab, ATH12K_DBG_WMI, "wmi arp offload address %pI4",
>> +				   arp->target_ipaddr);
>> +		}
>> +
>> +		buf_ptr += sizeof(*arp);
>> +	}
>> +
>> +	*ptr = buf_ptr;
>> +}
>> +
>> +int ath12k_wmi_arp_ns_offload(struct ath12k *ar,
>> +			      struct ath12k_vif *arvif,
>> +			      struct wmi_arp_ns_offload_arg *offload,
>> +			      bool enable)
>> +{
>> +	struct wmi_set_arp_ns_offload_cmd *cmd;
>> +	struct wmi_tlv *tlv;
>> +	struct sk_buff *skb;
>> +	void *buf_ptr;
>> +	size_t len;
>> +	u8 ns_cnt, ns_ext_tuples = 0;
>> +
>> +	ns_cnt = offload->ipv6_count;
>> +
>> +	len = sizeof(*cmd) +
>> +	      sizeof(*tlv) +
>> +	      WMI_MAX_NS_OFFLOADS * sizeof(struct wmi_ns_offload_params) +
>> +	      sizeof(*tlv) +
>> +	      WMI_MAX_ARP_OFFLOADS * sizeof(struct wmi_arp_offload_params);
>> +
>> +	if (ns_cnt > WMI_MAX_NS_OFFLOADS) {
>> +		ns_ext_tuples = ns_cnt - WMI_MAX_NS_OFFLOADS;
>> +		len += sizeof(*tlv) +
>> +		       ns_ext_tuples * sizeof(struct wmi_ns_offload_params);
>> +	}
>> +
>> +	skb = ath12k_wmi_alloc_skb(ar->wmi->wmi_ab, len);
>> +	if (!skb)
>> +		return -ENOMEM;
>> +
>> +	buf_ptr = skb->data;
>> +	cmd = buf_ptr;
>> +	cmd->tlv_header = ath12k_wmi_tlv_cmd_hdr(WMI_TAG_SET_ARP_NS_OFFLOAD_CMD,
>> +						 sizeof(*cmd));
>> +	cmd->flags = cpu_to_le32(0);
>> +	cmd->vdev_id = cpu_to_le32(arvif->vdev_id);
>> +	cmd->num_ns_ext_tuples = cpu_to_le32(ns_ext_tuples);
>> +
>> +	buf_ptr += sizeof(*cmd);
>> +
>> +	ath12k_wmi_fill_ns_offload(ar, offload, &buf_ptr, enable, 0);
>> +	ath12k_wmi_fill_arp_offload(ar, offload, &buf_ptr, enable);
>> +
>> +	if (ns_ext_tuples)
>> +		ath12k_wmi_fill_ns_offload(ar, offload, &buf_ptr, enable, 1);
>> +
>> +	return ath12k_wmi_cmd_send(ar->wmi, skb, WMI_SET_ARP_NS_OFFLOAD_CMDID);
>> +}
>> diff --git a/drivers/net/wireless/ath/ath12k/wmi.h b/drivers/net/wireless/ath/ath12k/wmi.h
>> index 477e2a37eb9c..0542d599d33d 100644
>> --- a/drivers/net/wireless/ath/ath12k/wmi.h
>> +++ b/drivers/net/wireless/ath/ath12k/wmi.h
>> @@ -24,6 +24,7 @@
>>  
>>  struct ath12k_base;
>>  struct ath12k;
>> +struct ath12k_vif;
>>  
>>  /* There is no signed version of __le32, so for a temporary solution come
>>   * up with our own version. The idea is from fs/ntfs/endian.h.
>> @@ -5316,6 +5317,66 @@ struct wmi_hw_data_filter_arg {
>>  	u32 hw_filter_bitmap;
>>  };
>>  
>> +#define WMI_IPV6_UC_TYPE     0
>> +#define WMI_IPV6_AC_TYPE     1
>> +
>> +#define WMI_IPV6_MAX_COUNT   16
>> +#define WMI_IPV4_MAX_COUNT   2
>> +
>> +struct wmi_arp_ns_offload_arg {
>> +	u8  ipv4_addr[WMI_IPV4_MAX_COUNT][4];
>> +	u32 ipv4_count;
>> +	u32 ipv6_count;
>> +	u8  ipv6_addr[WMI_IPV6_MAX_COUNT][16];
>> +	u8  self_ipv6_addr[WMI_IPV6_MAX_COUNT][16];
>> +	u8  ipv6_type[WMI_IPV6_MAX_COUNT];
>> +	bool ipv6_valid[WMI_IPV6_MAX_COUNT];
>> +	u8  mac_addr[ETH_ALEN];
>> +};
>> +
>> +#define WMI_MAX_NS_OFFLOADS           2
>> +#define WMI_MAX_ARP_OFFLOADS          2
>> +
>> +#define WMI_ARPOL_FLAGS_VALID              BIT(0)
>> +#define WMI_ARPOL_FLAGS_MAC_VALID          BIT(1)
>> +#define WMI_ARPOL_FLAGS_REMOTE_IP_VALID    BIT(2)
>> +
>> +struct wmi_arp_offload_params {
>> +	__le32 tlv_header;
>> +	__le32 flags;
>> +	u8 target_ipaddr[4];
>> +	u8 remote_ipaddr[4];
>> +	struct ath12k_wmi_mac_addr_params target_mac;
>> +} __packed;
>> +
>> +#define WMI_NSOL_FLAGS_VALID               BIT(0)
>> +#define WMI_NSOL_FLAGS_MAC_VALID           BIT(1)
>> +#define WMI_NSOL_FLAGS_REMOTE_IP_VALID     BIT(2)
>> +#define WMI_NSOL_FLAGS_IS_IPV6_ANYCAST     BIT(3)
>> +
>> +#define WMI_NSOL_MAX_TARGET_IPS    2
>> +
>> +struct wmi_ns_offload_params {
>> +	__le32 tlv_header;
>> +	__le32 flags;
>> +	u8 target_ipaddr[WMI_NSOL_MAX_TARGET_IPS][16];
>> +	u8 solicitation_ipaddr[16];
>> +	u8 remote_ipaddr[16];
>> +	struct ath12k_wmi_mac_addr_params target_mac;
>> +} __packed;
>> +
>> +struct wmi_set_arp_ns_offload_cmd {
>> +	__le32 tlv_header;
>> +	__le32 flags;
>> +	__le32 vdev_id;
>> +	__le32 num_ns_ext_tuples;
>> +	/* The TLVs follow:
>> +	 * wmi_ns_offload_params  ns[WMI_MAX_NS_OFFLOADS];
>> +	 * wmi_arp_offload_params arp[WMI_MAX_ARP_OFFLOADS];
>> +	 * wmi_ns_offload_params  ns_ext[num_ns_ext_tuples];
>> +	 */
>> +} __packed;
>> +
>>  void ath12k_wmi_init_qcn9274(struct ath12k_base *ab,
>>  			     struct ath12k_wmi_resource_config_arg *config);
>>  void ath12k_wmi_init_wcn7850(struct ath12k_base *ab,
>> @@ -5481,4 +5542,9 @@ int ath12k_wmi_wow_config_pno(struct ath12k *ar, u32 vdev_id,
>>  			      struct wmi_pno_scan_req_arg  *pno_scan);
>>  int ath12k_wmi_hw_data_filter_cmd(struct ath12k *ar,
>>  				  struct wmi_hw_data_filter_arg *arg);
>> +int ath12k_wmi_arp_ns_offload(struct ath12k *ar,
>> +			      struct ath12k_vif *arvif,
>> +			      struct wmi_arp_ns_offload_arg *offload,
>> +			      bool enable);
>> +
>>  #endif
>> diff --git a/drivers/net/wireless/ath/ath12k/wow.c b/drivers/net/wireless/ath/ath12k/wow.c
>> index 77658a01bb41..92bcafd517e7 100644
>> --- a/drivers/net/wireless/ath/ath12k/wow.c
>> +++ b/drivers/net/wireless/ath/ath12k/wow.c
>> @@ -5,6 +5,9 @@
>>   */
>>  
>>  #include <linux/delay.h>
>> +#include <linux/inetdevice.h>
>> +#include <net/if_inet6.h>
>> +#include <net/ipv6.h>
>>  
>>  #include "mac.h"
>>  
>> @@ -591,6 +594,174 @@ static int ath12k_wow_clear_hw_filter(struct ath12k *ar)
>>  	return 0;
>>  }
>>  
>> +static void ath12k_wow_generate_ns_mc_addr(struct ath12k_base *ab,
>> +					   struct wmi_arp_ns_offload_arg *offload)
>> +{
>> +	int i;
>> +
>> +	for (i = 0; i < offload->ipv6_count; i++) {
>> +		offload->self_ipv6_addr[i][0] = 0xff;
>> +		offload->self_ipv6_addr[i][1] = 0x02;
>> +		offload->self_ipv6_addr[i][11] = 0x01;
>> +		offload->self_ipv6_addr[i][12] = 0xff;
>> +		offload->self_ipv6_addr[i][13] =
>> +					offload->ipv6_addr[i][13];
>> +		offload->self_ipv6_addr[i][14] =
>> +					offload->ipv6_addr[i][14];
>> +		offload->self_ipv6_addr[i][15] =
>> +					offload->ipv6_addr[i][15];
>> +		ath12k_dbg(ab, ATH12K_DBG_WOW, "NS solicited addr %pI6\n",
>> +			   offload->self_ipv6_addr[i]);
>> +	}
>> +}
>> +
>> +static void ath12k_wow_prepare_ns_offload(struct ath12k_vif *arvif,
>> +					  struct wmi_arp_ns_offload_arg *offload)
>> +{
>> +	struct inet6_dev *idev = arvif->idev;
> 
> as noted above does it make more sense to get the netdev associated with the
> arvif and then use in6_dev_get(net_device) to get the inet6_dev rather than
> caching the pointer from the ipv6_addr_changed() callback?
Ah.. I didn't note that we can get inet6_dev in such a way, just thought the only way is to cache it in ipv6_changed() callback.

will get it using the following in next version:
	struct ieee80211_vif *vif = container_of(arvif)
	struct ieee80211_sub_if_data *sub_if_data = container_of(vif)
	struct net_dev *ndev = sub_if_data->dev
	struct inet6_dev *idev = in6_dev_get(ndev)
> 
>> +	struct ath12k_base *ab = arvif->ar->ab;
>> +	struct inet6_ifaddr *ifa6;
>> +	struct ifacaddr6 *ifaca6;
>> +	u32 count = 0, scope;
>> +	struct list_head *p;
>> +
>> +	if (!idev)
>> +		return;
>> +
>> +	ath12k_dbg(ab, ATH12K_DBG_WOW, "wow prepare ns offload\n");
>> +
>> +	read_lock_bh(&idev->lock);
>> +
>> +	/* get unicast address */
>> +	list_for_each(p, &idev->addr_list) {
>> +		if (count >= WMI_IPV6_MAX_COUNT)
>> +			goto unlock;
>> +
>> +		ifa6 = list_entry(p, struct inet6_ifaddr, if_list);
>> +		if (ifa6->flags & IFA_F_DADFAILED)
>> +			continue;
>> +
>> +		scope = ipv6_addr_src_scope(&ifa6->addr);
>> +		if (scope != IPV6_ADDR_SCOPE_LINKLOCAL &&
>> +		    scope != IPV6_ADDR_SCOPE_GLOBAL) {
>> +			ath12k_dbg(ab, ATH12K_DBG_WOW,
>> +				   "Unsupported ipv6 scope: %d\n", scope);
>> +			continue;
>> +		}
>> +
>> +		memcpy(offload->ipv6_addr[count], &ifa6->addr.s6_addr,
>> +		       sizeof(ifa6->addr.s6_addr));
>> +		offload->ipv6_type[count] = WMI_IPV6_UC_TYPE;
>> +		ath12k_dbg(ab, ATH12K_DBG_WOW, "mac count %d ipv6 uc %pI6 scope %d\n",
>> +			   count, offload->ipv6_addr[count],
>> +			   scope);
>> +		count++;
>> +	}
>> +
>> +	/* get anycast address */
>> +	rcu_read_lock();
>> +
>> +	for (ifaca6 = rcu_dereference(idev->ac_list); ifaca6;
>> +	     ifaca6 = rcu_dereference(ifaca6->aca_next)) {
>> +		if (count >= WMI_IPV6_MAX_COUNT) {
>> +			rcu_read_unlock();
>> +			goto unlock;
>> +		}
>> +
>> +		scope = ipv6_addr_src_scope(&ifaca6->aca_addr);
>> +		if (scope != IPV6_ADDR_SCOPE_LINKLOCAL &&
>> +		    scope != IPV6_ADDR_SCOPE_GLOBAL) {
>> +			ath12k_dbg(ab, ATH12K_DBG_WOW,
>> +				   "Unsupported ipv scope: %d\n", scope);
>> +			continue;
>> +		}
>> +
>> +		memcpy(offload->ipv6_addr[count], &ifaca6->aca_addr,
>> +		       sizeof(ifaca6->aca_addr));
>> +		offload->ipv6_type[count] = WMI_IPV6_AC_TYPE;
>> +		ath12k_dbg(ab, ATH12K_DBG_WOW, "mac count %d ipv6 ac %pI6 scope %d\n",
>> +			   count, offload->ipv6_addr[count],
>> +			   scope);
>> +		count++;
>> +	}
>> +
>> +	rcu_read_unlock();
>> +
>> +unlock:
>> +	read_unlock_bh(&idev->lock);
>> +
>> +	offload->ipv6_count = count;
>> +	ath12k_wow_generate_ns_mc_addr(ab, offload);
>> +}
>> +
>> +static void ath12k_wow_prepare_arp_offload(struct ath12k_vif *arvif,
>> +					   struct wmi_arp_ns_offload_arg *offload)
>> +{
>> +	struct ieee80211_vif *vif = arvif->vif;
>> +	struct ieee80211_vif_cfg vif_cfg = vif->cfg;
>> +	struct ath12k_base *ab = arvif->ar->ab;
>> +	u32 ipv4_cnt;
>> +
>> +	ath12k_dbg(ab, ATH12K_DBG_WOW, "wow prepare arp offload\n");
>> +
>> +	ipv4_cnt = min(vif_cfg.arp_addr_cnt, WMI_IPV4_MAX_COUNT);
>> +	memcpy(offload->ipv4_addr, vif_cfg.arp_addr_list, ipv4_cnt * sizeof(u32));
>> +	offload->ipv4_count = ipv4_cnt;
>> +
>> +	ath12k_dbg(ab, ATH12K_DBG_WOW,
>> +		   "wow arp_addr_cnt %d vif->addr %pM, offload_addr %pI4\n",
>> +		   vif_cfg.arp_addr_cnt, vif->addr, offload->ipv4_addr);
>> +}
>> +
>> +static int ath12k_wow_arp_ns_offload(struct ath12k *ar, bool enable)
>> +{
>> +	struct wmi_arp_ns_offload_arg *offload;
>> +	struct ath12k_vif *arvif;
>> +	int ret;
>> +
>> +	lockdep_assert_held(&ar->conf_mutex);
>> +
>> +	offload = kmalloc(sizeof(*offload), GFP_KERNEL);
>> +	if (!offload)
>> +		return -ENOMEM;
>> +
>> +	list_for_each_entry(arvif, &ar->arvifs, list) {
>> +		if (arvif->vdev_type != WMI_VDEV_TYPE_STA)
>> +			continue;
>> +
>> +		memset(offload, 0, sizeof(*offload));
>> +
>> +		memcpy(offload->mac_addr, arvif->vif->addr, ETH_ALEN);
>> +		ath12k_wow_prepare_ns_offload(arvif, offload);
>> +		ath12k_wow_prepare_arp_offload(arvif, offload);
>> +
>> +		ret = ath12k_wmi_arp_ns_offload(ar, arvif, offload, enable);
>> +		if (ret) {
>> +			ath12k_warn(ar->ab, "failed to set arp ns offload vdev %i: enable %d, ret %d\n",
>> +				    arvif->vdev_id, enable, ret);
>> +			return ret;
>> +		}
>> +	}
>> +
>> +	kfree(offload);
>> +
>> +	return 0;
>> +}
>> +
>> +static int ath12k_wow_protocol_offload(struct ath12k *ar, bool enable)
>> +{
>> +	int ret;
>> +
>> +	ret = ath12k_wow_arp_ns_offload(ar, enable);
>> +	if (ret) {
>> +		ath12k_warn(ar->ab, "failed to offload ARP and NS %d %d\n",
>> +			    enable, ret);
>> +		return ret;
>> +	}
>> +
>> +	return 0;
>> +}
>> +
>>  int ath12k_wow_op_suspend(struct ieee80211_hw *hw,
>>  			  struct cfg80211_wowlan *wowlan)
>>  {
>> @@ -614,6 +785,13 @@ int ath12k_wow_op_suspend(struct ieee80211_hw *hw,
>>  		goto cleanup;
>>  	}
>>  
>> +	ret = ath12k_wow_protocol_offload(ar, true);
>> +	if (ret) {
>> +		ath12k_warn(ar->ab, "failed to set wow protocol offload events: %d\n",
>> +			    ret);
>> +		goto cleanup;
>> +	}
>> +
>>  	ret = ath12k_mac_wait_tx_complete(ar);
>>  	if (ret) {
>>  		ath12k_warn(ar->ab, "failed to wait tx complete: %d\n", ret);
>> @@ -700,6 +878,13 @@ int ath12k_wow_op_resume(struct ieee80211_hw *hw)
>>  		goto exit;
>>  	}
>>  
>> +	ret = ath12k_wow_protocol_offload(ar, false);
>> +	if (ret) {
>> +		ath12k_warn(ar->ab, "failed to clear wow protocol offload events: %d\n",
>> +			    ret);
>> +		goto exit;
>> +	}
>> +
>>  exit:
>>  	if (ret) {
>>  		switch (ah->state) {
>
Baochen Qiang May 31, 2024, 3:49 a.m. UTC | #3
On 5/31/2024 11:42 AM, Baochen Qiang wrote:
>> as noted above does it make more sense to get the netdev associated with the
>> arvif and then use in6_dev_get(net_device) to get the inet6_dev rather than
>> caching the pointer from the ipv6_addr_changed() callback?
> Ah.. I didn't note that we can get inet6_dev in such a way, just thought the only way is to cache it in ipv6_changed() callback.
> 
> will get it using the following in next version:
> 	struct ieee80211_vif *vif = container_of(arvif)
sorry, should be:
	struct ieee80211_vif *vif = arvif->vif

> 	struct ieee80211_sub_if_data *sub_if_data = container_of(vif)
> 	struct net_dev *ndev = sub_if_data->dev
> 	struct inet6_dev *idev = in6_dev_get(ndev)
Baochen Qiang May 31, 2024, 5:11 a.m. UTC | #4
On 5/31/2024 11:42 AM, Baochen Qiang wrote:
>>> +static void ath12k_wow_prepare_ns_offload(struct ath12k_vif *arvif,
>>> +					  struct wmi_arp_ns_offload_arg *offload)
>>> +{
>>> +	struct inet6_dev *idev = arvif->idev;
>> as noted above does it make more sense to get the netdev associated with the
>> arvif and then use in6_dev_get(net_device) to get the inet6_dev rather than
>> caching the pointer from the ipv6_addr_changed() callback?
> Ah.. I didn't note that we can get inet6_dev in such a way, just thought the only way is to cache it in ipv6_changed() callback.
> 
> will get it using the following in next version:
> 	struct ieee80211_vif *vif = container_of(arvif)
> 	struct ieee80211_sub_if_data *sub_if_data = container_of(vif)
> 	struct net_dev *ndev = sub_if_data->dev
> 	struct inet6_dev *idev = in6_dev_get(ndev)
Just found that ieee80211_sub_if_data is internal to mac80211, so not possible to get netdev in this way.

any other ideas on how to get netdev?
Jeff Johnson May 31, 2024, 5:26 p.m. UTC | #5
On 5/30/2024 10:11 PM, Baochen Qiang wrote:
> 
> 
> On 5/31/2024 11:42 AM, Baochen Qiang wrote:
>>>> +static void ath12k_wow_prepare_ns_offload(struct ath12k_vif *arvif,
>>>> +					  struct wmi_arp_ns_offload_arg *offload)
>>>> +{
>>>> +	struct inet6_dev *idev = arvif->idev;
>>> as noted above does it make more sense to get the netdev associated with the
>>> arvif and then use in6_dev_get(net_device) to get the inet6_dev rather than
>>> caching the pointer from the ipv6_addr_changed() callback?
>> Ah.. I didn't note that we can get inet6_dev in such a way, just thought the only way is to cache it in ipv6_changed() callback.
>>
>> will get it using the following in next version:
>> 	struct ieee80211_vif *vif = container_of(arvif)
>> 	struct ieee80211_sub_if_data *sub_if_data = container_of(vif)
>> 	struct net_dev *ndev = sub_if_data->dev
>> 	struct inet6_dev *idev = in6_dev_get(ndev)
> Just found that ieee80211_sub_if_data is internal to mac80211, so not possible to get netdev in this way.
> 
> any other ideas on how to get netdev?

Thinking about this some more, it seems like you'd want to send these down to
firmware immediately so that they'd be available for NS offload. Does firmware
support NS offload even when host is awake (I think the downstream Android
driver supports that)?

So perhaps an alternative approach is to collect the information you need from
the notification and then schedule a workqueue to actually send the
information to firmware?
Baochen Qiang June 3, 2024, 2:47 a.m. UTC | #6
On 6/1/2024 1:26 AM, Jeff Johnson wrote:
> On 5/30/2024 10:11 PM, Baochen Qiang wrote:
>>
>>
>> On 5/31/2024 11:42 AM, Baochen Qiang wrote:
>>>>> +static void ath12k_wow_prepare_ns_offload(struct ath12k_vif *arvif,
>>>>> +					  struct wmi_arp_ns_offload_arg *offload)
>>>>> +{
>>>>> +	struct inet6_dev *idev = arvif->idev;
>>>> as noted above does it make more sense to get the netdev associated with the
>>>> arvif and then use in6_dev_get(net_device) to get the inet6_dev rather than
>>>> caching the pointer from the ipv6_addr_changed() callback?
>>> Ah.. I didn't note that we can get inet6_dev in such a way, just thought the only way is to cache it in ipv6_changed() callback.
>>>
>>> will get it using the following in next version:
>>> 	struct ieee80211_vif *vif = container_of(arvif)
>>> 	struct ieee80211_sub_if_data *sub_if_data = container_of(vif)
>>> 	struct net_dev *ndev = sub_if_data->dev
>>> 	struct inet6_dev *idev = in6_dev_get(ndev)
>> Just found that ieee80211_sub_if_data is internal to mac80211, so not possible to get netdev in this way.
>>
>> any other ideas on how to get netdev?
> 
> Thinking about this some more, it seems like you'd want to send these down to
> firmware immediately so that they'd be available for NS offload. Does firmware
> support NS offload even when host is awake (I think the downstream Android
> driver supports that)?
Checked with firmware team and they confirmed it is supported. But do we really want to do this? Any benefit from offloading when awake?

And won't there be any issues? at least it would make me confused if a arp/NS request/response pair are seen in sniffer but neither of them seen in host ...
> So perhaps an alternative approach is to collect the information you need from
> the notification and then schedule a workqueue to actually send the
> information to firmware?
Baochen Qiang June 3, 2024, 2:48 a.m. UTC | #7
On 6/1/2024 1:26 AM, Jeff Johnson wrote:
> On 5/30/2024 10:11 PM, Baochen Qiang wrote:
>>
>>
>> On 5/31/2024 11:42 AM, Baochen Qiang wrote:
>>>>> +static void ath12k_wow_prepare_ns_offload(struct ath12k_vif *arvif,
>>>>> +					  struct wmi_arp_ns_offload_arg *offload)
>>>>> +{
>>>>> +	struct inet6_dev *idev = arvif->idev;
>>>> as noted above does it make more sense to get the netdev associated with the
>>>> arvif and then use in6_dev_get(net_device) to get the inet6_dev rather than
>>>> caching the pointer from the ipv6_addr_changed() callback?
>>> Ah.. I didn't note that we can get inet6_dev in such a way, just thought the only way is to cache it in ipv6_changed() callback.
>>>
>>> will get it using the following in next version:
>>> 	struct ieee80211_vif *vif = container_of(arvif)
>>> 	struct ieee80211_sub_if_data *sub_if_data = container_of(vif)
>>> 	struct net_dev *ndev = sub_if_data->dev
>>> 	struct inet6_dev *idev = in6_dev_get(ndev)
>> Just found that ieee80211_sub_if_data is internal to mac80211, so not possible to get netdev in this way.
>>
>> any other ideas on how to get netdev?
> 
> Thinking about this some more, it seems like you'd want to send these down to
> firmware immediately so that they'd be available for NS offload. Does firmware
> support NS offload even when host is awake (I think the downstream Android
> driver supports that)?
And really curious about the use case in Android scenario.

> 
> So perhaps an alternative approach is to collect the information you need from
> the notification and then schedule a workqueue to actually send the
> information to firmware?
Johannes Berg June 3, 2024, 7:36 a.m. UTC | #8
On Fri, 2024-05-31 at 13:11 +0800, Baochen Qiang wrote:
> 
> On 5/31/2024 11:42 AM, Baochen Qiang wrote:
> > > > +static void ath12k_wow_prepare_ns_offload(struct ath12k_vif *arvif,
> > > > +					  struct wmi_arp_ns_offload_arg *offload)
> > > > +{
> > > > +	struct inet6_dev *idev = arvif->idev;
> > > as noted above does it make more sense to get the netdev associated with the
> > > arvif and then use in6_dev_get(net_device) to get the inet6_dev rather than
> > > caching the pointer from the ipv6_addr_changed() callback?
> > Ah.. I didn't note that we can get inet6_dev in such a way, just thought the only way is to cache it in ipv6_changed() callback.
> > 
> > will get it using the following in next version:
> > 	struct ieee80211_vif *vif = container_of(arvif)
> > 	struct ieee80211_sub_if_data *sub_if_data = container_of(vif)
> > 	struct net_dev *ndev = sub_if_data->dev
> > 	struct inet6_dev *idev = in6_dev_get(ndev)
> Just found that ieee80211_sub_if_data is internal to mac80211, so not possible to get netdev in this way.
> 
> any other ideas on how to get netdev?

You can go via the wdev.

johannes
Baochen Qiang June 3, 2024, 9:15 a.m. UTC | #9
On 6/3/2024 3:36 PM, Johannes Berg wrote:
> On Fri, 2024-05-31 at 13:11 +0800, Baochen Qiang wrote:
>>
>> On 5/31/2024 11:42 AM, Baochen Qiang wrote:
>>>>> +static void ath12k_wow_prepare_ns_offload(struct ath12k_vif *arvif,
>>>>> +					  struct wmi_arp_ns_offload_arg *offload)
>>>>> +{
>>>>> +	struct inet6_dev *idev = arvif->idev;
>>>> as noted above does it make more sense to get the netdev associated with the
>>>> arvif and then use in6_dev_get(net_device) to get the inet6_dev rather than
>>>> caching the pointer from the ipv6_addr_changed() callback?
>>> Ah.. I didn't note that we can get inet6_dev in such a way, just thought the only way is to cache it in ipv6_changed() callback.
>>>
>>> will get it using the following in next version:
>>> 	struct ieee80211_vif *vif = container_of(arvif)
>>> 	struct ieee80211_sub_if_data *sub_if_data = container_of(vif)
>>> 	struct net_dev *ndev = sub_if_data->dev
>>> 	struct inet6_dev *idev = in6_dev_get(ndev)
>> Just found that ieee80211_sub_if_data is internal to mac80211, so not possible to get netdev in this way.
>>
>> any other ideas on how to get netdev?
> 
> You can go via the wdev.
> 
Ah, great! I can get it with ieee80211_vif_to_wdev(arvif->vif)->netdev. Thank you for pointing that.

> johannes
Baochen Qiang June 3, 2024, 9:32 a.m. UTC | #10
On 6/1/2024 1:26 AM, Jeff Johnson wrote:
> On 5/30/2024 10:11 PM, Baochen Qiang wrote:
>>
>>
>> On 5/31/2024 11:42 AM, Baochen Qiang wrote:
>>>>> +static void ath12k_wow_prepare_ns_offload(struct ath12k_vif *arvif,
>>>>> +					  struct wmi_arp_ns_offload_arg *offload)
>>>>> +{
>>>>> +	struct inet6_dev *idev = arvif->idev;
>>>> as noted above does it make more sense to get the netdev associated with the
>>>> arvif and then use in6_dev_get(net_device) to get the inet6_dev rather than
>>>> caching the pointer from the ipv6_addr_changed() callback?
>>> Ah.. I didn't note that we can get inet6_dev in such a way, just thought the only way is to cache it in ipv6_changed() callback.
>>>
>>> will get it using the following in next version:
>>> 	struct ieee80211_vif *vif = container_of(arvif)
>>> 	struct ieee80211_sub_if_data *sub_if_data = container_of(vif)
>>> 	struct net_dev *ndev = sub_if_data->dev
>>> 	struct inet6_dev *idev = in6_dev_get(ndev)
>> Just found that ieee80211_sub_if_data is internal to mac80211, so not possible to get netdev in this way.
>>
>> any other ideas on how to get netdev?
> 
> Thinking about this some more, it seems like you'd want to send these down to
> firmware immediately so that they'd be available for NS offload. Does firmware
> support NS offload even when host is awake (I think the downstream Android
> driver supports that)?
> 
> So perhaps an alternative approach is to collect the information you need from
> the notification and then schedule a workqueue to actually send the
> information to firmware?
As is pointed out by johannes in another reply, if you are OK with original proposal I can go getting ieee80211_vif_to_wdev(arvif->vif)->netdev in ath12k_wow_prepare_ns_offloa().
Jeff Johnson June 3, 2024, 2:22 p.m. UTC | #11
On 6/2/2024 7:48 PM, Baochen Qiang wrote:
> 
> 
> On 6/1/2024 1:26 AM, Jeff Johnson wrote:
>> On 5/30/2024 10:11 PM, Baochen Qiang wrote:
>>>
>>>
>>> On 5/31/2024 11:42 AM, Baochen Qiang wrote:
>>>>>> +static void ath12k_wow_prepare_ns_offload(struct ath12k_vif *arvif,
>>>>>> +					  struct wmi_arp_ns_offload_arg *offload)
>>>>>> +{
>>>>>> +	struct inet6_dev *idev = arvif->idev;
>>>>> as noted above does it make more sense to get the netdev associated with the
>>>>> arvif and then use in6_dev_get(net_device) to get the inet6_dev rather than
>>>>> caching the pointer from the ipv6_addr_changed() callback?
>>>> Ah.. I didn't note that we can get inet6_dev in such a way, just thought the only way is to cache it in ipv6_changed() callback.
>>>>
>>>> will get it using the following in next version:
>>>> 	struct ieee80211_vif *vif = container_of(arvif)
>>>> 	struct ieee80211_sub_if_data *sub_if_data = container_of(vif)
>>>> 	struct net_dev *ndev = sub_if_data->dev
>>>> 	struct inet6_dev *idev = in6_dev_get(ndev)
>>> Just found that ieee80211_sub_if_data is internal to mac80211, so not possible to get netdev in this way.
>>>
>>> any other ideas on how to get netdev?
>>
>> Thinking about this some more, it seems like you'd want to send these down to
>> firmware immediately so that they'd be available for NS offload. Does firmware
>> support NS offload even when host is awake (I think the downstream Android
>> driver supports that)?
> And really curious about the use case in Android scenario.

It's all about saving power. The application processor consumes way more power
than the firmware processors, so any work that can be performed by firmware
extends battery life.

A laptop running on battery would presumably benefit from this as well.

But let's get basic functionality in place using ieee80211_vif_to_wdev()
diff mbox series

Patch

diff --git a/drivers/net/wireless/ath/ath12k/core.h b/drivers/net/wireless/ath/ath12k/core.h
index af5c489b9fd2..f98c3e7c413e 100644
--- a/drivers/net/wireless/ath/ath12k/core.h
+++ b/drivers/net/wireless/ath/ath12k/core.h
@@ -285,6 +285,7 @@  struct ath12k_vif {
 	u32 punct_bitmap;
 	bool ps;
 	struct ath12k_vif_cache *cache;
+	struct inet6_dev *idev;
 };
 
 struct ath12k_vif_iter {
diff --git a/drivers/net/wireless/ath/ath12k/mac.c b/drivers/net/wireless/ath/ath12k/mac.c
index 1d45692c3102..70311b94fef8 100644
--- a/drivers/net/wireless/ath/ath12k/mac.c
+++ b/drivers/net/wireless/ath/ath12k/mac.c
@@ -6,6 +6,7 @@ 
 
 #include <net/mac80211.h>
 #include <linux/etherdevice.h>
+
 #include "mac.h"
 #include "core.h"
 #include "debug.h"
@@ -8505,6 +8506,16 @@  static int ath12k_mac_op_remain_on_channel(struct ieee80211_hw *hw,
 	return ret;
 }
 
+static __maybe_unused void ath12k_mac_op_ipv6_changed(struct ieee80211_hw *hw,
+						      struct ieee80211_vif *vif,
+						      struct inet6_dev *idev)
+{
+	struct ath12k_vif *arvif = ath12k_vif_to_arvif(vif);
+
+	/* just cache here, would revisit it during WoW offload */
+	arvif->idev = idev;
+}
+
 static const struct ieee80211_ops ath12k_ops = {
 	.tx				= ath12k_mac_op_tx,
 	.wake_tx_queue			= ieee80211_handle_wake_tx_queue,
@@ -8547,6 +8558,10 @@  static const struct ieee80211_ops ath12k_ops = {
 	.resume				= ath12k_wow_op_resume,
 	.set_wakeup			= ath12k_wow_op_set_wakeup,
 #endif
+
+#if IS_ENABLED(CONFIG_IPV6)
+	.ipv6_addr_change		= ath12k_mac_op_ipv6_changed,
+#endif
 };
 
 static void ath12k_mac_update_ch_list(struct ath12k *ar,
diff --git a/drivers/net/wireless/ath/ath12k/wmi.c b/drivers/net/wireless/ath/ath12k/wmi.c
index 851f320e73c7..8148e518969e 100644
--- a/drivers/net/wireless/ath/ath12k/wmi.c
+++ b/drivers/net/wireless/ath/ath12k/wmi.c
@@ -7780,3 +7780,151 @@  int ath12k_wmi_wow_config_pno(struct ath12k *ar, u32 vdev_id,
 
 	return ath12k_wmi_cmd_send(ar->wmi, skb, WMI_NETWORK_LIST_OFFLOAD_CONFIG_CMDID);
 }
+
+static void ath12k_wmi_fill_ns_offload(struct ath12k *ar,
+				       struct wmi_arp_ns_offload_arg *offload,
+				       void **ptr,
+				       bool enable,
+				       bool ext)
+{
+	struct wmi_ns_offload_params *ns;
+	struct wmi_tlv *tlv;
+	void *buf_ptr = *ptr;
+	u32 ns_cnt, ns_ext_tuples;
+	int i, max_offloads;
+
+	ns_cnt = offload->ipv6_count;
+
+	tlv  = buf_ptr;
+
+	if (ext) {
+		ns_ext_tuples = offload->ipv6_count - WMI_MAX_NS_OFFLOADS;
+		tlv->header = ath12k_wmi_tlv_hdr(WMI_TAG_ARRAY_STRUCT,
+						 ns_ext_tuples * sizeof(*ns));
+		i = WMI_MAX_NS_OFFLOADS;
+		max_offloads = offload->ipv6_count;
+	} else {
+		tlv->header = ath12k_wmi_tlv_hdr(WMI_TAG_ARRAY_STRUCT,
+						 WMI_MAX_NS_OFFLOADS * sizeof(*ns));
+		i = 0;
+		max_offloads = WMI_MAX_NS_OFFLOADS;
+	}
+
+	buf_ptr += sizeof(*tlv);
+
+	for (; i < max_offloads; i++) {
+		ns = buf_ptr;
+		ns->tlv_header = ath12k_wmi_tlv_cmd_hdr(WMI_TAG_NS_OFFLOAD_TUPLE,
+							sizeof(*ns));
+
+		if (enable) {
+			if (i < ns_cnt)
+				ns->flags |= cpu_to_le32(WMI_NSOL_FLAGS_VALID);
+
+			memcpy(ns->target_ipaddr[0], offload->ipv6_addr[i], 16);
+			memcpy(ns->solicitation_ipaddr, offload->self_ipv6_addr[i], 16);
+
+			if (offload->ipv6_type[i])
+				ns->flags |= cpu_to_le32(WMI_NSOL_FLAGS_IS_IPV6_ANYCAST);
+
+			memcpy(ns->target_mac.addr, offload->mac_addr, ETH_ALEN);
+
+			if (!is_zero_ether_addr(ns->target_mac.addr))
+				ns->flags |= cpu_to_le32(WMI_NSOL_FLAGS_MAC_VALID);
+
+			ath12k_dbg(ar->ab, ATH12K_DBG_WMI,
+				   "wmi index %d ns_solicited %pI6 target %pI6",
+				   i, ns->solicitation_ipaddr,
+				   ns->target_ipaddr[0]);
+		}
+
+		buf_ptr += sizeof(*ns);
+	}
+
+	*ptr = buf_ptr;
+}
+
+static void ath12k_wmi_fill_arp_offload(struct ath12k *ar,
+					struct wmi_arp_ns_offload_arg *offload,
+					void **ptr,
+					bool enable)
+{
+	struct wmi_arp_offload_params *arp;
+	struct wmi_tlv *tlv;
+	void *buf_ptr = *ptr;
+	int i;
+
+	/* fill arp tuple */
+	tlv = buf_ptr;
+	tlv->header = ath12k_wmi_tlv_hdr(WMI_TAG_ARRAY_STRUCT,
+					 WMI_MAX_ARP_OFFLOADS * sizeof(*arp));
+	buf_ptr += sizeof(*tlv);
+
+	for (i = 0; i < WMI_MAX_ARP_OFFLOADS; i++) {
+		arp = buf_ptr;
+		arp->tlv_header = ath12k_wmi_tlv_cmd_hdr(WMI_TAG_ARP_OFFLOAD_TUPLE,
+							 sizeof(*arp));
+
+		if (enable && i < offload->ipv4_count) {
+			/* Copy the target ip addr and flags */
+			arp->flags = cpu_to_le32(WMI_ARPOL_FLAGS_VALID);
+			memcpy(arp->target_ipaddr, offload->ipv4_addr[i], 4);
+
+			ath12k_dbg(ar->ab, ATH12K_DBG_WMI, "wmi arp offload address %pI4",
+				   arp->target_ipaddr);
+		}
+
+		buf_ptr += sizeof(*arp);
+	}
+
+	*ptr = buf_ptr;
+}
+
+int ath12k_wmi_arp_ns_offload(struct ath12k *ar,
+			      struct ath12k_vif *arvif,
+			      struct wmi_arp_ns_offload_arg *offload,
+			      bool enable)
+{
+	struct wmi_set_arp_ns_offload_cmd *cmd;
+	struct wmi_tlv *tlv;
+	struct sk_buff *skb;
+	void *buf_ptr;
+	size_t len;
+	u8 ns_cnt, ns_ext_tuples = 0;
+
+	ns_cnt = offload->ipv6_count;
+
+	len = sizeof(*cmd) +
+	      sizeof(*tlv) +
+	      WMI_MAX_NS_OFFLOADS * sizeof(struct wmi_ns_offload_params) +
+	      sizeof(*tlv) +
+	      WMI_MAX_ARP_OFFLOADS * sizeof(struct wmi_arp_offload_params);
+
+	if (ns_cnt > WMI_MAX_NS_OFFLOADS) {
+		ns_ext_tuples = ns_cnt - WMI_MAX_NS_OFFLOADS;
+		len += sizeof(*tlv) +
+		       ns_ext_tuples * sizeof(struct wmi_ns_offload_params);
+	}
+
+	skb = ath12k_wmi_alloc_skb(ar->wmi->wmi_ab, len);
+	if (!skb)
+		return -ENOMEM;
+
+	buf_ptr = skb->data;
+	cmd = buf_ptr;
+	cmd->tlv_header = ath12k_wmi_tlv_cmd_hdr(WMI_TAG_SET_ARP_NS_OFFLOAD_CMD,
+						 sizeof(*cmd));
+	cmd->flags = cpu_to_le32(0);
+	cmd->vdev_id = cpu_to_le32(arvif->vdev_id);
+	cmd->num_ns_ext_tuples = cpu_to_le32(ns_ext_tuples);
+
+	buf_ptr += sizeof(*cmd);
+
+	ath12k_wmi_fill_ns_offload(ar, offload, &buf_ptr, enable, 0);
+	ath12k_wmi_fill_arp_offload(ar, offload, &buf_ptr, enable);
+
+	if (ns_ext_tuples)
+		ath12k_wmi_fill_ns_offload(ar, offload, &buf_ptr, enable, 1);
+
+	return ath12k_wmi_cmd_send(ar->wmi, skb, WMI_SET_ARP_NS_OFFLOAD_CMDID);
+}
diff --git a/drivers/net/wireless/ath/ath12k/wmi.h b/drivers/net/wireless/ath/ath12k/wmi.h
index 477e2a37eb9c..0542d599d33d 100644
--- a/drivers/net/wireless/ath/ath12k/wmi.h
+++ b/drivers/net/wireless/ath/ath12k/wmi.h
@@ -24,6 +24,7 @@ 
 
 struct ath12k_base;
 struct ath12k;
+struct ath12k_vif;
 
 /* There is no signed version of __le32, so for a temporary solution come
  * up with our own version. The idea is from fs/ntfs/endian.h.
@@ -5316,6 +5317,66 @@  struct wmi_hw_data_filter_arg {
 	u32 hw_filter_bitmap;
 };
 
+#define WMI_IPV6_UC_TYPE     0
+#define WMI_IPV6_AC_TYPE     1
+
+#define WMI_IPV6_MAX_COUNT   16
+#define WMI_IPV4_MAX_COUNT   2
+
+struct wmi_arp_ns_offload_arg {
+	u8  ipv4_addr[WMI_IPV4_MAX_COUNT][4];
+	u32 ipv4_count;
+	u32 ipv6_count;
+	u8  ipv6_addr[WMI_IPV6_MAX_COUNT][16];
+	u8  self_ipv6_addr[WMI_IPV6_MAX_COUNT][16];
+	u8  ipv6_type[WMI_IPV6_MAX_COUNT];
+	bool ipv6_valid[WMI_IPV6_MAX_COUNT];
+	u8  mac_addr[ETH_ALEN];
+};
+
+#define WMI_MAX_NS_OFFLOADS           2
+#define WMI_MAX_ARP_OFFLOADS          2
+
+#define WMI_ARPOL_FLAGS_VALID              BIT(0)
+#define WMI_ARPOL_FLAGS_MAC_VALID          BIT(1)
+#define WMI_ARPOL_FLAGS_REMOTE_IP_VALID    BIT(2)
+
+struct wmi_arp_offload_params {
+	__le32 tlv_header;
+	__le32 flags;
+	u8 target_ipaddr[4];
+	u8 remote_ipaddr[4];
+	struct ath12k_wmi_mac_addr_params target_mac;
+} __packed;
+
+#define WMI_NSOL_FLAGS_VALID               BIT(0)
+#define WMI_NSOL_FLAGS_MAC_VALID           BIT(1)
+#define WMI_NSOL_FLAGS_REMOTE_IP_VALID     BIT(2)
+#define WMI_NSOL_FLAGS_IS_IPV6_ANYCAST     BIT(3)
+
+#define WMI_NSOL_MAX_TARGET_IPS    2
+
+struct wmi_ns_offload_params {
+	__le32 tlv_header;
+	__le32 flags;
+	u8 target_ipaddr[WMI_NSOL_MAX_TARGET_IPS][16];
+	u8 solicitation_ipaddr[16];
+	u8 remote_ipaddr[16];
+	struct ath12k_wmi_mac_addr_params target_mac;
+} __packed;
+
+struct wmi_set_arp_ns_offload_cmd {
+	__le32 tlv_header;
+	__le32 flags;
+	__le32 vdev_id;
+	__le32 num_ns_ext_tuples;
+	/* The TLVs follow:
+	 * wmi_ns_offload_params  ns[WMI_MAX_NS_OFFLOADS];
+	 * wmi_arp_offload_params arp[WMI_MAX_ARP_OFFLOADS];
+	 * wmi_ns_offload_params  ns_ext[num_ns_ext_tuples];
+	 */
+} __packed;
+
 void ath12k_wmi_init_qcn9274(struct ath12k_base *ab,
 			     struct ath12k_wmi_resource_config_arg *config);
 void ath12k_wmi_init_wcn7850(struct ath12k_base *ab,
@@ -5481,4 +5542,9 @@  int ath12k_wmi_wow_config_pno(struct ath12k *ar, u32 vdev_id,
 			      struct wmi_pno_scan_req_arg  *pno_scan);
 int ath12k_wmi_hw_data_filter_cmd(struct ath12k *ar,
 				  struct wmi_hw_data_filter_arg *arg);
+int ath12k_wmi_arp_ns_offload(struct ath12k *ar,
+			      struct ath12k_vif *arvif,
+			      struct wmi_arp_ns_offload_arg *offload,
+			      bool enable);
+
 #endif
diff --git a/drivers/net/wireless/ath/ath12k/wow.c b/drivers/net/wireless/ath/ath12k/wow.c
index 77658a01bb41..92bcafd517e7 100644
--- a/drivers/net/wireless/ath/ath12k/wow.c
+++ b/drivers/net/wireless/ath/ath12k/wow.c
@@ -5,6 +5,9 @@ 
  */
 
 #include <linux/delay.h>
+#include <linux/inetdevice.h>
+#include <net/if_inet6.h>
+#include <net/ipv6.h>
 
 #include "mac.h"
 
@@ -591,6 +594,174 @@  static int ath12k_wow_clear_hw_filter(struct ath12k *ar)
 	return 0;
 }
 
+static void ath12k_wow_generate_ns_mc_addr(struct ath12k_base *ab,
+					   struct wmi_arp_ns_offload_arg *offload)
+{
+	int i;
+
+	for (i = 0; i < offload->ipv6_count; i++) {
+		offload->self_ipv6_addr[i][0] = 0xff;
+		offload->self_ipv6_addr[i][1] = 0x02;
+		offload->self_ipv6_addr[i][11] = 0x01;
+		offload->self_ipv6_addr[i][12] = 0xff;
+		offload->self_ipv6_addr[i][13] =
+					offload->ipv6_addr[i][13];
+		offload->self_ipv6_addr[i][14] =
+					offload->ipv6_addr[i][14];
+		offload->self_ipv6_addr[i][15] =
+					offload->ipv6_addr[i][15];
+		ath12k_dbg(ab, ATH12K_DBG_WOW, "NS solicited addr %pI6\n",
+			   offload->self_ipv6_addr[i]);
+	}
+}
+
+static void ath12k_wow_prepare_ns_offload(struct ath12k_vif *arvif,
+					  struct wmi_arp_ns_offload_arg *offload)
+{
+	struct inet6_dev *idev = arvif->idev;
+	struct ath12k_base *ab = arvif->ar->ab;
+	struct inet6_ifaddr *ifa6;
+	struct ifacaddr6 *ifaca6;
+	u32 count = 0, scope;
+	struct list_head *p;
+
+	if (!idev)
+		return;
+
+	ath12k_dbg(ab, ATH12K_DBG_WOW, "wow prepare ns offload\n");
+
+	read_lock_bh(&idev->lock);
+
+	/* get unicast address */
+	list_for_each(p, &idev->addr_list) {
+		if (count >= WMI_IPV6_MAX_COUNT)
+			goto unlock;
+
+		ifa6 = list_entry(p, struct inet6_ifaddr, if_list);
+		if (ifa6->flags & IFA_F_DADFAILED)
+			continue;
+
+		scope = ipv6_addr_src_scope(&ifa6->addr);
+		if (scope != IPV6_ADDR_SCOPE_LINKLOCAL &&
+		    scope != IPV6_ADDR_SCOPE_GLOBAL) {
+			ath12k_dbg(ab, ATH12K_DBG_WOW,
+				   "Unsupported ipv6 scope: %d\n", scope);
+			continue;
+		}
+
+		memcpy(offload->ipv6_addr[count], &ifa6->addr.s6_addr,
+		       sizeof(ifa6->addr.s6_addr));
+		offload->ipv6_type[count] = WMI_IPV6_UC_TYPE;
+		ath12k_dbg(ab, ATH12K_DBG_WOW, "mac count %d ipv6 uc %pI6 scope %d\n",
+			   count, offload->ipv6_addr[count],
+			   scope);
+		count++;
+	}
+
+	/* get anycast address */
+	rcu_read_lock();
+
+	for (ifaca6 = rcu_dereference(idev->ac_list); ifaca6;
+	     ifaca6 = rcu_dereference(ifaca6->aca_next)) {
+		if (count >= WMI_IPV6_MAX_COUNT) {
+			rcu_read_unlock();
+			goto unlock;
+		}
+
+		scope = ipv6_addr_src_scope(&ifaca6->aca_addr);
+		if (scope != IPV6_ADDR_SCOPE_LINKLOCAL &&
+		    scope != IPV6_ADDR_SCOPE_GLOBAL) {
+			ath12k_dbg(ab, ATH12K_DBG_WOW,
+				   "Unsupported ipv scope: %d\n", scope);
+			continue;
+		}
+
+		memcpy(offload->ipv6_addr[count], &ifaca6->aca_addr,
+		       sizeof(ifaca6->aca_addr));
+		offload->ipv6_type[count] = WMI_IPV6_AC_TYPE;
+		ath12k_dbg(ab, ATH12K_DBG_WOW, "mac count %d ipv6 ac %pI6 scope %d\n",
+			   count, offload->ipv6_addr[count],
+			   scope);
+		count++;
+	}
+
+	rcu_read_unlock();
+
+unlock:
+	read_unlock_bh(&idev->lock);
+
+	offload->ipv6_count = count;
+	ath12k_wow_generate_ns_mc_addr(ab, offload);
+}
+
+static void ath12k_wow_prepare_arp_offload(struct ath12k_vif *arvif,
+					   struct wmi_arp_ns_offload_arg *offload)
+{
+	struct ieee80211_vif *vif = arvif->vif;
+	struct ieee80211_vif_cfg vif_cfg = vif->cfg;
+	struct ath12k_base *ab = arvif->ar->ab;
+	u32 ipv4_cnt;
+
+	ath12k_dbg(ab, ATH12K_DBG_WOW, "wow prepare arp offload\n");
+
+	ipv4_cnt = min(vif_cfg.arp_addr_cnt, WMI_IPV4_MAX_COUNT);
+	memcpy(offload->ipv4_addr, vif_cfg.arp_addr_list, ipv4_cnt * sizeof(u32));
+	offload->ipv4_count = ipv4_cnt;
+
+	ath12k_dbg(ab, ATH12K_DBG_WOW,
+		   "wow arp_addr_cnt %d vif->addr %pM, offload_addr %pI4\n",
+		   vif_cfg.arp_addr_cnt, vif->addr, offload->ipv4_addr);
+}
+
+static int ath12k_wow_arp_ns_offload(struct ath12k *ar, bool enable)
+{
+	struct wmi_arp_ns_offload_arg *offload;
+	struct ath12k_vif *arvif;
+	int ret;
+
+	lockdep_assert_held(&ar->conf_mutex);
+
+	offload = kmalloc(sizeof(*offload), GFP_KERNEL);
+	if (!offload)
+		return -ENOMEM;
+
+	list_for_each_entry(arvif, &ar->arvifs, list) {
+		if (arvif->vdev_type != WMI_VDEV_TYPE_STA)
+			continue;
+
+		memset(offload, 0, sizeof(*offload));
+
+		memcpy(offload->mac_addr, arvif->vif->addr, ETH_ALEN);
+		ath12k_wow_prepare_ns_offload(arvif, offload);
+		ath12k_wow_prepare_arp_offload(arvif, offload);
+
+		ret = ath12k_wmi_arp_ns_offload(ar, arvif, offload, enable);
+		if (ret) {
+			ath12k_warn(ar->ab, "failed to set arp ns offload vdev %i: enable %d, ret %d\n",
+				    arvif->vdev_id, enable, ret);
+			return ret;
+		}
+	}
+
+	kfree(offload);
+
+	return 0;
+}
+
+static int ath12k_wow_protocol_offload(struct ath12k *ar, bool enable)
+{
+	int ret;
+
+	ret = ath12k_wow_arp_ns_offload(ar, enable);
+	if (ret) {
+		ath12k_warn(ar->ab, "failed to offload ARP and NS %d %d\n",
+			    enable, ret);
+		return ret;
+	}
+
+	return 0;
+}
+
 int ath12k_wow_op_suspend(struct ieee80211_hw *hw,
 			  struct cfg80211_wowlan *wowlan)
 {
@@ -614,6 +785,13 @@  int ath12k_wow_op_suspend(struct ieee80211_hw *hw,
 		goto cleanup;
 	}
 
+	ret = ath12k_wow_protocol_offload(ar, true);
+	if (ret) {
+		ath12k_warn(ar->ab, "failed to set wow protocol offload events: %d\n",
+			    ret);
+		goto cleanup;
+	}
+
 	ret = ath12k_mac_wait_tx_complete(ar);
 	if (ret) {
 		ath12k_warn(ar->ab, "failed to wait tx complete: %d\n", ret);
@@ -700,6 +878,13 @@  int ath12k_wow_op_resume(struct ieee80211_hw *hw)
 		goto exit;
 	}
 
+	ret = ath12k_wow_protocol_offload(ar, false);
+	if (ret) {
+		ath12k_warn(ar->ab, "failed to clear wow protocol offload events: %d\n",
+			    ret);
+		goto exit;
+	}
+
 exit:
 	if (ret) {
 		switch (ah->state) {