From c6fbdca342de6edd4040bf75e5eabedc93d51e4f Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Mon, 29 Apr 2019 18:13:49 +0200 Subject: [PATCH] Move network settings to ad-hoc screen Can now set gateway/DNS/proxy settings explicitly. - Read from .ovpn (hosts only) - Pull from server (PUSH_REPLY) - Set manually --- Passepartout-iOS/Base.lproj/Main.storyboard | 62 ++ Passepartout-iOS/Global/SwiftGen+Segues.swift | 1 + .../Scenes/ConfigurationViewController.swift | 2 +- .../NetworkSettingsViewController.swift | 613 ++++++++++++++++++ .../Scenes/ServiceViewController.swift | 20 +- Passepartout.xcodeproj/project.pbxproj | 8 + .../Resources/en.lproj/Localizable.strings | 9 + .../Sources/Model/ConnectionProfile.swift | 4 + .../Sources/Model/ConnectionService.swift | 15 +- .../Model/ProfileNetworkSettings.swift | 168 +++++ .../Profiles/HostConnectionProfile.swift | 4 + .../PlaceholderConnectionProfile.swift | 4 + .../Profiles/ProviderConnectionProfile.swift | 4 + Passepartout/Sources/SwiftGen+Strings.swift | 35 + 14 files changed, 945 insertions(+), 4 deletions(-) create mode 100644 Passepartout-iOS/Scenes/NetworkSettingsViewController.swift create mode 100644 Passepartout/Sources/Model/ProfileNetworkSettings.swift diff --git a/Passepartout-iOS/Base.lproj/Main.storyboard b/Passepartout-iOS/Base.lproj/Main.storyboard index f9056474..9d7f80c9 100644 --- a/Passepartout-iOS/Base.lproj/Main.storyboard +++ b/Passepartout-iOS/Base.lproj/Main.storyboard @@ -233,6 +233,7 @@ + @@ -384,6 +385,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Passepartout-iOS/Global/SwiftGen+Segues.swift b/Passepartout-iOS/Global/SwiftGen+Segues.swift index a7163d8a..eea1a797 100644 --- a/Passepartout-iOS/Global/SwiftGen+Segues.swift +++ b/Passepartout-iOS/Global/SwiftGen+Segues.swift @@ -21,6 +21,7 @@ internal enum StoryboardSegue { case debugLogSegueIdentifier = "DebugLogSegueIdentifier" case endpointSegueIdentifier = "EndpointSegueIdentifier" case hostParametersSegueIdentifier = "HostParametersSegueIdentifier" + case networkSettingsSegueIdentifier = "NetworkSettingsSegueIdentifier" case providerPoolSegueIdentifier = "ProviderPoolSegueIdentifier" case providerPresetSegueIdentifier = "ProviderPresetSegueIdentifier" } diff --git a/Passepartout-iOS/Scenes/ConfigurationViewController.swift b/Passepartout-iOS/Scenes/ConfigurationViewController.swift index ec2e5316..2eb8460a 100644 --- a/Passepartout-iOS/Scenes/ConfigurationViewController.swift +++ b/Passepartout-iOS/Scenes/ConfigurationViewController.swift @@ -59,7 +59,7 @@ class ConfigurationViewController: UIViewController, TableModelHost { } model.add(.tls) model.add(.compression) - model.add(.network) +// model.add(.network) model.add(.other) // headers diff --git a/Passepartout-iOS/Scenes/NetworkSettingsViewController.swift b/Passepartout-iOS/Scenes/NetworkSettingsViewController.swift new file mode 100644 index 00000000..fc83776a --- /dev/null +++ b/Passepartout-iOS/Scenes/NetworkSettingsViewController.swift @@ -0,0 +1,613 @@ +// +// NetworkSettingsViewController.swift +// Passepartout-iOS +// +// Created by Davide De Rosa on 4/29/19. +// Copyright (c) 2019 Davide De Rosa. All rights reserved. +// +// https://github.com/passepartoutvpn +// +// This file is part of Passepartout. +// +// Passepartout 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. +// +// Passepartout 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 Passepartout. If not, see . +// + +import UIKit +import Passepartout_Core +import TunnelKit +import SwiftyBeaver + +private let log = SwiftyBeaver.self + +private enum FieldTag: Int { + case dnsDomain = 101 + + case dnsAddress = 200 + + case proxyAddress = 301 + + case proxyPort = 302 + + case proxyBypass = 400 +} + +private struct Offsets { + static let dnsAddress = 1 + + static let proxyBypass = 2 +} + +// FIXME: init networkSettings with HOST profile.sessionConfiguration +// FIXME: omit "Client" for PROVIDER + +class NetworkSettingsViewController: UITableViewController { + var profile: ConnectionProfile? + + private lazy var networkChoices: ProfileNetworkChoices = { + if let choices = profile?.networkChoices { + return choices + } + if let _ = profile as? ProviderConnectionProfile { + return ProfileNetworkChoices(choice: .server) + } + return ProfileNetworkChoices(choice: .client) + }() + + private let networkSettings = ProfileNetworkSettings() + + private lazy var clientNetworkSettings: ProfileNetworkSettings? = { + guard let hostProfile = profile as? HostConnectionProfile else { + return nil + } + return ProfileNetworkSettings(from: hostProfile.parameters.sessionConfiguration) + }() + + private var choices: [NetworkChoice] { + guard let _ = clientNetworkSettings else { + return [.server, .manual] + } + return [.client, .server, .manual] + } + + // MARK: TableModelHost + + let model: TableModel = TableModel() + + func reloadModel() { + model.clear() + + // sections + model.add(.choices) + if networkChoices.gateway != .server { + model.add(.manualGateway) + } + if networkChoices.dns != .server { + model.add(.manualDNS) + } + if networkChoices.proxy != .server { + model.add(.manualProxy) + } + + // headers + model.setHeader("", for: .choices) + model.setHeader(L10n.Configuration.Cells.DefaultGateway.caption, for: .manualGateway) + model.setHeader(L10n.Configuration.Cells.DnsServer.caption, for: .manualDNS) + model.setHeader(L10n.Configuration.Cells.ProxyHttp.caption, for: .manualProxy) + + // footers +// model.setFooter(L10n.Configuration.Sections.Reset.footer, for: .reset) + + // rows + model.set([.gateway, .dns, .proxy], in: .choices) + model.set([.gatewayAll, .gatewayIPv4, .gatewayIPv6, .gatewayNone], in: .manualGateway) + + var dnsRows: [RowType] = Array(repeating: .dnsAddress, count: networkSettings.dnsServers?.count ?? 0) + dnsRows.insert(.dnsDomain, at: 0) + if networkChoices.dns == .manual { + dnsRows.append(.dnsAddAddress) + } + model.set(dnsRows, in: .manualDNS) + + var proxyRows: [RowType] = Array(repeating: .proxyBypass, count: networkSettings.proxyBypassDomains?.count ?? 0) + proxyRows.insert(.proxyAddress, at: 0) + proxyRows.insert(.proxyPort, at: 1) + if networkChoices.proxy == .manual { + proxyRows.append(.proxyAddBypass) + } + model.set(proxyRows, in: .manualProxy) + } + + // MARK: UIViewController + + override func viewDidLoad() { + super.viewDidLoad() + + updateGateway(networkChoices.gateway) + updateDNS(networkChoices.dns) + updateProxy(networkChoices.proxy) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + reloadModel() + tableView.reloadData() + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + profile?.networkChoices = networkChoices + if networkChoices.gateway == .manual { + let settings = profile?.manualNetworkSettings ?? ProfileNetworkSettings() + settings.copyGateway(from: networkSettings) + profile?.manualNetworkSettings = settings + } + if networkChoices.dns == .manual { + let settings = profile?.manualNetworkSettings ?? ProfileNetworkSettings() + settings.copyDNS(from: networkSettings) + profile?.manualNetworkSettings = settings + } + if networkChoices.proxy == .manual { + let settings = profile?.manualNetworkSettings ?? ProfileNetworkSettings() + settings.copyProxy(from: networkSettings) + profile?.manualNetworkSettings = settings + } + } + + // MARK: Actions + + private func updateGateway(_ choice: NetworkChoice) { + networkChoices.gateway = choice + switch networkChoices.gateway { + case .client: + if let settings = clientNetworkSettings { + networkSettings.copyGateway(from: settings) + } + + case .server: + break + + case .manual: + if let settings = profile?.manualNetworkSettings { + networkSettings.copyGateway(from: settings) + } + } + } + + private func updateDNS(_ choice: NetworkChoice) { + networkChoices.dns = choice + switch networkChoices.dns { + case .client: + if let settings = clientNetworkSettings { + networkSettings.copyDNS(from: settings) + } + + case .server: + break + + case .manual: + if let settings = profile?.manualNetworkSettings { + networkSettings.copyDNS(from: settings) + } + } + } + + private func updateProxy(_ choice: NetworkChoice) { + networkChoices.proxy = choice + switch networkChoices.proxy { + case .client: + if let settings = clientNetworkSettings { + networkSettings.copyProxy(from: settings) + } + + case .server: + break + + case .manual: + if let settings = profile?.manualNetworkSettings { + networkSettings.copyProxy(from: settings) + } + } + } + + private func commitTextField(_ field: UITextField) { + + // DNS: domain, servers + // Proxy: address, port, bypass domains + + if field.tag == FieldTag.dnsDomain.rawValue { + networkSettings.dnsDomainName = field.text + } else if field.tag == FieldTag.proxyAddress.rawValue { + networkSettings.proxyAddress = field.text + } else if field.tag == FieldTag.proxyPort.rawValue { + networkSettings.proxyPort = UInt16(field.text ?? "0") + } else if field.tag >= FieldTag.dnsAddress.rawValue && field.tag < FieldTag.proxyAddress.rawValue { + let i = field.tag - FieldTag.dnsAddress.rawValue + networkSettings.dnsServers?[i] = field.text ?? "" + } else if field.tag >= FieldTag.proxyBypass.rawValue { + let i = field.tag - FieldTag.proxyBypass.rawValue + networkSettings.proxyBypassDomains?[i] = field.text ?? "" + } + + log.debug("Network settings: \(networkSettings)") + } +} + +// MARK: - + +extension NetworkSettingsViewController { + enum SectionType: Int { + case choices + + case manualGateway + + case manualDNS + + case manualProxy + } + + enum RowType: Int { + case gateway + + case dns + + case proxy + + case gatewayAll + + case gatewayIPv4 + + case gatewayIPv6 + + case gatewayNone + + case dnsDomain + + case dnsAddress + + case dnsAddAddress + + case proxyAddress + + case proxyPort + + case proxyBypass + + case proxyAddBypass + } + + override func numberOfSections(in tableView: UITableView) -> Int { + return model.count + } + + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + return model.header(for: section) + } + + override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { + return model.footer(for: section) + } + + override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + return model.headerHeight(for: section) + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return model.count(for: section) + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let row = model.row(at: indexPath) + + switch row { + case .gateway: + let cell = Cells.setting.dequeue(from: tableView, for: indexPath) + cell.leftText = model.header(for: .manualGateway) + cell.rightText = networkChoices.gateway.description + return cell + + case .dns: + let cell = Cells.setting.dequeue(from: tableView, for: indexPath) + cell.leftText = model.header(for: .manualDNS) + cell.rightText = networkChoices.dns.description + return cell + + case .proxy: + let cell = Cells.setting.dequeue(from: tableView, for: indexPath) + cell.leftText = model.header(for: .manualProxy) + cell.rightText = networkChoices.proxy.description + return cell + + case .gatewayAll, .gatewayIPv4, .gatewayIPv6, .gatewayNone: + let cell = Cells.setting.dequeue(from: tableView, for: indexPath) + var policies: [SessionProxy.RoutingPolicy]? + + switch row { + case .gatewayAll: + cell.leftText = L10n.Global.Cells.enabled + policies = [.IPv4, .IPv6] + + case .gatewayIPv4: + cell.leftText = "IPv4" + policies = [.IPv4] + + case .gatewayIPv6: + cell.leftText = "IPv6" + policies = [.IPv6] + + case .gatewayNone: + cell.leftText = L10n.Global.Cells.disabled + + default: + break + } + cell.applyChecked(networkSettings.gatewayPolicies == policies, Theme.current) + cell.isTappable = (networkChoices.gateway == .manual) + return cell + + case .dnsDomain: + let cell = Cells.field.dequeue(from: tableView, for: indexPath) + cell.caption = L10n.Configuration.Cells.DnsDomain.caption + cell.field.tag = FieldTag.dnsDomain.rawValue + cell.field.text = networkSettings.dnsDomainName + cell.field.clearButtonMode = .always + cell.field.keyboardType = .asciiCapable + cell.captionWidth = 160.0 + cell.delegate = self + if networkChoices.dns == .manual { + cell.field.isEnabled = true + cell.field.placeholder = "example.com" + } else { + cell.field.isEnabled = false + cell.field.placeholder = nil + } + return cell + + case .dnsAddress: + let i = indexPath.row - Offsets.dnsAddress + + let cell = Cells.field.dequeue(from: tableView, for: indexPath) + cell.caption = L10n.NetworkSettings.Cells.Address.caption + cell.field.tag = FieldTag.dnsAddress.rawValue + i + cell.field.text = networkSettings.dnsServers?[i] + cell.field.clearButtonMode = .always + cell.field.keyboardType = .decimalPad + cell.captionWidth = 160.0 + cell.delegate = self + if networkChoices.dns == .manual { + cell.field.isEnabled = true + cell.field.placeholder = "8.8.8.8" + } else { + cell.field.isEnabled = false + cell.field.placeholder = nil + } + return cell + + case .dnsAddAddress: + let cell = Cells.setting.dequeue(from: tableView, for: indexPath) + cell.applyAction(Theme.current) + cell.leftText = L10n.NetworkSettings.Cells.AddDnsServer.caption + return cell + + case .proxyAddress: + let cell = Cells.field.dequeue(from: tableView, for: indexPath) + cell.caption = L10n.NetworkSettings.Cells.Address.caption + cell.field.tag = FieldTag.proxyAddress.rawValue + cell.field.text = networkSettings.proxyAddress + cell.field.clearButtonMode = .always + cell.field.keyboardType = .decimalPad + cell.captionWidth = 160.0 + cell.delegate = self + if networkChoices.proxy == .manual { + cell.field.isEnabled = true + cell.field.placeholder = "192.168.1.1" + } else { + cell.field.isEnabled = false + cell.field.placeholder = nil + } + return cell + + case .proxyPort: + let cell = Cells.field.dequeue(from: tableView, for: indexPath) + cell.caption = L10n.NetworkSettings.Cells.Port.caption + cell.field.tag = FieldTag.proxyPort.rawValue + cell.field.text = networkSettings.proxyPort?.description + cell.field.clearButtonMode = .always + cell.field.keyboardType = .numberPad + cell.captionWidth = 160.0 + cell.delegate = self + if networkChoices.proxy == .manual { + cell.field.isEnabled = true + cell.field.placeholder = "8080" + } else { + cell.field.isEnabled = false + cell.field.placeholder = nil + } + return cell + + case .proxyBypass: + let i = indexPath.row - Offsets.proxyBypass + + let cell = Cells.field.dequeue(from: tableView, for: indexPath) + cell.caption = L10n.NetworkSettings.Cells.ProxyBypass.caption + cell.field.tag = FieldTag.proxyBypass.rawValue + i + cell.field.text = networkSettings.proxyBypassDomains?[i] + cell.field.clearButtonMode = .always + cell.field.keyboardType = .asciiCapable + cell.captionWidth = 160.0 + cell.delegate = self + if networkChoices.proxy == .manual { + cell.field.isEnabled = true + cell.field.placeholder = "excluded.com" + } else { + cell.field.isEnabled = false + cell.field.placeholder = nil + } + return cell + + case .proxyAddBypass: + let cell = Cells.setting.dequeue(from: tableView, for: indexPath) + cell.applyAction(Theme.current) + cell.leftText = L10n.NetworkSettings.Cells.AddProxyBypass.caption + return cell + } + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let cell = tableView.cellForRow(at: indexPath) + + switch model.row(at: indexPath) { + case .gateway: + let vc = OptionViewController() + vc.title = (cell as? SettingTableViewCell)?.leftText + vc.options = choices + vc.descriptionBlock = { $0.description } + + vc.selectedOption = networkChoices.gateway + vc.selectionBlock = { [weak self] in + self?.updateGateway($0) + self?.navigationController?.popViewController(animated: true) + } + navigationController?.pushViewController(vc, animated: true) + + case .dns: + let vc = OptionViewController() + vc.title = (cell as? SettingTableViewCell)?.leftText + vc.options = choices + vc.descriptionBlock = { $0.description } + + vc.selectedOption = networkChoices.dns + vc.selectionBlock = { [weak self] in + self?.updateDNS($0) + self?.navigationController?.popViewController(animated: true) + } + navigationController?.pushViewController(vc, animated: true) + + case .proxy: + let vc = OptionViewController() + vc.title = (cell as? SettingTableViewCell)?.leftText + vc.options = choices + vc.descriptionBlock = { $0.description } + + vc.selectedOption = networkChoices.proxy + vc.selectionBlock = { [weak self] in + self?.updateProxy($0) + self?.navigationController?.popViewController(animated: true) + } + navigationController?.pushViewController(vc, animated: true) + + case .gatewayAll: + guard networkChoices.gateway == .manual else { + return + } + networkSettings.gatewayPolicies = [.IPv4, .IPv6] + tableView.reloadData() + + case .gatewayIPv4: + guard networkChoices.gateway == .manual else { + return + } + networkSettings.gatewayPolicies = [.IPv4] + tableView.reloadData() + + case .gatewayIPv6: + guard networkChoices.gateway == .manual else { + return + } + networkSettings.gatewayPolicies = [.IPv6] + tableView.reloadData() + + case .gatewayNone: + guard networkChoices.gateway == .manual else { + return + } + networkSettings.gatewayPolicies = nil + tableView.reloadData() + + case .dnsAddAddress: + tableView.deselectRow(at: indexPath, animated: true) + + var dnsServers = networkSettings.dnsServers ?? [] + dnsServers.append("") + networkSettings.dnsServers = dnsServers + reloadModel() + tableView.insertRows(at: [indexPath], with: .automatic) + + case .proxyAddBypass: + tableView.deselectRow(at: indexPath, animated: true) + + var bypassDomains = networkSettings.proxyBypassDomains ?? [] + bypassDomains.append("") + networkSettings.proxyBypassDomains = bypassDomains + reloadModel() + tableView.insertRows(at: [indexPath], with: .automatic) + + default: + break + } + } + + override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { + switch model.row(at: indexPath) { + case .dnsAddress, .proxyBypass: + return true + + default: + return false + } + } + + override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { + switch model.row(at: indexPath) { + case .dnsAddress: + // start at row 1 + networkSettings.dnsServers?.remove(at: indexPath.row - Offsets.dnsAddress) + + case .proxyBypass: + // start at row 2 + networkSettings.proxyBypassDomains?.remove(at: indexPath.row - Offsets.proxyBypass) + + default: + break + } + + reloadModel() + tableView.deleteRows(at: [indexPath], with: .automatic) + } +} + +extension NetworkSettingsViewController: FieldTableViewCellDelegate { + func fieldCellDidEdit(_ cell: FieldTableViewCell) { + commitTextField(cell.field) + } + + func fieldCellDidEnter(_: FieldTableViewCell) { + } +} + +extension NetworkChoice: CustomStringConvertible { + public var description: String { + switch self { + case .client: + return L10n.NetworkSettings.Cells.Choice.client + + case .server: + return L10n.NetworkSettings.Cells.Choice.server + + case .manual: + return L10n.Global.Cells.manual + } + } +} diff --git a/Passepartout-iOS/Scenes/ServiceViewController.swift b/Passepartout-iOS/Scenes/ServiceViewController.swift index ad76a344..188552d5 100644 --- a/Passepartout-iOS/Scenes/ServiceViewController.swift +++ b/Passepartout-iOS/Scenes/ServiceViewController.swift @@ -174,6 +174,11 @@ class ServiceViewController: UIViewController, TableModelHost { vc?.originalConfigurationURL = service.configurationURL(for: uncheckedHostProfile) vc?.delegate = self + case .networkSettingsSegueIdentifier: + let vc = destination as? NetworkSettingsViewController + vc?.title = L10n.Service.Cells.NetworkSettings.caption + vc?.profile = profile + case .debugLogSegueIdentifier: break } @@ -580,6 +585,8 @@ extension ServiceViewController: UITableViewDataSource, UITableViewDelegate, Tog case hostParameters + case networkSettings + case vpnResolvesHostname case vpnSurvivesSleep @@ -720,6 +727,11 @@ extension ServiceViewController: UITableViewDataSource, UITableViewDelegate, Tog } return cell + case .networkSettings: + let cell = Cells.setting.dequeue(from: tableView, for: indexPath) + cell.leftText = L10n.Service.Cells.NetworkSettings.caption + return cell + // provider cells case .providerPool: @@ -908,6 +920,10 @@ extension ServiceViewController: UITableViewDataSource, UITableViewDelegate, Tog perform(segue: StoryboardSegue.Main.hostParametersSegueIdentifier, sender: cell) return true + case .networkSettings: + perform(segue: StoryboardSegue.Main.networkSettingsSegueIdentifier, sender: cell) + return true + case .trustedAddCurrentWiFi: if #available(iOS 12, *) { IntentDispatcher.donateTrustCurrentNetwork() @@ -1059,10 +1075,10 @@ extension ServiceViewController: UITableViewDataSource, UITableViewDelegate, Tog } if isProvider { model.set([.account], in: .authentication) - model.set([.providerPool, .endpoint, .providerPreset], in: .configuration) + model.set([.providerPool, .endpoint, .providerPreset, .networkSettings], in: .configuration) model.set([.providerRefresh], in: .providerInfrastructure) } else { - model.set([.account, .endpoint, .hostParameters], in: .configuration) + model.set([.account, .endpoint, .hostParameters, .networkSettings], in: .configuration) } if isActiveProfile { if isProvider { diff --git a/Passepartout.xcodeproj/project.pbxproj b/Passepartout.xcodeproj/project.pbxproj index 6779c01c..848480e2 100644 --- a/Passepartout.xcodeproj/project.pbxproj +++ b/Passepartout.xcodeproj/project.pbxproj @@ -110,6 +110,8 @@ 0EF56BBB2185AC8500B0C8AB /* SwiftGen+Segues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF56BBA2185AC8500B0C8AB /* SwiftGen+Segues.swift */; }; 0EF5CF252141CE58004FF1BD /* HUD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF5CF242141CE58004FF1BD /* HUD.swift */; }; 0EF5CF292141F31F004FF1BD /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E4FD7ED20D539A0002221FF /* Utils.swift */; }; + 0EFB901822764689006405E4 /* ProfileNetworkSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EFB901722764689006405E4 /* ProfileNetworkSettings.swift */; }; + 0EFB901A2276D7F1006405E4 /* NetworkSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EFB90192276D7F1006405E4 /* NetworkSettingsViewController.swift */; }; 0EFBFAC121AC464800887A8C /* CreditsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EFBFAC021AC464800887A8C /* CreditsViewController.swift */; }; 0EFD943E215BE10800529B64 /* IssueReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EFD943D215BE10800529B64 /* IssueReporter.swift */; }; 0EFD9440215BED8E00529B64 /* LabelViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EFD943F215BED8E00529B64 /* LabelViewController.swift */; }; @@ -286,6 +288,8 @@ 0EEB53B1225D525B00746300 /* Downloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Downloader.swift; sourceTree = ""; }; 0EF56BBA2185AC8500B0C8AB /* SwiftGen+Segues.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SwiftGen+Segues.swift"; sourceTree = ""; }; 0EF5CF242141CE58004FF1BD /* HUD.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HUD.swift; sourceTree = ""; }; + 0EFB901722764689006405E4 /* ProfileNetworkSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileNetworkSettings.swift; sourceTree = ""; }; + 0EFB90192276D7F1006405E4 /* NetworkSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkSettingsViewController.swift; sourceTree = ""; }; 0EFBFAC021AC464800887A8C /* CreditsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreditsViewController.swift; sourceTree = ""; }; 0EFD943D215BE10800529B64 /* IssueReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueReporter.swift; sourceTree = ""; }; 0EFD943F215BED8E00529B64 /* LabelViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelViewController.swift; sourceTree = ""; }; @@ -510,6 +514,7 @@ 0EC7F20420E24308004EA58E /* DebugLog.swift */, 0ED38AE621404F100004D387 /* EndpointDataSource.swift */, 0E89DFC4213DF7AE00741BA1 /* Preferences.swift */, + 0EFB901722764689006405E4 /* ProfileNetworkSettings.swift */, 0E89DFC7213E8FC500741BA1 /* SessionProxy+Communication.swift */, 0E2B494120FD16540094784C /* TransientStore.swift */, 0E4C9CB820DB9BC600A0C59C /* TrustedNetworks.swift */, @@ -610,6 +615,7 @@ 0E1D72B3213C118500BA1586 /* ConfigurationViewController.swift */, 0E6BE13E20CFBAB300A6DD36 /* DebugLogViewController.swift */, 0E158AD920E11B0B00C85A82 /* EndpointViewController.swift */, + 0EFB90192276D7F1006405E4 /* NetworkSettingsViewController.swift */, 0ED31C2B20CF2D6F0027975F /* ProviderPoolViewController.swift */, 0E1D72B1213BFFCF00BA1586 /* ProviderPresetViewController.swift */, 0E57F63D20C83FC5008323CF /* ServiceViewController.swift */, @@ -1047,6 +1053,7 @@ 0E58BD9322404EF1006FB157 /* Intents.intentdefinition in Sources */, 0E3152D3223FA05400F61841 /* EndpointDataSource.swift in Sources */, 0E3152D4223FA05400F61841 /* Preferences.swift in Sources */, + 0EFB901822764689006405E4 /* ProfileNetworkSettings.swift in Sources */, 0E3152C0223FA03D00F61841 /* Utils.swift in Sources */, 0E3152CB223FA04D00F61841 /* Pool.swift in Sources */, 0E3152C7223FA04800F61841 /* VPNStatus.swift in Sources */, @@ -1075,6 +1082,7 @@ 0E4FD7F120D58618002221FF /* Macros.swift in Sources */, 0EF5CF252141CE58004FF1BD /* HUD.swift in Sources */, 0E05C5D720D1645F006EE732 /* ToggleTableViewCell.swift in Sources */, + 0EFB901A2276D7F1006405E4 /* NetworkSettingsViewController.swift in Sources */, 0E05C5D420D1645F006EE732 /* FieldTableViewCell.swift in Sources */, 0E36D25C224034AD006AF062 /* ShortcutsConnectToViewController.swift in Sources */, 0E05C61D20D27C82006EE732 /* Theme.swift in Sources */, diff --git a/Passepartout/Resources/en.lproj/Localizable.strings b/Passepartout/Resources/en.lproj/Localizable.strings index 0a5aea00..f73c650b 100644 --- a/Passepartout/Resources/en.lproj/Localizable.strings +++ b/Passepartout/Resources/en.lproj/Localizable.strings @@ -109,6 +109,7 @@ "service.cells.provider.preset.caption" = "Preset"; "service.cells.provider.refresh.caption" = "Refresh infrastructure"; "service.cells.host.parameters.caption" = "Parameters"; +"service.cells.network_settings.caption" = "Network settings"; "service.cells.vpn_survives_sleep.caption" = "Keep alive on sleep"; "service.cells.vpn_resolves_hostname.caption" = "Resolve server hostname"; //"service.cells.vpn_prefers_udp.caption" = "Prefer UDP socket"; @@ -200,6 +201,14 @@ "configuration.cells.renegotiation_seconds.value.after" = "after %@"; "configuration.cells.random_endpoint.caption" = "Randomize endpoint"; +"network_settings.cells.choice.client" = "Read .ovpn"; +"network_settings.cells.choice.server" = "Pull from server"; +"network_settings.cells.address.caption" = "Address"; +"network_settings.cells.port.caption" = "Port"; +"network_settings.cells.add_dns_server.caption" = "Add address"; +"network_settings.cells.proxy_bypass.caption" = "Bypass domain"; +"network_settings.cells.add_proxy_bypass.caption" = "Add bypass domain"; + "debug_log.buttons.previous" = "Previous"; "debug_log.buttons.next" = "Next"; "debug_log.alerts.empty_log.message" = "The debug log is empty."; diff --git a/Passepartout/Sources/Model/ConnectionProfile.swift b/Passepartout/Sources/Model/ConnectionProfile.swift index b0569e13..55603f27 100644 --- a/Passepartout/Sources/Model/ConnectionProfile.swift +++ b/Passepartout/Sources/Model/ConnectionProfile.swift @@ -42,6 +42,10 @@ public protocol ConnectionProfile: class, EndpointDataSource, CustomStringConver var requiresCredentials: Bool { get } + var networkChoices: ProfileNetworkChoices? { get set } + + var manualNetworkSettings: ProfileNetworkSettings? { get set } + func generate(from configuration: TunnelKitProvider.Configuration, preferences: Preferences) throws -> TunnelKitProvider.Configuration func with(newId: String) -> ConnectionProfile diff --git a/Passepartout/Sources/Model/ConnectionService.swift b/Passepartout/Sources/Model/ConnectionService.swift index 1a6cac31..5afb68cf 100644 --- a/Passepartout/Sources/Model/ConnectionService.swift +++ b/Passepartout/Sources/Model/ConnectionService.swift @@ -299,6 +299,7 @@ public class ConnectionService: Codable { return nil } } + return profile } @@ -531,7 +532,19 @@ public class ConnectionService: Codable { } } - let cfg = try profile.generate(from: baseConfiguration, preferences: preferences) + var cfg = try profile.generate(from: baseConfiguration, preferences: preferences) + + // override network settings + if let choices = profile.networkChoices, let settings = profile.manualNetworkSettings { + var builder = cfg.builder() + var sessionBuilder = builder.sessionConfiguration.builder() + sessionBuilder.applyGateway(from: choices, settings: settings) + sessionBuilder.applyDNS(from: choices, settings: settings) + sessionBuilder.applyProxy(from: choices, settings: settings) + builder.sessionConfiguration = sessionBuilder.build() + cfg = builder.build() + } + let protocolConfiguration = try cfg.generatedTunnelProtocol( withBundleIdentifier: GroupConstants.App.tunnelIdentifier, appGroup: appGroup, diff --git a/Passepartout/Sources/Model/ProfileNetworkSettings.swift b/Passepartout/Sources/Model/ProfileNetworkSettings.swift new file mode 100644 index 00000000..1a72af6d --- /dev/null +++ b/Passepartout/Sources/Model/ProfileNetworkSettings.swift @@ -0,0 +1,168 @@ +// +// ProfileNetworkSettings.swift +// Passepartout +// +// Created by Davide De Rosa on 04/28/19. +// Copyright (c) 2019 Davide De Rosa. All rights reserved. +// +// https://github.com/passepartoutvpn +// +// This file is part of Passepartout. +// +// Passepartout 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. +// +// Passepartout 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 Passepartout. If not, see . +// + +import Foundation +import TunnelKit + +public enum NetworkChoice: String, Codable { + case client + + case server // erase client settings + + case manual +} + +public class ProfileNetworkChoices: Codable { + public var gateway: NetworkChoice + + public var dns: NetworkChoice + + public var proxy: NetworkChoice + + public init(choice: NetworkChoice) { + gateway = choice + dns = choice + proxy = choice + } +} + +public class ProfileNetworkSettings: Codable, CustomStringConvertible { + public var gatewayPolicies: [SessionProxy.RoutingPolicy]? + + public var dnsServers: [String]? + + public var dnsDomainName: String? + + public var proxyAddress: String? + + public var proxyPort: UInt16? + + public var proxyServer: Proxy? { + guard let address = proxyAddress, let port = proxyPort, !address.isEmpty, port > 0 else { + return nil + } + return Proxy(address, port) + } + + public var proxyBypassDomains: [String]? + + public init() { + gatewayPolicies = [.IPv4, .IPv6] + } + + public init(from configuration: SessionProxy.Configuration) { + gatewayPolicies = configuration.routingPolicies + dnsDomainName = configuration.searchDomain + dnsServers = configuration.dnsServers + proxyAddress = configuration.httpProxy?.address + proxyPort = configuration.httpProxy?.port + proxyBypassDomains = configuration.proxyBypassDomains + } + + public func copy(from settings: ProfileNetworkSettings) { + copyGateway(from: settings) + copyDNS(from: settings) + copyProxy(from: settings) + } + + public func copyGateway(from settings: ProfileNetworkSettings) { + gatewayPolicies = settings.gatewayPolicies + } + + public func copyDNS(from settings: ProfileNetworkSettings) { + dnsDomainName = settings.dnsDomainName + dnsServers = settings.dnsServers?.filter { !$0.isEmpty } + } + + public func copyProxy(from settings: ProfileNetworkSettings) { + proxyAddress = settings.proxyAddress + proxyPort = settings.proxyPort + proxyBypassDomains = settings.proxyBypassDomains?.filter { !$0.isEmpty } + } + + // MARK: CustomStringConvertible + + public var description: String { + let comps: [String] = [ + "gw: \(gatewayPolicies?.description ?? "")", + "dns: {domain: \(dnsDomainName ?? ""), servers: \(dnsServers?.description ?? "[]")}", + "proxy: {address: \(proxyAddress ?? ""), port: \(proxyPort?.description ?? ""), bypass: \(proxyBypassDomains?.description ?? "[]")}" + ] + return "{\(comps.joined(separator: ", "))}" + } +} + +extension SessionProxy.ConfigurationBuilder { + public mutating func applyGateway(from choices: ProfileNetworkChoices, settings: ProfileNetworkSettings) { + switch choices.gateway { + case .client: + break + + case .server: + routingPolicies = nil + + case .manual: + routingPolicies = settings.gatewayPolicies + } + } + + public mutating func applyDNS(from choices: ProfileNetworkChoices, settings: ProfileNetworkSettings) { + switch choices.dns { + case .client: + break + + case .server: + dnsServers = nil + searchDomain = nil + + case .manual: + dnsServers = settings.dnsServers?.filter { !$0.isEmpty } + searchDomain = settings.dnsDomainName + } + } + + public mutating func applyProxy(from choices: ProfileNetworkChoices, settings: ProfileNetworkSettings) { + switch choices.proxy { + case .client: + break + + case .server: + httpProxy = nil + httpsProxy = nil + proxyBypassDomains = nil + + case .manual: + if let proxyServer = settings.proxyServer { + httpProxy = proxyServer + httpsProxy = proxyServer + proxyBypassDomains = settings.proxyBypassDomains?.filter { !$0.isEmpty } + } else { + httpProxy = nil + httpsProxy = nil + proxyBypassDomains = nil + } + } + } +} diff --git a/Passepartout/Sources/Model/Profiles/HostConnectionProfile.swift b/Passepartout/Sources/Model/Profiles/HostConnectionProfile.swift index d7bf27ad..939f9634 100644 --- a/Passepartout/Sources/Model/Profiles/HostConnectionProfile.swift +++ b/Passepartout/Sources/Model/Profiles/HostConnectionProfile.swift @@ -54,6 +54,10 @@ public class HostConnectionProfile: ConnectionProfile, Codable, Equatable { return false } + public var networkChoices: ProfileNetworkChoices? + + public var manualNetworkSettings: ProfileNetworkSettings? + public func generate(from configuration: TunnelKitProvider.Configuration, preferences: Preferences) throws -> TunnelKitProvider.Configuration { guard let endpointProtocols = parameters.sessionConfiguration.endpointProtocols, !endpointProtocols.isEmpty else { preconditionFailure("No endpointProtocols") diff --git a/Passepartout/Sources/Model/Profiles/PlaceholderConnectionProfile.swift b/Passepartout/Sources/Model/Profiles/PlaceholderConnectionProfile.swift index 0b8e454f..43c267a1 100644 --- a/Passepartout/Sources/Model/Profiles/PlaceholderConnectionProfile.swift +++ b/Passepartout/Sources/Model/Profiles/PlaceholderConnectionProfile.swift @@ -35,6 +35,10 @@ public class PlaceholderConnectionProfile: ConnectionProfile { public var requiresCredentials: Bool = false + public var networkChoices: ProfileNetworkChoices? + + public var manualNetworkSettings: ProfileNetworkSettings? + public func generate(from configuration: TunnelKitProvider.Configuration, preferences: Preferences) throws -> TunnelKitProvider.Configuration { fatalError("Generating configuration from a PlaceholderConnectionProfile") } diff --git a/Passepartout/Sources/Model/Profiles/ProviderConnectionProfile.swift b/Passepartout/Sources/Model/Profiles/ProviderConnectionProfile.swift index ed9d7e86..b7feb08d 100644 --- a/Passepartout/Sources/Model/Profiles/ProviderConnectionProfile.swift +++ b/Passepartout/Sources/Model/Profiles/ProviderConnectionProfile.swift @@ -57,6 +57,10 @@ public class ProviderConnectionProfile: ConnectionProfile, Codable, Equatable { public var manualProtocol: EndpointProtocol? + public var networkChoices: ProfileNetworkChoices? + + public var manualNetworkSettings: ProfileNetworkSettings? + public var usesProviderEndpoint: Bool { return (manualAddress != nil) || (manualProtocol != nil) } diff --git a/Passepartout/Sources/SwiftGen+Strings.swift b/Passepartout/Sources/SwiftGen+Strings.swift index 17e972d8..f6cf9903 100644 --- a/Passepartout/Sources/SwiftGen+Strings.swift +++ b/Passepartout/Sources/SwiftGen+Strings.swift @@ -417,6 +417,37 @@ public enum L10n { } } + public enum NetworkSettings { + public enum Cells { + public enum AddDnsServer { + /// Add address + public static let caption = L10n.tr("Localizable", "network_settings.cells.add_dns_server.caption") + } + public enum AddProxyBypass { + /// Add bypass domain + public static let caption = L10n.tr("Localizable", "network_settings.cells.add_proxy_bypass.caption") + } + public enum Address { + /// Address + public static let caption = L10n.tr("Localizable", "network_settings.cells.address.caption") + } + public enum Choice { + /// Read .ovpn + public static let client = L10n.tr("Localizable", "network_settings.cells.choice.client") + /// Pull from server + public static let server = L10n.tr("Localizable", "network_settings.cells.choice.server") + } + public enum Port { + /// Port + public static let caption = L10n.tr("Localizable", "network_settings.cells.port.caption") + } + public enum ProxyBypass { + /// Bypass domain + public static let caption = L10n.tr("Localizable", "network_settings.cells.proxy_bypass.caption") + } + } + } + public enum Organizer { public enum Alerts { public enum AddHost { @@ -699,6 +730,10 @@ public enum L10n { /// Mask network data public static let caption = L10n.tr("Localizable", "service.cells.masks_private_data.caption") } + public enum NetworkSettings { + /// Network settings + public static let caption = L10n.tr("Localizable", "service.cells.network_settings.caption") + } public enum Provider { public enum Pool { /// Location