summaryrefslogtreecommitdiffhomepage
path: root/ios/MullvadREST/Relay/RelaySelectorWrapper.swift
blob: e2dbaaf0b8ddb77b56e4477bedb5fb847a2f313f (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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
//
//  RelaySelectorWrapper.swift
//  PacketTunnel
//
//  Created by pronebird on 08/08/2023.
//  Copyright © 2025 Mullvad VPN AB. All rights reserved.
//

import MullvadSettings
import MullvadTypes

public final class RelaySelectorWrapper: RelaySelectorProtocol, Sendable {
    public let relayCache: RelayCacheProtocol

    public init(relayCache: RelayCacheProtocol) {
        self.relayCache = relayCache
    }

    public func selectRelays(
        tunnelSettings: LatestTunnelSettings,
        connectionAttemptCount: UInt
    ) throws -> SelectedRelays {
        let relays = try relayCache.read().relays
        try validateWireguardCustomPort(tunnelSettings, relays: relays)

        // Filter for obfuscation
        let obfuscation = RelayObfuscator(
            relays: relays,
            tunnelSettings: tunnelSettings,
            connectionAttemptCount: connectionAttemptCount,
            obfuscationBypass: IdentityObfuscationProvider()
        ).obfuscate()

        return switch tunnelSettings.tunnelMultihopState {
        case .off:
            try SinglehopPicker(
                obfuscation: obfuscation,
                tunnelSettings: tunnelSettings,
                connectionAttemptCount: connectionAttemptCount
            ).pick()
        case .on:
            try MultihopPicker(
                obfuscation: obfuscation,
                tunnelSettings: tunnelSettings,
                connectionAttemptCount: connectionAttemptCount
            ).pick()
        }
    }

    public func findCandidates(tunnelSettings: LatestTunnelSettings) throws -> RelayCandidates {
        let relays = try relayCache.read().relays

        let obfuscation = RelayObfuscator(
            relays: relays,
            tunnelSettings: tunnelSettings,
            connectionAttemptCount: 0,
            obfuscationBypass: IdentityObfuscationProvider()
        ).obfuscate()

        let findCandidates:
            (REST.ServerRelaysResponse, Bool) throws
                -> [RelayWithLocation<REST.ServerRelay>] = { relays, daitaEnabled in
                    try RelaySelector.WireGuard.findCandidates(
                        by: .any,
                        in: relays,
                        filterConstraint: tunnelSettings.relayConstraints.filter,
                        daitaEnabled: daitaEnabled
                    )
                }

        return if tunnelSettings.daita.isAutomaticRouting {
            // When "Direct only" is not enabled the user will pick from the exit relays and
            // is then multihopped to a compatible server if necessary. We need to apply the
            // obfuscated relays to exit selection too so that the user doesn't pick
            // anything that isn't available for the entry server IF multihop DOESN'T kick in.
            RelayCandidates(
                entryRelays: try findCandidates(
                    obfuscation.obfuscatedRelays,
                    tunnelSettings.daita.daitaState.isEnabled
                ),
                exitRelays: try findCandidates(
                    // If multihop is explicitly enabled as well, any exit should be viable.
                    tunnelSettings.tunnelMultihopState.isEnabled ? obfuscation.allRelays : obfuscation.obfuscatedRelays,
                    false
                )
            )
        } else if tunnelSettings.tunnelMultihopState.isEnabled {
            // Any exit is viable due to multihop. DAITA and obfuscation is applied on
            // the entry only.
            RelayCandidates(
                entryRelays: try findCandidates(
                    obfuscation.obfuscatedRelays,
                    tunnelSettings.daita.daitaState.isEnabled
                ),
                exitRelays: try findCandidates(
                    obfuscation.allRelays,
                    false
                )
            )
        } else {
            // Singlehop. Always apply DAITA and obfuscation.
            RelayCandidates(
                entryRelays: nil,
                exitRelays: try findCandidates(
                    obfuscation.obfuscatedRelays,
                    tunnelSettings.daita.daitaState.isEnabled
                )
            )
        }
    }

    private func validateWireguardCustomPort(
        _ tunnelSettings: LatestTunnelSettings,
        relays: REST.ServerRelaysResponse
    ) throws {
        if [.automatic, .off].contains(tunnelSettings.wireGuardObfuscation.state) {
            if case let .only(port) = tunnelSettings.relayConstraints.port {
                let isPortWithinValidWireGuardRanges: Bool =
                    relays.wireguard.portRanges
                    .contains { range in
                        if let minPort = range.first, let maxPort = range.last {
                            return (minPort...maxPort).contains(port)
                        }
                        return false
                    }
                guard isPortWithinValidWireGuardRanges else {
                    throw NoRelaysSatisfyingConstraintsError(.invalidPort)
                }
            }
        }
    }
}