Skip to content

Commit

Permalink
Adds support for managing VPN settings. (#565)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/0/1205958731729757/f

iOS PR: duckduckgo/iOS#2165
macOS PR: duckduckgo/macos-browser#1858
What kind of version bump will this require?: Major/Minor/Patch

Description

Adds support for the settings we'll be showing in our macOS Settings pane.
  • Loading branch information
diegoreymendez authored Nov 28, 2023
1 parent 023a764 commit 1331652
Show file tree
Hide file tree
Showing 23 changed files with 379 additions and 194 deletions.
1 change: 1 addition & 0 deletions Sources/NetworkProtection/AppLaunching.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public enum AppLaunchCommand: Codable {
case justOpen
case shareFeedback
case showStatus
case showSettings
case startVPN
case stopVPN
case enableOnDemand
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@ public enum DebugCommand: Codable {
}

public enum ExtensionRequest: Codable {
case changeTunnelSetting(_ change: TunnelSettings.Change)
case changeTunnelSetting(_ change: VPNSettings.Change)
case debugCommand(_ command: DebugCommand)
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public final class NetworkProtectionCodeRedemptionCoordinator: NetworkProtection
private let versionStore: NetworkProtectionLastVersionRunStore
private let errorEvents: EventMapping<NetworkProtectionError>

convenience public init(environment: TunnelSettings.SelectedEnvironment,
convenience public init(environment: VPNSettings.SelectedEnvironment,
tokenStore: NetworkProtectionTokenStore,
versionStore: NetworkProtectionLastVersionRunStore = .init(),
errorEvents: EventMapping<NetworkProtectionError>) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public actor NetworkProtectionDeviceManager: NetworkProtectionDeviceManagement {

private let errorEvents: EventMapping<NetworkProtectionError>?

public init(environment: TunnelSettings.SelectedEnvironment,
public init(environment: VPNSettings.SelectedEnvironment,
tokenStore: NetworkProtectionTokenStore,
keyStore: NetworkProtectionKeyStore,
serverListStore: NetworkProtectionServerListStore? = nil,
Expand Down
1 change: 0 additions & 1 deletion Sources/NetworkProtection/NetworkProtectionOptionKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,5 @@ public enum NetworkProtectionOptionKey {
public static let tunnelFatalErrorCrashSimulation = "tunnelFatalErrorCrashSimulation"
public static let tunnelMemoryCrashSimulation = "tunnelMemoryCrashSimulation"
public static let includedRoutes = "includedRoutes"
public static let excludedRoutes = "excludedRoutes"
public static let connectionTesterEnabled = "connectionTesterEnabled"
}
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ final class NetworkProtectionBackendClient: NetworkProtectionClient {

private let endpointURL: URL

init(environment: TunnelSettings.SelectedEnvironment = .default) {
init(environment: VPNSettings.SelectedEnvironment = .default) {
endpointURL = environment.endpointURL
}

Expand Down
76 changes: 34 additions & 42 deletions Sources/NetworkProtection/PacketTunnelProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {

// MARK: - Tunnel Settings

private let settings: TunnelSettings
private let settings: VPNSettings

// MARK: - Server Selection

Expand All @@ -126,7 +126,6 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {
public let lastSelectedServerInfoPublisher = CurrentValueSubject<NetworkProtectionServerInfo?, Never>.init(nil)

private var includedRoutes: [IPAddressRange]?
private var excludedRoutes: [IPAddressRange]?

// MARK: - User Notifications

Expand Down Expand Up @@ -300,7 +299,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {
tokenStore: NetworkProtectionTokenStore,
debugEvents: EventMapping<NetworkProtectionError>?,
providerEvents: EventMapping<Event>,
tunnelSettings: TunnelSettings = TunnelSettings(defaults: .standard)) {
settings: VPNSettings) {
os_log("[+] PacketTunnelProvider", log: .networkProtectionMemoryLog, type: .debug)

self.notificationsPresenter = notificationsPresenter
Expand All @@ -310,11 +309,11 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {
self.providerEvents = providerEvents
self.tunnelHealth = tunnelHealthStore
self.controllerErrorStore = controllerErrorStore
self.settings = tunnelSettings
self.settings = settings

super.init()

tunnelSettings.changePublisher
settings.changePublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] change in
self?.handleSettingsChange(change)
Expand Down Expand Up @@ -428,17 +427,6 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {

private func loadRoutes(from options: [String: Any]?) {
self.includedRoutes = (options?[NetworkProtectionOptionKey.includedRoutes] as? [String])?.compactMap(IPAddressRange.init(from:)) ?? []

self.excludedRoutes = (options?[NetworkProtectionOptionKey.excludedRoutes] as? [String])?.compactMap(IPAddressRange.init(from:))
?? [ // fallback to default local network exclusions
"10.0.0.0/8", // 255.0.0.0
"172.16.0.0/12", // 255.240.0.0
"192.168.0.0/16", // 255.255.0.0
"169.254.0.0/16", // 255.255.0.0 : Link-local
"127.0.0.0/8", // 255.0.0.0 : Loopback
"224.0.0.0/4", // 240.0.0.0 : Multicast
"100.64.0.0/16", // 255.255.0.0 : Shared Address Space
]
}

// MARK: - Tunnel Start
Expand Down Expand Up @@ -524,14 +512,15 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {
return serverSelectionMethod
}

private func startTunnel(environment: TunnelSettings.SelectedEnvironment, onDemand: Bool, completionHandler: @escaping (Error?) -> Void) {
private func startTunnel(environment: VPNSettings.SelectedEnvironment, onDemand: Bool, completionHandler: @escaping (Error?) -> Void) {
Task {
do {
os_log("🔵 Generating tunnel config", log: .networkProtection, type: .info)
os_log("🔵 Excluded ranges are: %{public}@", log: .networkProtection, type: .info, String(describing: settings.excludedRanges))
let tunnelConfiguration = try await generateTunnelConfiguration(environment: environment,
serverSelectionMethod: currentServerSelectionMethod,
includedRoutes: includedRoutes ?? [],
excludedRoutes: excludedRoutes ?? [])
excludedRoutes: settings.excludedRanges)
startTunnel(with: tunnelConfiguration, onDemand: onDemand, completionHandler: completionHandler)
os_log("🔵 Done generating tunnel config", log: .networkProtection, type: .info)
} catch {
Expand Down Expand Up @@ -584,9 +573,12 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {
}

Task { [weak self] in
await self?.handleAdapterStopped()
if case .superceded = reason {
self?.notificationsPresenter.showSupersededNotification()
if let self {
await self.handleAdapterStopped()

if case .superceded = reason {
self.notificationsPresenter.showSupersededNotification()
}
}

completionHandler()
Expand Down Expand Up @@ -660,12 +652,12 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {
}

@MainActor
public func updateTunnelConfiguration(environment: TunnelSettings.SelectedEnvironment, serverSelectionMethod: NetworkProtectionServerSelectionMethod, reassert: Bool = true) async throws {
public func updateTunnelConfiguration(environment: VPNSettings.SelectedEnvironment = .default, serverSelectionMethod: NetworkProtectionServerSelectionMethod, reassert: Bool = true) async throws {

let tunnelConfiguration = try await generateTunnelConfiguration(environment: environment,
serverSelectionMethod: serverSelectionMethod,
includedRoutes: includedRoutes ?? [],
excludedRoutes: excludedRoutes ?? [])
excludedRoutes: settings.excludedRanges)

try await withCheckedThrowingContinuation { [weak self] (continuation: CheckedContinuation<Void, Error>) in
guard let self = self else {
Expand Down Expand Up @@ -696,7 +688,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {
}

@MainActor
private func generateTunnelConfiguration(environment: TunnelSettings.SelectedEnvironment, serverSelectionMethod: NetworkProtectionServerSelectionMethod, includedRoutes: [IPAddressRange], excludedRoutes: [IPAddressRange]) async throws -> TunnelConfiguration {
private func generateTunnelConfiguration(environment: VPNSettings.SelectedEnvironment = .default, serverSelectionMethod: NetworkProtectionServerSelectionMethod, includedRoutes: [IPAddressRange], excludedRoutes: [IPAddressRange]) async throws -> TunnelConfiguration {

let configurationResult: (TunnelConfiguration, NetworkProtectionServerInfo)

Expand Down Expand Up @@ -757,8 +749,9 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {
handleResetAllState(completionHandler: completionHandler)
case .triggerTestNotification:
handleSendTestNotification(completionHandler: completionHandler)
case .setExcludedRoutes(let excludedRoutes):
setExcludedRoutes(excludedRoutes, completionHandler: completionHandler)
case .setExcludedRoutes:
// No longer supported, will remove, but keeping the enum to prevent ABI issues
completionHandler?(nil)
case .setIncludedRoutes(let includedRoutes):
setIncludedRoutes(includedRoutes, completionHandler: completionHandler)
case .simulateTunnelFailure:
Expand All @@ -783,13 +776,21 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {
}
}

private func handleSettingChangeAppRequest(_ change: TunnelSettings.Change, completionHandler: ((Data?) -> Void)? = nil) {
private func handleSettingChangeAppRequest(_ change: VPNSettings.Change, completionHandler: ((Data?) -> Void)? = nil) {
settings.apply(change: change)
handleSettingsChange(change, completionHandler: completionHandler)
}

private func handleSettingsChange(_ change: TunnelSettings.Change, completionHandler: ((Data?) -> Void)? = nil) {
// swiftlint:disable:next cyclomatic_complexity
private func handleSettingsChange(_ change: VPNSettings.Change, completionHandler: ((Data?) -> Void)? = nil) {
switch change {
case .setExcludeLocalNetworks:
Task {
if case .connected = connectionStatus {
try? await updateTunnelConfiguration(reassert: false)
}
completionHandler?(nil)
}
case .setSelectedServer(let selectedServer):
let serverSelectionMethod: NetworkProtectionServerSelectionMethod

Expand Down Expand Up @@ -822,11 +823,13 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {
}
completionHandler?(nil)
}
case .setIncludeAllNetworks,
case .setConnectOnLogin,
.setIncludeAllNetworks,
.setEnforceRoutes,
.setExcludeLocalNetworks,
.setNotifyStatusChanges,
.setRegistrationKeyValidity,
.setSelectedEnvironment:
.setSelectedEnvironment,
.setShowInMenuBar:
// Intentional no-op, as some setting changes don't require any further operation
break
}
Expand Down Expand Up @@ -935,17 +938,6 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {
completionHandler?(nil)
}

private func setExcludedRoutes(_ excludedRoutes: [IPAddressRange], completionHandler: ((Data?) -> Void)? = nil) {
Task {
self.excludedRoutes = excludedRoutes

if case .connected = connectionStatus {
try? await updateTunnelConfiguration(reassert: false)
}
completionHandler?(nil)
}
}

private func setIncludedRoutes(_ includedRoutes: [IPAddressRange], completionHandler: ((Data?) -> Void)? = nil) {
Task {
self.includedRoutes = includedRoutes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ final public class NetworkProtectionLocationListCompositeRepository: NetworkProt
private let client: NetworkProtectionClient
private let tokenStore: NetworkProtectionTokenStore

convenience public init(environment: TunnelSettings.SelectedEnvironment, tokenStore: NetworkProtectionTokenStore) {
convenience public init(environment: VPNSettings.SelectedEnvironment, tokenStore: NetworkProtectionTokenStore) {
self.init(
client: NetworkProtectionBackendClient(environment: environment),
tokenStore: tokenStore
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// UserDefaults+connectOnLogin.swift
//
// Copyright © 2023 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Combine
import Foundation

extension UserDefaults {
private var connectOnLoginKey: String {
"networkProtectionSettingConnectOnLogin"
}

@objc
dynamic var networkProtectionSettingConnectOnLogin: Bool {
get {
bool(forKey: connectOnLoginKey)
}

set {
set(newValue, forKey: connectOnLoginKey)
}
}

var networkProtectionSettingConnectOnLoginPublisher: AnyPublisher<Bool, Never> {
publisher(for: \.networkProtectionSettingConnectOnLogin).eraseToAnyPublisher()
}

func resetNetworkProtectionSettingConnectOnLogin() {
removeObject(forKey: connectOnLoginKey)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ extension UserDefaults {
"networkProtectionSettingExcludeLocalNetworks"
}

static let excludeLocalNetworksDefaultValue = true

@objc
dynamic var networkProtectionSettingExcludeLocalNetworks: Bool {
get {
bool(forKey: excludeLocalNetworksKey)
value(forKey: excludeLocalNetworksKey) as? Bool ?? Self.excludeLocalNetworksDefaultValue
}

set {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// UserDefaults+notifyStatusChanges.swift
//
// Copyright © 2023 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Combine
import Foundation

extension UserDefaults {
private var notifyStatusChangesKey: String {
"networkProtectionNotifyStatusChanges"
}

private static let notifyStatusChangesDefaultValue = true

@objc
dynamic var networkProtectionNotifyStatusChanges: Bool {
get {
value(forKey: notifyStatusChangesKey) as? Bool ?? Self.notifyStatusChangesDefaultValue
}

set {
set(newValue, forKey: notifyStatusChangesKey)
}
}

var networkProtectionNotifyStatusChangesPublisher: AnyPublisher<Bool, Never> {
publisher(for: \.networkProtectionNotifyStatusChanges).eraseToAnyPublisher()
}

func resetNetworkProtectionSettingNotifyStatusChanges() {
removeObject(forKey: notifyStatusChangesKey)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,15 @@ extension UserDefaults {
}
}

private func registrationKeyValidityFromRawValue(_ rawValue: NSNumber?) -> TunnelSettings.RegistrationKeyValidity {
private func registrationKeyValidityFromRawValue(_ rawValue: NSNumber?) -> VPNSettings.RegistrationKeyValidity {
guard let timeInterval = networkProtectionSettingRegistrationKeyValidityRawValue?.doubleValue else {
return .automatic
}

return .custom(timeInterval)
}

var networkProtectionSettingRegistrationKeyValidity: TunnelSettings.RegistrationKeyValidity {
var networkProtectionSettingRegistrationKeyValidity: VPNSettings.RegistrationKeyValidity {
get {
registrationKeyValidityFromRawValue(networkProtectionSettingRegistrationKeyValidityRawValue)
}
Expand All @@ -58,7 +58,7 @@ extension UserDefaults {
}
}

var networkProtectionSettingRegistrationKeyValidityPublisher: AnyPublisher<TunnelSettings.RegistrationKeyValidity, Never> {
var networkProtectionSettingRegistrationKeyValidityPublisher: AnyPublisher<VPNSettings.RegistrationKeyValidity, Never> {
let registrationKeyValidityFromRawValue = self.registrationKeyValidityFromRawValue

return publisher(for: \.networkProtectionSettingRegistrationKeyValidityRawValue).map { serverName in
Expand Down
Loading

0 comments on commit 1331652

Please sign in to comment.