diff --git a/ios/MullvadSettings/WireGuardObfuscationSettings.swift b/ios/MullvadSettings/WireGuardObfuscationSettings.swift index ab2be2f96e0d..7259c970591f 100644 --- a/ios/MullvadSettings/WireGuardObfuscationSettings.swift +++ b/ios/MullvadSettings/WireGuardObfuscationSettings.swift @@ -8,19 +8,27 @@ import Foundation +/// Whether UDP-over-TCP obfuscation is enabled +/// +/// `.automatic` means an algorithm will decide whether to use it or not. public enum WireGuardObfuscationState: Codable { case automatic case on case off } +/// The port to select when using UDP-over-TCP obfuscation +/// +/// `.automatic` means an algorith will decide between using `port80` or `port5001` public enum WireGuardObfuscationPort: UInt16, Codable { case automatic = 0 case port80 = 80 case port5001 = 5001 + /// The `UInt16` representation of the port. + /// - Returns: `0` if `.automatic`, `80` or `5001` otherwise. public var portValue: UInt16 { - rawValue + self == .automatic ? 0 : rawValue } public init?(rawValue: UInt16) { diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 2bec1aba1358..820982df23da 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -95,7 +95,7 @@ 583DA21425FA4B5C00318683 /* LocationDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583DA21325FA4B5C00318683 /* LocationDataSource.swift */; }; 583FE01029C0F532006E85F9 /* CustomSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583FE00F29C0F532006E85F9 /* CustomSplitViewController.swift */; }; 583FE02429C1ACB3006E85F9 /* RESTCreateApplePaymentResponse+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06FAE67828F83CA50033DD93 /* RESTCreateApplePaymentResponse+Localization.swift */; }; - 584023222A406BF5007B27AC /* TunnelObfuscator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584023212A406BF5007B27AC /* TunnelObfuscator.swift */; }; + 584023222A406BF5007B27AC /* UDPOverTCPObfuscator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584023212A406BF5007B27AC /* UDPOverTCPObfuscator.swift */; }; 584023292A407F5F007B27AC /* libtunnel_obfuscator_proxy.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 584023282A407F5F007B27AC /* libtunnel_obfuscator_proxy.a */; }; 58421030282D8A3C00F24E46 /* UpdateAccountDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5842102F282D8A3C00F24E46 /* UpdateAccountDataOperation.swift */; }; 58421032282E42B000F24E46 /* UpdateDeviceDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58421031282E42B000F24E46 /* UpdateDeviceDataOperation.swift */; }; @@ -268,7 +268,7 @@ 58C7A4582A863FB90060C66F /* TunnelMonitorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C7A42C2A85067A0060C66F /* TunnelMonitorProtocol.swift */; }; 58C7A4592A863FB90060C66F /* WgStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A3BDAF28A1821A00C8C2C6 /* WgStats.swift */; }; 58C7A45B2A8640030060C66F /* PacketTunnelPathObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58225D272A84F23B0083D7F1 /* PacketTunnelPathObserver.swift */; }; - 58C7A45C2A8640490060C66F /* MullvadLogging.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58D223F3294C8FF00029F5F8 /* MullvadLogging.framework */; platformFilter = ios; }; + 58C7A45C2A8640490060C66F /* MullvadLogging.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58D223F3294C8FF00029F5F8 /* MullvadLogging.framework */; }; 58C7A4692A8643A90060C66F /* IPv4Header.h in Headers */ = {isa = PBXBuildFile; fileRef = 58218E1428B65058000C624F /* IPv4Header.h */; settings = {ATTRIBUTES = (Public, ); }; }; 58C7A46A2A8643A90060C66F /* ICMPHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = 58218E1628B65396000C624F /* ICMPHeader.h */; settings = {ATTRIBUTES = (Public, ); }; }; 58C7A4702A8649ED0060C66F /* PingerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C7A46F2A8649ED0060C66F /* PingerTests.swift */; }; @@ -506,11 +506,17 @@ A900E9BE2ACC654100C95F67 /* APIProxy+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A900E9BD2ACC654100C95F67 /* APIProxy+Stubs.swift */; }; A900E9C02ACC661900C95F67 /* AccessTokenManager+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A900E9BF2ACC661900C95F67 /* AccessTokenManager+Stubs.swift */; }; A917352129FAAA5200D5DCFD /* TransportStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A917352029FAAA5200D5DCFD /* TransportStrategyTests.swift */; }; + A91D78E32B03BDF200FCD5D3 /* TunnelObfuscation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5840231F2A406BF5007B27AC /* TunnelObfuscation.framework */; }; + A91D78E42B03C01600FCD5D3 /* MullvadSettings.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58B2FDD32AA71D2A003EB5C6 /* MullvadSettings.framework */; }; A93D13782A1F60A6001EB0B1 /* shadowsocks.h in Headers */ = {isa = PBXBuildFile; fileRef = 586F2BE129F6916F009E6924 /* shadowsocks.h */; settings = {ATTRIBUTES = (Private, ); }; }; A94D691A2ABAD66700413DD4 /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 58FE25E22AA72AE9003D1918 /* WireGuardKitTypes */; }; A94D691B2ABAD66700413DD4 /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 58FE25E72AA7399D003D1918 /* WireGuardKitTypes */; }; A95F86B72A1F53BA00245DAC /* URLSessionTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06FAE67C28F83CA50033DD93 /* URLSessionTransport.swift */; }; A95F86B82A1F547000245DAC /* ShadowsocksProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01F1FF1B29F06124007083C3 /* ShadowsocksProxy.swift */; }; + A97D25AE2B0BB18100946B2D /* ProtocolObfuscator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97D25AD2B0BB18100946B2D /* ProtocolObfuscator.swift */; }; + A97D25B02B0BB5C400946B2D /* ProtocolObfuscationStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97D25AF2B0BB5C400946B2D /* ProtocolObfuscationStub.swift */; }; + A97D25B22B0CB02D00946B2D /* ProtocolObfuscatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97D25B12B0CB02D00946B2D /* ProtocolObfuscatorTests.swift */; }; + A97D25B42B0CB59300946B2D /* TunnelObfuscationStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97D25B32B0CB59300946B2D /* TunnelObfuscationStub.swift */; }; A97D30172AE6B5E90045C0E4 /* StoredWgKeyData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97D30162AE6B5E90045C0E4 /* StoredWgKeyData.swift */; }; A97F1F442A1F4E1A00ECEFDE /* MullvadTransport.h in Headers */ = {isa = PBXBuildFile; fileRef = A97F1F432A1F4E1A00ECEFDE /* MullvadTransport.h */; settings = {ATTRIBUTES = (Public, ); }; }; A97F1F472A1F4E1A00ECEFDE /* MullvadTransport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A97F1F412A1F4E1A00ECEFDE /* MullvadTransport.framework */; }; @@ -981,6 +987,13 @@ remoteGlobalIDString = 7A88DCCD2A8FABBE00D2FF0E; remoteInfo = Routing; }; + A91D78E12B03BDE500FCD5D3 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 58CE5E58224146200008646E /* Project object */; + proxyType = 1; + remoteGlobalIDString = 5840231E2A406BF5007B27AC; + remoteInfo = TunnelObfuscation; + }; A97F1F452A1F4E1A00ECEFDE /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 58CE5E58224146200008646E /* Project object */; @@ -1265,7 +1278,7 @@ 583FE00F29C0F532006E85F9 /* CustomSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSplitViewController.swift; sourceTree = ""; }; 583FE01129C0F99A006E85F9 /* PresentationControllerDismissalInterceptor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationControllerDismissalInterceptor.swift; sourceTree = ""; }; 5840231F2A406BF5007B27AC /* TunnelObfuscation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TunnelObfuscation.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 584023212A406BF5007B27AC /* TunnelObfuscator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelObfuscator.swift; sourceTree = ""; }; + 584023212A406BF5007B27AC /* UDPOverTCPObfuscator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDPOverTCPObfuscator.swift; sourceTree = ""; }; 584023272A407679007B27AC /* tunnel_obfuscator_proxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = tunnel_obfuscator_proxy.h; path = "tunnel-obfuscator-proxy/include/tunnel_obfuscator_proxy.h"; sourceTree = ""; }; 584023282A407F5F007B27AC /* libtunnel_obfuscator_proxy.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libtunnel_obfuscator_proxy.a; path = "../target/x86_64-apple-ios/debug/libtunnel_obfuscator_proxy.a"; sourceTree = ""; }; 5840250322B11AB700E4CFEC /* MullvadEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadEndpoint.swift; sourceTree = ""; }; @@ -1617,6 +1630,10 @@ A9467E7E2A29DEFE000DC21F /* RelayCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayCacheTests.swift; sourceTree = ""; }; A9467E872A2DCD57000DC21F /* ShadowsocksConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowsocksConfiguration.swift; sourceTree = ""; }; A9467E8A2A2E0317000DC21F /* ShadowsocksConfigurationCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowsocksConfigurationCache.swift; sourceTree = ""; }; + A97D25AD2B0BB18100946B2D /* ProtocolObfuscator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtocolObfuscator.swift; sourceTree = ""; }; + A97D25AF2B0BB5C400946B2D /* ProtocolObfuscationStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtocolObfuscationStub.swift; sourceTree = ""; }; + A97D25B12B0CB02D00946B2D /* ProtocolObfuscatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtocolObfuscatorTests.swift; sourceTree = ""; }; + A97D25B32B0CB59300946B2D /* TunnelObfuscationStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelObfuscationStub.swift; sourceTree = ""; }; A97D30162AE6B5E90045C0E4 /* StoredWgKeyData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredWgKeyData.swift; sourceTree = ""; }; A97F1F412A1F4E1A00ECEFDE /* MullvadTransport.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MullvadTransport.framework; sourceTree = BUILT_PRODUCTS_DIR; }; A97F1F432A1F4E1A00ECEFDE /* MullvadTransport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MullvadTransport.h; sourceTree = ""; }; @@ -1761,6 +1778,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + A91D78E42B03C01600FCD5D3 /* MullvadSettings.framework in Frameworks */, + A91D78E32B03BDF200FCD5D3 /* TunnelObfuscation.framework in Frameworks */, 58238CB92AD57EC700768310 /* MullvadREST.framework in Frameworks */, A94D691A2ABAD66700413DD4 /* WireGuardKitTypes in Frameworks */, 58FE65952AB1D90600E53CB5 /* MullvadTypes.framework in Frameworks */, @@ -2324,7 +2343,7 @@ 589C6A7A2A45ACCA00DAD3EF /* Info.plist */, 584023272A407679007B27AC /* tunnel_obfuscator_proxy.h */, 589C6A7B2A45AE0100DAD3EF /* TunnelObfuscation.h */, - 584023212A406BF5007B27AC /* TunnelObfuscator.swift */, + 584023212A406BF5007B27AC /* UDPOverTCPObfuscator.swift */, ); path = TunnelObfuscation; sourceTree = ""; @@ -2402,6 +2421,7 @@ 586C14592AC4735F00245C01 /* PacketTunnelActor+Public.swift */, 583832262AC3193600EA2071 /* PacketTunnelActor+SleepCycle.swift */, 7AD0AA192AD69B6E00119E10 /* PacketTunnelActorProtocol.swift */, + A97D25AD2B0BB18100946B2D /* ProtocolObfuscator.swift */, 58E7A0312AA0715100C57861 /* Protocols */, 58ED3A132A7C199C0085CE65 /* StartOptions.swift */, 5824030C2A811B0000163DE8 /* State.swift */, @@ -2653,6 +2673,7 @@ 58C7A46F2A8649ED0060C66F /* PingerTests.swift */, 5838321C2AC1C54600EA2071 /* TaskSleepTests.swift */, 58092E532A8B832E00C3CC72 /* TunnelMonitorTests.swift */, + A97D25B12B0CB02D00946B2D /* ProtocolObfuscatorTests.swift */, ); path = PacketTunnelCoreTests; sourceTree = ""; @@ -2915,6 +2936,8 @@ 5838321A2AC1B18400EA2071 /* PacketTunnelActor+Mocks.swift */, 7AD0AA1B2AD6A63F00119E10 /* PacketTunnelActorStub.swift */, 7AD0AA202AD6CB0000119E10 /* URLRequestProxyStub.swift */, + A97D25AF2B0BB5C400946B2D /* ProtocolObfuscationStub.swift */, + A97D25B32B0CB59300946B2D /* TunnelObfuscationStub.swift */, ); path = Mocks; sourceTree = ""; @@ -3400,6 +3423,7 @@ buildRules = ( ); dependencies = ( + A91D78E22B03BDE500FCD5D3 /* PBXTargetDependency */, 58C7A45F2A8640490060C66F /* PBXTargetDependency */, 58FE65982AB1D90600E53CB5 /* PBXTargetDependency */, 58238CBC2AD57EC800768310 /* PBXTargetDependency */, @@ -4075,7 +4099,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 584023222A406BF5007B27AC /* TunnelObfuscator.swift in Sources */, + 584023222A406BF5007B27AC /* UDPOverTCPObfuscator.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4298,6 +4322,7 @@ 7AD0AA1F2AD6C8B900119E10 /* URLRequestProxyProtocol.swift in Sources */, 7A6B4F592AB8412E00123853 /* TunnelMonitorTimings.swift in Sources */, 58FE25DB2AA72A8F003D1918 /* StartOptions.swift in Sources */, + A97D25AE2B0BB18100946B2D /* ProtocolObfuscator.swift in Sources */, 583832212AC3174700EA2071 /* PacketTunnelActor+NetworkReachability.swift in Sources */, 58FE25D82AA72A8F003D1918 /* ConfigurationBuilder.swift in Sources */, 7AEF7F1A2AD00F52006FE45D /* AppMessageHandler.swift in Sources */, @@ -4335,12 +4360,15 @@ 58FE25F02AA77664003D1918 /* RelaySelectorStub.swift in Sources */, 581F23AF2A8CF94D00788AB6 /* PingerMock.swift in Sources */, 7A3FD1B62AD542110042BEA6 /* ServerRelaysResponse+Stubs.swift in Sources */, + A97D25B42B0CB59300946B2D /* TunnelObfuscationStub.swift in Sources */, + A97D25B02B0BB5C400946B2D /* ProtocolObfuscationStub.swift in Sources */, 7A3FD1B72AD54ABD0042BEA6 /* AnyTransport.swift in Sources */, 58FE25F22AA77674003D1918 /* SettingsReaderStub.swift in Sources */, 58F7753D2AB8473200425B47 /* BlockedStateErrorMapperStub.swift in Sources */, 58FE25D42AA729B5003D1918 /* PacketTunnelActorTests.swift in Sources */, 7A3FD1B52AD4465A0042BEA6 /* AppMessageHandlerTests.swift in Sources */, 58C7A4702A8649ED0060C66F /* PingerTests.swift in Sources */, + A97D25B22B0CB02D00946B2D /* ProtocolObfuscatorTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4831,7 +4859,6 @@ }; 58C7A45F2A8640490060C66F /* PBXTargetDependency */ = { isa = PBXTargetDependency; - platformFilter = ios; target = 58D223F2294C8FF00029F5F8 /* MullvadLogging */; targetProxy = 58C7A45E2A8640490060C66F /* PBXContainerItemProxy */; }; @@ -4976,6 +5003,11 @@ target = 7A88DCCD2A8FABBE00D2FF0E /* Routing */; targetProxy = 7ABCA5B52A9349F20044A708 /* PBXContainerItemProxy */; }; + A91D78E22B03BDE500FCD5D3 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 5840231E2A406BF5007B27AC /* TunnelObfuscation */; + targetProxy = A91D78E12B03BDE500FCD5D3 /* PBXContainerItemProxy */; + }; A97F1F462A1F4E1A00ECEFDE /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = A97F1F402A1F4E1A00ECEFDE /* MullvadTransport */; diff --git a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift index bac002e436dc..ecd2082d9502 100644 --- a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift +++ b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift @@ -170,7 +170,8 @@ final class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate { return SelectedRelay( endpoint: selectorResult.endpoint, hostname: selectorResult.relay.hostname, - location: selectorResult.location + location: selectorResult.location, + retryAttempts: 0 ) } diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift index 77e1eb24cdfc..bd1583b11fbe 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift @@ -823,16 +823,18 @@ final class TunnelManager: StorePaymentObserver { fileprivate func selectRelay() throws -> SelectedRelay { let cachedRelays = try relayCacheTracker.getCachedRelays() + let retryAttempts = tunnelStatus.observedState.connectionState?.connectionAttemptCount ?? 0 let selectorResult = try RelaySelector.evaluate( relays: cachedRelays.relays, constraints: settings.relayConstraints, - numberOfFailedAttempts: tunnelStatus.observedState.connectionState?.connectionAttemptCount ?? 0 + numberOfFailedAttempts: retryAttempts ) return SelectedRelay( endpoint: selectorResult.endpoint, hostname: selectorResult.relay.hostname, - location: selectorResult.location + location: selectorResult.location, + retryAttempts: retryAttempts ) } @@ -992,7 +994,6 @@ final class TunnelManager: StorePaymentObserver { let updatedConstraints = updatedSettings.relayConstraints let selectNewRelay = currentConstraints != updatedConstraints - // TODO: Handle using an obfuscator here self.setSettings(updatedSettings, persist: true) self.reconnectTunnel(selectNewRelay: selectNewRelay, completionHandler: nil) } diff --git a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift index 76e7eede9d23..25f37358e224 100644 --- a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift +++ b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift @@ -14,6 +14,7 @@ import MullvadTypes import NetworkExtension import PacketTunnelCore import RelayCache +import TunnelObfuscation class PacketTunnelProvider: NEPacketTunnelProvider { private let internalQueue = DispatchQueue(label: "PacketTunnel-internalQueue") @@ -72,6 +73,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { let devicesProxy = proxyFactory.createDevicesProxy() deviceChecker = DeviceChecker(accountsProxy: accountsProxy, devicesProxy: devicesProxy) + let obfuscator = ProtocolObfuscator() actor = PacketTunnelActor( timings: PacketTunnelActorTimings(), @@ -80,7 +82,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider { defaultPathObserver: PacketTunnelPathObserver(packetTunnelProvider: self, eventQueue: internalQueue), blockedStateErrorMapper: BlockedStateErrorMapper(), relaySelector: RelaySelectorWrapper(relayCache: relayCache), - settingsReader: SettingsReader() + settingsReader: SettingsReader(), + protocolObfuscator: obfuscator ) let urlRequestProxy = URLRequestProxy(dispatchQueue: internalQueue, transportProvider: transportProvider) diff --git a/ios/PacketTunnel/PacketTunnelProvider/RelaySelectorWrapper.swift b/ios/PacketTunnel/PacketTunnelProvider/RelaySelectorWrapper.swift index eb5bd6dcee4c..cedeb1ea7b3f 100644 --- a/ios/PacketTunnel/PacketTunnelProvider/RelaySelectorWrapper.swift +++ b/ios/PacketTunnel/PacketTunnelProvider/RelaySelectorWrapper.swift @@ -28,7 +28,8 @@ struct RelaySelectorWrapper: RelaySelectorProtocol { return SelectedRelay( endpoint: selectorResult.endpoint, hostname: selectorResult.relay.hostname, - location: selectorResult.location + location: selectorResult.location, + retryAttempts: connectionAttemptFailureCount ) } } diff --git a/ios/PacketTunnel/PacketTunnelProvider/SettingsReader.swift b/ios/PacketTunnel/PacketTunnelProvider/SettingsReader.swift index 48874253a568..10282b5ff32e 100644 --- a/ios/PacketTunnel/PacketTunnelProvider/SettingsReader.swift +++ b/ios/PacketTunnel/PacketTunnelProvider/SettingsReader.swift @@ -20,7 +20,8 @@ struct SettingsReader: SettingsReaderProtocol { privateKey: deviceData.wgKeyData.privateKey, interfaceAddresses: [deviceData.ipv4Address, deviceData.ipv6Address], relayConstraints: settings.relayConstraints, - dnsServers: settings.dnsSettings.selectedDNSServers + dnsServers: settings.dnsSettings.selectedDNSServers, + obfuscation: settings.wireGuardObfuscation ) } } diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift index 9e3598db656c..b624b29cd227 100644 --- a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift +++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift @@ -10,6 +10,7 @@ import Foundation import MullvadLogging import MullvadTypes import NetworkExtension +import TunnelObfuscation import WireGuardKitTypes /** @@ -43,6 +44,7 @@ public actor PacketTunnelActor { let blockedStateErrorMapper: BlockedStateErrorMapperProtocol let relaySelector: RelaySelectorProtocol let settingsReader: SettingsReaderProtocol + let protocolObfuscator: ProtocolObfuscation nonisolated let commandChannel = CommandChannel() @@ -53,7 +55,8 @@ public actor PacketTunnelActor { defaultPathObserver: DefaultPathObserverProtocol, blockedStateErrorMapper: BlockedStateErrorMapperProtocol, relaySelector: RelaySelectorProtocol, - settingsReader: SettingsReaderProtocol + settingsReader: SettingsReaderProtocol, + protocolObfuscator: ProtocolObfuscation ) { self.timings = timings self.tunnelAdapter = tunnelAdapter @@ -62,6 +65,7 @@ public actor PacketTunnelActor { self.blockedStateErrorMapper = blockedStateErrorMapper self.relaySelector = relaySelector self.settingsReader = settingsReader + self.protocolObfuscator = protocolObfuscator consumeCommands(channel: commandChannel) } @@ -218,7 +222,10 @@ extension PacketTunnelActor { - nextRelay: which relay should be selected next. - reason: reason for reconnect */ - private func tryStart(nextRelay: NextRelay = .random, reason: ReconnectReason = .userInitiated) async throws { + private func tryStart( + nextRelay: NextRelay = .random, + reason: ReconnectReason = .userInitiated + ) async throws { let settings: Settings = try settingsReader.read() guard let connectionState = try makeConnectionState(nextRelay: nextRelay, settings: settings, reason: reason), @@ -239,7 +246,13 @@ extension PacketTunnelActor { state = .reconnecting(connectionState) } - let endpoint = connectionState.selectedRelay.endpoint + var endpoint = connectionState.selectedRelay.endpoint + endpoint = protocolObfuscator.obfuscate( + endpoint, + settings: settings, + retryAttempts: connectionState.selectedRelay.retryAttempts + ) + let configurationBuilder = ConfigurationBuilder( privateKey: activeKey, interfaceAddresses: settings.interfaceAddresses, diff --git a/ios/PacketTunnelCore/Actor/ProtocolObfuscator.swift b/ios/PacketTunnelCore/Actor/ProtocolObfuscator.swift new file mode 100644 index 000000000000..467dd4c9df63 --- /dev/null +++ b/ios/PacketTunnelCore/Actor/ProtocolObfuscator.swift @@ -0,0 +1,67 @@ +// +// ProtocolObfuscator.swift +// PacketTunnelCore +// +// Created by Marco Nikic on 2023-11-20. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import MullvadTypes +import TunnelObfuscation + +public protocol ProtocolObfuscation { + func obfuscate(_ endpoint: MullvadEndpoint, settings: Settings, retryAttempts: UInt) -> MullvadEndpoint +} + +public class ProtocolObfuscator: ProtocolObfuscation { + var tunnelObfuscator: TunnelObfuscation? + + public init() {} + + /// Obfuscates a Mullvad endpoint based on a number of retry attempts. + /// + /// This retry logic used is explained at the following link + /// https://github.com/mullvad/mullvadvpn-app/blob/main/docs/relay-selector.md#default-constraints-for-tunnel-endpoints + /// - Parameters: + /// - endpoint: The endpoint to obfuscate. + /// - settings: Whether obfuscation should be used or not. + /// - retryAttempts: The number of times a connection was attempted to `endpoint` + /// - Returns: `endpoint` if obfuscation is disabled, or an obfuscated endpoint otherwise. + public func obfuscate(_ endpoint: MullvadEndpoint, settings: Settings, retryAttempts: UInt = 0) -> MullvadEndpoint { + var obfuscatedEndpoint = endpoint + let shouldObfuscate = switch settings.obfuscation.state { + case .automatic: + retryAttempts % 4 == 2 || retryAttempts % 4 == 3 + case .on: + true + case .off: + false + } + + guard shouldObfuscate else { + tunnelObfuscator = nil + return endpoint + } + var tcpPort = settings.obfuscation.port + if tcpPort == .automatic { + tcpPort = retryAttempts % 2 == 0 ? .port80 : .port5001 + } + let obfuscator = Obfuscator( + remoteAddress: obfuscatedEndpoint.ipv4Relay.ip, + tcpPort: tcpPort.portValue + ) + obfuscator.start() + tunnelObfuscator = obfuscator + + let localObfuscatorEndpoint = IPv4Endpoint(ip: .loopback, port: obfuscator.localUdpPort) + obfuscatedEndpoint = MullvadEndpoint( + ipv4Relay: localObfuscatorEndpoint, + ipv4Gateway: obfuscatedEndpoint.ipv4Gateway, + ipv6Gateway: obfuscatedEndpoint.ipv6Gateway, + publicKey: obfuscatedEndpoint.publicKey + ) + + return obfuscatedEndpoint + } +} diff --git a/ios/PacketTunnelCore/Actor/Protocols/RelaySelectorProtocol.swift b/ios/PacketTunnelCore/Actor/Protocols/RelaySelectorProtocol.swift index 7cd300353764..4224233f7026 100644 --- a/ios/PacketTunnelCore/Actor/Protocols/RelaySelectorProtocol.swift +++ b/ios/PacketTunnelCore/Actor/Protocols/RelaySelectorProtocol.swift @@ -17,18 +17,22 @@ public protocol RelaySelectorProtocol { /// Struct describing the selected relay. public struct SelectedRelay: Equatable, Codable { /// Selected relay endpoint. - public var endpoint: MullvadEndpoint + public let endpoint: MullvadEndpoint /// Relay hostname. - public var hostname: String + public let hostname: String /// Relay geo location. - public var location: Location + public let location: Location + + /// Number of retried attempts to connect to a relay. + public let retryAttempts: UInt /// Designated initializer. - public init(endpoint: MullvadEndpoint, hostname: String, location: Location) { + public init(endpoint: MullvadEndpoint, hostname: String, location: Location, retryAttempts: UInt) { self.endpoint = endpoint self.hostname = hostname self.location = location + self.retryAttempts = retryAttempts } } diff --git a/ios/PacketTunnelCore/Actor/Protocols/SettingsReaderProtocol.swift b/ios/PacketTunnelCore/Actor/Protocols/SettingsReaderProtocol.swift index dc3bfbcd0faa..9d75149b455b 100644 --- a/ios/PacketTunnelCore/Actor/Protocols/SettingsReaderProtocol.swift +++ b/ios/PacketTunnelCore/Actor/Protocols/SettingsReaderProtocol.swift @@ -7,6 +7,7 @@ // import Foundation +import MullvadSettings import MullvadTypes import Network import WireGuardKitTypes @@ -36,16 +37,21 @@ public struct Settings { /// DNS servers selected by user. public var dnsServers: SelectedDNSServers + /// Obfuscation settings + public var obfuscation: WireGuardObfuscationSettings + public init( privateKey: PrivateKey, interfaceAddresses: [IPAddressRange], relayConstraints: RelayConstraints, - dnsServers: SelectedDNSServers + dnsServers: SelectedDNSServers, + obfuscation: WireGuardObfuscationSettings ) { self.privateKey = privateKey self.interfaceAddresses = interfaceAddresses self.relayConstraints = relayConstraints self.dnsServers = dnsServers + self.obfuscation = obfuscation } } diff --git a/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift b/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift index d11dd478023c..79562eec280f 100644 --- a/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift +++ b/ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift @@ -88,7 +88,8 @@ final class AppMessageHandlerTests: XCTestCase { let selectedRelay = SelectedRelay( endpoint: selectorResult.endpoint, hostname: selectorResult.relay.hostname, - location: selectorResult.location + location: selectorResult.location, + retryAttempts: 0 ) _ = try? await appMessageHandler.handleAppMessage( diff --git a/ios/PacketTunnelCoreTests/Mocks/PacketTunnelActor+Mocks.swift b/ios/PacketTunnelCoreTests/Mocks/PacketTunnelActor+Mocks.swift index 3e01bec31c4d..c33f20457d28 100644 --- a/ios/PacketTunnelCoreTests/Mocks/PacketTunnelActor+Mocks.swift +++ b/ios/PacketTunnelCoreTests/Mocks/PacketTunnelActor+Mocks.swift @@ -34,7 +34,8 @@ extension PacketTunnelActor { defaultPathObserver: defaultPathObserver, blockedStateErrorMapper: blockedStateErrorMapper, relaySelector: relaySelector, - settingsReader: settingsReader + settingsReader: settingsReader, + protocolObfuscator: ProtocolObfuscationStub() ) } } diff --git a/ios/PacketTunnelCoreTests/Mocks/ProtocolObfuscationStub.swift b/ios/PacketTunnelCoreTests/Mocks/ProtocolObfuscationStub.swift new file mode 100644 index 000000000000..7426075b61d8 --- /dev/null +++ b/ios/PacketTunnelCoreTests/Mocks/ProtocolObfuscationStub.swift @@ -0,0 +1,17 @@ +// +// ProtocolObfuscationStub.swift +// PacketTunnelCoreTests +// +// Created by Marco Nikic on 2023-11-20. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +@testable import MullvadTypes +@testable import PacketTunnelCore + +struct ProtocolObfuscationStub: ProtocolObfuscation { + func obfuscate(_ endpoint: MullvadEndpoint, settings: Settings, retryAttempts: UInt) -> MullvadEndpoint { + endpoint + } +} diff --git a/ios/PacketTunnelCoreTests/Mocks/RelaySelectorStub.swift b/ios/PacketTunnelCoreTests/Mocks/RelaySelectorStub.swift index c601c860d4e7..4922c080c584 100644 --- a/ios/PacketTunnelCoreTests/Mocks/RelaySelectorStub.swift +++ b/ios/PacketTunnelCoreTests/Mocks/RelaySelectorStub.swift @@ -44,7 +44,7 @@ extension RelaySelectorStub { cityCode: "got", latitude: 0, longitude: 0 - ) + ), retryAttempts: 0 ) } } diff --git a/ios/PacketTunnelCoreTests/Mocks/SettingsReaderStub.swift b/ios/PacketTunnelCoreTests/Mocks/SettingsReaderStub.swift index 1893f4363d31..c95133b09108 100644 --- a/ios/PacketTunnelCoreTests/Mocks/SettingsReaderStub.swift +++ b/ios/PacketTunnelCoreTests/Mocks/SettingsReaderStub.swift @@ -7,6 +7,7 @@ // import Foundation +@testable import MullvadSettings import MullvadTypes import PacketTunnelCore import WireGuardKitTypes @@ -27,7 +28,8 @@ extension SettingsReaderStub { privateKey: PrivateKey(), interfaceAddresses: [IPAddressRange(from: "127.0.0.1/32")!], relayConstraints: RelayConstraints(), - dnsServers: .gateway + dnsServers: .gateway, + obfuscation: WireGuardObfuscationSettings(state: .off, port: .automatic) ) return SettingsReaderStub { diff --git a/ios/PacketTunnelCoreTests/Mocks/TunnelObfuscationStub.swift b/ios/PacketTunnelCoreTests/Mocks/TunnelObfuscationStub.swift new file mode 100644 index 000000000000..e07011598659 --- /dev/null +++ b/ios/PacketTunnelCoreTests/Mocks/TunnelObfuscationStub.swift @@ -0,0 +1,24 @@ +// +// TunnelObfuscationStub.swift +// PacketTunnelCoreTests +// +// Created by Marco Nikic on 2023-11-21. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import Network +@testable import TunnelObfuscation + +struct TunnelObfuscationStub: TunnelObfuscation { + let remotePort: UInt16 + init(remoteAddress: IPAddress, tcpPort: UInt16) { + remotePort = tcpPort + } + + func start() {} + + func stop() {} + + var localUdpPort: UInt16 { 42 } +} diff --git a/ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift b/ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift index 65c0fbcf09cd..dccadc344285 100644 --- a/ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift +++ b/ios/PacketTunnelCoreTests/PacketTunnelActorTests.swift @@ -7,7 +7,7 @@ // import Combine -import MullvadSettings +@testable import MullvadSettings import MullvadTypes import Network @testable import PacketTunnelCore @@ -206,7 +206,8 @@ final class PacketTunnelActorTests: XCTestCase { privateKey: PrivateKey(), interfaceAddresses: [IPAddressRange(from: "127.0.0.1/32")!], relayConstraints: RelayConstraints(), - dnsServers: .gateway + dnsServers: .gateway, + obfuscation: WireGuardObfuscationSettings(state: .off, port: .automatic) ) } } diff --git a/ios/PacketTunnelCoreTests/ProtocolObfuscatorTests.swift b/ios/PacketTunnelCoreTests/ProtocolObfuscatorTests.swift new file mode 100644 index 000000000000..dd644a74e14b --- /dev/null +++ b/ios/PacketTunnelCoreTests/ProtocolObfuscatorTests.swift @@ -0,0 +1,116 @@ +// +// ProtocolObfuscatorTests.swift +// PacketTunnelCoreTests +// +// Created by Marco Nikic on 2023-11-21. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +@testable import MullvadSettings +@testable import MullvadTypes +import Network +@testable import PacketTunnelCore +@testable import WireGuardKitTypes +import XCTest + +final class ProtocolObfuscatorTests: XCTestCase { + var obfuscator: ProtocolObfuscator! + var address: IPv4Address! + var gateway: IPv4Address! + var v4Endpoint: IPv4Endpoint! + var endpoint: MullvadEndpoint! + + override func setUpWithError() throws { + obfuscator = ProtocolObfuscator() + address = try XCTUnwrap(IPv4Address("1.2.3.4")) + gateway = try XCTUnwrap(IPv4Address("5.6.7.8")) + v4Endpoint = IPv4Endpoint(ip: address, port: 56) + endpoint = MullvadEndpoint( + ipv4Relay: v4Endpoint, + ipv4Gateway: gateway, + ipv6Gateway: .any, + publicKey: Data() + ) + } + + func testObfuscateOffDoesNotChangeEndpoint() { + let settings = settings(.off, obfuscationPort: .automatic) + let nonObfuscatedEndpoint = obfuscator.obfuscate(endpoint, settings: settings) + + XCTAssertEqual(endpoint, nonObfuscatedEndpoint) + } + + func testObfuscateOnPort80() throws { + let settings = settings(.on, obfuscationPort: .port80) + let obfuscatedEndpoint = obfuscator.obfuscate(endpoint, settings: settings) + let obfuscationProtocol = try XCTUnwrap(obfuscator.tunnelObfuscator as? TunnelObfuscationStub) + + validate(obfuscatedEndpoint, against: obfuscationProtocol, expect: .port80) + } + + func testObfuscateOnPort5001() throws { + let settings = settings(.on, obfuscationPort: .port5001) + let obfuscatedEndpoint = obfuscator.obfuscate(endpoint, settings: settings) + let obfuscationProtocol = try XCTUnwrap(obfuscator.tunnelObfuscator as? TunnelObfuscationStub) + + validate(obfuscatedEndpoint, against: obfuscationProtocol, expect: .port5001) + } + + func testObfuscateOnPortAutomaticIsPort80OnEvenRetryAttempts() throws { + let settings = settings(.on, obfuscationPort: .automatic) + let obfuscatedEndpoint = obfuscator.obfuscate(endpoint, settings: settings, retryAttempts: 2) + let obfuscationProtocol = try XCTUnwrap(obfuscator.tunnelObfuscator as? TunnelObfuscationStub) + + validate(obfuscatedEndpoint, against: obfuscationProtocol, expect: .port80) + } + + func testObfuscateOnPortAutomaticIsPort5001OnOddRetryAttempts() throws { + let settings = settings(.on, obfuscationPort: .automatic) + let obfuscatedEndpoint = obfuscator.obfuscate(endpoint, settings: settings, retryAttempts: 3) + let obfuscationProtocol = try XCTUnwrap(obfuscator.tunnelObfuscator as? TunnelObfuscationStub) + + validate(obfuscatedEndpoint, against: obfuscationProtocol, expect: .port5001) + } + + func testObfuscateAutomaticIsPort80EveryThirdAttempts() throws { + let settings = settings(.automatic, obfuscationPort: .automatic) + let obfuscatedEndpoint = obfuscator.obfuscate(endpoint, settings: settings, retryAttempts: 6) + let obfuscationProtocol = try XCTUnwrap(obfuscator.tunnelObfuscator as? TunnelObfuscationStub) + + validate(obfuscatedEndpoint, against: obfuscationProtocol, expect: .port80) + } + + func testObfuscateAutomaticIsPort5001EveryFourthAttempts() throws { + let settings = settings(.automatic, obfuscationPort: .automatic) + let obfuscatedEndpoint = obfuscator.obfuscate(endpoint, settings: settings, retryAttempts: 7) + let obfuscationProtocol = try XCTUnwrap(obfuscator.tunnelObfuscator as? TunnelObfuscationStub) + + validate(obfuscatedEndpoint, against: obfuscationProtocol, expect: .port5001) + } + + private func validate( + _ obfuscatedEndpoint: MullvadEndpoint, + against obfuscationProtocol: TunnelObfuscationStub, + expect port: WireGuardObfuscationPort + ) { + XCTAssertEqual(obfuscatedEndpoint.ipv4Relay.ip, .loopback) + XCTAssertEqual(obfuscatedEndpoint.ipv4Relay.port, obfuscationProtocol.localUdpPort) + XCTAssertEqual(obfuscationProtocol.remotePort, port.portValue) + } + + private func settings( + _ obfuscationState: WireGuardObfuscationState, + obfuscationPort: WireGuardObfuscationPort + ) -> Settings { + Settings( + privateKey: PrivateKey(), + interfaceAddresses: [IPAddressRange(from: "127.0.0.1/32")!], + relayConstraints: RelayConstraints(), + dnsServers: .gateway, + obfuscation: WireGuardObfuscationSettings( + state: obfuscationState, + port: obfuscationPort + ) + ) + } +} diff --git a/ios/TunnelObfuscation/TunnelObfuscator.swift b/ios/TunnelObfuscation/UDPOverTCPObfuscator.swift similarity index 88% rename from ios/TunnelObfuscation/TunnelObfuscator.swift rename to ios/TunnelObfuscation/UDPOverTCPObfuscator.swift index f84ea7306740..9af841056134 100644 --- a/ios/TunnelObfuscation/TunnelObfuscator.swift +++ b/ios/TunnelObfuscation/UDPOverTCPObfuscator.swift @@ -10,11 +10,18 @@ import Foundation import Network import TunnelObfuscatorProxy +public protocol TunnelObfuscation { + init(remoteAddress: IPAddress, tcpPort: UInt16) + func start() + func stop() + var localUdpPort: UInt16 { get } +} + /// Class that implements UDP over TCP obfuscation by accepting traffic on a local UDP port and proxying it over TCP to the remote endpoint. -public final class TunnelObfuscator { +public final class UDPOverTCPObfuscator: TunnelObfuscation { private let stateLock = NSLock() private let remoteAddress: IPAddress - private let tcpPort: UInt16 + internal let tcpPort: UInt16 private var proxyHandle = ProxyHandle(context: nil, port: 0) private var isStarted = false diff --git a/ios/TunnelObfuscationTests/TunnelObfuscationTests.swift b/ios/TunnelObfuscationTests/TunnelObfuscationTests.swift index 0774ef9829f2..7edbc6d238f0 100644 --- a/ios/TunnelObfuscationTests/TunnelObfuscationTests.swift +++ b/ios/TunnelObfuscationTests/TunnelObfuscationTests.swift @@ -20,7 +20,7 @@ final class TunnelObfuscationTests: XCTestCase { let tcpListener = try TCPUnsafeListener() try await tcpListener.start() - let obfuscator = TunnelObfuscator(remoteAddress: IPv4Address.loopback, tcpPort: tcpListener.listenPort) + let obfuscator = UDPOverTCPObfuscator(remoteAddress: IPv4Address.loopback, tcpPort: tcpListener.listenPort) obfuscator.start() // Accept incoming connections