Adding IOCTL interface to networking loopback device driver

The loopback driver was introduced to readers in this article (here). We will now add an IOCTL interface to the loopback network device driver.

The netdevice operations structure net_device_ops provides a function registration for IOCTLs. In earlier versions of the Linux kernel, the OP callback in the net_device_ops structure was ndo_do_ioctl. However, this callback is now deprecated in recent kernels (since Linux 5.14 release). The net_device_ops callback that is now defined is ndo_siocdevprivate (since Linux 5.15 release).

The below code provides the ioctl interface function registration and a single ioctl to reset the statistics.

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/netdevice.h>
#include <linux/etherdevice.h>
#include <linux/skbuff.h>
#include <linux/init.h>
#include <linux/uaccess.h>
#include <linux/sockios.h>

#define DRV_NAME "vivek_net"

MODULE_AUTHOR("Vivekananda Uppunda");
MODULE_DESCRIPTION("Virtual netdev with software buffer management");
MODULE_LICENSE("GPL");

#define VNET_IOCTL_RESET_STATS   (SIOCDEVPRIVATE + 0)

/* -------------------------------------------------- */
/* Private data */
/* -------------------------------------------------- */
struct vnet_priv {
    struct sk_buff_head tx_queue;  /* software TX buffer */
};

/* -------------------------------------------------- */
/* Software RX processing (simple loopback) */
/* -------------------------------------------------- */
static void vnet_process_tx_queue(struct net_device *dev)
{
    struct vnet_priv *priv = netdev_priv(dev);
    struct sk_buff *skb;

    while ((skb = skb_dequeue(&priv->tx_queue)) != NULL) {
        /* Convert TX skb into RX skb */
        skb->dev = dev;
        skb->protocol = eth_type_trans(skb, dev);
        skb->ip_summed = CHECKSUM_UNNECESSARY;

        dev->stats.rx_packets++;
        dev->stats.rx_bytes += skb->len;

        netif_rx(skb);
    }
}

/* -------------------------------------------------- */
/* IOCTL function */
/* -------------------------------------------------- */
static int vnet_siocdevprivate(struct net_device *dev,
                               struct ifreq *ifr,
			       void __user *data,
                               int cmd)
{
    pr_info("vnet_siocdevprivate\n");

    switch (cmd) {
    case VNET_IOCTL_RESET_STATS:
        memset(&dev->stats, 0, sizeof(dev->stats));
        pr_info("%s: stats reset via ioctl\n", dev->name);
        break;

    default:
        pr_info("%s: default not supported\n", dev->name);
        return -EOPNOTSUPP;
    }

    return 0;
}

/* -------------------------------------------------- */
/* Transmit function */
/* -------------------------------------------------- */
static netdev_tx_t vnet_start_xmit(struct sk_buff *skb,
                                   struct net_device *dev)
{
    struct vnet_priv *priv = netdev_priv(dev);

    dev->stats.tx_packets++;
    dev->stats.tx_bytes += skb->len;

    /* Queue packet into software buffer */
    skb_queue_tail(&priv->tx_queue, skb);

    /* Immediately process queued packets (simple model) */
    vnet_process_tx_queue(dev);

    return NETDEV_TX_OK;
}

/* -------------------------------------------------- */
/* Open / Close */
/* -------------------------------------------------- */
static int vnet_open(struct net_device *dev)
{
    struct vnet_priv *priv = netdev_priv(dev);

    skb_queue_head_init(&priv->tx_queue);
    netif_start_queue(dev);

    pr_info("%s: device opened\n", dev->name);
    return 0;
}

static int vnet_stop(struct net_device *dev)
{
    struct vnet_priv *priv = netdev_priv(dev);
    struct sk_buff *skb;

    netif_stop_queue(dev);

    /* Flush software queue */
    while ((skb = skb_dequeue(&priv->tx_queue)) != NULL)
        dev_kfree_skb(skb);

    pr_info("%s: device closed\n", dev->name);
    return 0;
}

/* -------------------------------------------------- */
/* net_device operations */
/* -------------------------------------------------- */
static const struct net_device_ops vnet_netdev_ops = {
    .ndo_open             = vnet_open,
    .ndo_stop             = vnet_stop,
    .ndo_start_xmit       = vnet_start_xmit,
    .ndo_siocdevprivate   = vnet_siocdevprivate,
};

/* -------------------------------------------------- */
/* Device setup */
/* -------------------------------------------------- */
static void vnet_setup(struct net_device *dev)
{
    ether_setup(dev);

    dev->netdev_ops = &vnet_netdev_ops;
    dev->flags |= IFF_NOARP;
    dev->features |= NETIF_F_HW_CSUM;

    eth_hw_addr_random(dev);
}

/* -------------------------------------------------- */
/* Module init / exit */
/* -------------------------------------------------- */
static struct net_device *vnet_dev;

static int __init vnet_init(void)
{
    int ret;

    vnet_dev = alloc_netdev(sizeof(struct vnet_priv),
                            DRV_NAME"%d",
                            NET_NAME_UNKNOWN,
                            vnet_setup);
    if (!vnet_dev)
        return -ENOMEM;

    ret = register_netdev(vnet_dev);
    if (ret) {
        free_netdev(vnet_dev);
        return ret;
    }

    pr_info(DRV_NAME ": virtual network device registered\n");
    return 0;
}

static void __exit vnet_exit(void)
{
    unregister_netdev(vnet_dev);
    free_netdev(vnet_dev);
    pr_info(DRV_NAME ": module unloaded\n");
}

module_init(vnet_init);
module_exit(vnet_exit);

The above code does the following.

  1. Registers a loopback networking device
  2. Registers an ioctl callback in net_device_ops structure vnet_siocdevprivate
  3. The IOCTL created is VNET_IOCTL_RESET_STATS
    • It is used to reset the statistics for the network device

We will write a sample program which sends a reset command to the virtual loopback network driver in the next article and discuss the functioning of the same.

Sample application to send IOCTL to virtual loopback networking driver

Leave a Reply

Your email address will not be published. Required fields are marked *