summaryrefslogtreecommitdiffhomepage
path: root/ios/RelaySelector
diff options
context:
space:
mode:
authormojganii <mojgan.jelodar@codic.se>2023-12-07 16:54:59 +0100
committerBug Magnet <marco.nikic@mullvad.net>2023-12-11 11:44:56 +0100
commitfa4bc7091ac57230e5bf8da9c0a1b6c4027f88ad (patch)
treed6ec8af6b8a81c8b794841a27ce9d7bf870ec110 /ios/RelaySelector
parentfef5f92b867126342192901c6abc7ec8514a4ea9 (diff)
downloadmullvadvpn-fa4bc7091ac57230e5bf8da9c0a1b6c4027f88ad.tar.xz
mullvadvpn-fa4bc7091ac57230e5bf8da9c0a1b6c4027f88ad.zip
Moving MullvadTransport into MullvadREST
Diffstat (limited to 'ios/RelaySelector')
-rw-r--r--ios/RelaySelector/Haversine.swift48
-rw-r--r--ios/RelaySelector/Midpoint.swift49
-rw-r--r--ios/RelaySelector/RelaySelector.swift331
3 files changed, 0 insertions, 428 deletions
diff --git a/ios/RelaySelector/Haversine.swift b/ios/RelaySelector/Haversine.swift
deleted file mode 100644
index 946b3ca2c3..0000000000
--- a/ios/RelaySelector/Haversine.swift
+++ /dev/null
@@ -1,48 +0,0 @@
-//
-// 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.0 }
- var toDegrees: Double { self * 180.0 / Double.pi }
- var squared: Double { pow(self, 2.0) }
-}
diff --git a/ios/RelaySelector/Midpoint.swift b/ios/RelaySelector/Midpoint.swift
deleted file mode 100644
index d01983a96f..0000000000
--- a/ios/RelaySelector/Midpoint.swift
+++ /dev/null
@@ -1,49 +0,0 @@
-//
-// 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
deleted file mode 100644
index 20a8496d0d..0000000000
--- a/ios/RelaySelector/RelaySelector.swift
+++ /dev/null
@@ -1,331 +0,0 @@
-//
-// RelaySelector.swift
-// RelaySelector
-//
-// Created by pronebird on 11/06/2019.
-// Copyright © 2019 Mullvad VPN AB. All rights reserved.
-//
-
-import Foundation
-import MullvadREST
-import MullvadTypes
-
-private let defaultPort: UInt16 = 53
-
-public enum RelaySelector {
- /**
- Returns random shadowsocks TCP bridge, otherwise `nil` if there are no shadowdsocks bridges.
- */
- public static func shadowsocksTCPBridge(from relays: REST.ServerRelaysResponse) -> REST.ServerShadowsocks? {
- relays.bridge.shadowsocks.filter { $0.protocol == "tcp" }.randomElement()
- }
-
- /// Return a random Shadowsocks bridge relay, or `nil` if no relay were found.
- ///
- /// Non `active` relays are filtered out.
- /// - Parameter relays: The list of relays to randomly select from.
- /// - Returns: A Shadowsocks relay or `nil` if no active relay were found.
- public static func shadowsocksRelay(from relaysResponse: REST.ServerRelaysResponse) -> REST.BridgeRelay? {
- relaysResponse.bridge.relays.filter { $0.active }.randomElement()
- }
-
- /// Returns the closest Shadowsocks relay using the given `constraints`, or a random relay if `constraints` were
- /// unsatisfiable.
- ///
- /// - Parameters:
- /// - constraints: The user selected `constraints`
- /// - relays: The list of relays to randomly select from.
- /// - Returns: A Shadowsocks relay or `nil` if no active relay were found.
- public static func closestShadowsocksRelayConstrained(
- by constraints: RelayConstraints,
- in relaysResponse: REST.ServerRelaysResponse
- ) -> REST.BridgeRelay? {
- let mappedBridges = mapRelays(relays: relaysResponse.bridge.relays, locations: relaysResponse.locations)
- let filteredRelays = applyConstraints(constraints, relays: mappedBridges)
- guard filteredRelays.isEmpty == false else { return 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
- }
-
- /**
- Filters relay list using given constraints and selects random relay.
- Throws an error if there are no relays satisfying the given constraints.
- */
- public static func evaluate(
- relays: REST.ServerRelaysResponse,
- constraints: RelayConstraints,
- numberOfFailedAttempts: UInt
- ) throws -> RelaySelectorResult {
- let mappedRelays = mapRelays(relays: relays.wireguard.relays, locations: relays.locations)
- let filteredRelays = applyConstraints(constraints, relays: mappedRelays)
- let port = applyConstraints(
- constraints,
- rawPortRanges: relays.wireguard.portRanges,
- numberOfFailedAttempts: numberOfFailedAttempts
- )
-
- guard let relayWithLocation = pickRandomRelayByWeight(relays: filteredRelays), let port else {
- throw NoRelaysSatisfyingConstraintsError()
- }
-
- let endpoint = MullvadEndpoint(
- ipv4Relay: IPv4Endpoint(
- ip: relayWithLocation.relay.ipv4AddrIn,
- port: port
- ),
- ipv6Relay: nil,
- ipv4Gateway: relays.wireguard.ipv4Gateway,
- ipv6Gateway: relays.wireguard.ipv6Gateway,
- publicKey: relayWithLocation.relay.publicKey
- )
-
- return RelaySelectorResult(
- endpoint: endpoint,
- relay: relayWithLocation.relay,
- location: relayWithLocation.serverLocation
- )
- }
-
- /// Determines whether a `REST.ServerRelay` satisfies the given relay filter.
- public static func relayMatchesFilter(_ relay: AnyRelay, filter: RelayFilter) -> Bool {
- if case let .only(providers) = filter.providers, providers.contains(relay.provider) == false {
- return false
- }
-
- switch filter.ownership {
- case .any:
- return true
- case .owned:
- return relay.owned
- case .rented:
- return !relay.owned
- }
- }
-
- /// Produce a list of `RelayWithLocation` items satisfying the given constraints
- private static func applyConstraints<T: AnyRelay>(
- _ constraints: RelayConstraints,
- relays: [RelayWithLocation<T>]
- ) -> [RelayWithLocation<T>] {
- return relays.filter { relayWithLocation -> Bool in
- switch constraints.filter {
- case .any:
- break
- case let .only(filter):
- if !relayMatchesFilter(relayWithLocation.relay, filter: filter) {
- return false
- }
- }
-
- switch constraints.location {
- case .any:
- return true
- case let .only(relayConstraint):
- switch relayConstraint {
- case let .country(countryCode):
- return relayWithLocation.serverLocation.countryCode == countryCode &&
- relayWithLocation.relay.includeInCountry
-
- case let .city(countryCode, cityCode):
- return relayWithLocation.serverLocation.countryCode == countryCode &&
- relayWithLocation.serverLocation.cityCode == cityCode
-
- case let .hostname(countryCode, cityCode, hostname):
- return relayWithLocation.serverLocation.countryCode == countryCode &&
- relayWithLocation.serverLocation.cityCode == cityCode &&
- relayWithLocation.relay.hostname == hostname
- }
- }
- }.filter { relayWithLocation -> Bool in
- relayWithLocation.relay.active
- }
- }
-
- /// Produce a port that is either user provided or randomly selected, satisfying the given constraints.
- private static func applyConstraints(
- _ constraints: RelayConstraints,
- rawPortRanges: [[UInt16]],
- numberOfFailedAttempts: UInt
- ) -> UInt16? {
- switch constraints.port {
- case let .only(port):
- return port
-
- case .any:
- // 1. First two attempts should pick a random port.
- // 2. The next two should pick port 53.
- // 3. Repeat steps 1 and 2.
- let useDefaultPort = (numberOfFailedAttempts % 4 == 2) || (numberOfFailedAttempts % 4 == 3)
-
- return useDefaultPort ? defaultPort : pickRandomPort(rawPortRanges: rawPortRanges)
- }
- }
-
- 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()
- }
-
- // Pick a random number in the range 1 - totalWeight. This chooses the relay with a
- // non-zero weight.
- var i = (1 ... totalWeight).randomElement()!
-
- let randomRelay = relays.first { relay -> Bool in
- let (result, isOverflow) = i
- .subtractingReportingOverflow(weightFunction(relay))
-
- i = isOverflow ? 0 : result
-
- return i == 0
- }
-
- assert(randomRelay != nil, "At least one relay must've had a weight above 0")
-
- return randomRelay
- }
-
- 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
- }
-
- 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
- }
- }
- }
-
- private static func mapRelays<T: AnyRelay>(
- relays: [T],
- locations: [String: REST.ServerLocation]
- ) -> [RelayWithLocation<T>] {
- relays.compactMap { relay in
- guard let serverLocation = locations[relay.location] else { return nil }
- return makeRelayWithLocationFrom(serverLocation, relay: relay)
- }
- }
-
- 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)
- }
-}
-
-public struct NoRelaysSatisfyingConstraintsError: LocalizedError {
- public var errorDescription: String? {
- "No relays satisfying constraints."
- }
-}
-
-public struct RelaySelectorResult: Codable, Equatable {
- public var endpoint: MullvadEndpoint
- public var relay: REST.ServerRelay
- public var location: Location
-}
-
-public protocol AnyRelay {
- var hostname: String { get }
- var owned: Bool { get }
- var location: String { get }
- var provider: String { get }
- var weight: UInt64 { get }
- var active: Bool { get }
- var includeInCountry: Bool { get }
-}
-
-extension REST.ServerRelay: AnyRelay {}
-extension REST.BridgeRelay: AnyRelay {}
-
-private struct RelayWithLocation<T: AnyRelay> {
- let relay: T
- let serverLocation: Location
-}
-
-private struct RelayWithDistance<T: AnyRelay> {
- let relay: T
- let distance: Double
-}