From 151e66202713ac3d24e52f2931fb1d802fea086a Mon Sep 17 00:00:00 2001 From: isamnezia <156459471+isamnezia@users.noreply.github.com> Date: Mon, 15 Apr 2024 01:04:01 +0300 Subject: [PATCH] VPNC control and logging (#748) VPNC control and logging --- client/cmake/ios.cmake | 1 + client/platforms/ios/Log.swift | 13 +++++ client/platforms/ios/LogController.swift | 37 ++++---------- .../ios/PacketTunnelProvider+OpenVPN.swift | 2 + .../ios/PacketTunnelProvider+WireGuard.swift | 2 +- .../platforms/ios/PacketTunnelProvider.swift | 43 ++++++++++++++++ client/platforms/ios/VPNCController.swift | 50 +++++++++++++++++++ client/platforms/ios/WGConfig.swift | 3 ++ client/platforms/ios/ios_controller.mm | 17 ++++--- client/ui/controllers/installController.cpp | 13 +++++ client/ui/controllers/settingsController.cpp | 3 ++ 11 files changed, 149 insertions(+), 35 deletions(-) create mode 100644 client/platforms/ios/VPNCController.swift diff --git a/client/cmake/ios.cmake b/client/cmake/ios.cmake index 37d509ee..97678bf7 100644 --- a/client/cmake/ios.cmake +++ b/client/cmake/ios.cmake @@ -108,6 +108,7 @@ target_sources(${PROJECT} PRIVATE ${CLIENT_ROOT_DIR}/platforms/ios/Log.swift ${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift ${CLIENT_ROOT_DIR}/platforms/ios/ScreenProtection.swift + ${CLIENT_ROOT_DIR}/platforms/ios/VPNCController.swift ) target_sources(${PROJECT} PRIVATE diff --git a/client/platforms/ios/Log.swift b/client/platforms/ios/Log.swift index 7e9b7fdb..d2222f9d 100644 --- a/client/platforms/ios/Log.swift +++ b/client/platforms/ios/Log.swift @@ -16,6 +16,11 @@ struct Log { private static let appGroupID = "group.org.amnezia.AmneziaVPN" + static let appLogURL = { + let sharedContainerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupID)! + return sharedContainerURL.appendingPathComponent("app.log", isDirectory: false) + }() + static let neLogURL = { let sharedContainerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupID)! return sharedContainerURL.appendingPathComponent("ne.log", isDirectory: false) @@ -70,8 +75,12 @@ struct Log { } static func log(_ type: OSLogType, title: String = "", message: String, url: URL = neLogURL) { + NSLog("\(title) \(message)") + guard isLoggingEnabled else { return } + osLog.log(level: type, "\(title) \(message)") + let date = Date() let level = Record.Level(from: type) let messages = message.split(whereSeparator: \.isNewline) @@ -107,3 +116,7 @@ extension Log: CustomStringConvertible { .joined(separator: "\n") } } + +func log(_ type: OSLogType, title: String = "", message: String) { + Log.log(type, title: "App: \(title)", message: message, url: Log.appLogURL) +} diff --git a/client/platforms/ios/LogController.swift b/client/platforms/ios/LogController.swift index e10b9ce2..872fa53d 100644 --- a/client/platforms/ios/LogController.swift +++ b/client/platforms/ios/LogController.swift @@ -1,50 +1,33 @@ import Foundation -import NetworkExtension public func swiftUpdateLogData(_ qtString: std.string) -> std.string { let qtLog = Log(String(describing: qtString)) var log = qtLog + if let appLog = Log(at: Log.appLogURL) { + appLog.records.forEach { + log.records.append($0) + } + } + if let neLog = Log(at: Log.neLogURL) { neLog.records.forEach { log.records.append($0) } + } - log.records.sort { - $0.date < $1.date - } + log.records.sort { + $0.date < $1.date } return std.string(log.description) } public func swiftDeleteLog() { + Log.clear(at: Log.appLogURL) Log.clear(at: Log.neLogURL) } public func toggleLogging(_ isEnabled: Bool) { Log.isLoggingEnabled = isEnabled } - -public func clearSettings() { - NETunnelProviderManager.loadAllFromPreferences { managers, error in - if let error { - NSLog("clearSettings removeFromPreferences error: \(error.localizedDescription)") - return - } - - managers?.forEach { manager in - manager.removeFromPreferences { error in - if let error { - NSLog("NE removeFromPreferences error: \(error.localizedDescription)") - } else { - manager.loadFromPreferences { error in - if let error { - NSLog("NE loadFromPreferences after remove error: \(error.localizedDescription)") - } - } - } - } - } - } -} diff --git a/client/platforms/ios/PacketTunnelProvider+OpenVPN.swift b/client/platforms/ios/PacketTunnelProvider+OpenVPN.swift index e8c920e0..489c12c8 100644 --- a/client/platforms/ios/PacketTunnelProvider+OpenVPN.swift +++ b/client/platforms/ios/PacketTunnelProvider+OpenVPN.swift @@ -94,6 +94,8 @@ extension PacketTunnelProvider { } func stopOpenVPN(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { + ovpnLog(.info, message: "Stopping tunnel: reason: \(reason.description)") + stopHandler = completionHandler if vpnReachability.isTracking { vpnReachability.stopTracking() diff --git a/client/platforms/ios/PacketTunnelProvider+WireGuard.swift b/client/platforms/ios/PacketTunnelProvider+WireGuard.swift index 5d1505f3..31f37aea 100644 --- a/client/platforms/ios/PacketTunnelProvider+WireGuard.swift +++ b/client/platforms/ios/PacketTunnelProvider+WireGuard.swift @@ -200,7 +200,7 @@ extension PacketTunnelProvider { // } func stopWireguard(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { - wg_log(.info, staticMessage: "Stopping tunnel") + wg_log(.info, message: "Stopping tunnel: reason: \(reason.description)") wgAdapter.stop { error in ErrorNotifier.removeLastErrorFile() diff --git a/client/platforms/ios/PacketTunnelProvider.swift b/client/platforms/ios/PacketTunnelProvider.swift index 0c812f93..cac5f35b 100644 --- a/client/platforms/ios/PacketTunnelProvider.swift +++ b/client/platforms/ios/PacketTunnelProvider.swift @@ -200,3 +200,46 @@ extension WireGuardLogLevel { } } } + +extension NEProviderStopReason: CustomStringConvertible { + public var description: String { + switch self { + case .none: + return "No specific reason" + case .userInitiated: + return "The user stopped the NE" + case .providerFailed: + return "The NE failed to function correctly" + case .noNetworkAvailable: + return "No network connectivity is currently available" + case .unrecoverableNetworkChange: + return "The device’s network connectivity changed" + case .providerDisabled: + return "The NE was disabled" + case .authenticationCanceled: + return "The authentication process was canceled" + case .configurationFailed: + return "The VPNC is invalid" + case .idleTimeout: + return "The session timed out" + case .configurationDisabled: + return "The VPNC was disabled" + case .configurationRemoved: + return "The VPNC was removed" + case .superceded: + return "VPNC was superceded by a higher-priority VPNC" + case .userLogout: + return "The user logged out" + case .userSwitch: + return "The current console user changed" + case .connectionFailed: + return "The connection failed" + case .sleep: + return "A stop reason indicating the VPNC enabled disconnect on sleep and the device went to sleep" + case .appUpdate: + return "appUpdat" + @unknown default: + return "@unknown default" + } + } +} diff --git a/client/platforms/ios/VPNCController.swift b/client/platforms/ios/VPNCController.swift new file mode 100644 index 00000000..3ad0cbbf --- /dev/null +++ b/client/platforms/ios/VPNCController.swift @@ -0,0 +1,50 @@ +import Foundation +import NetworkExtension + +public func removeVPNC(_ vpncName: std.string) { + let vpncName = String(describing: vpncName) + + Task { + await getManagers()?.first { manager in + if let name = manager.localizedDescription, name == vpncName { + Task { + await remove(manager) + } + + return true + } else { + return false + } + } + } +} + +public func clearSettings() { + Task { + await getManagers()?.forEach { manager in + Task { + await remove(manager) + } + } + } +} + +func getManagers() async -> [NETunnelProviderManager]? { + do { + return try await NETunnelProviderManager.loadAllFromPreferences() + } catch { + log(.error, title: "VPNC: ", message: "loadAllFromPreferences error: \(error.localizedDescription)") + return nil + } +} + +func remove(_ manager: NETunnelProviderManager) async { + let vpncName = manager.localizedDescription ?? "Unknown" + do { + try await manager.removeFromPreferences() + try await manager.loadFromPreferences() + log(.info, title: "VPNC: ", message: "Remove \(vpncName)") + } catch { + log(.error, title: "VPNC: ", message: "Failed to remove \(vpncName) (\(error.localizedDescription))") + } +} diff --git a/client/platforms/ios/WGConfig.swift b/client/platforms/ios/WGConfig.swift index 4458c330..3dbdf7d0 100644 --- a/client/platforms/ios/WGConfig.swift +++ b/client/platforms/ios/WGConfig.swift @@ -86,6 +86,9 @@ struct WGConfig: Decodable { AllowedIPs = \(allowedIPs.joined(separator: ", ")) Endpoint = \(hostName):\(port) PersistentKeepalive = \(persistentKeepAlive) + + SplitTunnelType = \(splitTunnelType) + SplitTunnelSites = \(splitTunnelSites.joined(separator: ", ")) """ } } diff --git a/client/platforms/ios/ios_controller.mm b/client/platforms/ios/ios_controller.mm index f2d85b02..c10182ba 100644 --- a/client/platforms/ios/ios_controller.mm +++ b/client/platforms/ios/ios_controller.mm @@ -89,9 +89,11 @@ bool IosController::initialize() 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"; + qDebug() << "IosController::initialize : VPN already connected with" << manager.localizedDescription; emit connectionStateChanged(Vpn::ConnectionState::Connected); break; @@ -138,7 +140,7 @@ bool IosController::connectVpn(amnezia::Proto proto, const QJsonObject& configur [NETunnelProviderManager loadAllFromPreferencesWithCompletionHandler:^(NSArray * _Nullable managers, NSError * _Nullable error) { @try { if (error) { - qDebug() << "IosController::connectVpn : Error:" << [error.localizedDescription UTF8String]; + qDebug() << "IosController::connectVpn : VPNC: loadAllFromPreferences error:" << [error.localizedDescription UTF8String]; emit connectionStateChanged(Vpn::ConnectionState::Error); ok = false; return; @@ -151,7 +153,7 @@ bool IosController::connectVpn(amnezia::Proto proto, const QJsonObject& configur for (NETunnelProviderManager *manager in managers) { if ([manager.localizedDescription isEqualToString:tunnelName.toNSString()]) { m_currentTunnel = manager; - qDebug() << "IosController::connectVpn : Using existing tunnel"; + qDebug() << "IosController::connectVpn : Using existing tunnel:" << manager.localizedDescription; if (manager.connection.status == NEVPNStatusConnected) { emit connectionStateChanged(Vpn::ConnectionState::Connected); return; @@ -162,10 +164,10 @@ bool IosController::connectVpn(amnezia::Proto proto, const QJsonObject& configur } if (!m_currentTunnel) { - qDebug() << "IosController::connectVpn : Creating new tunnel"; 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; } } @@ -598,13 +600,14 @@ void IosController::startTunnel() 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::start" << protocolName << ": Connect " << protocolName << " Tunnel Load Error" << loadError.localizedDescription.UTF8String; + qDebug().nospace() << "IosController::startTunnel :" << m_currentTunnel.localizedDescription << protocolName << ": Connect " << protocolName << " Tunnel Load Error" << loadError.localizedDescription.UTF8String; emit connectionStateChanged(Vpn::ConnectionState::Error); return; } @@ -615,11 +618,11 @@ void IosController::startTunnel() BOOL started = [m_currentTunnel.connection startVPNTunnelWithOptions:nil andReturnError:&startError]; if (!started || startError) { - qDebug().nospace() << "IosController::start" << protocolName << " : Connect " << protocolName << " Tunnel Start Error" + 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::start" << protocolName << " : Starting the tunnel succeeded"; + qDebug().nospace() << "IosController::startTunnel :" << m_currentTunnel.localizedDescription << protocolName << " : Starting the tunnel succeeded"; } }]; }); diff --git a/client/ui/controllers/installController.cpp b/client/ui/controllers/installController.cpp index 2f624916..2a0fa8f5 100644 --- a/client/ui/controllers/installController.cpp +++ b/client/ui/controllers/installController.cpp @@ -16,6 +16,10 @@ #include "ui/models/protocols/awgConfigModel.h" #include "ui/models/protocols/wireguardConfigModel.h" +#ifdef Q_OS_IOS + #include +#endif + namespace { Logger logger("ServerController"); @@ -564,6 +568,15 @@ void InstallController::removeApiConfig() { auto serverConfig = m_serversModel->getServerConfig(m_serversModel->getDefaultServerIndex()); +#ifdef Q_OS_IOS + QString vpncName = QString("%1 (%2) %3") + .arg(serverConfig[config_key::description].toString()) + .arg(serverConfig[config_key::hostName].toString()) + .arg(serverConfig[config_key::vpnproto].toString()); + + AmneziaVPN::removeVPNC(vpncName.toStdString()); +#endif + serverConfig.remove(config_key::dns1); serverConfig.remove(config_key::dns2); serverConfig.remove(config_key::containers); diff --git a/client/ui/controllers/settingsController.cpp b/client/ui/controllers/settingsController.cpp index c3640719..077214cf 100644 --- a/client/ui/controllers/settingsController.cpp +++ b/client/ui/controllers/settingsController.cpp @@ -78,6 +78,9 @@ void SettingsController::toggleLogging(bool enable) #endif if (enable == true) { checkIfNeedDisableLogs(); + + qInfo().noquote() << QString("Logging has enabled on %1 version %2 %3").arg(APPLICATION_NAME, APP_VERSION, GIT_COMMIT_HASH); + qInfo().noquote() << QString("%1 (%2)").arg(QSysInfo::prettyProductName(), QSysInfo::currentCpuArchitecture()); } emit loggingStateChanged(); }