diff --git a/ios/MullvadREST/ApiHandlers/ServerRelaysResponse.swift b/ios/MullvadREST/ApiHandlers/ServerRelaysResponse.swift index 63f0822e63b6..a575da03cc06 100644 --- a/ios/MullvadREST/ApiHandlers/ServerRelaysResponse.swift +++ b/ios/MullvadREST/ApiHandlers/ServerRelaysResponse.swift @@ -34,6 +34,19 @@ extension REST { public let ipv4AddrIn: IPv4Address public let weight: UInt64 public let includeInCountry: Bool + + public func override(ipv4AddrIn: IPv4Address?) -> Self { + return BridgeRelay( + hostname: hostname, + active: active, + owned: owned, + location: location, + provider: provider, + ipv4AddrIn: ipv4AddrIn ?? self.ipv4AddrIn, + weight: weight, + includeInCountry: includeInCountry + ) + } } public struct ServerRelay: Codable, Equatable { @@ -47,6 +60,21 @@ extension REST { public let ipv6AddrIn: IPv6Address public let publicKey: Data public let includeInCountry: Bool + + public func override(ipv4AddrIn: IPv4Address?, ipv6AddrIn: IPv6Address?) -> Self { + return ServerRelay( + hostname: hostname, + active: active, + owned: owned, + location: location, + provider: provider, + weight: weight, + ipv4AddrIn: ipv4AddrIn ?? self.ipv4AddrIn, + ipv6AddrIn: ipv6AddrIn ?? self.ipv6AddrIn, + publicKey: publicKey, + includeInCountry: includeInCountry + ) + } } public struct ServerWireguardTunnels: Codable, Equatable { diff --git a/ios/MullvadREST/Relay/AnyRelay.swift b/ios/MullvadREST/Relay/AnyRelay.swift new file mode 100644 index 000000000000..6c3c49aa5596 --- /dev/null +++ b/ios/MullvadREST/Relay/AnyRelay.swift @@ -0,0 +1,29 @@ +// +// AnyRelay.swift +// MullvadREST +// +// Created by Jon Petersson on 2024-01-31. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import MullvadTypes +import Network + +public protocol AnyRelay { + var hostname: String { get } + var owned: Bool { get } + var location: String { get } + var provider: String { get } + var weight: UInt64 { get } + var active: Bool { get } + var includeInCountry: Bool { get } + + func override(ipv4AddrIn: IPv4Address?, ipv6AddrIn: IPv6Address?) -> Self +} + +extension REST.ServerRelay: AnyRelay {} +extension REST.BridgeRelay: AnyRelay { + public func override(ipv4AddrIn: IPv4Address?, ipv6AddrIn: IPv6Address?) -> REST.BridgeRelay { + override(ipv4AddrIn: ipv4AddrIn) + } +} diff --git a/ios/MullvadREST/Relay/IPOverrideWrapper.swift b/ios/MullvadREST/Relay/IPOverrideWrapper.swift new file mode 100644 index 000000000000..531112cf5425 --- /dev/null +++ b/ios/MullvadREST/Relay/IPOverrideWrapper.swift @@ -0,0 +1,72 @@ +// +// IPOverrideWrapper.swift +// MullvadREST +// +// Created by Jon Petersson on 2024-02-05. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import MullvadSettings +import MullvadTypes + +public class IPOverrideWrapper: RelayCacheProtocol { + private let relayCache: RelayCacheProtocol + private let ipOverrideRepository: any IPOverrideRepositoryProtocol + + public init(relayCache: RelayCacheProtocol, ipOverrideRepository: any IPOverrideRepositoryProtocol) { + self.relayCache = relayCache + self.ipOverrideRepository = ipOverrideRepository + } + + public func read() throws -> CachedRelays { + let cache = try relayCache.read() + let relayResponse = apply(overrides: ipOverrideRepository.fetchAll(), to: cache.relays) + + return CachedRelays(relays: relayResponse, updatedAt: cache.updatedAt) + } + + public func write(record: CachedRelays) throws { + try relayCache.write(record: record) + } + + private func apply( + overrides: [IPOverride], + to relayResponse: REST.ServerRelaysResponse + ) -> REST.ServerRelaysResponse { + let wireguard = relayResponse.wireguard + let bridge = relayResponse.bridge + + let overridenWireguardRelays = wireguard.relays.map { relay in + return apply(overrides: overrides, to: relay) + } + let overridenBridgeRelays = bridge.relays.map { relay in + return apply(overrides: overrides, to: relay) + } + + return REST.ServerRelaysResponse( + locations: relayResponse.locations, + wireguard: REST.ServerWireguardTunnels( + ipv4Gateway: wireguard.ipv4Gateway, + ipv6Gateway: wireguard.ipv6Gateway, + portRanges: wireguard.portRanges, + relays: overridenWireguardRelays + ), + bridge: REST.ServerBridges( + shadowsocks: bridge.shadowsocks, + relays: overridenBridgeRelays + ) + ) + } + + private func apply(overrides: [IPOverride], to relay: T) -> T { + return overrides + .first { $0.hostname == relay.hostname } + .flatMap { + relay.override( + ipv4AddrIn: $0.ipv4Address, + ipv6AddrIn: $0.ipv6Address + ) + } + ?? relay + } +} diff --git a/ios/MullvadREST/Relay/RelayCache.swift b/ios/MullvadREST/Relay/RelayCache.swift index 6cc9ddc61680..47ea44c6f5d2 100644 --- a/ios/MullvadREST/Relay/RelayCache.swift +++ b/ios/MullvadREST/Relay/RelayCache.swift @@ -14,6 +14,8 @@ public protocol RelayCacheProtocol { func write(record: CachedRelays) throws } +/// - Warning: `RelayCache` should not be used directly. It should be used through `IPOverrideWrapper` to have +/// ip overrides applied. public final class RelayCache: RelayCacheProtocol { private let fileCache: any FileCacheProtocol diff --git a/ios/MullvadREST/Relay/RelaySelector.swift b/ios/MullvadREST/Relay/RelaySelector.swift index bc0378c21789..6fc016d2c7e6 100644 --- a/ios/MullvadREST/Relay/RelaySelector.swift +++ b/ios/MullvadREST/Relay/RelaySelector.swift @@ -306,19 +306,6 @@ public struct RelaySelectorResult: Codable, Equatable { public var location: Location } -public protocol AnyRelay { - var hostname: String { get } - var owned: Bool { get } - var location: String { get } - var provider: String { get } - var weight: UInt64 { get } - var active: Bool { get } - var includeInCountry: Bool { get } -} - -extension REST.ServerRelay: AnyRelay {} -extension REST.BridgeRelay: AnyRelay {} - private struct RelayWithLocation { let relay: T let serverLocation: Location diff --git a/ios/MullvadSettings/IPOverride.swift b/ios/MullvadSettings/IPOverride.swift index db65a9086919..c25fdf4604af 100644 --- a/ios/MullvadSettings/IPOverride.swift +++ b/ios/MullvadSettings/IPOverride.swift @@ -16,6 +16,10 @@ public struct RelayOverrides: Codable { } } +public struct IPOverrideFormatError: LocalizedError { + public let errorDescription: String? +} + public struct IPOverride: Codable, Equatable { public let hostname: String public var ipv4Address: IPv4Address? @@ -27,7 +31,7 @@ public struct IPOverride: Codable, Equatable { case ipv6Address = "ipv6_addr_in" } - init(hostname: String, ipv4Address: IPv4Address?, ipv6Address: IPv6Address?) throws { + public init(hostname: String, ipv4Address: IPv4Address?, ipv6Address: IPv6Address?) throws { self.hostname = hostname self.ipv4Address = ipv4Address self.ipv6Address = ipv6Address @@ -49,7 +53,3 @@ public struct IPOverride: Codable, Equatable { } } } - -public struct IPOverrideFormatError: LocalizedError { - public let errorDescription: String? -} diff --git a/ios/MullvadSettings/IPOverrideRepository.swift b/ios/MullvadSettings/IPOverrideRepository.swift index 75e57f77ca13..867a1c077fd5 100644 --- a/ios/MullvadSettings/IPOverrideRepository.swift +++ b/ios/MullvadSettings/IPOverrideRepository.swift @@ -12,13 +12,13 @@ import MullvadLogging public protocol IPOverrideRepositoryProtocol { func add(_ overrides: [IPOverride]) func fetchAll() -> [IPOverride] - func fetchByHostname(_ hostname: String) -> IPOverride? func deleteAll() func parse(data: Data) throws -> [IPOverride] } public class IPOverrideRepository: IPOverrideRepositoryProtocol { private let logger = Logger(label: "IPOverrideRepository") + private let readWriteLock = NSLock() public init() {} @@ -54,13 +54,11 @@ public class IPOverrideRepository: IPOverrideRepositoryProtocol { return (try? readIpOverrides()) ?? [] } - public func fetchByHostname(_ hostname: String) -> IPOverride? { - return fetchAll().first { $0.hostname == hostname } - } - public func deleteAll() { do { - try SettingsManager.store.delete(key: .ipOverrides) + try readWriteLock.withLock { + try SettingsManager.store.delete(key: .ipOverrides) + } } catch { logger.error("Could not delete all overrides. \nError: \(error)") } @@ -74,17 +72,20 @@ public class IPOverrideRepository: IPOverrideRepositoryProtocol { } private func readIpOverrides() throws -> [IPOverride] { - let parser = makeParser() - let data = try SettingsManager.store.read(key: .ipOverrides) - - return try parser.parseUnversionedPayload(as: [IPOverride].self, from: data) + try readWriteLock.withLock { + let parser = makeParser() + let data = try SettingsManager.store.read(key: .ipOverrides) + return try parser.parseUnversionedPayload(as: [IPOverride].self, from: data) + } } private func writeIpOverrides(_ overrides: [IPOverride]) throws { let parser = makeParser() let data = try parser.produceUnversionedPayload(overrides) - try SettingsManager.store.write(data, for: .ipOverrides) + try readWriteLock.withLock { + try SettingsManager.store.write(data, for: .ipOverrides) + } } private func makeParser() -> SettingsParser { diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 5036a52bbdb5..17c25c9e920f 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -486,6 +486,8 @@ 7A3FD1B82AD54AE60042BEA6 /* TimeServerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BDEB9A2A98F58600F578F2 /* TimeServerProxy.swift */; }; 7A42DEC92A05164100B209BE /* SettingsInputCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A42DEC82A05164100B209BE /* SettingsInputCell.swift */; }; 7A516C2E2B6D357500BBD33D /* URL+Scoping.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A516C2D2B6D357500BBD33D /* URL+Scoping.swift */; }; + 7A516C3A2B7111A700BBD33D /* IPOverrideWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A516C392B7111A700BBD33D /* IPOverrideWrapper.swift */; }; + 7A516C3C2B712F0B00BBD33D /* IPOverrideWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A516C3B2B712F0B00BBD33D /* IPOverrideWrapperTests.swift */; }; 7A5869952B32E9C700640D27 /* LinkButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869942B32E9C700640D27 /* LinkButton.swift */; }; 7A5869972B32EA4500640D27 /* AppButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5869962B32EA4500640D27 /* AppButton.swift */; }; 7A58699B2B482FE200640D27 /* UITableViewCell+Disable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A58699A2B482FE200640D27 /* UITableViewCell+Disable.swift */; }; @@ -560,6 +562,8 @@ 7AD0AA1D2AD6A86700119E10 /* PacketTunnelActorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD0AA192AD69B6E00119E10 /* PacketTunnelActorProtocol.swift */; }; 7AD0AA1F2AD6C8B900119E10 /* URLRequestProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD0AA1E2AD6C8B900119E10 /* URLRequestProxyProtocol.swift */; }; 7AD0AA212AD6CB0000119E10 /* URLRequestProxyStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD0AA202AD6CB0000119E10 /* URLRequestProxyStub.swift */; }; + 7ADCB2D82B6A6EB300C88F89 /* AnyRelay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ADCB2D72B6A6EB300C88F89 /* AnyRelay.swift */; }; + 7ADCB2DA2B6A730400C88F89 /* IPOverrideRepositoryStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ADCB2D92B6A730400C88F89 /* IPOverrideRepositoryStub.swift */; }; 7AE044BB2A935726003915D8 /* Routing.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A88DCD02A8FABBE00D2FF0E /* Routing.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7AEF7F1A2AD00F52006FE45D /* AppMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AEF7F192AD00F52006FE45D /* AppMessageHandler.swift */; }; 7AF10EB22ADE859200C090B9 /* AlertViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF10EB12ADE859200C090B9 /* AlertViewController.swift */; }; @@ -1666,6 +1670,8 @@ 7A3FD1B42AD4465A0042BEA6 /* AppMessageHandlerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppMessageHandlerTests.swift; sourceTree = ""; }; 7A42DEC82A05164100B209BE /* SettingsInputCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInputCell.swift; sourceTree = ""; }; 7A516C2D2B6D357500BBD33D /* URL+Scoping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Scoping.swift"; sourceTree = ""; }; + 7A516C392B7111A700BBD33D /* IPOverrideWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideWrapper.swift; sourceTree = ""; }; + 7A516C3B2B712F0B00BBD33D /* IPOverrideWrapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideWrapperTests.swift; sourceTree = ""; }; 7A5869942B32E9C700640D27 /* LinkButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkButton.swift; sourceTree = ""; }; 7A5869962B32EA4500640D27 /* AppButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppButton.swift; sourceTree = ""; }; 7A58699A2B482FE200640D27 /* UITableViewCell+Disable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableViewCell+Disable.swift"; sourceTree = ""; }; @@ -1732,6 +1738,8 @@ 7AD0AA1B2AD6A63F00119E10 /* PacketTunnelActorStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelActorStub.swift; sourceTree = ""; }; 7AD0AA1E2AD6C8B900119E10 /* URLRequestProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLRequestProxyProtocol.swift; sourceTree = ""; }; 7AD0AA202AD6CB0000119E10 /* URLRequestProxyStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLRequestProxyStub.swift; sourceTree = ""; }; + 7ADCB2D72B6A6EB300C88F89 /* AnyRelay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyRelay.swift; sourceTree = ""; }; + 7ADCB2D92B6A730400C88F89 /* IPOverrideRepositoryStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideRepositoryStub.swift; sourceTree = ""; }; 7AEF7F192AD00F52006FE45D /* AppMessageHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppMessageHandler.swift; sourceTree = ""; }; 7AF10EB12ADE859200C090B9 /* AlertViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertViewController.swift; sourceTree = ""; }; 7AF10EB32ADE85BC00C090B9 /* RelayFilterCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RelayFilterCoordinator.swift; sourceTree = ""; }; @@ -2745,8 +2753,10 @@ 58B0A2A4238EE67E00BC001D /* Info.plist */, A9B6AC192ADE8FBB00F7802A /* InMemorySettingsStore.swift */, F07BF2572A26112D00042943 /* InputTextFormatterTests.swift */, + 7ADCB2D92B6A730400C88F89 /* IPOverrideRepositoryStub.swift */, 7A5869C22B5820CE00640D27 /* IPOverrideRepositoryTests.swift */, 7AB4CCB82B69097E006037F5 /* IPOverrideTests.swift */, + 7A516C3B2B712F0B00BBD33D /* IPOverrideWrapperTests.swift */, A9B6AC172ADE8F4300F7802A /* MigrationManagerTests.swift */, 58C3FA652A38549D006A450A /* MockFileCache.swift */, F09D04B42AE93CB6003D4F89 /* OutgoingConnectionProxy+Stub.swift */, @@ -3461,8 +3471,10 @@ F0DC779F2B2222D20087F09D /* Relay */ = { isa = PBXGroup; children = ( + 7ADCB2D72B6A6EB300C88F89 /* AnyRelay.swift */, 585DA87626B024A600B8C587 /* CachedRelays.swift */, F0DDE4272B220A15006B57A7 /* Haversine.swift */, + 7A516C392B7111A700BBD33D /* IPOverrideWrapper.swift */, F0DDE4292B220A15006B57A7 /* Midpoint.swift */, 5820675A26E6576800655B05 /* RelayCache.swift */, F0DDE4282B220A15006B57A7 /* RelaySelector.swift */, @@ -4438,6 +4450,7 @@ 06799AF228F98E4800ACD94E /* RESTAccessTokenManager.swift in Sources */, A90763B12B2857D50045ADF0 /* Socks5Endpoint.swift in Sources */, 06799AF328F98E4800ACD94E /* RESTAuthenticationProxy.swift in Sources */, + 7A516C3A2B7111A700BBD33D /* IPOverrideWrapper.swift in Sources */, F0DDE4142B220458006B57A7 /* ShadowSocksProxy.swift in Sources */, A90763B62B2857D50045ADF0 /* Socks5ConnectNegotiation.swift in Sources */, F06045E62B231EB700B2D37A /* URLSessionTransport.swift in Sources */, @@ -4459,6 +4472,7 @@ A90763B42B2857D50045ADF0 /* NWConnection+Extensions.swift in Sources */, F06045EA2B23217E00B2D37A /* ShadowsocksTransport.swift in Sources */, 06799AFC28F98EE300ACD94E /* AddressCache.swift in Sources */, + 7ADCB2D82B6A6EB300C88F89 /* AnyRelay.swift in Sources */, 06799AF028F98E4800ACD94E /* REST.swift in Sources */, 06799ADF28F98E4800ACD94E /* RESTDevicesProxy.swift in Sources */, 06799ADA28F98E4800ACD94E /* RESTResponseHandler.swift in Sources */, @@ -4574,6 +4588,7 @@ 7A6F2FA52AFA3CB2006D0856 /* AccountExpiryTests.swift in Sources */, A9A5FA082ACB05160083449F /* StorePaymentBlockObserver.swift in Sources */, A9E0317C2ACBFC7E0095D843 /* TunnelStore+Stubs.swift in Sources */, + 7A516C3C2B712F0B00BBD33D /* IPOverrideWrapperTests.swift in Sources */, A9A5FA092ACB05160083449F /* SendStoreReceiptOperation.swift in Sources */, A9A5FA0A2ACB05160083449F /* StorePaymentEvent.swift in Sources */, A9A5FA0B2ACB05160083449F /* StorePaymentManager.swift in Sources */, @@ -4624,6 +4639,7 @@ A9A5FA2F2ACB05160083449F /* FixedWidthIntegerArithmeticsTests.swift in Sources */, A9A5FA302ACB05160083449F /* InputTextFormatterTests.swift in Sources */, F0B0E6972AFE6E7E001DC66B /* XCTest+Async.swift in Sources */, + 7ADCB2DA2B6A730400C88F89 /* IPOverrideRepositoryStub.swift in Sources */, A9A5FA312ACB05160083449F /* MockFileCache.swift in Sources */, A9A5FA322ACB05160083449F /* RelayCacheTests.swift in Sources */, A9A5FA332ACB05160083449F /* RelaySelectorTests.swift in Sources */, diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index 7a1ddb1358aa..eab4b8bf5d08 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -45,6 +45,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD private(set) var accessMethodRepository = AccessMethodRepository() private(set) var shadowsocksLoader: ShadowsocksLoaderProtocol! private(set) var configuredTransportProvider: ProxyConfigurationTransportProvider! + private(set) var ipOverrideRepository = IPOverrideRepository() // MARK: - Application lifecycle @@ -66,8 +67,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD setUpProxies(containerURL: containerURL) - let relayCache = RelayCache(cacheDirectory: containerURL) - relayCacheTracker = RelayCacheTracker(relayCache: relayCache, application: application, apiProxy: apiProxy) + let ipOverrideWrapper = IPOverrideWrapper( + relayCache: RelayCache(cacheDirectory: containerURL), + ipOverrideRepository: ipOverrideRepository + ) + + relayCacheTracker = RelayCacheTracker( + relayCache: ipOverrideWrapper, + application: application, + apiProxy: apiProxy + ) addressCacheTracker = AddressCacheTracker(application: application, apiProxy: apiProxy, store: addressCache) @@ -93,7 +102,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD shadowsocksLoader = ShadowsocksLoader( shadowsocksCache: shadowsocksCache, - relayCache: relayCache, + relayCache: ipOverrideWrapper, constraintsUpdater: constraintsUpdater ) diff --git a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift index ed61054537fa..5db7d9799e5b 100644 --- a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift @@ -79,6 +79,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo private var outgoingConnectionService: OutgoingConnectionServiceHandling private var accessMethodRepository: AccessMethodRepositoryProtocol private let configuredTransportProvider: ProxyConfigurationTransportProvider + private let ipOverrideRepository: IPOverrideRepository private var outOfTimeTimer: Timer? @@ -96,7 +97,9 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo outgoingConnectionService: OutgoingConnectionServiceHandling, appPreferences: AppPreferencesDataSource, accessMethodRepository: AccessMethodRepositoryProtocol, - transportProvider: ProxyConfigurationTransportProvider + transportProvider: ProxyConfigurationTransportProvider, + ipOverrideRepository: IPOverrideRepository + ) { self.tunnelManager = tunnelManager self.storePaymentManager = storePaymentManager @@ -108,6 +111,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo self.outgoingConnectionService = outgoingConnectionService self.accessMethodRepository = accessMethodRepository self.configuredTransportProvider = transportProvider + self.ipOverrideRepository = ipOverrideRepository super.init() @@ -770,7 +774,8 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo navigationController: navigationController, interactorFactory: interactorFactory, accessMethodRepository: accessMethodRepository, - proxyConfigurationTester: configurationTester + proxyConfigurationTester: configurationTester, + ipOverrideRepository: ipOverrideRepository ) coordinator.didFinish = { [weak self] _ in diff --git a/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideCoordinator.swift b/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideCoordinator.swift index 53ffe916131c..7c444e781b2b 100644 --- a/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideCoordinator.swift @@ -14,7 +14,6 @@ import UIKit class IPOverrideCoordinator: Coordinator, Presenting, SettingsChildCoordinator { private let navigationController: UINavigationController private let interactor: IPOverrideInteractor - private let repository: IPOverrideRepositoryProtocol private lazy var ipOverrideViewController: IPOverrideViewController = { let viewController = IPOverrideViewController( @@ -29,11 +28,13 @@ class IPOverrideCoordinator: Coordinator, Presenting, SettingsChildCoordinator { navigationController } - init(navigationController: UINavigationController, repository: IPOverrideRepositoryProtocol) { + init( + navigationController: UINavigationController, + repository: IPOverrideRepositoryProtocol, + tunnelManager: TunnelManager + ) { self.navigationController = navigationController - self.repository = repository - - interactor = IPOverrideInteractor(repository: repository) + interactor = IPOverrideInteractor(repository: repository, tunnelManager: tunnelManager) } func start(animated: Bool) { diff --git a/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideInteractor.swift b/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideInteractor.swift index f400e7849f7b..75264eaf3010 100644 --- a/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideInteractor.swift +++ b/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideInteractor.swift @@ -14,6 +14,7 @@ import MullvadTypes struct IPOverrideInteractor { private let logger = Logger(label: "IPOverrideInteractor") private let repository: IPOverrideRepositoryProtocol + private let tunnelManager: TunnelManager private let statusSubject = CurrentValueSubject(.noImports) var statusPublisher: AnyPublisher { @@ -28,8 +29,9 @@ struct IPOverrideInteractor { } } - init(repository: IPOverrideRepositoryProtocol) { + init(repository: IPOverrideRepositoryProtocol, tunnelManager: TunnelManager) { self.repository = repository + self.tunnelManager = tunnelManager resetToDefaultStatus() } @@ -46,6 +48,8 @@ struct IPOverrideInteractor { func deleteAllOverrides() { repository.deleteAll() + + updateTunnel() resetToDefaultStatus() } @@ -60,11 +64,28 @@ struct IPOverrideInteractor { logger.error("Error importing ip overrides: \(error)") } + updateTunnel() + // After an import - successful or not - the UI should be reset back to default // state after a certain amount of time. resetToDefaultStatus(delay: .seconds(10)) } + private func updateTunnel() { + do { + try tunnelManager.refreshRelayCacheTracker() + } catch { + logger.error(error: error, message: "Could not refresh relay cache tracker.") + } + + switch tunnelManager.tunnelStatus.observedState { + case .connecting, .connected, .reconnecting: + tunnelManager.reconnectTunnel(selectNewRelay: true) + default: + break + } + } + private func resetToDefaultStatus(delay: Duration = .zero) { DispatchQueue.main.asyncAfter(deadline: .now() + delay.timeInterval) { statusSubject.send(defaultStatus) diff --git a/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideTextViewController.swift b/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideTextViewController.swift index 1687ff59c59b..ed0ae506c05b 100644 --- a/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideTextViewController.swift +++ b/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideTextViewController.swift @@ -23,7 +23,6 @@ class IPOverrideTextViewController: UIViewController { primaryAction: UIAction(handler: { [weak self] _ in self?.interactor.import(text: self?.textView.text ?? "") self?.dismiss(animated: true) - }) ) }() diff --git a/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideViewController.swift b/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideViewController.swift index b4ba782c0f04..7442914d7177 100644 --- a/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideViewController.swift +++ b/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideViewController.swift @@ -241,8 +241,10 @@ class IPOverrideViewController: UIViewController { extension IPOverrideViewController: UIDocumentPickerDelegate { func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { if let url = urls.first { - url.securelyScoped { [weak self] url in - self?.interactor.import(url: url) + url.securelyScoped { [weak self] scopedUrl in + if let scopedUrl { + self?.interactor.import(url: scopedUrl) + } } } } diff --git a/ios/MullvadVPN/Coordinators/Settings/SettingsCoordinator.swift b/ios/MullvadVPN/Coordinators/Settings/SettingsCoordinator.swift index 46452201526f..4ace0cd561ad 100644 --- a/ios/MullvadVPN/Coordinators/Settings/SettingsCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/Settings/SettingsCoordinator.swift @@ -43,6 +43,7 @@ final class SettingsCoordinator: Coordinator, Presentable, Presenting, SettingsV private var modalRoute: SettingsNavigationRoute? private let accessMethodRepository: AccessMethodRepositoryProtocol private let proxyConfigurationTester: ProxyConfigurationTesterProtocol + private let ipOverrideRepository: IPOverrideRepository let navigationController: UINavigationController @@ -68,12 +69,14 @@ final class SettingsCoordinator: Coordinator, Presentable, Presenting, SettingsV navigationController: UINavigationController, interactorFactory: SettingsInteractorFactory, accessMethodRepository: AccessMethodRepositoryProtocol, - proxyConfigurationTester: ProxyConfigurationTesterProtocol + proxyConfigurationTester: ProxyConfigurationTesterProtocol, + ipOverrideRepository: IPOverrideRepository ) { self.navigationController = navigationController self.interactorFactory = interactorFactory self.accessMethodRepository = accessMethodRepository self.proxyConfigurationTester = proxyConfigurationTester + self.ipOverrideRepository = ipOverrideRepository } /// Start the coordinator fllow. @@ -265,7 +268,8 @@ final class SettingsCoordinator: Coordinator, Presentable, Presenting, SettingsV case .ipOverride: return .childCoordinator(IPOverrideCoordinator( navigationController: navigationController, - repository: IPOverrideRepository() + repository: ipOverrideRepository, + tunnelManager: interactorFactory.tunnelManager )) case .faq: diff --git a/ios/MullvadVPN/Extensions/URL+Scoping.swift b/ios/MullvadVPN/Extensions/URL+Scoping.swift index 5f5b68637c7c..a957d4291ea5 100644 --- a/ios/MullvadVPN/Extensions/URL+Scoping.swift +++ b/ios/MullvadVPN/Extensions/URL+Scoping.swift @@ -9,10 +9,12 @@ import Foundation extension URL { - func securelyScoped(_ completionHandler: (Self) -> Void) { + func securelyScoped(_ completionHandler: (Self?) -> Void) { if startAccessingSecurityScopedResource() { completionHandler(self) stopAccessingSecurityScopedResource() + } else { + completionHandler(nil) } } } diff --git a/ios/MullvadVPN/RelayCacheTracker/RelayCacheTracker.swift b/ios/MullvadVPN/RelayCacheTracker/RelayCacheTracker.swift index eb9050318f25..67a81450204f 100644 --- a/ios/MullvadVPN/RelayCacheTracker/RelayCacheTracker.swift +++ b/ios/MullvadVPN/RelayCacheTracker/RelayCacheTracker.swift @@ -21,6 +21,7 @@ protocol RelayCacheTrackerProtocol { func getNextUpdateDate() -> Date func addObserver(_ observer: RelayCacheTrackerObserver) func removeObserver(_ observer: RelayCacheTrackerObserver) + func refreshCachedRelays() throws } final class RelayCacheTracker: RelayCacheTrackerProtocol { @@ -144,6 +145,20 @@ final class RelayCacheTracker: RelayCacheTrackerProtocol { } } + func refreshCachedRelays() throws { + let newCachedRelays = try cache.read() + + nslock.lock() + cachedRelays = newCachedRelays + nslock.unlock() + + DispatchQueue.main.async { + self.observerList.forEach { observer in + observer.relayCacheTracker(self, didUpdateCachedRelays: newCachedRelays) + } + } + } + func getNextUpdateDate() -> Date { nslock.lock() defer { nslock.unlock() } @@ -208,17 +223,8 @@ final class RelayCacheTracker: RelayCacheTrackerProtocol { updatedAt: Date() ) - nslock.lock() - cachedRelays = newCachedRelays - nslock.unlock() - try cache.write(record: newCachedRelays) - - DispatchQueue.main.async { - self.observerList.forEach { observer in - observer.relayCacheTracker(self, didUpdateCachedRelays: newCachedRelays) - } - } + try refreshCachedRelays() } private func scheduleRepeatingTimer(startTime: DispatchWallTime) { diff --git a/ios/MullvadVPN/SceneDelegate.swift b/ios/MullvadVPN/SceneDelegate.swift index c65b09881422..6105d06fdfb5 100644 --- a/ios/MullvadVPN/SceneDelegate.swift +++ b/ios/MullvadVPN/SceneDelegate.swift @@ -77,7 +77,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, SettingsMigrationUIHand ), appPreferences: AppPreferences(), accessMethodRepository: accessMethodRepository, - transportProvider: appDelegate.configuredTransportProvider + transportProvider: appDelegate.configuredTransportProvider, + ipOverrideRepository: appDelegate.ipOverrideRepository ) appCoordinator?.onShowSettings = { [weak self] in diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift index b0c55cdfad37..225c25d42216 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift @@ -536,6 +536,10 @@ final class TunnelManager: StorePaymentObserver { ) } + func refreshRelayCacheTracker() throws { + try relayCacheTracker.refreshCachedRelays() + } + // MARK: - Tunnel observeration /// Add tunnel observer. diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsInteractorFactory.swift b/ios/MullvadVPN/View controllers/Settings/SettingsInteractorFactory.swift index af9faeb52e12..31cf1b094499 100644 --- a/ios/MullvadVPN/View controllers/Settings/SettingsInteractorFactory.swift +++ b/ios/MullvadVPN/View controllers/Settings/SettingsInteractorFactory.swift @@ -11,10 +11,11 @@ import MullvadREST final class SettingsInteractorFactory { private let storePaymentManager: StorePaymentManager - private let tunnelManager: TunnelManager private let apiProxy: APIQuerying private let relayCacheTracker: RelayCacheTracker + let tunnelManager: TunnelManager + init( storePaymentManager: StorePaymentManager, tunnelManager: TunnelManager, diff --git a/ios/MullvadVPNTests/IPOverrideRepositoryStub.swift b/ios/MullvadVPNTests/IPOverrideRepositoryStub.swift new file mode 100644 index 000000000000..633bc44bdb5f --- /dev/null +++ b/ios/MullvadVPNTests/IPOverrideRepositoryStub.swift @@ -0,0 +1,33 @@ +// +// IPOverrideRepositoryStub.swift +// MullvadVPNTests +// +// Created by Jon Petersson on 2024-01-31. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import MullvadSettings + +struct IPOverrideRepositoryStub: IPOverrideRepositoryProtocol { + let overrides: [IPOverride] + + init(overrides: [IPOverride] = []) { + self.overrides = overrides + } + + func add(_ overrides: [IPOverride]) {} + + func fetchAll() -> [IPOverride] { + overrides + } + + func fetchByHostname(_ hostname: String) -> IPOverride? { + nil + } + + func deleteAll() {} + + func parse(data: Data) throws -> [IPOverride] { + overrides + } +} diff --git a/ios/MullvadVPNTests/IPOverrideRepositoryTests.swift b/ios/MullvadVPNTests/IPOverrideRepositoryTests.swift index 0e90a79e21fb..961d938b54ef 100644 --- a/ios/MullvadVPNTests/IPOverrideRepositoryTests.swift +++ b/ios/MullvadVPNTests/IPOverrideRepositoryTests.swift @@ -67,15 +67,6 @@ final class IPOverrideRepositoryTests: XCTestCase { XCTAssertTrue(storedOverrides.first?.ipv6Address == .broadcast) } - func testFetchOverrideByHostname() throws { - let hostname = "Host 1" - let override = try IPOverride(hostname: hostname, ipv4Address: .any, ipv6Address: nil) - repository.add([override]) - - let storedOverride = repository.fetchByHostname(hostname) - XCTAssertTrue(storedOverride?.hostname == hostname) - } - func testDeleteAllOverrides() throws { let override = try IPOverride(hostname: "Host 1", ipv4Address: .any, ipv6Address: nil) repository.add([override]) diff --git a/ios/MullvadVPNTests/IPOverrideWrapperTests.swift b/ios/MullvadVPNTests/IPOverrideWrapperTests.swift new file mode 100644 index 000000000000..bbaba178abce --- /dev/null +++ b/ios/MullvadVPNTests/IPOverrideWrapperTests.swift @@ -0,0 +1,102 @@ +// +// IPOverrideWrapperTests.swift +// MullvadVPNTests +// +// Created by Jon Petersson on 2024-02-05. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +@testable import MullvadREST +import MullvadSettings +import Network +import XCTest + +final class IPOverrideWrapperTests: XCTestCase { + func testOverrideServerRelayInCache() throws { + let relays = [ + mockServerRelay.override(ipv4AddrIn: .loopback, ipv6AddrIn: .broadcast), + mockServerRelay, + ] + + let fileCache = MockFileCache( + initialState: .exists(CachedRelays(relays: .mock(serverRelays: relays), updatedAt: .distantPast)) + ) + + let override = try IPOverride(hostname: "Host 1", ipv4Address: .loopback, ipv6Address: .broadcast) + + let overrideWrapper = IPOverrideWrapper( + relayCache: RelayCache(fileCache: fileCache), + ipOverrideRepository: IPOverrideRepositoryStub(overrides: [override]) + ) + + let storedCache = try overrideWrapper.read() + + // Assert that relay was overridden. + let host1 = storedCache.relays.wireguard.relays.first + XCTAssertEqual(host1?.ipv4AddrIn, .loopback) + XCTAssertEqual(host1?.ipv6AddrIn, .broadcast) + + // Assert that relay was NOT overridden. + let host2 = storedCache.relays.wireguard.relays.last + XCTAssertEqual(host2?.ipv4AddrIn, .any) + XCTAssertEqual(host2?.ipv6AddrIn, .any) + } + + func testOverrideBridgeRelayInCache() throws { + let relays = [ + mockBridgeRelay.override(ipv4AddrIn: .loopback), + mockBridgeRelay, + ] + + let fileCache = MockFileCache( + initialState: .exists(CachedRelays(relays: .mock(brideRelays: relays), updatedAt: .distantPast)) + ) + + let override = try IPOverride(hostname: "Host 1", ipv4Address: .loopback, ipv6Address: .broadcast) + + let overrideWrapper = IPOverrideWrapper( + relayCache: RelayCache(fileCache: fileCache), + ipOverrideRepository: IPOverrideRepositoryStub(overrides: [override]) + ) + + let storedCache = try overrideWrapper.read() + + // Assert that relay was overridden. + let host1 = storedCache.relays.bridge.relays.first + XCTAssertEqual(host1?.ipv4AddrIn, .loopback) + + // Assert that relay was NOT overridden. + let host2 = storedCache.relays.bridge.relays.last + XCTAssertEqual(host2?.ipv4AddrIn, .any) + } +} + +extension IPOverrideWrapperTests { + var mockServerRelay: REST.ServerRelay { + REST.ServerRelay( + hostname: "", + active: true, + owned: true, + location: "", + provider: "", + weight: 0, + ipv4AddrIn: .any, + ipv6AddrIn: .any, + publicKey: Data(), + includeInCountry: true + ) + } + + var mockBridgeRelay: REST.BridgeRelay { + REST.BridgeRelay( + hostname: "", + active: true, + owned: true, + location: "", + provider: "", + ipv4AddrIn: .any, + weight: 0, + includeInCountry: true + ) + } +} diff --git a/ios/MullvadVPNTests/RelayCacheTests.swift b/ios/MullvadVPNTests/RelayCacheTests.swift index 153f4738ee37..fffd9e33830e 100644 --- a/ios/MullvadVPNTests/RelayCacheTests.swift +++ b/ios/MullvadVPNTests/RelayCacheTests.swift @@ -10,7 +10,7 @@ import XCTest final class RelayCacheTests: XCTestCase { - func testCanReadCache() throws { + func testReadCache() throws { let fileCache = MockFileCache( initialState: .exists(CachedRelays(relays: .mock(), updatedAt: .distantPast)) ) @@ -20,7 +20,7 @@ final class RelayCacheTests: XCTestCase { XCTAssertEqual(fileCache.getState(), .exists(relays)) } - func testCanWriteCache() throws { + func testWriteCache() throws { let fileCache = MockFileCache( initialState: .exists(CachedRelays(relays: .mock(), updatedAt: .distantPast)) ) @@ -31,7 +31,7 @@ final class RelayCacheTests: XCTestCase { XCTAssertEqual(fileCache.getState(), .exists(newCachedRelays)) } - func testCanReadPrebundledRelaysWhenNoCacheIsStored() throws { + func testReadPrebundledRelaysWhenNoCacheIsStored() throws { let fileCache = MockFileCache(initialState: .fileNotFound) let cache = RelayCache(fileCache: fileCache) @@ -39,17 +39,20 @@ final class RelayCacheTests: XCTestCase { } } -private extension REST.ServerRelaysResponse { - static func mock() -> Self { +extension REST.ServerRelaysResponse { + static func mock( + serverRelays: [REST.ServerRelay] = [], + brideRelays: [REST.BridgeRelay] = [] + ) -> Self { REST.ServerRelaysResponse( locations: [:], wireguard: REST.ServerWireguardTunnels( ipv4Gateway: .loopback, ipv6Gateway: .loopback, portRanges: [], - relays: [] + relays: serverRelays ), - bridge: REST.ServerBridges(shadowsocks: [], relays: []) + bridge: REST.ServerBridges(shadowsocks: [], relays: brideRelays) ) } } diff --git a/ios/MullvadVPNTests/RelayCacheTracker+Stubs.swift b/ios/MullvadVPNTests/RelayCacheTracker+Stubs.swift index 4adcccececbf..b65aa0d4b791 100644 --- a/ios/MullvadVPNTests/RelayCacheTracker+Stubs.swift +++ b/ios/MullvadVPNTests/RelayCacheTracker+Stubs.swift @@ -30,4 +30,6 @@ struct RelayCacheTrackerStub: RelayCacheTrackerProtocol { func addObserver(_ observer: RelayCacheTrackerObserver) {} func removeObserver(_ observer: RelayCacheTrackerObserver) {} + + func refreshCachedRelays() throws {} } diff --git a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift index 3cbf52c6756a..d2aba984ca81 100644 --- a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift +++ b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift @@ -34,30 +34,19 @@ class PacketTunnelProvider: NEPacketTunnelProvider { let addressCache = REST.AddressCache(canWriteToCache: false, cacheDirectory: containerURL) addressCache.loadFromFile() - let relayCache = RelayCache(cacheDirectory: containerURL) - - let urlSession = REST.makeURLSession() - let urlSessionTransport = URLSessionTransport(urlSession: urlSession) - let shadowsocksCache = ShadowsocksConfigurationCache(cacheDirectory: containerURL) - - // This init cannot fail as long as the security group identifier is valid - let transportStrategy = TransportStrategy( - datasource: AccessMethodRepository(), - shadowsocksLoader: ShadowsocksLoader( - shadowsocksCache: shadowsocksCache, - relayCache: relayCache, - constraintsUpdater: constraintsUpdater - ) - ) - - let transportProvider = TransportProvider( - urlSessionTransport: urlSessionTransport, - addressCache: addressCache, - transportStrategy: transportStrategy + let ipOverrideWrapper = IPOverrideWrapper( + relayCache: RelayCache(cacheDirectory: containerURL), + ipOverrideRepository: IPOverrideRepository() ) super.init() + let transportProvider = setUpTransportProvider( + appContainerURL: containerURL, + ipOverrideWrapper: ipOverrideWrapper, + addressCache: addressCache + ) + let adapter = WgAdapter(packetTunnelProvider: self) let tunnelMonitor = TunnelMonitor( @@ -82,7 +71,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { tunnelMonitor: tunnelMonitor, defaultPathObserver: PacketTunnelPathObserver(packetTunnelProvider: self, eventQueue: internalQueue), blockedStateErrorMapper: BlockedStateErrorMapper(), - relaySelector: RelaySelectorWrapper(relayCache: relayCache), + relaySelector: RelaySelectorWrapper(relayCache: ipOverrideWrapper), settingsReader: SettingsReader(), protocolObfuscator: ProtocolObfuscator() ) @@ -141,6 +130,31 @@ class PacketTunnelProvider: NEPacketTunnelProvider { override func wake() { actor.onWake() } + + private func setUpTransportProvider( + appContainerURL: URL, + ipOverrideWrapper: IPOverrideWrapper, + addressCache: REST.AddressCache + ) -> TransportProvider { + let urlSession = REST.makeURLSession() + let urlSessionTransport = URLSessionTransport(urlSession: urlSession) + let shadowsocksCache = ShadowsocksConfigurationCache(cacheDirectory: appContainerURL) + + let transportStrategy = TransportStrategy( + datasource: AccessMethodRepository(), + shadowsocksLoader: ShadowsocksLoader( + shadowsocksCache: shadowsocksCache, + relayCache: ipOverrideWrapper, + constraintsUpdater: constraintsUpdater + ) + ) + + return TransportProvider( + urlSessionTransport: urlSessionTransport, + addressCache: addressCache, + transportStrategy: transportStrategy + ) + } } extension PacketTunnelProvider { diff --git a/ios/PacketTunnel/PacketTunnelProvider/RelaySelectorWrapper.swift b/ios/PacketTunnel/PacketTunnelProvider/RelaySelectorWrapper.swift index 5127e0a55b32..73f6fa267407 100644 --- a/ios/PacketTunnel/PacketTunnelProvider/RelaySelectorWrapper.swift +++ b/ios/PacketTunnel/PacketTunnelProvider/RelaySelectorWrapper.swift @@ -12,7 +12,7 @@ import MullvadTypes import PacketTunnelCore struct RelaySelectorWrapper: RelaySelectorProtocol { - let relayCache: RelayCache + let relayCache: RelayCacheProtocol func selectRelay( with constraints: RelayConstraints,