[ANNOUNCE] WireGuard for Windows and WireGuardNT, Version 1.0
We believe that this document is fully human-written.
Hacker News Article AI Analysis
Content Label
Human
AI Generated
0%
Human
100%
Window 1 - Human
Jason A. Donenfeld
Jason at zx2c4.com
Sat Apr 18 16:23:52 UTC 2026
Previous message (by thread): WireGuard Windows 0.6.1 - Timeline of issues (tunnels lost & import still broken)
Messages sorted by:
[ date ]
[ thread ]
[ subject ]
[ author ]
Hey again,
I’m happy to announce the v1.0 release of WireGuardNT and WireGuard
for Windows. The final “1.0 blockers” have been completed at last, and
I’m quite happy to have reached this milestone. It should now be
available from the built-in updater. And you can download it fresh
from:
- https://download.wireguard.com/windows-client/wireguard-installer.exe
- https://www.wireguard.com/install/
And to learn more about each of these two Windows projects:
- https://git.zx2c4.com/wireguard-windows/about/
- https://git.zx2c4.com/wireguard-nt/about/
Before I say more, I wanted to note that the WireGuard Project runs on
support from large companies and individuals alike. You can help out
at: https://www.wireguard.com/donations/ . If your company uses
WireGuard, consider talking to your employer about becoming a large
donor and appearing on that page. If you use a VPN from a VPN
provider, consider writing to them to suggest they donate to the
project. It does make a difference and is the reason the project is
able to live on.
The 1.0 release of WireGuardNT is a pile of bug fixes, after having
done a big read through of the source code and countless hours of new
testing. But it also has two big improvements, which have long been
considered release blockers for me.
Firstly, 1.0 now makes use of
NdisWdfGetAdapterContextFromAdapterHandle(). WireGuard’s IOCTL works
by piggybacking on the NDIS device node, so that it inherits NDIS’
setup and permissions. Each IOCTL is thus passed through the device’s
“functional device object”.
Window 2 - Human
There’s no documented function to go from
a pointer in the functional device object to the WireGuard-specific
state allocated. The functional device object’s DeviceExtension field
points to the NDIS_MINIPORT_BLOCK structure, which itself has a
pointer to the WireGuard-specific state. But that latter pointer is at
a potentially unstable offset, as it’s not within the documented part
of NDIS_MINIPORT_BLOCK. So, previously, I was using the “Reserved”
member of the functional device object to stuff a pointer in, but who
knows when that was to be used by something, a ticking time bomb.
Fortunately, every Windows 10 version since the first one has the
NdisWdfGetAdapterContextFromAdapterHandle() function, originally added
for NetAdapterCx, which means it’s not going away any time soon and
its behavior won’t change. This function simply goes to the right
offset in NDIS_MINIPORT_BLOCK where the driver-specific state is
stored. Put together, we get this handy function:
static WG_DEVICE *
WgDeviceFromFdo(_In_ DEVICE_OBJECT *DeviceObject)
{
if (DeviceObject->DeviceType != FILE_DEVICE_PHYSICAL_NETCARD ||
!DeviceObject->DeviceExtension)
return NULL;
return NdisWdfGetAdapterContextFromAdapterHandle(DeviceObject->DeviceExtension);
}
This seems to work well and will hopefully ensure reliability into the future.
The second big 1.0 blocker that’s been solved is proper MTU change
notifications. As you may or may not know, WireGuard pads packets to
the nearest 16 bytes, but only up to the MTU of the interface, in
order to protect against traffic analysis attacks. This means the
WireGuard driver needs to know its own MTU. On Linux, we have full
access to this information, as its considered a property of the
network interface itself, so we can extract it trivially with
`skb->dev->mtu`, and do various calculations. But on Windows, the MTU
is a combined property of the network adapter’s minimum and maximum,
the tcp/ip interface’s selected MTU, which splits into v4 and v6
cases, and the same split cases for the tcp/ip interface’s
subinterfaces.
Window 3 - Human
This is sort of complicated, but I guess it fit a
device model that at one point made sense. The driver is responsible
for controlling the adapter’s minimum and maximum MTU. PowerShell’s
Set-NetIpInterface will change the interface-level MTU (via
SetIpInterfaceEntry()), while netsh.exe will change the
subinterface-level MTU; both of these wind up affecting the other in
subtle ways, and the net result is the same.
Typically, the normal way of getting notifications about these
changes, from userspace or from kernel space, is with
NotifyIpInterfaceChange(), which calls a callback function with
MibParameterNotification when something has changed. But, the callback
never fires for MTU changes! That’s the only one missing. The struct
the callback receives has a field for the MTU, but still, it’s never
fired. Somebody on the relevant team at Microsoft told me in 2021,
“this is a plain oversight and should be fixed,” and somebody else
mentioned backporting the fix to the 2019 release. But for whatever
reason, this never happened, and now it’s 2026. In the interim period,
I had a really horrific, but still stable, workaround: I started a
thread, and every 3 seconds I called GetIpInterfaceEntry() on the LUID
of every running WireGuard adapter. You heard that right… I just
polled with a sleep. Gross dot net. But it was the only documented way
of doing this! At the same time, I wrote a little program I could run
on each new release of Windows to see at which point they fixed the
bug, so that I could adjust the version check to avoid the poll loop
on old versions.
Unfortunately, the bug never got fixed. But I didn’t quite feel
comfortable shipping a 1.0 with such a distasteful workaround. So I
get to work… All userspace updates to the MTU go through a file called
\Device\Nsi. The NSI driver is then responsible for dispatching this
out to the various interfaces, and also keeping current with the
various changes in the various interfaces.
Window 4 - Human
After attaching to
\Device\Nsi using the standard NT filter-style pattern with
IoAttachDeviceToDeviceStack(), I then intercept the
IOCTL_NSI_SET_ALL_PARAMETERS message that I reverse engineered,
looking at the NSI_SET_ALL_PARAMETERS struct, matching on object types
NlInterfaceObject and NlSubInterfaceObject, and reading out the NlMTU
parameter from NSI_IP_INTERFACE_RW and NSI_IP_SUBINTERFACE_RW. The
parts of these structures we care about seem extremely stable. It
appears to work well, and now the WireGuard driver can adapt to new
MTU changes instantly, rather than within 3 seconds. And there’s no
ugly polling loop. You can peruse this code in driver/nsi.c and
driver/undocumented.h if you’re curious.
That’s a lot of work – it’s a whole separate .c file in the repo – for
just getting access to one value. But that’s how things go, and it’s
information that simply must be had in order to implement WireGuard
properly.
Finally, there are a bunch of other little changes and fixes and now
we compile in C23 mode, so we have access to the typeof() keyword. We
also in theory could move to using alignas(n) instead of
__declspec(align(n)), but C standard alignas() doesn’t work on types,
only members of structs and on variables, which makes it sort of
uglier to use. If you want a struct to always be aligned, then you put
the alignas(n) on the first member. I find this awkward, so we’re
sticking with __declspec(align(n)), which also seems pretty close to
gcc’s __attribute__((aligned(n))) (which is how Linux defines its
__aligned(n) macro).
On the WireGuard for Windows front – WireGuardNT, just discussed, is
the bundled driver component of that – there are 42 bug and
correctness fixes of various varieties. And then there’s one nice
improvement for older versions of Windows 10. Windows 10 1809 added
support for SetInterfaceDnsSettings(), for setting the system DNS
server programatically.
Window 5 - Human
Before that, the only documented way was to
shell out to netsh.exe, which is what we did. It was pretty ugly, and
the way of doing that involved some really gnarly parsing.
Fortunately, newer Windows doesn’t need to do this. But it occurred to
me – since these older versions of Windows are essentially complete, I
can just reverse engineer what netsh.exe is doing under the hood, and
then do that myself, and not worry about that ever changing, since
that’s only a fallback path used for these old Windows versions. It
turns out to be pretty easy – set two variables in a normal part of
the registry and send ControlService(SERVICE_CONTROL_PARAMCHANGE) to
the Dnscache service. Easy peasy.
Anyway, please let me know how it goes and if you encounter any issues.
Jason
Previous message (by thread): WireGuard Windows 0.6.1 - Timeline of issues (tunnels lost & import still broken)
Messages sorted by:
[ date ]
[ thread ]
[ subject ]
[ author ]
More information about the WireGuard
mailing list