From ff1473732ce512b3ca8b53b66c3ccb76be275230 Mon Sep 17 00:00:00 2001 From: cd-amn Date: Mon, 15 Jun 2026 21:22:06 +0400 Subject: [PATCH] feat: TrafficGuard owns Xray adapter IP swap on Windows --- client/core/protocols/xrayProtocol.cpp | 4 ++ client/core/vpnTrafficGuard.cpp | 24 ++++++++ client/vpnConnection.cpp | 6 ++ ipc/ipc_interface.rep | 4 ++ ipc/ipcserver.cpp | 76 ++++++++++++++++++++++++++ ipc/ipcserver.h | 2 + 6 files changed, 116 insertions(+) diff --git a/client/core/protocols/xrayProtocol.cpp b/client/core/protocols/xrayProtocol.cpp index 9a12cc0f7..1384ed328 100644 --- a/client/core/protocols/xrayProtocol.cpp +++ b/client/core/protocols/xrayProtocol.cpp @@ -272,11 +272,15 @@ ErrorCode XrayProtocol::setupRouting() { return IpcClient::withInterface( [this](QSharedPointer iface) -> ErrorCode { +#ifndef Q_OS_WIN auto createTun = iface->createTun(m_tunName, amnezia::protocols::xray::defaultLocalAddr); if (!createTun.waitForFinished() || !createTun.returnValue()) { qCritical() << "Failed to assign IP address for TUN"; return ErrorCode::InternalError; } +#else + Q_UNUSED(iface) +#endif emit tunnelAddressesUpdated(m_vpnGateway, m_vpnLocalAddress); return ErrorCode::NoError; diff --git a/client/core/vpnTrafficGuard.cpp b/client/core/vpnTrafficGuard.cpp index 0e4398cd1..a43e0f144 100644 --- a/client/core/vpnTrafficGuard.cpp +++ b/client/core/vpnTrafficGuard.cpp @@ -456,6 +456,30 @@ void VpnTrafficGuard::bringUp(Tunnel* tunnel) void VpnTrafficGuard::commit(Tunnel* tunnel) { if (!tunnel) return; + +#if defined(AMNEZIA_DESKTOP) && defined(Q_OS_WIN) + if (VpnProtocol::isXrayBased(tunnel->container())) { + const QJsonObject &cfg = tunnel->config(); + const QString ipv4 = cfg.value(QStringLiteral("deviceIpv4Address")).toString(); + const QString ipv6 = cfg.value(QStringLiteral("deviceIpv6Address")).toString(); + const QString handover = tunnel->handoverIfname(); + IpcClient::withInterface([&](QSharedPointer iface) { + if (!handover.isEmpty() && handover != tunnel->ifname()) { + auto rm = iface->removeAdapterAddress(handover, ipv4, ipv6); + if (!rm.waitForFinished(2000) || !rm.returnValue()) { + qWarning() << "VpnTrafficGuard::commit: removeAdapterAddress failed for" + << handover; + } + } + auto ap = iface->applyAdapterAddress(tunnel->ifname(), ipv4, ipv6); + if (!ap.waitForFinished(15000) || !ap.returnValue()) { + qWarning() << "VpnTrafficGuard::commit: applyAdapterAddress failed for" + << tunnel->ifname(); + } + }); + } +#endif + applyPolicy(tunnel); connect(tunnel, &Tunnel::activated, this, [this, tunnel] { if (auto p = tunnel->protocol()) { diff --git a/client/vpnConnection.cpp b/client/vpnConnection.cpp index d119d2f36..0726393f6 100644 --- a/client/vpnConnection.cpp +++ b/client/vpnConnection.cpp @@ -29,6 +29,7 @@ #include "platforms/ios/ios_controller.h" #endif +#include "core/utils/constants/protocolConstants.h" #include "core/utils/networkUtilities.h" using namespace ProtocolUtils; @@ -222,6 +223,7 @@ void VpnConnection::connectToVpn(const QString &serverId, DockerContainer contai config.insert("ifname", preAllocatedIfname); if (isXray) { config.insert("tunName", preAllocatedIfname); + config.insert("deviceIpv4Address", amnezia::protocols::xray::defaultLocalAddr); } m_active = new Tunnel(preAllocatedIfname, container, config, resolvedRemote, this); wireTunnelSignals(m_active, /*isActive=*/true); @@ -550,11 +552,15 @@ void VpnConnection::startTunnelSwitch(DockerContainer container, config.insert("ifname", stagingIfname); if (VpnProtocol::isXrayBased(container)) { config.insert("tunName", stagingIfname); + config.insert("deviceIpv4Address", amnezia::protocols::xray::defaultLocalAddr); } appendKillSwitchConfig(config); appendSplitTunnelingConfig(config); m_staging = new Tunnel(stagingIfname, container, config, resolvedRemote, this); + if (m_active) { + m_staging->setHandoverIfname(m_active->ifname()); + } wireTunnelSignals(m_staging, /*isActive=*/false); setConnectionState(Vpn::ConnectionState::Switching); diff --git a/ipc/ipc_interface.rep b/ipc/ipc_interface.rep index 3e61e8c1e..46edde152 100644 --- a/ipc/ipc_interface.rep +++ b/ipc/ipc_interface.rep @@ -31,6 +31,10 @@ class IpcInterface SLOT( bool createTun(const QString &dev, const QString &subnet) ); SLOT( bool deleteTun(const QString &dev) ); + SLOT( bool applyAdapterAddress(const QString &ifname, const QString &ipv4, const QString &ipv6) ); + + SLOT( bool removeAdapterAddress(const QString &ifname, const QString &ipv4, const QString &ipv6) ); + SLOT( bool StartRoutingIpv6() ); SLOT( bool StopRoutingIpv6() ); diff --git a/ipc/ipcserver.cpp b/ipc/ipcserver.cpp index 94750b10c..c7ad04726 100644 --- a/ipc/ipcserver.cpp +++ b/ipc/ipcserver.cpp @@ -29,6 +29,9 @@ #endif #ifdef Q_OS_WIN + #include + #include + #include #include "tapcontroller_win.h" #endif @@ -212,6 +215,79 @@ bool IpcServer::deleteTun(const QString &dev) return Router::deleteTun(dev); } +bool IpcServer::applyAdapterAddress(const QString &ifname, const QString &ipv4, const QString &ipv6) +{ +#ifdef Q_OS_WIN + bool ok = true; + // Router::createTun on Windows assigns the address and blocks until it + // becomes live on the adapter (NotifyUnicastIpAddressChange callback). + if (!ipv4.isEmpty()) { + ok &= Router::createTun(ifname, ipv4); + } + if (!ipv6.isEmpty()) { + NET_LUID luid; + if (ConvertInterfaceAliasToLuid(reinterpret_cast(ifname.utf16()), &luid) != NO_ERROR) { + qWarning() << "IpcServer::applyAdapterAddress: cannot resolve" << ifname; + return false; + } + const QByteArray ip = ipv6.section('/', 0, 0).toUtf8(); + MIB_UNICASTIPADDRESS_ROW row; + InitializeUnicastIpAddressEntry(&row); + row.InterfaceLuid.Value = luid.Value; + row.Address.si_family = AF_INET6; + row.OnLinkPrefixLength = 128; + row.DadState = IpDadStatePreferred; + if (InetPtonA(AF_INET6, ip.toStdString().c_str(), &row.Address.Ipv6.sin6_addr) != 1) { + qWarning() << "IpcServer::applyAdapterAddress: cannot parse" << ipv6; + return false; + } + DWORD r = CreateUnicastIpAddressEntry(&row); + ok &= (r == NO_ERROR || r == ERROR_OBJECT_ALREADY_EXISTS); + } + return ok; +#else + Q_UNUSED(ifname) + Q_UNUSED(ipv4) + Q_UNUSED(ipv6) + return true; +#endif +} + +bool IpcServer::removeAdapterAddress(const QString &ifname, const QString &ipv4, const QString &ipv6) +{ +#ifdef Q_OS_WIN + NET_LUID luid; + if (ConvertInterfaceAliasToLuid(reinterpret_cast(ifname.utf16()), &luid) != NO_ERROR) { + qWarning() << "IpcServer::removeAdapterAddress: cannot resolve" << ifname; + return false; + } + + auto removeOne = [&](const QString& addr, int family) -> bool { + if (addr.isEmpty()) return true; + const QByteArray ip = addr.section('/', 0, 0).toUtf8(); + MIB_UNICASTIPADDRESS_ROW row; + InitializeUnicastIpAddressEntry(&row); + row.InterfaceLuid.Value = luid.Value; + row.Address.si_family = static_cast(family); + void* dst = (family == AF_INET) + ? static_cast(&row.Address.Ipv4.sin_addr) + : static_cast(&row.Address.Ipv6.sin6_addr); + if (InetPtonA(family, ip.toStdString().c_str(), dst) != 1) return false; + DWORD r = DeleteUnicastIpAddressEntry(&row); + return r == NO_ERROR || r == ERROR_NOT_FOUND; + }; + + bool ok = removeOne(ipv4, AF_INET); + ok &= removeOne(ipv6, AF_INET6); + return ok; +#else + Q_UNUSED(ifname) + Q_UNUSED(ipv4) + Q_UNUSED(ipv6) + return true; +#endif +} + bool IpcServer::updateResolvers(const QString &ifname, const QList &resolvers) { #ifdef MZ_DEBUG diff --git a/ipc/ipcserver.h b/ipc/ipcserver.h index e7c37215e..1d7b0aae8 100644 --- a/ipc/ipcserver.h +++ b/ipc/ipcserver.h @@ -43,6 +43,8 @@ public: virtual void setLogsEnabled(bool enabled) override; virtual bool createTun(const QString &dev, const QString &subnet) override; virtual bool deleteTun(const QString &dev) override; + virtual bool applyAdapterAddress(const QString &ifname, const QString &ipv4, const QString &ipv6) override; + virtual bool removeAdapterAddress(const QString &ifname, const QString &ipv4, const QString &ipv6) override; virtual bool StartRoutingIpv6() override; virtual bool StopRoutingIpv6() override; virtual bool disableAllTraffic() override;