summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorLinus Färnstrand <linus@mullvad.net>2024-11-13 14:59:30 +0100
committerLinus Färnstrand <linus@mullvad.net>2024-11-13 14:59:30 +0100
commite763bf54a1d49f1452d6328acb2b2aaea26beae9 (patch)
tree110cc5a0a3e23044f699c2c4558c3089f558e570
parent31cde2f9c2b26787135ea2e91a7ca836ffac08ce (diff)
parent1b6a8f547f9150c63ed7b5ff3a52bda86762d33a (diff)
downloadmullvadvpn-e763bf54a1d49f1452d6328acb2b2aaea26beae9.tar.xz
mullvadvpn-e763bf54a1d49f1452d6328acb2b2aaea26beae9.zip
Merge branch 'update-relay-selector-ios-911'
-rw-r--r--ios/MullvadREST/Extensions/UInt+Counting.swift24
-rw-r--r--ios/MullvadREST/Relay/ObfuscationMethodSelector.swift30
-rw-r--r--ios/MullvadREST/Relay/ObfuscatorPortSelector.swift128
-rw-r--r--ios/MullvadREST/Relay/RelaySelector.swift118
-rw-r--r--ios/MullvadREST/Relay/RelaySelectorWrapper.swift18
-rw-r--r--ios/MullvadSettings/WireGuardObfuscationSettings.swift4
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj40
-rw-r--r--ios/MullvadVPNTests/MullvadREST/ApiHandlers/ServerRelaysResponse+Stubs.swift15
-rw-r--r--ios/MullvadVPNTests/MullvadREST/Extensions/UIntTests.swift31
-rw-r--r--ios/MullvadVPNTests/MullvadREST/Relay/ObfuscationMethodSelectorTests.swift87
-rw-r--r--ios/MullvadVPNTests/MullvadREST/Relay/ObfuscatorPortSelectorTests.swift194
-rw-r--r--ios/PacketTunnelCore/Actor/PacketTunnelActor.swift4
-rw-r--r--ios/PacketTunnelCore/Actor/ProtocolObfuscator.swift56
-rw-r--r--ios/PacketTunnelCoreTests/Mocks/ProtocolObfuscationStub.swift7
-rw-r--r--ios/PacketTunnelCoreTests/ProtocolObfuscatorTests.swift91
15 files changed, 683 insertions, 164 deletions
diff --git a/ios/MullvadREST/Extensions/UInt+Counting.swift b/ios/MullvadREST/Extensions/UInt+Counting.swift
new file mode 100644
index 0000000000..c822e24991
--- /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 0000000000..4803d38139
--- /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 0000000000..597a8fdc96
--- /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<UInt16>
+}
+
+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<UInt16> {
+ switch tunnelSettings.wireGuardObfuscation.udpOverTcpPort {
+ case .automatic:
+ return (connectionAttemptCount % 2 == 0) ? .only(80) : .only(5001)
+ case .port5001:
+ return .only(5001)
+ case .port80:
+ return .only(80)
+ }
+ }
+
+ private func obfuscateShadowsocksPort(
+ tunnelSettings: LatestTunnelSettings,
+ shadowsocksPortRanges: [[UInt16]]
+ ) -> RelayConstraint<UInt16> {
+ 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 98279daca5..20e6153532 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<T: AnyRelay>(relays: [RelayWithLocation<T>])
-> RelayWithLocation<T>? {
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<T>(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<T: AnyRelay>(
- _ serverLocation: REST.ServerLocation,
- relay: T
- ) -> RelayWithLocation<T>? {
- 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<UInt16>] {
- rawPortRanges.compactMap { inputRange -> ClosedRange<UInt16>? 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<T: AnyRelay>(
_ relayConstraint: RelayConstraint<UserSelectedRelays>,
@@ -166,6 +107,65 @@ public enum RelaySelector {
}
}
+ // MARK: - private
+
+ static func parseRawPortRanges(_ rawPortRanges: [[UInt16]]) -> [ClosedRange<UInt16>] {
+ rawPortRanges.compactMap { inputRange -> ClosedRange<UInt16>? 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<T: AnyRelay>(
+ _ serverLocation: REST.ServerLocation,
+ relay: T
+ ) -> RelayWithLocation<T>? {
+ 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<T: AnyRelay>(
relays: [RelayWithLocation<T>]
) throws -> [RelayWithLocation<T>] {
diff --git a/ios/MullvadREST/Relay/RelaySelectorWrapper.swift b/ios/MullvadREST/Relay/RelaySelectorWrapper.swift
index 03fd113ecb..48f5bf87d2 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 98fab6ca2b..2828c4da9b 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 ef1968698b..7770f47b10 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 = "<group>"; };
7AD0AA1E2AD6C8B900119E10 /* URLRequestProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLRequestProxyProtocol.swift; sourceTree = "<group>"; };
7AD0AA202AD6CB0000119E10 /* URLRequestProxyStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLRequestProxyStub.swift; sourceTree = "<group>"; };
+ 7AD63A382CD520FD00445268 /* ObfuscatorPortSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObfuscatorPortSelector.swift; sourceTree = "<group>"; };
+ 7AD63A3A2CD5278900445268 /* ObfuscationMethodSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObfuscationMethodSelector.swift; sourceTree = "<group>"; };
+ 7AD63A3C2CD9065100445268 /* ObfuscatorPortSelectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObfuscatorPortSelectorTests.swift; sourceTree = "<group>"; };
+ 7AD63A3E2CDA53E900445268 /* ObfuscationMethodSelectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObfuscationMethodSelectorTests.swift; sourceTree = "<group>"; };
+ 7AD63A432CDA662900445268 /* UInt+Counting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UInt+Counting.swift"; sourceTree = "<group>"; };
+ 7AD63A462CDA665A00445268 /* UIntTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIntTests.swift; sourceTree = "<group>"; };
7ADCB2D72B6A6EB300C88F89 /* AnyRelay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyRelay.swift; sourceTree = "<group>"; };
7ADCB2D92B6A730400C88F89 /* IPOverrideRepositoryStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideRepositoryStub.swift; sourceTree = "<group>"; };
7AE90B672C2D726000375A60 /* NSParagraphStyle+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSParagraphStyle+Extensions.swift"; sourceTree = "<group>"; };
@@ -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 = "<group>";
};
+ 7AD63A422CDA661B00445268 /* Extensions */ = {
+ isa = PBXGroup;
+ children = (
+ 7AD63A432CDA662900445268 /* UInt+Counting.swift */,
+ );
+ path = Extensions;
+ sourceTree = "<group>";
+ };
+ 7AD63A452CDA665200445268 /* Extensions */ = {
+ isa = PBXGroup;
+ children = (
+ 7AD63A462CDA665A00445268 /* UIntTests.swift */,
+ );
+ path = Extensions;
+ sourceTree = "<group>";
+ };
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 e2fe5ce043..09511a0494 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 0000000000..0b99cfdadb
--- /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 0000000000..5acfedf8e4
--- /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 0000000000..32d0eeade2
--- /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<UInt16> = .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<UInt16> = 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 6f6b0a02cd..74f8a5a0e2 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 d43fafa6f5..e51549d5a9 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<Obfuscator: TunnelObfuscation>: 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<Obfuscator: TunnelObfuscation>: 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 acb69753f1..6a01b73c64 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 8a0923af41..425c34705b 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<TunnelObfuscationStub>!
- 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<TunnelObfuscationStub>()
- 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 testObfuscateAutomatic() throws {
+ let settings = settings(.automatic, obfuscationPort: .automatic)
- 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)
+ try (UInt(0) ... 3).forEach { attempt in
+ var obfuscatedEndpoint = obfuscator.obfuscate(endpoint, settings: settings, retryAttempts: attempt)
- validate(obfuscatedEndpoint, against: obfuscationProtocol, expect: .port5001)
+ 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
+ ))
}
}