--- /dev/null
+// SPDX-License-Identifier: GPL-2.0
+
+#include <linux/debugfs.h>
+
+#include "netdevsim.h"
+
+#define NSIM_DEV_HWSTATS_TRAFFIC_MS    100
+
+static struct list_head *
+nsim_dev_hwstats_get_list_head(struct nsim_dev_hwstats *hwstats,
+                              enum netdev_offload_xstats_type type)
+{
+       switch (type) {
+       case NETDEV_OFFLOAD_XSTATS_TYPE_L3:
+               return &hwstats->l3_list;
+       }
+
+       WARN_ON_ONCE(1);
+       return NULL;
+}
+
+static void nsim_dev_hwstats_traffic_bump(struct nsim_dev_hwstats *hwstats,
+                                         enum netdev_offload_xstats_type type)
+{
+       struct nsim_dev_hwstats_netdev *hwsdev;
+       struct list_head *hwsdev_list;
+
+       hwsdev_list = nsim_dev_hwstats_get_list_head(hwstats, type);
+       if (WARN_ON(!hwsdev_list))
+               return;
+
+       list_for_each_entry(hwsdev, hwsdev_list, list) {
+               if (hwsdev->enabled) {
+                       hwsdev->stats.rx_packets += 1;
+                       hwsdev->stats.tx_packets += 2;
+                       hwsdev->stats.rx_bytes += 100;
+                       hwsdev->stats.tx_bytes += 300;
+               }
+       }
+}
+
+static void nsim_dev_hwstats_traffic_work(struct work_struct *work)
+{
+       struct nsim_dev_hwstats *hwstats;
+
+       hwstats = container_of(work, struct nsim_dev_hwstats, traffic_dw.work);
+       mutex_lock(&hwstats->hwsdev_list_lock);
+       nsim_dev_hwstats_traffic_bump(hwstats, NETDEV_OFFLOAD_XSTATS_TYPE_L3);
+       mutex_unlock(&hwstats->hwsdev_list_lock);
+
+       schedule_delayed_work(&hwstats->traffic_dw,
+                             msecs_to_jiffies(NSIM_DEV_HWSTATS_TRAFFIC_MS));
+}
+
+static struct nsim_dev_hwstats_netdev *
+nsim_dev_hwslist_find_hwsdev(struct list_head *hwsdev_list,
+                            int ifindex)
+{
+       struct nsim_dev_hwstats_netdev *hwsdev;
+
+       list_for_each_entry(hwsdev, hwsdev_list, list) {
+               if (hwsdev->netdev->ifindex == ifindex)
+                       return hwsdev;
+       }
+
+       return NULL;
+}
+
+static int nsim_dev_hwsdev_enable(struct nsim_dev_hwstats_netdev *hwsdev,
+                                 struct netlink_ext_ack *extack)
+{
+       if (hwsdev->fail_enable) {
+               hwsdev->fail_enable = false;
+               NL_SET_ERR_MSG_MOD(extack, "Stats enablement set to fail");
+               return -ECANCELED;
+       }
+
+       hwsdev->enabled = true;
+       return 0;
+}
+
+static void nsim_dev_hwsdev_disable(struct nsim_dev_hwstats_netdev *hwsdev)
+{
+       hwsdev->enabled = false;
+       memset(&hwsdev->stats, 0, sizeof(hwsdev->stats));
+}
+
+static int
+nsim_dev_hwsdev_report_delta(struct nsim_dev_hwstats_netdev *hwsdev,
+                            struct netdev_notifier_offload_xstats_info *info)
+{
+       netdev_offload_xstats_report_delta(info->report_delta, &hwsdev->stats);
+       memset(&hwsdev->stats, 0, sizeof(hwsdev->stats));
+       return 0;
+}
+
+static void
+nsim_dev_hwsdev_report_used(struct nsim_dev_hwstats_netdev *hwsdev,
+                           struct netdev_notifier_offload_xstats_info *info)
+{
+       if (hwsdev->enabled)
+               netdev_offload_xstats_report_used(info->report_used);
+}
+
+static int nsim_dev_hwstats_event_off_xstats(struct nsim_dev_hwstats *hwstats,
+                                            struct net_device *dev,
+                                            unsigned long event, void *ptr)
+{
+       struct netdev_notifier_offload_xstats_info *info;
+       struct nsim_dev_hwstats_netdev *hwsdev;
+       struct list_head *hwsdev_list;
+       int err = 0;
+
+       info = ptr;
+       hwsdev_list = nsim_dev_hwstats_get_list_head(hwstats, info->type);
+       if (!hwsdev_list)
+               return 0;
+
+       mutex_lock(&hwstats->hwsdev_list_lock);
+
+       hwsdev = nsim_dev_hwslist_find_hwsdev(hwsdev_list, dev->ifindex);
+       if (!hwsdev)
+               goto out;
+
+       switch (event) {
+       case NETDEV_OFFLOAD_XSTATS_ENABLE:
+               err = nsim_dev_hwsdev_enable(hwsdev, info->info.extack);
+               break;
+       case NETDEV_OFFLOAD_XSTATS_DISABLE:
+               nsim_dev_hwsdev_disable(hwsdev);
+               break;
+       case NETDEV_OFFLOAD_XSTATS_REPORT_USED:
+               nsim_dev_hwsdev_report_used(hwsdev, info);
+               break;
+       case NETDEV_OFFLOAD_XSTATS_REPORT_DELTA:
+               err = nsim_dev_hwsdev_report_delta(hwsdev, info);
+               break;
+       }
+
+out:
+       mutex_unlock(&hwstats->hwsdev_list_lock);
+       return err;
+}
+
+static void nsim_dev_hwsdev_fini(struct nsim_dev_hwstats_netdev *hwsdev)
+{
+       dev_put(hwsdev->netdev);
+       kfree(hwsdev);
+}
+
+static void
+__nsim_dev_hwstats_event_unregister(struct nsim_dev_hwstats *hwstats,
+                                   struct net_device *dev,
+                                   enum netdev_offload_xstats_type type)
+{
+       struct nsim_dev_hwstats_netdev *hwsdev;
+       struct list_head *hwsdev_list;
+
+       hwsdev_list = nsim_dev_hwstats_get_list_head(hwstats, type);
+       if (WARN_ON(!hwsdev_list))
+               return;
+
+       hwsdev = nsim_dev_hwslist_find_hwsdev(hwsdev_list, dev->ifindex);
+       if (!hwsdev)
+               return;
+
+       list_del(&hwsdev->list);
+       nsim_dev_hwsdev_fini(hwsdev);
+}
+
+static void nsim_dev_hwstats_event_unregister(struct nsim_dev_hwstats *hwstats,
+                                             struct net_device *dev)
+{
+       mutex_lock(&hwstats->hwsdev_list_lock);
+       __nsim_dev_hwstats_event_unregister(hwstats, dev,
+                                           NETDEV_OFFLOAD_XSTATS_TYPE_L3);
+       mutex_unlock(&hwstats->hwsdev_list_lock);
+}
+
+static int nsim_dev_hwstats_event(struct nsim_dev_hwstats *hwstats,
+                                 struct net_device *dev,
+                                 unsigned long event, void *ptr)
+{
+       switch (event) {
+       case NETDEV_OFFLOAD_XSTATS_ENABLE:
+       case NETDEV_OFFLOAD_XSTATS_DISABLE:
+       case NETDEV_OFFLOAD_XSTATS_REPORT_USED:
+       case NETDEV_OFFLOAD_XSTATS_REPORT_DELTA:
+               return nsim_dev_hwstats_event_off_xstats(hwstats, dev,
+                                                        event, ptr);
+       case NETDEV_UNREGISTER:
+               nsim_dev_hwstats_event_unregister(hwstats, dev);
+               break;
+       }
+
+       return 0;
+}
+
+static int nsim_dev_netdevice_event(struct notifier_block *nb,
+                                   unsigned long event, void *ptr)
+{
+       struct net_device *dev = netdev_notifier_info_to_dev(ptr);
+       struct nsim_dev_hwstats *hwstats;
+       int err = 0;
+
+       hwstats = container_of(nb, struct nsim_dev_hwstats, netdevice_nb);
+       err = nsim_dev_hwstats_event(hwstats, dev, event, ptr);
+       if (err)
+               return notifier_from_errno(err);
+
+       return NOTIFY_OK;
+}
+
+static int
+nsim_dev_hwstats_enable_ifindex(struct nsim_dev_hwstats *hwstats,
+                               int ifindex,
+                               enum netdev_offload_xstats_type type,
+                               struct list_head *hwsdev_list)
+{
+       struct nsim_dev_hwstats_netdev *hwsdev;
+       struct nsim_dev *nsim_dev;
+       struct net_device *netdev;
+       bool notify = false;
+       struct net *net;
+       int err = 0;
+
+       nsim_dev = container_of(hwstats, struct nsim_dev, hwstats);
+       net = nsim_dev_net(nsim_dev);
+
+       rtnl_lock();
+       mutex_lock(&hwstats->hwsdev_list_lock);
+       hwsdev = nsim_dev_hwslist_find_hwsdev(hwsdev_list, ifindex);
+       if (hwsdev)
+               goto out_unlock_list;
+
+       netdev = dev_get_by_index(net, ifindex);
+       if (!netdev) {
+               err = -ENODEV;
+               goto out_unlock_list;
+       }
+
+       hwsdev = kzalloc(sizeof(*hwsdev), GFP_KERNEL);
+       if (!hwsdev) {
+               err = -ENOMEM;
+               goto out_put_netdev;
+       }
+
+       hwsdev->netdev = netdev;
+       list_add_tail(&hwsdev->list, hwsdev_list);
+       mutex_unlock(&hwstats->hwsdev_list_lock);
+
+       if (netdev_offload_xstats_enabled(netdev, type)) {
+               nsim_dev_hwsdev_enable(hwsdev, NULL);
+               notify = true;
+       }
+
+       if (notify)
+               rtnl_offload_xstats_notify(netdev);
+       rtnl_unlock();
+       return err;
+
+out_put_netdev:
+       dev_put(netdev);
+out_unlock_list:
+       mutex_unlock(&hwstats->hwsdev_list_lock);
+       rtnl_unlock();
+       return err;
+}
+
+static int
+nsim_dev_hwstats_disable_ifindex(struct nsim_dev_hwstats *hwstats,
+                                int ifindex,
+                                enum netdev_offload_xstats_type type,
+                                struct list_head *hwsdev_list)
+{
+       struct nsim_dev_hwstats_netdev *hwsdev;
+       int err = 0;
+
+       rtnl_lock();
+       mutex_lock(&hwstats->hwsdev_list_lock);
+       hwsdev = nsim_dev_hwslist_find_hwsdev(hwsdev_list, ifindex);
+       if (hwsdev)
+               list_del(&hwsdev->list);
+       mutex_unlock(&hwstats->hwsdev_list_lock);
+
+       if (!hwsdev) {
+               err = -ENOENT;
+               goto unlock_out;
+       }
+
+       if (netdev_offload_xstats_enabled(hwsdev->netdev, type)) {
+               netdev_offload_xstats_push_delta(hwsdev->netdev, type,
+                                                &hwsdev->stats);
+               rtnl_offload_xstats_notify(hwsdev->netdev);
+       }
+       nsim_dev_hwsdev_fini(hwsdev);
+
+unlock_out:
+       rtnl_unlock();
+       return err;
+}
+
+static int
+nsim_dev_hwstats_fail_ifindex(struct nsim_dev_hwstats *hwstats,
+                             int ifindex,
+                             enum netdev_offload_xstats_type type,
+                             struct list_head *hwsdev_list)
+{
+       struct nsim_dev_hwstats_netdev *hwsdev;
+       int err = 0;
+
+       mutex_lock(&hwstats->hwsdev_list_lock);
+
+       hwsdev = nsim_dev_hwslist_find_hwsdev(hwsdev_list, ifindex);
+       if (!hwsdev) {
+               err = -ENOENT;
+               goto err_hwsdev_list_unlock;
+       }
+
+       hwsdev->fail_enable = true;
+
+err_hwsdev_list_unlock:
+       mutex_unlock(&hwstats->hwsdev_list_lock);
+       return err;
+}
+
+enum nsim_dev_hwstats_do {
+       NSIM_DEV_HWSTATS_DO_DISABLE,
+       NSIM_DEV_HWSTATS_DO_ENABLE,
+       NSIM_DEV_HWSTATS_DO_FAIL,
+};
+
+struct nsim_dev_hwstats_fops {
+       const struct file_operations fops;
+       enum nsim_dev_hwstats_do action;
+       enum netdev_offload_xstats_type type;
+};
+
+static ssize_t
+nsim_dev_hwstats_do_write(struct file *file,
+                         const char __user *data,
+                         size_t count, loff_t *ppos)
+{
+       struct nsim_dev_hwstats *hwstats = file->private_data;
+       struct nsim_dev_hwstats_fops *hwsfops;
+       struct list_head *hwsdev_list;
+       int ifindex;
+       int err;
+
+       hwsfops = container_of(debugfs_real_fops(file),
+                              struct nsim_dev_hwstats_fops, fops);
+
+       err = kstrtoint_from_user(data, count, 0, &ifindex);
+       if (err)
+               return err;
+
+       hwsdev_list = nsim_dev_hwstats_get_list_head(hwstats, hwsfops->type);
+       if (WARN_ON(!hwsdev_list))
+               return -EINVAL;
+
+       switch (hwsfops->action) {
+       case NSIM_DEV_HWSTATS_DO_DISABLE:
+               err = nsim_dev_hwstats_disable_ifindex(hwstats, ifindex,
+                                                      hwsfops->type,
+                                                      hwsdev_list);
+               break;
+       case NSIM_DEV_HWSTATS_DO_ENABLE:
+               err = nsim_dev_hwstats_enable_ifindex(hwstats, ifindex,
+                                                     hwsfops->type,
+                                                     hwsdev_list);
+               break;
+       case NSIM_DEV_HWSTATS_DO_FAIL:
+               err = nsim_dev_hwstats_fail_ifindex(hwstats, ifindex,
+                                                   hwsfops->type,
+                                                   hwsdev_list);
+               break;
+       }
+       if (err)
+               return err;
+
+       return count;
+}
+
+#define NSIM_DEV_HWSTATS_FOPS(ACTION, TYPE)                    \
+       {                                                       \
+               .fops = {                                       \
+                       .open = simple_open,                    \
+                       .write = nsim_dev_hwstats_do_write,     \
+                       .llseek = generic_file_llseek,          \
+                       .owner = THIS_MODULE,                   \
+               },                                              \
+               .action = ACTION,                               \
+               .type = TYPE,                                   \
+       }
+
+static const struct nsim_dev_hwstats_fops nsim_dev_hwstats_l3_disable_fops =
+       NSIM_DEV_HWSTATS_FOPS(NSIM_DEV_HWSTATS_DO_DISABLE,
+                             NETDEV_OFFLOAD_XSTATS_TYPE_L3);
+
+static const struct nsim_dev_hwstats_fops nsim_dev_hwstats_l3_enable_fops =
+       NSIM_DEV_HWSTATS_FOPS(NSIM_DEV_HWSTATS_DO_ENABLE,
+                             NETDEV_OFFLOAD_XSTATS_TYPE_L3);
+
+static const struct nsim_dev_hwstats_fops nsim_dev_hwstats_l3_fail_fops =
+       NSIM_DEV_HWSTATS_FOPS(NSIM_DEV_HWSTATS_DO_FAIL,
+                             NETDEV_OFFLOAD_XSTATS_TYPE_L3);
+
+#undef NSIM_DEV_HWSTATS_FOPS
+
+int nsim_dev_hwstats_init(struct nsim_dev *nsim_dev)
+{
+       struct nsim_dev_hwstats *hwstats = &nsim_dev->hwstats;
+       struct net *net = nsim_dev_net(nsim_dev);
+       int err;
+
+       mutex_init(&hwstats->hwsdev_list_lock);
+       INIT_LIST_HEAD(&hwstats->l3_list);
+
+       hwstats->netdevice_nb.notifier_call = nsim_dev_netdevice_event;
+       err = register_netdevice_notifier_net(net, &hwstats->netdevice_nb);
+       if (err)
+               goto err_mutex_destroy;
+
+       hwstats->ddir = debugfs_create_dir("hwstats", nsim_dev->ddir);
+       if (IS_ERR(hwstats->ddir)) {
+               err = PTR_ERR(hwstats->ddir);
+               goto err_unregister_notifier;
+       }
+
+       hwstats->l3_ddir = debugfs_create_dir("l3", hwstats->ddir);
+       if (IS_ERR(hwstats->l3_ddir)) {
+               err = PTR_ERR(hwstats->l3_ddir);
+               goto err_remove_hwstats_recursive;
+       }
+
+       debugfs_create_file("enable_ifindex", 0600, hwstats->l3_ddir, hwstats,
+                           &nsim_dev_hwstats_l3_enable_fops.fops);
+       debugfs_create_file("disable_ifindex", 0600, hwstats->l3_ddir, hwstats,
+                           &nsim_dev_hwstats_l3_disable_fops.fops);
+       debugfs_create_file("fail_next_enable", 0600, hwstats->l3_ddir, hwstats,
+                           &nsim_dev_hwstats_l3_fail_fops.fops);
+
+       INIT_DELAYED_WORK(&hwstats->traffic_dw,
+                         &nsim_dev_hwstats_traffic_work);
+       schedule_delayed_work(&hwstats->traffic_dw,
+                             msecs_to_jiffies(NSIM_DEV_HWSTATS_TRAFFIC_MS));
+       return 0;
+
+err_remove_hwstats_recursive:
+       debugfs_remove_recursive(hwstats->ddir);
+err_unregister_notifier:
+       unregister_netdevice_notifier_net(net, &hwstats->netdevice_nb);
+err_mutex_destroy:
+       mutex_destroy(&hwstats->hwsdev_list_lock);
+       return err;
+}
+
+static void nsim_dev_hwsdev_list_wipe(struct nsim_dev_hwstats *hwstats,
+                                     enum netdev_offload_xstats_type type)
+{
+       struct nsim_dev_hwstats_netdev *hwsdev, *tmp;
+       struct list_head *hwsdev_list;
+
+       hwsdev_list = nsim_dev_hwstats_get_list_head(hwstats, type);
+       if (WARN_ON(!hwsdev_list))
+               return;
+
+       mutex_lock(&hwstats->hwsdev_list_lock);
+       list_for_each_entry_safe(hwsdev, tmp, hwsdev_list, list) {
+               list_del(&hwsdev->list);
+               nsim_dev_hwsdev_fini(hwsdev);
+       }
+       mutex_unlock(&hwstats->hwsdev_list_lock);
+}
+
+void nsim_dev_hwstats_exit(struct nsim_dev *nsim_dev)
+{
+       struct nsim_dev_hwstats *hwstats = &nsim_dev->hwstats;
+       struct net *net = nsim_dev_net(nsim_dev);
+
+       cancel_delayed_work_sync(&hwstats->traffic_dw);
+       debugfs_remove_recursive(hwstats->ddir);
+       unregister_netdevice_notifier_net(net, &hwstats->netdevice_nb);
+       nsim_dev_hwsdev_list_wipe(hwstats, NETDEV_OFFLOAD_XSTATS_TYPE_L3);
+       mutex_destroy(&hwstats->hwsdev_list_lock);
+}