diff --git a/Core/FeatureFlag.swift b/Core/FeatureFlag.swift
index b877485851..de9c1df97e 100644
--- a/Core/FeatureFlag.swift
+++ b/Core/FeatureFlag.swift
@@ -49,6 +49,9 @@ public enum FeatureFlag: String {
/// https://app.asana.com/0/72649045549333/1208231259093710/f
case networkProtectionUserTips
+
+ /// https://app.asana.com/0/72649045549333/1208617860225199/f
+ case networkProtectionEnforceRoutes
}
extension FeatureFlag: FeatureFlagSourceProviding {
@@ -104,6 +107,8 @@ extension FeatureFlag: FeatureFlagSourceProviding {
return .remoteReleasable(.feature(.autocompleteTabs))
case .networkProtectionUserTips:
return .remoteReleasable(.subfeature(NetworkProtectionSubfeature.userTips))
+ case .networkProtectionEnforceRoutes:
+ return .remoteDevelopment(.subfeature(NetworkProtectionSubfeature.enforceRoutes))
case .adAttributionReporting:
return .remoteReleasable(.feature(.adAttributionReporting))
}
diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj
index 6f33031202..695adc1f30 100644
--- a/DuckDuckGo.xcodeproj/project.pbxproj
+++ b/DuckDuckGo.xcodeproj/project.pbxproj
@@ -10980,7 +10980,7 @@
repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit";
requirement = {
kind = exactVersion;
- version = 208.1.0;
+ version = 209.0.0;
};
};
9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = {
diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index cb461c0e9c..c542bb4b81 100644
--- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -32,8 +32,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/DuckDuckGo/BrowserServicesKit",
"state" : {
- "revision" : "6be781530a2516c703b8e1bcf0c90e6e763d3300",
- "version" : "208.1.0"
+ "revision" : "614ea57db48db644ce7f3a3de9c20c9a7fbb08ff",
+ "version" : "209.0.0"
}
},
{
diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme
index b9ef06c977..a096dee425 100644
--- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme
+++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme
@@ -65,6 +65,9 @@
+
+
diff --git a/DuckDuckGo/AppDependencyProvider.swift b/DuckDuckGo/AppDependencyProvider.swift
index 35d7bf45d0..245901da44 100644
--- a/DuckDuckGo/AppDependencyProvider.swift
+++ b/DuckDuckGo/AppDependencyProvider.swift
@@ -113,7 +113,7 @@ final class AppDependencyProvider: DependencyProvider {
entitlementsCache: entitlementsCache,
subscriptionEndpointService: subscriptionService,
authEndpointService: authService)
-
+
let subscriptionManager = DefaultSubscriptionManager(storePurchaseManager: DefaultStorePurchaseManager(),
accountManager: accountManager,
subscriptionEndpointService: subscriptionService,
@@ -126,17 +126,14 @@ final class AppDependencyProvider: DependencyProvider {
let accessTokenProvider: () -> String? = {
return { accountManager.accessToken }
}()
-#if os(macOS)
- networkProtectionKeychainTokenStore = NetworkProtectionKeychainTokenStore(keychainType: .dataProtection(.unspecified),
- serviceName: "\(Bundle.main.bundleIdentifier!).authToken",
- errorEvents: .networkProtectionAppDebugEvents,
- accessTokenProvider: accessTokenProvider)
-#else
+
networkProtectionKeychainTokenStore = NetworkProtectionKeychainTokenStore(accessTokenProvider: accessTokenProvider)
-#endif
+
networkProtectionTunnelController = NetworkProtectionTunnelController(accountManager: accountManager,
tokenStore: networkProtectionKeychainTokenStore,
- persistentPixel: persistentPixel)
+ featureFlagger: featureFlagger,
+ persistentPixel: persistentPixel,
+ settings: vpnSettings)
vpnFeatureVisibility = DefaultNetworkProtectionVisibility(userDefaults: .networkProtectionGroupDefaults,
accountManager: accountManager)
}
diff --git a/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift b/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift
index e440446044..a72c4f0ab0 100644
--- a/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift
+++ b/DuckDuckGo/NetworkProtectionConvenienceInitialisers.swift
@@ -59,6 +59,7 @@ extension NetworkProtectionVPNSettingsViewModel {
convenience init() {
self.init(
notificationsAuthorization: NotificationsAuthorizationController(),
+ controller: AppDependencyProvider.shared.networkProtectionTunnelController,
settings: AppDependencyProvider.shared.vpnSettings
)
}
diff --git a/DuckDuckGo/NetworkProtectionDebugViewController.swift b/DuckDuckGo/NetworkProtectionDebugViewController.swift
index 0c0c34568e..fb38929911 100644
--- a/DuckDuckGo/NetworkProtectionDebugViewController.swift
+++ b/DuckDuckGo/NetworkProtectionDebugViewController.swift
@@ -63,6 +63,7 @@ final class NetworkProtectionDebugViewController: UITableViewController {
enum DebugFeatureRows: Int, CaseIterable {
case toggleAlwaysOn
+ case enforceRoutes
}
enum SimulateFailureRows: Int, CaseIterable {
@@ -324,6 +325,14 @@ final class NetworkProtectionDebugViewController: UITableViewController {
} else {
cell.accessoryType = .checkmark
}
+ case .enforceRoutes:
+ cell.textLabel?.text = "Enforce Routes"
+
+ if !AppDependencyProvider.shared.vpnSettings.enforceRoutes {
+ cell.accessoryType = .none
+ } else {
+ cell.accessoryType = .checkmark
+ }
default:
break
}
@@ -334,6 +343,9 @@ final class NetworkProtectionDebugViewController: UITableViewController {
case .toggleAlwaysOn:
debugFeatures.alwaysOnDisabled.toggle()
tableView.reloadRows(at: [indexPath], with: .none)
+ case .enforceRoutes:
+ AppDependencyProvider.shared.vpnSettings.enforceRoutes.toggle()
+ tableView.reloadRows(at: [indexPath], with: .none)
default:
break
}
diff --git a/DuckDuckGo/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtectionTunnelController.swift
index c13dedcb06..ed22d7cd91 100644
--- a/DuckDuckGo/NetworkProtectionTunnelController.swift
+++ b/DuckDuckGo/NetworkProtectionTunnelController.swift
@@ -17,9 +17,10 @@
// limitations under the License.
//
-import Foundation
+import BrowserServicesKit
import Combine
import Core
+import Foundation
import NetworkExtension
import NetworkProtection
import Subscription
@@ -34,6 +35,7 @@ enum VPNConfigurationRemovalReason: String {
final class NetworkProtectionTunnelController: TunnelController, TunnelSessionProvider {
static var shouldSimulateFailure: Bool = false
+ private let featureFlagger: FeatureFlagger
private var internalManager: NETunnelProviderManager?
private let debugFeatures = NetworkProtectionDebugFeatures()
private let tokenStore: NetworkProtectionKeychainTokenStore
@@ -42,6 +44,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr
private let notificationCenter: NotificationCenter = .default
private var previousStatus: NEVPNStatus = .invalid
private let persistentPixel: PersistentPixelFiring
+ private let settings: VPNSettings
private var cancellables = Set()
// MARK: - Manager, Session, & Connection
@@ -119,9 +122,25 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr
}
}
- init(accountManager: AccountManager, tokenStore: NetworkProtectionKeychainTokenStore, persistentPixel: PersistentPixelFiring) {
- self.tokenStore = tokenStore
+ // MARK: - Enforce Routes
+
+ private var enforceRoutes: Bool {
+ featureFlagger.isFeatureOn(.networkProtectionEnforceRoutes)
+ }
+
+ // MARK: - Initializers
+
+ init(accountManager: AccountManager,
+ tokenStore: NetworkProtectionKeychainTokenStore,
+ featureFlagger: FeatureFlagger,
+ persistentPixel: PersistentPixelFiring,
+ settings: VPNSettings) {
+
+ self.featureFlagger = featureFlagger
self.persistentPixel = persistentPixel
+ self.settings = settings
+ self.tokenStore = tokenStore
+
subscribeToSnoozeTimingChanges()
subscribeToStatusChanges()
subscribeToConfigurationChanges()
@@ -180,6 +199,16 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr
tunnelManager.connection.stopVPNTunnel()
}
+ func command(_ command: VPNCommand) async throws {
+ guard let activeSession = await AppDependencyProvider.shared.networkProtectionTunnelController.activeSession(),
+ activeSession.status == .connected else {
+
+ return
+ }
+
+ try? await activeSession.sendProviderRequest(.command(command))
+ }
+
func removeVPN(reason: VPNConfigurationRemovalReason) async {
do {
try await tunnelManager?.removeFromPreferences()
@@ -293,6 +322,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr
return tunnelManager
}
+ @MainActor
private func setupAndSave(_ tunnelManager: NETunnelProviderManager) async throws {
setup(tunnelManager)
@@ -319,6 +349,7 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr
/// Setups the tunnel manager if it's not set up already.
///
+ @MainActor
private func setup(_ tunnelManager: NETunnelProviderManager) {
tunnelManager.localizedDescription = "DuckDuckGo VPN"
tunnelManager.isEnabled = true
@@ -327,9 +358,24 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr
let protocolConfiguration = NETunnelProviderProtocol()
protocolConfiguration.serverAddress = "127.0.0.1" // Dummy address... the NetP service will take care of grabbing a real server
+ protocolConfiguration.providerConfiguration = [:]
+
// always-on
protocolConfiguration.disconnectOnSleep = false
+ // Enforce routes
+ protocolConfiguration.enforceRoutes = enforceRoutes
+
+ // We will control excluded networks through includedRoutes / excludedRoutes
+ protocolConfiguration.excludeLocalNetworks = false
+
+ #if DEBUG
+ if #available(iOS 17.4, *) {
+ // This is useful to ensure debugging is never blocked by the VPN
+ protocolConfiguration.excludeDeviceCommunication = true
+ }
+ #endif
+
return protocolConfiguration
}()
diff --git a/DuckDuckGo/NetworkProtectionVPNSettingsView.swift b/DuckDuckGo/NetworkProtectionVPNSettingsView.swift
index 6d8ab17c8c..b01354f454 100644
--- a/DuckDuckGo/NetworkProtectionVPNSettingsView.swift
+++ b/DuckDuckGo/NetworkProtectionVPNSettingsView.swift
@@ -50,9 +50,6 @@ struct NetworkProtectionVPNSettingsView: View {
footerText: UserText.netPExcludeLocalNetworksSettingFooter
) {
Toggle("", isOn: $viewModel.excludeLocalNetworks)
- .onTapGesture {
- viewModel.toggleExcludeLocalNetworks()
- }
}
dnsSection()
diff --git a/DuckDuckGo/NetworkProtectionVPNSettingsViewModel.swift b/DuckDuckGo/NetworkProtectionVPNSettingsViewModel.swift
index 4866dd6055..9442ff6597 100644
--- a/DuckDuckGo/NetworkProtectionVPNSettingsViewModel.swift
+++ b/DuckDuckGo/NetworkProtectionVPNSettingsViewModel.swift
@@ -29,6 +29,7 @@ enum NetworkProtectionNotificationsViewKind: Equatable {
}
final class NetworkProtectionVPNSettingsViewModel: ObservableObject {
+ private let controller: TunnelController
private let settings: VPNSettings
private var cancellables: Set = []
@@ -39,11 +40,32 @@ final class NetworkProtectionVPNSettingsViewModel: ObservableObject {
self.settings.notifyStatusChanges
}
- @Published public var excludeLocalNetworks: Bool = true
+ @Published public var excludeLocalNetworks: Bool {
+ didSet {
+ guard oldValue != excludeLocalNetworks else {
+ return
+ }
+
+ settings.excludeLocalNetworks = excludeLocalNetworks
+
+ Task {
+ // We need to allow some time for the setting to propagate
+ // But ultimately this should actually be a user choice
+ try await Task.sleep(interval: 0.1)
+ try await controller.command(.restartAdapter)
+ }
+ }
+ }
+
@Published public var usesCustomDNS = false
@Published public var dnsServers: String = UserText.vpnSettingDNSServerDefaultValue
- init(notificationsAuthorization: NotificationsAuthorizationControlling, settings: VPNSettings) {
+ init(notificationsAuthorization: NotificationsAuthorizationControlling,
+ controller: TunnelController,
+ settings: VPNSettings) {
+
+ self.controller = controller
+ self.excludeLocalNetworks = settings.excludeLocalNetworks
self.settings = settings
self.notificationsAuthorization = notificationsAuthorization
@@ -77,10 +99,6 @@ final class NetworkProtectionVPNSettingsViewModel: ObservableObject {
settings.notifyStatusChanges = enabled
}
- func toggleExcludeLocalNetworks() {
- settings.excludeLocalNetworks.toggle()
- }
-
private static func localizedString(forRegionCode: String) -> String {
Locale.current.localizedString(forRegionCode: forRegionCode) ?? forRegionCode.capitalized
}