diff options
| author | mojganii <mojgan.jelodar@codic.se> | 2023-12-07 16:54:59 +0100 |
|---|---|---|
| committer | Bug Magnet <marco.nikic@mullvad.net> | 2023-12-11 11:44:56 +0100 |
| commit | fa4bc7091ac57230e5bf8da9c0a1b6c4027f88ad (patch) | |
| tree | d6ec8af6b8a81c8b794841a27ce9d7bf870ec110 /ios/RelaySelector | |
| parent | fef5f92b867126342192901c6abc7ec8514a4ea9 (diff) | |
| download | mullvadvpn-fa4bc7091ac57230e5bf8da9c0a1b6c4027f88ad.tar.xz mullvadvpn-fa4bc7091ac57230e5bf8da9c0a1b6c4027f88ad.zip | |
Moving MullvadTransport into MullvadREST
Diffstat (limited to 'ios/RelaySelector')
| -rw-r--r-- | ios/RelaySelector/Haversine.swift | 48 | ||||
| -rw-r--r-- | ios/RelaySelector/Midpoint.swift | 49 | ||||
| -rw-r--r-- | ios/RelaySelector/RelaySelector.swift | 331 |
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 -} |
