summaryrefslogtreecommitdiffhomepage
path: root/ios/RelaySelector
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2022-10-21 12:27:05 +0200
committerAndrej Mihajlov <and@mullvad.net>2022-10-21 15:41:07 +0200
commitdd4eed601c887ac2f8a039b2ca01622058163bbc (patch)
tree0826a3d01ad4879452f5673c7cf10e7692db2017 /ios/RelaySelector
parent81bea421816b764873f7334774aa4a1f79f7354e (diff)
downloadmullvadvpn-dd4eed601c887ac2f8a039b2ca01622058163bbc.tar.xz
mullvadvpn-dd4eed601c887ac2f8a039b2ca01622058163bbc.zip
Move common code into two new targets: RelaySelector, TunnelProviderMessaging
Diffstat (limited to 'ios/RelaySelector')
-rw-r--r--ios/RelaySelector/RelaySelector.swift179
1 files changed, 179 insertions, 0 deletions
diff --git a/ios/RelaySelector/RelaySelector.swift b/ios/RelaySelector/RelaySelector.swift
new file mode 100644
index 0000000000..bc88795b7d
--- /dev/null
+++ b/ios/RelaySelector/RelaySelector.swift
@@ -0,0 +1,179 @@
+//
+// RelaySelector.swift
+// RelaySelector
+//
+// Created by pronebird on 11/06/2019.
+// Copyright © 2019 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadREST
+import MullvadTypes
+
+public enum RelaySelector {
+ public static func evaluate(
+ relays: REST.ServerRelaysResponse,
+ constraints: RelayConstraints
+ ) throws -> RelaySelectorResult {
+ let filteredRelays = applyConstraints(constraints, relays: Self.parseRelaysResponse(relays))
+
+ guard let relayWithLocation = pickRandomRelay(relays: filteredRelays),
+ let port = pickRandomPort(rawPortRanges: relays.wireguard.portRanges)
+ 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.location
+ )
+ }
+
+ /// Produce a list of `RelayWithLocation` items satisfying the given constraints
+ private static func applyConstraints(
+ _ constraints: RelayConstraints,
+ relays: [RelayWithLocation]
+ ) -> [RelayWithLocation] {
+ return relays.filter { relayWithLocation -> Bool in
+ switch constraints.location {
+ case .any:
+ return true
+ case let .only(relayConstraint):
+ switch relayConstraint {
+ case let .country(countryCode):
+ return relayWithLocation.location.countryCode == countryCode &&
+ relayWithLocation.relay.includeInCountry
+
+ case let .city(countryCode, cityCode):
+ return relayWithLocation.location.countryCode == countryCode &&
+ relayWithLocation.location.cityCode == cityCode
+
+ case let .hostname(countryCode, cityCode, hostname):
+ return relayWithLocation.location.countryCode == countryCode &&
+ relayWithLocation.location.cityCode == cityCode &&
+ relayWithLocation.relay.hostname == hostname
+ }
+ }
+ }.filter { relayWithLocation -> Bool in
+ return relayWithLocation.relay.active
+ }
+ }
+
+ private static func pickRandomRelay(relays: [RelayWithLocation]) -> RelayWithLocation? {
+ let totalWeight = relays.reduce(0) { accummulatedWeight, relayWithLocation in
+ return accummulatedWeight + relayWithLocation.relay.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 choses the relay with a
+ // non-zero weight.
+ var i = (1 ... totalWeight).randomElement()!
+
+ let randomRelay = relays.first { relayWithLocation -> Bool in
+ let (result, isOverflow) = i
+ .subtractingReportingOverflow(relayWithLocation.relay.weight)
+
+ 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
+ return 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>] {
+ return 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 parseRelaysResponse(
+ _ response: REST
+ .ServerRelaysResponse
+ ) -> [RelayWithLocation] {
+ return response.wireguard.relays.compactMap { serverRelay -> RelayWithLocation? in
+ guard let serverLocation = response.locations[serverRelay.location] else { return nil }
+
+ let locationComponents = serverRelay.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: serverRelay, location: location)
+ }
+ }
+}
+
+public struct NoRelaysSatisfyingConstraintsError: LocalizedError {
+ public var errorDescription: String? {
+ return "No relays satisfying constraints."
+ }
+}
+
+public struct RelaySelectorResult: Codable {
+ public var endpoint: MullvadEndpoint
+ public var relay: REST.ServerRelay
+ public var location: Location
+}
+
+private struct RelayWithLocation {
+ var relay: REST.ServerRelay
+ var location: Location
+}