diff --git a/client/3rd-prebuilt b/client/3rd-prebuilt index d473b050c..caaa79ff9 160000 --- a/client/3rd-prebuilt +++ b/client/3rd-prebuilt @@ -1 +1 @@ -Subproject commit d473b050cdbbef3c35ab91d434806c4d19d7e35d +Subproject commit caaa79ff99ef11421c72b5e511ea33815847c189 diff --git a/client/3rd/amneziawg-apple b/client/3rd/amneziawg-apple index 811af0a83..137de1c6d 160000 --- a/client/3rd/amneziawg-apple +++ b/client/3rd/amneziawg-apple @@ -1 +1 @@ -Subproject commit 811af0a83b3faeade89a9093a588595666d32066 +Subproject commit 137de1c6d4adaae1c7098664546f5f587d54461b diff --git a/client/android/xray/src/main/kotlin/Xray.kt b/client/android/xray/src/main/kotlin/Xray.kt index d0a1c4e38..462005951 100644 --- a/client/android/xray/src/main/kotlin/Xray.kt +++ b/client/android/xray/src/main/kotlin/Xray.kt @@ -166,7 +166,7 @@ class Xray : Protocol() { mtu = config.mtu.toLong() proxy = "socks5://127.0.0.1:${config.socksPort}" device = "fd://$fd" - logLevel = "warning" + logLevel = "warn" } LibXray.startTun2Socks(tun2SocksConfig, fd.toLong()).isNotNullOrBlank { err -> throw VpnStartException("Failed to start tun2socks: $err") diff --git a/client/cmake/3rdparty.cmake b/client/cmake/3rdparty.cmake index 6c3726148..4272290d7 100644 --- a/client/cmake/3rdparty.cmake +++ b/client/cmake/3rdparty.cmake @@ -38,7 +38,7 @@ elseif(APPLE AND NOT IOS) endif() set(OPENSSL_INCLUDE_DIR "${OPENSSL_ROOT_DIR}/macos/include") set(OPENSSL_LIB_SSL_PATH "${OPENSSL_ROOT_DIR}/macos/lib/libssl.a") - set(OPENSSL_LIB_CRYPTO_PATH "${OPENSSL_ROOT_DIR}/macos/lib/libcrypto.a") + set(OPENSSL_LIB_CRYPTO_PATH "${OPENSSL_ROOT_DIR}/macos/lib/libcrypto.a") elseif(IOS) set(LIBSSH_INCLUDE_DIR "${LIBSSH_ROOT_DIR}/ios/arm64") set(LIBSSH_LIB_PATH "${LIBSSH_ROOT_DIR}/ios/arm64/libssh.a") @@ -62,7 +62,7 @@ elseif(LINUX) set(OPENSSL_LIB_SSL_PATH "${OPENSSL_ROOT_DIR}/linux/x86_64/libssl.a") set(OPENSSL_LIB_CRYPTO_PATH "${OPENSSL_ROOT_DIR}/linux/x86_64/libcrypto.a") endif() - + file(COPY ${OPENSSL_LIB_SSL_PATH} ${OPENSSL_LIB_CRYPTO_PATH} DESTINATION ${OPENSSL_LIBRARIES_DIR}) diff --git a/client/core/networkUtilities.cpp b/client/core/networkUtilities.cpp index a73fd16f3..1a52a974a 100644 --- a/client/core/networkUtilities.cpp +++ b/client/core/networkUtilities.cpp @@ -1,11 +1,12 @@ #include "networkUtilities.h" +#include +#include #ifdef Q_OS_WIN #include #include #include #include - #include #include #include #include @@ -30,6 +31,15 @@ #include #include #include + #include + #include + #include + #include + #include + #include + #include + #include + #include #endif #include @@ -239,12 +249,14 @@ DWORD GetAdaptersAddressesWrapper(const ULONG Family, } #endif -QString NetworkUtilities::getGatewayAndIface() +QPair NetworkUtilities::getGatewayAndIface() { #ifdef Q_OS_WIN constexpr int BUFF_LEN = 100; char buff[BUFF_LEN] = {'\0'}; - QString result; + + QString resGateway; + int resIndex = -1; PIP_ADAPTER_ADDRESSES pAdapterAddresses = nullptr; DWORD dwRetVal = @@ -252,7 +264,7 @@ QString NetworkUtilities::getGatewayAndIface() if (dwRetVal != NO_ERROR) { qDebug() << "ipv4 stack detect GetAdaptersAddresses failed."; - return ""; + return {}; } PIP_ADAPTER_ADDRESSES pCurAddress = pAdapterAddresses; @@ -267,7 +279,9 @@ QString NetworkUtilities::getGatewayAndIface() struct sockaddr_in addr; if (inet_pton(AF_INET, buff, &addr.sin_addr) == 1) { qDebug() << "this is true v4 !"; - result = gw; + + resGateway = gw; + resIndex = pCurAddress->IfIndex; } } } @@ -275,7 +289,7 @@ QString NetworkUtilities::getGatewayAndIface() } free(pAdapterAddresses); - return result; + return { resGateway, QNetworkInterface::interfaceFromIndex(resIndex) }; #endif #ifdef Q_OS_LINUX constexpr int BUFFER_SIZE = 100; @@ -292,7 +306,7 @@ QString NetworkUtilities::getGatewayAndIface() if ((sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE)) < 0) { perror("socket failed"); - return ""; + return {}; } memset(msgbuf, 0, sizeof(msgbuf)); @@ -316,7 +330,7 @@ QString NetworkUtilities::getGatewayAndIface() /* send msg */ if (send(sock, nlmsg, nlmsg->nlmsg_len, 0) < 0) { perror("send failed"); - return ""; + return {}; } /* receive response */ @@ -325,7 +339,7 @@ QString NetworkUtilities::getGatewayAndIface() received_bytes = recv(sock, ptr, sizeof(buffer) - msg_len, 0); if (received_bytes < 0) { perror("Error in recv"); - return ""; + return {}; } nlh = (struct nlmsghdr *) ptr; @@ -335,7 +349,7 @@ QString NetworkUtilities::getGatewayAndIface() (nlmsg->nlmsg_type == NLMSG_ERROR)) { perror("Error in received packet"); - return ""; + return {}; } /* If we received all data break */ @@ -388,10 +402,12 @@ QString NetworkUtilities::getGatewayAndIface() } } close(sock); - return gateway_address; + return { gateway_address, QNetworkInterface::interfaceFromName(interface) }; #endif #if defined(Q_OS_MAC) && !defined(Q_OS_IOS) && !defined(MACOS_NE) QString gateway; + int index = -1; + int mib[] = {CTL_NET, PF_ROUTE, 0, 0, NET_RT_FLAGS, RTF_GATEWAY}; int afinet_type[] = {AF_INET, AF_INET6}; @@ -401,17 +417,17 @@ QString NetworkUtilities::getGatewayAndIface() size_t needed = 0; if (sysctl(mib, sizeof(mib) / sizeof(int), nullptr, &needed, nullptr, 0) < 0) - return ""; + return {}; char* buf; if ((buf = new char[needed]) == 0) - return ""; + return {}; if (sysctl(mib, sizeof(mib) / sizeof(int), buf, &needed, nullptr, 0) < 0) { qDebug() << "sysctl: net.route.0.0.dump"; delete[] buf; - return gateway; + return {}; } struct rt_msghdr* rt; @@ -449,7 +465,10 @@ QString NetworkUtilities::getGatewayAndIface() &(reinterpret_cast(sa_tab[RTAX_GATEWAY]))->sin_addr, sizeof(struct in_addr)); if (inet_ntop(AF_INET, srcStr4, dstStr4, INET_ADDRSTRLEN) != nullptr) + { gateway = dstStr4; + index = rt->rtm_index; + } break; } } @@ -463,7 +482,10 @@ QString NetworkUtilities::getGatewayAndIface() &(reinterpret_cast(sa_tab[RTAX_GATEWAY]))->sin6_addr, sizeof(struct in6_addr)); if (inet_ntop(AF_INET6, srcStr6, dstStr6, INET6_ADDRSTRLEN) != nullptr) + { gateway = dstStr6; + index = rt->rtm_index; + } break; } } @@ -472,6 +494,6 @@ QString NetworkUtilities::getGatewayAndIface() free(buf); } - return gateway; + return { gateway, QNetworkInterface::interfaceFromIndex(index) }; #endif } diff --git a/client/core/networkUtilities.h b/client/core/networkUtilities.h index 1bd1114ca..102097568 100644 --- a/client/core/networkUtilities.h +++ b/client/core/networkUtilities.h @@ -6,7 +6,7 @@ #include #include #include - +#include class NetworkUtilities : public QObject { @@ -17,7 +17,7 @@ public: static bool checkIPv4Format(const QString &ip); static bool checkIpSubnetFormat(const QString &ip); static bool checkIpv6Enabled(); - static QString getGatewayAndIface(); + static QPair getGatewayAndIface(); // Returns the Interface Index that could Route to dst static int AdapterIndexTo(const QHostAddress& dst); diff --git a/client/platforms/ios/PacketTunnelProvider+OpenVPN.swift b/client/platforms/ios/PacketTunnelProvider+OpenVPN.swift index 831651c08..118545c2c 100644 --- a/client/platforms/ios/PacketTunnelProvider+OpenVPN.swift +++ b/client/platforms/ios/PacketTunnelProvider+OpenVPN.swift @@ -131,7 +131,7 @@ extension PacketTunnelProvider { } startHandler = completionHandler - ovpnAdapter?.connect(using: packetFlow) + ovpnAdapter?.connect(using: openVPNPacketFlow()) } func handleOpenVPNStatusMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) { @@ -153,7 +153,7 @@ extension PacketTunnelProvider { } func stopOpenVPN(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { - ovpnLog(.info, message: "Stopping tunnel: reason: \(reason.description)") + ovpnLog(.info, message: "Stopping tunnel: reason: \(reason.amneziaDescription)") stopHandler = completionHandler if vpnReachability.isTracking { @@ -293,5 +293,3 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate { ovpnLog(.info, message: logMessage) } } - -extension NEPacketTunnelFlow: OpenVPNAdapterPacketFlow {} diff --git a/client/platforms/ios/PacketTunnelProvider+WireGuard.swift b/client/platforms/ios/PacketTunnelProvider+WireGuard.swift index 5d6e66dea..58c2cfb13 100644 --- a/client/platforms/ios/PacketTunnelProvider+WireGuard.swift +++ b/client/platforms/ios/PacketTunnelProvider+WireGuard.swift @@ -176,7 +176,7 @@ extension PacketTunnelProvider { } func stopWireguard(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { - wg_log(.info, message: "Stopping tunnel: reason: \(reason.description)") + wg_log(.info, message: "Stopping tunnel: reason: \(reason.amneziaDescription)") wgAdapter?.stop { error in ErrorNotifier.removeLastErrorFile() diff --git a/client/platforms/ios/PacketTunnelProvider+Xray.swift b/client/platforms/ios/PacketTunnelProvider+Xray.swift index dc53474b1..6a08bb6a9 100644 --- a/client/platforms/ios/PacketTunnelProvider+Xray.swift +++ b/client/platforms/ios/PacketTunnelProvider+Xray.swift @@ -107,6 +107,8 @@ extension PacketTunnelProvider { return } + self?.updateActiveInterfaceIndexForCurrentPath() + // Launch xray self?.setupAndStartXray(configData: updatedData) { xrayError in if let xrayError { @@ -133,6 +135,15 @@ extension PacketTunnelProvider { completionHandler() } + func sockCallback(fd: uintptr_t) { + if activeIfaceIdx != 0 { + withUnsafePointer(to: activeIfaceIdx) { ptr in + setsockopt(Int32(fd), IPPROTO_IP, IP_BOUND_IF, ptr, socklen_t(MemoryLayout.size)) + setsockopt(Int32(fd), IPPROTO_IPV6, IPV6_BOUND_IF, ptr, socklen_t(MemoryLayout.size)) + } + } + } + private func setupAndStartXray(configData: Data, completionHandler: @escaping (Error?) -> Void) { let path = Constants.cachesDirectory.appendingPathComponent("config.json", isDirectory: false).path @@ -142,6 +153,17 @@ extension PacketTunnelProvider { return } + updateActiveInterfaceIndexForCurrentPath() + + let ctx = Unmanaged.passUnretained(self).toOpaque() + let cb: libxray_sockcallback = { (fd, ctx) in + guard let ctx = ctx else { return } + let instance = Unmanaged.fromOpaque(ctx).takeUnretainedValue() + + instance.sockCallback(fd: fd) + } + LibXraySetSockCallback(cb, ctx) + LibXrayRunXray(nil, path, Int64.max) diff --git a/client/platforms/ios/PacketTunnelProvider.swift b/client/platforms/ios/PacketTunnelProvider.swift index 9a5a58466..eb9867b39 100644 --- a/client/platforms/ios/PacketTunnelProvider.swift +++ b/client/platforms/ios/PacketTunnelProvider.swift @@ -1,5 +1,6 @@ import Foundation import NetworkExtension +import Network import os import Darwin import OpenVPNAdapter @@ -38,6 +39,12 @@ struct Constants { class PacketTunnelProvider: NEPacketTunnelProvider { var wgAdapter: WireGuardAdapter? var ovpnAdapter: OpenVPNAdapter? + private lazy var openVPNPacketFlowAdapter = PacketTunnelFlowAdapter(flow: packetFlow) + private let pathMonitorQueue = DispatchQueue(label: Constants.processQueueName + ".path-monitor") + private let pathMonitor = NWPathMonitor() + private var didReceiveInitialPathUpdate = false + private var currentPath: Network.NWPath? + private var currentPathSignature: String? var splitTunnelType: Int? var splitTunnelSites: [String]? @@ -47,6 +54,73 @@ class PacketTunnelProvider: NEPacketTunnelProvider { var startHandler: ((Error?) -> Void)? var stopHandler: (() -> Void)? var protoType: TunnelProtoType? + + var activeIfaceIdx: UInt32 = 0 + + func openVPNPacketFlow() -> OpenVPNAdapterPacketFlow { + openVPNPacketFlowAdapter + } + + override init() { + super.init() + pathMonitor.pathUpdateHandler = { [weak self] path in + guard let self else { return } + self.currentPath = path + let signature = self.pathSignature(for: path) + let hasMeaningfulChange = self.currentPathSignature != signature + self.currentPathSignature = signature + self.updateActiveInterfaceIndex(for: path) + + guard self.didReceiveInitialPathUpdate else { + self.didReceiveInitialPathUpdate = true + return + } + + guard hasMeaningfulChange, self.protoType != nil else { return } + + DispatchQueue.main.async { + self.handle(networkChange: path) { _ in } + } + } + pathMonitor.start(queue: pathMonitorQueue) + + currentPath = pathMonitor.currentPath + currentPathSignature = pathSignature(for: pathMonitor.currentPath) + } + + func updateActiveInterfaceIndex(for path: Network.NWPath?) { + guard let path else { + activeIfaceIdx = 0 + return + } + + let preferredTypes: [NWInterface.InterfaceType] = [.wiredEthernet, .wifi, .cellular, .other] + + let nonLoopbackInterfaces = path.availableInterfaces.filter { $0.type != .loopback } + let activeInterfaces = nonLoopbackInterfaces.filter { path.usesInterfaceType($0.type) } + + let candidate = preferredTypes.compactMap { type in + activeInterfaces.first { $0.type == type } + }.first ?? activeInterfaces.first ?? nonLoopbackInterfaces.first + + if let candidate { + activeIfaceIdx = UInt32(candidate.index) + } else { + activeIfaceIdx = 0 + } + } + + func updateActiveInterfaceIndexForCurrentPath() { + if let currentPath { + currentPathSignature = pathSignature(for: currentPath) + updateActiveInterfaceIndex(for: currentPath) + return + } + + currentPath = pathMonitor.currentPath + currentPathSignature = pathSignature(for: pathMonitor.currentPath) + updateActiveInterfaceIndex(for: pathMonitor.currentPath) + } override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) { guard let message = String(data: messageData, encoding: .utf8) else { @@ -104,6 +178,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider { return } + didReceiveInitialPathUpdate = false + updateActiveInterfaceIndexForCurrentPath() + switch protoType { case .wireguard: startWireguard(activationAttemptId: activationAttemptId, @@ -157,28 +234,63 @@ class PacketTunnelProvider: NEPacketTunnelProvider { of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { - guard Constants.kDefaultPathKey != keyPath else { return } - // Since iOS 11, we have observed that this KVO event fires repeatedly when connecting over Wifi, - // even though the underlying network has not changed (i.e. `isEqualToPath` returns false), - // leading to "wakeup crashes" due to excessive network activity. Guard against false positives by - // comparing the paths' string description, which includes properties not exposed by the class - guard let lastPath: NWPath = change?[.oldKey] as? NWPath, - let defPath = defaultPath, - lastPath != defPath || lastPath.description != defPath.description else { + guard Constants.kDefaultPathKey == keyPath else { return } - DispatchQueue.main.async { [weak self] in - guard let self, self.defaultPath != nil else { return } - self.handle(networkChange: self.defaultPath!) { _ in } - } } - private func handle(networkChange changePath: NWPath, completion: @escaping (Error?) -> Void) { + private func handle(networkChange changePath: Network.NWPath, completion: @escaping (Error?) -> Void) { + updateActiveInterfaceIndex(for: changePath) wg_log(.info, message: "Tunnel restarted.") startTunnel(options: nil, completionHandler: completion) } } +private extension PacketTunnelProvider { + func pathSignature(for path: Network.NWPath) -> String { + var signatureComponents = [String(describing: path.status)] + signatureComponents.append(path.isExpensive ? "exp" : "noexp") + signatureComponents.append(path.isConstrained ? "con" : "nocon") + + let preferredTypes: [NWInterface.InterfaceType] = [.wiredEthernet, .wifi, .cellular, .loopback, .other] + let sortedInterfaces = path.availableInterfaces.sorted { lhs, rhs in + if lhs.type == rhs.type { + return lhs.index < rhs.index + } + + let lhsOrder = preferredTypes.firstIndex(of: lhs.type) ?? preferredTypes.count + let rhsOrder = preferredTypes.firstIndex(of: rhs.type) ?? preferredTypes.count + + if lhsOrder == rhsOrder { + return lhs.index < rhs.index + } + + return lhsOrder < rhsOrder + } + + for interface in sortedInterfaces { + let typeName: String + switch interface.type { + case .wiredEthernet: typeName = "ethernet" + case .wifi: typeName = "wifi" + case .cellular: typeName = "cellular" + case .loopback: typeName = "loopback" + case .other: typeName = "other" + @unknown default: typeName = "unknown" + } + signatureComponents.append("\(typeName):\(interface.index)") + } + + // Include currently used interface preference ordering + for type in preferredTypes { + let usesType = path.usesInterfaceType(type) + signatureComponents.append("uses-\(type):\(usesType)") + } + + return signatureComponents.joined(separator: "|") + } +} + extension WireGuardLogLevel { var osLogLevel: OSLogType { switch self { @@ -190,8 +302,27 @@ extension WireGuardLogLevel { } } -extension NEProviderStopReason: CustomStringConvertible { - public var description: String { +final class PacketTunnelFlowAdapter: NSObject, OpenVPNAdapterPacketFlow { + private let flow: NEPacketTunnelFlow + + init(flow: NEPacketTunnelFlow) { + self.flow = flow + super.init() + } + + @objc(readPacketsWithCompletionHandler:) + func readPackets(completionHandler: @escaping ([Data], [NSNumber]) -> Void) { + flow.readPackets(completionHandler: completionHandler) + } + + @objc(writePackets:withProtocols:) + func writePackets(_ packets: [Data], withProtocols protocols: [NSNumber]) -> Bool { + flow.writePackets(packets, withProtocols: protocols) + } +} + +extension NEProviderStopReason { + var amneziaDescription: String { switch self { case .none: return "No specific reason" @@ -223,6 +354,8 @@ extension NEProviderStopReason: CustomStringConvertible { return "The current console user changed" case .connectionFailed: return "The connection failed" + case .internalError: + return "The network extension reported an internal error" case .sleep: return "A stop reason indicating the VPNC enabled disconnect on sleep and the device went to sleep" case .appUpdate: diff --git a/client/platforms/linux/daemon/linuxroutemonitor.cpp b/client/platforms/linux/daemon/linuxroutemonitor.cpp index 2010ba9b9..39926f232 100644 --- a/client/platforms/linux/daemon/linuxroutemonitor.cpp +++ b/client/platforms/linux/daemon/linuxroutemonitor.cpp @@ -165,7 +165,7 @@ bool LinuxRouteMonitor::rtmSendRoute(int action, int flags, int type, if (rtm->rtm_type == RTN_THROW) { struct in_addr ip4; - inet_pton(AF_INET, NetworkUtilities::getGatewayAndIface().toUtf8(), &ip4); + inet_pton(AF_INET, NetworkUtilities::getGatewayAndIface().first.toUtf8(), &ip4); nlmsg_append_attr(nlmsg, sizeof(buf), RTA_GATEWAY, &ip4, sizeof(ip4)); nlmsg_append_attr32(nlmsg, sizeof(buf), RTA_PRIORITY, 0); rtm->rtm_type = RTN_UNICAST; diff --git a/client/protocols/xrayprotocol.cpp b/client/protocols/xrayprotocol.cpp index 3807d9ba3..d61d688ba 100755 --- a/client/protocols/xrayprotocol.cpp +++ b/client/protocols/xrayprotocol.cpp @@ -1,17 +1,19 @@ #include "xrayprotocol.h" +#include "core/ipcclient.h" +#include "utilities.h" +#include "core/networkUtilities.h" + #include #include #include #include - -#include "core/networkUtilities.h" -#include "utilities.h" +#include XrayProtocol::XrayProtocol(const QJsonObject &configuration, QObject *parent) : VpnProtocol(configuration, parent) { readXrayConfiguration(configuration); - m_routeGateway = NetworkUtilities::getGatewayAndIface(); + m_routeGateway = NetworkUtilities::getGatewayAndIface().first; m_vpnGateway = amnezia::protocols::xray::defaultLocalAddr; m_vpnLocalAddress = amnezia::protocols::xray::defaultLocalAddr; m_t2sProcess = IpcClient::InterfaceTun2Socks(); @@ -25,71 +27,18 @@ XrayProtocol::~XrayProtocol() ErrorCode XrayProtocol::start() { - qDebug().noquote() << "XrayProtocol xrayExecPath():" << xrayExecPath(); + qDebug() << "XrayProtocol::start()"; - if (!QFileInfo::exists(xrayExecPath())) { - setLastError(ErrorCode::XrayExecutableMissing); - return lastError(); - } + IpcClient::Interface()->xrayStart(QJsonDocument(m_xrayConfig).toJson()); -#ifdef QT_DEBUG - m_xrayCfgFile.setAutoRemove(false); -#endif - m_xrayCfgFile.open(); - QString config = QJsonDocument(m_xrayConfig).toJson(); - config.replace(m_remoteHost, m_remoteAddress); - m_xrayCfgFile.write(config.toUtf8()); - m_xrayCfgFile.close(); - - QStringList args = QStringList() << "-c" << m_xrayCfgFile.fileName() << "-format=json"; - - qDebug().noquote() << "XrayProtocol::start()" << xrayExecPath() << args.join(" "); - - m_xrayProcess.setProcessChannelMode(QProcess::MergedChannels); - m_xrayProcess.setProgram(xrayExecPath()); - - if (Utils::processIsRunning(Utils::executable("xray", false))) { - qDebug().noquote() << "kill previos xray"; - Utils::killProcessByName(Utils::executable("xray", false)); - } - - m_xrayProcess.setArguments(args); - - connect(&m_xrayProcess, &QProcess::readyReadStandardOutput, this, [this]() { -#ifdef QT_DEBUG - qDebug().noquote() << "xray:" << m_xrayProcess.readAllStandardOutput(); -#endif - }); - - connect(&m_xrayProcess, QOverload::of(&QProcess::finished), this, - [this](int exitCode, QProcess::ExitStatus exitStatus) { - qDebug().noquote() << "XrayProtocol finished, exitCode, exitStatus" << exitCode << exitStatus; - setConnectionState(Vpn::ConnectionState::Disconnected); - if ((exitStatus != QProcess::NormalExit) || (exitCode != 0)) { - emit protocolError(amnezia::ErrorCode::XrayExecutableCrashed); - emit setConnectionState(Vpn::ConnectionState::Error); - } - }); - - m_xrayProcess.start(); - m_xrayProcess.waitForStarted(); - - if (m_xrayProcess.state() == QProcess::ProcessState::Running) { - setConnectionState(Vpn::ConnectionState::Connecting); - QThread::msleep(1000); - return startTun2Sock(); - } else - return ErrorCode::XrayExecutableMissing; + setConnectionState(Vpn::ConnectionState::Connecting); + return startTun2Sock(); } ErrorCode XrayProtocol::startTun2Sock() { m_t2sProcess->start(); -#ifdef Q_OS_WIN - m_configData.insert("inetAdapterIndex", NetworkUtilities::AdapterIndexTo(QHostAddress(m_remoteAddress))); -#endif - connect(m_t2sProcess.data(), &IpcProcessTun2SocksReplica::stateChanged, this, [&](QProcess::ProcessState newState) { qDebug() << "PrivilegedProcess stateChanged" << newState; }); @@ -99,11 +48,10 @@ ErrorCode XrayProtocol::startTun2Sock() setConnectionState(Vpn::ConnectionState::Connecting); QList dnsAddr; - dnsAddr.push_back(QHostAddress(m_configData.value(config_key::dns1).toString())); + dnsAddr.push_back(QHostAddress(m_primaryDNS)); // We don't use secondary DNS if primary DNS is AmneziaDNS - if (!m_configData.value(amnezia::config_key::dns1).toString(). - contains(amnezia::protocols::dns::amneziaDnsIp)) { - dnsAddr.push_back(QHostAddress(m_configData.value(config_key::dns2).toString())); + if (!m_primaryDNS.contains(amnezia::protocols::dns::amneziaDnsIp)) { + dnsAddr.push_back(QHostAddress(m_secondaryDNS)); } #ifdef Q_OS_WIN QThread::msleep(8000); @@ -117,37 +65,13 @@ ErrorCode XrayProtocol::startTun2Sock() QThread::msleep(1000); IpcClient::Interface()->createTun("tun2", amnezia::protocols::xray::defaultLocalAddr); IpcClient::Interface()->updateResolvers("tun2", dnsAddr); -#endif -#if defined(Q_OS_LINUX) || defined(Q_OS_MACOS) - // killSwitch toggle - if (QVariant(m_configData.value(config_key::killSwitchOption).toString()).toBool()) { - m_configData.insert("vpnServer", m_remoteAddress); - IpcClient::Interface()->enableKillSwitch(m_configData, 0); - } #endif if (m_routeMode == Settings::RouteMode::VpnAllSites) { - IpcClient::Interface()->routeAddList(m_vpnGateway, QStringList() << "0.0.0.0/1"); - IpcClient::Interface()->routeAddList(m_vpnGateway, QStringList() << "128.0.0.0/1"); - IpcClient::Interface()->routeAddList(m_routeGateway, QStringList() << m_remoteAddress); + IpcClient::Interface()->routeAddList(m_vpnGateway, QStringList() << "1.0.0.0/8" << "2.0.0.0/7" << "4.0.0.0/6" << "8.0.0.0/5" << "16.0.0.0/4" << "32.0.0.0/3" << "64.0.0.0/2" << "128.0.0.0/1"); } IpcClient::Interface()->StopRoutingIpv6(); #ifdef Q_OS_WIN IpcClient::Interface()->updateResolvers("tun2", dnsAddr); - QList netInterfaces = QNetworkInterface::allInterfaces(); - for (int i = 0; i < netInterfaces.size(); i++) { - for (int j = 0; j < netInterfaces.at(i).addressEntries().size(); j++) { - // killSwitch toggle - if (m_vpnLocalAddress == netInterfaces.at(i).addressEntries().at(j).ip().toString()) { - if (QVariant(m_configData.value(config_key::killSwitchOption).toString()).toBool()) { - IpcClient::Interface()->enableKillSwitch(m_configData, netInterfaces.at(i).index()); - } - m_configData.insert("vpnAdapterIndex", netInterfaces.at(i).index()); - m_configData.insert("vpnGateway", m_vpnGateway); - m_configData.insert("vpnServer", m_remoteAddress); - IpcClient::Interface()->enablePeerTraffic(m_configData); - } - } - } #endif setConnectionState(Vpn::ConnectionState::Connected); } @@ -167,8 +91,6 @@ ErrorCode XrayProtocol::startTun2Sock() void XrayProtocol::stop() { #ifdef AMNEZIA_DESKTOP - QRemoteObjectPendingReply disableKillSwitchResp = IpcClient::Interface()->disableKillSwitch(); - disableKillSwitchResp.waitForFinished(1000); QRemoteObjectPendingReply StartRoutingIpv6Resp = IpcClient::Interface()->StartRoutingIpv6(); StartRoutingIpv6Resp.waitForFinished(1000); QRemoteObjectPendingReply restoreResolvers = IpcClient::Interface()->restoreResolvers(); @@ -179,9 +101,9 @@ void XrayProtocol::stop() #endif #endif qDebug() << "XrayProtocol::stop()"; - m_xrayProcess.disconnect(); - m_xrayProcess.kill(); - m_xrayProcess.waitForFinished(3000); + + IpcClient::Interface()->xrayStop(); + if (m_t2sProcess) { m_t2sProcess->stop(); QThread::msleep(200); @@ -190,26 +112,13 @@ void XrayProtocol::stop() setConnectionState(Vpn::ConnectionState::Disconnected); } -QString XrayProtocol::xrayExecPath() -{ -#ifdef Q_OS_WIN - return Utils::executable(QString("xray/xray"), true); -#else - return Utils::executable(QString("xray"), true); -#endif -} - void XrayProtocol::readXrayConfiguration(const QJsonObject &configuration) { - m_configData = configuration; QJsonObject xrayConfiguration = configuration.value(ProtocolProps::key_proto_config_data(Proto::Xray)).toObject(); if (xrayConfiguration.isEmpty()) { xrayConfiguration = configuration.value(ProtocolProps::key_proto_config_data(Proto::SSXray)).toObject(); } m_xrayConfig = xrayConfiguration; - m_localPort = QString(amnezia::protocols::xray::defaultLocalProxyPort).toInt(); - m_remoteHost = configuration.value(amnezia::config_key::hostName).toString(); - m_remoteAddress = NetworkUtilities::getIPAddress(m_remoteHost); m_routeMode = static_cast(configuration.value(amnezia::config_key::splitTunnelType).toInt()); m_primaryDNS = configuration.value(amnezia::config_key::dns1).toString(); m_secondaryDNS = configuration.value(amnezia::config_key::dns2).toString(); diff --git a/client/protocols/xrayprotocol.h b/client/protocols/xrayprotocol.h index c79ef608f..10f81fbc8 100644 --- a/client/protocols/xrayprotocol.h +++ b/client/protocols/xrayprotocol.h @@ -3,8 +3,8 @@ #include "QProcess" -#include "containers/containers_defs.h" -#include "openvpnprotocol.h" +#include "core/ipcclient.h" +#include "vpnprotocol.h" #include "settings.h" class XrayProtocol : public VpnProtocol @@ -17,29 +17,16 @@ public: ErrorCode startTun2Sock(); void stop() override; -protected: +private: void readXrayConfiguration(const QJsonObject &configuration); - -protected: + QJsonObject m_xrayConfig; - -private: - static QString xrayExecPath(); - static QString tun2SocksExecPath(); - -private: - int m_localPort; - QString m_remoteHost; - QString m_remoteAddress; Settings::RouteMode m_routeMode; - QJsonObject m_configData; QString m_primaryDNS; QString m_secondaryDNS; #ifndef Q_OS_IOS - QProcess m_xrayProcess; QSharedPointer m_t2sProcess; #endif - QTemporaryFile m_xrayCfgFile; }; #endif // XRAYPROTOCOL_H diff --git a/ipc/ipc_interface.rep b/ipc/ipc_interface.rep index 1d5beb4fa..2320c6a4a 100644 --- a/ipc/ipc_interface.rep +++ b/ipc/ipc_interface.rep @@ -38,6 +38,9 @@ class IpcInterface SLOT( bool updateResolvers(const QString& ifname, const QList& resolvers) ); SLOT( bool restoreResolvers() ); + SLOT(void xrayStart(const QString &config)); + SLOT(void xrayStop()); + SLOT( bool startNetworkCheck(const QString& serverIpv4Gateway, const QString& deviceIpv4Address) ); SLOT( bool stopNetworkCheck() ); diff --git a/ipc/ipcserver.cpp b/ipc/ipcserver.cpp index e44337bf0..94d4be22e 100644 --- a/ipc/ipcserver.cpp +++ b/ipc/ipcserver.cpp @@ -15,8 +15,8 @@ #include "logger.h" #include "router.h" - #include "killswitch.h" +#include "xray.h" #ifdef Q_OS_WIN #include "tapcontroller_win.h" @@ -240,3 +240,13 @@ bool IpcServer::refreshKillSwitch(bool enabled) { return KillSwitch::instance()->refresh(enabled); } + +void IpcServer::xrayStart(const QString& cfg) +{ + return Xray::getInstance().startXray(cfg); +} + +void IpcServer::xrayStop() +{ + return Xray::getInstance().stopXray(); +} \ No newline at end of file diff --git a/ipc/ipcserver.h b/ipc/ipcserver.h index 483635394..5a63302cb 100644 --- a/ipc/ipcserver.h +++ b/ipc/ipcserver.h @@ -44,6 +44,8 @@ public: virtual bool refreshKillSwitch( bool enabled ) override; virtual bool updateResolvers(const QString& ifname, const QList& resolvers) override; virtual bool restoreResolvers() override; + virtual void xrayStart(const QString& cfg) override; + virtual void xrayStop() override; virtual bool startNetworkCheck(const QString& serverIpv4Gateway, const QString& deviceIpv4Address) override; virtual bool stopNetworkCheck() override; diff --git a/service/server/CMakeLists.txt b/service/server/CMakeLists.txt index 75365bd89..3330e9c56 100644 --- a/service/server/CMakeLists.txt +++ b/service/server/CMakeLists.txt @@ -12,6 +12,23 @@ qt_standard_project_setup() configure_file(${CMAKE_SOURCE_DIR}/version.h.in ${CMAKE_CURRENT_BINARY_DIR}/version.h) +set(AMNEZIA_XRAY_ROOT_DIR "${CMAKE_CURRENT_LIST_DIR}/../../client/3rd-prebuilt/3rd-prebuilt/amnezia_xray") +if(WIN32) + if("${CMAKE_SIZEOF_VOID_P}" STREQUAL "8") + set(AMNEZIA_XRAY_LIB_PATH "${AMNEZIA_XRAY_ROOT_DIR}/windows/x86_64/amnezia_xray.lib") + set(AMNEZIA_XRAY_INCLUDE_DIR "${AMNEZIA_XRAY_ROOT_DIR}/windows/x86_64") + else() + set(AMNEZIA_XRAY_LIB_PATH "${AMNEZIA_XRAY_ROOT_DIR}/windows/x86/amnezia_xray.lib") + set(AMNEZIA_XRAY_INCLUDE_DIR "${AMNEZIA_XRAY_ROOT_DIR}/windows/x86") + endif() +elseif(APPLE AND NOT IOS) + set(AMNEZIA_XRAY_LIB_PATH "${AMNEZIA_XRAY_ROOT_DIR}/macos/x86_64/amnezia_xray.a") + set(AMNEZIA_XRAY_INCLUDE_DIR "${AMNEZIA_XRAY_ROOT_DIR}/macos/x86_64") +elseif(LINUX) + set(AMNEZIA_XRAY_LIB_PATH "${AMNEZIA_XRAY_ROOT_DIR}/linux/x86_64/amnezia_xray.a") + set(AMNEZIA_XRAY_INCLUDE_DIR "${AMNEZIA_XRAY_ROOT_DIR}/linux/x86_64") +endif() + set(QSIMPLECRYPTO_DIR ${CMAKE_CURRENT_LIST_DIR}/../../client/3rd/QSimpleCrypto/src) @@ -46,6 +63,7 @@ endif() set(OPENSSL_USE_STATIC_LIBS TRUE) include_directories( + ${AMNEZIA_XRAY_INCLUDE_DIR} ${OPENSSL_INCLUDE_DIR} ${QSIMPLECRYPTO_DIR} ) @@ -63,6 +81,7 @@ set(HEADERS ${CMAKE_CURRENT_LIST_DIR}/router.h ${CMAKE_CURRENT_LIST_DIR}/killswitch.h ${CMAKE_CURRENT_LIST_DIR}/systemservice.h + ${CMAKE_CURRENT_LIST_DIR}/xray.h ${CMAKE_CURRENT_BINARY_DIR}/version.h ${QSIMPLECRYPTO_DIR}/include/QAead.h ${QSIMPLECRYPTO_DIR}/include/QBlockCipher.h @@ -85,6 +104,7 @@ set(SOURCES ${CMAKE_CURRENT_LIST_DIR}/router.cpp ${CMAKE_CURRENT_LIST_DIR}/killswitch.cpp ${CMAKE_CURRENT_LIST_DIR}/systemservice.cpp + ${CMAKE_CURRENT_LIST_DIR}/xray.cpp ${QSIMPLECRYPTO_DIR}/sources/QAead.cpp ${QSIMPLECRYPTO_DIR}/sources/QBlockCipher.cpp ${QSIMPLECRYPTO_DIR}/sources/QRsa.cpp @@ -222,6 +242,7 @@ if(WIN32) gdi32 Advapi32 Kernel32 + ${AMNEZIA_XRAY_LIB_PATH} ${OPENSSL_LIB_CRYPTO_PATH} qt6keychain ) @@ -269,7 +290,12 @@ if(APPLE) ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/macos/daemon/macosfirewall.cpp ) - set(LIBS ${OPENSSL_LIB_CRYPTO_PATH} qt6keychain) + set(LIBS + resolv + ${AMNEZIA_XRAY_LIB_PATH} + ${OPENSSL_LIB_CRYPTO_PATH} + qt6keychain + ) endif() @@ -304,7 +330,14 @@ if(LINUX) ${CMAKE_CURRENT_SOURCE_DIR}/../../client/platforms/linux/daemon/linuxfirewall.cpp ) - set(LIBS ${OPENSSL_LIB_CRYPTO_PATH} qt6keychain -static-libstdc++ -static-libgcc -ldl) + set(LIBS + ${AMNEZIA_XRAY_LIB_PATH} + ${OPENSSL_LIB_CRYPTO_PATH} + qt6keychain + -static-libstdc++ + -static-libgcc + -ldl + ) endif() diff --git a/service/server/router_win.cpp b/service/server/router_win.cpp index d2975a88c..7ad6505df 100644 --- a/service/server/router_win.cpp +++ b/service/server/router_win.cpp @@ -13,6 +13,8 @@ LONG (NTAPI * NtResumeProcess)(HANDLE ProcessHandle) = NULL; #define STATUS_SUCCESS ((NTSTATUS)0x00000000L) +QList RouterWin::kIpv6Subnets = { "fc00::/7", "2000::/4", "3000::/4" }; + RouterWin &RouterWin::Instance() { static RouterWin s; @@ -448,49 +450,36 @@ bool RouterWin::restoreResolvers() { return m_dnsUtil->restoreResolvers(); } +QNetworkInterface RouterWin::findLoopbackIface() +{ + for (auto iface : QNetworkInterface::allInterfaces()) { + if (iface.flags() & QNetworkInterface::IsLoopBack) { + return iface; + } + } + return {}; +} + bool RouterWin::StopRoutingIpv6() { - { - QProcess p; - QString command = QString("interface ipv6 add route fc00::/7 interface={NetworkInterface.IPv6LoopbackInterfaceIndex} metric=0 store=active"); - p.start(command); - p.waitForFinished(); - } - { - QProcess p; - QString command = QString("interface ipv6 add route 2000::/4 interface={NetworkInterface.IPv6LoopbackInterfaceIndex} metric=0 store=active"); - p.start(command); - p.waitForFinished(); - } - { - QProcess p; - QString command = QString("interface ipv6 add route 3000::/4 interface={NetworkInterface.IPv6LoopbackInterfaceIndex} metric=0 store=active"); - p.start(command); - p.waitForFinished(); + qDebug() << "RouterWin::StopRoutingIpv6"; + + if (auto loopback = findLoopbackIface(); loopback.isValid()) { + for (auto subnet : kIpv6Subnets) { + QProcess{}.execute("netsh", { "interface", "ipv6", "add", "route", subnet, QString("interface=%1").arg(loopback.index()), "metric=0", "store=active" }); + } } return true; } bool RouterWin::StartRoutingIpv6() { - { - QProcess p; - QString command = QString("interface ipv6 delete route fc00::/7 interface={NetworkInterface.IPv6LoopbackInterfaceIndex}"); - p.start(command); - p.waitForFinished(); - } - { - QProcess p; - QString command = QString("interface ipv6 delete route 2000::/4 interface={NetworkInterface.IPv6LoopbackInterfaceIndex}"); - p.start(command); - p.waitForFinished(); - } - { - QProcess p; - QString command = QString("interface ipv6 delete route 3000::/4 interface={NetworkInterface.IPv6LoopbackInterfaceIndex}"); - p.start(command); - p.waitForFinished(); + qDebug() << "RouterWin::StartRoutingIpv6"; + + if (auto loopback = findLoopbackIface(); loopback.isValid()) { + for (auto subnet : kIpv6Subnets) { + QProcess{}.execute("netsh", { "interface", "ipv6", "delete", "route", subnet, QString("interface=%1").arg(loopback.index()) }); + } } return true; } - diff --git a/service/server/router_win.h b/service/server/router_win.h index f46d2d17f..c2412cc55 100644 --- a/service/server/router_win.h +++ b/service/server/router_win.h @@ -7,13 +7,13 @@ #include #include #include +#include #include "../client/platforms/windows/daemon/dnsutilswindows.h" #include //includes Windows.h #include - #include #include #include @@ -50,6 +50,8 @@ public: bool restoreResolvers(); private: + static QList kIpv6Subnets; + RouterWin(RouterWin const &) = delete; RouterWin& operator= (RouterWin const&) = delete; @@ -59,6 +61,8 @@ private: BOOL InitNtFunctions(); BOOL SuspendProcess(BOOL fSuspend, DWORD dwProcessId); + QNetworkInterface findLoopbackIface(); + private: RouterWin() {m_dnsUtil = new DnsUtilsWindows(this);} QMultiMap m_ipForwardRows; diff --git a/service/server/xray.cpp b/service/server/xray.cpp new file mode 100644 index 000000000..2d3f9a797 --- /dev/null +++ b/service/server/xray.cpp @@ -0,0 +1,100 @@ +#include "xray.h" +#include "core/networkUtilities.h" + +#include +#include +#include +#include +#include + +#ifdef Q_OS_DARWIN + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include +#endif +#ifdef Q_OS_WIN + #include + #include +#endif +#ifdef Q_OS_LINUX + #include +#endif + +void Xray::startXray(const QString &cfg) +{ + qDebug() << "Xray::startXray()"; + + auto defaultIface = NetworkUtilities::getGatewayAndIface().second; +#ifdef Q_OS_LINUX + m_defaultIfaceName = defaultIface.name().toUtf8(); +#else + m_defaultIfaceIdx = defaultIface.index(); +#endif + + if (auto err = amnezia_xray_setsockcallback(ctxSockCallback, this); err != nullptr) { + qDebug() << "[xray] sockopt failed: " << err; + free(err); + return; + } + + QByteArray bytes = cfg.toUtf8(); + if (auto err = amnezia_xray_configure(bytes.data()); err != nullptr) { + qDebug() << "[xray] configuration failed: " << err; + free(err); + return; + } + + amnezia_xray_setloghandler(ctxLogHandler, this); + + if (auto err = amnezia_xray_start(); err != nullptr) { + qDebug() << "[xray] failed to start: " << err; + free(err); + return; + } +} + +void Xray::stopXray() +{ + qDebug() << "Xray::stopXray()"; + if (auto err = amnezia_xray_stop(); err != nullptr) { + qDebug() << "[xray] failed to stop: " << err; + free(err); + return; + } +} + +void Xray::logHandler(char* str) +{ + QMetaObject::invokeMethod(qApp, [str = QString::fromUtf8(str)] { + qDebug() << "[xray]" << str; + }, Qt::QueuedConnection); +} + +void Xray::sockCallback(uintptr_t fd) +{ +#ifdef Q_OS_MAC + if (m_defaultIfaceIdx > 0) { + setsockopt(fd, IPPROTO_IP, IP_BOUND_IF, &m_defaultIfaceIdx, sizeof(m_defaultIfaceIdx)); + setsockopt(fd, IPPROTO_IPV6, IPV6_BOUND_IF, &m_defaultIfaceIdx, sizeof(m_defaultIfaceIdx)); + } +#endif +#ifdef Q_OS_WIN + if (DWORD idx = m_defaultIfaceIdx; idx > 0) { + setsockopt(fd, IPPROTO_IPV6, IPV6_UNICAST_IF, reinterpret_cast(&idx), sizeof(idx)); + idx = htonl(idx); // IP_UNICAST_IF expects index in network byte order + setsockopt(fd, IPPROTO_IP, IP_UNICAST_IF, reinterpret_cast(&idx), sizeof(idx)); + } +#endif +#ifdef Q_OS_LINUX + if (!m_defaultIfaceName.isEmpty()) { + setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, m_defaultIfaceName.data(), m_defaultIfaceName.size()); + } +#endif +} diff --git a/service/server/xray.h b/service/server/xray.h new file mode 100644 index 000000000..c199734aa --- /dev/null +++ b/service/server/xray.h @@ -0,0 +1,36 @@ +#ifndef XRAY_H +#define XRAY_H + +#include + +class Xray +{ +public: + static Xray& getInstance() + { + static Xray instance; + return instance; + } + + void startXray(const QString& cfg); + void stopXray(); + +private: + static void ctxSockCallback(uintptr_t fd, void* ctx) { + reinterpret_cast(ctx)->sockCallback(fd); + } + static void ctxLogHandler(char* str, void* ctx) { + reinterpret_cast(ctx)->logHandler(str); + } + + void sockCallback(uintptr_t fd); + void logHandler(char* str); + +#ifdef Q_OS_LINUX + QByteArray m_defaultIfaceName; +#else + int m_defaultIfaceIdx; +#endif +}; + +#endif // XRAY_H