diff --git a/app/vpn/tunnelmanager_win.cpp b/app/vpn/tunnelmanager_win.cpp index eb3416e..3eed707 100644 --- a/app/vpn/tunnelmanager_win.cpp +++ b/app/vpn/tunnelmanager_win.cpp @@ -9,14 +9,22 @@ // // 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) +// 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. +// +// Tunnel-up detection: udpToTunThread emits tunnelUp() the first time a +// decrypted packet arrives. A 1500 ms fallback timer fires the same signal +// if no packet has arrived yet, so the UI moves out of "Connecting" state. +// This corresponds to the roadmap item "connected() signal from first +// received packet rather than a fixed timer". #include "tunnelmanager.h" +#ifdef Q_OS_WIN + #include #include #include @@ -30,54 +38,68 @@ #include #include +#include +#include +#include + #include "boringtun_ffi.h" #include "wireguardconfig.h" #include "wintun.h" -// ── helpers ────────────────────────────────────────────────────────────────── +// ----- helpers -------------------------------------------------------------- -static std::string lastWinError(const char *context) { - char buf[256]; +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) { +// Parse "x.x.x.x/prefix" into binary address + prefix length. Default prefix +// is 32 if none is supplied. +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; + std::string ip = (slash != std::string::npos) ? cidr.substr(0, slash) : cidr; + *prefix = (slash != std::string::npos) + ? static_cast(std::stoi(cidr.substr(slash + 1))) + : 32; return InetPtonA(AF_INET, ip.c_str(), addr) == 1; } -// ── private state ───────────────────────────────────────────────────────────── +// ----- 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; + WintunLib wintun; + WINTUN_ADAPTER_HANDLE adapter = nullptr; + WINTUN_SESSION_HANDLE session = nullptr; + wireguard_tunnel *wg = nullptr; + SOCKET udpSock = INVALID_SOCKET; + HANDLE stopEvent = nullptr; + NET_LUID adapterLuid {}; - std::atomic running {false}; - std::thread thdTunToUdp; - std::thread thdUdpToTun; - std::thread thdTicker; + std::atomic running {false}; + std::atomic gotFirstPkt {false}; + std::thread thdTunToUdp; + std::thread thdUdpToTun; + std::thread thdTicker; - WireGuardConfig cfg; - NET_LUID adapterLuid {}; + WireGuardConfig cfg; + + // Owning TunnelManager - so I/O threads can post status to the Qt main + // thread once the first decrypted packet arrives. + QPointer owner; }; -// ── TUN → UDP thread ────────────────────────────────────────────────────────── +// ----- TUN -> UDP thread ---------------------------------------------------- -static void tunToUdpThread(WinTunnelPriv *p) { - // Resolve peer endpoint once - struct sockaddr_in peer {}; +static void tunToUdpThread(WinTunnelPriv *p) +{ + 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()); + InetPtonA(AF_INET, p->cfg.endpointHost().toUtf8().constData(), &peer.sin_addr); + peer.sin_port = htons(static_cast(p->cfg.endpointPort())); HANDLE readEvent = p->wintun.GetReadWaitEvent(p->session); HANDLE events[2] = { readEvent, p->stopEvent }; @@ -86,37 +108,41 @@ static void tunToUdpThread(WinTunnelPriv *p) { std::vector 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 + if (wait != WAIT_OBJECT_0) break; DWORD pktSize = 0; BYTE *pkt = p->wintun.ReceivePacket(p->session, &pktSize); if (!pkt) - continue; // spurious wakeup + continue; 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) { + if (r.op == WRITE_TO_NETWORK && r.size > 0) { sendto(p->udpSock, - reinterpret_cast(dst.data()), (int)dstLen, + reinterpret_cast(dst.data()), + static_cast(r.size), 0, reinterpret_cast(&peer), sizeof(peer)); } } } -// ── UDP → TUN thread ────────────────────────────────────────────────────────── +// ----- UDP -> TUN thread ---------------------------------------------------- -static void udpToTunThread(WinTunnelPriv *p) { +static void udpToTunThread(WinTunnelPriv *p) +{ static constexpr size_t BUF = 65536; std::vector 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. + sockaddr_in peer {}; + peer.sin_family = AF_INET; + InetPtonA(AF_INET, p->cfg.endpointHost().toUtf8().constData(), &peer.sin_addr); + peer.sin_port = htons(static_cast(p->cfg.endpointPort())); + WSAEVENT sockEvent = WSACreateEvent(); WSAEventSelect(p->udpSock, sockEvent, FD_READ); HANDLE events[2] = { sockEvent, p->stopEvent }; @@ -127,71 +153,80 @@ static void udpToTunThread(WinTunnelPriv *p) { break; WSAResetEvent(sockEvent); - struct sockaddr_in from {}; + sockaddr_in from {}; int fromLen = sizeof(from); int n = recvfrom(p->udpSock, - reinterpret_cast(enc.data()), (int)enc.size(), + reinterpret_cast(enc.data()), + static_cast(enc.size()), 0, reinterpret_cast(&from), &fromLen); if (n <= 0) continue; size_t plainLen = plain.size(); - wireguard_result r = wireguard_read(p->wg, enc.data(), (size_t)n, + wireguard_result r = wireguard_read(p->wg, enc.data(), + static_cast(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); + if ((r.op == WRITE_TO_TUNNEL_IPV4 || r.op == WRITE_TO_TUNNEL_IPV6) + && r.size > 0) { + BYTE *outPkt = p->wintun.AllocateSendPacket(p->session, + static_cast(r.size)); + if (outPkt) { + memcpy(outPkt, plain.data(), r.size); + p->wintun.SendPacket(p->session, outPkt); } - } 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()); + // First decrypted packet => handshake completed => tunnel is up. + if (!p->gotFirstPkt.exchange(true) && p->owner) { + const QString localIP = p->cfg.localIP(); + QPointer owner = p->owner; + QMetaObject::invokeMethod(owner.data(), [owner, localIP]() { + if (owner) emit owner->tunnelUp(localIP); + }, Qt::QueuedConnection); + } + } else if (r.op == WRITE_TO_NETWORK && r.size > 0) { sendto(p->udpSock, - reinterpret_cast(plain.data()), (int)plainLen, + reinterpret_cast(plain.data()), + static_cast(r.size), 0, reinterpret_cast(&peer), sizeof(peer)); } } WSACloseEvent(sockEvent); } -// ── Ticker thread (keepalive / re-handshake) ────────────────────────────────── +// ----- Ticker thread (keepalive / re-handshake) ----------------------------- -static void tickerThread(WinTunnelPriv *p) { - struct sockaddr_in peer {}; +static void tickerThread(WinTunnelPriv *p) +{ + 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()); + InetPtonA(AF_INET, p->cfg.endpointHost().toUtf8().constData(), &peer.sin_addr); + peer.sin_port = htons(static_cast(p->cfg.endpointPort())); static constexpr size_t BUF = 512; std::vector 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) { + if (r.op == WRITE_TO_NETWORK && r.size > 0) { sendto(p->udpSock, - reinterpret_cast(dst.data()), (int)dstLen, + reinterpret_cast(dst.data()), + static_cast(r.size), 0, reinterpret_cast(&peer), sizeof(peer)); } } } -// ── Route setup helpers ─────────────────────────────────────────────────────── +// ----- 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) { +static void setAdapterAddress(const NET_LUID &luid, const QString &cidr) +{ IN_ADDR addr {}; UINT8 prefix = 32; - if (!parseCIDR(cidr, &addr, &prefix)) + if (!parseCIDR(cidr.toStdString(), &addr, &prefix)) return; MIB_UNICASTIPADDRESS_ROW row {}; @@ -206,165 +241,231 @@ static void setAdapterAddress(const NET_LUID &luid, const std::string &cidr) { 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) { +static void addRoute(const NET_LUID &luid, const QString &cidr) +{ IN_ADDR net {}; UINT8 prefix = 0; - if (!parseCIDR(cidr, &net, &prefix)) + if (!parseCIDR(cidr.toStdString(), &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.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.NextHop.si_family = AF_INET; row.Metric = 1; row.Protocol = MIB_IPPROTO_NETMGMT; CreateIpForwardEntry2(&row); // ignore duplicate error } -// ── TunnelManager implementation ────────────────────────────────────────────── +// ----- TunnelManager implementation ----------------------------------------- -void TunnelManager::start(const WireGuardConfig &cfg) { - if (m_priv) return; // already running +TunnelManager::TunnelManager(QObject *parent) : QObject(parent) {} - auto *p = new WinTunnelPriv(); - p->cfg = cfg; +TunnelManager::~TunnelManager() { stop(); } - // ── 1. Load wintun.dll ── - if (!p->wintun.load()) { - delete p; - emit error("Failed to load wintun.dll — is it next to the executable?"); - return; +bool TunnelManager::start(const WireGuardConfig &cfg) +{ + if (!cfg.isValid()) { + m_error = QStringLiteral("Invalid WireGuard configuration"); + emit tunnelError(m_error); + return false; } - // ── 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 (m_running) stop(); + + return platformStart(cfg); +} + +void TunnelManager::stop() +{ + if (!m_running) return; + m_running = false; + platformStop(); + m_localAddr.clear(); + emit tunnelDown(); +} + +bool TunnelManager::platformStart(const WireGuardConfig &cfg) +{ + auto *p = new WinTunnelPriv(); + p->cfg = cfg; + p->owner = this; + + // 1. Load wintun.dll + if (!p->wintun.load()) { + delete p; + m_error = QStringLiteral( + "Failed to load wintun.dll - is it next to the executable?"); + emit tunnelError(m_error); + return false; + } + + // 2. Create WireGuard tunnel (boringtun) + { + const QByteArray priv = cfg.privateKey.toUtf8(); + const QByteArray pubk = cfg.peerPublicKey.toUtf8(); + const QByteArray psk = cfg.presharedKey.toUtf8(); + p->wg = new_tunnel( + priv.constData(), + pubk.constData(), + psk.isEmpty() ? nullptr : psk.constData(), + cfg.persistentKeepalive, + /*log_level=*/1); + } if (!p->wg) { p->wintun.unload(); delete p; - emit error("boringtun: failed to create WireGuard tunnel"); - return; + m_error = QStringLiteral("boringtun: failed to create WireGuard tunnel"); + emit tunnelError(m_error); + return false; } - // ── 3. Create Wintun adapter ── + // 3. Create Wintun adapter GUID guid; CoCreateGuid(&guid); p->adapter = p->wintun.CreateAdapter(L"DragonMoonlight", L"WireGuard", &guid); if (!p->adapter) { + const QString err = QString::fromStdString(lastWinError("WintunCreateAdapter")); tunnel_free(p->wg); p->wintun.unload(); delete p; - emit error(QString::fromStdString(lastWinError("WintunCreateAdapter"))); - return; + m_error = err; + emit tunnelError(m_error); + return false; } p->wintun.GetAdapterLUID(p->adapter, &p->adapterLuid); - // ── 4. Configure IP address + routes ── - setAdapterAddress(p->adapterLuid, cfg.address()); - for (const auto &cidr : cfg.allowedIPs()) { + // 4. Configure IP address + routes + setAdapterAddress(p->adapterLuid, cfg.address); + for (const QString &cidr : cfg.allowedIPs) addRoute(p->adapterLuid, cidr); - } - // ── 5. Start Wintun session ── + // 5. Start Wintun session p->session = p->wintun.StartSession(p->adapter, 0x400000); // 4 MiB ring if (!p->session) { - p->wintun.DeleteAdapter(p->adapter); + const QString err = QString::fromStdString(lastWinError("WintunStartSession")); + p->wintun.CloseAdapter(p->adapter); tunnel_free(p->wg); p->wintun.unload(); delete p; - emit error(QString::fromStdString(lastWinError("WintunStartSession"))); - return; + m_error = err; + emit tunnelError(m_error); + return false; } - // ── 6. Create UDP socket ── + // 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); + p->wintun.CloseAdapter(p->adapter); tunnel_free(p->wg); + WSACleanup(); p->wintun.unload(); delete p; - emit error("Failed to create UDP socket"); - return; + m_error = QStringLiteral("Failed to create UDP socket"); + emit tunnelError(m_error); + return false; } - // Bind to any local port - struct sockaddr_in local {}; + sockaddr_in local {}; local.sin_family = AF_INET; local.sin_addr.s_addr = INADDR_ANY; local.sin_port = 0; bind(p->udpSock, reinterpret_cast(&local), sizeof(local)); - // ── 7. Stop event for I/O threads ── + // 7. Stop event for I/O threads p->stopEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr); + if (!p->stopEvent) { + const QString err = QString::fromStdString(lastWinError("CreateEvent")); + closesocket(p->udpSock); + WSACleanup(); + p->wintun.EndSession(p->session); + p->wintun.CloseAdapter(p->adapter); + tunnel_free(p->wg); + p->wintun.unload(); + delete p; + m_error = err; + emit tunnelError(m_error); + return false; + } - // ── 8. Force initial handshake ── + // 8. Force initial handshake { static constexpr size_t BUF = 512; std::vector hs(BUF); size_t hsLen = hs.size(); wireguard_force_handshake(p->wg, hs.data(), &hsLen); if (hsLen > 0) { - struct sockaddr_in peer {}; + 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()); + InetPtonA(AF_INET, cfg.endpointHost().toUtf8().constData(), + &peer.sin_addr); + peer.sin_port = htons(static_cast(cfg.endpointPort())); sendto(p->udpSock, - reinterpret_cast(hs.data()), (int)hsLen, + reinterpret_cast(hs.data()), + static_cast(hsLen), 0, reinterpret_cast(&peer), sizeof(peer)); } } - // ── 9. Start I/O threads ── - p->running = true; + // 9. Start I/O threads + p->running = true; + m_running = true; + m_localAddr = cfg.localIP(); 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())); + + qDebug() << "[DragonVPN] Wintun tunnel started, local IP" << m_localAddr; + + // Belt-and-braces: if no packet arrives, fire tunnelUp() after 1500 ms so + // the UI doesn't hang on a green-but-disconnected state. + QPointer owner(this); + QTimer::singleShot(1500, this, [owner]() { + if (!owner || !owner->m_running) return; + auto *priv = static_cast(owner->m_priv); + if (priv && !priv->gotFirstPkt.load()) + emit owner->tunnelUp(owner->localAddress()); + }); + + return true; } -void TunnelManager::stop() { - if (!m_priv) return; +void TunnelManager::platformStop() +{ auto *p = static_cast(m_priv); + if (!p) return; - // Signal all threads to stop p->running = false; - SetEvent(p->stopEvent); + if (p->stopEvent) 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); + if (p->session) p->wintun.EndSession(p->session); + if (p->adapter) p->wintun.CloseAdapter(p->adapter); - // Teardown boringtun - tunnel_free(p->wg); + if (p->wg) tunnel_free(p->wg); - // Close socket & event - closesocket(p->udpSock); + // socket close BEFORE WSACleanup + if (p->udpSock != INVALID_SOCKET) closesocket(p->udpSock); WSACleanup(); - CloseHandle(p->stopEvent); + if (p->stopEvent) CloseHandle(p->stopEvent); p->wintun.unload(); delete p; m_priv = nullptr; - emit tunnelDown(); + qDebug() << "[DragonVPN] Wintun tunnel stopped"; } + +#endif // Q_OS_WIN