From 2b3dd0b34368e5f5361598070751b5bd4d4e9c8a Mon Sep 17 00:00:00 2001 From: AnhTVc Date: Thu, 5 Jun 2025 10:36:42 +0700 Subject: [PATCH 1/4] fix bug ios 18 fix bug: can not start vpn in ios18 --- client/3rd-prebuilt | 2 +- .../ios/PacketTunnelProvider+OpenVPN.swift | 234 ----- client/platforms/ios/ios_controller.mm | 865 ------------------ 3 files changed, 1 insertion(+), 1100 deletions(-) delete mode 100644 client/platforms/ios/PacketTunnelProvider+OpenVPN.swift delete mode 100644 client/platforms/ios/ios_controller.mm diff --git a/client/3rd-prebuilt b/client/3rd-prebuilt index 0f3748ef..a72a1aed 160000 --- a/client/3rd-prebuilt +++ b/client/3rd-prebuilt @@ -1 +1 @@ -Subproject commit 0f3748efd7cc04e0c914304b68931f925bed1259 +Subproject commit a72a1aeddfb041eaebcf9e7e09ad8adc0c3afbee diff --git a/client/platforms/ios/PacketTunnelProvider+OpenVPN.swift b/client/platforms/ios/PacketTunnelProvider+OpenVPN.swift deleted file mode 100644 index 3e0a4a07..00000000 --- a/client/platforms/ios/PacketTunnelProvider+OpenVPN.swift +++ /dev/null @@ -1,234 +0,0 @@ -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 - } - } - - // 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 deleted file mode 100644 index 85fb50b7..00000000 --- a/client/platforms/ios/ios_controller.mm +++ /dev/null @@ -1,865 +0,0 @@ -#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(); - - 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; - - 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; - - 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]; -} From bc21d68e5a0e93f293f3aa6fdf29700e0fc9812e Mon Sep 17 00:00:00 2001 From: AnhTVc Date: Fri, 6 Jun 2025 16:23:28 +0700 Subject: [PATCH 2/4] 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]; +} From 8078e59a834aff2e8f241c3a90b6848a5f32b7be Mon Sep 17 00:00:00 2001 From: AnhTVc Date: Sun, 8 Jun 2025 23:34:40 +0700 Subject: [PATCH 3/4] update convension --- .../ios/PacketTunnelProvider+OpenVPN.swift | 2 - client/platforms/ios/ios_controller.mm | 39 ++++--------------- 2 files changed, 7 insertions(+), 34 deletions(-) diff --git a/client/platforms/ios/PacketTunnelProvider+OpenVPN.swift b/client/platforms/ios/PacketTunnelProvider+OpenVPN.swift index 12bbaa54..4652e2b2 100644 --- a/client/platforms/ios/PacketTunnelProvider+OpenVPN.swift +++ b/client/platforms/ios/PacketTunnelProvider+OpenVPN.swift @@ -181,10 +181,8 @@ extension PacketTunnelProvider: OpenVPNAdapterDelegate { ipv6Settings.includedRoutes = [ipv6DefaultRoute] NSLog("[Route] Added default IPv6 route (::/0)") } - } } - // Set the network settings for the current tunneling session. setTunnelNetworkSettings(networkSettings, completionHandler: completionHandler) } diff --git a/client/platforms/ios/ios_controller.mm b/client/platforms/ios/ios_controller.mm index d96bde40..197d74ac 100644 --- a/client/platforms/ios/ios_controller.mm +++ b/client/platforms/ios/ios_controller.mm @@ -122,16 +122,13 @@ bool IosController::initialize() 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", @@ -142,9 +139,6 @@ bool IosController::connectVpn(amnezia::Proto proto, const QJsonObject& configur QStringList lines = ovpnConfig.split('\n'); QStringList filteredLines; - - bool insideTlsAuthBlock = false; - for (const QString &line : lines) { QString trimmedLine = line.trimmed(); @@ -160,16 +154,11 @@ bool IosController::connectVpn(amnezia::Proto proto, const QJsonObject& configur 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") @@ -302,10 +291,8 @@ void IosController::checkStatus() void IosController::vpnStatusDidChange(void *pNotification) { NETunnelProviderSession *session = (NETunnelProviderSession *)pNotification; - - qDebug() << "IosController::vpnStatusDidChange - Session pointer:" << session; - - if (session /* && session == TunnelManager.session*/ ) { + + if (session /* && session == TunnelManager.session */ ) { qDebug() << "IosController::vpnStatusDidChange" << iosStatusToState(session.status) << session; if (session.status == NEVPNStatusDisconnected) { @@ -313,16 +300,7 @@ void IosController::vpnStatusDidChange(void *pNotification) [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: @@ -426,7 +404,7 @@ 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); @@ -435,10 +413,7 @@ bool IosController::setupOpenVPN() } 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(); From 9f8c40f4b052e3f0e1833da7e9d119215ee9f48d Mon Sep 17 00:00:00 2001 From: AnhTVc Date: Sat, 5 Jul 2025 00:13:01 +0700 Subject: [PATCH 4/4] Update build_ios.sh update script build unsigned ipa --- deploy/build_ios.sh | 71 +++++++++------------------------------------ 1 file changed, 14 insertions(+), 57 deletions(-) diff --git a/deploy/build_ios.sh b/deploy/build_ios.sh index 5dc11ff1..e91c9b79 100755 --- a/deploy/build_ios.sh +++ b/deploy/build_ios.sh @@ -34,66 +34,23 @@ clang -v # Generate XCodeProj $QT_BIN_DIR/qt-cmake . -B $BUILD_DIR -GXcode -DQT_HOST_PATH=$QT_MACOS_ROOT_DIR -KEYCHAIN=amnezia.build.ios.keychain -KEYCHAIN_FILE=$HOME/Library/Keychains/${KEYCHAIN}-db -# Setup keychain -if [ "${IOS_SIGNING_CERT_BASE64+x}" ]; then - echo "Import certificate" +cd $BUILD_DIR +xcodebuild archive \ + -project AmneziaVPN.xcodeproj \ + -scheme AmneziaVPN \ + -configuration Release \ + -archivePath ./build/AmneziaVPN.xcarchive \ + CODE_SIGNING_ALLOWED=NO \ + CODE_SIGN_IDENTITY="" \ + CODE_SIGNING_REQUIRED=NO - TRUST_CERT_CER=$BUILD_DIR/trust-cert.cer - SIGNING_CERT_P12=$BUILD_DIR/signing-cert.p12 +mkdir -p Payload - echo $IOS_TRUST_CERT_BASE64 | base64 --decode > $TRUST_CERT_CER - echo $IOS_SIGNING_CERT_BASE64 | base64 --decode > $SIGNING_CERT_P12 +cp -R ./build/AmneziaVPN.xcarchive/Products/Applications/AmneziaVPN.app Payload/ - shasum -a 256 $TRUST_CERT_CER - shasum -a 256 $SIGNING_CERT_P12 +zip -r AmneziaVPN_unsigned.ipa Payload - KEYCHAIN_PASS=$IOS_SIGNING_CERT_PASSWORD +rm -rf Payload - security create-keychain -p $KEYCHAIN_PASS $KEYCHAIN || true - security default-keychain -s $KEYCHAIN - security unlock-keychain -p $KEYCHAIN_PASS $KEYCHAIN - - security default-keychain - security list-keychains - - security import $TRUST_CERT_CER -k $KEYCHAIN -P "" -T /usr/bin/codesign - security import $SIGNING_CERT_P12 -k $KEYCHAIN -P $IOS_SIGNING_CERT_PASSWORD -T /usr/bin/codesign - - security set-key-partition-list -S "apple-tool:,apple:,codesign:" -s -k $KEYCHAIN_PASS $KEYCHAIN - security find-identity -p codesigning - security set-keychain-settings $KEYCHAIN_FILE - security set-keychain-settings -t 3600 $KEYCHAIN_FILE - security unlock-keychain -p $KEYCHAIN_PASS $KEYCHAIN_FILE - - # Copy provisioning prifiles - mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles/" - - echo $IOS_APP_PROVISIONING_PROFILE | base64 --decode > ~/Library/MobileDevice/Provisioning\ Profiles/app.mobileprovision - echo $IOS_NE_PROVISIONING_PROFILE | base64 --decode > ~/Library/MobileDevice/Provisioning\ Profiles/ne.mobileprovision - - shasum -a 256 ~/Library/MobileDevice/Provisioning\ Profiles/app.mobileprovision - shasum -a 256 ~/Library/MobileDevice/Provisioning\ Profiles/ne.mobileprovision - - profile_uuid=`grep UUID -A1 -a ~/Library/MobileDevice/Provisioning\ Profiles/app.mobileprovision | grep -io "[-A-F0-9]\{36\}"` - profile_ne_uuid=`grep UUID -A1 -a ~/Library/MobileDevice/Provisioning\ Profiles/ne.mobileprovision | grep -io "[-A-F0-9]\{36\}"` - - mv ~/Library/MobileDevice/Provisioning\ Profiles/app.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/$profile_uuid.mobileprovision - mv ~/Library/MobileDevice/Provisioning\ Profiles/ne.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/$profile_ne_uuid.mobileprovision -else - echo "Failed to import certificate, aborting..." - exit 1 -fi - -# Build project -xcodebuild \ -"OTHER_CODE_SIGN_FLAGS=--keychain '$KEYCHAIN_FILE'" \ --configuration Release \ --scheme AmneziaVPN \ --destination "generic/platform=iOS,name=Any iOS'" \ --project $BUILD_DIR/AmneziaVPN.xcodeproj - -# restore keychain -security default-keychain -s login.keychain +echo " Build setup completed successfully."