diff --git a/Passepartout/App/iOS/CHANGELOG.md b/Passepartout/App/iOS/CHANGELOG.md index 9143adcd..201973c1 100644 --- a/Passepartout/App/iOS/CHANGELOG.md +++ b/Passepartout/App/iOS/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Support `--data-ciphers` from OpenVPN 2.5 [tunnelkit#193](https://github.com/passepartoutvpn/tunnelkit/issues/193) +- Support DNS over HTTPS/TLS in "Network settings". [#91](https://github.com/passepartoutvpn/passepartout-apple/issues/91) ### Changed diff --git a/Passepartout/App/iOS/Scenes/NetworkSettingsViewController.swift b/Passepartout/App/iOS/Scenes/NetworkSettingsViewController.swift index 9806eb1e..1328951c 100644 --- a/Passepartout/App/iOS/Scenes/NetworkSettingsViewController.swift +++ b/Passepartout/App/iOS/Scenes/NetworkSettingsViewController.swift @@ -32,6 +32,8 @@ import Convenience private let log = SwiftyBeaver.self private enum FieldTag: Int { + case dnsCustom = 50 + case dnsAddress = 100 case dnsDomain = 200 @@ -76,7 +78,14 @@ class NetworkSettingsViewController: UITableViewController { sections.append(.manualGateway) } if networkChoices.dns != .server { - sections.append(.manualDNSServers) + sections.append(.manualDNSProtocol) + switch networkSettings.dnsProtocol { + case .https, .tls: + break + + default: + sections.append(.manualDNSServers) + } sections.append(.manualDNSDomains) } if networkChoices.proxy != .server { @@ -100,6 +109,16 @@ class NetworkSettingsViewController: UITableViewController { model.set([.gatewayIPv4, .gatewayIPv6], forSection: .manualGateway) model.set([.mtuBytes], forSection: .manualMTU) + var dnsProtocolRows: [RowType] = [.dnsProtocol] + switch networkSettings.dnsProtocol { + case .https, .tls: + dnsProtocolRows.append(.dnsCustom) + + default: + break + } + model.set(dnsProtocolRows, forSection: .manualDNSProtocol) + var dnsServers: [RowType] = Array(repeating: .dnsAddress, count: networkSettings.dnsServers?.count ?? 0) if networkChoices.dns == .manual { dnsServers.append(.dnsAddAddress) @@ -111,7 +130,7 @@ class NetworkSettingsViewController: UITableViewController { dnsDomains.append(.dnsAddDomain) } model.set(dnsDomains, forSection: .manualDNSDomains) - + var proxyRows: [RowType] = Array(repeating: .proxyBypass, count: networkSettings.proxyBypassDomains?.count ?? 0) proxyRows.insert(.proxyAddress, at: 0) proxyRows.insert(.proxyPort, at: 1) @@ -122,11 +141,10 @@ class NetworkSettingsViewController: UITableViewController { model.set(proxyRows, forSection: .manualProxy) // refine sections before add (DNS is tricky) + model.setHeader(L10n.Core.NetworkSettings.Dns.title, forSection: .manualDNSProtocol) if !dnsServers.isEmpty { - model.setHeader(L10n.Core.NetworkSettings.Dns.title, forSection: .manualDNSServers) } else if !dnsDomains.isEmpty { sections.removeAll { $0 == .manualDNSServers } - model.setHeader(L10n.Core.NetworkSettings.Dns.title, forSection: .manualDNSDomains) } else { sections.removeAll { $0 == .manualDNSServers } sections.removeAll { $0 == .manualDNSDomains } @@ -241,7 +259,21 @@ class NetworkSettingsViewController: UITableViewController { let text = field.text ?? "" - if field.tag >= FieldTag.dnsAddress.rawValue && field.tag < FieldTag.dnsDomain.rawValue { + if field.tag == FieldTag.dnsCustom.rawValue { + switch networkSettings.dnsProtocol { + case .https: + guard let string = field.text, let url = URL(string: string) else { + break + } + networkSettings.dnsHTTPSURL = url + + case .tls: + networkSettings.dnsTLSServerName = field.text + + default: + break + } + } else if field.tag >= FieldTag.dnsAddress.rawValue && field.tag < FieldTag.dnsDomain.rawValue { let i = field.tag - FieldTag.dnsAddress.rawValue if let _ = networkSettings.dnsServers { networkSettings.dnsServers?[i] = text @@ -304,6 +336,8 @@ extension NetworkSettingsViewController { case manualGateway + case manualDNSProtocol + case manualDNSServers case manualDNSDomains @@ -326,6 +360,10 @@ extension NetworkSettingsViewController { case gatewayIPv6 + case dnsProtocol + + case dnsCustom + case dnsAddress case dnsAddAddress @@ -409,6 +447,34 @@ extension NetworkSettingsViewController { cell.isOn = networkSettings.gatewayPolicies?.contains(.IPv6) ?? false return cell + case .dnsProtocol: + let cell = Cells.setting.dequeue(from: tableView, for: indexPath) + cell.leftText = L10n.Core.Global.Captions.protocol + cell.rightText = (networkSettings.dnsProtocol ?? .fallback)?.description + return cell + + case .dnsCustom: + let cell = Cells.field.dequeue(from: tableView, for: indexPath) + cell.caption = nil + cell.field.tag = FieldTag.dnsCustom.rawValue + switch networkSettings.dnsProtocol { + case .https: + cell.field.placeholder = AppConstants.Placeholders.dohURL + cell.field.text = networkSettings.dnsHTTPSURL?.absoluteString + + case .tls: + cell.field.placeholder = AppConstants.Placeholders.dotServerName + cell.field.text = networkSettings.dnsTLSServerName + + default: + break + } + cell.field.clearButtonMode = .always + cell.field.keyboardType = .asciiCapable + cell.captionWidth = 0.0 + cell.delegate = self + return cell + case .dnsAddress: let i = indexPath.row - Offsets.dnsAddress @@ -579,6 +645,24 @@ extension NetworkSettingsViewController { } navigationController?.pushViewController(vc, animated: true) + case .dnsProtocol: + let vc = SingleOptionViewController() + vc.applyTint(.current) + vc.title = (cell as? SettingTableViewCell)?.leftText + if #available(iOS 14, macOS 11, *) { + vc.options = [.plain, .https, .tls] + } else { + vc.options = [.plain] + } + vc.descriptionBlock = { $0.description } + + vc.selectedOption = networkSettings.dnsProtocol ?? .fallback + vc.selectionBlock = { [weak self] in + self?.networkSettings.dnsProtocol = $0 + self?.navigationController?.popViewController(animated: true) + } + navigationController?.pushViewController(vc, animated: true) + case .dnsAddAddress: tableView.deselectRow(at: indexPath, animated: true) diff --git a/Passepartout/App/macOS/Base.lproj/Service.storyboard b/Passepartout/App/macOS/Base.lproj/Service.storyboard index 2ea18e64..1109dade 100644 --- a/Passepartout/App/macOS/Base.lproj/Service.storyboard +++ b/Passepartout/App/macOS/Base.lproj/Service.storyboard @@ -136,14 +136,14 @@ - + - + - + @@ -163,28 +163,74 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + + + + + + + + + - + + + + + - + + + + + + + @@ -209,9 +255,14 @@ + + + + + @@ -219,7 +270,7 @@ - + diff --git a/Passepartout/App/macOS/CHANGELOG.md b/Passepartout/App/macOS/CHANGELOG.md index c35e70b8..4e9b2838 100644 --- a/Passepartout/App/macOS/CHANGELOG.md +++ b/Passepartout/App/macOS/CHANGELOG.md @@ -5,11 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## 1.14.0 (2021-01-08) +## Unreleased ### Added - Country flags in provider infrastructure menu. +- Support DNS over HTTPS/TLS in "Network settings". [#91](https://github.com/passepartoutvpn/passepartout-apple/issues/91) ### Changed diff --git a/Passepartout/App/macOS/Scenes/Service/Customization/DNSViewController.swift b/Passepartout/App/macOS/Scenes/Service/Customization/DNSViewController.swift index 51ce7d87..89b526dc 100644 --- a/Passepartout/App/macOS/Scenes/Service/Customization/DNSViewController.swift +++ b/Passepartout/App/macOS/Scenes/Service/Customization/DNSViewController.swift @@ -25,32 +25,35 @@ import Cocoa import PassepartoutCore +import TunnelKit class DNSViewController: NSViewController, ProfileCustomization { - private struct Templates { - static let server = "0.0.0.0" - - static let domain = "" - } - @IBOutlet private weak var popupChoice: NSPopUpButton! @IBOutlet private weak var viewSettings: NSView! + @IBOutlet private weak var textDNSCustom: NSTextField! + @IBOutlet private weak var viewDNSAddresses: NSView! @IBOutlet private weak var viewDNSDomains: NSView! + @IBOutlet private weak var labelDNSProtocol: NSTextField! + + @IBOutlet private weak var popupDNSProtocol: NSPopUpButton! + @IBOutlet private var constraintChoiceBottom: NSLayoutConstraint! @IBOutlet private var constraintSettingsTop: NSLayoutConstraint! + @IBOutlet private var constraintCustomBottom: NSLayoutConstraint! + + @IBOutlet private var constraintAddressesBottom: NSLayoutConstraint! + private lazy var tableDNSDomains: TextTableView = .get() private lazy var tableDNSAddresses: TextTableView = .get() - private lazy var choices = NetworkChoice.choices(for: profile) - private lazy var currentChoice = profile?.networkChoices?.dns ?? ProfileNetworkChoices.with(profile: profile).dns private lazy var clientNetworkSettings = profile?.clientNetworkSettings @@ -66,6 +69,8 @@ class DNSViewController: NSViewController, ProfileCustomization { override func viewDidLoad() { super.viewDidLoad() + labelDNSProtocol.stringValue = L10n.Core.Global.Captions.protocol.asCaption + tableDNSAddresses.title = L10n.App.NetworkSettings.Dns.Cells.Addresses.title.asCaption viewDNSAddresses.addSubview(tableDNSAddresses) tableDNSAddresses.translatesAutoresizingMaskIntoConstraints = false @@ -89,31 +94,80 @@ class DNSViewController: NSViewController, ProfileCustomization { loadSettings(from: currentChoice) popupChoice.removeAllItems() - for choice in choices { - popupChoice.addItem(withTitle: choice.description) + popupDNSProtocol.removeAllItems() + let menuChoice = NSMenu() + var indexOfChoice = 0 + for (i, choice) in NetworkChoice.choices(for: profile).enumerated() { + let item = NSMenuItem(title: choice.description, action: nil, keyEquivalent: "") + item.representedObject = choice + menuChoice.addItem(item) if choice == currentChoice { - popupChoice.selectItem(at: popupChoice.numberOfItems - 1) + indexOfChoice = i } } - tableDNSAddresses.rowTemplate = Templates.server - tableDNSDomains.rowTemplate = Templates.domain + popupChoice.menu = menuChoice + tableDNSAddresses.rowTemplate = AppConstants.Placeholders.dnsAddress + tableDNSDomains.rowTemplate = AppConstants.Placeholders.dnsDomain + let menuProtocol = NSMenu() + var availableProtocols: [DNSProtocol] = [.plain] + if #available(iOS 14, macOS 11, *) { + availableProtocols.append(.https) + availableProtocols.append(.tls) + } + var indexOfDNSProtocol = 0 + for (i, proto) in availableProtocols.enumerated() { + let item = NSMenuItem(title: proto.description, action: nil, keyEquivalent: "") + item.representedObject = proto + menuProtocol.addItem(item) + if proto == networkSettings.dnsProtocol { + indexOfDNSProtocol = i + } + } + popupChoice.menu = menuChoice + popupChoice.selectItem(at: indexOfChoice) + popupDNSProtocol.menu = menuProtocol + popupDNSProtocol.selectItem(at: indexOfDNSProtocol) } // MARK: Actions @IBAction private func pickChoice(_ sender: Any?) { - let choice = choices[popupChoice.indexOfSelectedItem] + guard let choice = popupChoice.selectedItem?.representedObject as? NetworkChoice else { + return + } loadSettings(from: choice) delegate?.profileCustomization(self, didUpdateDNS: choice, withManualSettings: networkSettings) } + @IBAction private func pickProtocol(_ sender: Any?) { + guard let choice = popupChoice.selectedItem?.representedObject as? NetworkChoice else { + return + } + guard let proto = popupDNSProtocol.selectedItem?.representedObject as? DNSProtocol else { + return + } + networkSettings.dnsProtocol = proto + updateProtocolVisibility() + + delegate?.profileCustomization(self, didUpdateDNS: choice, withManualSettings: networkSettings) + } + func commitManualSettings() { guard currentChoice == .manual else { return } view.endEditing() - networkSettings.dnsServers = tableDNSAddresses.rows + switch networkSettings.dnsProtocol { + case .https: + networkSettings.dnsHTTPSURL = URL(string: textDNSCustom.stringValue) + + case .tls: + networkSettings.dnsTLSServerName = textDNSCustom.stringValue + + default: + networkSettings.dnsServers = tableDNSAddresses.rows + } networkSettings.dnsSearchDomains = tableDNSDomains.rows delegate?.profileCustomization(self, didUpdateDNS: .manual, withManualSettings: networkSettings) @@ -145,5 +199,30 @@ class DNSViewController: NSViewController, ProfileCustomization { constraintChoiceBottom.priority = isServer ? .defaultHigh : .defaultLow constraintSettingsTop.priority = isServer ? .defaultLow : .defaultHigh viewSettings.isHidden = isServer + + updateProtocolVisibility() + } + + private func updateProtocolVisibility() { + let isCustom: Bool + switch networkSettings.dnsProtocol { + case .https: + isCustom = true + textDNSCustom.placeholderString = AppConstants.Placeholders.dohURL + textDNSCustom.stringValue = networkSettings.dnsHTTPSURL?.absoluteString ?? "" + + case .tls: + isCustom = true + textDNSCustom.placeholderString = AppConstants.Placeholders.dotServerName + textDNSCustom.stringValue = networkSettings.dnsTLSServerName ?? "" + + default: + isCustom = false + } + + constraintCustomBottom.priority = isCustom ? .defaultHigh : .defaultLow + constraintAddressesBottom.priority = isCustom ? .defaultLow : .defaultHigh + textDNSCustom.isHidden = !isCustom + viewDNSAddresses.isHidden = isCustom } } diff --git a/Passepartout/App/macOS/Scenes/Service/Customization/ProfileCustomizationViewController.swift b/Passepartout/App/macOS/Scenes/Service/Customization/ProfileCustomizationViewController.swift index 3ae1d371..cc194b8c 100644 --- a/Passepartout/App/macOS/Scenes/Service/Customization/ProfileCustomizationViewController.swift +++ b/Passepartout/App/macOS/Scenes/Service/Customization/ProfileCustomizationViewController.swift @@ -201,8 +201,11 @@ extension ProfileCustomizationContainerViewController: ProfileCustomizationDeleg func profileCustomization(_ profileCustomization: ProfileCustomization, didUpdateDNS choice: NetworkChoice, withManualSettings newSettings: ProfileNetworkSettings) { pendingChoices?.dns = choice - pendingManualNetworkSettings.dnsSearchDomains = newSettings.dnsSearchDomains + pendingManualNetworkSettings.dnsProtocol = newSettings.dnsProtocol + pendingManualNetworkSettings.dnsHTTPSURL = newSettings.dnsHTTPSURL + pendingManualNetworkSettings.dnsTLSServerName = newSettings.dnsTLSServerName pendingManualNetworkSettings.dnsServers = newSettings.dnsServers + pendingManualNetworkSettings.dnsSearchDomains = newSettings.dnsSearchDomains } func profileCustomization(_ profileCustomization: ProfileCustomization, didUpdateProxy choice: NetworkChoice, withManualSettings newSettings: ProfileNetworkSettings) { diff --git a/Passepartout/App/macOS/Scenes/Service/Customization/ProxyViewController.swift b/Passepartout/App/macOS/Scenes/Service/Customization/ProxyViewController.swift index bf4890d5..614a0185 100644 --- a/Passepartout/App/macOS/Scenes/Service/Customization/ProxyViewController.swift +++ b/Passepartout/App/macOS/Scenes/Service/Customization/ProxyViewController.swift @@ -79,7 +79,8 @@ class ProxyViewController: NSViewController, ProfileCustomization { tableProxyBypass.leftAnchor.constraint(equalTo: viewProxyBypass.leftAnchor), tableProxyBypass.rightAnchor.constraint(equalTo: viewProxyBypass.rightAnchor), ]) - + tableProxyBypass.rowTemplate = Templates.bypass + loadSettings(from: currentChoice) popupChoice.removeAllItems() @@ -89,7 +90,6 @@ class ProxyViewController: NSViewController, ProfileCustomization { popupChoice.selectItem(at: popupChoice.numberOfItems - 1) } } - tableProxyBypass.rowTemplate = Templates.bypass } // MARK: Actions diff --git a/Passepartout/Core/Sources/AppConstants.swift b/Passepartout/Core/Sources/AppConstants.swift index 9d51b4cc..2fd84344 100644 --- a/Passepartout/Core/Sources/AppConstants.swift +++ b/Passepartout/Core/Sources/AppConstants.swift @@ -268,6 +268,22 @@ public class AppConstants { public static let api = githubRaw(repo: "api") } + public struct Placeholders { + public static let empty = "" + + public static let address = "0.0.0.0" + + public static let hostname = "example.com" + + public static let dohURL = "https://example.com/dns-query" + + public static let dotServerName = hostname + + public static let dnsAddress = address + + public static let dnsDomain = empty + } + public struct Credits { public static let author = "Davide De Rosa" diff --git a/Passepartout/Core/Sources/Model/ProfileNetworkSettings.swift b/Passepartout/Core/Sources/Model/ProfileNetworkSettings.swift index 4c69725e..ebb0b8ed 100644 --- a/Passepartout/Core/Sources/Model/ProfileNetworkSettings.swift +++ b/Passepartout/Core/Sources/Model/ProfileNetworkSettings.swift @@ -75,8 +75,14 @@ public class ProfileNetworkSettings: Codable, CustomStringConvertible { public var gatewayPolicies: [OpenVPN.RoutingPolicy]? + public var dnsProtocol: DNSProtocol? + public var dnsServers: [String]? + public var dnsHTTPSURL: URL? + + public var dnsTLSServerName: String? + public var dnsSearchDomains: [String]? public var proxyAddress: String? @@ -102,8 +108,11 @@ public class ProfileNetworkSettings: Codable, CustomStringConvertible { public init(from configuration: OpenVPN.Configuration) { gatewayPolicies = configuration.routingPolicies - dnsSearchDomains = configuration.searchDomains + dnsProtocol = configuration.dnsProtocol dnsServers = configuration.dnsServers + dnsHTTPSURL = configuration.dnsHTTPSURL + dnsTLSServerName = configuration.dnsTLSServerName + dnsSearchDomains = configuration.searchDomains proxyAddress = configuration.httpProxy?.address proxyPort = configuration.httpProxy?.port proxyAutoConfigurationURL = configuration.proxyAutoConfigurationURL @@ -123,8 +132,11 @@ public class ProfileNetworkSettings: Codable, CustomStringConvertible { } public func copyDNS(from settings: ProfileNetworkSettings) { - dnsSearchDomains = settings.dnsSearchDomains + dnsProtocol = settings.dnsProtocol dnsServers = settings.dnsServers?.filter { !$0.isEmpty } + dnsHTTPSURL = settings.dnsHTTPSURL + dnsTLSServerName = settings.dnsTLSServerName + dnsSearchDomains = settings.dnsSearchDomains } public func copyProxy(from settings: ProfileNetworkSettings) { @@ -143,7 +155,7 @@ public class ProfileNetworkSettings: Codable, CustomStringConvertible { public var description: String { let comps: [String] = [ "gw: \(gatewayPolicies?.description ?? "")", - "dns: {domains: \(dnsSearchDomains?.description ?? "[]"), servers: \(dnsServers?.description ?? "[]")}", + "dns: {protocol: \(dnsProtocol ?? .fallback), https: \(dnsHTTPSURL?.absoluteString ?? ""), tls: \(dnsTLSServerName?.description ?? ""), servers: \(dnsServers?.description ?? "[]"), domains: \(dnsSearchDomains?.description ?? "[]")}", "proxy: {address: \(proxyAddress ?? ""), port: \(proxyPort?.description ?? ""), PAC: \(proxyAutoConfigurationURL?.absoluteString ?? ""), bypass: \(proxyBypassDomains?.description ?? "[]")}", "mtu: {bytes: \(mtuBytes?.description ?? "default")}" ] @@ -171,11 +183,17 @@ extension OpenVPN.ConfigurationBuilder { break case .server: + dnsProtocol = nil dnsServers = nil + dnsHTTPSURL = nil + dnsTLSServerName = nil searchDomains = nil case .manual: + dnsProtocol = settings.dnsProtocol dnsServers = settings.dnsServers?.filter { !$0.isEmpty } + dnsHTTPSURL = settings.dnsHTTPSURL + dnsTLSServerName = settings.dnsTLSServerName searchDomains = settings.dnsSearchDomains } } diff --git a/Podfile b/Podfile index e693b62c..10d52085 100644 --- a/Podfile +++ b/Podfile @@ -8,7 +8,7 @@ $tunnelkit_specs = ['Protocols/OpenVPN', 'Extra/LZO'] def shared_pods #pod_version $tunnelkit_name, $tunnelkit_specs, '~> 3.1.0' - pod_git $tunnelkit_name, $tunnelkit_specs, 'e388842' + pod_git $tunnelkit_name, $tunnelkit_specs, '5014e65' #pod_path $tunnelkit_name, $tunnelkit_specs, '..' pod 'SSZipArchive' pod 'Kvitto', :git => 'https://github.com/keeshux/Kvitto', :branch => 'enable-macos-spec' diff --git a/Podfile.lock b/Podfile.lock index 3fd636cc..9c397c15 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -52,8 +52,8 @@ DEPENDENCIES: - Kvitto (from `https://github.com/keeshux/Kvitto`, branch `enable-macos-spec`) - MBProgressHUD - SSZipArchive - - TunnelKit/Extra/LZO (from `https://github.com/passepartoutvpn/tunnelkit`, commit `e388842`) - - TunnelKit/Protocols/OpenVPN (from `https://github.com/passepartoutvpn/tunnelkit`, commit `e388842`) + - TunnelKit/Extra/LZO (from `https://github.com/passepartoutvpn/tunnelkit`, commit `5014e65`) + - TunnelKit/Protocols/OpenVPN (from `https://github.com/passepartoutvpn/tunnelkit`, commit `5014e65`) SPEC REPOS: https://github.com/cocoapods/specs.git: @@ -71,7 +71,7 @@ EXTERNAL SOURCES: :branch: enable-macos-spec :git: https://github.com/keeshux/Kvitto TunnelKit: - :commit: e388842 + :commit: '5014e65' :git: https://github.com/passepartoutvpn/tunnelkit CHECKOUT OPTIONS: @@ -82,7 +82,7 @@ CHECKOUT OPTIONS: :commit: e263fcd1f40a6a482a0f1e424ba98009c4ad2b96 :git: https://github.com/keeshux/Kvitto TunnelKit: - :commit: e388842 + :commit: '5014e65' :git: https://github.com/passepartoutvpn/tunnelkit SPEC CHECKSUMS: @@ -95,6 +95,6 @@ SPEC CHECKSUMS: SwiftyBeaver: 2e8acd6fc90c6d0a27055867a290794926d57c02 TunnelKit: 2a6aadea2d772a2760b153aee27d1c334c9ca6db -PODFILE CHECKSUM: d6449ccbaad5d2ca50f6a27a651baaa17ef9db1a +PODFILE CHECKSUM: 90f34e726cde9553212321b6f95cd79f284e0dca COCOAPODS: 1.10.0