A native Windows GUI client that connects to an ezvpn server built from this
repo. Like the Apple client, the Windows client is split across two repositories:
- This repo (
ezvpn) — the Rust core, packaged asezvpn.dll(a C-ABIcdylib) plus a small C FFI (src/ffi_windows.rs, headerwindows/ezvpn.h). This is where the Windows FFI Rust code and build script live. ezvpn-windows— the .NET solution: a WinUI 3 app that P/Invokesezvpn.dll. Build/run/install instructions live in that repo's README.
It is intentionally scoped for development-signed personal use — the MSI is unsigned.
In scope:
- Dual-stack split tunnel — IPv4, IPv6, or both, to explicit routed prefixes. Both route lists are optional and independent.
- Automatic reconnect — reuses the desktop client's
run_with_reconnectloop (exponential backoff, network-consistency check). - Automatic underlay bypass — the desktop
BypassRouteManagercarves the few server/relay underlay addresses that overlap a routed prefix back out of the tunnel.
Always out of scope (never part of this project):
- In-app split DNS — DNS is configured outside the tunnel (see
docs/Architecture.md). - MSIX / Microsoft Store packaging and code signing — the installer is a plain, unsigned MSI.
The data plane is the same portable code the desktop CLI uses. The key difference from the Apple app is who owns the TUN device:
| Concern | Apple app extension | Windows GUI (ezvpn.dll) |
|---|---|---|
| TUN device | created by the OS; ezvpn wraps the handed-over utun fd (TunDevice::from_raw_fd) |
created by ezvpn itself (wintun via TunDevice::create) |
| FFI shape | fd-based: ezvpn_connect → ezvpn_run(utun_fd) → ezvpn_stop |
VpnClient-based: ezvpn_start → ezvpn_status → ezvpn_stop |
| Routing / IP / MTU | NEPacketTunnelNetworkSettings (extension) |
netsh (inside VpnClient) |
| Underlay bypass | excludedRoutes computed at connect |
live BypassRouteManager host routes |
| Single-instance lock, control socket | not used | reused from the desktop client |
There is no Windows equivalent of NEPacketTunnelProvider that hands an app
a ready TUN fd. So the Windows FFI wraps the desktop
VpnClient — which already creates the wintun
adapter, installs routes, auto-reconnects, and publishes a status snapshot on
Windows — instead of the slim fd-driven MobileSession. That also means the FFI is
a start / status / stop shape rather than connect / run(fd) / stop, and the
GUI reads status in-process rather than over the named-pipe control endpoint.
Key source in this repo:
src/ffi_windows.rs— the C entry points (ezvpn_start,ezvpn_status,ezvpn_conn_path,ezvpn_stop,ezvpn_init_logging).src/tunnel/client.rs— theVpnClientthe FFI drives (VpnClient::new,run_with_reconnect,status_handle).src/control.rs— theStatusSnapshot/ClientStatusthe FFI serializes.windows/ezvpn.h— the C header (also the authoritative JSON config/status shapes).build-windows.ps1— buildsezvpn.dlland stages it with the header indist/windows.
The app drives the tunnel with these calls (full signatures and JSON shapes in
windows/ezvpn.h):
ezvpn_start(config_json, out_buf, out_len)— parse the config, create an iroh endpoint and aVpnClient, and spawn its reconnecting run loop on a background thread. Returns an opaque handle once started (not yet connected).ezvpn_status(handle, out_buf, out_len)— snapshot the live client status (state, assigned IPs, gateway, routes, bypass addresses) as JSON. Cheap enough to poll for the"connected"state: it leaves out the connection path and custom-relay health.ezvpn_conn_path(handle, out_buf, out_len)— on demand (the app's "Connection path…" button, as on Apple and Android): every iroh path with the selected one marked, plus each custom relay's/healthzresult. Same JSON as the Apple/Androidezvpn_conn_path; not for polling, since it makes the relay health requests.ezvpn_stop(handle)— signal the loop to stop, wait for the route/adapter teardown to finish, and free the handle.
Ezvpn.App (WinUI 3, elevated)
create/edit profiles ──▶ ezvpn_start(json) ──▶ ezvpn.dll
poll status ──▶ ezvpn_status() (iroh connect + handshake
"Connection path…" ──▶ ezvpn_conn_path() + wintun + routes + reconnect)
disconnect ──▶ ezvpn_stop()
The whole tunnel runs in-process in the elevated GUI — there is no Windows Service and no IPC.
- Runs elevated (Administrator): creating the wintun adapter and editing the
routing table require it. The
ezvpn-windowsapp ships anapp.manifestwithrequestedExecutionLevel = requireAdministrator. wintun.dll(from wintun.net, the official WireGuard project) must sit next toezvpn.dllor onPATH. Theezvpn-windowsMSI bundles it.
From this repo, in PowerShell:
./build-windows.ps1 # release build (default), x86_64-pc-windows-msvc
./build-windows.ps1 -Profile debug
./build-windows.ps1 -Target aarch64-pc-windows-msvcThis stages dist/windows/ezvpn.dll (+ ezvpn.dll.lib, ezvpn.h). The CI
release workflow zips it into an ezvpn-windows.dll.zip release asset, which
ezvpn-windows downloads by default (pinned by URL + checksum). For local FFI
dev, set EZVPN_LOCAL_DLL=1 when building ezvpn-windows to link this
dist/windows build directly.
Then follow the
ezvpn-windows README to build
and run the app or the MSI installer.