Understanding Virtual Network Card Setup on Windows: Exploring WinTun and WireGuard for Tunnels

A while ago, I was searching for a virtual network card interface on the Windows operating system, mainly for setting up tunnels. However, the Windows operating system, unlike Linux, is not open source, resulting in scarce resources on this topic. Therefore, it took quite a long time to find a relevant implementation framework. Eventually, I found two open-source projects providing virtual interface drivers:

  • Wintun interface of the Wireguard project [1]
  • Tap interface of OpenVPN [2]

Both of these projects are well-known open-source VPN projects for setting up tunnels. Currently, I’m not very familiar with the OpenVPN project and haven’t adapted the Tap interface, so I’ll focus on introducing the WinTun interface here. I am very, very fond of this implementation, to the point of irrational fondness.

Introduction

Speaking of the Wintun project, one cannot overlook its parent: the WireGuard project (hereafter referred to as WG). Github portal [3]

virtual network card >

As an open-source VPN project, the WG project is different from OpenVPN, Openswan, Strongswan, etc., in that its implementation is highly concise, with the Linux kernel code implementation being less than 4000 lines. Compared to the above three “line-charging” projects (with code starting at 100,000 lines), it is extremely streamlined. Consequently, it has received numerous accolades, including from Linux’s progenitor, Linus Torvalds, who described it as follows:

Btw, on an unrelated issue: I see that Jason actually made the pull request to have wireguard included in the kernel.Can I just once again state my love for it and hope it gets merged soon? Maybe the code isn’t perfect, but I’ve skimmed it, and compared to the horrors that are OpenVPN and IPSec, it’s a work of art.Linus

In short, it means: I fancy you, and I’m going to incorporate you into my Linux project. Therefore, from Linux kernel 5.6 onwards, WG tunnel functionality is built-in, making configuration incredibly straightforward. With just a few lines of code, a WG tunnel can be completed:

ip link add dev wg0 type wireguardip address add dev wg0 10.0.0.1/24wg set wg0 listen-port 51820 private-key ./private.key peer NIk5TyDpRDoU9tfIckTTXCsz1eht2aEmdN7l0Q31ow0= allowed-ips 10.0.0.2/32 endpoint 192.168.1.5:51820ip link set dev wg0 up

The configuration is very simple. Additionally, a Windows client is provided, which is why the project includes the Wintun virtual network interface.

The client page is also very simple, without any unnecessary elements (Client link [4]):

virtual network card >

Once the tunnel negotiation is successfully completed on the client, a virtual network card will be established based on the tunnel name, and the interface will be automatically deleted after the tunnel is dismantled. Since my tunnel name is Tun-1, a network interface named Tun-1 appeared in the “Network Connections” of the “Control Panel”:

Okay, let’s start introducing this virtual network interface.

WinTun Virtual Network Interface

Github portal [5]

WinTun official website portal [6]

Common interface driver development for Windows [7] and installation can be quite complicated. Typical driver installation packages include: .inf files, .sys files, .cat files; and driver signature is also involved, without which installation cannot succeed. Especially during the development and debugging phase, each signing becomes quite tedious.

However, the usage of the WinTun interface is very simple and efficient. Very simple and efficient. Very simple and efficient.

  1. Include the header file: wintun.h
  2. Load the dynamic library, and parse the function pointers in the dynamic library

It provides interfaces through dynamic libraries, which can be loaded to perform virtual interface creation, destruction, packet transmission and reception, etc., via function pointers in the dynamic libraries. Additionally, an example is provided for learning purposes [8]. By referring to the example (example.c) in the open-source code, I ported the Wintun interface into my project. Very straightforward, and I adore it.

The example code is just 400 lines, most of which consists of log information for monitoring program status and packet transmission and reception.

Loading Function Pointers from the Dynamic Library

This function’s purpose:

  • Load the dynamic library, retrieve function pointers from the dynamic library, and perform operations on the virtual network card interface through the function pointers thereafter.
static HMODULEInitializeWintun(void){    HMODULE Wintun =        LoadLibraryExW(L"wintun.dll", NULL, LOAD_LIBRARY_SEARCH_APPLICATION_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32);    if (!Wintun)        return NULL;#define X(Name, Type) ((Name = (Type)GetProcAddress(Wintun, #Name)) == NULL)    if (X(WintunCreateAdapter, WINTUN_CREATE_ADAPTER_FUNC) || X(WintunDeleteAdapter, WINTUN_DELETE_ADAPTER_FUNC) ||        X(WintunDeletePoolDriver, WINTUN_DELETE_POOL_DRIVER_FUNC) || X(WintunEnumAdapters, WINTUN_ENUM_ADAPTERS_FUNC) ||        X(WintunFreeAdapter, WINTUN_FREE_ADAPTER_FUNC) || X(WintunOpenAdapter, WINTUN_OPEN_ADAPTER_FUNC) ||        X(WintunGetAdapterLUID, WINTUN_GET_ADAPTER_LUID_FUNC) ||        X(WintunGetAdapterName, WINTUN_GET_ADAPTER_NAME_FUNC) ||        X(WintunSetAdapterName, WINTUN_SET_ADAPTER_NAME_FUNC) ||        X(WintunGetRunningDriverVersion, WINTUN_GET_RUNNING_DRIVER_VERSION_FUNC) ||        X(WintunSetLogger, WINTUN_SET_LOGGER_FUNC) || X(WintunStartSession, WINTUN_START_SESSION_FUNC) ||        X(WintunEndSession, WINTUN_END_SESSION_FUNC) || X(WintunGetReadWaitEvent, WINTUN_GET_READ_WAIT_EVENT_FUNC) ||        X(WintunReceivePacket, WINTUN_RECEIVE_PACKET_FUNC) ||        X(WintunReleaseReceivePacket, WINTUN_RELEASE_RECEIVE_PACKET_FUNC) ||        X(WintunAllocateSendPacket, WINTUN_ALLOCATE_SEND_PACKET_FUNC) || X(WintunSendPacket, WINTUN_SEND_PACKET_FUNC))#undef X    {        DWORD LastError = GetLastError();        FreeLibrary(Wintun);        SetLastError(LastError);        return NULL;    }    return Wintun;}

main() Function

Purpose:

  • Create a virtual network card through function pointers
  • Create packet transmission and reception threads for the virtual network card
intmain(void){    HMODULE Wintun = InitializeWintun();    if (!Wintun)        return LogError(L"Failed to initialize Wintun", GetLastError());    WintunSetLogger(ConsoleLogger);    Log(WINTUN_LOG_INFO, L"Wintun library loaded");    WintunEnumAdapters(L"Example", PrintAdapter, 0);    DWORD LastError;    HaveQuit = FALSE;    QuitEvent = CreateEventW(NULL, TRUE, FALSE, NULL);    if (!QuitEvent)    {        LastError = LogError(L"Failed to create event", GetLastError());        goto cleanupWintun;    }    if (!SetConsoleCtrlHandler(CtrlHandler, TRUE))    {        LastError = LogError(L"Failed to set console handler", GetLastError());        goto cleanupQuit;    }    GUID ExampleGuid = { 0xdeadbabe, 0xcafe, 0xbeef, { 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef } };    WINTUN_ADAPTER_HANDLE Adapter = WintunOpenAdapter(L"Example", L"Demo");    if (!Adapter)    {        Adapter = WintunCreateAdapter(L"Example", L"Demo", &ExampleGuid, NULL);        if (!Adapter)        {            LastError = GetLastError();            LogError(L"Failed to create adapter", LastError);            goto cleanupQuit;        }    }    DWORD Version = WintunGetRunningDriverVersion();    Log(WINTUN_LOG_INFO, L"Wintun v%u.%u loaded", (Version >> 16) & 0xff, (Version >> 0) & 0xff);    MIB_UNICASTIPADDRESS_ROW AddressRow;    InitializeUnicastIpAddressEntry(&AddressRow);    WintunGetAdapterLUID(Adapter, &AddressRow.InterfaceLuid);    AddressRow.Address.Ipv4.sin_family = AF_INET;    AddressRow.Address.Ipv4.sin_addr.S_un.S_addr = htonl((10 << 24) | (6 << 16) | (7 << 8) | (7 << 0)); /* 10.6.7.7 */    AddressRow.OnLinkPrefixLength = 24; /* This is a /24 network */    LastError = CreateUnicastIpAddressEntry(&AddressRow);    if (LastError != ERROR_SUCCESS && LastError != ERROR_OBJECT_ALREADY_EXISTS)    {        LogError(L"Failed to set IP address", LastError);        goto cleanupAdapter;    }    WINTUN_SESSION_HANDLE Session = WintunStartSession(Adapter, 0x400000);    if (!Session)    {        LastError = LogLastError(L"Failed to create adapter");        goto cleanupAdapter;    }    Log(WINTUN_LOG_INFO, L"Launching threads and mangling packets...");    HANDLE Workers[] = { CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ReceivePackets, (LPVOID)Session, 0, NULL),                         CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)SendPackets, (LPVOID)Session, 0, NULL) };    if (!Workers[0] || !Workers[1])    {        LastError = LogError(L"Failed to create threads", GetLastError());        goto cleanupWorkers;    }    WaitForMultipleObjectsEx(_countof(Workers), Workers, TRUE, INFINITE, TRUE);    LastError = ERROR_SUCCESS;cleanupWorkers:    HaveQuit = TRUE;    SetEvent(QuitEvent);    for (size_t i = 0; i < _countof(Workers); ++i)    {        if (Workers[i])        {            WaitForSingleObject(Workers[i], INFINITE);            CloseHandle(Workers[i]);        }    }    WintunEndSession(Session);cleanupAdapter:    WintunDeleteAdapter(Adapter, FALSE, NULL);    WintunFreeAdapter(Adapter);cleanupQuit:    SetConsoleCtrlHandler(CtrlHandler, FALSE);    CloseHandle(QuitEvent);cleanupWintun:    FreeLibrary(Wintun);    return LastError;}

The operations for sending and receiving packets via the interface are also very straightforward. However, the relationship with the Windows networking protocol stack still requires further exploration.

Special Notes

The Wintun interface is strictly a layer 3 logical interface. As stated below:

Wintun is a very simple and minimal TUN driver for the Windows kernel, which provides userspace programs with a simple network adapter for reading and writing packets. It is akin to Linux’s /dev/net/tun and BSD’s /dev/tun. Originally designed for use in WireGuard, Wintun is meant to be generally useful for a wide variety of layer 3 networking protocols and experiments. The driver is open source, so anybody can inspect and build it. Due to Microsoft’s driver signing requirements, we provide precompiled and signed versions that may be distributed with your software. The goal of the project is to be as simple as possible, opting to do things in the most pure and straight-forward way provided by NDIS.

Here is a minor issue: Packets cannot be captured on this interface using Wireshark. If you want to view the encapsulated packet information, log recording, rather than packet capturing, is needed.

The reason for this issue is unclear. I suspect: Wireshark captures layer 2 packets (full Ethernet frames), but the packets on layer 3 logical interfaces have yet to be encapsulated into Ethernet frames, making it impossible to capture this interface. This is just my speculation; the root cause is unknown.

Alright, the basic introduction is complete. Let me reiterate my stance on WireGuard and WinTun: I fancy you, very much.