dragonmoonlight/app/vpn/tunnelmanager_win.cpp

370 lines
13 KiB
C++

// tunnelmanager_win.cpp — Windows WireGuard tunnel implementation.
//
// Uses Wintun (kernel TUN driver) for the virtual NIC and boringtun (via FFI)
// for the WireGuard crypto. Requires elevation: the process must run as
// Administrator (see DragonMoonlight.manifest) so that WintunCreateAdapter
// can install the kernel driver.
//
// Packet format on Windows: raw IP (no 4-byte AF header unlike macOS utun).
//
// I/O model
// ---------
// TUN → UDP thread: WintunReceivePacket → wireguard_write → sendto
// UDP → TUN thread: recvfrom → wireguard_read → WintunAllocSendPacket/Send
// Ticker thread: wireguard_tick every 100 ms (keepalives / handshake)
//
// Shutdown: SetEvent(stopEvent) unblocks WaitForMultipleObjects in all threads.
#include "tunnelmanager.h"
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <iphlpapi.h>
#include <netioapi.h>
#include <atomic>
#include <cstring>
#include <stdexcept>
#include <string>
#include <thread>
#include <vector>
#include "boringtun_ffi.h"
#include "wireguardconfig.h"
#include "wintun.h"
// ── helpers ──────────────────────────────────────────────────────────────────
static std::string lastWinError(const char *context) {
char buf[256];
FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
nullptr, GetLastError(), 0, buf, sizeof(buf), nullptr);
return std::string(context) + ": " + buf;
}
// Parse "x.x.x.x/prefix" into binary address + prefix length.
static bool parseCIDR(const std::string &cidr, IN_ADDR *addr, UINT8 *prefix) {
auto slash = cidr.find('/');
std::string ip = (slash != std::string::npos) ? cidr.substr(0, slash) : cidr;
*prefix = (slash != std::string::npos) ? (UINT8)std::stoi(cidr.substr(slash + 1)) : 32;
return InetPtonA(AF_INET, ip.c_str(), addr) == 1;
}
// ── private state ─────────────────────────────────────────────────────────────
struct WinTunnelPriv {
WintunLib wintun;
WINTUN_ADAPTER_HANDLE adapter = nullptr;
WINTUN_SESSION_HANDLE session = nullptr;
wireguard_tunnel *wg = nullptr;
SOCKET udpSock = INVALID_SOCKET;
HANDLE stopEvent = nullptr;
std::atomic<bool> running {false};
std::thread thdTunToUdp;
std::thread thdUdpToTun;
std::thread thdTicker;
WireGuardConfig cfg;
NET_LUID adapterLuid {};
};
// ── TUN → UDP thread ──────────────────────────────────────────────────────────
static void tunToUdpThread(WinTunnelPriv *p) {
// Resolve peer endpoint once
struct sockaddr_in peer {};
peer.sin_family = AF_INET;
InetPtonA(AF_INET, p->cfg.endpointHost().c_str(), &peer.sin_addr);
peer.sin_port = htons((u_short)p->cfg.endpointPort());
HANDLE readEvent = p->wintun.GetReadWaitEvent(p->session);
HANDLE events[2] = { readEvent, p->stopEvent };
static constexpr size_t BUF = 65536;
std::vector<uint8_t> dst(BUF);
while (p->running.load()) {
// Wait for a packet or stop signal
DWORD wait = WaitForMultipleObjects(2, events, FALSE, INFINITE);
if (wait != WAIT_OBJECT_0) // stopEvent or error
break;
DWORD pktSize = 0;
BYTE *pkt = p->wintun.ReceivePacket(p->session, &pktSize);
if (!pkt)
continue; // spurious wakeup
size_t dstLen = dst.size();
wireguard_result r = wireguard_write(p->wg, pkt, pktSize,
dst.data(), &dstLen);
p->wintun.ReleaseReceivePacket(p->session, pkt);
if (r.op == WRITE_TO_NETWORK && dstLen > 0) {
sendto(p->udpSock,
reinterpret_cast<const char *>(dst.data()), (int)dstLen,
0, reinterpret_cast<sockaddr *>(&peer), sizeof(peer));
}
}
}
// ── UDP → TUN thread ──────────────────────────────────────────────────────────
static void udpToTunThread(WinTunnelPriv *p) {
static constexpr size_t BUF = 65536;
std::vector<uint8_t> enc(BUF), plain(BUF);
// Make the socket non-blocking via select() with stopEvent polling
// Strategy: use WSAEventSelect so we can WaitForMultipleObjects on the socket too.
WSAEVENT sockEvent = WSACreateEvent();
WSAEventSelect(p->udpSock, sockEvent, FD_READ);
HANDLE events[2] = { sockEvent, p->stopEvent };
while (p->running.load()) {
DWORD wait = WaitForMultipleObjects(2, events, FALSE, INFINITE);
if (wait != WAIT_OBJECT_0)
break;
WSAResetEvent(sockEvent);
struct sockaddr_in from {};
int fromLen = sizeof(from);
int n = recvfrom(p->udpSock,
reinterpret_cast<char *>(enc.data()), (int)enc.size(),
0, reinterpret_cast<sockaddr *>(&from), &fromLen);
if (n <= 0)
continue;
size_t plainLen = plain.size();
wireguard_result r = wireguard_read(p->wg, enc.data(), (size_t)n,
plain.data(), &plainLen);
if ((r.op == WRITE_TO_TUNNEL_IPV4 || r.op == WRITE_TO_TUNNEL_IPV6) && plainLen > 0) {
BYTE *pkt = p->wintun.AllocateSendPacket(p->session, (DWORD)plainLen);
if (pkt) {
memcpy(pkt, plain.data(), plainLen);
p->wintun.SendPacket(p->session, pkt);
}
} else if (r.op == WRITE_TO_NETWORK) {
// Handshake response — send back to peer
struct sockaddr_in peer {};
peer.sin_family = AF_INET;
InetPtonA(AF_INET, p->cfg.endpointHost().c_str(), &peer.sin_addr);
peer.sin_port = htons((u_short)p->cfg.endpointPort());
sendto(p->udpSock,
reinterpret_cast<const char *>(plain.data()), (int)plainLen,
0, reinterpret_cast<sockaddr *>(&peer), sizeof(peer));
}
}
WSACloseEvent(sockEvent);
}
// ── Ticker thread (keepalive / re-handshake) ──────────────────────────────────
static void tickerThread(WinTunnelPriv *p) {
struct sockaddr_in peer {};
peer.sin_family = AF_INET;
InetPtonA(AF_INET, p->cfg.endpointHost().c_str(), &peer.sin_addr);
peer.sin_port = htons((u_short)p->cfg.endpointPort());
static constexpr size_t BUF = 512;
std::vector<uint8_t> dst(BUF);
while (p->running.load()) {
// Wait 100 ms or until stop
if (WaitForSingleObject(p->stopEvent, 100) != WAIT_TIMEOUT)
break;
size_t dstLen = dst.size();
wireguard_result r = wireguard_tick(p->wg, dst.data(), &dstLen);
if (r.op == WRITE_TO_NETWORK && dstLen > 0) {
sendto(p->udpSock,
reinterpret_cast<const char *>(dst.data()), (int)dstLen,
0, reinterpret_cast<sockaddr *>(&peer), sizeof(peer));
}
}
}
// ── Route setup helpers ───────────────────────────────────────────────────────
// Configure the Wintun adapter's unicast address via the IP Helper API.
static void setAdapterAddress(const NET_LUID &luid, const std::string &cidr) {
IN_ADDR addr {};
UINT8 prefix = 32;
if (!parseCIDR(cidr, &addr, &prefix))
return;
MIB_UNICASTIPADDRESS_ROW row {};
InitializeUnicastIpAddressEntry(&row);
row.InterfaceLuid = luid;
row.Address.si_family = AF_INET;
row.Address.Ipv4.sin_family = AF_INET;
row.Address.Ipv4.sin_addr = addr;
row.OnLinkPrefixLength = prefix;
row.DadState = IpDadStatePreferred;
CreateUnicastIpAddressEntry(&row); // ignore error: may already exist
}
// Add a host/network route through the Wintun adapter.
static void addRoute(const NET_LUID &luid, const std::string &cidr) {
IN_ADDR net {};
UINT8 prefix = 0;
if (!parseCIDR(cidr, &net, &prefix))
return;
MIB_IPFORWARD_ROW2 row {};
InitializeIpForwardEntry(&row);
row.InterfaceLuid = luid;
row.DestinationPrefix.Prefix.si_family = AF_INET;
row.DestinationPrefix.Prefix.Ipv4.sin_family = AF_INET;
row.DestinationPrefix.Prefix.Ipv4.sin_addr = net;
row.DestinationPrefix.PrefixLength = prefix;
row.NextHop.si_family = AF_INET; // on-link
row.Metric = 1;
row.Protocol = MIB_IPPROTO_NETMGMT;
CreateIpForwardEntry2(&row); // ignore duplicate error
}
// ── TunnelManager implementation ──────────────────────────────────────────────
void TunnelManager::start(const WireGuardConfig &cfg) {
if (m_priv) return; // already running
auto *p = new WinTunnelPriv();
p->cfg = cfg;
// ── 1. Load wintun.dll ──
if (!p->wintun.load()) {
delete p;
emit error("Failed to load wintun.dll — is it next to the executable?");
return;
}
// ── 2. Create WireGuard tunnel (boringtun) ──
p->wg = new_tunnel(
cfg.privateKey().c_str(),
cfg.peerPublicKey().c_str(),
cfg.presharedKey().empty() ? nullptr : cfg.presharedKey().c_str(),
cfg.persistentKeepalive(),
1 // log level: errors only
);
if (!p->wg) {
p->wintun.unload();
delete p;
emit error("boringtun: failed to create WireGuard tunnel");
return;
}
// ── 3. Create Wintun adapter ──
GUID guid;
CoCreateGuid(&guid);
p->adapter = p->wintun.CreateAdapter(L"DragonMoonlight", L"WireGuard", &guid);
if (!p->adapter) {
tunnel_free(p->wg);
p->wintun.unload();
delete p;
emit error(QString::fromStdString(lastWinError("WintunCreateAdapter")));
return;
}
p->wintun.GetAdapterLUID(p->adapter, &p->adapterLuid);
// ── 4. Configure IP address + routes ──
setAdapterAddress(p->adapterLuid, cfg.address());
for (const auto &cidr : cfg.allowedIPs()) {
addRoute(p->adapterLuid, cidr);
}
// ── 5. Start Wintun session ──
p->session = p->wintun.StartSession(p->adapter, 0x400000); // 4 MiB ring
if (!p->session) {
p->wintun.DeleteAdapter(p->adapter);
tunnel_free(p->wg);
p->wintun.unload();
delete p;
emit error(QString::fromStdString(lastWinError("WintunStartSession")));
return;
}
// ── 6. Create UDP socket ──
WSADATA wsd {};
WSAStartup(MAKEWORD(2, 2), &wsd);
p->udpSock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (p->udpSock == INVALID_SOCKET) {
p->wintun.EndSession(p->session);
p->wintun.DeleteAdapter(p->adapter);
tunnel_free(p->wg);
p->wintun.unload();
delete p;
emit error("Failed to create UDP socket");
return;
}
// Bind to any local port
struct sockaddr_in local {};
local.sin_family = AF_INET;
local.sin_addr.s_addr = INADDR_ANY;
local.sin_port = 0;
bind(p->udpSock, reinterpret_cast<sockaddr *>(&local), sizeof(local));
// ── 7. Stop event for I/O threads ──
p->stopEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
// ── 8. Force initial handshake ──
{
static constexpr size_t BUF = 512;
std::vector<uint8_t> hs(BUF);
size_t hsLen = hs.size();
wireguard_force_handshake(p->wg, hs.data(), &hsLen);
if (hsLen > 0) {
struct sockaddr_in peer {};
peer.sin_family = AF_INET;
InetPtonA(AF_INET, cfg.endpointHost().c_str(), &peer.sin_addr);
peer.sin_port = htons((u_short)cfg.endpointPort());
sendto(p->udpSock,
reinterpret_cast<const char *>(hs.data()), (int)hsLen,
0, reinterpret_cast<sockaddr *>(&peer), sizeof(peer));
}
}
// ── 9. Start I/O threads ──
p->running = true;
p->thdTunToUdp = std::thread(tunToUdpThread, p);
p->thdUdpToTun = std::thread(udpToTunThread, p);
p->thdTicker = std::thread(tickerThread, p);
m_priv = p;
emit tunnelUp(QString::fromStdString(cfg.address()));
}
void TunnelManager::stop() {
if (!m_priv) return;
auto *p = static_cast<WinTunnelPriv *>(m_priv);
// Signal all threads to stop
p->running = false;
SetEvent(p->stopEvent);
// Wait for threads
if (p->thdTunToUdp.joinable()) p->thdTunToUdp.join();
if (p->thdUdpToTun.joinable()) p->thdUdpToTun.join();
if (p->thdTicker.joinable()) p->thdTicker.join();
// Teardown Wintun
p->wintun.EndSession(p->session);
p->wintun.DeleteAdapter(p->adapter);
// Teardown boringtun
tunnel_free(p->wg);
// Close socket & event
closesocket(p->udpSock);
WSACleanup();
CloseHandle(p->stopEvent);
p->wintun.unload();
delete p;
m_priv = nullptr;
emit tunnelDown();
}