summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorBug Magnet <marco.nikic@mullvad.net>2023-07-11 10:15:22 +0200
committerEmīls <emils@mullvad.net>2023-07-21 15:37:34 +0200
commitc995de06a0b84b2e03e301fcced374d40fe3ac29 (patch)
tree1b1d26be6136980194cf515f172e7c1980e7c21a
parent0623b3e256f15bcabfcc114a063221480ed02183 (diff)
downloadmullvadvpn-c995de06a0b84b2e03e301fcced374d40fe3ac29.tar.xz
mullvadvpn-c995de06a0b84b2e03e301fcced374d40fe3ac29.zip
Add filtering by Haversine distance
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj18
-rw-r--r--ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved22
-rw-r--r--ios/MullvadVPNTests/CoordinatesTests.swift58
-rw-r--r--ios/MullvadVPNTests/RelaySelectorTests.swift67
-rw-r--r--ios/RelaySelector/Haversine.swift48
-rw-r--r--ios/RelaySelector/Midpoint.swift49
-rw-r--r--ios/RelaySelector/RelaySelector.swift77
7 files changed, 296 insertions, 43 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 0b41c08aba..081f4dfa93 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -418,8 +418,10 @@
A9D99BA52A1F808900DE27D3 /* RelayCache.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 063F02732902B63F001FA09F /* RelayCache.framework */; };
A9D99BA62A1F809C00DE27D3 /* libRelaySelector.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5898D29829017DAC00EB5EBA /* libRelaySelector.a */; };
A9D99BA92A1F81B700DE27D3 /* MullvadTransport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A97F1F412A1F4E1A00ECEFDE /* MullvadTransport.framework */; };
- A9EC20EF2A5D79ED0040D56E /* TunnelObfuscation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5840231F2A406BF5007B27AC /* TunnelObfuscation.framework */; };
A9EC20F02A5D79ED0040D56E /* TunnelObfuscation.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5840231F2A406BF5007B27AC /* TunnelObfuscation.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ A9EC20E62A5C488D0040D56E /* Haversine.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9EC20E52A5C488D0040D56E /* Haversine.swift */; };
+ A9EC20E82A5D3A8C0040D56E /* CoordinatesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9EC20E72A5D3A8C0040D56E /* CoordinatesTests.swift */; };
+ A9EC20F42A5D96030040D56E /* Midpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9EC20F32A5D96030040D56E /* Midpoint.swift */; };
E1187ABC289BBB850024E748 /* OutOfTimeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1187ABA289BBB850024E748 /* OutOfTimeViewController.swift */; };
E1187ABD289BBB850024E748 /* OutOfTimeContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1187ABB289BBB850024E748 /* OutOfTimeContentView.swift */; };
E158B360285381C60002F069 /* String+AccountFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = E158B35F285381C60002F069 /* String+AccountFormatting.swift */; };
@@ -1193,6 +1195,9 @@
A9A8A8EA2A262AB30086D569 /* FileCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileCache.swift; sourceTree = "<group>"; };
A9CF11FC2A0518E7001D9565 /* AddressCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressCacheTests.swift; sourceTree = "<group>"; };
A9D99B9F2A1F7F3A00DE27D3 /* TransportProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransportProvider.swift; sourceTree = "<group>"; };
+ A9EC20E52A5C488D0040D56E /* Haversine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Haversine.swift; sourceTree = "<group>"; };
+ A9EC20E72A5D3A8C0040D56E /* CoordinatesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoordinatesTests.swift; sourceTree = "<group>"; };
+ A9EC20F32A5D96030040D56E /* Midpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Midpoint.swift; sourceTree = "<group>"; };
E1187ABA289BBB850024E748 /* OutOfTimeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutOfTimeViewController.swift; sourceTree = "<group>"; };
E1187ABB289BBB850024E748 /* OutOfTimeContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutOfTimeContentView.swift; sourceTree = "<group>"; };
E158B35F285381C60002F069 /* String+AccountFormatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+AccountFormatting.swift"; sourceTree = "<group>"; };
@@ -1292,7 +1297,6 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
- A9EC20EF2A5D79ED0040D56E /* TunnelObfuscation.framework in Frameworks */,
58F0974E2A20C31100DA2DAD /* WireGuardKitTypes in Frameworks */,
5898D2A92901844E00EB5EBA /* libRelaySelector.a in Frameworks */,
58D223F9294C8FF00029F5F8 /* MullvadLogging.framework in Frameworks */,
@@ -1965,6 +1969,8 @@
5898D29929017DAC00EB5EBA /* RelaySelector */ = {
isa = PBXGroup;
children = (
+ A9EC20E52A5C488D0040D56E /* Haversine.swift */,
+ A9EC20F32A5D96030040D56E /* Midpoint.swift */,
58781CD422AFBA39009B9D8E /* RelaySelector.swift */,
);
path = RelaySelector;
@@ -2004,17 +2010,18 @@
isa = PBXGroup;
children = (
58B0A2A4238EE67E00BC001D /* Info.plist */,
- F07BF2572A26112D00042943 /* InputTextFormatterTests.swift */,
A9CF11FC2A0518E7001D9565 /* AddressCacheTests.swift */,
+ A9EC20E72A5D3A8C0040D56E /* CoordinatesTests.swift */,
5896AE85246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift */,
58915D622A25F8400066445B /* DeviceCheckOperationTests.swift */,
58C3FA672A385C89006A450A /* FileCacheTests.swift */,
582A8A3928BCE19B00D0F9FB /* FixedWidthIntegerArithmeticsTests.swift */,
+ F07BF2572A26112D00042943 /* InputTextFormatterTests.swift */,
+ 58C3FA652A38549D006A450A /* MockFileCache.swift */,
A9467E7E2A29DEFE000DC21F /* RelayCacheTests.swift */,
584B26F3237434D00073B10E /* RelaySelectorTests.swift */,
5807E2C1243203D000F5FF30 /* StringTests.swift */,
58165EBD2A262CBB00688EAD /* WgKeyRotationTests.swift */,
- 58C3FA652A38549D006A450A /* MockFileCache.swift */,
);
path = MullvadVPNTests;
sourceTree = "<group>";
@@ -3129,6 +3136,8 @@
buildActionMask = 2147483647;
files = (
5898D29F29017DD000EB5EBA /* RelaySelector.swift in Sources */,
+ A9EC20E62A5C488D0040D56E /* Haversine.swift in Sources */,
+ A9EC20F42A5D96030040D56E /* Midpoint.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -3166,6 +3175,7 @@
5807E2C3243203E700F5FF30 /* String+Split.swift in Sources */,
580810E92A30E17300B74552 /* DeviceCheckRemoteServiceProtocol.swift in Sources */,
F07BF2582A26112D00042943 /* InputTextFormatterTests.swift in Sources */,
+ A9EC20E82A5D3A8C0040D56E /* CoordinatesTests.swift in Sources */,
58B0A2A8238EE68200BC001D /* RelaySelectorTests.swift in Sources */,
A9467E802A29E0A6000DC21F /* AddressCacheTests.swift in Sources */,
);
diff --git a/ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
deleted file mode 100644
index 02691892fe..0000000000
--- a/ios/MullvadVPN.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "pins" : [
- {
- "identity" : "swift-log",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/apple/swift-log.git",
- "state" : {
- "revision" : "173f567a2dfec11d74588eea82cecea555bdc0bc",
- "version" : "1.4.0"
- }
- },
- {
- "identity" : "wireguard-apple",
- "kind" : "remoteSourceControl",
- "location" : "https://github.com/mullvad/wireguard-apple.git",
- "state" : {
- "revision" : "11a00c20dc03f2751db47e94f585c0778c7bde82"
- }
- }
- ],
- "version" : 2
-}
diff --git a/ios/MullvadVPNTests/CoordinatesTests.swift b/ios/MullvadVPNTests/CoordinatesTests.swift
new file mode 100644
index 0000000000..f67bc2179c
--- /dev/null
+++ b/ios/MullvadVPNTests/CoordinatesTests.swift
@@ -0,0 +1,58 @@
+//
+// CoordinatesTests.swift
+// MullvadVPNTests
+//
+// Created by Marco Nikic on 2023-07-11.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import CoreLocation
+@testable import RelaySelector
+import XCTest
+
+final class CoordinatesTests: XCTestCase {
+ func testHaversine() {
+ let distance1 = Haversine.distance(36.12, -86.67, 33.94, -118.4)
+ XCTAssertEqual(2887.259_950_607_108_7, distance1)
+
+ let distance2 = Haversine.distance(90.0, 5.0, 90.0, 79.0)
+ XCTAssertEqual(0.0000000000004696822692507987, distance2)
+
+ let distance3 = Haversine.distance(0, 0, 0, 0)
+ XCTAssertEqual(0, distance3)
+
+ let distance4 = Haversine.distance(49.0, 12.0, 49.0, 12.0)
+ XCTAssertEqual(0, distance4)
+
+ let distance5 = Haversine.distance(6.0, 27.0, 7.0, 27.0)
+ XCTAssertEqual(111.226_342_571_094_7, distance5)
+
+ let distance6 = Haversine.distance(0.0, 179.5, 0.0, -179.5)
+ XCTAssertEqual(111.226_342_571_100_6, distance6)
+ }
+
+ func testMidpoint() {
+ let midpoint1 = Midpoint.location(
+ in: [
+ CLLocationCoordinate2D(latitude: 0, longitude: 90),
+ CLLocationCoordinate2D(latitude: 90, longitude: 0),
+ ]
+ )
+
+ let midpoint2 = Midpoint.location(
+ in: [
+ CLLocationCoordinate2D(latitude: -20, longitude: 90),
+ CLLocationCoordinate2D(latitude: -20, longitude: -90),
+ ]
+ )
+
+ XCTAssertEqual(CLLocationCoordinate2D(latitude: 45, longitude: 90), midpoint1)
+ XCTAssertEqual(CLLocationCoordinate2D(latitude: -90, longitude: 0), midpoint2)
+ }
+}
+
+extension CLLocationCoordinate2D: Equatable {
+ public static func == (lhs: Self, rhs: Self) -> Bool {
+ lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude
+ }
+}
diff --git a/ios/MullvadVPNTests/RelaySelectorTests.swift b/ios/MullvadVPNTests/RelaySelectorTests.swift
index 0e97d09eff..fa8fc6659b 100644
--- a/ios/MullvadVPNTests/RelaySelectorTests.swift
+++ b/ios/MullvadVPNTests/RelaySelectorTests.swift
@@ -94,6 +94,17 @@ class RelaySelectorTests: XCTestCase {
XCTAssertEqual(selectedRelay?.hostname, "se-sto-br-001")
}
+
+ func testClosestShadowsocksRelayIsRandomWhenNoContraintsAreSatisfied() throws {
+ let constraints = RelayConstraints(location: .only(.country("INVALID COUNTRY")))
+
+ let selectedRelay = try XCTUnwrap(RelaySelector.closestShadowsocksRelayConstrained(
+ by: constraints,
+ in: sampleRelays
+ ))
+
+ XCTAssertTrue(sampleRelays.bridge.relays.contains(selectedRelay))
+ }
}
private let sampleRelays = REST.ServerRelaysResponse(
@@ -134,6 +145,18 @@ private let sampleRelays = REST.ServerRelaysResponse(
latitude: 43.666667,
longitude: -79.416667
),
+ "us-atl": REST.ServerLocation(
+ country: "USA",
+ city: "Atlanta, GA",
+ latitude: 40.73061,
+ longitude: -73.935242
+ ),
+ "us-dal": REST.ServerLocation(
+ country: "USA",
+ city: "Dallas, TX",
+ latitude: 32.89748,
+ longitude: -97.040443
+ ),
],
wireguard: REST.ServerWireguardTunnels(
ipv4Gateway: .loopback,
@@ -188,6 +211,30 @@ private let sampleRelays = REST.ServerRelaysResponse(
publicKey: Data(),
includeInCountry: true
),
+ REST.ServerRelay(
+ hostname: "us-dal-wg-001",
+ active: true,
+ owned: true,
+ location: "us-dal",
+ provider: "",
+ weight: 100,
+ ipv4AddrIn: .loopback,
+ ipv6AddrIn: .loopback,
+ publicKey: Data(),
+ includeInCountry: true
+ ),
+ REST.ServerRelay(
+ hostname: "us-nyc-wg-301",
+ active: true,
+ owned: true,
+ location: "us-nyc",
+ provider: "",
+ weight: 100,
+ ipv4AddrIn: .loopback,
+ ipv6AddrIn: .loopback,
+ publicKey: Data(),
+ includeInCountry: true
+ ),
]
),
bridge: REST.ServerBridges(shadowsocks: [
@@ -233,5 +280,25 @@ private let sampleRelays = REST.ServerRelaysResponse(
weight: 100,
includeInCountry: true
),
+ REST.BridgeRelay(
+ hostname: "us-atl-br-101",
+ active: true,
+ owned: false,
+ location: "us-atl",
+ provider: "100TB",
+ ipv4AddrIn: .loopback,
+ weight: 100,
+ includeInCountry: true
+ ),
+ REST.BridgeRelay(
+ hostname: "us-dal-br-101",
+ active: true,
+ owned: false,
+ location: "us-dal",
+ provider: "100TB",
+ ipv4AddrIn: .loopback,
+ weight: 100,
+ includeInCountry: true
+ ),
])
)
diff --git a/ios/RelaySelector/Haversine.swift b/ios/RelaySelector/Haversine.swift
new file mode 100644
index 0000000000..74fe3218c7
--- /dev/null
+++ b/ios/RelaySelector/Haversine.swift
@@ -0,0 +1,48 @@
+//
+// Haversine.swift
+// RelaySelector
+//
+// Created by Marco Nikic on 2023-06-29.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadTypes
+
+public enum Haversine {
+ /// Approximation of the radius of the average circumference,
+ /// where the boundaries are the meridian (6367.45 km) and the equator (6378.14 km).
+ static let earthRadiusInKm = 6372.8
+
+ /// Implemented as per https://rosettacode.org/wiki/Haversine_formula#Swift
+ /// Computes the great circle distance between two points on a sphere.
+ ///
+ /// The inputs are converted to radians, and the output is in kilometers.
+ /// - Parameters:
+ /// - lat1: The first point's latitude
+ /// - lon1: The first point's longitude
+ /// - lat2: The second point's latitude
+ /// - lon2: The second point's longitude
+ /// - Returns: The haversine distance between the two points.
+ static func distance(
+ _ latitude1: Double,
+ _ longitude1: Double,
+ _ latitude2: Double,
+ _ longitude2: Double
+ ) -> Double {
+ let dLat = latitude1.toRadians - latitude2.toRadians
+ let dLon = longitude1.toRadians - longitude2.toRadians
+
+ let haversine = sin(dLat / 2).squared + sin(dLon / 2)
+ .squared * cos(latitude1.toRadians) * cos(latitude2.toRadians)
+ let c = 2 * asin(sqrt(haversine))
+
+ return Self.earthRadiusInKm * c
+ }
+}
+
+extension Double {
+ var toRadians: Double { self * Double.pi / 180 }
+ var toDegrees: Double { self * 180.0 / Double.pi }
+ var squared: Double { pow(self, 2) }
+}
diff --git a/ios/RelaySelector/Midpoint.swift b/ios/RelaySelector/Midpoint.swift
new file mode 100644
index 0000000000..d01983a96f
--- /dev/null
+++ b/ios/RelaySelector/Midpoint.swift
@@ -0,0 +1,49 @@
+//
+// Midpoint.swift
+// RelaySelector
+//
+// Created by Marco Nikic on 2023-07-11.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import CoreLocation
+import Foundation
+import MullvadTypes
+
+public enum Midpoint {
+ /// Computes the approximate midpoint of a set of locations.
+ ///
+ /// This works by calculating the mean Cartesian coordinates, and converting them
+ /// back to spherical coordinates. This is approximate, because the semi-minor (polar)
+ /// axis is assumed to equal the semi-major (equatorial) axis.
+ ///
+ /// https://en.wikipedia.org/wiki/Spherical_coordinate_system#Cartesian_coordinates
+ static func location(in coordinates: [CLLocationCoordinate2D]) -> CLLocationCoordinate2D {
+ var x = 0.0, y = 0.0, z = 0.0
+ var count = 0
+
+ coordinates.forEach { coordinate in
+ let cos_lat = cos(coordinate.latitude.toRadians)
+ let sin_lat = sin(coordinate.latitude.toRadians)
+ let cos_lon = cos(coordinate.longitude.toRadians)
+ let sin_lon = sin(coordinate.longitude.toRadians)
+
+ x += cos_lat * cos_lon
+ y += cos_lat * sin_lon
+ z += sin_lat
+
+ count += 1
+ }
+
+ let inv_total_weight = 1.0 / Double(count)
+ x *= inv_total_weight
+ y *= inv_total_weight
+ z *= inv_total_weight
+
+ let longitude = atan2(y, x)
+ let hypotenuse = sqrt(x * x + y * y)
+ let latitude = atan2(z, hypotenuse)
+
+ return CLLocationCoordinate2D(latitude: latitude.toDegrees, longitude: longitude.toDegrees)
+ }
+}
diff --git a/ios/RelaySelector/RelaySelector.swift b/ios/RelaySelector/RelaySelector.swift
index 5643f5d73f..558c81452c 100644
--- a/ios/RelaySelector/RelaySelector.swift
+++ b/ios/RelaySelector/RelaySelector.swift
@@ -42,9 +42,43 @@ public enum RelaySelector {
) -> REST.BridgeRelay? {
let mappedBridges = mapRelays(relays: relaysResponse.bridge.relays, locations: relaysResponse.locations)
let filteredRelays = applyConstraints(constraints, relays: mappedBridges)
- let randomBridgeRelay = pickRandomRelay(relays: filteredRelays)
+ guard filteredRelays.isEmpty == false else { return shadowsocksRelay(from: relaysResponse) }
- return randomBridgeRelay?.relay ?? shadowsocksRelay(from: relaysResponse)
+ // Compute the midpoint location from all the filtered relays
+ // Take *either* the first five relays, OR the relays below maximum bridge distance
+ // sort all of them by Haversine distance from the computed midpoint location
+ // then use the roulette selection to pick a bridge
+
+ let midpointDistance = Midpoint.location(in: filteredRelays.map { $0.serverLocation.geoCoordinate })
+ let maximumBridgeDistance = 1500.0
+ let relaysWithDistance = filteredRelays.map {
+ RelayWithDistance(
+ relay: $0.relay,
+ distance: Haversine.distance(
+ midpointDistance.latitude,
+ midpointDistance.longitude,
+ $0.serverLocation.latitude,
+ $0.serverLocation.longitude
+ )
+ )
+ }.sorted {
+ $0.distance < $1.distance
+ }.filter {
+ $0.distance <= maximumBridgeDistance
+ }.prefix(5)
+
+ var greatestDistance = 0.0
+ relaysWithDistance.forEach {
+ if $0.distance > greatestDistance {
+ greatestDistance = $0.distance
+ }
+ }
+
+ let randomRelay = rouletteSelection(relays: Array(relaysWithDistance), weightFunction: { relay in
+ UInt64(1 + greatestDistance - relay.distance)
+ })
+
+ return randomRelay?.relay ?? filteredRelays.randomElement()?.relay
}
/**
@@ -64,7 +98,7 @@ public enum RelaySelector {
numberOfFailedAttempts: numberOfFailedAttempts
)
- guard let relayWithLocation = pickRandomRelay(relays: filteredRelays), let port else {
+ guard let relayWithLocation = pickRandomRelayByWeight(relays: filteredRelays), let port else {
throw NoRelaysSatisfyingConstraintsError()
}
@@ -82,7 +116,7 @@ public enum RelaySelector {
return RelaySelectorResult(
endpoint: endpoint,
relay: relayWithLocation.relay,
- location: relayWithLocation.location
+ location: relayWithLocation.serverLocation
)
}
@@ -98,16 +132,16 @@ public enum RelaySelector {
case let .only(relayConstraint):
switch relayConstraint {
case let .country(countryCode):
- return relayWithLocation.location.countryCode == countryCode &&
+ return relayWithLocation.serverLocation.countryCode == countryCode &&
relayWithLocation.relay.includeInCountry
case let .city(countryCode, cityCode):
- return relayWithLocation.location.countryCode == countryCode &&
- relayWithLocation.location.cityCode == cityCode
+ return relayWithLocation.serverLocation.countryCode == countryCode &&
+ relayWithLocation.serverLocation.cityCode == cityCode
case let .hostname(countryCode, cityCode, hostname):
- return relayWithLocation.location.countryCode == countryCode &&
- relayWithLocation.location.cityCode == cityCode &&
+ return relayWithLocation.serverLocation.countryCode == countryCode &&
+ relayWithLocation.serverLocation.cityCode == cityCode &&
relayWithLocation.relay.hostname == hostname
}
}
@@ -136,11 +170,15 @@ public enum RelaySelector {
}
}
- private static func pickRandomRelay<T: AnyRelay>(relays: [RelayWithLocation<T>]) -> RelayWithLocation<T>? {
- let totalWeight = relays.reduce(0) { accummulatedWeight, relayWithLocation in
- accummulatedWeight + relayWithLocation.relay.weight
- }
+ private static func pickRandomRelayByWeight<T: AnyRelay>(relays: [RelayWithLocation<T>])
+ -> RelayWithLocation<T>? {
+ rouletteSelection(relays: relays, weightFunction: { relayWithLocation in relayWithLocation.relay.weight })
+ }
+ private static func rouletteSelection<T>(relays: [T], weightFunction: (T) -> UInt64) -> T? {
+ let totalWeight = relays.map { weightFunction($0) }.reduce(0) { accumulated, weight in
+ accumulated + weight
+ }
// Return random relay when all relays within the list have zero weight.
guard totalWeight > 0 else {
return relays.randomElement()
@@ -150,9 +188,9 @@ public enum RelaySelector {
// non-zero weight.
var i = (1 ... totalWeight).randomElement()!
- let randomRelay = relays.first { relayWithLocation -> Bool in
+ let randomRelay = relays.first { relay -> Bool in
let (result, isOverflow) = i
- .subtractingReportingOverflow(relayWithLocation.relay.weight)
+ .subtractingReportingOverflow(weightFunction(relay))
i = isOverflow ? 0 : result
@@ -228,7 +266,7 @@ public enum RelaySelector {
longitude: serverLocation.longitude
)
- return RelayWithLocation(relay: relay, location: location)
+ return RelayWithLocation(relay: relay, serverLocation: location)
}
}
@@ -266,5 +304,10 @@ extension REST.BridgeRelay: AnyRelay {}
fileprivate struct RelayWithLocation<T: AnyRelay> {
let relay: T
- let location: Location
+ let serverLocation: Location
+}
+
+fileprivate struct RelayWithDistance<T: AnyRelay> {
+ let relay: T
+ let distance: Double
}