diff --git a/IVPNClient.xcodeproj/project.pbxproj b/IVPNClient.xcodeproj/project.pbxproj index 5cd4e16c3..bb239b40e 100644 --- a/IVPNClient.xcodeproj/project.pbxproj +++ b/IVPNClient.xcodeproj/project.pbxproj @@ -154,6 +154,7 @@ 828E9C95231E5780001E1FCF /* Data+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 828E9C94231E5780001E1FCF /* Data+Ext.swift */; }; 828E9C96231E5780001E1FCF /* Data+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 828E9C94231E5780001E1FCF /* Data+Ext.swift */; }; 8290195F243CB27500777B6E /* ControlPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8290195E243CB27500777B6E /* ControlPanelView.swift */; }; + 8292D0FC2B45AEE8001EA123 /* TunnelKit+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8292D0FB2B45AEE8001EA123 /* TunnelKit+Ext.swift */; }; 8292E19B21748B0500123538 /* UserDefaults+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 825A43FC215CCFE70076131F /* UserDefaults+Ext.swift */; }; 8292E1A72174C10700123538 /* Peer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8292E1A62174C10700123538 /* Peer.swift */; }; 8292E1A92174C11600123538 /* Interface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8292E1A82174C11600123538 /* Interface.swift */; }; @@ -588,6 +589,7 @@ 828D8A6C258245AD00CB0E5B /* TwoFactorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwoFactorViewController.swift; sourceTree = ""; }; 828E9C94231E5780001E1FCF /* Data+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Ext.swift"; sourceTree = ""; }; 8290195E243CB27500777B6E /* ControlPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlPanelView.swift; sourceTree = ""; }; + 8292D0FB2B45AEE8001EA123 /* TunnelKit+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TunnelKit+Ext.swift"; sourceTree = ""; }; 8292E1A62174C10700123538 /* Peer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Peer.swift; sourceTree = ""; }; 8292E1A82174C11600123538 /* Interface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Interface.swift; sourceTree = ""; }; 8292E1AA2174C12200123538 /* Tunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tunnel.swift; sourceTree = ""; }; @@ -1156,6 +1158,7 @@ 82CE598F25ED3C7A0078099D /* URL+Ext.swift */, 82B329CA29F7C9F400F3ED9B /* UIWindow+Ext.swift */, 822BC6892A7CF3A700C733DF /* Decodable+Ext.swift */, + 8292D0FB2B45AEE8001EA123 /* TunnelKit+Ext.swift */, ); path = Extensions; sourceTree = ""; @@ -2259,6 +2262,7 @@ 8277F1CD22118D08007C6F15 /* ProofsViewModel.swift in Sources */, 8269CAC32264962F00CF488A /* AntiTrackerViewController.swift in Sources */, 82E5449224EE584E006DEF8D /* UIImageView+Ext.swift in Sources */, + 8292D0FC2B45AEE8001EA123 /* TunnelKit+Ext.swift in Sources */, 82E81AE72449C44F00D81FB7 /* PaymentComponentView.swift in Sources */, 8232FBF62240E40F006B81D2 /* Error+Ext.swift in Sources */, 820203932186EE0E00D756AA /* WireGuardSettingsViewController.swift in Sources */, @@ -3464,7 +3468,7 @@ repositoryURL = "https://github.com/passepartoutvpn/tunnelkit"; requirement = { kind = exactVersion; - version = 4.1.0; + version = 6.2.0; }; }; 829F5FC529A13CAE009E1AD3 /* XCRemoteSwiftPackageReference "KeychainAccess" */ = { diff --git a/IVPNClient/Config/Config.swift b/IVPNClient/Config/Config.swift index 59d11fb73..8941be8af 100644 --- a/IVPNClient/Config/Config.swift +++ b/IVPNClient/Config/Config.swift @@ -109,4 +109,8 @@ struct Config { return value } + // MARK: Log files + + static let maxBytes = 100000 + } diff --git a/IVPNClient/Managers/VPNManager.swift b/IVPNClient/Managers/VPNManager.swift index 08d39beaa..97d41507e 100644 --- a/IVPNClient/Managers/VPNManager.swift +++ b/IVPNClient/Managers/VPNManager.swift @@ -398,32 +398,15 @@ class VPNManager { } func getOpenVPNLog(completion: @escaping (String?) -> Void) { - guard let session = openvpnManager?.connection as? NETunnelProviderSession else { - completion(nil) - return - } + let maxBytes = UInt64(Config.maxBytes) - do { - try session.sendProviderMessage(OpenVPNProvider.Message.requestLog.data) { data in - guard let data = data, !data.isEmpty else { - completion(nil) - return - } - - guard let newestLog = String(data: data, encoding: .utf8), !newestLog.isEmpty else { - completion(nil) - return - } - - completion(newestLog) - return - } - } catch { + guard let url = FileManager.openvpnLogTextFileURL else { completion(nil) return } - completion(nil) + let lines = url.trailingLines(bytes: maxBytes) + completion(lines.joined(separator: "\n")) } func getWireGuardLog(completion: @escaping (String?) -> Void) { diff --git a/IVPNClient/Utilities/Extensions/FileManager+Extension.swift b/IVPNClient/Utilities/Extensions/FileManager+Extension.swift index ff6d18e06..4ce46bf51 100644 --- a/IVPNClient/Utilities/Extensions/FileManager+Extension.swift +++ b/IVPNClient/Utilities/Extensions/FileManager+Extension.swift @@ -54,10 +54,15 @@ extension FileManager { return sharedFolderURL?.appendingPathComponent("WireGuard.log") } + static var openvpnLogTextFileURL: URL? { + return sharedFolderURL?.appendingPathComponent("Tunnel.log") + } + static func deleteFile(at url: URL) -> Bool { do { try FileManager.default.removeItem(at: url) } catch { + log(.error, message: error.localizedDescription) return false } return true diff --git a/IVPNClient/Utilities/Extensions/NETunnelProviderProtocol+Ext.swift b/IVPNClient/Utilities/Extensions/NETunnelProviderProtocol+Ext.swift index 11a60eca9..a346df9de 100644 --- a/IVPNClient/Utilities/Extensions/NETunnelProviderProtocol+Ext.swift +++ b/IVPNClient/Utilities/Extensions/NETunnelProviderProtocol+Ext.swift @@ -33,55 +33,44 @@ extension NETunnelProviderProtocol { // MARK: OpenVPN static func makeOpenVPNProtocol(settings: ConnectionSettings, accessDetails: AccessDetails) -> NETunnelProviderProtocol { - guard let host = getHost() else { - return NETunnelProviderProtocol() - } + var proto = NETunnelProviderProtocol() - let username = accessDetails.username - let socketType: SocketType = settings.protocolType() == "TCP" ? .tcp : .udp - let credentials = OpenVPN.Credentials(username, KeyChain.vpnPassword ?? "") - let staticKey = OpenVPN.StaticKey.init(file: OpenVPNConf.tlsAuth, direction: OpenVPN.StaticKey.Direction.client) - let port = UInt16(getPort(settings: settings)) - - var sessionBuilder = OpenVPN.ConfigurationBuilder() - sessionBuilder.ca = OpenVPN.CryptoContainer(pem: OpenVPNConf.caCert) - sessionBuilder.cipher = .aes256cbc - sessionBuilder.compressionFraming = .disabled - sessionBuilder.endpointProtocols = [EndpointProtocol(socketType, port)] - sessionBuilder.hostname = host.host - sessionBuilder.tlsWrap = OpenVPN.TLSWrap.init(strategy: .auth, key: staticKey!) - - if let dnsServers = openVPNdnsServers(), !dnsServers.isEmpty, dnsServers != [""] { - sessionBuilder.dnsServers = dnsServers - log(.info, message: "DNS server: \(dnsServers)") - - switch DNSProtocolType.preferred() { - case .doh: - sessionBuilder.dnsProtocol = .https - sessionBuilder.dnsHTTPSURL = URL.init(string: DNSProtocolType.getServerURL(address: UserDefaults.shared.customDNS)) - case .dot: - sessionBuilder.dnsProtocol = .tls - sessionBuilder.dnsTLSServerName = DNSProtocolType.getServerName(address: UserDefaults.shared.customDNS) - default: - sessionBuilder.dnsProtocol = .plain - } + guard let host = getHost() else { + return proto } - var builder = OpenVPNProvider.ConfigurationBuilder(sessionConfiguration: sessionBuilder.build()) - builder.shouldDebug = true - builder.debugLogFormat = "$Dyyyy-MM-dd HH:mm:ss$d $L $M" - builder.masksPrivateData = true + let credentials = OpenVPN.Credentials(accessDetails.username, KeyChain.vpnPassword ?? "") - let configuration = builder.build() - let keychain = Keychain(group: Config.appGroup) - _ = try? keychain.set(password: credentials.password, for: credentials.username, context: Config.openvpnTunnelProvider) - let proto = try! configuration.generatedTunnelProtocol( - withBundleIdentifier: Config.openvpnTunnelProvider, + let params = OpenVPN.Config.Parameters( + title: Config.openvpnTunnelTitle, appGroup: Config.appGroup, - context: Config.openvpnTunnelProvider, - credentials: credentials + hostname: host.host, + port: UInt16(getPort(settings: settings)), + socketType: settings.protocolType() == "TCP" ? .tcp : .udp, + dnsServers: openVPNdnsServers(), + dnsProtocol: DNSProtocolType.preferred(), + customDNS: UserDefaults.shared.customDNS ) - proto.disconnectOnSleep = !UserDefaults.shared.keepAlive + + var cfg = OpenVPN.Config.make(params: params) + cfg.username = credentials.username + + let keychain = Keychain(group: Config.appGroup) + var passwordReference = Data() + do { + passwordReference = try keychain.set(password: credentials.password, for: credentials.username, context: Config.openvpnTunnelProvider) + } catch { + log(.error, message: "Keychain failure: \(error)") + } + + var extra = NetworkExtensionExtra() + extra.passwordReference = passwordReference + + do { + proto = try cfg.asTunnelProtocol(withBundleIdentifier: Config.openvpnTunnelProvider, extra: extra) + } catch { + log(.error, message: "Keychain failure: \(error)") + } if #available(iOS 15.1, *) { if #available(iOS 16, *) { } else { @@ -89,10 +78,9 @@ extension NETunnelProviderProtocol { } } - if #available(iOS 14.2, *) { - proto.includeAllNetworks = disableLanAccess() - proto.excludeLocalNetworks = !disableLanAccess() - } + proto.includeAllNetworks = disableLanAccess() + proto.excludeLocalNetworks = !disableLanAccess() + proto.disconnectOnSleep = !UserDefaults.shared.keepAlive return proto } diff --git a/IVPNClient/Utilities/Extensions/TunnelKit+Ext.swift b/IVPNClient/Utilities/Extensions/TunnelKit+Ext.swift new file mode 100644 index 000000000..bd54592a2 --- /dev/null +++ b/IVPNClient/Utilities/Extensions/TunnelKit+Ext.swift @@ -0,0 +1,82 @@ +// +// TunnelKit+Ext.swift +// IVPN iOS app +// https://github.com/ivpn/ios-app +// +// Created by Juraj Hilje on 2024-01-03. +// Copyright (c) 2024 IVPN Limited. +// +// This file is part of the IVPN iOS app. +// +// The IVPN iOS app is free software: you can redistribute it and/or +// modify it under the terms of the GNU General Public License as published by the Free +// Software Foundation, either version 3 of the License, or (at your option) any later version. +// +// The IVPN iOS app is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +// details. +// +// You should have received a copy of the GNU General Public License +// along with the IVPN iOS app. If not, see . +// + +import Foundation +import TunnelKitCore +import TunnelKitOpenVPN + +extension OpenVPN { + + struct Config { + + struct Parameters { + let title: String + let appGroup: String + let hostname: String + let port: UInt16 + let socketType: SocketType + let dnsServers: [String]? + let dnsProtocol: DNSProtocolType + let customDNS: String + } + + static func make(params: Parameters) -> OpenVPN.ProviderConfiguration { + let tlsKey = OpenVPN.StaticKey(file: OpenVPNConf.tlsAuth, direction: .client)! + + var builder = OpenVPN.ConfigurationBuilder() + builder.ca = OpenVPN.CryptoContainer(pem: OpenVPNConf.caCert) + builder.cipher = .aes256cbc + builder.compressionFraming = .disabled + builder.remotes = [Endpoint(params.hostname, EndpointProtocol(params.socketType, params.port))] + builder.tlsWrap = TLSWrap(strategy: .auth, key: tlsKey) + builder.routingPolicies = [.IPv4, .IPv6] + + if let dnsServers = params.dnsServers, !dnsServers.isEmpty, dnsServers != [""] { + builder.dnsServers = dnsServers + + switch params.dnsProtocol { + case .doh: + builder.dnsProtocol = .https + builder.dnsHTTPSURL = URL.init(string: DNSProtocolType.getServerURL(address: params.customDNS)) + case .dot: + builder.dnsProtocol = .tls + builder.dnsTLSServerName = DNSProtocolType.getServerName(address: params.customDNS) + default: + builder.dnsProtocol = .plain + } + } + + let cfg = builder.build() + + var configuration = OpenVPN.ProviderConfiguration(params.title, appGroup: params.appGroup, configuration: cfg) + configuration.shouldDebug = true + configuration.debugLogFormat = "$Dyyyy-MM-dd HH:mm:ss$d $L $M" + configuration.masksPrivateData = true + configuration.debugLogPath = FileManager.openvpnLogTextFileURL?.lastPathComponent + + return configuration + } + + } + +} diff --git a/IVPNClient/Utilities/Extensions/URL+Ext.swift b/IVPNClient/Utilities/Extensions/URL+Ext.swift index e49218958..f2afee3dc 100644 --- a/IVPNClient/Utilities/Extensions/URL+Ext.swift +++ b/IVPNClient/Utilities/Extensions/URL+Ext.swift @@ -43,4 +43,53 @@ extension URL { return "" } + func trailingContent(bytes: UInt64) -> String { + var file: FileHandle? + + defer { + try? file?.close() + } + + do { + file = try FileHandle(forReadingFrom: self) + + guard let size = try file?.seekToEnd() else { + log(.error, message: "Cannot seek") + return "" + } + + var offset: UInt64 + if bytes < size { + offset = size - bytes + } else { + offset = 0 + } + + try file?.seek(toOffset: offset) + + guard let data = try file?.readToEnd() else { + log(.error, message: "No data") + return "" + } + + guard let string = String(data: data, encoding: .utf8) else { + log(.error, message: "Cannot encode string") + return "" + } + + return string + } catch { + log(.error, message: "Error while reading file: \(error)") + return "" + } + } + + func trailingLines(bytes: UInt64) -> [String] { + return trailingContent(bytes: bytes) + .components(separatedBy: "\n") + .filter { + !$0.isEmpty + } + } + }