diff --git a/IVPNClient.xcodeproj/project.pbxproj b/IVPNClient.xcodeproj/project.pbxproj index 63f8a42d4..6025cf94f 100644 --- a/IVPNClient.xcodeproj/project.pbxproj +++ b/IVPNClient.xcodeproj/project.pbxproj @@ -80,6 +80,7 @@ 82351FCC241FBC8E00E6E0FD /* VPNStatusViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82351FCB241FBC8E00E6E0FD /* VPNStatusViewModelTests.swift */; }; 82351FCE2420CE6800E6E0FD /* MapMarkerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82351FCD2420CE6800E6E0FD /* MapMarkerView.swift */; }; 82351FD224222F7700E6E0FD /* ConnectionInfoPopupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82351FD124222F7700E6E0FD /* ConnectionInfoPopupView.swift */; }; + 82364E9D2EAA58EB004FCBE1 /* ObfuscationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82364E9C2EAA58EB004FCBE1 /* ObfuscationViewController.swift */; }; 82365E7F2AB86020006434C3 /* V2RaySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82365E7E2AB86020006434C3 /* V2RaySettings.swift */; }; 823ACC292626E69F006F69AB /* GeoLookupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 823ACC282626E69F006F69AB /* GeoLookupTests.swift */; }; 823ACC312626FF3E006F69AB /* IpProtocolView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 823ACC302626FF3E006F69AB /* IpProtocolView.swift */; }; @@ -520,6 +521,7 @@ 82351FCB241FBC8E00E6E0FD /* VPNStatusViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNStatusViewModelTests.swift; sourceTree = ""; }; 82351FCD2420CE6800E6E0FD /* MapMarkerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapMarkerView.swift; sourceTree = ""; }; 82351FD124222F7700E6E0FD /* ConnectionInfoPopupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionInfoPopupView.swift; sourceTree = ""; }; + 82364E9C2EAA58EB004FCBE1 /* ObfuscationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObfuscationViewController.swift; sourceTree = ""; }; 82365E7E2AB86020006434C3 /* V2RaySettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = V2RaySettings.swift; sourceTree = ""; }; 823ACC282626E69F006F69AB /* GeoLookupTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoLookupTests.swift; sourceTree = ""; }; 823ACC302626FF3E006F69AB /* IpProtocolView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IpProtocolView.swift; sourceTree = ""; }; @@ -1283,6 +1285,7 @@ 82AB0874291A6B5F0084625A /* AddCustomPortViewController.swift */, 82052E5529C1D83700227CF9 /* MTUViewController.swift */, 829F5EAE2A56E067005919AF /* AntiTrackerListViewController.swift */, + 82364E9C2EAA58EB004FCBE1 /* ObfuscationViewController.swift */, ); path = ViewControllers; sourceTree = ""; @@ -2407,6 +2410,7 @@ 8285D253246D28FA0088C00F /* AnimatedCircleLayer.swift in Sources */, 9CBFF0302102254800FE1757 /* Settings.swift in Sources */, 9C3031371DB4307D00C38B0C /* SettingsViewController.swift in Sources */, + 82364E9D2EAA58EB004FCBE1 /* ObfuscationViewController.swift in Sources */, 82EEB6C625F9398600915837 /* DNSProtocolType.swift in Sources */, 821F1C7E21FF544200107311 /* VPNServerViewModel.swift in Sources */, 82F189A2225CE8A90038ABA0 /* UIView+Ext.swift in Sources */, @@ -2834,7 +2838,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = IVPNClient/IVPNClient.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 10; DEVELOPMENT_TEAM = WQXXM75BYN; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -2852,7 +2856,7 @@ "$(inherited)", "$(PROJECT_DIR)/IVPNClient/liboqs", ); - MARKETING_VERSION = 2.12.5; + MARKETING_VERSION = 2.13.0; ONLY_ACTIVE_ARCH = YES; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "-D DEBUG"; @@ -3323,7 +3327,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = IVPNClient/IVPNClient.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 10; DEVELOPMENT_TEAM = WQXXM75BYN; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -3341,7 +3345,7 @@ "$(inherited)", "$(PROJECT_DIR)/IVPNClient/liboqs", ); - MARKETING_VERSION = 2.12.5; + MARKETING_VERSION = 2.13.0; ONLY_ACTIVE_ARCH = YES; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "-D DEBUG"; @@ -3362,7 +3366,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = IVPNClient/IVPNClient.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 10; DEVELOPMENT_TEAM = WQXXM75BYN; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -3380,7 +3384,7 @@ "$(inherited)", "$(PROJECT_DIR)/IVPNClient/liboqs", ); - MARKETING_VERSION = 2.12.5; + MARKETING_VERSION = 2.13.0; ONLY_ACTIVE_ARCH = YES; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "-D RELEASE"; diff --git a/IVPNClient/Managers/DNSManager.swift b/IVPNClient/Managers/DNSManager.swift index 059e08143..5b762d7bd 100644 --- a/IVPNClient/Managers/DNSManager.swift +++ b/IVPNClient/Managers/DNSManager.swift @@ -75,37 +75,6 @@ class DNSManager { } } - static func saveResolvedDNS(server: String, key: String) { - guard !server.trim().isEmpty else { - return - } - - DNSResolver.resolve(host: server) { list in - var addresses: [String] = [] - - for ip in list { - if let host = ip.host { - addresses.append(host) - } - } - - switch key { - case UserDefaults.Key.resolvedDNSOutsideVPN: - UserDefaults.standard.set(addresses, forKey: UserDefaults.Key.resolvedDNSOutsideVPN) - NotificationCenter.default.post(name: Notification.Name.UpdateResolvedDNS, object: nil) - case UserDefaults.Key.resolvedDNSInsideVPN: - UserDefaults.shared.set(addresses, forKey: UserDefaults.Key.resolvedDNSInsideVPN) - NotificationCenter.default.post(name: Notification.Name.UpdateResolvedDNSInsideVPN, object: nil) - default: - break - } - - if addresses.isEmpty { - NotificationCenter.default.post(name: Notification.Name.ResolvedDNSError, object: nil) - } - } - } - // MARK: - Private methods - private func getDnsSettings(model: SecureDNS) -> NEDNSSettings { diff --git a/IVPNClient/Models/SecureDNS.swift b/IVPNClient/Models/SecureDNS.swift index 4250056a6..54fab02bc 100644 --- a/IVPNClient/Models/SecureDNS.swift +++ b/IVPNClient/Models/SecureDNS.swift @@ -30,7 +30,6 @@ struct SecureDNS: Codable { if let address = address { serverURL = DNSProtocolType.getServerURL(address: address) serverName = DNSProtocolType.getServerName(address: address) - DNSManager.saveResolvedDNS(server: DNSProtocolType.getServerToResolve(address: address), key: UserDefaults.Key.resolvedDNSOutsideVPN) } else { serverURL = nil serverName = nil @@ -100,8 +99,15 @@ struct SecureDNS: Codable { } func validation() -> (Bool, String?) { + // Validate DNS IP address + let servers = UserDefaults.standard.value(forKey: UserDefaults.Key.resolvedDNSOutsideVPN) as? [String] ?? [] + if servers.isEmpty { + return (false, "Please enter DNS IP address") + } + + // Validate DNS DoH/DoT URL guard let address = address, !address.isEmpty else { - return (false, "Please enter DNS server info") + return (false, "Please enter DNS DoH/DoT URL") } return (true, nil) diff --git a/IVPNClient/Scenes/Base.lproj/Main.storyboard b/IVPNClient/Scenes/Base.lproj/Main.storyboard index a92ca6e70..411d67420 100644 --- a/IVPNClient/Scenes/Base.lproj/Main.storyboard +++ b/IVPNClient/Scenes/Base.lproj/Main.storyboard @@ -1,9 +1,9 @@ - + - + @@ -21,7 +21,7 @@ - + - - - diff --git a/IVPNClient/Scenes/SecureDNS/SecureDNSViewController.swift b/IVPNClient/Scenes/SecureDNS/SecureDNSViewController.swift index b343e5348..241634f81 100644 --- a/IVPNClient/Scenes/SecureDNS/SecureDNSViewController.swift +++ b/IVPNClient/Scenes/SecureDNS/SecureDNSViewController.swift @@ -73,7 +73,6 @@ class SecureDNSViewController: UITableViewController { super.viewDidLoad() tableView.backgroundColor = UIColor.init(named: Theme.ivpnBackgroundQuaternary) secureDNSView.setupView(model: model) - addObservers() hideKeyboardOnTap() } @@ -86,16 +85,12 @@ class SecureDNSViewController: UITableViewController { @objc func saveTapped() { saveAddress() + saveURL() view.endEditing(true) } // MARK: - Private methods - - private func addObservers() { - NotificationCenter.default.addObserver(self, selector: #selector(updateDNSProfile), name: Notification.Name.UpdateResolvedDNS, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(resolvedDNSError), name: Notification.Name.ResolvedDNSError, object: nil) - } - private func saveDNSProfile() { let validation = model.validation() @@ -141,29 +136,25 @@ class SecureDNSViewController: UITableViewController { } private func saveAddress() { - guard var server = secureDNSView.serverField.text else { + guard let address = secureDNSView.serverIPField.text else { return } - server = DNSProtocolType.sanitizeServer(address: server) - model.address = server + UserDefaults.standard.set(address.commaSeparatedToArray(), forKey: UserDefaults.Key.resolvedDNSOutsideVPN) - if server.isEmpty { - UserDefaults.standard.set([], forKey: UserDefaults.Key.resolvedDNSOutsideVPN) + if address.isEmpty { removeDNSProfile() secureDNSView.enableSwitch.setOn(false, animated: true) } - - secureDNSView.setupView(model: model) } - @objc private func resolvedDNSError() { - secureDNSView.serverField.text = "" - model.address = "" - removeDNSProfile() - secureDNSView.enableSwitch.setOn(false, animated: true) - secureDNSView.setupView(model: model) - showResolvedDNSError() + private func saveURL() { + guard var server = secureDNSView.serverField.text else { + return + } + + server = DNSProtocolType.sanitizeServer(address: server) + model.address = server } } @@ -175,11 +166,11 @@ extension SecureDNSViewController { override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { let type = DNSProtocolType.init(rawValue: model.type) - if type == .dot && indexPath.section == 1 && indexPath.row == 2 { + if type == .dot && indexPath.section == 2 && indexPath.row == 1 { return 0 } - if type == .doh && indexPath.section == 1 && indexPath.row == 3 { + if type == .doh && indexPath.section == 2 && indexPath.row == 2 { return 0 } @@ -209,11 +200,16 @@ extension SecureDNSViewController { extension SecureDNSViewController: UITextFieldDelegate { func textFieldShouldReturn(_ textField: UITextField) -> Bool { - if textField == secureDNSView.serverField { + if textField == secureDNSView.serverIPField { textField.resignFirstResponder() saveAddress() } + if textField == secureDNSView.serverField { + textField.resignFirstResponder() + saveURL() + } + return true } diff --git a/IVPNClient/Scenes/SecureDNS/View/SecureDNSView.swift b/IVPNClient/Scenes/SecureDNS/View/SecureDNSView.swift index a24402a3a..f3b7ab903 100644 --- a/IVPNClient/Scenes/SecureDNS/View/SecureDNSView.swift +++ b/IVPNClient/Scenes/SecureDNS/View/SecureDNSView.swift @@ -27,8 +27,8 @@ class SecureDNSView: UITableView { // MARK: - @IBOutlets - @IBOutlet weak var enableSwitch: UISwitch! + @IBOutlet weak var serverIPField: UITextField! @IBOutlet weak var serverField: UITextField! - @IBOutlet weak var resolvedIPLabel: UILabel! @IBOutlet weak var serverURLLabel: UILabel! @IBOutlet weak var serverNameLabel: UILabel! @IBOutlet weak var typeControl: UISegmentedControl! @@ -45,6 +45,8 @@ class SecureDNSView: UITableView { func setupView(model: SecureDNS) { let type = DNSProtocolType.init(rawValue: model.type) + let servers = UserDefaults.standard.value(forKey: UserDefaults.Key.resolvedDNSOutsideVPN) as? [String] ?? [] + serverIPField.text = servers.joined(separator: ",") serverField.text = model.address serverURLLabel.text = model.serverURL serverNameLabel.text = model.serverName @@ -52,7 +54,6 @@ class SecureDNSView: UITableView { mobileNetworkSwitch.isOn = model.mobileNetwork wifiNetworkSwitch.isOn = model.wifiNetwork updateEnableSwitch() - updateResolvedDNS() } @objc func updateEnableSwitch() { @@ -61,17 +62,10 @@ class SecureDNSView: UITableView { } } - @objc func updateResolvedDNS() { - let resolvedDNS = UserDefaults.standard.value(forKey: UserDefaults.Key.resolvedDNSOutsideVPN) as? [String] - ?? [] - resolvedIPLabel.text = resolvedDNS.map { String($0) }.joined(separator: ",") - } - // MARK: - Observers - private func addObservers() { NotificationCenter.default.addObserver(self, selector: #selector(updateEnableSwitch), name: UIScene.didActivateNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(updateResolvedDNS), name: Notification.Name.UpdateResolvedDNS, object: nil) } } diff --git a/IVPNClient/Scenes/TableCells/ProtocolTableViewCell.swift b/IVPNClient/Scenes/TableCells/ProtocolTableViewCell.swift index 9d09a37c0..a56867b20 100644 --- a/IVPNClient/Scenes/TableCells/ProtocolTableViewCell.swift +++ b/IVPNClient/Scenes/TableCells/ProtocolTableViewCell.swift @@ -70,6 +70,8 @@ class ProtocolTableViewCell: UITableViewCell { } else if connectionProtocol == .openvpn(.udp, 0) || connectionProtocol == .wireguard(.udp, 0) { setupSelectAction(title: protocolLabelText) } else if connectionProtocol == .wireguard(.udp, 2) { + setupAction(title: "Obfuscation") + } else if connectionProtocol == .wireguard(.udp, 3) { setupAction(title: "WireGuard details") } else { updateLabel(title: title, isChecked: isChecked) diff --git a/IVPNClient/Scenes/ViewControllers/AdvancedViewController.swift b/IVPNClient/Scenes/ViewControllers/AdvancedViewController.swift index a07397e0b..84c2f1c0d 100644 --- a/IVPNClient/Scenes/ViewControllers/AdvancedViewController.swift +++ b/IVPNClient/Scenes/ViewControllers/AdvancedViewController.swift @@ -37,14 +37,6 @@ class AdvancedViewController: UITableViewController { @IBOutlet weak var loggingSwitch: UISwitch! @IBOutlet weak var loggingCell: UITableViewCell! @IBOutlet weak var sendLogsLabel: UILabel! - @IBOutlet weak var v2raySwitch: UISwitch! - @IBOutlet weak var v2rayProtocolControl: UISegmentedControl! - - // MARK: - Properties - - - var protocolType: String { - return v2rayProtocolControl.selectedSegmentIndex == 1 ? "tcp" : "udp" - } // MARK: - @IBActions - @@ -60,34 +52,6 @@ class AdvancedViewController: UITableViewController { evaluateReconnect(sender: sender as UIView) } - @IBAction func toggleV2ray(_ sender: UISwitch) { - if sender.isOn && Application.shared.settings.connectionProtocol.tunnelType() != .wireguard { - showAlert(title: "OpenVPN and IKEv2 not supported", message: "V2Ray is supported only for WireGuard protocol.") { _ in - sender.setOn(false, animated: true) - } - return - } - - if !sender.isOn { - Application.shared.settings.connectionProtocol = Config.defaultProtocol - } - - UserDefaults.shared.set(sender.isOn, forKey: UserDefaults.Key.isV2ray) - evaluateReconnect(sender: sender as UIView) - WidgetCenter.shared.reloadTimelines(ofKind: "IVPNWidget") - } - - @IBAction func selectV2rayProtocol(_ sender: UISegmentedControl) { - let v2rayProtocol = sender.selectedSegmentIndex == 1 ? "tcp" : "udp" - UserDefaults.shared.set(v2rayProtocol, forKey: UserDefaults.Key.v2rayProtocol) - - if UserDefaults.shared.isV2ray { - Application.shared.settings.connectionProtocol = Config.defaultProtocol - evaluateReconnect(sender: sender as UIView) - WidgetCenter.shared.reloadTimelines(ofKind: "IVPNWidget") - } - } - @IBAction func toggleAskToReconnect(_ sender: UISwitch) { UserDefaults.shared.set(!sender.isOn, forKey: UserDefaults.Key.notAskToReconnect) } @@ -127,8 +91,6 @@ class AdvancedViewController: UITableViewController { preventSameCountryMultiHopSwitch.setOn(UserDefaults.standard.preventSameCountryMultiHop, animated: false) preventSameISPMultiHopSwitch.setOn(UserDefaults.standard.preventSameISPMultiHop, animated: false) loggingSwitch.setOn(UserDefaults.shared.isLogging, animated: false) - v2raySwitch.setOn(UserDefaults.shared.isV2ray, animated: false) - v2rayProtocolControl.selectedSegmentIndex = UserDefaults.shared.v2rayProtocol == "tcp" ? 1 : 0 setupLoggingView() } @@ -224,7 +186,7 @@ class AdvancedViewController: UITableViewController { extension AdvancedViewController { override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - if indexPath.section == 3 && indexPath.row == 0 { + if indexPath.section == 2 && indexPath.row == 0 { return 60 } @@ -232,7 +194,7 @@ extension AdvancedViewController { } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - if indexPath.section == 3 && indexPath.row == 1 { + if indexPath.section == 2 && indexPath.row == 1 { tableView.deselectRow(at: indexPath, animated: true) sendLogs() } @@ -242,14 +204,7 @@ extension AdvancedViewController { let footer = view as! UITableViewHeaderFooterView footer.textLabel?.textColor = UIColor.init(named: Theme.ivpnLabel6) - var urlString = "" - switch section { - case 1: - urlString = "https://www.ivpn.net/knowledgebase/ios/v2ray/" - default: - urlString = "https://www.ivpn.net/knowledgebase/ios/known-issues-with-native-ios-kill-switch/" - } - + let urlString = "https://www.ivpn.net/knowledgebase/ios/known-issues-with-native-ios-kill-switch/" let label = ActiveLabel(frame: .zero) let customType = ActiveType.custom(pattern: "Learn more") label.numberOfLines = 0 diff --git a/IVPNClient/Scenes/ViewControllers/CustomDNSViewController.swift b/IVPNClient/Scenes/ViewControllers/CustomDNSViewController.swift index 2fcbe5a3d..5f006e739 100644 --- a/IVPNClient/Scenes/ViewControllers/CustomDNSViewController.swift +++ b/IVPNClient/Scenes/ViewControllers/CustomDNSViewController.swift @@ -26,9 +26,9 @@ import UIKit class CustomDNSViewController: UITableViewController { @IBOutlet weak var customDNSSwitch: UISwitch! + @IBOutlet weak var customDNSIPTextField: UITextField! @IBOutlet weak var customDNSTextField: UITextField! @IBOutlet weak var secureDNSSwitch: UISwitch! - @IBOutlet weak var resolvedIPLabel: UILabel! @IBOutlet weak var serverURLLabel: UILabel! @IBOutlet weak var serverNameLabel: UILabel! @IBOutlet weak var typeControl: UISegmentedControl! @@ -43,7 +43,7 @@ class CustomDNSViewController: UITableViewController { return } - guard let server = customDNSTextField.text, !server.isEmpty else { + guard let server = customDNSIPTextField.text, !server.isEmpty else { showAlert(title: "", message: "Please enter DNS server info") { _ in sender.setOn(false, animated: true) } @@ -88,31 +88,27 @@ class CustomDNSViewController: UITableViewController { // MARK: - Methods - @objc func cancelTapped() { + customDNSIPTextField.text = UserDefaults.shared.resolvedDNSInsideVPN.joined(separator: ",") customDNSTextField.text = UserDefaults.shared.customDNS view.endEditing(true) } @objc func saveTapped() { saveAddress() + saveURL() view.endEditing(true) } func saveAddress() { - guard var server = customDNSTextField.text else { + guard let address = customDNSIPTextField.text else { return } - server = DNSProtocolType.sanitizeServer(address: server) - customDNSTextField.text = server + UserDefaults.shared.set(address.commaSeparatedToArray(), forKey: UserDefaults.Key.resolvedDNSInsideVPN) - let serverToResolve = DNSProtocolType.getServerToResolve(address: server) - DNSManager.saveResolvedDNS(server: serverToResolve, key: UserDefaults.Key.resolvedDNSInsideVPN) - - UserDefaults.shared.set(server, forKey: UserDefaults.Key.customDNS) - - if server.isEmpty { + if address.isEmpty { UserDefaults.shared.set(false, forKey: UserDefaults.Key.isCustomDNS) - UserDefaults.shared.set([String](), forKey: UserDefaults.Key.resolvedDNSInsideVPN) + UserDefaults.shared.set("", forKey: UserDefaults.Key.customDNS) customDNSSwitch.setOn(false, animated: true) } @@ -123,17 +119,26 @@ class CustomDNSViewController: UITableViewController { } } - @objc func updateResolvedDNS() { - let resolvedDNS = UserDefaults.shared.value(forKey: UserDefaults.Key.resolvedDNSInsideVPN) as? [String] - ?? [] - resolvedIPLabel.text = resolvedDNS.map { String($0) }.joined(separator: ",") + func saveURL() { + guard var server = customDNSTextField.text else { + return + } + + server = DNSProtocolType.sanitizeServer(address: server) + customDNSTextField.text = server + + UserDefaults.shared.set(server, forKey: UserDefaults.Key.customDNS) + + setupView() + + if UserDefaults.shared.isCustomDNS { + evaluateReconnect(sender: customDNSTextField) + } } // MARK: - Private methods - private func addObservers() { - NotificationCenter.default.addObserver(self, selector: #selector(updateResolvedDNS), name: Notification.Name.UpdateResolvedDNSInsideVPN, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(resolvedDNSError), name: Notification.Name.ResolvedDNSError, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(setupView), name: Notification.Name.CustomDNSUpdated, object: nil) } @@ -142,6 +147,7 @@ class CustomDNSViewController: UITableViewController { let customDNS = UserDefaults.shared.customDNS tableView.backgroundColor = UIColor.init(named: Theme.ivpnBackgroundQuaternary) customDNSSwitch.isOn = UserDefaults.shared.isCustomDNS + customDNSIPTextField.text = UserDefaults.shared.resolvedDNSInsideVPN.joined(separator: ",") customDNSTextField.text = customDNS customDNSTextField.delegate = self serverURLLabel.text = DNSProtocolType.getServerURL(address: customDNS) @@ -149,16 +155,6 @@ class CustomDNSViewController: UITableViewController { secureDNSSwitch.isOn = preferred != .plain typeControl.isEnabled = preferred != .plain typeControl.selectedSegmentIndex = preferred == .dot ? 1 : 0 - updateResolvedDNS() - } - - @objc private func resolvedDNSError() { - customDNSTextField.text = "" - UserDefaults.shared.set("", forKey: UserDefaults.Key.customDNS) - UserDefaults.shared.set(false, forKey: UserDefaults.Key.isCustomDNS) - customDNSSwitch.setOn(false, animated: true) - setupView() - showResolvedDNSError() } } @@ -167,23 +163,30 @@ class CustomDNSViewController: UITableViewController { extension CustomDNSViewController { + override func numberOfSections(in tableView: UITableView) -> Int { + let type = DNSProtocolType.preferredSettings() + if type == .plain { + return 3 + } + + return 4 + } + override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - if indexPath.section == 1 && indexPath.row > 0 { + if indexPath.section == 3 { let type = DNSProtocolType.preferredSettings() - if type != .doh && indexPath.row == 3 { + if type != .doh && indexPath.row == 1 { return 0 } - if type != .dot && indexPath.row == 4 { + if type != .dot && indexPath.row == 2 { return 0 } - if type == .plain && indexPath.row == 5 { + if type == .plain { return 0 } - - return UITableView.automaticDimension } return UITableView.automaticDimension @@ -212,11 +215,16 @@ extension CustomDNSViewController { extension CustomDNSViewController: UITextFieldDelegate { func textFieldShouldReturn(_ textField: UITextField) -> Bool { - if textField == customDNSTextField { + if textField == customDNSIPTextField { textField.resignFirstResponder() saveAddress() } + if textField == customDNSTextField { + textField.resignFirstResponder() + saveURL() + } + return true } @@ -230,6 +238,7 @@ extension CustomDNSViewController: UITextFieldDelegate { navigationItem.leftBarButtonItem = nil navigationItem.rightBarButtonItem = nil DispatchQueue.async { + self.customDNSIPTextField.text = UserDefaults.shared.resolvedDNSInsideVPN.joined(separator: ",") self.customDNSTextField.text = UserDefaults.shared.customDNS } } diff --git a/IVPNClient/Scenes/ViewControllers/ObfuscationViewController.swift b/IVPNClient/Scenes/ViewControllers/ObfuscationViewController.swift new file mode 100644 index 000000000..9c5516850 --- /dev/null +++ b/IVPNClient/Scenes/ViewControllers/ObfuscationViewController.swift @@ -0,0 +1,130 @@ +// +// ObfuscationViewController.swift +// IVPN iOS app +// https://github.com/ivpn/ios-app +// +// Created by Juraj Hilje on 2025-10-23. +// Copyright (c) 2025 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 UIKit +import ActiveLabel +import WidgetKit + +class ObfuscationViewController: UITableViewController { + + // MARK: - @IBOutlets - + + @IBOutlet weak var v2raySwitch: UISwitch! + @IBOutlet weak var v2rayProtocolControl: UISegmentedControl! + + // MARK: - Properties - + + var protocolType: String { + return v2rayProtocolControl.selectedSegmentIndex == 1 ? "tcp" : "udp" + } + + // MARK: - @IBActions - + + @IBAction func toggleV2ray(_ sender: UISwitch) { + if sender.isOn && Application.shared.settings.connectionProtocol.tunnelType() != .wireguard { + showAlert(title: "OpenVPN and IKEv2 not supported", message: "V2Ray is supported only for WireGuard protocol.") { _ in + sender.setOn(false, animated: true) + } + return + } + + if !sender.isOn { + Application.shared.settings.connectionProtocol = Config.defaultProtocol + } + + UserDefaults.shared.set(sender.isOn, forKey: UserDefaults.Key.isV2ray) + setupView() + evaluateReconnect(sender: sender as UIView) + WidgetCenter.shared.reloadTimelines(ofKind: "IVPNWidget") + } + + @IBAction func selectV2rayProtocol(_ sender: UISegmentedControl) { + let v2rayProtocol = sender.selectedSegmentIndex == 1 ? "tcp" : "udp" + UserDefaults.shared.set(v2rayProtocol, forKey: UserDefaults.Key.v2rayProtocol) + + if UserDefaults.shared.isV2ray { + Application.shared.settings.connectionProtocol = Config.defaultProtocol + evaluateReconnect(sender: sender as UIView) + WidgetCenter.shared.reloadTimelines(ofKind: "IVPNWidget") + } + } + + // MARK: - View Lifecycle - + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + setupView() + } + + // MARK: - Methods - + + private func setupView() { + tableView.backgroundColor = UIColor.init(named: Theme.ivpnBackgroundQuaternary) + v2raySwitch.setOn(UserDefaults.shared.isV2ray, animated: false) + v2rayProtocolControl.selectedSegmentIndex = UserDefaults.shared.v2rayProtocol == "tcp" ? 1 : 0 + v2rayProtocolControl.isEnabled = UserDefaults.shared.isV2ray + } + +} + +// MARK: - UITableViewDelegate - + +extension ObfuscationViewController { + + override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return UITableView.automaticDimension + } + + override func tableView(_ tableView: UITableView, willDisplayFooterView view: UIView, forSection section: Int) { + let footer = view as! UITableViewHeaderFooterView + footer.textLabel?.textColor = UIColor.init(named: Theme.ivpnLabel6) + + let urlString = "https://www.ivpn.net/knowledgebase/ios/v2ray/" + let label = ActiveLabel(frame: .zero) + let customType = ActiveType.custom(pattern: "Learn more") + label.numberOfLines = 0 + label.font = UIFont.systemFont(ofSize: 13) + label.enabledTypes = [customType] + label.text = footer.textLabel?.text + label.textColor = UIColor.init(named: Theme.ivpnLabel6) + label.customColor[customType] = UIColor.init(named: Theme.ivpnBlue) + label.handleCustomTap(for: customType) { _ in + self.openWebPage(urlString) + } + footer.addSubview(label) + footer.textLabel?.text = "" + label.bindFrameToSuperviewBounds(leading: 16, trailing: -16) + } + + override func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { + if let header = view as? UITableViewHeaderFooterView { + header.textLabel?.textColor = UIColor.init(named: Theme.ivpnLabel6) + } + } + + override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + cell.backgroundColor = UIColor.init(named: Theme.ivpnBackgroundPrimary) + } + +} diff --git a/IVPNClient/Scenes/ViewControllers/ProtocolViewController.swift b/IVPNClient/Scenes/ViewControllers/ProtocolViewController.swift index 53b0d28e7..ede8bdebf 100644 --- a/IVPNClient/Scenes/ViewControllers/ProtocolViewController.swift +++ b/IVPNClient/Scenes/ViewControllers/ProtocolViewController.swift @@ -70,9 +70,9 @@ class ProtocolViewController: UITableViewController { if connectionProtocol.tunnelType() == .wireguard { if UserDefaults.shared.isMultiHop && !UserDefaults.shared.isV2ray { - collection.append([.wireguard(.udp, 1), .wireguard(.udp, 2)]) + collection.append([.wireguard(.udp, 1), .wireguard(.udp, 2), .wireguard(.udp, 3)]) } else { - collection.append([.wireguard(.udp, 0), .wireguard(.udp, 1), .wireguard(.udp, 2)]) + collection.append([.wireguard(.udp, 0), .wireguard(.udp, 1), .wireguard(.udp, 2), .wireguard(.udp, 3)]) } } @@ -274,6 +274,11 @@ extension ProtocolViewController { } if connectionProtocol == .wireguard(.udp, 2) { + performSegue(withIdentifier: "WireGuardObfuscation", sender: self) + return + } + + if connectionProtocol == .wireguard(.udp, 3) { performSegue(withIdentifier: "WireGuardSettings", sender: self) return } diff --git a/wireguard-tunnel-provider/PacketTunnelProvider.swift b/wireguard-tunnel-provider/PacketTunnelProvider.swift index b1709287a..a643917be 100644 --- a/wireguard-tunnel-provider/PacketTunnelProvider.swift +++ b/wireguard-tunnel-provider/PacketTunnelProvider.swift @@ -109,13 +109,13 @@ class PacketTunnelProvider: NEPacketTunnelProvider { } guard let addresses = UserDefaults.shared.isIPv6 ? KeyChain.wgIpAddresses : KeyChain.wgIpAddress, let wgPrivateKey = KeyChain.wgPrivateKey else { - tunnelSetupFailed() + tunnelSetupFailed(reason: "Cannot read WGIpAddressKey or WGPrivateKey from KeyChain") completionHandler(PacketTunnelProviderError.couldNotStartBackend) return } guard let tunnelSettings = getTunnelSettings(ipAddress: addresses) else { - tunnelSetupFailed() + tunnelSetupFailed(reason: "getTunnelSettings() error") completionHandler(PacketTunnelProviderError.couldNotStartBackend) return } @@ -125,7 +125,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { networkMonitor!.start(queue: DispatchQueue(label: "NetworkMonitor")) guard let privateKeyHex = wgPrivateKey.base64KeyToHex() else { - tunnelSetupFailed() + tunnelSetupFailed(reason: "base64KeyToHex() error") completionHandler(PacketTunnelProviderError.couldNotStartBackend) return } @@ -134,7 +134,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { let handle = wgTurnOn(settings, tunnelFileDescriptor ?? 0) guard handle >= 0 else { - tunnelSetupFailed() + tunnelSetupFailed(reason: "handle = wgTurnOn error") completionHandler(PacketTunnelProviderError.couldNotStartBackend) return } @@ -150,7 +150,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { setTunnelNetworkSettings(tunnelSettings) { error in if error != nil { - self.tunnelSetupFailed() + self.tunnelSetupFailed(reason: "setTunnelNetworkSettings() error: \(error.debugDescription)") completionHandler(PacketTunnelProviderError.couldNotStartBackend) } else { WidgetCenter.shared.reloadTimelines(ofKind: "IVPNWidget") @@ -180,8 +180,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider { networkMonitor?.cancel() } - private func tunnelSetupFailed() { - wg_log(.error, message: "Tunnel setup failed") + private func tunnelSetupFailed(reason: String) { + wg_log(.error, message: "Tunnel setup failed: \(reason)") UserDefaults.shared.set(".tunnelSetupFailed", forKey: UserDefaults.Key.wireguardTunnelProviderError) UserDefaults.shared.synchronize() }