diff --git a/ios/MullvadREST/Extensions/UInt+Counting.swift b/ios/MullvadREST/Extensions/UInt+Counting.swift new file mode 100644 index 000000000000..c822e2499185 --- /dev/null +++ b/ios/MullvadREST/Extensions/UInt+Counting.swift @@ -0,0 +1,24 @@ +// +// UInt+Counting.swift +// MullvadVPN +// +// Created by Jon Petersson on 2024-11-05. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +extension UInt { + /// Determines whether a number has a specific order in a given set. + /// Eg. `6.isOrdered(nth: 3, forEverySetOf: 4)` -> "Is a 6 ordered third in an arbitrary + /// amount of sets of four?". The result of this is `true`, since in a range of eg. 0-7 a six + /// would be considered third if the range was divided into sets of 4. + public func isOrdered(nth: UInt, forEverySetOf set: UInt) -> Bool { + guard nth > 0, set > 0 else { + assertionFailure("Both 'nth' and 'set' must be positive") + return false + } + + return self % set == nth - 1 + } +} diff --git a/ios/MullvadREST/Relay/ObfuscationMethodSelector.swift b/ios/MullvadREST/Relay/ObfuscationMethodSelector.swift new file mode 100644 index 000000000000..4803d38139ee --- /dev/null +++ b/ios/MullvadREST/Relay/ObfuscationMethodSelector.swift @@ -0,0 +1,30 @@ +// +// ObfuscationMethodSelector.swift +// MullvadREST +// +// Created by Jon Petersson on 2024-11-01. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import MullvadSettings + +public struct ObfuscationMethodSelector { + /// 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 + public static func obfuscationMethodBy( + connectionAttemptCount: UInt, + tunnelSettings: LatestTunnelSettings + ) -> WireGuardObfuscationState { + if tunnelSettings.wireGuardObfuscation.state == .automatic { + if connectionAttemptCount.isOrdered(nth: 3, forEverySetOf: 4) { + .shadowsocks + } else if connectionAttemptCount.isOrdered(nth: 4, forEverySetOf: 4) { + .udpOverTcp + } else { + .off + } + } else { + tunnelSettings.wireGuardObfuscation.state + } + } +} diff --git a/ios/MullvadREST/Relay/ObfuscatorPortSelector.swift b/ios/MullvadREST/Relay/ObfuscatorPortSelector.swift new file mode 100644 index 000000000000..05b098b7f222 --- /dev/null +++ b/ios/MullvadREST/Relay/ObfuscatorPortSelector.swift @@ -0,0 +1,128 @@ +// +// ObfuscatorPortSelector.swift +// MullvadVPN +// +// Created by Jon Petersson on 2024-11-01. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import MullvadSettings +import MullvadTypes + +struct ObfuscatorPortSelectorResult { + let relays: REST.ServerRelaysResponse + let port: RelayConstraint +} + +struct ObfuscatorPortSelector { + let relays: REST.ServerRelaysResponse + + func obfuscate( + tunnelSettings: LatestTunnelSettings, + connectionAttemptCount: UInt + ) throws -> ObfuscatorPortSelectorResult { + var relays = relays + var port = tunnelSettings.relayConstraints.port + let obfuscationMethod = ObfuscationMethodSelector.obfuscationMethodBy( + connectionAttemptCount: connectionAttemptCount, + tunnelSettings: tunnelSettings + ) + + switch obfuscationMethod { + case .udpOverTcp: + port = obfuscateUdpOverTcpPort( + tunnelSettings: tunnelSettings, + connectionAttemptCount: connectionAttemptCount + ) + case .shadowsocks: + relays = obfuscateShadowsocksRelays(tunnelSettings: tunnelSettings) + port = obfuscateShadowsocksPort( + tunnelSettings: tunnelSettings, + shadowsocksPortRanges: relays.wireguard.shadowsocksPortRanges + ) + default: + break + } + + return ObfuscatorPortSelectorResult(relays: relays, port: port) + } + + private func obfuscateShadowsocksRelays(tunnelSettings: LatestTunnelSettings) -> REST.ServerRelaysResponse { + let relays = relays + let wireGuardObfuscation = tunnelSettings.wireGuardObfuscation + + return wireGuardObfuscation.state == .shadowsocks + ? filterShadowsocksRelays(from: relays, for: wireGuardObfuscation.shadowsocksPort) + : relays + } + + private func filterShadowsocksRelays( + from relays: REST.ServerRelaysResponse, + for port: WireGuardObfuscationShadowsockPort + ) -> REST.ServerRelaysResponse { + let portRanges = RelaySelector.parseRawPortRanges(relays.wireguard.shadowsocksPortRanges) + + // If the selected port is within the shadowsocks port ranges we can select from all relays. + guard + case let .custom(port) = port, + !portRanges.contains(where: { $0.contains(port) }) + else { + return relays + } + + let filteredRelays = relays.wireguard.relays.filter { relay in + relay.shadowsocksExtraAddrIn != nil + } + + return REST.ServerRelaysResponse( + locations: relays.locations, + wireguard: REST.ServerWireguardTunnels( + ipv4Gateway: relays.wireguard.ipv4Gateway, + ipv6Gateway: relays.wireguard.ipv6Gateway, + portRanges: relays.wireguard.portRanges, + relays: filteredRelays, + shadowsocksPortRanges: relays.wireguard.shadowsocksPortRanges + ), + bridge: relays.bridge + ) + } + + private func obfuscateUdpOverTcpPort( + tunnelSettings: LatestTunnelSettings, + connectionAttemptCount: UInt + ) -> RelayConstraint { + switch tunnelSettings.wireGuardObfuscation.udpOverTcpPort { + case .automatic: + (connectionAttemptCount % 2 == 0) ? .only(80) : .only(5001) + case .port5001: + .only(5001) + case .port80: + .only(80) + } + } + + private func obfuscateShadowsocksPort( + tunnelSettings: LatestTunnelSettings, + shadowsocksPortRanges: [[UInt16]] + ) -> RelayConstraint { + let wireGuardObfuscation = tunnelSettings.wireGuardObfuscation + + let shadowsockPort: () -> UInt16? = { + switch wireGuardObfuscation.shadowsocksPort { + case let .custom(port): + port + default: + RelaySelector.pickRandomPort(rawPortRanges: shadowsocksPortRanges) + } + } + + guard + wireGuardObfuscation.state == .shadowsocks, + let port = shadowsockPort() + else { + return tunnelSettings.relayConstraints.port + } + + return .only(port) + } +} diff --git a/ios/MullvadREST/Relay/RelaySelector.swift b/ios/MullvadREST/Relay/RelaySelector.swift index 98279daca570..20e615353236 100644 --- a/ios/MullvadREST/Relay/RelaySelector.swift +++ b/ios/MullvadREST/Relay/RelaySelector.swift @@ -30,36 +30,11 @@ public enum RelaySelector { } } - // MARK: - private - static func pickRandomRelayByWeight(relays: [RelayWithLocation]) -> RelayWithLocation? { rouletteSelection(relays: relays, weightFunction: { relayWithLocation in relayWithLocation.relay.weight }) } - private static func pickRandomPort(rawPortRanges: [[UInt16]]) -> UInt16? { - let portRanges = parseRawPortRanges(rawPortRanges) - let portAmount = portRanges.reduce(0) { partialResult, closedRange in - partialResult + closedRange.count - } - - guard var portIndex = (0 ..< portAmount).randomElement() else { - return nil - } - - for range in portRanges { - if portIndex < range.count { - return UInt16(portIndex) + range.lowerBound - } else { - portIndex -= range.count - } - } - - assertionFailure("Port selection algorithm is broken!") - - return nil - } - static func rouletteSelection(relays: [T], weightFunction: (T) -> UInt64) -> T? { let totalWeight = relays.map { weightFunction($0) }.reduce(0) { accumulated, weight in accumulated + weight @@ -97,40 +72,6 @@ public enum RelaySelector { } } - private static func makeRelayWithLocationFrom( - _ serverLocation: REST.ServerLocation, - relay: T - ) -> RelayWithLocation? { - let locationComponents = relay.location.split(separator: "-") - guard locationComponents.count > 1 else { return nil } - - let location = Location( - country: serverLocation.country, - countryCode: String(locationComponents[0]), - city: serverLocation.city, - cityCode: String(locationComponents[1]), - latitude: serverLocation.latitude, - longitude: serverLocation.longitude - ) - - return RelayWithLocation(relay: relay, serverLocation: location) - } - - private static func parseRawPortRanges(_ rawPortRanges: [[UInt16]]) -> [ClosedRange] { - rawPortRanges.compactMap { inputRange -> ClosedRange? in - guard inputRange.count == 2 else { return nil } - - let startPort = inputRange[0] - let endPort = inputRange[1] - - if startPort <= endPort { - return startPort ... endPort - } else { - return nil - } - } - } - /// Produce a list of `RelayWithLocation` items satisfying the given constraints static func applyConstraints( _ relayConstraint: RelayConstraint, @@ -166,6 +107,65 @@ public enum RelaySelector { } } + // MARK: - private + + static func parseRawPortRanges(_ rawPortRanges: [[UInt16]]) -> [ClosedRange] { + rawPortRanges.compactMap { inputRange -> ClosedRange? in + guard inputRange.count == 2 else { return nil } + + let startPort = inputRange[0] + let endPort = inputRange[1] + + if startPort <= endPort { + return startPort ... endPort + } else { + return nil + } + } + } + + static func pickRandomPort(rawPortRanges: [[UInt16]]) -> UInt16? { + let portRanges = parseRawPortRanges(rawPortRanges) + let portAmount = portRanges.reduce(0) { partialResult, closedRange in + partialResult + closedRange.count + } + + guard var portIndex = (0 ..< portAmount).randomElement() else { + return nil + } + + for range in portRanges { + if portIndex < range.count { + return UInt16(portIndex) + range.lowerBound + } else { + portIndex -= range.count + } + } + + assertionFailure("Port selection algorithm is broken!") + + return nil + } + + private static func makeRelayWithLocationFrom( + _ serverLocation: REST.ServerLocation, + relay: T + ) -> RelayWithLocation? { + let locationComponents = relay.location.split(separator: "-") + guard locationComponents.count > 1 else { return nil } + + let location = Location( + country: serverLocation.country, + countryCode: String(locationComponents[0]), + city: serverLocation.city, + cityCode: String(locationComponents[1]), + latitude: serverLocation.latitude, + longitude: serverLocation.longitude + ) + + return RelayWithLocation(relay: relay, serverLocation: location) + } + private static func filterByActive( relays: [RelayWithLocation] ) throws -> [RelayWithLocation] { diff --git a/ios/MullvadREST/Relay/RelaySelectorWrapper.swift b/ios/MullvadREST/Relay/RelaySelectorWrapper.swift index 03fd113ecb00..48f5bf87d2ff 100644 --- a/ios/MullvadREST/Relay/RelaySelectorWrapper.swift +++ b/ios/MullvadREST/Relay/RelaySelectorWrapper.swift @@ -20,20 +20,28 @@ public final class RelaySelectorWrapper: RelaySelectorProtocol { tunnelSettings: LatestTunnelSettings, connectionAttemptCount: UInt ) throws -> SelectedRelays { - let relays = try relayCache.read().relays + let obfuscationResult = try ObfuscatorPortSelector( + relays: try relayCache.read().relays + ).obfuscate( + tunnelSettings: tunnelSettings, + connectionAttemptCount: connectionAttemptCount + ) + + var constraints = tunnelSettings.relayConstraints + constraints.port = obfuscationResult.port return switch tunnelSettings.tunnelMultihopState { case .off: try SinglehopPicker( - relays: relays, - constraints: tunnelSettings.relayConstraints, + relays: obfuscationResult.relays, + constraints: constraints, connectionAttemptCount: connectionAttemptCount, daitaSettings: tunnelSettings.daita ).pick() case .on: try MultihopPicker( - relays: relays, - constraints: tunnelSettings.relayConstraints, + relays: obfuscationResult.relays, + constraints: constraints, connectionAttemptCount: connectionAttemptCount, daitaSettings: tunnelSettings.daita ).pick() diff --git a/ios/MullvadSettings/WireGuardObfuscationSettings.swift b/ios/MullvadSettings/WireGuardObfuscationSettings.swift index 98fab6ca2ba2..2828c4da9b73 100644 --- a/ios/MullvadSettings/WireGuardObfuscationSettings.swift +++ b/ios/MullvadSettings/WireGuardObfuscationSettings.swift @@ -8,9 +8,9 @@ import Foundation -/// Whether obfuscation is enabled and which method is used +/// Whether obfuscation is enabled and which method is used. /// -/// `.automatic` means an algorithm will decide whether to use it or not. +/// `.automatic` means an algorithm will decide whether to use obfuscation or not. public enum WireGuardObfuscationState: Codable { @available(*, deprecated, renamed: "udpOverTcp") case on diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index dabfa71078a4..fc70a7ab4460 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -603,6 +603,12 @@ 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 */; }; + 7AD63A392CD520FD00445268 /* ObfuscatorPortSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD63A382CD520FD00445268 /* ObfuscatorPortSelector.swift */; }; + 7AD63A3B2CD5278900445268 /* ObfuscationMethodSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD63A3A2CD5278900445268 /* ObfuscationMethodSelector.swift */; }; + 7AD63A3D2CD9065D00445268 /* ObfuscatorPortSelectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD63A3C2CD9065100445268 /* ObfuscatorPortSelectorTests.swift */; }; + 7AD63A3F2CDA53F600445268 /* ObfuscationMethodSelectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD63A3E2CDA53E900445268 /* ObfuscationMethodSelectorTests.swift */; }; + 7AD63A442CDA663300445268 /* UInt+Counting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD63A432CDA662900445268 /* UInt+Counting.swift */; }; + 7AD63A472CDA666100445268 /* UIntTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD63A462CDA665A00445268 /* UIntTests.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, ); }; }; @@ -1926,6 +1932,12 @@ 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 = ""; }; + 7AD63A382CD520FD00445268 /* ObfuscatorPortSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObfuscatorPortSelector.swift; sourceTree = ""; }; + 7AD63A3A2CD5278900445268 /* ObfuscationMethodSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObfuscationMethodSelector.swift; sourceTree = ""; }; + 7AD63A3C2CD9065100445268 /* ObfuscatorPortSelectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObfuscatorPortSelectorTests.swift; sourceTree = ""; }; + 7AD63A3E2CDA53E900445268 /* ObfuscationMethodSelectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObfuscationMethodSelectorTests.swift; sourceTree = ""; }; + 7AD63A432CDA662900445268 /* UInt+Counting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UInt+Counting.swift"; sourceTree = ""; }; + 7AD63A462CDA665A00445268 /* UIntTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIntTests.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 = ""; }; 7AE90B672C2D726000375A60 /* NSParagraphStyle+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSParagraphStyle+Extensions.swift"; sourceTree = ""; }; @@ -2393,6 +2405,7 @@ children = ( F06045F02B2324DA00B2D37A /* ApiHandlers */, 062B45A228FD4C0F00746E77 /* Assets */, + 7AD63A422CDA661B00445268 /* Extensions */, 582FFA82290A84E700895745 /* Info.plist */, 06799ABE28F98E1D00ACD94E /* MullvadREST.h */, F0DC779F2B2222D20087F09D /* Relay */, @@ -2439,6 +2452,7 @@ 440E9EF32BDA942E00B1FD11 /* MullvadREST */ = { isa = PBXGroup; children = ( + 7AD63A452CDA665200445268 /* Extensions */, F072D3D02C071A9100906F64 /* Shadowsocks */, 440E9EF42BDA943B00B1FD11 /* ApiHandlers */, 440E9EF52BDA954000B1FD11 /* Relay */, @@ -2460,6 +2474,8 @@ children = ( A9EC20E72A5D3A8C0040D56E /* CoordinatesTests.swift */, 7ACE19142C1C429A00260BB6 /* MultihopDecisionFlowTests.swift */, + 7AD63A3E2CDA53E900445268 /* ObfuscationMethodSelectorTests.swift */, + 7AD63A3C2CD9065100445268 /* ObfuscatorPortSelectorTests.swift */, A9467E7E2A29DEFE000DC21F /* RelayCacheTests.swift */, 7ACE19122C1C352100260BB6 /* RelayPickingTests.swift */, 584B26F3237434D00073B10E /* RelaySelectorTests.swift */, @@ -3902,6 +3918,22 @@ path = SelectLocation; sourceTree = ""; }; + 7AD63A422CDA661B00445268 /* Extensions */ = { + isa = PBXGroup; + children = ( + 7AD63A432CDA662900445268 /* UInt+Counting.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 7AD63A452CDA665200445268 /* Extensions */ = { + isa = PBXGroup; + children = ( + 7AD63A462CDA665A00445268 /* UIntTests.swift */, + ); + path = Extensions; + sourceTree = ""; + }; 7AE241492C20682B0076CE33 /* Presentation controllers */ = { isa = PBXGroup; children = ( @@ -4193,11 +4225,13 @@ F0DDE4292B220A15006B57A7 /* Midpoint.swift */, 7ACE19102C1C349200260BB6 /* MultihopDecisionFlow.swift */, F0F3161A2BF358590078DBCF /* NoRelaysSatisfyingConstraintsError.swift */, + 7AD63A3A2CD5278900445268 /* ObfuscationMethodSelector.swift */, 5820675A26E6576800655B05 /* RelayCache.swift */, 7A3AD5002C1068A800E9AD90 /* RelayPicking.swift */, F0DDE4282B220A15006B57A7 /* RelaySelector.swift */, F0B894F42BF7528700817A42 /* RelaySelector+Shadowsocks.swift */, F0B894F22BF7526700817A42 /* RelaySelector+Wireguard.swift */, + 7AD63A382CD520FD00445268 /* ObfuscatorPortSelector.swift */, 5824037F2A827DF300163DE8 /* RelaySelectorProtocol.swift */, F0F316182BF3572B0078DBCF /* RelaySelectorResult.swift */, 7AEBA5292C2179F20018BEC5 /* RelaySelectorWrapper.swift */, @@ -5212,6 +5246,7 @@ F05F39972B21C735006E60A7 /* RelayCache.swift in Sources */, A932D9EF2B5ADD0700999395 /* ProxyConfigurationTransportProvider.swift in Sources */, F01528BB2BFF3FEE00B01D00 /* ShadowsocksRelaySelector.swift in Sources */, + 7AD63A392CD520FD00445268 /* ObfuscatorPortSelector.swift in Sources */, 06799AE728F98E4800ACD94E /* RESTURLSession.swift in Sources */, A90763B52B2857D50045ADF0 /* Socks5Constants.swift in Sources */, A90763BA2B2857D50045ADF0 /* Socks5Error.swift in Sources */, @@ -5250,6 +5285,7 @@ A90763B12B2857D50045ADF0 /* Socks5Endpoint.swift in Sources */, 06799AF328F98E4800ACD94E /* RESTAuthenticationProxy.swift in Sources */, F0B894F12BF751E300817A42 /* RelayWithDistance.swift in Sources */, + 7AD63A442CDA663300445268 /* UInt+Counting.swift in Sources */, 7A516C3A2B7111A700BBD33D /* IPOverrideWrapper.swift in Sources */, A90763B62B2857D50045ADF0 /* Socks5ConnectNegotiation.swift in Sources */, 7ACE19112C1C349200260BB6 /* MultihopDecisionFlow.swift in Sources */, @@ -5258,6 +5294,7 @@ F0DDE42B2B220A15006B57A7 /* RelaySelector.swift in Sources */, F0B894F32BF7526700817A42 /* RelaySelector+Wireguard.swift in Sources */, 7AEBA52A2C2179F20018BEC5 /* RelaySelectorWrapper.swift in Sources */, + 7AD63A3B2CD5278900445268 /* ObfuscationMethodSelector.swift in Sources */, F0B894EF2BF751C500817A42 /* RelayWithLocation.swift in Sources */, F0DDE42C2B220A15006B57A7 /* Midpoint.swift in Sources */, A90763C72B2858DC0045ADF0 /* CancellableChain.swift in Sources */, @@ -5384,13 +5421,16 @@ A9A5FA0E2ACB05160083449F /* StorePaymentObserver.swift in Sources */, A9A5FA0F2ACB05160083449F /* StoreSubscription.swift in Sources */, A9A5FA102ACB05160083449F /* PacketTunnelTransport.swift in Sources */, + 7AD63A472CDA666100445268 /* UIntTests.swift in Sources */, A9A5FA112ACB05160083449F /* TransportMonitor.swift in Sources */, A9B6AC1A2ADE8FBB00F7802A /* InMemorySettingsStore.swift in Sources */, A9A5FA132ACB05160083449F /* LoadTunnelConfigurationOperation.swift in Sources */, + 7AD63A3F2CDA53F600445268 /* ObfuscationMethodSelectorTests.swift in Sources */, 44DD7D292B7113CA0005F67F /* MockTunnel.swift in Sources */, A9A5FA142ACB05160083449F /* MapConnectionStatusOperation.swift in Sources */, A9A5FA152ACB05160083449F /* RedeemVoucherOperation.swift in Sources */, A9A5FA162ACB05160083449F /* RotateKeyOperation.swift in Sources */, + 7AD63A3D2CD9065D00445268 /* ObfuscatorPortSelectorTests.swift in Sources */, F072D3CF2C07122400906F64 /* SettingsUpdaterTests.swift in Sources */, 7ACE19132C1C352100260BB6 /* RelayPickingTests.swift in Sources */, F09D04B52AE93CB6003D4F89 /* OutgoingConnectionProxy+Stub.swift in Sources */, diff --git a/ios/MullvadVPNTests/MullvadREST/ApiHandlers/ServerRelaysResponse+Stubs.swift b/ios/MullvadVPNTests/MullvadREST/ApiHandlers/ServerRelaysResponse+Stubs.swift index e2fe5ce04396..09511a0494a1 100644 --- a/ios/MullvadVPNTests/MullvadREST/ApiHandlers/ServerRelaysResponse+Stubs.swift +++ b/ios/MullvadVPNTests/MullvadREST/ApiHandlers/ServerRelaysResponse+Stubs.swift @@ -11,7 +11,8 @@ import Foundation import WireGuardKitTypes enum ServerRelaysResponseStubs { - static let portRanges: [[UInt16]] = [[4000, 4001], [5000, 5001]] + static let wireguardPortRanges: [[UInt16]] = [[4000, 4001], [5000, 5001]] + static let shadowsocksPortRanges: [[UInt16]] = [[51900, 51949]] static let sampleRelays = REST.ServerRelaysResponse( locations: [ @@ -73,7 +74,7 @@ enum ServerRelaysResponseStubs { wireguard: REST.ServerWireguardTunnels( ipv4Gateway: .loopback, ipv6Gateway: .loopback, - portRanges: portRanges, + portRanges: wireguardPortRanges, relays: [ REST.ServerRelay( hostname: "es1-wireguard", @@ -87,7 +88,7 @@ enum ServerRelaysResponseStubs { publicKey: PrivateKey().publicKey.rawValue, includeInCountry: true, daita: true, - shadowsocksExtraAddrIn: nil + shadowsocksExtraAddrIn: ["0.0.0.0"] ), REST.ServerRelay( hostname: "se10-wireguard", @@ -101,7 +102,7 @@ enum ServerRelaysResponseStubs { publicKey: PrivateKey().publicKey.rawValue, includeInCountry: true, daita: false, - shadowsocksExtraAddrIn: nil + shadowsocksExtraAddrIn: ["0.0.0.0"] ), REST.ServerRelay( hostname: "se2-wireguard", @@ -115,7 +116,7 @@ enum ServerRelaysResponseStubs { publicKey: PrivateKey().publicKey.rawValue, includeInCountry: true, daita: false, - shadowsocksExtraAddrIn: nil + shadowsocksExtraAddrIn: ["0.0.0.0"] ), REST.ServerRelay( hostname: "se6-wireguard", @@ -129,7 +130,7 @@ enum ServerRelaysResponseStubs { publicKey: PrivateKey().publicKey.rawValue, includeInCountry: true, daita: false, - shadowsocksExtraAddrIn: nil + shadowsocksExtraAddrIn: ["0.0.0.0"] ), REST.ServerRelay( hostname: "us-dal-wg-001", @@ -174,7 +175,7 @@ enum ServerRelaysResponseStubs { shadowsocksExtraAddrIn: nil ), ], - shadowsocksPortRanges: [] + shadowsocksPortRanges: shadowsocksPortRanges ), bridge: REST.ServerBridges(shadowsocks: [ REST.ServerShadowsocks(protocol: "tcp", port: 443, cipher: "aes-256-gcm", password: "mullvad"), diff --git a/ios/MullvadVPNTests/MullvadREST/Extensions/UIntTests.swift b/ios/MullvadVPNTests/MullvadREST/Extensions/UIntTests.swift new file mode 100644 index 000000000000..0b99cfdadb55 --- /dev/null +++ b/ios/MullvadVPNTests/MullvadREST/Extensions/UIntTests.swift @@ -0,0 +1,31 @@ +// +// UIntTests.swift +// MullvadVPN +// +// Created by Jon Petersson on 2024-11-05. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +@testable import MullvadREST +import XCTest + +class UIntTests: XCTestCase { + func testCountingSets() { + for setSize in UInt(1) ..< 20 { + let sampleSize: UInt = (setSize * 2) - 1 + + var count: UInt = 0 + (UInt(0) ... sampleSize).forEach { index in + count = count == setSize ? 1 : count + 1 + + let lowerHalfCount = count - 1 + let upperHalfCount = lowerHalfCount + setSize + + XCTAssertEqual( + index.isOrdered(nth: count, forEverySetOf: setSize), + index == lowerHalfCount || index == upperHalfCount + ) + } + } + } +} diff --git a/ios/MullvadVPNTests/MullvadREST/Relay/ObfuscationMethodSelectorTests.swift b/ios/MullvadVPNTests/MullvadREST/Relay/ObfuscationMethodSelectorTests.swift new file mode 100644 index 000000000000..5acfedf8e4d7 --- /dev/null +++ b/ios/MullvadVPNTests/MullvadREST/Relay/ObfuscationMethodSelectorTests.swift @@ -0,0 +1,87 @@ +// +// ObfuscationMethodSelectorTests.swift +// MullvadVPN +// +// Created by Jon Petersson on 2024-11-05. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +@testable import MullvadREST +import MullvadSettings +import XCTest + +class ObfuscationMethodSelectorTests: XCTestCase { + var tunnelSettings = LatestTunnelSettings() + + func testMethodSelectionIsOff() throws { + (UInt(0) ... 10).forEach { attempt in + tunnelSettings.wireGuardObfuscation = WireGuardObfuscationSettings(state: .off) + + var method = ObfuscationMethodSelector.obfuscationMethodBy( + connectionAttemptCount: attempt, + tunnelSettings: tunnelSettings + ) + XCTAssertEqual(method, .off) + + tunnelSettings.wireGuardObfuscation = WireGuardObfuscationSettings(state: .automatic) + + method = ObfuscationMethodSelector.obfuscationMethodBy( + connectionAttemptCount: attempt, + tunnelSettings: tunnelSettings + ) + if attempt.isOrdered(nth: 1, forEverySetOf: 4) || attempt.isOrdered(nth: 2, forEverySetOf: 4) { + XCTAssertEqual(method, .off) + } else { + XCTAssertNotEqual(method, .off) + } + } + } + + func testMethodSelectionIsShadowsock() throws { + (UInt(0) ... 10).forEach { attempt in + tunnelSettings.wireGuardObfuscation = WireGuardObfuscationSettings(state: .shadowsocks) + + var method = ObfuscationMethodSelector.obfuscationMethodBy( + connectionAttemptCount: attempt, + tunnelSettings: tunnelSettings + ) + XCTAssertEqual(method, .shadowsocks) + + tunnelSettings.wireGuardObfuscation = WireGuardObfuscationSettings(state: .automatic) + + method = ObfuscationMethodSelector.obfuscationMethodBy( + connectionAttemptCount: attempt, + tunnelSettings: tunnelSettings + ) + if attempt.isOrdered(nth: 3, forEverySetOf: 4) { + XCTAssertEqual(method, .shadowsocks) + } else { + XCTAssertNotEqual(method, .shadowsocks) + } + } + } + + func testMethodSelectionUdpOverTcp() throws { + (UInt(0) ... 10).forEach { attempt in + tunnelSettings.wireGuardObfuscation = WireGuardObfuscationSettings(state: .udpOverTcp) + + var method = ObfuscationMethodSelector.obfuscationMethodBy( + connectionAttemptCount: attempt, + tunnelSettings: tunnelSettings + ) + XCTAssertEqual(method, .udpOverTcp) + + tunnelSettings.wireGuardObfuscation = WireGuardObfuscationSettings(state: .automatic) + + method = ObfuscationMethodSelector.obfuscationMethodBy( + connectionAttemptCount: attempt, + tunnelSettings: tunnelSettings + ) + if attempt.isOrdered(nth: 4, forEverySetOf: 4) { + XCTAssertEqual(method, .udpOverTcp) + } else { + XCTAssertNotEqual(method, .udpOverTcp) + } + } + } +} diff --git a/ios/MullvadVPNTests/MullvadREST/Relay/ObfuscatorPortSelectorTests.swift b/ios/MullvadVPNTests/MullvadREST/Relay/ObfuscatorPortSelectorTests.swift new file mode 100644 index 000000000000..32d0eeade255 --- /dev/null +++ b/ios/MullvadVPNTests/MullvadREST/Relay/ObfuscatorPortSelectorTests.swift @@ -0,0 +1,194 @@ +// +// ObfuscatorPortSelectorTests.swift +// MullvadVPN +// +// Created by Jon Petersson on 2024-11-04. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +@testable import MullvadREST +@testable import MullvadSettings +@testable import MullvadTypes +import XCTest + +final class ObfuscatorPortSelectorTests: XCTestCase { + let defaultWireguardPort: RelayConstraint = .only(56) + + let sampleRelays = ServerRelaysResponseStubs.sampleRelays + var tunnelSettings = LatestTunnelSettings() + + override func setUp() { + tunnelSettings.relayConstraints.port = defaultWireguardPort + } + + func testObfuscateOffDoesNotChangeEndpoint() throws { + tunnelSettings.wireGuardObfuscation = WireGuardObfuscationSettings(state: .off) + + let obfuscationResult = try ObfuscatorPortSelector( + relays: sampleRelays + ).obfuscate( + tunnelSettings: tunnelSettings, + connectionAttemptCount: 0 + ) + + XCTAssertEqual(obfuscationResult.port, defaultWireguardPort) + } + + // MARK: UdpOverTcp + + func testObfuscateUdpOverTcpPort80() throws { + tunnelSettings.wireGuardObfuscation = WireGuardObfuscationSettings( + state: .udpOverTcp, + udpOverTcpPort: .port80 + ) + + let obfuscationResult = try ObfuscatorPortSelector( + relays: sampleRelays + ).obfuscate( + tunnelSettings: tunnelSettings, + connectionAttemptCount: 0 + ) + + XCTAssertEqual(obfuscationResult.port, .only(80)) + } + + func testObfuscateUdpOverTcpPort5001() throws { + tunnelSettings.wireGuardObfuscation = WireGuardObfuscationSettings( + state: .udpOverTcp, + udpOverTcpPort: .port5001 + ) + + let obfuscationResult = try ObfuscatorPortSelector( + relays: sampleRelays + ).obfuscate( + tunnelSettings: tunnelSettings, + connectionAttemptCount: 0 + ) + + XCTAssertEqual(obfuscationResult.port, .only(5001)) + } + + func testObfuscateUpdOverTcpPortAutomaticIsPort80OnEvenRetryAttempts() throws { + tunnelSettings.wireGuardObfuscation = WireGuardObfuscationSettings( + state: .udpOverTcp, + udpOverTcpPort: .automatic + ) + + try (0 ... 10).filter { $0.isMultiple(of: 2) }.forEach { attempt in + let obfuscationResult = try ObfuscatorPortSelector( + relays: sampleRelays + ).obfuscate( + tunnelSettings: tunnelSettings, + connectionAttemptCount: UInt(attempt) + ) + + XCTAssertEqual(obfuscationResult.port, .only(80)) + } + } + + func testObfuscateUpdOverTcpPortAutomaticIsPort5001OnOddRetryAttempts() throws { + tunnelSettings.wireGuardObfuscation = WireGuardObfuscationSettings( + state: .udpOverTcp, + udpOverTcpPort: .automatic + ) + + try (0 ... 10).filter { !$0.isMultiple(of: 2) }.forEach { attempt in + let obfuscationResult = try ObfuscatorPortSelector( + relays: sampleRelays + ).obfuscate( + tunnelSettings: tunnelSettings, + connectionAttemptCount: UInt(attempt) + ) + + XCTAssertEqual(obfuscationResult.port, .only(5001)) + } + } + + // MARK: Shadowsocks + + func testObfuscateShadowsocksPortCustom() throws { + tunnelSettings.wireGuardObfuscation = WireGuardObfuscationSettings( + state: .shadowsocks, + shadowsocksPort: .custom(5500) + ) + + let obfuscationResult = try ObfuscatorPortSelector( + relays: sampleRelays + ).obfuscate( + tunnelSettings: tunnelSettings, + connectionAttemptCount: 0 + ) + + XCTAssertEqual(obfuscationResult.port, .only(5500)) + } + + func testObfuscateShadowsocksPortAutomatic() throws { + tunnelSettings.wireGuardObfuscation = WireGuardObfuscationSettings( + state: .shadowsocks, + shadowsocksPort: .automatic + ) + + let obfuscationResult = try ObfuscatorPortSelector( + relays: sampleRelays + ).obfuscate( + tunnelSettings: tunnelSettings, + connectionAttemptCount: 0 + ) + + let portRanges = RelaySelector.parseRawPortRanges(sampleRelays.wireguard.shadowsocksPortRanges) + + XCTAssertTrue(try portRanges.contains(where: { range in + range.contains(try XCTUnwrap(obfuscationResult.port.value)) + })) + } + + func testObfuscateShadowsocksRelayFilteringWithPortOutsideDefaultRanges() throws { + let allPorts: Range = 1 ..< 65000 + let defaultPortRanges = RelaySelector.parseRawPortRanges(sampleRelays.wireguard.shadowsocksPortRanges) + + let portsOutsideDefaultRange = allPorts.filter { port in + !defaultPortRanges.contains { range in + range.contains(port) + } + } + + let port = try XCTUnwrap(portsOutsideDefaultRange.randomElement()) + + tunnelSettings.wireGuardObfuscation = WireGuardObfuscationSettings( + state: .shadowsocks, + shadowsocksPort: .custom(port) + ) + + let obfuscationResult = try ObfuscatorPortSelector( + relays: sampleRelays + ).obfuscate( + tunnelSettings: tunnelSettings, + connectionAttemptCount: 0 + ) + + let relaysWithExtraAddresses = sampleRelays.wireguard.relays.filter { relay in + !relay.shadowsocksExtraAddrIn.isNil + } + + XCTAssertEqual(obfuscationResult.relays.wireguard.relays.count, relaysWithExtraAddresses.count) + } + + func testObfuscateShadowsocksRelayFilteringWithPortInsideDefaultRanges() throws { + let defaultPortRanges = RelaySelector.parseRawPortRanges(sampleRelays.wireguard.shadowsocksPortRanges) + let port = try XCTUnwrap(defaultPortRanges.randomElement()?.randomElement()) + + tunnelSettings.wireGuardObfuscation = WireGuardObfuscationSettings( + state: .shadowsocks, + shadowsocksPort: .custom(port) + ) + + let obfuscationResult = try ObfuscatorPortSelector( + relays: sampleRelays + ).obfuscate( + tunnelSettings: tunnelSettings, + connectionAttemptCount: 0 + ) + + XCTAssertEqual(obfuscationResult.relays.wireguard.relays.count, sampleRelays.wireguard.relays.count) + } +} diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift index 6f6b0a02cd25..74f8a5a0e262 100644 --- a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift +++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift @@ -443,11 +443,11 @@ extension PacketTunnelActor { let obfuscatedEndpoint = protocolObfuscator.obfuscate( connectionState.connectedEndpoint, - settings: settings, + settings: settings.tunnelSettings, retryAttempts: connectionState.selectedRelays.retryAttempt ) - let transportLayer = protocolObfuscator.transportLayer.map { $0 } ?? .udp + return State.ConnectionData( selectedRelays: connectionState.selectedRelays, relayConstraints: connectionState.relayConstraints, diff --git a/ios/PacketTunnelCore/Actor/ProtocolObfuscator.swift b/ios/PacketTunnelCore/Actor/ProtocolObfuscator.swift index d43fafa6f5ef..e51549d5a945 100644 --- a/ios/PacketTunnelCore/Actor/ProtocolObfuscator.swift +++ b/ios/PacketTunnelCore/Actor/ProtocolObfuscator.swift @@ -7,11 +7,13 @@ // import Foundation +import MullvadREST import MullvadRustRuntime +import MullvadSettings import MullvadTypes public protocol ProtocolObfuscation { - func obfuscate(_ endpoint: MullvadEndpoint, settings: Settings, retryAttempts: UInt) -> MullvadEndpoint + func obfuscate(_ endpoint: MullvadEndpoint, settings: LatestTunnelSettings, retryAttempts: UInt) -> MullvadEndpoint var transportLayer: TransportLayer? { get } var remotePort: UInt16 { get } } @@ -21,14 +23,10 @@ public class ProtocolObfuscator: ProtocolObfuscat public init() {} - /// Obfuscates a Mullvad endpoint based on a number of retry attempts. + /// Obfuscates a Mullvad endpoint. /// - /// 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 var transportLayer: TransportLayer? { return tunnelObfuscator?.transportLayer @@ -36,42 +34,36 @@ public class ProtocolObfuscator: ProtocolObfuscat private(set) public var remotePort: UInt16 = 0 - public func obfuscate(_ endpoint: MullvadEndpoint, settings: Settings, retryAttempts: UInt = 0) -> MullvadEndpoint { - var obfuscatedEndpoint = endpoint + public func obfuscate( + _ endpoint: MullvadEndpoint, + settings: LatestTunnelSettings, + retryAttempts: UInt = 0 + ) -> MullvadEndpoint { + let obfuscationMethod = ObfuscationMethodSelector.obfuscationMethodBy( + connectionAttemptCount: retryAttempts, + tunnelSettings: settings + ) + remotePort = endpoint.ipv4Relay.port - let shouldObfuscate = switch settings.obfuscation.state { - case .automatic: - retryAttempts % 4 == 2 || retryAttempts % 4 == 3 - case .on, .udpOverTcp, .shadowsocks: - true - case .off: - false - } - guard shouldObfuscate else { + guard obfuscationMethod != .off else { tunnelObfuscator = nil return endpoint } - var tcpPort = settings.obfuscation.udpOverTcpPort - if tcpPort == .automatic { - tcpPort = retryAttempts % 2 == 0 ? .port80 : .port5001 - } + let obfuscator = Obfuscator( - remoteAddress: obfuscatedEndpoint.ipv4Relay.ip, - tcpPort: tcpPort.portValue ?? 0 + remoteAddress: endpoint.ipv4Relay.ip, + tcpPort: remotePort ) - remotePort = tcpPort.portValue ?? 0 + 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 MullvadEndpoint( + ipv4Relay: IPv4Endpoint(ip: .loopback, port: obfuscator.localUdpPort), + ipv4Gateway: endpoint.ipv4Gateway, + ipv6Gateway: endpoint.ipv6Gateway, + publicKey: endpoint.publicKey ) - - return obfuscatedEndpoint } } diff --git a/ios/PacketTunnelCoreTests/Mocks/ProtocolObfuscationStub.swift b/ios/PacketTunnelCoreTests/Mocks/ProtocolObfuscationStub.swift index acb69753f1ea..6a01b73c647c 100644 --- a/ios/PacketTunnelCoreTests/Mocks/ProtocolObfuscationStub.swift +++ b/ios/PacketTunnelCoreTests/Mocks/ProtocolObfuscationStub.swift @@ -7,13 +7,18 @@ // import Foundation +@testable import MullvadSettings @testable import MullvadTypes @testable import PacketTunnelCore struct ProtocolObfuscationStub: ProtocolObfuscation { var remotePort: UInt16 { 42 } - func obfuscate(_ endpoint: MullvadEndpoint, settings: Settings, retryAttempts: UInt) -> MullvadEndpoint { + func obfuscate( + _ endpoint: MullvadEndpoint, + settings: LatestTunnelSettings, + retryAttempts: UInt + ) -> MullvadEndpoint { endpoint } diff --git a/ios/PacketTunnelCoreTests/ProtocolObfuscatorTests.swift b/ios/PacketTunnelCoreTests/ProtocolObfuscatorTests.swift index 8a0923af41d7..425c34705b3e 100644 --- a/ios/PacketTunnelCoreTests/ProtocolObfuscatorTests.swift +++ b/ios/PacketTunnelCoreTests/ProtocolObfuscatorTests.swift @@ -10,21 +10,19 @@ @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 { + let address = try XCTUnwrap(IPv4Address("1.2.3.4")) + let gateway = try XCTUnwrap(IPv4Address("5.6.7.8")) + let v4Endpoint = IPv4Endpoint(ip: address, port: 56) + 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, @@ -34,82 +32,63 @@ final class ProtocolObfuscatorTests: XCTestCase { } func testObfuscateOffDoesNotChangeEndpoint() { - let settings = settings(.off, obfuscationPort: .automatic, quantumResistance: .automatic) + let settings = settings(.off, obfuscationPort: .automatic) let nonObfuscatedEndpoint = obfuscator.obfuscate(endpoint, settings: settings) XCTAssertEqual(endpoint, nonObfuscatedEndpoint) } - func testObfuscateOnPort80() throws { - let settings = settings(.udpOverTcp, obfuscationPort: .port80, quantumResistance: .automatic) + func testObfuscateUdpOverTcp() throws { + let settings = settings(.udpOverTcp, obfuscationPort: .automatic) let obfuscatedEndpoint = obfuscator.obfuscate(endpoint, settings: settings) let obfuscationProtocol = try XCTUnwrap(obfuscator.tunnelObfuscator as? TunnelObfuscationStub) - validate(obfuscatedEndpoint, against: obfuscationProtocol, expect: .port80) + validate(obfuscatedEndpoint, against: obfuscationProtocol) } - func testObfuscateOnPort5001() throws { - let settings = settings(.udpOverTcp, obfuscationPort: .port5001, quantumResistance: .automatic) + func testObfuscateShadowsocks() throws { + let settings = settings(.shadowsocks, obfuscationPort: .automatic) 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(.udpOverTcp, obfuscationPort: .automatic, quantumResistance: .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(.udpOverTcp, obfuscationPort: .automatic, quantumResistance: .automatic) - let obfuscatedEndpoint = obfuscator.obfuscate(endpoint, settings: settings, retryAttempts: 3) - let obfuscationProtocol = try XCTUnwrap(obfuscator.tunnelObfuscator as? TunnelObfuscationStub) - - validate(obfuscatedEndpoint, against: obfuscationProtocol, expect: .port5001) + validate(obfuscatedEndpoint, against: obfuscationProtocol) } - func testObfuscateAutomaticIsPort80EveryThirdAttempts() throws { - let settings = settings(.automatic, obfuscationPort: .automatic, quantumResistance: .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, quantumResistance: .automatic) - let obfuscatedEndpoint = obfuscator.obfuscate(endpoint, settings: settings, retryAttempts: 7) - let obfuscationProtocol = try XCTUnwrap(obfuscator.tunnelObfuscator as? TunnelObfuscationStub) - - validate(obfuscatedEndpoint, against: obfuscationProtocol, expect: .port5001) + func testObfuscateAutomatic() throws { + let settings = settings(.automatic, obfuscationPort: .automatic) + + try (UInt(0) ... 3).forEach { attempt in + var obfuscatedEndpoint = obfuscator.obfuscate(endpoint, settings: settings, retryAttempts: attempt) + + switch attempt { + case 0, 1: + XCTAssertEqual(endpoint, obfuscatedEndpoint) + case 2, 3: + var obfuscationProtocol = try XCTUnwrap(obfuscator.tunnelObfuscator as? TunnelObfuscationStub) + validate(obfuscatedEndpoint, against: obfuscationProtocol) + default: + XCTExpectFailure("Should not end up here, test setup is wrong") + } + } } +} +extension ProtocolObfuscatorTests { private func validate( _ obfuscatedEndpoint: MullvadEndpoint, - against obfuscationProtocol: TunnelObfuscationStub, - expect port: WireGuardObfuscationUdpOverTcpPort + against obfuscationProtocol: TunnelObfuscationStub ) { XCTAssertEqual(obfuscatedEndpoint.ipv4Relay.ip, .loopback) XCTAssertEqual(obfuscatedEndpoint.ipv4Relay.port, obfuscationProtocol.localUdpPort) - XCTAssertEqual(obfuscationProtocol.remotePort, port.portValue) } private func settings( _ obfuscationState: WireGuardObfuscationState, - obfuscationPort: WireGuardObfuscationUdpOverTcpPort, - quantumResistance: TunnelQuantumResistance - ) -> Settings { - Settings( - privateKey: PrivateKey(), - interfaceAddresses: [IPAddressRange(from: "127.0.0.1/32")!], - tunnelSettings: LatestTunnelSettings(wireGuardObfuscation: WireGuardObfuscationSettings( - state: obfuscationState, - udpOverTcpPort: obfuscationPort - )) - ) + obfuscationPort: WireGuardObfuscationUdpOverTcpPort + ) -> LatestTunnelSettings { + LatestTunnelSettings(wireGuardObfuscation: WireGuardObfuscationSettings( + state: obfuscationState, + udpOverTcpPort: obfuscationPort + )) } }