/* $Id: VBoxNetAdpCtl.cpp $ */
/** @file
 * Apps - VBoxAdpCtl, Configuration tool for vboxnetX adapters.
 */

/*
 * Copyright (C) 2009-2012 Oracle Corporation
 *
 * This file is part of VirtualBox Open Source Edition (OSE), as
 * available from http://www.virtualbox.org. This file is free software;
 * you can redistribute it and/or modify it under the terms of the GNU
 * General Public License (GPL) as published by the Free Software
 * Foundation, in version 2 as it comes in the "COPYING" file of the
 * VirtualBox OSE distribution. VirtualBox OSE is distributed in the
 * hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
 */



/*******************************************************************************
*   Header Files                                                               *
*******************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <fcntl.h>
#ifdef RT_OS_LINUX
# include <net/if.h>
# include <linux/types.h>
/* Older versions of ethtool.h rely on these: */
typedef unsigned long long u64;
typedef __uint32_t u32;
typedef __uint16_t u16;
typedef __uint8_t u8;
# include <linux/ethtool.h>
# include <linux/sockios.h>
#endif
#ifdef RT_OS_SOLARIS
# include <sys/ioccom.h>
#endif

/** @todo Error codes must be moved to some header file */
#define ADPCTLERR_BAD_NAME         2
#define ADPCTLERR_NO_CTL_DEV       3
#define ADPCTLERR_IOCTL_FAILED     4
#define ADPCTLERR_SOCKET_FAILED    5

/** @todo These are duplicates from src/VBox/HostDrivers/VBoxNetAdp/VBoxNetAdpInternal.h */
#define VBOXNETADP_CTL_DEV_NAME    "/dev/vboxnetctl"
#define VBOXNETADP_MAX_INSTANCES   128
#define VBOXNETADP_NAME            "vboxnet"
#define VBOXNETADP_MAX_NAME_LEN    32
#define VBOXNETADP_CTL_ADD   _IOWR('v', 1, VBOXNETADPREQ)
#define VBOXNETADP_CTL_REMOVE _IOW('v', 2, VBOXNETADPREQ)
typedef struct VBoxNetAdpReq
{
    char szName[VBOXNETADP_MAX_NAME_LEN];
} VBOXNETADPREQ;
typedef VBOXNETADPREQ *PVBOXNETADPREQ;

#define VBOXADPCTL_IFCONFIG_PATH1 "/sbin/ifconfig"
#define VBOXADPCTL_IFCONFIG_PATH2 "/bin/ifconfig"
static char *g_pszIfConfig;

#if defined(RT_OS_LINUX)
# define VBOXADPCTL_DEL_CMD "del"
# define VBOXADPCTL_ADD_CMD "add"
#elif defined(RT_OS_SOLARIS)
# define VBOXADPCTL_DEL_CMD "removeif"
# define VBOXADPCTL_ADD_CMD "addif"
#else
# define VBOXADPCTL_DEL_CMD "delete"
# define VBOXADPCTL_ADD_CMD "add"
#endif

static void showUsage(void)
{
    fprintf(stderr, "Usage: VBoxNetAdpCtl <adapter> <address> ([netmask <address>] | remove)\n");
    fprintf(stderr, "     | VBoxNetAdpCtl [<adapter>] add\n");
    fprintf(stderr, "     | VBoxNetAdpCtl <adapter> remove\n");
}

static void setPathIfConfig(void)
{
    struct stat s;
    if (   !stat(VBOXADPCTL_IFCONFIG_PATH1, &s)
        && S_ISREG(s.st_mode))
        g_pszIfConfig = (char*)VBOXADPCTL_IFCONFIG_PATH1;
    else
        g_pszIfConfig = (char*)VBOXADPCTL_IFCONFIG_PATH2;
}

static int executeIfconfig(const char *pcszAdapterName, const char *pcszArg1,
                           const char *pcszArg2 = NULL,
                           const char *pcszArg3 = NULL,
                           const char *pcszArg4 = NULL,
                           const char *pcszArg5 = NULL)
{
    const char * const argv[] =
    {
        g_pszIfConfig,
        pcszAdapterName,
        pcszArg1, /* [address family] */
        pcszArg2, /* address */
        pcszArg3, /* ['netmask'] */
        pcszArg4, /* [network mask] */
        pcszArg5, /* [network mask] */
        NULL  /* terminator */
    };
    char * const envp[] = { (char*)"LC_ALL=C", NULL };
    int rc = EXIT_SUCCESS;
    pid_t childPid = fork();
    switch (childPid)
    {
        case -1: /* Something went wrong. */
            perror("fork() failed");
            rc = EXIT_FAILURE;
            break;
        case 0: /* Child process. */
            if (execve(argv[0], (char * const*)argv, envp) == -1)
                rc = EXIT_FAILURE;
            break;
        default: /* Parent process. */
            waitpid(childPid, &rc, 0);
            break;
    }

    return rc;
}

#define MAX_ADDRESSES 128
#define MAX_ADDRLEN   64

static bool removeAddresses(char *pszAdapterName)
{
    char szBuf[1024];
    char aszAddresses[MAX_ADDRESSES][MAX_ADDRLEN];
    int rc;
    int fds[2];
    char * const argv[] = { g_pszIfConfig, pszAdapterName, NULL };
    char * const envp[] = { (char*)"LC_ALL=C", NULL };

    memset(aszAddresses, 0, sizeof(aszAddresses));

    rc = pipe(fds);
    if (rc < 0)
        return false;

    pid_t pid = fork();
    if (pid < 0)
        return false;

    if (pid == 0)
    {
        /* child */
        close(fds[0]);
        close(STDOUT_FILENO);
        rc = dup2(fds[1], STDOUT_FILENO);
        if (rc >= 0)
            execve(argv[0], argv, envp);
        return false;
    }

    /* parent */
    close(fds[1]);
    FILE *fp = fdopen(fds[0], "r");
    if (!fp)
        return false;

    int cAddrs;
    for (cAddrs = 0; cAddrs < MAX_ADDRESSES && fgets(szBuf, sizeof(szBuf), fp);)
    {
        int cbSkipWS = strspn(szBuf, " \t");
        char *pszWord = strtok(szBuf + cbSkipWS, " ");
        /* We are concerned with IPv6 address lines only. */
        if (!pszWord || strcmp(pszWord, "inet6"))
            continue;
#ifdef RT_OS_LINUX
        pszWord = strtok(NULL, " ");
        /* Skip "addr:". */
        if (!pszWord || strcmp(pszWord, "addr:"))
            continue;
#endif
        pszWord = strtok(NULL, " ");
        /* Skip link-local addresses. */
        if (!pszWord || !strncmp(pszWord, "fe80", 4))
            continue;
        strncpy(aszAddresses[cAddrs++], pszWord, MAX_ADDRLEN-1);
    }
    fclose(fp);

    for (int i = 0; i < cAddrs; i++)
    {
        if (executeIfconfig(pszAdapterName, "inet6",
                            VBOXADPCTL_DEL_CMD, aszAddresses[i]) != EXIT_SUCCESS)
            return false;
    }

    return true;
}

static int doIOCtl(unsigned long uCmd, VBOXNETADPREQ *pReq)
{
    int fd = open(VBOXNETADP_CTL_DEV_NAME, O_RDWR);
    if (fd == -1)
    {
        fprintf(stderr, "VBoxNetAdpCtl: Error while %s %s: ",
                uCmd == VBOXNETADP_CTL_REMOVE ? "removing" : "adding",
                pReq->szName[0] ? pReq->szName : "new interface");
        perror("failed to open " VBOXNETADP_CTL_DEV_NAME);
        return ADPCTLERR_NO_CTL_DEV;
    }

    int rc = ioctl(fd, uCmd, pReq);
    if (rc == -1)
    {
        fprintf(stderr, "VBoxNetAdpCtl: Error while %s %s: ",
                uCmd == VBOXNETADP_CTL_REMOVE ? "removing" : "adding",
                pReq->szName[0] ? pReq->szName : "new interface");
        perror("VBoxNetAdpCtl: ioctl failed for " VBOXNETADP_CTL_DEV_NAME);
        rc = ADPCTLERR_IOCTL_FAILED;
    }

    close(fd);

    return rc;
}

static int checkAdapterName(const char *pcszNameIn, char *pszNameOut)
{
    int iAdapterIndex = -1;

    if (   strlen(pcszNameIn) >= VBOXNETADP_MAX_NAME_LEN
        || sscanf(pcszNameIn, "vboxnet%d", &iAdapterIndex) != 1
        || iAdapterIndex < 0 || iAdapterIndex >= VBOXNETADP_MAX_INSTANCES )
    {
        fprintf(stderr, "VBoxNetAdpCtl: Setting configuration for '%s' is not supported.\n", pcszNameIn);
        return ADPCTLERR_BAD_NAME;
    }
    sprintf(pszNameOut, "vboxnet%d", iAdapterIndex);
    if (strcmp(pszNameOut, pcszNameIn))
    {
        fprintf(stderr, "VBoxNetAdpCtl: Invalid adapter name '%s'.\n", pcszNameIn);
        return ADPCTLERR_BAD_NAME;
    }

    return 0;
}

int main(int argc, char *argv[])
{
    char szAdapterName[VBOXNETADP_MAX_NAME_LEN];
    char *pszAdapterName = NULL;
    const char *pszAddress = NULL;
    const char *pszNetworkMask = NULL;
    const char *pszOption = NULL;
    int rc = EXIT_SUCCESS;
    bool fRemove = false;
    VBOXNETADPREQ Req;

    setPathIfConfig();

    switch (argc)
    {
        case 5:
        {
            /* Add a netmask to existing interface */
            if (strcmp("netmask", argv[3]))
            {
                fprintf(stderr, "Invalid argument: %s\n\n", argv[3]);
                showUsage();
                return 1;
            }
            pszOption = "netmask";
            pszNetworkMask = argv[4];
            pszAdapterName = argv[1];
            pszAddress = argv[2];
            break;
        }

        case 4:
        {
            /* Remove a single address from existing interface */
            if (strcmp("remove", argv[3]))
            {
                fprintf(stderr, "Invalid argument: %s\n\n", argv[3]);
                showUsage();
                return 1;
            }
            fRemove = true;
            pszAdapterName = argv[1];
            pszAddress = argv[2];
            break;
        }

        case 3:
        {
            pszAdapterName = argv[1];
            memset(&Req, '\0', sizeof(Req));
#ifdef RT_OS_LINUX
            if (strcmp("speed", argv[2]) == 0)
            {
                /*
                 * This ugly hack is needed for retrieving the link speed on
                 * pre-2.6.33 kernels (see @bugref{6345}).
                 */
                if (strlen(pszAdapterName) >= IFNAMSIZ)
                {
                    showUsage();
                    return -1;
                }
                struct ifreq IfReq;
                struct ethtool_value EthToolVal;
                struct ethtool_cmd EthToolReq;
                int fd = socket(AF_INET, SOCK_DGRAM, 0);
                if (fd < 0)
                {
                    fprintf(stderr, "VBoxNetAdpCtl: Error while retrieving link "
                            "speed for %s: ", pszAdapterName);
                    perror("VBoxNetAdpCtl: failed to open control socket");
                    return ADPCTLERR_SOCKET_FAILED;
                }
                /* Get link status first. */
                memset(&EthToolVal, 0, sizeof(EthToolVal));
                memset(&IfReq, 0, sizeof(IfReq));
                snprintf(IfReq.ifr_name, sizeof(IfReq.ifr_name), "%s", pszAdapterName);

                EthToolVal.cmd = ETHTOOL_GLINK;
                IfReq.ifr_data = (caddr_t)&EthToolVal;
                rc = ioctl(fd, SIOCETHTOOL, &IfReq);
                if (rc == 0)
                {
                    if (EthToolVal.data)
                    {
                        memset(&IfReq, 0, sizeof(IfReq));
                        snprintf(IfReq.ifr_name, sizeof(IfReq.ifr_name), "%s", pszAdapterName);
                        EthToolReq.cmd = ETHTOOL_GSET;
                        IfReq.ifr_data = (caddr_t)&EthToolReq;
                        rc = ioctl(fd, SIOCETHTOOL, &IfReq);
                        if (rc == 0)
                        {
                            printf("%u", EthToolReq.speed);
                        }
                        else
                        {
                            fprintf(stderr, "VBoxNetAdpCtl: Error while retrieving link "
                                    "speed for %s: ", pszAdapterName);
                            perror("VBoxNetAdpCtl: ioctl failed");
                            rc = ADPCTLERR_IOCTL_FAILED;
                        }
                    }
                    else
                        printf("0");
                }
                else
                {
                    fprintf(stderr, "VBoxNetAdpCtl: Error while retrieving link "
                            "status for %s: ", pszAdapterName);
                    perror("VBoxNetAdpCtl: ioctl failed");
                    rc = ADPCTLERR_IOCTL_FAILED;
                }

                close(fd);
                return rc;
            }
#endif
            rc = checkAdapterName(pszAdapterName, szAdapterName);
            if (rc)
                return rc;
            snprintf(Req.szName, sizeof(Req.szName), "%s", szAdapterName);
            pszAddress = argv[2];
            if (strcmp("remove", pszAddress) == 0)
            {
                /* Remove an existing interface */
#ifdef RT_OS_SOLARIS
                return 1;
#else
                return doIOCtl(VBOXNETADP_CTL_REMOVE, &Req);
#endif
            }
            else if (strcmp("add", pszAddress) == 0)
            {
                /* Create an interface with given name */
#ifdef RT_OS_SOLARIS
                return 1;
#else
                rc = doIOCtl(VBOXNETADP_CTL_ADD, &Req);
                if (rc == 0)
                    puts(Req.szName);
#endif
                return rc;
            }
            break;
        }

        case 2:
        {
            /* Create a new interface */
            if (strcmp("add", argv[1]) == 0)
            {
#ifdef RT_OS_SOLARIS
                return 1;
#else
                memset(&Req, '\0', sizeof(Req));
                rc = doIOCtl(VBOXNETADP_CTL_ADD, &Req);
                if (rc == 0)
                    puts(Req.szName);
#endif
                return rc;
            }
            /* Fall through */
        }

        default:
            fprintf(stderr, "Invalid number of arguments.\n\n");
            /* Fall through */
        case 1:
            showUsage();
            return 1;
    }

    rc = checkAdapterName(pszAdapterName, szAdapterName);
    if (rc)
        return rc;

    pszAdapterName = szAdapterName;

    if (fRemove)
    {
        if (strchr(pszAddress, ':'))
            rc = executeIfconfig(pszAdapterName, "inet6", VBOXADPCTL_DEL_CMD, pszAddress);
        else
        {
#if defined(RT_OS_LINUX)
            rc = executeIfconfig(pszAdapterName, "0.0.0.0");
#else
            rc = executeIfconfig(pszAdapterName, VBOXADPCTL_DEL_CMD, pszAddress);
#endif

#ifdef RT_OS_SOLARIS
            /* On Solaris we can unplumb the ipv4 interface */
            executeIfconfig(pszAdapterName, "inet", "unplumb");
#endif
        }
    }
    else
    {
        /* We are setting/replacing address. */
        if (strchr(pszAddress, ':'))
        {
#ifdef RT_OS_SOLARIS
            /* On Solaris we need to plumb the interface first if it's not already plumbed. */
            if (executeIfconfig(pszAdapterName, "inet6") != 0)
                executeIfconfig(pszAdapterName, "inet6", "plumb", "up");
#endif
            /*
             * Before we set IPv6 address we'd like to remove
             * all previously assigned addresses except the
             * self-assigned one.
             */
            if (!removeAddresses(pszAdapterName))
                rc = EXIT_FAILURE;
            else
                rc = executeIfconfig(pszAdapterName, "inet6", VBOXADPCTL_ADD_CMD, pszAddress, pszOption, pszNetworkMask);
        }
        else
        {
#ifdef RT_OS_SOLARIS
            /* On Solaris we need to plumb the interface first if it's not already plumbed. */
            if (executeIfconfig(pszAdapterName, "inet") != 0)
                executeIfconfig(pszAdapterName, "plumb", "up");
#endif
            rc = executeIfconfig(pszAdapterName, pszAddress, pszOption, pszNetworkMask);
        }
    }
    return rc;
}

