/* $Id: VBoxNetAdp-linux.c $ */
/** @file
 * VBoxNetAdp - Virtual Network Adapter Driver (Host), Linux Specific Code.
 */

/*
 * Copyright (C) 2009-2023 Oracle and/or its affiliates.
 *
 * This file is part of VirtualBox base platform packages, as
 * available from https://www.virtualbox.org.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation, in version 3 of the
 * License.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see <https://www.gnu.org/licenses>.
 *
 * The contents of this file may alternatively be used under the terms
 * of the Common Development and Distribution License Version 1.0
 * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
 * in the VirtualBox distribution, in which case the provisions of the
 * CDDL are applicable instead of those of the GPL.
 *
 * You may elect to license modified versions of this file under the
 * terms and conditions of either the GPL or the CDDL or both.
 *
 * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
 */


/*********************************************************************************************************************************
*   Header Files                                                                                                                 *
*********************************************************************************************************************************/
#include "the-linux-kernel.h"
#include "version-generated.h"
#include "revision-generated.h"
#include "product-generated.h"
#include <linux/ethtool.h>
#include <linux/netdevice.h>
#include <linux/etherdevice.h>
#include <linux/miscdevice.h>

#define LOG_GROUP LOG_GROUP_NET_ADP_DRV
#include <VBox/log.h>
#include <iprt/errcore.h>
#include <iprt/process.h>
#include <iprt/initterm.h>
#include <iprt/mem.h>
#include <iprt/string.h>

/*
#include <iprt/assert.h>
#include <iprt/semaphore.h>
#include <iprt/spinlock.h>
#include <iprt/string.h>
#include <iprt/uuid.h>
#include <iprt/alloca.h>
*/

#define VBOXNETADP_OS_SPECFIC 1
#include "../VBoxNetAdpInternal.h"


/*********************************************************************************************************************************
*   Defined Constants And Macros                                                                                                 *
*********************************************************************************************************************************/
#define VBOXNETADP_LINUX_NAME      "vboxnet%d"
#define VBOXNETADP_CTL_DEV_NAME    "vboxnetctl"

#define VBOXNETADP_FROM_IFACE(iface) ((PVBOXNETADP) ifnet_softc(iface))

/** Set netdev MAC address. */
#if RTLNX_VER_MIN(5,17,0)
# define VBOX_DEV_ADDR_SET(dev, addr, len) dev_addr_mod(dev, 0, addr, len)
#else /* < 5.17.0 */
# define VBOX_DEV_ADDR_SET(dev, addr, len) memcpy(dev->dev_addr, addr, len)
#endif


/*********************************************************************************************************************************
*   Internal Functions                                                                                                           *
*********************************************************************************************************************************/
static int  __init VBoxNetAdpLinuxInit(void);
static void __exit VBoxNetAdpLinuxUnload(void);

static int VBoxNetAdpLinuxOpen(struct inode *pInode, struct file *pFilp);
static int VBoxNetAdpLinuxClose(struct inode *pInode, struct file *pFilp);
#if RTLNX_VER_MAX(2,6,36)
static int VBoxNetAdpLinuxIOCtl(struct inode *pInode, struct file *pFilp,
                                unsigned int uCmd, unsigned long ulArg);
#else  /* >= 2,6,36 */
static long VBoxNetAdpLinuxIOCtlUnlocked(struct file *pFilp,
                                         unsigned int uCmd, unsigned long ulArg);
#endif /* >= 2,6,36 */

static void vboxNetAdpEthGetDrvinfo(struct net_device *dev, struct ethtool_drvinfo *info);
#if RTLNX_VER_MIN(4,20,0)
static int vboxNetAdpEthGetLinkSettings(struct net_device *pNetDev, struct ethtool_link_ksettings *pLinkSettings);
#else  /* < 4,20,0 */
static int vboxNetAdpEthGetSettings(struct net_device *dev, struct ethtool_cmd *cmd);
#endif /* < 4,20,0 */


/*********************************************************************************************************************************
*   Global Variables                                                                                                             *
*********************************************************************************************************************************/
module_init(VBoxNetAdpLinuxInit);
module_exit(VBoxNetAdpLinuxUnload);

MODULE_AUTHOR(VBOX_VENDOR);
MODULE_DESCRIPTION(VBOX_PRODUCT " Network Adapter Driver");
MODULE_LICENSE("GPL");
#ifdef MODULE_VERSION
MODULE_VERSION(VBOX_VERSION_STRING " r" RT_XSTR(VBOX_SVN_REV) " (" RT_XSTR(INTNETTRUNKIFPORT_VERSION) ")");
#endif

/**
 * The (common) global data.
 */
static struct file_operations gFileOpsVBoxNetAdp =
{
    owner:      THIS_MODULE,
    open:       VBoxNetAdpLinuxOpen,
    release:    VBoxNetAdpLinuxClose,
#if RTLNX_VER_MAX(2,6,36)
    ioctl:      VBoxNetAdpLinuxIOCtl,
#else /* RTLNX_VER_MIN(2,6,36) */
    unlocked_ioctl: VBoxNetAdpLinuxIOCtlUnlocked,
#endif /* RTLNX_VER_MIN(2,6,36) */
};

/** The miscdevice structure. */
static struct miscdevice g_CtlDev =
{
    minor:      MISC_DYNAMIC_MINOR,
    name:       VBOXNETADP_CTL_DEV_NAME,
    fops:       &gFileOpsVBoxNetAdp,
# if RTLNX_VER_MAX(2,6,18)
    devfs_name: VBOXNETADP_CTL_DEV_NAME
# endif
};

# if RTLNX_VER_MIN(2,6,19)
static const struct ethtool_ops gEthToolOpsVBoxNetAdp =
# else
static struct ethtool_ops gEthToolOpsVBoxNetAdp =
# endif
{
    .get_drvinfo        = vboxNetAdpEthGetDrvinfo,
# if RTLNX_VER_MIN(4,20,0)
    .get_link_ksettings = vboxNetAdpEthGetLinkSettings,
# else
    .get_settings       = vboxNetAdpEthGetSettings,
# endif
    .get_link           = ethtool_op_get_link,
};


struct VBoxNetAdpPriv
{
    struct net_device_stats Stats;
};

typedef struct VBoxNetAdpPriv VBOXNETADPPRIV;
typedef VBOXNETADPPRIV *PVBOXNETADPPRIV;

static int vboxNetAdpLinuxOpen(struct net_device *pNetDev)
{
    netif_start_queue(pNetDev);
    return 0;
}

static int vboxNetAdpLinuxStop(struct net_device *pNetDev)
{
    netif_stop_queue(pNetDev);
    return 0;
}

static int vboxNetAdpLinuxXmit(struct sk_buff *pSkb, struct net_device *pNetDev)
{
    PVBOXNETADPPRIV pPriv = netdev_priv(pNetDev);

    /* Update the stats. */
    pPriv->Stats.tx_packets++;
    pPriv->Stats.tx_bytes += pSkb->len;
#if RTLNX_VER_MAX(2,6,31)
    /* Update transmission time stamp. */
    pNetDev->trans_start = jiffies;
#endif
    /* Nothing else to do, just free the sk_buff. */
    dev_kfree_skb(pSkb);
    return 0;
}

static struct net_device_stats *vboxNetAdpLinuxGetStats(struct net_device *pNetDev)
{
    PVBOXNETADPPRIV pPriv = netdev_priv(pNetDev);
    return &pPriv->Stats;
}


/* ethtool_ops::get_drvinfo */
static void vboxNetAdpEthGetDrvinfo(struct net_device *pNetDev, struct ethtool_drvinfo *info)
{
    PVBOXNETADPPRIV pPriv = netdev_priv(pNetDev);
    NOREF(pPriv);

    RTStrPrintf(info->driver, sizeof(info->driver),
                "%s", VBOXNETADP_NAME);

    /*
     * Would be nice to include VBOX_SVN_REV, but it's not available
     * here.  Use file's svn revision via svn keyword?
     */
    RTStrPrintf(info->version, sizeof(info->version),
                "%s", VBOX_VERSION_STRING);

    RTStrPrintf(info->fw_version, sizeof(info->fw_version),
                "0x%08X", INTNETTRUNKIFPORT_VERSION);

    RTStrPrintf(info->bus_info, sizeof(info->driver),
                "N/A");
}


# if RTLNX_VER_MIN(4,20,0)
/* ethtool_ops::get_link_ksettings */
static int vboxNetAdpEthGetLinkSettings(struct net_device *pNetDev, struct ethtool_link_ksettings *pLinkSettings)
{
    /* We just need to set field we care for, the rest is done by ethtool_get_link_ksettings() helper in ethtool. */
    ethtool_link_ksettings_zero_link_mode(pLinkSettings, supported);
    ethtool_link_ksettings_zero_link_mode(pLinkSettings, advertising);
    ethtool_link_ksettings_zero_link_mode(pLinkSettings, lp_advertising);
    pLinkSettings->base.speed       = SPEED_10;
    pLinkSettings->base.duplex      = DUPLEX_FULL;
    pLinkSettings->base.port        = PORT_TP;
    pLinkSettings->base.phy_address = 0;
    pLinkSettings->base.transceiver = XCVR_INTERNAL;
    pLinkSettings->base.autoneg     = AUTONEG_DISABLE;
    return 0;
}
#else /* RTLNX_VER_MAX(4,20,0) */
/* ethtool_ops::get_settings */
static int vboxNetAdpEthGetSettings(struct net_device *pNetDev, struct ethtool_cmd *cmd)
{
    cmd->supported      = 0;
    cmd->advertising    = 0;
#if RTLNX_VER_MIN(2,6,27)
    ethtool_cmd_speed_set(cmd, SPEED_10);
#else
    cmd->speed          = SPEED_10;
#endif
    cmd->duplex         = DUPLEX_FULL;
    cmd->port           = PORT_TP;
    cmd->phy_address    = 0;
    cmd->transceiver    = XCVR_INTERNAL;
    cmd->autoneg        = AUTONEG_DISABLE;
    cmd->maxtxpkt       = 0;
    cmd->maxrxpkt       = 0;
    return 0;
}
#endif /* RTLNX_VER_MAX(4,20,0) */


#if RTLNX_VER_MIN(2,6,29)
static const struct net_device_ops vboxNetAdpNetdevOps = {
    .ndo_open               = vboxNetAdpLinuxOpen,
    .ndo_stop               = vboxNetAdpLinuxStop,
    .ndo_start_xmit         = vboxNetAdpLinuxXmit,
    .ndo_get_stats          = vboxNetAdpLinuxGetStats
};
#endif

static void vboxNetAdpNetDevInit(struct net_device *pNetDev)
{
    PVBOXNETADPPRIV pPriv;

    ether_setup(pNetDev);
#if RTLNX_VER_MIN(2,6,29)
    pNetDev->netdev_ops = &vboxNetAdpNetdevOps;
#else /* RTLNX_VER_MAX(2,6,29) */
    pNetDev->open = vboxNetAdpLinuxOpen;
    pNetDev->stop = vboxNetAdpLinuxStop;
    pNetDev->hard_start_xmit = vboxNetAdpLinuxXmit;
    pNetDev->get_stats = vboxNetAdpLinuxGetStats;
#endif /* RTLNX_VER_MAX(2,6,29) */
#if RTLNX_VER_MIN(4,10,0)
    pNetDev->max_mtu = 65536;
    pNetDev->features =   NETIF_F_TSO
                        | NETIF_F_TSO6
                        | NETIF_F_TSO_ECN
                        | NETIF_F_SG
                        | NETIF_F_HW_CSUM;
#endif /* RTLNX_VER_MIN(4,10,0) */

    pNetDev->ethtool_ops = &gEthToolOpsVBoxNetAdp;

    pPriv = netdev_priv(pNetDev);
    memset(pPriv, 0, sizeof(*pPriv));
}


int vboxNetAdpOsCreate(PVBOXNETADP pThis, PCRTMAC pMACAddress)
{
    int rc = VINF_SUCCESS;
    struct net_device *pNetDev;

    /* No need for private data. */
    pNetDev = alloc_netdev(sizeof(VBOXNETADPPRIV),
                           pThis->szName[0] ? pThis->szName : VBOXNETADP_LINUX_NAME,
#if RTLNX_VER_MIN(3,17,0)
                           NET_NAME_UNKNOWN,
#endif
                           vboxNetAdpNetDevInit);
    if (pNetDev)
    {
        int err;

        if (pNetDev->dev_addr)
        {
            VBOX_DEV_ADDR_SET(pNetDev, pMACAddress, ETH_ALEN);
            Log2(("vboxNetAdpOsCreate: pNetDev->dev_addr = %.6Rhxd\n", pNetDev->dev_addr));

            /*
             * We treat presence of VBoxNetFlt filter as our "carrier",
             * see vboxNetFltSetLinkState().
             *
             * operstates.txt: "On device allocation, networking core
             * sets the flags equivalent to netif_carrier_ok() and
             * !netif_dormant()" - so turn carrier off here.
             */
            netif_carrier_off(pNetDev);

            err = register_netdev(pNetDev);
            if (!err)
            {
                strncpy(pThis->szName, pNetDev->name, sizeof(pThis->szName));
                pThis->szName[sizeof(pThis->szName) - 1] = '\0';
                pThis->u.s.pNetDev = pNetDev;
                Log2(("vboxNetAdpOsCreate: pThis=%p pThis->szName = %p\n", pThis, pThis->szName));
                return VINF_SUCCESS;
            }
        }
        else
        {
            LogRel(("VBoxNetAdp: failed to set MAC address (dev->dev_addr == NULL)\n"));
            err = EFAULT;
        }
        free_netdev(pNetDev);
        rc = RTErrConvertFromErrno(err);
    }
    return rc;
}

void vboxNetAdpOsDestroy(PVBOXNETADP pThis)
{
    struct net_device *pNetDev = pThis->u.s.pNetDev;
    AssertPtr(pThis->u.s.pNetDev);

    pThis->u.s.pNetDev = NULL;
    unregister_netdev(pNetDev);
    free_netdev(pNetDev);
}

/**
 * Device open. Called on open /dev/vboxnetctl
 *
 * @param   pInode      Pointer to inode info structure.
 * @param   pFilp       Associated file pointer.
 */
static int VBoxNetAdpLinuxOpen(struct inode *pInode, struct file *pFilp)
{
    Log(("VBoxNetAdpLinuxOpen: pid=%d/%d %s\n", RTProcSelf(), current->pid, current->comm));

#ifdef VBOX_WITH_HARDENING
    /*
     * Only root is allowed to access the device, enforce it!
     */
    if (!capable(CAP_SYS_ADMIN))
    {
        Log(("VBoxNetAdpLinuxOpen: admin privileges required!\n"));
        return -EPERM;
    }
#endif

    return 0;
}


/**
 * Close device.
 *
 * @param   pInode      Pointer to inode info structure.
 * @param   pFilp       Associated file pointer.
 */
static int VBoxNetAdpLinuxClose(struct inode *pInode, struct file *pFilp)
{
    Log(("VBoxNetAdpLinuxClose: pid=%d/%d %s\n",
         RTProcSelf(), current->pid, current->comm));
    pFilp->private_data = NULL;
    return 0;
}

/**
 * Device I/O Control entry point.
 *
 * @param   pFilp       Associated file pointer.
 * @param   uCmd        The function specified to ioctl().
 * @param   ulArg       The argument specified to ioctl().
 */
#if RTLNX_VER_MAX(2,6,36)
static int VBoxNetAdpLinuxIOCtl(struct inode *pInode, struct file *pFilp,
                                unsigned int uCmd, unsigned long ulArg)
#else /* RTLNX_VER_MIN(2,6,36) */
static long VBoxNetAdpLinuxIOCtlUnlocked(struct file *pFilp,
                                         unsigned int uCmd, unsigned long ulArg)
#endif /* RTLNX_VER_MIN(2,6,36) */
{
    VBOXNETADPREQ Req;
    PVBOXNETADP pAdp;
    int rc;
    char *pszName = NULL;

    Log(("VBoxNetAdpLinuxIOCtl: param len %#x; uCmd=%#x; add=%#x\n", _IOC_SIZE(uCmd), uCmd, VBOXNETADP_CTL_ADD));
    if (RT_UNLIKELY(_IOC_SIZE(uCmd) != sizeof(Req))) /* paranoia */
    {
        Log(("VBoxNetAdpLinuxIOCtl: bad ioctl sizeof(Req)=%#x _IOC_SIZE=%#x; uCmd=%#x.\n", sizeof(Req), _IOC_SIZE(uCmd), uCmd));
        return -EINVAL;
    }

    switch (uCmd)
    {
        case VBOXNETADP_CTL_ADD:
            Log(("VBoxNetAdpLinuxIOCtl: _IOC_DIR(uCmd)=%#x; IOC_OUT=%#x\n", _IOC_DIR(uCmd), IOC_OUT));
            if (RT_UNLIKELY(copy_from_user(&Req, (void *)ulArg, sizeof(Req))))
            {
                Log(("VBoxNetAdpLinuxIOCtl: copy_from_user(,%#lx,) failed; uCmd=%#x.\n", ulArg, uCmd));
                return -EFAULT;
            }
            Log(("VBoxNetAdpLinuxIOCtl: Add %s\n", Req.szName));

            if (Req.szName[0])
            {
                pAdp = vboxNetAdpFindByName(Req.szName);
                if (pAdp)
                {
                    Log(("VBoxNetAdpLinuxIOCtl: '%s' already exists\n", Req.szName));
                    return -EINVAL;
                }
                pszName = Req.szName;
            }
            rc = vboxNetAdpCreate(&pAdp, pszName);
            if (RT_FAILURE(rc))
            {
                Log(("VBoxNetAdpLinuxIOCtl: vboxNetAdpCreate -> %Rrc\n", rc));
                return -(rc == VERR_OUT_OF_RESOURCES ? ENOMEM : EINVAL);
            }

            Assert(strlen(pAdp->szName) < sizeof(Req.szName));
            strncpy(Req.szName, pAdp->szName, sizeof(Req.szName) - 1);
            Req.szName[sizeof(Req.szName) - 1] = '\0';

            if (RT_UNLIKELY(copy_to_user((void *)ulArg, &Req, sizeof(Req))))
            {
                /* this is really bad! */
                /** @todo remove the adapter again? */
                printk(KERN_ERR "VBoxNetAdpLinuxIOCtl: copy_to_user(%#lx,,%#zx); uCmd=%#x!\n", ulArg, sizeof(Req), uCmd);
                return -EFAULT;
            }
            Log(("VBoxNetAdpLinuxIOCtl: Successfully added '%s'\n", Req.szName));
            break;

        case VBOXNETADP_CTL_REMOVE:
            if (RT_UNLIKELY(copy_from_user(&Req, (void *)ulArg, sizeof(Req))))
            {
                Log(("VBoxNetAdpLinuxIOCtl: copy_from_user(,%#lx,) failed; uCmd=%#x.\n", ulArg, uCmd));
                return -EFAULT;
            }
            Log(("VBoxNetAdpLinuxIOCtl: Remove %s\n", Req.szName));

            pAdp = vboxNetAdpFindByName(Req.szName);
            if (!pAdp)
            {
                Log(("VBoxNetAdpLinuxIOCtl: '%s' not found\n", Req.szName));
                return -EINVAL;
            }

            rc = vboxNetAdpDestroy(pAdp);
            if (RT_FAILURE(rc))
            {
                Log(("VBoxNetAdpLinuxIOCtl: vboxNetAdpDestroy('%s') -> %Rrc\n", Req.szName, rc));
                return -EINVAL;
            }
            Log(("VBoxNetAdpLinuxIOCtl: Successfully removed '%s'\n", Req.szName));
            break;

        default:
            printk(KERN_ERR "VBoxNetAdpLinuxIOCtl: unknown command %x.\n", uCmd);
            return -EINVAL;
    }

    return 0;
}

int  vboxNetAdpOsInit(PVBOXNETADP pThis)
{
    /*
     * Init linux-specific members.
     */
    pThis->u.s.pNetDev = NULL;

    return VINF_SUCCESS;
}



/**
 * Initialize module.
 *
 * @returns appropriate status code.
 */
static int __init VBoxNetAdpLinuxInit(void)
{
    int rc;
    /*
     * Initialize IPRT.
     */
    rc = RTR0Init(0);
    if (RT_SUCCESS(rc))
    {
        Log(("VBoxNetAdpLinuxInit\n"));

        rc = vboxNetAdpInit();
        if (RT_SUCCESS(rc))
        {
            rc = misc_register(&g_CtlDev);
            if (rc)
            {
                printk(KERN_ERR "VBoxNetAdp: Can't register " VBOXNETADP_CTL_DEV_NAME " device! rc=%d\n", rc);
                return rc;
            }
            LogRel(("VBoxNetAdp: Successfully started.\n"));
            return 0;
        }
        else
            LogRel(("VBoxNetAdp: failed to register vboxnet0 device (rc=%d)\n", rc));
    }
    else
        LogRel(("VBoxNetAdp: failed to initialize IPRT (rc=%d)\n", rc));

    return -RTErrConvertToErrno(rc);
}


/**
 * Unload the module.
 *
 * @todo We have to prevent this if we're busy!
 */
static void __exit VBoxNetAdpLinuxUnload(void)
{
    Log(("VBoxNetAdpLinuxUnload\n"));

    /*
     * Undo the work done during start (in reverse order).
     */

    vboxNetAdpShutdown();
    /* Remove control device */
    misc_deregister(&g_CtlDev);

    RTR0Term();

    Log(("VBoxNetAdpLinuxUnload - done\n"));
}

