summaryrefslogtreecommitdiffhomepage
path: root/ios/MullvadVPN/RelaySelector.swift
blob: a3f9f7e4748b841763bbf87e03b24efa9e8bb347 (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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
//
//  RelaySelector.swift
//  PacketTunnel
//
//  Created by pronebird on 11/06/2019.
//  Copyright © 2019 Mullvad VPN AB. All rights reserved.
//

import Foundation
import Network

struct RelaySelectorResult: Codable {
    var endpoint: MullvadEndpoint
    var relay: REST.ServerRelay
    var location: Location
}

private struct RelayWithLocation {
    var relay: REST.ServerRelay
    var location: Location
}

extension RelaySelectorResult {
    var tunnelConnectionInfo: TunnelConnectionInfo {
        return TunnelConnectionInfo(
            ipv4Relay: self.endpoint.ipv4Relay,
            ipv6Relay: self.endpoint.ipv6Relay,
            hostname: self.relay.hostname,
            location: self.location
        )
    }
}

enum RelaySelector {}

extension RelaySelector {

    static func evaluate(relays: REST.ServerRelaysResponse, constraints: RelayConstraints) -> RelaySelectorResult? {
        let filteredRelays = Self.applyConstraints(constraints, relays: Self.parseRelaysResponse(relays))
        let totalWeight = filteredRelays.reduce(0) { $0 + $1.relay.weight }

        guard totalWeight > 0 else { return nil }
        guard var i = (0...totalWeight).randomElement() else { return nil }

        let relayWithLocation = filteredRelays.first { (relayWithLocation) -> Bool in
            i -= relayWithLocation.relay.weight
            return i <= 0
        }.unsafelyUnwrapped

        guard let port = relays.wireguard.portRanges.randomElement()?.randomElement() else {
            return nil
        }

        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 .only(let relayConstraint):
                switch relayConstraint {
                case .country(let countryCode):
                    return relayWithLocation.location.countryCode == countryCode &&
                        relayWithLocation.relay.includeInCountry

                case .city(let countryCode, let cityCode):
                    return relayWithLocation.location.countryCode == countryCode &&
                        relayWithLocation.location.cityCode == cityCode

                case .hostname(let countryCode, let cityCode, let 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 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)
        }
    }

}