summaryrefslogtreecommitdiffhomepage
path: root/ios/MullvadREST/Relay/RelaySelector+Shadowsocks.swift
blob: 94f0e5288535945b7b554a33df58d4c6db573c1b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
//
//  RelaySelector+Shadowsocks.swift
//  MullvadREST
//
//  Created by Mojgan on 2024-05-17.
//  Copyright © 2025 Mullvad VPN AB. All rights reserved.
//

import Foundation
import MullvadTypes

extension RelaySelector {
    public enum Shadowsocks {
        /**
         Returns random shadowsocks TCP bridge, otherwise `nil` if there are no shadowdsocks bridges.
         */
        public static func tcpBridge(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 relay(from relaysResponse: REST.ServerRelaysResponse) -> REST.BridgeRelay? {
            relaysResponse.bridge.relays.filter { $0.active }.randomElement()
        }

        /// Returns the closest Shadowsocks relay using the given `location`, or a random relay if `constraints` were
        /// unsatisfiable.
        ///
        /// - Parameters:
        ///   - location: The user selected `location`
        ///   - port: The user selected port
        ///   - filter: The user filtered criteria
        ///   - relays: The list of relays to randomly select from.
        /// - Returns: A Shadowsocks relay or `nil` if no active relay were found.
        public static func closestRelay(
            location: RelayConstraint<UserSelectedRelays>,
            port: RelayConstraint<UInt16>,
            filter: RelayConstraint<RelayFilter>,
            in relaysResponse: REST.ServerRelaysResponse
        ) -> REST.BridgeRelay? {
            let mappedBridges = RelayWithLocation.locateRelays(
                relays: relaysResponse.bridge.relays,
                locations: relaysResponse.locations
            )
            let filteredRelays =
                (try? applyConstraints(
                    location,
                    filterConstraint: filter,
                    daitaEnabled: false,
                    relays: mappedBridges
                )) ?? []
            guard filteredRelays.isEmpty == false else { return relay(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
        }
    }
}