/*
* tapctl -- Utility to manipulate TUN/TAP adapters on Windows
* https://community.openvpn.net/openvpn/wiki/Tapctl
*
* Copyright (C) 2002-2025 OpenVPN Inc <sales@openvpn.net>
* Copyright (C) 2018-2025 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, see <https://www.gnu.org/licenses/>.
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include "tap.h"
#include "error.h"
#include <objbase.h>
#include <setupapi.h>
#include <stdio.h>
#include <wchar.h>
#ifdef _MSC_VER
#pragma comment(lib, "ole32.lib")
#pragma comment(lib, "setupapi.lib")
#endif
/* clang-format off */
const WCHAR title_string[] =
_L(PACKAGE_NAME) L" " _L(PACKAGE_VERSION)
;
static const WCHAR usage_message[] =
L"%ls\n"
L"\n"
L"Usage:\n"
L"\n"
L"tapctl <command> [<command specific options>]\n"
L"\n"
L"Commands:\n"
L"\n"
L"create Create a new TUN/TAP adapter\n"
L"list List TUN/TAP adapters\n"
L"delete Delete specified network adapter\n"
L"help Display this text\n"
L"\n"
L"Hint: Use \"tapctl help <command>\" to display help for particular command.\n"
;
static const WCHAR usage_message_create[] =
L"%ls\n"
L"\n"
L"Creates a new TUN/TAP adapter\n"
L"\n"
L"Usage:\n"
L"\n"
L"tapctl create [<options>]\n"
L"\n"
L"Options:\n"
L"\n"
L"--name <name> Set TUN/TAP adapter name. Should the adapter with given name \n"
L" already exist, an error is returned. If this option is not \n"
L" specified, a default adapter name is chosen by Windows. \n"
L" Note: This name can also be specified as OpenVPN's --dev-node \n"
L" option. \n"
L"--hwid <hwid> Adapter hardware ID. Default value is root\\tap0901, which \n"
L" describes tap-windows6 driver. To work with ovpn-dco driver, \n"
L" driver, specify 'ovpn-dco'. \n"
L"\n"
L"Output:\n"
L"\n"
L"This command prints newly created TUN/TAP adapter's GUID to stdout. \n"
;
static const WCHAR usage_message_list[] =
L"%ls\n"
L"\n"
L"Lists TUN/TAP adapters\n"
L"\n"
L"Usage:\n"
L"\n"
L"tapctl list\n"
L"\n"
L"Options:\n"
L"\n"
L"--hwid <hwid> Adapter hardware ID. By default, root\\tap0901, tap0901 and \n"
L" ovpn-dco adapters are listed. Use this switch to limit the list.\n"
L"\n"
L"Output:\n"
L"\n"
L"This command prints all TUN/TAP adapters to stdout. \n"
;
static const WCHAR usage_message_delete[] =
L"%ls\n"
L"\n"
L"Deletes the specified network adapter\n"
L"\n"
L"Usage:\n"
L"\n"
L"tapctl delete <adapter GUID | adapter name>\n"
;
/* clang-format on */
/**
* Print the help message.
*/
static void
usage(void)
{
fwprintf(stderr, usage_message, title_string);
}
/**
* Checks if adapter with given name doesn't already exist
*/
static BOOL
is_adapter_name_available(LPCWSTR name, struct tap_adapter_node *adapter_list, BOOL log)
{
for (struct tap_adapter_node *a = adapter_list; a; a = a->pNext)
{
if (wcsicmp(name, a->szName) == 0)
{
if (log)
{
LPOLESTR adapter_id = NULL;
StringFromIID((REFIID)&a->guid, &adapter_id);
fwprintf(stderr,
L"Adapter \"%ls\" already exists (GUID %"
L"ls).\n",
a->szName, adapter_id);
CoTaskMemFree(adapter_id);
}
return FALSE;
}
}
return TRUE;
}
/**
* Returns unique adapter name based on hwid or NULL if name cannot be generated.
* Caller is responsible for freeing it.
*/
static LPWSTR
get_unique_adapter_name(LPCWSTR hwid, struct tap_adapter_node *adapter_list)
{
if (hwid == NULL)
{
return NULL;
}
LPCWSTR base_name;
if (wcsicmp(hwid, L"ovpn-dco") == 0)
{
base_name = L"OpenVPN Data Channel Offload";
}
else if (wcsicmp(hwid, L"root\\" _L(TAP_WIN_COMPONENT_ID)) == 0)
{
base_name = L"OpenVPN TAP-Windows6";
}
else
{
return NULL;
}
if (is_adapter_name_available(base_name, adapter_list, FALSE))
{
return wcsdup(base_name);
}
size_t name_len = wcslen(base_name) + 10;
LPWSTR name = malloc(name_len * sizeof(WCHAR));
if (name == NULL)
{
return NULL;
}
for (int i = 1; i < 100; ++i)
{
swprintf_s(name, name_len, L"%ls #%d", base_name, i);
if (is_adapter_name_available(name, adapter_list, FALSE))
{
return name;
}
}
return NULL;
}
/**
* Program entry point
*/
int __cdecl wmain(int argc, LPCWSTR argv[])
{
int iResult;
BOOL bRebootRequired = FALSE;
/* Ask SetupAPI to keep quiet. */
SetupSetNonInteractiveMode(TRUE);
if (argc < 2)
{
usage();
return 1;
}
else if (wcsicmp(argv[1], L"help") == 0)
{
/* Output help. */
if (argc < 3)
{
usage();
}
else if (wcsicmp(argv[2], L"create") == 0)
{
fwprintf(stderr, usage_message_create, title_string);
}
else if (wcsicmp(argv[2], L"list") == 0)
{
fwprintf(stderr, usage_message_list, title_string);
}
else if (wcsicmp(argv[2], L"delete") == 0)
{
fwprintf(stderr, usage_message_delete, title_string);
}
else
{
fwprintf(stderr,
L"Unknown command \"%ls"
L"\". Please, use \"tapctl help\" to list supported commands.\n",
argv[2]);
}
return 1;
}
else if (wcsicmp(argv[1], L"create") == 0)
{
LPCWSTR szName = NULL;
LPCWSTR szHwId = L"root\\" _L(TAP_WIN_COMPONENT_ID);
/* Parse options. */
for (int i = 2; i < argc; i++)
{
if (wcsicmp(argv[i], L"--name") == 0)
{
szName = argv[++i];
}
else if (wcsicmp(argv[i], L"--hwid") == 0)
{
szHwId = argv[++i];
}
else
{
fwprintf(
stderr,
L"Unknown option \"%ls"
L"\". Please, use \"tapctl help create\" to list supported options. Ignored.\n",
argv[i]);
}
}
/* Create TUN/TAP adapter. */
GUID guidAdapter;
LPOLESTR szAdapterId = NULL;
DWORD dwResult =
tap_create_adapter(NULL, L"Virtual Ethernet", szHwId, &bRebootRequired, &guidAdapter);
if (dwResult != ERROR_SUCCESS)
{
fwprintf(stderr, L"Creating TUN/TAP adapter failed (error 0x%x).\n", dwResult);
iResult = 1;
goto quit;
}
/* Get existing network adapters. */
struct tap_adapter_node *pAdapterList = NULL;
dwResult = tap_list_adapters(NULL, NULL, &pAdapterList);
if (dwResult != ERROR_SUCCESS)
{
fwprintf(stderr, L"Enumerating adapters failed (error 0x%x).\n", dwResult);
iResult = 1;
goto create_delete_adapter;
}
LPWSTR adapter_name =
szName ? wcsdup(szName) : get_unique_adapter_name(szHwId, pAdapterList);
if (adapter_name)
{
/* Check for duplicates when name was specified,
* otherwise get_adapter_default_name() takes care of it */
if (szName && !is_adapter_name_available(adapter_name, pAdapterList, TRUE))
{
iResult = 1;
goto create_cleanup_pAdapterList;
}
/* Rename the adapter. */
dwResult = tap_set_adapter_name(&guidAdapter, adapter_name, FALSE);
if (dwResult != ERROR_SUCCESS)
{
StringFromIID((REFIID)&guidAdapter, &szAdapterId);
fwprintf(stderr,
L"Renaming TUN/TAP adapter %ls"
L" to \"%ls\" failed (error 0x%x).\n",
szAdapterId, adapter_name, dwResult);
CoTaskMemFree(szAdapterId);
iResult = 1;
goto quit;
}
}
iResult = 0;
create_cleanup_pAdapterList:
free(adapter_name);
tap_free_adapter_list(pAdapterList);
if (iResult)
{
goto create_delete_adapter;
}
/* Output adapter GUID. */
StringFromIID((REFIID)&guidAdapter, &szAdapterId);
fwprintf(stdout, L"%ls\n", szAdapterId);
CoTaskMemFree(szAdapterId);
iResult = 0;
goto quit;
create_delete_adapter:
tap_delete_adapter(NULL, &guidAdapter, &bRebootRequired);
iResult = 1;
goto quit;
}
else if (wcsicmp(argv[1], L"list") == 0)
{
WCHAR szzHwId[0x100] =
L"root\\" _L(TAP_WIN_COMPONENT_ID) L"\0" _L(TAP_WIN_COMPONENT_ID) L"\0"
L"ovpn-dco\0";
/* Parse options. */
for (int i = 2; i < argc; i++)
{
if (wcsicmp(argv[i], L"--hwid") == 0)
{
memset(szzHwId, 0, sizeof(szzHwId));
++i;
memcpy_s(szzHwId,
sizeof(szzHwId) - 2 * sizeof(WCHAR) /*requires double zero termination*/,
argv[i], wcslen(argv[i]) * sizeof(WCHAR));
}
else
{
fwprintf(
stderr,
L"Unknown option \"%ls"
L"\". Please, use \"tapctl help list\" to list supported options. Ignored.\n",
argv[i]);
}
}
/* Output list of adapters with given hardware ID. */
struct tap_adapter_node *pAdapterList = NULL;
DWORD dwResult = tap_list_adapters(NULL, szzHwId, &pAdapterList);
if (dwResult != ERROR_SUCCESS)
{
fwprintf(stderr, L"Enumerating TUN/TAP adapters failed (error 0x%x).\n", dwResult);
iResult = 1;
goto quit;
}
for (struct tap_adapter_node *pAdapter = pAdapterList; pAdapter; pAdapter = pAdapter->pNext)
{
LPOLESTR szAdapterId = NULL;
StringFromIID((REFIID)&pAdapter->guid, &szAdapterId);
fwprintf(stdout,
L"%ls\t%"
L"ls\n",
szAdapterId, pAdapter->szName);
CoTaskMemFree(szAdapterId);
}
iResult = 0;
tap_free_adapter_list(pAdapterList);
}
else if (wcsicmp(argv[1], L"delete") == 0)
{
if (argc < 3)
{
fwprintf(
stderr,
L"Missing adapter GUID or name. Please, use \"tapctl help delete\" for usage info.\n");
return 1;
}
GUID guidAdapter;
if (FAILED(IIDFromString(argv[2], (LPIID)&guidAdapter)))
{
/* The argument failed to covert to GUID. Treat it as the adapter name. */
struct tap_adapter_node *pAdapterList = NULL;
DWORD dwResult = tap_list_adapters(NULL, NULL, &pAdapterList);
if (dwResult != ERROR_SUCCESS)
{
fwprintf(stderr, L"Enumerating TUN/TAP adapters failed (error 0x%x).\n", dwResult);
iResult = 1;
goto quit;
}
for (struct tap_adapter_node *pAdapter = pAdapterList;; pAdapter = pAdapter->pNext)
{
if (pAdapter == NULL)
{
fwprintf(stderr, L"\"%ls\" adapter not found.\n", argv[2]);
iResult = 1;
goto delete_cleanup_pAdapterList;
}
else if (wcsicmp(argv[2], pAdapter->szName) == 0)
{
memcpy(&guidAdapter, &pAdapter->guid, sizeof(GUID));
break;
}
}
iResult = 0;
delete_cleanup_pAdapterList:
tap_free_adapter_list(pAdapterList);
if (iResult)
{
goto quit;
}
}
/* Delete the network adapter. */
DWORD dwResult = tap_delete_adapter(NULL, &guidAdapter, &bRebootRequired);
if (dwResult != ERROR_SUCCESS)
{
fwprintf(stderr,
L"Deleting adapter \"%ls"
L"\" failed (error 0x%x).\n",
argv[2], dwResult);
iResult = 1;
goto quit;
}
iResult = 0;
goto quit;
}
else
{
fwprintf(stderr,
L"Unknown command \"%ls"
L"\". Please, use \"tapctl help\" to list supported commands.\n",
argv[1]);
return 1;
}
quit:
if (bRebootRequired)
{
fwprintf(stderr, L"A system reboot is required.\n");
}
return iResult;
}
bool
dont_mute(unsigned int flags)
{
UNREFERENCED_PARAMETER(flags);
return true;
}
void
x_msg_va(const unsigned int flags, const char *format, va_list arglist)
{
/* Output message string. Note: Message strings don't contain line terminators. */
vfprintf(stderr, format, arglist);
fwprintf(stderr, L"\n");
if ((flags & M_ERRNO) != 0)
{
/* Output system error message (if possible). */
DWORD dwResult = GetLastError();
LPWSTR szErrMessage = NULL;
if (FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ALLOCATE_BUFFER
| FORMAT_MESSAGE_IGNORE_INSERTS,
0, dwResult, 0, (LPWSTR)&szErrMessage, 0, NULL)
&& szErrMessage)
{
/* Trim trailing whitespace. Set terminator after the last non-whitespace character.
* This prevents excessive trailing line breaks. */
for (size_t i = 0, i_last = 0;; i++)
{
if (szErrMessage[i])
{
if (!iswspace(szErrMessage[i]))
{
i_last = i + 1;
}
}
else
{
szErrMessage[i_last] = 0;
break;
}
}
/* Output error message. */
fwprintf(stderr, L"Error 0x%x: %ls\n", dwResult, szErrMessage);
LocalFree(szErrMessage);
}
else
{
fwprintf(stderr, L"Error 0x%x\n", dwResult);
}
}
}