From bc21d68e5a0e93f293f3aa6fdf29700e0fc9812e Mon Sep 17 00:00:00 2001 From: AnhTVc Date: Fri, 6 Jun 2025 16:23:28 +0700 Subject: [PATCH] update fix update fix missing file --- .../ios/PacketTunnelProvider+OpenVPN.swift | 248 +++++ client/platforms/ios/ios_controller.mm | 923 ++++++++++++++++++ 2 files changed, 1171 insertions(+) create mode 100644 client/platforms/ios/PacketTunnelProvider+OpenVPN.swift create mode 100644 client/platforms/ios/ios_controller.mm diff --git a/client/platforms/ios/PacketTunnelProvider+OpenVPN.swift b/client/platforms/ios/PacketTunnelProvider+OpenVPN.swift new file mode 100644 index 00000000..12bbaa54 --- /dev/null +++ b/client/platforms/ios/PacketTunnelProvider+OpenVPN.swift @@ -0,0 +1,248 @@ +import Foundation +import NetworkExtension +import OpenVPNAdapter + +struct OpenVPNConfig: Decodable { + let config: String + let splitTunnelType: Int + let splitTunnelSites: [String] + + var str: String { + "splitTunnelType: \(splitTunnelType) splitTunnelSites: \(splitTunnelSites) config: \(config)" + } +} + +extension PacketTunnelProvider { + func startOpenVPN(completionHandler: @escaping (Error?) -> Void) { + guard let protocolConfiguration = self.protocolConfiguration as? NETunnelProviderProtocol, + let providerConfiguration = protocolConfiguration.providerConfiguration, + let openVPNConfigData = providerConfiguration[Constants.ovpnConfigKey] as? Data else { + ovpnLog(.error, message: "Can't start") + return + } + + do { + let openVPNConfig = try JSONDecoder().decode(OpenVPNConfig.self, from: openVPNConfigData) + ovpnLog(.info, title: "config: ", message: openVPNConfig.str) + let ovpnConfiguration = Data(openVPNConfig.config.utf8) + setupAndlaunchOpenVPN(withConfig: ovpnConfiguration, completionHandler: completionHandler) + } catch { + ovpnLog(.error, message: "Can't parse config: \(error.localizedDescription)") + + if let underlyingError = (error as NSError).userInfo[NSUnderlyingErrorKey] as? NSError { + ovpnLog(.error, message: "Can't parse config: \(underlyingError.localizedDescription)") + } + + return + } + } + + private func setupAndlaunchOpenVPN(withConfig ovpnConfiguration: Data, + withShadowSocks viaSS: Bool = false, + completionHandler: @escaping (Error?) -> Void) { + ovpnLog(.info, message: "Setup and launch") + + let str = String(decoding: ovpnConfiguration, as: UTF8.self) + + let configuration = OpenVPNConfiguration() + configuration.fileContent = ovpnConfiguration + if str.contains("cloak") { + configuration.setPTCloak() + } + + let evaluation: OpenVPNConfigurationEvaluation? + do { + ovpnAdapter = OpenVPNAdapter() + ovpnAdapter?.delegate = self + evaluation = try ovpnAdapter?.apply(configuration: configuration) + + } catch { + completionHandler(error) + return + } + + if evaluation?.autologin == false { + ovpnLog(.info, message: "Implement login with user credentials") + } + + vpnReachability.startTracking { [weak self] status in + guard status == .reachableViaWiFi else { return } + self?.ovpnAdapter?.reconnect(afterTimeInterval: 5) + } + + startHandler = completionHandler + ovpnAdapter?.connect(using: packetFlow) + } + + func handleOpenVPNStatusMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) { + guard let completionHandler = completionHandler else { return } + let bytesin = ovpnAdapter?.transportStatistics.bytesIn + let bytesout = ovpnAdapter?.transportStatistics.bytesOut + + guard let bytesin, let bytesout else { + completionHandler(nil) + return + } + + let response: [String: Any] = [ + "rx_bytes": bytesin, + "tx_bytes": bytesout + ] + + completionHandler(try? JSONSerialization.data(withJSONObject: response, options: [])) + } + + func stopOpenVPN(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { + ovpnLog(.info, message: "Stopping tunnel: reason: \(reason.description)") + + stopHandler = completionHandler + if vpnReachability.isTracking { + vpnReachability.stopTracking() + } + ovpnAdapter?.disconnect() + } +} + +extension PacketTunnelProvider: OpenVPNAdapterDelegate { + // OpenVPNAdapter calls this delegate method to configure a VPN tunnel. + // `completionHandler` callback requires an object conforming to `OpenVPNAdapterPacketFlow` + // protocol if the tunnel is configured without errors. Otherwise send nil. + // `OpenVPNAdapterPacketFlow` method signatures are similar to `NEPacketTunnelFlow` so + // you can just extend that class to adopt `OpenVPNAdapterPacketFlow` protocol and + // send `self.packetFlow` to `completionHandler` callback. + func openVPNAdapter( + _ openVPNAdapter: OpenVPNAdapter, + configureTunnelWithNetworkSettings networkSettings: NEPacketTunnelNetworkSettings?, + completionHandler: @escaping (Error?) -> Void + ) { + // In order to direct all DNS queries first to the VPN DNS servers before the primary DNS servers + // send empty string to NEDNSSettings.matchDomains + networkSettings?.dnsSettings?.matchDomains = [""] + + if splitTunnelType == 1 { + var ipv4IncludedRoutes = [NEIPv4Route]() + + guard let splitTunnelSites else { + completionHandler(NSError(domain: "Split tunnel sited not setted up", code: 0)) + return + } + + for allowedIPString in splitTunnelSites { + if let allowedIP = IPAddressRange(from: allowedIPString) { + ipv4IncludedRoutes.append(NEIPv4Route( + destinationAddress: "\(allowedIP.address)", + subnetMask: "\(allowedIP.subnetMask())")) + } + } + + networkSettings?.ipv4Settings?.includedRoutes = ipv4IncludedRoutes + } else { + if splitTunnelType == 2 { + var ipv4ExcludedRoutes = [NEIPv4Route]() + var ipv4IncludedRoutes = [NEIPv4Route]() + var ipv6IncludedRoutes = [NEIPv6Route]() + + guard let splitTunnelSites else { + completionHandler(NSError(domain: "Split tunnel sited not setted up", code: 0)) + return + } + + for excludeIPString in splitTunnelSites { + if let excludeIP = IPAddressRange(from: excludeIPString) { + ipv4ExcludedRoutes.append(NEIPv4Route( + destinationAddress: "\(excludeIP.address)", + subnetMask: "\(excludeIP.subnetMask())")) + } + } + + if let allIPv4 = IPAddressRange(from: "0.0.0.0/0") { + ipv4IncludedRoutes.append(NEIPv4Route( + destinationAddress: "\(allIPv4.address)", + subnetMask: "\(allIPv4.subnetMask())")) + } + if let allIPv6 = IPAddressRange(from: "::/0") { + ipv6IncludedRoutes.append(NEIPv6Route( + destinationAddress: "\(allIPv6.address)", + networkPrefixLength: NSNumber(value: allIPv6.networkPrefixLength))) + } + networkSettings?.ipv4Settings?.includedRoutes = ipv4IncludedRoutes + networkSettings?.ipv6Settings?.includedRoutes = ipv6IncludedRoutes + networkSettings?.ipv4Settings?.excludedRoutes = ipv4ExcludedRoutes + } + if splitTunnelType == 0 || splitTunnelType == nil { + // Full tunnel: send all traffic via VPN + if let ipv4Settings = networkSettings?.ipv4Settings { + ipv4Settings.includedRoutes = [NEIPv4Route.default()] + NSLog("[Route] Added default IPv4 route (0.0.0.0/0)") + } + + if let ipv6Settings = networkSettings?.ipv6Settings { + let ipv6DefaultRoute = NEIPv6Route(destinationAddress: "::", networkPrefixLength: 0) + ipv6Settings.includedRoutes = [ipv6DefaultRoute] + NSLog("[Route] Added default IPv6 route (::/0)") + } + + } + } + + // Set the network settings for the current tunneling session. + setTunnelNetworkSettings(networkSettings, completionHandler: completionHandler) + } + + // Process events returned by the OpenVPN library + func openVPNAdapter( + _ openVPNAdapter: OpenVPNAdapter, + handleEvent event: OpenVPNAdapterEvent, + message: String?) { + switch event { + case .connected: + if reasserting { + reasserting = false + } + + guard let startHandler = startHandler else { return } + + startHandler(nil) + self.startHandler = nil + case .disconnected: + guard let stopHandler = stopHandler else { return } + + if vpnReachability.isTracking { + vpnReachability.stopTracking() + } + + stopHandler() + self.stopHandler = nil + case .reconnecting: + reasserting = true + default: + break + } + } + + // Handle errors thrown by the OpenVPN library + func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleError error: Error) { + // Handle only fatal errors + guard let fatal = (error as NSError).userInfo[OpenVPNAdapterErrorFatalKey] as? Bool, + fatal == true else { return } + + if vpnReachability.isTracking { + vpnReachability.stopTracking() + } + + if let startHandler { + startHandler(error) + self.startHandler = nil + } else { + cancelTunnelWithError(error) + } + } + + // Use this method to process any log message returned by OpenVPN library. + func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleLogMessage logMessage: String) { + // Handle log messages + ovpnLog(.info, message: logMessage) + } +} + +extension NEPacketTunnelFlow: OpenVPNAdapterPacketFlow {} diff --git a/client/platforms/ios/ios_controller.mm b/client/platforms/ios/ios_controller.mm new file mode 100644 index 00000000..d96bde40 --- /dev/null +++ b/client/platforms/ios/ios_controller.mm @@ -0,0 +1,923 @@ +#include "ios_controller.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "../protocols/vpnprotocol.h" +#import "ios_controller_wrapper.h" + +const char* Action::start = "start"; +const char* Action::restart = "restart"; +const char* Action::stop = "stop"; +const char* Action::getTunnelId = "getTunnelId"; +const char* Action::getStatus = "status"; + +const char* MessageKey::action = "action"; +const char* MessageKey::tunnelId = "tunnelId"; +const char* MessageKey::config = "config"; +const char* MessageKey::errorCode = "errorCode"; +const char* MessageKey::host = "host"; +const char* MessageKey::port = "port"; +const char* MessageKey::isOnDemand = "is-on-demand"; +const char* MessageKey::SplitTunnelType = "SplitTunnelType"; +const char* MessageKey::SplitTunnelSites = "SplitTunnelSites"; + +static UIViewController* getViewController() { + NSArray *windows = [[UIApplication sharedApplication]windows]; + for (UIWindow *window in windows) { + if (window.isKeyWindow) { + return window.rootViewController; + } + } + return nil; +} + +Vpn::ConnectionState iosStatusToState(NEVPNStatus status) { + switch (status) { + case NEVPNStatusInvalid: + return Vpn::ConnectionState::Unknown; + case NEVPNStatusDisconnected: + return Vpn::ConnectionState::Disconnected; + case NEVPNStatusConnecting: + return Vpn::ConnectionState::Connecting; + case NEVPNStatusConnected: + return Vpn::ConnectionState::Connected; + case NEVPNStatusReasserting: + return Vpn::ConnectionState::Connecting; + case NEVPNStatusDisconnecting: + return Vpn::ConnectionState::Disconnecting; + default: + return Vpn::ConnectionState::Unknown; +} +} + +namespace { +IosController* s_instance = nullptr; +} + +IosController::IosController() : QObject() +{ + s_instance = this; + m_iosControllerWrapper = [[IosControllerWrapper alloc] initWithCppController:this]; + + [[NSNotificationCenter defaultCenter] + removeObserver: (__bridge NSObject *)m_iosControllerWrapper]; + [[NSNotificationCenter defaultCenter] + addObserver: (__bridge NSObject *)m_iosControllerWrapper selector:@selector(vpnStatusDidChange:) name:NEVPNStatusDidChangeNotification object:nil]; + [[NSNotificationCenter defaultCenter] + addObserver: (__bridge NSObject *)m_iosControllerWrapper selector:@selector(vpnConfigurationDidChange:) name:NEVPNConfigurationChangeNotification object:nil]; + +} + +IosController* IosController::Instance() { + if (!s_instance) { + s_instance = new IosController(); + } + + return s_instance; +} + +bool IosController::initialize() +{ + __block bool ok = true; + [NETunnelProviderManager loadAllFromPreferencesWithCompletionHandler:^(NSArray * _Nullable managers, NSError * _Nullable error) { + @try { + if (error) { + qDebug() << "IosController::initialize : Error:" << [error.localizedDescription UTF8String]; + emit connectionStateChanged(Vpn::ConnectionState::Error); + ok = false; + return; + } + + NSInteger managerCount = managers.count; + qDebug() << "IosController::initialize : We have received managers:" << (long)managerCount; + + + for (NETunnelProviderManager *manager in managers) { + qDebug() << "IosController::initialize : VPNC: " << manager.localizedDescription; + + if (manager.connection.status == NEVPNStatusConnected) { + m_currentTunnel = manager; + qDebug() << "IosController::initialize : VPN already connected with" << manager.localizedDescription; + emit connectionStateChanged(Vpn::ConnectionState::Connected); + break; + + // TODO: show connected state + } + } + } + @catch (NSException *exception) { + qDebug() << "IosController::setTunnel : exception" << QString::fromNSString(exception.reason); + ok = false; + } + }]; + + return ok; +} + +bool IosController::connectVpn(amnezia::Proto proto, const QJsonObject& configuration) +{ + + m_proto = proto; + m_rawConfig = configuration; + m_serverAddress = configuration.value(config_key::hostName).toString().toNSString(); + + if (proto == amnezia::Proto::OpenVpn) { + QJsonObject ovpn = configuration["openvpn_config_data"].toObject(); + QString ovpnConfig = ovpn["config"].toString(); + + // Danh sách directive không hỗ trợ từng dòng + QStringList unsupportedDirectives = { + "resolv-retry", + "persist-key", + "persist-tun", + "block-ipv6", + "redirect-gateway" + }; + + QStringList lines = ovpnConfig.split('\n'); + QStringList filteredLines; + + bool insideTlsAuthBlock = false; + + for (const QString &line : lines) { + QString trimmedLine = line.trimmed(); + + bool shouldIgnore = false; + for (const QString &bad : unsupportedDirectives) { + if (trimmedLine.startsWith(bad)) { + shouldIgnore = true; + break; + } + } + + if (!shouldIgnore) { + filteredLines.append(line); + } + } + + // Nối lại cấu hình đã lọc + ovpnConfig = filteredLines.join("\n"); + + // Gán lại vào config JSON + ovpn["config"] = ovpnConfig; + m_rawConfig["openvpn_config_data"] = ovpn; + } + + + QString tunnelName; + if (configuration.value(config_key::description).toString().isEmpty()) { + tunnelName = QString("%1 %2") + .arg(configuration.value(config_key::hostName).toString()) + .arg(ProtocolProps::protoToString(proto)); + } + else { + tunnelName = QString("%1 (%2) %3") + .arg(configuration.value(config_key::description).toString()) + .arg(configuration.value(config_key::hostName).toString()) + .arg(ProtocolProps::protoToString(proto)); + } + + qDebug() << "IosController::connectVpn" << tunnelName; + + m_currentTunnel = nullptr; + + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + __block bool ok = true; + __block bool isNewTunnelCreated = false; + + [NETunnelProviderManager loadAllFromPreferencesWithCompletionHandler:^(NSArray * _Nullable managers, NSError * _Nullable error) { + @try { + if (error) { + qDebug() << "IosController::connectVpn : VPNC: loadAllFromPreferences error:" << [error.localizedDescription UTF8String]; + emit connectionStateChanged(Vpn::ConnectionState::Error); + ok = false; + return; + } + + NSInteger managerCount = managers.count; + qDebug() << "IosController::connectVpn : We have received managers:" << (long)managerCount; + + + for (NETunnelProviderManager *manager in managers) { + if ([manager.localizedDescription isEqualToString:tunnelName.toNSString()]) { + m_currentTunnel = manager; + qDebug() << "IosController::connectVpn : Using existing tunnel:" << manager.localizedDescription; + if (manager.connection.status == NEVPNStatusConnected) { + emit connectionStateChanged(Vpn::ConnectionState::Connected); + return; + } + + break; + } + } + + if (!m_currentTunnel) { + isNewTunnelCreated = true; + m_currentTunnel = [[NETunnelProviderManager alloc] init]; + m_currentTunnel.localizedDescription = [NSString stringWithUTF8String:tunnelName.toStdString().c_str()]; + qDebug() << "IosController::connectVpn : Creating new tunnel" << m_currentTunnel.localizedDescription; + } + + } + @catch (NSException *exception) { + qDebug() << "IosController::connectVpn : exception" << QString::fromNSString(exception.reason); + ok = false; + m_currentTunnel = nullptr; + } + @finally { + dispatch_semaphore_signal(semaphore); + } + }]; + + dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); + if (!ok) return false; + + [[NSNotificationCenter defaultCenter] + removeObserver:(__bridge NSObject *)m_iosControllerWrapper]; + + [[NSNotificationCenter defaultCenter] + addObserver:(__bridge NSObject *)m_iosControllerWrapper + selector:@selector(vpnStatusDidChange:) + name:NEVPNStatusDidChangeNotification + object:m_currentTunnel.connection]; + + + if (proto == amnezia::Proto::OpenVpn) { + return setupOpenVPN(); + } + if (proto == amnezia::Proto::Cloak) { + return setupCloak(); + } + if (proto == amnezia::Proto::WireGuard) { + return setupWireGuard(); + } + if (proto == amnezia::Proto::Awg) { + return setupAwg(); + } + if (proto == amnezia::Proto::Xray) { + return setupXray(); + } + if (proto == amnezia::Proto::SSXray) { + return setupSSXray(); + } + + return false; +} + +void IosController::disconnectVpn() +{ + if (!m_currentTunnel) { + return; + } + + if ([m_currentTunnel.connection isKindOfClass:[NETunnelProviderSession class]]) { + [(NETunnelProviderSession *)m_currentTunnel.connection stopTunnel]; + } +} + + +void IosController::checkStatus() +{ + NSString *actionKey = [NSString stringWithUTF8String:MessageKey::action]; + NSString *actionValue = [NSString stringWithUTF8String:Action::getStatus]; + NSString *tunnelIdKey = [NSString stringWithUTF8String:MessageKey::tunnelId]; + NSString *tunnelIdValue = !m_tunnelId.isEmpty() ? m_tunnelId.toNSString() : @""; + + NSDictionary* message = @{actionKey: actionValue, tunnelIdKey: tunnelIdValue}; + sendVpnExtensionMessage(message, [&](NSDictionary* response){ + uint64_t txBytes = [response[@"tx_bytes"] intValue]; + uint64_t rxBytes = [response[@"rx_bytes"] intValue]; + emit bytesChanged(rxBytes - m_rxBytes, txBytes - m_txBytes); + m_rxBytes = rxBytes; + m_txBytes = txBytes; + }); +} + +void IosController::vpnStatusDidChange(void *pNotification) +{ + NETunnelProviderSession *session = (NETunnelProviderSession *)pNotification; + + qDebug() << "IosController::vpnStatusDidChange - Session pointer:" << session; + + if (session /* && session == TunnelManager.session*/ ) { + qDebug() << "IosController::vpnStatusDidChange" << iosStatusToState(session.status) << session; + + if (session.status == NEVPNStatusDisconnected) { + if (@available(iOS 16.0, *)) { + [session fetchLastDisconnectErrorWithCompletionHandler:^(NSError * _Nullable error) { + if (error != nil) { + qDebug() << "Disconnect error" << error.domain << error.code << error.localizedDescription; + + //MARK: Debug error + if (error.userInfo) { + qDebug() << " UserInfo:"; + for (NSString *key in error.userInfo.allKeys) { + id value = error.userInfo[key]; + qDebug() << " " << key << ":" << value; + } + } + ///// + if ([error.domain isEqualToString:NEVPNConnectionErrorDomain]) { + switch (error.code) { + case NEVPNConnectionErrorOverslept: + qDebug() << "Disconnect error info" << "The VPN connection was terminated because the system slept for an extended period of time."; + break; + case NEVPNConnectionErrorNoNetworkAvailable: + qDebug() << "Disconnect error info" << "The VPN connection could not be established because the system is not connected to a network."; + break; + case NEVPNConnectionErrorUnrecoverableNetworkChange: + qDebug() << "Disconnect error info" << "The VPN connection was terminated because the network conditions changed in such a way that the VPN connection could not be maintained."; + break; + case NEVPNConnectionErrorConfigurationFailed: + qDebug() << "Disconnect error info" << "The VPN connection could not be established because the configuration is invalid. "; + break; + case NEVPNConnectionErrorServerAddressResolutionFailed: + qDebug() << "Disconnect error info" << "The address of the VPN server could not be determined."; + break; + case NEVPNConnectionErrorServerNotResponding: + qDebug() << "Disconnect error info" << "Network communication with the VPN server has failed."; + break; + case NEVPNConnectionErrorServerDead: + qDebug() << "Disconnect error info" << "The VPN server is no longer functioning."; + break; + case NEVPNConnectionErrorAuthenticationFailed: + qDebug() << "Disconnect error info" << "The user credentials were rejected by the VPN server."; + break; + case NEVPNConnectionErrorClientCertificateInvalid: + qDebug() << "Disconnect error info" << "The client certificate is invalid."; + break; + case NEVPNConnectionErrorClientCertificateNotYetValid: + qDebug() << "Disconnect error info" << "The client certificate will not be valid until some future point in time."; + break; + case NEVPNConnectionErrorClientCertificateExpired: + qDebug() << "Disconnect error info" << "The validity period of the client certificate has passed."; + break; + case NEVPNConnectionErrorPluginFailed: + qDebug() << "Disconnect error info" << "The VPN plugin died unexpectedly."; + break; + case NEVPNConnectionErrorConfigurationNotFound: + qDebug() << "Disconnect error info" << "The VPN configuration could not be found."; + break; + case NEVPNConnectionErrorPluginDisabled: + qDebug() << "Disconnect error info" << "The VPN plugin could not be found or needed to be updated."; + break; + case NEVPNConnectionErrorNegotiationFailed: + qDebug() << "Disconnect error info" << "The VPN protocol negotiation failed."; + break; + case NEVPNConnectionErrorServerDisconnected: + qDebug() << "Disconnect error info" << "The VPN server terminated the connection."; + break; + case NEVPNConnectionErrorServerCertificateInvalid: + qDebug() << "Disconnect error info" << "The server certificate is invalid."; + break; + case NEVPNConnectionErrorServerCertificateNotYetValid: + qDebug() << "Disconnect error info" << "The server certificate will not be valid until some future point in time."; + break; + case NEVPNConnectionErrorServerCertificateExpired: + qDebug() << "Disconnect error info" << "The validity period of the server certificate has passed."; + break; + default: + qDebug() << "Disconnect error info" << "Unknown code."; + break; + } + } + + NSError *underlyingError = error.userInfo[@"NSUnderlyingError"]; + if (underlyingError != nil) { + qDebug() << "Disconnect underlying error" << underlyingError.domain << underlyingError.code << underlyingError.localizedDescription; + + if ([underlyingError.domain isEqualToString:@"NEAgentErrorDomain"]) { + switch (underlyingError.code) { + case 1: + qDebug() << "Disconnect underlying error" << "General. Use sysdiagnose."; + break; + case 2: + qDebug() << "Disconnect underlying error" << "Plug-in unavailable. Use sysdiagnose."; + break; + default: + qDebug() << "Disconnect underlying error" << "Unknown code. Use sysdiagnose."; + break; + } + } + } + } + }]; + } else { + qDebug() << "Disconnect error is unavailable on iOS < 16.0"; + } + } + + emit connectionStateChanged(iosStatusToState(session.status)); + } +} + +void IosController::vpnConfigurationDidChange(void *pNotification) +{ + qDebug() << "IosController::vpnConfigurationDidChange" << pNotification; +} + +bool IosController::setupOpenVPN() +{ + QJsonObject ovpn = m_rawConfig[ProtocolProps::key_proto_config_data(amnezia::Proto::OpenVpn)].toObject(); + QString ovpnConfig = ovpn[config_key::config].toString(); + + QJsonObject openVPNConfig {}; + openVPNConfig.insert(config_key::config, ovpnConfig); + + if (ovpn.contains(config_key::mtu)) { + openVPNConfig.insert(config_key::mtu, ovpn[config_key::mtu]); + } else { + openVPNConfig.insert(config_key::mtu, protocols::openvpn::defaultMtu); + } + + + + + openVPNConfig.insert(config_key::splitTunnelType, m_rawConfig[config_key::splitTunnelType]); + + QJsonArray splitTunnelSites = m_rawConfig[config_key::splitTunnelSites].toArray(); + + for(int index = 0; index < splitTunnelSites.count(); index++) { + splitTunnelSites[index] = splitTunnelSites[index].toString().remove(" "); + } + + openVPNConfig.insert(config_key::splitTunnelSites, splitTunnelSites); + + QJsonDocument openVPNConfigDoc(openVPNConfig); + QString openVPNConfigStr(openVPNConfigDoc.toJson(QJsonDocument::Compact)); + + return startOpenVPN(openVPNConfigStr); +} + +bool IosController::setupCloak() +{ + m_serverAddress = @"127.0.0.1"; + QJsonObject ovpn = m_rawConfig[ProtocolProps::key_proto_config_data(amnezia::Proto::OpenVpn)].toObject(); + QString ovpnConfig = ovpn[config_key::config].toString(); + + QJsonObject cloak = m_rawConfig[ProtocolProps::key_proto_config_data(amnezia::Proto::Cloak)].toObject(); + + cloak["NumConn"] = 1; + if (cloak.contains("remote")) { + cloak["RemoteHost"] = cloak["remote"].toString(); + } + if (cloak.contains("port")) { + cloak["RemotePort"] = cloak["port"].toString(); + } + cloak.remove("remote"); + cloak.remove("port"); + cloak.remove("transport_proto"); + + QJsonObject jsonObject {}; + foreach(const QString& key, cloak.keys()) { + if(key == "NumConn" or key == "StreamTimeout"){ + jsonObject.insert(key, cloak.value(key).toInt()); + }else{ + jsonObject.insert(key, cloak.value(key).toString()); + } + } + QJsonDocument doc(jsonObject); + QString strJson(doc.toJson(QJsonDocument::Compact)); + QString cloakBase64 = strJson.toUtf8().toBase64(); + ovpnConfig.append("\n\n"); + ovpnConfig.append(cloakBase64); + ovpnConfig.append("\n\n"); + + QJsonObject openVPNConfig {}; + openVPNConfig.insert(config_key::config, ovpnConfig); + + if (ovpn.contains(config_key::mtu)) { + openVPNConfig.insert(config_key::mtu, ovpn[config_key::mtu]); + } else { + openVPNConfig.insert(config_key::mtu, protocols::openvpn::defaultMtu); + } + + openVPNConfig.insert(config_key::splitTunnelType, m_rawConfig[config_key::splitTunnelType]); + + QJsonArray splitTunnelSites = m_rawConfig[config_key::splitTunnelSites].toArray(); + + for(int index = 0; index < splitTunnelSites.count(); index++) { + splitTunnelSites[index] = splitTunnelSites[index].toString().remove(" "); + } + + openVPNConfig.insert(config_key::splitTunnelSites, splitTunnelSites); + + QJsonDocument openVPNConfigDoc(openVPNConfig); + QString openVPNConfigStr(openVPNConfigDoc.toJson(QJsonDocument::Compact)); + + return startOpenVPN(openVPNConfigStr); +} + +bool IosController::setupWireGuard() +{ + QJsonObject config = m_rawConfig[ProtocolProps::key_proto_config_data(amnezia::Proto::WireGuard)].toObject(); + + QJsonObject wgConfig {}; + wgConfig.insert(config_key::dns1, m_rawConfig[config_key::dns1]); + wgConfig.insert(config_key::dns2, m_rawConfig[config_key::dns2]); + + if (config.contains(config_key::mtu)) { + wgConfig.insert(config_key::mtu, config[config_key::mtu]); + } else { + wgConfig.insert(config_key::mtu, protocols::wireguard::defaultMtu); + } + + wgConfig.insert(config_key::hostName, config[config_key::hostName]); + wgConfig.insert(config_key::port, config[config_key::port]); + wgConfig.insert(config_key::client_ip, config[config_key::client_ip]); + wgConfig.insert(config_key::client_priv_key, config[config_key::client_priv_key]); + wgConfig.insert(config_key::server_pub_key, config[config_key::server_pub_key]); + wgConfig.insert(config_key::psk_key, config[config_key::psk_key]); + wgConfig.insert(config_key::splitTunnelType, m_rawConfig[config_key::splitTunnelType]); + + QJsonArray splitTunnelSites = m_rawConfig[config_key::splitTunnelSites].toArray(); + + for(int index = 0; index < splitTunnelSites.count(); index++) { + splitTunnelSites[index] = splitTunnelSites[index].toString().remove(" "); + } + + wgConfig.insert(config_key::splitTunnelSites, splitTunnelSites); + + if (config.contains(config_key::allowed_ips) && config[config_key::allowed_ips].isArray()) { + wgConfig.insert(config_key::allowed_ips, config[config_key::allowed_ips]); + } else { + QJsonArray allowed_ips { "0.0.0.0/0", "::/0" }; + wgConfig.insert(config_key::allowed_ips, allowed_ips); + } + + if (config.contains(config_key::persistent_keep_alive)) { + wgConfig.insert(config_key::persistent_keep_alive, config[config_key::persistent_keep_alive]); + } else { + wgConfig.insert(config_key::persistent_keep_alive, "25"); + } + + if (config.contains(config_key::isObfuscationEnabled) && config.value(config_key::isObfuscationEnabled).toBool()) { + wgConfig.insert(config_key::initPacketMagicHeader, config[config_key::initPacketMagicHeader]); + wgConfig.insert(config_key::responsePacketMagicHeader, config[config_key::responsePacketMagicHeader]); + wgConfig.insert(config_key::underloadPacketMagicHeader, config[config_key::underloadPacketMagicHeader]); + wgConfig.insert(config_key::transportPacketMagicHeader, config[config_key::transportPacketMagicHeader]); + + wgConfig.insert(config_key::initPacketJunkSize, config[config_key::initPacketJunkSize]); + wgConfig.insert(config_key::responsePacketJunkSize, config[config_key::responsePacketJunkSize]); + + wgConfig.insert(config_key::junkPacketCount, config[config_key::junkPacketCount]); + wgConfig.insert(config_key::junkPacketMinSize, config[config_key::junkPacketMinSize]); + wgConfig.insert(config_key::junkPacketMaxSize, config[config_key::junkPacketMaxSize]); + } + + QJsonDocument wgConfigDoc(wgConfig); + QString wgConfigDocStr(wgConfigDoc.toJson(QJsonDocument::Compact)); + + return startWireGuard(wgConfigDocStr); +} + +bool IosController::setupXray() +{ + QJsonObject config = m_rawConfig[ProtocolProps::key_proto_config_data(amnezia::Proto::Xray)].toObject(); + QJsonDocument xrayConfigDoc(config); + + QString xrayConfigStr(xrayConfigDoc.toJson(QJsonDocument::Compact)); + + QJsonObject finalConfig; + finalConfig.insert(config_key::dns1, m_rawConfig[config_key::dns1].toString()); + finalConfig.insert(config_key::dns2, m_rawConfig[config_key::dns2].toString()); + finalConfig.insert(config_key::config, xrayConfigStr); + + QJsonDocument finalConfigDoc(finalConfig); + QString finalConfigStr(finalConfigDoc.toJson(QJsonDocument::Compact)); + + return startXray(finalConfigStr); +} + +bool IosController::setupSSXray() +{ + QJsonObject config = m_rawConfig[ProtocolProps::key_proto_config_data(amnezia::Proto::SSXray)].toObject(); + QJsonDocument ssXrayConfigDoc(config); + + QString ssXrayConfigStr(ssXrayConfigDoc.toJson(QJsonDocument::Compact)); + + QJsonObject finalConfig; + finalConfig.insert(config_key::dns1, m_rawConfig[config_key::dns1]); + finalConfig.insert(config_key::dns2, m_rawConfig[config_key::dns2]); + finalConfig.insert(config_key::config, ssXrayConfigStr); + + QJsonDocument finalConfigDoc(finalConfig); + QString finalConfigStr(finalConfigDoc.toJson(QJsonDocument::Compact)); + + return startXray(finalConfigStr); +} + +bool IosController::setupAwg() +{ + QJsonObject config = m_rawConfig[ProtocolProps::key_proto_config_data(amnezia::Proto::Awg)].toObject(); + + QJsonObject wgConfig {}; + wgConfig.insert(config_key::dns1, m_rawConfig[config_key::dns1]); + wgConfig.insert(config_key::dns2, m_rawConfig[config_key::dns2]); + + if (config.contains(config_key::mtu)) { + wgConfig.insert(config_key::mtu, config[config_key::mtu]); + } else { + wgConfig.insert(config_key::mtu, protocols::awg::defaultMtu); + } + + wgConfig.insert(config_key::hostName, config[config_key::hostName]); + wgConfig.insert(config_key::port, config[config_key::port]); + wgConfig.insert(config_key::client_ip, config[config_key::client_ip]); + wgConfig.insert(config_key::client_priv_key, config[config_key::client_priv_key]); + wgConfig.insert(config_key::server_pub_key, config[config_key::server_pub_key]); + wgConfig.insert(config_key::psk_key, config[config_key::psk_key]); + wgConfig.insert(config_key::splitTunnelType, m_rawConfig[config_key::splitTunnelType]); + + QJsonArray splitTunnelSites = m_rawConfig[config_key::splitTunnelSites].toArray(); + + for(int index = 0; index < splitTunnelSites.count(); index++) { + splitTunnelSites[index] = splitTunnelSites[index].toString().remove(" "); + } + + wgConfig.insert(config_key::splitTunnelSites, splitTunnelSites); + + if (config.contains(config_key::allowed_ips) && config[config_key::allowed_ips].isArray()) { + wgConfig.insert(config_key::allowed_ips, config[config_key::allowed_ips]); + } else { + QJsonArray allowed_ips { "0.0.0.0/0", "::/0" }; + wgConfig.insert(config_key::allowed_ips, allowed_ips); + } + + if (config.contains(config_key::persistent_keep_alive)) { + wgConfig.insert(config_key::persistent_keep_alive, config[config_key::persistent_keep_alive]); + } else { + wgConfig.insert(config_key::persistent_keep_alive, "25"); + } + + wgConfig.insert(config_key::initPacketMagicHeader, config[config_key::initPacketMagicHeader]); + wgConfig.insert(config_key::responsePacketMagicHeader, config[config_key::responsePacketMagicHeader]); + wgConfig.insert(config_key::underloadPacketMagicHeader, config[config_key::underloadPacketMagicHeader]); + wgConfig.insert(config_key::transportPacketMagicHeader, config[config_key::transportPacketMagicHeader]); + + wgConfig.insert(config_key::initPacketJunkSize, config[config_key::initPacketJunkSize]); + wgConfig.insert(config_key::responsePacketJunkSize, config[config_key::responsePacketJunkSize]); + + wgConfig.insert(config_key::junkPacketCount, config[config_key::junkPacketCount]); + wgConfig.insert(config_key::junkPacketMinSize, config[config_key::junkPacketMinSize]); + wgConfig.insert(config_key::junkPacketMaxSize, config[config_key::junkPacketMaxSize]); + + QJsonDocument wgConfigDoc(wgConfig); + QString wgConfigDocStr(wgConfigDoc.toJson(QJsonDocument::Compact)); + + return startWireGuard(wgConfigDocStr); +} + +bool IosController::startOpenVPN(const QString &config) +{ + qDebug() << "IosController::startOpenVPN"; + + NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init]; + tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID]; + tunnelProtocol.providerConfiguration = @{@"ovpn": [[NSString stringWithUTF8String:config.toStdString().c_str()] dataUsingEncoding:NSUTF8StringEncoding]}; + tunnelProtocol.serverAddress = m_serverAddress; + + m_currentTunnel.protocolConfiguration = tunnelProtocol; + + startTunnel(); +} + +bool IosController::startWireGuard(const QString &config) +{ + qDebug() << "IosController::startWireGuard"; + + NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init]; + tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID]; + tunnelProtocol.providerConfiguration = @{@"wireguard": [[NSString stringWithUTF8String:config.toStdString().c_str()] dataUsingEncoding:NSUTF8StringEncoding]}; + tunnelProtocol.serverAddress = m_serverAddress; + + m_currentTunnel.protocolConfiguration = tunnelProtocol; + + startTunnel(); +} + +bool IosController::startXray(const QString &config) +{ + qDebug() << "IosController::startXray"; + + NETunnelProviderProtocol *tunnelProtocol = [[NETunnelProviderProtocol alloc] init]; + tunnelProtocol.providerBundleIdentifier = [NSString stringWithUTF8String:VPN_NE_BUNDLEID]; + tunnelProtocol.providerConfiguration = @{@"xray": [[NSString stringWithUTF8String:config.toStdString().c_str()] dataUsingEncoding:NSUTF8StringEncoding]}; + tunnelProtocol.serverAddress = m_serverAddress; + + m_currentTunnel.protocolConfiguration = tunnelProtocol; + + startTunnel(); +} + +void IosController::startTunnel() +{ + NSString *protocolName = @"Unknown"; + + NETunnelProviderProtocol *tunnelProtocol = (NETunnelProviderProtocol *)m_currentTunnel.protocolConfiguration; + if (tunnelProtocol.providerConfiguration[@"wireguard"] != nil) { + protocolName = @"WireGuard"; + } else if (tunnelProtocol.providerConfiguration[@"ovpn"] != nil) { + protocolName = @"OpenVPN"; + } + + m_rxBytes = 0; + m_txBytes = 0; + + [m_currentTunnel setEnabled:YES]; + + [m_currentTunnel saveToPreferencesWithCompletionHandler:^(NSError *saveError) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + + if (saveError) { + qDebug().nospace() << "IosController::startTunnel" << protocolName << ": Connect " << protocolName << " Tunnel Save Error" << saveError.localizedDescription.UTF8String; + emit connectionStateChanged(Vpn::ConnectionState::Error); + return; + } + + [m_currentTunnel loadFromPreferencesWithCompletionHandler:^(NSError *loadError) { + if (loadError) { + qDebug().nospace() << "IosController::startTunnel :" << m_currentTunnel.localizedDescription << protocolName << ": Connect " << protocolName << " Tunnel Load Error" << loadError.localizedDescription.UTF8String; + emit connectionStateChanged(Vpn::ConnectionState::Error); + return; + } + + NSError *startError = nil; + qDebug() << iosStatusToState(m_currentTunnel.connection.status); + + BOOL started = [m_currentTunnel.connection startVPNTunnelWithOptions:nil andReturnError:&startError]; + + if (!started || startError) { + qDebug().nospace() << "IosController::startTunnel :" << m_currentTunnel.localizedDescription << protocolName << " : Connect " << protocolName << " Tunnel Start Error" + << (startError ? startError.localizedDescription.UTF8String : ""); + emit connectionStateChanged(Vpn::ConnectionState::Error); + } else { + qDebug().nospace() << "IosController::startTunnel :" << m_currentTunnel.localizedDescription << protocolName << " : Starting the tunnel succeeded"; + } + }]; + }); + }]; +} + +bool IosController::isOurManager(NETunnelProviderManager* manager) { + NETunnelProviderProtocol* tunnelProto = (NETunnelProviderProtocol*)manager.protocolConfiguration; + + if (!tunnelProto) { + qDebug() << "Ignoring manager because the proto is invalid"; + return false; + } + + if (!tunnelProto.providerBundleIdentifier) { + qDebug() << "Ignoring manager because the bundle identifier is null"; + return false; + } + + if (![tunnelProto.providerBundleIdentifier isEqualToString:[NSString stringWithUTF8String:VPN_NE_BUNDLEID]]) { + qDebug() << "Ignoring manager because the bundle identifier doesn't match"; + return false; + } + + qDebug() << "Found the manager with the correct bundle identifier:" << QString::fromNSString(tunnelProto.providerBundleIdentifier); + + return true; +} + +void IosController::sendVpnExtensionMessage(NSDictionary* message, std::function callback) +{ + if (!m_currentTunnel) { + qDebug() << "Cannot set an extension callback without a tunnel manager"; + return; + } + + NSError *error = nil; + NSData *data = [NSJSONSerialization dataWithJSONObject:message options:0 error:&error]; + + if (!data || error) { + qDebug() << "Failed to serialize message to VpnExtension as JSON. Error:" + << [error.localizedDescription UTF8String]; + return; + } + + void (^completionHandler)(NSData *) = ^(NSData *responseData) { + if (!responseData) { + if (callback) callback(nil); + return; + } + + NSError *deserializeError = nil; + NSDictionary *response = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:&deserializeError]; + + if (response && [response isKindOfClass:[NSDictionary class]]) { + if (callback) callback(response); + return; + } else if (deserializeError) { + qDebug() << "Failed to deserialize the VpnExtension response"; + } + + if (callback) callback(nil); + }; + + NETunnelProviderSession *session = (NETunnelProviderSession *)m_currentTunnel.connection; + + NSError *sendError = nil; + + if ([session respondsToSelector:@selector(sendProviderMessage:returnError:responseHandler:)]) { + [session sendProviderMessage:data returnError:&sendError responseHandler:completionHandler]; + } else { + qDebug() << "Method sendProviderMessage:responseHandler:error: does not exist"; + } + + if (sendError) { + qDebug() << "Failed to send message to VpnExtension. Error:" + << [sendError.localizedDescription UTF8String]; + } + +} + +bool IosController::shareText(const QStringList& filesToSend) { + NSMutableArray *sharingItems = [NSMutableArray new]; + + for (int i = 0; i < filesToSend.size(); i++) { + NSURL *logFileUrl = [[NSURL alloc] initFileURLWithPath:filesToSend[i].toNSString()]; + [sharingItems addObject:logFileUrl]; + } + + UIViewController *qtController = getViewController(); + if (!qtController) return; + + UIActivityViewController *activityController = [[UIActivityViewController alloc] initWithActivityItems:sharingItems applicationActivities:nil]; + + __block bool isAccepted = false; + + [activityController setCompletionWithItemsHandler:^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) { + isAccepted = completed; + emit finished(); + }]; + + [qtController presentViewController:activityController animated:YES completion:nil]; + UIPopoverPresentationController *popController = activityController.popoverPresentationController; + if (popController) { + popController.sourceView = qtController.view; + popController.sourceRect = CGRectMake(100, 100, 100, 100); + } + + QEventLoop wait; + QObject::connect(this, &IosController::finished, &wait, &QEventLoop::quit); + wait.exec(); + + return isAccepted; +} + +QString IosController::openFile() { + UIDocumentPickerViewController *documentPicker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[@"public.item"] inMode:UIDocumentPickerModeOpen]; + + DocumentPickerDelegate *documentPickerDelegate = [[DocumentPickerDelegate alloc] init]; + documentPicker.delegate = documentPickerDelegate; + + UIViewController *qtController = getViewController(); + if (!qtController) return; + + [qtController presentViewController:documentPicker animated:YES completion:nil]; + + __block QString filePath; + + documentPickerDelegate.documentPickerClosedCallback = ^(NSString *path) { + if (path) { + filePath = QString::fromUtf8(path.UTF8String); + } else { + filePath = QString(); + } + emit finished(); + }; + + QEventLoop wait; + QObject::connect(this, &IosController::finished, &wait, &QEventLoop::quit); + wait.exec(); + + return filePath; +} + +void IosController::requestInetAccess() { + NSURL *url = [NSURL URLWithString:@"http://captive.apple.com/generate_204"]; + if (!url) { + qDebug() << "IosController::requestInetAccess URL error"; + return; + } + + NSURLSession *session = [NSURLSession sharedSession]; + NSURLSessionDataTask *task = [session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + if (error) { + qDebug() << "IosController::requestInetAccess error:" << error.localizedDescription; + } else { + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + QString responseBody = QString::fromUtf8((const char*)data.bytes, data.length); + } + }]; + [task resume]; +}