/*
 *  tapctl -- Utility to manipulate TUN/TAP adapters on Windows
 *            https://community.openvpn.net/openvpn/wiki/Tapctl
 *
 *  Copyright (C) 2018-2023 Simon Rozman <simon@rozman.si>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2
 *  as published by the Free Software Foundation.
 *
 *  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, write to the Free Software Foundation, Inc.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#elif defined(_MSC_VER)
#include <config-msvc.h>
#endif

#include "tap.h"
#include "error.h"

#include <windows.h>
#include <cfgmgr32.h>
#include <objbase.h>
#include <setupapi.h>
#include <stdio.h>
#include <tchar.h>
#include <newdev.h>

#ifdef _MSC_VER
#pragma comment(lib, "advapi32.lib")
#pragma comment(lib, "ole32.lib")
#pragma comment(lib, "setupapi.lib")
#pragma comment(lib, "newdev.lib")
#endif


const static GUID GUID_DEVCLASS_NET = { 0x4d36e972L, 0xe325, 0x11ce, { 0xbf, 0xc1, 0x08, 0x00, 0x2b, 0xe1, 0x03, 0x18 } };

const static TCHAR szAdapterRegKeyPathTemplate[] = TEXT("SYSTEM\\CurrentControlSet\\Control\\Network\\%") TEXT(PRIsLPOLESTR) TEXT("\\%") TEXT(PRIsLPOLESTR) TEXT("\\Connection");
#define ADAPTER_REGKEY_PATH_MAX (_countof(TEXT("SYSTEM\\CurrentControlSet\\Control\\Network\\")) - 1 + 38 + _countof(TEXT("\\")) - 1 + 38 + _countof(TEXT("\\Connection")))

/**
 * Dynamically load a library and find a function in it
 *
 * @param libname     Name of the library to load
 * @param funcname    Name of the function to find
 * @param m           Pointer to a module. On return this is set to the
 *                    the handle to the loaded library. The caller must
 *                    free it by calling FreeLibrary() if not NULL.
 *
 * @return            Pointer to the function
 *                    NULL on error -- use GetLastError() to find the error code.
 *
 **/
static void *
find_function(const WCHAR *libname, const char *funcname, HMODULE *m)
{
    WCHAR libpath[MAX_PATH];
    void *fptr = NULL;

    /* Make sure the dll is loaded from the system32 folder */
    if (!GetSystemDirectoryW(libpath, _countof(libpath)))
    {
        return NULL;
    }

    /* +1 for the path seperator '\' */
    const size_t path_length = wcslen(libpath) + 1 + wcslen(libname);
    if (path_length >= _countof(libpath))
    {
        SetLastError(ERROR_INSUFFICIENT_BUFFER);
        return NULL;
    }
    wcscat_s(libpath, _countof(libpath), L"\\");
    wcscat_s(libpath, _countof(libpath), libname);

    *m = LoadLibraryW(libpath);
    if (*m == NULL)
    {
        return NULL;
    }
    fptr = GetProcAddress(*m, funcname);
    if (!fptr)
    {
        FreeLibrary(*m);
        *m = NULL;
        return NULL;
    }
    return fptr;
}

/**
 * Returns length of string of strings
 *
 * @param szz           Pointer to a string of strings (terminated by an empty string)
 *
 * @return Number of characters not counting the final zero terminator
 **/
static inline size_t
_tcszlen(_In_z_ LPCTSTR szz)
{
    LPCTSTR s;
    for (s = szz; s[0]; s += _tcslen(s) + 1)
    {
    }
    return s - szz;
}


/**
 * Checks if string is contained in the string of strings. Comparison is made case-insensitive.
 *
 * @param szzHay        Pointer to a string of strings (terminated by an empty string) we are
 *                      looking in
 *
 * @param szNeedle      The string we are searching for
 *
 * @return Pointer to the string in szzHay that matches szNeedle is found; NULL otherwise
 */
static LPCTSTR
_tcszistr(_In_z_ LPCTSTR szzHay, _In_z_ LPCTSTR szNeedle)
{
    for (LPCTSTR s = szzHay; s[0]; s += _tcslen(s) + 1)
    {
        if (_tcsicmp(s, szNeedle) == 0)
        {
            return s;
        }
    }

    return NULL;
}


/**
 * Function that performs a specific task on a device
 *
 * @param hDeviceInfoSet  A handle to a device information set that contains a device
 *                      information element that represents the device.
 *
 * @param pDeviceInfoData  A pointer to an SP_DEVINFO_DATA structure that specifies the
 *                      device information element in hDeviceInfoSet.
 *
 * @param pbRebootRequired  A pointer to a BOOL flag. If the device requires a system restart,
 *                      this flag is set to TRUE. Otherwise, the flag is left unmodified. This
 *                      allows the flag to be globally initialized to FALSE and reused for multiple
 *                      adapter manipulations.
 *
 * @return ERROR_SUCCESS on success; Win32 error code otherwise
 **/
typedef DWORD (*devop_func_t)(
    _In_ HDEVINFO hDeviceInfoSet,
    _In_ PSP_DEVINFO_DATA pDeviceInfoData,
    _Inout_ LPBOOL pbRebootRequired);


/**
 * Checks device install parameters if a system reboot is required.
 *
 * @param hDeviceInfoSet  A handle to a device information set that contains a device
 *                      information element that represents the device.
 *
 * @param pDeviceInfoData  A pointer to an SP_DEVINFO_DATA structure that specifies the
 *                      device information element in hDeviceInfoSet.
 *
 * @param pbRebootRequired  A pointer to a BOOL flag. If the device requires a system restart,
 *                      this flag is set to TRUE. Otherwise, the flag is left unmodified. This
 *                      allows the flag to be globally initialized to FALSE and reused for multiple
 *                      adapter manipulations.
 *
 * @return ERROR_SUCCESS on success; Win32 error code otherwise
 **/
static DWORD
check_reboot(
    _In_ HDEVINFO hDeviceInfoSet,
    _In_ PSP_DEVINFO_DATA pDeviceInfoData,
    _Inout_ LPBOOL pbRebootRequired)
{
    if (pbRebootRequired == NULL)
    {
        return ERROR_BAD_ARGUMENTS;
    }

    SP_DEVINSTALL_PARAMS devinstall_params = { .cbSize = sizeof(SP_DEVINSTALL_PARAMS) };
    if (!SetupDiGetDeviceInstallParams(
            hDeviceInfoSet,
            pDeviceInfoData,
            &devinstall_params))
    {
        DWORD dwResult = GetLastError();
        msg(M_NONFATAL | M_ERRNO, "%s: SetupDiGetDeviceInstallParams failed", __FUNCTION__);
        return dwResult;
    }

    if ((devinstall_params.Flags & (DI_NEEDREBOOT | DI_NEEDRESTART)) != 0)
    {
        *pbRebootRequired = TRUE;
    }

    return ERROR_SUCCESS;
}


/**
 * Deletes the device.
 *
 * @param hDeviceInfoSet  A handle to a device information set that contains a device
 *                      information element that represents the device.
 *
 * @param pDeviceInfoData  A pointer to an SP_DEVINFO_DATA structure that specifies the
 *                      device information element in hDeviceInfoSet.
 *
 * @param pbRebootRequired  A pointer to a BOOL flag. If the device requires a system restart,
 *                      this flag is set to TRUE. Otherwise, the flag is left unmodified. This
 *                      allows the flag to be globally initialized to FALSE and reused for multiple
 *                      adapter manipulations.
 *
 * @return ERROR_SUCCESS on success; Win32 error code otherwise
 **/
static DWORD
delete_device(
    _In_ HDEVINFO hDeviceInfoSet,
    _In_ PSP_DEVINFO_DATA pDeviceInfoData,
    _Inout_ LPBOOL pbRebootRequired)
{
    SP_REMOVEDEVICE_PARAMS params =
    {
        .ClassInstallHeader =
        {
            .cbSize = sizeof(SP_CLASSINSTALL_HEADER),
            .InstallFunction = DIF_REMOVE,
        },
        .Scope = DI_REMOVEDEVICE_GLOBAL,
        .HwProfile = 0,
    };

    /* Set class installer parameters for DIF_REMOVE. */
    if (!SetupDiSetClassInstallParams(
            hDeviceInfoSet,
            pDeviceInfoData,
            &params.ClassInstallHeader,
            sizeof(SP_REMOVEDEVICE_PARAMS)))
    {
        DWORD dwResult = GetLastError();
        msg(M_NONFATAL | M_ERRNO, "%s: SetupDiSetClassInstallParams failed", __FUNCTION__);
        return dwResult;
    }

    /* Call appropriate class installer. */
    if (!SetupDiCallClassInstaller(
            DIF_REMOVE,
            hDeviceInfoSet,
            pDeviceInfoData))
    {
        DWORD dwResult = GetLastError();
        msg(M_NONFATAL | M_ERRNO, "%s: SetupDiCallClassInstaller(DIF_REMOVE) failed", __FUNCTION__);
        return dwResult;
    }

    /* Check if a system reboot is required. */
    check_reboot(hDeviceInfoSet, pDeviceInfoData, pbRebootRequired);
    return ERROR_SUCCESS;
}


/**
 * Changes the device state.
 *
 * @param hDeviceInfoSet  A handle to a device information set that contains a device
 *                      information element that represents the device.
 *
 * @param pDeviceInfoData  A pointer to an SP_DEVINFO_DATA structure that specifies the
 *                      device information element in hDeviceInfoSet.
 *
 * @param bEnable       TRUE to enable the device; FALSE to disable.
 *
 * @param pbRebootRequired  A pointer to a BOOL flag. If the device requires a system restart,
 *                      this flag is set to TRUE. Otherwise, the flag is left unmodified. This
 *                      allows the flag to be globally initialized to FALSE and reused for multiple
 *                      adapter manipulations.
 *
 * @return ERROR_SUCCESS on success; Win32 error code otherwise
 **/
static DWORD
change_device_state(
    _In_ HDEVINFO hDeviceInfoSet,
    _In_ PSP_DEVINFO_DATA pDeviceInfoData,
    _In_ BOOL bEnable,
    _Inout_ LPBOOL pbRebootRequired)
{
    SP_PROPCHANGE_PARAMS params =
    {
        .ClassInstallHeader =
        {
            .cbSize = sizeof(SP_CLASSINSTALL_HEADER),
            .InstallFunction = DIF_PROPERTYCHANGE,
        },
        .StateChange = bEnable ? DICS_ENABLE : DICS_DISABLE,
        .Scope = DICS_FLAG_GLOBAL,
        .HwProfile = 0,
    };

    /* Set class installer parameters for DIF_PROPERTYCHANGE. */
    if (!SetupDiSetClassInstallParams(
            hDeviceInfoSet,
            pDeviceInfoData,
            &params.ClassInstallHeader,
            sizeof(SP_PROPCHANGE_PARAMS)))
    {
        DWORD dwResult = GetLastError();
        msg(M_NONFATAL | M_ERRNO, "%s: SetupDiSetClassInstallParams failed", __FUNCTION__);
        return dwResult;
    }

    /* Call appropriate class installer. */
    if (!SetupDiCallClassInstaller(
            DIF_PROPERTYCHANGE,
            hDeviceInfoSet,
            pDeviceInfoData))
    {
        DWORD dwResult = GetLastError();
        msg(M_NONFATAL | M_ERRNO, "%s: SetupDiCallClassInstaller(DIF_PROPERTYCHANGE) failed", __FUNCTION__);
        return dwResult;
    }

    /* Check if a system reboot is required. */
    check_reboot(hDeviceInfoSet, pDeviceInfoData, pbRebootRequired);
    return ERROR_SUCCESS;
}


/**
 * Enables the device.
 *
 * @param hDeviceInfoSet  A handle to a device information set that contains a device
 *                      information element that represents the device.
 *
 * @param pDeviceInfoData  A pointer to an SP_DEVINFO_DATA structure that specifies the
 *                      device information element in hDeviceInfoSet.
 *
 * @param pbRebootRequired  A pointer to a BOOL flag. If the device requires a system restart,
 *                      this flag is set to TRUE. Otherwise, the flag is left unmodified. This
 *                      allows the flag to be globally initialized to FALSE and reused for multiple
 *                      adapter manipulations.
 *
 * @return ERROR_SUCCESS on success; Win32 error code otherwise
 **/
static DWORD
enable_device(
    _In_ HDEVINFO hDeviceInfoSet,
    _In_ PSP_DEVINFO_DATA pDeviceInfoData,
    _Inout_ LPBOOL pbRebootRequired)
{
    return change_device_state(hDeviceInfoSet, pDeviceInfoData, TRUE, pbRebootRequired);
}


/**
 * Disables the device.
 *
 * @param hDeviceInfoSet  A handle to a device information set that contains a device
 *                      information element that represents the device.
 *
 * @param pDeviceInfoData  A pointer to an SP_DEVINFO_DATA structure that specifies the
 *                      device information element in hDeviceInfoSet.
 *
 * @param pbRebootRequired  A pointer to a BOOL flag. If the device requires a system restart,
 *                      this flag is set to TRUE. Otherwise, the flag is left unmodified. This
 *                      allows the flag to be globally initialized to FALSE and reused for multiple
 *                      adapter manipulations.
 *
 * @return ERROR_SUCCESS on success; Win32 error code otherwise
 **/
static DWORD
disable_device(
    _In_ HDEVINFO hDeviceInfoSet,
    _In_ PSP_DEVINFO_DATA pDeviceInfoData,
    _Inout_ LPBOOL pbRebootRequired)
{
    return change_device_state(hDeviceInfoSet, pDeviceInfoData, FALSE, pbRebootRequired);
}


/**
 * Reads string value from registry key.
 *
 * @param hKey          Handle of the registry key to read from. Must be opened with read
 *                      access.
 *
 * @param szName        Name of the value to read.
 *
 * @param pszValue      Pointer to string to retrieve registry value. If the value type is
 *                      REG_EXPAND_SZ the value is expanded using ExpandEnvironmentStrings().
 *                      The string must be released with free() after use.
 *
 * @return ERROR_SUCCESS on success; Win32 error code otherwise
 */
static DWORD
get_reg_string(
    _In_ HKEY hKey,
    _In_ LPCTSTR szName,
    _Out_ LPTSTR *pszValue)
{
    if (pszValue == NULL)
    {
        return ERROR_BAD_ARGUMENTS;
    }

    DWORD dwValueType = REG_NONE, dwSize = 0;
    DWORD dwResult = RegQueryValueEx(
        hKey,
        szName,
        NULL,
        &dwValueType,
        NULL,
        &dwSize);
    if (dwResult != ERROR_SUCCESS)
    {
        SetLastError(dwResult); /* MSDN does not mention RegQueryValueEx() to set GetLastError(). But we do have an error code. Set last error manually. */
        msg(M_NONFATAL | M_ERRNO, "%s: enumerating \"%" PRIsLPTSTR "\" registry value failed", __FUNCTION__, szName);
        return dwResult;
    }

    switch (dwValueType)
    {
        case REG_SZ:
        case REG_EXPAND_SZ:
        {
            /* Read value. */
            LPTSTR szValue = (LPTSTR)malloc(dwSize);
            if (szValue == NULL)
            {
                msg(M_FATAL, "%s: malloc(%u) failed", __FUNCTION__, dwSize);
                return ERROR_OUTOFMEMORY;
            }

            dwResult = RegQueryValueEx(
                hKey,
                szName,
                NULL,
                NULL,
                (LPBYTE)szValue,
                &dwSize);
            if (dwResult != ERROR_SUCCESS)
            {
                SetLastError(dwResult); /* MSDN does not mention RegQueryValueEx() to set GetLastError(). But we do have an error code. Set last error manually. */
                msg(M_NONFATAL | M_ERRNO, "%s: reading \"%" PRIsLPTSTR "\" registry value failed", __FUNCTION__, szName);
                free(szValue);
                return dwResult;
            }

            if (dwValueType == REG_EXPAND_SZ)
            {
                /* Expand the environment strings. */
                DWORD
                    dwSizeExp = dwSize * 2,
                    dwCountExp =
#ifdef UNICODE
                    dwSizeExp / sizeof(TCHAR);
#else
                    dwSizeExp / sizeof(TCHAR) - 1;     /* Note: ANSI version requires one extra char. */
#endif
                LPTSTR szValueExp = (LPTSTR)malloc(dwSizeExp);
                if (szValueExp == NULL)
                {
                    free(szValue);
                    msg(M_FATAL, "%s: malloc(%u) failed", __FUNCTION__, dwSizeExp);
                    return ERROR_OUTOFMEMORY;
                }

                DWORD dwCountExpResult = ExpandEnvironmentStrings(
                    szValue,
                    szValueExp, dwCountExp
                    );
                if (dwCountExpResult == 0)
                {
                    msg(M_NONFATAL | M_ERRNO, "%s: expanding \"%" PRIsLPTSTR "\" registry value failed", __FUNCTION__, szName);
                    free(szValueExp);
                    free(szValue);
                    return dwResult;
                }
                else if (dwCountExpResult <= dwCountExp)
                {
                    /* The buffer was big enough. */
                    free(szValue);
                    *pszValue = szValueExp;
                    return ERROR_SUCCESS;
                }
                else
                {
                    /* Retry with a bigger buffer. */
                    free(szValueExp);
#ifdef UNICODE
                    dwSizeExp = dwCountExpResult * sizeof(TCHAR);
#else
                    /* Note: ANSI version requires one extra char. */
                    dwSizeExp = (dwCountExpResult + 1) * sizeof(TCHAR);
#endif
                    dwCountExp = dwCountExpResult;
                    szValueExp = (LPTSTR)malloc(dwSizeExp);
                    if (szValueExp == NULL)
                    {
                        free(szValue);
                        msg(M_FATAL, "%s: malloc(%u) failed", __FUNCTION__, dwSizeExp);
                        return ERROR_OUTOFMEMORY;
                    }

                    dwCountExpResult = ExpandEnvironmentStrings(
                        szValue,
                        szValueExp, dwCountExp);
                    free(szValue);
                    *pszValue = szValueExp;
                    return ERROR_SUCCESS;
                }
            }
            else
            {
                *pszValue = szValue;
                return ERROR_SUCCESS;
            }
        }

        default:
            msg(M_NONFATAL, "%s: \"%" PRIsLPTSTR "\" registry value is not string (type %u)", __FUNCTION__, dwValueType);
            return ERROR_UNSUPPORTED_TYPE;
    }
}


/**
 * Returns network adapter ID.
 *
 * @param hDeviceInfoSet  A handle to a device information set that contains a device
 *                      information element that represents the device.
 *
 * @param pDeviceInfoData  A pointer to an SP_DEVINFO_DATA structure that specifies the
 *                      device information element in hDeviceInfoSet.
 *
 * @param iNumAttempts  After the device is created, it might take some time before the
 *                      registry key is populated. This parameter specifies the number of
 *                      attempts to read NetCfgInstanceId value from registry. A 1sec sleep
 *                      is inserted between retry attempts.
 *
 * @param pguidAdapter  A pointer to GUID that receives network adapter ID.
 *
 * @return ERROR_SUCCESS on success; Win32 error code otherwise
 **/
static DWORD
get_net_adapter_guid(
    _In_ HDEVINFO hDeviceInfoSet,
    _In_ PSP_DEVINFO_DATA pDeviceInfoData,
    _In_ int iNumAttempts,
    _Out_ LPGUID pguidAdapter)
{
    DWORD dwResult = ERROR_BAD_ARGUMENTS;

    if (pguidAdapter == NULL || iNumAttempts < 1)
    {
        return ERROR_BAD_ARGUMENTS;
    }

    /* Open HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Class\<class>\<id> registry key. */
    HKEY hKey = SetupDiOpenDevRegKey(
        hDeviceInfoSet,
        pDeviceInfoData,
        DICS_FLAG_GLOBAL,
        0,
        DIREG_DRV,
        KEY_READ);
    if (hKey == INVALID_HANDLE_VALUE)
    {
        dwResult = GetLastError();
        msg(M_NONFATAL | M_ERRNO, "%s: SetupDiOpenDevRegKey failed", __FUNCTION__);
        return dwResult;
    }

    while (iNumAttempts > 0)
    {
        /* Query the NetCfgInstanceId value. Using get_reg_string() right on might clutter the output with error messages while the registry is still being populated. */
        LPTSTR szCfgGuidString = NULL;
        dwResult = RegQueryValueEx(hKey, TEXT("NetCfgInstanceId"), NULL, NULL, NULL, NULL);
        if (dwResult != ERROR_SUCCESS)
        {
            if (dwResult == ERROR_FILE_NOT_FOUND && --iNumAttempts > 0)
            {
                /* Wait and retry. */
                Sleep(1000);
                continue;
            }

            SetLastError(dwResult); /* MSDN does not mention RegQueryValueEx() to set GetLastError(). But we do have an error code. Set last error manually. */
            msg(M_NONFATAL | M_ERRNO, "%s: querying \"NetCfgInstanceId\" registry value failed", __FUNCTION__);
            break;
        }

        /* Read the NetCfgInstanceId value now. */
        dwResult = get_reg_string(
            hKey,
            TEXT("NetCfgInstanceId"),
            &szCfgGuidString);
        if (dwResult != ERROR_SUCCESS)
        {
            break;
        }

        dwResult = SUCCEEDED(CLSIDFromString(szCfgGuidString, (LPCLSID)pguidAdapter)) ? ERROR_SUCCESS : ERROR_INVALID_DATA;
        free(szCfgGuidString);
        break;
    }

    RegCloseKey(hKey);
    return dwResult;
}


/**
 * Returns a specified Plug and Play device property.
 *
 * @param hDeviceInfoSet  A handle to a device information set that contains a device
 *                      information element that represents the device.
 *
 * @param pDeviceInfoData  A pointer to an SP_DEVINFO_DATA structure that specifies the
 *                      device information element in hDeviceInfoSet.
 *
 * @param dwProperty     Specifies the property to be retrieved. See
 *                       https://msdn.microsoft.com/en-us/library/windows/hardware/ff551967.aspx
 *
 * @pdwPropertyRegDataType  A pointer to a variable that receives the data type of the
 *                       property that is being retrieved. This is one of the standard
 *                       registry data types. This parameter is optional and can be NULL.
 *
 * @param ppData         A pointer to pointer to data that receives the device property. The
 *                       data must be released with free() after use.
 *
 * @return ERROR_SUCCESS on success; Win32 error code otherwise
 **/
static DWORD
get_device_reg_property(
    _In_ HDEVINFO hDeviceInfoSet,
    _In_ PSP_DEVINFO_DATA pDeviceInfoData,
    _In_ DWORD dwProperty,
    _Out_opt_ LPDWORD pdwPropertyRegDataType,
    _Out_ LPVOID *ppData)
{
    DWORD dwResult = ERROR_BAD_ARGUMENTS;

    if (ppData == NULL)
    {
        return ERROR_BAD_ARGUMENTS;
    }

    /* Try with stack buffer first. */
    BYTE bBufStack[128];
    DWORD dwRequiredSize = 0;
    if (SetupDiGetDeviceRegistryProperty(
            hDeviceInfoSet,
            pDeviceInfoData,
            dwProperty,
            pdwPropertyRegDataType,
            bBufStack,
            sizeof(bBufStack),
            &dwRequiredSize))
    {
        /* Copy from stack. */
        *ppData = malloc(dwRequiredSize);
        if (*ppData == NULL)
        {
            msg(M_FATAL, "%s: malloc(%u) failed", __FUNCTION__, dwRequiredSize);
            return ERROR_OUTOFMEMORY;
        }

        memcpy(*ppData, bBufStack, dwRequiredSize);
        return ERROR_SUCCESS;
    }
    else
    {
        dwResult = GetLastError();
        if (dwResult == ERROR_INSUFFICIENT_BUFFER)
        {
            /* Allocate on heap and retry. */
            *ppData = malloc(dwRequiredSize);
            if (*ppData == NULL)
            {
                msg(M_FATAL, "%s: malloc(%u) failed", __FUNCTION__, dwRequiredSize);
                return ERROR_OUTOFMEMORY;
            }

            if (SetupDiGetDeviceRegistryProperty(
                    hDeviceInfoSet,
                    pDeviceInfoData,
                    dwProperty,
                    pdwPropertyRegDataType,
                    *ppData,
                    dwRequiredSize,
                    &dwRequiredSize))
            {
                return ERROR_SUCCESS;
            }
            else
            {
                dwResult = GetLastError();
                msg(M_NONFATAL | M_ERRNO, "%s: SetupDiGetDeviceRegistryProperty(%u) failed", __FUNCTION__, dwProperty);
                return dwResult;
            }
        }
        else
        {
            msg(M_NONFATAL | M_ERRNO, "%s: SetupDiGetDeviceRegistryProperty(%u) failed", __FUNCTION__, dwProperty);
            return dwResult;
        }
    }
}


DWORD
tap_create_adapter(
    _In_opt_ HWND hwndParent,
    _In_opt_ LPCTSTR szDeviceDescription,
    _In_ LPCTSTR szHwId,
    _Inout_ LPBOOL pbRebootRequired,
    _Out_ LPGUID pguidAdapter)
{
    DWORD dwResult;
    HMODULE libnewdev = NULL;

    if (szHwId == NULL
        || pbRebootRequired == NULL
        || pguidAdapter == NULL)
    {
        return ERROR_BAD_ARGUMENTS;
    }

    /* Create an empty device info set for network adapter device class. */
    HDEVINFO hDevInfoList = SetupDiCreateDeviceInfoList(&GUID_DEVCLASS_NET, hwndParent);
    if (hDevInfoList == INVALID_HANDLE_VALUE)
    {
        dwResult = GetLastError();
        msg(M_NONFATAL, "%s: SetupDiCreateDeviceInfoList failed", __FUNCTION__);
        return dwResult;
    }

    /* Get the device class name from GUID. */
    TCHAR szClassName[MAX_CLASS_NAME_LEN];
    if (!SetupDiClassNameFromGuid(
            &GUID_DEVCLASS_NET,
            szClassName,
            _countof(szClassName),
            NULL))
    {
        dwResult = GetLastError();
        msg(M_NONFATAL, "%s: SetupDiClassNameFromGuid failed", __FUNCTION__);
        goto cleanup_hDevInfoList;
    }

    /* Create a new device info element and add it to the device info set. */
    SP_DEVINFO_DATA devinfo_data = { .cbSize = sizeof(SP_DEVINFO_DATA) };
    if (!SetupDiCreateDeviceInfo(
            hDevInfoList,
            szClassName,
            &GUID_DEVCLASS_NET,
            szDeviceDescription,
            hwndParent,
            DICD_GENERATE_ID,
            &devinfo_data))
    {
        dwResult = GetLastError();
        msg(M_NONFATAL, "%s: SetupDiCreateDeviceInfo failed", __FUNCTION__);
        goto cleanup_hDevInfoList;
    }

    /* Set a device information element as the selected member of a device information set. */
    if (!SetupDiSetSelectedDevice(
            hDevInfoList,
            &devinfo_data))
    {
        dwResult = GetLastError();
        msg(M_NONFATAL, "%s: SetupDiSetSelectedDevice failed", __FUNCTION__);
        goto cleanup_hDevInfoList;
    }

    /* Set Plug&Play device hardware ID property. */
    if (!SetupDiSetDeviceRegistryProperty(
            hDevInfoList,
            &devinfo_data,
            SPDRP_HARDWAREID,
            (const BYTE *)szHwId, (DWORD)((_tcslen(szHwId) + 1) * sizeof(TCHAR))))
    {
        dwResult = GetLastError();
        msg(M_NONFATAL, "%s: SetupDiSetDeviceRegistryProperty failed", __FUNCTION__);
        goto cleanup_hDevInfoList;
    }

    /* Register the device instance with the PnP Manager */
    if (!SetupDiCallClassInstaller(
            DIF_REGISTERDEVICE,
            hDevInfoList,
            &devinfo_data))
    {
        dwResult = GetLastError();
        msg(M_NONFATAL, "%s: SetupDiCallClassInstaller(DIF_REGISTERDEVICE) failed", __FUNCTION__);
        goto cleanup_hDevInfoList;
    }

    /* Install the device using DiInstallDevice()
     * We instruct the system to use the best driver in the driver store
     * by setting the drvinfo argument of DiInstallDevice as NULL. This
     * assumes a driver is already installed in the driver store.
     */
#ifdef HAVE_DIINSTALLDEVICE
    if (!DiInstallDevice(hwndParent, hDevInfoList, &devinfo_data, NULL, 0, pbRebootRequired))
#else
    /* mingw does not resolve DiInstallDevice, so load it at run time. */
    typedef BOOL (WINAPI *DiInstallDeviceFn)(HWND, HDEVINFO, SP_DEVINFO_DATA *,
                                             SP_DRVINFO_DATA *, DWORD, BOOL *);
    DiInstallDeviceFn installfn
        = find_function(L"newdev.dll", "DiInstallDevice", &libnewdev);

    if (!installfn)
    {
        dwResult = GetLastError();
        msg(M_NONFATAL | M_ERRNO, "%s: Failed to locate DiInstallDevice()", __FUNCTION__);
        goto cleanup_hDevInfoList;
    }

    if (!installfn(hwndParent, hDevInfoList, &devinfo_data, NULL, 0, pbRebootRequired))
#endif
    {
        dwResult = GetLastError();
        msg(M_NONFATAL | M_ERRNO, "%s: DiInstallDevice failed", __FUNCTION__);
        goto cleanup_remove_device;
    }

    /* Get network adapter ID from registry. Retry for max 30sec. */
    dwResult = get_net_adapter_guid(hDevInfoList, &devinfo_data, 30, pguidAdapter);

cleanup_remove_device:
    if (dwResult != ERROR_SUCCESS)
    {
        /* The adapter was installed. But, the adapter ID was unobtainable. Clean-up. */
        SP_REMOVEDEVICE_PARAMS removedevice_params =
        {
            .ClassInstallHeader =
            {
                .cbSize = sizeof(SP_CLASSINSTALL_HEADER),
                .InstallFunction = DIF_REMOVE,
            },
            .Scope = DI_REMOVEDEVICE_GLOBAL,
            .HwProfile = 0,
        };

        /* Set class installer parameters for DIF_REMOVE. */
        if (SetupDiSetClassInstallParams(
                hDevInfoList,
                &devinfo_data,
                &removedevice_params.ClassInstallHeader,
                sizeof(SP_REMOVEDEVICE_PARAMS)))
        {
            /* Call appropriate class installer. */
            if (SetupDiCallClassInstaller(
                    DIF_REMOVE,
                    hDevInfoList,
                    &devinfo_data))
            {
                /* Check if a system reboot is required. */
                check_reboot(hDevInfoList, &devinfo_data, pbRebootRequired);
            }
            else
            {
                msg(M_NONFATAL | M_ERRNO, "%s: SetupDiCallClassInstaller(DIF_REMOVE) failed", __FUNCTION__);
            }
        }
        else
        {
            msg(M_NONFATAL | M_ERRNO, "%s: SetupDiSetClassInstallParams failed", __FUNCTION__);
        }
    }

cleanup_hDevInfoList:
    if (libnewdev)
    {
        FreeLibrary(libnewdev);
    }
    SetupDiDestroyDeviceInfoList(hDevInfoList);
    return dwResult;
}


/**
 * Performs a given task on an adapter.
 *
 * @param hwndParent    A handle to the top-level window to use for any user adapter that is
 *                      related to non-device-specific actions (such as a select-device dialog
 *                      box that uses the global class driver list). This handle is optional
 *                      and can be NULL. If a specific top-level window is not required, set
 *                      hwndParent to NULL.
 *
 * @param pguidAdapter  A pointer to GUID that contains network adapter ID.
 *
 * @param funcOperation  A pointer for the function to perform specific task on the adapter.
 *
 * @param pbRebootRequired  A pointer to a BOOL flag. If the device requires a system restart,
 *                      this flag is set to TRUE. Otherwise, the flag is left unmodified. This
 *                      allows the flag to be globally initialized to FALSE and reused for multiple
 *                      adapter manipulations.
 *
 * @return ERROR_SUCCESS on success; Win32 error code otherwise
 **/
static DWORD
execute_on_first_adapter(
    _In_opt_ HWND hwndParent,
    _In_ LPCGUID pguidAdapter,
    _In_ devop_func_t funcOperation,
    _Inout_ LPBOOL pbRebootRequired)
{
    DWORD dwResult;

    if (pguidAdapter == NULL)
    {
        return ERROR_BAD_ARGUMENTS;
    }

    /* Create a list of network devices. */
    HDEVINFO hDevInfoList = SetupDiGetClassDevsEx(
        &GUID_DEVCLASS_NET,
        NULL,
        hwndParent,
        DIGCF_PRESENT,
        NULL,
        NULL,
        NULL);
    if (hDevInfoList == INVALID_HANDLE_VALUE)
    {
        dwResult = GetLastError();
        msg(M_NONFATAL, "%s: SetupDiGetClassDevsEx failed", __FUNCTION__);
        return dwResult;
    }

    /* Retrieve information associated with a device information set. */
    SP_DEVINFO_LIST_DETAIL_DATA devinfo_list_detail_data = { .cbSize = sizeof(SP_DEVINFO_LIST_DETAIL_DATA) };
    if (!SetupDiGetDeviceInfoListDetail(hDevInfoList, &devinfo_list_detail_data))
    {
        dwResult = GetLastError();
        msg(M_NONFATAL, "%s: SetupDiGetDeviceInfoListDetail failed", __FUNCTION__);
        goto cleanup_hDevInfoList;
    }

    /* Iterate. */
    for (DWORD dwIndex = 0;; dwIndex++)
    {
        /* Get the device from the list. */
        SP_DEVINFO_DATA devinfo_data = { .cbSize = sizeof(SP_DEVINFO_DATA) };
        if (!SetupDiEnumDeviceInfo(
                hDevInfoList,
                dwIndex,
                &devinfo_data))
        {
            if (GetLastError() == ERROR_NO_MORE_ITEMS)
            {
                LPOLESTR szAdapterId = NULL;
                StringFromIID((REFIID)pguidAdapter, &szAdapterId);
                msg(M_NONFATAL, "%s: Adapter %" PRIsLPOLESTR " not found", __FUNCTION__, szAdapterId);
                CoTaskMemFree(szAdapterId);
                dwResult = ERROR_FILE_NOT_FOUND;
                goto cleanup_hDevInfoList;
            }
            else
            {
                /* Something is wrong with this device. Skip it. */
                msg(M_WARN | M_ERRNO, "%s: SetupDiEnumDeviceInfo(%u) failed", __FUNCTION__, dwIndex);
                continue;
            }
        }

        /* Get adapter GUID. */
        GUID guidAdapter;
        dwResult = get_net_adapter_guid(hDevInfoList, &devinfo_data, 1, &guidAdapter);
        if (dwResult != ERROR_SUCCESS)
        {
            /* Something is wrong with this device. Skip it. */
            continue;
        }

        /* Compare GUIDs. */
        if (memcmp(pguidAdapter, &guidAdapter, sizeof(GUID)) == 0)
        {
            dwResult = funcOperation(hDevInfoList, &devinfo_data, pbRebootRequired);
            break;
        }
    }

cleanup_hDevInfoList:
    SetupDiDestroyDeviceInfoList(hDevInfoList);
    return dwResult;
}


DWORD
tap_delete_adapter(
    _In_opt_ HWND hwndParent,
    _In_ LPCGUID pguidAdapter,
    _Inout_ LPBOOL pbRebootRequired)
{
    return execute_on_first_adapter(hwndParent, pguidAdapter, delete_device, pbRebootRequired);
}


DWORD
tap_enable_adapter(
    _In_opt_ HWND hwndParent,
    _In_ LPCGUID pguidAdapter,
    _In_ BOOL bEnable,
    _Inout_ LPBOOL pbRebootRequired)
{
    return execute_on_first_adapter(hwndParent, pguidAdapter, bEnable ? enable_device : disable_device, pbRebootRequired);
}

/* stripped version of ExecCommand in interactive.c */
static DWORD
ExecCommand(const WCHAR *cmdline)
{
    DWORD exit_code;
    STARTUPINFOW si;
    PROCESS_INFORMATION pi;
    DWORD proc_flags = CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT;
    WCHAR *cmdline_dup = NULL;

    ZeroMemory(&si, sizeof(si));
    ZeroMemory(&pi, sizeof(pi));

    si.cb = sizeof(si);

    /* CreateProcess needs a modifiable cmdline: make a copy */
    cmdline_dup = _wcsdup(cmdline);
    if (cmdline_dup && CreateProcessW(NULL, cmdline_dup, NULL, NULL, FALSE,
                                      proc_flags, NULL, NULL, &si, &pi))
    {
        WaitForSingleObject(pi.hProcess, INFINITE);
        if (!GetExitCodeProcess(pi.hProcess, &exit_code))
        {
            exit_code = GetLastError();
        }

        CloseHandle(pi.hProcess);
        CloseHandle(pi.hThread);
    }
    else
    {
        exit_code = GetLastError();
    }

    free(cmdline_dup);
    return exit_code;
}

DWORD
tap_set_adapter_name(
    _In_ LPCGUID pguidAdapter,
    _In_ LPCTSTR szName,
    _In_ BOOL bSilent)
{
    DWORD dwResult;
    int msg_flag = bSilent ? M_WARN : M_NONFATAL;
    msg_flag |= M_ERRNO;

    if (pguidAdapter == NULL || szName == NULL)
    {
        return ERROR_BAD_ARGUMENTS;
    }

    /* Get the device class GUID as string. */
    LPOLESTR szDevClassNetId = NULL;
    StringFromIID((REFIID)&GUID_DEVCLASS_NET, &szDevClassNetId);

    /* Get the adapter GUID as string. */
    LPOLESTR szAdapterId = NULL;
    StringFromIID((REFIID)pguidAdapter, &szAdapterId);

    /* Render registry key path. */
    TCHAR szRegKey[ADAPTER_REGKEY_PATH_MAX];
    _stprintf_s(
        szRegKey, _countof(szRegKey),
        szAdapterRegKeyPathTemplate,
        szDevClassNetId,
        szAdapterId);

    /* Open network adapter registry key. */
    HKEY hKey = NULL;
    dwResult = RegOpenKeyEx(
        HKEY_LOCAL_MACHINE,
        szRegKey,
        0,
        KEY_QUERY_VALUE,
        &hKey);
    if (dwResult != ERROR_SUCCESS)
    {
        SetLastError(dwResult); /* MSDN does not mention RegOpenKeyEx() to set GetLastError(). But we do have an error code. Set last error manually. */
        msg(msg_flag, "%s: RegOpenKeyEx(HKLM, \"%" PRIsLPTSTR "\") failed", __FUNCTION__, szRegKey);
        goto cleanup_szAdapterId;
    }

    LPTSTR szOldName = NULL;
    dwResult = get_reg_string(hKey, TEXT("Name"), &szOldName);
    if (dwResult != ERROR_SUCCESS)
    {
        SetLastError(dwResult);
        msg(msg_flag, "%s: Error reading adapter name", __FUNCTION__);
        goto cleanup_hKey;
    }

    /* rename adapter via netsh call */
    const TCHAR *szFmt = TEXT("netsh interface set interface name=\"%")
                         TEXT(PRIsLPTSTR) TEXT("\" newname=\"%") TEXT(PRIsLPTSTR) TEXT("\"");
    size_t ncmdline = _tcslen(szFmt) + _tcslen(szOldName) + _tcslen(szName) + 1;
    WCHAR *szCmdLine = malloc(ncmdline * sizeof(TCHAR));
    _stprintf_s(szCmdLine, ncmdline, szFmt, szOldName, szName);

    free(szOldName);

    dwResult = ExecCommand(szCmdLine);
    free(szCmdLine);

    if (dwResult != ERROR_SUCCESS)
    {
        SetLastError(dwResult);
        msg(msg_flag, "%s: Error renaming adapter", __FUNCTION__);
        goto cleanup_hKey;
    }

cleanup_hKey:
    RegCloseKey(hKey);
cleanup_szAdapterId:
    CoTaskMemFree(szAdapterId);
    CoTaskMemFree(szDevClassNetId);
    return dwResult;
}


DWORD
tap_list_adapters(
    _In_opt_ HWND hwndParent,
    _In_opt_ LPCTSTR szzHwIDs,
    _Out_ struct tap_adapter_node **ppAdapter)
{
    DWORD dwResult;

    if (ppAdapter == NULL)
    {
        return ERROR_BAD_ARGUMENTS;
    }

    /* Create a list of network devices. */
    HDEVINFO hDevInfoList = SetupDiGetClassDevsEx(
        &GUID_DEVCLASS_NET,
        NULL,
        hwndParent,
        DIGCF_PRESENT,
        NULL,
        NULL,
        NULL);
    if (hDevInfoList == INVALID_HANDLE_VALUE)
    {
        dwResult = GetLastError();
        msg(M_NONFATAL, "%s: SetupDiGetClassDevsEx failed", __FUNCTION__);
        return dwResult;
    }

    /* Retrieve information associated with a device information set. */
    SP_DEVINFO_LIST_DETAIL_DATA devinfo_list_detail_data = { .cbSize = sizeof(SP_DEVINFO_LIST_DETAIL_DATA) };
    if (!SetupDiGetDeviceInfoListDetail(hDevInfoList, &devinfo_list_detail_data))
    {
        dwResult = GetLastError();
        msg(M_NONFATAL, "%s: SetupDiGetDeviceInfoListDetail failed", __FUNCTION__);
        goto cleanup_hDevInfoList;
    }

    /* Get the device class GUID as string. */
    LPOLESTR szDevClassNetId = NULL;
    StringFromIID((REFIID)&GUID_DEVCLASS_NET, &szDevClassNetId);

    /* Iterate. */
    *ppAdapter = NULL;
    struct tap_adapter_node *pAdapterTail = NULL;
    for (DWORD dwIndex = 0;; dwIndex++)
    {
        /* Get the device from the list. */
        SP_DEVINFO_DATA devinfo_data = { .cbSize = sizeof(SP_DEVINFO_DATA) };
        if (!SetupDiEnumDeviceInfo(
                hDevInfoList,
                dwIndex,
                &devinfo_data))
        {
            if (GetLastError() == ERROR_NO_MORE_ITEMS)
            {
                break;
            }
            else
            {
                /* Something is wrong with this device. Skip it. */
                msg(M_WARN | M_ERRNO, "%s: SetupDiEnumDeviceInfo(%u) failed", __FUNCTION__, dwIndex);
                continue;
            }
        }

        /* Get device hardware ID(s). */
        DWORD dwDataType = REG_NONE;
        LPTSTR szzDeviceHardwareIDs = NULL;
        dwResult = get_device_reg_property(
            hDevInfoList,
            &devinfo_data,
            SPDRP_HARDWAREID,
            &dwDataType,
            (LPVOID)&szzDeviceHardwareIDs);
        if (dwResult != ERROR_SUCCESS)
        {
            /* Something is wrong with this device. Skip it. */
            continue;
        }

        /* Check that hardware ID is REG_SZ/REG_MULTI_SZ, and optionally if it matches ours. */
        if (dwDataType == REG_SZ)
        {
            if (szzHwIDs && !_tcszistr(szzHwIDs, szzDeviceHardwareIDs))
            {
                /* This is not our device. Skip it. */
                goto cleanup_szzDeviceHardwareIDs;
            }
        }
        else if (dwDataType == REG_MULTI_SZ)
        {
            if (szzHwIDs)
            {
                for (LPTSTR s = szzDeviceHardwareIDs;; s += _tcslen(s) + 1)
                {
                    if (s[0] == 0)
                    {
                        /* This is not our device. Skip it. */
                        goto cleanup_szzDeviceHardwareIDs;
                    }
                    else if (_tcszistr(szzHwIDs, s))
                    {
                        /* This is our device. */
                        break;
                    }
                }
            }
        }
        else
        {
            /* Unexpected hardware ID format. Skip device. */
            goto cleanup_szzDeviceHardwareIDs;
        }

        /* Get adapter GUID. */
        GUID guidAdapter;
        dwResult = get_net_adapter_guid(hDevInfoList, &devinfo_data, 1, &guidAdapter);
        if (dwResult != ERROR_SUCCESS)
        {
            /* Something is wrong with this device. Skip it. */
            goto cleanup_szzDeviceHardwareIDs;
        }

        /* Get the adapter GUID as string. */
        LPOLESTR szAdapterId = NULL;
        StringFromIID((REFIID)&guidAdapter, &szAdapterId);

        /* Render registry key path. */
        TCHAR szRegKey[ADAPTER_REGKEY_PATH_MAX];
        _stprintf_s(
            szRegKey, _countof(szRegKey),
            szAdapterRegKeyPathTemplate,
            szDevClassNetId,
            szAdapterId);

        /* Open network adapter registry key. */
        HKEY hKey = NULL;
        dwResult = RegOpenKeyEx(
            HKEY_LOCAL_MACHINE,
            szRegKey,
            0,
            KEY_READ,
            &hKey);
        if (dwResult != ERROR_SUCCESS)
        {
            SetLastError(dwResult); /* MSDN does not mention RegOpenKeyEx() to set GetLastError(). But we do have an error code. Set last error manually. */
            msg(M_WARN | M_ERRNO, "%s: RegOpenKeyEx(HKLM, \"%" PRIsLPTSTR "\") failed", __FUNCTION__, szRegKey);
            goto cleanup_szAdapterId;
        }

        /* Read adapter name. */
        LPTSTR szName = NULL;
        dwResult = get_reg_string(
            hKey,
            TEXT("Name"),
            &szName);
        if (dwResult != ERROR_SUCCESS)
        {
            SetLastError(dwResult);
            msg(M_WARN | M_ERRNO, "%s: Cannot determine %" PRIsLPOLESTR " adapter name", __FUNCTION__, szAdapterId);
            goto cleanup_hKey;
        }

        /* Append to the list. */
        size_t hwid_size = (_tcszlen(szzDeviceHardwareIDs) + 1) * sizeof(TCHAR);
        size_t name_size = (_tcslen(szName) + 1) * sizeof(TCHAR);
        struct tap_adapter_node *node = (struct tap_adapter_node *)malloc(sizeof(struct tap_adapter_node) + hwid_size + name_size);
        if (node == NULL)
        {
            msg(M_FATAL, "%s: malloc(%u) failed", __FUNCTION__, sizeof(struct tap_adapter_node) + hwid_size + name_size);
            dwResult = ERROR_OUTOFMEMORY; goto cleanup_szName;
        }

        memcpy(&node->guid, &guidAdapter, sizeof(GUID));
        node->szzHardwareIDs = (LPTSTR)(node + 1);
        memcpy(node->szzHardwareIDs, szzDeviceHardwareIDs, hwid_size);
        node->szName = (LPTSTR)((LPBYTE)node->szzHardwareIDs + hwid_size);
        memcpy(node->szName, szName, name_size);
        node->pNext = NULL;
        if (pAdapterTail)
        {
            pAdapterTail->pNext = node;
            pAdapterTail = node;
        }
        else
        {
            *ppAdapter = pAdapterTail = node;
        }

cleanup_szName:
        free(szName);
cleanup_hKey:
        RegCloseKey(hKey);
cleanup_szAdapterId:
        CoTaskMemFree(szAdapterId);
cleanup_szzDeviceHardwareIDs:
        free(szzDeviceHardwareIDs);
    }

    dwResult = ERROR_SUCCESS;

    CoTaskMemFree(szDevClassNetId);
cleanup_hDevInfoList:
    SetupDiDestroyDeviceInfoList(hDevInfoList);
    return dwResult;
}


void
tap_free_adapter_list(
    _In_ struct tap_adapter_node *pAdapterList)
{
    /* Iterate over all nodes of the list. */
    while (pAdapterList)
    {
        struct tap_adapter_node *node = pAdapterList;
        pAdapterList = pAdapterList->pNext;

        /* Free the adapter node. */
        free(node);
    }
}
